wth2jhl.online

Back

📘 原文信息

一、为什么这个项目值得研究#

2026 年做 Agent 框架的厂商很多,但 DeepSeek Harness 走了一条不一样的路:它不把 Agent 当作一个写死的循环,而是把构成 Agent 的每一块能力——会话存储、提示词组装、工具注册表、模型适配器、Agent 循环本身——全部做成可挂载、可拆卸、可替换的插件。这套架构背后的设计文档叫 A Programming Paradigm for Spatiotemporal Composability ↗,它在描述 Cordis 时提出了一个有点哲学的命题:任何一个能力都应该可以被”加载 → 观察/拦截 → 卸载”,而卸载要回滚全部副作用。

更值得注意的是它的工程纪律。仓库根目录的 AGENTS.md 直接写着:

With no external consumers, prefer the correct foundation over compatibility shims: rename or repackage freely and update every reference together.

也就是说优先做对的基础,而不是堆兼容垫片。这种”先正确、再稳定”的开发哲学,本身就值得学习。

下面我按”架构总览 → 启动模型 → 核心数据流 → 关键子系统 → 扩展点 → 实操”六层带你拆解它。

二、仓库布局:三层 + 一条 Vendored 链#

deepseek-harness/
├─ vendor/           # vendored Cordis 源码(不是 npm 依赖,直接链入)
├─ packages/         # 219 个 @deepseek-ai/dsh-* 工作区包
│  ├─ core/          # 产品 API 主干:session/system-prompt/tools/agent/agent-loop
│  ├─ llm/           # LLM 能力接缝 + DeepSeek provider
│  ├─ bundle/        # 可安装的 dsh --profile 补丁层(base / web-app / headless)
│  ├─ shell/ fs/ lsp/ skill/ web/... # 一个个能力接缝,每个都是 Service Definition + Provider + Consumer
│  ├─ api/ typert/   # 远程 BFF + 类型图生成器
│  ├─ boot/ sdk/ examples/ support/ util/
│  └─ session/ interaction/ subagent/ workflow/ ...
├─ apps/             # 产品组装:cli、web
├─ examples/         # 可运行的 cordis.yml 叶节点
├─ python/           # Python SDK + 打包运行时
├─ native/           # Landlock 启动器(沙箱)
└─ website/          # VitePress 文档站
plaintext

几个关键观察:

  • packages/ 不是按业务领域切的,而是按能力接缝切的。shell/、fs/、lsp/、subagent/、workflow/ 这种目录的命名,都在表达”我是一个可替换的能力”。
  • vendor/ 直接链入 Cordis。pnpm-workspace.yaml 里用 link:vendor/cosmokit、link:vendor/schemastery 把它当本地源用,这样既能跟随上游节奏,又能随仓改。
  • .agents/notes/ 里有大量 Agent Notes(架构决定、bug 修复、归档决策),这是一个把”团队工程知识沉淀在仓库里”的实践,可以借鉴。

三、Cordis:5 个核心概念#

Harness 几乎所有架构语义都建立在 Cordis 上,理解 Cordis 是理解 dsh 的前提。仓库的 docs/cordis-primer.md 给出了”五个想法”:

  1. 插件 = 实现 Service 的对象。可以是带可选 inject 和 apply(ctx) 的函数,也可以是 Service 子类。
  2. Context = 服务的注册表。一个插件在某个 context 上声明 ctx.tools、ctx.llm、ctx.sessions 等 key,别人按 key 找。
  3. inject 声明依赖。Cordis 会等服务实际存在后再激活插件,加载顺序由依赖关系决定,而不是手写 boot 序列。
  4. 类型化事件(typed events)。通过 TS declaration merging 声明,分发模式有四种:emit(fire-and-forget)、waterfall(中间件链)、parallel(并发 fan-out)、serial(有序串行)。事件本身是契约,文档里会用 @mode 标签标注。
  5. 注册 = 可逆的副作用。ctx.effect()、ctx.on() 注册的东西都会返回 disposer,卸载时按逆序回滚。

这里有一个非常容易被忽视的细节:waterfall 是”中间件式”分发——监听器收到 (...args, next),next() 把控制权交给下一个,return 不调用 next() 就是短路。这跟 Express、Koa 长得像,实质上是把”修改请求 / 决定 / 上下文”作为核心动作,再委托。这条性质让”策略可以拦截、可以放行、可以替换”的语义变得自然。

四、Profile + Bundle:启动时如何把插件树组装起来#

