本文结合
deepseek-harness仓库源码与官方docs/cordis-tutorial/教程编写,并在最后一章与 Claude Code、Codex 以及 AgentScope(阿里) 做深度对比。读完你应能:理解 Harness 的设计哲学、跑通从"Hello 插件"到"真实编码 Agent"的完整链路、看懂cordis.yml组合与 HMR,并清楚它与其他主流框架的区别。
一、DeepSeek Harness 是什么

1.1 一句话定义
DeepSeek Harness 是 DeepSeek 官方出品的 Agent(智能体)运行框架 。它不是一个"写死的助手",而是一个可组合的插件运行时 :你用一份 YAML 配置文件(cordis.yml)把"会话管理、系统提示词、工具、LLM 适配器、文件访问、子进程、沙箱、乃至 agent 主循环本身"等全部能力像搭积木一样拼起来。
它的底层是一个名为 Cordis 的微型插件框架(源码 vendored 在 vendor/cordis/)。Cordis 提供一个共享的 Context(上下文),每个能力都是一个挂载到 ctx 上的插件。
一句话定位:
Claude Code 是"产品",DeepSeek Harness 是"框架",AgentScope 是"开发库"。 前者给你一个开箱即用的编码助手;Harness 给你一套可以自由拼装、自托管、可嵌入自己产品的 Agent 引擎;而 AgentScope 给你一套以 Python 为主的"构建智能体应用的库",侧重多智能体与工程化工具链。
1.2 多维度拆解:它到底是什么
要真正理解 Harness,需要从五个维度同时看它:
| 维度 | 它是什么 | 反例(它不是什么) |
|---|---|---|
| 交付形态 | 一套框架 + 一组官方插件 + 一个 CLI/ACP 入口 | 一个封闭、开箱即用的产品 |
| 内核范式 | 基于 Cordis 的插件运行时,一切皆插件 | 一个硬编码逻辑的单体程序 |
| 组合方式 | 声明式 cordis.yml 配置驱动、按 id 装配 |
命令式代码里 new 实例 |
| 能力边界 | 抽象服务(Service Definition)+ 可替换 Provider + 消费者 | 把实现细节写死在调用方 |
| 适用范围 | 可自托管、可嵌入自有产品、可长期会话 | 仅终端交互的助手 |
1.3 设计思路:为什么这么设计(核心分析)
DeepSeek Harness 采取"一切皆插件"的设计思路。我们采用插件式开放架构来构建 Agent Harness:模型、工具、技能、会话、沙箱、存储、循环、调度、UI 等所有 Agent 能力均由插件组合而成,可自由替换、灵活重组。
Harness 的设计不是"为了可插拔而可插拔",而是围绕几个明确的工程目标与约束做出的系统性取舍。下面逐条分析。
思路一:用"一切皆插件"消除硬编码的循环逻辑
绝大多数编码助手的 agent 主循环(call model → run tools → repeat)是写死在核心里的。Harness 的反直觉决定是:整个仓库只有 dsh-agent-loop 一个包包含具体循环逻辑 (packages/core/agent-loop/README.md 明确如此),其余全是抽象服务或扩展点插件。
设计动机:
- 循环只描述"驱动协议",不掺带具体能力。hooks、sandbox、plan mode、retry、subagent、compaction 等行为全部通过监听
agent/*、tools/*、session/*事件实现,而非改循环代码。 - 这带来"行为在扩展点上、不在循环里"的硬约束------
AGENTS.md原话:"Plugins, not loop changes: new behavior goes on documented extension points; changingagent-looprequires updating docs/architecture.md." - 底层原理:当循环成为唯一且稳定的驱动者,所有可变行为都被推到事件订阅侧,于是能力的增删=插件的挂载/卸载,与主循环彻底解耦。
思路二:能力分层的"三角色"模型(capability-seam)
每个能力被刻意拆成三个相互独立演化 的角色(见 docs/glossary.md#capability-seam):
- Service Definition (服务定义):拥有
ctx.<key>与词汇表类型的 CordisService,是抽象类或具体注册表(如ShellExecutor、WebRuntime),绝不是 TypeScriptinterface------因为它要作为真实服务被挂载。 - Service Provider (服务提供者):一种或多种实现,如
dsh-shell-local/dsh-shell-pwsh。 - Consumer (消费者):注入该服务、面向模型暴露工具的插件,如
dsh-tool-bash。
设计动机:
- 角色独立演化:当只有 provider 需要换(本地→沙箱→E2B)时,定义和消费者代码一行都不用动。这把"变化"限制在最窄的边界内。
- 以 shell 为例:
dsh-shell(定义)→dsh-shell-local/dsh-shell-pwsh(provider,按平台disabled)+dsh-bash-sandbox(沙箱 policy)。LLM 同理:dsh-llm(定义)→dsh-llm-deepseek(原生)/dsh-llm-pi-ai(多 provider 孪生)。 - Swappable capability:seam 是"完整能力",不是单个角色------文档特别强调"reserve the term for that meaning",因为误把某一角色当能力,会导致消费者直接依赖实现而破坏可替换性。
思路三:注册即副作用(effect),把生命周期交给框架
AGENTS.md 铁律:"Registrations are effects: every contribution goes through ctx.effect() / ctx.on(); a registry's register() returns the disposer."
设计动机:
- 插件不持有自己资源的"拆除责任" 。任何注册(
ctx.tools.register、ctx.on、子插件、服务实例)都附着在调用它的插件上,插件卸载时自动撤销。 ctx.plugin(child)让一个插件把另一个插件挂为"子",父子一起 dispose,递归卸载。- 底层原理 :资源所有权 = 插件生命周期,而非手动
if分支。这从机制上消灭了"忘记移除监听器/关闭定时器"这类资源泄漏------docs/defensive-patterns.md把"Dispose must reach quiescence"列为头号缺陷类规则:拆除要异步 await 到真正静止,而不是只发一个 kill。
思路四:依赖注入是"持续跟踪",而非一次性检查
消费方写 inject: ['tools'],Cordis 会让插件保持 PENDING 直到 ctx.tools 存在,且运行期若服务消失(provider 被卸载/热替换),依赖方随之卸载,服务恢复后再加载。
设计动机:
- 配置可替换服务 :卸载
dsh-shell-local、挂载另一个shellprovider,所有inject: ['shell']的插件自动重启用新实现------这就是"框架级热替换"的物理基础。 - 顺序无关 :
cordis.yml里插件行序不影响正确性,只影响就绪先后。彻底移除某服务后,依赖方保持 PENDING,既不崩溃也不会半运行。 - 底层原理:依赖图是运行时动态满足的,而非构建期静态绑定,因此组合(composition)本身是数据(YAML),不是代码。
思路五:"模型可见 ⟺ 已记录"的审计约束
AGENTS.md 硬约束:"Model-visible ⟺ logged: anything that reaches a model request must be reconstructable from the session log; a new model-visible input requires a session event."
设计动机:
- 任何送达模型的输入(工具结果、系统提示词切片、变量)都必须对应一条会话事件,使得会话日志即真相(source of truth) ------可以回放、审计、fork、resume。
- 会话是一等公民:
session(JSONL/SQLite 持久化、投影、血缘)、session-query(SQLite 全文检索)、compaction(压缩+工具结果裁剪)共同支撑长期记忆与合规。 - 底层原理:把"可重建性"上升为架构不变量,而非依赖开发者自觉。这使得调试一个错误回答 = 重放那条会话事件流,而不是猜测模型当时"看到了什么"。
思路六:显式优于隐式,错则明报,绝不静默
AGENTS.md 多项规则:
- "Misconfiguration fails loud at load when self-contained, otherwise at the earliest resolvable point; never silently skip a missing referent."
- "No hardcoded tunables in plugins: deployment-varying choices are validated
Configfields changeable from cordis.yml." - 跨边界不透明 id 用
Branded<B>品牌类型,非裸string;只在校验边界 (config、模型/工具 JSON、文件、worker、进程、线)做运行时校验,同进程类型边界信任 TypeScript。
设计动机:
- 可部署性 来自"可变项都是可校验的 Config",而非代码里
?? default的隐藏默认值(那是协议常量/安全不变量才固定的)。 - 可诊断性来自"缺引用就明报",避免"插件没反应却不知道为什么"(见教程第九章诊断器)。
- 底层原理:把"部署差异"与"安全不变量"两类变化分离------前者进 Config 受 schema 校验,后者写死且不可被配置绕过。
思路七:安全是分层的,而非单点
docs/defensive-patterns.md 给出具体规则:
- 生成命令拿到的是清洗过的环境 (丢弃
*KEY*/*SECRET*/*TOKEN*/*PASSWORD*),防止 harness 凭据泄漏进输出或 spill 文件。 - 临时/spill 文件用私有(0700)目录、随机名、独占 owner-only 打开(
'wx'、0o600),避免可预测路径导致的 symlink 竞争与泄露。 - 沙箱是一等能力:
sandbox(bwrap/Landlock/Seatbelt),执行与文件系统访问都可套沙箱 policy。
设计动机:把"执行不可信输出"当头等威胁,从环境、文件、进程三层同时设防,而非依赖"用户别跑奇怪命令"的约定。
1.3.1 一切皆插件设计

