DeepSeek Harness (dsh)
An open-source agent harness from DeepSeek AI, MIT-licensed, TypeScript, published 2026-08-13 — the repository was created that morning and had 37,128 stars and 2,869 forks by that evening. It ships as a developer preview whose README says, in capitals, that there will be compatibility-breaking changes.
Read it as a primary source about architecture, not about results. It is a day old, it publishes
no evaluation, and the star count measures attention rather than quality — a BENCHMARK.md exists and
its entire content is an instruction to install the Python SDK and run the jsonrpc-agent variant.
The claim: everything is a plugin
The design rests on Cordis, and the architecture doc states the consequence plainly:
"Every part of the product is a plugin, including the model adapter, the tool registry, the session
log, and the agent loop itself, so every part is replaceable from configuration." 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.
This is the strongest form of the agent-middleware idea the spoke has seen stated. LangChain's
middleware hooks a loop that remains LangChain's; here the loop is core/agent-loop, one row in a
tree, and swapping it is a config edit rather than a fork.
How a running instance is composed
A running dsh is a plugin tree assembled at boot from ordered layers:
- A profile is a named composition stored in the Harness home — the bundles it stacks, any
out-of-tree plugins, and the user's
cordis.patch.yml.webandheadlessship as templates. - A bundle distributes Cordis config rows plus the code they mount, and stays patchable by the layers above it.
Layers apply in order: each bundle in the profile's order, then the profile's patch, then the
home-level patch, then any --patch overlay. A patch targets a row by id and replaces its config
outright, or inserts new rows. dsh --profile web --dump-config prints the tree the machine actually
boots, and every row it prints can be replaced.
Three bundles are named: dsh-base (model adapters, tools, persistence, sandbox and approval policy,
settings, credentials, telemetry) is the first layer of every profile; dsh-web-app adds the browser
application; dsh-headless adds a one-shot runner with no server at all.
The loop, stated as a protocol
The turn flow is written out as pseudocode. No other harness here documents its loop at this resolution, so the shape is quoted rather than paraphrased. A step is one model request plus the tools it calls; a turn is zero or more steps, opening before its first input is claimed and closing once nothing is owed:
turn/start
claim next-step input plus one queued message
assemble prompt sections + tool schemas
-> agent/pre-step reject | enter(messages)
step/start
append entered messages as user/message
derive model history from the log
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
turn/end
Extension happens through events in three domains, and picking the domain is the first decision in
most changes: session events are durable facts appended to the log (use one when the fact must
survive a reload), agent events carry a live agent for observing or intercepting work in flight,
and capability events attach policy and adapters to a seam (fs/*, tools/*, telemetry/*)
without importing the loop. agent/pre-step, agent/request, llm/stream and the three tools/*
events are waterfalls whose listeners must call next() to delegate.
agent/pre-step is where a listener decides what the model sees — it may rewrite the claimed messages
or reject them outright, and a rejected first claim still closes a durable turn that spent no step, so
the log records the attempt.
Model-visible means logged
The session log is the source of the context the model sees; deriveMessages() projects model history
from it, and raw assistant/chunk events are kept for replay and UI fidelity. The rule is enforced,
not merely advised: "Anything that reaches a model request must be reconstructable from the log, and a
runtime invariant asserts it." Adding a new model-visible input therefore requires a new session
event type.
That invariant is a direct answer to the failure model-or-harness-taxonomy uses as its worked
example — an agent ignoring an earlier instruction, where you cannot tell whether compaction dropped it
or the model disregarded it. If every model-visible byte is reconstructable from an append-only log,
that ambiguity is decidable after the fact. Nobody at DeepSeek claims this motivation, and the harness
still has a compaction package that can drop things; what the invariant buys is that the dropping is
recorded.
Capability seams
A seam is a swappable capability with three roles — a Service Definition declaring the interface, a Service Provider implementing it, and a Consumer using it (commonly a model-facing tool). One role alone is not a seam, and adding a capability means designing all three.
The payoff claimed: filesystem and subprocess providers share one execution world, so pointing them at a remote sandbox moves Bash, PTY and LSP with them, with no provider forks. Subagent providers vary behind one interface from a fresh child agent to a delegated turn in another product.
What is in the box
Roughly fifty packages, and the list is a fair census of what a 2026 harness is expected to have:
mcp (a client bridge registering external MCP server tools on
ctx.tools), acp, lsp, sandbox and e2b, subagent, skill, compaction, plan, goal,
guard, todo, workflow, jobs, schedule, credentials, terminal, shell, workspace,
spill. Apps are a cli and a web client; there is a Python SDK alongside the TypeScript core.
The skill package owns a ctx.skills registry that is deliberately backend-agnostic — it "does not
know whether skills come from local files, embedded plugin data, HTTP, or another backend" — with the
filesystem provider shipped separately. Registrations are scope-layered: host rows land in the global
layer, a plugin mounted by an agent preset lands in that preset's layer, and a read merges the global
layer with the viewing scope's chain, nearest layer winning a duplicate name. That is
agent-scope-isolation implemented at the registry level rather than bolted on.
Not locked to DeepSeek's own models
Two LLM adapters ship against the same seam. llm-deepseek owns the deepseek-official route with a
hand-rolled fetch + SSE implementation of DeepSeek's wire format. llm-pi-ai is a generic
multi-provider adapter backed by @earendil-works/pi-ai, where a route naming an installed provider
inherits its endpoint, protocol and catalog, and a route pi-ai doesn't ship is declared outright — so
"an OpenAI-compatible gateway, a self-hosted server, or a provider newer than the installed catalog is
configuration rather than a code change." The two routes are named distinctly on purpose so one
composition can mount both DeepSeek paths side by side.
Credentials are referenced by env-var name rather than embedded, and a reference that resolves to
nothing fails the request with MISSING_CREDENTIAL instead of falling through to whatever unrelated
key the environment holds.
Built to be read by agents
The repo carries AGENTS.md (agents-md) and CLAUDE.md (claude-md) at the root and inside
package directories, and the architecture doc opens by recommending you "use an agent to explore the
codebase and understand its architecture." Documentation is fully bilingual — every README has a
.zh.md twin driven by an i18n YAML file.
What to watch
The developer-preview warning is the load-bearing caveat: an architecture whose whole promise is
stable seams is shipping while the seams are still moving. The claim to test later is whether
"replaceable from configuration" survives contact with third-party plugins — the dsh-plugin GitHub
topic is the place that will show it.
Related
cordis · agentic-coding-harness · agent-framework · agent-middleware · model-or-harness-taxonomy · agent-scope-isolation · deepseek · claude-code · oh-my-pi · agents-md