mcpField
A
agent-runtime-hooks

agent-runtime-hooks

github.com/lobehub开发83.6k98 次安装今天更新
代码仓库

关于 agent-runtime-hooks

为使用 `execAgent` 的开发者提供 Agent Runtime 生命周期钩子,可观察步骤、工具调用、压缩及人工干预等事件。支持本地处理、模拟工具结果和 Webhook 决策控制。

主要功能

  1. 01覆盖 16 种生命周期事件
  2. 02按注册顺序分发钩子
  3. 03本地处理器可模拟工具结果
  4. 04通过 Webhook 接收事件
  5. 05工具调用前后可允许或拒绝

适用场景

  1. 01记录 Agent 步骤和工具调用
  2. 02模拟工具结果进行测试
  3. 03通过 Webhook 审核工具执行
  4. 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'。

评论

支持 Markdown