
昨天 DeepSeek 最重磅的消息并不是 DeepSeek-V4-Pro-0813 模型上线,而是 DeepSeek Harness(下文简称 dsh )的开源亮相。从官宣发布到本文发布时间为止,一天多的时间 Github 已经 83k Star,热度还在持续上升中。
DeepSeek 官方对 DeepSeek Harness 的定义如下:
DeepSeek Harness(dsh)是由 DeepSeek AI 开发的开源 agent harness(智能体框架)。
它采用一切皆插件的架构,并由 Cordis 驱动,其设计参见论文 A Programming Paradigm for Spatiotemporal Composability。
如果说 Claude Code、Codex是一辆整车,那么 dsh 就相当于提供了"底盘+发动机+变速箱+一套组装工具",用户完全可以用它自己去造一辆车。
对于正在研究 Agent Harness 的我而言,dsh 的开源无异于雪中送炭,第一时间我就开始学习 dsh 的源码。
1 零件清单:dsh 的包目录到底有什么
| 造车类比 | dsh 组件 | 对应包 |
|---|---|---|
| 发动机 | LLM 适配层 | packages/llm/ |
| 变速箱 | Agent 循环驱动 | packages/core/agent-loop/ |
| 底盘 | Cordis 插件框架 | vendor/cordis/ |
| 方向盘 | 工具注册表 + 执行流水线 | packages/core/tools/ |
| 仪表盘 | Web UI | apps/web/(React + Vite)+ packages/host/ + packages/client/ |
| 安全带+气囊 | 沙箱隔离 | packages/sandbox/ |
| 行车记录仪 | 会话日志(Session Log) | packages/core/session/ + packages/session/ |
| 导航系统 | LSP 集成 | packages/lsp/ |
| 油门刹车 | Shell/终端能力 | packages/shell/ + packages/terminal/ |
| 后视镜 | 子代理委派 | packages/subagent/ |
| 改装接口 | 自修改引擎 | packages/extensions/ |
| 行车电脑 | Plan 模式 + 工作流 | packages/plan/ + packages/workflow/ |
•
每个包都是独立可替换的零件
•
用户不需要全部用上,按需组装
•
所有零件通过 Cordis 的 inject 声明依赖,自动接线
•
npm scope 统一为 @deepseek-ai/dsh-*
2 组装手册:Profile + Bundle 怎么拼
这是"造车"的核心环节--选零件、组装、上路。
2.1 两个核心概念
•
Bundle = 一套预选好的零件包
•
dsh-base:所有车都要有的基础件(模型适配器、工具、持久化、沙箱、审批策略、设置、凭证、遥测)
•
dsh-headless:无人驾驶模式(一次性任务,无 server)
•
dsh-web-app:带仪表盘的完整车(浏览器应用)
•
Profile = 你的车型配置单
•
存放在 $DSH_HOME/profiles/<name>/
•
列出要用哪些 Bundle,按什么顺序叠
•
可以打补丁覆盖任意配置
2.2 组装顺序
空根(光秃秃的底盘)
-> bundle: dsh-base (装上发动机、接线)
-> bundle: dsh-web-app (装上仪表盘、座椅)
-> profile patch (你定的颜色、配置)
-> home patch (你家车库的默认设置)
-> --patch(命令行) (临时改装,试驾用)
2.3 三步造车
# 第一步:用默认配置跑起来,看看啥样
npx @deepseek-ai/dsh web
# 第二步:创建你自己的 Profile
dsh --profile my-agent # 首次使用自动创建
# 第三步:往你的车上加零件
dsh plugin --profile my-agent add @my-org/custom-tools
打开 packages/bundle/base/cordis.patch.yml,看看 dsh-base 这个"基础件包"到底装了什么:
- insert:
- id: llm
name: '@deepseek-ai/dsh-llm'
- id: session
name: '@deepseek-ai/dsh-session'
- id: agent
name: '@deepseek-ai/dsh-agent'
- id: agent-default-model
name: '@deepseek-ai/dsh-agent-default-model'
config:
provider: deepseek-official
model: deepseek-v4-flash
- id: sandbox
name: '@deepseek-ai/dsh-sandbox-local'
- id: tool-bash
name: '@deepseek-ai/dsh-tool-bash'
disabled: !!js process.platform === 'win32'
- id: tool-fs
name: '@deepseek-ai/dsh-tool-fs'
- id: agent-loop
name: '@deepseek-ai/dsh-agent-loop'
config:
agents: []
每一行就是一个插件条目:id 是你在 patch 里引用它的名字,name 是 npm 包名,config 是传给插件的配置。!!js 表示内联 JavaScript 表达式--比如 tool-bash 在 Windows 上自动禁用。
想换模型?把 agent-default-model 的 config.model 改掉。想加搜索?插一行 tool-web。想换沙箱后端?把 sandbox 的 name 换成远程沙箱的实现包。改配置,不改代码。
3 为什么 Cordis 是底盘
官方文档对 Cordis 的描述是:
Cordis 是 dsh 底层的框架:插件向共享上下文贡献服务、类型化事件和可逆的副作用。产品的每一部分都是插件,包括模型适配器、工具注册表、会话日志,以及 agent loop(智能体循环)本身,因此每一部分都可以从配置替换。
不存在需要打补丁的特权内核:扩展 dsh 的方式是把插件挂载到其他插件旁边,而各项注册都是副作用,会在其插件卸载时撤销。
正如官网 Slogan 所说:"一切皆插件",从插件目录可以看出,整个 dsh 基本就是基于 Cordis 用插件实现的。

