DeepSeek Harness (dsh) 整体架构解析

DeepSeek AI 开源的插件化 Agent Harness —— 基于 Cordis 构建,一切皆插件

MIT License Developer Preview · 0.1.0-rc.5 Node ^22.19 || ≥24 · ESM pnpm workspaces · ~130+ 包
⚠ 开发者预览 — 正在快速迭代,兼容性随时可能被破坏。设计倾向:在第一次正式发版前选择"正确的地基"而非兼容垫片 —— 可自由重命名/重组包并同步更新所有引用;后端拒绝旧磁盘格式。

01项目定位

dsh 是一个 Agent 执行框架:围绕"一次模型请求 + 其引发的工具调用"的循环,把 会话存储、提示词组装、工具注册、模型适配、权限审批、Web UI 等全部产品能力组织成 一个可组合、可替换的插件树。运行时由 cordis.yml 描述,插件通过事件与服务协作, 没有特权核心需要打补丁 —— 要扩展 dsh,就挂一个插件,甚至可以用插件修改自身的运行时。

仓库结构
packages/ 约 130+ workspace 包(全部 @deepseek-ai/dsh-*)
vendor/ vendored Cordis 源码(pin 副本)
apps/ CLI 与 Web 宿主
python/ native/ examples/ docs/ website/
一个 dsh 进程 = ?
由 Profile 组装出的插件树:每条 cordis.yml 行挂载一个插件, 插件贡献 ctx.* 服务、类型化事件与可逆副作用(effect)。

02核心思想:一切皆插件

这是整个架构的第一原则:模型适配器、工具注册表、会话日志、agent 循环本身 —— 全部是插件。插件贡献三类东西到共享的 Cordis Context:

贡献形式含义关键性质
ctx.effect() 注册一次性副作用(如注册服务、挂载文件) 返回 disposer;插件卸载时自动回滚,热重载(HMR)安全
ctx.on() / ctx.waterfall() 订阅事件 / 注册可拦截链 waterfall 监听者必须调用 next() 委托,否则短路整条链
Service 子类 声明一个 ctx.<key> 能力服务 能力缝三件套的"Service Definition"角色,绝不用 TS interface
◆ 推论 — 注册即副作用、卸载即回滚;没有特权核心,任何一部分都可以从配置被替换。 "插件,而非循环改动":新行为落在文档化的扩展点上,改 agent-loop 本身必须同步更新 docs/architecture.md

03组合分层:Profile → Bundle → 插件树

一个运行的 dsh 是从空条目列表开始、按固定顺序叠加配置层得到的插件树。

最终插件树(运行中的 dsh)
所有服务 / 事件 / 工具已挂载
▲ 逐层覆盖,后层以行 id 为键替换整行 config
--patch 覆盖层
命令行临时补丁
Harness 主页 cordis.patch.yml
用户自己的补丁层
Profile 自身 cordis.patch.yml
Profile 模板自带覆盖
── profile 列出的 bundles 按顺序叠 ──
Bundle N …
dsh-web-app / dsh-headless(可选)
dsh-base(第一个 bundle)
模型适配器 · 工具 · 持久化 · 沙箱 · 审批 · 设置 · 凭据 · 遥测
空条目列表
加载起点
组合顺序:每个 bundle(按 profile 列出的顺序)→ profile patch → home patch → --patch。patch 以行 id 定位,替换整行 config 或插入新行。

验证你机器上实际启动的树:

dsh --profile web --dump-config — 打印的每一行都可以被你的 patch 覆盖。

04运行形态

Web UI(dsh web)
浏览器应用,默认 http://127.0.0.1:3080;由 dsh-web-app bundle 提供
Headless(dsh --profile headless "task")
一次性执行任务,无服务器;由 dsh-headless bundle 提供
ACP 服务器
自动化专用的 Agent Client Protocol 服务器
SDK / JSON-RPC
进程外运行时:JSON-RPC 协议 + TS 客户端 + 服务端插件
Hook 桥
Claude Code / Codex hook 桥 + 共享线协议库

