DeepSeek Harness 源码解析:DeepSeek 官方 Agent 框架的插件化架构设计

DeepSeek Harness 源码解析:DeepSeek 官方 Agent 框架的插件化架构设计

深入剖析 DeepSeek 官方开源 Agent 框架——DeepSeek Harness(dsh)的插件化架构设计、核心模块与运行机制

项目简介

DeepSeek Harnessdsh)是 DeepSeek AI 开源的 Agent 框架(agent harness)。它的核心理念是 “一切皆插件”(everything is a plugin)——模型适配器、工具注册表、会话日志、甚至 Agent 循环本身都是一个可替换的插件。整个框架基于 Cordis 插件框架构建,其设计哲学详见论文 A Programming Paradigm for Spatiotemporal Composability

与传统的”硬编码循环 + 内置工具”的 Agent 实现不同,dsh 没有特权核心:你通过在其它插件旁挂载一个插件来扩展它,而注册(registration)是可逆的 effect——插件卸载时自动回滚。

核心特性

  • 🧩 一切皆插件:模型适配器、工具、会话日志、Agent 循环均可从配置替换
  • 🔌 能力缝(Capability Seam):Service Definition / Provider / Consumer 三角色架构,一个 Provider 切换即可改变整个产品行为
  • 📜 追加式会话日志:模型可见的一切都必须可从日志重建,运行时不变式强制保证
  • 🔄 可逆注册:所有注册通过 ctx.effect() / ctx.on() 完成,卸载时自动回滚
  • 🛡️ 进程沙箱:Landlock / bubblewrap / sandbox-exec / Windows ACL 多平台原生沙箱
  • 🌐 Web UI + Headless:浏览器应用与一次性命令行运行器双模式
  • 🔧 Code Mode:模型通过 run_code 编写程序调用所有工具,而非逐个调用

技术栈

  • 语言:TypeScript(strict: true + noImplicitAny),纯 ESM
  • 运行时:Node.js ^22.19 || >=24
  • 框架:Cordis(vendored 插件框架)
  • 构建:tsc + tsdown
  • 包管理:pnpm workspaces(54 个包组、219+ 个子包)
  • 测试:vitest(单测 100% 覆盖率门控 + 快照测试 + e2e)
  • Lint:oxlint + oxlint-tsgolint

目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
deepseek-harness/
├── apps/ # 应用入口
│ ├── cli/ # dsh CLI 入口 (bin.ts → profile-boot.ts)
│ └── web/ # Web 前端应用

├── packages/ # @deepseek-ai/dsh-<pkg> 工作区(54 个包组)
│ ├── core/ # 产品 API 脊柱
│ │ ├── session/ # 追加式 SessionEvent 日志 + 内存存储
│ │ ├── system-prompt/ # Prompt 段与工具 schema 组装
│ │ ├── tools/ # 作用域工具注册表 + 守卫执行管线
│ │ ├── agent/ # Agent 接口 + 实时注册表 + agent/* 事件
│ │ ├── agent-loop/ # 默认驱动器(ReactLoopAgent)
│ │ ├── scope/ # 每 Agent 作用域注册原语
│ │ └── agent-default-model/ # 默认模型选择
│ │
│ ├── llm/ # LLM 能力
│ │ ├── llm/ # Service Definition + 流式协议
│ │ ├── llm-deepseek/ # DeepSeek 适配器
│ │ ├── llm-pi-ai/ # Pi-AI 适配器
│ │ └── token-meter/ # Token 计量
│ │
│ ├── shell/ # Shell 能力
│ │ ├── shell/ # Service Definition
│ │ ├── bash-local/ # 本地 bash 执行器
│ │ ├── bash-sandbox/ # 沙箱化 bash
│ │ ├── pwsh-local/ # PowerShell 执行器
│ │ └── tool-bash/ # 模型面向的 bash 工具
│ │
│ ├── fs/ # 文件系统能力
│ │ ├── fs/ # Service Definition + 策略
│ │ ├── fs-local/ # 本地文件系统
│ │ ├── fs-sandbox/ # 沙箱文件系统
│ │ └── tool-fs/ # 文件操作工具(read/write/edit/grep/glob)
│ │
│ ├── sandbox/ # 进程沙箱
│ │ ├── sandbox/ # Service Definition
│ │ ├── sandbox-local/ # 本地多平台沙箱 Provider
│ │ └── sandbox-policy/ # 沙箱策略
│ │
│ ├── compaction/ # 上下文压缩
│ │ ├── compaction/ # Service Definition
│ │ └── compaction-basic/ # 基础压缩引擎
│ │
│ ├── subagent/ # 子代理能力
│ ├── web/ # Web 搜索/抓取能力
│ ├── terminal/ # 持久化终端会话
│ ├── lsp/ # 语言服务器能力
│ ├── skill/ # 技能注册表
│ ├── session/ # 持久化、投影、标题、遥测
│ ├── interaction/ # 审批/交互/权限/命令
│ ├── bundle/ # 可安装的 dsh --profile 补丁层
│ │ ├── base/ # 基础层(模型/工具/持久化/沙箱/审批)
│ │ ├── web-app/ # 浏览器应用层
│ │ └── headless/ # 一次性运行器层
│ ├── boot/ # 共享启动胶水
│ ├── sdk/ # JSON-RPC 协议 + 服务端 + TS 客户端
│ ├── client/ # Web 客户端运行时/UI 组件
│ ├── host/ # Host API 代理
│ └── ... # 更多能力包

