DeepSeek Harness / 02
声明式与命令式:Cordis 的五个核心概念
把搜索服务从 Provider A 换成 Provider B,改配置再重启进程通常已经够用。DSH 仍然选择了一条更重的路:插件直接运行在 Harness 进程内,可以注册有状态服务、监听事件,甚至替换 Agent Loop。
问题不在于哪种插件“更先进”,而在于运行时是否需要替换进程内、带状态、被其他组件依赖的能力。一旦答案是需要,依赖、清理和失败回滚就不能再交给插件作者临场发挥,Cordis 正是用来托住这部分复杂度的。
两种插件世界观
| 插件模型 | 插件交付什么 | 默认优点 | 默认代价 |
|---|---|---|---|
| 声明式 | 文件、Prompt、Skill、MCP 配置、Hook | 门槛低,进程边界清楚,重启即可替换 | 只能在宿主预留的扩展点内工作 |
| 命令式 | 进程内代码、服务、事件和状态 | 能组合有状态能力,也能改变运行时行为 | 必须处理依赖、清理、并发和更新失败 |
二者的能力上限未必不同。你也可以在独立 MCP Server 里实现自己的热更新,但生命周期责任由服务作者承担。差别主要在默认下限:Cordis 强制插件进入统一的依赖和清理协议,让复杂替换不必从零处理边缘情况。
对于搜索、Skill 和普通 Hook,声明式模型通常更划算。Cordis 的价值集中在会话存储、上下文管理器、后台任务和 Agent Loop 这类跨 Turn 持有状态的组件上。
五个概念不是五个零件
官方 Cordis Primer 用五个概念描述插件运行时。重点不是背 API,而是看它们怎样形成一个闭环。
| 概念 | 解决的问题 | 形成的约束 |
|---|---|---|
| Plugin | 谁向系统贡献能力 | 插件只负责在自己的作用域内注册能力和行为 |
| Context | 能力在哪里被发现 | 消费方按稳定服务键取能力,不绑定具体实现 |
| inject | 什么时候可以启动 | 依赖未满足时等待,提供方变化时重新评估 |
| Typed Event | 横切逻辑怎样协作 | 通知、决策和环绕拦截使用不同分发语义 |
| Effect / Fiber | 插件离开后怎样收场 | 每个副作用有归属,并在卸载时逆序撤销 |
Plugin:贡献能力,而不是占有主流程
Cordis 插件可以是函数、对象或类。形态不是关键,关键是插件通过 Context 提供服务、注册监听器或产生受托管副作用。它不需要知道完整系统长什么样,也不应该靠直接导入另一个插件的实例协作。
这使插件成为运行时组合单元,而不只是一个工具目录。模型适配器、工具注册表、会话日志和 Agent Loop 在组合层面没有特权差异。
Context:服务容器也是作用域边界
ctx.llm、ctx.tools 这类稳定键表达“我要某种能力”,而不是“我要某个实现”。提供方可以替换,消费者不需要改 import。
Context 还可以派生局部作用域。extend 继承父级能力,isolate 则让某项服务在子树中拥有独立世界。工程上,这意味着某个 Agent 可以看到自己的工具、文件系统或策略,而不污染根上下文中的其他 Agent。
inject:把启动顺序变成依赖关系
传统启动脚本通常靠人工排列模块。Cordis 让插件声明所需服务:依赖未就绪时,插件处于等待状态;服务出现后才激活;提供方离开时,依赖分支也会停下并等待新的实现。
这就是“空间可组合性”的工程含义:系统不是按一条固定加载顺序拼起来,而是根据当前作用域中有哪些服务,持续解析谁能够工作。
Typed Event:区分通知与决策
服务适合直接调用,事件适合一对多通知和横切拦截。当前官方 Primer 重点区分四种分发方式:
| 模式 | 语义 | 典型场景 |
|---|---|---|
emit |
同步广播,不汇总结果 | 会话事件、状态通知 |
parallel |
并发执行并等待全部完成 | 相互独立的异步观察者 |
serial |
顺序执行,允许首个有效结果拍板 | 多个候选处理器 |
waterfall |
监听器包裹下游,可委托也可短路 | 模型请求、工具审批、重试决策 |
waterfall 是理解 DSH 策略面的关键。观察者必须调用下游,否则一个本想记日志的插件也会意外截断模型请求;只有真正拥有否决权的策略插件才应短路。它把“在哪里可以干预”从核心代码里的条件分支变成可组合的决策链。
Effect 与 Fiber:注册必须可逆
事件监听器、定时器、服务注册和后台任务都会改变进程。Cordis 把这些变化归到插件实例对应的 Fiber 上,并要求非托管资源登记 disposer。卸载时,Fiber 逆序撤销副作用,尽量把运行时恢复到插件安装前的状态。
这就是“时间可组合性”的工程含义:安装插件不只是会做什么,还必须定义它离开后如何撤销。HMR 的本质因此不是替换文件,而是卸载旧 Fiber、清理副作用、激活新 Fiber;若新实现失败,则需要保留或恢复最后一个可用状态。
为什么 Agent Loop 值得这份复杂度
如果 Loop 是核心中的硬编码控制流,插件最多只能在预设节点挂 Hook。若 Loop 本身是服务,就可以整体替换调度策略,例如从单 Agent 循环切换到多 Agent 协作,或重新定义工具并发、上下文装配和错误恢复。
这解释了 DSH 的复杂度为何一路延伸到 Fiber、disposer、依赖通知和事务性 HMR:它们不是互不相关的功能,而是在为“控制流本身也能被替换”提供基础设施。
这套设计的失败方式
inject写错或依赖永远不出现,插件会停在等待状态;诊断工具必须能解释它在等什么。- 插件绕过 Effect 创建资源,卸载后仍会留下监听器、连接或任务,热更新就不再可靠。
waterfall观察者忘记委托,会把整条执行链静默截断。- 进程内插件共享故障域,恶意或低质量插件比独立进程插件拥有更大的破坏面。
- 生命周期正确不等于业务状态正确;数据库事务、外部副作用和迁移仍需插件自己设计。
因此,Cordis 提供的是较高的工程下限,不是自动正确。只有当运行中替换有状态能力确实重要时,这份复杂度才值得承担。
事实基线见官方 Cordis Primer、Architecture 与 Cordis 论文。想动手验证的读者可参考 NanoCordis——一个约 1600 行的教育复现,覆盖了 Plugin、Context、inject、Event 和 Effect 的核心机制,但有意省略了 streaming、per-session scopes、subagents、sandboxing 和审计事件;这些省略恰好标记了”Demo 能跑”与”生产可用”之间的差距。
小结
- Context 让能力按服务键和作用域发现,
inject让加载顺序由依赖推导。 - Typed Event 把通知、协作和策略否决分开,
waterfall是主要拦截机制。 - Effect 与 Fiber 让副作用有归属、可撤销,构成 HMR 的前提。
- Cordis 真正服务的是运行中替换有状态组件,尤其是 Agent Loop,而不是让普通插件显得更复杂。
下一篇建议继续看: