Lifecycle hook reference#

All hooks receive AgentRunContext unless noted. Lifecycle hooks are ordinary async functions returning None. accept is synchronous and returns bool. Lifecycle defaults are no-ops; middleware defaults transparently call the next layer. Override only what your extension needs.

Setup hooks#

Runs once per request, not once per Agent instance#

Hook

State at entry

Intended operation

on_tool()

Fresh local registry already contains constructor tools; server registry is empty.

Register dynamic tools through context.register_tool or context.register_server_tool.

on_state()

All dynamic tools registered; new input not appended.

Restore history and insert system instructions.

on_message()

History ready; input in context.input_message.

Replace or validate current input; do not append it yourself.

Request hooks#

Success and failure are separate paths#

Hook

Boundary and constraints

before_run()

Transformed input has been appended and published; initialize per-request work.

after_run()

A final answer is ready with no remaining queued input.

on_success()

All after_run hooks succeeded; precedes RUN_COMPLETED. Release success state.

on_error()

A non-cancellation request error is being propagated. Release failure state.

Turn hooks#

A request runs one turn per model/tool cycle#

Hook

Boundary and constraints

before_turn()

After before_run, or after the previous turn’s after_turn; before compaction, before_model, and the provider call. Messages or tools edited here shape this turn only.

after_turn()

The turn’s AssistantMessage and every ToolMessage it requested are appended and published. Receives the assistant message that opened the turn, including when its tool calls failed or steering superseded them. The final answer closes its turn before after_run.

Model and tool hooks#

May execute multiple times within one request#

Hook

Boundary and constraints

before_model()

Receives context and ModelRequest; inspect it or change context.state.messages, context.tools, and context.server_tools.

after_model()

Complete AssistantMessage already appended; receives ModelResponse including usage.

before_tool()

Before one tool call. A raised exception fails the request before execution.

after_tool()

Tool execution has finished but ToolMessage is not appended yet. Inspect the separate error argument and modify the result before context and persistence receive it.

Middleware hooks#

Wrap the operation instead of observing one side of it#

Hook

Boundary and constraints

on_model_request()

Wraps the provider stream. Pass a ModelRequest to call_next and close the returned iterator when leaving early.

on_tool_call()

Wraps one registered local tool handler. It may adjust arguments, transform or short-circuit the result, or raise a normal tool failure.

These hooks use the same extension priority ordering as lifecycle hooks, with lower priorities forming the outer middleware layers. Provider-hosted tools run inside on_model_request and do not use the local on_tool_call boundary.

Emitting outward events#

Any lifecycle hook may publish visible progress with await context.emit(AgentEvent(...)). The event enters the request-owned queue at that exact point in hook priority order. Internal runtime methods and extensions never expose nested AgentEvent async generators; only stream() drains the queue and yields to callers.

Custom output does not become a phase transition simply because its payload says “working”. Do not synthesize MODEL_STARTED, TOOL_STARTED, or terminal events that are owned by the core loop. Use context.publish instead for internal extension-to-extension notifications that should not reach the UI.

before_model receives (context, request). The runtime refreshes request messages and both tool registries before each extension and again before the provider call. Request fields are frozen; edit context.state.messages, context.tools, or context.server_tools to affect subsequent hooks and the provider. The supplied request is a shallow view at entry, not a live view of later registry changes.

Notification and input hooks#

These hooks are not UI stream callbacks#

Hook

Contract

on_event()

Awaited for ExtensionEvent published inside the request; inspect concrete event types.

accept()

Receives config and ExternalEvent from external callers, potentially on another thread. Return whether delivery was accepted.

Use ExternalEventExtension for pending responses instead of writing your own cross-thread Future handling. If overriding its on_event or on_error methods, call super() so the base can wake and release pending waits.

Priority, naming, and exceptions#

priority = 100 is the default. Lower values run earlier within each hook stage. Two instances with the same name cannot be registered. Give independently configured instances explicit names through extension.name when needed.

If a hook fails, previously completed hooks are not rolled back. Cleanup must be idempotent and tolerate partial initialization. Cancellation has its own internal RunCancelledEvent, not an on_cancel method. See State, concurrency, and cleanup.