|
| 1 | +# Multi-Agent Hooks [Experimental] |
| 2 | + |
| 3 | +!!! warning "Experimental Feature" |
| 4 | + This feature is experimental and may change in future versions. Use with caution in production environments. |
| 5 | + |
| 6 | +Multi-agent hooks extend the [hook system](../agents/hooks.md) to multi-agent primitives, enabling monitoring, debugging, and customization of multi-agent execution workflows. These hooks allow you to observe and modify behavior across the entire multi-agent lifecycle. |
| 7 | + |
| 8 | +## Overview |
| 9 | + |
| 10 | +Multi-agent hooks provide event-driven extensibility for orchestrators that coordinate multiple agents. Unlike single-agent hooks that focus on individual agent execution, multi-agent hooks capture orchestration-level events such as node transitions, orchestrator initialization, and overall invocation lifecycle. |
| 11 | + |
| 12 | +Multi-agent hooks enable use cases such as: |
| 13 | + |
| 14 | +- Monitoring multi-agent execution flow and node transitions |
| 15 | +- Debugging complex orchestration patterns |
| 16 | +- Adding validation and error handling at the orchestration level |
| 17 | +- Implementing custom logging and metrics collection |
| 18 | + |
| 19 | +## Basic Usage |
| 20 | + |
| 21 | +Multi-agent hook callbacks are registered against specific orchestration event types and receive strongly-typed event objects when those events occur during multi-agent execution. |
| 22 | + |
| 23 | +### Registering Multi-Agent Hook Callbacks |
| 24 | + |
| 25 | +You can register callbacks for specific events using `add_callback`: |
| 26 | + |
| 27 | +```python |
| 28 | +# Create your orchestrator (Graph or Swarm) |
| 29 | +orchestrator = Graph(...) |
| 30 | + |
| 31 | +# Register individual callbacks |
| 32 | +def my_callback(event: BeforeNodeCallEvent) -> None: |
| 33 | + print(f"Custom callback triggered") |
| 34 | + |
| 35 | +orchestrator.hooks.add_callback(BeforeNodeCallEvent, my_callback) |
| 36 | +``` |
| 37 | + |
| 38 | +### Creating a Multi-Agent Hook Provider |
| 39 | + |
| 40 | +The `HookProvider` protocol allows a single object to register callbacks for multiple events: |
| 41 | + |
| 42 | +```python |
| 43 | +class MultiAgentLoggingHook(HookProvider): |
| 44 | + def register_hooks(self, registry: HookRegistry) -> None: |
| 45 | + registry.add_callback(MultiAgentInitializedEvent, self.log_initialization) |
| 46 | + registry.add_callback(BeforeMultiAgentInvocationEvent, self.log_invocation_start) |
| 47 | + registry.add_callback(AfterMultiAgentInvocationEvent, self.log_invocation_end) |
| 48 | + registry.add_callback(BeforeNodeCallEvent, self.log_node_start) |
| 49 | + registry.add_callback(AfterNodeCallEvent, self.log_node_end) |
| 50 | + |
| 51 | + def log_initialization(self, event: MultiAgentInitializedEvent) -> None: |
| 52 | + print(f"Multi-agent orchestrator initialized: {type(event.source).__name__}") |
| 53 | + |
| 54 | + def log_invocation_start(self, event: BeforeMultiAgentInvocationEvent) -> None: |
| 55 | + print("Multi-agent invocation started") |
| 56 | + |
| 57 | + def log_invocation_end(self, event: AfterMultiAgentInvocationEvent) -> None: |
| 58 | + print("Multi-agent invocation completed") |
| 59 | + |
| 60 | + def log_node_start(self, event: BeforeNodeCallEvent) -> None: |
| 61 | + print(f"Starting node execution: {event.node_id}") |
| 62 | + |
| 63 | + def log_node_end(self, event: AfterNodeCallEvent) -> None: |
| 64 | + print(f"Completed node execution: {event.node_id}") |
| 65 | + |
| 66 | +# Use with orchestrator |
| 67 | +orchestrator = Graph(hooks=[MultiAgentLoggingHook()]) |
| 68 | +``` |
| 69 | + |
| 70 | +## Multi-Agent Hook Event Lifecycle |
| 71 | + |
| 72 | +The following diagram shows when multi-agent hook events are emitted during orchestrator execution: |
| 73 | + |
| 74 | +```mermaid |
| 75 | +flowchart LR |
| 76 | +subgraph Init["Initialization"] |
| 77 | + direction TB |
| 78 | + MultiAgentInitializedEvent["MultiAgentInitializedEvent"] |
| 79 | +end |
| 80 | +subgraph Invocation["Invocation Lifecycle"] |
| 81 | + direction TB |
| 82 | + BeforeMultiAgentInvocationEvent["BeforeMultiAgentInvocationEvent"] |
| 83 | + AfterMultiAgentInvocationEvent["AfterMultiAgentInvocationEvent"] |
| 84 | + BeforeMultiAgentInvocationEvent --> NodeExecution |
| 85 | + NodeExecution --> AfterMultiAgentInvocationEvent |
| 86 | +end |
| 87 | +subgraph NodeExecution["Node Execution (Repeated)"] |
| 88 | + direction TB |
| 89 | + BeforeNodeCallEvent["BeforeNodeCallEvent"] |
| 90 | + AfterNodeCallEvent["AfterNodeCallEvent"] |
| 91 | + BeforeNodeCallEvent --> AfterNodeCallEvent |
| 92 | +end |
| 93 | +Init --> Invocation |
| 94 | +``` |
| 95 | + |
| 96 | +### Available Multi-Agent Events |
| 97 | + |
| 98 | +The multi-agent hooks system provides events for different states of multi-agent orchestrator execution: |
| 99 | + |
| 100 | +| Event | Description | |
| 101 | +|------------------------------------|------------------------------------------------------------------------------------------------| |
| 102 | +| `MultiAgentInitializedEvent` | Triggered when multi-agent orchestrator is initialized | |
| 103 | +| `BeforeMultiAgentInvocationEvent` | Triggered before orchestrator execution starts | |
| 104 | +| `AfterMultiAgentInvocationEvent` | Triggered after orchestrator execution completes. Uses reverse callback ordering | |
| 105 | +| `BeforeNodeCallEvent` | Triggered before individual node execution starts | |
| 106 | +| `AfterNodeCallEvent` | Triggered after individual node execution completes. Uses reverse callback ordering | |
| 107 | + |
| 108 | +## Multi-Agent Hook Behaviors |
| 109 | + |
| 110 | +### Event Properties |
| 111 | + |
| 112 | +Multi-agent hook events provide access to: |
| 113 | + |
| 114 | +- **source**: The multi-agent orchestrator instance (for example: Graph/Swarm) |
| 115 | +- **node_id**: Identifier of the node being executed (for node-level events) |
| 116 | +- **invocation_state**: Configuration and context data passed through the orchestrator invocation |
| 117 | + |
| 118 | +### Callback Ordering |
| 119 | + |
| 120 | +Similar to single-agent hooks, After events (`AfterNodeCallEvent`, `AfterMultiAgentInvocationEvent`) use reverse callback ordering to ensure proper cleanup semantics. |
| 121 | + |
| 122 | +### Accessing Orchestrator State |
| 123 | + |
| 124 | +Multi-agent hooks can access the orchestrator instance directly through the `source` property, enabling inspection of the orchestrator's current state, configuration, and execution context. |
| 125 | + |
| 126 | +## Advanced Usage |
| 127 | + |
| 128 | +### Accessing Invocation State in Hooks |
| 129 | + |
| 130 | +Like single-agent hooks, multi-agent hooks include access to `invocation_state`, which provides configuration and context data passed through the orchestrator's lifecycle. |
| 131 | + |
| 132 | +```python |
| 133 | +class ContextAwareMultiAgentHook(HookProvider): |
| 134 | + def register_hooks(self, registry: HookRegistry) -> None: |
| 135 | + registry.add_callback(BeforeNodeCallEvent, self.log_with_context) |
| 136 | + |
| 137 | + def log_with_context(self, event: BeforeNodeCallEvent) -> None: |
| 138 | + # Access shared context across all agents |
| 139 | + user_id = event.invocation_state.get("user_id", "unknown") |
| 140 | + session_id = event.invocation_state.get("session_id") |
| 141 | + |
| 142 | + # Access orchestrator-specific configuration |
| 143 | + orchestrator_config = event.invocation_state.get("orchestrator_config", {}) |
| 144 | + |
| 145 | + print(f"User {user_id} executing node {event.node_id} " |
| 146 | + f"in session {session_id} with config: {orchestrator_config}") |
| 147 | + |
| 148 | +# Use with shared state |
| 149 | +orchestrator = Graph(hooks=[ContextAwareMultiAgentHook()]) |
| 150 | +result = orchestrator( |
| 151 | + "Process the request", |
| 152 | + user_id="user123", |
| 153 | + session_id="sess456", |
| 154 | + orchestrator_config={"max_retries": 3, "timeout": 30} |
| 155 | +) |
| 156 | +``` |
| 157 | + |
| 158 | +### Conditional Node Execution |
| 159 | + |
| 160 | +Implement custom logic to modify orchestration behavior: |
| 161 | + |
| 162 | +```python |
| 163 | +class ConditionalExecutionHook(HookProvider): |
| 164 | + def __init__(self, skip_conditions: dict[str, callable]): |
| 165 | + self.skip_conditions = skip_conditions |
| 166 | + |
| 167 | + def register_hooks(self, registry: HookRegistry) -> None: |
| 168 | + registry.add_callback(BeforeNodeCallEvent, self.check_execution_conditions) |
| 169 | + |
| 170 | + def check_execution_conditions(self, event: BeforeNodeCallEvent) -> None: |
| 171 | + node_id = event.node_id |
| 172 | + if node_id in self.skip_conditions: |
| 173 | + condition_func = self.skip_conditions[node_id] |
| 174 | + if condition_func(event.invocation_state): |
| 175 | + print(f"Skipping node {node_id} due to condition") |
| 176 | + # Note: Actual node skipping would require orchestrator-specific implementation |
| 177 | +``` |
| 178 | + |
| 179 | +## Best Practices |
| 180 | + |
| 181 | +### Performance Considerations |
| 182 | + |
| 183 | +Keep multi-agent hook callbacks lightweight since they execute synchronously: |
| 184 | + |
| 185 | +```python |
| 186 | +class AsyncMultiAgentProcessor(HookProvider): |
| 187 | + def register_hooks(self, registry: HookRegistry) -> None: |
| 188 | + registry.add_callback(AfterNodeCallEvent, self.queue_node_processing) |
| 189 | + |
| 190 | + def queue_node_processing(self, event: AfterNodeCallEvent) -> None: |
| 191 | + # Queue heavy processing for background execution |
| 192 | + self.background_queue.put({ |
| 193 | + 'node_id': event.node_id, |
| 194 | + 'orchestrator_type': type(event.source).__name__, |
| 195 | + 'timestamp': time.time() |
| 196 | + }) |
| 197 | +``` |
| 198 | + |
| 199 | +### Orchestrator-Agnostic Design |
| 200 | + |
| 201 | +Design hooks to work with different orchestrator types: |
| 202 | + |
| 203 | +```python |
| 204 | +class UniversalMultiAgentHook(HookProvider): |
| 205 | + def register_hooks(self, registry: HookRegistry) -> None: |
| 206 | + registry.add_callback(BeforeNodeCallEvent, self.handle_node_execution) |
| 207 | + |
| 208 | + def handle_node_execution(self, event: BeforeNodeCallEvent) -> None: |
| 209 | + orchestrator_type = type(event.source).__name__ |
| 210 | + print(f"Executing node {event.node_id} in {orchestrator_type} orchestrator") |
| 211 | + |
| 212 | + # Handle orchestrator-specific logic if needed |
| 213 | + if orchestrator_type == "Graph": |
| 214 | + self.handle_graph_node(event) |
| 215 | + elif orchestrator_type == "Swarm": |
| 216 | + self.handle_swarm_node(event) |
| 217 | + |
| 218 | + def handle_graph_node(self, event: BeforeNodeCallEvent) -> None: |
| 219 | + # Graph-specific handling |
| 220 | + pass |
| 221 | + |
| 222 | + def handle_swarm_node(self, event: BeforeNodeCallEvent) -> None: |
| 223 | + # Swarm-specific handling |
| 224 | + pass |
| 225 | +``` |
| 226 | + |
| 227 | +## Integration with Single-Agent Hooks |
| 228 | + |
| 229 | +Multi-agent hooks complement single-agent hooks. Individual agents within the orchestrator can still have their own hooks, creating a layered monitoring and customization system: |
| 230 | + |
| 231 | +```python |
| 232 | +# Single-agent hook for individual agents |
| 233 | +class AgentLevelHook(HookProvider): |
| 234 | + def register_hooks(self, registry: HookRegistry) -> None: |
| 235 | + registry.add_callback(BeforeToolCallEvent, self.log_tool_use) |
| 236 | + |
| 237 | + def log_tool_use(self, event: BeforeToolCallEvent) -> None: |
| 238 | + print(f"Agent tool call: {event.tool_use['name']}") |
| 239 | + |
| 240 | +# Multi-agent hook for orchestrator |
| 241 | +class OrchestratorLevelHook(HookProvider): |
| 242 | + def register_hooks(self, registry: HookRegistry) -> None: |
| 243 | + registry.add_callback(BeforeNodeCallEvent, self.log_node_execution) |
| 244 | + |
| 245 | + def log_node_execution(self, event: BeforeNodeCallEvent) -> None: |
| 246 | + print(f"Orchestrator node execution: {event.node_id}") |
| 247 | + |
| 248 | +# Create agents with individual hooks |
| 249 | +agent1 = Agent(tools=[tool1], hooks=[AgentLevelHook()]) |
| 250 | +agent2 = Agent(tools=[tool2], hooks=[AgentLevelHook()]) |
| 251 | + |
| 252 | +# Create orchestrator with multi-agent hooks |
| 253 | +orchestrator = Graph( |
| 254 | + agents={"agent1": agent1, "agent2": agent2}, |
| 255 | + hooks=[OrchestratorLevelHook()] |
| 256 | +) |
| 257 | +``` |
| 258 | + |
| 259 | +This layered approach provides comprehensive observability and control across both individual agent execution and orchestrator-level coordination. |
0 commit comments