DSHarness 系统拆解 固定基线 47f943859b · 36 已复核 / 0 撰写中 / 36 章
English
工具系统·第 18 章

工具并行器与结果顺序

barrier、滚动窗口、重分类与模型顺序提交

已复核上游 47f943859b范围: 追踪 executionMode 分类、并发池、屏障、开跑前重分类、settlement 和 additional context 顺序。

结论:调度器控制的 dispatch/body 可以重叠;Native 权威提交保持有序

DeepSeek Harness 的并行工具调用不是把模型给出的数组直接交给 Promise.all。每个调用先由可见工具定义做 fail-closed 分类;Agent Loop 再按模型顺序建立有界滚动窗口,把独占调用变成屏障。pre-policy、审批与 guard 有序运行;around-dispatch/body 可以重叠;Native 顶层的 post/finalize、tool/result 与 additional context 按模型顺序提交。异步 tools/result observer 的续体与 Code Mode nested dispatch-log side work 不在这条提交保证内。默认窗口是 10,设为 1 可恢复 scheduler-controlled dispatch 的全串行。

一句话状态机

classify → ordered prepare → concurrent dispatch → model-order finalize → durable result → next-step context。Assistant message 必须先完整落盘,Loop 才抽取其中全部 tool-call blocks 并进入这一状态机。PARALLEL-LOOP-ENTRYPARALLEL-GROUPING

这套设计优先保护 replay、policy 顺序和父 Session 状态,而不是追求最大吞吐。代价也清楚:快结果会被慢的早序 sibling 阻塞;分类器无法表达“两个不同路径的写可以并发”;每 Agent/每 Step 的上限也不是进程级资源预算。

1. 先分清四种顺序

顺序保证可以乱序的部分
模型顺序Assistant content 中 tool-call block 的顺序,是所有索引与提交的基准
启动顺序调用按模型顺序进入 prepare/dispatch;后序不会抢先启动body 完成顺序
完成顺序并行 body 可任意完成这是实际并发所在
提交顺序post-policy、tool/result、contexts 与 concludes-turn 按模型顺序异步只读 observer 自己的完成时间不在该保证内

源码把完成的 dispatch 放入按模型索引编号的 slots;committed 游标只能跨过连续 ready slots。因此后序调用即使先返回,也不能先进入 post、Session history 或下一步上下文。PARALLEL-ROLLING-POOL

2. executionMode() 是严格 opt-in、按调用参数求值的分类器

输入状态分类原因
可见定义的 isConcurrencySafe(args) 恰好返回 trueparallel唯一 opt-in 路径
无 classifier、返回 false 或 truthy 非 booleanexclusive默认拒绝重叠
未知、被 scope 隐藏、code-only direct collapseexclusive解析不到可执行定义
Classifier 抛出exclusive分类失败不升级权限
defineTool 参数无效exclusivehelper 在调用 typed classifier 前 soft-validate
不进入模型 contract

ToolExecutionMode 只有 parallel/exclusive 两个 tagged arms;isConcurrencySafe 是 host metadata,schema projection 只发送 name、description 与 parameters。PARALLEL-MODE-CONTRACTPARALLEL-CLASSIFIERPARALLEL-TYPED-CLASSIFIER

3. 这是 unary promise,不是资源冲突图

Classifier contract 只给函数当前调用的 parsed arguments,并要求它作为同步 unary judgment 使用;它没有 siblings 或当前 pool 参数。JavaScript 并不从运行时阻止 closure 访问其他状态或触发 I/O,但那会违反该分类合同并引入调度器无法观察的依赖。返回 true 的含义非常强:该调用承诺可与任何另一个也返回 true 的 sibling 重叠。它可以表达“同一个工具的 read mode 可并行、write mode 独占”,却不能表达“只有两个写目标不同时才安全”。

作者责任

并行 body 不得修改 parent-owned state;共享 recorder/provider/cache 的竞态必须可交换、内部串行化或 fail closed。Scheduler 不验证这一承诺。声明过宽会把潜在竞态直接提升为生产行为;声明过窄只损失延迟。上游设计说明也明确选择了这一保守权衡。PARALLEL-SAFETY-NOTE

