Subagent 是一个 seam:从进程内到委派 Claude Code
子 Agent 是能力接缝,进程内、远程、别家产品都能接。核心源码:packages/subagent/。
同一个子任务「调研这个模块并汇报」,切换四个 provider 分别委派一次。观察两件事:接缝两端哪些东西完全不变,哪些东西随实现而变。再打开「附带 persona 要求」开关,看能力门闩怎么把超纲请求拦在启动之前。
| 启动时能力 | spawn |
|---|---|
| outputSchema(结构化输出) | 支持 |
| depthLimit(委派深度上限) | 支持 |
| toolFilter(限工具) | 支持 |
| persona(换人设) | 支持 |
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 行。先说结论。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 里说不清楚。
fork 与 spawn 是两个 provider差别不在参数,在种子:fork 把父日志切到最后一个 turn/end 做子会话种子,spawn 从零开始。inheritsParentContext 只是描述性字段,供工具层生成不骗人的措辞。
一次性 run 与可继续 ActivationSubagentRun 是一锤子买卖:等结果、dispose、结束。可继续子 Agent 没有 run,它是一份持久会话加至多一个驻留 Activation,父级用 send_message 追加轮次、interrupt_agent 打断、收 report。
后台结束不会静默可继续子 Agent 结算时,管理器无条件给父级投一条 subagent-settled 通知,带最终输出。它与子 Agent 主动的 report 用不同的消息来源 kind,transcript 不会把运行时的记账算成子 Agent 说的话。
第一段证据是能力门闩本体,逻辑不贴代码也说得清。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 的种子函数,七行说完带上下文到底带的是什么:
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)
}
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 里是一个配置开关的事。
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 的会话谱系。
推演一次超纲委派
部署启用了 subagent_claude_code 工具,模型发起委派时带上了 outputSchema(要求结构化输出)。请按本课能力门闩的代码推演:错误在哪一行抛出、错误码是什么、Claude Code 的 CLI 进程有没有被启动过?再回答:如果 DSH 选择接受请求但忽略 schema,父 Agent 拿到的工具结果会出什么问题,为什么这比当场报错更难排查?