DeepSeek Open-Sourced an Agent Framework That Turns Everything Into a Plugin
What makes DeepSeek Harness special is that it turns the model adapter, the tools, and the session log — even the core loop — into plugins. This piece starts with what it is, then takes apart how it is assembled, its core loop, and its event log, to explain exactly what problem "everything is a plugin" solves.
Getting it running takes just one command.
npx @deepseek-ai/dsh web
It spins up a web interface on your machine, by default at 127.0.0.1:3080. You enter a DeepSeek API key in the settings, pick a project directory, and then you can have it read code, edit files, and run commands. It feels much like any other AI coding assistant.
DeepSeek Harness, whose command-line tool is called dsh, is an agent framework open-sourced by DeepSeek. It keeps hammering on one line: everything is a plugin. The model adapter is a plugin, the tools are plugins, the session log is a plugin, and even the core loop that decides what to do next is a plugin.
What It Is
Let’s first pin down the word harness. A large model only spits out text; it can’t touch your computer. The program that reads files, edits code, and runs commands is the layer around it. That layer is called a harness. opencode and Claude Code belong to this category, and so does dsh.
The biggest difference from a web-based chatbot is right here: it can directly work on the projects on your computer. It’s assembled from a few pieces.
flowchart TD
U["You<br/>make requests in a web page or terminal"] --> UI["Interface layer<br/>web / command line / SDK / desktop"]
UI --> Core["Core loop<br/>decides what to do next"]
Core --> Model["Model<br/>only offers ideas"]
Core --> Tools["Tools<br/>read and write files, run commands, search the web"]
Tools --> Work["Your project"]
Core --> Log["Session log<br/>records every step"]
The project is currently in developer preview, and the team openly says there will be breaking changes. If you want to try it, install and run it with the command above, but don’t rush it into production.
What It Can Do
The web page isn’t the only entry point. With the same dsh, switching to a different profile gives you a different way to run it.
dsh webstarts the web interface, good for using it while sitting at your computer.dsh --profile headless "summarize this repository"runs one task, exits, and prints the result, good for dropping into scripts.dsh --profile sdkstarts a JSON-RPC service so other programs can call it through a TypeScript or Python SDK.dsh --profile acptalks to automation tools over another protocol.- There’s also an Electron desktop version.
The model has a set of tools at hand. It can read, write, and edit files, search by file name or content, run bash commands (or pwsh on Windows), push long-running commands to the background, and search and fetch web pages. Going further out, it can connect to MCP servers to attach external tools, open a persistent terminal that spans multiple calls, ask a language server for definitions and references, load skill manuals, and keep a to-do list.
When a task gets too heavy, it can hand work off to a subagent. A subagent has its own separate session and log, and the main agent can send it messages or interrupt it midway. The project also ships a subagent provider that can hand a whole turn straight to Codex or Claude Code to run; it’s off by default.
Permissions are another gate. Every action falls into one of three sandbox levels — read-only, workspace-write, danger-full-access — paired with an approval policy of ask or never. The default is workspace-write plus ask: when the model wants to write a file outside the workspace or run a dangerous command, the interface pops up a dialog to ask you first.
Sessions are persistent. Every step is written to disk, they survive a restart, and you can fork a new session off from a particular message.
How It Works
The Plugin Framework, Cordis
Underneath dsh is a plugin framework called Cordis. Five ideas cover it.
- A plugin is a piece of code that implements a service, either an object with an
applyfunction or aServicesubclass. - The context
ctxis the container that holds services. A service occupies a stable key such asctx.tools,ctx.llm, orctx.sessions, and other plugins look it up by key instead of importing a concrete implementation. - A plugin declares which services it needs with
inject, and it waits for them to be ready before starting, so load order never has to be wired by hand. - Plugins talk through typed events, dispatched in five modes.
emitonly notifies and returns nothing;waterfallwraps the downstream listeners and can rewrite the result;parallelruns them concurrently;serialruns them in order and awaits them;bailstops at the first listener that returns a bail value. - Registrations are reversible effects. Prompt sections, tool definitions, and listeners are installed through
ctx.effect()orctx.on()and unwind automatically when the plugin unloads.
Looking services up by key, and registrations that undo themselves, are the two things that let it turn everything into a plugin.
Startup Is Stacked Up Layer by Layer
When dsh starts, it stacks plugins in order on top of an empty list. The order goes like this.
flowchart TD
E["Empty plugin list"] --> B1["bundle dsh-base<br/>model adapter, tools, storage, permissions"]
B1 --> B2["bundle dsh-web-app<br/>adds the browser interface"]
B2 --> P1["the profile's cordis.patch.yml"]
P1 --> P2["cordis.patch.yml in the user's home"]
P2 --> P3["command-line --patch overrides"]
P3 --> R["the plugin tree that finally runs"]
A profile is a startup recipe that says which plugins to stack this time, kept in your dsh directory. The distribution ships a few ready-made ones for different uses, web gives you the browser UI, headless runs one task with no UI, and sdk and acp let other programs call it. A bundle is a packaged set of plugin config plus mount code, and dsh-base is the first layer shared by all of these.
A patch is a list of edits. It finds an existing entry by id and replaces that entry’s whole config, or inserts a new entry. The diagram shows two patch layers. The profile’s patch applies only to that one profile, at ~/.dsh/profiles/<name>/cordis.patch.yml. The home-level patch applies to every profile, at ~/.dsh/cordis.patch.yml, and it is applied later, so it wins when both touch the same entry.
Run dsh --profile web --dump-config and it prints the final tree. Every entry it prints can be swapped out by your own patch.
How a Single Request Plays Out
Suppose you say, add remember-password to login.
sequenceDiagram
participant You as You
participant Core as Core Loop
participant Log as Session Log
participant LLM as Model
participant Tool as Tools
You->>Core: Add remember-password to login
Core->>Log: Record turn/start and your message
Core->>Log: Derive history messages from the log
Core->>LLM: System prompt + history + tool definitions
LLM-->>Core: I want to edit auth.ts
Core->>Log: Record tool/call
Core->>Tool: Run edit
Tool-->>Core: Done
Core->>Log: Record tool/result
Core->>LLM: Here's the result, continue
LLM-->>Core: All done
Core->>Log: Record turn/end
Core-->>You: Stream the process to you
Let’s break it down.
- Your message lands in the inbox first, queued in two columns: one waiting for its own turn, one waiting for the next step boundary.
- The loop wakes up, claims this batch of input, and opens a turn.
- It assembles this request, stitching together the system prompt, the definitions of the relevant tools, and the history messages derived from the session log.
- It calls the model and receives the reply as a stream.
- The model says it wants to use the
edittool to change a few lines. The loop first records this call in the log, runs it through the permission pipeline, executes it, and then feeds the result back. - The loop takes the result and asks the model for the next step. The model can keep calling tools, or say it’s done.
- With no outstanding work, the turn ends.
The division of labor here is worth noting. One model request plus the tools it calls is a step. A turn contains zero or more steps. In the log, events like turn/start, step/start, tool/call, tool/result, and turn/end string the whole path together.
One word keeps coming up in this loop: log. The loop doesn’t store the conversation in an array; it appends every step to an append-only session log. The history the model sees is derived from the log, and the project states this rule bluntly: anything the model can see must be reconstructable from the log.
Swap One Piece, Leave the Rest Alone
dsh splits a capability into three roles, which the project calls a capability seam. One role defines the interface, called the Service Definition. One implements it, called the Service Provider. The last is the code that uses it, called the Consumer.
Take running commands as an example.
flowchart LR
Tool["bash tool<br/>Consumer"] --> Def["ctx.shell<br/>Service Definition"]
Def --> Local["dsh-bash-local<br/>local execution"]
Def --> Sandbox["dsh-bash-sandbox<br/>sandbox execution"]
dsh-shell defines ctx.shell; dsh-bash-local and dsh-bash-sandbox are the two providers, and the bash tool the model sees is the consumer. To switch command execution from local to sandbox, you only swap the provider — the tool’s schema and prompt don’t change at all. Filesystem, process, terminal, and language server are split apart the same way, so pointing them all at a remote sandbox moves Bash, the terminal, and LSP over together.
Why Do It This Way
There’s No Core You Have to Modify
With many frameworks, adding behavior means changing the core. dsh does the opposite: new behavior hangs off extension points that already exist. The more important move is in the implementation — registration itself is a side effect, so when a plugin unloads, everything it registered is automatically undone. That’s why plugins can hot-reload without load order or leftover state from unloads throwing things into disarray.
Even the core loop is replaceable. Extension plugins depend on the interface, not on that particular loop implementation, so to swap in a different loop you just replace the loop plugin. The project even has an experimental capability that lets the model define and run new Cordis plugins, adding features to itself at runtime.
This plugin structure also produces a handy side benefit. Two sets of plugins in dsh specifically read the hooks config from Claude Code and Codex and run them on the same engine. It borrows other people’s existing hooks and uses them directly.
The Log Is the State
The session log is append-only, and the model’s history is derived from it. Together these two things solve an old problem. If the conversation lives in a mutable array and you keep a separate log on the side, the two will eventually drift apart. Here the log is the state, so a mismatch can’t happen by construction.
Replay, tracing, forking, and telemetry all grow from the same log, with nothing extra to wire up. The log keeps getting longer, so there’s compaction: old conversation is condensed into a summary, and the raw records that got folded away stay in the log — they just stop being sent to the model one by one.
Storage is where it parts ways with some other tools. opencode keeps the log and the derived history together in one local SQLite file, which you can query directly. dsh writes each session’s log as an append-only JSONL file, recomputes the history from that file, and leaves SQLite for auxiliary jobs like session search and credentials. A committed log file is never renamed or overwritten; a format upgrade emits a new versioned file alongside it, so the logs can migrate on their own. The trade is that cross-session queries need a separate index, which it fills in with SQLite full-text search.
The Costs Are Right There Too
There are many abstraction layers. If you want to add something to dsh, the first thing is to figure out which extension point it should hang off, and the learning curve is much steeper than writing a small tool. Plugins talk to each other through service names and events, so tracing one call path means hopping across several packages. The project is marked developer preview, and its interfaces can still change at any time.
This design moves complexity from the framework authors’ hands into the plugin authors’ hands. If you just install and use it, you never touch that complexity. If you want to add to it, or run the same thing in a completely different environment, the freedom it buys is what makes that price worth paying.
Comments