query.ts 工具分发链

目标

这页只回答一个问题:

query() 主循环里,模型产出的 tool_use 是如何被系统分发、执行,再回灌成 tool_result 的?

一句话结论

query() 本身不直接执行每个工具,而是把工具执行委托给工具调度层:

  • toolOrchestration.ts
  • StreamingToolExecutor.ts
  • toolExecution.ts

关键角色

1. query.ts

负责主循环:

  • 调模型
  • 收流式输出
  • 识别 tool_use
  • 进入工具执行阶段
  • 把结果重新拼回消息流

2. toolOrchestration.ts

负责“传统分发”逻辑:

  • 把一批 tool calls 按 concurrency-safe 与否分组
  • 并发安全的批次并发跑
  • 非并发安全的批次串行跑

3. StreamingToolExecutor.ts

负责“流式工具执行”逻辑:

  • 工具一边流入一边执行
  • 可并发的工具可同时跑
  • 非并发工具需要独占
  • 结果按原始顺序产出
  • progress 可抢先产出

4. runToolUse()

真正跑某一个具体工具。

toolOrchestration.ts 的核心思路

分批规则:partitionToolCalls()

它会先看每个 tool 是否 isConcurrencySafe

  • 如果是并发安全工具,就可以和相邻并发安全工具归为同一批
  • 如果不是,就单独成批,串行执行

也就是把一串 tool_use 切成这样:

[并发批][单个独占工具][并发批][单个独占工具]

并发批:runToolsConcurrently()

并发批内部:

  • 多个工具同时执行
  • 并发上限由 CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY 控制
  • 每个工具最后还是独立调用 runToolUse()

串行批:runToolsSerially()

串行批内部:

  • 一个一个执行
  • 每执行完一个工具,可能更新 currentContext
  • 再带着更新后的 context 进入下一个工具

为什么 context 需要回传?

因为有些工具不只是“产出消息”,还可能修改上下文。

所以调度层不仅要返回:

  • message

还要返回:

  • newContext

也就是说,tool execution 不只是 side effect,还可能改变后续工具和后续 query 的运行环境。

StreamingToolExecutor.ts 的价值

相比传统“整批执行”,StreamingToolExecutor 更适合流式场景。

它做了几件很重要的事

1. 工具一到就排队

addTool() 收到新的 tool_use 后:

  • 先找工具定义
  • 判断是否 concurrency-safe
  • 放进内部队列
  • 立刻尝试 processQueue()

2. 用队列状态控制调度

每个工具都有状态:

  • queued
  • executing
  • completed
  • yielded

这让它可以既并发执行,又按顺序吐结果。

3. 允许 progress 抢先输出

工具运行中的 progress 不需要等最终结果 ready 才给上层, 会放进 pendingProgress 并尽快 yield。

4. Bash 错误会级联取消兄弟工具

实现里有个很有意思的策略:

  • 如果某个工具报错,不一定取消其它工具
  • 但如果是 BashTool 报错,会触发 sibling abort

原因写得挺直白:

  • bash 常常有隐式依赖链
  • 前一条命令炸了,后面一串并发命令继续跑很可能没意义

5. 用户中断和 streaming fallback 会生成 synthetic error

也就是:

  • 不是简单粗暴消失
  • 而是给模型补一个明确的错误型 tool_result

这样主循环和模型都能感知:

  • 这个工具没正常完成
  • 原因是被拒绝 / 被中断 / streaming fallback

和 AgentTool 的关系

AgentTool 并不绕过这套分发系统。

也就是说,当模型输出:

tool_use(name=Agent)

调度层做的事跟 BashTool / ReadTool 本质一样:

  1. 找到 AgentTool
  2. runToolUse()
  3. runToolUse() 内部再进入 AgentTool.call()
  4. AgentTool.call() 再决定如何起子 agent

所以:

agent 调用本质上是“经过统一 tool dispatch 的一种特殊工具调用”。

关键设计意义

1. 主循环不用知道每个工具的细节

query() 只需要知道:

  • 有 tool_use
  • 扔给工具调度器

2. 并发控制被抽离出来

主循环不需要自己处理:

  • 哪些工具能并行
  • 哪些要串行
  • progress 何时吐
  • 中断如何传播

3. AgentTool 能自然接入现有工具基础设施

因为 agent 就是工具,所以它自动获得:

  • 权限检查链路
  • 并发调度机制
  • progress 通道
  • 错误回传机制

当前理解

如果把主循环看成大脑, 那工具调度层就是它的手和神经系统:

  • query.ts 决定“该去用工具了”
  • toolOrchestration / StreamingToolExecutor 决定“这些工具怎么排队、怎么跑、怎么回消息”
  • runToolUse() 决定“具体执行哪个工具实现”

后续待追

  • toolExecution.ts
  • query.ts 内调用工具调度器的具体位置
  • AgentTool.call() 如何与 runToolUse() 的协议对接