Hooks & Events System¶
Hooks¶
Hooks (src/agentpool/hooks/) intercept agent turns at 4 points: pre_turn, post_turn, pre_tool_use, post_tool_use. Three hook types: CallableHook (in-process), CommandHook (subprocess), PromptHook (LLM evaluation). Hooks run in parallel, results combined with priority: deny > ask > allow.
HookAwareTurn Architecture¶
Hooks fire through the HookAwareTurn mixin (src/agentpool/orchestrator/turn.py), which is inherited by both NativeTurn and ACPTurn. This provides a single choke point for hook execution inside Turn.execute().
Firing flow:
fire_pre_turn_hooks()runs before the LLM call (turn start). ReturnsHookResult | None. Ifdecision="deny", the turn is blocked.fire_post_turn_hooks(result, duration_ms)runs in thefinallyblock after the response, even on error. Injectsduration_msfrom elapsed wall time.fire_pre_tool_hooks(tool_name, tool_input)runs before a tool call. For native agents, this is blocking (returnsdecision="deny"raisesModelRetry). For ACP agents, this is advisory (logs a warning but cannot block).fire_post_tool_hooks(tool_name, tool_output)runs after a tool call. Can returnmodified_outputto replace tool results. For ACP agents, modifies the event payload.- All four methods check a
hooks_fireddouble-fire guard (aset[str]onAgentRunContext) to prevent the same hook from firing twice in one turn.
Difference by agent type:
| Aspect | Native (NativeTurn) | ACP (ACPTurn) |
|---|---|---|
pre_turn |
Blocking via HookAwareTurn |
Blocking via HookAwareTurn |
post_turn |
Via HookAwareTurn finally block |
Via HookAwareTurn finally block |
pre_tool_use |
Blocking via _ToolInterceptCapability (model retry on deny) |
Blocking via ACPClientHandler.request_permission() + advisory on ToolCallStart event |
post_tool_use |
Via _ToolInterceptCapability |
Advisory on ToolCallComplete event (modify event payload) |
| Standalone fallback | BaseAgent._run_stream_once() guarded by AGENT_TYPE != "native" |
Still uses BaseAgent._run_stream_once() (future work to route through ACPTurn.execute()) |
Double-fire guard: The old path in BaseAgent._run_stream_once() fires hooks first and adds keys to run_ctx.hooks_fired. The Turn.execute() path (called via _stream_events()) checks hooks_fired and skips if the key is already present. This ensures the old ACP standalone path and the new Turn path don't fire duplicates.
Deprecated APIs (v0.5.0 removed)¶
AgentHooks.as_capability()— removed. Hooks now fire automatically viaHookAwareTurn.pre_run/post_runconfig field aliases inHooksConfig— removed. Usepre_turn/post_turnonly.run_pre_run_hooks()/run_post_run_hooks()methods — removed. Userun_pre_turn_hooks()/run_post_turn_hooks()._wrap_before_run(),_wrap_after_run(),_wrap_before_tool_execute(),_wrap_after_tool_execute()— removed.
Migration Guide: Hook Rename and HookAwareTurn¶
1. YAML config rename — In your agent YAML config, rename hook fields:
# OLD (v0.4.x)
hooks:
pre_run:
- type: callable
callable: mymodule:my_pre_hook
post_run:
- type: callable
callable: mymodule:my_post_hook
# NEW (v0.5.0+)
hooks:
pre_turn:
- type: callable
callable: mymodule:my_pre_hook
post_turn:
- type: callable
callable: mymodule:my_post_hook
The old pre_run/post_run field names were briefly supported with deprecation warnings in v0.4.x and are removed in v0.5.0. Only pre_turn/post_turn are accepted now.
2. AgentHooks.as_capability() removed — If you programmatically called agent_hooks.as_capability() to inject hooks as a PydanticAI capability, remove that call. Hooks now fire automatically through HookAwareTurn at the Turn.execute() level. No manual wiring is needed.
# OLD (v0.4.x) — removed
capability = agent_hooks.as_capability(hook_manager)
# NEW (v0.5.0+) — hooks fire automatically via HookAwareTurn
# Just configure hooks in YAML or pass AgentHooks to the agent constructor
3. v0.5.0 breaking changes (summary):
| Removed API | Replacement |
|---|---|
HooksConfig.pre_run / post_run |
pre_turn / post_turn |
AgentHooks.run_pre_run_hooks() |
run_pre_turn_hooks() |
AgentHooks.run_post_run_hooks() |
run_post_turn_hooks() |
AgentHooks.as_capability() |
Automatic via HookAwareTurn |
_wrap_before_run(), _wrap_after_run(), _wrap_before_tool_execute(), _wrap_after_tool_execute() |
Removed (no replacement — hooks fire via HookAwareTurn internally) |
Events¶
Event Types (src/agentpool/agents/events/events.py): RichAgentStreamEvent union type covers streaming deltas, tool calls (start/progress/complete), run lifecycle (started/error/failed), subagent events, session resume, compaction, plan updates, and custom events.
EventBus (src/agentpool/orchestrator/core.py): Cross-turn event streaming for protocol servers. Bounded async queues per session, replay buffers, scoped subscriptions ("session", "descendants", "subtree", "all").
Signal Architecture (anyenv.signals.Signal): In-process type-safe pub/sub on MessageNode (message_received, message_sent) and Talk (connection_processed, message_forwarded). SignalEmittingGraphRun bridges pydantic-graph steps to signals.