DeepSeek Harness · 编排与子 Agent Subagent 是一个 seam:从进程内到委派 Claude Code 子 Agent 是能力接缝,进程内、远��、别家产品都能接。核心源码: packages/subagent/ 。 一句话速览 子 Agent 是能力接缝,进程内、远程、别家产品都能接
课程目标 读完你能说清三件事:为什么 DSH 把子 Agent 定义成一个 provider 注册表(seam,能力接缝),让进程内新开、带上下文 fork、委派给 Claude Code 或 Codex 共用同一个接口;能力不匹配时为什么选择启动前报错(fail loud)而没有静默忽略;以及一次性 run 和可继续 Activation 这两种子 Agent 生命周期各自解决什么问题。
交互演示 · 委派切换台 同一个子任务「调研这个模块并汇报」,切换四个 provider 分别委派一次。观察两件事:接缝两端哪些东西完全不变,哪些东西随实现而变。再打开「附带 persona 要求」开关,看能力门闩怎么把超纲请求拦在启动之前。
spawn fork Claude Code Codex 附带 persona 要求 (给子 Agent 换一份人设)
播放 单步 重置
父 Agent 的会话日志(DSH 进程内)
user 「帮我把支付模块接进来」 assistant 好的,我先看看现有结构。 turn/end 第 1 轮结束(种子边界) user 「顺便查下这个模块的历史问题」 tool/call subagent「调研这个模块并汇报」 tool/result 子 Agent 的调研结论(一条工具结果)
ctx.subagents · provider 注册表(seam)
spawn fork acp codex claude-code sdk
能力门闩与子 Agent 舞台
启动时能力 spawn outputSchema (结构化输出) 支持 depthLimit (委派深度上限) 支持 toolFilter (限工具) 支持 persona (换人设) 支持
SubagentError: subagent provider "claude-code" does not support the "persona" capability · code: UNSUPPORTED_CAPABILITY
子 Agent 进程内 (上下文为空) 子 Agent 工作中
选一个 provider 点「播放」,或滚动到此处自动播放 spawn。 演示为教学化模拟。能力表数据来自各 provider 源码:spawn 与 fork 四项全支持( subagent-spawn-in-process/src/index.ts 第 42 行、 subagent-fork-in-process/src/index.ts 第 62 行),Claude Code 与 Codex 均为 NO_START_CAPABILITIES (各自 src/index.ts 第 54、49 行)。报错文案逐字复刻 packages/subagent/subagent/src/index.ts 第 490 至 493 行。
逻辑拆解 · seam 定义在哪一层 先说结论。DSH 没有做一个单独的子 Agent 功能,它做的是一个叫 ctx.subagents 的注册表:任何实现了 SubagentProvider 约定的传输层,都可以按名字注册进来。官方发行版注册了六个: spawn (进程内新开)、 fork (进程内带上下文)、 acp (协议桥)、 codex 、 claude-code (各起一个真实的产品 CLI 进程)、 sdk (远程 DSH 实例)。出处在 docs/subsystems/subagent.zh.md 第 5 至 7 行。 输入是什么:工具层把模型的委派请求组装成 SubagentStartRequest ,带上 prompt、父 Agent、取消信号,外加四个可选项(结构化输出 schema、深度上限、工具过滤、persona)。发生什么:服务先查所选 provider 的静态能力表,四个可选项每一项都要有对应的能力 flag,缺一项就在启动之前抛 UNSUPPORTED_CAPABILITY 。输出是什么:一个 SubagentRun 句柄,父 Agent 等它的 result ,最后落成一条普通的工具结果。对父 Agent 来说,四种 provider 回来的都是同一种东西。 值得停一下的是 fork 的身份。很多框架把带不带父上下文做成一个布尔参数。DSH 把 fork 做成了一个独立 provider,原因是它俩差的不只是一个开关:fork 要从父日志里切出「已完成轮次的平衡前缀」当种子,切到最后一个 turn/end 为止,进行中的轮次不平衡、回放不了,必须排除。这是会话日志层面的约定,塞进一个 flag 里说不清楚。
tool-subagent 模型说「去调研」
StartRequest
ctx.subagents 注册表 按名字选 provider · 校验能力表 缺能力 → UNSUPPORTED_CAPABILITY
spawn / fork 进程内子 Agent fork 带父日志平衡前缀 四项能力全支持
acp / sdk 协议桥与远程实例 localAgent: undefined
claude-code / codex 起真实产品 CLI 进程 用父会话的 cwd 启动时能力全为否
回到父 Agent SubagentRun.result → 一条工具结果 非 completed 映射为 isError 接缝之上一切相同;接缝之下,传输方式随 provider 各不相同
教学化结构图:节点与连线用于解释源码关系,内容经过课程化整理。
fork 与 spawn 是两个 provider 差别不在参数,在种子:fork 把父日志切到最后一个 turn/end 做子会话种子,spawn 从零开始。 inheritsParentContext 只是描述性字段,供工具层生成不骗人的措辞。 一次性 run 与可继续 Activation SubagentRun 是一锤子买卖:等结果、dispose、结束。可继续子 Agent 没有 run,它是一份持久会话加至多一个驻留 Activation,父级用 send_message 追加轮次、 interrupt_agent 打断、收 report 。 后台结束不会静默 可继续子 Agent 结算时,管理器无条件给父级投一条 subagent-settled 通知,带最终输出。它与子 Agent 主动的 report 用不同的消息来源 kind,transcript 不会把运行时的记账算成子 Agent 说的话。
关键证据 · 能力门闩与 fork 种子 第一段证据是能力门闩本体,逻辑不贴代码也说得清。 assertCapabilities 把请求里的四个可选项排成一张需求清单:请求带了 outputSchema,就要求 provider 能力表里 outputSchema 为真;带了 maxDepth 就要求 depthLimit;toolFilter 和 persona 同理。然后逐项对照,第一个对不上的当场抛 SubagentError ,错误文案直说哪个 provider 不支持哪个能力,错误码 UNSUPPORTED_CAPABILITY。没有降级、没有警告后继续,子进程在这之前一个都不会启动。 出处: packages/subagent/subagent/src/index.ts 第 481 至 495 行,核对日期 2026-08-13。演示区的报错文案逐字复刻自第 490 至 493 行。 第二段证据是 fork 的种子函数,七行说完带上下文到底带的是什么:
packages/subagent/subagent-fork-in-process/src/index.ts 第 48 至 54 行节选
function completedTurnPrefix(parent: Agent): SessionEvent[] {
const events = parent.session.events
const lastEnd = events.findLast(e => e.type === 'turn/end')
if (lastEnd === undefined) return []
// seq === array index (the append contract), so slice up to and including it.
return events.slice(0, lastEnd.seq + 1)
}
源码快照说明: 依据本地仓库 deepseek-harness-master,核对文件 packages/subagent/subagent/src/index.ts 与 packages/subagent/subagent-fork-in-process/src/index.ts ,核对日期 2026-08-13。代码块保留源码原文。 还有两处机制不贴代码,用文字标出处。Claude Code provider 的整个实现只是:解析 claude 可执行文件,在父会话的 cwd 里通过官方 Agent SDK 起一个真实 CLI 进程,把它挂到共享的 subprocess owner 之下( subagent-claude-code/src/index.ts 第 62 至 91 行)。发行版的四个官方 preset 里,codex 与 claude-code 的委派工具行都带着 disabled: true 出厂,注释写明:复制 preset、删掉 disabled,就能只对复制版的会话开放这个产品后端( apps/cli/config/agent-presets/standard/agent.cordis.yml 第 200 至 219 行)。委派别家产品在 DSH 里是一个配置开关的事。
横向对比 · 产品内的委派形态 vs 传输层注册表 Claude Code 把委派做在产品层。Task 工具(AgentTool)一个入口,参数里塞进各种形态: subagent_type 挑角色、 run_in_background 后台、 isolation: "worktree" 开隔离副本、 model 换模型(书稿 study/chapters/05-multi-agent.md 第 32 至 52 行引 AgentTool.tsx 原文)。往上还有 Coordinator Mode 和 Agent Teams 两套模式。表达力很强,但每种形态都是这一个产品里的功能分支:委派对象永远是另一个 Claude Code 实例,把任务交给另一个产品这件事没有位置。DSH 的选择相反,产品差异下沉到 provider 一层,接缝之上只有一个词汇表。 Grok Build 走的是第三条路:把子 Agent 的配置解析抽成纯逻辑库 xai-grok-subagent-resolution ,按 explicit override > role > persona > parent 的优先级解析出生效配置(该 crate src/lib.rs 第 7 至 8 行),执行仍在自家 shell 进程内。它抽象的是子 Agent 长什么样,DSH 抽象的是子 Agent 跑在哪。Grok 的角色、上下文继承与树形谱系,站内已有三课展开: 子 Agent 的四种角色 、 上下文继承与深度控制 、 子 Agent 的会话谱系 。
课堂练习
01 推演一次超纲委派 部署启用了 subagent_claude_code 工具,模型发起委派时带上了 outputSchema (要求结构化输出)。请按本课能力门闩的代码推演:错误在哪一行抛出、错误码是什么、Claude Code 的 CLI 进程有没有被启动过?再回答:如果 DSH 选择接受请求但忽略 schema,父 Agent 拿到的工具结果会出什么问题,为什么这比当场报错更难排查?
Takeaway: 子 Agent 在 DSH 里是一个 provider 注册表,进程内 fork 和委派 Claude Code 走同一个接口,差异全部沉在接缝之下。能力不匹配在启动前 fail loud,绝不接受后静默降级。想给自己的 Agent 系统加委派给别家的能力,先问接缝定义在哪一层,再问能力表怎么校验。