一个跑起来的 dsh 不是”一个进程 + 一堆 import”,而是按顺序叠加的插件树。这棵树有三层概念:

  • Profile:存在 Harness home 里的命名组合,列出它要叠加哪些 bundle,放自己的 cordis.patch.yml。
  • Bundle:Cordis 配置行 + 它挂载代码的”发行包”,被它插入的行仍然可以被上一层 patch。
  • dsh-base、dsh-web-app、dsh-headless:三个出厂 bundle。dsh-base 是每个 profile 的第一层(模型适配器、工具、持久化、沙箱/审批、设置、凭据、遥测),dsh-web-app 加浏览器前端,dsh-headless 加一次性 runner、没有 server。

叠加顺序是固定且关键的:

空列表 → profile 里列的 bundle(按顺序)→ profile 的 cordis.patch.yml → home 的 cordis.patch.yml → --patch 命令行覆盖。

每一条 patch 都按 id 精准替换某一行的配置,或者插入新行。可以一眼看出当前启动的插件树:

dsh --profile web --dump-config
sh

这非常像”Linux 的 initramfs 按层叠加 + overlayfs”——底层是默认配置,上层是补丁,所有行为都是可追溯的。这种”配置可解释 + 可叠加 + 可回放”的设计,是它在 DevOps 友好度上的核心优势。

五、Turn / Step:Agent 的状态机#

Agent 循环是 dsh 最核心的设计。我把 docs/architecture.md 里的状态机画成下面这张表:

阶段触发事件关键动作
Turn 开启turn/start领取 next-step 输入 + 一条 next-turn 消息
Pre-stepagent/pre-step决定模型能看到什么;可改写或拒绝
Step 开启step/start把进入的消息记为 user/message,从日志派生模型历史
模型请求agent/request → llm/stream → assistant/chunk* → assistant/message流式生成
工具调用tool/call* → tools/pre-execute → tools/execute → tools/post-execute → tool/result*工具三段拦截
Step 结束step/end判断是否还要再来一个 step
Turn 收尾agent/turn-stopping → turn/end关闭 turn

几个非常重要的不变量:

  • Pre-step 是 waterfall(中间件式):监听器可以改写、可以拒绝。被拒绝或空消息也会关闭一个没有任何 step 的 turn,日志会如实记录这次尝试。这一点对调试和审计非常关键。
  • 每一步都从日志派生历史。assistant/chunk 原始事件也保留,所以能重放、能完整渲染 UI。“模型看得到的东西,必须能从日志重建” 是这个项目的硬约束——任何新的模型可见输入,都必须先扩展 SessionEventMap,然后从日志渲染。
  • 三个工具事件是 waterfall,所以可以做权限拦截、可以做改写、可以做包装。agent/turn-stopping 是 serial 且没有 next(),它的语义就是”停下来,不要再继续”。

prepend: true 这个参数仅在你必须排在普通注册之前时才用——默认不要 prepend,这是 Cordis 的实用规则。

六、Agent Handle:对外暴露的统一接口#

Harness 的代码组织里有一个非常清晰的边界设计:Agent 是接口,agent-loop 是默认实现。所有 UI、Hook、编排器都只对 Agent 接口编程,从不直接 import agent-loop。

关键设计:

  • Inbox 是双队列:next-turn(普通的后续 turn)和 next-step(本 step 内立刻插入)。所有插入、替换、清除都会产生持久的 agent/inbox/spliced 事件,整套协议是事件源化的。
  • followup / steer / inject 是 send 的固定预设,而不是各自独立的入口。这是用同一套语义覆盖三种典型用法的简化手段。
  • runMaintenance 必须在 idle 阶段调用,会同步启动并保留 signal;它的承诺是”非 turn 维护任务跑完再 idle”。
  • whenIdle() 观察的是”整个 agent”,而不是单个 turn——它会跟随”在观察到的 driver 退休前启动的替换工作”。这个语义对上层编排非常重要:如果你要把”收到 idle”当作一次 run 结束,需要显式拥有这个区间。

七、Session 日志:整套设计的真相之源#

Session 日志是 dsh 的”事件源数据库”。所有模型可见的事实都先写日志,UI、投影、fork、resume、遥测都从这条流派生。它有几个关键能力:

  • append-only:只能追加,不能改写历史。
  • 多格式持久化:支持 JSONL、SQLite(SCHEMA_VERSION 单调递增)。
  • SESSION_FORMAT_VERSION = 0,没有兼容性承诺——开发期可以自由破坏。
  • Fork / Resume 都是从日志切片派生;ctx.sessions.fork(source, boundary?, childSessionId?) 是公开 API。
  • deriveMessages() 是投影:从日志投影出模型历史;assistant/chunk 原事件保留,保证可重放、UI 完整。

