前两篇得到了一组比较明确的运行时语义:
Plugin
├── declares dependencies
└── produces effects
Fiber
├── tracks dependencies
├── owns effects
└── load / unload
Service Registry
└── notifies affected fibers
Context
└── exposes services and effect APIs
这篇不实现一个完整 Cordis,也不实现 DeepSeek Harness。目标只有一个:验证动态依赖系统最关键的生命周期行为。
我们需要得到下面的结果:
A provides db
B requires db
B provides repo
C requires repo
全部加载以后:
A ACTIVE
B ACTIVE
C ACTIVE
卸载 A:
db disappears
↓
B unloads
↓
repo disappears
↓
C unloads
重新加载 A:
db appears
↓
B reloads
↓
repo appears
↓
C reloads
实现放在本单元 demo/ 目录中,可以直接使用 Node.js 运行。
Runtime 只保留四个核心对象
Demo 中的 PluginRuntime 维护:
services
fibers
consumers
providerVersion
其中:
services
service name → provider
fibers
plugin id → Fiber
consumers
service name → Set<Fiber>
providerVersion
monotonic provider identity
为了让实现更容易观察,Demo 显式维护反向依赖索引。Cordis 当前 vendored 实现的 reflect.notify() 会扫描 Registry 中的 Fiber,再根据 inject 和 isolation scope 筛选;两者语义相同,数据结构取舍不同。[1]
Fiber 状态简化为:
PENDING
LOADING
ACTIVE
UNLOADING
DISPOSED
没有实现 FAILED、Config、Scope、Isolation、HMR 等功能。
Plugin Definition
每个 Plugin 只声明两件事:
{
name: 'repo',
requires: ['db'],
setup(ctx) {
// ...
},
}
requires 是 Coeffect,也就是插件对运行环境的要求。
setup() 中调用:
ctx.provide(...)
ctx.effect(...)
产生 Effect。
这两部分共同决定 Plugin 的动态生命周期。
Effect ownership
Context 的实现非常小:
class PluginContext {
constructor(runtime, fiber) {
this.runtime = runtime
this.fiber = fiber
}
provide(name, value) {
return this.effect(() => {
this.runtime.provide(name, value, this.fiber)
return () => {
this.runtime.removeService(name, this.fiber)
}
})
}
effect(setup) {
const dispose = setup()
this.fiber.effects.push(dispose)
return dispose
}
}
关键点是 provide() 自身也通过 effect() 安装。
因此:
Fiber unload
↓
dispose effects
↓
remove provided service
Service 生命周期天然附着在 Fiber 上。
reconcile 不需要轮询
Runtime 在 Service 发生变化后调用:
notifyServiceChanged(name)
只取出依赖该 Service 的 Fiber:
const affected = this.consumers.get(name)
随后调用:
fiber.reconcile()
Fiber 根据依赖计算新的 epoch:
computeEpoch() {
const providers = []
for (const name of this.definition.requires) {
const service = this.runtime.services.get(name)
if (!service) return null
providers.push(`${name}@${service.version}`)
}
return providers.join('|')
}
如果缺少依赖:
epoch = null
Fiber 应该处于 PENDING。
如果所有依赖存在:
db@1|cache@2
Fiber 可以 ACTIVE。
Provider 替换以后,即使 Service name 不变:
db@1
↓
db@7
epoch 也会改变,从而触发 reload。
处理生命周期中的再次变化
一个容易遗漏的问题是:
B 正在 UNLOADING
↓
依赖重新出现
如果 Runtime 此时直接调用 load(),同一个 Plugin 会同时执行 setup 和 cleanup。
Demo 使用一个串行 reconcile loop:
async reconcile() {
this.needsReconcile = true
if (this.reconciling) return this.reconciling
this.reconciling = (async () => {
while (this.needsReconcile && !this.disposed) {
this.needsReconcile = false
await this.reconcileOnce()
}
})()
await this.reconciling
this.reconciling = null
}
新的环境变化只设置:
needsReconcile = true
当前生命周期动作结束以后,再根据最新环境计算一次。
这和 Cordis 使用 epoch + inertia 所解决的问题一致:一个 Fiber 的生命周期转换需要串行化,但目标依赖状态可以在转换期间继续变化。[2]
依赖链如何传播
Demo 中三个 Plugin:
const database = {
name: 'database',
setup(ctx) {
ctx.provide('db', { query() {} })
},
}
const repository = {
name: 'repository',
requires: ['db'],
setup(ctx) {
ctx.provide('repo', { find() {} })
},
}
const feature = {
name: 'feature',
requires: ['repo'],
setup(ctx) {
ctx.effect(() => {
console.log('feature started')
return () => console.log('feature stopped')
})
},
}
初始安装时可以故意采用错误顺序:
await runtime.install(feature)
await runtime.install(repository)
await runtime.install(database)
前两个 Fiber 因依赖不满足停在 PENDING:
feature PENDING
repository PENDING
database ACTIVE
database 提供 db 后:
db changed
↓
repository reconcile
↓
repository ACTIVE
↓
repo changed
↓
feature reconcile
↓
feature ACTIVE
加载顺序由依赖关系推导,不再由调用方手工保证。
卸载 Provider
执行:
await runtime.uninstall('database')
database Fiber 清理 db:
remove db
↓
notify repository
repository 进入 UNLOADING,并清理自己提供的 repo:
remove repo
↓
notify feature
feature 随后卸载。
最终状态:
database DISPOSED
repository PENDING
feature PENDING
这里有一个值得注意的边界。
repository 和 feature 没有被删除。它们仍然是已经安装的 Plugin,只是当前依赖环境不允许它们运行。
因此需要区分:
installed
和:
active
动态插件系统中,这两个状态通常不能合并。
Provider 恢复
再次安装 database:
await runtime.install(database)
新 Provider 拥有新的 version。
db@2 appears
↓
repository reload
↓
repo appears
↓
feature reload
下游 Plugin 不需要重新注册,也不需要由 Application 手工启动。
Runtime 从依赖声明中恢复整条活动链。
Demo 刻意省略了什么
真实 Cordis 还需要处理:
Context scope / isolate
intercept config
async plugin setup
setup failure rollback
nested effects
events
plugin registry identity
service availability check
HMR
diagnostics
reentrant disposal
这些能力不会改变 Demo 要验证的核心模型:
Service change
↓
dependent Fiber reconcile
↓
load / unload
↓
Effect apply / revert
↓
more Service changes
整个依赖图通过这条局部规则逐渐到达新的稳定状态。
从 Demo 回到 Agent Harness
将 Demo 中的 Service 名替换成 Agent 能力:
llm
tools
sessions
sandbox
agentLoop
就可以得到一个简化的 Harness:
LLM Plugin
└── provides llm
Tool Registry Plugin
└── provides tools
Agent Loop Plugin
├── requires llm
├── requires tools
└── requires sessions
如果替换 LLM Provider:
old llm provider disappears
↓
agentLoop temporarily unloads
↓
new llm provider appears
↓
agentLoop reloads against new dependency epoch
这就是 Everything is a Plugin 能够成立所需要的底层生命周期能力。
第三单元到这里形成了一条完整的演化路径:
Pi
stable Core + Extensions
↓
动态能力继续扩大
↓
需要管理 Plugin 的副作用和依赖
↓
Spatiotemporal Composability
↓
Cordis
Context + Service + Fiber + Effect
↓
DeepSeek Harness
Everything is a Plugin
下一单元会把视角移到 Web:Agent 执行生命周期与浏览器连接分离以后,Server Runtime、SSE、Replay、Client Runtime 和 UI Projection 应该怎样组织。
Demo
运行:
cd demo
node src/demo.mjs
文件:
demo/
├── src/
│ ├── runtime.mjs
│ └── demo.mjs
└── README.md
参考资料
[1] Cordis source: https://github.com/deepseek-ai/deepseek-harness/tree/master/vendor/cordis/src
[2] Cordis paper: https://github.com/cordiverse/paper
[3] DeepSeek Harness Architecture: https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/architecture.md