关于 agent-runtime-hooks
为使用 `execAgent` 的开发者提供 Agent Runtime 生命周期钩子,可观察步骤、工具调用、压缩及人工干预等事件。支持本地处理、模拟工具结果和 Webhook 决策控制。
主要功能
- 01覆盖 16 种生命周期事件
- 02按注册顺序分发钩子
- 03本地处理器可模拟工具结果
- 04通过 Webhook 接收事件
- 05工具调用前后可允许或拒绝
适用场景
- 01记录 Agent 步骤和工具调用
- 02模拟工具结果进行测试
- 03通过 Webhook 审核工具执行
- 04跟踪人工审批与子 Agent 事件
MCP 相关
README 提及 MCP 工具与其他工具共用同一事件路径;可通过 identifier、apiName 或 toolSource 筛选相关事件。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ lobehub/ lobehub --skill agent-runtime-hooks安装后在 Claude Code 中输入/agent-runtime-hooks确认已加载。
详细介绍
Agent Runtime Hooks
通过 execAgent({ hooks }) 注册生命周期 Hook。HookDispatcher 会按操作存储 Hook,并按注册顺序分发。
Hook 类型
16 种 Hook 类型,分为 5 个类别:
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 (optional result control)
│ │ └─ 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 creation/start returns
│ │ └─ onCallAgentError ── Sub-agent failed
│ │
│ └─ afterStep ──────────── After step completes
│
├─ (next step...)
│
├─ onComplete ───────────── Operation reaches terminal state
└─ onError ──────────────── Error during execution
关键文件
| 文件 | 作用 |
|---|---|
packages/agent-runtime/src/types/hooks.ts | 事件类型及必需/可选字段 |
apps/server/src/services/agentRuntime/hooks/types.ts | 注册和 webhook 类型 |
apps/server/src/services/agentRuntime/hooks/HookDispatcher.ts | 注册、分发、本地模拟以及 HTTP/QStash 投递 |
apps/server/src/services/agentRuntime/hooks/webhookPayload.ts | 事件映射和邮件增强 |
apps/server/src/modules/AgentRuntime/adapters/toolCallHookContext.ts | 共享的工具事件上下文构建器 |
apps/server/src/modules/AgentRuntime/adapters/ServerToolTransport.ts | 工具通知和执行结果 |
apps/server/src/modules/AgentRuntime/RuntimeExecutors.ts | 工具、压缩和人工干预执行 |
apps/server/src/services/agentRuntime/AgentRuntimeService/ (executeStep/, intervention.ts) | 步骤事件和人工干预的继续执行 |
apps/server/src/services/agentRuntime/CompletionLifecycle/index.ts | 终止事件 |
apps/server/src/services/aiAgent/subAgentRuns.ts | 子 Agent 事件 |
注册
const hooks: AgentHook[] = [
{
id: 'observe-step',
type: 'afterStep',
handler: async (event) => {
console.log(event.operationId, event.stepIndex);
},
},
];
await aiAgentService.execAgent({ agentId, prompt, hooks });
操作完成后会通过 hookDispatcher.unregister(operationId) 清理注册项。
Webhook 钩子
仅使用 Webhook 的钩子在本地模式和队列模式下都会触发。对于同时配置了 handler 和 webhook 的钩子,本地分发会调用处理程序;队列分发会使用序列化后的 Webhook。持久化的只有 Webhook 配置,因此进程重启后函数将不可用。
const hook: AgentHook = {
id: 'tool-notification',
type: 'afterToolCall',
matcher: '^fs/readFile$',
webhook: {
url: 'https://example.com/hooks',
delivery: 'fetch',
timeout: 5, // seconds; default 30
headers: { Authorization: 'Bearer ${HOOK_TOKEN}' },
allowedEnvVars: ['HOOK_TOKEN'],
},
};
matcher是针对${identifier}/${apiName}的正则表达式,仅支持工具事件。省略、留空或设为*时,将匹配所有工具。- Header 环境模板仅在发送时从
allowedEnvVars中解析;应持久化模板,切勿持久化解析后的密钥。 - 通知会忽略响应内容,并可返回 HTTP 204。
beforeToolCall搭配responseHandling: 'toolCall'时,会在执行工具前等待可选的 HTTP 决策。返回 HTTP 2xx JSON,内容为{ "decision": "allow" }或{ "decision": "deny", "reason": "禁止执行该操作" },即可进行干预。类型为{ decision: 'allow' } | { decision: 'deny'; reason?: string };如果拒绝时省略了reason或其值为空,则使用Blocked by beforeToolCall hook.。拒绝会在工具结果和卡片中保留原因,并标记为hook_denied。afterToolCall也接受responseHandling: 'toolCall'。它会在归档和模型使用结果之前,同步检查完整请求和结果;拒绝时,会用接收方提供的可选原因替换原始结果,并将结果标记为已拦截。如果拒绝时省略了reason或其值为空,则使用Blocked by afterToolCall hook.,遵循与beforeToolCall相同的Blocked by ${type} hook.规则。与beforeToolCall一样,原因会进入模型上下文和工具卡片。它不会撤销工具的副作用、重试或费用。有关延迟完成和运行时边界,请参阅结果控制约定。- 环境响应模式为
ignore(默认通知)和toolCall(用于 before 和 after 的可选决策)。只有已订阅的事件会启用。请将已移除的toolResult/toolCallAndResult值替换为toolCall;使用事件列表选择仅在 after 阶段进行检查。现有的 after 响应只要包含有效决策,现在就会控制结果。 - HTTP 204、成功但响应体为空、无效 JSON,以及不含顶层
decision的 JSON(包括数组、标量、null 或仅包含旧版嵌套hookSpecificOutput的 JSON)都属于通知响应,即使设置了onError: 'block'也是如此。继续检查后续钩子;不要将通知视为允许跳过后续策略。显式无效决策、非 2xx 状态、超大响应体、无效 UTF-8、网络故障和超时遵循onError: 'continue' | 'block'(默认为continue);阻止执行时,before 和 after 钩子都会将hook_control_error作为工具结果内容和原因。取消操作绝不会授予权限。 - 允许存在额外的响应字段,但这些字段会被丢弃。仅会使用
decision和拒绝时可选的字符串reason;updatedInput/additionalContext不会更改工具参数或对话上下文。 - 配置:
packages/types/src/agentHook.ts;响应解析:packages/types/src/agentHookResponse.ts;HTTP 交付:apps/server/src/services/agentRuntime/hooks/httpWebhook.ts。
Hook 配置生命周期
- 环境钩子(
AGENT_HOOK_WEBHOOK_*)始终取自执行工作进程的当前环境,包括人工批准和延迟完成之后。绝不持久化其配置,也不恢复先前环境中的钩子。 - 代码提供的 webhook 钩子(
execAgent({ hooks }))保留现有的按操作序列化和持久化行为。从运行时快照或持久化操作记录中恢复这些调用方钩子;保留环境变量模板,不将密钥展开后存入存储。 - 按顺序评估每个操作中恢复的调用方钩子,然后在最后一轮评估一次当前环境钩子。不要跨操作对调用方钩子 ID 去重。仅由环境提供的钩子无需持久化操作存储。移除环境钩子后,它便不再控制后续结果;当没有匹配的结果控制规则时,允许该结果通过。不要在批准或重启期间冻结先前环境策略。
- 延迟完成只向其空占位符发布一次;重复回调保留第一个最终结果,包括拒绝结果。传播调用方策略存储错误,不要将读取失败视为空钩子列表。不要将默认放行扩展至已配置钩子的交付失败:这些失败遵循
onError。 - 在回归测试中覆盖批准期间和快照过期后的环境钩子移除/添加、无审查标记的发布,以及持久化调用方钩子的持续强制执行。
在结果钩子之后发布
- 在归档、写入消息内容/状态、流式传输和模型消费之前,等待结果控制完成。直接持久化允许的结果或已净化的拒绝结果;不要添加
toolResultControl待处理/已允许/已阻止元数据,也不要通过历史记录投影隐藏原始输出。 - 人工批准来源负责认领决策,并在其私有解析/发件箱中保留答复。
MessageModel.resolveHumanApproval默认仅认领;只有运行时会在结果控制之后以publishResult: true调用它。停止/拒绝回执不包含已执行的工具输出,因此可以直接发布。 - 通过现有的干预操作标识或延迟回调的父操作恢复原始调用方钩子。将内存中的钩子判定与工具自有状态分开;工具返回
type: 'blocked'并不代表钩子判定。 - 每次延迟回填,包括启动失败的成员,都遵循相同的先运行钩子、后写入顺序。使用现有空占位符仲裁重复的最终写入;不要引入替代的审查状态机。
- 当线程具有子 Agent 或群组成员完成桥接时,该桥接独占其工具结果的发布。线程生命周期钩子只更新线程元数据;绝不要预先填入未经门控的摘要,包括批准恢复之后。
- 覆盖钩子运行期间被暂存的答复、原子发布与回滚、策略移除后的重复回调,以及部分成员启动失败。历史记录读取方不应需要任何结果审查逻辑。
事件
| 事件 | 时机和载荷 |
|---|---|
beforeStep | 步骤开始前;AgentHookEvent |
afterStep | 步骤结束后;内容、工具调用/结果和使用量总计 |
onComplete | 终止状态;原因,例如 done、error、interrupted、max_steps 或 cost_limit |
onError | 操作错误;errorMessage、errorDetail 和可用的错误元数据 |
beforeToolCall | 工具执行前;共享工具上下文和本地 mock() 回调 |
afterToolCall | 工具执行后;结构化的 result 和 mocked |
onToolCallError | 工具执行抛出错误;共享工具上下文和 error |
beforeHumanIntervention | 审批前;pendingTools、operationId、stepIndex |
afterHumanIntervention | 审批决定和继续执行;action、可选的 toolCallId 和 rejectionReason |
onStopByHumanIntervention | 人工拒绝后停止运行;可选的 toolCallId 和 rejectionReason |
beforeCompact | 压缩前;messageCount、tokenCount、stepIndex |
afterCompact | 压缩后;groupId、messagesBefore、messagesAfter、summary |
onCompactError | 压缩错误;error、tokenCount、stepIndex |
beforeCallAgent | 创建子 Agent 前;agentId、instruction |
afterCallAgent | 创建/启动返回后;agentId、subOperationId、success、可选的 threadId |
onCallAgentError | 子 Agent 调用失败;agentId、error |
CallAgent 事件会在由 parentOperationId 标识的父操作上分发。独立子 Agent 会提供 threadId;共享组成员使用共享对话。子 Agent 完成情况由其自身的 onComplete 事件报告。
工具上下文
对三个工具事件使用 buildToolCallHookContext()。
- 必需字段:原生
toolCallId、assistantMessageId、identifier、apiName、生效的args、callIndex、stepIndex、operationId和executor。 - 可选关联信息来自运行上下文:Agent、主题、会话、线程、组、任务、工作区、文档、来源/工具消息和父操作 ID。
userId使用runtime.userId ?? origin.userId。toolSource标识工具的来源;executor标识服务/客户端分发路由。executionTarget和activeDeviceId描述运行的执行计划和设备选择。afterToolCall.result包含content、success、可选的executionTime,以及错误/状态数据,例如state.type: 'blocked'。mocked标记由本地钩子提供的结果。
沙盒工具和 MCP 工具使用相同的事件路径。可通过 identifier、apiName 或 toolSource 进行筛选。
本地模拟
dispatchBeforeToolCall() 会提供 event.mock(result)。第一个被接受的模拟结果生效,并会短路其余模拟处理器。若有模拟结果,该方法返回 { isMocked: true, result };否则返回 null 以继续执行。
event.mock({ content: '{"items":[]}', success: true });
工具观察载荷使用 BeforeToolCallObservationEvent;回调属于内存中的处理器事件。dispatchBeforeToolCall() 不会发送 Webhook。
Webhook 载荷
createWebhookPayloadBuilder() 会选择 eventFields,添加 hookId/hookType,然后合并 webhook.body。对于重叠字段,最终值由正文决定。工具结果通知载荷使用 redactResultForEvents() 精简原始技能 Work 数据;控制端会收到完整结果。Work 注册会保留进程内的完整结果。finalState 可供本地处理器使用;序列化载荷则携带事件的数据字段。
选择电子邮件时,构建器会根据最终有效的 userId 解析 userEmail,并替换提供的电子邮件值。仅包含电子邮件的投影会使用事件 ID。显式无效 ID、缺少电子邮件或查询错误都会导致电子邮件字段被省略。
电子邮件查询会在每个分发器中共享缓存,最多缓存 1,000 位用户,缓存时间为 5 分钟。查询超时由数据库层处理。构建器的可选第四个 { signal } 参数允许等待方独立取消;取消时会返回 undefined,以便调用方停止投递。Fetch 和 QStash 会使用增强后的载荷。
投递错误
分发会等待每个处理器或 Webhook 完成。普通错误会被记录,分发会继续。fallback: 'none' 的 Webhook 会累积为 CriticalHookDeliveryError,并在同级 Hook 全部运行后抛出。QStash 投递默认会回退到 fetch;对于使用 QStash 签名的端点,请使用 fallback: 'none'。