Cordis:DeepSeek Harness 为什么把 Agent 运行时重写成一套"可逆的组件系统"

核心问题:为什么 DeepSeek Harness 选择 Cordis?它解决了传统 Agent Harness 的什么问题?它是否代表了 Agent Runtime 的一种新架构?

本文基于源码级研究:deepseek-ai/deepseek-harnesscordiverse/cordis(v4.x)、论文 arXiv:2608.25512。所有关键结论标注来源性质:【源码事实】(可直接从仓库代码/文档证明)、【论文观点】(来自论文摘要与 README 原文)、【研究推论】(本文作者基于前两者的判断)。论文 HTML 全文在本次研究中不可得,涉及论文细节均只引用摘要原文。

1. 开头:为什么 Agent 需要 Runtime

大多数 Agent 框架解决的是两个问题:下一轮对话模型看见什么(Prompt Composition),以及进程之间怎么调用(MCP 之类的工具协议)。但只要一个 Agent 活得足够久——长会话、热改工具、动态挂载能力——第三个问题就会浮出水面:

同一个进程里的能力,如何动态地出现、消失、替换,而不影响其他组件?

传统答案是"重启进程"。但对一个跑着 30 个会话、持有浏览器连接、缓存着向量索引、正在执行长任务的 Agent Harness 来说,重启意味着一切归零。这不是 Prompt 层的问题,也不是进程层的问题,而是运行时层的问题。

DeepSeek 的 Harness 项目给出的答案是一个叫 Cordis 的框架。README 里一句话点明了架构【源码事实,deepseek-harness/README.md:7】:

It is built on an everything-is-a-plugin architecture and powered by Cordis, whose design is described in A Programming Paradigm for Spatiotemporal Composability.

要理解这句话的分量,得先理解 Cordis 到底做了什么。

2. DeepSeek Harness 是什么

DeepSeek Harness(下称 DSH)是 DeepSeek 开源的 Agent 运行框架:一个 TypeScript monorepo,packages/ 下 50 多个包(agent、agent-loop、session、tools、llm、mcp、sandbox、plan、workflow、api、web……),apps/cli 提供 CLI 与 Web 界面,native/ 下有 Landlock 沙箱辅助。它就是一个功能完整的"编码 Agent 底座":多会话、多模型 provider、工具调用、MCP、LSP、终端、子代理、计划模式、目标管理。

但它最有研究价值的部分不是功能列表,而是它的装配方式:整个系统由 Cordis 插件树组成,agent loop 是插件、工具注册表是插件、session 存储是插件、LLM provider 是插件,甚至"Agent 修改自己运行时"的那组工具也是插件。

3. “Everything is a Plugin” 到底意味着什么

这句宣传语很容易被理解成"我们代码组织得好"。它真正的含义要回到源码。

DSH 的核心服务全部继承 Cordis 的 Service 基类,并用静态字段声明依赖【源码事实】:

// packages/core/agent-loop/src/index.ts:359-360
export class AgentLoop extends Service implements AgentFactory {
  static inject = ['agents', 'sessions', 'llm', 'tools', 'systemPrompt', 'sessionProjections']
// packages/core/tools/src/index.ts:780-781
export class ToolRuntime extends Service {
  static inject = ['systemPrompt']
// packages/llm/llm-deepseek/src/index.ts:85 —— provider 也是插件
export const inject = ['llm']

static inject 不是普通的依赖注入注解。在 Cordis 里,它是一份运行时激活契约AgentLoop fiber 只有在 agentssessionsllm 等服务全部处于 ACTIVE 状态时才会执行构造;任何一个上游服务被卸载,AgentLoop自动去激活并撤销它注册过的一切;服务恢复后,它再自动重新激活。依赖不是"启动时装配好",而是"持续被运行时追踪和被强制兑现"。

对照传统 Harness(含我自己跑在上面的 OpenClaw、以及 Claude Code、Codex、OpenCode 一类),组件依赖通常是"初始化图 + 事件总线",一旦装配完成,运行期几乎不再处理"依赖中途消失"这种情形。DeepSeek Harness 把这件事交给了框架层。

哪些东西不是插件?源码验证的边界很清晰【源码事实】:CLI 入口(apps/cli/src/bin.tsargs.ts)、构建脚本(scripts/)、启动编排函数(profile-boot.ts 里的 boot()/composeEntries() 调用者),以及 Service 内部创建的领域对象——Agent 实例本身就不是插件,它只是 this.ctx.extend({ agent: this }) 造出的上下文视图(packages/core/agent-loop/src/agent.ts:107)。也就是说:“编排和领域对象不是插件,能力是插件”。

graph TD DSH[DeepSeek Harness] --> P1[AgentLoop ★插件] DSH --> P2[ToolRuntime ★插件] DSH --> P3[SessionStore ★插件] DSH --> P4[LlmRuntime ★插件] DSH --> P5[McpClient ★插件] DSH --> P6[PlanMode/Skill/Hooks ★插件] DSH --> P7[web-cordis 自我修改工具集 ★插件] DSH --> N1[CLI 入口 ✗非插件] DSH --> N2[boot 编排 ✗非插件] DSH --> N3[Agent 领域对象 ✗非插件] style P1 fill:#4a90d9,color:#fff style P7 fill:#e67e22,color:#fff

(★ = 经源码验证的 Cordis Service/Plugin)

4. Cordis 是什么:一个最小心智模型

Cordis 自称 A Meta-Framework of Spatiotemporal Composability(时空可组合性元框架)【源码事实,cordis README】。抛开术语,一个 Cordis 程序由这些概念构成:

Context ─── 一切交互的中介(Proxy 对象)
 ├── Plugin     ── 单位组件:function | class | { apply }
 ├── Service    ── 挂在 ctx.<name> 上的稳定能力槽位
 ├── Dependency ── inject 声明(我需要什么才激活)
 ├── Effect     ── 对世界的改变(注册监听/定时器/工具……)
 └── Dispose    ── 每个 Effect 自带的逆操作,运行时强制保存
      ↑ 所有以上都挂在 Fiber 上:Plugin 的一次"实例化生命周期"

各概念一句话定义(均对应 packages/core/src/ 中的真实实现):

