- 本文的示例基于 npm 上的
@deepseek-ai/cordis4.0.4,以及配套的@deepseek-ai/cordis-plugin-loader、@deepseek-ai/dsh-tools等 rc 包。DSH 仓库在 commit5badb15中 vendor 了一份 Cordis,版本号为 4.0.5-alpha.1。 - 上游
cordiverse/cordis的 README 写明:API 还不稳定,可能随时变化1。 - 本文标注“✅ 已实际运行”的示例来自 DSH 官方的 Cordis 教程(第 1 到 5 章),笔者用 npm 包在本地实际跑通,并用 TypeScript strict 模式做了类型检查,全程不需要 API Key。资料截至 2026-10-09。
- Cordis 是一个元框架(meta-framework),只负责插件的加载、卸载和依赖管理。DSH 的每个具体组件都是对等的 Cordis 插件,彼此通过服务和事件协作,在配置层自由组合23。
- 论文把问题拆成两个正交的维度:时间可组合性(一个组件被移除时,它产生的副作用能完全撤销)和空间可组合性(组件之间的依赖可以声明,并被自动响应式地管理)4。
- 五个核心概念:插件、上下文(服务容器)、用 inject 声明依赖、类型化事件(五种分发模式)、注册即可逆的副作用(effect)3。
- 对 agent 来说,这意味着模型适配器、工具、沙箱、甚至 agent 循环都可以热插拔,而且卸载时一定会被清理干净2。
1. Cordis 从哪里来#
- 上游仓库是
cordiverse/cordis,2022-05-17 创建,自称 “Meta-Framework of Spatiotemporal Composability”。截至 2026-10-09 约有 9,000 个 star,GitHub 统计中提交最多的贡献者是shigma5。 - 中文媒体报道说:Cordis 由 Koishi 聊天机器人框架的创始人 Shigma 维护了四年,最初是 Koishi 的内核;Shigma 目前在 DeepSeek-AI 工作6。这是二手资料。
- DSH 以 vendor 方式引入了一份 Cordis,改名为
@deepseek-ai/cordis7。第三方评测提到,发布时 vendor 的版本基于 4.0.0-rc.7,并带有 18 个本地补丁8。这也是二手资料。 - 设计思想发表为论文 A Programming Paradigm for Spatiotemporal Composability(arXiv:2608.25512,2026-08-26),作者是 Yifan Shi、Wei Zhang、Tianyi Cui4。
2. 论文的核心思想:时空可组合性#
论文摘要的要点4:
- 现代软件,从插件系统到会自我演进的 agent harness,越来越需要动态组合,但这件事的形式化基础还很薄弱。
- 他们识别出两个正交的维度:
- 时间可组合性:组件被移除时,能完全撤销它的副作用;
- 空间可组合性:能声明组件之间的依赖,并对依赖的变化做出响应。
- 对应的两个机制:
- 可逆副作用:每次修改上下文时都附带一个逆操作,由运行时保管;
- 响应式协作用:每次上下文变化,都会按组件的需求声明进行分类,从而驱动组件的激活与停用。
- 把副作用上下文和协作用上下文统一成一个 Context 类型,所有效应都经由它中转。论文称之为 context paradigm,并据此给出了一套动态组合的演算。
- Cordis 是这套思想的实现:一个带效应追踪和协作用解析的核心库,加上一个支持配置协调和热模块替换(HMR)的声明式组件加载器。
- 时间维度就像插线板:电器(插件)拔下来,它占用的插座、拉出去的线(定时器、监听器、注册的工具)全部自动收回,不留残余。
- 空间维度就像乐高:一块积木声明“我需要一个 2×4 的底座”。底座出现时它自动装上去;底座被拿走时它也自动拆下来。谁先放、谁后放无所谓。
3. 五个核心概念#
| 概念 | 一句话解释 |
|---|---|
| 插件 | 一个带有可选 inject 和 apply(ctx) 的函数或对象,也可以是一个 Service 子类。它的生命周期由 Cordis 挂载到当前上下文中 |
| 上下文(Context) | 服务的容器。一个服务占据一个稳定的 ctx.<key>,比如 ctx.tools、ctx.llm、ctx.sessions。其他插件通过 key 查找服务,而不是导入具体实现 |
| inject | 插件声明自己需要哪些服务,等这些服务都就绪后才启动。加载顺序由依赖关系决定,不需要手动编排启动流程 |
| 类型化事件 | 通过 TypeScript 的声明合并注册事件名,然后以 emit、waterfall、parallel、serial 或 bail 五种模式之一分发 |
| 可逆的注册 | 提示词片段、工具 schema、适配器、监听器都是通过 ctx.effect() 或 ctx.on() 装上去的,reload 或拆除时会按预期撤销 |
以上取自 DSH 的 Cordis 入门3。
4. 动手:第一个插件(✅ 已实际运行)#
import type { Context } from '@deepseek-ai/cordis'
export const name = 'hello'
export function apply(ctx: Context) { console.log('hello from my first plugin')}# cordis.yml:一个配置项列表,loader 会挂载其中的每一项- name: './hello.ts'$ node --import tsx node_modules/@deepseek-ai/cordis/bin.jshello from my first plugin代码来自官方教程第 1 章9。运行过程如下:
- 启动器创建根
Context,并挂载 Loader 插件; - Loader 读取
cordis.yml,解析出./hello.ts,把它作为子插件挂载上去; - Cordis 调用你的
apply(ctx)。
你的文件里没有任何框架启动代码:插件只描述自己贡献了什么,应用由 cordis.yml 组合出来9。
5. 生命周期与 effect(✅ 已实际运行)#
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 () => { // 返回的 disposer 会在插件卸载时自动执行 clearInterval(timer) console.log('heartbeat cleaned up') } })}
export function apply(ctx: Context) { const fiber = ctx.plugin(heartbeat) // 从代码里挂载一个子插件,得到它的 fiber(运行时句柄) ctx.effect(() => { const timer = setTimeout(async () => { await fiber.dispose() // 卸载子插件:它的所有 effect 都会被撤销 console.log('disposed') process.exit(0) }, 700) return () => clearTimeout(timer) })}heartbeat plugin loadingtickticktickheartbeat cleaned updisposed代码来自官方教程第 2 章10。每个已加载的插件实例都有一个 fiber,它的状态机如下10:
UNLOADING → PENDING 这条边是笔者根据教程第 3 章“依赖消失会卸载,依赖恢复后会重新加载”的描述补上的示意,并不是教程原图里的内容。
以下注册 API 本身就是 effect,不需要你手动清理10:
ctx.on(event, listener):插件卸载时监听器会被移除;ctx.plugin(child):子插件随父插件一起被释放;- 服务注册,以及 harness 的各种注册表,例如
ctx.tools.register(...)。
disposer 按注册顺序的逆序启动,但多个异步 disposer 会并发执行。如果拆除步骤必须按顺序进行,要把它们放进同一个 disposer 里,依次 await10。
6. 服务与 inject(✅ 已实际运行)#
// greeter.ts:提供一个服务import { Service, type Context } from '@deepseek-ai/cordis'
declare module '@deepseek-ai/cordis' { interface Context { greeter: GreeterService // 编译期:通过声明合并,让 ctx.greeter 有类型 }}
export class GreeterService extends Service { constructor(ctx: Context) { super(ctx, 'greeter') // 运行期:以 greeter 这个名字注册 }
greet(who: string) { return `Hello, ${who}!` }}
export const name = 'greeter'
export function apply(ctx: Context) { ctx.plugin(GreeterService)}// consumer.ts:消费这个服务(no-check:它依赖上面的 greeter.ts,两者已在本地项目中一起做过类型检查并运行)import type { Context } from '@deepseek-ai/cordis'import type {} from './greeter.ts' // 只为了引入类型声明,运行时不会导入任何东西
export const name = 'consumer'export const inject = ['greeter'] // greeter 就绪之前,这个插件会一直处于 PENDING
export function apply(ctx: Context) { console.log(ctx.greeter.greet('world'))}# 故意把 consumer 写在 greeter 前面- name: './consumer.ts'- name: './greeter.ts'Hello, world!代码来自官方教程第 3 章11。笔者特意把 consumer 放在列表的前面,输出依然正确,这证明加载顺序由依赖关系决定,与文件中的位置无关。
教程里还有三个关键点11:
- 依赖在加载之后也会持续追踪。如果运行中某个服务消失了,所有依赖它的插件也会跟着卸载,等服务恢复后再重新加载。这正是“通过配置替换实现”的基础:卸载
dsh-bash-local,再挂载另一个shell提供方,所有注入了'shell'的插件都会重启,并改用新的实现。 - 可选依赖不要写在
inject里,而是在使用处用ctx.get('greeter')探测。 - 服务名共用一个扁平的命名空间,自己的服务要加上有辨识度的前缀。
7. 事件与五种分发模式(✅ waterfall 示例已实际运行)#
| 模式 | 调用方式 | 语义 |
|---|---|---|
emit | ctx.emit(name, ...args) | 同步广播,不等待、不收集返回值 |
parallel | await ctx.parallel(name, ...args) | 所有监听器并发执行,一起等待 |
serial | await ctx.serial(name, ...args) | 按顺序执行;第一个返回非空值的监听器胜出,后面的不再执行 |
bail | ctx.bail(name, ...args) | serial 的同步版本 |
waterfall | ctx.waterfall(name, ...args, next) | 环绕中间件:可以改写下游的结果,也可以不调用 next() 直接短路 |
以上取自官方教程第 4 章12。
import type { Context } from '@deepseek-ai/cordis'
declare module '@deepseek-ai/cordis' { interface Events { 'demo/transform'(input: string, next: () => Promise<string>): Promise<string> }}
export const name = 'waterfall-demo'
export function apply(ctx: Context) { // 监听器 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() })
void (async () => { console.log(await ctx.waterfall('demo/transform', 'hello', async () => 'hello')) console.log(await ctx.waterfall('demo/transform', 'blocked words', async () => 'blocked words')) })()}HELLO** BLOCKED **代码来自官方教程第 4 章12。第二行输出是这样产生的:
- 监听器 1 先执行,调用
next(),于是进入监听器 2; - 监听器 2 发现输入里有
blocked,没有调用next()就直接返回,所以最内层的默认逻辑根本没有执行; - 返回途中,监听器 1 把这个结果转成了大写12。
8. 配置:schema 校验(✅ 已实际运行)#
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[]}
// 同名导出:既是 TypeScript 接口,也是运行时用来校验的 schemaexport 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}!`) }}- name: './config-demo.ts' config: targets: ['alpha', 'beta']Hello, alpha!Hello, beta!代码来自官方教程第 5 章14。如果把 targets 改成字符串 'not-an-array',插件的 fiber 会进入 FAILED 状态。
教程说,传入错误的配置时,启动器会打印 ValidationError 并以状态码 1 退出14。笔者用 npm 包实测时观察到两点不同:
- 默认什么都不输出。必须在
cordis.yml里加上@deepseek-ai/cordis-plugin-logger-console这个插件,才能看到[E] config-demo ValidationError: invalid config: - $.targets expected array but got not-an-array; - 进程的退出码是 0。
这与教程第 1 章的提醒一致:模块解析失败之类的错误是通过 logger 服务报告的,在 console 导出器开始工作之前就可能丢失9。调试插件时,第一件事就是挂上 logger-console。
9. 组合:从插件到 agent#
9.1 配置项的元数据#
- id: greeter # 稳定的标识:loader 据此判断是“修改”还是“删除后再添加” name: './greeter.ts'- id: consumer name: './consumer.ts' disabled: true # 保留这个配置项,但不挂载它取自教程第 6 章15。还有两种用法:
- 组(group):嵌套一份子列表,作为一个整体加载和卸载;
isolate:给一个组提供某个服务名的独立实例。例如两个组可以各自拥有配置不同的shell提供方,互不影响15。
这也是 DSH 的 preset 用 isolate: { terminals: true } 隔离终端服务的原因(见 5.1 DeepSeek Harness 全景 › 3.3 Agent 预设(Preset))。
HMR:@deepseek-ai/dsh-hmr 插件会监视文件。文件保存时,旧实例先卸载,它的 effect 全部撤销,然后加载新代码。编辑 cordis.yml 时,loader 也会按 id 做比较,只挂载、卸载或重新配置发生变化的部分15。
9.2 Seam:可替换能力的三种角色#
DSH 把可以替换的能力称为 seam,每个 seam 由三种角色构成216:
文件系统和进程的提供方共享同一个执行世界。所以,把它们指向远程沙箱,Bash、PTY、LSP 就一起搬了过去,而不需要为某个提供方单独 fork 代码2。替换一个提供方,就能改变整个产品的行为。
10. 与 pi 扩展系统的对比#
| 维度 | Cordis(DSH) | pi 的扩展系统 |
|---|---|---|
| 组合单位 | 插件,包括服务、监听器、effect | 扩展:一个工厂函数,通过 ExtensionAPI 注册各种能力 |
| 依赖 | inject 声明,响应式地加载和卸载 | 扩展之间通过 pi.events 通信,没有正式的依赖声明 |
| 清理 | 所有注册都是 effect,卸载时自动撤销 | session_shutdown 等生命周期回调,由扩展自己负责清理 |
| 核心 | 没有特权内核,连 agent 循环都是插件 | 核心(agent loop、会话、内置工具)是固定的,扩展围绕它工作 |
| 配置 | YAML 插件树,profile、bundle、patch 层层叠加 | settings.json,以及扩展目录 |
| 热重载 | HMR,按 id 做差量协调 | /reload 替换扩展运行时 |
这张表是笔者根据两边的官方文档归纳的。
小结#
- Cordis 的本质:用“依赖注入加上可逆副作用”,让组件可以安全地热插拔。
- 理解 DSH 的钥匙:
ctx.<服务名>是能力,inject是依赖,effect保证干净地退出,waterfall 负责拦截和决策。 - 下一篇 5.3 DSH 核心机制与插件开发 讲 DSH 用这些积木搭出了什么:事件溯源的会话、工具执行流水线、fail-closed 沙箱,以及动手写一个工具插件。
相关笔记#
参考资料#
注释与出处#
-
cordiverse/cordis,
packages/core/README.md(“Cordis is under active development. The API is not yet stable”),https://github.com/cordiverse/cordis/blob/main/packages/core/README.md ↩ -
deepseek-ai/deepseek-harness,
docs/architecture.zh.md,https://github.com/deepseek-ai/deepseek-harness/blob/5badb15009ae1756c3afe0ae0cef1faafc290ccc/docs/architecture.zh.md ↩ ↩2 ↩3 ↩4 -
deepseek-ai/deepseek-harness,
docs/cordis-primer.zh.md,https://github.com/deepseek-ai/deepseek-harness/blob/5badb15009ae1756c3afe0ae0cef1faafc290ccc/docs/cordis-primer.zh.md ↩ ↩2 ↩3 -
Shi, Zhang, Cui,A Programming Paradigm for Spatiotemporal Composability,arXiv:2608.25512(2026-08-26),https://arxiv.org/abs/2608.25512 ↩ ↩2 ↩3
-
GitHub REST API,
GET /repos/cordiverse/cordis(created_at 为 2022-05-17,约 9,076 个 star)以及/contributors(2026-10-09 查询) ↩ -
36氪的报道(二手资料),https://eu.36kr.com/zh/p/3938795963137411 ↩
-
deepseek-ai/deepseek-harness,
vendor/cordis/package.json(name 为@deepseek-ai/cordis,version 为 4.0.5-alpha.1),https://github.com/deepseek-ai/deepseek-harness/tree/5badb15009ae1756c3afe0ae0cef1faafc290ccc/vendor/cordis ↩ -
developersdigest.tech,DeepSeek Harness (dsh) first look(第三方评测),https://www.developersdigest.tech/blog/deepseek-harness-dsh-first-look ↩
-
deepseek-ai/deepseek-harness,
docs/cordis-tutorial/01-first-plugin.zh.md,https://github.com/deepseek-ai/deepseek-harness/blob/5badb15009ae1756c3afe0ae0cef1faafc290ccc/docs/cordis-tutorial/01-first-plugin.zh.md ↩ ↩2 ↩3 -
deepseek-ai/deepseek-harness,
docs/cordis-tutorial/02-lifecycle-and-effects.zh.md,https://github.com/deepseek-ai/deepseek-harness/blob/5badb15009ae1756c3afe0ae0cef1faafc290ccc/docs/cordis-tutorial/02-lifecycle-and-effects.zh.md ↩ ↩2 ↩3 ↩4 -
deepseek-ai/deepseek-harness,
docs/cordis-tutorial/03-services.zh.md,https://github.com/deepseek-ai/deepseek-harness/blob/5badb15009ae1756c3afe0ae0cef1faafc290ccc/docs/cordis-tutorial/03-services.zh.md ↩ ↩2 -
deepseek-ai/deepseek-harness,
docs/cordis-tutorial/04-events.zh.md,https://github.com/deepseek-ai/deepseek-harness/blob/5badb15009ae1756c3afe0ae0cef1faafc290ccc/docs/cordis-tutorial/04-events.zh.md ↩ ↩2 ↩3 ↩4 ↩5 -
deepseek-ai/deepseek-harness,
docs/tool-execution-pipeline.zh.md,https://github.com/deepseek-ai/deepseek-harness/blob/5badb15009ae1756c3afe0ae0cef1faafc290ccc/docs/tool-execution-pipeline.zh.md ↩ -
deepseek-ai/deepseek-harness,
docs/cordis-tutorial/05-config.zh.md,https://github.com/deepseek-ai/deepseek-harness/blob/5badb15009ae1756c3afe0ae0cef1faafc290ccc/docs/cordis-tutorial/05-config.zh.md ↩ ↩2 -
deepseek-ai/deepseek-harness,
docs/cordis-tutorial/06-composition-and-hmr.zh.md,https://github.com/deepseek-ai/deepseek-harness/blob/5badb15009ae1756c3afe0ae0cef1faafc290ccc/docs/cordis-tutorial/06-composition-and-hmr.zh.md ↩ ↩2 ↩3 -
deepseek-ai/deepseek-harness,
docs/glossary.zh.md(capability-seam 一节),https://github.com/deepseek-ai/deepseek-harness/blob/5badb15009ae1756c3afe0ae0cef1faafc290ccc/docs/glossary.zh.md ↩