11 KiB
DeepSeek Harness Architecture
English | 中文
Read this before changing anything under packages/. It assumes you know Cordis; if you do not, start with the primer or the tutorial.
We recommend using an agent to explore the codebase and understand its architecture.
Cordis
Cordis is the framework under dsh: plugins contribute services, typed events, and reversible effects to a shared context. 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.
Profiles and bundles
A running dsh is a plugin tree composed at boot from ordered layers.
A profile is a named composition stored in the Harness home. It lists the bundles it stacks, holds any out-of-tree plugins it installs, and keeps the user's own cordis.patch.yml. web, headless, sdk, and acp ship as templates.
A bundle is a distribution format for Cordis config rows and the code they mount, so whatever it inserts stays patchable by the layers above it.
Each declares itself in its own package.json under a dsh field: dsh.profile lists a profile's bundles, and dsh.bundle points at a bundle's patch file.
dsh-base is the first layer of every profile: model adapters, tools, persistence, sandbox and approval policy, settings, credentials, telemetry. dsh-web-app adds the browser application, dsh-headless adds a one-shot runner with no server, dsh-sdk-app adds the SDK JSON-RPC server, and dsh-acp-app adds the automation-only ACP server.
Layers apply to an empty entry list in this order: each bundle in the profile's listed order, then the profile's cordis.patch.yml, then the home-level one, then any --patch overlay. A patch targets a row by id and replaces its whole config, or inserts new rows.
Custom profiles default to live patch reload. The shipped web profile is live; headless, sdk, and acp apply all layers once at startup because replacing a one-shot or stdio application's dependencies after it owns work would invalidate that lifecycle.
To see the tree your machine actually boots:
dsh --profile web --dump-config
Any row it prints can be replaced by a patch of your own.
Composition mechanics are in app-boot; config fields are in the generated config catalog.
Application launch
Every supported Node application starts at the dsh CLI with a named profile. The shipped applications are dsh web (the deliberate alias for --profile web), dsh --profile headless, dsh --profile sdk, and dsh --profile acp. The TypeScript SDK resolves its same-version dsh dependency and selects sdk; custom plugin composition remains a profile plus ordered patch files, not another executable or inline application tree.
Vendored CLIs, build-only and test-only executables, direct in-process plugin mounting, and the private browser WebWorker preview are not Harness application launchers. verify-application-entrypoints keeps every package bin, executable source, and root demo in an explicit class and rejects a Node application path that bypasses dsh.
The packaged Python SDK runtime is the sole temporary application exception. Its private dsh-sdk-python-runtime carrier and dsh-sdk-python-runtime-closure deploy manifest preserve the current Python API, wire, default cordis.yml, environment variables, wheel names, dsh-jsonrpc-agent-pkg-<platform>-<arch> executables, sidecars, and platform set. A later Python migration will launch dsh --profile sdk, delete the private direct-config carrier, and then rename that executable family to deepseek-harness-sdk-runtime-<platform>-<arch>.
Core packages
Here are some core packages that contribute to the Cordis tree.
| Package | Owns | ctx key |
|---|---|---|
core/session |
The append-only SessionEvent log and in-memory store |
ctx.sessions |
core/system-prompt |
Prompt-section and tool-schema assembly | ctx.systemPrompt |
core/tools |
The scoped tool registry and guarded execution pipeline | ctx.tools |
core/agent |
The Agent interface, live registry, and agent/* events |
ctx.agents |
core/agent-loop |
The default driver implementing that interface | ctx.agentLoop |
core/scope |
The per-agent scoped-registration primitive | library, no key |
llm/llm |
Message and stream vocabulary plus the adapter seam | ctx.llm |
webhook/webhook |
Authenticated-delivery dispatch and Workspace Session creation | ctx.webhookRuntime |
Events
Events are the extension points, and picking the right domain is the first decision in most changes.
- Session events are durable facts appended to the log and broadcast through
session/event. Use one when the fact must survive a reload. - Agent events (
agent/*) carry a liveAgent: inbox, step, status, request, validation, continuation. Use one to observe or intercept work in flight. - Capability events attach policy and adapters to a seam (
fs/*,tools/*,telemetry/*) without importing the loop.
The event map lists every event's producers and consumers.
Turn flow
A step is one model request plus the tools it calls. A turn is zero or more steps: it opens before its first input is claimed and closes 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)
reject, or a first enter rewritten empty -> close the turn with no step
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
tools owe another request, or next-step input arrived -> claim -> next step
-> agent/turn-stopping
turn/end
turn/*, step/*, user/message, assistant/*, and tool/* are durable session events; the rest are live extension points across three domains. agent/pre-step, agent/request, llm/stream, and the three tools/* events are waterfalls, whose listeners must call next() to delegate; agent/turn-stopping is serial and has no next().
Input reaches the driver through one inbox. Some messages wake it immediately; injected context waits in the inbox until another message does.
agent/pre-step decides what the model sees. Listeners may rewrite the claimed messages or reject them outright; a rejected or empty first claim still closes a durable turn that spent no step, so the log records the attempt. Each step reads the prompt sections and tool schemas that plugins registered.
Details: the sequence diagram, the tool pipeline, and cancellation and error recovery.
Session log
The session log is the source of the context the model sees. deriveMessages() projects model history from it, and raw assistant/chunk events preserve replay and UI fidelity. Fork, resume, transcripts, telemetry, and persistence all derive from this stream.
Model-visible means logged. 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.
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. A package may combine roles, but one role alone is not a seam; adding a capability means designing all three (capability graph).
Seams are why one provider swap changes the whole product. 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 just as widely behind one interface, from a fresh child agent to a delegated turn in another product.
Experimental Agent Teams is a private opt-in coordination seam on ctx.agentTeams, with a durable roster, task board, and mailbox layered over continuable subagents.
Where new behavior goes
New behavior attaches to a documented extension point. Changing the loop itself updates this map.
| Goal | Mechanism |
|---|---|
| Add a model provider | register its adapter on ctx.llm |
| Add a model-facing capability | register on ctx.tools; its schema joins prompt assembly |
| Give one session a different capability set | compose an agent preset; a service row there needs an isolate realm |
| Add shell execution | register a ctx.shell backend; the local one spawns through ctx.subprocess |
| Add persistent terminal execution | register a ctx.terminals backend plus dsh-tool-terminal |
| Add a human command | register on ctx.commands; it dispatches without a model turn |
| Add background work | register on ctx.jobs; job_* tools collect or stop it |
| Start a Session from an external webhook | register a trusted rule on ctx.webhookRuntime and mount a provider adapter |
| Add filesystem access or policy | register a ctx.fs provider or listen to fs/* events |
| Confine spawned processes | use a ctx.sandbox backend; consumers wrap argv before spawning |
| Intercept a request, tool, or turn | use its agent/* or tools/* event; agent/turn-stopping stops a turn |
| Add model-facing context | call agent.inject(); it lands in the next admitted request |
| Add UI or editor integration | drive ctx.agents and render from session/event |
| Add a Web Client Chat node | register a ConversationNodeDefinition + keyed renderer |
| Add durable session state | extend SessionEventMap; render and replay from the log |
| Generate session titles | register the sole ctx.sessionTitle provider |
| Manage a same-session objective | use ctx.goals; continue through agent/* |
| Fork a live session | ctx.sessions.fork(source, boundary?, childSessionId?) |
| Scope a registration to one agent | use that agent's agent.ctx |
The extension cookbook maps features to capabilities and indexes the step-by-step guides for packages, tools, LLM adapters, and settings cards. The Conversation subsystem owns Chat-node assembly.