# Conflicts: # docs/config-catalog.md # docs/cordis-catalog/services.md # docs/event-producer-consumer.md # docs/module-graph.md # packages/README.md # packages/bash/tool-bash/src/index.ts # packages/core/tools/tests/gen-tool-catalog.spec.ts # pnpm-lock.yaml # scripts/doc-budgets.manifest.json
14 KiB
DeepSeek Harness Architecture
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 contribute service keys, typed events, and disposable registrations: services expose stable calls (ctx.llm, ctx.tools, ctx.sessions), events provide interception and notifications (agent/request, tools/pre-execute, session/event), and registrations install prompt sections, tools, providers, adapters, or listeners.
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 agent registry, public Agent handle, agent/* events |
ctx.agentLoop |
dsh-agent-loop |
shipped ReactLoopAgent driver |
Capability Services
| ctx key | Package family | Role |
|---|---|---|
ctx.llm |
llm/ |
adapter registry and streaming model calls |
ctx.bash |
bash/ |
foreground/background command execution |
ctx.sandbox |
sandbox/ |
same-world process confinement (argv wrapping, per-call policy) |
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 |
compact/ |
session-log compaction |
ctx.subagents |
subagent/ |
named delegation providers |
ctx.modes |
mode/ |
logged session modes (plan mode) |
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 |
Event
Events form the service extension API; see the exhaustive events catalog and producer/consumer map.
Event Domains
- Session events are durable, replayable facts: boundaries, messages, tool activity, steering, compaction, and tool-owned records append to the log and flow through
session/event. - Agent events carry the live
Agenthandle for status, diagnostics, prompt admission, request shaping, result validation, and continuation policy. - Capability events belong to their owning seam;
tools/*,llm/*,system-prompt/*,fs/*, andsubagent/*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 work from prompt through checkpoint. Every pause is a service call or event available to plugins.
A session is one agent's append-only event log. A turn drains one queued batch and runs until the model stops asking for tools and no plugin requests continuation. A step is one model request plus the tool executions caused by that response. In the flow below (sequence companion), quoted names are durable session events and event names are extension points.
Turn Flow
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 queued messages
emit agent/status(running)
TURN:
'turn/start'
each queued message -> agent/prompt-submit
allowed prompt -> 'user/message' plus injected context
every prompt blocked -> 'turn/end'(rejected)
STEP loop:
drain steering
assemble system prompt and tool schemas
agent/session-prefix (first step)
agent/pre-step
'step/start'
snapshot the derived messages (the reconstruction boundary)
agent/request (config only) -> log request/header -> llm/stream (frozen)
'assistant/chunk'
agent/step-result
'assistant/message'
each tool call:
'tool/call'
tools/pre-execute -> monotonic guards -> tools/execute -> tools/post-execute -> tools/result
'tool/result'
append post-tool context and steering
'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 {{name}} variables; unknown or valueless references fail the turn. dsh-system-prompt owns the harness identity and default persona, which an agent scope may shadow. The loop supplies model and cwd (prompt-ownership RFC).
Post-tool context lands after all tool results so tool-call/result adjacency stays stable. Steering drains between steps; ordinary leftover steering after a turn is re-queued as input. A terminal agent/turn-stop is the explicit exception: it runs after ordinary continuation and steering folding, then remains authoritative through turn close and flush so steering from those later listeners is discarded rather than becoming another step or turn; ordinary queued prompts are preserved.
Failure Boundaries
The turn is the containment boundary. A throwing listener, adapter error finish, or failed step ends it with an error reason and reports agent/error without killing the driver. cancel() clears queued and steering work, aborts the active model/tool boundary when possible, and records the turn end. Disposal stops the loop, awaits quiescence, unregisters the agent, and drains service disposers.
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 drive Agent through send(), steer(), inject(), cancel(), and whenIdle(). The caller fiber and factory provider structurally co-own programmatic lifecycles; the consumer handle is the only other teardown capability. All owners await one disposer.
Agent Scope
Every live agent owns a scoped agent.ctx. Its registrations shadow globals, receive only that agent's dispatches, and unwind with it; async effects such as background-task cleanup are awaited. CreateAgentOptions.setup(agentCtx) composes the scope before publication. Typed resolvers derive carrier checks from merged Events signatures and scopeTarget (semantic-gates RFC). See the agent-scope RFC and subagent composition controls.
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 RFC).
Durability is a plugin concern. Persistence backends buffer synchronous session/event notifications and the loop awaits a turn-end checkpoint before moving on. The SessionPersistence seam stores SessionEvent directly, with metadata in SessionHeader; JSONL and SQLite share one contract suite.
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, and persistence as one repo-wide contract.
Streaming is a raw chunk protocol (block-start through finish) with BlockAssembler as the shared chunk-to-block assembler. The loop logs raw chunks while assembling them for dispatch. LlmAdapter is the provider seam: subclass, implement stream(), and register with ctx.llm.registerAdapter(models, adapter). StreamChunk conventions live in llm-streaming.md.
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 deliberately: LLM combines interface and consumer because adapters implement it; filesystem wraps provider primitives with policy; web keeps search/fetch provider registries behind one service; skills and subagents use named 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 RFC records isolation. dsh-paths owns shared paths.
Bundles And Apps
dsh-agent-spine-demo bundles the default spine (README). dsh-stdio-demo adds a terminal front door; dsh-acp-demo adds stdout-pure ACP over JSON-RPC (ui/). dsh-jsonrpc-agent boots external cordis.yml; the Python SDK supplies its default only without an explicit config channel and drives dsh-jsonrpc over line-delimited JSON-RPC (Python SDK). Deployments remain thin leaves with swappable backends and optional product 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, tool use, or continuation | listen on the relevant agent/* or tools/* waterfall; 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 |
| 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
- Domain terms in the glossary
- Type definitions in core-data-structures/
- Exact event and service signatures in events
- services catalogs
- package contracts in the package map
- RFCs