一、为什么工具要「过流水线」
在 nanobot 里,工具是 Tool 抽象 + 注册表,执行相对直接。Harness 走得更重:它在 ctx.tools(约 5600 行,最大的核心包)里把执行做成了带把关的流水线。官方对 ctx.tools 的定义一句话点题:
注册能力,负责 Code Mode 传输,并让调用依次经过策略前处理、单调守卫、环绕分派、策略后处理和最终结果观测。
注意主语是「调用依次经过」——循环本身不知道钩子/权限/沙箱的存在,这些都挂在流水线上。这正是 Cordis「拦截和策略优先用事件」的实践规则。
二、一条调用的完整旅程
官方文档给了精确顺序(见下图)。从模型吐出 tool-call 到模型看到结果,中间有十步:
- 模型产出
tool-call块; - 记录
tool/call事件(执行「前」就落盘)——这是「模型可见即已记录」不变量的体现; tools/pre-execute瀑布:钩子、权限、沙箱;- 单调守卫(monotonic guards):deny / abstain,身份受保护;
ctx.approval一次性询问(无人应答 → deny);tools/execute瀑布:超时、重试、指标(环绕分派);- 工具本体
execute(); tools/post-execute瀑布:接受 / 拦截 / 替换 / 补充上下文;finalizeContent→tools/result(冻结的权威结果);- 记录
tool/result事件(唯一面向模型的产出)。
几个值得记住的点:
tool/call先于执行、tool/result是终态——两者都是SessionEvent,直接服务「日志即真相源」的设计。- 守卫与审批分离:单调守卫是「不得重排序的所有者策略」,而
ctx.approval在守卫之前处理「询问」;审批不可用就 deny,不会卡住循环。 post-execute能补上下文:工具可以在结果外额外注入 user/message,且通过additionalContextsFIFO 保证「调用与结果相邻」。
三、并发调度:一个批次里多个工具怎么跑
tool-calls.ts 的 executeToolCalls 是编排精华,三种语义:
- 独占屏障(exclusive):某些调用必须串行独占;
- 有界并行池:
fillPool维护maxParallelToolCalls,后到的 call 会重新分类——一旦某 call 不是 parallel,就在此开一道新屏障; - 模型序提交(commitReady):只沿模型序连续提交已就绪的 slot(
tools.finalize/finish),保证结果顺序与模型意图一致。
最讲究的是 abort 语义:
已开始的调用照常提交;未开始的写合成错误结果
TOOL_ABORTED_BEFORE_DISPATCH。
为什么要「合成一个错误」而不是静默丢弃?因为会话日志必须保持可重放(replay)——如果中途消失一个调用,日志就对不上了。合成错误让 replay 仍然有效。这又把我们拉回那条核心不变量:可见即记录。
四、小结
Harness 的工具观:**
- 执行不是「调一下函数」,而是一条不变循环、可变策略的流水线;
- 并发不是「全并行」,而是「屏障 + 有界池 + 模型序提交」的精密编排;
- 连中止都要为「日志可重放」让路。
下一篇我们看最精彩的一处:能力 seam 与统一沙箱世界——为什么换一个 Provider,Bash / PTY / LSP 会一次性全搬走。