Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions modules/tool-delegate/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
68 changes: 38 additions & 30 deletions modules/tool-delegate/amplifier_module_tool_delegate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)


Expand All @@ -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)
Expand Down Expand Up @@ -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,
},
)

Expand Down Expand Up @@ -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,
},
)

Expand Down Expand Up @@ -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})
Expand All @@ -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,
},
)

Expand Down Expand Up @@ -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,
},
)

Expand Down Expand Up @@ -1009,27 +1022,22 @@ 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(
success=True,
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", {}),
Expand All @@ -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)})
Expand All @@ -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(
Expand All @@ -1068,24 +1078,21 @@ 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."
)
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})
Expand All @@ -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})