1.3.2 多种运行模式
针对不同的使用场景,DeepSeek Harness 提供四种模式,每种模式会默认加载不同的插件集合:
- 标准模式:提供完整的工具组合;
- PTC 模式:程序化工具调用(Programmatic Tool Calling),由模型生成的一段代码来组合多轮工具调用;
- 极简模式:仅保留一个 shell 工具与一个文件编辑工具,用于最小环境下的模型基准测试;
- 创造模式:可以检查当前运行时、在内存中试验 Cordis 插件,并据此组合和创作新的模式。

1.4 这七个思路如何收敛为一个系统
把以上七点串起来,Harness 的设计主线是:
用插件运行时(Cordis)承载一切能力,用"抽象服务 + 可替换 provider"隔离变化,用 effect 把生命周期交给框架,用注入的动态满足实现配置驱动的组合,用事件把行为推到扩展点,用会话日志作为可重建的真相,用显式校验与安全分层守住部署与执行边界。
它因此呈现出与 Claude Code / Codex(产品)、AgentScope(Python 开发库)截然不同的取向:前者关心"用户开箱即用",后者关心"研究者快速搭多智能体",而 Harness 关心的是 "平台/产品工程师如何可靠地自托管并长期演化一个 Agent 底座" 。这也是为什么它的工程纪律极严(100% 覆盖率门禁、type-equiv 文档同步、品牌类型、声明式 surface),因为底座的可靠性是上层一切的前提。
二、核心心智模型:一切皆插件
第一章回答了"为什么这么设计 "(七条思路 + 设计动机);本章回答"它实际怎么跑起来 ",并用一张整体架构图把这七条思路落到物理结构上。如果你跳过了第一章,只需记住一句话:Harness 里没有"写死的助手",所有能力都是挂在共享
ctx上的插件。
DeepSeek Harness 基于具有时空可组合性的 Cordis 插件系统构建。Cordis 元框架只负责插件的加载与卸载以及依赖关系,Agent Harness 的所有具体组件都是不同的 Cordis 插件。插件通过 Cordis 服务与事件彼此协作,并可以在配置层自由组合。
开发者无需改动 DeepSeek Harness 的源码本身,就能以插件的方式独立选择、替换或扩展其中的任一能力。这就是 DeepSeek Harness 最重要的设计原则:一切皆插件。
整个仓库遵循一条铁律(见 AGENTS.md):
Everything is a plugin. 一切皆是插件。
这意味着:
- 工具是插件(
dsh-tools) - 大语言模型适配器是插件(
dsh-llm+ DeepSeek provider) - 文件系统访问是插件(
dsh-fs) - shell / 子进程 / 终端是插件(
dsh-shell/dsh-subprocess/dsh-terminal) - 连 agent 主循环(agent-loop)本身都是可替换的插件 (
dsh-agent-loop)
所有插件共享同一个 ctx,通过三种机制协作:
| 机制 | 关键字 | 作用 |
|---|---|---|
| 依赖注入 | inject: ['tools'] |
插件声明它依赖某服务,Cordis 在该服务就绪后再启动它 |
| 注册 / effect | ctx.effect() / ctx.on() |
插件贡献能力(注册工具、监听事件);卸载时自动撤销 |
| 事件 | ctx.on(event, cb) / ctx.waterfall() |
解耦的插件间通信 |
关键设计:注册是"副作用"(effect) 。每个贡献都通过
ctx.effect()完成,插件卸载时贡献自动撤销。这是 Cordis 生命周期管理的核心,也是它区别于"手动管理全局单例"类框架(如很多 Python agent 库)的根本点。

