Cordis 如何让插件可卸载、可依赖、可重组

把一个工具塞进注册表并不难。真正麻烦的是它离开以后发生什么。
假设一个 Workspace 插件注册了文件服务、监听器、Prompt 片段和一个定时任务。用户切换项目时,插件被卸载:监听器有没有解除?Prompt 片段会不会重复?依赖文件服务的工具应该立即消失,还是继续拿着已经失效的引用?如果配置热更新再次装载同一个插件,系统里会不会留下两份副作用?
这才是 DeepSeek Harness 选择 Cordis 的关键。首篇文章讨论的是"为什么连 Agent Core 都进入插件图",本篇只回答下一层问题:当插件、服务和依赖会在运行期间出现与消失时,Cordis 用什么机制维持边界?
我的结论是:Cordis 的价值不在于再发明一套依赖注入,而在于把服务依赖和副作用回收都纳入 Context 生命周期。它让"装上能力"和"完整撤销能力"成为同一份插件合同的两面;代价是团队必须把依赖、事件模式、disposer 和最终组合图当作一等工程对象。
普通注册表只回答"有什么",生命周期还要回答"何时有效"

最简单的插件系统通常有一个 Map:插件启动时注册工具,主程序按名称查找,进程退出时操作系统回收全部资源。这种模式在静态、小规模应用里完全够用。
但 Agent Runtime 的能力边界经常不是静态的:
- 某个 Agent 或 Workspace 只在自己的作用域里拥有一组工具;
- 文件、Shell、模型或 Web Provider 可能随配置和部署环境变化;
- Prompt 片段、工具 Schema 和事件监听器要随插件一起装卸;
- 热更新或产品 Patch 会重建一部分组合,而不是重启整个宿主。
这时,registry.set() 只能说明某个对象现在存在,不能说明谁创建了它、谁依赖它、何时失效、销毁时需要撤销哪些动作。把这些问题散落在每个插件的启动和停止函数里,会让清理顺序与依赖顺序逐渐变成隐含知识。
Cordis 试图把这种隐含知识显式化。固定版本的 DeepSeek Harness Cordis Primer 将机制压缩成五个概念:Plugin、Context、inject、typed events 和 reversible effects。它们不是五个并列名词,而是一条生命周期链。
Context:服务仓库,也是插件作用域

Cordis 的 Context 首先是服务仓库。服务占用稳定的 ctx.<key>,例如 ctx.tools、ctx.llm 或 ctx.sessions;消费者通过 key 使用能力,而不是直接导入某个具体实现。
但如果只看到"按 key 取服务",就会把 Context 误解成普通容器。它还限定插件运行的作用域:插件在这个 Context 中注册服务、事件和其他副作用,这些动作可以随着作用域销毁而回收。
这让一个能力至少有两个坐标:
- 空间坐标:它在哪个 Context 中可见,哪些消费者能访问;
- 时间坐标:它从何时开始有效,随哪个插件或子作用域一起结束。
DeepSeek Harness 的固定架构文档因此强调,模型适配器、工具注册表、Session Log、Agent Loop 都是共享 Context 中的插件贡献。所谓"没有特权核心",并不是核心不存在,而是核心能力也要遵守相同的作用域和生命周期规则。
inject:把启动顺序改写成依赖条件

普通启动脚本喜欢写成固定顺序:先数据库,再工具,再 Agent Loop。顺序一旦变化,消费者可能在 Provider 尚未就绪时启动;为了修补问题,代码里又会出现更多等待、重试和条件分支。
Cordis 让插件通过 inject 声明所需服务。固定 Primer 的表述很明确:声明了必需服务的插件要等这些服务存在后才激活。于是,装载条件来自服务需求,而不是开发者手工维护的一串启动序号。
可以把它理解为:
text
Provider 出现
-> 所需 service 可用
-> 满足 inject 的 Consumer 激活
-> Consumer 注册自己的 effects
Provider / 上层作用域消失
-> Consumer 生命周期结束
-> Consumer effects 回收
这个变化很重要。Provider/Consumer 不再只是静态架构图上的箭头,而是能影响插件何时存在的运行条件。
但"声明依赖"不等于"动态变化天然安全"。多个 Provider 同时变化时的顺序、消费者持有的外部资源、正在执行的请求以及失败恢复,都仍需要实现和测试。Cordis 提供的是协调语义,不是可靠性证明。
effect:注册动作必须带着撤销方法

