Skip to content

Multi-Agent Hooks [Experimental]

Experimental Feature

This feature is experimental and may change in future versions. Use with caution in production environments.

Multi-agent hooks extend the hook system 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.

Overview

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.

Multi-agent hooks enable use cases such as:

  • Monitoring multi-agent execution flow and node transitions
  • Debugging complex orchestration patterns
  • Adding validation and error handling at the orchestration level
  • Implementing custom logging and metrics collection

Basic Usage

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.

Registering Multi-Agent Hook Callbacks

You can register callbacks for specific events using add_callback:

# Create your orchestrator (Graph or Swarm)
orchestrator = Graph(...)

# Register individual callbacks
def my_callback(event: BeforeNodeCallEvent) -> None:
    print(f"Custom callback triggered")

orchestrator.hooks.add_callback(BeforeNodeCallEvent, my_callback)

Creating a Multi-Agent Hook Provider

The HookProvider protocol allows a single object to register callbacks for multiple events:

class MultiAgentLoggingHook(HookProvider):
    def register_hooks(self, registry: HookRegistry) -> None:
        registry.add_callback(MultiAgentInitializedEvent, self.log_initialization)
        registry.add_callback(BeforeMultiAgentInvocationEvent, self.log_invocation_start)
        registry.add_callback(AfterMultiAgentInvocationEvent, self.log_invocation_end)
        registry.add_callback(BeforeNodeCallEvent, self.log_node_start)
        registry.add_callback(AfterNodeCallEvent, self.log_node_end)

    def log_initialization(self, event: MultiAgentInitializedEvent) -> None:
        print(f"Multi-agent orchestrator initialized: {type(event.source).__name__}")

    def log_invocation_start(self, event: BeforeMultiAgentInvocationEvent) -> None:
        print("Multi-agent invocation started")

    def log_invocation_end(self, event: AfterMultiAgentInvocationEvent) -> None:
        print("Multi-agent invocation completed")

    def log_node_start(self, event: BeforeNodeCallEvent) -> None:
        print(f"Starting node execution: {event.node_id}")

    def log_node_end(self, event: AfterNodeCallEvent) -> None:
        print(f"Completed node execution: {event.node_id}")

# Use with orchestrator
orchestrator = Graph(hooks=[MultiAgentLoggingHook()])

Multi-Agent Hook Event Lifecycle

The following diagram shows when multi-agent hook events are emitted during orchestrator execution:

flowchart LR
subgraph Init["Initialization"]
    direction TB
    MultiAgentInitializedEvent["MultiAgentInitializedEvent"]
end
subgraph Invocation["Invocation Lifecycle"]
    direction TB
    BeforeMultiAgentInvocationEvent["BeforeMultiAgentInvocationEvent"]
    AfterMultiAgentInvocationEvent["AfterMultiAgentInvocationEvent"]
    BeforeMultiAgentInvocationEvent --> NodeExecution
    NodeExecution --> AfterMultiAgentInvocationEvent
end
subgraph NodeExecution["Node Execution (Repeated)"]
    direction TB
    BeforeNodeCallEvent["BeforeNodeCallEvent"]
    AfterNodeCallEvent["AfterNodeCallEvent"]
    BeforeNodeCallEvent --> AfterNodeCallEvent
end
Init --> Invocation

Available Multi-Agent Events

The multi-agent hooks system provides events for different states of multi-agent orchestrator execution:

Event Description
MultiAgentInitializedEvent Triggered when multi-agent orchestrator is initialized
BeforeMultiAgentInvocationEvent Triggered before orchestrator execution starts
AfterMultiAgentInvocationEvent Triggered after orchestrator execution completes. Uses reverse callback ordering
BeforeNodeCallEvent Triggered before individual node execution starts
AfterNodeCallEvent Triggered after individual node execution completes. Uses reverse callback ordering

Multi-Agent Hook Behaviors

Event Properties

Multi-agent hook events provide access to:

  • source: The multi-agent orchestrator instance (for example: Graph/Swarm)
  • node_id: Identifier of the node being executed (for node-level events)
  • invocation_state: Configuration and context data passed through the orchestrator invocation

Callback Ordering

Similar to single-agent hooks, After events (AfterNodeCallEvent, AfterMultiAgentInvocationEvent) use reverse callback ordering to ensure proper cleanup semantics.

Accessing Orchestrator State

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.

Advanced Usage

Accessing Invocation State in Hooks

Like single-agent hooks, multi-agent hooks include access to invocation_state, which provides configuration and context data passed through the orchestrator's lifecycle.

