返回专栏
Agent SDK/05 · DSH/5.2

Cordis 与一切皆插件

Cordis 是一个元框架(meta-framework),只负责插件的加载、卸载和依赖管理。

预计阅读
22分钟
全文字数
3,331字
资料截至
2026-10-09
Agent SDK · DSH5.2
版本与时效声明
  • 本文的示例基于 npm 上的 @deepseek-ai/cordis 4.0.4,以及配套的 @deepseek-ai/cordis-plugin-loader、@deepseek-ai/dsh-tools 等 rc 包。DSH 仓库在 commit 5badb15 中 vendor 了一份 Cordis,版本号为 4.0.5-alpha.1。
  • 上游 cordiverse/cordis 的 README 写明:API 还不稳定,可能随时变化1。
  • 本文标注“✅ 已实际运行”的示例来自 DSH 官方的 Cordis 教程(第 1 到 5 章),笔者用 npm 包在本地实际跑通,并用 TypeScript strict 模式做了类型检查,全程不需要 API Key。资料截至 2026-10-09。
本文要点
  1. Cordis 是一个元框架(meta-framework),只负责插件的加载、卸载和依赖管理。DSH 的每个具体组件都是对等的 Cordis 插件,彼此通过服务和事件协作,在配置层自由组合23。
  2. 论文把问题拆成两个正交的维度:时间可组合性(一个组件被移除时,它产生的副作用能完全撤销)和空间可组合性(组件之间的依赖可以声明,并被自动响应式地管理)4。
  3. 五个核心概念:插件、上下文(服务容器)、用 inject 声明依赖、类型化事件(五种分发模式)、注册即可逆的副作用(effect)3。
  4. 对 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. 论文的核心思想:时空可组合性#

空间可组合性

组件之间的依赖

可以声明,并被响应式管理

机制:响应式协作用(reactive coeffects)

上下文变化时,按组件声明的需求

自动激活或停用组件

时间可组合性

组件被移除时

它的副作用能被完全撤销

机制:可逆副作用(revertible effects)

每次修改上下文都附带一个逆操作,由运行时保管

统一到同一个 Context 中

(context paradigm)

多个组件的副作用交织在一起,

互不干扰

空间可组合性

组件之间的依赖

可以声明,并被响应式管理

机制:响应式协作用(reactive coeffects)

上下文变化时,按组件声明的需求

自动激活或停用组件

时间可组合性

组件被移除时

它的副作用能被完全撤销

机制:可逆副作用(revertible effects)

每次修改上下文都附带一个逆操作,由运行时保管

统一到同一个 Context 中

(context paradigm)

多个组件的副作用交织在一起,

互不干扰

论文摘要的要点4:

  1. 现代软件,从插件系统到会自我演进的 agent harness,越来越需要动态组合,但这件事的形式化基础还很薄弱。
  2. 他们识别出两个正交的维度:
    • 时间可组合性:组件被移除时,能完全撤销它的副作用;
    • 空间可组合性:能声明组件之间的依赖,并对依赖的变化做出响应。
  3. 对应的两个机制:
    • 可逆副作用:每次修改上下文时都附带一个逆操作,由运行时保管;
    • 响应式协作用:每次上下文变化,都会按组件的需求声明进行分类,从而驱动组件的激活与停用。
  4. 把副作用上下文和协作用上下文统一成一个 Context 类型,所有效应都经由它中转。论文称之为 context paradigm,并据此给出了一套动态组合的演算。
  5. 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.js
hello from my first plugin

代码来自官方教程第 1 章9。运行过程如下:

  1. 启动器创建根 Context,并挂载 Loader 插件;
  2. Loader 读取 cordis.yml,解析出 ./hello.ts,把它作为子插件挂载上去;
  3. 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 loading
tick
tick
tick
heartbeat cleaned up
disposed

代码来自官方教程第 2 章10。每个已加载的插件实例都有一个 fiber,它的状态机如下10:

已声明,但所需的服务还没就绪

依赖都满足了

apply 执行完毕

apply 或配置校验抛出异常

卸载、热重载,或依赖的服务消失

所有 disposer 执行完毕

依赖消失后等待它恢复

PENDING

LOADING

ACTIVE

FAILED

UNLOADING

DISPOSED

已声明,但所需的服务还没就绪

依赖都满足了

apply 执行完毕

apply 或配置校验抛出异常

卸载、热重载,或依赖的服务消失

所有 disposer 执行完毕

依赖消失后等待它恢复

PENDING

LOADING

ACTIVE

FAILED

UNLOADING

DISPOSED

UNLOADING → PENDING 这条边是笔者根据教程第 3 章“依赖消失会卸载,依赖恢复后会重新加载”的描述补上的示意,并不是教程原图里的内容。