4. Group 不是预先切好的数组,而是逐步发现的屏障序列

model calls: P1  P2  X3  P4

classify P1 → open a candidate parallel suffix
start P1, reclassify/start P2
reclassify X3 → stop replenishing, drain P1/P2
run X3 alone through post/commit
classify/start P4

外层 loop 每轮只分类当前 first:如果它独占,就把 singleton group 完整 await;如果它并行,就把剩余 suffix 交给 runGroup,由 pool 在每个后序调用真正开始前再次分类。遇到 exclusive 时,当前 group 返回实际 consumed 数,屏障留给下一轮。PARALLEL-GROUPING

屏障覆盖完整提交

独占不只是“body 同时只能有一个”。整个 singleton group 要完成 prepare、body、post、finalizer、result append 与 context acceptance,下一组才会开始。测试覆盖 parallel → exclusive → parallel 三组与 registry replacement 后的重分类。PARALLEL-GROUP-TESTS

5. Rolling pool 如何保持上限又避免固定窗口空转

inFlight 保存已经启动、尚未被 driver reap 的 dispatch Promise;Promise settle 到 driver 删除之间,已完成项可能短暂仍在 Map 中。Pool 先按序填到 maxParallelToolCalls;任意 Promise settle 后由 Promise.race 唤醒 driver,移除该 slot、尝试提交连续 ready results,再补一个新调用。它不会等待整批窗口都完成,因此一个慢调用旁边的空位仍能持续处理后序安全调用。

cap = 2
start C1, C2
C2 settles first  → slot[1] ready, cannot commit, one capacity opens
start C3
C1 settles        → commit C1, C2; continue rolling
C3 settles        → commit C3
上限语义

Cap 限制的是同一 Agent Step 中同时未 settle 的 parallel dispatches,不限制已经完成但等待前序提交的 slots,也不限制别的 Agent。测试证明 cap=2 的补位顺序以及 cap=1 的全串行行为。PARALLEL-ROLLING-POOLPARALLEL-CAP-TESTS

6. 为什么只把 pipeline 中段并发化

阶段并发性理由
append tool/call有序先建立 durable identity 与原始参数事实
create execution、pre-execute、approval、guards有序Policy 可能维护顺序敏感状态
tools/execute wrappers + body允许重叠真正的外部 I/O/计算延迟所在;wrapper 必须 reentrant
post-execute、render/finalizer、result notification有序调用observer 返回的 Promise 不会被 await,其异步续体可重叠
append tool/result、accept context有序模型历史与下一 Step 的权威顺序

ToolRuntime 暴露给 scheduler 的 symbol-keyed 私有视图把同一条执行管线拆为 preparedispatchfinalize/finish。普通调用仍可用完整的 execute()PARALLEL-PIPELINE-SPLITPARALLEL-PREPAREPARALLEL-DISPATCHPARALLEL-FINALIZE

7. “结果有序”具体是怎样落进 Session 的

每个 started call 先 append tool/call,保存 provider 给出的原始 argument string,并记住 seq。提交时 Loop 用最终 content/isError/error info/meta 创建 tool/result,通过 sourceEventSeqs:[callSeq] 精确指回该 call。随后才逐条接受该结果的 additionalContextsPARALLEL-DURABLE-PAIRING

生产测试让 C2 先 settle,并确认在 C1 释放前没有任何 result;最终 deriveMessages() 仍得到 C1、C2 顺序。PARALLEL-ORDER-TESTS

8. Head-of-line blocking 是有意购买的一致性

收益

Provider 看见的下一请求、Session replay、post hooks 和 next-step contexts 都与模型原始调用顺序一致,不需要让每个 adapter 理解完成时序。后序结果不会因为网络更快而改变策略状态。

成本