  • Context(context.ts):服务的容器与中介。每个 Context 是一个 Proxy,ctx.foo 的读、ctx.on 的注册、ctx.effect 的登记,全部经过它的拦截。
  • Plugin(registry.ts):function(ctx, config)、类、或带 apply 的对象,附带 inject/provide/Config 元数据。
  • Service(service.ts):class Db extends Service 构造时自动 ctx.reflect.provide(name, self)——服务注册本身就是被登记的 effect。
  • Fiber(fiber.ts):一个 Plugin 实例的运行记录。同一个 Plugin 类 ctx.plugin() 两次得到两个 Fiber,各有 uidparent.registry.counter 全局递增)。状态机:PENDING → LOADING → ACTIVE / FAILED → UNLOADING → DISPOSED
  • Effect / Disposectx.effect(fn) 要求 fn 返回 disposer(或 disposer 的可迭代/异步可迭代集合),runtime 存下来,在卸载时逆序执行。
  • Isolate(context.ts isolate()):给服务槽位换 symbol 标签,让同名服务在不同 Context 域里互不可见——命名空间的运行时版。
  • Intercept(context.ts intercept()):父域为子插件预设某服务的 config 覆写,合并发生在 Service.resolveConfig 的层级链上。

论文给这套东西一个术语:context paradigm——“将 effect context 与 coeffect context 统一为单一 context 类型,所有效应与余效应都经它中介”【论文观点,摘要原文】。在源码里,这个"统一"字面成立:provide/on/timer/tool 注册全走同一个 Fiber.effect 通道,因此全都可逆。

5. Spatial Composability:组件之间怎么"活着地"连接

【论文观点】 论文定义 spatial composability 为 “the ability to declare and reactively manage inter-component dependencies”——声明并响应式管理组件间依赖。每个上下文变化会对照组件的 coeffect specification(即 inject 声明)分类,驱动其 activation/deactivation。

工程上这是最难写对的一类代码。看 Cordis 的实现【源码事实,core/src/fiber.ts】:

_refresh() {
  let epoch: string | boolean = false
  epoch = ''
  for (const name of Object.keys(this.inject)) {
    const impl = this._store[name]
    if (!impl) {
      epoch = INACTIVE   // 任何一个依赖缺席 → 立刻去激活
      break
    }
    epoch += ':' + impl.fiber.uid   // 依赖的"实例身份"进入 epoch
  }
  this._setEpoch(epoch)
}

一行 epoch += ':' + impl.fiber.uid 是整个框架的灵魂。组件的激活条件不只是"依赖存在",而是"依赖是这一个实例"。当 Provider 被替换——新的 Fiber、新的 uid——epoch 字符串变化,_setEpoch 触发先 _unload()(执行全部 disposer)再 _reload()(重跑插件构造函数)。消费者无感知、零代码地完成热迁移。

服务端的对应机制在 reflect.tsprovide() / notify()【源码事实】:

// provide 本身就登记为 effect:fiber 死,服务槽位自动撤销
provide(name, value, check) {
  return this.ctx.fiber.effect(() => {
    this.store[key] = impl
    this.notify([name])                    // 唤醒新依赖者
    return async () => {                   // disposer:注销 + 通知
      delete this.store[key]
      const fibers = this.notify([name])   // 让受影响 fiber 重算 epoch
      await Promise.allSettled(fibers.map(f => f.await()))
    }
  })
}

notify 会遍历注册表中所有 fiber,凡是 inject 里含这个名字的,全部 _checkImpl + _refresh。于是四种情形有了统一答案:

Provider 事件Consumer 行为(Cordis 自动执行)
出现epoch 从 INACTIVE 变有效 → _reload() 激活,构造器重跑
消失impl 缺席 → epoch=INACTIVE → _unload(),副作用全部逆序撤销
替换(新实例)uid 变化 → epoch 变化 → 先 unload 后 reload
重新出现同"出现";Consumer 拿到的是新实例,且它自己也是重新构造的

【研究推论】 这就是与一切"启动时解析依赖"的 IoC 容器的本质差别:Spring/Guice 的依赖是装配期事实,Cordis 的依赖是运行期持续约束。对 Agent Runtime 来说,“db 连接池被换了”、“浏览器上下文重建了"是一等场景,不是异常场景。

工程案例 A:替换数据库服务

AgentLoop inject ['llm', 'tools', 'sessions'] 
      ↑ sessions 背后是 SqliteStore
运维/插件热替换 SqliteStore → 新 Fiber(uid=42) → notify
SessionStore 旧 fiber 的全部 effect(监听器、投影、事务句柄)逆序释放
新 SessionStore 构造,AgentLoop 的 epoch 里 ":42" 变化
AgentLoop 去激活 → 再激活,内部缓存的旧引用不可能被误用

传统做法要么全局单例 + 祈祷没人换,要么手写失效通知。Cordis 里这是 _refresh() 的例行公事。

6. Temporal Composability:卸载一个组件到底要清理什么

论文定义 temporal composability 为 “the ability to completely revert a component’s side effects upon removal”【论文观点】,并形式化为 revertible effects:每个上下文变换携带一个运行时持有的逆操作。

【研究推论】 工程上对应的问题是:一个插件生命周期里"对世界做的每件事"能否被证明完整撤销?想想真实插件干了什么:

Plugin 激活
 ├── WebSocket 连接          (要 close)
 ├── 3 个事件监听器          (要 off)
 ├── 2 个 setInterval        (要 clear)
 ├── provide 了 1 个服务     (要撤销,且通知下游)
 ├── 注册了 2 个工具         (要从 tool registry 摘除)
 └── 加载了 3 个子插件       (要各自递归清理)

传统 activate()/deactivate() 模式的失控路径很典型:deactivate 是手写的,新加 effect 时忘了加对应清理;deactivate 里清理顺序反了(先关了连接、后关依赖连接的监听);异步清理没收敛,unload 返回时还有在途 Promise。Cordis 的解法是把责任倒置:你不写 deactivate,你只被要求在制造 effect 的那一刻交出 disposer

Fiber.effect 的实现【源码事实,fiber.ts】:

effect(execute: () => Effect, label = 'anonymous'): AsyncDisposable {
  this.assertActive()                       // 失活的 context 禁止新 effect
  const disposables: Disposable[] = []
  const dispose = () => {
    let task!: void | Promise<void>
    for (const dispose of disposables.splice(0).reverse()) {  // LIFO 逆序
      if (task) task = task.then(dispose)
      else {
        const result = dispose()
        if (isObject(result) && 'then' in result) task = result
      }
    }
    return task
  }
  let task: void | Promise<void>
  try {
    task = this._execute(runner)            // 支持 fn 返回
                                            // disposable | 可迭代 | Promise | 异步可迭代
  } catch (reason) {
    dispose()                               // 半途失败:已登记的部分立刻回滚
    throw reason
  }
  disposables.push(this._disposables.push(wrapper))
  return wrapper
}

注意三个细节,都是教科书级的处理:

  1. LIFO + 链式 awaitdisposables.reverse() 后逐个执行,异步 disposer 串进 Promise 链——后建立的先拆除,拆除顺序与建立顺序严格对称。
  2. 部分初始化失败的回滚catch 分支先调 dispose() 再重抛——构造到一半炸了,已经造出来的东西照样被收干净。
  3. 执行体四种返回形态:返回函数(disposer)、返回迭代器(yield 多个 disposer)、返回 Promise、返回异步迭代器(async function*,长循环可随时被中断——_execute 里每轮 runner.epoch !== oldEpoch 检查)。

于是"注册即撤销"成为普适模式,ctx.on 就是它的糖【源码事实,events.ts】:

private register(label, name, callback, options) {
  return this.ctx.fiber.effect(() => {
    hooks[method]({ ctx: this.ctx, callback, ...options })
    return () => this.unregister(name, callback)   // 监听器的逆操作
  }, label)
}

工程案例 B:卸载一个挂了两轮会话的 WebSocket 插件

一个 inject: ['ws'] 的插件被 ctx.plugin() 两次(两个 Fiber)。宿主插件 unload 时:两个子 Fiber 各自逆序清监听器、timer,其 provide 的下游被 notify 后进入 pending,Fiber.dispose 里还有 while (this.inertia) await this.inertia——等所有在途激活彻底落定才返回。【源码事实】 这正是 DSH 的 cordis_unmount 工具敢承诺"只在其自有工具、监听器、服务、定时器和其他 effect 完全停稳后返回"的底气(self-referential toolset note,.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.zh.md)。

7. Fiber:实例身份为何是必需品

回答"为什么 epoch 里放 uid 而不是名字”——因为 Plugin ≠ 实例registry.ts 里,一个插件 callback 对应一个 Runtime,Runtime 挂着 fibers: DisposableList<Fiber>——同一份插件代码可以并发存在 N 个实例(不同 config、不同父域)。没有实例身份,就会出现分布式系统经典事故:新组件握着一个"同名但已死"的旧实例引用

Cordis 用三重机制堵死这条路【源码事实】:

  1. Impl 记录 { name, fiber, value }——每个服务值带着它的主人 Fiber;strict 读取要求 impl.fiber.state === FiberState.ACTIVE,死 fiber 的服务直接不可见。
  2. ReflectService 的 Proxy get 沿 defSite.fiber → parent 链查找,且校验 inject 集合:没声明过的服务,访问即抛 cannot get property "x" without inject
  3. getTraceable/shadow 机制区分定义域(def site)与使用域(use site):跨 fiber 传对象时,内部携带的 ctx 会被追踪成"以定义它的域解析服务",杜绝借他人之手绕过隔离(utils.ts 注释原文:“def site: where the accessing code was defined, governs service resolution; use site: … governs intercept, isolate and effects”)。

一个 Fiber 的真实生命周期(枚举 FiberState_getState()/_setEpoch() 转移逻辑均见 fiber.ts)【源码事实】:

stateDiagram-v2 [*] --> PENDING: ctx.plugin() 创建 PENDING --> LOADING: 依赖齐 (_setEpoch 有效) PENDING --> PENDING: 依赖缺 (epoch=INACTIVE) LOADING --> ACTIVE: effect 执行完 LOADING --> FAILED: _execute 抛错 (_error) ACTIVE --> UNLOADING: 依赖消失/替换 (_setEpoch INACTIVE) UNLOADING --> LOADING: 依赖回来 (_refresh 重激活) UNLOADING --> DISPOSED: uid=null 彻底销毁 FAILED --> LOADING: update() 清除 _error 重试 ACTIVE --> ACTIVE: epoch 变但依赖仍齐 (重算不卸载) DISPOSED --> [*]

注意 UNLOADING → LOADING 这条回边:它是 §5 “Provider 重新出现 → Consumer 自动重激活"的状态机表达,也是 DSH cordis_run mode:"update" 能"更新过去"而不新建会话的基础。FAILED 只能靠 update()(清空 _error)恢复,_getStateif (this._error) return FAILED 就是这条规则【源码事实】。

Fiber.uid = null 还被用作"已死"标志:assertActive() 检查 uid !== null,失活 Context 上任何新 effect 注册抛 INACTIVE_EFFECT【研究推论】 卸载后的旧实例"不可被依赖、也不可再制造新副作用”,这是把 JS 的 object identity 问题上升成了组件层的形式约束——也解释了论文为什么专门提"instance identity"这类机制(摘要中的 observational equivalence 前提是组件可分辨)。

8. Failure Isolation:一个插件炸了,为什么世界还在

看一个失败路径:_reload() 里【源码事实】——

try {
  await this._execute(this._runner)
} catch (reason) {
  this.ctx.logger.error(reason)
  this._error = reason
  this._runner.epoch = INACTIVE        // 就地失败,状态 = FAILED
}

没有 rethrow 到宿主,没有进程退出。失败被消化为这一个 Fiber 的状态。隔离靠三层结构:

  • 失败不扩散:插件抛错 → 该 Fiber FAILED,effect 已回滚(§6 的 dispose 逻辑),兄弟 Fiber 的 epoch 不受影响。若它是别人的 Provider,其 provide 的 disposer 已把槽位撤销,下游 Fiber 依 reactive 规则退回 PENDING/INACTIVE,等待或超时——降级为局部功能缺失,而非崩溃
  • 清理失败不扩散_unload() 里每个 disposer 独立 Promise.all + try/catch + logger.error,一个 disposer 抛错不阻止其他 disposer。
  • 卸载观察者失败不扩散:DSH vendor 分支补的 emitPluginDisposed 加固(fiber.ts 本地修改,见 §11)。

工程案例 C:MCP 服务器连不上

packages/mcp/mcp-client 是插件。远端 MCP server 挂了 → 连接 effect 失败 → 该 server 的 Fiber FAILED → 依赖它的 tools 槽位撤销 → AgentLoop 里这些工具从 schema 消失,其余工具、会话、LLM 服务照常。整个过程没有 try/catch 散落在业务代码里——传统 harness 通常要靠"重启子进程 + 全量重连"完成同样的降级。

【研究推论】 对比 process restart 哲学(Erlang/OTP 的 supervisor tree):OTP 的隔离粒度是进程树,恢复手段是"重启干净实例";Cordis 的隔离粒度是组件,恢复手段是"epoch 重算 + 自动 reload"——相当于把 OTP 的 one-for-one 策略搬进单进程,并额外补上了 OTP 不管的副作用可逆性(BEAM 进程死掉即无副作用残留,是因为状态全在进程内;JS 插件的副作用天然逃逸到共享堆里,必须有 disposer 纪律才等价)。

9. 依赖与配置:intercept、Config schema、isolate

三个配角机制值得单独说,因为它们在 Agent 场景各有对应:

