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.llmctx.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 PrimerArchitectureCordis 论文。想动手验证的读者可参考 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,而不是让普通插件显得更复杂。

下一篇建议继续看: