DeepSeek Harness (dsh) 整体架构解析
DeepSeek AI 开源的插件化 Agent Harness —— 基于 Cordis 构建,一切皆插件。
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/
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 --profile web --dump-config — 打印的每一行都可以被你的 patch 覆盖。
04运行形态
dsh web)http://127.0.0.1:3080;由 dsh-web-app bundle 提供dsh --profile headless "task")dsh-headless bundle 提供05核心包:产品 API 脊梁
六个包构成主干,在 Cordis 树上挂出各自的 ctx 服务:
| 包 | 拥有 | ctx key |
|---|---|---|
core/session | append-only 的 SessionEvent 日志 + 内存存储 | ctx.sessions |
core/system-prompt | 提示词区块与工具 schema 组装 | ctx.systemPrompt |
core/tools | 带作用域的工具注册表 + 守卫的执行管线 | ctx.tools |
core/agent | Agent 接口、活体注册表、agent/* 事件 | ctx.agents |
core/agent-loop | 默认驱动,实现 Agent 接口 | ctx.agentLoop |
core/scope | per-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/*),
紫色 = 活体扩展点。
07会话日志:模型可见 ⟺ 已落盘
会话日志是模型所看到上下文的唯一来源。核心不变量:
SessionEventMap,从日志渲染。
会话事件是带类型的(SessionEventMap 声明合并 + 可合并扩展映射),默认"读取即必须知晓"——
构建时不知道其类型的组件拒绝写入日志,除非事件携带信封的 ignorable: true。
事件 JSDoc 需要 @mode 与 payload @param;只有结构性格式变更才提升 SESSION_FORMAT_VERSION(当前 0)。
08能力缝(Seam):Service Definition / Provider / Consumer
缝 = 可替换能力,恰有三个角色,三者齐备才算一条完整的缝,单独一个角色不是:
(拥有
ctx.<key> 与词汇类型)ShellExecutor / WebRuntime…dsh-bash-local / dsh-bash-sandbox通常是模型面对的工具
dsh-tool-bashpackages/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 + 审批 |
安全边界是架构内一等公民,通过同一套事件与缝接入 |
循环本体的直观对比
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 框架或在回调里绕turn/start → agent/pre-step // 瀑布:可改写/拒绝输入 → deriveMessages() // 从会话日志投影 → agent/request // 瀑布:请求前拦截 → llm/stream // 瀑布:流式对话 → tools/pre→execute→post // 瀑布:守卫管线 → step/end → 欠请求?循环 : agent/turn-stopping → turn/end // 每一环都是事件:监听者就是扩展点
agent-loop 只是又一个插件,扩展点是事件而非回调;
② 单一权威状态:传统记忆是附加模块、容易与 UI/回放/遥测分叉,dsh 的会话日志是唯一真相,一切视图派生自它;
③ 组合即配置:传统在代码里组装 graph 与回调,dsh 的一切都是 cordis.yml 行,行可被 patch、可被 preset 替换。
11扩展点总表:新行为放哪里
| 目标 | 机制 |
|---|---|
| 加一个模型 Provider | 在 ctx.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 沙箱 POC | POC |
hooks/ | Claude Code / Codex hook 桥 + 线协议库 | Product |
sdk/ acp/ interaction/ | JSON-RPC 协议+客户端+服务端、自动化 ACP 服务器、审批/命令/ask-user | Product |
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,绝不依赖具体 Provider。dsh-agent-loop 可换;
UI、hook、工具插件只用 dsh-agent。依赖图是生成的:docs/module-graph.md(pnpm run gen-module-graph,CI 门禁新鲜度)。
13工程约定(节选)
- 类型安全 — 处处
strict: true+noImplicitAny;跨类型化同进程边界的值不做运行时校验("信任 TS"),校验放在 parser/config、queued、模型/工具 JSON、持久化/文件、worker、进程、线等真实边界。 - 不透明跨边界 id 全部 branded(
Branded<B>),绝不裸string。 - 类型化事件用声明合并(merge-extensible maps);闭联合以
assertNever收尾,可合并联合走文档化 default。 - 插件内无硬编码可调参数 — 部署可变的选项是
Config字段,可从 cordis.yml 改;协议常量与安全不变量保持固定。 - 误配置大声失败 — 加载时即败(自包含场景),否则在最早可解析点失败;绝不静默跳过缺失引用。
- 显式 > 隐式 — 包边界上的默认值是拥有方实现中显式的
resolve(request): Spec步骤,绝不是在run()里藏一个?? default。 - 发布状态的提交点 — 操作成功后才发出通知/更新派生状态;缓存、提示词、UI 回声、回放、查询视图都从一个权威源派生。
- 完整结果上施加边界 — 字节/词元/条目/时间限制施加在"完整发出或保留的值"(含包装与元数据)上。
- 包边界词汇 — 文档用"response fields / JSON validation / ESM exports"这类精确词,不用隐喻;注释写完整契约而非推理过程。
- ESM everywhere;本地相对导入用
.ts后缀;包间用包名;源码启动经 tsx ESM hook。
14质量门槛
| 门禁 | 内容 |
|---|---|
test | vitest 单元测试 |
test:coverage | CI 覆盖率门禁:packages/*/*/src 逐文件 100%(不是 test,才是门禁) |
test:e2e | 真实 API 测试;无 DEEPSEEK_API_KEY 自动跳过 |
test:snapshot | 无 key 的 ACP/headless 重放,对比期望输出 —— 每个非平凡的产品行为变更都要求配套快照 |
typecheck / lint / duplication | 严格 TS、lint、跨文件克隆检测 |
hygiene | knip + publint + workspace 约束 + NodeNext 消费者检查 |
doc-sync | 全部文档门禁(链接、wrap、预算、JSDoc、类型等值) |
website:build | VitePress 构建(兼死链检查) |
| 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资料来源
docs/architecture.md— 架构总图(改动 packages/ 前必读)docs/glossary.md— 规范术语表docs/agent-lifecycle.md— 序列图;docs/tool-execution-pipeline.md— 工具管线docs/capability-seams.md— 能力缝图谱;docs/event-producer-consumer.md— 事件生产/消费表docs/subsystems/— 每子系统一页的类型定义与语义packages/README.md— 包分组总表;vendor/README.md— vendored Cordis 同步流程