class ContextAwareMultiAgentHook(HookProvider):
    def register_hooks(self, registry: HookRegistry) -> None:
        registry.add_callback(BeforeNodeCallEvent, self.log_with_context)

    def log_with_context(self, event: BeforeNodeCallEvent) -> None:
        # Access shared context across all agents
        user_id = event.invocation_state.get("user_id", "unknown")
        session_id = event.invocation_state.get("session_id")

        # Access orchestrator-specific configuration
        orchestrator_config = event.invocation_state.get("orchestrator_config", {})

        print(f"User {user_id} executing node {event.node_id} "
              f"in session {session_id} with config: {orchestrator_config}")

# Use with shared state
orchestrator = Graph(hooks=[ContextAwareMultiAgentHook()])
result = orchestrator(
    "Process the request",
    user_id="user123",
    session_id="sess456",
    orchestrator_config={"max_retries": 3, "timeout": 30}
)

Conditional Node Execution

Implement custom logic to modify orchestration behavior:

class ConditionalExecutionHook(HookProvider):
    def __init__(self, skip_conditions: dict[str, callable]):
        self.skip_conditions = skip_conditions

    def register_hooks(self, registry: HookRegistry) -> None:
        registry.add_callback(BeforeNodeCallEvent, self.check_execution_conditions)

    def check_execution_conditions(self, event: BeforeNodeCallEvent) -> None:
        node_id = event.node_id
        if node_id in self.skip_conditions:
            condition_func = self.skip_conditions[node_id]
            if condition_func(event.invocation_state):
                print(f"Skipping node {node_id} due to condition")
                # Note: Actual node skipping would require orchestrator-specific implementation

Best Practices

Performance Considerations

Keep multi-agent hook callbacks lightweight since they execute synchronously:

class AsyncMultiAgentProcessor(HookProvider):
    def register_hooks(self, registry: HookRegistry) -> None:
        registry.add_callback(AfterNodeCallEvent, self.queue_node_processing)

    def queue_node_processing(self, event: AfterNodeCallEvent) -> None:
        # Queue heavy processing for background execution
        self.background_queue.put({
            'node_id': event.node_id,
            'orchestrator_type': type(event.source).__name__,
            'timestamp': time.time()
        })

Orchestrator-Agnostic Design

Design hooks to work with different orchestrator types:

class UniversalMultiAgentHook(HookProvider):
    def register_hooks(self, registry: HookRegistry) -> None:
        registry.add_callback(BeforeNodeCallEvent, self.handle_node_execution)

    def handle_node_execution(self, event: BeforeNodeCallEvent) -> None:
        orchestrator_type = type(event.source).__name__
        print(f"Executing node {event.node_id} in {orchestrator_type} orchestrator")

        # Handle orchestrator-specific logic if needed
        if orchestrator_type == "Graph":
            self.handle_graph_node(event)
        elif orchestrator_type == "Swarm":
            self.handle_swarm_node(event)

    def handle_graph_node(self, event: BeforeNodeCallEvent) -> None:
        # Graph-specific handling
        pass

    def handle_swarm_node(self, event: BeforeNodeCallEvent) -> None:
        # Swarm-specific handling
        pass

Integration with Single-Agent Hooks

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:

# Single-agent hook for individual agents
class AgentLevelHook(HookProvider):
    def register_hooks(self, registry: HookRegistry) -> None:
        registry.add_callback(BeforeToolCallEvent, self.log_tool_use)

    def log_tool_use(self, event: BeforeToolCallEvent) -> None:
        print(f"Agent tool call: {event.tool_use['name']}")

# Multi-agent hook for orchestrator
class OrchestratorLevelHook(HookProvider):
    def register_hooks(self, registry: HookRegistry) -> None:
        registry.add_callback(BeforeNodeCallEvent, self.log_node_execution)

    def log_node_execution(self, event: BeforeNodeCallEvent) -> None:
        print(f"Orchestrator node execution: {event.node_id}")

# Create agents with individual hooks
agent1 = Agent(tools=[tool1], hooks=[AgentLevelHook()])
agent2 = Agent(tools=[tool2], hooks=[AgentLevelHook()])

# Create orchestrator with multi-agent hooks
orchestrator = Graph(
    agents={"agent1": agent1, "agent2": agent2},
    hooks=[OrchestratorLevelHook()]
)

This layered approach provides comprehensive observability and control across both individual agent execution and orchestrator-level coordination.