Files
deepseek-harness/docs/architecture.md

16 KiB

DeepSeek Harness Architecture

English | 中文

The DeepSeek Harness SDK builds agent harnesses on Cordis. The principle is simple: everything is a plugin. The shipped loop is one plugin, not a privileged kernel.

Overview

A harness is one Cordis context. Packages add services (ctx.llm, ctx.tools, ctx.sessions, ctx.sessionTitle), typed events (agent/request, tools/pre-execute, session/event), and disposable registrations.

packages/core/ groups the default agent flow; surrounding capabilities are equally first-class Cordis plugins.

Default Services

ctx key Package Role
dsh-scope scoped-context registration primitive (library)
ctx.sessions dsh-session in-memory event-sourced sessions
ctx.systemPrompt dsh-system-prompt ordered prompt sections, tool schemas, and prompt variables
ctx.tools dsh-tools tool registry and execution pipeline
ctx.agents dsh-agent live agents, delegated creation, agent/* events, and process-local initiator scope
ctx.agentLoop dsh-agent-loop concrete Agent driver

Capability Services

ctx key Package family Role
ctx.llm llm/ adapter registry and streaming model calls
ctx.tokenMeter llm/token-meter singleton replay-aware request/surface pressure
ctx.bash bash/ foreground/background command execution
ctx.sandbox sandbox/ same-world process confinement (argv wrapping, 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.skills skill/ skill provider registry and 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.tasks tasks/ background task registry + generic task_* control tools
ctx.workflows workflow/ script-driven multi-agent orchestration
ctx.sessionPersistence session-persistence/ durable storage for session logs
ctx.sessionQuery session-query/ live-preferred logical-corpus exact reads and relationship traces
ctx.sessionTitle session-title/ log-backed fallback titles and one optional asynchronous provider

Event

Events form the service extension API; see the exhaustive events catalog and producer/consumer map.

Event Domains

  • Session events are durable facts appended to the log and emitted through session/event.
  • Agent events carry the live Agent for status, prompt admission, request shaping, validation, and continuation.
  • Capability events let owning seams attach policy and adapters without importing the loop.

Interception Semantics

Waterfall events behave like around-middleware: a listener delegates by calling next(); returning without it vetoes or takes over. Full rule: Cordis waterfall semantics.

Default Loop Lifecycle

The shipped loop drains prompt-to-checkpoint work through plugin-visible services and events.

A session is an append-only log. Each ordinary turn claims one queued send() item; injection claims none. Successors await the preceding claimed turn's checkpoint but may share its running interval (decision). A turn ends when model and plugins stop it. A step is one model request plus tools. Below (sequence companion), quotes mark durable events.

No id mints <config-id>-session-<uuid>; sessionId resumes/creates and resumeSessionId needs history. Resume restores lineage and delegation depth before publication. Setup failures emit agent-loop/config-start-failed; teardown stays 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 a queued message
  emit agent/status(running)
  TURN:
    'turn/start'
    claimed message -> agent/prompt-submit
      allowed prompt -> 'user/message' plus injected context
      blocked prompt -> 'prompt/blocked' -> 'turn/end'(rejected)
    STEP loop:
      drain steering
      assemble system prompt and tool schemas
      agent/session-prefix (first step)
      agent/pre-step
      snapshot the derived messages (the reconstruction boundary)
      'step/start'
      agent/request (config only) -> log request/header -> llm/stream (frozen)
      on final adapter-path or terminal in-band failure:
        'step/end'
        agent/request-error(original error, failure facts, immutable prior failures, signal)
        retry in the next numbered step or preserve the original error
      otherwise:
        'assistant/chunk'
        agent/step-result
        'assistant/message' (transformed content or empty success anchor after step-result rejection)
        schedule tool calls by ctx.tools.executionMode:
          exclusive -> one-call barrier
          parallel -> rolling pool, <= maxParallelToolCalls in flight; reclassify before start
          each start -> 'tool/call' -> ordered tools/pre-execute -> concurrent tools/execute
          each model-order result -> ordered tools/post-execute -> 'tool/result'
        append accepted tool-batch context after all recorded results, then steering
        agent/post-step
        'step/end'
        agent/turn-continuation
        agent/turn-stop (terminal policy)
        stop unless tools or continuation policy ask for another step
    'turn/end'
    checkpoint persistence and notify idle/running status

Each step assembles ordered prompt sections, tool schemas, and variables; unknown references fail the turn. dsh-system-prompt owns identity and persona, while the loop supplies model and cwd (prompt ownership).

Tool-time context settles after recorded results. Steering drains before agent/post-step; leftovers become queued input. Terminal agent/turn-stop runs after continuation and steering folding, stays authoritative through flush, and discards later steering while preserving queued prompts.

Pruning precedes summaries; overflow retries require durable progress. Bounded transient retries compose on agent/request-error; cancellation wins (compaction, retry).

Failure Boundaries

The turn is the containment boundary. Adapter failures close the step and enter agent/request-error with exact failure facts. Retry opens a numbered step; exhaustion stores the failure on turn/end. Failed chunks commit no message or tool.

Other failures use agent/error. Cancellation and disposal beat recovery; undispatched model tool calls receive synthetic tool/call and ABORTED result pairs before turn/end. cancel() clears queues and aborts active work; disposal awaits quiescence before unregistering.

Every session event is turn-enclosed. Reloading preserves an interrupted tail and closes it with a synthetic interrupted turn end. Failures after durable turn close report only through agent/error because no safe in-turn position remains. Each turn has one TurnEndReason; TurnEndReasonMap owns the variants.

Agent Handles

ctx.agents owns live agents and returns AgentHandle { agent, dispose() }. Plugins use send(), steer(), inject(), cancel(), and whenIdle(). The caller fiber, factory provider, and consumer handle co-own teardown through one awaited disposer.

Agent Scope

Every live agent owns a scoped agent.ctx. Registrations shadow globals, receive only that agent's dispatches, and unwind with it; async cleanup is awaited. CreateAgentOptions.setup(agentCtx) composes before publication. Typed resolvers derive carrier checks from merged Events signatures and scopeTarget (semantic gates). See agent scope and subagent composition. AgentLoop propagates its initiator; private orchestration derives agent.session, while other identities stay explicit (decision).

State

Session Log

The session log is the source of truth. deriveMessages() projects session events into the Message[] sent to the model; raw assistant/chunk events stay in the log for replay and UI fidelity. Replay, fork, resume, transcript rendering, telemetry, and persistence all derive from the same event stream.

Model-visible ⟺ logged: the log reconstructs every request — messages at step/start fronted by the header's session prefix, headers by folding request/header — and dev invariants assert this (reconstructability).

Durability is a plugin concern. Backends buffer synchronous session/event notifications; the loop awaits a turn-end checkpoint. SessionPersistence stores SessionEvent directly and metadata in SessionHeader; JSONL defaults to checksummed Zstandard, with SQLite under one contract.

Plugin-owned log-only events may opt into ctx.sessions.appendOutOfBand(): they join an open turn or receive a balanced, flushed zero-step turn. session/title uses that path as a latest-wins fold with source-message seqs and provenance. Its first-message fallback is immediate; at most one optional provider may replace it asynchronously without delaying the agent response. Forks inherit logged titles unchanged (decision).

Model Content

Messages contain typed blocks (text, reasoning, tool-call, tool-result) derived from merge-extensible ContentBlockMap; the same pattern types MessageSource, FinishReason, TurnTrigger, and TurnEndReason. New block types coordinate adapters, UI bridges, compaction pricing, token metering, and persistence as one repo-wide contract; replay measurement types live in token-meter.md.

Streaming uses raw chunks and BlockAssembler. One LlmAdapter.stream() is one provider attempt; adapters report facts, while recovery lives on agent/request-error. The loop logs chunks and successful provenance/replay state. Remote adapters use per-read idle watchdogs. Replay state reaches targets only when routes share an adapter instance (contract).

Extension And Composition

Capability Pattern

A swappable capability usually splits into interface / implementation / consumer: the interface owns its ctx key and events, an implementation registers a backend, and a consumer exposes model behavior through tools or prompts. Bash is the reference; the capability graph shows every family.

Some seams bend the template: LLM combines interface and consumer; filesystem wraps providers with policy; web, skills, and subagents own registries. Session titles pair a built-in fallback with a single-provider registry and shared LLM helper. Subagents spawn fresh, fork a completed-turn prefix, or use ACP children (subagent.md).

dsh-workspace-context composes baselines on agent/session-prefix and appends ctx.fs-discovered nested changes on tools/post-execute; its decision records isolation. dsh-paths owns shared paths.

Bundles And Apps

dsh-agent-spine-demo bundles the default spine, including fallback-only session titles; model title providers remain opt-in (README). dsh-tui-demo owns the terminal; dsh-cli-demo runs one persisted headless turn; dsh-acp-demo adds stdout-pure ACP (ui/). dsh-jsonrpc-agent boots external cordis.yml; the Python SDK supplies a default only without an explicit config and drives line-delimited JSON-RPC (Python SDK). Deployments remain thin leaves with swappable backends and optional tools (examples/, runnable wirings, graph atlas).

Where New Behavior Goes

New behavior should attach to a documented extension point; changing the shipped loop requires updating this map.

Goal Mechanism
Add a model provider register an adapter on ctx.llm
Add a model-facing capability register a tool on ctx.tools; schemas flow into prompt assembly
Add command execution implement and register a ctx.bash backend
Add a long-running/background capability register the work on ctx.tasks; the generic task_* tools collect/stop it
Add filesystem access or policy implement a ctx.fs provider or listen on fs/* policy events
Confine spawned processes a ctx.sandbox backend; consumers wrap their argv before spawning
Intercept prompts, requests, model completion/failure, tool use, or continuation listen on the relevant agent/* or tools/* event; use serial agent/turn-stop for a monotonic terminal stop
Add a session-stable request prefix outside history compose it on agent/session-prefix, once per loop instance; logged on the request header
Add UI or editor integration drive ctx.agents and render from session/event
Add durable session state add a SessionEventMap member and render/replay from the log
Add asynchronous session-title generation register the sole provider on ctx.sessionTitle
Fork a live session use ctx.sessions.fork(source, boundary?, childSessionId?)
Scope a tool, prompt section, or listener to ONE agent register it through that agent's agent.ctx (see Agent Scope)

The extension cookbook carries plugin skeletons and the feature-to-seam map; step-by-step guides cover packages, tools, LLM adapters, and vendored packages.

Quick Reference