  • typed Config + schema 校验Plugin.Config 是 StandardSchema,resolveConfig 在激活前校验,失败抛 ValidationError【源码事实】。DSH 把这条用在刀刃上:self-referential 笔记里"模型编写的工具 schema 必须在注册时失败,而不是等组装提示词时才报错"的正确性要求,正是复用此通道(feature note)。
  • intercept:父级可以拦截某服务名的 config 层。Service.resolveConfig 沿原型链收集所有 intercept 覆写再 merge【源码事实】。这是"同一插件在不同上下文用不同配置"的机制——多租户 Agent、profile overlay 的基础原语。
  • isolate:把服务名解析到另一个 symbol 空间【源码事实】。DSH 的 client/server 双 realm、cordis-dynamic 分组隔离用的都是这类机制。

10. Loader 与 HMR:声明式组合 + 真正的运行时热替换

@cordisjs/plugin-loader + plugin-include 把插件树变成 YAML。DSH 的示例配置(apps/cli/config/examples/cordis/cordis.yml)【源码事实】:

- id: webserver
  config:
    host: 127.0.0.1
    port: 3081
- insert:
    - id: cordis-host-runner
      name: '@deepseek-ai/dsh-cordis-host-runner'
    - id: tool-cordis
      name: '@deepseek-ai/dsh-tool-cordis'

注意头部注释原文(同文件 1-4 行):“Temporary Plugin code can reach every injected live capability; treat this deployment like shell access, not as a security boundary.” ——官方自己把安全姿态写死在示例里,§14 细说。

文件变了怎么办?plugin-hmr 做的是真 HMR,且它的三阶段协议是 temporal composability 最完整的工程应用【源码事实,hmr/src/index.ts】:

watcher 检测文件变更
  ↓ analyzeChanges(): 沿 ESM 模块依赖图分类 accepted / declined
  ↓ 框架内部模块被改 → loader.exit()(全量重启,诚实承认热不了)
Stage 1: re-import —— 备份并清空 loadCache/CJS cache,重导入全部受影响
         的入口模块;任何一个导出失效 → rollback() 整体还原缓存(未碰任何 fiber)
Stage 2: unload —— registry.delete(plugin) 逐个同步卸载旧 fiber
         (disposer 链自然执行;uid 清空使祖先链可判)
         hasInactiveAncestor 过滤:将被级联重建的 fiber 不重复重建
Stage 3: reload —— fiber.parent.registry.plugin(replacement, fiber.config)
         并发重建;失败不回滚——"left failed, exactly as on a cold start"
flowchart LR A[文件变更] --> B{依赖图分析} B -->|框架内部| C[全量重启] B -->|插件可达| D[Stage1 re-import] D -->|失败| R[rollback 缓存快照] D -->|成功| E[Stage2 unload 旧 fiber] E --> F[Stage3 并发重建] F -->|单个失败| G[FAILED 等下次修改重试] F -->|成功| H[热替换完成]

【研究推论】 Stage 1 的 all-or-nothing 与 Stage 3 的"no rollback by design"是清醒的边界划分:模块重导入是可逆操作(快照即可),而"新代码本身有 bug"不是框架该回滚的事——回滚到新代码等于掩盖。Web 前端 HMR(Vite/React Refresh)解决的是代码替换(保住 DOM/状态),Cordis HMR 解决的是组件替换(保住依赖图上下游的活性语义),后者需要 coeffect 通知机制,前者不需要——这是 §12 对比的关键差异。

11. DeepSeek Harness 到底怎么消费 Cordis

一个能回答"为什么选择 Cordis"的细节:DSH 把 Cordis 整个 vendor 成了源码vendor/cordis 不是 node_modules,是直接躺进仓库的 @deepseek-ai/cordis@4.0.2,连同 loader/include/group/timer/hmr/logger-console 与 cosmokit/schemastery 共 9 个包【源码事实】。

动机记录在 .agents/notes/implemented/process/2026-06-11-vendor-cordis-as-source.md:仓库启动时 Cordis 还在 4.0.0-rc.6,而 harness 的 agent loop 正确性直接绑定在 fiber 生命周期、effect disposal、waterfall dispatch 这些内部机制上;RC 阶段上游可能随时破坏契约,vendor 后可在仓库内直接修。vendor/README.md 累积了 19 条本地修改日志,vendor/AGENTS.md 禁止无痕改动、pre-commit 有守卫脚本。

diff 统计(vs 上游 cordis/packages/core/src,9 个文件全有差异)【源码事实】:fiber.ts 451 行最大,events.ts 345、reflect.ts 195。改动分三类:包名 rescope、JSDoc 补全(给 API 生成器用)、以及实质性语义补丁——effectInertia WeakMap(disposer 单次调用语义 + 结构 owner 可 join 在途清理)、emitPluginDisposed 观察者失败隔离、UNLOADING 状态拒绝新 effect、Fiber.update() 返回 waterfall 结果使 Loader 能 await 重启。

最有意思的第 19 条:Node 24.0–24.11.1 的 ModuleLoader 接口被上游误标为 v2 导致 resolveSync 参数顺序反了,DSH 加了运行时 shape 探测修复,并配了钉住 24.9 的 node-compat 矩阵测试【源码事实,vendor/README.md】。

【研究推论】 这条日志同时暴露了两件事:第一,Cordis 的 HMR 依赖 Node 内部加载器 API(需 --expose-internals),它的"热"是贴着 V8/ESM 实现细节的,脆性真实存在;第二,“选择 Cordis"对 DeepSeek 而言不是选型消费,而是框架共建——上游作者 Shi Yifan 同时是论文一作(摘要署名单位含 DeepSeek-AI),Cordis 仓库与 Harness 仓库互为镜像文档。DSH README 直接链到 arXiv 论文,primer 文档就挂在 Harness 的文档站下。

12. 和 React / OSGi / VSCode / HMR / ZIO 什么关系

先交代资料边界:arXiv HTML 全文未获取到,无法确认论文是否含 related work 章节【论文观点不可得】。本章全部为**【研究推论】**,基于 Cordis 源码与各项目公开文档。

