Cordis:把运行中的应用变成一棵可逆的插件树
从 cordis.yml、Loader 和 Context 开始,理解 Cordis 如何让 DeepSeek Harness 组合、替换并安全卸载运行时能力。
短结论:Cordis 把启动、依赖和清理拆成三个运行时责任:Loader 负责组合,Context 负责连接,Fiber 负责拥有并撤销插件副作用。
如果只记住一个画面,请看上方的头图:一行 cordis.yml 经过 Loader 进入 Context,最后变成一个可以运行、替换和卸载的 Fiber。下面用四张源码驱动的流程图把这件事讲清楚。
01 / 一行配置如何进入运行时
这条链路来自 Cordis 教程的启动器:根 Context 被创建后,Loader 读取当前目录的 cordis.yml,再把配置中的模块挂载进运行时。配置决定组合,插件决定贡献,Fiber 记录这一次挂载的所有权。
- id: hello
name: './hello.ts'插件只需要贡献自己的入口:
import type { Context } from '@deepseek-ai/cordis'
export function apply(ctx: Context) {
console.log('hello from my first plugin')
}02 / 插件树解决哪三个问题
| 需求 | Cordis 的动作 | 运行时结果 |
|---|---|---|
| 增加能力 | 在组合层增加插件行 | 不改中央启动入口 |
| 替换实现 | 消费者依赖服务名,不依赖文件 | provider 可替换 |
| 回收副作用 | Fiber 卸载时执行 disposers | 监听器、注册和资源不残留 |
重点不是“插件”这个名词,而是每个插件实例都有明确的运行时所有权。这也是 Cordis 不只是模块加载器的原因:代码被找到之后,还要知道依赖何时满足、资源由谁清理。
03 / Context 是连接层,不是全局变量
Context 同时提供服务访问和插件注册。DSH 中的 ctx.tools、ctx.llm、ctx.agents 与 ctx.sessions 都是按名称连接的能力;消费者不需要直接导入某个 provider 的实现类。
当插件通过 inject 声明服务依赖时,Cordis 会让它停留在 PENDING,直到依赖可用后再进入 LOADING。启动时机因此由依赖关系决定,而不是由配置文件的偶然顺序决定。
04 / 换 provider,不换 consumer
消费者只声明能力名称:
- id: terminal-tool
name: './terminal-tool.ts'
inject: ['shell']组合层可以把 shell 指向本地实现,也可以指向 E2B 实现。消费者代码仍然依赖同一个服务名,这正是 DSH profile 可以选择不同运行时提供方的基础。
05 / HMR 的核心是 Fiber 可逆
HMR 不是再次 import 文件,而是先卸载旧 Fiber,再挂载新 Fiber。Cordis 的生命周期状态包含 PENDING、LOADING、ACTIVE、FAILED、UNLOADING 和 DISPOSED;图中每个状态都对应一次可观察的运行时阶段。
外部资源通过 effect 交给 Fiber 管理:
ctx.effect(() => {
const timer = setInterval(() => console.log('tick'), 200)
return () => clearInterval(timer)
})主体创建资源,返回的 disposer 在卸载时撤销资源。ctx.on()、ctx.plugin() 和服务注册也遵循同一套可逆关系。
06 / 这就是 DSH 的组织方式
在 DeepSeek Harness 中,模型适配器、工具、会话、agent 管理和 agent loop 都以插件进入运行时。可以把它压缩成四个词:
- 配置选择组合:profile 决定加载哪些插件。
- Context 连接能力:服务定义、provider 和 consumer 通过名称协作。
- Fiber 拥有实例:每次挂载都有独立生命周期。
- effect 撤销副作用:卸载不是约定,而是运行时动作。
所以“一切皆插件”不是目录结构,而是替换位置和资源所有权都显式可见。
下一步怎么读
实现依据:Loader 启动器、Context、Plugin registry、Fiber lifecycle 与 Cordis primer。
邮件列表
加入我们的社区
订阅邮件列表,及时获取最新消息和更新