先抓住问题
动态组合不是“能加载插件”,而是能在运行中安全地改变系统组成。
传统插件系统通常能完成一次启动:把模块加载进进程、注册命令和事件,然后开始工作。真正困难发生在运行中:某个模块升级、依赖服务掉线、策略更换、用户切换 sandbox,其他模块会不会持有失效对象、遗留事件监听器,或被迫让整进程重启?
Cordis 把问题拆成两条互补的轴。时间轴处理组件离开时如何回收自己的影响;空间轴处理组件依赖的能力由谁提供、提供者变化时谁需要重新装配。
02 / Temporal
副作用是一种资源,创建它时就要交出撤销它的方法。
定时器、事件监听器、端口、路由、服务注册、子插件都不是函数返回后自然消失的东西。它们持续占据运行时状态。一个能热重载的系统,必须让每个插件拥有自己创建的资源,并能在卸载时完整回收。
type Dispose = () => void | Promise<void>
class Context {
private disposers: Dispose[] = []
effect(setup: () => void | Dispose): Dispose {
const cleanup = setup() ?? (() => {})
let active = true
const dispose = async () => {
if (!active) return
active = false
await cleanup()
}
this.disposers.push(dispose)
return dispose
}
async dispose() {
while (this.disposers.length) await this.disposers.pop()!()
}
}
后创建的 effect 经常依赖先创建的 effect。先注册监听器、后关闭数据库,会让监听器的收尾失去它需要的连接。Cordis 将 disposer 以 LIFO 组合,得到“后创建、先撤销”的安全默认值。
03 / Spatial
服务不是一次性注入的对象,而是会出现、消失、替换的运行时能力。
普通 DI 常在启动时把对象传给消费者。Cordis 则要求消费者声明它需要的 capability,运行时持续观察这个要求是否被满足。论文把“程序对环境的需求”称为 coeffect。
Provider
SQLite Plugin
provide: ['database']创建连接并提供能力Consumer
Search Plugin
inject: ['database']只在依赖可用时激活function sqlite(ctx: Context) {
const db = createSqliteConnection('./app.db')
// 先登记连接关闭;后登记 provider。
// 卸载时按 LIFO:先 unprovide 并等待消费者,再关闭 db。
ctx.effect(() => () => db.close())
ctx.provide('database', db)
}
const search = {
inject: ['database'],
apply(ctx: Context) {
ctx.command('search <keyword>')
.action((_, keyword) => ctx.database.search(keyword))
},
}
关键不只是“服务存在”。消费者会记录自己加载时解析到的 provider Fiber identity。`database → D1 / uid 42` 换成 `database → D2 / uid 97`,即使 API 一模一样,消费者也会被重载,避免继续握着已经关闭的旧连接。
04 / Lifecycle
Fiber 是组件的运行实例:它让异步加载、撤销与依赖替换拥有可判定的时序。
一个 Plugin 是可复用定义;一个 Fiber 是它在某个 Context 下的一次实例化。Fiber 保存自己的 effect accumulator、当前依赖解析结果、加载状态和正在进行的异步任务。
没有 `database` provider,Search Fiber 不能启动。它不是报错后勉强运行,而是保持 PENDING。
- 加载前,Fiber 计算 `inject` 指向的 provider identities。
- 加载中,每次 effect 都把 inverse 纳入该 Fiber 的 accumulator。
- 依赖变动,identity 改变时 Fiber 进入 LOADING 或 UNLOADING,而不是让旧引用继续存活。
- 卸载,消费者先完成 disposer;提供者保留自身能力直到相关消费者稳定,才真正释放。
05 / Guarantees
“最终状态只取决于最终配置”是条件结论,不是对任意 JavaScript 的魔法。
卸载只撤销自己的贡献
前提是不同组件的 effect 彼此独立或可交换。事件监听表、路由表通常满足;有顺序语义的 middleware 则不天然满足。
提供者后退出
消费者只有在依赖已提供时才能启动;提供者退出时,消费者先清理,因此清理阶段仍可读取旧能力。
无环、有限,才会静稳
循环依赖会让组件永远 PENDING;无界自我注册会破坏终止性。组件边界与依赖图设计仍是人的责任。
历史不应污染最终配置
在独立 effect、无环依赖、无失败 Fiber、完整 provision 等前提下,不同重载路径应收敛到同一个静稳态。
06 / DeepSeek Harness
DSH 把 Agent 的每一项能力放入同一棵可重组的插件树。
DSH 不把 agent loop、模型、工具、会话和 sandbox 固化为不可替换的内核。它以 Profile 与 Bundle 叠加出一棵 Cordis 配置树;条目按服务可用性激活,补丁改变声明式配置,Loader 再将运行态逐步 reconcile 到目标树。
- LLM adapter
- Agent loop
- Session log
- Tool registry
- Filesystem
- Subprocess / PTY
- Sandbox
- Approval policy
- Web client
- Commands
- Goals
- Subagents
为何它更像 Runtime Infra?
模型、工具、执行环境和交互层都通过 capability seam 连接。替换 provider 时,依赖这个 seam 的消费者会得到一致的重组,而不是靠每个工具自行识别“远程模式”。
为何它还不是 AgentOS?
Cordis 管的是进程内组件语义。CPU 调度、内存隔离、网络分区、跨节点一致性和不可信代码隔离仍由宿主 OS、容器、沙箱与分布式系统承担。
07 / Next
按这个顺序读源码,抽象就不会飘在空中。
- Cordis 论文先读 3.1、3.2、4.4、5.1;公式服务于 lifecycle,不必从头硬啃。
- Fiber看 effect 如何收集 disposer,如何用 epoch 驱动 loading/unloading。
- ReflectService看 provide、notify、service resolution 与 Context Proxy。
- DSH Architecture再把 Cordis 的 Context 映射到 Agent、Tool、Session 和 Sandbox seam。
- dsh-base 配置树确认 “Everything is a Plugin” 不是宣传语,而是启动配置。