以下注册 API 本身就是 effect,不需要你手动清理10:

  • ctx.on(event, listener):插件卸载时监听器会被移除;
  • ctx.plugin(child):子插件随父插件一起被释放;
  • 服务注册,以及 harness 的各种注册表,例如 ctx.tools.register(...)。
关于 disposer 的执行顺序

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 示例已实际运行)#

模式调用方式语义
emitctx.emit(name, ...args)同步广播,不等待、不收集返回值
parallelawait ctx.parallel(name, ...args)所有监听器并发执行,一起等待
serialawait ctx.serial(name, ...args)按顺序执行;第一个返回非空值的监听器胜出,后面的不再执行
bailctx.bail(name, ...args)serial 的同步版本
waterfallctx.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. 监听器 1 先执行,调用 next(),于是进入监听器 2;
  2. 监听器 2 发现输入里有 blocked,没有调用 next() 就直接返回,所以最内层的默认逻辑根本没有执行;
  3. 返回途中,监听器 1 把这个结果转成了大写12。
waterfall 的纪律

只做观察或标注的 waterfall 监听器,必须调用 next();不调用就直接返回,表示有意短路。日志监听器如果忘了调用 next(),就会悄无声息地吞掉所有下游的默认行为12。

DSH 用 waterfall 处理那些允许插件包装或代为回答的决策,例如:

  • agent/request:插件可以替换模型调用的配置;
  • approval/request:策略可以代替用户作答;
  • tools/pre-execute:权限、沙箱、hooks 都在这里介入1213。

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 接口,也是运行时用来校验的 schema
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}!`)
}
}
- name: './config-demo.ts'
config:
targets: ['alpha', 'beta']
Hello, alpha!
Hello, beta!

代码来自官方教程第 5 章14。如果把 targets 改成字符串 'not-an-array',插件的 fiber 会进入 FAILED 状态。

笔者实测与教程描述的差异

教程说,传入错误的配置时,启动器会打印 ValidationError 并以状态码 1 退出14。笔者用 npm 包实测时观察到两点不同:

  1. 默认什么都不输出。必须在 cordis.yml 里加上 @deepseek-ai/cordis-plugin-logger-console 这个插件,才能看到 [E] config-demo ValidationError: invalid config: - $.targets expected array but got not-an-array;
  2. 进程的退出码是 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:

inject: ['shell']

Service Definition

(声明接口,占用 ctx.shell)

Provider A:bash-local

直接在本机执行

Provider B:bash-sandbox

在沙箱里执行

Provider C:远程沙箱……

Consumer:tool-bash

(模型可以调用的 bash 工具)

inject: ['shell']

Service Definition

(声明接口,占用 ctx.shell)

Provider A:bash-local

直接在本机执行

Provider B:bash-sandbox

在沙箱里执行

Provider C:远程沙箱……

Consumer:tool-bash

(模型可以调用的 bash 工具)

一个 seam 能带来什么

文件系统和进程的提供方共享同一个执行世界。所以,把它们指向远程沙箱,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 沙箱,以及动手写一个工具插件。

相关笔记#

参考资料#

注释与出处#

  1. 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 ↩

  2. deepseek-ai/deepseek-harness,docs/architecture.zh.md,https://github.com/deepseek-ai/deepseek-harness/blob/5badb15009ae1756c3afe0ae0cef1faafc290ccc/docs/architecture.zh.md ↩ ↩2 ↩3 ↩4

  3. 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

  4. Shi, Zhang, Cui,A Programming Paradigm for Spatiotemporal Composability,arXiv:2608.25512(2026-08-26),https://arxiv.org/abs/2608.25512 ↩ ↩2 ↩3

  5. GitHub REST API,GET /repos/cordiverse/cordis(created_at 为 2022-05-17,约 9,076 个 star)以及 /contributors(2026-10-09 查询) ↩

  6. 36氪的报道(二手资料),https://eu.36kr.com/zh/p/3938795963137411 ↩

  7. 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 ↩

  8. developersdigest.tech,DeepSeek Harness (dsh) first look(第三方评测),https://www.developersdigest.tech/blog/deepseek-harness-dsh-first-look ↩

  9. 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

  10. 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

  11. 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

  12. 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

  13. 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 ↩

  14. 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

  15. 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

  16. deepseek-ai/deepseek-harness,docs/glossary.zh.md(capability-seam 一节),https://github.com/deepseek-ai/deepseek-harness/blob/5badb15009ae1756c3afe0ae0cef1faafc290ccc/docs/glossary.zh.md ↩

输入关键词开始搜索。多个关键词用空格分隔。