Agent Hooks Reference¶
Reference for lionagi.hooks.HookPoint — the closed vocabulary of session lifecycle hook points — and the built-in handlers registered via lionagi.hooks.loader.DEFAULT_HOOKS.
These Session-level HookBus hooks are separate from the per-iModel service lifecycle. See HookRegistry service hooks for model invocation callbacks.
HookPoint catalog¶
Dispatched (active emit callsites)¶
| Point | Value | Callsite |
|---|---|---|
MESSAGE_ADD | message.add | branch.py _persist_via_bus — every inbound message |
USER_PROMPT_SUBMIT | prompt.submit | operations/chat/chat.py and operations/run/run.py, immediately before provider invocation / streaming begins — fires only when the operation context carries a turn-origin token (blocking, via blocking_emit) |
BRANCH_END | branch.end | cli/_runs.py teardown_persist — once per branch the teardown owns, only when the run reached a genuine terminal outcome (never for the "running" reconciliation-suppression case) |
API_PRE_CALL | api.pre_call | operations/_api_hooks.py emit_api_pre_call, called from operations/chat/chat.py (before imodel.invoke()) and operations/run/run.py (before the CLI stream starts) — only when the calling Branch is session-bound (branch._hooks is not None); a standalone iModel never reaches this callsite |
API_POST_CALL | api.post_call | operations/_api_hooks.py emit_api_post_call, called from the same two sites once the call has settled — success, a provider-reported failure (api_call.status), or a raised exception (error=..., status="error") |
API_STREAM_CHUNK | api.stream_chunk | operations/_api_hooks.py emit_api_stream_chunk, called from operations/run/run.py for every chunk of a session-bound stream; payload carries a redacted chunk_type only, not the raw chunk |
TOOL_PRE | tool.pre | operations/act/act.py, before tool invocation (blocking via blocking_emit) |
TOOL_POST | tool.post | operations/act/act.py, after successful tool invocation |
TOOL_ERROR | tool.error | operations/act/act.py, on tool invocation failure |
Registered in DEFAULT_HOOKS (handlers wired; emit callsite deferred to ADR-0023b)¶
| Point | Value | Default handler |
|---|---|---|
SESSION_START | session.start | persist_session_start |
SESSION_END | session.end | persist_session_end |
BRANCH_CREATE | branch.create | persist_branch_provenance |
Dormant (vocabulary only; no handler and no emit callsite)¶
| Point | Value | Status |
|---|---|---|
ARTIFACT_CREATED | artifact.created | Deprecated compatibility vocabulary: no emit site or payload contract exists |
ARTIFACT_CREATED is retained only for enum compatibility. Do not register new handlers against it until an artifact owner defines a typed payload and a production emit site. HookBus.on() emits a UserWarning when a handler is registered against any point in lionagi.hooks.DORMANT_POINTS (today, just ARTIFACT_CREATED) so that mistake surfaces at registration time instead of being discovered later as a handler that silently never ran.
Bus dispatch semantics¶
HookBus.emit fires handlers sequentially and logs exceptions without propagating them (isolation invariant). Exceptions: TOOL_PRE and USER_PROMPT_SUBMIT route through blocking_emit, which propagates exceptions so guards can raise PermissionError to abort the tool call or prompt submission. Before a blocking exception is re-raised, the bus records a denial HookSignal whose payload includes denied: true and an exception summary.
USER_PROMPT_SUBMIT fires at most once per genuine user-originated turn, regardless of how many internal calls that turn drives underneath it — see lionagi/operations/_turn_origin.py for the tri-state disposition (unset / forwarded / no-origin) that makes this exactly-once property hold across chat(), chat_and_record(), communicate(), operate(), run(), and ReAct().
StopHook may be raised by any handler to skip remaining handlers on the same point without propagating as an error.
Override semantics (build_session_bus)¶
build_session_bus(agent_hooks=...) is a low-level construction utility for callers that explicitly own a Session bus. It is not consumed automatically by Session or AgentSpec/profile construction. A point present in the mapping replaces its default handler list; an empty list disables a default. Points not mentioned keep their defaults.
bus = build_session_bus(
agent_hooks={"api.post_call": ["log_api_metrics"]},
observer=session.observer,
)
Assigning a separately built bus to Session internals is unsupported. Callers that need the standard Session-owned bus should use session.hooks and register runtime handlers with on().
Tool-event hooks at the invoke chokepoint¶
lionagi.protocols.action.tool_hooks defines a second, mutation-capable tool-event layer, separate from the HookBus points above. It attaches to ActionManager (manager.add_tool_pre_hook(hook) / manager.add_tool_post_hook(hook)) and runs inside ActionManager.invoke, outermost around every tool call the manager mediates — plain function tools, Tool objects, and MCP-discovered tools alike. Constructing a FunctionCalling directly (bypassing the manager) skips this layer entirely; that is a documented, tested limit, not an oversight.
Call order on a single tool invocation:
tool-pre hooks (registration order)
-> Tool.preprocessor (spec-level security/user chain, security_pre last)
-> [rewritten arguments revalidated against Tool.request_options]
-> the tool callable
-> Tool.postprocessor (spec-level chain)
-> tool-post hooks (registration order)
A tool-pre hook receives (tool_name, arguments) and returns None (allow, unchanged), a dict (allow, replace the arguments), or a ToolPreDecision (decision ∈ allow | deny | ask, plus optional reason / updated_input). deny, ask (no interactive-approval surface exists in this runtime), and any unrecognized decision value all fail closed, raising ToolHookDeniedError (a PermissionError) directly out of ActionManager.invoke before the tool ever runs. security_pre always stays the last pre-stage validator: tool-pre hooks run entirely outside the spec-level chain, so any rewrite they make is visible to security_pre, never the other way around.
Whichever layer rewrites the arguments — tool-pre hooks or the spec-level chain — the final dict is revalidated against the tool's request_options (when declared) immediately before the callable executes; a validation failure is captured as a FAILED event (matching the existing spec-level-preprocessor-error convention) rather than raised out of invoke. A tool with no request_options never had schema enforcement, and this step does not invent one.
A tool-post hook receives (tool_name, arguments, result, error) — exactly one of result/error is set — after invocation completes, success or failure. Post hooks are advisory only: a raised exception from one is logged and skipped, and nothing a post hook returns can change the already- completed outcome.
Built-in handlers¶
| Handler | Registered for |
|---|---|
persist_session_start | SESSION_START |
persist_session_end | SESSION_END |
persist_branch_provenance | BRANCH_CREATE |
persist_branch_end | BRANCH_END |
persist_message | name-addressable; routed explicitly by hooks/persist.py, not in DEFAULT_HOOKS |
log_api_metrics | (name-addressable; not in DEFAULT_HOOKS) |
log_tool_call | (name-addressable; not in DEFAULT_HOOKS) |
log_tool_use | (name-addressable; not in DEFAULT_HOOKS; deprecated — use log_tool_call) |
All handlers are name-addressable via the loader registry and can be referenced as strings in the explicit agent_hooks mapping passed to build_session_bus.
AgentSpec coding() guards¶
AgentSpec.coding(secure=True) (the default) wires two security guards via _wire_secure_guards:
guard_destructiveas a pre-hook onbash— blocks destructive shell commands (rm -rf,git push --force,git reset --hard,git clean -fd,drop table,drop database,truncate table,mkfs,dd if=,> /dev/sd*).guard_paths(allowed_paths=[workspace_root])as a pre-hook onreaderandeditor— restricts file access to the workspace root (cwdif provided, elsePath.cwd()at call time). Relative paths are resolved against the workspace root, not the process cwd.
Set secure=False to disable these defaults and manage guards manually.