📘 原文信息
- 项目名:DeepSeek Harness(命令名
dsh)- 副标题:An open-source agent harness developed by DeepSeek AI
- 仓库:https://github.com/deepseek-ai/deepseek-harness ↗
- 文档:https://github.com/deepseek-ai/deepseek-harness/tree/main/docs ↗
- License:MIT
- 总规模:219 个工作区包,46 万行 TypeScript(不含
node_modules、lib/);核心 6 包合计约 4.3 万行- 状态:Developer Preview,会持续做破坏性变更
- 底层框架:Cordis ↗——一个 vendored 的 TypeScript 插件框架
一、为什么这个项目值得研究#
2026 年做 Agent 框架的厂商很多,但 DeepSeek Harness 走了一条不一样的路:它不把 Agent 当作一个写死的循环,而是把构成 Agent 的每一块能力——会话存储、提示词组装、工具注册表、模型适配器、Agent 循环本身——全部做成可挂载、可拆卸、可替换的插件。这套架构背后的设计文档叫 A Programming Paradigm for Spatiotemporal Composability ↗,它在描述 Cordis 时提出了一个有点哲学的命题:任何一个能力都应该可以被”加载 → 观察/拦截 → 卸载”,而卸载要回滚全部副作用。
更值得注意的是它的工程纪律。仓库根目录的 AGENTS.md 直接写着:
With no external consumers, prefer the correct foundation over compatibility shims: rename or repackage freely and update every reference together.
也就是说优先做对的基础,而不是堆兼容垫片。这种”先正确、再稳定”的开发哲学,本身就值得学习。
下面我按”架构总览 → 启动模型 → 核心数据流 → 关键子系统 → 扩展点 → 实操”六层带你拆解它。
二、仓库布局:三层 + 一条 Vendored 链#
deepseek-harness/
├─ vendor/ # vendored Cordis 源码(不是 npm 依赖,直接链入)
├─ packages/ # 219 个 @deepseek-ai/dsh-* 工作区包
│ ├─ core/ # 产品 API 主干:session/system-prompt/tools/agent/agent-loop
│ ├─ llm/ # LLM 能力接缝 + DeepSeek provider
│ ├─ bundle/ # 可安装的 dsh --profile 补丁层(base / web-app / headless)
│ ├─ shell/ fs/ lsp/ skill/ web/... # 一个个能力接缝,每个都是 Service Definition + Provider + Consumer
│ ├─ api/ typert/ # 远程 BFF + 类型图生成器
│ ├─ boot/ sdk/ examples/ support/ util/
│ └─ session/ interaction/ subagent/ workflow/ ...
├─ apps/ # 产品组装:cli、web
├─ examples/ # 可运行的 cordis.yml 叶节点
├─ python/ # Python SDK + 打包运行时
├─ native/ # Landlock 启动器(沙箱)
└─ website/ # VitePress 文档站plaintext几个关键观察:
packages/不是按业务领域切的,而是按能力接缝切的。shell/、fs/、lsp/、subagent/、workflow/这种目录的命名,都在表达”我是一个可替换的能力”。vendor/直接链入 Cordis。pnpm-workspace.yaml里用link:vendor/cosmokit、link:vendor/schemastery把它当本地源用,这样既能跟随上游节奏,又能随仓改。.agents/notes/里有大量 Agent Notes(架构决定、bug 修复、归档决策),这是一个把”团队工程知识沉淀在仓库里”的实践,可以借鉴。
三、Cordis:5 个核心概念#
Harness 几乎所有架构语义都建立在 Cordis 上,理解 Cordis 是理解 dsh 的前提。仓库的 docs/cordis-primer.md 给出了”五个想法”:
- 插件 = 实现
Service的对象。可以是带可选inject和apply(ctx)的函数,也可以是Service子类。 - Context = 服务的注册表。一个插件在某个 context 上声明
ctx.tools、ctx.llm、ctx.sessions等 key,别人按 key 找。 inject声明依赖。Cordis 会等服务实际存在后再激活插件,加载顺序由依赖关系决定,而不是手写 boot 序列。- 类型化事件(typed events)。通过 TS declaration merging 声明,分发模式有四种:
emit(fire-and-forget)、waterfall(中间件链)、parallel(并发 fan-out)、serial(有序串行)。事件本身是契约,文档里会用@mode标签标注。 - 注册 = 可逆的副作用。
ctx.effect()、ctx.on()注册的东西都会返回 disposer,卸载时按逆序回滚。
这里有一个非常容易被忽视的细节:waterfall 是”中间件式”分发——监听器收到 (...args, next),next() 把控制权交给下一个,return 不调用 next() 就是短路。这跟 Express、Koa 长得像,实质上是把”修改请求 / 决定 / 上下文”作为核心动作,再委托。这条性质让”策略可以拦截、可以放行、可以替换”的语义变得自然。
四、Profile + Bundle:启动时如何把插件树组装起来#
一个跑起来的 dsh 不是”一个进程 + 一堆 import”,而是按顺序叠加的插件树。这棵树有三层概念:
- Profile:存在 Harness home 里的命名组合,列出它要叠加哪些 bundle,放自己的
cordis.patch.yml。 - Bundle:Cordis 配置行 + 它挂载代码的”发行包”,被它插入的行仍然可以被上一层 patch。
dsh-base、dsh-web-app、dsh-headless:三个出厂 bundle。dsh-base是每个 profile 的第一层(模型适配器、工具、持久化、沙箱/审批、设置、凭据、遥测),dsh-web-app加浏览器前端,dsh-headless加一次性 runner、没有 server。
叠加顺序是固定且关键的:
空列表 → profile 里列的 bundle(按顺序)→ profile 的
cordis.patch.yml→ home 的cordis.patch.yml→--patch命令行覆盖。
每一条 patch 都按 id 精准替换某一行的配置,或者插入新行。可以一眼看出当前启动的插件树:
dsh --profile web --dump-configsh这非常像”Linux 的 initramfs 按层叠加 + overlayfs”——底层是默认配置,上层是补丁,所有行为都是可追溯的。这种”配置可解释 + 可叠加 + 可回放”的设计,是它在 DevOps 友好度上的核心优势。
五、Turn / Step:Agent 的状态机#
Agent 循环是 dsh 最核心的设计。我把 docs/architecture.md 里的状态机画成下面这张表:
| 阶段 | 触发事件 | 关键动作 |
|---|---|---|
| Turn 开启 | turn/start | 领取 next-step 输入 + 一条 next-turn 消息 |
| Pre-step | agent/pre-step | 决定模型能看到什么;可改写或拒绝 |
| Step 开启 | step/start | 把进入的消息记为 user/message,从日志派生模型历史 |
| 模型请求 | agent/request → llm/stream → assistant/chunk* → assistant/message | 流式生成 |
| 工具调用 | tool/call* → tools/pre-execute → tools/execute → tools/post-execute → tool/result* | 工具三段拦截 |
| Step 结束 | step/end | 判断是否还要再来一个 step |
| Turn 收尾 | agent/turn-stopping → turn/end | 关闭 turn |
几个非常重要的不变量:
- Pre-step 是 waterfall(中间件式):监听器可以改写、可以拒绝。被拒绝或空消息也会关闭一个没有任何 step 的 turn,日志会如实记录这次尝试。这一点对调试和审计非常关键。
- 每一步都从日志派生历史。
assistant/chunk原始事件也保留,所以能重放、能完整渲染 UI。“模型看得到的东西,必须能从日志重建” 是这个项目的硬约束——任何新的模型可见输入,都必须先扩展SessionEventMap,然后从日志渲染。 - 三个工具事件是 waterfall,所以可以做权限拦截、可以做改写、可以做包装。
agent/turn-stopping是 serial 且没有next(),它的语义就是”停下来,不要再继续”。
prepend: true 这个参数仅在你必须排在普通注册之前时才用——默认不要 prepend,这是 Cordis 的实用规则。
六、Agent Handle:对外暴露的统一接口#
Harness 的代码组织里有一个非常清晰的边界设计:Agent 是接口,agent-loop 是默认实现。所有 UI、Hook、编排器都只对 Agent 接口编程,从不直接 import agent-loop。
interface Agent {
readonly id: SessionId
readonly options: AgentOptions // provider / model / maxTokens
readonly session: Session // 实时 session,日志是真相之源
readonly inbox: Inbox // Agent 拥有的待办持久化投影
readonly status: AgentStatus // 'idle' | 'running'
readonly ctx: Context // Agent 自己的作用域
cancel(cause: AgentCancelCause, options?: CancelOptions): void
whenIdle(): Promise<void>
runMaintenance<T>(task: (signal) => Promise<T>): Promise<T>
send(message: UserMessage, target: InboxTarget, wakeup: boolean): void
followup(message: UserMessage): void // = send(next-turn, true)
steer(message: UserMessage): void // = send(next-step, true)
inject(message: UserMessage): void // = send(next-step, false) 不唤醒
}ts关键设计:
- Inbox 是双队列:
next-turn(普通的后续 turn)和next-step(本 step 内立刻插入)。所有插入、替换、清除都会产生持久的agent/inbox/spliced事件,整套协议是事件源化的。 followup/steer/inject是send的固定预设,而不是各自独立的入口。这是用同一套语义覆盖三种典型用法的简化手段。runMaintenance必须在 idle 阶段调用,会同步启动并保留 signal;它的承诺是”非 turn 维护任务跑完再 idle”。whenIdle()观察的是”整个 agent”,而不是单个 turn——它会跟随”在观察到的 driver 退休前启动的替换工作”。这个语义对上层编排非常重要:如果你要把”收到 idle”当作一次 run 结束,需要显式拥有这个区间。
七、Session 日志:整套设计的真相之源#
Session 日志是 dsh 的”事件源数据库”。所有模型可见的事实都先写日志,UI、投影、fork、resume、遥测都从这条流派生。它有几个关键能力:
- append-only:只能追加,不能改写历史。
- 多格式持久化:支持 JSONL、SQLite(SCHEMA_VERSION 单调递增)。
SESSION_FORMAT_VERSION = 0,没有兼容性承诺——开发期可以自由破坏。- Fork / Resume 都是从日志切片派生;
ctx.sessions.fork(source, boundary?, childSessionId?)是公开 API。 deriveMessages()是投影:从日志投影出模型历史;assistant/chunk原事件保留,保证可重放、UI 完整。
这条不变量用一句话表达就是:模型能看到的东西 = 日志能重建的东西。它不是口号,而是有专门的运行时断言(packages/core/session/src/invariant.ts)在守护。
八、能力接缝(Capability Seams):可替换的”形”#
Harness 把”沙箱、文件系统、子进程、LSP、子 agent、shell、终端、网页抓取、压缩、上下文、技能、计划、todo、调度、工作流……”这些能力统一抽象为 Capability Seam,每个 seam 都有三个角色:
- Service Definition:声明接口(契约)。
- Service Provider:实现接口(
shell-local/shell-pwsh、fs-local/fs-e2b、subagent-fresh/subagent-delegated等等)。 - Consumer:使用接口,典型的是”模型可见的工具”。
光有其中任一角色都不算 seam——新增能力 = 设计三个角色。这种”角色分离”的好处:
- 换一个 provider 就等于换了整套能力。文件系统提供者和子进程提供者共享一个”执行世界”,把 fs 和 subprocess 同时指向一个远程沙箱,等于把 Bash、PTY、LSP 全搬过去,不需要 fork。
- 工具(模型可见)和能力(系统内部)分开,工具只是”薄薄的 Consumer”,把策略挂在能力层,而不是每个工具自己实现。
- 同一接口后面可以挂各种实现:本地子 agent、远程 turn、跨产品 turn。
九、扩展点全景#
docs/architecture.md 给出了一张非常完整的”我要做 X,用 Y”的对照表,我精简翻译如下:
| 目标 | 用什么 |
|---|---|
| 加一个模型 provider | 注册 adapter 到 ctx.llm |
| 给模型加新能力 | 注册到 ctx.tools,schema 自动加入 prompt 装配 |
| 给某个 session 一套不同能力 | 组合 agent preset,需要 isolate realm |
| 加 shell 执行 | 注册 ctx.shell 后端,本地版通过 ctx.subprocess spawn |
| 加持久终端 | 注册 ctx.terminals + dsh-tool-terminal |
| 加人类命令 | 注册到 ctx.commands,无需模型 turn 即可分发 |
| 加后台任务 | 注册到 ctx.jobs,用 job_* 工具收集/停止 |
| 加文件系统访问 / 策略 | 注册 ctx.fs provider 或监听 fs/* 事件 |
| 约束子进程 | 用 ctx.sandbox 后端,消费者在 spawn 前包装 argv |
| 拦截请求 / 工具 / turn | 用对应的 agent/* 或 tools/* 事件 |
| 加模型可见上下文 | agent.inject(),下次 admitted request 时进入 |
| 加 Web Chat 节点 | 注册 ConversationNodeDefinition + keyed renderer |
| 加持久 session 状态 | 扩展 SessionEventMap,从日志渲染 |
| 给 session 命名 | 注册唯一的 ctx.sessionTitle provider |
| 管理同 session 的目标 | 用 ctx.goals,通过 agent/* 持续 |
| fork live session | ctx.sessions.fork(...) |
| 把注册范围限定到一个 agent | 用该 agent 的 agent.ctx |
架构原则是:新行为挂到已有的扩展点,不要修改 loop 本身。一旦你动了 loop,这张表就要跟着更新。
十、Cordis 实用规则(从 cordis-primer.md 提炼)#
- 行为封装到插件:工具流水线 →
ctx.tools,模型流 →ctx.llm,实时 agent 协调 →ctx.agents。 - 拦截用事件;直接调用能力用 service 方法。两者不要混用语义。
- 每个注册都要有 disposer。优先
ctx.effect()或自带 disposer 的 helper。 - teardown 顺序重要时,把相关工作放在一个 effect 里,dispose 才会按预期逆序展开。
- 不要随便
prepend: true,只有”必须排在普通注册之前”才用。 ctx.waterfall适合”中间件式”拦截:合作型监听器通常改写请求对象再 delegate;完全替换结果也是合法语义。- 单决策事件的设计就是短路——策略监听器可以 return 不调
next(),纯观察型监听器必须 delegate。 - cordis.yml 里只允许
!!js,不要写!js(这是个安全的 loader 配置细节)。
十一、怎么跑起来#
11.1 直接用 npm#
npx @deepseek-ai/dsh websh默认开在 http://127.0.0.1:3080。
11.2 从源码跑#
git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install
pnpm run build
pnpm dsh websh11.3 一次性 headless runner#
DEEPSEEK_API_KEY=... pnpm dsh --profile headless "总结这个仓库并识别主要包"sh11.4 自动化协议#
仓库自带 ACP server:
pnpm run demo:acpsh可以让 IDE / 编辑器以 Agent Client Protocol 接入。
11.5 Web UI 三步上手#
文档给的步骤非常简洁:
- Settings → Models 填 DeepSeek API Key,保存后立即生效,无需重启。
- 点击 Choose workspace,把项目目录加进来;没选 workspace 之前 session composer 是灰的。
- 发任务,例如:“Summarize this repository and identify its main packages.”;模型会自动读 / 写文件、跑命令、委派、维护 plan,需要审批的操作会先弹审批。
十二、怎么写第一个插件#
docs/cookbook/extension-cookbook.md 给出了标准流程。简化版:
- 新建一个包
packages/<group>/<pkg>/,package.json里name: "@deepseek-ai/dsh-<pkg>",dsh.bundle指向 patch 文件。 - 写 cordis.yml,声明要注入的 service(
ctx.llm/ctx.tools/ctx.sessions等)。 - 写插件代码:函数式 Service,带
inject: ['llm'],在apply(ctx)里ctx.effect(() => ...)注册副作用,dispose 自动回滚。 - 通过 bundle 挂载:在
bundle/base/里加一行,或者自己写一个 profile 的cordis.patch.yml。 - 加测试:仓库强制
test:coverage是 CI 闸门(packages/*/*/src按文件 100% 覆盖率)。 - 跑
pnpm run doc-sync同步文档,跑pnpm run hygiene检查 knip / publint / workspace 约束。
每个 cookbook 都有完整模板,推荐照着 docs/cookbook/adding-a-tool.md、docs/cookbook/adding-an-llm-adapter.md、docs/cookbook/adding-a-package.md 走一遍,基本就掌握了 dsh 的扩展机制。
十三、我从这次拆解里学到的#
- “真相之源”放对地方,所有上层就自然简单。把 session 日志作为唯一真相,fork / resume / 投影 / UI / 遥测 全从它派生,不需要任何额外的同步协议。
- “事件接缝”比”调用栈接缝”更适合 Agent。Agent 的每个环节都可能需要被拦截、改写、否决,waterfall 事件比函数调用更灵活——因为它天然支持短路、改写、并发、串行四种语义。
- “配置可叠加、可 patch、可回放” 是 DevOps 友好的关键。一份
dsh --dump-config就能看到完整插件树,任何一行都能 patch,任何 patch 都能追溯。 - 接口和实现分离要硬性执行。
Agent是接口,agent-loop是默认实现,扩展插件只 importagent,绝不 importagent-loop,这条规则让 loop 永远可替换。 - 能力(seam)是组织单位,不是”业务领域”。把
shell/、fs/、lsp/、subagent/当作第一公民目录,目录里同时放 Service Definition / Provider / Consumer,这种组织方式比按业务切更利于扩展。 - Developer Preview 期就该做正确的基础。仓库明确写了”无外部消费者时优先做对的基础,而不是兼容垫片”,这种工程纪律在生产项目里会被各种保守妥协扼杀,值得在自己的项目里也尽量保持。
- 文档本身就是架构。
docs/architecture.md+docs/subsystems/*.md+docs/cookbook/*.md把”哪里能扩展、怎么扩展”写得明明白白,这种”架构即文档、文档即测试”的纪律是开源项目长期健康的关键。
十四、参考#
- 仓库 README:
README.md/README.zh.md - 架构总览:
docs/architecture.md - Cordis 五想法:
docs/cordis-primer.md - Turn / Step 状态机:
docs/architecture.md#turn-flow - Agent 接口:
docs/subsystems/core.md - Session 日志:
docs/subsystems/session.md - 工具流水线:
docs/subsystems/tools.md - LLM 流式:
docs/subsystems/llm-streaming.md - 提示词装配:
docs/subsystems/system-prompt.md - 扩展索引:
docs/cookbook/extension-cookbook.md - 包布局:
packages/README.md+ 仓库根AGENTS.md
⚠️ Developer Preview 提示:文档明确写”This project is in developer preview and iterating rapidly. THERE WILL BE COMPATIBILITY-BREAKING CHANGES.” 学习时关注设计哲学,而不要把现在的 API 当成稳定契约。