项目简介

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

目录结构

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.ymlwebheadless 作为模板内置。
  • 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           │
└─────────────────────────────────────────────────────────┘

关键安全原则

  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),消费者匹配精确方言而非跨后端并集

审批能力缝

// 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-stepagent/requestllm/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 框架,几个亮点:

  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

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