--- name: agent-runtime-hooks description: "Agent runtime lifecycle hooks for observing and intercepting agent execution. Use when adding hooks to agent operations, mocking tool calls, logging step events, handling human intervention, sub-agent calls, context compression, or building eval/tracing integrations. Triggers on 'hooks', 'beforeToolCall', 'afterToolCall', 'beforeStep', 'afterStep', 'onComplete', 'onError', 'tool mock', 'agent lifecycle', 'human intervention', 'callAgent', 'compact'." user-invocable: false --- # Agent Runtime Hooks Lifecycle hooks for observing and intercepting agent execution. Hooks are registered per-operation via `execAgent({ hooks })` and dispatched by `HookDispatcher`. ## Hook Types 16 hook types across 5 categories: ``` execAgent({ hooks }) │ ├─ beforeStep ──────────── Before each step executes │ │ │ ├─ [call_llm] LLM inference │ │ │ ├─ [call_tool] │ │ ├─ beforeToolCall ── Before tool executes (supports mocking) │ │ ├─ (tool execution) │ │ ├─ afterToolCall ─── After tool completes (observation only) │ │ └─ onToolCallError ─ Tool threw an exception │ │ │ ├─ [request_human_approve] │ │ ├─ beforeHumanIntervention ── Before agent pauses │ │ ├─ afterHumanIntervention ─── After approve/reject + resume │ │ └─ onStopByHumanIntervention ── User rejected, agent halted │ │ │ ├─ [compress_context] │ │ ├─ beforeCompact ──── Before compression starts │ │ ├─ afterCompact ───── After compression completes │ │ └─ onCompactError ─── Compression failed │ │ │ ├─ [callAgent] (via execSubAgentTask) │ │ ├─ beforeCallAgent ── Before sub-agent starts │ │ ├─ afterCallAgent ─── After sub-agent completes │ │ └─ onCallAgentError ── Sub-agent failed │ │ │ └─ afterStep ──────────── After step completes │ ├─ (next step...) │ ├─ onComplete ───────────── Operation reaches terminal state └─ onError ──────────────── Error during execution ``` ## Key Files | File | Role | | ---------------------------------------------------------- | ------------------------------------------------------ | | `packages/agent-runtime/src/types/hooks.ts` | Type definitions (AgentHookType, all event interfaces) | | `src/server/services/agentRuntime/hooks/types.ts` | Server-side types (AgentHook, re-exports) | | `src/server/services/agentRuntime/hooks/HookDispatcher.ts` | Registration, dispatch, dispatchBeforeToolCall | | `src/server/modules/AgentRuntime/RuntimeExecutors.ts` | Tool/Compact/HumanIntervention hook dispatch | | `src/server/services/agentRuntime/AgentRuntimeService.ts` | Step hooks + HumanIntervention resume/reject | | `src/server/services/aiAgent/index.ts` | CallAgent hook dispatch | ## Registration Flow ```ts const hooks: AgentHook[] = [ { id: 'my-hook', type: 'afterStep', handler: async (event) => { ... } }, ]; await aiAgentService.execAgent({ agentId, prompt, hooks }); // Internally: hookDispatcher.register(operationId, hooks) // Cleanup: hookDispatcher.unregister(operationId) ``` ## Hook Reference ### Step Level **`beforeStep`** — Before each step. `event: AgentHookEvent` **`afterStep`** — After each step. `event: AgentHookEvent` (content, toolsCalling, totalCost, etc.) **`onComplete`** — Terminal state. `event: AgentHookEvent` (reason: done/error/interrupted/max_steps/cost_limit) **`onError`** — Error occurred. `event: AgentHookEvent` (errorMessage, errorDetail) ### Tool Call Level **`beforeToolCall`** — Before tool executes. **Supports mocking** via `event.mock()`. ```ts // event: ToolCallHookEvent { (identifier, apiName, args, callIndex, stepIndex, operationId, mock); } // Mock example: event.mock({ content: '{"error":"rate limited"}' }); ``` Dispatch method: `hookDispatcher.dispatchBeforeToolCall()` (returns mock result or null). **`afterToolCall`** — After tool completes. Observation only. ```ts // event: AfterToolCallHookEvent { (identifier, apiName, args, callIndex, content, success, mocked, executionTimeMs, stepIndex); } ``` **`onToolCallError`** — Tool threw an exception (catch block, not just `success=false`). ```ts // event: ToolCallErrorHookEvent { (identifier, apiName, args, callIndex, error, stepIndex); } ``` ### Human Intervention **`beforeHumanIntervention`** — Before agent pauses for approval. ```ts // event: BeforeHumanInterventionHookEvent { operationId, stepIndex, pendingTools: [{ identifier, apiName }] } ``` **`afterHumanIntervention`** — After approve/reject, agent resumes. ```ts // event: AfterHumanInterventionHookEvent { operationId, action: 'approve' | 'reject' | 'rejectAndContinue', toolCallId?, rejectionReason? } ``` **`onStopByHumanIntervention`** — User rejected, agent halted. ```ts // event: StopByHumanInterventionHookEvent { operationId, toolCallId?, rejectionReason? } ``` ### Context Compression **`beforeCompact`** — Before compression starts. ```ts // event: BeforeCompactHookEvent { (operationId, stepIndex, messageCount, tokenCount); } ``` **`afterCompact`** — After compression completes. ```ts // event: AfterCompactHookEvent { (operationId, stepIndex, groupId, messagesBefore, messagesAfter, summary); } ``` **`onCompactError`** — Compression failed. ```ts // event: CompactErrorHookEvent { (operationId, stepIndex, tokenCount, error); } ``` ### Sub-Agent (CallAgent) **`beforeCallAgent`** — Before calling sub-agent. Dispatched on **parent** operation. ```ts // event: BeforeCallAgentHookEvent { (operationId, agentId, instruction); } ``` **`afterCallAgent`** — Sub-agent completed. Dispatched on **parent** operation. ```ts // event: AfterCallAgentHookEvent { (operationId, agentId, subOperationId, threadId, success); } ``` **`onCallAgentError`** — Sub-agent failed. Dispatched on **parent** operation. ```ts // event: CallAgentErrorHookEvent { (operationId, agentId, error); } ``` Note: CallAgent hooks require `parentOperationId` in `ExecSubAgentTaskParams`. ## Design Notes - **Fire-and-forget**: All handlers return `Promise`. Errors are non-fatal. - **Exception**: `beforeToolCall` supports mock via `event.mock()` — uses `dispatchBeforeToolCall()` which returns the mock result. - **Sequential**: Same-type hooks run in registration order. - **Local only**: `beforeToolCall` mock only works in local mode (in-memory hooks). Webhook mode does not support mocking. - **Scoped per operation**: Auto-cleaned via `hookDispatcher.unregister()` on completion. - **Sandbox/MCP**: No separate hooks — they go through `executeTool`, so `beforeToolCall`/`afterToolCall` cover them. Use `event.identifier` to filter. ## Real-World Example: agent-evals See `devtools/agent-evals/helpers/runner.ts` — `createEvalHooks()` uses `afterStep`, `onComplete`, `afterToolCall`, and `beforeToolCall` (for mock).