上周我让 Claude Code 干一个重构活,跑到一半发现它在偷偷开 subagent,一层套一层,等我反应过来 token 已经烧穿了三层上下文。
我盯着屏幕上那个我根本没授权的子任务,第一反应不是「模型真聪明」,而是「这玩意到底是谁派出去的,它派了几层,我怎么知道它还能不能再派」。
说实话,这是我用 Claude Code 大半年一直别扭的地方。子 Agent 很好用,但 spawn 不 spawn、嵌套多深、能不能续谈,全靠模型读 prompt 自己悟,没有硬约束。你在 subagent 的 description 里写「不要嵌套」,它心情好就听,心情不好就当没看见。
然后这周 DeepSeek 扔出了 Harness v0.1,4 天 145,505 颗星 GitHub 2026-08-17。我把它 subagent 子系统的类型源码从头读了一遍,读完最大的感受不是「免费真香」,而是他们把我上面那个别扭,做成了一套带类型、带能力协商、带持久化深度的显式协议。
这篇就拆这件事。不聊 npx 一键启动,不聊 star 数奇观,聊点只有翻源码才看得见的东西。
先给结论
如果你只想知道这东西跟我有没有关系,看这张表。
| 你是谁 | 现在该干嘛 |
|---|---|
| 天天用 Claude Code subagent,被嵌套和上下文串台坑过 | 必读,DSH 的协议设计能直接纠正你对 agent 边界的理解 |
| 在评估自建 agent runtime / 做多 agent 编排 | 强烈建议读源码,它的 Capability Seam 抽象值得抄 |
| 想找个 Cursor 替代品写业务代码 | 别冲动,这是 harness 不是 IDE,rc.7 还在天天 breaking |
| 只想蹭热点发朋友圈 | 看到这就够了,下面很硬 |
一句话定位,DSH 不是 Cursor 竞品,它是 Agent = Model + Harness 这个公式里的 Harness 层 deepseek.com/harness。模型负责思考,harness 负责给模型「理解环境、使用工具、在真实场景持续工作」的能力。Claude Code、Codex 自己也是 harness,只不过它们是封装好的产品;DSH 想做的是把 harness 本身拆开,让每一层都可替换。
一切皆插件,包括 agent loop 自己
要看懂子 Agent 协议,得先看懂 DSH 的骨架,因为子 Agent 在这套骨架里根本不是核心组件,而是一个「可选能力」。
DSH 基于一个叫 Cordis 的内核(cordiverse/cordis,5,366 stars),而且直接 vendored 进了仓库 DSH vendor/。Cordis 本身啥 agent 能力都没有,它只做一件事,插件的加载、卸载和依赖管理。在这个内核之上,DSH 把所有东西都做成了插件,model adapter 是插件,tool registry 是插件,session log 是插件,连 agent loop 本身都是插件。
这句话分量很重。没有一个不可 patch 的特权核心。你想换模型,挂个 adapter 插件;你想换掉整个 agent 循环逻辑,也是挂个插件的事。扩展 DSH 不是去 fork 源码改核心,而是在别的插件旁边再挂一个,注册是 effect,插件卸载时自动回滚。
配置层叫 Profile + Bundle。一个 profile 是有序 bundle 层叠出来的插件树。dsh-base 是每个 profile 的第一层,装 model adapters、tools、persistence、sandbox、approval policy;dsh-web-app 加浏览器,dsh-headless 加一次性 runner。patch 按行 id 替换整行 config。
这里有个关键概念叫 Capability Seam,一个 seam = Service Definition(声明接口)+ Service Provider(实现)+ Consumer(通常是给模型用的工具)。三者齐备,这个能力才算在系统里「立住了」。
子 Agent,就是这样一个 seam。
子 Agent 为什么是 seam 而不是 loop 的一部分
大多数 agent 框架(包括 Claude Code)的处理方式是,把子 Agent 调用硬编码成一个工具,叫 Agent 工具,模型决定调不调、调哪个 subagent_type。这是「loop 的一部分」。
DSH 不这么干。它把 subagent 做成了一个和 bash 平级的可选能力 ,类型定义不在 core 里。但 subagent 和 bash 有个本质区别,bash 只允许一个 executor,你不能同时注册两个 bash 实现;subagent 却允许多个 provider 按名字共存 ,全部挂在 ctx.subagents 上。
这个设计的潜台词是,「怎么 spawn 一个子 agent」根本没有唯一答案。你可以在同进程里 spawn,可以 fork 一个带父上下文的,可以走远程 ACP 协议,甚至可以把整个 Claude Code 或 Codex 当成后端拉起来。框架不该替你决定,它只负责定协议。
DSH 内置了 6 个 provider DSH 源码 packages/subagent/,我给你一个个摆出来。
| Provider | 机制 | 要不要父上下文 |
|---|---|---|
spawn-in-process |
同进程新建 agent | 不继承 |
fork-in-process |
同进程 fork,继承父已完成 turn 前缀 | 继承 seed |
acp |
Agent Client Protocol 远程 agent | 不继承 |
codex |
拉起 codex app-server --stdio |
不复制父对话 |
claude-code |
调 @anthropic-ai/claude-agent-sdk@0.3.220 |
persistSession:false |
dsh-sdk |
DSH 自己的 JSON-RPC SDK | 按 descriptor |
注意后面两个,DSH 把自己的直接竞品做成了 subagent backend,这事我单独开一节讲,戏剧性很强。
四个布尔值,把「能力不匹配」从运行时提前到启动时
这是我觉得整个 DSH 最值得抄的设计。每个 provider 在启动前必须声明自己支持哪些能力,就四个布尔值。
ts
// 来源:DSH 源码 packages/subagent/src/types.ts
interface SubagentCapabilities {
readonly outputSchema: boolean // 能不能返回结构化输出
readonly depthLimit: boolean // 能不能遵守委派深度上限
readonly toolFilter: boolean // 能不能裁剪子 agent 的工具集
readonly persona: boolean // 能不能给子 agent 独立人格
}
看着简单对吧。狠的是后半句,如果父 agent 发起一个请求,要求子 agent 必须支持某个能力(比如我要你返回结构化 outputSchema),而这个 provider 声明它不支持,DSH 的做法不是「先接着,做不到拉倒」,而是启动时直接抛 SubagentError('UNSUPPORTED_CAPABILITY') 拒绝启动。
官方管这叫 fail loud, no silent degradation,失败要响,不许静悄悄降级。
你可能觉得这有什么了不起。我给你看 Claude Code 的对照面。Claude Code 的 Agent 工具靠 prompt 里的 description 让模型「理解」这个 subagent 能干什么。模型读完 description,觉得它能干,就把任务派过去;结果这个 subagent 其实不支持结构化输出,或者工具集不对,它不会提前拒绝,它会「接了然后做不好」,最后给你一坨似是而非的自然语言。你排查半天才发现是能力不匹配。
DSH 把这件事从「模型靠语义猜」变成了「provider 用类型声明,运行时按契约校验」。四个布尔值就是一份合同,签字画押,做不到就别上桌。
这才是协议。
一次性 vs 可续,两种生命周期
DSH 的子 agent 有两种活法,one-shot 和 continuable。这个区分也是 Claude Code 完全没有的。
One-shot(一次性) 好理解,SubagentRun 就是一个句柄,派出去,干完,返回一个 SubagentResult,完事。没有 steering,没有 resume。结果里带 output、structured(如果请求了 outputSchema)和 stopReason,stopReason 就那几种,completed / aborted / error / max-tokens / refusal。
Continuable(可续) 才是真正有意思的东西。它是一个持久化的子 Session,可以跨多次对话甚至跨冷启动存在。一个 continuable 子 agent 最多有一个进程内 Activation(驻留期),Activation 不是 request 也不是 Task,它能在里面跑多个 FIFO turn,甚至在它自己派出去的子代还在跑的时候保持驻留。
这里有几个操作得讲清楚。
startContinuable() 先预留一个稳定的 child id,provider 返回一个 detached 的 ContinuableCreateSpec,里面只有可选的 parent-history seed,然后 continuation manager 自己造 agent、建 inbox、提交初始 prompt。注意,provider 不参与 continuable 的后续生命周期 。冷恢复根本不经过 provider,manager 拿通用 descriptor 加 ctx.agents.resume() 就把它拉起来了。
followup() 是唯一的续消息操作。如果 Activation 在 running,就入队;如果在 waiting,就唤醒;如果压根没有 Activation,就冷启动一个新的。一个接口把三种状态全兜住。
interrupt() 是唯一公开的停止操作,授权检查后调 Agent.cancel(cause, {keepInbox:true}),不 await 静止,不清 inbox,也不动它的后代。意思是你打断它,它收件箱里没处理的消息还在,后代也继续跑。
还有个 reportFrom(),子到父的回报通道,凭证是子自己,调用者不能指定收件人。谁派的它,它就报给谁,不靠消息里的字段授权。
这套东西解决的问题是,长任务里子 agent 不该是「一锤子买卖」。你派一个子 agent 去做代码检索,它可能需要被追问、被打断、被要求阶段性汇报,然后过两天冷启动回来接着干。Claude Code 的 subagent 做不到,它就是一次性函数调用,没有「会话」的概念。
委派深度持久化,嵌套三层是硬拒绝不是软提醒
回到我开头那个痛点,模型偷偷 spawn 了三层 subagent。DSH 怎么防的。
它把委派深度持久化在 SessionHeader.delegationDepth 字段里,运行时还有个 AgentOptions.subagentDepth,两者取较大值。子 agent 创建时,持久化父 depth 加 1。关键是冷启动不能把这个深度降下来 ,你 resume 一个 depth 为 2 的 session,它还是 2,没法靠重启洗白。一旦超过配置的 maxDepth,直接硬拒绝。
这跟 Claude Code 的区别是本质性的。Claude Code 的「不要嵌套」写在 prompt 里,是软约束,模型听不听是缘分;DSH 的 maxDepth 写在持久化的 session header 里,是硬约束,超了就是 error,模型再想 spawn 也 spawn 不出来。
再讲一个 fork 的细节,很见功力。fork-in-process 用 CreateAgentOptions.seed 给子 agent 传父 log,但传的不是整个历史,而是父 log 的平衡已完成 turn 前缀 ,也就是截到最后一个 turn/end 为止,进行中的那个不平衡 turn 被排除掉。为什么这么抠,因为 DSH 的 session log 是 append-only 的,有个运行时不变量叫 Model-visible 等价于 logged,任何到达模型的东西必须能从 log 重建。传一个半截 turn 过去,replay 的时候 invariants 会对不上。传「平衡前缀」就是为了保证子 agent replay 出来的状态跟父 agent 在那个时间点完全一致。
这就是源码级阅读才能看到的东西,人家不是随便 fork 一下,是连「截到哪」都有协议层面的理由。
三种 Harness 哲学横评
铺垫够了,上桌。Claude Code、Codex、DSH 三家在子 Agent 这件事上的设计哲学,我整理成这张表。
| 维度 | Claude Code | Codex | DeepSeek Harness |
|---|---|---|---|
| 子 agent 模型 | Agent 工具加 subagent_type,单进程,模型决定 spawn | app-server stdio 沙箱进程,审批驱动 | 命名 provider 注册表,多 provider 共存,能力显式协商 |
| 谁定边界 | 模型,靠 prompt 和 description | 产品加审批策略 | provider 能力声明加 start 时 fail-loud 校验 |
| 隔离 | 同进程独立上下文 | 进程加 sandbox | provider 可选,同进程 spawn/fork、远程 ACP、外部产品进程 |
| 可替换性 | Skill/MCP 可加,loop 不可换 | 沙箱/审批可配,核心固定 | 一切皆插件,agent loop 本身可换 |
| 委派深度 | 软约束,prompt 引导 | 产品自管 | 持久化 delegationDepth 加 maxDepth 硬拒绝 |
| 父子上下文 | 子收 prompt,不收父历史 | 不传父对话 | fork 可传平衡 turn 前缀,其他不传 |
| 可续子 agent | 无 | 无,ephemeral | continuable 加 Activation 加 followup/interrupt/report |
| 跨产品委派 | 不能委派给 Codex | 不能委派给 CC | claude-code 和 codex 都是内置 provider,可互委 |
我挑三个最有 insight 的差异展开。
第一,谁来决定子任务的边界。 Claude Code 把这个权力交给了模型,模型读 description 自己判断要不要 spawn、spawn 谁。这很灵活,但边界是隐式的、语义的、随时可能漂移。Codex 把权力收给产品和审批策略,更可控但更死。DSH 走了第三条路,边界由 provider 的能力声明决定,启动那一刻用类型系统校验,过了就过了,过不了直接拒绝。这是把「边界」从一个 prompt 工程问题变成了一个协议契约问题。
第二,可续子 agent 这件事,只有 DSH 有。 Claude Code 和 Codex 的子 agent 都是 ephemeral 的,一次调用一次消亡。但真实的长任务里,子 agent 经常需要被追问和续谈。DSH 的 continuable 加 Activation 加 cold resume 是目前我在开源 agent 框架里看到最完整的子会话生命周期设计。代价是复杂度,这套 continuation manager 的心智负担不低,但它解决的是真问题。
第三,跨产品委派。 这个下一节细讲,它是整张表里最颠覆的一格。
把竞品做成自己的 subagent backend
这一节是全文最有戏剧性的部分,你坐稳。
DSH 内置了 codex 和 claude-code 两个 provider。意思是,你在 DSH 里跑一个 agent,这个 agent 可以把子任务委派给 Codex,也可以委派给 Claude Code。DSH 把它的直接竞品,变成了自己的执行后端。
我读了官方那份 Agent Note(2026-08-04-claude-code-and-codex-subagent-backends.md),里面的设计决策非常刻意,我给你拆。
这两个 provider 都是 one-shot only ,inheritsParentContext: false,不声明任何可选能力。每次委派,拉起一个全新的产品进程和一个不可续的产品会话。只接受独立文本任务,把父 session 的 cwd 传过去,不复制父对话。
具体到实现,Codex provider 启动 codex app-server --stdio,建一个 ephemeral:true 的 thread,turn/completed 是权威终态。如果任务执行中需要审批而没人在,审批策略默认 cancel 或 decline,不授予任何权限。Codex 这边 pin 的是 0.147.0,走 Responses 协议。
Claude Code provider 用 @anthropic-ai/claude-agent-sdk 的 query(),persistSession:false,禁用 AskUserQuestion,而且故意不传 canUseTool 和 elicitation 回调,意思是一旦需要人机交互就直接失败,不弹问题。pin 的是 SDK 0.3.220 / CLI 2.1.220 DSH package.json。
产品进程由 dsh-subprocess 统一管,凭据清洗、进程树终止、整树退出观察都在这一层。
为什么设计得这么「绝」,每次都全新进程、不传上下文、无人交互就失败。官方原话是,每次委派付出全新产品进程加独立模型上下文的代价,产品 payload 回到父级的只有最终文本。这不是偷懒,这是安全边界。
你想,如果子 agent 是一个完整的 Claude Code 进程,它默认继承了你父会话的全部上下文和权限,那它就不是「子 agent」了,它是你父 agent 的一个分身,边界瞬间崩塌。把它做成 one-shot、无上下文、无审批就死,等于在 DSH 的编排层和外部产品之间画了一道清清楚楚的线,你就是个干独立文本活的临时工,干完交结果,别想碰我的会话状态。
还有个细节,这两个 provider 默认不装在 dsh-base 里,是 production-install exclusion,需要你在 Profile 里显式安装、放到 host 平面,再由 Agent Preset 决定要不要暴露对应工具。也就是说,DSH 团队自己都觉得这是个高级能力,不打算让新手一上来就玩跨产品委派。
这一节的架构判断我留给你,一个框架敢把竞品做成自己的 backend,要么是狂妄,要么是它对自己「编排层」的定位有足够的自信。我倾向于后者。
预览版的坑,以及现在值不值得上手
吹完了,说点实在的。这东西现在是 rc,而且是 rc.7,不是 1.0。
仓库 2026-08-13 才创建,到我写稿这天 4 天 GitHub repo.created_at。npm 上 latest 是 rc.6,next 是 rc.7 npm view 2026-08-17。官方 README 明明白白写着 THERE WILL BE COMPATIBILITY-BREAKING CHANGES。SESSION_FORMAT_VERSION 是 0,没有兼容承诺,SQLite 用单调的 SCHEMA_VERSION 拒绝旧格式,意思是你今天存的 session,明天升级可能就读不出来了。
环境门槛也不低,Node 要 ^22.19.0 || >=24.0.0,包管理器 pin 了 pnpm 11.7.0 DSH package.json engines。头号贡献者 tianyicui 一个人提了 5262 个 commit gh contributors,你大概能感受到这项目现在有多早期、多集中在少数人手里。
启动命令倒是简单。
bash
# 需要 Node >= 22.19,官方一键启动 Web UI(我本机沙箱网络超时没跑成界面,命令来自 README)
npx @deepseek-ai/dsh web
# 默认监听 http://127.0.0.1:3080
但我必须诚实,我这边 npx 在沙箱里安装超时,没拿到实时 Web UI 截图。这篇是源码级拆解,我读完了架构文档、subagent 的 types.ts/index.ts/continuation.ts、Codex 和 Claude Code provider 的 Agent Note、sandbox 文档和 base bundle README,但我不编造 UI 操作体验。市面上那些「我跑了一遍 DSH 真香」的文章,你留个心眼,很多可能 npx 都没装完。
我的上手建议分三档。
- 如果你是做 agent 基础设施的,现在就去读源码,重点读
packages/subagent/,Capability Seam 和 continuation manager 这两套抽象值回票价,哪怕最后不用 DSH。 - 如果你是想拿它当生产工具写业务的,等 1.0。rc 阶段天天 breaking,session 格式都不兼容,别拿生产项目陪跑。
- 如果你是来凑热闹的,star 一下放观察列表就行,生态刚起来,awesome list 都是今天才建的。
常见问题
DSH 和 Cursor 到底是不是竞品。
不是。Cursor 是 IDE,是带编辑器的产品;DSH 是 agent harness/runtime,是给模型提供工具、会话、沙箱、子 agent 编排的底层框架。它确实带个 Web UI,但定位是 harness 的一个界面,不是 IDE。更贴切的类比是,DSH 想做 agent 界的 Express/Fastify,而 Cursor 是 agent 界的 Chrome。
6 个 provider 里我日常该用哪个。
你在 DSH 内部编排,优先 spawn-in-process(要隔离就用它)和 fork-in-process(需要继承父上下文做接力时用)。跨机器或跨团队用 acp。claude-code 和 codex 是高级能力,需要跨产品委派时再开,而且要接受每次全新进程的代价。dsh-sdk 是给外部程序通过 JSON-RPC 接入用的。
continuable 子 agent 听着强大,是不是该一律用它。
不是。continuable 有持久化 session、有 Activation、有 inbox,复杂度和资源开销都比 one-shot 高一大截。绝大多数一次性子任务,one-shot 就够了。只有当你明确需要追问、打断、跨冷启动续谈时,才值得上 continuable。别为了用而用。
委派深度 maxDepth 设多少合适。
没有银弹,但我的经验是别超过 2 到 3 层。DSH 把它持久化硬拒绝是好事,但你要自己想清楚为什么需要深层委派。大多数「需要嵌套」的场景,其实是任务拆解没做好,而不是真的需要 5 层 subagent。把 maxDepth 当设计压力,逼自己把任务拆平。
Claude Code 用户现在能从 DSH 偷师什么。
就算不用 DSH,你也可以借鉴它的协议思维,在自己的 subagent description 里把「输入契约、输出契约、能力边界」写死,把不支持的能力显式标出来而不是让模型猜;在编排逻辑里自己做深度计数和硬截断;把一次性任务和可续会话在设计层面分开。协议不一定要框架给,你自己在 prompt 层和脚本层也能立。
写在最后
回到我开头盯着屏幕的那个下午。
我当时别扭的根本不是「模型偷偷开 subagent」这件事本身,而是没有人在协议层面回答「谁有权 spawn、能 spawn 成什么样、嵌套多深」这三个问题。Claude Code 把答案藏在 prompt 里,Codex 把答案收在产品策略里,DSH 第一次把答案摆到了台面上,用类型、用能力声明、用持久化深度,写成了一份可以校验、可以拒绝、可以跨产品复用的合同。
我不确定 DSH 最后能不能成,rc.7 的 breaking 节奏和单核心贡献者的集中度都是真实风险。但我确定一件事,agent harness 这个层未来一定会往「显式协议」方向走,而不是永远靠模型读 prompt 自己悟。隐式的东西终将被显式化,能被类型系统约束的,就不该留给心情。
你在用 Claude Code 的 subagent 时踩过什么坑,嵌套失控、上下文串台、还是子任务一去不回,评论区聊聊,我想看看有多少人跟我受过一样的罪。
做 Agent 开发的朋友,下篇我打算动手把 DSH 的 continuation manager 抄一遍,用最小可运行代码讲透 continuable 子 session 的状态机。感兴趣的话,关注「码哥跳动」,发出来你能第一时间看到。身边有人在做 agent 编排选型的,这篇可以直接甩给他,省得他再踩一遍软约束的坑。
🤖 配套实战手册
整理了一份《AI Agent 入门实战手册》,从零到可运行代码,含 5 个踩坑场景和生产级模板。
回复「agent」即可获取。
转发给正在学 AI 开发的朋友 ,少走弯路 🚀