一个慢的早序调用会阻挡所有已完成后序结果的 post/finalization 和模型可见性;其 slots 仍驻留内存。Rolling pool 可继续启动安全后序调用,但无法让模型提前消费它们。这是延迟与确定性之间的明确取舍,而不是 scheduler 漏掉一次唤醒。

9. Live reclassification 关闭了大部分排队期漂移,但不是 definition snapshot

Scheduler 在 barrier 后重新分类 first,并在 parallel pool 每次补位前重新读取后序调用的当前可见定义。同步的 result observer 若替换一个工具,尚未开始的调用会看到新 classifier;测试证明当前 pool 会先排空,再把新 exclusive 定义作为屏障运行。PARALLEL-RECLASS-TESTS

静态发现:classify-to-body-resolution 窗口

分类并没有把 ToolDefinition 固定进 execution。startCall 在分类后 await 有序 prepare,dispatch 还会 await tools/execute waterfall,真正 body 最后才按名称解析 live definition。期间若定义从 parallel 替换成 mutating exclusive,新 body 仍可能占用旧 parallel slot;execution 甚至已捕获旧 definition 的 finalizer,形成“旧 finalizer + 新 body”。完整修复需要把同一 ToolDefinition snapshot/bind 到分类与执行,或用 registry generation 在实际 body resolution 点做原子校验与调度;prepare 后重分类只缩窄其中一个窗口。PARALLEL-GROUPINGPARALLEL-PREPAREPARALLEL-DISPATCH

10. Result observer 被有序调用,但不是可 await 的调度门

finishScheduledExecution 同步发布 frozen result;notifyResult 依次调用 callbacks,但把 callback 返回的 Promise 交给独立 catch,不等待它们完成。因此 observer 在第一个 await 之前的 registry change 能影响下一次分类,await 之后的 change 则可能晚于补位。

扩展边界

需要阻挡后序调用的策略必须放在 awaited pre/post waterfall 或同步完成;异步 tools/result 适合 telemetry,不是 barrier。现有“result observer makes next call exclusive”测试使用同步 callback,只证明同步 mutation。PARALLEL-RESULT-OBSERVERPARALLEL-RECLASS-TESTS

11. 取消:停止补位、排空已启动调用、为未启动调用补合成结果

取消落点Body耐久结果后序行为
Group 开始前全部不启动每个模型调用写 synthetic call/result,码为 ABORTED_BEFORE_DISPATCH不发下一次 LLM 请求
pre/approval 中当前 body 不启动;不再准备 siblings当前与未启动 siblings 获得 before-dispatch error结束本 Step
parallel body 运行中signal 传播;Scheduler 等所有 started Promise 静止成功通常转成 ABORTED;tool-owned error 可保留;全部按模型顺序提交不补位、不跨过后续 exclusive barrier
result/post commit 中已启动工作仍被排空最终取消检查可替换尚为 success 的结果contexts 保留到 inbox

Loop 的 synthetic pair 保证这条 Assistant message 中每个 tool-call 都能在 replay 中闭合;这正是当前实现相对早期设计说明的重要变化。PARALLEL-ABORTPARALLEL-ABORT-RESULTPARALLEL-ABORT-TESTS

12. 取消不会丢掉已接受的 additional context

Started calls 的 post-policy 仍按序运行;其 additionalContexts 在对应 result 提交后送入 Agent 的 next-step inbox。若 Turn 因取消结束,这些消息暂不写为 user/message,而是在下一次 wake 时进入新 Step。测试明确验证 contexts 的 C1、C2 顺序以及取消后 inbox 停驻。PARALLEL-CONTEXT-TEST

concludesTurn 同样只在成功结果按序提交时聚合。它不会取消同一 Assistant message 已提交的 siblings;Scheduler 完成整个批次后,Agent 才把该 Step 判为 completed。PARALLEL-AGENT-STEP

13. 普通工具失败与 scheduler invariant failure 走不同通道

