Files
deepseek-harness/docs/architecture.md
T

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.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.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 (catalog, producer/consumer map).

Event Domains

  • Session events are durable log facts emitted through session/event.
  • Agent events carry live Agent for 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 vetoes 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. 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

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, 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/, 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 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.