DeepSeek released deepseek-ai/deepseek-harness on 2026-08-13. As of today, 2026-09-08, the latest tag is dsh-v0.1.5-alpha.1 — the eighth pre-1.0 release in eighteen days. The repo carries 216k stars and 25.5k forks. The README is short. The architecture document is not. What makes this release worth reading for an engineer is the part the headline skips: the harness is not a wrapper around an LLM. It is a plugin tree on top of a vendored framework called Cordis, and the entire agent loop is composed from events whose dispatch modes — emit, waterfall, parallel, serial, bail — are part of the event’s public contract.
That’s a different shape from every other open agent harness I’ve read this month.
The bet, in one sentence
Replace the privileged core with a Cordis context where every capability is a plugin — model adapter, tool registry, session log, agent loop — so each is replaceable from configuration without forking the project.
The README states it plainly: “There is no privileged core to patch: you extend dsh by mounting a plugin beside the others, and registrations are effects that unwind when their plugin unloads.” That sentence is doing more work than it looks like. It says the architecture.md’s plugin tree is not a metaphor. It is the runtime.
What’s actually hard about an everything-is-a-plugin runtime
Three things show up when you try to build one:
- Composition has to be deterministic. If two plugins both try to claim
ctx.fs, the framework needs a hard rule about who wins. Cordis’s answer is “the last write wins per row” —cordis.patch.ymlpatches replace a row’s wholeconfig, not fields within it. That forces plugin authors to restate every setting they want to keep. It’s strict on purpose. - Reversibility has to be honest. If a plugin adds a tool schema, the schema must disappear when the plugin unloads. Cordis exposes this as
ctx.effect()andctx.on()— every registration returns a disposer, and teardown walks the disposer chain. The agent loop relies on this: it addsagent/*listeners that must unwind cleanly when a subagent unloads, and a leak here would corrupt the session log. - Plugin order must be expressible without imports. Cordis solves this through
inject: a plugin that names required services waits until those services exist. Load order is expressed through service requirements rather than manual boot sequencing. The dsh-base bundle leans on this heavily —dsh-web-app,dsh-headless, anddsh-sdk-appall depend ondsh-base, and they mount on top of it in declaration order.
The architecture document calls these “three disciplines.” They’re really the cost of a plugin tree. You pay it once at the framework level (Cordis) and then never again at the application level (dsh).
The mechanism, in one quote
The full agent loop fits in this pseudocode excerpted from architecture.md:
turn/start
claim next-step input plus one queued message
assemble prompt sections + tool schemas; project runtime context
-> agent/pre-step reject | enter(messages, startsRequestSeries?)
step/start
agent/request -> prepareCall (cancellation commits neither system nor users)
reconcile system/message using the prepared call capability
append entered messages as user/message; log request/header and request/context as needed
derive and freeze model history from the log
stream the bound prepared call -> llm/stream -> agent/assistant-stream start
agent/assistant-stream chunk*
assistant/message | assistant/attempt -> agent/assistant-stream end
tool/call* -> tools/pre-execute -> tools/execute -> tools/post-execute -> tool/result*
step/end
-> agent/turn-stopping
turn/end
What that sequence encodes: the loop never reads from any in-memory state to assemble a prompt. It reads from the session log. The log is the source of the context the model sees, and a runtime invariant asserts it: “Anything that reaches a model request must be reconstructable from the log, and a runtime invariant asserts it. This is why a new model-visible input requires a new session event: extend SessionEventMap and render from the log.”
That’s the line that distinguishes dsh from agent harnesses that treat the model as a callable function. dsh treats the model as a projection over the log. Same idea in different words.
The waterfall semantics, in one paragraph
Cordis events come in five dispatch modes. The relevant one for the agent loop is waterfall: around-middleware where a listener receives (...args, next), calls next() to delegate, and short-circuits by returning without calling next(). Three of the agent loop’s events are waterfalls (agent/pre-step, agent/request, llm/stream) and three are the tools/* lifecycles. The architecture document spells it out: “the four are waterfalls, whose listeners must call next() to delegate; agent/turn-stopping is serial and has no next().”
Why this matters: cooperative listeners can mutate a shared request or decision object and then delegate. A policy listener can return without next() when it owns the decision, while a listener that only annotates or observes must delegate. There’s a discipline here that you don’t get with simple observer patterns — short-circuiting is the design, and the framework treats it as such. If you’ve ever tried to bolt policy onto a callback-based agent loop, you’ve written the same dance by hand. Cordis names it.
The bail mode is the cousin: listeners observe in registration order until one returns a bail value. Useful for “first match wins” listeners — a routing layer where you want the first matching rule to take the decision and stop the cascade.
What the numbers actually show
The release cadence is the headline number. From the GitHub Releases API:
dsh-v0.1.1-rc.2— 2026-08-21dsh-v0.1.2-alpha.1— 2026-08-27dsh-v0.1.2-alpha.2throughalpha.5— 2026-08-30 to 2026-09-02 (one per day)dsh-v0.1.2-rc.1— 2026-09-03dsh-v0.1.3-alpha.1— 2026-09-04dsh-v0.1.3-alpha.2— 2026-09-07dsh-v0.1.5-alpha.1— today, 2026-09-08
That’s eight tags in eighteen days, with one day skipped (Aug 22–26). The skip correlates with the Aug 17–22 blog gap I caught up on Aug 31; DeepSeek’s release lag around that window is probably coincidence, but the rhythm before and after is roughly one tag per 1.7 days of calendar time. For a “developer preview” project that warns “THERE WILL BE COMPATIBILITY-BREAKING CHANGES” in the README, this is a fast iteration curve.
The other headline is the architecture split: TypeScript Host and Client aggregates that cannot be flattened into one program. Two tsconfig.json files exist at the root — tsconfig.host.json and tsconfig.client.json — and the architecture document explains why: “Host packages in tsconfig.host.json and Client packages in tsconfig.client.json; three packages (host/webserver, compaction/compaction, typert/registry) are referenced by both aggregates as shared leaves.” Both sides declaration-merge the Cordis Context interface under the same keys with different services, and “one program seeing both merges reports a collision.”
A more typical monorepo would let you build the whole thing with tsc -b. DeepSeek’s repo can’t, because the same plugin tree has a Node loader entry and a browser entry, and the type system has to know which one it’s looking at. The cost: two build steps, two tsdown invocations, two paths facades. The benefit: the same plugin works in the Electron desktop app, the Web UI, and the SDK headless server.
Six packages split Host and Client tsconfigs: api/remotes, api/gateway, api/session-controller, api/workspace-controller, client/connection, and session-query/session-log-export. That list is itself a tell — five of the six are server-side packages that also need browser entries. The session log export is the exception: it ships a Node archive producer that the browser controller imports but doesn’t build itself.
Trade-offs and what it doesn’t fix
Three honest limits, each ending with the engine that wins that cell:
1. The vendored Cordis is the lock-in. Cordis lives under vendor/ and the architecture document tells plugin authors to “follow the generated service/event reference on the subsystem pages” rather than to import Cordis as an external dependency. If you’ve already built on Koa, Express middleware, or LangChain callbacks, the waterfall semantics feel natural. If you’ve built on observer-only event emitters, the short-circuit contract is a new thing to internalize. dsh wins when you’re starting fresh; Cordis loses when you have to port an existing codebase into a plugin tree.
2. The OTel default is FEEDBACK_ONLY, not DISABLED. The dsh-base README is explicit: “OTel session upload defaults to FEEDBACK_ONLY for all users, including deepseek-official: new text feedback, message ratings, edits, and withdrawals release the complete canonical prefix through that event, including context.” This is not zero telemetry. It’s opt-out telemetry that lights up the moment a user submits a feedback event. If you ship dsh to enterprise customers under a “no data leaves our network” contract, you need to set DISABLED explicitly. The default is a reasonable default for a frontier lab’s own hosted deployment and a reasonable default to second-guess for everyone else’s. The same applies to the opt-in deepseek-session-log-contributor, which “remains a separate request path” — separate, but still on by default for deepseek-official.
3. The sdk-minimal profile deliberately bypasses dsh-base. The shipped web, headless, sdk, and acp profiles all include dsh-base as the first layer. sdk-minimal is the deliberate exception: “one bundle owns its complete explicit SDK tree and does not apply dsh-base.” If you’re integrating dsh as an SDK into a larger product, the minimal profile is the on-ramp, but you also lose the safety defaults — file-write sandbox, tool approval policy, session persistence — that come for free on the base-backed profiles. You have to restate them in your own bundle. The architecture doc calls this out without apology, but it’s still a footgun for first-time SDK integrators who copy the minimal example and ship to production.
The session log is the only thing that matters
There’s a deeper claim hiding in the architecture document, and it’s the one I’d bet on:
Model-visible means logged. Anything that reaches a model request must be reconstructable from the log, and a runtime invariant asserts it.
This is a stronger statement than “the log is durable.” It says the log is authoritative. The session projection layer (dsh-session-projection) reads committed events incrementally, registers typed state for the agent loop’s readers, and exposes stateOf() for host consumers. Three properties follow:
- Fork, resume, transcripts, telemetry, and persistence all derive from these durable settlements.
- Live UI incrementality comes from
agent/assistant-stream, but the durable attempt stream is committed before the live end frame is published. A hard process loss before settlement leaves no durable attempt stream. - Generation paths in the JSONL store are never renamed, replaced, or deleted. The “v0 → v1 → vN” migrations ship in adjacent packages, each owning exactly one step.
The reason this matters: the agent loop can be replaced wholesale (it’s a Cordis plugin), the model adapter can be swapped (it lives on ctx.llm), the tool registry can be replaced (it lives on ctx.tools), the filesystem provider can be pointed at a remote sandbox. None of these changes corrupt the session log, because the log is the only thing the model sees. That separation is what makes “everything is a plugin” possible without the runtime falling apart.
The header-only stat and list operations rescan each session directory, select its numerically highest canonical generation, and translate a supported historical header without loading events or publishing a successor. That sentence is doing quiet work — it means historical sessions can be read by a newer code path without rewriting them, and the storage format can evolve without invalidating old logs. For a project at v0.1.5-alpha that’s the right invariant to publish now, before anyone has shipped production sessions they need to migrate.
How to actually run it
Three commands cover the lifecycle. From the README:
npx @deepseek-ai/dsh web
That’s the entry point. It starts the Web UI at http://127.0.0.1:3080 by default and opens it in the default browser for a local launch. An SSH launch only prints the host URL because the SSH client owns the local forwarded address. Pass --no-open to run the server without opening a browser. From source:
git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install
pnpm run build
pnpm dsh web
The pnpm run build step is mandatory — it prepares the repository artifacts that pnpm dsh web consumes. There is a Node.js 22.19+ and 24+ requirement, pnpm@11.7.0 is pinned through Corepack, Git 2.26+ is required for the worktree-local Lefthook hooks (which the postinstall script configures via scripts/install-lefthook.mjs). On Windows, bash tools are disabled and PowerShell twins activate; the architecture doc warns that “A Windows host that prefers the unconfined PowerShell executor can switch the shell rows in its profile patch — the switch must disable both PowerShell rows and re-enable both bash rows, otherwise the profile fails to load.”
The Docker story lives separately. There’s no Dockerfile at the root that I could find — the shipped launchers assume a Node runtime you control. That’s a deliberate choice for a project that’s still iterating on the plugin surface; containerizing a Cordis plugin tree at v0.1.5-alpha would freeze the bundle composition in ways that don’t help anyone.
Where the open questions sit
A few things I’d want answered before betting production on this:
- The
cordis.patch.ymlauthoring UX. Patches replace a row’s wholeconfig, which means every setting has to be restated on override. There’s nomergestrategy. For a small profile that’s fine. For a custom profile that extendsdsh-baseand then layers two of its own bundles on top, the patch file becomes a YAML mirror of the upstream defaults. Whether the project ships tooling to diff patches against the upstream row set is something I couldn’t confirm from the public docs. - The ACP integration depth. The
dsh-acp-appbundle adds “the automation-only ACP server” — Agent Communication Protocol, the open standard several agent harnesses adopted in 2026. Whether the implementation speaks the full ACP surface or a constrained subset isn’t documented in the architecture overview. The bundle exists; the surface area isn’t enumerated. - The Electron desktop build pipeline. The architecture doc describes a private
dsh-app://protocol, versioned framed byte pipes, Node IPC reserved for lifecycle control, and a reserved$DSH_HOME/profiles/desktopnpm project owned by the desktop app. None of this is published as a separate docs page — it’s a section ofarchitecture.md. For a team evaluating whether to ship the desktop app to non-technical users, the deployment story would benefit from its own page. - The Cordis upstream relationship. Cordis is vendored under
vendor/and the sync procedure lives invendor/README.md. Whether DeepSeek plans to maintain Cordis as an external open-source project (so non-dsh plugin authors can build on the same framework) or keep it permanently vendored (so dsh stays the only consumer) is the kind of strategic question a release cadence like this one makes urgent.
The last tag shipped at 16:16:04 UTC today. The next one will probably ship within two days. Whether the project reaches dsh-v1.0.0 before the breaking-changes warning in the README gets stale is the bet.
References and where to dig further
- deepseek-ai/deepseek-harness README — entry point and install commands
- docs/architecture.md — the plugin tree, profile/bundle composition, agent loop pseudocode, capability seams
- docs/cordis-primer.md — the five core ideas and dispatch modes
- A Programming Paradigm for Spatiotemporal Composability — the Cordis paper linked from the README
- packages/bundle/base/README.md — what
dsh-baseactually provides and the OTelFEEDBACK_ONLYdefault - docs/development.md — Node.js 22.19+, pnpm@11.7.0, the Host/Client aggregate split
- cordiverse/cordis — the vendored framework’s public home, if you’re reading the plugin tree without the dsh wrapper
- Releases page — the alpha cadence from
v0.1.1-rc.2(Aug 21) tov0.1.5-alpha.1(today)
Comments
Powered by GitHub Discussions via Giscus. Sign in with GitHub to leave a comment.