deepseek-ai/deepseek-harness: DeepSeek Harness: Everything is a Plugin.

总结

核心问题:怎样让一个长期运行的软件系统,尤其是插件系统和自演化 Agent Harness,能够在不中断整体运行的情况下,动态加载、卸载、替换组件,并且正确处理组件造成的副作用和组件之间的依赖关系

底层是 Cordis 的插件框架

术语 含义
Component 组件,Plugin是具体的组件
Fiber 组件运行实例
Context 组件共享的环境
Effect 组件对 Context 做的修改
Coeffect 组件需求
Realm 隔离域
Context Paradigm 论文提出的范式

问题背景

传统的软件组合主要是静态组合

  • function call
  • module import
  • class inheritance

这些关系通常在编译期或程序启动时就固定了

论文以 VSCode 为例

  • 时间问题:基本所有拓展的激活或卸载都需要重启
  • 空间问题:拓展之间基本没有明确依赖关系

现代系统越来越需要 动态组合 Dynamic Composition ,组件可以在运行时被加载、卸载和重新配置

把动态组合分成两个正交维度:

  • 时间可组合性 Temporal Composability:组件卸载后,对共享环境造成的影响能够被安全撤销
  • 空间可组合性 Spatial Composability:组件能够声明、发现和解析彼此的依赖,并且在依赖变化时协调组件生命周期

映射到两个已有的程序语言理论概念

  • Effect:如何修改运行环境
  • Coffect:对运行环境有什么要求

传统 Effect / Coeffect 用于静态类型系统和编译期分析

关键是把 Effect 和 Coeffect 从静态分析概念提升成 Runtime 可操作的机制

理论基础

Revertible Effects

假设插件 A 加载后做了一系列行为,卸载 A 时不能要求开发者再手工写,这种 activate() / deactivate() 分离的模式很容易漏

核心想法:效应函数返回的不只是新状态,还带一个逆操作

$$ f:\Gamma\rightarrow\Gamma $$
变成
$$ e:\Gamma\rightarrow\Gamma\times(\Gamma\rightarrow\Gamma) $$

在论文中大写字母一般表示通用概念,小写字母代表具体实例

后续的 $\gamma$ 表示的是某一个具体的 Runtime 状态

1
2
3
4
5
6
7
effect()

执行修改

同时生成 inverse

Runtime 保存 inverse

inverse 必须在 effect 实际执行时产生,因为撤销操作往往依赖当时的现场状态

提出 Effect Context 用于管理 inverse

$$ \partial\Gamma=\Gamma\times(\Gamma\rightarrow\Gamma) $$
在运行过程中会不断把 `inverse` 加进逆的累加器 Accumulator,使得 `inverse` 不需要额外维护,强制跟踪自动组合
1
2
3
4
5
6
7
Component A
effect 1
effect 2
effect 3

Accumulator
[g1, g2, g3] (按照 LIFO )

卸载时按照 LIFO 的顺序退出

但这会遇到一个问题,比如

1
2
3
4
A.effect1
B.effect1
A.effect2
B.effect2

如果只卸载 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\equiv(k:K)\rightharpoonup V_k $$
其中 $\Sigma$ 表示 Context 中存储的具体依赖映射 Coeffect Context,或者说是 Coeffect Table,主要维护的是依赖表

定义的四个基础操作

符号 含义 作用
$\sigma(k)$ 读取 get
$\sigma[k\mapsto v]$ 更新/添加 set
$\sigma\setminus k$ 删除 remove
$k\in dom(\sigma)$ 检查存在 membership

set 后同时返回 inverse,因为注册插件后相当于修改了 Context,所以本身就是 Revertible Effect

假设组件 B 依赖

1
2
3
Database
Search
Memory

如果 Database 还没有被其他组件提供,此时启动 B:

1
2
3
4
5
6
7
B activate

ctx.get("Database")

不存在

runtime failure

所以组件运行前需要先声明自己需要哪些依赖 Coeffect Specification

$$ \Omega_{\Sigma} = \operatorname{Set}(K) $$
运行过程中需要判断当前 `Context` 是否满足
$$ \sigma\models d \iff \forall k\in d,\;k\in\operatorname{dom}(\sigma) $$
`Context` 会一直变化,因此必须判断 `Context` 从 $\sigma$ 变化为 $\sigma'$ 对依赖 $d$ 有什么影响
$$ \operatorname{notify}_d(\sigma,\sigma')= \begin{cases} \text{activating}, & \sigma\not\models d\land\sigma'\models d\\ \text{deactivating}, & \sigma\models d\land\sigma'\not\models d\\ \text{neutral}, & \text{otherwise} \end{cases} $$
---

到这里还有一个问题,前面的 Context 是一个全局共享表

但在真实系统中不同插件可能需要同一个 key,但需要的 value 不一样

1
2
Plugin A -> Database → MySQL
Plugin B -> Database → PostgreSQL

加入 Isolation[隔离] 让同一个 key,在不同 Context 中解析成不同 value

采用 Derived 派生实现 isolation realm 隔离域

$$ k\rightarrow\rho(k)\rightarrow\sigma(\rho(k)) $$
加入 Interception[拦截],类似 Java 的 AOP (Aspect Oriented Programming),在原行为执行过程中插入控制逻辑

可增加的信息有两部分

  • Context:$\iota(k)$
  • Component:$\mu(k)$
$$ \iota(k) \oplus \mu(k) $$

Context 的为主

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Search Plugin:
{
"cache": false,
"timeout": 10
}

Context:
{
"cache": true
}

{
"cache": true,
"timeout": 10
}

这些主要是为后面权限控制、sandbox 等场景铺路

Context Paradigm

通过上面的理论可知

  • Effect Context:关注「组件改变了什么」
  • Coeffect Context:关注「组件需要什么」

论文指出一个真正的 Runtime Context 应该同时包含 Effect 和 Coeffect

定义:

$$ \Gamma_\infty \equiv \mu\Gamma.\Gamma\times(\Gamma\rightarrow\Gamma)\times\Sigma $$
- $\mu\Gamma.\Gamma$:递归的环境 - $\Gamma\rightarrow\Gamma$:Accumulator - $\sum$:Coeffect Context

Unified Context 支持树状结构 Hierarchical composition,这个说法很像 python 里的类继承

$\Gamma_\infty$ 的递归结构支持分层控制,父 Context 聚合多个子 Context,在保持模块化的同时实现跨层统一管理

这种层级结构使得 Plugin 能够形成层级化、可加载、可卸载的组合系统

把前面的逻辑拼接起来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
flowchart TD

C[Component]

C --> CO[Effects]
C --> EF[Coeffects]


CO --> E[Specification Check]
EF --> S[Execute Effect]

S --> U
E --> U

subgraph U[Unified Context Γ∞]
CS[Effect Accumulator]
EA[Coeffect Context Σ]
end

U --> CC[Context Change]

CC --> N[Notify]

N --> A[Activate]
N --> D[Deactivate]

A --> RE[Run Effects]

D --> AI[Apply Inverse]
AI --> R[Recover State]

对比传统范式

  • A. Functional:Explicit state threading(显式状态传递)-> 为了管理 Effect,需要写大量样板代码
  • B. Imperative:Implicit mutation(隐式修改)-> 无法知晓状态,无法追溯

Harness 想同时获得函数式的显式状态传递,也想获得命令式的隐式修改

状态放入 Context,但通过 Effect/Coeffect 管理

Context Paradigm 是介于函数式和命令式之间的一种范式

动态组合演算

理论部分解决了一个组件如何描述自己的依赖(Coeffect)和修改(Effect),以及 Context 如何管理它们

接下来需要考虑多个的交互

Fiber状态机

把一个 Component 定义成三元组

$$ C = (d, p, e) $$
  • $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 基础上增加运行时状态

$$ f=(\pi,\tau,\sigma,\theta) $$
  • $\pi$:父 Context / 父 Fiber,表示层级关系

  • $\tau$:retirement flag,表示 Fiber 是否进入退休阶段

    • $\bot$:正常存在
    • $\top$:准备删除,不再接受新的生命周期变化
  • $\sigma$:Fiber 自己维护的 coeffect table,用于记录依赖状态

  • $\theta$:生命周期状态[可以理解为进程存在,停止/运行]

    $$ \theta \in \{Inactive, Active \} $$

回到Corids

最简单情况下,Fiber 只有两个运行状态

基础生命周期

image-20260821092426343
  • L-Reload:激活 Fiber,执行 Effect 并修改 Context
  • L-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,重新定义生命周期状态

动态生命周期

$$ \theta_n \in { Inactive(\zeta), Reloading(i,g,\omega), Active(g,\omega), Unloading(g,\omega,\zeta) } $$
image-20260821094804187

其中:

  • $\zeta$:结果信息
  • $i$:当前 effect 执行到哪里
  • $g$:accumulator 已经完成的操作
  • $w$:已经提交到状态

目的是为了即使中途暂停、失败,也知道如何继续或者恢复,ReloadingUnloading 就是为了中间状态的处理设计的

  • Reloading -> Unloading:处理状态转移中途的异常问题,保证部分 Effect 一定经过 inverse 恢复
  • L-Iter:把状态变化从从一次操作变成可暂停、可继续的序列
  • L-DivertContext 变化导致的目标状态转向
  • 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
2
3
4
5
6
ctx.effect(() => {
ctx.set("db", database)
return () => {
ctx.delete("db")
}
})

支持

  • 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
2
3
4
5
6
7
ctx
|
+-- @@store 保存 realm → value 的 binding
|
+-- @@isolate 保存 key → realm 的映射
|
+-- @@intercept 保存 key → metadata

ctx.get

1
2
3
function get(ctx,key)
realm ← ctx[@@isolate][key]
return ctx[@@store][realm]
$$ k \rightarrow \rho(k) \rightarrow \sigma(\rho(k)) $$

论文称其为 two-layer resolution

ctx.set

1
2
3
4
5
function set(ctx,key,value)
realm ← ctx[@@isolate][key]
ctx[@@store][realm] ← value
notify(ctx,key)
return ctx.effect(callback)

确定 realm -> 写入@@store 的 binding -> 通知依赖 -> 记录 inverse

Algorithm 2 Coeffect operations p57

notify 遍历 Fiber 判断:

  1. Fiber 是否声明依赖

    1
    key ∈ fiber.inject
  2. 是否属于同一个 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 hierarchy
  • fiber.inertia:保存正在执行的异步 transition

ctx.use

1
2
3
4
5
6
7
8
function use(ctx, component, config)
fiber ← Fiber(parent: ctx,
inject: component.inject) // 创建 Fiber
fiber.ctx ← ctx[fiber ↦ fiber] // 创建 Fiber Context

fiber.apply ← () =>
component.apply(fiber.ctx, config) // 保存
ctx.effect(callback) // 注册 Effect

本身就是一个 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
2
3
4
5
6
function reload(fiber)
fiber.committedresolve(fiber.inject) // 解析当前依赖
recover ← await execute(fiber.apply, () ↦ fiber.target = target0) // 执行 component effect
fiber.dispose ← recover ∘ fiber.dispose // 记录 inverse
if fiber.target = target0 // 如果 target 未变化,则进入 ACTIVE
fiber.stateACTIVE
1
2
3
4
5
6
function unload(fiber)
await all(notify(fiber.ctx, provided(fiber)).map(f ↦ f.await())) // 等待依赖停止
await fiber.dispose() // 执行 inverse
if fiber.target = ⊥
fiber.stateINACTIVE
fiber.inertianull // 清空残留

Algorithm 5 Component lifecycle p59

Context Access

ctx.get/set 提供底层 coeffect binding 操作

组件访问 ctx[key] 需要经过 Fiber-aware resolution

resolve(ctx,key)

1
2
3
4
5
6
7
8
9
10
if key ∈ fiber.committed  // committed 存在,当前Fiber已经有该依赖
return binding

if key ∈ fiber.inject // 组件声明需要该依赖,但当前没有有效 binding
INACTIVE_ACCESS

if fiber == root // root 仍不存在,访问了未声明依赖
UNDECLARED_ACCESS

fiber ← fiber.parent.fiber

Algorithm 6 Proxy-mediated context access p61

Component Loader

Component Loader 是负责把静态的 Component 定义加载到 Runtime 中的基础设施,它连接了“组件代码”和“运行时实例化”

整体分三部分:

  1. Entry
  2. Reconciliation
  3. 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
2
3
4
5
6
7
8
id: database

url: database-plugin

config:
host: localhost

disabled: false

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 即可完成模块更新

三个阶段:

  1. Module classification:通过 import dependency graph 判断哪些模块可以热替换,哪些必须重启
  2. Safe-entry decision:判断哪些 Entry 可以安全更新
  3. Transactional reload:状态可回退,失败则恢复旧状态

讨论

论文自省

System Boundary

一个 service 有多个 provider 怎么办?并不是所有 Effect 都可逆

定义边界,如果:

  1. 系统可以独占修改它;
  2. 系统可以恢复修改前状态;

那么它在 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

动态插件系统最大的问题:插件能访问什么?恶意插件怎么办?

两层控制:

  1. Dependency Access Control:基于插件能力的保护
  2. Interception:隔离控制

但 Inception 的安全程度还是弱于 Sandbox

Mutual Dependencies

组件循环依赖怎么办?

需要拆组件才行,这也会导致复杂度的提高