18 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 agent 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.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/ |
managed child-process trees for bash, LSP, and ACP subagent backends |
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/ |
filesystem provider primitives 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-persistence/ |
durable session-log storage |
ctx.sessionQuery |
session-query/ |
live-preferred exact/filter/trace queries over SQLite FTS, workspace-authorized model tools |
ctx.sessionTitle |
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.invariants |
support/invariants |
package-name-selected registry of package-owned runtime checks |
Event
Events are the service extension API (catalog, producer/consumer map).
Event Domains
- Session events are durable log facts emitted through
session/event. - Agent events carry live
Agentfor inbox notifications, step entry, status, request shaping, validation, and continuation. - Capability events let owning seams attach policy and adapters without a loop import.
Interception Semantics
Waterfalls are around-middleware: listeners delegate with next(); returning without it vetoes or takes over (semantics).
Default Loop Lifecycle
A session is append-only. An ordinary turn claims one queued follow-up; injection claims none. A successor awaits its predecessor's checkpoint but may share its running interval (decision). A turn ends when model or plugins stop it; a step is one model request plus tools. Quotes in the sequence below 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 failures emit agent-loop/config-start-failed; teardown is silent.
Turn Flow
choose declarative identity and fresh/resume path
-> prepare private session + agent.ctx -> await unpublished setup
-> enter session + agent -> session/created -> agent/created
-> enable driving -> agent/session-start(source) -> start driver
forever:
wait for waking inbox work
claim next-step input plus one next-turn message with a pure deletion splice
-> emit agent/inbox/claimed({ message, turn }) for each claimed message
-> emit agent/status(running) if starting an interval
-> agent/pre-step(messages, { turn, step, signal })
reject or listener failure -> the claimed batch stays removed; stop the driver
enter:
'turn/start'
STEP loop:
'step/start'
append the returned batch as separate 'user/message' events
assemble system prompt and tool schemas
snapshot the derived messages (the reconstruction boundary)
agent/request (config only) -> prepare adapter defaults/provenance under turn signal -> log request/header -> 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; scheduler failure -> stop starts, drain dispatches
start -> 'tool/call' -> ordered 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 next-step messages
-> agent/pre-step (messages may be empty for a tool continuation)
-> append the entered batch and continue
otherwise agent/turn-stopping -> re-check the next-step inbox
'turn/end' -> agent/settled
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() always queues non-waking next-step context. A collecting or running driver claims it at the nearest later prompt boundary; an idle driver leaves it pending until followup() or steer() wakes the driver. Post-tool additionalContexts enter the same next-step inbox after their tool results.
agent/pre-step receives the exclusive batch already removed from the inbox and finalizes whether the loop enters the proposed step. Its PreStepContext carries the exact upcoming turn and step plus the cancellation signal. { kind: 'reject' } opens no step; { kind: 'enter', messages } supplies the complete batch appended after step/start. A tool continuation with no newly claimed inbox input submits an empty batch so listeners can still contribute current-step context. Waterfall rewrites settle only in the final returned messages value.
Pruning precedes summaries; overflow retries require durable progress. agent/request-error may authorize a same-step retry before the step closes; cancellation wins, and the retry reuses that step's frozen prompt assembly. Adapter-owned retryPolicy makes normal mode bounded; always mode delegates specialized recovery before retrying until success or cancellation (compaction, retry foundation, provider policy).
Failure Boundaries
Final-adapter selection, dispatch, and iteration failures become terminal finish { kind: 'error' | 'aborted', failure } chunks before the loop handles them. agent/request-error receives request coordinates, normalized LlmFailure, the prepared registration's retry policy when available, and the signal; middleware and consumer errors remain thrown outside request 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 asynchronous model-capability preparation; undispatched tools get synthetic tool/call/ABORTED_BEFORE_DISPATCH pairs. Effective cancel(cause) emits its cause before queue clearing and abort; observers cannot veto; idle calls emit nothing. Durability records user or parent cancellation as aborted, teardown as disposed; teardown awaits quiescence. The cause affects reporting, not late result-context handling (decision).
Turn and step events are turn-enclosed; the loop appends injected user/message events only from entered batches inside a turn. Reload closes an interrupted tail with a synthetic turn end. After close, only agent/error reports failures. Each turn has one TurnEndReason.
Agent Handles
ctx.agents returns AgentHandle { agent, dispose() }. Plugins drive agents with followup(), steer(), and inject(); cancel() stops work, while the awaited disposer owns teardown. followup() only queues an identified message: its MessageId follows durable inbox insertion, claiming, and discard notifications, not a prompt-specific output or turn ending. agent/status and whenIdle() describe whole-agent activity; only a caller that explicitly owns an activity interval may summarize that interval 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 at step/start plus the folded request/header reconstruct every request; the header also marks adapter-materialized defaults so the next proposal can discard them and resolve the selected route without losing explicit conversation settings. Package-owned dsh-agent-loop/invariant can assert reconstructability through ctx.invariants (reconstructability).
Durability is a plugin concern. Backends eagerly drain synchronous session/event notifications. session/flush barriers precede each request and top-level tool dispatch, then follow turn/end before another queued turn or idle observation. SessionPersistence stores SessionEvent directly and metadata in SessionHeader; JSONL defaults to checksummed Zstandard, while SQLite shares the contract (decision).
Log-only events may sit between turns. Owners append through Session, flushing only for durability. session/title relies on eager persistence and lifecycle drains. Latest title wins with provenance; fallback and provider work never delays responses. Such records are fork boundaries, so forks inherit titles (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
A swappable capability usually has interface / implementation / consumer layers: service/events, backend, and model-facing tools/prompts. Bash is the reference; the capability graph maps each family.
Exceptions combine LLM interface/consumer, filesystem policy, web registries, and named skill/subagent providers. Subagents spawn fresh, fork a completed-turn prefix, or use ACP children (subagent.md).
dsh-workspace-context uses the first agent/pre-step to prepend its baseline directly to the next-step inbox, replacing any still-pending predecessor, and appends ctx.fs-discovered changes through tools/post-execute; its decision records isolation. Because claim precedes pre-step, that baseline may miss the current request. dsh-paths owns shared paths.
Bundles And Apps
dsh-agent-spine-demo bundles a spine and optional goals. App packages own TUI, CLI, ACP automation, and JSON-RPC front doors (README, acp/, ui/). 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, render from session/event; terminal-only overlays use ctx.tui |
| 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.