通过分析代码,我总结如下:
插件 = Service 对象:每个插件提供一个服务,通过 inject 说"我需要谁"
Context = 服务仓库:所有服务挂在 ctx 上,ctx.fs 就是文件系统,ctx.llm 就是模型
自动接线:你不用管先装发动机还是先装变速箱,Cordis 自动解析依赖
类型化事件通信:四种模式(emit 广播 / waterfall 瀑布 / parallel 并行 / serial 串行)
可逆注册:插上就通电,拔了就断电,自动清理,不留垃圾
同时,通过三个角色的分离,实现了换"零件"不用改"线路":
| 角色 | 职责 | 例子 |
|---|---|---|
| Service Definition | 接口声明 | ctx.fs 能力定义 |
| Service Provider | 具体实现 | 本地 FS / 远程 Sandbox FS |
| Consumer | 使用方 | Bash 工具、LSP 工具 |
比如把 ctx.fs 从本地文件系统换成远程沙箱, Bash、PTY、LSP 工具全部自动指向远程。就像用户想把轮胎从公路胎换成雪地胎,ABS、ESP、仪表盘全部自动适配。
并不是空口无凭,直接看代码:
Service Definition (packages/fs/fs/src/index.ts)------定义接口,不写实现:
declare module '@deepseek-ai/cordis' {
interface Context {
fs: FileSystem
}
}
export abstract class FileSystem extends Service {
constructor(ctx: Context) {
super(ctx, 'fs')
}
abstract resolve(path: string, opts?: { cwd?: string }): Promise<FsTarget>
abstract readText(target: FsTarget, signal?: AbortSignal): Promise<string>
abstract writeText(target: FsTarget, content: string, expected?: FsWriteIntent): Promise<FsWriteOutcome>
abstract editText(target: FsTarget, edit: FsEditRequest, expected?: { version: FsVersion }): Promise<FsEditOutcome>
// ...
}
通过 declare module 把 FileSystem 注册到 ctx.fs,是个抽象类------只声明方法签名,不写实现。
Service Provider (packages/fs/fs-local/src/index.ts)------本地文件系统实现:
export class LocalFileSystem extends FileSystem {
static Config = z.object({
cwd: z.string().default(process.cwd()),
})
override async writeText(
target: FsTarget,
content: string,
expected?: FsWriteIntent,
): Promise<FsWriteOutcome> {
return this.withLock(target.targetKey, async () => {
const existing = await probe(target.targetKey)
if (expected?.kind === 'replaceIfVersion') {
if (existing.version !== expected.version) {
throw new FsError(`file changed since it was read`, 'FS_STALE_VERSION')
}
}
await writeFileAtomic(target.targetKey, content, existing?.mode)
return { operation: existing ? 'update' : 'create', version: ... }
})
}
}
LocalFileSystem 继承 FileSystem,用 Node.js 原生 fs API 实现每个方法。注意 withLock 保证了 read-guard-write 的原子性,expected.version 是乐观锁防陈旧写入。想换成远程沙箱实现?写一个 RemoteSandboxFileSystem extends FileSystem 就行。
Consumer (packages/fs/tool-fs/src/index.ts)------模型面向的工具:
export const inject = ['tools', 'fs', 'systemPrompt']
export function apply(ctx: Context, config: Config): void {
applyReadTool(ctx, { limit: config.readLimit, ... })
applyWriteTool(ctx, sandbox)
applyEditTool(ctx, sandbox)
}
工具包通过 inject = ['tools', 'fs', 'systemPrompt'] 声明"我需要这三个服务",然后注册 read/write/edit 工具。它不直接操作文件,而是调用 ctx.fs--至于 ctx.fs 背后是本地还是远程,它不关心。
4 上路试驾:Agent 循环的完整流程
车造好了,是时候点火跑一圈看看,dsh 的流程如下:
turn/start(点火)
claim next-step input + one queued message(踩离合挂挡)
-> agent/pre-step reject | enter
reject, or first enter rewritten empty -> close the turn with no step
step/start
assemble prompt sections + tool schemas(组合油门信号)
append entered messages as user/message
derive model history from the log(看后视镜,回顾路况)
agent/request -> llm/stream -> assistant/chunk* -> assistant/message(发动机输出动力)
tool/call* -> tools/pre-execute -> tools/execute -> tools/post-execute -> tool/result*(变速箱换挡)
step/end
tools owe another request, or next-step input arrived -> claim -> next step
-> agent/turn-stopping(到站刹车)
turn/end(熄火)
看源码更直观。packages/core/agent-loop/src/agent.ts 里的 turn() 方法,就是上面那个流程的代码实现:
private async turn(): Promise<boolean> {
const { signal } = phase.abort
const turn = phase.turn + 1
this.session.append('turn/start', { turn })
while (true) {
signal.throwIfAborted()
const step = phase.step + 1
// 1. pre-step:claim inbox 消息,决定是否进入
const decision = await this.preStep(target, { turn, step })
if (decision.kind === 'reject') {
turnEnds = { kind: 'blocked' }
return false
}
// 2. step/start -> 组装 prompt -> 调 LLM -> 执行工具 -> step/end
this.session.append('step/start', { turn, step })
for (const message of decision.messages) {
this.session.append('user/message', message)
}
const stepEnd = await this.step(decision.assembly)
this.session.append('step/end', { turn, step })
// 3. 检查是否该结束 turn
if (turnEnds && this.inbox.nextStep.length === 0) {
await this.dispatch.serial('agent/turn-stopping', { turn, signal })
}
if (turnEnds && this.inbox.nextStep.length === 0) break
target = 'next-step'
}
this.session.append('turn/end', { turn, reason: turnEnds })
return true
}
整个 turn 就是一个 while (true) 循环:pre-step 拿消息 -> step 执行 -> 检查结束条件。每一步都往 this.session 里 append 日志,这就是 Session Log 作为"唯一真相源"的来历。
step() 方法内部长这样:
private async step(assembly: PromptAssembly): Promise<StepEndReason | null> {
const system = renderPrompt(assembly)
while (true) {
// 从 session log 投影出模型历史
const { request } = await this.buildRequest(
turn, step, assembly.tools, system,
this.session.deriveMessages(), // <-- 关键:从日志重建
signal,
)
// 流式调 LLM
const stream = this.loopCtx.llm.stream(request)
for await (const chunk of stream) {
this.session.append('assistant/chunk', { turn, step, chunk })
assembler.push(chunk)
}
// 处理工具调用
const message = createAssistantMessage({ content: assembler.blocks() })
// ... tool/call -> execute -> result
}
}
this.session.deriveMessages() 是关键------模型看到的对话历史不是内存里维护的,而是每次都从 Session Log 重新投影出来的。这意味着你可以随时 fork 一个 session,换个模型继续跑,历史完全可以重建。
值得注意的几个设计:
•
日志是唯一真相源,模型看到的任何东西都必须从 Session Log 重建,deriveMessages() 从日志投影出模型历史。
•
三种插队方式:
•
followup():下一轮再说(红灯排队)
•
steer():立即插队(紧急变道)
•
inject():悄悄塞进去但不唤醒(副驾递水)
•
并行换挡:默认最多 10 个工具并行执行,maxParallelToolCalls 可配,设 1 则串行
•
Waterfall 事件:agent/pre-step、agent/request、llm/stream、tools/* 为瀑布流,listener 必须调用 next() 委托;agent/turn-stopping 为 serial,无 next()
5 改装车间:自修改能力------Agent 能改自己的车
这个是我目前看到的最疯狂的设计:已经上路在开了,车子能自己换零件。
当 Agent 发现需要搜索能力的时候,自己挂载 web 搜索插件;
当 Agent 发现某个工具在浪费 token,自己卸载它;
当 Agent 遇到新文件类型时,自己挂载对应的 LSP 。
这个逻辑并不是预先加载所有零件给 Agent 挑选,也不是"配置文件热更新",是 Agent 在运行过程中的自我重构,到目前为止,这个是大多数 Agent 框架想都没想过的方向。
packages/extensions/ 提供 4 个子包:
•
tool-cordis/:模型面向的运行时检查工具
•
cordis-host-runner/:host 端动态包执行
•
cordis-client-runner/:browser 端动态包执行
•
ui-cordis/:浏览器界面
看 packages/extensions/tool-cordis/src/index.ts 的源码,Agent 能调用 6 个工具:
export const inject = ['tools', 'systemPrompt', 'dynamicCordisRunner', 'cordisInspect']
export function apply(ctx: Context): void {
// 1. 自省:查看当前运行时里有哪些插件和服务
ctx.tools.register(defineTool({
name: 'cordis_inspect_list',
description: 'List every Cordis Inspect Provider currently known to the Host...',
parameters: {},
execute() {
return Promise.resolve({ providers: ctx.cordisInspect.list() })
},
}))
// 2. 自省:查看某个插件的详细信息
ctx.tools.register(defineTool({
name: 'cordis_inspect_self',
description: 'Inspect dynamic Cordis objects owned by the current Session...',
parameters: { pluginId: { type: 'string' }, packageId: { type: 'string' } },
execute(args, exec) {
// 返回插件 / 包的详细信息
},
}))
// 3. 造零件:定义一个新的动态插件
ctx.tools.register(defineTool({
name: 'cordis_define',
description: 'Define an immutable Cordis Package...',
parameters: {
name: { type: 'string', required: true },
purpose: { type: 'string', required: true },
code: { type: 'object', properties: { host: { type: 'string' }, client: { type: 'string' } } },
},
execute(args, exec) {
const receipt = ctx.dynamicCordisRunner.define({
sessionId: requireAgent(exec).id,
plugin: args.plugin,
name: args.name,
purpose: args.purpose,
code: { ... },
})
return Promise.resolve({ pluginId: String(receipt.pluginId), packageId: String(receipt.packageId) })
},
}))
// 4. 装上去:激活一个动态插件
ctx.tools.register(defineTool({
name: 'cordis_run',
description: 'Activate one exact Package of a dynamic Plugin...',
parameters: { pluginId: { type: 'string', required: true }, packageId: { type: 'string', required: true } },
async execute(args, exec) {
await ctx.dynamicCordisRunner.run(
requireAgent(exec),
CordisDynamicPluginId(args.pluginId),
CordisDynamicPackageId(args.packageId),
args.mode, exec.signal,
)
},
}))
// 5. 拆下来:停止一个动态插件
ctx.tools.register(defineTool({
name: 'cordis_stop',
description: 'Stop the current Run of a dynamic Plugin...',
parameters: { pluginId: { type: 'string', required: true } },
async execute(args, exec) {
await ctx.dynamicCordisRunner.stop(requireAgent(exec), CordisDynamicPluginId(args.pluginId))
},
}))
// 6. 扔掉:永久删除一个动态插件
ctx.tools.register(defineTool({
name: 'cordis_undefine',
description: 'Permanently remove a dynamic Plugin owned by the current Session...',
parameters: { pluginId: { type: 'string', required: true } },
async execute(args, exec) {
await ctx.dynamicCordisRunner.undefine(requireAgent(exec), CordisDynamicPluginId(args.pluginId))
},
}))
}
6 个工具,对应"看、造、装、拆、扔"五个动作。Agent 先用 cordis_inspect_* 看清楚自己当前有哪些零件,再用 cordis_define 写一段 JS 代码定义新插件,用 cordis_run 挂载上去,不需要了用 cordis_stop 停掉,最后 cordis_undefine 彻底删除。
最后
目前 dsh 的版本号是 0.1.0-rc.5,还处在开发者预览阶段,官方也明确提示:"未来将出现破坏兼容性的变更"。现在就拿来"造车"可能还稍微早了点,但是这个框架本身的很多思路和设计确实是很值得学习和借鉴的。