DeepSeek Harness 快照测试:fork 子代理重放的 seedLength 边界与混合 spawn+fork 场景
【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness
导读
本文聚焦 DeepSeek Harness(@deepseek-ai/dsh-*插件化 Agent 框架)快照测试体系中的一个关键细节:fork(继承父会话上下文的子代理)与 spawn(全新会话的子代理)在完整转录快照重放中的路由差异。原 Agent Note 2026-06-22-fork-snapshot-scenarios.md 记录了两个新增场景的动机与决策:subagent-fork-in-process与subagent-mixed。读完本文,你将理解seedLength边界为何是 fork 子代理重放正确的基石、为什么"父代理必须完成一个回合"才能测试到该边界,以及如何在默认门禁(default gate)中以无 key 方式重放这两个场景,并掌握重新录制快照的具体命令。
背景:fork 与 spawn 的本质差异
DeepSeek Harness 为 Agent 之间的委派提供了两类进程内(in-process)子代理后端,二者在快照重放中行为完全不同:
| 后端 | 委派工具 | 子会话形态 | 子日志中的seedLength |
|---|---|---|---|
subagent-spawn-in-process | subagent | 全新会话,从零开始 | 缺省(等价 0) |
subagent-fork-in-process | subagent_fork | 以父会话已平衡完成的回合前缀作为种子 | 非零(= 继承的事件条数) |
两者在 packages/bundle/base/cordis.patch.yml 中注册:subagent-spawn-in-process以providerName: spawn挂载,subagent-fork-in-process以providerName: fork挂载,对应的委派工具名为subagent与subagent_fork。
fork 后端的核心实现在 packages/subagent/subagent-fork-in-process/src/index.ts:completedTurnPrefix取父会话日志中**最后一个turn/end之前(含)**的全部事件作为种子——正在进行的工具调用回合是不平衡的,不能作为合法的子会话种子;若父会话一个回合都未完成,则种子为空(等价于全新 spawn)。
问题:fork 子代理的重放曾存在静默错路由
在新增 fork 场景之前,快照测试体系存在一个真实缺口:
- 完整转录快照层(full-transcript snapshot tier)——唯一会真实启动
acp-agent并端到端重放嵌套转录的测试层——只有 spawn 型子代理场景(subagent-spawn、subagent-multi); - fork 路由逻辑仅由
llm-replay的单元测试(一个手工合成的子 fixture)和持久化往返测试覆盖; - 因此,一个"让单元测试全绿、却在完整转录层逃逸"的 fork 路由回归,正是快照层本该捕获、却因缺少真实 fork 场景而无法捕获的 bug 类别。
根本原因在于子代理脚本的推导方式:llm-replay通过deriveReplayScript把子会话日志按(turn, step)分组为每个stream()调用一个重放条目。对 spawn 子代理,其日志只含自己的模型调用,推导天然正确;但对 fork 子代理,其.jsonl以父会话的事件开头(包括父会话的assistant/chunk),随后才是子会话自己的回合——若直接对整个日志推导,父会话记录的响应会被当成子代理的模型调用重放出去,造成静默错路由。
决策一:持久化seedLength边界
修复方案在配套笔记 2026-06-22-fork-child-replay-seed-boundary.md 中记录,核心是"显式优于隐式":
- 会话头新增
seedLength:SessionHeader增加可选seedLength: number,表示该会话有多少条前导事件是继承而来而非本会话产生。fork 后端创建子会话时盖章(= 种子前缀长度);spawn 则缺省(≡ 0)。它经CreateSessionOptions.meta传递,并在SessionStore.prepare中写入。 seedLength必须是显式的,绝不从seed.length推断:重建(resume/load)路径会用整个存储日志重新播种,此时seed.length是完整长度而非原始边界;因此 resume 路径从已加载的头中把持久化的seedLength原样传回(与createdAt的保留方式一致)。- 两个持久化后端都往返保存:
- JSONL:会话头行的
seedLength字段(见 packages/session/session-persistence-jsonl/src/format.ts 的toHeaderLine/fromHeaderLine); - SQLite:
sessions表的seed_length列,随seed_length/source_event_seqs/surface_op一起构成 schema 版本 4;预发布策略下,任何非当前user_version一律拒绝打开、不做迁移。
- JSONL:会话头行的
- 重放从边界之后推导子脚本:
llm-replay的parseSessionHeader读取seedLength(缺省 0),loadSessionScripts对子 fixture 执行parseSessionLog(text).slice(header.seedLength)——即只取边界处及之后的事件、只重放子会话自己的模型调用(见 packages/test-support/llm-replay/src/index.ts)。spawn 子代理seedLength为 0,slice 是空操作,因此既有 spawn 场景字节级不变。
决策二:录制两个真实 fork 场景
原笔记最终录制了两个针对真实 API 的场景,且都在默认门禁中无 key(keyless)重放:
subagent-fork-in-process(聚焦回归)
父代理先完成一个回合(确立一个事实),再通过subagent_fork委派一个子任务。fork 子代理继承对话(其日志携带非零seedLength),因而能直接从父上下文作答。这是聚焦的回归用例:子 fixture 的seedLength是重放切片依赖的边界,且来自真实 fork 而非手工合成。对应 fixture 位于 snapshots/sdk/subagent-fork-in-process/(session.jsonl+session.1.jsonl两个子日志 +snapshot.yml)。
subagent-mixed(混合 spawn+fork)
父代理完成一个回合后,在同一转录中先后委派一次subagent(全新 spawn 子代理,seedLength0)和一次subagent_fork(fork 子代理,非零seedLength)。这是 seed-boundary 与 per-session-replay 笔记都点名"未来要补"的混合场景:一个转录同时覆盖两种传输方式、两条切片分支(seedLength0 = 空操作;seedLength > 0= 裁剪继承前缀),且两个子代理按createdAt排序为 spawn 在前、fork 在后。对应 fixture 位于 snapshots/sdk/subagent-mixed/(session.jsonl父日志 +session.1.jsonl/session.2.jsonl两个子日志)。从子日志头可见实际形态:fork 子会话头携带parentSession与"seedLength":39,而 spawn 子会话没有这两个字段。
两个场景的snapshot.yml均为version: 1、profile: sdk、composition: default、recording: live。
为什么必须完成 turn-1
fork 后端只把父会话已平衡完成的回合前缀作为种子。若父代理在第一个回合就 fork,则没有任何已完成的回合可继承,种子为空(≡ 全新 spawn,seedLength0)——这测不到切片逻辑。因此两个场景都使用双提示输入:
- 第一个提示完成一个回合(确立一个"暗号"(codeword),供子代理稍后被要求回忆,如 fixture 中
Remember this fact for later: the project codeword is SAFFRON. Reply with the single word OK and stop.); - 第二个提示发起 fork 委派。
子代理转录中回忆出的暗号只是模型的自然行为,承载验证功能的产物是子 fixture 中记录的seedLength——它才是重放切片真正消费的边界。
快照基础设施如何支撑 fork 场景
两个进程内后端早已接入cordis.yml/cordis.snapshot.yml,作为两个模型可见工具暴露(subagent→ spawn,subagent_fork→ fork);harness 采集每个子日志,重放按seedLength键控的每子代理 fixture。缺的只是"驱动一个 fork 子代理走过全流程的录制场景"。装配细节:
- packages/bundle/base/cordis.patch.yml 声明
subagent-spawn-in-process、subagent-fork-in-process(providerName: fork)与tool-subagent-fork(provider: fork、toolName: subagent_fork); llm-replay插件通过Config.file($DSH_SNAPSHOT_FILE)、Config.overrideFile($DSH_SNAPSHOT_OVERRIDE)、Config.childFiles($DSH_SNAPSHOT_CHILD_FILES,路径分隔符列表)解析 fixture 路径,见 packages/test-support/llm-replay/src/index.ts;- 重放按"首次调用的活会话"顺序绑定父/子脚本:
installLlmReplay以createdAt排序[primary, ...children],新出现的活会话认领下一个未绑定的脚本;assertConsumed()在 teardown 时把"录制脚本从未绑定""脚本未消费完"变成明确诊断(packages/test-support/llm-replay/src/index.ts)。
影响与验证:护栏真的咬住了
原笔记记录的 Consequences 已由实现证实:
- 守卫上移:fork 路由切片现在受完整转录层守护,而非仅单元测试。移除
slice(seedLength)(重放整个子日志)会让两个新场景同时变红——fork 子代理会收到父会话记录的 chunk 而非自己的——证明护栏有效(场景落地时验证过红→绿)。这是本笔记最有说服力的验证点:guard 是"可证咬人"的,不是纸面声明。 - 首个双后端场景:
subagent-mixed是第一个在单个转录中驱动两个不同子代理后端、同时跨 spawn 与 fork 子代理验证 per-session 重放键控的快照场景。 - 边界明确:进程外(ACP)子代理重放形态不同(每个子代理是独立进程、独立重放),仍由
TODO(acp-subagent-replay)跟踪——这两个场景仅限进程内。 - 可重录:
pnpm run test:snapshot:record会从实时 API 重新生成全部四个 fork/spawn fixture;与其他录制场景一致,两个新场景在没有 key 时会自跳过(self-skip)。
实操:录制与重放
仓库根目录 package.json 中的相关脚本:
# 从实时 API 重新录制全部快照 fixture(含本笔记的 fork/spawn 四个 fixture) pnpm run test:snapshot:record # 以重放模式跑快照门禁 pnpm run test:snapshot:refreshtest:snapshot:record:DSH_SNAPSHOT=record vitest run --config vitest.snapshot.config.ts --update;test:snapshot:refresh:DSH_SNAPSHOT=refresh vitest run --config vitest.snapshot.config.ts。
录制出的产物即 snapshots/sdk/subagent-fork-in-process/ 与 snapshots/sdk/subagent-mixed/ 下的父日志、子日志与snapshot.yml,可在任何无 LLM key 的环境中重放验证。
延伸阅读
- 种子边界机制的完整设计:2026-06-22-fork-child-replay-seed-boundary.md
- per-session 子代理快照重放的提出:2026-06-22-subagent-snapshot-replay.md
- fork 后端子代理实现:packages/subagent/subagent-fork-in-process/src/index.ts
- 重放插件实现:packages/test-support/llm-replay/src/index.ts
- 子代理子系统总览:docs/subsystems/subagent.md
【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考