- Mount LocalProcessManager in the sandbox e2e compositions (bwrap/landlock/ seatbelt + the spine multi-project e2e) and add the spine demo's dsh-process-local devDependency, so SandboxBashExecutor's new inject resolves when those suites are enabled. - Extend the Windows test/coverage skip to packages/process/* — the POSIX process-group suite moved there from packages/bash. - Update the stale disposal contract: the bash seam JSDoc, BashProcess JSDoc, and core bash doc (en+zh) now state that composition teardown (the process manager's disposal) owns kill-and-await, and an executor-only reload leaves background processes running. - Record ctx.processes in the architecture capability table and extension map (en+zh) and the root AGENTS.md layout tree; reword the timeout-library note so it describes where the plumbing and classification live today.
16 KiB
DeepSeek Harness Architecture
English | 中文
DeepSeek Harness SDK uses Cordis: everything is a plugin, including the loop.
Overview
Harnesses are Cordis contexts with package-contributed 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 registration 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 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.processes |
process/ |
managed child-process groups under the bash executors |
ctx.pty |
pty/ |
owner-scoped persistent terminal sessions |
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.lsp |
lsp/ |
semantic navigation registry |
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.planMode |
plan/ |
logged plan collaboration state |
ctx.tasks |
tasks/ |
background task registry + generic task_* control tools |
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 interface, SQLite FTS backend, and workspace-authorized model tools |
ctx.sessionTitle |
session-title/ |
log-backed fallbacks plus one optional asynchronous provider |
ctx.invariants |
support/invariants |
package-name-selected registry for package-owned runtime checks |
Event
Events form the service extension API; see the 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
Agentfor 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 loop runs through plugin services and events.
A session is append-only. Each ordinary turn claims one queued message; injection claims none. Successors await the preceding checkpoint but may share its running interval (decision). 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 a queued message
emit agent/status(running)
TURN:
'turn/start'
claimed message + contexts -> agent/prompt-submit
allowed prompt -> 'user/message' with prompt-prefix context baked in; append separate contexts
blocked prompt -> 'prompt/blocked' -> 'turn/end'(rejected)
STEP loop:
drain steering with the same prefix/separate context placement (no prompt-submit)
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 -> checkpoint -> 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 -> checkpoint -> 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 -> checkpoint complete response/results
'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
Steps assemble ordered prompt sections, tool schemas, and variables; unknown references fail turns. dsh-system-prompt owns identity and persona; the loop supplies model and cwd (ownership).
Async inject() and post-tool additionalContexts settle after results; steering drains before agent/post-step. Leftovers queue. Terminal agent/turn-stop remains authoritative through close/flush and discards later steering, not queued prompts.
Pruning precedes summaries; overflow retries require durable progress. Bounded retries compose on agent/request-error; cancellation wins (compaction, retry).
Failure Boundaries
Adapter failures close the step before agent/request-error with exact Error, LlmFailure, and history. Retries open steps; success clears history; exhaustion stores failure on turn/end. Failed chunks commit nothing.
Other failures use agent/error. Cancellation and disposal beat recovery; undispatched tools get synthetic tool/call/ABORTED_BEFORE_DISPATCH pairs. The signal retires before turn/end. Effective cancel() emits its cause, clears queues, and aborts; observers cannot veto, idle calls emit nothing, and durability records aborted. Disposal awaits quiescence (decision).
Session events are turn-enclosed; reload closes an interrupted tail with a synthetic interrupted turn end. Post-close failures use agent/error. Each turn has one TurnEndReason.
Agent Handles
ctx.agents returns AgentHandle { agent, dispose() }. Plugins use intent helpers followup(), queue(), steer(), and inject(); callers with exact routing facts use mandatory-field send() (decision). cancel() and whenIdle() control lifecycle. Caller, provider, and handle co-own teardown.
Agent Scope
Each agent owns a scoped agent.ctx over global tool, prompt, and command storage (decision); scoped listeners filter and contributions unwind with awaited cleanup. CreateAgentOptions.setup(agentCtx) composes before publication; typed resolvers derive carrier checks from Events and scopeTarget (gates). AgentLoop runs inside ctx.agents.withInitiator(); private orchestration derives agent.session, while turn, step, signal, cwd, and authority stay explicit (decision). See agent scope and subagent composition.
State
Session Log
The session log is authoritative. deriveMessages() projects model history; raw assistant/chunk events preserve replay and UI fidelity. Fork, resume, transcripts, telemetry, and persistence share that stream.
Model-visible ⟺ logged: step/start messages plus the header's session prefix and folded request/header reconstruct every request; dsh-agent-loop/invariant asserts this through ctx.invariants (decision).
Durability is a plugin concern; backends buffer synchronous session/event notifications. Checkpoints drain before adapter dispatch, recorded top-level tool calls before tool dispatch, complete response/result batches at agent/post-step, and final turn ends. SessionPersistence stores SessionEvent plus SessionHeader metadata; JSONL defaults to checksummed Zstandard, with SQLite under one contract (decision).
ctx.sessions.appendOutOfBand() joins plugin-owned log-only events to an open turn or creates a balanced, flushed zero-step turn. session/title folds latest-wins with source seqs and provenance; its immediate fallback and sole optional async provider never delay the agent response. Forks inherit titles (decision).
Model Content
Messages use typed blocks from merge-extensible ContentBlockMap; the same pattern 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 facts and agent/request-error owns recovery. The loop logs chunks and successful provenance/replay state. Remote adapters use per-read idle watchdogs. Replay state crosses routes only when they share an adapter instance (contract).
Extension And Composition
Capability Pattern
A swappable capability usually splits into interface / implementation / consumer: service/events, a backend, and model-facing tools/prompts. Bash is the reference; the capability graph maps each family.
Exceptions combine layers: LLM interface/consumer; filesystem policy; web registries; named skill/subagent providers. 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 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 supplies a default only without explicit config (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 an adapter on ctx.llm |
| Add a model-facing capability | register on ctx.tools; schemas enter prompt assembly |
| Add shell execution | implement and register a ctx.bash backend (the local one spawns through ctx.processes) |
| Add persistent terminal execution | register a ctx.pty backend and dsh-tool-pty |
| Add a human command | register on ctx.commands; adapters discover and dispatch it 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 on fs/* policy events |
| Confine spawned processes | a ctx.sandbox backend; consumers wrap their argv before spawning |
| Intercept a request, tool, or turn | use its agent/* or tools/* event; agent/turn-stop is the serial terminal stop |
| Add a session-stable prefix outside history | compose agent/session-prefix; the request header logs it |
| Add UI or editor integration | drive ctx.agents and render from session/event; terminal-only overlays use ctx.tui |
| 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 |
| Manage a same-session objective | use ctx.goals; continue through Agent and agent/* |
| Fork a live session | use ctx.sessions.fork(source, boundary?, childSessionId?) |
| Scope a registration to one agent | use 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
- Domain terms in the glossary
- Type definitions in core-data-structures/
- Exact signatures in the event and service catalogs
- package contracts in the package map
- Agent Notes