# Agent Note: 按单次调用安全性并行执行工具调用

Status: implemented

[English](2026-07-10-parallel-tool-call-execution.md) | 中文

## 问题

一条 assistant 消息可以包含多个并列的 `tool-call` 块。尽管模型已经同时请求了这些调用，串行执行仍会叠加各个独立读取和 Web 请求的延迟。

并发属于宿主调度范畴，不是面向模型的工具元数据。循环需要在不硬编码工具名称、不向 JSON Schema 暴露调度策略的前提下，判断哪些调用可以重叠执行。

会话日志仍是权威记录：每个已启动的调用都有审计事件，正常完成和取消都会使调用与结果配对；无论完成顺序如何，模型历史都按原始调用顺序观察已提交的结果。

## 决策

每个工具都可以提供可选的 `isConcurrencySafe(args)` 分类器。该分类器必须是同步纯函数：它只检查当前调用已解析的参数，不执行 I/O 或任何变更。只有显式返回 `true` 才表示选择并行；分类器缺失、参数无效、分类器抛错或返回任何其他值，都会使该调用按独占方式执行。规范类型契约见[工具数据结构](../../../../docs/core-data-structures/tools.md)。

分类器有意设计为一元函数。返回 `true` 表示工具承诺：此调用可以与任何同样返回 `true` 的并列调用重叠执行。调度器不会比较调用，也不会证明它们的资源访问相容。

这个一元分类器仍然可以感知输入。工具可以将只读操作分类为并行，将变更操作分类为独占。该接口无法表达「仅当路径不同时，这些写入才安全」之类的关系规则，因此，安全性依赖并列调用的调用仍按独占方式执行。

`defineTool()` 先验证参数，再调用类型化分类器。无效参数会被归为独占，且只有该调用真正执行时才会产生常规参数错误。`ctx.tools.executionMode(exec)` 会解析当前有效的工具定义，并返回带标签的 `parallel` 或 `exclusive` 模式；未知工具将以安全方式退化为独占。

使用带标签的模式，而不是公开布尔型调度器 API，使得以后可以表达感知资源的变体，无需改变分类器契约。

## 调度与顺序

循环会等待完整的 assistant 消息，对每个调用只解析一次，为每个调用创建独立的 `ToolExecution`，再按模型顺序扫描。连续的并行调用组成一组；每个独占调用单独组成一组，并构成顺序屏障。各组按顺序执行。分类采用惰性方式：每经过一个屏障，调度器都会解析下一个调用；补充并行池之前，还会重新分类每个后续调用。如果注册表变更使该调用变为独占，当前池会先完全排空，然后该调用才作为下一个屏障启动。

例如：

```text
[parallel read(A), parallel read(B), exclusive write(A), parallel read(C)]

→ [read(A), read(B)]
→ [write(A)]
→ [read(C)]
```

`read(A)` 和 `read(B)` 可以重叠执行。`write(A)` 要等两者都完成后才启动，`read(C)` 则要等写入完成后才启动。

每组都使用一个由 `maxParallelToolCalls` 限制上限的滚动池：循环先按模型顺序启动调用，直到达到上限；每有一个调用结算，就再启动一个。独占组是容量为 1 的池。将上限设为 `1` 可保持串行执行。

只有派发和工具主体会重叠执行。`tools/pre-execute` 和 `tools/post-execute` 按模型顺序运行，因为中间件可能维护对顺序敏感的状态。`tools/execute` 包装层会环绕并发派发运行，因此必须能在不同执行之间重入。

每个已启动的调用都会在进入 pre-execute 门禁之前立即追加 `tool/call`。已完成的派发占据模型顺序的槽位；提交游标只有在下一个槽位就绪时，才会追加 `tool/result` 并收集 `additionalContexts`。实时界面可以显示多个待处理调用，但结果和工具执行后的上下文仍按模型顺序排列。

如果在一组启动前中止，系统不会记录该组的任何调用。如果在一组执行期间中止，系统会停止补充池，等待已启动的调用，按顺序提交其结果，在这些结果之后排空已接受的批次上下文，然后通过现有中止路径结束该步骤。从未启动的调用没有审计事件。调度器发生意外故障时，会停止新的派发，等待每项已启动的派发结算，并重新抛出第一个故障。由于该故障是内部终态，而非工具结果，循环不会为被拒绝或未提交的调用虚构工具结果。

Code Mode 仍不使用此调度器，因为模型只会发出一个原生 `run_code` 调用。`run_code` 及其内部派发队列仍按串行方式执行；`mode: 'both'` 中的原生并列调用使用常规调度器。

## 安全契约

工具返回 `true` 即承诺：其主体可以与其他并行调用同时运行。它不得直接变更父会话或其他由父级拥有的状态；它将输出返回给循环，由循环按模型顺序提交。

