RUNTIME FIELD GUIDE

从插件框架到
可演化 Agent Runtime

用一个统一的 Context,理解 Cordis 如何记录副作用、重解依赖、协调异步生命周期,以及 DeepSeek Harness 如何把这些能力组织成 Agent 的运行底座。

Context
Effect 我改变了什么? 注册、监听、句柄、子插件
Coeffect 我依赖谁? 服务、能力、作用域、策略
Fiber 把两面编排成
可加载、可卸载、可重载的组件生命周期

动态组合不是“能加载插件”,而是能在运行中安全地改变系统组成。

传统插件系统通常能完成一次启动:把模块加载进进程、注册命令和事件,然后开始工作。真正困难发生在运行中:某个模块升级、依赖服务掉线、策略更换、用户切换 sandbox,其他模块会不会持有失效对象、遗留事件监听器,或被迫让整进程重启?

Cordis 把问题拆成两条互补的轴。时间轴处理组件离开时如何回收自己的影响;空间轴处理组件依赖的能力由谁提供、提供者变化时谁需要重新装配。

加载effect记录 inverse
解析coeffect锁定 provider
执行fiber协调生命周期
重组reconcile趋向最终配置

副作用是一种资源,创建它时就要交出撤销它的方法。

定时器、事件监听器、端口、路由、服务注册、子插件都不是函数返回后自然消失的东西。它们持续占据运行时状态。一个能热重载的系统,必须让每个插件拥有自己创建的资源,并能在卸载时完整回收。

最小心智模型
type Dispose = () => void | Promise<void>

class Context {
  private disposers: Dispose[] = []

  effect(setup: () => void | Dispose): Dispose {
    const cleanup = setup() ?? (() => {})
    let active = true

    const dispose = async () => {
      if (!active) return
      active = false
      await cleanup()
    }

    this.disposers.push(dispose)
    return dispose
  }

  async dispose() {
    while (this.disposers.length) await this.disposers.pop()!()
  }
}
为什么逆序清理?

后创建的 effect 经常依赖先创建的 effect。先注册监听器、后关闭数据库,会让监听器的收尾失去它需要的连接。Cordis 将 disposer 以 LIFO 组合,得到“后创建、先撤销”的安全默认值。

加载f : S → S′
再撤销
清理g : S′ → S
g(f(S)) ≈ S观察上等价,不要求物理状态逐比特复原

服务不是一次性注入的对象,而是会出现、消失、替换的运行时能力。

普通 DI 常在启动时把对象传给消费者。Cordis 则要求消费者声明它需要的 capability,运行时持续观察这个要求是否被满足。论文把“程序对环境的需求”称为 coeffect。

Provider

SQLite Plugin

provide: ['database']创建连接并提供能力

Consumer

Search Plugin

inject: ['database']只在依赖可用时激活
插件声明
function sqlite(ctx: Context) {
  const db = createSqliteConnection('./app.db')

  // 先登记连接关闭;后登记 provider。
  // 卸载时按 LIFO:先 unprovide 并等待消费者,再关闭 db。
  ctx.effect(() => () => db.close())
  ctx.provide('database', db)
}

const search = {
  inject: ['database'],
  apply(ctx: Context) {
    ctx.command('search <keyword>')
      .action((_, keyword) => ctx.database.search(keyword))
  },
}

关键不只是“服务存在”。消费者会记录自己加载时解析到的 provider Fiber identity。`database → D1 / uid 42` 换成 `database → D2 / uid 97`,即使 API 一模一样,消费者也会被重载,避免继续握着已经关闭的旧连接。

Fiber 是组件的运行实例:它让异步加载、撤销与依赖替换拥有可判定的时序。

一个 Plugin 是可复用定义;一个 Fiber 是它在某个 Context 下的一次实例化。Fiber 保存自己的 effect accumulator、当前依赖解析结果、加载状态和正在进行的异步任务。

Database D1PENDING
Search SPENDING

没有 `database` provider,Search Fiber 不能启动。它不是报错后勉强运行,而是保持 PENDING。

  1. 加载前,Fiber 计算 `inject` 指向的 provider identities。
  2. 加载中,每次 effect 都把 inverse 纳入该 Fiber 的 accumulator。
  3. 依赖变动,identity 改变时 Fiber 进入 LOADING 或 UNLOADING,而不是让旧引用继续存活。
  4. 卸载,消费者先完成 disposer;提供者保留自身能力直到相关消费者稳定,才真正释放。

“最终状态只取决于最终配置”是条件结论,不是对任意 JavaScript 的魔法。

Recovery Exactness

卸载只撤销自己的贡献

前提是不同组件的 effect 彼此独立或可交换。事件监听表、路由表通常满足;有顺序语义的 middleware 则不天然满足。

Ordering

提供者后退出

消费者只有在依赖已提供时才能启动;提供者退出时,消费者先清理,因此清理阶段仍可读取旧能力。

Progress

无环、有限,才会静稳

循环依赖会让组件永远 PENDING;无界自我注册会破坏终止性。组件边界与依赖图设计仍是人的责任。

Confluence

历史不应污染最终配置

在独立 effect、无环依赖、无失败 Fiber、完整 provision 等前提下,不同重载路径应收敛到同一个静稳态。

行为是否自动可逆合理策略
事件监听、定时器、端口句柄通常可以由 Context 记录 disposer
服务注册、子插件、路由通常可以以 effect 组合与 LIFO 回收
已发送消息、外部 HTTP、扣款不能普遍回滚延迟提交、幂等键、Saga 补偿
绕开 Context 的全局修改运行时不可见收口为 capability 或隔离进程

DSH 把 Agent 的每一项能力放入同一棵可重组的插件树。

DSH 不把 agent loop、模型、工具、会话和 sandbox 固化为不可替换的内核。它以 Profile 与 Bundle 叠加出一棵 Cordis 配置树;条目按服务可用性激活,补丁改变声明式配置,Loader 再将运行态逐步 reconcile 到目标树。

Profile: web
Base Runtime
  • LLM adapter
  • Agent loop
  • Session log
  • Tool registry
Execution Boundary
  • Filesystem
  • Subprocess / PTY
  • Sandbox
  • Approval policy
Interaction Surface
  • Web client
  • Commands
  • Goals
  • Subagents

为何它更像 Runtime Infra?

模型、工具、执行环境和交互层都通过 capability seam 连接。替换 provider 时,依赖这个 seam 的消费者会得到一致的重组,而不是靠每个工具自行识别“远程模式”。

为何它还不是 AgentOS?

Cordis 管的是进程内组件语义。CPU 调度、内存隔离、网络分区、跨节点一致性和不可信代码隔离仍由宿主 OS、容器、沙箱与分布式系统承担。

按这个顺序读源码,抽象就不会飘在空中。

  1. Cordis 论文先读 3.1、3.2、4.4、5.1;公式服务于 lifecycle,不必从头硬啃。
  2. Fiber看 effect 如何收集 disposer,如何用 epoch 驱动 loading/unloading。
  3. ReflectService看 provide、notify、service resolution 与 Context Proxy。
  4. DSH Architecture再把 Cordis 的 Context 映射到 Agent、Tool、Session 和 Sandbox seam。
  5. dsh-base 配置树确认 “Everything is a Plugin” 不是宣传语,而是启动配置。