Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. Environment block (OS, shell, platform).
  2. Memory prompt (MEMORY.md content as <memory> block).
  3. Resource hints (available MCP servers, available skills).
  4. Scope block (if agent has restricted skills/MCPs/members, appends a <scope> XML block listing allowed resources).
  5. Tool whitelist computation (restricts config.tools based 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:

  1. Agent descriptions (if the agent has delegation members).
  2. Memory auto-recall (BM25 search on last user message, as <recall> block).
  3. 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_agent be async to support hooks that need I/O during agent construction?
  • Should preprocess support returning multiple messages (e.g. for multi-skill invocation)?