执行期间触及的任何共享状态都必须支持并发。这也包括工具包装层和提供方：它们可以在内部串行化，也可以实施自身容量限制，但必须在并发派发时不破坏状态。

## 配置与声明

`maxParallelToolCalls` 是 AgentLoop 的正整数部署上限，由工厂创建的所有 agent（智能体）共享。默认值为 `10`；`1` 保持串行执行。字段和默认值的精确定义见生成的[配置目录](../../../../docs/config-catalog.md)。

当前实现中的声明保持保守。Web 搜索、Web 获取和文件系统读取选择并行。文件系统写入与编辑、bash 工具、subagent 委派、工作流、用户交互、todo 变更、Code Mode 以及 Cordis 变更工具仍按独占方式执行。subagent 可能共享父级的工作区或外部资源，而一元分类器无法证明并列委派的作用互不重叠。Bash 没有已证明的输入敏感分类器，因此仍按独占方式执行。

文件系统读取依赖一个范围很窄的记录器例外：其同步观察更新可以不按顺序结算，但写入和编辑在变更前会重新检查已观察的版本，因此陈旧状态只会导致 `FS_STALE_VERSION`。

## 验证

单元测试覆盖固定了安全退化的分类、类型化参数验证、分组、屏障、替换注册表后的实时重新分类、滚动上限、独立执行对象、中间件顺序、有序结果与上下文、中止排空，以及调度器故障后的完全停稳。第一方测试固定每项并行声明。

快照覆盖固定了可见的多调用 transcript（文本记录）：待处理调用可以重叠执行，已完成结果仍按模型顺序排列。Code Mode 覆盖固定其串行边界。此调度属于确定性循环行为，因此无需依赖提供方的 e2e 测试。

## 备选方案

**保持串行执行。** 这可以避免新的顺序和中止情形，但会保留独立并列调用所产生的不必要延迟。

**使用一个工具级布尔值。** 固定的 `supportsParallelToolCalls` 标志更小，但无法区分同一工具的只读操作和变更操作。感知参数的分类器保留了这项区分。

**使用有状态的分类。** 向分类器提供实时 agent、注册表或 I/O 访问，会使决策依赖分类器的运行时机，并在分类与派发之间留下缺口。可变授权和陈旧状态检查仍属于执行时职责。

**使用感知并列调用或感知资源的分类。** 调度器可以成对比较调用，或让每个调用声明资源读写要求。这样可以并行化不冲突的写入，却要求不相关工具共享资源标识和冲突语义。一元契约选择放弃这部分并发性，并在安全性取决于关系时安全退化。

**并行执行完整的工具流水线。** 这样可以让循环继续使用公开的单调用 API，但会并发运行 pre-execute 和 post-execute 中间件。现有防护和钩子桥可能承载有序状态，因此只允许派发重叠。

**公开分阶段方法或调度 waterfall。** 公开的 `prepare` / `dispatch` / `finalize` 方法或 `tools/execution-mode` 事件，会在出现另一个消费方之前扩大扩展接口。循环使用内部调度器视图，而 `executionMode(exec)` 为策略 seam 保留了插入点。

**将调度器故障转换为工具结果。** AgentLoop 无法判断被拒绝的派发是否已调用工具主体；ToolRegistry 负责工具主体调用状态和类型化工具结果。因此，内部调度器故障保持为终态，而不会被重新分类为 `ABORTED` 结果。

**在模型流式输出时启动调用。** 这可能进一步降低延迟，但会改变 assistant 消息的权威性、回放以及调用/结果配对。调度器只在 assistant 消息完成后才启动。

**使用固定大小的窗口。** 如果在启动下一个窗口前等待当前窗口的每个调用，一个缓慢调用就会使容量闲置。滚动池在保持上限的同时避免了这项延迟。

**向模型暴露并发元数据。** 模型已可以发出并列调用。宿主调度元数据会扩大请求，却无助于工具选择。

## 影响

该设计以安全退化为原则，对工具作者而言也很简单，但无法利用必须通过比较并列调用才能确认安全的并发性。工具过于宽泛地选择并行，可能暴露潜在的共享状态竞态。

在某些情形下，并行调用会先行启动，而串行执行原本会在轮到这些调用之前中止。因此，调度器只记录已启动的调用，在中止时将其排空，且取消后绝不启动替换调用。

有序提交可能会让快速结果等待较慢的早期并列调用。这保留了回放和模型历史顺序，同时实时界面仍可显示待处理进度。

并发外部调用可能会争用配额或进程容量。提供方负责自身容量控制；循环上限只限制一个 agent 步骤中的调用数量。

工具注册是调度边界。调度器会在每个屏障之后以及每次补充池之前重新分类，因此注册表变更会影响尚未启动的调用。已启动的调用保留它们进入池时所依据的调度决策。

终态调度器故障可能会在故障步骤关闭前留下已记录但没有结果的调用。等待仍在运行的派发可确保完全停稳，而不会将这些内部故障误报为工具结果。