2.1 整体架构图(基于源码)
下图依据 packages/bundle/base/cordis.patch.yml(base bundle 的 45+ 个 id 行)与 packages/core/agent-loop/README.md(agent-loop 注入的 5 个服务)绘制,反映真实组合关系,而非示意。
2.2 图中关系对照源码说明
1. "一切皆插件"的物理形态
- 启动器 =
node --import tsx ../../vendor/cordis/bin.js,它只创建 rootContext并挂载 Loader。 - Loader 读取
cordis.yml或--profile补丁层(dsh-bundle)。base bundle 在packages/bundle/base/cordis.patch.yml里用 45+ 个id行声明了全部默认插件,行序无关(激活由"服务可用性"驱动)。 - 每一行就是一个插件;
id(如agent-loop、tools、llm-deepseek)是 stable 标识,后续补丁层按id覆盖它。
2. agent-loop 是唯一的"具体循环"
- 据
dsh-agent-loop/README.md:整个 harness 只有这一个包包含具体循环逻辑,其他全是抽象服务或扩展点插件。 - 它注入并依赖 5 个接口服务:
agents、sessions、llm、tools、systemPrompt(图中虚线)。这 5 个都是ctx上的服务,具体 provider 可热替换。 - "call model → run tools → repeat"之外的所有行为(hooks、sandbox、plan、retry、subagent、compaction)都通过监听
agent/*、tools/*、session/*事件实现------这就是图中的事件总线。
3. 工具是"注册"而非"硬编码"
bash、fs、web、subagent、workflow、todo、skill等执行/编排插件,通过ctx.tools.register(...)(effect)把工具挂进dsh-tools注册表,再由 agent-loop 在tools/result等事件里消费。两插件互不知对方存在。
4. Provider 可替换三角色
- 以 shell 为例:
dsh-shell(定义)→dsh-shell-local/dsh-shell-pwsh(provider,按平台disabled)+dsh-bash-sandbox(沙箱 policy)。LLM 同理:dsh-llm(定义)→dsh-llm-deepseek(原生)/dsh-llm-pi-ai(多 provider 孪生)。
5. 自修改与互操作
extensions让 agent 运行时装载/卸载插件;hooks-claude-code/hooks-codex桥接外部 Hook;acp暴露自动化协议服务器。这些都在 base bundle 之外,按需叠加。
图中
dsh-agent-loop的 5 条虚线注入、dsh-tools的 7+ 条工具注册、--profile补丁层装配,均直接来自packages/bundle/base/cordis.patch.yml与packages/core/agent-loop/README.md的源码事实。
三、环境准备(5 分钟)
3.1 你需要先知道的背景(新手必读)
本文示例用 TypeScript 写插件,但不需要你精通 TS。只要理解下面四点即可:
- ESM 与
import:代码用import { x } from 'pkg'引入依赖;本文所有相对导入都带.ts后缀(如'./hello.ts'),这是 Cordis loader 的约定。 - workspace 包名 :
@deepseek-ai/cordis、@deepseek-ai/dsh-tools等是仓库内部的 npm 包名(pnpm workspace 解析),不是从网络下载的。import type { Context } from '@deepseek-ai/cordis'就是从 Cordis 取类型。 cordis.yml是 YAML 列表 :每个- name: ...是一项插件;缩进用两个空格,不要混用 Tab。ctx是什么 :贯穿全文的ctx是 Cordis 的共享上下文,所有插件通过它注册能力与监听事件。你可以把它当成"整个运行时的总接线板"。
3.2 环境前置条件
前置条件(详见 docs/development.md):
- Node.js 22.19+ 或 24+(CI 覆盖 22.19 / 24 / 26)
- pnpm (启用 Corepack:
corepack enable),仓库锁定pnpm@11.7.0 - Git 2.26+
- 可选:DeepSeek API Key(
DEEPSEEK_API_KEY),仅真实跑模型时需要;本教程第三至第十章(含 HMR 与工具管线)完全无密钥可运行
bash
git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install
pnpm run typecheck # 验证环境就绪
创建教程临时目录(tmp/ 已被 git 忽略,不会被提交):
bash
mkdir -p tmp/cordis-tutorial
cd tmp/cordis-tutorial
后续所有示例都从这同一个目录运行:
python
node --import tsx ../../vendor/cordis/bin.js
这个单文件启动器会:① 创建根 Context;② 挂载 Loader 插件;③ 从当前目录读取 ./cordis.yml 并加载里面列出的每个插件。无需任何构建步骤(--import tsx 让 Node 直接跑 TS)。
四、动手:你的第一个插件
4.1 写插件
在 tmp/cordis-tutorial 下创建 hello.ts:
javascript
import type { Context } from '@deepseek-ai/cordis'
export const name = 'hello'
export function apply(ctx: Context) {
console.log('hello from my first plugin')
}
- 插件通过命名导出
apply函数被 loader 挂载。 ctx是 Cordis 上下文,插件通过它注册所有贡献。name是可选显示名,用于诊断信息。
4.2 组合应用
创建 cordis.yml:
arduino
- name: './hello.ts'
这是一个配置项列表 。name 是模块指定符(相对路径或 npm 包名)。
4.3 运行
python
node --import tsx ../../vendor/cordis/bin.js
输出:
sql
hello from my first plugin
4.4 三种插件形态
scala
import { Service, type Context } from '@deepseek-ai/cordis'
// 1. 函数插件(最常用)
export function apply(ctx: Context) {}
// 2. 对象插件:带 apply 方法的对象
export const objectPlugin = { name: 'obj', apply(ctx: Context) {} }
// 3. 类插件:Service 子类(需要公开服务时用,见第六章)
export class MyService extends Service {
constructor(ctx: Context) { super(ctx, 'myService') }
}
新手建议:在需要公开服务之前,一律使用函数形态。
4.5 容错行为(新手必知)
- 若插件
apply抛错 → 进程直接崩溃并报错(不会静默跳过)。 - 若
cordis.yml里的模块路径/包名拼错 (解析失败)→ Cordis 只通过 logger 报告,不会崩溃。新插件"没反应"时,先检查拼写。
五、生命周期与 effect(资源自动回收)
Cordis 插件可能因修改配置、热重载、显式资源释放或所需服务消失而卸载。通过 Cordis API 建立的注册属于 effect,会在所属插件卸载时撤销;在这些 API 之外管理的资源必须包装在 ctx.effect() 中。
创建 lifecycle.ts:
javascript
import type { Context } from '@deepseek-ai/cordis'
export const name = 'lifecycle-demo'
function heartbeat(ctx: Context) {
console.log('heartbeat plugin loading')
ctx.effect(() => {
const timer = setInterval(() => console.log('tick'), 200)
return () => {
clearInterval(timer)
console.log('heartbeat cleaned up')
}
})
}
export function apply(ctx: Context) {
const fiber = ctx.plugin(heartbeat)
ctx.effect(() => {
const timer = setTimeout(async () => {
await fiber.dispose()
console.log('disposed')
process.exit(0)
}, 700)
return () => clearTimeout(timer)
})
}
运行后输出:
heartbeat plugin loading
tick / tick / tick
heartbeat cleaned up
disposed
三点关键:
ctx.plugin(heartbeat)把一个来自代码 的函数挂载为插件,与 YAML loader 为每个配置项做的完全一致。调用返回 fiber------已加载插件实例的运行时句柄。- effect 主体在加载期间运行,返回的 disposer 在卸载期间运行。生命周期与插件一致的资源,你绝不需要手动调用 disposer。
fiber.dispose()会等该插件所有清理(含异步 disposer)完成后才结束,并递归卸载它挂载的子插件。
Fiber 状态机
每个已加载插件实例都有 fiber,在以下状态间转换:
markdown
PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED
↘ FAILED
- PENDING:已声明,但所需服务尚不可用(见第六章)。
- LOADING / ACTIVE :
apply正在运行/已完成。 - FAILED :
apply或配置校验抛异常。 - UNLOADING / DISPOSED:disposer 正在运行/已拆除。
已经是 effect 的操作(你很少需要手写 ctx.effect())
ctx.on(event, listener):监听器随插件卸载自动移除。ctx.plugin(child):子插件随父插件一同 dispose。- 服务注册、harness 注册表(如
ctx.tools.register(...))的返回 disposer 都附着在调用插件上,自动撤销。
顺序注意:disposer 按注册逆序 启动,但多个异步 disposer 并发运行;若拆除必须按顺序,请把步骤放进同一个 disposer 内依次 awaits。
六、服务(Service):能力的注册与消费
服务 是插件提供、其他插件通过 ctx 消费的具名能力。在 harness 中,ctx.tools、ctx.llm、ctx.agents 都是服务。消费方只指定 'tools' 这样的能力名,而不导入提供方------因此配置可以选择提供方,无需改动消费方代码。
6.1 提供服务
greeter.ts:
typescript
import { Service, type Context } from '@deepseek-ai/cordis'
declare module '@deepseek-ai/cordis' {
interface Context {
greeter: GreeterService
}
}
export class GreeterService extends Service {
constructor(ctx: Context) {
super(ctx, 'greeter')
}
greet(who: string) {
return `Hello, ${who}!`
}
}
export const name = 'greeter'
export function apply(ctx: Context) {
ctx.plugin(GreeterService)
}
两部分协同:
- 运行时 :
super(ctx, 'greeter')以名称greeter注册实例,ctx.greeter随处可访问;注册属于 effect,卸载时移除。 - 编译时 :
declare module用 TS 声明合并把greeter加入Context接口,使消费方获得类型安全(无此声明运行时仍工作,但失去类型)。
6.2 消费服务(inject)
consumer.ts:
javascript
import type { Context } from '@deepseek-ai/cordis'
export const name = 'consumer'
export const inject = ['greeter']
export function apply(ctx: Context) {
console.log(ctx.greeter.greet('world'))
}
inject 列出该插件需要的服务。Cordis 会让插件保持 PENDING 直到每项服务都存在,因此在 apply 内可保证 ctx.greeter 已就绪------加载顺序无关紧要。
arduino
- name: './greeter.ts'
- name: './consumer.ts'
输出 Hello, world!。交换两行顺序输出不变 ;若彻底移除 greeter.ts(或拼错包名),消费方会保持 PENDING / 启动失败------它既不崩溃、也不会在依赖缺失时半运行,而是明确停在等待状态(诊断器见第九章)。
术语区分 :本例
export const name = 'consumer'是插件显示名 ;inject: ['greeter']里的greeter是服务名 (由super(ctx, 'greeter')注册)。二者命名空间不同------插件可任意取名,但注入必须精确匹配服务名,否则永远 PENDING。
6.3 inject 是持续跟踪,而非一次性检查
若运行期间所需服务消失(如提供方被卸载、热替换),每个依赖插件会随之卸载,服务恢复后再加载。结合 effect,这防止消费方保留对不可用服务的引用。也正是配置能替换服务的原因 :卸载 dsh-shell-local、挂载另一个 shell 提供方,所有 inject: ['shell'] 的插件会重启并用新实现。
6.4 可选依赖
inject 是硬性依赖。缺失仍可工作时跳过 inject 并探测:
javascript
export function apply(ctx: Context) {
const greeter = ctx.get('greeter')
console.log(greeter?.greet('maybe') ?? 'no greeter available')
}
原则 :扩展插件依赖 Service Definition(抽象服务) 而非具体 provider。这样 LLM 适配器、执行器等都能热替换、互不影响。服务名共用扁平命名空间,自有服务请加前缀(harness 已占用
tools、llm等)。
七、事件系统:解耦通信与拦截
服务支持直接调用;事件 让插件无需知道谁在监听就能广播。harness 用事件处理工具结果、模型请求、审批决定等交互。
7.1 声明、发出、监听
stats.ts(计数服务):
typescript
import { Service, type Context } from '@deepseek-ai/cordis'
declare module '@deepseek-ai/cordis' {
interface Context { stats: StatsService }
interface Events {
'stats/report'(name: string, count: number): void
}
}
export class StatsService extends Service {
private counts = new Map<string, number>()
constructor(ctx: Context) { super(ctx, 'stats') }
bump(name: string) {
const next = (this.counts.get(name) ?? 0) + 1
this.counts.set(name, next)
this.ctx.emit('stats/report', name, next)
}
}
reporter.ts:
typescript
import type { Context } from '@deepseek-ai/cordis'
import type {} from './stats.ts'
export const name = 'reporter'
export const inject = ['stats']
export function apply(ctx: Context) {
ctx.on('stats/report', (name, count) => {
console.log(`[stats] ${name} -> ${count}`)
})
ctx.stats.bump('tool_call'); ctx.stats.bump('tool_call'); ctx.stats.bump('prompt')
}
import type {} from './stats.ts' 让 TS 看到声明合并(运行时无副作用)。输出:
csharp
[stats] tool_call -> 1
[stats] tool_call -> 2
[stats] prompt -> 1
ctx.on() 属于 effect,监听器随插件消失,绝不需手动 removeListener。
注意
declare module '@deepseek-ai/cordis' { interface Events { ... } }这处声明合并 :它把'stats/report'及其签名写入 Cordis 的全局事件表,于是ctx.emit/ctx.on在编译期就检查事件名与参数类型。漏写声明合并,事件仍是合法的string事件,但失去类型保护------这是基于 Cordis 开发时最常踩的坑。
7.2 五种分发模式
| 模式 | 调用 | 语义 |
|---|---|---|
| emit | ctx.emit(name, ...) |
同步广播;不等待/不收集返回值 |
| parallel | await ctx.parallel(...) |
全部并发并一同等待 |
| serial | await ctx.serial(...) |
顺序等待;首个非 null/false/undefined 胜出并停止 |
| bail | ctx.bail(...) |
serial 的同步版 |
| waterfall | ctx.waterfall(name, ...args, next) |
环绕中间件,可转换或短路 |
7.3 waterfall:转换或短路
typescript
declare module '@deepseek-ai/cordis' {
interface Events {
'demo/transform'(input: string, next: () => Promise<string>): Promise<string>
}
}
// 监听器 1:包裹下游结果
ctx.on('demo/transform', async (input, next) => {
const downstream = await next()
return downstream.toUpperCase()
})
// 监听器 2:拥有决策时短路
ctx.on('demo/transform', async (input, next) => {
if (input.includes('blocked')) return '** blocked **'
return next()
})
await ctx.waterfall('demo/transform', 'hello', async () => 'hello') // HELLO
await ctx.waterfall('demo/transform', 'blocked words', async () => '...') // ** BLOCKED **
纪律:只观察/标注的 waterfall 监听器必须调用 next() ;不调用代表有意短路。日志监听器若忘记 next() 会静默吞掉所有下游默认行为------这是本仓库常设规则。Harness 用 waterfall 处理协作决策:agent/request 允许插件替换模型调用配置,approval/request 允许策略代替用户作答。
八、配置:声明式与明确报错
cordis.yml 每个配置项都可带 config 块,插件导出 schema 在 apply 前校验。错误配置导致加载失败并给出准确错误:插件绝不会在配置不完整时启动。
typescript
import type { Context } from '@deepseek-ai/cordis'
import Schema from '@deepseek-ai/schemastery'
export const name = 'config-demo'
export interface Config { greeting: string; targets: string[] }
export const Config: Schema<Config> = Schema.object({
greeting: Schema.string().default('Hello'),
targets: Schema.array(String).default(['world']),
})
export function apply(ctx: Context, config: Config) {
for (const target of config.targets) console.log(`${config.greeting}, ${target}!`)
}
arduino
- name: './config-demo.ts'
config:
targets: ['alpha', 'beta']
输出 Hello, alpha! / Hello, beta!(未给 greeting 时用 schema 默认值补齐,apply 永远收到完整且已验证的配置)。
传入无效值:
css
config: { targets: 'not-an-array' }
c
ValidationError: invalid config:
- $.targets expected array but got not-an-array (at targets)
fiber 进入 FAILED,启动器打印错误后退出码 1。明确报错优于静默跳过是本仓库的一贯约定。
loader 还支持 !!js 标签用于加载时计算值:
arduino
config:
greeting: !!js process.env.DEMO_GREETING ?? 'Hello'
!!js 仅在 config 与条目 disabled 内有效;disabled: !!js ... 可按平台/环境门控一行(本仓库扩展)。
九、组合、HMR 与诊断
cordis.yml 选择应用的插件树。配置项还能携带 id、disabled、嵌套 group、isolate 等元数据:
yaml
- id: greeter
name: './greeter.ts'
- id: consumer
name: './consumer.ts'
disabled: true # 保留条目但跳过挂载
id 提供稳定标识,使 loader 区分"修改现有项"与"先删后加"。disabled: true 卸载插件而不删条目;改回即连同 PENDING 依赖一起重载。group 可把子列表作为单元加载/卸载;isolate 为组提供某服务名的独立实例(两组各自看到不同配置的 shell 提供方,互不影响)。
9.1 热模块替换(HMR)
卸载释放 effect,加载遵循依赖,因此 HMR 可先卸载再加载以替换运行中的插件。@deepseek-ai/cordis-plugin-hmr 监视文件,保存时执行该过程:
yaml
- id: logger
name: '@deepseek-ai/cordis-plugin-logger-console'
- id: timer
name: '@deepseek-ai/cordis-plugin-timer'
- id: hmr
name: '@deepseek-ai/cordis-plugin-hmr'
config: { root: ['.'] }
- id: hello
name: './hello.ts'
编辑 hello.ts 保存后:
less
hello from my first plugin
2026-07-22 15:44:36 [I] hmr watching [ '.' ]
2026-07-22 15:44:39 [I] hmr reload plugin at hello.ts
hello from my EDITED plugin
旧实例先卸载(effect 回卷),新代码后加载。编辑 cordis.yml 本身也会触发更新:loader 按 id 比较,只改动变化部分。不带 id 的条目每次读取都获新 id,会被当作先删后加重新挂载 ------这就是显式 id 的意义。
9.2 诊断始终不加载的插件
依赖驱动加载的另一面:若 inject 指定了无人提供的服务,它会一直 PENDING、不输出。这不是错误(PENDING 是合法态)。可直接枚举状态:
javascript
import { FiberState, type Context } from '@deepseek-ai/cordis'
export const name = 'diagnose'
export function apply(ctx: Context) {
setTimeout(() => {
for (const runtime of ctx.registry.values())
for (const fiber of runtime.fibers)
if (fiber.state === FiberState.PENDING)
console.log(`${fiber.name} is PENDING --- a required service is missing`)
}, 500)
}
inject: ['timer'] 无提供方时,诊断器会打印 needs-timer is PENDING --- a required service is missing。 "插件没反应"时,先看 fiber 状态。
十、把工具接进真实 Agent
继续用第三章创建的
tmp/cordis-tutorial目录,所有文件都放在这里,运行命令仍是node --import tsx ../../vendor/cordis/bin.js。
这是理解 Harness 的"啊哈时刻":写一个可被模型调用的工具,穿过真实执行管线。无需密钥、不调模型。
10.1 工具插件 greet-tool.ts
php
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
import { CallId } from '@deepseek-ai/dsh-llm'
export const name = 'greet-tool'
export const inject = ['tools']
export function apply(ctx: Context) {
ctx.tools.register(defineTool({
name: 'greet',
description: 'Greet the named person.',
parameters: {
name: { type: 'string', required: true, description: 'Who to greet' },
},
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
async execute(args) {
return `Hello, ${args.name}!`
},
}))
void (async () => {
const result = await ctx.tools.execute({
callId: CallId('demo-1'),
name: 'greet',
arguments: { name: 'Cordis' },
signal: new AbortController().signal,
})
console.log('tool replied:', JSON.stringify(result.content))
})()
}
10.2 观察插件 tool-logger.ts
typescript
import type { Context } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/dsh-tools'
export const name = 'tool-logger'
export const inject = ['tools']
export function apply(ctx: Context) {
ctx.on('tools/result', (exec, result) => {
const text = result.content.map(b => (b.type === 'text' ? b.text : '')).join('')
console.log(`[tool-logger] ${exec.name} -> ${text}`)
})
}
10.3 组合运行
arduino
- name: '@deepseek-ai/dsh-system-prompt'
- name: '@deepseek-ai/dsh-tools'
- name: './tool-logger.ts'
- name: './greet-tool.ts'
python
node --import tsx ../../vendor/cordis/bin.js
css
[tool-logger] greet -> Hello, Cordis!
tool replied: [{"type":"text","text":"Hello, Cordis!"}]
要点:
defineTool把parameters转成给模型的 JSON Schema,并在execute前校验参数。- 日志插件先触发:
tools/result在结果物化过程中发出,早于execute的 promise 兑现。两插件互不知对方------它们被注册表服务与事件连接。 - 工具插件若在组合里缺
systemPrompt提供方,会保持 PENDING (缺依赖),正是inject机制的体现。
此刻你已能读懂
examples/headless-agent/cordis.yml的每一项。真实 agent = 这套组合 + LLM 适配器 + agent-loop + 持久化 + 入口。
10.4 具象化:一次 Agent 循环的时序(思路一 / 思路五)
下面这张时序图直接来自仓库生成的权威生命周期图(docs/agent-lifecycle.md,由 scripts/gen-doc-graphs.ts 产出)。此时你已理解 tools/result、agent/*、session/event 等事件,正好用它把第一章的思路一 (只有 agent-loop 是具体循环、其余行为靠事件订阅)与思路五(任何送达模型的东西都写进会话日志、日志即真相)落到一次真实回合(turn/step)的每一步。
rust
sequenceDiagram
participant User
participant Agent
participant Driver as dsh-agent-loop
participant Hooks as hook listeners
participant Prompt as ctx.systemPrompt
participant LLM as ctx.llm
participant Tools as ctx.tools
participant Session
participant SDK as UI/SDK listener
User->>Agent: followup(content)
Agent-->>SDK: agent/inbox/spliced / agent/inbox/inserted
Agent->>Driver: queued work wakes driver
Driver-->>SDK: agent/status running
Driver->>Session: turn/start
Note over Agent,Driver: claim pending next-step input + one queued prompt
Driver->>Hooks: agent/pre-step waterfall
Hooks-->>Driver: authoritative reject or enter(messages)
alt pre-step rejected / failed
Driver-->>Driver: claimed batch stays removed, turn spends no step
else enter proposed step
Driver->>Session: step/start
Driver->>Session: user/message per entered message
Driver->>Prompt: system-prompt/assemble waterfall
Driver->>LLM: agent/request waterfall → llm/stream waterfall
LLM-->>Driver: StreamChunk*
Driver->>Session: assistant/chunk*
Session-->>SDK: session/event assistant/chunk*
alt adapter/terminal request failure
Driver->>Session: step/end
Driver->>Hooks: agent/request-error waterfall
Hooks-->>Driver: retry action or keep original error
else model request succeeded
Driver->>Session: assistant/message
Driver->>Tools: classify pending call by executionMode
opt call starts
Driver->>Session: tool/call
Driver->>Tools: ordered pre, concurrent execute
Tools-->>Session: tool-owned events when applicable
end
opt next model-order result ready
Driver->>Tools: ordered post
Driver->>Session: tool/result
end
end
Driver->>Session: step/end
opt natural stop + inbox empty
Driver->>Hooks: agent/turn-stopping serial checkpoint
end
end
Driver->>Session: turn/end
Driver-->>SDK: agent/status idle
怎么读这张图(对应两条思路)
-
思路一(一切皆插件,循环只驱动、不实现)
Driver(dsh-agent-loop)只做"取输入 → 发事件 → 等结果"的骨架。hooks、sandbox、plan、retry、subagent、compaction 没有出现在循环体里 ,而是作为agent/pre-step、agent/request、agent/request-error、agent/turn-stopping等 waterfall / serial 事件被外部插件订阅。- 例如
dsh-compaction-basic用agent/pre-step在请求构造前做压力检查、agent/request-error只在上下文溢出时触发裁剪------这些都是"挂"在循环事件上的行为,而非循环内部的分支。这正是"Plugins, not loop changes"。
-
思路五(模型可见 ⟺ 已记录,日志即真相)
- 每一个送达模型 的东西都被记成会话事件:
system-prompt/assemble(提示词切片)、agent/request(模型请求)、llm/stream→assistant/chunk*(流式输出)、assistant/message(一次成功调用)、tool/call/tool/result(工具调用与结果)。 - 这些事件的耐久副本 全在
session/event上(Session-->>SDK: session/event ...),而agent/*只是"活的协调 API"(队列/状态/拦截/转向/续跑/错误)。 - 因此一个错误回答可被完整重放 :从
session/event流重建出模型当时看到的提示词、调用了哪些工具、拿到了什么结果------无需猜测。这就是"日志即真相"的物理落点。
- 每一个送达模型 的东西都被记成会话事件:
一句话:循环负责"走流程",插件负责"加行为",会话日志负责"留真相" 。三者靠事件总线连接,互不硬编码对方。
十一、能力分层与实战运行
11.1 能力分层(Capability Seams)
Harness 把每个能力拆成三角色,各自独立演化:
| 角色 | 职责 |
|---|---|
| Service Definition | 抽象接口(能力"是什么") |
| Service Provider | 具体实现(如本地 / 云端 / E2B) |
| Consumer | 面向模型的工具或调用方 |
以 shell 为例:dsh-shell 定义能力,dsh-shell-local / dsh-shell-pwsh 是 provider,dsh-shell 的模型工具是 Consumer。
完整能力清单(packages/README.md)节选:
- 核心 :
core(session、prompt、tools、agent、agent-loop)、api、typert、sdk - LLM :
llm(抽象 + DeepSeek provider) - 执行 :
shell、subprocess、terminal、code-runtime、sandbox(bwrap/Landlock/Seatbelt) - 工具 :
fs、lsp、web、skill、subagent、workflow、todo、plan - 会话/持久化 :
session、session-query、compaction、storage、attachment - 人机协作 :
interaction(审批/权限/ask-user)、hooks、acp - 自修改 :
extensions(agent 可运行时检视/挂载自己的插件) - 组合分发 :
bundle(dsh --profile补丁层)、preset
设计哲学:可维护依赖优于手写;跨边界 id 用
Branded<B>品牌类型(非裸string);运行时只在校验边界(config、模型/工具 JSON、文件、worker、进程、线)做校验,同进程类型边界信任 TypeScript。
11.2 实战:跑真实编码 Agent
需先构建(详见 docs/development.md)并设置 Key:
arduino
pnpm run build
ini
# 仓库根 .env 或环境变量
DEEPSEEK_API_KEY=sk-...
DEEPSEEK_BASE_URL=https://... # 可选
arduino
pnpm dsh --profile headless "summarize this workspace"
其他演示:
arduino
pnpm run demo:cordis # Agent 检视并修改自己的实时插件运行时
pnpm run demo:acp # 用 JSON-RPC stdio 暴露自动化 Agent 会话(ACP 协议)
dsh --profile 背后是 dsh-bundle 补丁层:base 组合被部署 overlay 修补。
11.3 常用命令速查
bash
pnpm install # 安装 + lefthook 钩子
pnpm run typecheck # 类型检查(pre-push 钩子)
pnpm run test # vitest 单测
pnpm run test:coverage # CI 覆盖率门禁(per-file 100%)
pnpm run lint
pnpm run build # tsc 发射 lib/types + tsdown 打包
pnpm run hygiene # knip + publint + 约束 + NodeNext 检查
pnpm run doc-sync # 文档门禁(含 type-equiv 校验)
pnpm dsh --profile headless "task" # 源码跑任务(需 Key)
pnpm run demo:cordis # 自引用 Cordis 演示(需 Key)
pnpm run demo:acp # ACP 自动化服务器(需 Key)
提交/推送前按
AGENTS.md的"relevant checks"原则,只跑覆盖你所改面的检查,不必无脑跑全量------CI 才负责穷尽覆盖。
十二、优势分析 & 与 Claude Code / Codex / AgentScope 的对比
12.1 三方 + 一框架横向对比
| 维度 | DeepSeek Harness | Claude Code | Codex (CLI) | AgentScope(阿里) |
|---|---|---|---|---|
| 本质 | Agent 框架/运行时 | 闭源产品(编码助手) | 闭源产品(编码 Agent) | 开源开发库(Python 为主) |
| 内核 | Cordis 插件运行时,一切皆插件 | 单体应用 | 单体应用 | 类 + 管道 DSL,ReAct 范式 |
| 语言 | TypeScript(Node) | 未公开 | 未公开 | 主要是 Python |
| 可组合性 | 极高:cordis.yml 拼装,含可换 agent-loop |
低(settings/hooks) | 低(settings/hooks) | 中(组件可换,但靠代码组装而非配置声明) |
| 模型绑定 | LLM 层可替换,默认 DeepSeek | 锁定 Claude | 锁定 OpenAI | 多模型(含通义/OpenAI/本地),模型无关 |
| 自托管/嵌入 | ✅ 完全可自托管、可嵌入产品 | ❌ SaaS | ❌ SaaS | ✅ 开源可自部署 |
| Hook 互操作 | 内置 Claude Code/Codex 桥接 | --- | --- | 无(独立生态) |
| 会话持久化 | 一等公民(JSONL/SQLite/血缘/全文检索) | 有(对话历史) | 有(较弱) | 有(Memory/长期记忆模块) |
| 多 Agent | 一等:subagent、workflow、jobs |
有限 | 有限 | 强项:内置 Debate、Concurrent、Handoffs 等工作流 |
| 可视化/工程化 | acp + 文档化子系统 |
终端 UI | 终端 UI | Studio + Tracing + OpenJudge 评测 + RAG + TTS |
| 沙箱 | 一等:sandbox(bwrap/Landlock/Seatbelt) |
依赖 shell 限制 | 依赖沙箱环境 | 运行时沙箱(runtime sandbox) |
| 自动化协议 | ACP 服务器内置 | 无(靠 CLI/钩子) | 无 | A2A(Agent-to-Agent)智能体 |
| 源码开放 | ✅ 全仓库可读可改可贡献 | ❌ | ❌ | ✅(Apache-2.0 类开源) |
| 适用对象 | 平台/产品工程师、自研 Agent 团队 | 终端开发者 | 终端开发者 | 算法/应用开发者、多智能体研究者 |
12.2 DeepSeek Harness 的核心优势
- 真正的"可组合"而非"可配置"
Claude Code / Codex 让你配置 已有行为;Harness 让你重写行为------连 agent 主循环、文件访问策略、权限模型都能换成自己的插件。这是"框架 vs 产品"的本质差别。 - 模型无关的能力层
dsh-llm把 LLM 抽象成 Service,DeepSeek 只是其中一个 provider。理论上换 provider 不改上层工具与循环。这点与 AgentScope 的"多模型无关"理念一致,但 Harness 通过 Cordis 的inject/effect把这种替换做成声明式、配置驱动、可热替换,比 AgentScope 在代码里换类实例更彻底。 - Hook 互操作
通过hooks-claude-code/hooks-codex桥接包,你现有的 Claude Code / Codexhooks.json能直接在 Harness 上跑。迁移成本极低,且原生扩展点是"类型化拦截点",比 shell hook 更强。这是 AgentScope 完全不具备的跨生态兼容。 - 会话是一等公民
session+session-query提供持久化、投影、血缘、语义过滤、SQLite 全文检索------适合长期记忆与知识库型应用。AgentScope 也有 Memory/长期记忆模块,但 Harness 把"会话日志即真相"(model-visible ⟺ logged)上升到架构约束,保证任何送达模型的输入都能从会话日志重建------这对审计、回放、合规极有价值。 - 工程纪律极严
100% 覆盖率门禁、声明式cordis-surface、type-equiv 文档同步、品牌类型、显式边界校验------使它适合作为生产级产品的底座,而非玩具。AgentScope 的工程化(Studio/评测/Tracing)更偏"应用开发体验",Harness 的工程化更偏"框架本身的可靠性与可维护性"。 - 自修改能力
extensions包让 agent 运行时检视/挂载/卸载自己的插件(即demo:cordis)。这是 框架级 的插件自装载能力------区别于业务层的动态切换(如 AgentScope 的运行时换 agent),Harness 能在不重启进程的情况下增删真实 Cordis 插件并回卷其 effect。Claude Code / Codex 不提供此类机制。
12.3 与 AgentScope 的关键差异(重点)
AgentScope 是阿里开源的智能体应用开发库(Python 为主,1.0 论文见 arXiv:2508.16279),定位是"以开发者为中心构建 agentic 应用"。两者常被拿来比较,但取向不同:
| 取向 | DeepSeek Harness | AgentScope |
|---|---|---|
| 范式 | 插件运行时(Cordis),一切皆插件、配置驱动 | 组件库 + 管道 DSL,ReAct 范式,代码驱动 |
| 语言生态 | TypeScript / Node,天然适合前端/工具/IDE 集成 | Python,天然适合算法/数据/ML 研究者 |
| 组合方式 | 声明式 cordis.yml,依赖图自动排序、HMR、服务隔离 |
命令式代码组装 Agent/Pipeline/Workflow |
| 多智能体 | 一等公民 subagent/workflow/jobs,基于插件协作 |
强项:内置 Debate、Concurrent、Routing、Handoffs 等开箱即用工作流 |
| 可观测/评测 | 会话日志 + ACP 协议 + 子系统文档化 | 强项:Studio 可视化、Tracing、OpenJudge 评估器、RAG、TTS、Tuner |
| 生产落地 | 框架级可靠性(100% 覆盖、类型边界、沙箱) | 应用级工程化(沙箱、评测、可视化)齐全 |
| 模型 | 默认 DeepSeek,LLM 层可替换 | 多模型(通义/OpenAI/本地)开箱支持 |
| runtime sandbox | bwrap/Landlock/Seatbelt 一等支持 | runtime sandbox 支持 |
一句话总结差异:
- 想自建 Agent 平台/产品底座、需要可替换模型与循环、要长期会话与审计、要平滑迁移 Claude Code/Codex 钩子 → 选 DeepSeek Harness(TypeScript 生态、插件化、配置驱动)。
- 想用 Python 快速搭多智能体应用、要现成的辩论/并发/路由工作流、要 Studio 可视化与评测体系 → 选 AgentScope(应用开发体验、ML 生态)。
- 想给终端用户一个开箱即用的编码助手 → 选 Claude Code / Codex(产品形态)。
12.4 何时选谁(决策树)
- 终端开发者、要开箱即用编码助手 → Claude Code / Codex。
- 平台/产品工程师、自托管、嵌入自家产品、可替换模型/循环/工具、长期会话与记忆、复用 Claude Code/Codex 钩子 → DeepSeek Harness。
- 算法/应用开发者、Python 生态、多智能体编排、可视化与评测体系 → AgentScope。
十三、学习路线图与下一步
新手 30 分钟路径:
- 跑通第四章"第一个插件"(5 分钟,无密钥)
- 跑通第十章"把工具接进真实 Agent"(10 分钟,无密钥)
- 读
examples/headless-agent/cordis.yml,逐行对照本文(10 分钟) - 设置
DEEPSEEK_API_KEY,跑pnpm dsh --profile headless "..."(5 分钟)
深入阅读(按 docs/) :
docs/cordis-tutorial/:7 章完整 Cordis 动手教程(本文是其浓缩与扩展)docs/cordis-primer.md:概念速查docs/architecture.md:系统地图(改packages/前必读)docs/capability-seams.md:能力三层设计docs/user/:面向 Harness 插件开发(develop/basic/tool.md等)docs/cookbook/adding-a-tool.md:工具 UI 呈现设计packages/*/README.md:每个包的目的、API、扩展点
进阶方向:
- 写一个自定义
Service Definition + Provider(参考dsh-shell) - 用
dsh-bundle做自己的--profile补丁层 - 用
hooks-claude-code桥接现有 hook - 用
extensions实现 agent 自修改 - 运行
pnpm run doc-sync重新生成所有架构/生命周期图(含本文引用的docs/agent-lifecycle.md,由scripts/gen-doc-graphs.ts产出) - 对照 AgentScope 论文,体会"配置驱动插件运行时"vs"命令式组件库"两种架构取舍