DeepSeek Harness 的六个 subagent provider:只有两个能续接
先说清前提:本文写的是 deepseek-ai/deepseek-harness 这个仓库在 2026-08-16 的快照 47f9438 的源码与文档口径。该仓库建立于 2026-08-13,根 package.json 的版本是 0.1.0-rc.5,GitHub 上一个 Release 都没有,README 自述处于开发者预览阶段并明确写了未来会有破坏兼容性的变更。下面提到的每一个方法名、字段名、默认值,都要带着这层限定看——我们没有安装、没有运行过它,所有内容都是读文件得来的。
从一个具体决定开始:这个子代理你还想不想再追问
用子代理办事,最先要决定的不是「派几个」,而是「派完之后还想不想再跟它说话」。
一次性跑完拿结果,和跑完之后留在那儿、之后还能追加一句「刚才那份改一下」,在 dsh 里是两条不同的路径。前者叫 one-shot,后者叫 continuable(可续接)。而这条路径能不能走通,取决于你挑的 provider 到底实现了什么。
docs/subsystems/subagent.md:5 给 subagent 的定位是:这是一项可选能力,不是 agent loop 的一部分。它和 bash 的区别在于,同一个上下文里可以并存多个按名注册的 provider(挂在 ctx.subagents 上),而 bash 只允许一个执行器。多个 provider 并存,就意味着「这个能力有没有」这句话必须落到某一个具体的 provider 上问。
逐个读源码得到的能力矩阵
packages/subagent/ 下一共 11 个包目录。其中作为 provider 注册的是这六个,逐个读它们 src/index.ts 得到的能力位如下:
| provider 包 | 注册默认名 | 四个 start-time 能力位 | inheritsParentContext | 有无 prepareContinuable |
|---|---|---|---|---|
subagent-spawn-in-process | spawn(src/index.ts:31) | 全 true(:42) | false(:44) | 有 |
subagent-fork-in-process | fork(src/index.ts:37) | 全 true(:62) | true(:64) | 有 |
subagent-acp | acp(src/index.ts:67) | 全 false(:147) | false(:149) | 无 |
subagent-codex | codex(src/index.ts:48) | NO_START_CAPABILITIES(:49) | false(:50) | 无 |
subagent-claude-code | claude-code(src/index.ts:53) | NO_START_CAPABILITIES(:54) | false(:55) | 无 |
subagent-dsh-sdk | dsh-sdk(src/index.ts:72) | NO_START_CAPABILITIES(:94) | false(:96) | 无 |
四个 start-time 能力位分别是 outputSchema、depthLimit、toolFilter、persona(docs/subsystems/subagent.md:27-32)。
这张表读下来,分界线很干净:两个 in-process provider 全 true 且带 prepareContinuable;四个 out-of-process provider 全 false 且不带。ACP 与 SDK 两个包的源码注释把原因写在了 subagent-acp/src/index.ts:142-144 与 subagent-dsh-sdk/src/index.ts:89-91——跨进程的子代理无法履行这几项。这是源码注释里写明的说明,不是我们的推断。
第 11 个包 subagent-in-process-driver 不是 provider,它在自己的 README.md:5 里自述为「两个 in-process provider 共享的运行驱动」:spawn 不传 session seed,fork 传父的已完成轮次前缀,其余(深度、子代理创建、结果读取、取消、销毁)都在这里只实现一份。
顺带记一处口径差异:docs/subsystems/subagent.md:7 列 Service Provider 时写的是 dsh-subagent-spawn-in-process、-fork、-acp、-codex、-claude-code、-dsh-sdk 六个;而 packages/subagent/ 下实际有 11 个包目录,subagent-in-process-driver 未出现在那一句里。两处不一致,以我们实读的目录为准,不再延伸。
为什么这两类能力的发现方式是分开的
这才是这套设计里比较反直觉的一处。dsh 对「能力」用了两套完全不同的发现机制(docs/subsystems/subagent.md:13):
start-time 能力写在静态描述符 SubagentCapabilities 上,服务在 start() 之前就检查。请求了 provider 没有的能力会大声拒绝——抛 SubagentError('UNSUPPORTED_CAPABILITY'),而不是「接受下来然后悄悄忽略」。这一条在多 provider 的场景下很要紧:你给 codex 传一个 outputSchema,得到的是明确的报错,不是一个结构没生效、你还以为生效了的结果。
continuable 却不走描述符。可续子代理是由续接管理器自己组装的,因此「方法存在即能力」——SubagentProvider.prepareContinuable 这个可选方法在不在,就是这项能力有没有(docs/subsystems/subagent.md:13、:442-457)。对应到实现侧,我们对 packages/subagent/*/src/*.ts 搜这个标识符,命中的文件是 subagent-fork-in-process/src/index.ts、subagent-spawn-in-process/src/index.ts、subagent/src/ 下的 continuation.ts / index.ts / types.ts,以及 tool-subagent/src/index.ts。换句话说,出现这个名字的地方只有两个 in-process provider、服务自身,以及模型侧的委派工具包;另外四个 provider 的源码里一次都没有出现过它。
所以查前四个能力看描述符,查可续接看方法在不在。这两件事在文档里放在同一节里讲,但落到代码是两条路。
可续接意味着背后多了一套什么
docs/subsystems/subagent.md:118-124 用一段结构原文说明了可续接背后的东西:
persisted Session
-> optional live Activation
-> one retained AgentHandle
-> Agent inbox as the only turn FIFO
-> zero or more owned child Activations
一个可续的后台子代理 = 一个持久化的子 Session + 至多一个进程内 Activation。文档特意强调 Activation 不是一个请求、不是结果、不是取消、也不是 Task,它可以执行很多个 FIFO 轮次,并在它创建的后代还在跑的时候保持驻留(:116)。
启动语义也值得单看一眼:startContinuable() 在 inbox 接受初始 prompt 的那一刻就 resolve,返回 { childId, messageId },不等轮次开始,也不等消息落进 Session log;在此之前的任何失败,都以「两个 id 都不返回」的方式拒绝并回滚(:126)。
续接投递只有一个操作 followup(),路由只看 Activation 的驻留状态(:130-134):running 就在同一个 Activation 里排队,waiting 就唤醒同一个 Activation,没有 Activation 就冷恢复一个新的。这三种内部状态的定义在 :136。
授权那条也别忽略:follow-up 的权限来自精确的活跃 Agent 工具上下文,且必须是 SessionHeader.parentSession 里记录的直接父;MessageSource 与 senderSessionId 只做记录,不授予权限(:140)。
这一整套(持久子 Session、Activation 驻留、冷恢复、直接父授权)就是四个 out-of-process provider 拿不到的东西。
出厂组合里实际配了什么
能力矩阵是一回事,出厂默认组合是另一回事。packages/bundle/base/cordis.patch.yml 里的相关行是:
subagent(:292)subagent-spawn-in-process,配providerName: spawn(:295-298)subagent-fork-in-process,配providerName: fork(:300-303)tool-subagent-control(:307-308)与其/list-agents子路径(:310-311)tool-subagent一份,配provider: spawn+toolName: subagent+backgroundMode: continuable(:313-318)tool-subagent再来一份,配provider: fork+toolName: subagent_fork+backgroundMode: one-shot(:324-329)tool-subagent-report(:332-333)
两件事得单独指出来。
第一,我们对 packages/bundle/*/cordis.patch.yml 做 grep,没有找到 acp / codex / claude-code / dsh-sdk 这四个 provider 的组合行。它们在 packages/subagent/ 下有包,但出厂组合里没有对应的行。要用就得自己写组合层。
第二,fork 明明带 prepareContinuable,出厂组合却把它绑在 backgroundMode: one-shot 上。这一点源码里给了记录:packages/subagent/subagent-fork-in-process/src/index.ts:77-82 有一条 TODO(fork-continuable-prefix-reuse),注释里写「no shipped composition calls this」,并说明可续子代理的 report 工具与 prompt section 会排在被继承的历史之前,抵消掉 fork 存在的意义(前缀复用);要重开这条路需要逐字节一致的子系统提示词与工具 schema,并指向 issue #2124。这是源码注释的记录,不是能力缺失。
模型侧的委派工具默认值也在这里对一下:toolName 默认 subagent(packages/subagent/tool-subagent/src/index.ts:83),maxDepth 默认 3、也可以取字符串 'provider-managed'(:98),取 0 表示禁止再委派。这些是配置里的默认值,不是对运行表现的任何保证;该调成多少取决于你的用法,项目没有给通用值。
你自己怎么核这件事
如果你要在自己的组合里换 provider,判定动作可以这么走:
- 先看
packages/subagent/<provider 包>/src/index.ts,找capabilities那几行,确认四个 start-time 能力位; - 在同一个文件里搜
prepareContinuable,方法不在就说明这个 provider 走不了可续接这条路; - 回
packages/bundle/base/cordis.patch.yml看有没有它的组合行,没有就得自己加; - 如果你要的是「后台留着还能追问」,还得确认对应的
tool-subagent那一行配的是backgroundMode: continuable而不是one-shot; - 什么情况说明问题不在这儿:如果你的报错是
SubagentError('UNSUPPORTED_CAPABILITY'),那是第 1 步那四个能力位的事,与prepareContinuable无关——这两条是分开的检查路径。
最后提醒一句范围:无论你挑的是哪一个 provider,子代理最终都会在你的机器上执行工具、跑 shell、起子进程——这是 dsh 这类 harness 的固有面,仓库文档也没有把它藏起来。这不是「有隔离所以没事」的问题,接进自己的环境之前请按自身情况评估。
延伸阅读
- 从头读起:DeepSeek Harness 是什么:建仓三天、13 万 star 的 Agent 框架
- 本专题共 45 篇,完整分组目录见专题页
- DeepSeek Harness 的 skill 是什么格式、怎么被装载
- DeepSeek Harness 的定时调度:文档写 cron,协议与源码里没有
本文依据 DeepSeek Harness 官方仓库(github.com/deepseek-ai/deepseek-harness)的 README、docs/ 下的
架构与子系统文档、以及 packages/ 下的源码整理,核对日 2026-08-16,对应仓库快照 47f9438(版本 0.1.0-rc.5)。
本文内容为仓库源码与文档口径,我们没有安装、也没有运行过这个项目,
因此不涉及界面外观、操作手感与运行速度的任何描述。
该仓库建立于 2026-08-13,README 自述处于开发者预览阶段并明确说明未来会有破坏兼容性的变更,
文中出现的命令、配置与默认值随时可能变动,请以仓库最新内容为准。
安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。