Skip to content

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 (decisionallow | 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_destructive as a pre-hook on bash — 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 on reader and editor — restricts file access to the workspace root (cwd if provided, else Path.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.