The easiest part of a plugin system to implement is loading.
Given a module:
function plugin(ctx) {
ctx.registerTool(...)
ctx.on(...)
}
After invoking it, new capabilities enter the system.
When components are only loaded once at process startup and destroyed wholesale at process exit, this model is usually sufficient. Dynamic Harness has stronger constraints: plugins may be disabled, replaced, or hot-swapped; some plugins should only run when their dependencies exist; when dependencies disappear, they need to exit, and they need to re-activate when dependencies are restored.
Cordis summarizes this class of problems as Spatiotemporal Composability. A preprint published on August 13, 2026, "A Programming Paradigm for Spatiotemporal Composability," breaks the problem into two independent dimensions: [1]
- Temporal Composability: When a component leaves the system, the side effects it produced can be fully reverted.
- Spatial Composability: Components can declare requirements about the environment and dynamically adjust their validity as the environment changes.
The paper further uses Effect, Coeffect, and a unified Context to describe these two dimensions.
This article doesn't recapitulate the formal definitions from the paper's table of contents. Instead, it first establishes the engineering problems, then explains why these abstractions hold up.
The first challenge for dynamic plugins is "reversion"
After a plugin runs, it typically modifies multiple external structures:
function plugin(ctx) {
ctx.tools.register(tool)
ctx.events.on('request', listener)
const timer = setInterval(task, 1000)
}
After loading, the runtime environment has undergone three changes:
Tools + tool
Listeners + listener
Timers + timer
If uninstallation just deletes the plugin object, these changes still persist.
So dynamic plugin lifecycle cannot just record:
plugin = loaded
It also needs to record what modifications the plugin made to the environment.
The most direct approach is to have each operation return a corresponding cleanup function:
const disposeTool = registerTool(tool)
const disposeListener = on(event, listener)
return () => {
disposeListener()
disposeTool()
}
This is already very close to what the paper calls a revertible effect.
A registration can be expressed as a state transformation:
Context C
│
│ apply effect E
▼
Context C'
If a component needs to be safely removed, there needs to be a corresponding inverse operation:
Context C'
│
│ revert E
▼
Context C
The key point isn't that every byte of memory must be strictly restored, but that the observable impact a component has on the shared runtime environment can be reverted by the lifecycle system.
This is the engineering meaning of Temporal Composability.
Effects need to be held by the Runtime
If every plugin author manually maintains a set of cleanup callbacks, it could theoretically work. But as plugins develop asynchronous initialization, nested resources, multiple registrations, and error rollback, lifecycle consistency is easily lost.
A more stable model is:
Plugin Runtime
│
└── owns Effects
├── Tool registration
├── Listener
├── Service
└── Timer
During plugin execution, Effects produced are collected by the current running unit.
During uninstallation:
dispose runtime unit
↓
revert owned effects
Plugin lifecycle thus changes from "calling a function" to "creating a running unit that owns resources."
Cordis calls this running unit a Fiber. [2][3]
Note that it's not the same abstraction as JavaScript coroutines or Fiber in Effect-TS. Cordis's Fiber is closer to a Plugin Runtime Instance: it stores plugin configuration, dependency snapshots, lifecycle state, and currently owned Effects.
In the DeepSeek Harness vendored Cordis source code, Fiber state includes:
PENDING
LOADING
ACTIVE
FAILED
UNLOADING
DISPOSED
This already indicates that a plugin isn't a boolean loaded=true/false but a state machine that can go through dependency waiting, loading, unloading, and failure. [3]
Reversible side effects alone still can't handle dependencies
Consider two plugins:
Plugin A
└── provides FileSystem
Plugin B
└── requires FileSystem
If B starts before A, what happens?
Traditional Plugin Manager might rely on load order:
A
↓
B
As scale grows, manual ordering gradually becomes a dependency graph management problem.
More importantly, dependencies in dynamic systems change during runtime.
A can be uninstalled after B is already running:
time ───────────────────────────>
A: ACTIVE ─────────── DISPOSED
B: ?
If B continues running, its held dependencies have already become invalid; if Runtime only checks dependencies at startup, it also can't solve the problem.
Therefore, components not only need to describe "what I will modify" but also "what the environment must provide for me."
The paper uses Coeffect to represent this latter class of constraints. [1]
Effect describes what a component does to the environment:
component → environment
Coeffect describes what conditions the environment must provide to the component:
environment → component
For example:
requires FileSystem
requires ToolRegistry
requires SessionStore
These requirements together determine whether the component currently has conditions to run.
What Reactive Coeffect solves is dependency change
Ordinary dependency injection usually happens at object creation time:
construct B(FileSystem)
After object creation, dependency relationships are basically fixed.
Dynamic Harness's requirements are closer to:
FileSystem appears
↓
B becomes activatable
↓
B starts
FileSystem disappears
↓
B becomes invalid
↓
B unloads
FileSystem appears again
↓
B starts again
The paper calls this mechanism of re-evaluating component validity as Context changes a reactive coeffect. [1]
The "reactive" here is very important.
It means dependency checking isn't a one-time constructor validation, but a relationship that Runtime continuously maintains:
Context changed
↓
which coeffects are affected?
↓
re-evaluate components
Engineering implementation doesn't need to constantly poll the entire system. A more reasonable approach is to trigger dependency updates when Services are registered or removed.
Cordis's current implementation is indeed event-driven: after a service changes, it notifies Fibers that depend on that service, Fibers recalculate their dependency epoch, then decide whether to stay ACTIVE, enter UNLOADING, or re-enter LOADING. [3]
So this "recalculation" isn't a perpetually running while (true):
while (true) {
recalculateAllPlugins()
}
It happens when the environment undergoes structural changes.
Why it's called Spatial Composability
"Space" easily makes people think of physical locations, but here the meaning is closer to the combinatorial relationship between components at the same point in time.
Given:
A provides X
B requires X, provides Y
C requires Y
The valid system at any given moment isn't equal to the set of all installed plugins, but is determined by satisfiable dependency relationships in the current Context:
Installed:
A B C D E
Current Context:
A ACTIVE
B ACTIVE
C ACTIVE
D PENDING
E PENDING
When X disappears:
A removed
↓
X disappears
↓
B loses requirement
↓
B unloads
↓
Y disappears
↓
C unloads
What spatial composability focuses on is "which components can coexist together" at that moment.
When connected with Temporal Composability, it forms complete dynamic changes:
dependency disappears
↓
component becomes invalid
↓
revert component Effects
↓
Service provided by component disappears
↓
affect next layer of dependencies
This creates a chain reaction, but it doesn't require Runtime to do global recursive deduction. Each Service reversion produces the next local dependency change, until the system reaches a new stable state.
Why Context needs to unify Effect and Coeffect
If Effect and dependencies each maintain independent worlds, a problem emerges: which environment does the component modify, and which environment does the dependency observe?
The paper proposes unifying effect context and coeffect context into a single Context type. [1]
From an engineering perspective, it can be understood as:
Context
│
┌───────────┴───────────┐
│ │
component reads component writes
│ │
Coeffect Effect
│ │
└───────────┬───────────┘
▼
shared runtime
Context simultaneously bears:
- Looking up currently available Services;
- Registering new Services;
- Registering listeners and other Effects;
- Recording which running unit these Effects belong to;
- Forming sub-scopes/isolation;
- Notifying relevant dependencies after structural changes.
This way, a Plugin doesn't need to additionally receive multiple objects like global Application, ServiceRegistry, LifecycleManager, EventBus, etc. It interacts with the runtime environment through the same Context, while Runtime can uniformly track the lifecycle relationships produced by this interaction.
From single component to component system
What the paper ultimately cares about isn't just whether a single plugin can be safely uninstalled, but whether these properties can continue to hold when multiple components interleave changes.
For example:
A provides X
B requires X -> provides Y
C requires X + Y
After A uninstalls:
X removed
↓
B invalid
C invalid
↓
B effects reverted
C effects reverted
↓
Y removed
If A reappears later:
X available
↓
B activatable
↓
B provides Y
↓
C activatable
The entire system doesn't depend on a fixed startup order. The order is naturally produced by dependency relationships under the current Context.
This is also where "Composable" becomes truly valuable: each component only declares its contributions and requirements, and the system-composed lifecycle is derived by Runtime.
Why this model fits Agent Harness well
Agent Harness capabilities are naturally highly dynamic.
LLM Provider can be swapped, Tool Provider can be isolated per Session, Sandbox can change per execution environment, MCP Server may disconnect, SubAgent only exists during certain execution segments, and UI Contributions may appear and disappear as plugins enable/disable.
If all these relationships are maintained through manual startup order and manual cleanup, Harness will quickly accumulate massive cross-module lifecycle code.
The spatiotemporal composability model provides an alternative organization:
Plugin
├── declares dependencies
└── produces reversible effects
Context
├── resolves dependencies
└── owns shared capabilities
Fiber
├── tracks current dependency epoch
├── owns effects
└── drives load / unload
These three objects basically constitute Cordis's core.
The next article will dive into actual source code and answer more specific questions:
- When a dependent Service is uninstalled, how does the Fiber that depends on it discover the change;
- Why lower-layer Services continue to produce cascading invalidations;
- How Runtime avoids lifecycle races if dependencies reappear during uninstallation;
- 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