├── vendor/ # Vendored Cordis 源码
├── native/ # @deepseek-ai/node-addon-landlock-run(C 源码)
├── python/ # Python SDK
├── examples/ # 可运行的 cordis.yml 示例
├── docs/ # 架构文档、目录、事后分析
├── scripts/ # 仓库门控与生成器
└── website/ # VitePress 文档站点

这个 monorepo 的规模令人印象深刻——packages/ 下有 54 个包组、219+ 个子包,每个包都是一个独立的 @deepseek-ai/dsh-<name> npm 包,通过 Cordis 的 Service/Event 机制松耦合组装。


Cordis:插件框架基石

理解 dsh 的前提是理解 Cordis。Cordis 是 dsh 底层的 vendored 插件框架,其核心可以归纳为五个理念:

五大核心理念

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
┌─────────────────────────────────────────────────────────────┐
1. 插件是实现 Service 的对象 │
│ 函数(带 inject/apply)或 Service 子类 │
├─────────────────────────────────────────────────────────────┤
2. 上下文是服务的仓库 │
│ ctx.tools / ctx.llm / ctx.sessions │
│ 通过 key 查找,而非导入具体实现 │
├─────────────────────────────────────────────────────────────┤
3. 通过 inject 声明服务依赖 │
│ 加载顺序由服务需求表达,而非手动编排 │
├─────────────────────────────────────────────────────────────┤
4. 类型化事件通信 │
│ emit / waterfall / parallel / serial │
├─────────────────────────────────────────────────────────────┤
5. 注册是可逆的 effect │
│ ctx.effect() / ctx.on() 返回 disposer │
│ 插件卸载时自动回滚 │
└─────────────────────────────────────────────────────────────┘

四种事件派发模式

模式 是否 await 派发顺序 有返回值
emit 按注册顺序观察
waterfall 按注册顺序,around 中间件
parallel 所有监听器并行观察
serial 按注册顺序

Waterfall 语义ctx.waterfall 是 around 中间件。监听器收到 (...args, next),调用 next() 委托给下一个服务,返回不调用 next() 则短路。策略监听器可以在拥有决策权时短路返回,仅观察的监听器必须委托。

1
2
3
4
5
6
7
8
9
10
// 声明事件(TypeScript declaration merging)
declare module '@deepseek-ai/cordis' {
interface Context {
llm: LlmRuntime
}
interface Events {
// @mode waterfall
'llm/stream'(options: GenerateOptions, next: () => AsyncIterable<StreamChunk>): AsyncIterable<StreamChunk>
}
}

这种设计让 dsh 的每个部分——模型适配器、工具注册表、会话日志、Agent 循环——都是可替换的插件,不存在需要 patch 的特权核心。


Profile 与 Bundle:组合系统

一个运行中的 dsh 是一棵在启动时从有序层组合而成的插件树。

概念

  • Profile(配置文件):Harness home 中存储的命名组合,列出它堆叠的 bundle,持有树外插件和用户自己的 cordis.patch.ymlwebheadless 作为模板内置。
  • Bundle(打包层):Cordis 配置行及其挂载代码的分发格式,使其插入的内容可被上层补丁修改。
  • Patch(补丁):通过 id 定位行,替换其整个 config 或插入新行。

层叠顺序

1
2
3
4
5
6
7
8
9
10
11
┌───────────────────────────────────────────────────┐
│ 应用顺序(从下到上) │
├───────────────────────────────────────────────────┤
1. 空 root config (cordis.yml = []) │
2. Bundle 层(dsh.profile.bundles 顺序) │
│ ├─ @deepseek-ai/dsh-base │
│ └─ @deepseek-ai/dsh-web-app / dsh-headless │
3. Profile 的 cordis.patch.yml
4. Home 级 cordis.patch.yml ($DSH_HOME) │
5. --patch 覆盖层 │
└───────────────────────────────────────────────────┘

dsh-base 是每个 profile 的第一层:模型适配器、工具、持久化、沙箱与审批策略、设置、凭证、遥测。dsh-web-app 增加浏览器应用;dsh-headless 增加无服务器的一次性运行器。

查看实际启动的插件树:

1
dsh --profile web --dump-config

输出的任何行都可以被你自己的 patch 替换。


启动流程

入口点:apps/cli/src/bin.ts

dsh 的启动流程通过按需动态导入保持各模式互不干扰:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
┌─────────────────────────────────────────────────────────┐
1. CLI 参数解析 (parseDshArgs) │
│ - 解析命令行参数 │
│ - --help / --version / parse error 直接退出 │
├─────────────────────────────────────────────────────────┤
2. 模式分支 │
│ ├─ profile → runProfile() │
│ ├─ plugin → runPlugin() │
│ └─ dump-config → runDumpConfig() │
├─────────────────────────────────────────────────────────┤
3. Profile 组合 (composeProfile) │
│ - 加载 bundle 层 (dsh.profile.bundles) │
│ - 加载 profile patch (cordis.patch.yml) │
│ - 加载 home patch ($DSH_HOME/cordis.patch.yml) │
│ - 加载 --patch 覆盖层 │
│ - 解析遥测开关 │
├─────────────────────────────────────────────────────────┤
4. 启动插件树 (boot) │
│ - 挂载空 root config │
│ - 提供启动环境快照 (DSH_LAUNCH_ENVIRONMENT_KEY) │
│ - 提供命令行参数 (provideCmdline) │
│ - 按 inject 依赖顺序激活插件 │
├─────────────────────────────────────────────────────────┤
5. 信号处理与优雅关闭 │
│ - SIGTERM → exit 0
│ - SIGINT → exit 130
│ - installFailLoud 失败大声 │
├─────────────────────────────────────────────────────────┤
6. 用户补丁热重载 (watchUserPatches) │
│ - 监听 cordis.patch.yml 变化 │
│ - 实时重组插件树 │
└─────────────────────────────────────────────────────────┘

关键设计:补丁热重载

1
2
3
4
5
6
7
8
9
10
11
12
13
// 用户编辑 cordis.patch.yml 时实时重组插件树
const composeLive = (): PatchOptions[] => structuredClone([
...composed.bundlePatches, // bundle 层不变
...loadOptionalPatches(NAME, composed.profile.patchPath) ?? [], // 重读 profile patch
...loadOptionalPatches(NAME, homePatchPath()) ?? [], // 重读 home patch
...composed.overlays, // 覆盖层不变
])

await watchUserPatches(ctx, {
binName: NAME,
filename: composed.profile.patchPath,
compose: composeLive,
})

每次 structuredClone 确保补丁对象不别名:include 将 insert 行按引用推入挂载树,后续 id 定向 patch 会原地修改这些对象,复用同一解析对象会把用户覆盖烘焙进 bundle 的内存 insert 行。


核心架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
┌──────────────────────────────────────────────────────────────┐
│ 用户界面层 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Web UI │ │ Headless │ │ ACP │ │
│ │ (React) │ │ (CLI) │ │ (Auto) │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
└───────┼──────────────┼──────────────┼────────────────────────┘
└──────────────┴──────────────┘

┌──────────────┴──────────────┐
│ Agent Loop │
│ ┌─────────────────────┐ │
│ │ Turn / Step 驱动 │ │
│ │ Inbox 消息队列 │ │
│ │ Phase 状态机 │ │
│ └─────────────────────┘ │
└──────────────┬──────────────┘

┌─────────────────┼─────────────────┐
│ │ │
┌────┴────┐ ┌──────┴──────┐ ┌─────┴─────┐
│ ctx.llm │ │ ctx.tools │ │ctx.sessions│
│ LLM 适配│ │ 工具注册表 │ │ 会话日志 │
│ 流式协议 │ │ 执行管线 │ │ 追加式事件 │
└─────────┘ └──────┬──────┘ └───────────┘

┌──────────────┼──────────────┐
│ │ │
┌────┴────┐ ┌────┴────┐ ┌─────┴─────┐
│ctx.shell│ │ ctx.fs │ │ctx.sandbox│
Shell │ │ 文件系统 │ │ 进程沙箱 │
│ 能力缝 │ │ 能力缝 │ │ 能力缝 │
└─────────┘ └─────────┘ └───────────┘

核心包一览

职责 ctx key
core/session 追加式 SessionEvent 日志与内存存储 ctx.sessions
core/system-prompt Prompt 段与工具 schema 组装 ctx.systemPrompt
core/tools 作用域工具注册表与守卫执行管线 ctx.tools
core/agent Agent 接口、实时注册表、agent/* 事件 ctx.agents
core/agent-loop 默认驱动器实现该接口 ctx.agentLoop
core/scope 每 Agent 作用域注册原语 库,无 key
llm/llm 消息与流词汇表 + 适配器缝 ctx.llm

Agent Loop:Turn / Step 驱动器

文件packages/core/agent-loop/src/agent.ts

Agent Loop 是整个系统的”心脏”,实现了 LLM 调用与工具执行的交替进行。dsh 中的核心概念:

  • Step(步骤):一次模型请求加上它调用的工具
  • Turn(轮次):零个或多个步骤,在其第一个输入被认领前打开,在不欠任何东西时关闭

Phase 状态机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
┌─────────────────────────────────────────────────┐
│ idle │
│ (空闲,等待唤醒) │
└──────────────────┬──────────────────────────────┘
wakeDriver()

┌─────────────────────────────────────────────────┐
│ running │
│ turn → step → step → ... → turn → ... │
│ AbortController per running phase │
└──────┬──────────────────────────┬───────────────┘
cancel() │ runMaintenance()
↓ ↓
┌──────────────┐ ┌───────────────────────┐
│ aborted │ │ maintenance │
│ (被取消) │ │ (维护模式:压缩等) │
└──────────────┘ └───────────┬───────────┘
│ │ done
└─────────────────────────────┘

idle

消息投递:Inbox

Agent 通过一个 Inbox 接收输入,支持三种投递目标:

1
2
3
4
5
send(message: UserMessage, target: InboxTarget, wakeup: boolean): void

followup(input: UserMessage): void // → next-turn, wakeup=true (用户追问)
steer(input: UserMessage): void // → next-step, wakeup=true (中途引导)
inject(input: UserMessage): void // → next-step, wakeup=false (上下文注入)
  • next-turn:消息等到下一个 turn 边界才被认领
  • next-step:消息在当前 turn 的下一个 step 被认领
  • 唤醒输入不能加入已中止的活动,因此它启动下一个 turn

Turn / Step 执行流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
turn/start
├─ claim next-step input + one queued message
├─ assemble prompt sections + tool schemas
└→ agent/pre-step (waterfall) ─── reject | enter(messages)
│ rejected or empty first claim → 关闭 turn(不花费 step)

step/start
append entered messages as user/message
derive model history from the log

agent/request (waterfall) → llm/stream (waterfall)

assistant/chunk* → assistant/message

├─ finish.kind === 'max-tokens' → step 结束(sticky)
├─ finish.kind === 'error' → agent/request-error (waterfall) → retry?
└─ toolCalls.length > 0:
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 (serial) → 自然停止
turn/end

核心实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
private async step(assembly: PromptAssembly): Promise<StepEndReason | null> {
const { turn, step, abort: { signal } } = this.phase
const system = renderPrompt(assembly)

while (true) {
// 1. 构建冻结的请求,绑定到解析其默认值的适配器注册
const { request, preparedCall } = await this.buildRequest(
turn, step, assembly.tools, system, this.session.deriveMessages(), signal,
)
const assembler = new BlockAssembler()
const chunkSeqs: number[] = []

// 2. 流式接收响应
const stream = preparedCall?.stream(request) ?? this.loopCtx.llm.stream(request)
for await (const chunk of stream) {
signal.throwIfAborted()
chunkSeqs.push(this.session.append('assistant/chunk', { turn, step, chunk }).seq)
assembler.push(chunk)
}

// 3. 处理结束原因
const finish = assembler.finish
if (finish.kind === 'error' || finish.kind === 'aborted') {
// agent/request-error waterfall 决定重试或保留错误
const action = await this.dispatch.waterfall('agent/request-error', { ... })
if (action?.kind !== 'retry') throw new LlmError(...)
continue // 重试
}

// 4. 记录 assistant 消息
const message = createAssistantMessage({ content: assembler.blocks(), source: { ... } })
this.session.append('assistant/message', { turn, step, message, usage: assembler.usage },
{ surfaceOp: 'append', sourceEventSeqs: chunkSeqs })

if (finish.kind === 'max-tokens') return { kind: 'max-tokens' }

// 5. 执行工具调用
const toolCalls = message.content.filter(block => block.type === 'tool-call')
if (toolCalls.length === 0) return { kind: 'completed' }
const { concluded } = await executeToolCalls(this.loopCtx, turn, step, toolCalls, signal, ...)
return concluded ? { kind: 'completed' } : null
}
}

关键特性

  • max-tokens 粘性:一旦某个 step 触达输出上限,后续正常完成的 step 不会降级 turn 结果
  • 结构化错误LlmError 保留 provider 事实,其他错误扁平化为 errorChain 文本
  • 请求冻结:Loop 构建的请求携带 markAgentLoopRequest 身份并深冻结(修改会抛错),因为其内容是会话日志的纯函数

会话日志:唯一事实来源

文件packages/core/session/src/index.ts

会话日志是模型所见上下文的唯一来源。deriveMessages() 从日志投影模型历史,原始 assistant/chunk 事件保留重放与 UI 保真度。Fork、resume、transcript、遥测、持久化全部从这条流派生。

核心不变式:模型可见即已记录

Anything that reaches a model request must be reconstructable from the session log.

运行时不变式强制保证这一点——新增模型可见的输入需要新增一个 session event。

Session 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export class Session {
private log: SessionEvent[] = [] // 追加式事件日志
private readonly surfaceManager = new SurfaceManager(this.log) // 有序表面

get events(): readonly SessionEvent[] { ... } // 不可变快照
get seq(): number { return this.log.length } // seq = log.length 连续性契约

// 追加一个类型化事件,同步通知观察者
append<T extends SessionEventType>(
type: T,
data: SessionEventMap[T],
...opts: T extends SurfaceEventType ? [opts: SurfaceIntent] : []
): SessionEvent<T>

// 从有序表面投影 LLM 消息历史(缓存)
deriveMessages(): Message[]

// 请求头折叠(增量缓存)
requestHeader(): EpochHeader | undefined
}

事件类型

类别 事件 持久 说明
Turn 生命周期 turn/start, turn/end turn 边界
Step 生命周期 step/start, step/end step 边界
消息 user/message, assistant/message 表面事件
流式 assistant/chunk 原始 chunk(保真)
工具 tool/call, tool/result 表面事件
请求 request/header, request/context 模型路由元数据
压缩 compaction/start, compaction/end 压缩事务标记
代码分发 tool/code-dispatch-start, tool/code-dispatch Code Mode 子调用
会话 session/end-seed 种子边界标记

Surface 与压缩

表面(Surface)是有序的消息产生事件序列。每个表面事件通过 surfaceOp 声明它如何加入表面:

  • append:追加到末尾
  • replace:替换一组被遮蔽的节点(压缩用)
1
2
3
4
5
6
7
8
9
10
11
deriveMessages(): Message[] {
const surface = this.surface
const nodes = surface.nodes
// 缓存:每个表面节点只投影一次,O(new nodes)
for (const seq of nodes.slice(this.derivedNodes)) {
const msg = this.deriveEventMessage(this.log[seq]!)
if (msg) this.derived.push(msg)
}
this.derivedNodes = nodes.length
return [...this.derived] // 新鲜数组,共享冻结的 Message
}

压缩时 replaceGeneration 递增,缓存失效重建——这是一个 O(new nodes) 的增量设计。


工具系统

工具定义

文件packages/core/tools/src/schema.ts

dsh 使用 defineTool() 创建类型安全的工具定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export interface DefineToolOptions<S, O> {
name: string // 工具名(唯一)
description: string // 发送给模型的描述
parameters: S // 参数 schema(编译为 JSON Schema)
output: {
schema: O // 输出 schema
render(args, value): ContentBlock[] // 纯 Native 渲染
presentationMeta?(args, value): JsonValue
}
timeoutMs?: number // 协作式超时
isConcurrencySafe?(args): boolean // 并发安全分类器
execute(args, exec): Promise<Value> // 执行体
finalizeContent?(exec, result): ContentBlock[] | undefined // 最后内容变换
presentCall?(args): ToolCallView | undefined // 纯 pending 渲染意图
presentResult?(args, result): ToolResultView | undefined // 纯 completed 渲染意图
}

关键设计

  • UI 渲染意图是工具设计的一部分presentCall/presentResult 是纯函数,提前决定 generic/terminal/diff 等渲染类型
  • 呈现是仅显示的:可在任意旧 schema 的 logged args 上重放,软验证 + 回退到 generic UI
  • 输出 schema 强制:每次成功的 body 或策略替换值都通过 output schema 验证

工具执行管线

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
┌─────────────────────────────────────────────────────┐
│ Assistant 消息包含 tool-call block │
└──────────────────────┬──────────────────────────────┘

┌─────────────────────────────────────────────────────┐
Session 事件: tool/call(执行前记录) │
└──────────────────────┬──────────────────────────────┘

┌─────────────────────────────────────────────────────┐
│ tools/pre-execute waterfall │
│ hooks, permission, sandbox │
│ ├─ allow → 继续 │
│ ├─ deny → 跳过工具体 │
│ └─ ask → ctx.approval 一次性提示 │
└──────────────────────┬──────────────────────────────┘

┌─────────────────────────────────────────────────────┐
│ 注册的单调守卫(monotonic guards) │
│ deny 或弃权;身份保护 │
└──────────────────────┬──────────────────────────────┘

┌─────────────────────────────────────────────────────┐
│ tools/execute waterfall(around dispatch) │
│ timeout, retry, metrics │
│ → 注册的工具 execute() body │
│ → fs/write-intent 或 fs/edit-intent(tool-fs 变更) │
│ → 工具拥有的 session 事件 │
└──────────────────────┬──────────────────────────────┘

┌─────────────────────────────────────────────────────┐
│ tools/post-execute waterfall │
│ accept, block, replace, add context │
└──────────────────────┬──────────────────────────────┘

┌─────────────────────────────────────────────────────┐
│ Registry 外部规范化 │
│ pipeline/result snapshot throws → isError │
└──────────────────────┬──────────────────────────────┘

┌─────────────────────────────────────────────────────┐
│ ToolDefinition.finalizeContent │
│ 最后的 content-only 不变式检查 │
└──────────────────────┬──────────────────────────────┘

┌─────────────────────────────────────────────────────┐
│ tools/result 同步通知(冻结的权威结果) │
│ → Session 事件: tool/result │
│ → UI completed card │
└─────────────────────────────────────────────────────┘

并发调度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export async function executeToolCalls(ctx, turn, step, toolCalls, signal, acceptContext) {
const planned = toolCalls.map(block => ({ block, exec: { callId: block.id, ... } }))
let next = 0
while (next < planned.length) {
// 按执行模式分类:parallel 可与兄弟重叠,exclusive 独占
const mode = ctx.tools.executionMode(first.exec).kind
const group = mode === 'parallel' ? planned.slice(next) : [first]
const outcome = await runGroup(ctx, turn, step, group, mode, signal, acceptContext)
next += outcome.consumed
if (outcome.aborted) {
// 跳过剩余调用
for (const call of planned.slice(next)) appendSkippedToolCall(session, turn, step, call.block)
return { concluded }
}
}
}

调度策略

  • 只有精确的 true 才是 parallel;未知、隐藏、未声明、无效或抛异常的分类器都是 exclusive
  • 提交后重新分类,使注册表变更影响未启动的调用
  • 被中止时记录跳过的 tool call

Code Mode

dsh 的一个独特设计是 Code Mode:模型通过 run_code 工具编写一段程序(TypeScript/Python),程序内部通过 SDK 绑定调用所有其他工具,而非逐个调用原生工具。

1
2
3
// Code Mode 下,模型只能直接调用 run_code
const CODE_ONLY_INSTRUCTION = `\`${RUN_CODE_NAME}\` is the only tool you can call directly
— a tool call naming any other tool fails. Reach every tool the SDK declares below from inside the program.`

Code Mode 的子分发(sub-dispatch)也走完整的工具管线,携带 parent token,记录 tool/code-dispatch 事件,拒绝以 binding rejection 返回。


能力缝(Capability Seam)

能力缝是 dsh 最核心的架构模式。一个缝是可交换的能力,包含三个角色:

1
2
3
4
5
6
7
8
9
10
11
12
┌──────────────────────────────────────────────────────────┐
│ Capability Seam │
│ │
│ ┌──────────────────┐ 声明接口 │
│ │ Service Definition│◄──────────────────────── │
│ └────────┬─────────┘ │
│ │ implements │
│ ┌────────┴─────────┐ ┌──────────────────┐ │
│ │ Service Provider │ │ Consumer │ │
│ │ (实现) │ │ (使用,通常是工具) │ │
│ └──────────────────┘ └──────────────────┘ │
└──────────────────────────────────────────────────────────┘

一个包可以组合角色,但单独一个角色不是缝;添加能力意味着设计全部三个角色。

关键能力缝

ctx key 职责 Service Provider 实现
ctx.llm LLM 适配器注册表 llm-deepseek, llm-pi-ai, llm-replay
ctx.fs 文件系统提供者 fs-local, fs-sandbox, fs-e2b
ctx.shell Bash 执行器 bash-local, bash-sandbox, pwsh-local
ctx.subprocess 子进程 subprocess-local, subprocess-e2b
ctx.sandbox 进程沙箱 sandbox-local
ctx.compaction 上下文压缩 compaction-basic
ctx.subagents 子代理 spawn-in-process, fork-in-process, acp, codex, claude-code, dsh-sdk
ctx.web Web 搜索/抓取 web-search-exa, web-search-perplexity, web-search-deepseek, web-fetch-http
ctx.approval 审批决策 acp
ctx.sessionPersistence 会话持久化 session-persistence-jsonl, session-persistence-sqlite
ctx.settings 用户设置 settings-file
ctx.credentials 凭证引用 credentials-local
ctx.terminals 持久终端 terminal-bash
ctx.lsp 语言服务器 lsp-local
ctx.skills 技能注册表 skill-badge, skill-filesystem

为什么一个 Provider 切换改变整个产品

文件系统和子进程提供者共享一个执行世界,所以将它们指向远程沙箱会把 Bash、PTY 和 LSP 一起搬过去,无需 provider 分叉:

1
2
3
4
5
6
本地模式:                           远程沙箱模式:
ctx.fs → fs-local ctx.fs → fs-e2b ──┐
ctx.subprocess subprocess-local ctx.subprocess subprocess-e2b
ctx.shell bash-local ctx.shell bash-local (不变)

工具 (tool-bash, tool-fs) 不变 ┘

子代理提供者同样多样:从全新的子 Agent 到在另一个产品中的委托 turn。


LLM 流式协议

文件packages/llm/llm/src/types.ts + packages/llm/llm/src/index.ts

StreamChunk 协议

1
2
3
4
5
6
7
8
export type StreamChunk =
| { type: 'block-start'; index: number; blockType: ContentBlockType }
| { type: 'text-delta'; index: number; text: string }
| { type: 'reasoning-delta'; index: number; text: string }
| { type: 'tool-call-delta'; index: number; id: CallId; name?: string; argumentsDelta: string }
| { type: 'block-end'; index: number; block: ContentBlock }
| { type: 'usage'; usage: TokenUsage }
| { type: 'finish'; reason: FinishReason; replayState?: unknown }

LlmRuntime 服务

1
2
3
4
5
6
7
8
9
10
11
12
13
export class LlmRuntime extends Service {
// 流式调用,可能被 llm/stream 监听器包装
stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
return this.streamWithRegistration(options)
}

private streamWithRegistration(options, prepared?): AsyncIterable<StreamChunk> {
return this.ctx.waterfall(
this, 'llm/stream', options,
() => this.adapterStream(options, prepared), // 委托给解析的适配器
)
}
}

关键设计

  • llm/stream 是 waterfall:监听器可以重试、重放、路由,或 yield 自己的 chunk 来短路
  • Loop 构建的请求是深冻结的:其内容是会话日志的纯函数,监听器只读不写
  • prepareCall():为精确模型解析默认值,绑定适配器注册
  • 结构化错误LlmError 携带 provider 中性码(AUTH, RATE_LIMIT, NO_ADAPTER, ABORTED 等)
  • replayState:适配器私有的无损 JSON 状态,用于重放成功响应

Token 计量

1
2
3
4
5
6
7
8
export interface TokenUsage {
inputTokens: number // 未缓存输入(DISJOINT)
outputTokens: number
cacheReadTokens?: number // 缓存读取(单独计费)
cacheWriteTokens?: number // 缓存写入
reasoningTokens?: number // 推理 token
}
// 计费输入 = inputTokens + cacheReadTokens + cacheWriteTokens

适配器若 provider 将缓存命中折叠进总 prompt 计数(如 DeepSeek 的 prompt_tokens),需减去缓存部分。


沙箱与安全机制

文件packages/sandbox/sandbox/src/index.ts + packages/sandbox/sandbox-local/src/index.ts

沙箱模式

1
2
3
4
export type SandboxMode =
| 'read-only' // 仅允许 /dev/null 等必需 sink
| 'workspace-write' // 允许工作区 + 后端定义的 temp 区
| 'danger-full-access' // 绕过限制

Service Definition

1
2
3
4
5
6
7
8
9
10
11
export abstract class SandboxProvider extends Service {
// 包装 argv 使其在策略下受限执行;必须返回强制 argv 或 fail-closed
abstract confine(argv: readonly string[], policy: SandboxPolicy): ConfinedArgv
}

export interface ConfinedArgv {
argv: string[] // 包装后的 argv(runner + profile + -- + 原 argv)
enforcement: SandboxEnforcement // 'full' | 'partial'
denialSignatures: readonly string[] // 该后端的拒绝方言(EROFS/EACCES/EPERM)
runnerFailureRules: readonly RunnerFailureRule[]
}

多平台 Runner

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌─────────────────────────────────────────────────────────┐
│ LocalSandboxProvider.confine(argv, policy) │
├─────────────────────────────────────────────────────────┤
│ Linux: │
│ ├─ Landlock (原生 C addon, static musl) │
│ │ → landlockLauncher() + landlockProfileArgs(policy) │
│ └─ bubblewrap (bwrap) │
│ → ['bwrap', ...bwrapProfileArgs(policy)] │
│ │
│ macOS: │
│ └─ sandbox-exec (seatbelt) │
│ → seatbeltExec() + seatbeltProfileArgs(policy) │
│ │
│ Windows: │
│ └─ ACL restricted-token runner │
│ → windows-acl runner + workspace/temp SID │
└─────────────────────────────────────────────────────────┘

关键安全原则

  1. Fail-closed:无可用后端时拒绝未受限运行,抛出 SANDBOX_UNAVAILABLE
  2. 策略按调用携带:两个消费者可同时在不同策略下受限(bash 在 read-only,子 Agent 在 workspace-write)
  3. 原生 Landlock addonnative/landlock-run 是 C 源码,静态 musl 编译(-static -s),无 loader/libc 依赖,每架构原生编译
  4. 拒绝方言:每个后端报告自己的拒绝 stderr 子串(bwrap 的 EROFS、Landlock 的 EACCES、Seatbelt 的 EPERM),消费者匹配精确方言而非跨后端并集

审批能力缝

1
2
3
// ctx.approval 是一次性审批决策
// 通过 approval/request waterfall 派发
// 无应答者 → fail-closed 为 'unavailable'

权限预设(ctx.permissionPresets)打包沙箱模式和审批策略两个旋钮:workspace-write / danger-full-access


上下文压缩

文件packages/compaction/compaction/src/index.ts + packages/compaction/compaction-basic/src/region.ts

CompactionEngine 服务

1
2
3
4
5
6
7
8
9
10
export abstract class CompactionEngine extends Service {
// 自动压缩:压力策略或上下文溢出
abstract compactIfNeeded(agent, trigger: CompactionTrigger, signal): Promise<CompactionResult | null>

// 手动压缩:即使低于自动阈值
abstract compactNow(agent, signal, sourceCommandId?): Promise<CompactionResult | null>

// 强制压缩指定范围
abstract compactRegion(start, end, agent, signal?): Promise<CompactionResult>
}

触发机制

触发器 来源 时机
pressure agent/pre-step 请求派生前检测压力
context-overflow agent/request-error provider 确认上下文溢出

压缩流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
┌─────────────────────────────────────────────────┐
1. 检测触发 (pressure / context-overflow) │
├─────────────────────────────────────────────────┤
2. 可选 tool-result 修剪 │
│ ctx.toolResultPruner 重写超大 tool result │
│ (replayable 单节点表面替换) │
├─────────────────────────────────────────────────┤
3. 选择压缩范围 │
- 边缘必须平衡(assistant tool call 配对) │
- toolPairingBalancedBefore / After │
├─────────────────────────────────────────────────┤
4. compaction/start (持久标记 = 压缩锁) │
├─────────────────────────────────────────────────┤
5. 构建摘要输入 │
buildSummarizationInput: 从 shadowedSeqs │
│ 派生 region messages │
├─────────────────────────────────────────────────┤
6. 模型摘要 (frameSummary) │
├─────────────────────────────────────────────────┤
7. 表面替换 (surfaceOp: 'replace') │
- 替换选中的表面 span 为一个 summary 节点 │
- replaceGeneration++ → 缓存失效重建 │
- checkpointMessage 携带 CompactionId │
├─────────────────────────────────────────────────┤
8. compaction/end
└─────────────────────────────────────────────────┘

关键设计

  • 维护模式:压缩在 runMaintenance() 中运行,仅在 Agent 空闲时执行
  • 并发锁compaction/start 标记是持久锁,直到 compaction/end
  • 稳定性检查:摘要后验证 span 是否仍可替换(未被并发修改)
  • 恢复语义:在失败的 step 和失败的 turn 关闭之间恢复,只有修剪或摘要推进了表面替换代时才打开新的重试 turn

事件系统

事件是 dsh 的扩展点,选择正确的域是大多数变更的第一个决策。

三大事件域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌──────────────────────────────────────────────────────────┐
│ Session Events (持久事实) │
│ 追加到日志,通过 session/event 广播 │
│ 事实必须在重载后存活时使用 │
│ turn/*, step/*, user/message, assistant/*, tool/* │
├──────────────────────────────────────────────────────────┤
│ Agent Events (实时协调) │
│ 携带 live Agent: inbox, step, status, request │
│ 观察/拦截进行中的工作时使用 │
│ agent/pre-step, agent/request, agent/turn-stopping... │
├──────────────────────────────────────────────────────────┤
│ Capability Events (策略与适配器) │
│ 将策略和适配器附加到缝,不导入循环 │
│ fs/*, tools/*, telemetry/*, llm/stream │
└──────────────────────────────────────────────────────────┘

Waterfall 事件

agent/pre-stepagent/requestllm/stream 和三个 tools/* 事件是 waterfall——监听器必须调用 next() 委托;agent/turn-stopping 是 serial,没有 next()

1
2
3
4
5
6
7
8
9
// agent/pre-step 决定模型看到什么
const decision = await this.dispatch.waterfall(
'agent/pre-step', { messages: claimed, ...position, signal },
(): Promise<PreStepDecision> => Promise.resolve({
kind: 'enter',
messages: context === undefined ? claimed : [...claimed, context],
}),
)
// 监听器可以:rewrite claimed messages / reject(拒绝或空首次 claim 关闭 turn)

扩展点地图

新行为挂载到文档化的扩展点,而非修改循环本身:

目标 机制
添加模型 provider ctx.llm 上注册适配器
添加模型面向的能力 ctx.tools 上注册;其 schema 加入 prompt 组装
给一个 session 不同的能力集 组合 agent preset;服务行需要 isolate realm
添加 shell 执行 注册 ctx.shell 后端;本地版通过 ctx.subprocess 派生
添加人类命令 ctx.commands 上注册;无需模型 turn 即分发
添加后台工作 ctx.jobs 上注册;job_* 工具收集或停止
添加文件系统访问或策略 注册 ctx.fs provider 或监听 fs/* 事件
限制派生进程 使用 ctx.sandbox 后端;消费者在派生前包装 argv
拦截请求/工具/turn 使用其 agent/*tools/* 事件
添加模型面向的上下文 调用 agent.inject();落地到下一个 admitted 请求
添加持久 session 状态 扩展 SessionEventMap;从日志渲染和重放
Fork 一个 live session ctx.sessions.fork(source, boundary?, childSessionId?)
将注册限定到一个 agent 使用该 agent 的 agent.ctx

关键文件索引

文件 功能
apps/cli/src/bin.ts CLI 入口,模式分发
apps/cli/src/profile-boot.ts Profile 组合、启动、热重载
packages/core/agent-loop/src/agent.ts ReactLoopAgent:Turn/Step 驱动器
packages/core/agent-loop/src/tool-calls.ts executeToolCalls:并发工具调度
packages/core/session/src/index.ts Session:追加式事件日志、deriveMessages
packages/core/tools/src/index.ts ToolRuntime:注册表与执行管线
packages/core/tools/src/schema.ts defineTool:工具定义
packages/core/tools/src/invariant.ts 工具管线不变式
packages/core/system-prompt/src/index.ts Prompt 组装
packages/llm/llm/src/index.ts LlmRuntime:流式协议
packages/llm/llm/src/types.ts StreamChunk、GenerateOptions
packages/sandbox/sandbox/src/index.ts SandboxProvider:沙箱 Service Definition
packages/sandbox/sandbox-local/src/index.ts LocalSandboxProvider:多平台实现
packages/compaction/compaction/src/index.ts CompactionEngine:压缩 Service Definition
packages/compaction/compaction-basic/src/region.ts 基础压缩引擎实现
packages/boot/app-boot/src/profile.ts Profile/Bundle 解析与组合

架构设计亮点

1. 一切皆插件

没有特权核心,包括 Agent 循环本身都是可替换的插件。扩展通过挂载插件实现,注册是可逆 effect,卸载自动回滚。这使得 dsh 的每个部分都能从配置替换。

1
2
3
4
5
6
7
8
9
10
11
┌─────────────────────────────────────┐
│ UI 层 (React Web / Headless / ACP) │
├─────────────────────────────────────┤
│ 驱动层 (Agent Loop: Turn/Step) │
├─────────────────────────────────────┤
│ 核心服务 (Session, Tools, Prompt) │
├─────────────────────────────────────┤
│ 能力缝 (LLM, FS, Shell, Sandbox) │
├─────────────────────────────────────┤
│ Cordis 框架 (插件/事件/effect) │
└─────────────────────────────────────┘

2. 能力缝三角色

Service Definition / Provider / Consumer 三角色完整设计,一个 Provider 切换改变整个产品。文件系统和子进程共享执行世界,指向远程沙箱即搬运 Bash、PTY、LSP,无 provider 分叉。

3. 模型可见即已记录

会话日志是唯一事实来源,运行时不变式强制保证模型请求的一切可从日志重建。Fork、resume、transcript、遥测、持久化全部从这条流派生。新增模型可见输入需新增 session event。

4. 追加式日志 + 表面替换

追加式事件日志保证历史不可变,表面替换(surfaceOp: 'replace')实现压缩的非破坏性修改。增量缓存(header fold、context fold、derived messages)使每步成本为 O(new events/nodes)。

5. 多平台原生沙箱

Landlock(Linux 原生 C addon,静态 musl 编译)、bubblewrap、sandbox-exec、Windows ACL 四平台原生沙箱。Fail-closed 原则:无可用后端时拒绝未受限运行。策略按调用携带,两个消费者可同时在不同策略下受限。

6. Code Mode

模型通过 run_code 编写程序调用所有工具,而非逐个调用。子分发走完整工具管线,携带 parent token,记录 tool/code-dispatch 事件。这给了模型更强的组合能力。

7. 类型安全与工程化

  • strict: true + noImplicitAny,每个 any 解释为何不可窄化
  • 每个模块和导出有简洁 JSDoc
  • 不透明跨边界 ID 使用 branded type(Branded<B>),非裸 string
  • 运行时不变式断言拥有的关系,检查权威事件流而非服务/方法存在
  • 100% 单测覆盖率门控 + 快照测试 + e2e

8. 组合优于硬编码

  • Profile + Bundle + Patch 层叠组合,用户 cordis.patch.yml 可替换任何行
  • 用户补丁热重载,实时重组插件树
  • 部署可变选择是验证过的 Config 字段,可从 cordis.yml 修改

总结

DeepSeek Harness 是一个架构理念极致、工程化严谨的 Agent 框架,几个亮点:

  1. 一切皆插件:没有特权核心,Agent 循环本身都可替换,注册是可逆 effect
  2. 能力缝架构:Service Definition / Provider / Consumer 三角色,一个 Provider 切换改变整个产品
  3. 追加式会话日志:模型可见即已记录,运行时不变式强制,表面替换实现非破坏压缩
  4. 多平台原生沙箱:Landlock / bubblewrap / sandbox-exec / Windows ACL,fail-closed
  5. Code Mode:模型编写程序调用工具,更强的组合能力
  6. Cordis 事件系统:emit / waterfall / parallel / serial 四种派发模式,扩展点清晰
  7. 组合系统:Profile + Bundle + Patch 层叠,用户补丁热重载
  8. 极致类型安全:strict + branded type + 运行时不变式 + 100% 覆盖率门控

dsh 的 monorepo 规模令人印象深刻——54 个包组、219+ 个子包,每个包都是独立 npm 包,通过 Cordis 的 Service/Event 机制松耦合组装。”一切皆插件”不是口号,而是从代码到配置的一致实践:dsh --dump-config 输出的任何行都可以被你的 patch 替换。

对于想要构建可扩展、可组合的 Agent 框架的开发者来说,DeepSeek Harness 的源码是一个极好的学习素材——尤其是它的能力缝设计、会话日志不变式、工具执行管线和插件化组合系统。


项目地址https://github.com/deepseek-ai/deepseek-harness
版本:0.1.0-rc.5(developer preview)
文档https://github.com/deepseek-ai/deepseek-harness/tree/main/docs
框架Cordis


DeepSeek Harness 源码解析:DeepSeek 官方 Agent 框架的插件化架构设计
https://tingfeng347.github.io/2026/08/14/DeepSeek Harness 源码解析:DeepSeek 官方 Agent 框架的插件化架构设计/
作者
Tingfeng
发布于
2026年8月14日
许可协议