插件最常见的泄漏不是内存本身,而是忘记撤销注册:事件监听器仍在接收消息,Prompt 片段被重复拼接,工具 Schema 留在目录里,定时任务继续运行。
Cordis Primer 的实践规则是:每个注册都应有 disposer。插件可以通过 ctx.effect() 返回清理函数,也可以使用已经处理清理的 Cordis helper。若清理顺序重要,相关工作应放在同一个 effect 中,让销毁按预期次序展开。
因此,effect 不是"执行一次函数"的花哨名字,而是一份双向合同:
text
mount: 注册服务 / 监听事件 / 增加 Prompt / 启动资源
dispose: 撤销服务 / 解除监听 / 移除 Prompt / 释放资源
这也是"Everything is a Plugin"能够进入 Agent Core 的必要条件。如果 Agent Loop、Session 或工具目录只能装载、不能撤销,那么动态组合最终仍会退化为"一次启动、永久存在"的静态宿主。
这里同样不能过度推断。框架要求 disposer,不代表所有插件都写对了;本篇没有系统级反复装卸、故障注入或长时间资源监测,不能把可逆 effect 写成"已经验证无泄漏"。
typed events:事件模式本身也是接口合同

插件解耦以后,大量协调会通过事件完成。固定 Primer 区分 emit、waterfall、parallel 和 serial 四种 dispatch mode;它们是否等待、是否有返回值、按什么方式传播,都属于事件公开合同。
尤其容易误用的是 waterfall。它不是简单的"按顺序通知所有监听器",而是 around-middleware:监听器收到 next(),可以委托给后续监听器,也可以直接返回并短路。只想观察或补充信息的监听器必须继续委托;拥有单一决策权的策略监听器才适合截断。
这意味着,事件名相同远远不够。替换一个 Provider 或策略插件时,还要保证:
- 监听器知道自己是观察、包装、并行处理还是顺序决策;
- 应当委托的 waterfall 不会误短路;
- 返回值、错误和副作用顺序与消费者预期一致;
- 卸载时监听器会随 effect 一起解除。
所以事件模式不是实现细节,而是能力契约的一部分。
一条完整 seam 需要三个角色

DeepSeek Harness 的固定架构把可替换能力接缝拆成三个角色:
- Service Definition:声明稳定接口与 Context key;
- Provider:提供具体实现;
- Consumer:使用能力,常见形态是面向模型的 Tool 或上层 Runtime 组件。
例如,文件系统接缝不能只看 ctx.fs 的类型。还要看本地、远端或 Sandbox Provider 怎样实现它,以及文件工具、LSP、Shell 等消费者依赖哪些错误、安全和路径语义。
官方生成的 Capability Seams 图列出了大量服务与包之间的登记关系。这张图适合回答"谁声明、谁提供、谁直接消费",却不能单独回答"替换后是否兼容"。一个新 Provider 即使通过类型检查,也可能在流式事件、错误分类、权限策略或资源释放上破坏消费者假设。
因此,判断 seam 是否成立,至少要检查四层:接口能否对上、事件模式是否一致、生命周期能否回收、行为语义是否兼容。前三项是结构条件,第四项仍需要契约测试和真实运行证据。
Context、inject、effect 如何形成生命周期图

