The easiest part of building a plugin system is loading plugins.
Given a module:
Once it is invoked, the new capabilities become part of the system.
When components are loaded only once at process startup and torn down together when the process exits, this model is generally sufficient. A dynamic Harness imposes stricter constraints: plugins may be disabled, replaced, or hot-reloaded; some plugins should run only when their dependencies are present; and after a dependency disappears, they must exit and become active again once it returns.
Cordis summarizes these problems as Spatiotemporal Composability. The preprint “A Programming Paradigm for Spatiotemporal Composability,” released on August 13, 2026, breaks the problem down into two mutually independent dimensions: [1]
- Temporal Composability: When a component leaves the system, the side effects it has produced can be fully undone.
- Spatial Composability: A component can declare its requirements on the environment and dynamically adjust its own validity as the environment changes.
The paper further uses Effect, Coeffect, and a unified Context to describe these two dimensions.
Rather than reproducing the paper’s formal definitions in the order of its table of contents, this article first frames the engineering problem and then explains why these abstractions make sense.
Dynamic Plugins First Have to Deal with “Undoing”
A plugin typically modifies several external structures once it runs:
After loading, the runtime environment has undergone three changes:
Tools + tool
Listeners + listener
Timers + timer
If unloading merely deleted the plugin object, those changes would still be there.
So a dynamic plugin lifecycle cannot merely record:
plugin = loaded
It also needs to record every modification the plugin has made to the environment.
The most direct approach is for each operation to return a corresponding undo function:
This is already very close to the paper’s notion of a revertible effect.
A registration can be expressed as a state transition:
Context C
│
│ apply effect E
▼
Context C'
To remove the component safely, a corresponding inverse operation must exist:
Context C'
│
│ revert E
▼
Context C
The key point is not that every byte of memory must be restored exactly, but that the component’s observable effects on the shared runtime environment can be undone by the lifecycle system.
This is the engineering meaning of Temporal Composability.
Effects Need to Be Owned by the Runtime
If every plugin author manually maintained a set of cleanup callbacks, it could work in theory. But as plugins introduce asynchronous initialization, nested resources, multiple registrations, and error rollback, lifecycle consistency quickly becomes difficult to maintain.
A more stable model is:
Effects produced while a plugin executes are collected by the current runtime unit.
On unload:
dispose runtime unit
↓
revert owned effects
The plugin lifecycle therefore shifts from “calling a function” to “creating a resource-owning unit of execution.”
Cordis calls this runtime unit a Fiber. [2][3]
Note that it is not the same abstraction as a JavaScript coroutine or the Fiber in Effect-TS. A Cordis Fiber is closer to a Plugin Runtime Instance: it stores the plugin configuration, dependency snapshot, lifecycle state, and Effects it currently owns.
In the current source code of the Cordis implementation vendored into DeepSeek Harness, the Fiber states include:
PENDING
LOADING
ACTIVE
FAILED
UNLOADING
DISPOSED
This shows that a plugin is not a boolean loaded=true/false, but a state machine that can move through waiting for dependencies, loading, unloading, and failure. [3]
Reversible Side Effects Alone Cannot Handle Dependencies
Consider two plugins:
Plugin A
└── provides FileSystem
Plugin B
└── requires FileSystem
What happens if B starts before A?
A traditional Plugin Manager may rely on load order:
A
↓
B
As the system grows, manual ordering gradually becomes a dependency graph management problem.
More importantly, dependencies in a dynamic system change while it is running.
A may be unloaded after B is already running:
time ───────────────────────────>
A: ACTIVE ─────────── DISPOSED
B: ?
If B continues to run, the dependency it holds is no longer valid. A Runtime that checks dependencies only at startup cannot solve this either.
Therefore, in addition to describing “what I will modify,” a component also needs to describe “what the environment must provide.”
The paper uses Coeffect to represent this latter category of constraints. [1]
Effect describes the action a component takes on the environment:
component → environment
Coeffect describes the conditions the environment must supply to the component:
environment → component
For example:
requires FileSystem
requires ToolRegistry
requires SessionStore
Together, these requirements determine whether the component currently has the conditions it needs to run.
Reactive Coeffects Handle Changes in Dependencies
Ordinary dependency injection usually happens at object creation time:
construct B(FileSystem)
Once the object has been created, the dependency relationship is essentially fixed.
The requirements of a dynamic Harness are closer to:
The paper calls this mechanism for re-evaluating a component’s validity as the Context changes a reactive coeffect. [1]
The word “reactive” is crucial here.
It means that dependency checking is not a one-time constructor validation, but a relationship the Runtime continuously maintains:
Context changed
↓
which coeffects are affected?
↓
re-evaluate components
An engineering implementation does not need to poll the entire system repeatedly. A better approach is to trigger dependency updates whenever a Service is registered or removed.
The current Cordis implementation is event-driven in exactly this way: after a Service changes, the Runtime notifies the Fibers that depend on it. Each Fiber recomputes its dependency epoch and then decides whether to remain ACTIVE, move to UNLOADING, or begin LOADING again. [3]
Thus, “recalculation” does not mean a permanently running while (true) loop:
It happens when the environment undergoes a structural change.
Why It Is Called Spatial Composability
“Spatial” can evoke physical location, but here it means something closer to the compositional relationships among components at a given moment.
Given:
A provides X
B requires X, provides Y
C requires Y
At any given moment, the effective system is not the set of all installed plugins; it is determined by which dependency relationships can be satisfied in the current Context:
Installed:
A B C D E
Current Context:
A ACTIVE
B ACTIVE
C ACTIVE
D PENDING
E PENDING
After X disappears:
A removed
↓
X disappears
↓
B loses requirement
↓
B unloads
↓
Y disappears
↓
C unloads
Spatial composability is concerned with exactly which components can coexist at that moment.
Combined with Temporal Composability, it forms a complete model of dynamic change:
依赖消失
↓
组件失效
↓
撤销组件 Effects
↓
组件提供的 Service 消失
↓
影响下一层依赖
This can produce a chain reaction, but it does not require the Runtime to perform a global recursive derivation. Each Service revocation triggers the next local dependency change, until the system reaches a new stable state.
Why Context Needs to Unify Effect and Coeffect
If Effects and dependencies each maintained separate worlds, there would be one difficult question: which environment does a component modify, and which environment does its dependency observe?
The paper proposes unifying the effect context and coeffect context into a single Context type. [1]
From an engineering perspective, this can be understood as:
The Context simultaneously serves to:
- look up currently available Services;
- register new Services;
- register listeners and other Effects;
- record which runtime unit owns each Effect;
- create child Scopes / Isolations;
- notify relevant dependencies after structural changes.
This way, a Plugin does not need to separately obtain several objects, such as a global Application, ServiceRegistry, LifecycleManager, or EventBus. It interacts with the runtime environment through the same Context, while the Runtime can track the lifecycle relationships produced by those interactions in a unified way.
From a Single Component to a System of Components
The paper’s ultimate concern is not merely whether a single plugin can be unloaded safely, but whether these properties continue to hold as multiple components change in an interleaved fashion.
For example:
A provides X
B requires X -> provides Y
C requires X + Y
After A is unloaded:
X removed
↓
B invalid
C invalid
↓
B effects reverted
C effects reverted
↓
Y removed
If A later reappears:
X available
↓
B activatable
↓
B provides Y
↓
C activatable
The overall system does not rely on a fixed startup order. That order emerges naturally from the dependency relationships in the current Context.
This is where “Composable” becomes truly valuable: each component declares only its own contributions and requirements, and the Runtime derives the lifecycle of the composed system.
Why This Model Suits Agent Harnesses
The capabilities of an Agent Harness are inherently highly dynamic.
An LLM Provider can be replaced, a Tool Provider can be isolated per Session, a Sandbox can change with the execution environment, an MCP Server may disconnect, a SubAgent may exist only during part of an execution, and a UI Contribution may appear and disappear as plugins start and stop.
If all these relationships were maintained through manual startup ordering and manual cleanup, the Harness would quickly accumulate a large amount of cross-module lifecycle code.
The spatiotemporal composability model offers a different way to organize them:
These three objects essentially make up the core of Cordis.
The next article will get into the actual source code and answer several more concrete questions:
- How a Fiber detects a change when a Service it depends on is unloaded;
- Why lower-level Services continue to produce cascading invalidations;
- How the Runtime avoids lifecycle races if a dependency reappears during unloading;
- How DeepSeek Harness implements Everything is a Plugin on top of this mechanism.
References
[1] A Programming Paradigm for Spatiotemporal Composability: https://github.com/cordiverse/paper
[2] Cordis repository: https://github.com/cordiverse/cordis
[3] DeepSeek Harness vendored Cordis: https://github.com/deepseek-ai/deepseek-harness/tree/master/vendor/cordis
[4] DeepSeek Harness Cordis Primer: https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/cordis-primer.md