Plugin 是运行时单元,不等同于 npm 包
在 Cordis 中,Plugin 是被挂载到 Context 上、拥有独立生命周期的运行时单元。一个 npm 包可以导出一个或多个 Plugin,也可以只是承载 Bundle 的 patch;把包、Plugin 和分发单元区分开,才能正确理解 DSH。
Plugin 可以是带 apply(ctx) 的函数,也可以是继承 Service 的类。Cordis 为每次挂载创建一个 fiber,跟踪配置、依赖、状态和清理动作。DSH 因此不需要让功能永久粘在一个全局进程上。
Context 是服务解析器
Context 保存当前作用域可见的服务。Plugin 通过稳定的 ctx key 找到能力,例如 ctx.tools、ctx.llm、ctx.sessions 和 ctx.agents,而不是直接依赖某个具体实现。
Service 在 Context 上提供命名 API,使用者只依赖这个名字和接口。更换模型适配器、文件系统后端或 subagent provider 时,Consumer 无需随实现一起改写。
export class AgentLoop extends Service {
static inject = ["agents", "sessions", "llm", "tools", "systemPrompt"];
constructor(ctx: Context, config: Config) {
super(ctx, "agentLoop");
// The loop consumes services through ctx instead of importing providers.
}
}inject 表达依赖,而不是手工排序
Plugin 用 inject 声明所需服务。依赖尚未出现时,fiber 保持 pending;服务就绪后才激活。服务消失或实现改变时,Cordis 可以沿生命周期重新协调依赖。
因此 cordis.patch.yml 中 row 的视觉顺序主要服务于阅读和组合,并不等于靠数组顺序启动。真正的运行时加载关系来自服务可用性和显式依赖。
每个注册都是可逆 effect
工具、prompt section、provider、事件监听器和后台资源都通过 ctx.effect()、ctx.on() 或返回 disposer 的注册 API 进入运行时。它们归属于发起注册的 fiber。
Plugin 卸载时,Cordis 按相反顺序等待并执行 disposer。这个所有权模型让热更新、配置重载、测试隔离和应用关闭使用同一套清理语义,而不是让每个模块发明自己的 teardown。
ctx.tools.register(tool);
ctx.on("tools/result", observeResult);
ctx.effect(() => {
const stop = startWorker();
return () => stop();
});Everything is a plugin 的真正含义
DSH 不只把外围工具做成 Plugin。模型 adapter、工具 registry、session log、system prompt、agent registry,甚至默认 agent loop 都在同一棵 Cordis 树中。
这意味着扩展通常是在现有 Plugin 旁边挂载新行为,或替换某个 service provider,而不是修改一个不可替代的中心内核。配置描述的是应用架构本身。