把前面的机制合在一起,可以得到一张比"插件列表"更有用的检查图:
text
Context 创建作用域
-> Provider 插件挂载
-> service 出现在 ctx.<key>
-> inject 条件满足
-> Consumer 插件激活
-> effect 注册事件 / Prompt / Tool / 子资源
-> typed events 按合同协调运行
-> 配置变化、Provider 消失或作用域销毁
-> Consumer 停止并回收 effects
-> Provider effects 回收
-> Context 释放
这张图的关键不在箭头多,而在每个正向动作都应该找到反向路径:谁让它出现,什么条件让它有效,谁负责撤销,撤销后消费者如何停止。
如果团队无法回答这些问题,所谓动态插件化就只完成了"装上",没有完成"生命周期"。
什么时候值得做成 seam
不是每个函数都需要 Provider。把所有细节都抽象成 Context service,会把调用链变成庞大的组合图,增加调试、配置和迁移成本。
一个能力值得成为 seam,通常至少满足一项:
- 不同部署需要替换实现,例如本地与远端文件系统;
- 能力需要独立装卸或只在局部 Agent/Workspace 生效;
- 多个消费者需要共享稳定边界,但不能依赖具体包;
- 生命周期、权限或资源所有权需要由单独 Provider 管理;
- 测试需要装配更小的能力图或故障 Provider。
反过来,如果实现固定、调用者单一、生命周期与宿主完全相同,普通函数、显式对象或构造器注入可能更清楚。抽象的价值来自真实变化,不来自"插件化"这个标签。
团队采用前的生命周期检查表

| 检查对象 | 必须回答的问题 | 当前公开证据能证明什么 |
|---|---|---|
| Context | 能力在哪个作用域可见,谁拥有它 | 固定文档给出服务仓库与作用域模型 |
| inject | Consumer 依赖哪些 service,缺失时何时停止 | 固定 Primer 给出声明依赖与等待激活语义 |
| effect | 每项注册的 disposer 在哪里,清理顺序怎样 | 固定 Primer 要求 disposer 与可逆注册 |
| event | dispatch mode、委托、短路和返回值是什么 | 固定文档给出四种事件模式及 waterfall 规则 |
| seam | 定义、Provider、Consumer 是否齐全 | 固定架构和生成能力图可以核对角色关系 |
| 行为兼容 | 错误、流式、安全与资源语义是否一致 | 不能靠能力图证明,需要契约测试 |
| 系统可靠性 | 反复装卸、并发变化、长时间运行是否稳定 | 当前未验证,需要故障注入和资源监测 |
这张表是本篇唯一主要产物。它刻意把"源码已经设计出来的机制"和"采用者还必须验证的行为"分开,避免从架构图直接跳到生产结论。
结论
Cordis 让 DeepSeek Harness 的插件不只是"可以注册",而是拥有明确的存在范围、依赖条件和回收路径。Context 提供作用域,inject 表达激活条件,effect 把注册与撤销绑定,typed events 规定协调方式,Service Definition、Provider、Consumer 则组成完整能力接缝。
这套机制确实让 Agent Core 更容易被拆开和重组,但没有消灭复杂度。复杂度从手写启动顺序和核心分支,转移到了依赖图、事件合同、disposer、配置与诊断。对于多产品、多部署、局部能力和动态生命周期,这种转移可能值得;对于固定、简单的应用,普通依赖注入仍可能更便宜。
现阶段最准确的评价不是"Cordis 已经证明了动态 Agent Runtime 更可靠",而是:它给出了一套可以被源码检查、也必须被故障测试的生命周期合同。
参考资料
- DeepSeek Harness 固定 Commit Cordis Primer:github.com/deepseek-ai...
- DeepSeek Harness 固定 Commit 架构文档:github.com/deepseek-ai...
- DeepSeek Harness 固定 Commit Capability Seams:github.com/deepseek-ai...
- Cordis 官方仓库:github.com/cordiverse/...
- DeepSeek Harness Python SDK(动态版本边界):pypi.org/project/dee...
证据与推导边界
- Context、inject、typed events、effect/disposer:固定 Commit 官方文档事实。
- Service Definition、Provider、Consumer:固定架构与生成能力图事实。
- "复杂度转移到组合、契约和诊断":基于上述机制的作者工程分析。
- seam 适用条件与生命周期检查表:作者建议,不是 DeepSeek 官方采用承诺。
- 反复装卸、并发变化、资源泄漏、真实模型 E2E 与生产稳定性:尚未验证,不作结论。