19 KiB
DeepSeek Harness Architecture
English | 中文
DeepSeek Harness SDK uses Cordis: everything is a plugin, including the loop.
Overview
Harnesses are Cordis contexts; packages contribute services, typed events, and disposable registrations. packages/core/ groups the default flow; capabilities remain plugins.
Default Services
| ctx key | Package | Role |
|---|---|---|
| — | dsh-scope |
scoped-context registrations and shared layer storage (library) |
ctx.sessions |
dsh-session |
in-memory event-sourced sessions |
ctx.systemPrompt |
dsh-system-prompt |
ordered prompt sections, tool schemas, and variables |
ctx.tools |
dsh-tools |
tool registry and execution pipeline |
ctx.agents |
dsh-agent |
live agents, delegated creation, agent/* events, process-local initiator scope |
ctx.agentDefaultModel |
dsh-agent-default-model |
Settings-backed model selection shared by Agent front doors |
ctx.agentLoop |
dsh-agent-loop |
concrete Agent driver |
Capability Services
| ctx key | Package family | Role |
|---|---|---|
ctx.llm |
llm/ |
adapter registry, streaming model calls |
ctx.tokenMeter |
llm/token-meter |
replay-aware request and surface pressure |
ctx.bash |
bash/ |
foreground/background command execution |
ctx.subprocess |
subprocess/ |
executable lookup, managed trees, terminals |
ctx.pty |
pty/ |
owner-scoped persistent terminal sessions |
ctx.sandbox |
sandbox/ |
same-world process confinement through argv wrapping and per-call policy |
ctx.sandboxPolicy |
sandbox/ |
shared sandbox policy home |
ctx.codeRuntime |
code-runtime/ |
model-written program execution |
ctx.fs |
fs/ |
execution-world paths, bounded IO, and policy events |
ctx.lsp |
lsp/ |
semantic navigation registry |
ctx.skills |
skill/ |
skill provider registry, progressive disclosure |
ctx.web |
web/ |
search/fetch provider registries |
ctx.compact, ctx.toolResultPrune |
compact//compact-tool-result-prune |
summary compaction, optional model-free result pruning |
ctx.subagents |
subagent/ |
named delegation providers |
ctx.planMode |
plan/ |
logged plan collaboration state |
ctx.tasks |
tasks/ |
background task registry, generic task_* controls |
ctx.workflows |
workflow/ |
script-driven multi-agent orchestration |
ctx.goals |
goal/ |
persisted same-session goals |
ctx.sessionPersistence |
session/ |
durable session-log storage |
ctx.sessionQuery |
session-query/ |
live-preferred exact/filter/trace queries over SQLite FTS, workspace-authorized model tools |
ctx.sessionTitle |
session/session-title |
log-backed fallbacks, one optional asynchronous provider |
ctx.settings |
settings/ |
per-plugin user-settings namespaces layered over composition entries |
ctx.credentials |
credentials/ |
named secret references resolved per operation, never inlined in configuration |
ctx.directoryPicker |
host/directory-picker |
GUI-host directory picking (native/browse interactions) |
ctx.typert |
typert/registry |
runtime registry for generated package reflection and live Zod schemas |
ctx.typertGateway |
api/gateway |
dispatches TypeRT Remote unary calls through the API Gateway |
ctx.invariants |
support/invariants |
package-name-selected registry of package-owned runtime checks |
Event
Events are the service extension API (subsystems, producer/consumer map).
Event Domains
- Session events are durable log facts emitted through
session/event. - Agent events carry live
Agentfor inbox, step, status, request, validation, and continuation. - Capability events attach policy and adapters without a loop import.
Interception Semantics
Waterfalls are around-middleware: listeners delegate with next(); returning without it short-circuits or takes over (semantics).
Default Loop Lifecycle
A session is append-only. A turn claims one queued follow-up, waits for its predecessor's checkpoint, and may share its running interval (decision); injection claims none. A step is one model request plus tools. Fresh creation and persisted resume first acquire an exact unpublished SessionPreparation; Agent and session publication happen only after private setup against that Session is ready (decision). Quotes in the sequence mark durable events.
Creation without an id mints <config-id>-session-<uuid>; sessionId resumes or creates, while resumeSessionId requires history. Resume restores lineage and delegation depth before publication; setup failure emits agent-loop/config-start-failed.
Turn Flow
choose declarative identity and acquire fresh/restored SessionPreparation
-> prepare private agent.ctx around exact Session -> await unpublished setup -> invoke optional synchronous setup commit
-> enter session + agent -> session/created -> agent/created
-> enable driving -> agent/session-start(source) -> start driver
forever:
waking inbox insertion starts the driver before send returns
-> emit agent/status(running) if starting an interval
-> 'turn/start'
claim next-step input plus one next-turn message
-> emit agent/inbox/claimed({ message, turn }) for each claimed message
-> agent/pre-step({ agent, messages, turn, step, signal })
reject, empty input, cancellation, or listener failure
-> the claimed batch stays removed; close the no-step turn; stop the driver
enter -> step loop:
'step/start'
append the returned batch as separate 'user/message' events
assemble ordered prompt and tool schemas -> snapshot derived messages
agent/request (config only) -> prepare adapter defaults/provenance + context capacity under turn signal -> log request/header (+ request/context on route change) -> llm/stream (frozen, registration-bound)
'assistant/chunk'
'assistant/message'
schedule tool calls by ctx.tools.executionMode:
exclusive -> barrier
parallel -> rolling pool, <= maxParallelToolCalls; reclassify at start
start -> 'tool/call' -> tools/pre-execute -> concurrent tools/execute
model-order result -> ordered tools/post-execute -> 'tool/result'
'step/end'
tools owe another request or next-step inbox is nonempty
-> claim -> agent/pre-step -> append entered batch -> continue
otherwise agent/turn-stopping -> re-check the next-step inbox
'turn/end'
start the next waking queued message, or emit agent/status(idle)
idle inject:
queue non-waking next-step context
leave it pending until followup or steer wakes the driver
Each step assembles ordered prompt sections, tool schemas, and variables; unknown references fail the turn. dsh-system-prompt owns identity and persona; the loop supplies provider, model, and cwd (prompt ownership).
inject() queues non-waking next-step context; an idle driver leaves it pending until followup() or steer() wakes the driver. Post-tool additionalContexts use the same inbox. The agent/pre-step payload carries the exclusive claimed batch and the upcoming turn, step, and signal. Reject opens no step; enter supplies the complete batch appended after step/start. Empty tool continuations still traverse the waterfall, whose final value settles all rewrites.
Pruning precedes summaries; overflow retries require durable progress. agent/request-error may authorize a same-step retry of the frozen prompt; cancellation wins. Adapter retryPolicy bounds normal mode, while always mode retries after specialized recovery (compaction, retry foundation, provider policy). The generated agent lifecycle owns exact event order, and the agent-loop README owns queue, steering, retry, and cancellation mechanics.
Failure Boundaries
Adapter selection, dispatch, and iteration failures become terminal error or aborted finish chunks. agent/request-error receives request coordinates, normalized LlmFailure, available retry policy, and signal; middleware and consumer errors remain outside recovery. Failed chunks commit neither messages nor tool calls.
Other failures use agent/error; cancellation and disposal beat recovery. Before request-header commit, the turn signal cancels capability preparation; undispatched tools get synthetic tool/call/ABORTED_BEFORE_DISPATCH pairs. Effective cancel(cause) reports its cause before clearing and aborting; idle calls emit nothing. Waking input that lands after the abort fires but before convergence runs at the driver's convergence boundary, while a disposed cancel leaves it parked (cancel-convergence wake latch). Durability distinguishes aborted cancellation from disposed teardown, which awaits quiescence (decision).
Turn and step events are turn-enclosed; the loop appends user/message events only from entered batches inside a turn. A turn opens before the initial claim and pre-step, so rejection, empty input, cancellation, or failure closes a durable turn without any step events. Standalone compact/* { turn: null } events consume no turn, and their lock-time markers may interleave with inbox splices. Reload synthesizes interrupted turn ends; session/end-seed distinguishes stale compaction orphans from live locks. After close, only agent/error reports failures. Each turn has one TurnEndReason.
Agent Handles
ctx.agents owns agents and returns AgentHandle { agent, dispose() }. Plugins use send() or its followup(), steer(), and inject() presets. cancel() and whenIdle() control lifecycle, while awaited disposal owns teardown. A follow-up MessageId follows durable inbox insertion, claiming, and discard notifications, not prompt output or turn ending; only an owner of a whole activity interval may summarize it as a run result (decision).
Agent Scope
Each agent owns scoped agent.ctx; shared storage overlays its tool, prompt, and command entries on globals while preserving domain views (decision). Scoped listeners filter dispatch; contributions unwind with awaited cleanup. CreateAgentOptions.setup(agentCtx) composes before publication. Typed resolvers derive carrier checks from merged Events and scopeTarget (semantic gates). Details: agent scope, subagent composition. AgentLoop runs under ctx.agents.withInitiator(); private orchestration derives agent.session, but turn, step, signal, cwd, and authority stay explicit (decision).
State
Session Log
The session log is authoritative. deriveMessages() projects model history; raw assistant/chunk events preserve replay and UI fidelity. Fork, resume, transcript rendering, telemetry, and persistence derive from this stream.
Model-visible ⟺ logged: messages entering at step/start plus the folded request/header reconstruct every request. The header marks adapter defaults so later proposals discard them and re-resolve the route without losing explicit settings. request/context separately records registration-bound provider, model, and capacity metadata when the route changes; it does not participate in request reconstruction or header equality. dsh-agent-loop/invariant asserts reconstructability through ctx.invariants (reconstructability).
Durability is a plugin concern. Backends copy synchronous session/event notifications into fixed-window durable batches; session/flush bypasses the wait before requests and top-level tool dispatch, and after turn/end before another turn or idle. SessionPersistence stores events and header metadata; JSONL defaults to checksummed Zstandard and SQLite shares the contract (checkpoint decision, batching decision).
Between turns, owners append log-only events through Session, flushing only for durability. session/title relies on bounded background persistence and lifecycle drains; manual compaction flushes its bracket before the operation completes. Title work never delays responses; latest wins with provenance. Title records are inherited fork boundaries (decision).
Model Content
Messages use typed blocks from merge-extensible ContentBlockMap; the pattern also types MessageSource, FinishReason, TurnTrigger, and TurnEndReason. New blocks coordinate adapters, UI, compaction, token metering, and persistence; replay measurements live in token-meter.md.
Streaming uses raw chunks and BlockAssembler. Each LlmAdapter.stream() is one provider attempt; adapters report normalized failure facts, and a handling agent/request-error plugin returns a retry action. The loop logs chunks, successful provenance, and replay state. Remote adapters use per-read idle watchdogs. Replay crosses routes only through a shared adapter instance (contract).
Extension And Composition
Capability Pattern
Capabilities separate interface / implementation / consumer layers. Filesystem and subprocess providers define one execution world; Bash, PTY, and LSP run there without provider forks. See the capability graph.
Exceptions combine LLM interface/consumer, filesystem policy, web registries, and named skill/subagent providers. Subagents spawn fresh, fork a completed-turn prefix, use ACP children, or delegate one self-contained turn to a real product provider such as Codex (subagent.md).
dsh-workspace-context composes its baseline on the first agent/pre-step and folds it into the final entering batch right after the claimed prompt, so it reaches the first request with the direct prompt; rejection keeps it in the next-step inbox. When compaction removes that baseline from the visible surface, the next entering pre-step composes the current baseline and carries it in the same request. Filesystem changes projected after tools are likewise folded into the next entering pre-step instead of creating a later context-only step (decision). dsh-paths owns shared paths.
Bundles And Apps
dsh-agent-spine-demo bundles a spine and optional goals. App packages own CLI, ACP automation, and JSON-RPC front doors (README, acp/, interaction/). dsh-jsonrpc-agent boots external cordis.yml; the Python SDK defaults when config is absent (Python SDK). Thin deployments use swappable backends and optional tools (examples/, runnable wirings, graph atlas).
Where New Behavior Goes
New behavior attaches to a documented extension point; a loop change updates this map.
| Goal | Mechanism |
|---|---|
| Add a model provider | register its adapter on ctx.llm |
| Add a model-facing capability | register on ctx.tools; schemas join prompt assembly |
| Add shell execution | implement and register a ctx.bash backend; the local backend spawns through ctx.subprocess |
| Add persistent terminal execution | register a ctx.pty backend plus dsh-tool-pty |
| Add a human command | register on ctx.commands; adapters discover and dispatch without a model turn |
| Add background work | register on ctx.tasks; generic task_* tools collect or stop it |
| Add filesystem access or policy | implement a ctx.fs provider or listen to fs/* policy 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 is the stop boundary |
| Add model-facing context | call agent.inject() to queue sourced context for the next admitted request |
| Add UI or editor integration | drive ctx.agents and render from session/event |
| Add durable session state | extend SessionEventMap; render and replay from the log |
| Add asynchronous session-title generation | register the sole ctx.sessionTitle provider |
| Manage a same-session objective | use ctx.goals; continue through Agent and agent/* |
| Fork a live session | call ctx.sessions.fork(source, boundary?, childSessionId?) |
| Scope a registration to one agent | use its agent.ctx (see Agent Scope) |
The extension cookbook has plugin skeletons and the feature-to-seam map; guides cover packages, tools, LLM adapters, and vendored packages.