Home / Articles

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.

Publisher: REACTAPP.TOP19 min read4403 words

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.