  • React useEffect:同样是 effect+cleanup 模型,但依赖是数组比较[a, b] 浅比较)驱动、组件树是固定形状(位置即身份)。Cordis 的依赖是实例身份(uid 进入 epoch)驱动、组件树本身是动态的。React 没有 coeffect 协商:effect 依赖消失 = 未定义行为(stale closure 是常态 bug);Cordis 里依赖消失 = 自动 deactivate。结论:useEffect 是单组件视角的 temporal 半句,Cordis 是系统视角的完整版。
  • OSGi:最接近的前辈——bundle 生命周期、service registry、declarative services( satisfied/activating/active 三态,与 Cordis 的 PENDING/ACTIVE/UNLOADING 神似)。OSGi 缺的两样:① effect 可逆性没有强制,Bundle deactivate 里忘了 unregister 监听器照样泄漏;② 一切围绕 Java 类型系统,服务发现是 Class 键,对 JS/动态语言与 prompt 时代的 schema 不友好。Cordis 可视为"OSGi 的 DS 规范 + algebraic effects 的 disposer 纪律 + TypeScript 类型擦除后的运行时"融合体。
  • VSCode Extension Host:经验是双向的。VSCode 用进程隔离防"扩展搞崩主窗口”,代价是扩展间只能 RPC、生命周期粗(activate/deactivate 二元)、热更新基本靠 reload window。DSH 的取舍相反:同进程、靠 disposer 纪律做隔离、用 node:vm realm 做"防手滑不防恶意"的软边界——换来了 MCP 给不了的共享实时对象(见 §13),代价是官方直接承认"信任等同 bash,不是安全边界"。
  • 前端 HMR(Webpack/Vite/React Refresh):解决"换代码保状态",接受状态泄漏(React Fast Refresh 对非组件状态无能为力,官方文档明说)。Cordis HMR 解决"换组件保语义":新实例通过重激活自然重建自己的状态,旧实例状态必须清零(disposer 链),下游依赖通过 epoch 自动迁移。两者正交,可叠加。
  • ZIO / Effect-TS:类型系统路线——effect 作为值,编译期追踪依赖(ZLayer 的组合图),运行时解释执行。Cordis 明确反着来:不追踪类型,只追踪运行时注册Impl.fiber 是运行时字段,epoch 是字符串)。选型逻辑【研究推论】可归纳为:Agent 的插件作者包括模型自己(见 §14),你不能要求模型写出通过严格类型推导的 ZLayer 程序,但可以要求它交出函数返回值的 disposer。工程代价:类型安全退到边界(Config schema、Tool schema 校验),换来的是任何 JS 代码可即挂即用。
  • 论文术语的出处姿态【论文观点(摘要可证)+ 推论】:摘要自述 “lifting classical effect and coeffect concepts to runtime mechanisms”——它承认概念来自 PL 理论(algebraic effects;coeffects 一般归于 Petricek 等人的传统),贡献在运行时化而非概念发明。
能力ReactOSGiVSCode Ext前端 HMRZIOCordis
effect 强制可逆部分(cleanup 约定)✓(类型层)✓(运行时)
依赖响应式激活✓(DS)✓(Layer)✓(epoch)
实例身份追踪部分✓(Reference)✓(uid)
动态组件树部分
类型层依赖证明✓✓✗(边界校验)

13. 为什么 MCP 替代不了 Cordis

MCP 和 Cordis 经常被放进同一个"扩展生态"话题里,但它们处在正交层。MCP 回答的是跨进程能力供给协议(process boundary + JSON-RPC);Cordis 回答的是进程内组件组合语义(lifecycle + dependency + effect)。

DSH 自己同时大量使用两者,是最好的注脚【源码事实】:packages/mcp/mcp-client 是 Cordis 插件,MCP server 是它管理的外部进程。

为什么"都上 MCP"不行?看 DSH 工具集与浏览器之间的真实关系(§14 的 client/host 双半模型):

共享对象:ctx.llm 的实时流句柄、session 事件总线、
         正在执行的浏览器连接、组件 slot 树、主题 token
         ↓ 这些全是"活的带行为的引用",不是可序列化 payload
MCP:  序列化一切 → 只剩数据快照与 RPC 延迟,
      无法响应式:"这个 provider 的实例被换了"对 MCP server 不可见
Cordis: inject 声明直接拿到被 trace 过的活引用,
      provider 替换 → epoch → 自动重连,全程无需感知对方存在

具体到工程账本【研究推论,基于 §7/§10 源码机制】:

  • Browser/DB Pool/Session Cache 共享:MCP server 是独立进程,它拿不到宿主进程里的 Playwright 实例,只能拿宿主"代理执行后的结果"。跨边界共享实时对象必然退化为"代理 + 轮询/推送",而依赖变化通知(coeffect 反应)要在协议里重新发明一遍。Cordis 里同对象跨 fiber 传递由 traceable proxy 保证定义域解析正确。
  • 生命周期耦合:宿主 session 卸载 → 挂在 Fiber 上的 MCP client 插件被自动 dispose → 子进程回收。MCP 协议自身没有"客户端组件被卸载"这种事件,泄漏的僵尸 server 进程是这类系统的常态事故。
  • 成本模型:MCP 一次调用跨进程序列化;Cordis 的 tool 是函数引用。【研究推论】 所以合理架构是"跨信任边界用 MCP,边界内高内耦用 Cordis",而不是二选一——DSH 的实际布局正是这样。
维度传统 Agent Harness (Prompt 编排 + MCP)Cordis Runtime
Prompt Composition✓ 业务层实现✓ systemPrompt 变量槽 (agent-loop 构造函数里三行 ctx.systemPrompt.variable(...))
Tool Composition✓ 且工具注册=可逆 effect
进程隔离✓ MCP/子进程✓(外部进程由插件管理,见 DSH sandbox/mcp)
插件生命周期部分(加载/卸载二元)✓ 六态状态机
依赖追踪部分(启动期装配图)✓ 运行期持续
动态 Provider 替换弱/手写✓ uid epoch
组件级卸载
Effect 回滚✓ 核心机制
故障隔离部分(进程级)✓ 组件级
运行中重配置部分(配置热更≠结构热更)✓ update/HMR

(表格事实依据:左列基于 Claude Code/Codex/OpenCode/OpenClaw 公开文档的一般形态概括,属【研究推论】;右列全部对应本文引用过的 Cordis 源码。传统 harness 的插件体系各有优劣,此表是维度对照而非评分。)

14. web-cordis:Agent 修改自己的运行时——到什么程度