这条不变量用一句话表达就是:模型能看到的东西 = 日志能重建的东西。它不是口号,而是有专门的运行时断言(packages/core/session/src/invariant.ts)在守护。

八、能力接缝(Capability Seams):可替换的”形”#

Harness 把”沙箱、文件系统、子进程、LSP、子 agent、shell、终端、网页抓取、压缩、上下文、技能、计划、todo、调度、工作流……”这些能力统一抽象为 Capability Seam,每个 seam 都有三个角色:

  • Service Definition:声明接口(契约)。
  • Service Provider:实现接口(shell-local / shell-pwsh、fs-local / fs-e2b、subagent-fresh / subagent-delegated 等等)。
  • Consumer:使用接口,典型的是”模型可见的工具”。

光有其中任一角色都不算 seam——新增能力 = 设计三个角色。这种”角色分离”的好处:

  • 换一个 provider 就等于换了整套能力。文件系统提供者和子进程提供者共享一个”执行世界”,把 fs 和 subprocess 同时指向一个远程沙箱,等于把 Bash、PTY、LSP 全搬过去,不需要 fork。
  • 工具(模型可见)和能力(系统内部)分开,工具只是”薄薄的 Consumer”,把策略挂在能力层,而不是每个工具自己实现。
  • 同一接口后面可以挂各种实现:本地子 agent、远程 turn、跨产品 turn。

九、扩展点全景#

docs/architecture.md 给出了一张非常完整的”我要做 X,用 Y”的对照表,我精简翻译如下:

目标用什么
加一个模型 provider注册 adapter 到 ctx.llm
给模型加新能力注册到 ctx.tools,schema 自动加入 prompt 装配
给某个 session 一套不同能力组合 agent preset,需要 isolate realm
加 shell 执行注册 ctx.shell 后端,本地版通过 ctx.subprocess spawn
加持久终端注册 ctx.terminals + dsh-tool-terminal
加人类命令注册到 ctx.commands,无需模型 turn 即可分发
加后台任务注册到 ctx.jobs,用 job_* 工具收集/停止
加文件系统访问 / 策略注册 ctx.fs provider 或监听 fs/* 事件
约束子进程用 ctx.sandbox 后端,消费者在 spawn 前包装 argv
拦截请求 / 工具 / turn用对应的 agent/* 或 tools/* 事件
加模型可见上下文agent.inject(),下次 admitted request 时进入
加 Web Chat 节点注册 ConversationNodeDefinition + keyed renderer
加持久 session 状态扩展 SessionEventMap,从日志渲染
给 session 命名注册唯一的 ctx.sessionTitle provider
管理同 session 的目标用 ctx.goals,通过 agent/* 持续
fork live sessionctx.sessions.fork(...)
把注册范围限定到一个 agent用该 agent 的 agent.ctx

架构原则是:新行为挂到已有的扩展点,不要修改 loop 本身。一旦你动了 loop,这张表就要跟着更新。

十、Cordis 实用规则(从 cordis-primer.md 提炼)#

  • 行为封装到插件:工具流水线 → ctx.tools,模型流 → ctx.llm,实时 agent 协调 → ctx.agents。
  • 拦截用事件;直接调用能力用 service 方法。两者不要混用语义。
  • 每个注册都要有 disposer。优先 ctx.effect() 或自带 disposer 的 helper。
  • teardown 顺序重要时,把相关工作放在一个 effect 里,dispose 才会按预期逆序展开。
  • 不要随便 prepend: true,只有”必须排在普通注册之前”才用。
  • ctx.waterfall 适合”中间件式”拦截:合作型监听器通常改写请求对象再 delegate;完全替换结果也是合法语义。
  • 单决策事件的设计就是短路——策略监听器可以 return 不调 next(),纯观察型监听器必须 delegate。
  • cordis.yml 里只允许 !!js,不要写 !js(这是个安全的 loader 配置细节)。

十一、怎么跑起来#

11.1 直接用 npm#

npx @deepseek-ai/dsh web
sh

默认开在 http://127.0.0.1:3080。

11.2 从源码跑#

git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install
pnpm run build
pnpm dsh web
sh

11.3 一次性 headless runner#

DEEPSEEK_API_KEY=... pnpm dsh --profile headless "总结这个仓库并识别主要包"
sh

11.4 自动化协议#

仓库自带 ACP server:

pnpm run demo:acp
sh

可以让 IDE / 编辑器以 Agent Client Protocol 接入。

11.5 Web UI 三步上手#

文档给的步骤非常简洁:

  1. Settings → Models 填 DeepSeek API Key,保存后立即生效,无需重启。
  2. 点击 Choose workspace,把项目目录加进来;没选 workspace 之前 session composer 是灰的。
  3. 发任务,例如:“Summarize this repository and identify its main packages.”;模型会自动读 / 写文件、跑命令、委派、维护 plan,需要审批的操作会先弹审批。

十二、怎么写第一个插件#

docs/cookbook/extension-cookbook.md 给出了标准流程。简化版:

  1. 新建一个包 packages/<group>/<pkg>/,package.json 里 name: "@deepseek-ai/dsh-<pkg>",dsh.bundle 指向 patch 文件。
  2. 写 cordis.yml,声明要注入的 service(ctx.llm / ctx.tools / ctx.sessions 等)。
  3. 写插件代码:函数式 Service,带 inject: ['llm'],在 apply(ctx) 里 ctx.effect(() => ...) 注册副作用,dispose 自动回滚。
  4. 通过 bundle 挂载:在 bundle/base/ 里加一行,或者自己写一个 profile 的 cordis.patch.yml。
  5. 加测试:仓库强制 test:coverage 是 CI 闸门(packages/*/*/src 按文件 100% 覆盖率)。
  6. 跑 pnpm run doc-sync 同步文档,跑 pnpm run hygiene 检查 knip / publint / workspace 约束。

