Main Chain B · Step 14

ToolRuntime:三段把关流水线

13 页的调度器最终把每个调用交给 packages/core/tools/src/index.ts(1955 行)的 ToolRuntime。这一页拆解它的执行流水线:pre-execute 决定「让不让跑」,execute 跑 body,post-execute 决定「结果怎么呈现」。也是主干 B 的终点。

13 startCall:169 prepare · commitReady:151 finalize/finish → tools/src/index.ts:1348 execute → :1352 completeScheduledExecution → :1370 createExecution → (主干 B 终点) 工具结果 → 11 页 appendToolResult 落盘 → (旁支入口) 15 页:具体工具(bash)从哪来

示例本次示例:bash("ls -la") 的三段流水线

示例轨迹 14-1 · execute 的三段实况
# 13 页 prepare 调 ctx.tools[TOOL_RUNTIME_SCHEDULER].prepare(call.exec)
#   → 本页 createExecution(1364 行)
#     坍缩检查:mode 不是 code → collapsed=false
#     view(agent) 里可见 bash → 正常路径
#   → tools/pre-execute waterfall(scopeTarget(registry, agent) 过滤):
#     permission-presets 判定「读目录」为免审 → PreToolDecision: allow
#     (若判定必审 → ask → approval/request → 用户批准/拒绝,29 页)
#   → guards 通过 → prepared.kind = 'dispatch'
# 13 页 dispatch → tools/execute waterfall → next() = dispatchToolBody
#   → bash 工具的 execute 函数本体(17 页 330 行)执行
# 13 页 finalize → tools/post-execute waterfall:
#     结果 accept,无 additionalContexts
#   → freeze(exec) → tools/result emit(深冻结最终结果)
# 13 页 appendToolResult 落盘(seq 9)
来源:14 页行级解读 + 17 页 bash body + 29 页审批域
示例轨迹 14-2 · Code Mode 下的同一调用
# 若会话 mode='code',模型直接叫 bash:
# 1381 行 collapsed=true(可见但 code 模式坍缩)
# → 确定性拒绝为 UNKNOWN_TOOL,pre-execute/ask/guards 全都看不到
# → 理由:不能给策略机会去批准一个必然失败的调用
# 模型只能调 run_code,在程序里经 SDK 调 bash(子调度桥回同一流水线)
来源:14 页 §2 + code-mode.ts 的 run_code 桥

§1入口与三段分派