05核心包:产品 API 脊梁

◆ 深度解析入口 — 每个核心包都有独立页面:核心插件深度解析索引 (源码级机制拆解 + 自定制插件完整清单)。

六个包构成主干,在 Cordis 树上挂出各自的 ctx 服务:

拥有ctx key
core/sessionappend-only 的 SessionEvent 日志 + 内存存储ctx.sessions
core/system-prompt提示词区块与工具 schema 组装ctx.systemPrompt
core/tools带作用域的工具注册表 + 守卫的执行管线ctx.tools
core/agentAgent 接口、活体注册表、agent/* 事件ctx.agents
core/agent-loop默认驱动,实现 Agent 接口ctx.agentLoop
core/scopeper-agent 作用域注册原语(scope / agent.ctx)纯库,无 key
llm/llm消息与流词汇 + 适配器缝(ctx.llm)ctx.llm

作用域(Scope)机制

每个活体 agent 拥有自己的 agent.ctx:通过它注册的贡献(工具、提示词区块、变量、限制、监听者)既是 scope 可见又是 scope 生命周期 —— 一个事实同时驱动两者。影子(shadowing)规则:同名注册时最具体者胜出, 一个 agent 可以拥有自己的 persona 与工具变体。作用域是扁平的(不向子 agent 继承),子树行为用 lineage 数据表达。

06回合流:step 与 turn

step = 一次模型请求 + 其响应引发的工具执行;turn = 零或多步,打开于首个输入被认领之前, 在不再欠任何响应时关闭。绿色 = 持久化会话事件(turn/* step/* user/message assistant/* tool/*), 紫色 = 活体扩展点。

turn/start
回合开始(持久化)
▼ 认领下一个 step 的输入 + 一条排队消息(inbox)
agent/pre-step · waterfall
决定模型看到什么:可改写认领的消息,或整体拒绝(拒绝/空首认仍会关闭一个零步 turn,日志如实记录)
被拒绝或重写为空?
是 → 关闭 turn,零步结束
step/start
步骤开始
user/message
追加进入的消息
deriveMessages()
从会话日志投影模型历史(每步重读提示词区块与工具 schema)
agent/request · waterfall
请求前拦截
llm/stream · waterfall
流式对话
assistant/chunk* → assistant/message
助手输出落盘(保留原始 chunk 供回放/UI 保真)
tools/pre-execute → tools/execute → tools/post-execute · waterfall
守卫的执行管线
tool/result*
工具结果落盘
step/end
步骤结束
工具仍欠请求,或新输入到达?
是 → 认领 → 下一个 step;否 → 继续
agent/turn-stopping · serial(无 next())
回合停止前的终态钩子
turn/end
回合结束
waterfall 监听者必须调用 next() 委托,否则短路整条链;agent/turn-stopping 是串行事件,没有 next()。
输入通过唯一 inbox 到达驱动:部分消息立即唤醒它;注入的上下文(inject)在 inbox 中排队,直到另一条消息到来。 fork / resume / 转录 / 遥测 / 持久化全部派生自同一条日志流。

07会话日志:模型可见 ⟺ 已落盘

会话日志是模型所看到上下文的唯一来源。核心不变量:

◆ 模型可见即已落盘(Model-visible ⟺ logged) — 任何到达模型请求的东西都必须能从会话日志重建, 且由运行时不变量强制断言。因此新的模型可见输入 = 需要一个新的会话事件:扩展 SessionEventMap,从日志渲染。

会话事件是带类型的(SessionEventMap 声明合并 + 可合并扩展映射),默认"读取即必须知晓"—— 构建时不知道其类型的组件拒绝写入日志,除非事件携带信封的 ignorable: true。 事件 JSDoc 需要 @mode 与 payload @param;只有结构性格式变更才提升 SESSION_FORMAT_VERSION(当前 0)。

08能力缝(Seam):Service Definition / Provider / Consumer

= 可替换能力,恰有三个角色,三者齐备才算一条完整的缝,单独一个角色不是:

SERVICE DEFINITION
声明接口的 Cordis Service 子类
(拥有 ctx.<key> 与词汇类型)
ShellExecutor / WebRuntime
SERVICE PROVIDER(S)
实现该接口
dsh-bash-local / dsh-bash-sandbox
CONSUMER(S)
注入并使用该服务
通常是模型面对的工具
dsh-tool-bash
范例:packages/shell 是标准模板 —— dsh-shell(定义) + dsh-bash-local / dsh-bash-sandbox(提供者) + dsh-tool-bash(消费者)。

一条缝的换供换面:换一个 Provider 就换整个产品。FS 与子进程 Provider 共享同一个执行世界 —— 把它们指向远端沙箱,Bash / PTY / LSP 一起跟着走,无需分叉任何 Provider。Subagent Provider 家族同样如此: 从全新子 agent 到另一产品中的委托回合,共用同一个接口。

◆ 角色独立演化时才拆分包;同一包可同时拥有多个角色(如 dsh-llm 拥有自己的 Service Definition 与 Consumer)。 设计 Service Definition 时面向所有现有 Consumer,勿让单个 Consumer 绑架服务契约。

09事件域:三种扩展点

选对事件域是大多数改动的第一步:

携带何时使用
会话事件(session/event 广播) 可持久事实(turn/* step/* user/message assistant/* tool/*) 事实必须在重载后存活时
Agent 事件(agent/*) 活体 Agent:inbox、step、状态、请求、校验、续行 观察或拦截进行中的工作
能力事件(fs/* tools/* telemetry/*) 把策略与适配器挂到一条缝上,无需 import 循环 扩展能力而不触碰循环

带作用域的分派:关于某个 agent 活动的事件用该 agent 的 carrier 分派(只放行未打标签的监听者 + 该 agent 自身的); 关于注册表本身的事件(如"加了工具")保持不过滤。完整事件表见 docs/event-producer-consumer.md

10与传统 Agent 框架的异同

以 LangChain / LangGraph、AutoGen、OpenAI Agents SDK 等为代表的主流框架作对照: 概念词汇高度对齐(loop、tools、LLM 适配、提示词、记忆、子 agent、人工介入一应俱全), 但每个组件在 dsh 里的地位不同 —— 没有任何一个组件是框架内置的"特权核心"。

传统框架的组件dsh 的对应物地位差异(最关键的一列)
Agent loop(内置循环,框架核心) ctx.agentLoop(core/agent-loop 插件) 循环不是特权核心;dsh-agent-loop 与任何插件地位相同,可从配置整体替换
Memory / chat history + checkpointer(附加组件) append-only 的 SessionEvent 日志 + 持久化缝(JSONL / SQLite) 记忆不是模块而是唯一权威源:模型历史、回放、UI、遥测、fork 全部由日志派生
Tool registry + 框架回调/钩子 ctx.tools 作用域注册表 + tools/pre→execute→post 瀑布 拦截是事件而非回调:注册即副作用、卸载即回滚;可同时存在多个独立拦截者
LLM client 封装 ctx.llm 能力缝(Definition / Provider / Consumer) 换 Provider 就是换整个产品(DeepSeek / Pi-ai / retry / token-meter 同为适配器)
Prompt template ctx.systemPrompt 区块组装 每个 step 由已注册区块重新组装,区块本身可被作用域化/影子化
Graph / state machine 编排(代码级) cordis.yml 声明的插件树 + profile/bundle + patch + preset 组合是配置不是代码:任意行可被上层 patch 覆盖,运行时可 dump-config 自省
Subagent(通常内建) subagent 能力缝(进程内 / fork / Claude Code / Codex / ACP / SDK…) 一个接口多种委托:从全新子 agent 到"另一产品中的委托回合"
HITL / interrupt interaction 缝(审批 / 命令 / ask-user)+ agent/turn-stopping 人类协作同样是插件;连"停回合"都是事件而非框架特权
Checkpointer / persistence(常为可选) session 数据面(投影、标题、遥测、检索均在其上) 持久化是产品能力而非附加件,且后端可换(JSONL ⟷ SQLite)
沙箱 / 权限(通常在框架之外) sandbox(bwrap/Landlock/Seatbelt)+ guard + 审批 安全边界是架构内一等公民,通过同一套事件与缝接入

循环本体的直观对比

传统框架的 loop(内置、不可替换)
while (用户有输入) {
  history = memory.load()
  prompt  = template.render(history)
  reply   = llm.chat(prompt)        // 框架内部
  if (reply.hasToolCall)
    result = tools.execute(reply.calls)
  memory.append(reply, result)
}
// 想改循环?fork 框架或在回调里绕
dsh 的 loop(事件驱动、逐环可替换)
turn/start
  → agent/pre-step      // 瀑布:可改写/拒绝输入
  → deriveMessages()    // 从会话日志投影
  → agent/request       // 瀑布:请求前拦截
  → llm/stream          // 瀑布:流式对话
  → tools/pre→execute→post  // 瀑布:守卫管线
  → step/end
  → 欠请求?循环 : agent/turn-stopping → turn/end
// 每一环都是事件:监听者就是扩展点
◆ 最深刻的三个差异 — ① 循环可替换:传统框架把 loop 当不可动摇的核心,dsh 的 agent-loop 只是又一个插件,扩展点是事件而非回调; ② 单一权威状态:传统记忆是附加模块、容易与 UI/回放/遥测分叉,dsh 的会话日志是唯一真相,一切视图派生自它; ③ 组合即配置:传统在代码里组装 graph 与回调,dsh 的一切都是 cordis.yml 行,行可被 patch、可被 preset 替换。
◆ 不变的共性 — 核心循环语义完全一致:模型请求 → 工具执行 → 观测 → 再请求;组件词汇对齐:loop、tools、LLM 适配、提示词、记忆、子 agent、人工介入、持久化 —— dsh 不发明新概念,而是把每个概念从"内置模块"降格为"可替换插件"。

11扩展点总表:新行为放哪里

目标机制
加一个模型 Providerctx.llm 注册其适配器
加一个模型面对的能力ctx.tools 注册;schema 自动加入提示词组装
给单个会话不同的能力集组合 agent preset;其中的服务行需要 isolate realm
加 shell 执行注册 ctx.shell 后端;本地后端经 ctx.subprocess 派生进程
加持久化终端执行注册 ctx.terminals 后端 + dsh-tool-terminal
加人类命令注册 ctx.commands;不经模型回合直接分派
加后台工作注册 ctx.jobs;job_* 工具收集或停止
加文件系统访问/策略注册 ctx.fs provider 或监听 fs/* 事件
限制派生进程ctx.sandbox 后端;消费者在 spawn 前包装 argv
拦截请求 / 工具 / 回合用对应 agent/*tools/* 事件;agent/turn-stopping 停回合
加模型面对的上下文调用 agent.inject();落入下一次被受理的请求
加 UI / 编辑器集成驱动 ctx.agents;从 session/event 渲染
加 Web Chat 节点注册 ConversationNodeDefinition + keyed renderer
加持久会话状态扩展 SessionEventMap;从日志渲染与回放
生成会话标题注册唯一的 ctx.sessionTitle provider
同会话目标管理ctx.goals;经 agent/* 续行
fork 活体会话ctx.sessions.fork(source, boundary?, childSessionId?)
把注册限定到单个 agent使用该 agent 的 agent.ctx

12包分组一览

职责发布预期
core/产品 API 脊梁:会话、提示词、工具、agent 服务与具体循环Product · 稳定
llm/LLM 能力家族:抽象服务 + Provider 适配器(DeepSeek / Pi-ai / retry / token-meter)Product
api/ · typert/远程 BFF 组装、Typert RPC 网关;类型图生成/加载/注册表Product
session/ · session-query/持久化缝(JSONL/SQLite)+ 投影、标题、遥测;检索家族(全文搜索等)Product
subprocess/ shell/ terminal/进程执行缝家族:Bash 执行器缝、持久 PTY 缝、进程树Product
fs/ lsp/ web/ skill/ compaction/ subagent/ jobs/ workflow/能力缝家族:定义 + Provider + 模型面对工具Product
sandbox/进程束缚缝:bwrap / Landlock / Seatbelt 后端Product
context/ preset/ plan/ goal/ schedule/请求上下文、per-session 组合、计划协作、同会话目标、定时续行Product
guard/循环卫生:重复调用提醒 + tools/execute 超时执行器Product
e2b/E2B 沙箱 POCPOC
hooks/Claude Code / Codex hook 桥 + 线协议库Product
sdk/ acp/ interaction/JSON-RPC 协议+客户端+服务端、自动化 ACP 服务器、审批/命令/ask-userProduct
boot/ host/ client/启动胶水、Web 宿主半边(API 网关+路由)、浏览器半边(shell/wire/对象服务)Product
bundle/可安装的 dsh --profile patch 层(base / web-app / headless)Product
examples/ test-support/ util/Demo bundles、测试设施(testkit / invariant / replay / loader smoke)、零依赖工具(Branded<B> 等)Support

依赖方向铁律:扩展插件依赖 Service Definition,绝不依赖具体 Providerdsh-agent-loop 可换; UI、hook、工具插件只用 dsh-agent。依赖图是生成的:docs/module-graph.md(pnpm run gen-module-graph,CI 门禁新鲜度)。

13工程约定(节选)

14质量门槛

门禁内容
testvitest 单元测试
test:coverageCI 覆盖率门禁:packages/*/*/src 逐文件 100%(不是 test,才是门禁)
test:e2e真实 API 测试;无 DEEPSEEK_API_KEY 自动跳过
test:snapshot无 key 的 ACP/headless 重放,对比期望输出 —— 每个非平凡的产品行为变更都要求配套快照
typecheck / lint / duplication严格 TS、lint、跨文件克隆检测
hygieneknip + publint + workspace 约束 + NodeNext 消费者检查
doc-sync全部文档门禁(链接、wrap、预算、JSDoc、类型等值)
website:buildVitePress 构建(兼死链检查)
Agent Notes每个非平凡变更必须带 Agent Note(.agents/notes/,按 implemented/ / rejected/ / archived/ 分类)
双语文档中英成对(.i18n.yaml 驱动),同一事实只有一个家

15术语速查

turn / step / round
turn — 会话中一次受纳输入的排空,模型与其工具停止或终态策略介入后结束;step — 一次模型请求 + 其响应引发的工具执行,turn 含零或多步;round — 外层策略的一次迭代(如 goal round 或 Ralph fresh-agent 尝试),round 计数属于该策略。
scope / scope key / shadowing
per-agent 注册单位;key 是活体 agent 自身(对象同一性比较);同名最具体者胜出,scoped 注册不向子 agent 继承。
goal / goal activation
附着于现有会话的一个持久完成目标,带修订版 active/paused/blocked/complete 相位与 round 上限;activation 故意不进入持久重放 —— resume/fork 需要后续人类授权。
human command
斜杠前缀的人类指令,经 ctx.commands 解释执行,不成为模型消息(与模型工具、shell 执行都不同)。
Ralph loop
面向不可变目标的 fresh-agent 工作流:每轮一个全新子会话、无父代/前轮的对话种子,共享工作区 + 有界结构化 handoff 传递跨轮状态。
Model Experience
每个产品包 README 的固定章节:从模型视角写提示词、工具 schema、结果与诊断,只含任务相关概念,不含 UI/传输/实现词汇;稳定模型可见文本逐字固定。

16资料来源