The previous two articles established a fairly clear set of runtime semantics:
This article will implement neither a complete Cordis nor the DeepSeek Harness. There is only one goal: to validate the most critical lifecycle behavior of the dynamic dependency system.
We need to reach the following state:
A provides db
B requires db
B provides repo
C requires repo
Once everything is loaded:
A ACTIVE
B ACTIVE
C ACTIVE
Unload A:
db disappears
↓
B unloads
↓
repo disappears
↓
C unloads
Reload A:
db appears
↓
B reloads
↓
repo appears
↓
C reloads
The implementation is in this unit's demo/ directory and can be run directly with Node.js.
The Runtime Keeps Only Four Core Objects
In the demo, PluginRuntime maintains:
where:
services
service name → provider
fibers
plugin id → Fiber
consumers
service name → Set<Fiber>
providerVersion
monotonic provider identity
To make the implementation easier to observe, the demo explicitly maintains a reverse dependency index. In Cordis's currently vendored implementation, reflect.notify() scans the Fibers in the Registry and then filters them based on inject and isolation scope. The semantics are the same, but the data structures make different trade-offs. [1]
The Fiber states are simplified to:
PENDING
LOADING
ACTIVE
UNLOADING
DISPOSED
The demo does not implement FAILED, Config, Scope, Isolation, HMR, or other features.
Plugin Definition
Each Plugin declares only two things:
requires is a Coeffect—that is, the plugin's requirements for the runtime environment.
The following calls in setup() produce Effects:
Together, these two parts determine the Plugin's dynamic lifecycle.
Effect Ownership
The Context implementation is minimal:
The key point is that provide() is itself installed through effect().
Therefore:
Fiber unload
↓
dispose effects
↓
remove provided service
The Service lifecycle is therefore naturally attached to the Fiber.
reconcile Does Not Need Polling
After a Service changes, the Runtime calls:
It selects only the Fibers that depend on that Service:
Then it calls:
The Fiber computes a new epoch from its dependencies:
If a dependency is missing:
epoch = null
The Fiber should be in the PENDING state.
If all dependencies are present:
db@1|cache@2
The Fiber can be ACTIVE.
After a Provider is replaced, even if the Service name stays the same:
db@1
↓
db@7
The epoch changes as well, triggering a reload.
Handling Further Changes During a Lifecycle Transition
One easily overlooked problem is this:
B 正在 UNLOADING
↓
依赖重新出现
If the Runtime calls load() directly at that point, the same Plugin will execute setup and cleanup at the same time.
The demo uses a serialized reconcile loop:
A new environment change only sets:
needsReconcile = true
After the current lifecycle action finishes, the system recalculates based on the latest environment.
This addresses the same problem Cordis solves with epoch + inertia: a Fiber's lifecycle transitions need to be serialized, but the desired dependency state can continue to change during a transition. [2]
How the Dependency Chain Propagates
The demo has three Plugins:
For the initial installation, we can deliberately use the wrong order:
The first two Fibers remain in PENDING because their dependencies are not satisfied:
feature PENDING
repository PENDING
database ACTIVE
Once database provides db:
db changed
↓
repository reconcile
↓
repository ACTIVE
↓
repo changed
↓
feature reconcile
↓
feature ACTIVE
The loading order is derived from the dependency relationships rather than manually enforced by the caller.
Unloading a Provider
Run:
The database Fiber cleans up db:
remove db
↓
notify repository
repository enters UNLOADING and cleans up the repo it provides:
remove repo
↓
notify feature
feature then unloads.
The final state is:
database DISPOSED
repository PENDING
feature PENDING
There is one boundary condition worth noting.
repository and feature are not removed. They are still installed Plugins; their current dependency environment simply does not allow them to run.
Therefore, we must distinguish between:
installed
and:
active
In a dynamic plugin system, these two states generally cannot be conflated.
Provider Recovery
Install database again:
The new Provider is assigned a new version.
db@2 appears
↓
repository reload
↓
repo appears
↓
feature reload
Downstream Plugins do not need to register again, and the Application does not need to start them manually.
The Runtime restores the entire active chain from the dependency declarations.
What the Demo Deliberately Omits
A full Cordis implementation also needs to handle:
None of these capabilities changes the core model the demo is designed to validate:
Service change
↓
dependent Fiber reconcile
↓
load / unload
↓
Effect apply / revert
↓
more Service changes
With this local rule, the entire dependency graph gradually converges on a new stable state.
From the Demo to the Agent Harness
Replace the Service names in the demo with Agent capabilities:
llm
tools
sessions
sandbox
agentLoop
This yields a simplified Harness:
If the LLM Provider is replaced:
old llm provider disappears
↓
agentLoop temporarily unloads
↓
new llm provider appears
↓
agentLoop reloads against new dependency epoch
This is the underlying lifecycle capability that makes Everything is a Plugin possible.
By the end of the third unit, the series has a complete evolutionary path:
The next unit shifts the focus to the web: once the Agent execution lifecycle is decoupled from the browser connection, how Server Runtime, SSE, Replay, Client Runtime, and UI Projection should be organized.
Demo
Run:
cd demo
node src/demo.mjs
Files:
demo/
├── src/
│ ├── runtime.mjs
│ └── demo.mjs
└── README.md
References
[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