DeepSeek Harness 架构分析
仓库:
deepseek-ai/deepseek-harness协议:MIT 发布日期:2026-08-13(上线仅 1 天,developer preview 阶段)
技术栈:Node.js + TypeScript + pnpm workspace + Cordis 框架
代码量:1247 个 TS 源文件 + 684 个测试文件,packages/ 下按领域分目录
一、它到底是个什么东西
DeepSeek Harness(简称 dsh)是 DeepSeek 官方开源的 Agent 框架。dsh 不做产品,只做框架。它的核心口号是 Everything is a Plugin,包括模型适配器、工具注册表、会话日志、Agent 循环本身,全是可替换的插件。
一句话定位:如果你想自己造一个 Cursor / Claude Code / Codex 这样的 Agent 产品,dsh 给你造好了底盘。
二、它解决的痛点
痛点 1:Agent 框架的"核心不可改"
大多数 Agent 框架(LangChain、AutoGen、CrewAI)都有一个 privileged core。你想改 Agent 循环的行为,要么 fork 整个框架,要么继承一个 base class 然后祈祷上游别改。dsh 的做法是:没有核心 。Agent 循环本身就是一个插件(core/agent-loop),你挂一个自己的 Agent 循环插件就能替换它,其他插件不动。
这个设计来自 Cordis 框架。Cordis 源自 Koishi(一个聊天机器人框架)的生态,设计哲学是"插件贡献服务、类型化事件、可逆效应到共享上下文"。dsh 把这套搬到了 Agent 领域。
痛点 2:工具策略作为后补
很多框架的工具执行就是"调一个函数"。dsh 的工具执行管道有三层 waterfall 事件 + 一层 monotonic guard:
tools/pre-executewaterfall:hooks、permission、sandbox 拦截- Monotonic guards:deny 或 abstain,identity protected(不可被其他插件覆盖)
tools/executewaterfall:timeout、retry、metrics 包裹 dispatchtools/post-executewaterfall:accept、block、replace、add context
这还没完。工具执行完后还有 finalizeContent(同步 content-only invariant)和 tools/result(frozen authoritative outcome)。一个工具调用从头到尾经过 6 道关。
痛点 3:会话状态不一致
Agent 跑久了,内存状态、日志、UI 显示、持久化副本会漂移。dsh 的解法是:Session Log 是唯一真相源。所有模型可见的东西都必须从日志可重建,运行时 invariant 会断言这一点。Fork、resume、transcripts、telemetry、persistence 全从这条流派生。
这条铁律的代价是:你想给模型塞一个新的可见输入,必须先发明一个 session event。不能直接拼字符串。
痛点 4:Subagent 实现五花八门
你想让 Agent 调 Agent,可能是在同进程里跑一个子 Agent,可能是 fork 当前会话,可能是调 Codex CLI,可能是调 Claude Code,可能是走 ACP 协议。dsh 把这些全抽象成 subagent provider 接口,一个接口五种实现:
subagent-in-process-driver:同进程子 Agentsubagent-spawn-in-process:进程内 spawnsubagent-fork-in-process:fork 当前会话subagent-codex:委托给 OpenAI Codexsubagent-claude-code:委托给 Claude Codesubagent-acp:走 Agent Communication Protocol
加上 subagent-dsh-sdk(子 Agent 自己跑一个完整 dsh 实例),共 7 种 subagent 模式。每种都有完整的 continuation、inheritance、settlement、depth control 测试。
痛点 5:沙箱不可移植
Linux 有 Landlock、Mac 有 Seatbelt、Windows 有 ACL、云上有 E2B。dsh 把沙箱也抽象成 ctx.sandbox 服务,一个 provider 切换就移动了 Bash、PTY、LSP 的执行世界,不用 fork 任何工具。原生 Landlock 实现甚至写了一个 C 扩展(native/landlock-run/packages/linux-x64/),不是简单调 syscall。
三、架构总览
Cordis 框架的五个核心概念
要看懂 dsh,先得看懂 Cordis。五个概念:
- Plugin = Service 实现 :一个插件可以是带
inject和apply(ctx)的函数,也可以是Service子类。Cordis 把它的生命周期挂载到当前 context。 - Context = Service 仓库 :服务从 context 里用稳定 key 认领(
ctx.tools、ctx.llm、ctx.sessions),其他插件通过 key 找服务而不是 import 具体实现。 inject声明依赖:插件说自己需要哪些服务,Cordis 等这些服务都到位了才挂载它。启动顺序靠依赖声明而不是手动排序。- 类型化事件 :服务通过 TypeScript declaration merging 声明事件名,然后按
emit/waterfall/parallel/serial四种模式分发。分发模式是事件公共契约的一部分。 - 注册是可逆效应 :prompt section、tool schema、adapter、provider、listener 全通过
ctx.effect()或ctx.on()安装,reload 和 teardown 时按注册逆序自动撤销。
Profile 和 Bundle 的分层配置
一个运行中的 dsh 是启动时从有序层级组合出来的插件树。
- Profile :存在 Harness home 里的命名组合,列出它堆叠的 bundle,存放 out-of-tree 插件,保存用户的
cordis.patch.yml。web和headless是官方模板。 - Bundle :Cordis config row 和它们挂载的代码的分发格式。每个 bundle 在自己的
package.json里用dsh字段声明:dsh.profile列出 profile 的 bundle,dsh.bundle指向 bundle 的 patch 文件。
层级应用顺序:profile 列出的 bundle 顺序 → profile 的 cordis.patch.yml → home 级 patch → --patch overlay。Patch 通过 id 定位 row,替换整条 config 或插入新 row。
dsh --profile web --dump-config 能打印你机器实际启动的插件树。任何 row 都可以用你自己的 patch 替换。
Turn Flow(回合流)
这是 dsh 的心脏。一个 step 是一次模型请求加它调用的工具。一个 turn 是零或多个 step:它在第一个 input 被 claim 之前打开,在什么都不欠的时候关闭。
bash
turn/start
claim next-step input + 一条排队消息
组装 prompt section + tool schema
-> agent/pre-step reject | enter(messages)
reject,或第一次 enter 被重写为空 -> 关闭 turn 不产生 step
step/start
把 entered messages 追加为 user/message
从日志派生模型历史
agent/request -> llm/stream -> assistant/chunk* -> assistant/message
tool/call* -> tools/pre-execute -> tools/execute -> tools/post-execute -> tool/result*
step/end
工具还欠一次请求,或 next-step input 到了 -> claim -> 下一个 step
-> agent/turn-stopping
turn/end
turn/*、step/*、user/message、assistant/*、tool/* 是持久化 session event;其余是跨三个域的 live extension point。agent/pre-step、agent/request、llm/stream、三个 tools/* 事件是 waterfall(listener 必须 call next() 委托);agent/turn-stopping 是 serial 且没有 next()。
六个核心包
| 包 | 职责 | ctx key |
|---|---|---|
core/session |
append-only 的 SessionEvent 日志和内存存储 |
ctx.sessions |
core/system-prompt |
prompt section 和 tool schema 组装 | ctx.systemPrompt |
core/tools |
有 scope 的工具注册表和受保护的执行管道 | ctx.tools |
core/agent |
Agent 接口、live registry、agent/* 事件 |
ctx.agents |
core/agent-loop |
实现该接口的默认 driver | ctx.agentLoop |
core/scope |
每 agent 的 scoped-registration 原语 | library,无 key |
llm/llm |
message 和 stream 词汇 + adapter seam | ctx.llm |
Capability Seam(能力接缝)
一个 seam 是一个可替换的能力,三个角色:Service Definition 声明接口、Service Provider 实现、Consumer 使用(通常是 model-facing tool)。一个包可能身兼多职,但只一个角色不算 seam。
Seam 是为什么"一个 provider 切换就改变整个产品"。filesystem 和 subprocess provider 共享一个执行世界,所以指向远程沙箱就把 Bash、PTY、LSP 全搬过去,没有 provider fork。Subagent provider 也是同样道理,背后从 fresh child agent 到委托另一个产品,差异全藏在接口后面。
四、几个有意思的设计
1. 事件分发模式写进公共契约
Cordis 的四种分发模式(emit / waterfall / parallel / serial)不是实现细节,是事件的公共契约。新事件必须用 @mode tag 声明分发模式,生成的 catalog 会检查声明和调用点是否一致。
这比"看着文档写 listener"靠谱。声明 waterfall 的事件,listener 就知道必须 call next();声明 serial 的就知道有返回值且按顺序。类型系统替你检查。
2. Monotonic Guard 不受插件顺序影响
工具执行的 monotonic guard 是一个特别的设计。普通 waterfall listener 的执行顺序受注册顺序影响,但 monotonic guard 的"deny"决策不可被后续 listener 覆盖。Identity protected。
这意味着安全策略可以独立于业务逻辑插入,不用担心某个后注册的插件把你的 deny 给 next() 掉了。这比 Odysseus 的 tool_security.py 靠"先检查后执行"的注释约束强得多。
3. Session Log 的"model-visible means logged"铁律
dsh 在 core/session 里有一个 runtime invariant:任何到达模型请求的东西必须能从日志重建。如果你给模型塞了一个新的可见输入但没发明对应的 session event,invariant 会断言失败。
这逼着开发者把所有模型可见的状态变更都做成显式的、可重放的、可序列化的事件。代价是写新功能时要先扩展 SessionEventMap。收益是 Fork、resume、transcripts、telemetry、persistence 全自动从同一条流派生,不可能漂移。
4. 7 种 Subagent Provider 背后的一致接口
subagent 的差异有多大?同进程跑一个子 Agent 和调 Codex CLI 是完全不同的事。但 dsh 把它们统一到 ctx.agents 接口后面:
- 父 Agent 通过
tool-subagent工具发起委托 subagent provider决定委托怎么落地subagent-in-process-driver在同进程挂载子 Agentsubagent-codex通过 Codex CLI 协议委托subagent-claude-code通过 Claude Code 协议委托subagent-acp走 ACP 标准协议subagent-dsh-sdk让子 Agent 自己跑一个完整 dsh
每种 provider 都有 continuation(继续跑)、inheritance(继承父 Agent 的能力集)、settlement(结算结果回传)、depth control(限制嵌套深度)的测试。subagent-multi、subagent-parallel、subagent-mixed 三个测试 snapshot 证明这些 provider 可以混用。
5. 防御性编程模式文档化
docs/defensive-patterns.md 是一份"硬仗来的 bug 类规则"文档。每条规则都是一个真实发生过或差点发生的 defect class,写成防止复现的规则。举几条:
"报告正交结果要独立" :一个进程可能既 timeout 又 exit 0(因为它 trap 了信号)。每个独立事实(timedOut、signal、exitCode)要各自上报,不能把一个 flag 嵌进另一个的分支。否则调用方读到截断的 run 当成干净成功。
"异步状态不是同步状态" :agent.followup() 没有每消息完成或结果;background job 的完成和 turn boundary 竞态;reader.close() 既可能是 EOF 也可能是 disposal。不要把 agent/status 或 whenIdle() 当成一次 follow-up 的结果。如果自动化调用方真的拥有一个 run,必须显式定义自己的 interval(从它的消息 durable inbox receipt 到下一个 whole-agent idle)。
"Dispose 必须到达静默,不是仅请求" :teardown 发了 kill/abort 但在工作停之前返回会留孤儿。cleanup 必须 async 并 await 子进程的 exit(kill → await done),并且在 kill 之前关闭 listener/notification registry,这样迟到的 completion 会保持沉默。
"不要给 untrusted output 递环境变量或可预测路径" :spawned command 拿到的是被擦过的 env(drop *KEY*/*SECRET*/*TOKEN*/*PASSWORD*),harness 凭证不会泄漏到 output、env 或 spill 文件里。Temp/spill 文件用 0700 私有目录、随机名、exclusive owner-only open('wx'、0o600),因为可预测的 world-readable 路径会招来 symlink race 和披露。
"Unlink 链接型路径" :可能是 symlink 或 Windows junction 的路径,用 lstatSync().isSymbolicLink() 然后用 unlinkSync 删除。unlink 只删 link 拒绝真实目录,不会跟随 link 进入目标。Windows rmSync(link) 在 junction 上抛 ERR_FS_EISDIR;递归删除可能穿过 junction 进入目标。
这些不是理论规则,是从真实 bug 提炼的。docs/postmortem/ 有 4 份 postmortem 记录具体的失败案例。
6. Landlock 原生 C 扩展
Linux Landlock 是内核级文件访问控制。大多数项目要么不用,要么用纯 JS 的 binding。dsh 写了一个 C 扩展 native/landlock-run/packages/linux-x64/,还提供 arm64 prebuild。这意味着沙箱不是"最佳 effort",是内核强制。
对应 macOS 有 Seatbelt(bash-sandbox/tests/seatbelt.e2e.ts),Windows 有 ACL(pwsh-sandbox/tests/acl.e2e.ts),云上有 E2B(packages/e2b/)。四个平台四种沙箱后端,全走 ctx.sandbox 接口。
7. Typert:类型安全的插件协议生成器
packages/typert/generator/ 是一个 codegen 工具。它分析 Cordis 服务的 TypeScript 类型,生成跨进程的类型定义。这意味着你写一个 Cordis 插件,它的服务接口可以自动暴露给客户端(browser、子进程、远程)使用,类型安全。
packages/typert/protocol/ 是协议定义,packages/typert/registry/ 是服务注册,packages/typert/loader/ 是加载器。这一套让 dsh 的 Web UI 可以类型安全地调用 host 侧的服务。
五、短板和风险
1. Cordis 学习曲线陡峭
Cordis 不是主流框架。它的源头是 Koishi(一个中国 QQ 机器人生态),文档主要在中文社区。dsh 虽然有英文文档和 primer,但开发者要先理解 Service、Context、inject、四种 dispatch mode、reversible effects、waterfall semantics 这些概念,才能写第一个插件。
这比 LangChain 的"import一个chain"门槛高得多。dsh 自己也意识到这个问题,提供了 7 篇 cordis-tutorial 从零教起。
2. developer preview 阶段,不稳定
README 加粗写着:"THERE WILL BE COMPATIBILITY-BREAKING CHANGES." 上线仅 1 天,API 还在剧烈迭代。现在基于 dsh 做产品,要做好跟着改的准备。
3. 代码量巨大但分散
1247 个 TS 源文件分散在 packages/ 下的几十个目录里。一个完整的 Agent 能力(比如"subagent")横跨 7 个 package,要理解全貌得读多个目录。docs/subsystems/ 有 50+ 个文档页,但初学者定位"我要的功能在哪个包"仍需时间。
4. 文档密集但门槛高
docs/ 下有 architecture、cordis-primer、cordis-tutorial(7 篇)、cordis-api(6 篇)、subsystems(50+ 篇)、cookbook(6 篇)、postmortem(4 篇)、defensive-patterns、testing、glossary。文档质量很高,但量太大,对新开发者不友好。
5. 没有内置模型
dsh 不自带 DeepSeek 模型。你要自己接 model adapter(通过 ctx.llm)。虽然 subagent-codex 和 subagent-claude-code 可以委托给 Codex/Claude Code,但如果你要用 DeepSeek 自己的模型,得自己写 adapter 或等社区贡献。
6. 前端生态复杂
Web UI 在 packages/client/ 下,用 React + Vite。有 ui-primitives(30+ 组件)、ui-cordis(Cordis 运行时可视化)、ui-settings-plugins、ui-sidebar、ui-skill、ui-subagent、ui-attachment 等十几个 UI 包。定制 UI 需要理解 Cordis 的 client-runner 架构,不是简单的 React 组件复用。
六、一句话评价
DeepSeek Harness 是近两年 Agent 框架领域架构设计最讲究的一个。它不堆功能,堆抽象。Cordis 的 Service + Event + Seam 三件套,配合 Session Log 铁律、Monotonic Guard、Subagent Provider 接口,把"造一个 Agent 产品"这件事的工程门槛拉到了新高度。
代价是学习曲线陡峭,developer preview 不稳定,文档量大但门槛高。但对于想认真造 Agent 产品的团队,这是目前最值得研究的框架。
MIT 协议是它最大的优势。