Inside DeepSeek Harness: An Agent Architecture Built Entirely from Plugins
DeepSeek shipped its first Agent product, DeepSeek Harness. The emphasis this time is on runtime architecture — the official tagline is "everything is a plugin": model access, tool execution, session logging, the loop itself, and the UI all run through the plugin mechanism.

This post skips the parts of agent design that are already consensus across the field — detecting tool calls, executing them, feeding results back, looping — every framework looks about the same there, and it's not worth repeating. The focus is on where DeepSeek Harness actually diverges from everyone else: how Cordis, the plugin runtime underneath it, manages lifecycle; why the preset scope-inheritance chain is split into two layers; why Code Mode picked an isolation approach that sounds less "safe" on paper; and how the tool-registration shadowing algorithm is written. Along the way it compares a few specific engineering decisions against Codex, picking out only the places where they diverge.
I. Why Cordis
The foundation of "everything is a plugin" is Cordis, an open-source project by Shigma. It positions itself as a "Meta-Framework for Modern JavaScript Applications" — a general-purpose plugin framework handling dependency injection, scoped services, and lifecycle cleanup, with no direct relationship to agents or LLMs. It existed before DeepSeek Harness did.
Cordis already has real users in the open-source community. The most notable is Koishi, a cross-platform chatbot framework (QQ / Discord / Telegram / WeChat all supported). The chatbot use case is naturally "dozens of plugins bolted together, hot-swappable, reconfigured on the fly" — nearly identical to the agent use case, minus the LLM. DeepSeek Harness vendors the entire framework's source into its own repo, renames the scope to @deepseek-ai/cordis, and makes every internal package a peer dependency of it. That's a step beyond "using a third-party library" — the whole product is built on top of Cordis.
Worth comparing a few common plugin shapes first: traditional DI containers, lightweight hook-based approaches, and Cordis. Each makes different trade-offs, and they're worth lining up side by side.
Three gaps in traditional DI containers. The "mainstream" approach is a DI container plus lifecycle annotations. You bind a service — who's responsible for unbinding and cleanup? Traditional DI either ignores it (leaks) or requires you to hand-write lifecycle hooks. If an LLM provider gets hot-swapped, the ToolRegistry that depends on it should restart automatically — traditional DI can't do this kind of dependency-driven reload. On configuration: traditional DI reads its config once at startup, while in cordis.yml each line is a plugin instance, and editing the config directly triggers HMR.
Pi is also a star product in the agent space, but Pi's Extension takes a different path. Pi positions itself as a "self extensible coding agent"; its plugins are called Extensions, written in TS, injecting logic at extension points via hooks — no dependency injection, no dependency management between plugins (load order is priority), and no unloading. Cordis's plugins are "components" with declared dependencies, a lifecycle, and reversible side effects: dependency injection exists, reactive coeffects exist, plugins can declare dependencies on each other, and unloading fully reverses side effects.
That last point — "reversible side effects" — is the foundation the whole piece rests on. Cordis's design paper elevates this to a theoretical question, and asks it directly:
Is there a programming model where dynamism itself can have process-like lifecycle isolation?
The benefit of processes and containers is: kill it and restart, and state is wiped clean. The cost is that restarting throws away in-process caches, connections, and partial computation. Plugin systems want the same cleanup without the restart. The paper gives two definitions for this:
- Temporal composability: when a component is unloaded, every modification it made to the shared environment must be completely and safely reversed — this requires tracking every resource allocation, event registration, and state change the component performed.
- Spatial composability: when dependencies change, related components activate or deactivate automatically.
Temporal composability is the throughline of this post. Everything else — Fiber, effects, system boundaries, presets, Code Mode — is answering the same question: how do you engineer "unload = fully reversed"?
The Cordis ecosystem also brings along a few companion packages, vendored in with the project: a config-file loader (parsing declarative config like cordis.yml), an include plugin (mounting a config as a subtree — this is what the renamed preset mechanism uses), an HMR module (supporting plugin hot-swapping), plus logging and timer infrastructure plugins. Put these pieces together, and "everything is a plugin" actually clicks.
II. Fiber and Effects: The Foundation of Reversible Side Effects
In Cordis, a plugin's type definition is a union type:
type Plugin<T> = Plugin.Function<T> | Plugin.Constructor<T> | Plugin.Object<T>A bare function, a class, or an object with an apply method — all three forms ultimately resolve to a unified callback. Mounting a plugin calls ctx.plugin(plugin, config), which first looks for an existing Runtime record (keyed by the callback's identity, so mounting the same plugin multiple times shares one Runtime), creates one if none exists, then builds a Fiber and pushes it into that Runtime's fibers list.
A Fiber is the lifecycle state machine for a plugin instance, with six states: PENDING, LOADING, ACTIVE, FAILED, DISPOSED, UNLOADING. If a plugin declares inject: ['tools', 'shell'], its Fiber sits at PENDING until both services appear on the dependency chain — only then does the plugin's actual code run, entering ACTIVE. This waiting mechanism is Cordis's own dependency resolution; plugin authors don't need to hand-write "wait until the other side is ready" polling logic. This is spatial composability in action: components activate automatically once their dependencies appear.
Whether unloading a plugin is safe is decided by the effect mechanism. Anything a plugin registers — event listeners, services, timers — is registered through ctx.effect(). It runs immediately on registration, and the returned undo function is pushed onto a disposables list. Section 5.1.1 of the paper states: every context change in Cordis goes through the single primitive ctx.effect — providing a service, instantiating a component, any operation that modifies the context reduces to one ctx.effect call. It looks like this:
ctx.effect(() => {
// Perform a side effect: register a listener, start a timer, provide a service...
return () => { /* the reverse: undo the above */ } // return a disposer
})The callback can return a disposer function, a Promise, or an (async) iterator that yields disposers piece by piece — handing over one undo function per step of side effect performed.
Here, a "side effect" is defined as: any modification made to the shared environment through the Context. This table covers the everyday cases:
| Category | Example |
|---|---|
| Event registration | ctx.on("foo", handler) — attaching a listener to the shared event bus |
| Providing a service | ctx.provide(name, service) — "placing" a service onto the context |
| Mounting a sub-component | ctx.plugin(plugin) — a child plugin is itself "a side effect on the parent" |
| Context extension | ctx.extend() / ctx.intercept() / ctx.isolate() |
| External resources | Timers, file watchers, HTTP servers, child processes, DB connections, etc. |
| State changes | Modifying config, the registry, or the store |
Koishi uses ctx.command(...) to register commands and ctx.router.get(...) to register HTTP routes; DSH registers tools, attaches HMR listeners, starts schedulers — all side effects. "How to reverse it" is implemented directly: every side effect carries its own undo function, and the runtime stacks them LIFO, running the whole stack on unload:
disposables.splice(0).reverse().forEach(dispose => dispose())Undone one by one in reverse registration order, like popping a stack. A child Fiber's own dispose function is itself registered on the parent Fiber via parent.fiber.effect(...) — in the source, it's this one line at the end of the Fiber constructor:
this.dispose = parent.fiber.effect(() => { /* register the plugin */
return async () => { /* deregister the plugin */ }
})In other words, a plugin itself = one effect on the parent fiber: the plugin's startup logic is the side effect, and its unload logic is the reverse. A child component's reverse gets prepended onto the parent context's accumulator, forming a recursive structure (the paper calls this twisted composition, denoted 𝜕²Γ). So unloading a plugin cascades down through all the child plugins mounted beneath it, unwinding them in stack order — no separate "who depends on whom" cleanup table needs to be maintained.
Two more details make this mechanism reliable. First, idempotency: the first call to dispose() flips an armed flag to false; subsequent calls simply return — each reverse runs at most once. "Running it twice would apply the reverse to a state that never had the effect applied, with no way to recover." Second, interruption: if the callback returns an iterator, a guard is checked before each step, and the moment it's invalid, execution stops immediately, having only run the accumulated reverses so far — hot reload or a mid-unload failure can both stop safely. Event listeners follow the same reversible mechanism: ctx.on(...) returns an unsubscribe function, the listener is recorded in the fiber's hook table, and it's removed en masse when the fiber unloads. After _unload() finishes running all the undo functions, if it finds the dependency has become satisfied again, it immediately calls _reload() to bring it back up — "swapping out a service's implementation" is, at the runtime level, just an automatic unload-then-reload.
Roughly what a plugin looks like in practice (adding a clock to the UI):

// Session logger plugin: registers one event listener + one timer,
// cleaned up automatically on unload
export const name = 'session-logger'
export function apply(ctx: Context) {
// The effect callback runs immediately on registration
ctx.effect(() => {
// Side effect 1: subscribe to the event bus, append events to the log
const off = ctx.on('tool/result', (event) => appendToLog(event))
// Side effect 2: start a timer that periodically flushes the buffer to disk
const timer = setInterval(flushBuffer, 1000)
// Return the undo function: run in reverse registration order on unload
return () => {
clearInterval(timer) // registered later, undone first
off() // then unsubscribe the listener
}
})
}When this plugin mounts, the ctx.effect callback runs immediately: the listener attaches, the timer starts, and the returned undo function is pushed onto the disposables list. On unload (the plugin is removed, the session ends, HMR reloads), disposables.splice(0).reverse() runs each one in reverse order — clearInterval first, then off(). The undo logic is written right next to the registration logic; the author doesn't need to maintain a separate "where do I deregister this" checklist elsewhere.
This really does look like React. useEffect's shape is almost identical: useEffect(() => { subscribe(); return () => unsubscribe() }, deps) — perform the side effect in the callback, return a cleanup function, and it's called automatically when the component unmounts. Both share the same core convention: the side effect and its reverse are written together, and teardown is triggered by the framework. The differences are clear too: React's effect runs after render, cleaning up before rebuilding when dependencies change; Cordis's effect runs immediately on registration and is undone all at once on unload, with a clear LIFO guarantee on cleanup order (later registrations undo first, and child plugins cascade in reverse order along with their parent).
So why does "reversibility" deserve this much space? The paper lists "self-evolving agent harnesses" as one of two major motivations, and states it sharply:
A future harness will generate and deploy modifications to its own components while continuing to serve requests... Without temporal composability, every self-modification would force a full restart, discarding all in-process accumulated state; worse, a defective self-modification could disable the very process needed to recover.
This is the precondition for "self-evolution": an agent writes its own plugins/tools and installs them into itself. A bad install can be rolled back, and the rollback mechanism itself (the host process) can't be broken by the new component. Ordinary plugin systems can't do this, because restarting the host means losing all state in the current process. Compare this to the existing ecosystem: VSCode's extension host can't unload a single extension — disabling/uninstalling requires restarting the whole host; in the Koishi community, reloading the daemon after a config change is a routine operation. Cordis's reversibility makes "unload" a first-class operation symmetric with "load" — services don't interrupt, and other plugins are unaffected.
This mechanism incidentally solves two common engineering problems. First, failure atomicity: if a plugin throws mid-initialization (config validation fails, a dependency is missing), the fiber enters the FAILED state and disposes — whatever effects had already been applied get unwound in reverse, leaving no partially-initialized state behind. Second, cascading cleanup, without the author writing unload code. Section 5.3 of the paper makes the key claim:
Because context-mediated effects are automatically tracked and their reverses automatically composed, even an inexperienced author can obtain ordered cleanup for a plugin's context-mediated effects without writing an unload path — correctness shifts from depending on every author's discipline to being a one-time responsibility of the abstraction layer.
Unloading a plugin → cascades in reverse to unload its child plugins, release its timers/listeners/HTTP servers/DB connections. Cleanup correctness shifts from "every author writes deactivate correctly" to "the framework's structure guarantees it." The VSCode model has a shortfall here: its deactivate hook is separate from activate — "effect cleanup is separated from creation, violating locality of concerns, making complete cleanup hard to verify."
The key roles of the reversal mechanism can be summarized in four points: ① Development-time HMR (edit code without restarting, roll back transactionally on error); ② Runtime plugin install/uninstall in production (zero-downtime services); ③ Self-evolving agents (modify itself, self-recover if broken); ④ Failure atomicity and cascading cleanup (a broken plugin leaves no residue, and authors don't write unload code). It turns "unload" from manual cleanup into a structural operation the framework guarantees — this is the precondition for "everything is a plugin, and plugins are hot-swappable" to hold at all.
III. Four Constraints and the System Boundary: How the Framework Guarantees Effects Run
At this point you might ask: if an author writes a side effect outside of effect, say assigning directly to globalThis.foo, does this mechanism still hold? The paper answers this head-on. The framework's handling splits into two sides: API-level constraints, where context modification can only happen through effect; and an explicitly declared system boundary, where operations outside it aren't tracked.
The framework constrains how effects are used at four levels.
First, a single API entry point. To use any framework capability — events via ctx.on, services via ctx.provide, mounting plugins via ctx.plugin, configuring services via ctx.use — there's only one entry point, ctx, and every operation on ctx internally wraps ctx.effect. By API design, there's no path to bypass effect and still use framework functionality.
Second, Context is a Proxy. In the source it's one line: const self = new Proxy<this>(this, ReflectService.handler). Every get/set on ctx's properties goes through the proxy — this is also where reactive coeffects ("access implies notification") and interception come from. So even a direct assignment like ctx.foo = x gets intercepted and recorded.
Third, the lifecycle state machine. Every fiber has a state machine; the first line of ctx.effect() is this.assertActive(), and creating an effect on an already-unloaded fiber throws CordisError INACTIVE_EFFECT directly. Registering a side effect after unload is rejected by the framework, eliminating this class of leak in time.
Fourth, transactional HMR. Even if an author's reverse is buggy, if the new code fails to import (say, a syntax error), Algorithm 10's backup/restore rolls the entire reload back to the previous version — the system never gets stuck in a partially-loaded state. This is the last line of defense at the mechanism level.
Operations outside the boundary are explicitly untracked by the framework. Section §6.1 of the paper splits the environment in two: inside the boundary is where the system can exclusively modify and restore state — operations are recorded in Γ and can be recovered; outside the boundary is where either condition fails, and operations behave as idΓ — neither tracked nor restored. Note the boundary is drawn by location, not by medium: a scratch file on a private path, or memory only this system writes to, is inside the boundary; a public file, or something another process is also writing to, is outside it. Directly modifying globals, monkey-patching, writing to public files — all of these are outside-the-boundary operations. The framework doesn't track them, and doesn't claim it can restore them. This is a boundary explicitly declared by design.
A finer-grained layer is the two-phase acquire/emit split: the acquire phase (open/malloc/fork) is inside the boundary and reversible; the emit phase (data leaving the system via write/send) is outside the boundary and irreversible — recovery here can only rely on withholding (delaying emission until state is certain) or compensation (a compensating action: deleting a created file, refunding a received payment). Compensation also composes LIFO, but the meta-theory no longer guarantees it.
How is this handled engineering-wise, for resources outside the boundary? Section §6.1 of the paper offers one mechanism: a coeffect moves the boundary by reifying the external location — restricting all access to that location to a set of operations, each of which provides a reverse, so what was previously idΓ becomes trackable and recoverable. In engineering terms, this is the service abstraction: developers access these resources through ctx.database, ctx.assets, without touching database connections, file handles, or child processes directly. Resource lifecycle is locked inside the service's own effect; the service provider is responsible for writing a correct reverse, and the consumer only faces the high-level interface. Side effects shift from being scattered across plugin code to being concentrated inside service implementations, and the author's obligation concentrates onto a small number of service implementers.
One last question: how do you judge whether a reverse is correct? Section §3.3.2 of the paper doesn't require "physical restoration," because that's impossible (free doesn't restore heap layout, and generated names don't regenerate). The recovery guarantee is read as observational equivalence:
Two states are related when no observer can distinguish them. Compare behavior, not representation... the equivalence relation is assembled from the equivalences each coeffect carries.
"Reversal" means restoration at the observational level: no observer can tell the difference between the state before and after. That's the correctness standard for a reverse. It doesn't matter that heap layout wasn't restored, as long as no coeffect operation can detect a difference — that counts as successful recovery.
The guarantees in this chapter can be organized into one table:
| Layer | Who guarantees it | What it covers |
|---|---|---|
| Path enforcement | Framework (API design) | Using framework functionality only goes through ctx; every ctx operation wraps effect → necessarily tracked |
| Temporal enforcement | Framework (state machine) | Creating an effect on an unloaded fiber throws INACTIVE_EFFECT directly |
| Property interception | Framework (Proxy) | ctx property reads/writes are intercepted and can be recorded |
| Module-level transactions | Framework (HMR backup/restore) | Import failure rolls the whole thing back; never lands in a half-reloaded state |
| Reverse correctness | Author's obligation | The runtime doesn't verify the witness (g(δ)=γ) — the paper says so explicitly |
| Outside-boundary behavior | No guarantee | Globals / public files / emitted data = idΓ |
| Boundary shifting | Coeffect / service reification | Wrap the external location as a "reversible service"; access is restricted, reverse is provided by the service |
To summarize: the framework makes "context modification must go through ctx.effect" the only path, leaves "is the reverse correct" as the author's obligation, and explicitly declares the system boundary. Where this lands, engineering-wise, is service reification: side effects concentrate inside service implementations, and the obligation narrows from "every plugin author" to "a small number of service implementers." The paper has an unusually candid line here too: "the callback supplies a reverse, but whether that reverse truly restores the effect it accompanies is the obligation of the component's author, not a property verified by the runtime." The runtime guarantees "the reverse gets called" (structural), not "the reverse was written correctly" (semantic).
One aside worth noting: this design lands most naturally in TS/JS. Most compiled languages have type systems based on classes, with type layout fixed at compile time — there's no way to override it by dynamically loading new compiled output, and type information loaded into memory typically can't be unloaded either. TS/JS is a prototype-based system, where type structure can be adjusted dynamically: an agent creates a new tool, hangs it directly on the prototype, and every child component senses and can call it immediately; undoing it disconnects it from the prototype chain, and system state recovers instantly. Proxy provides dynamic routing that's "logically decoupled at the extreme, syntactically fully transparent" — when code writes ctx.tool, Proxy handles dynamic addressing and permission checks, something static languages struggle to implement. Module Augmentation lets a plugin dynamically modify the global Context's type definitions, so a new tool an agent generates gets its type hints and safety checks synced to the entire system instantly, letting "dynamic evolution" and "type safety" coexist. On top of that, JS has a programmatic module registry, so modules can be unloaded and their objects garbage-collected; in a language like Swift, thoroughly cleaning up already-loaded type metadata is close to impossible.
IV. How ctx.<service> Resolves: One Proxy Plus a Fiber Chain
How plugins get hold of each other's capabilities deserves its own section — it's the other half of what makes "everything is a plugin" actually work.
The root context is wrapped in a Proxy: new Proxy(this, ReflectService.handler). Accessing properties like ctx.tools, ctx.llm, ctx.session goes through the Proxy's get trap. Properties the object already has go straight through native Reflect.get; for an unfamiliar property name, it first checks whether there's a registered accessor, and if not, walks up the current Fiber:
let fiber = ctx.fiber
while (true) {
const impl = fiber.store?.[prop]
if (impl) return impl.value
if (prop in fiber.inject) throw new Error('service not active')
if (!fiber.runtime) throw new Error('unknown service')
fiber = fiber.parent.fiber
}It walks up toward the parent Fiber until it finds the layer that provides this service, or confirms nobody does and throws.
The place that actually "publishes" a service is the Service base class, which calls self.ctx.reflect.provide(name, self, check) at construction time. provide() itself is also wrapped in ctx.fiber.effect(...): it stuffs a {name, value, fiber, check} record into the current Fiber's store, then wakes up whoever's waiting on this service. The returned undo function removes that record from the store and re-notifies once more. Removing a service goes through exactly the same effect-undo mechanism as unloading a plugin — there's no separately opened special path. There's no dedicated "service registry" class in the system anywhere. Behind syntax like ctx.<name> is one Proxy interception plus one lookup along the Fiber chain. Who registered what at which layer directly determines who can see what.
This design has a direct consequence: scope isolation is natural. A child Fiber can see services registered by its parent Fiber; the reverse doesn't hold. Wanting a plugin's service to only take effect within a specific range (say, a filesystem implementation exclusive to one preset) requires no extra visibility-control logic — just mount that plugin under the Fiber at the corresponding level, and the lookup chain naturally confines it to that range.
The value of this mechanism becomes clear in a real scenario. DeepSeek Harness defines an "llm" service interface (packages/llm/llm) that only specifies "how to make one streaming call, how to handle retries," without caring which vendor's model it actually is. Two plugins implementing this interface are mounted in the system at the same time: dsh-llm-deepseek, adapted specifically for their own model; and dsh-llm-pi-ai, which wraps a third-party npm package, @earendil-works/pi-ai, internally, dedicated to protocol translation across many model vendors (this "pi" and the separate Pi agent harness people compare it to are two unrelated things that happen to share a name). Nearly 40 officially supported model vendors are mostly backed by this latter plugin. The two plugins don't know the other exists; installing or swapping either one requires zero changes to how the upper-layer agent loop calls things, because it's calling the interface itself. The official docs call this pairing a "design-validating twin": two completely independent implementations both satisfy the same definition, which is indirect proof the interface is general enough.
V. A Few Design Details in the Agent Loop Worth Calling Out
The overall shape of the loop is similar to most frameworks today — model input/output, detecting tool calls, executing them, feeding results back — not worth repeating. A few specific implementation choices are worth expanding on.
First, a turn's end condition can come from multiple sources. Besides "no new tool calls," a tool's execution result itself can carry a concludesTurn: true flag. The tool's execution logic uses this flag to declare "this turn should stop here," without waiting for the model to say "done" itself.
Second, max-tokens status is sticky:
if (turnEnds === null || turnEnds.kind !== 'max-tokens') turnEnds = stepEndOnce a step gets truncated because it hit the output limit, even if subsequent steps complete normally, the turn's final reported end reason won't get overwritten back to "completed normally." The signal upstream (stats, log display) sees — "this turn got truncated at some point" — doesn't get erased just because later steps finished cleanly.
Third, right before a turn actually closes, an agent/turn-stopping event is broadcast — a hook any plugin can intercept. A plugin calling agent.steer(...) to inject a new message keeps the turn running; doing nothing lets it actually end by default. "When does this really wrap up" has moved from inside the loop to outside it — any plugin can decide at the last moment, "not done yet, I still have something to say," and the loop itself doesn't need to know in advance how many kinds of "not done yet" there might be.
Fourth, on tool scheduling: before every actual invocation, the scheduler asks the tool whether it can run in parallel — the judgment doesn't rely on a static whitelist:
executionMode(exec): ToolExecutionMode {
const tool = this.resolveExecution(exec.name, exec.agent)
if (!tool?.isConcurrencySafe) return { kind: 'exclusive' }
return tool.isConcurrencySafe(exec.arguments) === true
? { kind: 'parallel' }
: { kind: 'exclusive' }
}The same tool, with different argument combinations, can give different answers (reading a file is always parallel-safe, writing one is only safe if the target paths don't conflict). When the scheduler assembles a batch, it re-evaluates the mode again right before launching the next call. If a call currently judged exclusive slips into a parallel batch, that batch gets cut off right there rather than forcing the exclusive call into it. Results are submitted in the order the model originally issued the calls, independent of the order scheduling completed them.
Fifth, pre- and post-execution checks around a tool are independent, pluggable stages, each registered separately, not written inside the tool-execution function itself. tools/pre-execute is a waterfall event where any plugin can intercept or require approval; only after passing does it enter built-in guard checks (e.g., getting flagged if the same tool with the same arguments gets called too many times in a row); only then is the tool body actually invoked; and after it finishes, tools/post-execute lets plugins post-process the result. These four stages are a fixed order of independently-registered plugin mount points. Adding a new approval policy or guard rule doesn't require touching the execution pipeline itself — just attach a new event listener.
VI. Preset's Two-Layer Scope Chain: Real Inheritance and a Shadow Routing Table
The preset-mounting mechanism solves a specific problem: how the same config gets reused across multiple sessions without re-parsing the config file every single time.
A preset is a directory, and at its core is one agent.cordis.yml that lists, in order, which plugins to mount and with what parameters. Mounting this file uses an Include plugin reused from the Cordis ecosystem, renamed PresetTree:
const handle = agentCtx.plugin(PresetTree, { path: pathToFileURL(preset.path).href })
await handle.await()PresetTree overrides two methods: when resolving bare module names, it looks up based on the system's own baseUrl, and the preset directory doesn't participate in resolution; writing config back is directly disabled, so preset source files can't be modified accidentally. Once mounting completes, it also checks two things: whether any line of plugin config is stuck in an unavailable state, and whether any service has "leaked" outside the preset's scope during mounting. If either condition fails, everything rolls back.
The way the scope chain is constructed is worth a closer look. From global scope down to a specific preset is a genuine context derivation: createScope() internally calls ctx.plugin(scope) to get a Fiber, then fiber.ctx.extend({[kScope]: key}) — this step genuinely creates a node in Cordis's context tree. Going from a preset down to a specific session uses a different approach:
this.bindings.set(agentKey, bindScopeParent(agentKey, standing.key))bindScopeParent records a "logical parent" relationship in a WeakMap, without opening any new node in Cordis's Fiber tree at all.
The motivation for splitting it this way is direct: a preset's "standard mounted instance" is only built once (ensureStanding()), and any number of subsequent sessions — writing, coding, research — can reuse that same instance, simply by appending one more binding to this WeakMap, without re-running the plugin-loading process for every new session. When a child agent needs to inherit the preset its parent agent currently has mounted, calling composeFrom(childCtx, parentCtx) is also just a lookup-plus-binding on this table — a synchronous operation that doesn't re-trigger any plugin's mounting flow. The child agent gets the exact same plugin instance as its parent, including the same already-registered batch of tools and prompt fragments.
Which preset is currently in effect for a session is determined by scanning the event log backward for the most recent agent-preset/selected event:
for (let index = session.events.length - 1; index >= 0; index -= 1) {
const event = session.events[index]
if (event?.type === 'agent-preset/selected') return event.data.agentPreset
}
return session.header.agentPresetOnly falling back to the default set at session creation if there's no such event at all. In theory, switching presets mid-session is an architecturally allowed operation — the lookup logic naturally supports this case, and whether it's actually used in practice is a product-layer decision.
The preset discovery mechanism is also worth a mention. When scanning a preset root directory, it only recognizes subdirectories whose names match a specific naming convention and which contain an agent.cordis.yml internally. It validates each one for whether the YAML parses and whether the structure is valid; broken entries are flagged without interrupting the entire scan. When multiple root directories are merged, an id that appears first wins, and a same-named preset appearing later in another root is ignored — leaving room for "a user's custom preset overrides the official one" (or the reverse), depending on how the root directories are registered.
VII. Code Mode's Isolation Choice: worker_threads
Having the model write code to orchestrate multiple tool calls isn't a new idea at this point — Codex built something similar too. What's worth examining closely is the isolation approach chosen and how it's implemented.
DeepSeek Harness runs this code using node:worker_threads — a plain Node worker thread, a different category of solution from a typical V8 isolate environment or vm2. There's a clear consideration behind this choice: same-process sandboxes like node:vm allow the prototype chain to escape into the main environment, and a hot loop genuinely can't be interrupted from outside — considered unreliable. Switching to a dedicated worker thread costs a new thread being spun up every time code runs, in exchange for a genuinely independent V8 heap and an execution environment that can be forcibly terminated from outside.
Code the model writes first passes through stripTypeScriptTypes to strip type annotations, using a wrapper prefix/suffix to hold position while stripping:
const stripped = stripTypeScriptTypes(prefix + program + suffix)This guarantees the code's original line/column numbers stay unchanged after stripping, so error locations line up correctly. The stripped code gets stuffed into a dynamically constructed async function:
const AsyncFunction = (async () => {}).constructor
const fn = new AsyncFunction(...bindingNames, ...errorClassNames, 'console', code)
const value = await fn(...bindings, ...errorClasses, consoleShim)Every tool namespace, every agreed-upon error class, and a replacement console all become this function's parameters and arguments. Code the model writes can use top-level await directly, and can also directly return a value.
The worker thread itself has resource limits — resourceLimits.maxOldGenerationSizeMb caps heap memory. There are also two independent timeouts: one polls event-loop utilization to calculate how much CPU time this code has consumed; the other is a simple setTimeout as a backstop on total elapsed time. Either one triggering forces an interruption.
Tool calls from within the code go through a message channel, without directly referencing functions:
{ type: 'call', id, global: 'tools', name, args } // worker → host
{ type: 'reply', id, ok: true, value } // host → workerWhen the host receives a call request, it treats it as a perfectly normal tool call, running it through the same pre-execute/guard/dispatch/post-execute pipeline described in Chapter V — sharing the exact same execution core as tool calls the model issues directly in structured form, just with a different entry point. Both ends of the channel manually validate every field on every message received. On the host side, looking up bound functions uses Object.hasOwn() to precisely check own properties; when constructing the tools namespace exposed to the code, it starts from Object.create(null) and mounts properties one by one via Object.defineProperty. These practices all serve the same purpose: preventing code from smuggling in fields like __proto__ or constructor to bypass the prototype chain and produce unintended behavior. The code comments state directly: "treat the worker as a hostile counterparty" — this trust assumption is explicitly stated.
Codex's approach differs at its core in the isolation layer: it uses an independent V8 isolate, with tools attached to a global tools object; the official description is "executing this JavaScript inside a brand-new V8 isolated environment." Both sides are solving the same problem — orchestrating multiple tool calls with code, reducing round trips — but chose different isolation technologies, weighing "the overhead of a new isolate each time" against "the overhead of spinning up a worker thread each time," along with what isolation primitives are readily available in each ecosystem.
VIII. The Layered Shadowing Algorithm for Tool Registration
How tools get "overridden locally" across different scopes is a piece of plugin-based design that's easy to overlook but critical — it determines whether a preset can actually replace a tool.
Each scope maintains its own ToolLayer, internally a table indexed by name. What determines "which tools the current agent can see" is the view() step: it first loads the global layer's tools as a base, then lets each ancestor scope override same-named entries in order, with ancestors closer to the current scope taking priority — this step only handles the inherited portion. Restriction rules (say, a preset explicitly disabling a tool) get filtered in after that. Finally, tools registered directly by the current scope itself override all previously-inherited same-named entries, even if a restriction rule disabled that name. The order here matters: a scope's own directly-registered tools take priority over both inheritance and restriction rules.
Registration itself has hard validation too:
register(definition: ToolDefinition): () => void {
if (definition.name === RUN_CODE_NAME) {
throw new Error(`tool name "${RUN_CODE_NAME}" is reserved for the Code Mode transport`)
}
assertSupportedJsonSchema(definition.output.schema)
return this.layers.effect(this.ctx, layer => layer.tools.insert(definition.name, definition))
}The name run_code is hardcoded as reserved — no plugin can register or override it. The reasoning is written right in the code comments: any agent might pick its own name for a code mode, and a name that looks idle under a default deployment could, the moment some preset gets mounted, conflict with Code Mode's call entry point at any time — so it's unconditionally reserved.
The registration action itself also goes through effect(): the undo function returned by register() is pushed into the current Fiber's disposables list. A tool registered by a plugin automatically gets stripped out of the layer when that plugin unloads — no extra "remember to delete the tool on unload" cleanup code needed. This is the same path as the effect-undo mechanism from Chapter II; tool registration is just one of many "registration as side effect" cases.
Tool execution itself also reuses this scheduler's concurrency contract. When model code in Code Mode calls a tool, that call enters the same host-side TOOL_RUNTIME_SCHEDULER's pending queue, sharing the same concurrency control and the same pre-checks as structured tool calls the model issues directly — this is also why the previous chapter said Code Mode and ordinary tool calls "share the exact same execution core, just with a different entry point."
IX. A Few Engineering Differences Compared to Codex
Replaceability of the core loop. Codex's tool interface, approval policy, and sandbox policy are all configurable, but the code driving the "sample-execute-feedback" loop is fixed in the core, and can't be swapped out as an independent unit. In DeepSeek Harness, this loop's specific implementation (ReactLoopAgent) is registered as a factory — theoretically it could be replaced by a completely different loop implementation. Currently only this one implementation is mounted in the system, and the factory only allows one registration.
Where the sandbox sits. Codex compiles bwrap/Landlock on Linux and Seatbelt on macOS directly into its core execution path, as infrastructure. DeepSeek Harness makes the sandbox a capability plugin — on Linux, it calls Landlock through a dedicated native Node extension, going through the same service-registration path described in earlier chapters; in theory it could be swapped for a different isolation implementation without the tool code calling it needing any changes.
Granularity of code decomposition. Codex's Rust code splits into roughly a hundred-odd crates; DeepSeek Harness's TypeScript code splits into over two hundred packages. Neither is a monolith, but all of Codex's decomposed modules ultimately serve the same non-replaceable core loop; DeepSeek Harness's decomposed modules (including the loop itself) theoretically all sit at the same replaceable tier — no single module holds a special status.
Isolation choice for code-orchestrated tool calls. Both sides independently arrived at the same functional judgment: tool calling should support code-based orchestration in addition to single structured requests. They chose different isolation technologies when implementing it: Codex picked an independent V8 isolate, DeepSeek Harness picked worker_threads. Chapter VII already covered the trade-off behind this difference.
Granularity of tool exposure. In Codex's tool interface, the exposure mode (DIRECT/DEFERRED/CODE_MODE/Hidden) is a static flag the tool declares for itself. In DeepSeek Harness, whether a tool is visible or overridden under a given scope is computed in real time at runtime along the scope chain — the same tool's visibility can be completely different across sessions, depending on which presets are mounted.
Appendix: Walking the Full Path from Process Start to a Single Tool Call
Process starts, first checking which launch profile is in use, which determines which plugin packages the whole process should load, handed to Cordis's loader to mount one by one, each plugin corresponding to one Fiber. Once loaded, the host layer has model access, credential management, sandboxing, and other runtime infrastructure ready.
A session begins, the agent registry allocates a specific instance, and this instance hangs off the "global-preset-session" scope chain. Which tools and which prompts it can see depends on its position on this chain, plus the shadowing rules described in Chapter VIII.
The Agent Loop takes over running this session: reading the current context, requesting the model, detecting tool calls, handing them to the unified tool pipeline for pre-check, execution, and post-processing.
Everything that happens at each step — whether it's the model's output or a tool's result — gets appended as an event into this session's log, never deleted, never overwritten. This log is simultaneously the source of persistence, the source of context for the next request, and the source for what the web UI displays — all three uses are projected from the same log. The web UI itself is also a group of plugins mounted into this same system.

This post covered quite a few specific mechanisms: Cordis's Fiber lifecycle and effect-undo stack, ctx.<service>'s Proxy resolution, a few detailed design choices in the agent loop, preset's two-layer scope chain, Code Mode's isolation choice, and the layered shadowing algorithm for tool registration. Put together, these mechanisms solve a handful of specific problems you keep running into when building agent products, or using agents deeply.
The first problem is the risk of adding and modifying capabilities. The common approach is to hang plugins outside a core loop that itself rarely changes, because any change to it means worrying about blast radius and re-running the full regression suite. DeepSeek Harness pushes risk control down to the plugin level: everything a plugin registers is recorded through effect, and unloading undoes it exactly, in reverse registration order. Adding or replacing a capability confines its blast radius to that plugin's own boundary, and shrinks the verification scope to that same boundary.
The second problem is wanting the same agent to serve different scenarios without re-setting-up the environment every time. Preset's two-layer scope chain solves exactly this: a preset's plugin tree only mounts once, and sessions for different purposes — writing, coding, research — share the same already-running instance, routed through a logical parent-child table.
The third problem is the round-trip cost of one tool call at a time in multi-step tasks. Code Mode hands this kind of orchestration logic over to the model to write as code itself, cutting down the "say one thing, wait a beat" round trips. DeepSeek Harness and Codex arrived at nearly the same judgment on this almost simultaneously — they just picked different isolation approaches.
The fourth problem is that tool visibility and permissions, across different scenarios, often only have two states: a tool is either globally available or globally unavailable. The layered shadowing algorithm turns visibility into something computed at runtime along the scope chain — the same tool can have completely different visibility across different presets and different sessions, without the tool's code needing a pile of conditional checks.
These design choices together point to a bigger judgment: an agent product's competitiveness, beyond model quality, also depends on this runtime layer. How the runtime is organized, how expensive it is to extend, how much it costs to swap out a component — these engineering decisions equally affect how pleasant an agent product is to actually use. DeepSeek Harness puts its emphasis on this layer, using Cordis, an existing plugin framework, plus the design philosophy of "reversible side effects," to translate this judgment into concrete code structure. How it actually holds up depends on how long this architecture can sustain itself in real usage, and whether it can hold up under more complex scenarios.
P.S. While writing this post, the sample plugin mentioned in it managed to crash the UI — looks like HMR stability still has some room to improve!

Outlook: Where a Runtime That Can Hot-Update Itself Might Go
The community has already built plugins connecting messaging tools to DSH — pulling in messages from Telegram, Discord, and so on. Back in the day, Koishi used Cordis to bolt together dozens of plugins for QQ, Discord, Telegram, and WeChat, and became a phenomenon in the chatbot world — leaving behind the daily ritual of "reload the daemon after every config change." Now the same runtime core has been carried over into an agent product, and messaging-tool plugins are starting to show up — the same path from back then is playing out again on DSH, except this time, the runtime comes with an agent loop built in from the start.
Another path is unfolding at the same time. Personal AI assistants like Hermes and OpenClaw grow by continuously adding skills to a skill library: capability grows, while the runtime's structure basically stays put. DSH's growth happens on a different axis: plugins can be hot-installed and hot-removed — the UI is a plugin, session logging is a plugin, the loop itself is a plugin, and even the web UI can be swapped out while continuing to serve. That line from the paper — "a future harness will generate and deploy modifications to its own components while continuing to serve requests" — is already starting to come true here.
So the question is worth asking seriously: that "lobster" that's constantly reloading its daemon, blowing itself up every so often — is it really about to clock out this time? Will DSH become the AI-native successor to the old "plugin bot framework + manual config" workflow?
There's no answer yet, but web UI as a plugin means the product form itself can hot-update: change the interface, swap the interaction model, add new views, without restarting the host, and without shipping a new release. If what's being replaced is a way of working, then it's the "assemble plugins, write config, restart to verify" loop itself that's being compressed.
For an individual developer, building an AI product used to mean writing a frontend, configuring a backend, and building an agent framework — three layers that didn't talk to each other. On DSH, all three layers are plugins, hung into the same runtime, and the UI can hot-update directly. The distance from an idea to a working prototype, even to a shippable product, is visibly shrinking. Something that works at the prototype stage can become part of the product just by adding a preset or configuring a plugin.
That said, there's still a stretch of road between "developer-leaning" and "aimed at everyday developers." Running DSH today requires a Node environment, and opening a terminal to hand-type commands to start the server. That workflow is routine for people who write code; for everyday developers, it's not exactly elegant. Until technical complexity comes down, "quickly building an AI product on your own" only holds for a small group of people. The upside of being plugin-based is exactly here though: how the host starts up, how the UI is presented — both are replaceable plugins. Wrapping this layer of complexity into an installer, into one-click launch, is a productization problem, and the architecture already has a place to put it.
Whether it can become the next phenomenon in this family ultimately depends on a few things: how many plugins the ecosystem grows, whether real-world scenarios hold up under it, and when the complexity layer aimed at everyday developers gets wrapped up.
Right now DeepSeek Harness is still a developer-leaning product, but its path toward becoming the "VSCode of the agent world" doesn't feel far off.
Recommended Plugins: The Ecosystem Is Growing Fast
At the time the original article was written, the #dsh-plugin topic on GitHub already had 1000+ repositories (1008 per the API at the time of writing). Beyond the official deepseek-ai/deepseek-harness, third-party plugins, clients, and tutorials keep growing. The fastest way into the full list is through a few curated indexes:
- awesome-dsh-plugin/awesome-dsh-plugin (256⭐) — a curated plugin list, bilingual Chinese/English
- AdamPlatin123/awesome-dsh-plugins (507⭐) — automatically scans and indexes all dsh plugin candidates
- 0xsline/awesome-deepseek-harness (227⭐) — a curated DSH ecosystem selection: plugins, tools, infrastructure
- Electricitysheep/dsh-handbook (74⭐) — a from-zero DSH deep-dive handbook, including plugin development
Web UI enhancements:
- zhu1090093659/dsh-web-ui (922⭐) — task panel, Git graph, theme hub bundle
- omdsh-dev/DSH-better-sidebar (371⭐) — a sidebar workbench: files, terminal, Git, subagents
- Small-tailqwq/dsh-deep-whale (237⭐) — a whale-mascot theme series
- lhh010/dsh-minigames (10⭐) — an 18-minigame slacking-off panel on the side
Vision / multimodal:
- liustack/modlens (857⭐) — the first vision plugin: OCR, layout, structured semantic evidence
- Anionex/dsh-vision-toolkit (232⭐) — image Q&A, long-screenshot OCR, UI reconstruction
Terminal and desktop — "wrapping complexity into an installer" already has a community working on it:
- ccch1mneyyy/dsh-TUI (487⭐) — a Claude Code-style full-screen terminal UI
- Ruler4396/dsh-launcher (40⭐) — a lightweight Windows launcher
- bruc3van/dsh-desktop (10⭐) — a community third-party desktop client
Memory / context — carrying conversations across session boundaries:
- csyangwen/dsh-memory-evolve (24⭐) — cross-session long-term memory + self-evolution
- Anionex/dsh-turn-rewind (25⭐) — conversation/code state rewind
Multi-agent / workflow — orchestrating agents as a team:
- NanmiCoder/dsh-agent-teams (142⭐) — the AgentTeams plugin
- icetomoyo/dsh_workflow (42⭐) — upgrades one-off scheduling into a governable Workflow layer
Messaging and notifications:
- PlutoKeating/dsh-lark-bot (4⭐) — a Feishu/Lark bot
- sliverp/DeepSeek-harness-qqbot — a QQ bot
- LoserFox/telegram — Telegram
- omdsh-dev/dsh-notification (26⭐) — desktop notifications
Dev tooling — the people building plugins built themselves tools first:
- omdsh-dev/dsh-genui (36⭐) — generative UI inside the conversation
- omdsh-dev/dsh-at-file (62⭐) — Codex-style @file references
- vlln/plugin-registry (21⭐) — plugin ecosystem infrastructure + dev onboarding
Star counts are as of when the original article was written — check the repos for current numbers. For the full list, see github.com/topics/dsh-plugin.
References consulted when writing the original article: