Cordis:DeepSeek Harness 为什么把 Agent 运行时重写成一套"可逆的组件系统"
核心问题:为什么 DeepSeek Harness 选择 Cordis?它解决了传统 Agent Harness 的什么问题?它是否代表了 Agent Runtime 的一种新架构?
本文基于源码级研究:
deepseek-ai/deepseek-harness、cordiverse/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 只有在 agents、sessions、llm 等服务全部处于 ACTIVE 状态时才会执行构造;任何一个上游服务被卸载,AgentLoop 会自动去激活并撤销它注册过的一切;服务恢复后,它再自动重新激活。依赖不是"启动时装配好",而是"持续被运行时追踪和被强制兑现"。
对照传统 Harness(含我自己跑在上面的 OpenClaw、以及 Claude Code、Codex、OpenCode 一类),组件依赖通常是"初始化图 + 事件总线",一旦装配完成,运行期几乎不再处理"依赖中途消失"这种情形。DeepSeek Harness 把这件事交给了框架层。
哪些东西不是插件?源码验证的边界很清晰【源码事实】:CLI 入口(apps/cli/src/bin.ts、args.ts)、构建脚本(scripts/)、启动编排函数(profile-boot.ts 里的 boot()/composeEntries() 调用者),以及 Service 内部创建的领域对象——Agent 实例本身就不是插件,它只是 this.ctx.extend({ agent: this }) 造出的上下文视图(packages/core/agent-loop/src/agent.ts:107)。也就是说:“编排和领域对象不是插件,能力是插件”。
(★ = 经源码验证的 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,各有uid(parent.registry.counter全局递增)。状态机:PENDING → LOADING → ACTIVE / FAILED → UNLOADING → DISPOSED。 - Effect / Dispose:
ctx.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.ts 的 provide() / 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
}
注意三个细节,都是教科书级的处理:
- LIFO + 链式 await:
disposables.reverse()后逐个执行,异步 disposer 串进 Promise 链——后建立的先拆除,拆除顺序与建立顺序严格对称。 - 部分初始化失败的回滚:
catch分支先调dispose()再重抛——构造到一半炸了,已经造出来的东西照样被收干净。 - 执行体四种返回形态:返回函数(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 用三重机制堵死这条路【源码事实】:
Impl记录{ name, fiber, value }——每个服务值带着它的主人 Fiber;strict 读取要求impl.fiber.state === FiberState.ACTIVE,死 fiber 的服务直接不可见。ReflectService的 Proxyget沿defSite.fiber → parent链查找,且校验inject集合:没声明过的服务,访问即抛cannot get property "x" without inject。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)【源码事实】:
注意 UNLOADING → LOADING 这条回边:它是 §5 “Provider 重新出现 → Consumer 自动重激活"的状态机表达,也是 DSH cordis_run mode:"update" 能"更新过去"而不新建会话的基础。FAILED 只能靠 update()(清空 _error)恢复,_getState 里 if (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"
【研究推论】 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:vmrealm 做"防手滑不防恶意"的软边界——换来了 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 等人的传统),贡献在运行时化而非概念发明。
| 能力 | React | OSGi | VSCode Ext | 前端 HMR | ZIO | Cordis |
|---|---|---|---|---|---|---|
| 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 | 停止 / 彻底移除插件及其全部版本 |
设计要点逐条对应前文的机制【源码事实】:
- 模型写的代码就是临时 Plugin,在
node:vmrealm 求值,挂在内部cordis-dynamic分组下,id 形如dyn-1;只存内存,重启即蒸发,不写文件不改 cordis.yml。 - 注册时校验复用 §9 的 Config schema 通道:“格式错误的工具 schema 必须在注册时失败,而不是等组装提示词时才报错”(feature note 原文)。
- 完全可 dispose 复用 §6:
cordis_unmount的"完全停稳后才返回"= Fiber dispose 链 +inertia收敛等待。 - 跨挂载组合复用 §5:mount A
ctx.provide('foo', v),mount Binject: ['foo']即激活/挂起;A 卸载,B 自动回 pending;重新 provide 时 B 的 apply 重跑。Agent 因此能用"provide/inject 会合"编排自己发明的多段代码——不需要任何新的组合机制。 - 观察能力有数据源:inspect 的 API/事件报告来自 AST 生成的
api-catalog.ts与实时服务存储的交集,并有verify-cordis-api门禁防漂移【源码事实】。模型看见的运行时视图与真实运行时同源。 - 信任姿态官方定死: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 在它之上做了一个会话级沙盒版自我修改实验,而"自我进化"(变更固化为持久能力)被刻意留在回路外。这是工程克制,不是能力缺口——恰是该读清楚的地方。
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 的机械部分不再需要重启的那层地板。
17. Cordis 的局限
不吹捧清单,全部有出处:
- API 不稳定:cordis README 原文"under active development, API is not yet stable"【源码事实】。DSH 被迫全程 vendor 就是最硬的证据。
- 理论到工程的距离:论文承诺的 metatheory(observational equivalence 等)在源码里没有对应验证物;92 页摘要可查、HTML 不可得,形式化结论的覆盖范围本次无法确认【资料边界】。epoch 字符串比较这类机制,离"演算"还有工程翻译的距离。
- Node 运行时耦合:HMR 依赖
--expose-internals/ModuleLoader 内部形状(vendor 第 19 条修复了 Node 24.0–24.11 的接口误标)【源码事实】——热能力贴着实现细节,跨 runtime(Bun/Deno)成本存疑。 - 无安全边界:
node:vm不是沙箱,官方文档三处不同措辞反复声明【源码事实】。同进程组件的故障隔离靠的是"disposer 纪律"这一社会契约,忘写 effect 的插件照样能泄漏状态。 - 类型安全退到边界:与 ZIO 路线的取舍代价(§12)——schema 拦得住形状,拦不住语义误用;“mount 的 B 拿到 A 提供的方法对象却猜错行为"这类问题框架不管。
- 生态半径目前≈两个仓库:dsh-plugin topic 刚起步;“self-referential toolset” 的多数正确性设计(错误教学文案、catalog 交集)是为模型读者定制的,人类生态尚未验证。
18. 我的判断:Agent Runtime 的下一步
以下均为【研究推论】:
- 生命周期动词将成为运行时的一等 API 面。Dsh 已经在给模型暴露 install→activate→(suspend)→deactivate→replace→rollback→observe→verify 的完整动词表。下一个可预期的增量是带时限/预算的激活(“这个能力只准跑 10 分钟/1000 token”——epoch 机制天然可扩展)和依赖感知的优雅降级(Provider 缺席时不 pending 而是切 mock——check 钩子已有雏形)。
- 第三层会先于"自我进化"普及。多数生产 harness 不需要模型写插件,但都需要"热换工具不重启会话”——这是 Cordis 机制的最小可用切片,预计会以更薄的形态(或概念)扩散。
- 真正的空白是验证层:Cordis 保证了机械可逆,没人保证语义正确。“模型改完自己,如何证明改好了”——形式化规约、差分测试、回滚策略选择——这层地基目前无人打,也是我认为下一步最值得投入的方向。
- 同进程 vs 跨进程钟摆会回摆一点:MCP 一家独大的格局下,Cordis 证明了高内耦组件共享活引用 + 可逆生命周期的价值,尤其在共享 Browser/向量索引/会话状态的场景。未来的主流形态大概率是 §13 结尾的"边界外 MCP、边界内组件运行时"双层结构。
19. 总结
- 为什么 DSH 选择 Cordis:因为它的正确性需求清单(注册时校验、依赖中途替换、完全可卸载、失败局部化、运行中重配置、模型可安全地碰运行时结构)在 Cordis 之前没有框架整体覆盖;且合作深到 vendor 共建。
- 解决了传统 harness 什么问题:把"能力"从一次性装配的静态代码,变成运行时可追踪、可撤销、可替换的组件;重启不再是动态性的唯一代价。
- 是否代表新架构:它确实补齐了 Agent 基础设施长期缺位的第三层(组件组合);但它提供的是原语不是成品——Self-Evolving 的安全、验证、持久化边界都还在地板之外。
一句话结论
Cordis 是一个以"可逆副作用(revertible effects)+ 响应式依赖(reactive coeffects)“为核心的组件运行时框架;DeepSeek Harness 需要它,是因为只有"能力可以作为带完整撤销保证的组件动态地挂载、替换、卸载”,Agent 才能在不停掉自己的前提下扩展和修复自己。
参考资料
- DeepSeek Harness: https://github.com/deepseek-ai/deepseek-harness (README、vendor/、packages/core/、packages/extensions/tool-cordis、apps/cli/config/examples/cordis、.agents/notes/)
- Cordis 核心库: https://github.com/cordiverse/cordis (packages/core/src/{context,fiber,reflect,registry,service,events,utils}.ts、packages/hmr/src/index.ts、packages/loader/src/*)
- 论文仓库: https://github.com/cordiverse/paper (arXiv:2608.25512 摘要,HTML 全文本次不可得)
- Cordis Primer: https://deepseek-harness.github.io/deepseek-harness/reference/cordis-primer
- DSH 实践文档: docs/user/develop/practice/dynamic-cordis.zh.md
- 对比对象公开文档:React useEffect、OSGi Compendium (Declarative Services)、VSCode Extension Host API、Vite HMR、ZIO 2.x 官方文档(相关段落均为【研究推论】依据)
本文所有源码引用基于 2026-09 主分支快照;【研究推论】不代表任何官方观点;论文细节以摘要与 README 为限,标注【资料边界】处需读者自行核对全文。