ToolOrchestration工具批执行解析
ToolOrchestration工具批执行解析
1. 这份文档回答什么问题
这份文档聚焦:
当 query.ts 收到一批 tool_use 之后,toolOrchestration.ts 是如何决定哪些工具并发跑,哪些工具串行跑的。
2. 核心定位
实现文件是:
src/services/tools/toolOrchestration.ts
这个模块不是单个工具的执行器,它更像:
工具批执行调度器。
它的职责不是“怎么跑 Bash”或“怎么跑 Read”,而是:
- 先看这一轮模型吐出了哪些工具调用
- 再按并发安全性切批
- 然后决定并发执行还是串行执行
3. runTools 是入口
入口函数是:
runTools(toolUseMessages, assistantMessages, canUseTool, toolUseContext)
它的工作流程可以压成三步:
partitionToolCalls(...)- 对每个批次判断是并发还是串行
- 统一把结果包装成
MessageUpdate流向上游返回
所以 query.ts 在调用 runTools(...) 时,其实是把一整批 tool_use 交给了一个“批处理调度层”。
4. partitionToolCalls 在做什么
最关键的函数是:
partitionToolCalls(...)
它会遍历本轮所有 tool_use,并为每一个工具调用判断:
- 这个工具是不是
isConcurrencySafe
判断方法不是只看工具名,而是:
- 先找到工具定义
- 用
inputSchema.safeParse(...)校验输入 - 若校验通过,再执行
tool.isConcurrencySafe(parsedInput.data)
这意味着:
- 并发安全性可以是“和参数相关”的
- 同一个工具,不同输入也可能出现不同的并发判定
5. 批次划分规则
partitionToolCalls(...) 返回的是一组 Batch:
isConcurrencySafe: booleanblocks: ToolUseBlock[]
划分规则很简单,但很关键:
5.1 非并发安全工具
如果某个工具不安全并发:
- 它会单独形成一个批次
- 后面必须串行执行
5.2 并发安全工具
如果连续多个工具都安全并发:
- 它们会被合并进同一个批次
- 后面会作为一个并发批处理
所以这个模块不是“全局并发”或“全局串行”,而是:
按工具序列切成若干并发批和串行批。
6. 为什么这样切,而不是全量并发
因为 Claude Code 的工具里有很多副作用型工具:
- 文件写入
- Shell
- 可能影响会话状态的操作
如果全量并发,会出现很多问题:
- 写入顺序错乱
- 上下文状态竞争
- 工具输出相互污染
- 权限和锁的行为不稳定
所以它只把“可证明读多写少、无明显副作用”的工具合并并发跑。
7. 串行路径
串行路径函数是:
runToolsSerially(...)
它的行为很直接:
- 逐个取出
toolUse - 把该 tool ID 标记为 in-progress
- 调
runToolUse(...) - 如果工具执行过程中有
contextModifier,立刻更新currentContext - 执行结束后把 tool ID 从 in-progress 集合移除
这里最关键的是:
串行路径里的上下文修改是立刻生效的。
所以后一个工具看到的是前一个工具已经更新后的 ToolUseContext。
8. 并发路径
并发路径函数是:
runToolsConcurrently(...)
它通过:
all(...)
把多个并发安全工具放进一个受限并发度的执行池。
并发上限来自:
CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY
默认值是:
10
这说明 Claude Code 虽然支持并发工具执行,但依旧给它加了并发阈值,不让它无上限地同时跑。
9. 并发路径为什么不立即改上下文
这是 runTools() 里最重要的细节之一。
在并发批里,执行过程中如果某个工具返回了 contextModifier,它并不会立刻修改 currentContext。
而是先按 toolUseID 收集到:
queuedContextModifiers
等整批并发工具都跑完后,再按原始 blocks 顺序依次应用这些 modifier。
这样做的目的很明确:
- 保持上下文更新顺序稳定
- 避免谁先跑完就先改上下文,导致竞态
所以并发批的策略是:
执行可以并发,上下文提交仍然按原始顺序串行落地。
10. runToolUse 在这里扮演什么角色
toolOrchestration.ts 自己不懂具体工具逻辑。
它最终还是把每个工具调用交给:
runToolUse(...)
所以职责边界是:
toolOrchestration.ts:决定怎么排队、怎么切批toolExecution.ts:真正执行单个工具
这种分层让“调度策略”和“单工具执行细节”分开了。
11. inProgressToolUseIDs 的作用
无论并发还是串行,模块都会更新:
toolUseContext.setInProgressToolUseIDs(...)
这说明工具调度不只是后台逻辑,它还会把“哪些工具正在跑”暴露给上层状态。
这类状态通常会被 UI、权限系统或中断逻辑使用。
12. 一句话总结
toolOrchestration.ts 的核心不是“执行工具”,而是:
把模型吐出的工具调用按并发安全性切成若干批次,让可并发的工具并发跑,不可并发的工具串行跑,同时仍保持上下文提交顺序稳定。