crates/hooks and is wired by crates/agent and crates/gateway.
The design goal is simple: the agent loop should know lifecycle phases and typed contributions. It should not need memory-specific, skills-specific, or future domain-specific branches for every side behavior.
Why hooks exist
Some work must happen around a turn but should not be part of the main model/tool loop:- classify turn policy before prompt assembly;
- add compact context before prompt compilation;
- contribute model-visible tool bundles;
- add prompt sections;
- run post-turn extraction or cleanup;
- persist hook-run diagnostics for recovery and audit.
Core types
Handlers declare capabilities. A hook that contributes tool bundles must declare the tool-bundle capability. A hook that only reads context and writes internal domain state should not be able to emit prompt or tool contributions by accident.
Turn phases
The phase names are stable because they are also useful in diagnostics, hook-run persistence, and future operator tooling.Contributions
Hooks communicate through typed contributions instead of raw JSON blobs.
The agent loop aggregates these sets at phase boundaries. Domain hooks interpret their own policy and context; the loop stays generic.
Execution policy
Each subscription has execution behavior:
Failure policy is separate. Best-effort hooks can fail without failing the turn; required hooks can fail closed. The runtime records safe diagnostics and avoids putting raw sensitive payloads in summaries.
Dependencies and ordering
Subscriptions are sorted by priority and id, while explicit dependencies determine required ordering. A hook can require another subscription on the same phase to run first. The runtime detects missing dependencies, disabled dependencies, and cycles. Memory uses phase ordering for recall: deterministic memory runs atturn.pre_prompt_context, then active memory runs at turn.post_preflight_prompt_context after the internal preflight plan has been built. The active hook receives typed prompt context plus the preflight-owned active recall plan or fallback.
Thread episodic context follows the same rule. The hook/runtime boundary carries typed prompt context and recall contributions; the agent loop should not know how memvid capsules, thread chunks, or artifact refs are searched. It only dispatches the phase and later consumes the compiled prompt context.
Persistence and recovery
Gateway hook-run persistence records lifecycle state for subscribed work:- started;
- completed;
- skipped;
- failed;
- timed out;
- backgrounded;
- latency and attempt metadata;
- safe diagnostics and contribution hashes.
Durable post-turn execution
The ordinaryFireAndRecord and Background paths use the process-level background queue. They are useful when the work can follow the current invocation, but they are not the authority for a side effect that must survive a terminal commit or a process restart.
Durable post-turn work uses an immutable subscription snapshot and an invocation-owned queue. run_phase_with_snapshot runs the phase with the subscriptions captured by the originating turn. run_phase_to_completion and run_phase_with_snapshot_to_completion also drain every background run created by that invocation before returning, including work queued before a later required hook fails. Concurrent invocations cannot drain or acknowledge one another’s work.
The agent serializes the durable post-turn request and runtime snapshot into the native terminal-effect outbox. The worker resolves the handler at the execution boundary, then checks its version, input contract, supported phase, declared capabilities, and idempotent_side_effect capability. A successful persisted hook run is the idempotency fence for an at-least-once retry. If a required subscription or handler has disappeared or no longer matches the snapshot, recovery fails closed instead of silently running different code.
This path keeps post-turn hooks out of the canonical terminal acknowledgement. The outbox worker applies its own deadline and retry policy, while the resilience worker and post-commit kick provide progress after normal operation and restart.
Memory as a hooked domain
Agent memory is the first heavy domain built on this runtime:MemoryPolicyClassifierHookruns atturn.pre_policy.MemoryDeterministicRecallHookruns atturn.pre_prompt_context.ActiveMemoryRecallHookruns atturn.post_preflight_prompt_context.MemoryToolBundleHookruns atturn.pre_tool_materialization.MemoryPromptContractHookruns atturn.pre_prompt_compile.MemoryPostTurnExtractorHookruns atturn.post_turn.
Adding a hook
When adding a hook:- Define typed input/output using existing hook request and contribution types where possible.
- Give the handler a stable id and kind.
- Declare only the capabilities it needs.
- Register it with a phase, priority, execution policy, failure policy, and visibility.
- Add tests for descriptor stability, capabilities, ordering/dependencies, policy behavior, and diagnostics.
- Keep domain decisions inside the hook or its domain service, not in the agent loop.
Related pages
- Agent Loop shows where turn phases are dispatched.
- Memory Architecture shows a full domain built on hooks.
- Prompt And Context explains how prompt sections and context reach the provider.