Hooks¶
The problem¶
You want visibility into a running agent — log when a run starts, push a metric on every tool call, send a webhook when a guardrail trips — without editing the agent loop and without risking that your logging code crashes the agent. Middleware can do this too, but middleware sits in the call path and can change or break things. Sometimes you want pure, safe observation.
That's what hooks are: read-only notifications fired at well-defined points in the run loop.
The lifecycle events¶
The run loop emits events as it progresses. You subscribe to the ones you care about.
%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#E8EAF6','primaryTextColor': '#1A237E','primaryBorderColor': '#3949AB','lineColor': '#546E7A','fontSize': '12px'}}}%%
flowchart TD
classDef evt fill:#E8EAF6,stroke:#3949AB,color:#1A237E,font-weight:bold
classDef loop fill:#E3F2FD,stroke:#1565C0,color:#0D47A1
classDef term fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20,font-weight:bold
classDef warn fill:#FFEBEE,stroke:#C62828,color:#B71C1C
RS["RUN_START"]:::term --> LS["LLM_START"]:::evt
LS --> LE["LLM_END (usage)"]:::evt
LE --> D{"tool calls?"}:::loop
D -->|"yes"| TS["TOOL_START"]:::evt
TS --> TE["TOOL_END (status, duration)"]:::evt
TE --> LS
D -->|"no"| RE["RUN_END"]:::term
LE -.->|"policy fires"| GT["GUARDRAIL_TRIP"]:::warn | Event | Fires |
|---|---|
RUN_START / RUN_END | An agent.run() begins / completes (success or failure) |
STEP_START / STEP_END | A think-act cycle begins / completes |
LLM_START / LLM_END | Before / after a model call (LLM_END carries usage) |
TOOL_START / TOOL_END | Before / after a tool runs (TOOL_END carries status, duration_ms) |
GUARDRAIL_TRIP | A guardrail forced a hard stop |
HANDOFF | An orchestrator delegated to a sub-agent |
FLOW_START / FLOW_END | A Flow began / finished |
Three guarantees that make hooks safe¶
The whole point of hooks is that you can attach them freely without worrying. The HookManager enforces:
- Read-only. Hooks receive a context dict; they cannot mutate run state. (Need to change behaviour? Use middleware.)
- Crash-isolated. An exception thrown inside a hook is caught and logged — it never propagates into the agent. Your broken metrics call won't take down the run.
- Per-agent. Hooks are registered on an agent instance, not globally, so one agent's observers don't fire for another.
Both async and sync callbacks are accepted (sync ones are run off the event loop), so logging to a DB or sending a webhook from a hook is fine.
Using them¶
from ravi.agents.hooks import HookManager, HookEvent
hooks = HookManager()
@hooks.on(HookEvent.RUN_START)
async def log_start(ctx):
print(f"Agent {ctx['agent_name']} starting run {ctx['run_id']}")
@hooks.on(HookEvent.TOOL_END)
async def track_tool(ctx):
await metrics.timing(ctx["tool_name"], ctx["duration_ms"])
@hooks.on(HookEvent.LLM_END)
async def track_cost(ctx):
usage = ctx["usage"] # kernel Usage: input_tokens / output_tokens
await billing.record(usage)
agent = ReActAgent("bot", model=model, hooks=hooks)
The Worker dispatches RUN_START/RUN_END around the whole run; the ReAct loop dispatches the LLM_* and TOOL_* events in place. Your callbacks just observe.
Hooks vs. Middleware — the one-line rule¶
- Need to see what happened? → Hook (safe, read-only, can't break the run).
- Need to change what happens? → Middleware (in the path, can modify/short-circuit/abort).
Where this lives¶
| Piece | Location |
|---|---|
HookManager, HookEvent | agents/hooks/manager.py |
| Dispatch sites | agents/runtime/worker.py, agents/core/react.py |
Back to: Core Concepts