这是全仓库最激进的部分。DSH 提供一组 opt-in 工具,让模型直接操作承载它自己的 Cordis 进程。文档站里的演示路径是 dsh web --patch apps/cli/config/examples/cordis/cordis.yml,把 cordis-host-runner + tool-cordis 插进 web profile【源码事实】。

packages/extensions/tool-cordis README 列出的七个工具【源码事实】:

工具语义
cordis_inspect_list/query/self只读:枚举 provider、查询服务方法签名/事件/工具 schema/slot 树、读本会话动态插件的版本指针与诊断
cordis_define登记一个包版本(host 半代码 + 可选浏览器半),仅校验语法,不执行
cordis_run激活:mode: run 首启 / update 切版本;带浏览器半的可能先返回 awaiting-approval工具从不等待最终结果
cordis_stop / cordis_undefine停止 / 彻底移除插件及其全部版本

设计要点逐条对应前文的机制【源码事实】:

  1. 模型写的代码就是临时 Plugin,在 node:vm realm 求值,挂在内部 cordis-dynamic 分组下,id 形如 dyn-1;只存内存,重启即蒸发,不写文件不改 cordis.yml。
  2. 注册时校验复用 §9 的 Config schema 通道:“格式错误的工具 schema 必须在注册时失败,而不是等组装提示词时才报错”(feature note 原文)。
  3. 完全可 dispose 复用 §6:cordis_unmount 的"完全停稳后才返回"= Fiber dispose 链 + inertia 收敛等待。
  4. 跨挂载组合复用 §5:mount A ctx.provide('foo', v),mount B inject: ['foo'] 即激活/挂起;A 卸载,B 自动回 pending;重新 provide 时 B 的 apply 重跑。Agent 因此能用"provide/inject 会合"编排自己发明的多段代码——不需要任何新的组合机制。
  5. 观察能力有数据源:inspect 的 API/事件报告来自 AST 生成的 api-catalog.ts实时服务存储的交集,并有 verify-cordis-api 门禁防漂移【源码事实】。模型看见的运行时视图与真实运行时同源。
  6. 信任姿态官方定死:feature note 专节标题即"加固的沙箱?"——结论:明确不是安全边界,ctx.shell/fs/web 直通真实运行时,“信任等级与 bash 相当”,需显式启用。

【研究推论】 这条链路(观察 → define → run →(失败)inspect self → define v2 → run update → stop/undefine)确实是一个完整的运行时自修改回路:agent 可以给自己造新工具、造浏览器 UI 半、带版本地迭代修复,全程不重启、不影响同进程其他会话的状态。但"Self-Evolving"要打足折扣,因为边界是作者亲手画的:动态包不跨重启存续、无安装路径、无自动转正(feature note 原文),持久化演化仍要走普通插件开发流程。准确的定性是:Cordis 为 self-modifying runtime 提供了原语层(可逆、可组合、可观察),DSH 在它之上做了一个会话级沙盒版自我修改实验,而"自我进化"(变更固化为持久能力)被刻意留在回路外。这是工程克制,不是能力缺口——恰是该读清楚的地方。

flowchart TD O[cordis_inspect 观察运行时] --> D[cordis_define 登记包版本] D --> RU[cordis_run 激活] RU -->|浏览器半| AP[awaiting-approval 人工放行] AP --> RU RU --> X[新工具/服务生效] X --> E{运行验证} E -->|失败| S[inspect_self 读诊断] S --> D2[define v2 → run update] E -->|成功| W[继续会话] X --> U[cordis_stop/undefine<br/>进程重启则全部消失]

15. 从 Agent Harness 到 Agent Runtime:三层组合模型

把 §1–§14 收拢成一个判断【研究推论,非 DeepSeek 官方表述】。Agent 基础设施在解决三层组合问题,且历史重心正在逐层下移:

第一层 Prompt Composition   —— 模型下一轮看见什么?
        (Claude Code 的 CLAUDE.md / OpenClaw 的 context 组装 / 各家 system prompt 工程)
第二层 Process Composition  —— 进程边界怎么调用、OS 回收什么?
        (MCP / 子进程工具 / A2A)
第三层 Component Composition —— 同一进程里的能力如何动态出现、
        消失、替换而不影响其他组件?
        (传统空白区 →  Cordis 的位置)

第一、二层已有成熟工具链和事实标准,第三层长期被"重启解决一切"掩盖。Cordis 的价值不是"又一个插件框架",而是它把第三层的两个核心困难(论文术语:revertible effects 与 reactive coeffects)做成了有形式化出处的运行时机制,并验证于一个真实的 Agent 全栈(DSH)。所以更准确的说法是【研究推论】:Agent Harness 正在从 Prompt Orchestrator 演化为 Agent Runtime,而 Cordis 这类组件模型是这条演化路径上第一次被认真补上的第三层地板。

16. Self-Evolving Agent 为什么需要这种运行时

设想一个自我扩展 Agent 的标准循环:写代码 → 装能力 → 跑任务 → 发现缺陷 → 修改 → 卸载旧版 → 加载新版 → 验证。若没有组件级生命周期,每一步的失败模式是:

  • 每改一次能力 → 重启进程 → session 状态丢失(30 个并发对话全灭)
  • 浏览器登录态丢失、向量缓存冷启动、在跑的长任务腰斩
  • 负责"发现缺陷并修复"的那个 recovery agent 也被一起重启——自举悖论:修改器与被修改系统同归于尽

