Everything is a Plugin
deepseek-ai/deepseek-harness: DeepSeek Harness: Everything is a Plugin.
总结
核心问题:怎样让一个长期运行的软件系统,尤其是插件系统和自演化 Agent Harness,能够在不中断整体运行的情况下,动态加载、卸载、替换组件,并且正确处理组件造成的副作用和组件之间的依赖关系
底层是 Cordis 的插件框架
| 术语 | 含义 |
|---|---|
Component |
组件,Plugin是具体的组件 |
Fiber |
组件运行实例 |
Context |
组件共享的环境 |
Effect |
组件对 Context 做的修改 |
Coeffect |
组件需求 |
Realm |
隔离域 |
Context Paradigm |
论文提出的范式 |
问题背景
传统的软件组合主要是静态组合
function callmodule importclass inheritance
这些关系通常在编译期或程序启动时就固定了
论文以 VSCode 为例
- 时间问题:基本所有拓展的激活或卸载都需要重启
- 空间问题:拓展之间基本没有明确依赖关系
现代系统越来越需要 动态组合 Dynamic Composition ,组件可以在运行时被加载、卸载和重新配置
把动态组合分成两个正交维度:
- 时间可组合性 Temporal Composability:组件卸载后,对共享环境造成的影响能够被安全撤销
- 空间可组合性 Spatial Composability:组件能够声明、发现和解析彼此的依赖,并且在依赖变化时协调组件生命周期
映射到两个已有的程序语言理论概念
- Effect:如何修改运行环境
- Coffect:对运行环境有什么要求
传统 Effect / Coeffect 用于静态类型系统和编译期分析
关键是把 Effect 和 Coeffect 从静态分析概念提升成 Runtime 可操作的机制
理论基础
Revertible Effects
假设插件 A 加载后做了一系列行为,卸载 A 时不能要求开发者再手工写,这种 activate() / deactivate() 分离的模式很容易漏
核心想法:效应函数返回的不只是新状态,还带一个逆操作
把
在论文中大写字母一般表示通用概念,小写字母代表具体实例
后续的 $\gamma$ 表示的是某一个具体的 Runtime 状态
1 | effect() |
inverse 必须在 effect 实际执行时产生,因为撤销操作往往依赖当时的现场状态
提出 Effect Context 用于管理 inverse
1 | Component A |
卸载时按照 LIFO 的顺序退出
但这会遇到一个问题,比如
1 | A.effect1 |
如果只卸载 A,由于 B 穿插在中间,A 的 inverse 接触到的 Context 已经不是 A.effect 结束时那个原始 Context
在 3.3 Context Paradigm 中证明了 Observational Equivalence,只要观察到的环境相同就视为
context等价,无须严格相等
Effect 可逆的两种实现方式 [Definition 27]
| In-place 原地实现 | Derived 派生实现 | |
|---|---|---|
| 修改原 Context | 是 | 否 |
| 创建新 Context | 否 | 是 |
| 恢复方式 | inverse | 丢弃新 Context |
| 隔离能力 | 较弱 | 较强 |
| 性质 | 时间上的逆 | 空间上的逆 |
Reactive Coeffects
传统的控制反转 (IoC) 通常把依赖建模成一个 key → value 的表
定义的四个基础操作
| 符号 | 含义 | 作用 |
|---|---|---|
| $\sigma(k)$ | 读取 | get |
| $\sigma[k\mapsto v]$ | 更新/添加 | set |
| $\sigma\setminus k$ | 删除 | remove |
| $k\in dom(\sigma)$ | 检查存在 | membership |
在 set 后同时返回 inverse,因为注册插件后相当于修改了 Context,所以本身就是 Revertible Effect
假设组件 B 依赖
1 | Database |
如果 Database 还没有被其他组件提供,此时启动 B:
1 | B activate |
所以组件运行前需要先声明自己需要哪些依赖 Coeffect Specification
到这里还有一个问题,前面的 Context 是一个全局共享表
但在真实系统中不同插件可能需要同一个 key,但需要的 value 不一样
1 | Plugin A -> Database → MySQL |
加入 Isolation[隔离] 让同一个 key,在不同 Context 中解析成不同 value
采用 Derived 派生实现 isolation realm 隔离域
可增加的信息有两部分
Context:$\iota(k)$Component:$\mu(k)$
以 Context 的为主
1 | Search Plugin: |
这些主要是为后面权限控制、sandbox 等场景铺路
Context Paradigm
通过上面的理论可知
- Effect Context:关注「组件改变了什么」
- Coeffect Context:关注「组件需要什么」
论文指出一个真正的 Runtime Context 应该同时包含 Effect 和 Coeffect
定义:
Unified Context 支持树状结构 Hierarchical composition,这个说法很像 python 里的类继承
$\Gamma_\infty$ 的递归结构支持分层控制,父 Context 聚合多个子 Context,在保持模块化的同时实现跨层统一管理
这种层级结构使得 Plugin 能够形成层级化、可加载、可卸载的组合系统
把前面的逻辑拼接起来
1 | flowchart TD |
对比传统范式
- A. Functional:Explicit state threading(显式状态传递)-> 为了管理 Effect,需要写大量样板代码
- B. Imperative:Implicit mutation(隐式修改)-> 无法知晓状态,无法追溯
Harness 想同时获得函数式的显式状态传递,也想获得命令式的隐式修改
状态放入 Context,但通过 Effect/Coeffect 管理
Context Paradigm 是介于函数式和命令式之间的一种范式
动态组合演算
理论部分解决了一个组件如何描述自己的依赖(Coeffect)和修改(Effect),以及 Context 如何管理它们
接下来需要考虑多个的交互
Fiber状态机
把一个 Component 定义成三元组
- $d$:Coeffect Specification,描述组件需要哪些 Context
- $p$:组件会向 Context 提供哪些 key
- $e$:组件激活时执行的 Effect
Fibers: One component may be instantiated many times over, each instantiation carrying a lifecycle state of its own.
Fiber 是 Component 的运行实例,在 Component 基础上增加运行时状态
$\pi$:父 Context / 父 Fiber,表示层级关系
$\tau$:retirement flag,表示 Fiber 是否进入退休阶段
- $\bot$:正常存在
- $\top$:准备删除,不再接受新的生命周期变化
$\sigma$:Fiber 自己维护的 coeffect table,用于记录依赖状态
$\theta$:生命周期状态[可以理解为进程存在,停止/运行]
$$ \theta \in \{Inactive, Active \} $$
最简单情况下,Fiber 只有两个运行状态
基础生命周期
L-Reload:激活 Fiber,执行 Effect 并修改ContextL-Unload:停止 Fiber,执行 Effect 对应的inverse,恢复Context
在此基础上还需要管理 Fiber 的存在,引入生命周期规则
生命周期规则分两类:
O-*:管理 Fiber 的存在L-*:管理 Fiber 的运行状态
| 生命周期规则 | 状态变化 | 作用 | 对 Context 的影响 |
|---|---|---|---|
| O-Insert | Component → Fiber | 实例化 Component | 无影响,$\theta$ 初始为 Inactive |
| L-Reload | Inactive → Active | 激活 Fiber,执行组件 Effect | 修改并记录 inverse |
| L-Unload | Active → Inactive | 停止 Fiber,执行 inverse effect | 撤销修改,恢复原状态 |
| O-Retire | Fiber → Retired | 标记 Fiber 准备删除 | 若仍 Active,需要先 L-Unload |
| O-Remove | Retired → Removed | 删除 Fiber 实例 | 不再影响 Context |
在这里 Inactive <----> Active 是假设生命周期切换瞬间完成,但是实际情况中中途可能失败、等待
于是引入 Transition state,重新定义生命周期状态
动态生命周期
其中:
- $\zeta$:结果信息
- $i$:当前 effect 执行到哪里
- $g$:
accumulator已经完成的操作 - $w$:已经提交到状态
目的是为了即使中途暂停、失败,也知道如何继续或者恢复,Reloading 和 Unloading 就是为了中间状态的处理设计的
Reloading -> Unloading:处理状态转移中途的异常问题,保证部分 Effect 一定经过 inverse 恢复L-Iter:把状态变化从从一次操作变成可暂停、可继续的序列L-Divert:Context变化导致的目标状态转向L-Raise:状态转移过程中抛出错误需要回滚
动态组合理论
第 4 章开头说明
establishes the metatheory of that calculus, namely preservation, global temporal and spatial composability, progress, and confluence.
5 个核心:
| 性质 | 解决的问题 |
|---|---|
Preservation |
运行过程中系统不会进入非法状态 |
Global Temporal Composability |
多个组件按不同时间顺序执行仍可组合 |
Global Spatial Composability |
组件依赖动态变化仍保持一致 |
Progress |
transition 不会卡死 |
Confluence |
不同执行路径最终结果一致 |
框架 Cordis
Cordis is a meta-framework of spatiotemporal composability.
it prescribes no concrete scenario; its sole responsibility is to supply universal dynamic composition semantics.
Koishi 是基于 Cordis 构建的开源聊天机器人框架,拥有 4000+ 社区插件,用于验证动态组合模型
Core Library
将论文理论与代码 Runtime 的 API 对应
| 理论概念 | Runtime API | 代码行为 |
|---|---|---|
| Unified Context | ctx |
一个 Fiber 运行时所处的 Context |
| Effect | ctx.effect(callback) |
注册 Effect callback,并返回 inverse |
| Coeffect Context | ctx[@@store] |
保存 realm → value 的 binding |
| Isolated Coeffect | ctx[@@isolate] |
保存 key 到 realm 的映射 |
| Interception | ctx[@@intercept] |
保存 key 对应的 metadata |
| Dependency lookup | ctx.get(key) |
从 Context store 中读取 binding |
| Dependency provision | ctx.set(key,value) |
向 Context 提供 binding |
| Isolation | ctx.isolate(key, realm) |
修改 key 的 isolation realm |
| Interception | ctx.intercept(key, metadata) |
修改 key 的 interception metadata |
| Fiber | ctx.use(component) |
将 Component 实例化为 Fiber |
Effect Tracking
Every context mutation in Cordis flows through a single primitive, ctx.effect
Cordis 中所有 Context 修改都会转换为
1 | ctx.effect(callback) |
所以最终都会被 Effect Tracking 管理,这使得组件卸载时可以自动恢复 Context
举例:
1 | ctx.effect(() => { |
支持
- self-disposal:dispose 后停止继续收集 inverse
- parent composition:当前 Effect 的 dispose 会加入父 Context 的 dispose 链
Algorithm 1 Effect tracking p56
Coeffect Operations
All coeffect operations act on three symbol-keyed slots that each context carries
每个 Context 包含三个内部表
1 | ctx |
ctx.get
1 | function get(ctx,key) |
论文称其为 two-layer resolution
ctx.set
1 | function set(ctx,key,value) |
确定 realm -> 写入@@store 的 binding -> 通知依赖 -> 记录 inverse
Algorithm 2 Coeffect operations p57
notify 遍历 Fiber 判断:
Fiber 是否声明依赖
1
key ∈ fiber.inject
是否属于同一个 isolation realm
1
fiber.ctx[@@isolate][key] = ctx[@@isolate][key]
都满足则 refresh(fiber)
Algorithm 3 Reactive notification p58
1 | ctx.isolate(key, realm) |
修改 $\rho(key)$ 使得一个 key 可以指向不同的 realm
1 | ctx.intercept(key, metadata) |
用于合并 key 的 metadata,影响访问行为,而不是改变 binding 本身
Component Lifecycle
A component is instantiated as a fiber by ctx.use
ctx.use() 将 Component 转换为 Fiber 实例
这里可以先回顾一下 Component 和 Fiber 的定义
Fiber 关键字段
fiber.parent:形成 Component hierarchyfiber.inertia:保存正在执行的异步 transition
ctx.use
1 | function use(ctx, component, config) |
本身就是一个 Effect
- 执行:
refresh(fiber) - recovery:设置
fiber.target = ⊥(这里的 target = ⊥ 是指没有目标状态了,和之前状态机那里的不一样) - 调用:
unload(fiber)
Algorithm 4 Component instantiation p58
refresh(fiber) 根据当前依赖目标决定状态转换
target 是根据当前 coeffect store 计算出的目标 provider 集合
- 计算 target -> compare -> reload/unload
- target != ⊥ -> reload
- target = ⊥ -> unload
1 | function reload(fiber) |
1 | function unload(fiber) |
Algorithm 5 Component lifecycle p59
Context Access
ctx.get/set 提供底层 coeffect binding 操作
组件访问 ctx[key] 需要经过 Fiber-aware resolution
resolve(ctx,key)
1 | if key ∈ fiber.committed // committed 存在,当前Fiber已经有该依赖 |
Algorithm 6 Proxy-mediated context access p61
Component Loader
Component Loader 是负责把静态的 Component 定义加载到 Runtime 中的基础设施,它连接了“组件代码”和“运行时实例化”
整体分三部分:
- Entry
- Reconciliation
- Managed Realms
Entry:Loader 管理的基本单位
Every configuration consists of entries. Each entry specifies a fiber and manages it.
一个 Entry 对应一个 Fiber,描述一个组件实例需要的信息
| 字段 | 作用 |
|---|---|
| id | 稳定标识 |
| url | 组件模块地址 |
| isolate | 隔离配置 |
| intercept | 拦截配置 |
| config | 传入组件的配置 |
| disabled | 是否禁用 |
1 | id: database |
Reconciliation:配置变化如何同步 Fiber
Loader 不会每次重新创建整个 Fiber 树,在Cordis 中会比较 旧配置与新配置 diff 的区域,只修改变化部分
变化判断:
| Entry 变化字段 | 含义 | Loader 行为 | 原因 |
|---|---|---|---|
id |
Fiber 身份变化 | rebuild Fiber | 身份改变,需要重新实例化对应 Fiber |
url |
Component 来源变化 | rebuild Fiber | 加载目标组件改变,需要重新创建实例 |
isolate |
key 的 isolation realm 变化 | realm reassignment | 更新 key → realm 映射,并迁移相关 binding |
intercept |
interception metadata 变化 | update metadata | interception 在访问时生效,不影响 Fiber 生命周期 |
config |
Component 配置变化 | 交给 Component 自己处理 | Loader 不决定组件内部配置变化如何响应 |
disabled |
组件启停状态变化 | unload / reload Fiber | disabled 直接影响 Fiber 是否运行 |
Hot Module Replacement
传统热插拔可能丢失信息
由于 Fiber 已经记录 Component 的 effect 和 coeffect,卸载旧 Fiber 可以自动恢复旧状态,重新实例化新 Fiber 即可完成模块更新
三个阶段:
- Module classification:通过 import dependency graph 判断哪些模块可以热替换,哪些必须重启
- Safe-entry decision:判断哪些 Entry 可以安全更新
- Transactional reload:状态可回退,失败则恢复旧状态
讨论
论文自省
System Boundary
一个 service 有多个 provider 怎么办?并不是所有 Effect 都可逆
定义边界,如果:
- 系统可以独占修改它;
- 系统可以恢复修改前状态;
那么它在 boundary 内,反之无法恢复,则在 boundary 外
Service Multiplexing
当多个 Component 都提供同一种 Service 时,Cordis 如何支持多个 Provider 共存,以及如何减少 Provider 切换对 Consumer 的影响
| 角色 | Cordis概念 |
|---|---|
| Provider | 提供 key 的 Component |
| Consumer | inject key 的 Component |
方案一:Exclusive Binding
多个 Provider 共享一个接口,但同一时间最多只有一个 Provider 被绑定
切换需要 unload 原来的和 load 另一个 Provider,但这样 Consumer 会受到影响
switching between them requires unloading one provider and loading another, momentarily perturbing every consumer’s dependency.
方案二:Service Broker
不要让 Consumer 直接依赖 Provider,在中间加一个 Component
使得多个 Provider 共存, Provider 更新时 Consumer 不受影响
updating a backing provider leaves the broker in place, so consumers see no change to their dependency and no reload is triggered.
| Exclusive Binding | Service Broker | |
|---|---|---|
| Consumer 依赖 | 具体 Provider | 稳定 Broker |
| Provider 数量 | 一个 active | 多个共存 |
| 切换 Provider | 重新绑定 Service | 更新 Broker 内部状态 |
| Consumer 是否 reload | 会 | 不会 |
| 复杂度 | 低 | 高 |
Access Control and Sandboxing
动态插件系统最大的问题:插件能访问什么?恶意插件怎么办?
两层控制:
- Dependency Access Control:基于插件能力的保护
- Interception:隔离控制
但 Inception 的安全程度还是弱于 Sandbox
Mutual Dependencies
组件循环依赖怎么办?
需要拆组件才行,这也会导致复杂度的提高


