DeepSeek Harness 源码解析:DeepSeek 官方 Agent 框架的插件化架构设计
项目简介
DeepSeek Harness(dsh)是 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
目录结构
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. 插件是实现 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() 则短路。策略监听器可以在拥有决策权时短路返回,仅观察的监听器必须委托。
// 声明事件(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.yml。web和headless作为模板内置。 - Bundle(打包层):Cordis 配置行及其挂载代码的分发格式,使其插入的内容可被上层补丁修改。
- Patch(补丁):通过 id 定位行,替换其整个 config 或插入新行。
层叠顺序
┌───────────────────────────────────────────────────┐
│ 应用顺序(从下到上) │
├───────────────────────────────────────────────────┤
│ 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 增加无服务器的一次性运行器。
查看实际启动的插件树:
dsh --profile web --dump-config
输出的任何行都可以被你自己的 patch 替换。
启动流程
入口点:apps/cli/src/bin.ts
dsh 的启动流程通过按需动态导入保持各模式互不干扰:
┌─────────────────────────────────────────────────────────┐
│ 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 变化 │
│ - 实时重组插件树 │
└─────────────────────────────────────────────────────────┘
关键设计:补丁热重载
// 用户编辑 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 行。
核心架构
┌──────────────────────────────────────────────────────────────┐
│ 用户界面层 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 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 状态机
┌─────────────────────────────────────────────────┐
│ idle │
│ (空闲,等待唤醒) │
└──────────────────┬──────────────────────────────┘
│ wakeDriver()
↓
┌─────────────────────────────────────────────────┐
│ running │
│ turn → step → step → ... → turn → ... │
│ AbortController per running phase │
└──────┬──────────────────────────┬───────────────┘
│ cancel() │ runMaintenance()
↓ ↓
┌──────────────┐ ┌───────────────────────┐
│ aborted │ │ maintenance │
│ (被取消) │ │ (维护模式:压缩等) │
└──────────────┘ └───────────┬───────────┘
│ │ done
└─────────────────────────────┘
↓
idle
消息投递:Inbox
Agent 通过一个 Inbox 接收输入,支持三种投递目标:
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 执行流程
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
核心实现
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 类
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:替换一组被遮蔽的节点(压缩用)
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() 创建类型安全的工具定义:
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 验证
工具执行管线
┌─────────────────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────────────────┘
并发调度
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 绑定调用所有其他工具,而非逐个调用原生工具。
// 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 最核心的架构模式。一个缝是可交换的能力,包含三个角色:
┌──────────────────────────────────────────────────────────┐
│ 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 分叉:
本地模式: 远程沙箱模式:
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 协议
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 服务
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 计量
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
沙箱模式
export type SandboxMode =
| 'read-only' // 仅允许 /dev/null 等必需 sink
| 'workspace-write' // 允许工作区 + 后端定义的 temp 区
| 'danger-full-access' // 绕过限制
Service Definition
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
┌─────────────────────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────────────────────┘
关键安全原则
- Fail-closed:无可用后端时拒绝未受限运行,抛出
SANDBOX_UNAVAILABLE - 策略按调用携带:两个消费者可同时在不同策略下受限(bash 在 read-only,子 Agent 在 workspace-write)
- 原生 Landlock addon:
native/landlock-run是 C 源码,静态 musl 编译(-static -s),无 loader/libc 依赖,每架构原生编译 - 拒绝方言:每个后端报告自己的拒绝 stderr 子串(bwrap 的 EROFS、Landlock 的 EACCES、Seatbelt 的 EPERM),消费者匹配精确方言而非跨后端并集
审批能力缝
// 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 服务
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. 检测触发 (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 的扩展点,选择正确的域是大多数变更的第一个决策。
三大事件域
┌──────────────────────────────────────────────────────────┐
│ 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-step、agent/request、llm/stream 和三个 tools/* 事件是 waterfall——监听器必须调用 next() 委托;agent/turn-stopping 是 serial,没有 next()。
// 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 的每个部分都能从配置替换。
┌─────────────────────────────────────┐
│ 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 框架,几个亮点:
- 一切皆插件:没有特权核心,Agent 循环本身都可替换,注册是可逆 effect
- 能力缝架构:Service Definition / Provider / Consumer 三角色,一个 Provider 切换改变整个产品
- 追加式会话日志:模型可见即已记录,运行时不变式强制,表面替换实现非破坏压缩
- 多平台原生沙箱:Landlock / bubblewrap / sandbox-exec / Windows ACL,fail-closed
- Code Mode:模型编写程序调用工具,更强的组合能力
- Cordis 事件系统:emit / waterfall / parallel / serial 四种派发模式,扩展点清晰
- 组合系统:Profile + Bundle + Patch 层叠,用户补丁热重载
- 极致类型安全: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
更多推荐


所有评论(0)