对照 Cordis 提供什么【源码事实→推论】:热替换(§10 HMR / §14 run-update)、局部回滚(§6)、依赖方自动迁移(§5)、失败隔离(§8)恰好逐项抵消上面四个失败模式。DSH 的 web-cordis 演示已把前六步连成闭环;“固化为持久能力"留白(§14)。【研究推论】 一个完整的 self-evolving agent 还需要三样 Cordis 不管的事:① 持久化边界(哪些临时包值得转正——是产品/策略问题);② 语义级验证(disposer 链保证"拆干净”,不保证"功能对");③ 真正的安全边界(官方明说 bash 级信任)。Cordis ≠ Self-Evolving Agent;Cordis = 让 Self-Evolving 的机械部分不再需要重启的那层地板。

graph TB AG[Agent] --> LOOP[Agent Loop ★插件] LOOP --> RT[Cordis Runtime] RT --> R1[Context 中介] RT --> R2[Dependency/epoch] RT --> R3[Effect/Dispose] RT --> R4[Lifecycle/Fiber] RT --> R5[Isolation/uid] RT --> R6[Reconcile/HMR] R6 --> TL[Tools ★ 可插拔] R6 --> MM[Memory ★] R6 --> BR[Browser ★] R6 --> EV[Evolver ★<br/>观察+define+run-update] TL --- M1[MCP/VectorDB/Playwright 经插件接入] MM --- M1 BR --- M1 EV --- AG

17. Cordis 的局限

不吹捧清单,全部有出处:

  1. API 不稳定:cordis README 原文"under active development, API is not yet stable"【源码事实】。DSH 被迫全程 vendor 就是最硬的证据。
  2. 理论到工程的距离:论文承诺的 metatheory(observational equivalence 等)在源码里没有对应验证物;92 页摘要可查、HTML 不可得,形式化结论的覆盖范围本次无法确认【资料边界】。epoch 字符串比较这类机制,离"演算"还有工程翻译的距离。
  3. Node 运行时耦合:HMR 依赖 --expose-internals/ModuleLoader 内部形状(vendor 第 19 条修复了 Node 24.0–24.11 的接口误标)【源码事实】——热能力贴着实现细节,跨 runtime(Bun/Deno)成本存疑。
  4. 无安全边界node:vm 不是沙箱,官方文档三处不同措辞反复声明【源码事实】。同进程组件的故障隔离靠的是"disposer 纪律"这一社会契约,忘写 effect 的插件照样能泄漏状态。
  5. 类型安全退到边界:与 ZIO 路线的取舍代价(§12)——schema 拦得住形状,拦不住语义误用;“mount 的 B 拿到 A 提供的方法对象却猜错行为"这类问题框架不管。
  6. 生态半径目前≈两个仓库:dsh-plugin topic 刚起步;“self-referential toolset” 的多数正确性设计(错误教学文案、catalog 交集)是为模型读者定制的,人类生态尚未验证。

18. 我的判断:Agent Runtime 的下一步

以下均为【研究推论】:

  1. 生命周期动词将成为运行时的一等 API 面。Dsh 已经在给模型暴露 install→activate→(suspend)→deactivate→replace→rollback→observe→verify 的完整动词表。下一个可预期的增量是带时限/预算的激活(“这个能力只准跑 10 分钟/1000 token”——epoch 机制天然可扩展)和依赖感知的优雅降级(Provider 缺席时不 pending 而是切 mock——check 钩子已有雏形)。
  2. 第三层会先于"自我进化"普及。多数生产 harness 不需要模型写插件,但都需要"热换工具不重启会话”——这是 Cordis 机制的最小可用切片,预计会以更薄的形态(或概念)扩散。
  3. 真正的空白是验证层:Cordis 保证了机械可逆,没人保证语义正确。“模型改完自己,如何证明改好了”——形式化规约、差分测试、回滚策略选择——这层地基目前无人打,也是我认为下一步最值得投入的方向。
  4. 同进程 vs 跨进程钟摆会回摆一点:MCP 一家独大的格局下,Cordis 证明了高内耦组件共享活引用 + 可逆生命周期的价值,尤其在共享 Browser/向量索引/会话状态的场景。未来的主流形态大概率是 §13 结尾的"边界外 MCP、边界内组件运行时"双层结构。

19. 总结

  • 为什么 DSH 选择 Cordis:因为它的正确性需求清单(注册时校验、依赖中途替换、完全可卸载、失败局部化、运行中重配置、模型可安全地碰运行时结构)在 Cordis 之前没有框架整体覆盖;且合作深到 vendor 共建。
  • 解决了传统 harness 什么问题:把"能力"从一次性装配的静态代码,变成运行时可追踪、可撤销、可替换的组件;重启不再是动态性的唯一代价。
  • 是否代表新架构:它确实补齐了 Agent 基础设施长期缺位的第三层(组件组合);但它提供的是原语不是成品——Self-Evolving 的安全、验证、持久化边界都还在地板之外。

一句话结论

Cordis 是一个以"可逆副作用(revertible effects)+ 响应式依赖(reactive coeffects)“为核心的组件运行时框架;DeepSeek Harness 需要它,是因为只有"能力可以作为带完整撤销保证的组件动态地挂载、替换、卸载”,Agent 才能在不停掉自己的前提下扩展和修复自己。

参考资料


本文所有源码引用基于 2026-09 主分支快照;【研究推论】不代表任何官方观点;论文细节以摘要与 README 为限,标注【资料边界】处需读者自行核对全文。