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.

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.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:
    print(f"[←] Model responded in {event.metrics.latency_ms:.0f}ms")

agent = Agent(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, AfterModelCallEvent

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

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

    def on_after(self, event: AfterModelCallEvent) -> None:
        print(f"Used {event.metrics.total_tokens} tokens")

agent = Agent(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
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
ModelStreamChunkEventEach token/chunk streamed from the model
AfterModelCallEventAfter the model finishes responding

Tool calls

EventWhen
BeforeToolCallEventBefore a tool is executed
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
messagesOverride the messages for this turn

AfterInvocationEvent

PropertyWritableDescription
resumeSet to trigger another agent turn

BeforeToolCallEvent

PropertyWritableDescription
selected_toolSwap the tool that will be called

BeforeResponseDispatch

PropertyWritableDescription
resultOverride 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
    print(f"Tool {event.tool.name} returned: {event.response}")

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.