Skip to content

Latest commit

 

History

History
129 lines (88 loc) · 8.94 KB

File metadata and controls

129 lines (88 loc) · 8.94 KB

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 and headless 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 at all.

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.

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.

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

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 live Agent: 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.

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
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 Chat nodes.