子代理对照:派生、隔离与回收,DeepSeek Harness 与三家的边界差在哪
先说清楚这篇比什么。我们手上有一个具体的麻烦:让一个子代理去跑测试,它自己又派了几个孙代理,最后整棵树卡在那儿不回来——这时候谁负责把它们收掉?顺着这个问题,四家的边界能被摆到同一张桌子上:派生的时候孩子继承了什么、能往下递归几层、并发额度怎么算、活干完谁来释放。
只对照四方都白纸黑字写明的机制。Claude Code 是闭源产品,下面凡涉及它的部分一律只引官方文档,不推断实现;Codex 与 Pi 引仓库内相对路径。三家不排名,落点只有一个:各自把边界划在哪,这个差异什么时候会咬到你。另外,DeepSeek Harness 的仓库 README 自述处于开发者预览阶段并明写会有破坏兼容性的变更,下面出现的配置项名、默认值随时可能变动,以仓库最新内容为准。
一、派生:孩子到底看得见父亲的什么
DeepSeek Harness 把这件事做成了两个并存的 provider,注册在 ctx.subagents 上,靠名字区分。packages/subagent/subagent-spawn-in-process/src/index.ts 里的 spawn provider 写着 inheritsParentContext = false,源码注释直说「a spawned child starts fresh — it never sees the parent conversation」;packages/subagent/subagent-fork-in-process/src/index.ts 里的 fork provider 写着 true,它的 completedTurnPrefix() 函数用 events.findLast(e => e.type === 'turn/end') 取父会话日志里最后一个 turn/end,切到那一条为止。也就是说 fork 继承的不是「父亲现在的样子」,而是「父亲最后一个完整回合结束时的样子」。docs/subsystems/subagent.md 对这个切法的说明是:这样得到的 seed 从 seq 0 起连续,invariants 的 replay 才会接受它,正在进行中、尚未平衡的那一轮被排除在外。
这里有个反直觉的地方值得单独拎出来:packages/subagent/subagent/src/types.ts 里 inheritsParentContext 的注释明写它只描述会话播种,「It says nothing about tool registration, injected services, or authority inheritance」。换句话说,fork 出来的孩子看得见父亲的对话,但工具注册、注入的服务、权限,一样都不自动跟过去。子系统文档也写明每个孩子拿到的是一个新的扁平 scope,而不是继承父亲的注册表。
Claude Code 官方文档写明,非 fork 的子代理「starts with a fresh, isolated context window」,看不到主会话历史、已调用的 Skills、已读过的文件;但它同时列出了一份确实会加载的清单——CLAUDE.md 层级(含 ~/.claude/CLAUDE.md、项目规则、CLAUDE.local.md)与父会话启动时的 git status 快照,而内置的 Explore 与 Plan 这两个子代理是唯二跳过这两项的,文档还明说没有任何 frontmatter 字段或按代理的设置能改变谁跳过。它的 fork 则是另一回事:文档写明 fork「inherits the entire conversation so far」,系统提示、工具、模型、消息历史都一样。
Codex 的口径写在工具描述里。codex-rs/core/src/tools/handlers/multi_agents_spec.rs 中,spawn_agent 的 agent_type 参数描述原文是「Omit to inherit the parent agent type with a full-history fork; otherwise, default is used」——省略这个参数时走的是全量历史 fork。同一文件里的模型说明也写着 spawned agent 默认继承当前模型。
Pi 这一侧要说清楚:packages/coding-agent/docs/usage.md 的设计原则一节写明它「intentionally does not include built-in MCP, sub-agents, permission popups, plan mode, to-dos, or background bash」——子代理不是内置能力,要靠扩展或包自己搭。仓库里确实有一个示例扩展 packages/coding-agent/examples/extensions/subagent/,其 README 的 Features 第一条写的就是「Isolated context: Each subagent runs in a separate pi process」,隔离粒度直接是进程。所以「Pi 的子代理默认继承什么」这个问题在它的文档口径下不成立,我们不比这一格。
二、深度:默认值差了三倍,而且限的位置不一样
递归深度是四家里数值最能直接摆出来的一项。
| 默认递归深度 | 配置入口 | |
|---|---|---|
| DeepSeek Harness | 3 | tool-subagent 的 maxDepth(也可设为 'provider-managed') |
| Claude Code | 主会话下方 3 层 | 环境变量 CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH |
| Codex | 1 | agents.max_depth |
| Pi | 我们没有找到对应说明 | —— |
DeepSeek Harness 的 3 来自 packages/subagent/tool-subagent/src/index.ts 里的 Schemastery 定义 maxDepth: z.union([z.natural().max(Number.MAX_SAFE_INTEGER), z.const('provider-managed' as const)]).default(3),注释写明 0 表示禁止再派。真正做加减法的在 packages/subagent/subagent/src/depth.ts:delegationDepthOf() 返回 Math.max(agent.session.header.delegationDepth ?? 0, runtime ?? 0),取持久化 header 与运行期 AgentOptions.subagentDepth 两者的较大值。函数注释自述了这么写的理由——持久化的 header 是权威且单调的,运行期字段只能加深不能降低,否则一个冷恢复回来的孩子会带着全新的 options 从零开始数,于是它就能像顶层代理一样往下派。
Claude Code 官方文档写明默认是主会话下方三层,到了限额之后的处理方式挺具体:除 fork 之外,每个子代理都会被收走 Agent 工具,于是它只能自己把活干完并返回一份摘要;而一个处在限额上的 fork 仍保留继承来的 Agent 工具,只是调用时返回错误。文档同时列了历史默认值的变化(v2.1.172 到 v2.1.216 是五层且不可改,v2.1.217 到 v2.1.218 默认降到一层,v2.1.219 又提到三层)——这类默认值本来就是会漂的。
Codex 的 DEFAULT_AGENT_MAX_DEPTH: i32 = 1 定义在 codex-rs/core/src/config/mod.rs,同文件里由 cfg.agents.max_depth 覆盖。1 和 3 差的不是三倍性能,是默认允不允许出现「孙代理」这件事本身。
三、并发额度:真正容易踩的是「干完了还占着位」
Claude Code 官方文档写明默认 20 个子代理同时运行时,再用 Agent 工具派新的会失败并提示不要重试,可用 CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS 调整。文档还写明这个限额只挡 Claude 用 Agent 工具派的子代理,另有两类运行同样占槽位却不受它挡:用 /subtask 起的会话内 fork 会占一个位但从不被这个限额挡住;恢复一个已完成的子代理会直接拿一个新位而不检查限额,所以恢复操作可以把运行计数顶到限额之上。另外文档写明启用 ultracode 的会话不强制执行这个限额,workflow 代理与 agent team 队友则各走各自的限额。
Codex 这边的数值在 codex-rs/core/src/config/mod.rs:DEFAULT_MULTI_AGENT_V2_MAX_CONCURRENT_THREADS_PER_SESSION: usize = 4,而旧路径的 DEFAULT_AGENT_MAX_THREADS: Option<usize> = Some(6)。更值得抄下来的是 close_agent 的工具描述原文:「Completed agents remain open and count toward the concurrency limit until closed.」——干完活的 agent 不会自动腾位,得显式关。这句话必须和上面那个 4 合起来读:占着并发计数的不只是「正在跑的」,还有「跑完了但没关的」。
Pi 的示例扩展把限额写死在 index.ts 顶部:MAX_PARALLEL_TASKS = 8、MAX_CONCURRENCY = 4、PER_TASK_OUTPUT_CAP = 50 * 1024。最后这个是回给父模型的每任务输出上限,README 的「Limitations」一节也复述了同一条。
DeepSeek Harness 这一格我们要老实说:在 packages/subagent 下没有找到子代理专用的并发上限,types.ts 里只写了 provider 契约层面的一句「a shared capacity controller may delay an operation but must not couple its settlement or cleanup to a sibling」,那是对实现者的约束,不是一个已经存在的默认值。另有一处相邻事实可以并排放着看:packages/core/agent-loop/src/constants.ts 里 DEFAULT_MAX_PARALLEL_TOOL_CALLS = 10,注释写的是「每个 agent step 内并行安全调用的默认上限」,而委派在 tool-subagent 里就是注册出来的一个工具。两处事实摆在这里,别再往下推——这不是子代理并发额度,源码里也没说它是。
四、回收:把「还没干完」和「已经干完但没释放」分开
DeepSeek Harness 的 docs/subsystems/subagent.md 把可续会话子代理的常驻期叫 Activation,并给了三个内部状态:running 是有活跃的准入或回合、或有正在唤醒的收件箱工作;waiting 是自己静默了但名下至少还有一个子 Activation 没完成释放;settled 是静默且名下每个孩子都已释放,此时管理器才处置 AgentHandle 并移除 Activation。文档明写孩子的释放要等四件事都成立:孩子的 Agent 静默、它自己的每个孩子都已释放、尽力而为的最后一次会话 flush 落定、AgentHandle 完成处置。整棵树是 child-first 往回收的,父亲名下集合非空就到不了 settled。
打断是另一回事。SubagentRuntime.interrupt() 的文档写明它同步做完授权后对活着的目标发 Agent.cancel(cause, { keepInbox: true }) 就返回,不等静默:Activation、未被认领的排队消息、已发布的后代都不动,已经被认领进那一轮的工作不会重新排队;等被打断的驱动空闲下来,下一条唤醒消息会接着跑那条被停住的 FIFO 队列。目标不存在——未知 id、一次性子代理、已经 settled——都是被接受的空操作。
Codex 把这两件事拆成两个工具,语义也写在描述里:interrupt_agent 打断当前回合并返回其先前状态,「The agent remains available for messages and follow-up tasks」;close_agent 则是关闭该 agent「and any open descendants」。Claude Code 官方文档写的是另一层:后台子代理成功结束后面板里的行立即移除,失败或被你停掉的保留 30 秒;transcript 存在 ~/.claude/projects/{project}/{sessionId}/subagents/ 下,文件名形如 agent-{agentId}.jsonl,按 cleanupPeriodDays 的保留期删除,默认 30 天。文档还写明一条容易被忽略的分支:你自己用 /tasks 里的 x 或 SDK 的 stop_task 停掉的子代理不会被 SendMessage 自动唤醒,那次发送会返回一条拒绝。Pi 的示例扩展 README 则写 Ctrl+C 会传播下去杀掉子进程。
五、会咬到你的那一处
DeepSeek Harness 这边有一处我们在另外三家的公开材料里没有找到对应说明的设计:它把「谁来管递归深度」做成了一个显式的能力位。packages/subagent/subagent/src/types.ts 里的 SubagentCapabilities 有四个布尔字段 outputSchema、depthLimit、toolFilter、persona,服务在 start() 之前逐个校验,缺了就用 SubagentError('UNSUPPORTED_CAPABILITY') 直接拒,文档自述这条规则叫「fail loud, no silent degradation」。
两个进程内后端(spawn 与 fork)四项全 true;而 packages/subagent/subagent/src/out-of-process.ts 导出的 NO_START_CAPABILITIES 是四项全 false(且被 Object.freeze 冻住),subagent-claude-code、subagent-codex、subagent-dsh-sdk 三个跨进程 provider 直接引用它;subagent-acp 没有引用这个常量,而是在自己的 src/index.ts 里逐字写了同样的四个 false,文件头注释自述它「shares no Cordis context and advertises no parent-enforced start capabilities」。于是就有了这个后果:tool-subagent 的 maxDepth 默认是数字 3,而 packages/subagent/tool-subagent/src/index.ts 的 mount() 里有一段检查——当 maxDepth 是数字且 provider 没有 depthLimit 能力时,直接抛错,提示你把 maxDepth 设为 'provider-managed',把递归预算交还给 provider。注释写明这样做是要在挂载时就失败,而不是等到第一次委派。同一个函数里还有一条:backgroundMode: 'continuable' 遇上没实现 prepareContinuable 的 provider 也在挂载时抛错。
所以,当你想把 DeepSeek Harness 的委派转接到外部产品后端上时,那个默认的 3 会在插件挂载阶段就把你拦下来。这一层的理由源码注释自己写了:'provider-managed' 就是留给「递归预算属于子运行时或其自身部署」的跨进程 provider 的,而一个 provider 执行不了的数字上限被判定为配置错误,所以要在能读到 provider 能力的最早时刻——挂载——失败。
顺带记一条边界:packages/subagent/subagent-fork-in-process/src/index.ts 的 prepareContinuable 上方有一段 TODO(fork-continuable-prefix-reuse) 注释,明说目前没有已发布的组合会调用它,它们把 fork 绑定在 backgroundMode: 'one-shot' 上。文档里出现的方法不等于当前有组合在用它,这一条按源码注释照实记下。
决策路径
从你的处境倒推,而不是从参数表里挑:
- 孩子要不要看见你现在聊到哪儿了。 要,且要全量——Claude Code 的 fork 与 Codex 省略
agent_type的spawn_agent都写明是全量历史;要,但只要已完成的部分——DeepSeek Harness 的 fork provider 切到最后一个turn/end;不要——spawn 系全部是全新会话。注意 DeepSeek Harness 这边「继承对话」不等于「继承工具与权限」,源码注释专门声明过。 - 你的活会不会自然长出第三层。 会的话,Codex 的默认
1需要你动agents.max_depth;DeepSeek Harness 与 Claude Code 的默认 3 够用。 - 你会不会一口气派十几个。 会的话先看两处:Codex 的
close_agent描述写明完成的 agent 仍占额度直到关闭,Claude Code 的文档写明恢复子代理不检查并发限额。这两条都是「明明没在跑却占着位」的来源。 - 有人得能中途叫停并保留现场。 这一点上 DeepSeek Harness 的
interrupt()与 Codex 的interrupt_agent都写明打断当前回合但保留后续接收能力;Pi 的示例扩展写的是 Ctrl+C 杀进程,语义不同,别按同一个心智模型用。 - 要不要把外部产品当后端。 这条路我们只在 DeepSeek Harness 一侧查到明写的实现(
packages/subagent下有subagent-claude-code、subagent-codex、subagent-acp、subagent-dsh-sdk这几个 provider 包),另外三家的文档与源码里我们没有找到对应说明,代价就是上一节那个能力位——外部后端不承诺任何启动期能力,你得在配置里认下这件事。
最后重复一遍前提:以上 DeepSeek Harness 的字段名与默认值来自我们实读的源码,而这个仓库自述处于开发者预览阶段并明确会有破坏兼容性的变更;Claude Code 的部分全部来自其官方文档,其中多条默认值文档自己就标注了在不同版本间变过。四家的这些数字都不是承诺,只是当下这一刻的口径。
本文依据 DeepSeek Harness 官方仓库(github.com/deepseek-ai/deepseek-harness)的 README、docs/ 下的
架构与子系统文档、以及 packages/ 下的源码整理,核对日 2026-08-17,对应仓库快照 47f9438(版本 0.1.0-rc.5)。
本文内容为仓库源码与文档口径,我们没有安装、也没有运行过这个项目,
因此不涉及界面外观、操作手感与运行速度的任何描述。
该仓库 README 自述处于开发者预览阶段并明确说明未来会有破坏兼容性的变更,
文中出现的命令、配置与默认值随时可能变动,请以仓库最新内容为准。
文中涉及的 Claude Code 内容依据其官方文档(code.claude.com/docs)整理,该产品闭源,本文不推断其实现;
Codex 依据 github.com/openai/codex 快照 c6058cc、Pi 依据 github.com/earendil-works/pi 快照 027a5847 整理。
本文只对照各方公开写明的机制,不对三者做优劣排名。