DeepSeek Harness · 编排与子 Agent

Subagent 是一个 seam:从进程内到委派 Claude Code

子 Agent 是能力接缝,进程内、远程、别家产品都能接。核心源码:packages/subagent/

课程目标读完你能说清三件事:为什么 DSH 把子 Agent 定义成一个 provider 注册表(seam,能力接缝),让进程内新开、带上下文 fork、委派给 Claude Code 或 Codex 共用同一个接口;能力不匹配时为什么选择启动前报错(fail loud)而没有静默忽略;以及一次性 run 和可继续 Activation 这两种子 Agent 生命周期各自解决什么问题。
交互演示 · 委派切换台

同一个子任务「调研这个模块并汇报」,切换四个 provider 分别委派一次。观察两件事:接缝两端哪些东西完全不变,哪些东西随实现而变。再打开「附带 persona 要求」开关,看能力门闩怎么把超纲请求拦在启动之前。

父 Agent 的会话日志(DSH 进程内)
user「帮我把支付模块接进来」
assistant好的,我先看看现有结构。
turn/end第 1 轮结束(种子边界)
user「顺便查下这个模块的历史问题」
tool/callsubagent「调研这个模块并汇报」
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(协议桥)、codexclaude-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.tspackages/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 系统加委派给别家的能力,先问接缝定义在哪一层,再问能力表怎么校验。