Files
deepseek-harness/.agents/notes/implemented/feature/2026-07-26-code-mode-live-parallel-dispatch.zh.md
Tianyi Cui f9cc62266c fix(tools): single ordered driver lane for the sub-dispatch scheduler; validate the cap
Responding to ds-review-bot round 2 on #658 (three critical findings, one
warning — all rooted in the pump/commit split racing ordered stages):

- ONE driver lane now owns every ordered stage: the start append, prepare
  (pre-execute/guards), and the head-of-line commit (post-execute, context
  deferral, settle append). start() is awaited before the next entry can
  start, so concurrent submissions can no longer run pre-execute pipelines
  concurrently; only the around-dispatch/body stage overlaps, matching the
  native loop's fillPool sequencing.
- An exclusive call's barrier now holds through its COMMIT: later starts
  wait for the exclusive pipeline (post-execute included) to finish, the
  native exclusive-group semantics.
- drainDispatches() awaits the driver run itself, so a commit already
  mid-flight when the program returns is drained before run_code closes
  the turn — the settle event and deferred contexts land inside it.
- maxParallelSubCalls is resolved and validated at construction (positive
  integer), so direct construction can no longer wedge the pool with 0.

New tests: overlapping-submission ordered-prepare, barrier-through-commit,
drain-mid-commit, cap rejection. 96 keyless snapshots replay unchanged;
Agent Note updated (both languages).
2026-07-26 15:26:52 +08:00

5.5 KiB
Raw Blame History

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

Status: implemented

English | 中文

范围Code Mode UI 堆叠 PRPull 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 契约并严格按提交顺序启动。所有有序阶段——start 事件追加、preparepre-execute/守卫)、队首 finalize/finish 提交post-execute + 上下文延迟提交 + settle 事件追加)——由单一驱动车道独占执行,因此有序策略阶段彼此绝不重叠,只有 around-dispatch/工具体阶段并发运行,与原生 loop 的时序完全一致(fillPool 先 await startCallcommitReady)。连续被分类为可并行的调用可以重叠执行,上限为 maxParallelSubCallsConfig 字段Loader schema 校验之外直接构造时也重新校验,默认值 10即 loop 调度器自身的默认值;设为 1 即恢复串行分发);独占调用则先排空池、独自运行,且其屏障保持到自身提交(含 post-execute完成为止与原生独占分组一致。run 结算时会中止仍在运行的分发,并放弃已排队未启动的分发(绑定调用被拒绝,不产生事件),随后排空到完全停稳——包括程序返回时已在途的提交——之后外层结果才结束该轮次。
  • client 侧CodeSubCall 拓宽为 RunningToolCall | ToolResultNodestart 事件把运行中形状写入分发索引(行组件从该形状推导出运行指示环,与原生运行中的调用处理完全一致),其结算事件则原位替换该条目,即使并行完成也保持启动顺序不变,并把 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 配对。PR6trajectory/waterfall 的 span现在可以依据这对事件的计时绘制如实的 span。spill PR下一个则继承结算事件作为自己唯一施加边界的位置。