From 4c904fce73a59593fdedf274cd1416b4951f2cc0 Mon Sep 17 00:00:00 2001 From: Diego Colombo Date: Thu, 12 Feb 2026 18:26:06 +0000 Subject: [PATCH] feat: enrich delegate events with constants, sub_session_id normalization, and metadata slot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor delegate tool event emissions for consistency and extensibility: - Extract event name constants (DELEGATE_AGENT_SPAWNED, etc.) replacing inline string literals throughout the module - Normalize all events to use sub_session_id (remove session_id backward- compat aliases from resume-path events) - Add metadata: None extensibility slot on all events for consuming hooks to populate as needed - Remove _build_delegate_metadata() and _extract_agent_from_session_id() helpers — decomposing agent names and guessing agents from session IDs was fragile and outside this module's responsibility - Remove agent field from resume-path events where it can't be reliably determined; spawn-path events retain it since the agent name is known - Update README lifecycle events documentation to reflect the revised event shapes 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com> --- modules/tool-delegate/README.md | 23 +++++++ .../__init__.py | 68 +++++++++++-------- 2 files changed, 61 insertions(+), 30 deletions(-) diff --git a/modules/tool-delegate/README.md b/modules/tool-delegate/README.md index 7e4b3f3..b81d8e6 100644 --- a/modules/tool-delegate/README.md +++ b/modules/tool-delegate/README.md @@ -68,6 +68,29 @@ modules: timeout: 300 ``` +## Lifecycle Events + +This module emits lifecycle events via the hook system, discoverable by consumers +through the `observability.events` capability registered at mount time. + +All events include a `metadata: None` property bag — an extensibility slot for +experimentation by consuming hooks. Foundation provides the slot; consumers +populate it as needed. + +| Event | Trigger | Data Includes | +|-------|---------|---------------| +| `delegate:agent_spawned` | Agent sub-session created | agent, sub_session_id, parent_session_id, context_depth, context_scope, metadata | +| `delegate:agent_resumed` | Agent sub-session resumed | sub_session_id, parent_session_id, metadata | +| `delegate:agent_completed` | Agent sub-session completed (spawn path includes agent) | sub_session_id, parent_session_id, success, metadata | +| `delegate:error` | Agent delegation failed (spawn path includes agent) | sub_session_id, parent_session_id, error, metadata | + +Note: `agent` is only present on spawn-path events where the agent name is +reliably known. Resume-path events omit it rather than guessing from session ID parsing. + +Event constants are defined in this module (`DELEGATE_AGENT_SPAWNED`, +`DELEGATE_AGENT_RESUMED`, `DELEGATE_AGENT_COMPLETED`, `DELEGATE_ERROR`), +not in `amplifier_core/events.py`, since delegation is a foundation-level concern. + ## Note This module is recommended over `tool-task` for new development due to its enhanced context control and bug fixes. diff --git a/modules/tool-delegate/amplifier_module_tool_delegate/__init__.py b/modules/tool-delegate/amplifier_module_tool_delegate/__init__.py index 7446005..3555afd 100644 --- a/modules/tool-delegate/amplifier_module_tool_delegate/__init__.py +++ b/modules/tool-delegate/amplifier_module_tool_delegate/__init__.py @@ -42,6 +42,14 @@ from amplifier_core import ModuleCoordinator, ToolResult from amplifier_foundation import ProviderPreference +# Delegate lifecycle events — foundation-level constants (not kernel). +# These are emitted by this module and discovered by consumers via the +# observability.events capability registered at mount time. +DELEGATE_AGENT_SPAWNED = "delegate:agent_spawned" +DELEGATE_AGENT_RESUMED = "delegate:agent_resumed" +DELEGATE_AGENT_COMPLETED = "delegate:agent_completed" +DELEGATE_ERROR = "delegate:error" + logger = logging.getLogger(__name__) @@ -61,10 +69,10 @@ async def mount(coordinator: ModuleCoordinator, config: dict[str, Any] | None = obs_events = coordinator.get_capability("observability.events") or [] obs_events.extend( [ - "delegate:agent_spawned", # When agent sub-session spawned - "delegate:agent_resumed", # When agent sub-session resumed - "delegate:agent_completed", # When agent sub-session completed - "delegate:error", # When delegation fails + DELEGATE_AGENT_SPAWNED, + DELEGATE_AGENT_RESUMED, + DELEGATE_AGENT_COMPLETED, + DELEGATE_ERROR, ] ) coordinator.register_capability("observability.events", obs_events) @@ -792,13 +800,14 @@ async def execute(self, input: dict) -> ToolResult: # Emit delegate:agent_spawned event if hooks: await hooks.emit( - "delegate:agent_spawned", + DELEGATE_AGENT_SPAWNED, { "agent": agent_name, "sub_session_id": sub_session_id, "parent_session_id": parent_session_id, "context_depth": context_depth, "context_scope": context_scope, + "metadata": None, }, ) @@ -890,12 +899,13 @@ async def execute(self, input: dict) -> ToolResult: # Emit delegate:agent_completed event if hooks: await hooks.emit( - "delegate:agent_completed", + DELEGATE_AGENT_COMPLETED, { "agent": agent_name, "sub_session_id": sub_session_id, "parent_session_id": parent_session_id, "success": True, + "metadata": None, }, ) @@ -927,12 +937,13 @@ async def execute(self, input: dict) -> ToolResult: logger.warning(timeout_msg) if hooks: await hooks.emit( - "delegate:error", + DELEGATE_ERROR, { "agent": agent_name, "sub_session_id": sub_session_id, "parent_session_id": parent_session_id, "error": timeout_msg, + "metadata": None, }, ) return ToolResult(success=False, error={"message": timeout_msg}) @@ -945,12 +956,13 @@ async def execute(self, input: dict) -> ToolResult: error_msg = f"Agent delegation failed ({error_type}): {error_detail}" if hooks: await hooks.emit( - "delegate:error", + DELEGATE_ERROR, { "agent": agent_name, "sub_session_id": sub_session_id, "parent_session_id": parent_session_id, "error": error_msg, + "metadata": None, }, ) @@ -978,10 +990,11 @@ async def _resume_existing_session( # Emit delegate:agent_resumed event if hooks: await hooks.emit( - "delegate:agent_resumed", + DELEGATE_AGENT_RESUMED, { - "session_id": full_session_id, + "sub_session_id": full_session_id, "parent_session_id": parent_session_id, + "metadata": None, }, ) @@ -1009,19 +1022,15 @@ async def _resume_existing_session( # Emit delegate:agent_completed event if hooks: await hooks.emit( - "delegate:agent_completed", + DELEGATE_AGENT_COMPLETED, { "sub_session_id": full_session_id, "parent_session_id": parent_session_id, "success": True, + "metadata": None, }, ) - # Extract agent name from session ID if possible - agent_name = "unknown" - if "_" in full_session_id: - agent_name = full_session_id.split("_")[-1] - # Return output with session info session_id_result = result["session_id"] return ToolResult( @@ -1029,7 +1038,6 @@ async def _resume_existing_session( output={ "response": result["output"], "session_id": session_id_result, - "agent": agent_name, "turn_count": result.get("turn_count", 1), "status": result.get("status", "success"), "metadata": result.get("metadata", {}), @@ -1040,11 +1048,12 @@ async def _resume_existing_session( # Session ID resolution error if hooks: await hooks.emit( - "delegate:error", + DELEGATE_ERROR, { - "session_id": session_id, + "sub_session_id": session_id, "parent_session_id": parent_session_id, "error": str(e), + "metadata": None, }, ) return ToolResult(success=False, error={"message": str(e)}) @@ -1053,11 +1062,12 @@ async def _resume_existing_session( # Session not found if hooks: await hooks.emit( - "delegate:error", + DELEGATE_ERROR, { - "session_id": session_id, + "sub_session_id": session_id, "parent_session_id": parent_session_id, "error": f"Session not found: {str(e)}", + "metadata": None, }, ) return ToolResult( @@ -1068,12 +1078,8 @@ async def _resume_existing_session( ) except TimeoutError: - # Extract agent name for the message - resume_agent = "unknown" - if "_" in session_id: - resume_agent = session_id.split("_")[-1] timeout_msg = ( - f"Resumed agent '{resume_agent}' timed out after {self.timeout}s " + f"Resumed agent timed out after {self.timeout}s " f"(delegate tool session-level timeout). " f"Increase or disable the timeout in tool-delegate settings " f"(settings.timeout) to allow longer-running agents." @@ -1081,11 +1087,12 @@ async def _resume_existing_session( logger.warning(timeout_msg) if hooks: await hooks.emit( - "delegate:error", + DELEGATE_ERROR, { - "session_id": session_id, + "sub_session_id": session_id, "parent_session_id": parent_session_id, "error": timeout_msg, + "metadata": None, }, ) return ToolResult(success=False, error={"message": timeout_msg}) @@ -1097,11 +1104,12 @@ async def _resume_existing_session( error_msg = f"Agent resume failed ({error_type}): {error_detail}" if hooks: await hooks.emit( - "delegate:error", + DELEGATE_ERROR, { - "session_id": session_id, + "sub_session_id": session_id, "parent_session_id": parent_session_id, "error": error_msg, + "metadata": None, }, ) return ToolResult(success=False, error={"message": error_msg})