Skip to content

Hooks

Hooks are a composable extensibility mechanism for extending agent functionality by subscribing to events throughout the agent lifecycle. The hook system enables both built-in components and user code to react to or modify agent behavior through strongly-typed event callbacks.

Overview

The hooks system is an evolution of the callback_handler approach with a more composable, type-safe system that supports multiple subscribers per event type.

A Hook Event is a specific event in the lifecycle that callbacks can be associated with. A Hook Callback is a callback function that is invoked when the hook event is emitted.

Hooks enable use cases such as:

  • Monitoring agent execution and tool usage
  • Modifying tool execution behavior
  • Adding validation and error handling

Basic Usage

Hook callbacks are registered against specific event types and receive strongly-typed event objects when those events occur during agent execution. Each event carries relevant data for that stage of the agent lifecycle - for example, BeforeInvocationEvent includes agent and request details, while BeforeToolInvocationEvent provides tool information and parameters.

Registering Individual Hook Callbacks

You can register callbacks for specific events using add_callback:

agent = Agent()

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

hooks.add_callback(BeforeInvocationEvent, my_callback)

Creating a Hook Provider

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

class LoggingHook(HookProvider):
    def register_hooks(self, registry: HookRegistry) -> None:
        registry.add_callback(BeforeInvocationEvent, self.log_start)
        registry.add_callback(AfterInvocationEvent, self.log_end)

    def log_start(self, event: BeforeInvocationEvent) -> None:
        print(f"Request started for agent: {event.agent.name}")

    def log_end(self, event: AfterInvocationEvent) -> None:
        print(f"Request completed for agent: {event.agent.name}")

# Passed in via the hooks parameter
agent = Agent(hooks=[LoggingHook()])

# Or added after the fact
agent.hooks.add_hook(LoggingHook())

Hook Event Lifecycle

The following diagram shows when hook events are emitted during a typical agent invocation where tools are invoked:

flowchart LR
 subgraph Start["Request Start Events"]
    direction TB
        BeforeInvocationEvent["BeforeInvocationEvent"]
        StartMessage["MessageAddedEvent"]
        BeforeInvocationEvent --> StartMessage
  end
 subgraph Model["Model Events"]
    direction TB
        AfterModelInvocationEvent["AfterModelInvocationEvent"]
        BeforeModelInvocationEvent["BeforeModelInvocationEvent"]
        ModelMessage["MessageAddedEvent"]
        BeforeModelInvocationEvent --> AfterModelInvocationEvent
        AfterModelInvocationEvent --> ModelMessage
  end
  subgraph Tool["Tool Events"]
    direction TB
        AfterToolInvocationEvent["AfterToolInvocationEvent"]
        BeforeToolInvocationEvent["BeforeToolInvocationEvent"]
        ToolMessage["MessageAddedEvent"]
        BeforeToolInvocationEvent --> AfterToolInvocationEvent
        AfterToolInvocationEvent --> ToolMessage
  end
  subgraph End["Request End Events"]
    direction TB
        AfterInvocationEvent["AfterInvocationEvent"]
  end
Start --> Model
Model <--> Tool
Tool --> End

Available Events

The hooks system provides events for different stages of agent execution:

Event Description
AgentInitializedEvent Triggered when an agent has been constructed and finished initialization at the end of Agent.__init__.
BeforeInvocationEvent Triggered at the beginning of a new agent request (__call__, stream_async, or structured_output)
AfterInvocationEvent Triggered at the end of an agent request, regardless of success or failure. Uses reverse callback ordering
MessageAddedEvent Triggered when a message is added to the agent's conversation history

Additional experimental events are also available:

Experimental events are subject to change

These events are exposed experimentally in order to gather feedback and refine the public contract. Because they are experimental, they are subject to change between releases.

Experimental Event Description
BeforeModelInvocationEvent Triggered before the model is invoked for inference
AfterModelInvocationEvent Triggered after model invocation completes. Uses reverse callback ordering
BeforeToolInvocationEvent Triggered before a tool is invoked.
AfterToolInvocationEvent Triggered after tool invocation completes. Uses reverse callback ordering

Hook Behaviors

Event Properties

Most event properties are read-only to prevent unintended modifications. However, certain properties can be modified to influence agent behavior. For example, BeforeToolInvocationEvent.selected_tool allows you to change which tool gets executed, while AfterToolInvocationEvent.result enables modification of tool results.

Callback Ordering

Some events come in pairs, such as Before/After events. The After event callbacks are always called in reverse order from the Before event callbacks to ensure proper cleanup semantics.

Advanced Usage

Tool Interception

Modify or replace tools before execution:

class ToolInterceptor(HookProvider):
    def register_hooks(self, registry: HookRegistry) -> None:
        registry.add_callback(BeforeToolInvocationEvent, self.intercept_tool)

    def intercept_tool(self, event: BeforeToolInvocationEvent) -> None:
        if event.tool_use.name == "sensitive_tool":
            # Replace with a safer alternative
            event.selected_tool = self.safe_alternative_tool
            event.tool_use["name"] = "safe_tool"

Result Modification

Modify tool results after execution:

class ResultProcessor(HookProvider):
    def register_hooks(self, registry: HookRegistry) -> None:
        registry.add_callback(AfterToolInvocationEvent, self.process_result)

    def process_result(self, event: AfterToolInvocationEvent) -> None:
        if event.tool_use.name == "calculator":
            # Add formatting to calculator results
            original_content = event.result["content"][0]["text"]
            event.result["content"][0]["text"] = f"Result: {original_content}"

Best Practices

Performance Considerations

Keep hook callbacks lightweight since they execute synchronously:

class AsyncProcessor(HookProvider):
    def register_hooks(self, registry: HookRegistry) -> None:
        registry.add_callback(AfterInvocationEvent, self.queue_processing)

    def queue_processing(self, event: AfterInvocationEvent) -> None:
        # Queue heavy processing for background execution
        self.background_queue.put(event.agent.messages[-1])

Composability

Design hooks to be composable and reusable:

class RequestLoggingHook(HookProvider):
    def register_hooks(self, registry: HookRegistry) -> None:
        registry.add_callback(BeforeInvocationEvent, self.log_request)
        registry.add_callback(AfterInvocationEvent, self.log_response)
        registry.add_callback(BeforeToolInvocationEvent, self.log_tool_use)

    ...

Event Property Modifications

When modifying event properties, log the changes for debugging and audit purposes:

class ResultProcessor(HookProvider):
    def register_hooks(self, registry: HookRegistry) -> None:
        registry.add_callback(AfterToolInvocationEvent, self.process_result)

    def process_result(self, event: AfterToolInvocationEvent) -> None:
        if event.tool_use.name == "calculator":
            original_content = event.result["content"][0]["text"]
            logger.info(f"Modifying calculator result: {original_content}")
            event.result["content"][0]["text"] = f"Result: {original_content}"