# Hook System Reference ## Overview The hook system provides **extensibility points** at both the Agent and AgentHarness layers. Hooks are asynchronous, can be cancelled via abort signal, and run in subscription order. --- ## Hook Categories ### 1. Message Transformation Hooks #### `convertToLlm` **Location**: `AgentLoopConfig.convertToLlm` **Purpose**: Convert `AgentMessage[]` to `Message[]` before LLM call. **When called**: Just before each LLM request. **Key contract**: - Must not throw or reject - Must handle all `AgentMessage` variants - Filter out UI-only messages (notifications, artifacts, etc.) **Example**: ```typescript convertToLlm: (messages) => messages.filter(m => m.role === "user" || m.role === "assistant" || m.role === "toolResult" ) ``` #### `transformContext` **Location**: `AgentLoopConfig.transformContext` (optional) **Purpose**: Manipulate context before LLM conversion. **When called**: Before `convertToLlm`. **Use cases**: - Context window management (pruning old messages) - Injecting external context - Message deduplication **Example**: ```typescript transformContext: async (messages, signal) => { if (estimateTokens(messages) > MAX_TOKENS) { return pruneOldMessages(messages); } return messages; } ``` --- ### 2. Lifecycle Hooks #### `beforeToolCall` **Location**: `AgentLoopConfig.beforeToolCall` (optional) **Context**: ```typescript interface BeforeToolCallContext { assistantMessage: AssistantMessage; toolCall: AgentToolCall; args: unknown; // Validated against tool schema context: AgentContext; } ``` **Return**: ```typescript interface BeforeToolCallResult { block?: boolean; // If true, tool won't execute reason?: string; // Error message shown in tool result } ``` **When called**: After args validation, before tool execution. **Use cases**: - Permission checks (user approval) - Rate limiting - Context-aware tool blocking **Example**: ```typescript beforeToolCall: async ({ toolCall, args, context }, signal) => { if (toolCall.name === "bash" && signal?.aborted) { return { block: true, reason: "Operation aborted" }; } return undefined; // Allow execution } ``` #### `afterToolCall` **Location**: `AgentLoopConfig.afterToolCall` (optional) **Context**: ```typescript interface AfterToolCallContext { assistantMessage: AssistantMessage; toolCall: AgentToolCall; args: unknown; result: AgentToolResult; isError: boolean; context: AgentContext; } ``` **Return**: ```typescript interface AfterToolCallResult { content?: (TextContent | ImageContent)[]; details?: unknown; isError?: boolean; usage?: Usage; terminate?: boolean; // Early termination hint } ``` **When called**: After tool execution, before emitting `tool_execution_end`. **Use cases**: - Modify tool results (redact sensitive data) - Update usage tracking - Trigger early termination **Example**: ```typescript afterToolCall: async ({ result }, signal) => { // Redact sensitive content const content = result.content.map(c => { if (c.type === "text") { return { ...c, text: redactSecrets(c.text) }; } return c; }); return { content }; } ``` #### `shouldStopAfterTurn` **Location**: `AgentLoopConfig.shouldStopAfterTurn` (optional) **Context**: ```typescript interface ShouldStopAfterTurnContext { message: AssistantMessage; toolResults: ToolResultMessage[]; context: AgentContext; newMessages: AgentMessage[]; } ``` **Return**: `boolean` **When called**: After `turn_end`, before draining steering/follow-up queues. **Use cases**: - Stop when goal achieved - Stop before context gets too large - Error recovery **Example**: ```typescript shouldStopAfterTurn: async ({ message, toolResults, context }) => { // Stop if model indicates task complete if (message.content.some(c => c.type === "text" && c.text.includes("TASK_COMPLETE"))) { return true; } // Stop if context too large if (estimateTokens(context.messages) > MAX_TOKENS * 0.8) { return true; } return false; } ``` #### `prepareNextTurn` **Location**: `AgentLoopConfig.prepareNextTurn` (optional) **Context**: Same as `ShouldStopAfterTurnContext` **Return**: ```typescript interface AgentLoopTurnUpdate { context?: AgentContext; model?: Model; thinkingLevel?: ThinkingLevel; } ``` **When called**: After `shouldStopAfterTurn`, if not stopping. **Use cases**: - Update model based on conversation context - Switch thinking level - Inject new context **Example**: ```typescript prepareNextTurn: async ({ message, toolResults, context }) => { // Switch to higher reasoning for complex tasks if (toolResults.length > 3) { return { thinkingLevel: "high" }; } return undefined; // Keep current config } ``` --- ### 3. Queue Draining Hooks #### `getSteeringMessages` **Location**: `AgentLoopConfig.getSteeringMessages` (optional) **Return**: `Promise` **When called**: After turn ends, before next LLM call. **Purpose**: Inject messages to interrupt agent mid-workflow. **Mode**: `"all"` or `"one-at-a-time"` (controls how many messages injected) **Example**: ```typescript getSteeringMessages: async () => { // Check for user input while agent is working if (userQueue.length > 0) { return userQueue.splice(0, 1); // one-at-a-time mode } return []; } ``` #### `getFollowUpMessages` **Location**: `AgentLoopConfig.getFollowUpMessages` (optional) **Return**: `Promise` **When called**: When agent would stop (no more tool calls, no steering messages). **Purpose**: Queue messages for after agent finishes. **Mode**: `"all"` or `"one-at-a-time"` **Example**: ```typescript getFollowUpMessages: async () => { // Check if user typed while agent was working if (followUpQueue.length > 0) { return followUpQueue.splice(0, 1); } return []; } ``` --- ## AgentHarness Hooks ### 1. System Prompt Hooks #### `before_agent_start` **Location**: `AgentHarness.on("before_agent_start")` **Event**: ```typescript { type: "before_agent_start"; prompt: string; images?: ImageContent[]; systemPrompt: string; resources: AgentHarnessResources; } ``` **Return**: ```typescript { messages?: AgentMessage[]; systemPrompt?: string; } ``` **When called**: Before agent starts, after system prompt generated. **Use cases**: - Add conversation hints - Inject images - Modify system prompt ### 2. Context Hooks #### `context` **Location**: `AgentHarness.on("context")` **Event**: ```typescript { type: "context"; messages: AgentMessage[]; } ``` **Return**: ```typescript { messages: AgentMessage[]; } ``` **When called**: Before `convertToLlm`. **Use cases**: - Message filtering - Context window management - Message augmentation ### 3. Tool Hooks #### `tool_call` **Location**: `AgentHarness.on("tool_call")` **Event**: ```typescript { type: "tool_call"; toolCallId: string; toolName: string; input: Record; } ``` **Return**: ```typescript { block?: boolean; reason?: string; } ``` **When called**: Before tool execution. **Use cases**: - Audit logging - Approval workflows - Input validation #### `tool_result` **Location**: `AgentHarness.on("tool_result")` **Event**: ```typescript { type: "tool_result"; toolCallId: string; toolName: string; input: Record; content: (TextContent | ImageContent)[]; details: unknown; isError: boolean; usage?: Usage; } ``` **Return**: ```typescript { content?: (TextContent | ImageContent)[]; details?: unknown; isError?: boolean; usage?: Usage; terminate?: boolean; } ``` **When called**: After tool execution. **Use cases**: - Result transformation - Usage tracking - Early termination ### 4. Session Hooks #### `session_before_compact` **Location**: `AgentHarness.on("session_before_compact")` **Event**: ```typescript { type: "session_before_compact"; preparation: BranchPreparation; branchEntries: SessionTreeEntry[]; customInstructions?: string; signal: AbortSignal; } ``` **Return**: ```typescript { cancel?: boolean; compaction?: CompactionResult; } ``` **When called**: Before compaction. **Use cases**: - Skip compaction in certain conditions - Provide custom summary - Abort compaction #### `session_before_tree` **Location**: `AgentHarness.on("session_before_tree")` **Event**: ```typescript { type: "session_before_tree"; preparation: { targetId: string; oldLeafId: string; commonAncestorId: string; entriesToSummarize: SessionTreeEntry[]; userWantsSummary: boolean; customInstructions?: string; replaceInstructions?: boolean; label?: string; }; signal: AbortSignal; } ``` **Return**: ```typescript { cancel?: boolean; summary?: { summary: string; details?: unknown; usage?: Usage; }; customInstructions?: string; replaceInstructions?: boolean; } ``` **When called**: Before tree navigation (branching). **Use cases**: - Skip branch summary - Provide custom summary - Cancel navigation ### 5. Provider Hooks #### `before_provider_request` **Location**: `AgentHarness.on("before_provider_request")` **Event**: ```typescript { type: "before_provider_request"; model: Model; sessionId: string; streamOptions: AgentHarnessStreamOptions; } ``` **Return**: ```typescript { streamOptions: AgentHarnessStreamOptionsPatch; } ``` **When called**: Just before each LLM request. **Use cases**: - Add authentication headers - Set request metadata - Configure caching #### `before_provider_payload` **Location**: `AgentHarness.on("before_provider_payload")` **Event**: ```typescript { type: "before_provider_payload"; model: Model; payload: unknown; } ``` **Return**: ```typescript { payload: unknown; } ``` **When called**: Just before sending payload to LLM. **Use cases**: - Payload transformation - Debug logging - Schema validation --- ## Hook Execution Order ### Full Turn Flow ``` 1. AgentHarness.prompt() │ ├─► emit "before_agent_start" │ └─► Hook can return new messages/systemPrompt │ ▼ 2. AgentLoopConfig creation │ ├─► transformContext hook → AgentLoop.transformContext ├─► convertToLlm hook → AgentLoop.convertToLlm ├─► beforeToolCall hook → AgentLoop.beforeToolCall ├─► afterToolCall hook → AgentLoop.afterToolCall ├─► prepareNextTurn hook → AgentLoop.prepareNextTurn ├─► shouldStopAfterTurn hook → AgentLoop.shouldStopAfterTurn ├─► getSteeringMessages hook → AgentLoop.getSteeringMessages └─► getFollowUpMessages hook → AgentLoop.getFollowUpMessages │ ▼ 3. streamAssistantResponse() │ ├─► emit "before_provider_request" (harness) │ └─► Hook can modify stream options ├─► transformContext() (agent) ├─► convertToLlm() (agent) ├─► streamFn() → LLM call └─► Emit message_start/update/end events │ ▼ 4. executeToolCalls() │ ├─► For each tool call: │ ├─► emit "tool_call" (harness) │ │ └─► Hook can block execution │ ├─► tool.execute() │ └─► emit "tool_result" (harness) │ └─► Hook can override result │ ▼ 5. turn_end │ ├─► emit "turn_end" (agent) ├─► shouldStopAfterTurn() (agent) │ └─► Return true to exit ├─► prepareNextTurn() (agent) │ └─► Hook can update context/model/thinkingLevel ├─► Drain steering queue └─► Drain follow-up queue │ ├─► If steering/follow-up: repeat from #3 └─► If no more: agent_end │ └─► emit "agent_end" (agent) ``` --- ## Queue Mode Behavior ### `"all"` Mode All queued messages are injected at once: ``` Agent would continue... → getFollowUpMessages returns [msg1, msg2, msg3] → All three injected together → Agent processes all before next turn ``` ### `"one-at-a-time"` Mode One message injected at a time: ``` Agent would continue... → getFollowUpMessages returns [msg1] → msg1 injected → Agent processes msg1 → After turn, getFollowUpMessages returns [msg2] → msg2 injected → Agent processes msg2 → ...and so on ``` --- ## Abort Signal Propagation All hooks receive an optional `AbortSignal`: ```typescript interface BeforeToolCallContext { // ... other fields // signal is NOT included - use agent.signal instead } ``` **Agent hooks**: - `transformContext`: receives `signal` - `beforeToolCall`: receives `signal` - `afterToolCall`: receives `signal` **Harness hooks**: - `before_agent_start`: receives `signal` - `context`: NO signal - `tool_call`: NO signal - `tool_result`: NO signal - `session_before_compact`: receives `signal` - `session_before_tree`: receives `signal` - `before_provider_request`: receives `signal` - `before_provider_payload`: NO signal --- ## Error Handling ### Hook Errors **Agent layer**: Hook errors are caught and encoded in tool results: ```typescript try { const beforeResult = await config.beforeToolCall(...); if (beforeResult?.block) { return immediateError(beforeResult.reason); } } catch (error) { return immediateError(error.message); } ``` **Harness layer**: Hook errors are wrapped and re-thrown: ```typescript try { const result = await handler(event); } catch (error) { throw normalizeHookError(error); } ``` ### Best Practices 1. **Always handle errors**: Wrap async operations in try/catch 2. **Respect abort signals**: Check `signal.aborted` in long operations 3. **Return safe defaults**: Return empty arrays/objects on errors 4. **Don't block**: Hooks should be fast (no network calls) 5. **Idempotent**: Hooks should be safe to run multiple times --- ## Common Patterns ### 1. Context Window Management ```typescript transformContext: async (messages, signal) => { if (signal?.aborted) return messages; const tokenCount = estimateTokens(messages); if (tokenCount > MAX_TOKENS * 0.9) { return pruneOldestMessages(messages, Math.floor(MAX_TOKENS * 0.3)); } return messages; } ``` ### 2. Permission-Gated Tools ```typescript beforeToolCall: async ({ toolCall, args }, signal) => { if (toolCall.name === "bash" && signal?.aborted) { return { block: true, reason: "Operation aborted" }; } if (toolCall.name === "bash" && !await canExecuteBash(args)) { return { block: true, reason: "Permission denied" }; } return undefined; } ``` ### 3. Result Redaction ```typescript afterToolCall: async ({ result }) => { const content = result.content.map(c => { if (c.type === "text") { return { ...c, text: redactSecrets(c.text) }; } return c; }); return { content }; } ``` ### 4. Early Termination ```typescript shouldStopAfterTurn: async ({ message }) => { // Check if model indicates completion if (message.content.some(c => c.type === "text" && c.text.includes("TASK_COMPLETE"))) { return true; } // Check if all tool calls set terminate return false; } ``` ### 5. Audit Logging ```typescript tool_call: async ({ toolCallId, toolName, input }) => { console.log(`[TOOL_CALL] ${toolName} (${toolCallId}):`, input); return undefined; } tool_result: async ({ toolCallId, toolName, content, isError }) => { console.log(`[TOOL_RESULT] ${toolName} (${toolCallId}):`, { hasError: isError, contentLength: content.length }); return undefined; } ``` --- ## Summary | Hook | Layer | When | Can Block? | |------|-------|------|------------| | `convertToLlm` | Agent | Before LLM call | No (sync) | | `transformContext` | Agent | Before `convertToLlm` | Yes (async) | | `beforeToolCall` | Agent | After validation | Yes (async) | | `afterToolCall` | Agent | After execution | Yes (async) | | `shouldStopAfterTurn` | Agent | After turn_end | Yes (async) | | `prepareNextTurn` | Agent | Before next turn | Yes (async) | | `getSteeringMessages` | Agent | After turn_end | Yes (async) | | `getFollowUpMessages` | Agent | When agent would stop | Yes (async) | All hooks are **optional** and have sensible defaults.