DeepSeek's New Agent Harness Makes the Core Loop a Plugin
Hello everyone, I'm Ruofeng.
On August 13th, DeepSeek open-sourced a project called deepseek-harness (command-line alias dsh), which rocketed to over 30,000 stars in a single day. When you click into it, you'll find something odd: the README is only about 1,700 words long, with no screenshots, no feature list, and it doesn't even clearly explain 'what this thing actually is.' It just throws out one line: Everything is a Plugin.
The star count comes with the DeepSeek brand. But I have a habit when looking at open-source projects: star counts can deceive, but architecture doesn't. The part of this project truly worth spending time on isn't how many stars it gained in a day, but the bet it's placing—turning every hard-coded part of an agent framework, even the 'layer that runs the model loop' itself, into a pluggable component that can be swapped at any time.
This article will dissect whether this bet is worth placing.
First, let's clarify what problem it's trying to solve
dsh is an agent harness. To put it plainly, it's a runtime that serves as a base for coding agents. You install Node, run a single line npx @deepseek-ai/dsh web, and it starts a local web interface containing an agent that can use tools, read and write files, and converse back and forth with you.
There are too many things like this this year. Claude Code is one, OpenAI's Codex CLI is one, Cline, Aider, and Cursor each occupy a spot. They share a common trait: the core loop is hard-coded. How the model is called, how tools are registered, how sessions are stored, how prompts are assembled—these decisions are welded into the product. If you want to swap a sandbox backend or insert a custom interceptor, you often have to fork the source code and modify it.
dsh's counter-example is stated very bluntly in its architecture documentation: There is no privileged core to patch. The model adapter is a plugin, the tool registry is a plugin, the session log is a plugin, and even the agent loop driving the entire conversation is itself a plugin.
Think about how big that difference is.
I specifically dug into the code structure of packages/core. The agent-loop package is defined in the documentation as 'the one concrete implementation of the public Agent contract.' Note this wording—it is 'an implementation,' not 'the implementation.' The documentation immediately emphasizes that all extension plugins depend on agent (the interface package) and never directly depend on agent-loop, so this loop is always replaceable.
This isn't PowerPoint rhetoric; it's a contract at the source code level.
First, the big picture: the five layers look like this
Before diving into the details, here's a panoramic view. I've categorized dsh's source code structure into five layers, breaking them down step by step from top to bottom.
At the very top is the user entry point and configuration composition layer. In the middle is the highlighted core agent loop and the capability seams it depends on. At the very bottom is the Cordis plugin spine that underpins everything. Every file name and function name marked on the cards in this diagram was read from the source code, not guessed from directory names. The following sections will discuss these five layers in order.
Cordis: a spine borrowed from a chatbot framework
Making everything a plugin sounds easy, but the first question to answer when actually doing it is: how do plugins assemble, disassemble, and not leave a mess behind after disassembly? dsh didn't build this mechanism from scratch; it used a framework called Cordis.
Interestingly, Cordis's origin story. I checked vendor/README.md; dsh 'vendored' Cordis along with a bunch of its base libraries into its own monorepo—meaning it copied the entire upstream source code in, renamed it to the @deepseek-ai scope, locked the version, rather than relying on it as an npm dependency. The upstream points to cordiverse/cordis.
Those familiar with frontend chatbots might already recognize this. Cordis is the next-generation kernel of the Koishi cross-platform chatbot framework. Koishi has been popular in the domestic chatbot community for many years. The author distilled years of experience dealing with 'plugin hot-swapping and runtime composition' into Cordis, even accompanying it with a paper, 'A Programming Paradigm for Spatiotemporal Composability.'
This background is crucial. dsh essentially took a plugin framework that had been polished for a long time in a chatbot ecosystem and moved it over to serve as the spine for an agent. It's not an experiment that appeared out of thin air; the foundation has been battle-tested.
More critically, DeepSeek didn't just copy it; they also modified it. Item 6 of the 'Local modifications' list in vendor/README.md details this: they hardened the lifecycle in cordis/src/fiber.ts, plugging three 'reentrant disposal gaps.' Specifically, the effect's owner-list wrapper is registered before the setup body, so unloads initiated from within setup wait for setup and all cleanups to complete; async cleanups remain visible to the owner until quiescence; effect creation is rejected when the owner is in the UNLOADING state, preventing registrations during cleanup from escaping the unload snapshot.
If you don't quite understand this paragraph, just remember one conclusion: the hardest part of the reversible effects mechanism is the edge cases during unload. DeepSeek genuinely tackled this tough nut; it wasn't just taking without giving.
Reversible Effects: the soul of the entire design
Cordis's most core idea, in the documentation's own words, is: Registrations are reversible effects. Registration is an effect, and all effects are reversible.
What does this mean? When you register a tool, a piece of prompt text, or an event listener inside a plugin, these actions in Cordis aren't 'done and forgotten.' Instead, they are wrapped via ctx.effect() or ctx.on() into effects with a 'counter-operation.' When this plugin is unloaded (whether due to a configuration change, hot reload, or because a service it depends on is gone), these registrations are cleanly rolled back in reverse order, leaving no orphaned listeners or leaked timers.
The tutorial has the most straightforward example. Registering a heartbeat timer is written like this:
function heartbeat(ctx: Context) {
ctx.effect(() => {
const timer = setInterval(() => console.log('tick'), 200)
return () => {
clearInterval(timer)
console.log('heartbeat cleaned up')
}
})
}
ctx.effect() receives a setup function and returns a disposer. Cordis guarantees that when this plugin is unloaded, the disposer will definitely be called. Thus, 'installing a plugin' and 'uninstalling a plugin' are symmetrical; whatever is registered is reclaimed.
Why is this important? Because agent frameworks are inherently 'highly dynamic.' You might want to temporarily swap a set of tools for a specific session, attach a one-time interceptor to an agent, or hot-swap a model adapter at runtime. These scenarios are hard to do cleanly in frameworks with hard-coded cores; you either have to manually manage a bunch of removeListener calls, or runtime changes are simply not allowed. Reversible effects turn this into a built-in capability of the framework. Plugin authors just need to register according to the rules, and the framework handles the cleanup for them.
Honestly, this is the point I admire most after reading the whole thing. It's not some clever trick; it's a discipline carried through to the end.
Four types of event dispatch: waterfall is the key to interception
For plugins to communicate, Cordis uses typed events. But it has more than one kind of emit; the documentation explicitly lists four dispatch modes, and the dispatch mode is part of the event's public contract.
| Mode | Awaits | Order | Has Return Value |
|---|---|---|---|
emit |
No | Observes by registration order | No |
waterfall |
No | By registration order | Yes |
parallel |
Yes | All listeners in parallel | No |
serial |
Yes | By registration order | Yes |
The four modes correspond to four intentions: observation, wrapping, fan-out, and sequential execution. This classification is very clear, and I quite like it.
Among them, the most critical is waterfall. It is essentially around-middleware. A listener receives (...args, next), and calling next() passes the (possibly rewritten) result to the next one; returning directly without calling next() short-circuits the entire chain.
This semantic is incredibly useful in an agent framework. The turn flow diagram in the documentation heavily uses waterfall. A round of conversation runs like this:
turn/start
Claim next input and queued messages
Assemble prompt segments + tool schemas
-> agent/pre-step reject | enter(messages) ← waterfall
step/start
agent/request -> llm/stream -> assistant/chunk* -> assistant/message
tool/call* -> tools/pre-execute -> tools/execute -> tools/post-execute -> tool/result*
step/end
-> agent/turn-stopping ← serial, no next
turn/end
Look at agent/pre-step; it's a waterfall. A listener can rewrite the messages the model sees at this step, or even directly reject it. This means 'what the model can see' can be intercepted and modified by any plugin, and after modification, calling next() continues the process without touching the loop's own code.
tools/pre-execute, tools/execute, and tools/post-execute linked together form a tool execution pipeline, with three openings—before, during, and after—all left for you to plug in strategies. Want to add permission checks? Hook into pre. Want to modify results? Hook into post. This approach of 'designing interception points as events' is a completely different species from frameworks with 'hard-coded cores.'
The session log is the single source of truth
Many agent frameworks store sessions as 'conversation messages'; the message list is the history. dsh doesn't do this. Its session is an append-only event log, with types declared in SessionEventMap. The message history is 'projected' from this log and never stored separately.
The documentation repeatedly emphasizes a runtime invariant: Model-visible means logged. Anything visible to the model must be reconstructable from the log. And this isn't a constraint written for humans; there are runtime assertions in the code checking this. The deriveMessages() function is specifically responsible for projecting the model history from the log, even preserving the original assistant/chunk streaming fragments, so replay, UI playback, and forking branches are all derived from this single log stream.
This design has a very practical benefit. If you want to fork a session, restore to a certain node, or perform context compaction, you don't need to invent new storage formats. They are all 'different projections of the same log.' The documentation also states that extending new 'model-visible inputs' requires extending SessionEventMap and then rendering from the log, not just casually stuffing a field into a message.
Frankly, the term event sourcing has been talked to death in backend circles, but dsh is the most thorough implementation I've seen that actually applies it to agent sessions and uses runtime assertions to guarantee 'visible means logged.'
Capability seams: swapping one provider changes the entire product
'All plugins' sounds like piling on abstractions, but it has a very practical landing point, which the documentation calls a capability seam.
A seam consists of three roles: Service Definition (declares the interface), Service Provider (implements it), and Consumer (consumes it, typically a model-visible tool). A line in the documentation is particularly spot-on: Seams are why one provider swap changes the whole product.
For example, the two capabilities of file system and subprocess share the same 'execution world.' So if you point their provider to a remote sandbox, tools like Bash, PTY, and LSP will move to the remote location along with them, without needing to fork each tool individually. The same logic applies to the sub-agent provider: behind one interface could be spawning a new sub-agent from scratch, or delegating this turn to another product.
This design intent is actually quite ambitious. It bets that the capability boundaries of agents will constantly change in the future. Today you might use a local shell; tomorrow everything might run in a sandbox, in a browser, or inside another agent. Abstracting these 'capabilities' into replaceable seams will outlast welding each tool in place.
Profile and Bundle: configuration as composition
Finally, let's talk about how an ordinary person can use this mechanism without writing code.
dsh's startup essentially composes a plugin tree from a cordis.yml file. It divides the composition unit into two conceptual layers. A Profile is an ordered overlay of a set of bundles, stored in the Harness's home directory. A Bundle is a Cordis configuration line plus the distribution format of the code it mounts. Each package declares itself in its own package.json using the dsh field: dsh.profile lists which bundles a profile stacks, and dsh.bundle points to the bundle's patch file.
The stacking order is fixed: the bundle order listed in the profile, the profile's own cordis.patch.yml, the home-level patch, and finally the --patch override. Any configuration line can be replaced by an upper-layer patch. To see the actual plugin tree launched on your machine, simply run:
dsh --profile web --dump-config
Every line it prints out can be replaced with your own patch.
I looked through apps/cli/src/plugin.ts and found that the dsh plugin command is essentially a 'thin pnpm forwarder.' When you run dsh plugin --profile xxx add some-pkg, it ultimately just runs pnpm in the profile directory to install the package, then uses the exportsPatch() function to read the package's manifest and check if it declares dsh.bundle.patch. If declared, it's automatically added to the layer stack; if not, it's installed as a regular dependency, with a polite warning. This 'plugin management = package management' design is very friendly to frontend developers because you're using the familiar pnpm.
Time for some criticism
After all this praise, as is my habit, I must clearly state the boundaries; otherwise, this article becomes an advertorial.
Actually, all the problems root back to one thing: this repository was only made public on August 13th, and the version number is 0.1.0-rc.5. The first paragraph of the README states its stance clearly: 'developer preview,' 'THERE WILL BE COMPATIBILITY-BREAKING CHANGES.' The 30,000+ stars are almost entirely traffic brought by the DeepSeek brand and have nothing to do with the project's maturity. I checked the issues section; open issues are zero, not because there are no bugs, but because third parties haven't had time to step on them yet. If you really want to run this in production, be mentally prepared for interfaces to be broken at any time.
The most direct manifestation of this 'rawness' is that it has almost no guardrails for newcomers. I downloaded the README and counted: 1,711 characters, and the entire text assumes you already know what an agent harness is and what Cordis is. There's not a single sentence explaining what this product can do, no screenshots, no use cases. For a 30,000-star project, a newcomer clicking in will likely be baffled by the first screen. This 'written for those in the know' documentation style is, honestly, a demerit for an open-source project trying to build an ecosystem.
The barrier to entry also needs to be stated upfront. The base is TypeScript + pnpm monorepo; the root package.json is pinned to [email protected], Node requires ^22.19.0 || >=24.0.0, and plugin management relies entirely on pnpm. If your team doesn't have a few people proficient in frontend, the onboarding cost will be an order of magnitude higher than using an out-of-the-box product like Claude Code.
That said, what's more fatal than the barrier to entry is the ecosystem. Under the dsh-plugin topic, there are still hardly any decent third-party plugins. The value of an 'all-plugin' architecture is tightly bound to the scale of its plugins. Right now, no matter how elegant the architecture, without plugins to swap, the replaceability remains theoretical. This is a classic chicken-and-egg problem; it depends on whether DeepSeek is willing to continuously invest manpower to nurture the community going forward.
Who should take this bet?
After dissecting it, my judgment is this.
dsh's true contribution isn't yet another coding agent, but a methodology that can be named. I'm willing to call it the 'Reversible Plugin Spine.' Its core belief is that a system's evolvability depends on how thin its 'least replaceable layer' is. Most agent frameworks weld the loop, session, and tool pipeline into a thick layer; dsh compresses the thickness of this layer down to 'just an interface contract,' with the rest being reversible plugins.
This approach isn't without cost. It trades for extreme replaceability at the price of a steep learning curve, the cognitive load brought by abstraction, and an early-stage status still in rc, where interfaces may change at any time.
So my advice on selection is very clear. If you are building agent infrastructure and want to create an internal platform you can fully control, swapping models and sandboxes at any time, dsh is worth a careful read of its architecture documentation and the Cordis tutorial. This 'reversible effects + capability seams' design can be directly ported into your own system. If you just want to find a good coding assistant for daily code writing, at this current stage, honestly stick with mature products like Claude Code or Codex CLI; it's more efficient. Don't bet your team's efficiency on a beautiful architecture.
Ultimately, with this open-source release from DeepSeek, the star count will fall back down, interfaces will change, and third-party plugins haven't grown out yet. But the direction it's betting on—'even the loop is a plugin'—I think is correct. The agent form is still rapidly morphing. What seems like a natural core loop today might be a bottleneck tomorrow. Making the spine thin and turning changes into reversible effects is the most honest engineering posture for facing uncertainty.
Cordis growing all the way from the chatbot ecosystem to the agent ecosystem is also quite fun in itself. It shows that the set of problems around 'plugin hot-swapping' and 'runtime composition' are相通 across domains. Who says building agents means only staring at solutions within the agent circle?