DeepSeek Harness 源码解析:DeepSeek 官方 Agent 框架的插件化架构设计
深入剖析 DeepSeek 官方开源 Agent 框架——DeepSeek Harness(dsh)的插件化架构设计、核心模块与运行机制
项目简介 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
目录结构 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/ │ └── web/ │ ├── packages/ │ ├── core/ │ │ ├── session/ │ │ ├── system-prompt/ │ │ ├── tools/ │ │ ├── agent/ │ │ ├── agent-loop/ │ │ ├── scope/ │ │ └── agent-default-model/ │ │ │ ├── llm/ │ │ ├── llm/ │ │ ├── llm-deepseek/ │ │ ├── llm-pi-ai/ │ │ └── token-meter/ │ │ │ ├── shell/ │ │ ├── shell/ │ │ ├── bash-local/ │ │ ├── bash-sandbox/ │ │ ├── pwsh-local/ │ │ └── tool-bash/ │ │ │ ├── fs/ │ │ ├── fs/ │ │ ├── fs-local/ │ │ ├── fs-sandbox/ │ │ └── tool-fs/ │ │ │ ├── sandbox/ │ │ ├── sandbox/ │ │ ├── sandbox-local/ │ │ └── sandbox-policy/ │ │ │ ├── compaction/ │ │ ├── compaction/ │ │ └── compaction-basic/ │ │ │ ├── subagent/ │ ├── web/ │ ├── terminal/ │ ├── lsp/ │ ├── skill/ │ ├── session/ │ ├── interaction/ │ ├── bundle/ │ │ ├── base/ │ │ ├── web-app/ │ │ └── headless/ │ ├── boot/ │ ├── sdk/ │ ├── client/ │ ├── host/ │ └── ... │ ├── vendor/ ├── native/ ├── python/ ├── examples/ ├── docs/ ├── scripts/ └── website/
这个 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 declare module '@deepseek-ai/cordis' { interface Context { llm : LlmRuntime } interface Events { '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 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 const composeLive = (): PatchOptions [] => structuredClone ([ ...composed.bundlePatches , ...loadOptionalPatches (NAME , composed.profile .patchPath ) ?? [], ...loadOptionalPatches (NAME , homePatchPath ()) ?? [], ...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 steer (input : UserMessage ): void inject (input : UserMessage ): void
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 ) { const { request, preparedCall } = await this .buildRequest ( turn, step, assembly.tools , system, this .session .deriveMessages (), signal, ) const assembler = new BlockAssembler () const chunkSeqs : number [] = [] 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) } const finish = assembler.finish if (finish.kind === 'error' || finish.kind === 'aborted' ) { const action = await this .dispatch .waterfall ('agent/request-error' , { ... }) if (action?.kind !== 'retry' ) throw new LlmError (...) continue } 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' } 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 } append<T extends SessionEventType >( type : T, data : SessionEventMap [T], ...opts : T extends SurfaceEventType ? [opts : SurfaceIntent ] : [] ): SessionEvent <T> 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 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 ] }
压缩时 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 output : { schema : O render (args, value): ContentBlock [] presentationMeta?(args, value): JsonValue } timeoutMs ?: number isConcurrencySafe?(args): boolean execute (args, exec): Promise <Value > finalizeContent?(exec, result): ContentBlock [] | undefined presentCall?(args): ToolCallView | undefined presentResult?(args, result): ToolResultView | undefined }
关键设计 :
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 ) { 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 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 { 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 outputTokens : number cacheReadTokens ?: number cacheWriteTokens ?: number reasoningTokens ?: number }
适配器若 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' | 'workspace-write' | 'danger-full-access'
Service Definition 1 2 3 4 5 6 7 8 9 10 11 export abstract class SandboxProvider extends Service { abstract confine (argv : readonly string [], policy : SandboxPolicy ): ConfinedArgv }export interface ConfinedArgv { argv : string [] enforcement : SandboxEnforcement denialSignatures : readonly string [] 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 │ └─────────────────────────────────────────────────────────┘
关键安全原则
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.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-step、agent/request、llm/stream 和三个 tools/* 事件是 waterfall——监听器必须调用 next() 委托;agent/turn-stopping 是 serial,没有 next()。
1 2 3 4 5 6 7 8 9 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], }), )
扩展点地图 新行为挂载到文档化的扩展点,而非修改循环本身:
目标
机制
添加模型 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 框架,几个亮点:
一切皆插件 :没有特权核心,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