失败类型承载形式是否继续本组
参数、policy denial、approval、guard、body、output、post/finalizer 失败ToolRuntime 规范化为 ToolExecutionResult.isError是;仍按模型顺序提交,模型可在下一 Step 自修正
内部 staged scheduler Promise 意外 reject抛到 Agent Turn error boundary停止新 dispatch,allSettled 排空已启动工作

第二类不是正常 extension result。Native scheduler 保留已经写下的 tool/call,不伪造它无法归因的 tool results,因此失败 Step 可能留下无结果 call;Turn 最终以 error reason 关闭。PARALLEL-SCHEDULER-FAILUREPARALLEL-FAILURE-TEST

14. 两个独立的“10”:Native Step 与 Code Mode run 各有自己的池

配置默认作用域动态性
agent-loop.maxParallelToolCalls10每个 Agent 的每个顶层 StepSettings 可更新;每个 group 开始读取一次,不扰动 in-flight group
tools.maxParallelSubCalls10每次 run_code 的 nested binding poolToolRuntime 构造时固定

两者都要求正整数,1 表示串行;它们不是同一个共享 semaphore。多个 Agent、多个并发 run 或 provider 自身的并发仍可相乘,外部 API quota、process pool 和连接池必须在各自 provider 层继续限流。PARALLEL-CAP-CONFIGPARALLEL-CODE-DRIVER

15. Code Mode 复制同一原则,但不是复用同一个 loop

run_code 对模型是一个顶层 exclusive tool;程序内部每次 tools.name(args) 进入另一个 per-run driver。该 driver 有 pending queue、in-flight set、commit queue 和 exclusiveActive,用单一 ordered lane 执行 start/prepare 与 commit/finalize,仅让 nested dispatch body 重叠。

program submissions
→ normalize lossless JSON + assign <outer>:code:N
→ pendingQueue (submission order)
→ classify at start
→ bounded inFlight bodies / exclusive barrier
→ commitQueue head cursor
→ program promise value or ToolCallError
→ defer contexts onto outer run_code result
两层历史

Nested call 不生成顶层 tool/call/result;它写 tool/code-dispatch-starttool/code-dispatch 供 UI/trajectory 重建。只有 outer run_code 的 curated logs/result 进入模型历史。PARALLEL-CODE-DRIVERPARALLEL-CODE-BINDING

16. Code Mode 的 barrier、context 与 settlement

Driver 总是先尝试提交 ready 的 commitQueue[0],再考虑启动 pending head。Exclusive 必须等 inFlight.size===0,并保持 exclusiveActive 直到自己的 post/commit 完成。已经启动的 binding 只在有序 commit 的 settle() 中按 tool outcome resolve/reject,因此程序观察到的是 policy 后 canonical value 或一条 ToolCallError message,而不是 body 的裸完成时刻;尚未启动就被取消的 queued binding 则由 abandon() reject。

Nested contexts 在 commit 时依次 exec.deferContext 到 outer execution;即使程序之后失败,它们仍可随 outer error result 返回。外层 run 一旦 settle,run-scoped controller 会取消 in-flight calls、拒绝 queued-unstarted calls,并在 outer tool 返回前 drain driver 与 durable log work。PARALLEL-CODE-BINDINGPARALLEL-CODE-DRAINPARALLEL-CODE-TESTS

17. Code Mode 的 durable nested event 顺序比 binding 顺序更弱

静态发现:日志 side work 可重排

settle() 在 ordered commit lane 中先把结果交给程序,再异步启动 shapeDispatchLog → session.append(tool/code-dispatch);这项 log work 不阻塞 binding,最终只在 run 结束时整体 drain。若前一个 log-content listener 比后一个慢,settle event 的 append 顺序可以与 submission 顺序不同。Start events 仍有序,UI 也按 subCallId 关联,所以这不是 call pairing 错误,但 event seq/time 不应被解释为 canonical completion order。PARALLEL-CODE-BINDINGPARALLEL-CODE-DRAIN

现有并发测试只比较 settle id 的集合;没有对异步 log shaping 下的 settle event 顺序作断言。这与 Native 顶层 tool/result 的严格 append 顺序必须分开陈述。PARALLEL-CODE-TESTS

