Files
deepseek-harness/.agents/notes/implemented/feature/2026-07-26-code-mode-live-parallel-dispatch.zh.md
T
Tianyi Cui 8a79679489 feat(tools): live dispatch lifecycle + native-contract parallel sub-calls in Code Mode
The bridge replaces its serialization queue with a pool that reuses the
native concurrency contract: submissions classify through
registry.executionMode (fail-closed isConcurrencySafe), start strictly in
submission order, overlap up to the validated maxParallelSubCalls config
(default 10; 1 restores serial), and exclusive calls drain the pool, run
alone, and bar later calls. Each started sub-call logs a
tool/code-dispatch-start event at pool entry; the existing
tool/code-dispatch settles the pair (started ⇔ settles exactly once;
abandoned queued calls log neither). SDK prompt guidance now states the
true Promise.all contract — re-recorded across every code/both-mode
snapshot (plus the stale cordis-dynamic-toolchain fixture gaining the
required description arg).

Client: CodeSubCall widens to RunningToolCall | ToolResultNode — starts
land the running shape (rows wear the native running ring), settles
replace in place preserving start order, callTime pairs to the start
time. Fixture emits start/settle pairs; jsdom pins the running sub-row;
runtime specs pin in-place settlement and out-of-order completion.
2026-07-26 06:02:36 +08:00

5.1 KiB
Raw Blame History

Agent Note:Code Mode 的实时分发生命周期,以及复用原生契约的并行执行

Status: implemented

English | 中文

范围:Code Mode UI 堆叠 PR(Pull Request)链的第三个 PR,涵盖 tool/code-dispatch-start 事件、web chat 中每个子调用的运行状态,以及桥接层调度器对原生并发契约的复用。构建在宿主侧基础与 chat 子调用行之上;原生契约本身归并行工具调用 Agent Note 所有。

问题

前两个 PR 之后仍留有两个缺口。子调用行过去只在每次分发结算(settle)后才出现:某次分发运行期间,UI 对它毫无展示,于是一个慢的子调用看上去就像父调用卡住了。而桥接层过去把每一次绑定调用都串行化(「即使 Promise.all 也一次只执行一个」),这是工具尚未携带并发元数据时留下的占位实现:如今 isConcurrencySafe 已经存在,agent loop(智能体循环)调度器早已在有界并发池中运行原生兄弟调用,而一个等待三个独立读取的 Code Mode 程序,付出的延迟却是原生路径的 3 倍。

决策

一对生命周期事件,一份调度契约,与原生共用。

  • 事件对:tool/code-dispatch-start(父/子 id、名称、规范化参数)在调度器真正启动某个调用时才追加,而非在提交时,因此因 run 结算而被放弃的排队调用不会留下任何日志。既有的 tool/code-dispatch 结算该事件对(subCallId 相同);每个已启动的调用恰好结算一次(中止也会作为 isError 结果经由流水线结算)。计时即这两个事件的 time 字段。两个事件都保持仅日志;模型上下文不受影响;格式保持 v0。
  • 桥接层调度器:已提交的调用在提交那一刻就经 registry.executionMode 分类(与 loop 所用完全相同的 fail-closed isConcurrencySafe 契约),并严格按提交顺序启动。连续被分类为可并行的调用可以重叠执行,上限为 maxParallelSubCalls(经校验的注册表 Config 字段,默认值 10,即 loop 调度器自身的默认值;设为 1 即恢复串行分发);独占调用则先排空池、独自运行,并阻挡其后的调用。这是把 loop 的分组语义适配到另一种场景:调用随时间陆续到达,而非作为单个已解析的批次一次性到达。run 结算时会中止仍在运行的分发,并放弃已排队未启动的分发(绑定调用被拒绝,不产生事件),随后排空到完全停稳,之后外层结果才结束该轮次。
  • client 侧:CodeSubCall 拓宽为 RunningToolCall | ToolResultNode:start 事件把运行中形状写入分发索引(行组件从该形状推导出运行指示环,与原生运行中的调用处理完全一致),其结算事件则原位替换该条目,即使并行完成也保持启动顺序不变,并把 start 事件的 time 作为 callTime(时长来源)带入。未观察到对应 start 的结算事件(窗口切在事件对中间,或日志录制于 start 事件引入之前)会直接追加,因此旧日志仍能照常渲染。
  • SDK 提示词:面向模型的「调用按顺序执行」一句替换为真实契约(相互独立的安全调用可以在 Promise.all 下重叠执行;相互依赖的工作以 await 顺序衔接);这是模型可见的变更,每一份 code-mode 快照都已重新录制。

曾考虑的替代方案

不加限制的并行(让 Promise.all 重叠一切)。 否决:写操作可能产生竞态;原生调度器之所以存在,正是因为安全性声明归工具所有,而不归调用方。原生与 Code Mode 使用同一套并发词汇,是已敲定的要求。

在提交时而非入池时发出 start 事件。 否决:提交即发 start 会把排了队却从未运行的调用显示成「运行中」,还得强行引入第三种「已放弃」终态事件才能使日志自洽。入池才发 start 保住了已启动 ⇔ 恰好结算一次这一不变式,且不需要第三种事件。

直接复用 loop 调度器的实现。 否决:loop 调度的是一个完整解析好的批次,并按模型顺序提交结果;桥接层调度的则是一条开放式的提交流,其结果返回给程序,而不是进入 transcript(文本记录)。因此两者共享的只是契约(分类、池、屏障),而不是实现机制。

后果

程序不需要任何新的模型侧 API,独立读取就获得了原生级的延迟:Promise.all 直接变得更好用,提示词指引也随之修改。web UI 实时显示每个子调用的运行指示环:fixture(测试前置数据)发出成对的 start/settle 事件;jsdom 锁定运行中形状;运行时 spec 锁定原位结算、乱序完成与 callTime 配对。PR6(trajectory/waterfall 的 span)现在可以依据这对事件的计时绘制如实的 span。spill PR(下一个)则继承结算事件,作为自己唯一施加边界的位置。