Everything Is a Plugin: The Cordis Design Behind DeepSeek Harness
How DeepSeek Harness builds an agent runtime from plugins, using Cordis ideas like reversible effects, reactive dependencies and a replaceable agent loop.
This article is published in English.
Sources: DeepSeek Quietly Built a Different Kind of AI Agent Framework
The usual recipe and where it cracks
Most agent frameworks start with the same ingredients: a language model, a handful of tools, a stored conversation and a loop wrapped around the lot. The sketch below shows the shape.
User asks something
↓
LLM thinks
↓
LLM calls a tool
↓
Tool returns data
↓
LLM continues
↓
Final answer
By definition, that already counts as an agent. The trouble starts when the system grows. Requirements stack up: multiple model providers, browser tools, shell execution, MCP servers, approval flows, memory, telemetry, authentication, different UIs, background agents, custom prompts, permissions and hot reloading. Soon every part depends on every other part. Changing the model provider ripples into the agent loop, removing a plugin leaves event listeners behind, and reloading a tool can break agents that are running at that moment. Plugins also rely on services that may show up or disappear mid-run.
DeepSeek Harness goes about it very differently. Its central idea sounds almost too simple: everything is a plugin, including the parts most frameworks call core.
Beneath it all is a framework called Cordis, which came out of a research paper named "A Programming Paradigm for Spatiotemporal Composability." That paper holds the interest, since the project isn't just another scheme for organizing JavaScript modules. It goes after a deeper engineering question: how can components join, leave, replace each other and change dependencies in a running program without the runtime descending into chaos?
What a harness is
DeepSeek Harness, abbreviated dsh, is an open-source agent harness released by DeepSeek. A harness is the infrastructure that surrounds the model. Whether the model is DeepSeek, Claude, GPT, Gemini or a local one, the harness takes care of everything around it.
┌───────────────┐
│ LLM │
└───────┬───────┘
│
┌──────────▼──────────┐
│ Agent Harness │
│ │
│ Prompt construction │
│ Tool execution │
│ Session history │
│ Permissions │
│ Streaming │
│ Agent lifecycle │
│ Model adapters │
└─────────────────────┘
This difference counts for a lot. Attention usually goes to how smart the model is, yet building something like Claude Code, Codex, Cursor or a long-running autonomous agent shows that the runtime around it matters just as much. DeepSeek Harness handles that runtime as a set of dynamically composed plugins, not as one monolithic application, and that decision is inherited directly from Cordis.
A dependency puzzle
Imagine building a coding agent out of three plugins.
GitPlugin
TerminalPlugin
CodingAgent
The coding agent needs Git and Terminal. At startup the simplest approach is to load them in order.
load Git
load Terminal
load CodingAgent
Easy, and most dependency-injection frameworks manage it fine. The harder question is what happens if the Git plugin disappears while the program runs. It might have been upgraded, its configuration might have changed, a user might have disabled it, or a different implementation might have taken over. The runtime must then stop everything that depends on that service.
CodingAgent
↓ -------->depends on
GitPlugin
If Git vanishes, the coding agent shouldn't keep acting as though it exists. And if another Git provider shows up later, perhaps the agent should start working again. Two separate problems hide here, which the Cordis paper names temporal composability and spatial composability.
Temporal composability: leaving cleanly
Suppose a plugin loads and registers an event listener.
events.on("message", handler)
Later the plugin gets unloaded, and the question is what becomes of the listener. If nothing removes it, the plugin is gone in name while a piece of it keeps running. Add that the plugin may have started timers, opened database connections, registered commands, altered shared state, added tools, attached event handlers and launched workers. Unloading it means reversing every one of those steps.
This is temporal composability: a component that joins the system should leave the system able to fully undo what the component did. Cordis treats such changes as revertible effects.
Effects without the jargon
An effect is just something your code does that alters the outside world or runtime state. Take a tool registration.
toolRegistry.add("search", searchTool)
Before the call, the registry looks like this.
tools = {}
Afterward it holds the new tool.
tools = {
search: searchTool
}
The forward operation is an add.
add(searchTool)
Cordis wants more than that, though. It wants the operation to carry its own inverse. Conceptually, an effect pairs an action with its undo.
Effect:
do: add search tool
undo: remove search tool
Without mathematical notation, the idea reads as follows.
state --effect--> new state
new state --inverse--> original state
That inverse is the key piece, and Cordis calls the whole bundle a revertible effect.
Every change brings its cleanup
Cordis exposes this through something like the following pattern, where a function applies a change and returns the function that undoes it.
ctx.effect(() => {
registerSomething()
return () => {
unregisterSomething()
}
})
The runtime applies the effect, stores the cleanup function and calls it later when the plugin goes away.
apply effect
↓
store cleanup function
↓
plugin runs
↓
plugin unloads
↓
run cleanup
When a plugin holds several effects, they're applied in one order.
effect A
effect B
effect C
The cleanups then run in reverse, the way a stack unwinds.
undo C
undo B
undo A
Why reverse? Consider a plugin that starts like this.
1. open database
2. start service using database
3. start agent using service
Shutting down in the same order would be dangerous, because the database closes while things still use it.
close database
stop service
stop agent
The safe order is the opposite.
stop agent
stop service
close database
That's last in, first out.
Last In
First Out
Cordis builds this behavior into component lifecycle management.
Reverse cleanup alone falls short
Here the paper gets more interesting. Say Plugin A changes a shared value, then Plugin B changes the same value, and afterward A tries to restore what it saw originally. That can wipe out B's modification.
Initial: x = 10
Plugin A: x = 20
Plugin B: x = 30
Plugin A unloads: restore x = 10
B is still active, yet its state is gone. So reversible effects are only safe under certain independence assumptions, which the paper formalizes as effect independence. Informally, two effects are independent when applying or reversing one doesn't invalidate the meaning of the other. Registering different tools usually qualifies, since they touch different locations.
Plugin A registers tool "search"
Plugin B registers tool "calculator"
Replacing the same configuration probably doesn't.
Plugin A replaces configuration X
Plugin B also replaces configuration X
This distinction returns later when Cordis' context system comes up.
Spatial composability: knowing what a plugin needs
Temporal composability asks whether a component can be removed safely. Spatial composability asks whether the system understands what the component depends on. Picture this plugin.
CodingAgent
It needs three things.
LLM
ToolRegistry
SessionStore
Traditional code might reach for globals.
global.llm
global.tools
global.sessions
In that case the architecture has no real knowledge of the dependencies. Cordis makes them explicit. Conceptually, a plugin declares what it injects.
inject: [
"llm",
"tools",
"sessions"
]
Now the runtime can see the plugin's requirements as a tree.
CodingAgent
├── requires LLM
├── requires ToolRegistry
└── requires SessionStore
That lets it react when those services change.
Reactive dependencies
This may be the best idea in Cordis. Suppose a component needs an LLM.
LLM
At first none exists.
LLM unavailable
So the component can't become active. Later a provider plugin loads and supplies one.
OpenAI Plugin loads
↓
provides LLM
↓
dependency becomes satisfied
↓
component activates
If that provider is removed, the capability goes with it.
OpenAI Plugin unloads
↓
LLM disappears
↓
dependency becomes unsatisfied
↓
dependent component unloads
When a different provider loads, the capability returns.
DeepSeek Plugin
↓
provides LLM
↓
component becomes active again
Dependencies, then, aren't checked once at startup. They're reactive, which is why the paper speaks of reactive coeffects.
Effects versus coeffects
The terminology sounds heavy, but the distinction is easy. An effect describes something a component changes, flowing from the plugin into the environment.
Plugin → Environment
Examples include these actions.
register tool
open database
subscribe to event
start timer
A coeffect describes something the component requires from its environment, flowing the other way.
Environment → Plugin
Typical examples are these needs.
need an LLM
need a logger
need session storage
need a tool registry
Put the two together and requirements flow in one direction.
requirements
Environment ───────────────────> Plugin
while effects flow back.
Environment <─────────────────── Plugin
effects
That's the basic idea behind Cordis. A component states what it needs, what it provides and which reversible effects it performs.
I NEED these services.
I PROVIDE these services.
I PERFORM these reversible effects.
The runtime then coordinates the lifecycle.
Where "spatiotemporal" comes from
Now the paper's title makes sense. Spatial composability concerns relationships between components.
A depends on B
B provides X
C depends on X
Temporal composability concerns how components move through time.
load
activate
replace
deactivate
unload
reload
Together they form the whole philosophy.
Spatiotemporal composability = dependency-aware composition + reversible lifecycle changes
The context object
DeepSeek Harness doesn't pass dozens of services through constructors by hand. Plugins interact through a shared context, usually named like this.
ctx
Conceptually it exposes the major services.
ctx
├── llm
├── tools
├── sessions
├── agents
├── agentLoop
├── logger
└── ...
Yet ctx is more than a JavaScript object stuffed with globals. Through it plugins find their dependencies, offer services, register effects, listen for events, isolate resources, intercept access to services and coordinate lifecycles, which is why Cordis presents the context as a unifying abstraction.
From theory to the actual project
DeepSeek Harness exposes its major subsystems through that shared context.
ctx.sessions
ctx.systemPrompt
ctx.tools
ctx.agents
ctx.agentLoop
ctx.llm
Notice something unusual: even the agent loop is just a service.
ctx.agentLoop
So the loop can be replaced, and it isn't sacred framework code. That leads to the defining principle of the design: there's no privileged core that every feature must patch.
Why the loop as plugin matters
Most agent frameworks contain a central loop resembling this one.
while not finished:
response = model(history)
if response.tool_call:
result = execute_tool(response.tool_call)
history.append(result)
else:
return response
Then every advanced feature starts modifying it. Approval before running commands? Patch the loop. Tracing? Patch the loop. Retries, multi-agent delegation, another inference backend? Patch the loop. Eventually it ends up as a hub with tools, models and memory all hanging off it.
Agent Loop
/ | \
/ | \
tools models memory
/ | \
approvals retries tracing
The loop turns into the application's god object. DeepSeek Harness sidesteps that by letting the loop take part in the same plugin architecture as every other piece.
The shape of one agent turn
Under the hood, an interaction passes through stages roughly like these.
turn/start
↓
claim incoming user input
↓
assemble prompt + available tools
↓
agent/pre-step
↓
step/start
↓
record user message
↓
reconstruct conversation history
↓
send model request
↓
stream assistant response
↓
tool call?
│
├── yes → execute tool
│ record result
│ continue another step
│
└── no → finish
↓
step/end
↓
turn/end
What matters is that every stage exposes events and service seams, giving plugins places to participate without rewriting the whole runtime.
Waterfall events as agent middleware
Cordis supports several modes for running events. One handy mode, the waterfall, behaves like middleware. Picture a request passing through a pipeline.
request
↓
Plugin A
↓
Plugin B
↓
Plugin C
↓
LLM
Plugin B can wrap execution with a handler that receives the next step.
async function handler(next) {
console.log("before")
const result = await next()
console.log("after")
return result
}
Execution then nests, with each plugin acting before and after the inner call.
Plugin A before
Plugin B before
Plugin C before
LLM
Plugin C after
Plugin B after
Plugin A after
That fits permissions, request rewriting, telemetry, caching, model routing, safety checks and context injection, none of which needs to be hard-wired into the model adapter.
The model sits behind a capability seam
The harness also abstracts the LLM, so an agent doesn't need to understand every provider SDK. It talks to a single service.
Agent
↓
ctx.llm
↓
Model Adapter
↓
Provider API
One provider might implement it for the DeepSeek API.
DeepSeek API
Another covers OpenAI.
OpenAI
A third covers Anthropic.
Anthropic
The rest of the agent architecture addresses the same LLM capability. Harness documentation sometimes calls this a capability seam, made of a service definition and a service provider.
Service Definition
↓
Service Provider
↓
Consumer
For the LLM, that chain runs from the interface to a particular provider.
LLM interface
↓
OpenAI provider
↓
Agent loop
So adding a model provider doesn't necessarily mean rewriting the runtime. You supply another implementation of the same capability.
Event-sourced sessions
Conversations aren't kept by the harness as one big mutable array that serves as the truth. Sessions are stored instead as an append-only stream of events, along these lines.
turn/start
user/message
assistant/chunk
assistant/chunk
assistant/message
tool/call
tool/result
assistant/message
turn/end
Model history can be rebuilt from those events, which makes the session log the source of truth.
Session log = source of truth
That replaces the common alternative.
Mutable chat history = source of truth
Why agents benefit from event sourcing
Agents have become complicated enough that storing a plain message list often isn't sufficient.
[
{"role": "user", "content": "..."},
{"role": "assistant", "content": "..."}
]
Debugging can require knowing which tool was exposed and which was invoked, which model wrote a response, which system prompt applied, where a request started, where a reasoning step ended, what configuration was active, whether a call was retried and what streamed output came back. The harness records structured session events so that everything the model could see can be rebuilt. One principle in its design states that anything visible to the model should be recoverable from the log. Rather than speculating about why the agent acted as it did, you can reconstruct the request behind it.
Fibers and lifecycle state
Cordis represents running components with a Fiber. It has nothing to do with operating-system fibers. A Cordis Fiber is a runtime component instance, and it moves through states like these.
PENDING
↓
LOADING
↓
ACTIVE
↓
UNLOADING
↓
DISPOSED
Failure states exist too. More importantly, a fiber tracks several facts about itself.
what dependencies I need
what providers currently satisfy them
what effects I have created
what cleanup functions belong to me
When dependencies change, the fiber can refresh its lifecycle.
Why provider identity matters
Suppose a component depends on a database service.
database
Right now it's supplied by one provider.
DatabaseProvider A
During a hot reload it gets replaced by another.
DatabaseProvider B
Even when both return equivalent JavaScript objects, Cordis can tell the provider identities apart. The reason is that dependency meaning isn't only that a value exists.
"value exists"
It can also mean the value came from a different component instance.
"this value came from a different component instance"
So swapping a provider can trigger dependents to reload properly, which avoids a large class of stale-reference bugs.
Unloading in dependency order
Consider a chain in which one plugin provides the database.
Plugin A provides database
A second plugin depends on the database, and a third depends on the second.
Plugin B depends on databasePlugin C depends on Plugin B
The dependency chain looks like this.
A
↓
B
↓
C
Now the first is removed. A naive runtime might close the database immediately, even though the other two may still be executing. Cordis coordinates the dependency graph instead. Unloading starts.
A begins unloading
Then the dependents unwind in order before the original completes its cleanup.
↓ mark A unavailableB notices dependency disappeared↓ B unloadsC notices B disappeared↓ C unloadsthenA completes cleanup
The order matters enormously, because it stops dependents from using something that's already half destroyed.
Isolation: one name, separate worlds
A single global service namespace can become limiting. Imagine two agents needing different databases.
Agent A → Database A
Agent B → Database B
Both still ask for the same thing.
database
Cordis supports isolation, so service resolution works from a pair.
service name + isolation realm
and not only from a name.
service name
Conceptually each realm resolves the same name to its own instance.
Realm A: database → DB_A
Realm B: database → DB_B
The same logical dependency can therefore resolve differently depending on context, which helps with multi-tenant systems, multiple agents, testing environments, sandboxed plugins and separate workspaces.
Interception
Cordis can also shape service access using metadata. Consider a tool registry where an admin agent might see these tools.
shell
browser
filesystem
deploy
while a restricted agent sees a smaller set.
browser
filesystem-readonly
Rather than build a whole separate registry, access can be intercepted according to context.
ctx + metadata
↓
service resolution
↓
modified view of capability
That's a strong pattern for policy systems.
Safer hot module replacement
Hot reloading sounds easy.
file changed
↓
reload module
In a plugin runtime it gets tricky. What if the new module contains an error, and what becomes of the old working version? The Cordis loader supports reload behavior that acts like a transaction.
working plugin
↓
code changes
↓
attempt new import
↓
success?
┌───────┴───────┐
yes no
↓ ↓
replace rollback
plugin old module
This fits naturally with reversible effects. A plugin with a clear lifecycle, dependency set and cleanup path is far less dangerous to replace.
A real-world case study
Cordis wasn't created solely for DeepSeek Harness. The paper looks at Koishi, an open-source chatbot framework built on it, with a community ecosystem the authors put at thousands of plugins. A real ecosystem makes dynamic composition far more persuasive than ten hand-built modules would. The paper stays careful, however, and doesn't assert that controlled benchmarks have shown big performance or productivity gains. More evaluation is still needed.
Not everything can be undone
Theory meets reality here. Some runtime effects are truly reversible, like opening and closing a file.
open file
→ close file
Others pair up equally well.
allocate memory
→ free memorystart worker
→ terminate workerregister event listener
→ unregister listener
Then there's sending an email.
send email
What would its inverse be?
unsend email?
Usually that's impossible, and the same goes for a group of other actions.
send HTTP request
transfer money
publish message
delete external data
After something affects the outside world, a true reversal may not exist. The best available option is compensation.
payment → refund
message → follow-up correction
database mutation → compensating transaction
Compensation isn't the same as mathematical reversal. The paper recognizes this boundary explicitly, and Cordis works best for effects the runtime can really control and recover.
Cordis trusts your cleanup
Suppose a plugin starts a server inside an effect and returns a cleanup function.
ctx.effect(() => {
startServer()
return () => {
// supposedly cleanup
}
}))
Cordis has no way to prove that the cleanup truly reverses the startup. The formal model says the inverse must return the environment to its earlier state, but the implementation depends on the programmer to write a valid disposer. Pairing an effect with a cleanup is thus a discipline.
effect + cleanup
It isn't automatic formal verification, and that's an important difference.
Why this suits AI agents
Traditional applications stay fairly stable after startup. Agents are heading the other way. Future agent systems may continuously perform operations like these.
install tool
remove tool
change model
spawn agent
kill agent
load skill
reload code
switch permissions
connect service
disconnect service
modify environment
Picture an agent that decides it needs GitHub support and loads a GitHub capability, then drops it later when it's no longer useful, or replaces an implementation that failed. A system like that calls for far stronger runtime composition semantics than a conventional dependency injector offers. Without them a long-running agent could collect leftovers.
orphan listeners
stale tools
dangling processes
broken dependencies
zombie services
duplicated handlers
Cordis tries to make runtime composition a first-class software concept.
Why the design feels elegant
With Cordis handling composition, DeepSeek Harness can model nearly everything the same way.
Model provider → plugin
Tool → plugin
Agent loop → plugin
Session store → plugin
UI → plugin
Telemetry → plugin
Approval system → plugin
Sandbox → plugin
Credential store → plugin
New features don't become special cases inside the framework. They become more participants in one composition system.
Profiles and bundles
If everything is a plugin, nobody wants to assemble fifty of them by hand each time. The harness therefore adds higher-level compositions. A profile describes a named system configuration, and a bundle distributes groups of plugins along with their configuration. A base bundle might look like this.
Base Bundle
├── model adapters
├── tools
├── sessions
├── sandbox
├── approvals
├── credentials
├── settings
└── telemetry
Another bundle could add a web application.
Web Application
A different one could produce a headless agent.
Headless Agent
The architecture stays modular without making everyday use painful.
Five ideas worth keeping
If everything else fades, these five points are the ones to remember.
Revertible effects come first: any change a component makes should arrive with a way to reverse it.
change + inverse
The second is reactive coeffects. Components declare what they need, and the runtime responds when dependencies appear, disappear or change.
dependency available → activate
dependency gone → deactivate
The third is context. Effects, dependencies and service access live inside one unified runtime context.
ctx
Fourth is a dynamic component calculus, which describes a component by what it needs, what it provides and what it changes, giving the runtime enough structure to reason about loading and unloading.
Dependencies + Provisions + Effects
The fifth is spatiotemporal composability, which combines the spatial and temporal sides.
spatial:
who depends on whom?
+
temporal:
how can components safely enter and leave?
Reduced to a sentence, it's reversible change plus reactive dependency.
Why it matters beyond one project
Most current agent frameworks still assume a fairly static application: start the program, load tools, load the model, run the agent, stop. Autonomous agents push toward runtimes that modify themselves while staying alive. Imagine an agent that can follow a sequence like this.
discover a capability
install it
use it
upgrade it
replace it
remove it
continue running
At that point plugin lifecycle management stops being a pleasant architectural feature and becomes foundational infrastructure. That's what makes the project interesting. The model isn't the most novel part. The runtime around it is.
The deeper lesson
Software architecture usually concerns composition at one moment in time. You assemble parts and run the program.
A + B + C
Cordis asks harder questions.
What if A disappears?
What if B is replaced?
What if C suddenly gains a dependency?
What if another implementation of A appears?
What if all of this happens while the system is running?
That alters the problem entirely. Components stop being static blocks and act like living participants: they arrive, build relationships, change their surroundings, depend on others and finally depart.
The takeaway
DeepSeek Harness can look like just another agent framework, and its appeal isn't that it can call an LLM or run tools, which plenty of frameworks do. The bigger idea is that the composition system itself is the architecture. Each capability is a component, each dependency is explicit, each controllable effect knows how to clean itself up, components respond when dependencies shift, and even the agent loop can be replaced and isn't untouchable framework code. The guiding principle is to make every capability a component, every dependency explicit and every effect carry its own undo.
In ordinary applications this is a tidy plugin architecture. In long-running, self-modifying AI agents it may matter much more. If agents eventually change their own runtime, software will need a principled method for changing without gradually damaging the environment it runs in. That's the problem DeepSeek Harness, and the Cordis research behind it, aims to solve.