每个 cookbook 都有完整模板,推荐照着 docs/cookbook/adding-a-tool.md、docs/cookbook/adding-an-llm-adapter.md、docs/cookbook/adding-a-package.md 走一遍,基本就掌握了 dsh 的扩展机制。

十三、我从这次拆解里学到的#

  1. “真相之源”放对地方,所有上层就自然简单。把 session 日志作为唯一真相,fork / resume / 投影 / UI / 遥测 全从它派生,不需要任何额外的同步协议。
  2. “事件接缝”比”调用栈接缝”更适合 Agent。Agent 的每个环节都可能需要被拦截、改写、否决,waterfall 事件比函数调用更灵活——因为它天然支持短路、改写、并发、串行四种语义。
  3. “配置可叠加、可 patch、可回放” 是 DevOps 友好的关键。一份 dsh --dump-config 就能看到完整插件树,任何一行都能 patch,任何 patch 都能追溯。
  4. 接口和实现分离要硬性执行。Agent 是接口,agent-loop 是默认实现,扩展插件只 import agent,绝不 import agent-loop,这条规则让 loop 永远可替换。
  5. 能力(seam)是组织单位,不是”业务领域”。把 shell/、fs/、lsp/、subagent/ 当作第一公民目录,目录里同时放 Service Definition / Provider / Consumer,这种组织方式比按业务切更利于扩展。
  6. Developer Preview 期就该做正确的基础。仓库明确写了”无外部消费者时优先做对的基础,而不是兼容垫片”,这种工程纪律在生产项目里会被各种保守妥协扼杀,值得在自己的项目里也尽量保持。
  7. 文档本身就是架构。docs/architecture.md + docs/subsystems/*.md + docs/cookbook/*.md 把”哪里能扩展、怎么扩展”写得明明白白,这种”架构即文档、文档即测试”的纪律是开源项目长期健康的关键。

十四、参考#

  • 仓库 README:README.md / README.zh.md
  • 架构总览:docs/architecture.md
  • Cordis 五想法:docs/cordis-primer.md
  • Turn / Step 状态机:docs/architecture.md#turn-flow
  • Agent 接口:docs/subsystems/core.md
  • Session 日志:docs/subsystems/session.md
  • 工具流水线:docs/subsystems/tools.md
  • LLM 流式:docs/subsystems/llm-streaming.md
  • 提示词装配:docs/subsystems/system-prompt.md
  • 扩展索引:docs/cookbook/extension-cookbook.md
  • 包布局:packages/README.md + 仓库根 AGENTS.md

⚠️ Developer Preview 提示:文档明确写”This project is in developer preview and iterating rapidly. THERE WILL BE COMPATIBILITY-BREAKING CHANGES.” 学习时关注设计哲学,而不要把现在的 API 当成稳定契约。

深入拆解 DeepSeek Harness:把"一切皆插件"的 Agent 框架做到极致
https://wth2jhl.online/blog/deepseek-harness-cordis-agent-architecture/
Author 小吴同学
Published at August 17, 2026