0075 - Hook
- Feature Name: Hook Lifecycle
- Start Date: 2026-03-15
- Discussion: #75
- Crates: core, runtime, daemon
Summary
The Hook trait is the central extensibility point for agent lifecycle. It defines five methods that the runtime calls at specific points: building an agent, registering tools, preprocessing input, injecting context before a run, and observing events. Everything that customizes agent behavior — skills, memory, MCP, scoping, prompt injection — composes through this trait.
Motivation
When the runtime was split out of the daemon (#75), a clean interface was needed
between the runtime (which executes agents) and the hook implementations (which
customize them). The runtime must not know about skills, memory, MCP, or daemon
infrastructure. It only knows it has a Hook and calls its methods at the right
times.
This separation enables two modes: the daemon (full hook with skills, MCP, memory, event broadcasting) and embedded use (no hook, or a minimal one).
Design
The trait
#![allow(unused)]
fn main() {
pub trait Hook: Send + Sync {
fn on_build_agent(&self, config: AgentConfig) -> AgentConfig;
fn on_register_tools(&self, tools: &mut ToolRegistry) -> impl Future<Output = ()>;
fn preprocess(&self, agent: &str, content: &str) -> String;
fn on_before_run(&self, agent: &str, session_id: u64, history: &[Message]) -> Vec<Message>;
fn on_event(&self, agent: &str, session_id: u64, event: &AgentEvent);
}
}
All methods have default no-op implementations. () implements Hook.
Lifecycle points
on_build_agent — called when an agent is registered with the runtime.
Receives the agent config, returns a modified config. This is where the system
prompt is composed. The RuntimeHook implementation chains:
- Environment block (OS, shell, platform).
- Memory prompt (MEMORY.md content as
<memory>block). - Resource hints (available MCP servers, available skills).
- Scope block (if agent has restricted skills/MCPs/members, appends a
<scope>XML block listing allowed resources). - Tool whitelist computation (restricts
config.toolsbased on scope).
on_register_tools — called at runtime startup. Registers tool schemas
(name, description, JSON schema) into the ToolRegistry. No handlers — dispatch
is separate. RuntimeHook registers: OS tools, skill tool, task/delegate tool,
ask_user tool, memory tools (if enabled), and MCP-discovered tools.
preprocess — called before a user message enters the conversation. Used
for slash command resolution: /skill-name args is transformed into the skill
body wrapped in a <skill> tag. Happens before tool dispatch.
on_before_run — called before each agent execution (send/stream). Returns
messages to inject into the conversation. RuntimeHook injects:
- Agent descriptions (if the agent has delegation members).
- Memory auto-recall (BM25 search on last user message, as
<recall>block). - Working directory announcement (as
<environment>block).
All injected messages are marked auto_injected: true — they’re ephemeral, not
persisted, stripped before each run, and refreshed.
on_event — called after each agent step. Receives every AgentEvent
(text deltas, tool calls, completions). DaemonBridge uses this to broadcast
events to console subscribers.
Composition
RuntimeHook<B: RuntimeBridge> is the engine hook. It composes SkillHandler,
McpHandler, Memory, and AgentScope maps. It implements Hook by
orchestrating all subsystems.
DaemonHook is a type alias: RuntimeHook<DaemonBridge>. The daemon bridge
adds ask_user dispatch, delegate dispatch, session CWD, and event broadcasting.
For embedded use, RuntimeHook<NoBridge> provides the full engine without
daemon infrastructure.
Tool dispatch
RuntimeHook::dispatch_tool is the central routing table — a match on tool
name. It’s not part of the Hook trait itself (the trait only registers
schemas). The runtime calls dispatch_tool when an agent produces a tool call.
Dispatch enforces scoping before routing.
Alternatives
Separate traits per concern. One trait for prompt building, one for tools, one for events. Rejected because they always compose together and the single trait is simpler to implement and reason about.
Closure-based hooks. Pass lambdas instead of a trait. Rejected because the hook needs shared state (skill registry, MCP connections, memory) that closures make awkward.
Unresolved Questions
- Should
on_build_agentbe async to support hooks that need I/O during agent construction? - Should
preprocesssupport returning multiple messages (e.g. for multi-skill invocation)?