CHAPTER 09 · STAGE 02 · 追踪完整对话

Tool:从模型调用到执行结果的完整管线

模型请求中只看到工具名称、说明和参数 JSON Schema。Host 端还需要参数校验、权限策略、超时、取消、并发分类、结果规范化、模型文本渲染和 UI 展示。

一个工具不只是 execute(args)

模型请求中只看到工具名称、说明和参数 JSON Schema。Host 端还需要参数校验、权限策略、超时、取消、并发分类、结果规范化、模型文本渲染和 UI 展示。

因此 ToolDefinition 同时包含模型可见 schema 和 Host-only 执行字段。ctx.tools.schemas() 使用明确 allowlist,只把允许的字段发送给模型,避免实现细节泄漏进请求。

ToolDefinition 的主要部分

初学阶段可以分成五组:

  1. 模型可见:name、description、parameters。
  2. 输出定义:成功值必须满足的 JSON Schema,以及怎样渲染成 ContentBlock。
  3. 执行函数:execute(args, exec)。
  4. 调度元数据:timeoutMs、是否允许并发。
  5. UI 表现:presentCall、presentResult 与 replayable meta。

执行函数返回 canonical JSON value,而不是任意字符串。输出 schema 校验成功后,output renderer 把值转换成模型内容。这让内部结构、模型内容和 UI 表现拥有清晰职责。

模型怎样发起 Tool Call

LLM Adapter 把 Provider 输出转换成统一 ToolCallBlock,其中包含 call id、工具名和原始 arguments JSON 字符串。

AgentLoop 首先把模型请求记录成 tool/call。参数解析或工具查找失败也需要形成对应 tool/result,确保日志中每个调用最终都有结果。

callId 用来把调用与结果配对。它不同于 MessageId 或 SessionId,品牌类型防止跨领域混用。

工具执行管线

一次调用大致经过:

查找 ToolDefinition
  → 解析并校验 arguments
  → tools/pre-execute
  → tools/execute
  → ToolDefinition.execute
  → 规范化成功、失败或取消
  → tools/post-execute
  → ToolDefinition.finalizeContent
  → materialize 最终 JSON 与模型内容
  → tools/result
  → Session tool/result

不同事件的 mode 和职责不同,不能把所有策略都塞进 pre-execute。

tools/pre-execute:允许、拒绝或询问

这是可重排的策略层,适合权限、计划模式和能力级检查。监听器可以返回 allow、deny、ask 等决定,也可以调用 next() 让其他策略继续处理。

如果某条规则必须是最终、单调的拒绝,使用 ctx.tools.guard()。普通 waterfall listener 可能被更外层包装或组合,guard 表达的是不可被后续放宽的约束。

tools/execute:包住真实执行生命周期

超时、重试、计时和执行期度量需要覆盖实际 dispatch 前后,因此适合放在 around-execution waterfall。

wrapper 可以替换 exec.signal,把带超时的 signal 传给下游,然后检查规范化结果。它必须调用 next() 才会真正执行工具。

tools/post-execute:变换结果

当插件需要修改结果、附加 context 或把某类结果变成错误时,使用 post-execute。它仍处于结果成为最终权威内容之前。

如果只想记录最终结果,不应该在这里做无意义的透传变换;直接监听 tools/result 更合适。

finalizeContent:工具拥有的最后模型文本变换

ToolDefinition 可以提供同步、总是可调用的最后内容变换。Registry 会在所有规范化 outcome 上调用一次,包括某些绕过 post-execute 的失败。

它只能替换模型 ContentBlock,其他结果字段仍由 Registry 拥有。该函数必须纯粹且不能抛错,因为它位于最终 materialization 前的最后一步。

tools/result:观察最终权威结果

这是不可变最终 outcome 的观察点,适合审计、指标和捕获。监听器不应该在这里试图修改模型结果。

区分 post-execute 和 result 的口诀是:要改变结果,用 post;只观察最终事实,用 result。

并发与屏障

ToolDefinition 可以通过纯同步分类器声明某个调用允许与兄弟调用重叠。只有明确返回 true 才进入 parallel-safe 组,缺省、异常或其他返回都按 exclusive 处理。

Exclusive 调用形成屏障。Parallel-safe 调用进入有界 rolling pool,但 pre-execute 和持久结果仍维持模型顺序;只有 dispatch/body 部分重叠。

允许并发的工具不能随意修改父级共享状态。共享记录若存在竞争,必须保证结果可交换或失败时保持安全。

取消与超时

超时策略通过 signal 请求工具停止,不会硬杀任意同进程代码。工具必须把 exec.signal 传给文件、网络、子进程或自己的等待逻辑。

Registry 不会遗弃正在运行的 Promise。即使超时或取消,也要等工具拥有的工作收敛,才能安全完成资源释放。

模型内容与 UI Presentation

模型需要简洁、协议正确的 ContentBlock;UI 可能需要标题、终端样式、diff 或文件位置。这两个表现不应混在 execute 的字符串里。

presentCall 只根据 args 生成 pending 展示;presentResult 根据持久结果和 meta 生成 replayable 展示。方法必须纯粹,因为 UI 在实时运行和日志 replay 时都可能调用。

Code Mode 与直接 Tool Call

在 Code Mode 中,模型可能只直接看到 run_code,其他工具以 SDK 形式从代码中调用。核心 Registry 仍负责实际查找、限制和执行,不能因为 schema 展示形式改变就绕开权限管线。

源码阅读顺序

  1. docs/cookbook/adding-a-tool.zh.md
  2. packages/core/tools/src/types.ts
  3. packages/core/tools/src/index.ts
  4. packages/core/agent-loop/src/tool-calls.ts
  5. 一个简单工具,例如 packages/todo/tool-todo/src/index.ts

本篇练习

  1. 画出 pre-execute、execute、post-execute、finalizeContent、result 的顺序。
  2. 为“权限拒绝”“工具计时”“修改错误文本”“记录最终指标”分别选择扩展点。
  3. 找一个真实 ToolDefinition,区分模型可见字段与 Host-only 字段。
  4. 解释为什么 isConcurrencySafe 不能默认 true。

验收标准:给出一个新工具需求时,能同时考虑 schema、canonical output、取消、权限、UI intent 和测试,而不是只写 execute 函数。