packages/core/tools/src/index.tsexecute → completeScheduledExecution1348-1368
1348  async execute(exec: ToolExecutionInput): Promise<ToolExecutionResult> {
1349    return this.prepareExecution(exec, prepared => this.completeScheduledExecution(prepared))
1350  }
1352  private async completeScheduledExecution(prepared: ScheduledToolPreparation): Promise<ToolExecutionResult> {
1353    switch (prepared.kind) {
1354      case 'dispatch': {
1355        const dispatched = await this.dispatchScheduledExecution(prepared.exec)
1357          ? await this.finalizeScheduledExecution(prepared.exec, dispatched.result)
1358          : this.finishScheduledExecution(prepared.exec, dispatched.result)
1368  }
1364-1367

普通调用者的唯一入口。13 页的并行调度器不走这里——它用 TOOL_RUNTIME_SCHEDULER 暴露的分段接口(prepare/dispatch/finalize/finish)自己编排(JSDoc 明说那是内部视角,不是插件扩展点)。

1353-1367

三段由 prepared.kind 决定:dispatch(正常:pre 通过 → body → 有 post 结果则 post)→ post-result(pre 直接给了结果,仍需 post)→ final-result(终局,post 也不走——如 UNKNOWN_TOOL)。

1364-1367

closed-union 的 assertNever 兜底——AGENTS.md「switch 判别标签」约定的标准形态(merge-extensible union 才用带文档的 default 兜底)。

§2createExecution:mode 坍缩与守卫

packages/core/tools/src/index.tscreateExecution(节选)1370-1458
1354  private createExecution(exec) {
1355    const deferredContexts: UserMessage[] = []
1356    const token = createExecutionToken()
1357    const callId = exec.callId
1358    const rootCallId = exec.rootCallId ?? callId
1370    const visible = this.get(name, agent)
1371    const collapsed = visible !== undefined && this.collapses(name, agent, parent !== undefined)
1372    const concludingExecutions = this.concludingExecutions
1374    const base = {
1375      token, callId, rootCallId, name, signal,
1380      ...agent !== undefined ? { agent } : {},
1381      ...parent !== undefined ? { parent } : {},
1382      deferContext(context) { deferredContexts.push(context) },
1385      concludeTurn() { concludingExecutions.add(this) },
1388    }
1356

createExecutionToken:相关性令牌——在策略流水线开始前就创建,让日志/遥测在 pre-execute 之前就能关联这次执行。

1370-1371

坍缩判定:可见(view 里找得到)但 mode 是 code 时模型调了非 run_code 工具 → collapsed。这类调用在策略流水线之前被确定性拒绝——注释原文:「pre-execute listeners, approval ask, and guards must never observe — or worse, approve — a call that can only fail」。未知工具(不可见)则保留历史 dispatch 阶段的 UNKNOWN_TOOL 路径,让策略仍能看到每个到达注册表的名字。

1381-1385

两个执行期能力:deferContext(推迟的上下文,结算时成为 additionalContexts → 11 页的 next-step)与 concludeTurn(请求以本调用终结 turn——13 页的 concluded 来源)。

§3三个 waterfall 的语义

事件阶段返回值语义作用域
tools/pre-executecreateExecutionPreToolDecision:allow / deny / ask(ask 走审批 seam 的 serviceAsk)scopeTarget(registry, exec.agent) 过滤
tools/executedispatchScheduledaround-dispatch:next() 即 dispatchToolBody;wrapper 只能替换 exec.signal同上过滤
tools/post-executefinalizeScheduledPostToolDecision:accept(可替换 content/value)/ block(转 isError)/ 可附加 additionalContexts同上过滤
tools/resultfinishScheduledemit(非否决):freeze(exec) 后的深冻结最终结果,供持久化/UI 观察—

invariant.ts(128 行)通过 internal/dispatch 强制阶段顺序(pre → execute → post → result)、结果冻结、run_code 子调度的事件围栏。取消语义的两个 canonical code:TOOL_ABORTED(body 已启动后被取消)与 TOOL_ABORTED_BEFORE_DISPATCH(body 未启动,13 页 §3 用过)。fuseToolSignals 融合 caller 与 wrapper 信号但不嵌套 AbortSignal.any,且 wrapper 只能替换 signal、不能解除 caller 取消——取消权单向传递。

§4主干 B 结束:回望全程

到这里,一条消息走完了它的全程。回望:

followup/steer/inject → inbox splice(07)→ kick/turn 循环(05)→ preStep claim + assemble(08、12)→ buildRequest 从日志折叠请求(09)→ llm/stream waterfall → adapter 流(10)→ 逐 chunk 落盘 + 完成锚点(11、06)→ executeToolCalls 调度(13)→ ToolRuntime 三段流水线(14)→ tool/result 落盘 → 还有欠账则下一 step。

整条链上你反复见到的模式:先落盘再改内存(turn/step 边界、inbox、chunk)、waterfall 决定 / emit 通知(pre-step、request、request-error、turn-stopping、llm/stream、tools/*)、冻结性贯穿(deepFreeze 从事件到请求到结果)、fail-loud(审计、坍缩、header 校验)。这些就是 dsh 的「语感」。

◈ 分岔点 → 15 页:能力缝与注入

主干到这里讲完了「工具调用如何执行」。但你还没见过「工具从哪来」——14 页的 registry 里注册的每个工具(bash、fs、web……)都是某个插件贡献的。15 页从这个分岔点出发:一个具体能力(shell)如何按「Service Definition / Provider / Consumer」三件套组装,并沿调用链追踪 ctx.tools.execute → ctx.shell.run → ctx.subprocess.spawn 的注入路径。