18. 测试覆盖很强,但仍有几类精确时序缺口

已证明证据
Native parallel overlap、三组 barrier、动态 replacement、rolling cap、cap=1确定性 gated tools
Native body 乱序但 result/history/context 有序先释放 C2、确认结果为空,再释放 C1
取消前、pre 中、body 中、barrier 前的 synthetic pairing 与 quiescenceSession event 与 inbox assertions
Native internal scheduler failure 排空且 Turn error私有 scheduler rejection 注入
Code Mode overlap、cap、exclusive barrier through post、run settlement drainper-run gated tools
覆盖缺口 1

Code Mode 名为“out-of-order completion”的 post/context test 实际使用 FIFO release(),源码注释也承认不能反向释放;它随后 releaseAll(),没有制造 B 先于 A 完成。Commit cursor 本身清晰,但这个测试名声称的 interleave 尚未被该 fixture 证明。PARALLEL-CODE-ORDER-TEST

其余覆盖缺口

Native 与 Code Mode 都没有直接测试“classifier 返回 parallel 后、实际 body resolution 前替换同名 definition”的窗口;也没有异步 result observer 或异步 dispatch-log 重排的 characterization test。它们是彼此独立的 case,不应压成一个固定测试数量。

19. 文档审计:两个 README 当前一致,所链接的 implemented note 已过时

packages/core/agent-loop/READMEpackages/core/tools/README 已准确描述 rolling pool、exclusive barrier、ordered policy/results/context、默认 cap 10,以及 Code Mode 的独立并行 bridge。PARALLEL-AGENT-READMEPARALLEL-README

明确漂移

它们链接的 2026-07-10 implemented Agent Note 仍说:取消时未启动调用“没有 audit event”;Code Mode 内部队列“保持串行且不在 scheduler 内”;验证仍只覆盖 Code Mode serial boundary。当前源码与测试已经分别改为 synthetic call/result 和 parallel per-run pool。该 note 不应继续被当作“完整安全约定”的现行真相,应更新或标记被后续实现取代。PARALLEL-NOTE-DRIFT

20. 静态 findings 与最终评价

Finding分类影响 / 建议
分类后、实际 body resolution 前可替换 live definition条件性并发安全风险补测试;绑定 definition 或在 body resolution 点校验 registry generation
Async result observer 不被 scheduler await扩展时序边界需要 gate 的逻辑放 awaited waterfall
Code nested settle append 可受异步 log shaping 重排可观测性边界按 subCallId 配对,不以 event seq 推断 body 完成顺序
Code “out-of-order” test 没有实际反转完成顺序回归覆盖缺口使用 per-id gates 明确先释放第二个
Implemented design note 与现行取消/Code scheduler 冲突文档漂移更新 note 与 verification 声明
两个 cap 都是局部窗口,不是全局 admission control运维容量边界Provider/host 继续实施全局 quota
Unary classifier 看不到资源关系刻意保守关系型安全一律 exclusive
总体判断

核心算法小而严谨:scheduler-controlled 并发集中在 dispatch/body,中间结果用 slot 保存,Native 顶层 post/result/context 回到单 lane;取消和正常工具错误也有明确收敛路径。异步 observer 与 Code nested-log 是显式例外。最需要优先修复的不是算法重写,而是关闭 live-definition 的 classify/body-resolution TOCTOU,分别补上 definition replacement、observer continuation、nested-log reorder 与真正反序完成的测试,并让设计文档重新追上代码。

验证状态

本章在固定 commit 上逐行追踪 ToolRuntime classifier/staged pipeline、Agent Loop scheduler/Session append、Code Mode bridge、配置、README、implemented note 与对应单元测试;并执行 targeted Vitest:3 个文件、120 项测试全部通过。运行结果证明现有测试保持绿色;上表标出的未覆盖时序仍是静态 findings,不冒充已有回归证明。

我的学习体会

内容仅自动保存到当前浏览器,不上传、不进入仓库。你可以导出 Markdown 自行归档。