Skip to content

Hooks ​

Hooks provide a strongly-typed lifecycle event system. They let you observe, log, modify, or intercept any stage of the agent loop without touching the core logic.

Default model is Amazon Bedrock

Agent() with no model= uses Amazon Bedrock (Claude Sonnet 4.6 in us-west-2). Configure AWS credentials (aws configure or AWS_* env vars), or pass an explicit model — see Installation and Model Providers.

Registering a hook ​

Inline callback (type-inferred) ​

The event type is inferred from the function's type hint:

python
from elsai import Agent
from elsai.agent import AgentConfig
from elsai.hooks import BeforeModelCallEvent, AfterModelCallEvent

def log_before(event: BeforeModelCallEvent) -> None:
    print(f"[→] Calling model for agent: {event.agent.name}")

def log_after(event: AfterModelCallEvent) -> None:
    if event.exception:
        print(f"[←] Model error: {event.exception}")
    elif event.stop_response:
        print(f"[←] Model responded ({event.stop_response.stop_reason})")

agent = Agent(config=AgentConfig(hooks=[log_before, log_after]))

Add hook after construction ​

python
agent = Agent()
agent.add_hook(log_before)
agent.add_hook(log_after)

Handle multiple event types ​

python
from elsai.hooks import BeforeToolCallEvent, AfterToolCallEvent

def log_tools(event: BeforeToolCallEvent | AfterToolCallEvent) -> None:
    print(f"Tool event: {type(event).__name__}")

agent.add_hook(log_tools)

Hook provider class ​

Implement HookProvider to bundle related hooks:

python
from elsai.hooks import HookProvider, HookRegistry, BeforeModelCallEvent, AfterInvocationEvent

class AuditLogger(HookProvider):
    def register_hooks(self, registry: HookRegistry) -> None:
        registry.add_callback(BeforeModelCallEvent, self.on_before)
        registry.add_callback(AfterInvocationEvent, self.on_after)

    def on_before(self, event: BeforeModelCallEvent) -> None:
        print(f"Calling {event.agent.model.__class__.__name__}")

    def on_after(self, event: AfterInvocationEvent) -> None:
        if event.result:
            tokens = event.result.metrics.accumulated_usage.get("totalTokens", 0)
            print(f"Used {tokens} tokens")

agent = Agent(config=AgentConfig(hooks=[AuditLogger()]))

Available events ​

Agent lifecycle ​

EventWhen
AgentInitializedEventAgent constructor completes
UserInputReceivedJust before processing a new user message
BeforeInvocationEventBefore the event loop starts for this turn. TypeSafe Classifier can route here.
AfterInvocationEventAfter the event loop ends for this turn
BeforeResponseDispatchBefore the final result is returned
AgentErrorEventWhen an unhandled exception occurs

Model calls ​

EventWhen
BeforeModelCallEventBefore sending messages to the LLM
AfterModelCallEventAfter the model finishes responding

When streaming is enabled, token deltas are emitted between these hooks. Consume them via Agent.stream_async — see Streaming.

Tool calls ​

EventWhen
BeforeToolCallEventBefore a tool is executed. Permissions cancel or interrupt here. TypeSafe Classifier can gate side-effect tools here.
AfterToolCallEventAfter a tool returns

Memory ​

EventWhen
BeforeMemoryWriteBefore a message is appended to history
MessageAddedEventAfter a message is appended to history

Multi-agent ​

EventWhen
BeforeNodeCallEventBefore a node (agent) starts in a graph/swarm
AfterNodeCallEventAfter a node finishes
MultiAgentInitializedEventWhen a Graph or Swarm is constructed

Writable event properties ​

Some events let you modify the agent's behaviour mid-flight:

python
from elsai.hooks import BeforeToolCallEvent

def tool_guard(event: BeforeToolCallEvent) -> None:
    # Swap the selected tool for a safer alternative
    if event.selected_tool.name == "dangerous_tool":
        event.selected_tool = safe_alternative_tool

agent.add_hook(tool_guard)

BeforeInvocationEvent ​

PropertyWritableDescription
messages✅Override the messages for this turn

AfterInvocationEvent ​

PropertyWritableDescription
resume✅Set to trigger another agent turn

BeforeToolCallEvent ​

PropertyWritableDescription
selected_tool✅Swap the tool that will be called

BeforeResponseDispatch ​

PropertyWritableDescription
result✅Override the final AgentResult

Async hooks ​

Hooks can be async:

python
import asyncio
from elsai.hooks import AfterToolCallEvent

async def async_logger(event: AfterToolCallEvent) -> None:
    await asyncio.sleep(0)  # yield to event loop
    name = event.selected_tool.name if event.selected_tool else "unknown"
    print(f"Tool {name} returned: {event.result}")

agent.add_hook(async_logger)

Paired events ​

Every Before event has a corresponding After event, even when an exception occurs — this guarantees proper cleanup:

python
from elsai.hooks import BeforeModelCallEvent, AfterModelCallEvent

class Timer(HookProvider):
    def __init__(self):
        self._start = {}

    def register_hooks(self, registry):
        registry.add_callback(BeforeModelCallEvent, self.start)
        registry.add_callback(AfterModelCallEvent, self.end)

    def start(self, event: BeforeModelCallEvent):
        import time
        self._start[id(event.agent)] = time.time()

    def end(self, event: AfterModelCallEvent):
        import time
        elapsed = time.time() - self._start.pop(id(event.agent), time.time())
        print(f"Model call took {elapsed:.2f}s")

Copyright © 2026 elsai foundry.