Agent Lifecycle Hooks¶
Hooks let application code observe or govern agent boundaries without moving that policy into model instructions. They are synchronous with the boundary: the run waits for each handler before it continues.
Events¶
| Event | Boundary | May edit | May deny |
|---|---|---|---|
user_prompt_submit |
Immediately before the prompt enters the adapter | yes | yes |
pre_tool_use |
After argument canonicalization and before approval/execution | yes | yes |
post_tool_use |
After one successful tool execution | no | no |
post_tool_use_failure |
After one failed tool execution | no | no |
A denied prompt is never transported to the model. A denied tool call does not execute and does not produce a post-failure event. Denial is policy behavior, not a tool execution failure.
Each executed tool call emits pre_tool_use and exactly one terminal event:
post_tool_use on success or post_tool_use_failure on failure.
Kedi Configuration¶
Handlers are ordinary Python callables available to the Kedi environment:
```
from pathlib import Path
from kedi import PreToolUseDecision, UserPromptSubmitDecision
audit_events = []
def redact_prompt(event):
return UserPromptSubmitDecision.edit(
event.content.replace("customer@example.com", "[email]")
)
def constrain_report_path(event):
if event.tool_name != "write_report":
return None
filename = Path(event.arguments["path"]).name
return PreToolUseDecision.edit(
{**event.arguments, "path": str(Path("reports") / filename)}
)
def observe_tool(event):
audit_events.append((event.event, event.tool_name, event.tool_call_id))
```
> hooks:
user_prompt_submit: `redact_prompt`
pre_tool_use: `constrain_report_path`
post_tool_use: `observe_tool`
post_tool_use_failure: `observe_tool`
A field may evaluate to one callable or a sequence of callables. Use the short form to control inherited lexical/profile handlers:
> hooks: disabled
disabled does not unregister handlers attached directly to an adapter
instance. The expanded form enables the handlers it declares.
The directive is valid at top level, inside a procedure, and inside a profile. Top-level hooks are captured by following procedures according to normal source order.
Decisions¶
Prompt handlers return None or UserPromptSubmitDecision:
from kedi import UserPromptSubmitDecision
UserPromptSubmitDecision.continue_()
UserPromptSubmitDecision.edit("replacement prompt")
UserPromptSubmitDecision.deny("prompt violates application policy")
Pre-tool handlers return None or PreToolUseDecision:
from kedi import PreToolUseDecision
PreToolUseDecision.continue_()
PreToolUseDecision.edit({"path": "reports/output.md"})
PreToolUseDecision.deny("writes are disabled in this run")
None means continue. Only edit may carry replacement content or arguments,
and only deny may carry a denial reason. Invalid combinations fail before the
boundary continues. Post-success and post-failure handlers are observers and
must return None.
Edited tool arguments are revalidated before approval and execution. A handler cannot bypass a tool signature, argument validator, risk resolver, secret-file classification, or approval policy by editing the request.
Event Data¶
Every event contains:
| Field | Meaning |
|---|---|
event |
Stable event name |
run_id |
Identity of the current agent run |
sequence |
Monotonic event sequence within that run |
adapter_shortname |
Active adapter |
parent_run_id |
Parent run for delegated work, when present |
agent_name |
Active agent identity, when present |
profile_name |
Active profile identity, when present |
Tool events additionally contain tool_call_id, logical tool_name, optional
native_tool_name, origin, immutable arguments, optional description, and
immutable metadata. origin is one of kedi, mcp, adapter_native, or
provider_builtin.
PostToolUseEvent.result contains the native successful result.
PostToolUseFailureEvent contains error_type, error_message, the original
exception when the adapter exposes it, duration_seconds when the execution
boundary can measure it, and an interrupted flag for cancellation or process
interruption. It is emitted only after tool execution starts; hook and approval
denials are not tool-execution failures. Payload values are available to the
handler but omitted from event representations and default telemetry.
Ordering and Errors¶
Handlers execute serially in this order:
- adapter constructor
hook_handler=; - adapter event-specific handlers registered with
adapter.on(...); - lexical/profile handlers in merge and source order.
An edit becomes the input seen by the next handler. A denial short-circuits the
remaining chain. A raised exception becomes HookExecutionError; cancellation
propagates and drains the active handler. Sync boundaries accept sync handlers.
Use an adapter's async API when a handler itself is async.
Hook dispatch is suspended while a handler runs. Agent work started by a hook does not recursively invoke the same active chain.
Python API¶
Register one handler for one or several events:
import kedi
audit_events = []
@kedi.on(("post_tool_use", "post_tool_use_failure"))
def audit_terminal_event(event):
audit_events.append((event.run_id, event.tool_call_id, event.event))
kedi.configure(), kedi.context(), @kedi.query, and @kedi.bind accept a
hooks= mapping. Adapter instances expose @adapter.on(...), and supported
adapter constructors accept one catch-all hook_handler= callable.
Profiles and Subagents¶
Profile-local hooks belong to that profile. A child profile runs its own local handlers. Direct top-level/procedure hook directives and Python API registrations are enforcement policy and propagate to descendants. A parent's profile-local handler is not copied into an unrelated child profile.
Child events use the child run_id and identify the child agent/profile.
parent_run_id links to a parent when a parent run context is available; it
may be None without that context. Do not use its absence alone to classify
a tool as a top-level call. Stream observation provides run lifecycle context
for UIs that need a complete parent/child activity tree.
Persistence¶
Interactive session dumps never pickle hook handlers. Kedi records only an
importable module and qualified name. A top-level function from an importable
module can be restored; lambdas, local functions, closures, bound methods, and
__main__ handlers make the dump fail explicitly. Nothing is silently omitted.
Adapter Support¶
| Adapter | Prompt | Pre tool | Post tool | Tool failure | Tool origins |
|---|---|---|---|---|---|
| Pydantic AI | yes | yes | yes | yes | Kedi, MCP, adapter-native |
| LangChain | yes | yes | yes | yes | Kedi, MCP, adapter-native |
| Claude Agent SDK | yes | yes | yes | yes | Kedi, MCP, provider built-ins |
| Codex App Server | yes | yes | yes | yes | Kedi projected tools |
| WebGPU | yes | yes | yes | yes | Kedi, MCP |
| ACP | yes | no | no | no | none |
| DSPy | no | no | no | no | none |
Kedi validates support per event. A configuration that requests an unsupported event fails before model transport and is reported by the LSP. Dynamic adapter selection defers that check to runtime.
Security and Telemetry¶
Hooks execute trusted Python and can inspect prompt, argument, result, and error payloads. Keep audit sinks bounded and redact before forwarding data to another system. A post hook cannot undo an effect that already succeeded.
The in-memory lists above are small examples, not durable audit logs. A failing post hook may make the run fail after the tool has already performed its effect; re-running that tool can duplicate the effect. Keep policy in pre-hooks and approval, and use Stream Events for non-blocking UI progress.
Kedi emits one telemetry span for a nonempty hook chain and records event name, handler count, adapter, origin, outcome, edit/deny status, duration, failures, and timeouts. Prompt text, arguments, tool result, error message, metadata, and handler return values are not captured by default.