From d899333cde07c96ece6ab39c98913d7c06f60a0e Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 5 Jul 2026 19:07:34 +0800 Subject: [PATCH] docs: split cordis primer from architecture map --- AGENTS.md | 4 +- docs/architecture.md | 141 ++++++++++++++---------------- docs/cordis-catalog/events.md | 2 +- docs/cordis-primer.md | 38 ++++++++ scripts/doc-budgets.manifest.json | 3 +- scripts/gen-cordis-catalog.ts | 2 +- scripts/verify-doc-refs.ts | 6 +- 7 files changed, 113 insertions(+), 83 deletions(-) create mode 100644 docs/cordis-primer.md diff --git a/AGENTS.md b/AGENTS.md index 9521dee491..bfb4060e5e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # AGENTS.md -This is the monorepo of the DeepSeek Harness group; it hosts **DeepSeek Harness SDK**, a plugin-based SDK for building agent harnesses. The codebase is built on the vendored Cordis framework, microkernel-style: **everything is a plugin**. Read [docs/architecture.md](docs/architecture.md) before changing anything under `packages/` — the service map, event taxonomy, loop lifecycle, and extension seams. The documentation standard is [docs/AGENTS.md](docs/AGENTS.md). +This is the monorepo of the DeepSeek Harness group; it hosts **DeepSeek Harness SDK**, a plugin-based SDK for building agent harnesses. The codebase is built on the vendored Cordis framework, microkernel-style: **everything is a plugin**. Read [docs/architecture.md](docs/architecture.md) before changing anything under `packages/` — the service map, event surface, loop lifecycle, and extension seams. The documentation standard is [docs/AGENTS.md](docs/AGENTS.md). ## Pre-release stance: foundation over blast radius @@ -85,7 +85,7 @@ Real-API tests and demos read `DEEPSEEK_API_KEY` (and optional `DEEPSEEK_BASE_UR - **Registrations are effects**: every contribution goes through `ctx.effect()` / `ctx.on()`; a registry's `register()` returns the disposer. - **Typed events via declaration merging**; extensible unions use the merge-extensible-map pattern (`ContentBlockMap`, `SessionEventMap`, …). Every new event's JSDoc carries an `@mode` tag and a `@param` per payload parameter (`this`/trailing `next` exempt); every public service-class method documents each parameter and non-void return (`@param`/`@returns`) — the catalog generator hard-errors otherwise ([completeness RFC](docs/rfc/implemented/process/2026-07-04-cordis-jsdoc-completeness-gate.md)); mode semantics are in the [generated events catalog](docs/cordis-catalog/events.md) header and [the catalog RFC](docs/rfc/implemented/process/2026-06-20-generated-cordis-catalog.md). - **Discriminated unions: `switch` on the tag**, not if-chains. Closed unions end with `default: assertNever(...)`; merge-extensible unions must NOT — handle known cases and fall through `default` with a comment. -- **Waterfall listeners MUST call `next()`** to delegate; returning without it is the veto ([semantics](docs/architecture.md#cordis-waterfall-semantics)). +- **Waterfall listeners MUST call `next()`** to delegate; returning without it is the veto ([semantics](docs/cordis-primer.md#cordis-waterfall-semantics)). - **Plugins, not loop changes**: new behavior goes on the documented extension seams; changing `agent-loop` requires updating docs/architecture.md. - **Capability seams are three packages** — interface / implementation / consumer ([capability seams](docs/rfc/implemented/architecture/2026-06-13-capability-seams.md)); don't split preemptively. - **Explicit > implicit at package seams**: no optional field silently filled by a hidden `?? default` inside `run()`; defaulting is an explicit `resolve(request): Spec` step in the owning implementation (the `dsh-bash` request/spec split is the template). diff --git a/docs/architecture.md b/docs/architecture.md index 07d952c5d6..304773914d 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,22 +1,16 @@ # DeepSeek Harness Architecture -The **DeepSeek Harness SDK** is an agent-runtime SDK built microkernel-style on the vendored Cordis framework. The governing principle is simple: **everything is a plugin**. The shipped loop plugin drives the default agent lifecycle, but it is still replaceable; most behavior attaches through typed service and event seams that a replacement loop would honor. +The **DeepSeek Harness SDK** is an agent-runtime SDK built microkernel-style on the vendored Cordis framework. The governing principle is simple: **everything is a plugin**. The shipped agent loop is one plugin in the default bundle, not a privileged kernel; most behavior attaches through typed service and event seams that another loop plugin can honor. -Read this page as the system map before changing `packages/`. It covers behavior: services, loop lifecycle, extension seams, and invariants. Type shapes live in [core-data-structures/](core-data-structures/core.md); exact signatures in the generated [events](cordis-catalog/events.md) and [services](cordis-catalog/services.md) catalogs; diagrams in the [documentation graph index](graph-atlas.md); package contracts in the [package map](../packages/README.md); rationale in the [RFCs](rfc/README.md). +Read this page as the system map before changing `packages/`. It explains how the runtime is shaped, how the default loop moves work, where state lives, and where extensions attach. Type shapes live in [core-data-structures/](core-data-structures/core.md); exact event and service signatures live in the generated [events](cordis-catalog/events.md) and [services](cordis-catalog/services.md) catalogs; package contracts live in the [package map](../packages/README.md); rationale lives in the [RFCs](rfc/README.md). If Cordis itself is new to you, start with the [Cordis primer](cordis-primer.md). -## Mental Model +## System Shape -A running harness is one Cordis context. Packages contribute three things to it: +A running harness is one Cordis context. Packages contribute service keys, typed events, and disposable registrations to that context. Services are the stable call surfaces (`ctx.llm`, `ctx.tools`, `ctx.sessions`); events are interception and notification points (`agent/request`, `tools/pre-execute`, `session/event`); registrations install prompt sections, tool schemas, providers, adapters, and listeners. -- **Services** on `ctx.`: stable call surfaces such as `ctx.llm`, `ctx.tools`, or `ctx.sessions`. -- **Events**: typed interception and notification seams such as `agent/request`, `tools/pre-execute`, or `session/event`. -- **Registrations**: prompt sections, tool schemas, adapters, providers, and listeners, all installed through disposable effects so teardown and hot reload unwind them. +The default distribution is a composition, not a hierarchy. `packages/core/` is a repository grouping for the default agent spine; capability seams around it are equally first-class plugins. Swapping the loop means shipping a different composition bundle, while ordinary extensions should depend on the public services and event vocabulary. -The default loop is intentionally ordinary: drain queued work, assemble a request, stream a model answer, run tools, decide whether to continue, flush durable state. The important part is where it pauses: each pause is a seam a plugin can program against. - -## Service Map - -The default agent spine is assembled from these packages under [`packages/core/`](../packages/core/README.md): +### Default Service Spine | ctx key | Package | Role | |---|---|---| @@ -24,122 +18,119 @@ The default agent spine is assembled from these packages under [`packages/core/` | `ctx.systemPrompt` | `dsh-system-prompt` | ordered prompt sections plus tool schemas | | `ctx.tools` | `dsh-tools` | tool registry and execution pipeline | | `ctx.agents` | `dsh-agent` | live agent registry, public `Agent` handle, `agent/*` vocabulary | -| `ctx.agentLoop` | `dsh-agent-loop` | the shipped `ReactLoopAgent` driver | +| `ctx.agentLoop` | `dsh-agent-loop` | shipped `ReactLoopAgent` driver | -Tool schemas ride in prompt assembly, so "what the model is told it can do" stays coherent ([assembly RFC](rfc/implemented/architecture/2026-06-11-tool-schemas-in-prompt-assembly.md)). Tool execution runs through `tools/pre-execute` → dispatch → `tools/post-execute`, the gate pair for sandbox, permission, hook, and plan-mode plugins ([pipeline graph](tool-execution-pipeline.md)). +Tool schemas ride in prompt assembly, so "what the model is told it can do" stays coherent with the tools registry. Tool execution runs through `tools/pre-execute` -> dispatch -> `tools/post-execute`, the gate pair for sandbox, permission, hook, and plan-mode plugins ([pipeline graph](tool-execution-pipeline.md)). -The swappable capability seams sit around that spine: +### Capability Services | ctx key | Package family | Role | |---|---|---| | `ctx.llm` | [`llm/`](../packages/llm/README.md) | adapter registry and streaming model calls | | `ctx.bash` | [`bash/`](../packages/bash/README.md) | foreground/background command execution | -| `ctx.fs` | [`fs/`](../packages/fs/README.md) | filesystem provider primitives; `fs/*` policy events | +| `ctx.fs` | [`fs/`](../packages/fs/README.md) | filesystem provider primitives and policy events | | `ctx.web` | [`web/`](../packages/web/README.md) | search/fetch provider registries | | `ctx.compact` | [`compact/`](../packages/compact/README.md) | session-surface compaction | | `ctx.subagents` | [`subagent/`](../packages/subagent/README.md) | named delegation providers | | `ctx.sessionPersistence` | [`session-persistence/`](../packages/session-persistence/README.md) | durable storage for session logs | -Extension plugins depend on interfaces and event vocabulary, never on `dsh-agent-loop`; swapping the loop means shipping a different bundle. The sanctioned exception is `dsh-agent-core`, whose job is to compose the default spine. +## Event Surface -## Cordis In Five Ideas +Events are the harness extension API. Each service owns the vocabulary for the behavior it controls, and the generated [events catalog](cordis-catalog/events.md) is the exhaustive reference. The [producer/consumer map](event-producer-consumer.md) shows which packages emit or listen to each event. -Cordis is vendored source the harness owns ([manifest + sync](../vendor/README.md)). A plugin author needs five ideas: plugins are modules with optional `inject` and `apply(ctx)`, or `Service` subclasses; services own `ctx` keys; `inject` waits for required services; events are typed by declaration merging and dispatch as emit, waterfall, parallel, or serial; registrations are disposable effects. +### Event Domains -## Cordis Waterfall Semantics +Use the event domain to decide where new behavior belongs: -`ctx.waterfall` is around-middleware, not a reducer. A listener receives `(...args, next)`: call `next()` to delegate, optionally wrapping the result; return without `next()` to short-circuit; use `prepend: true` only when it must run first. Values propagate through `next()`'s return value. Cooperative listeners mutate a shared object and then delegate; returning a replacement is a takeover because earlier mutations on the old object disappear downstream. For single-slot decision events such as `fs/write-intent`, returning without `next()` is the point: the first decider owns the decision. +- **Session events** are durable, replayable facts. Turn and step boundaries, user input, assistant output, tool calls, tool results, steering, compaction records, and tool-owned durable facts append to the session log and flow through `session/event`. +- **Agent events** are live runtime surfaces. They carry the live `Agent` handle for status, diagnostics, prompt admission, request mutation, result validation, and continuation policy. +- **Capability events** belong to the seam that owns the action. `tools/*`, `llm/*`, `system-prompt/*`, `fs/*`, and `subagent/*` let policy and adapters attach without importing the loop. -## Event Taxonomy +### Interception Semantics -Each service declares its own events; `agent/*` lives in `dsh-agent`, so extensions use the live agent vocabulary without depending on the concrete loop. Capability events belong to the seam that owns their vocabulary: `tools/*`, `llm/*`, `system-prompt/*`, `fs/*`, `subagent/*`, and `session/flush`. The generated [events catalog](cordis-catalog/events.md) is exhaustive; [event-producer-consumer.md](event-producer-consumer.md) shows topology. +Waterfall events behave like around-middleware: a listener delegates by calling `next()` and vetoes or takes over by returning without it. The full rule lives in [Cordis waterfall semantics](cordis-primer.md#cordis-waterfall-semantics). -Domain rule: `session/*` is durable, replayable fact; `agent/*` is live runtime surface ([event-domain RFC](rfc/implemented/architecture/2026-06-30-event-domain-semantics.md)). Reloadable UI state belongs on the session log; hooks, status observers, request mutation, prompt gating, step-result validation, and continuation policy belong on the live agent surface. +## Default Loop Lifecycle -## Loop Lifecycle (Session / Turn / Step) +The shipped loop drains queued work, assembles a request, streams a model answer, executes tools, decides whether to continue, and checkpoints durable state. The important architecture is where it pauses: each pause is a documented service call or event seam that another plugin can program against. -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 contract below ([sequence companion](agent-lifecycle.md)), every `'quoted'` line appends a durable session event and every `waterfall`/`serial` line is an extension seam. +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](agent-lifecycle.md)), quoted names are durable session events and event names are extension seams. + +### Agent Handles + +`ctx.agents` owns live agents and returns an `AgentHandle { agent, dispose() }`. `Agent` is the surface other plugins drive: `send()` queues work, `steer()` injects mid-turn content, `inject()` appends context and opens a one-shot injection turn when idle, `cancel()` is the public stop primitive, and `whenIdle()` observes quiescence. Lifecycle owners tear down with `await dispose()`. ### Turn Flow ```text -create agent -> emit agent/session-start(source) once per live agent +create agent -> emit agent/session-start(source) forever: - wait for queued messages (idle) + wait for queued messages emit agent/status(running) TURN: 'turn/start' - each queued msg: waterfall agent/prompt-submit allow, rewrite, attach context, or block - allow -> session('user/message'...); inject additionalContext - every prompt blocked -> 'turn/end'(rejected) zero-step turn, model never called + each queued message -> agent/prompt-submit + allowed prompt -> 'user/message' plus injected context + every prompt blocked -> 'turn/end'(rejected) STEP loop: drain steering - assembly = ctx.systemPrompt.assemble() waterfall system-prompt/assemble - await ctx.serial('agent/pre-step') surface mutation before history derivation - session('step/start') - req = {model, system, tools, messages: session.deriveMessages(), signal} - req = waterfall agent/request hooks, model switch, tool filtering - stream ctx.llm.stream(req) waterfall llm/stream - session('assistant/chunk') - if assembler.finish is error/aborted: throw - msg = waterfall agent/step-result before the log append - session('assistant/message' {content, usage?}) - each tool-call (sequential, abort-checked between calls): - session('tool/call'); ctx.tools.execute() - tools/pre-execute -> dispatch -> tools/post-execute - tools may append their own session events, e.g. todo/write - session('tool/result') - append buffered post-execute context -> session('context/message')* - drain steering -> session('steering/message') - session('step/end') - cont = waterfall agent/turn-continuation continue iff tool calls or steering by default - continue reasons become next-step steering - if action == stop: break - session('turn/end') - await ctx.parallel('session/flush', session) - leftover steering re-enqueued as queued messages - emit agent/status(idle) unless more queued + assemble system prompt and tool schemas + agent/pre-step + 'step/start' + derive messages from the session log + agent/request -> llm/stream + 'assistant/chunk' + agent/step-result + 'assistant/message' + each tool call: + 'tool/call' + tools/pre-execute -> dispatch -> tools/post-execute + 'tool/result' + append post-tool context and steering + 'step/end' + agent/turn-continuation + stop unless tools or continuation policy ask for another step + 'turn/end' + checkpoint persistence and notify idle/running status ``` -Post-tool context lands after all tool results so tool-call/result adjacency stays stable. Steering drains between steps; leftover steering after a turn is re-queued. +Post-tool context lands after all tool results so tool-call/result adjacency stays stable. Steering drains between steps; leftover steering after a turn is re-queued as ordinary input. ### Failure Boundaries The turn is the containment boundary. A throwing listener, adapter error finish, or failed step ends the current turn with an error reason and reports live diagnostics through `agent/error`; it does not kill the driver loop. `cancel()` clears queued and steering work, aborts the active model/tool boundary when possible, and records the appropriate turn end. Disposal stops the loop, awaits quiescence, unregisters the agent, and lets service disposers drain. -Every session event is turn-enclosed. Reloading a crashed session preserves the interrupted tail and closes it with a synthetic `interrupted` turn end. A failure after `turn/end`, such as a rejecting `session/flush`, reports through `agent/error` only because no safe in-turn position remains. A turn ends with one `TurnEndReason` (`completed`, `aborted`, `error`, `disposed`, `max-tokens`, `rejected`, or `interrupted`); per-variant semantics are in [session.md § TurnEndReasonMap](core-data-structures/session.md#why-a-turn-ended-turnendreasonmap). +Every session event is turn-enclosed. Reloading a crashed session preserves the interrupted tail and closes it with a synthetic `interrupted` turn end. A failure after the durable turn has closed reports through `agent/error` only because no safe in-turn position remains. A turn ends with one `TurnEndReason` (`completed`, `aborted`, `error`, `disposed`, `max-tokens`, `rejected`, or `interrupted`); per-variant semantics are in [session.md § TurnEndReasonMap](core-data-structures/session.md#why-a-turn-ended-turnendreasonmap). -## Agents And Subagents +## State And Model Surface -`Agent` is the handle every plugin programs against: `send()` queues work, `steer()` injects mid-turn content, `inject()` appends context and opens a one-shot `injection` turn when idle, `cancel()` is the single public stop primitive, and `whenIdle()` observes quiescence. The factory returns `AgentHandle { agent, dispose() }`; lifecycle owners tear down with `await dispose()`. Full semantics: [core.md](core-data-structures/core.md), [lifecycle RFC](rfc/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-seams.md). +### Session Log -**Subagents** are a seam, not an `Agent` method: `ctx.subagents` is a named-provider registry (`spawn` starts fresh, `fork` seeds from the parent's completed-turn prefix, ACP drives an out-of-process child); children are ordinary agents ([subagent.md](core-data-structures/subagent.md), [subagent RFC](rfc/implemented/feature/2026-06-21-subagent-capability-seam.md)). +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. -## The Session Log Is The Truth +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. -A `Session` is the single source of truth. `deriveMessages()` projects surface events into the `Message[]` sent to the model; raw `assistant/chunk` events stay in the log for replay/UI fidelity and are skipped. Every other consumer is a derived view too: replay/fork seeds from events, trace/telemetry listens to `session/event`, and resume goes through `ctx.agents.resume({ resumeSessionId })` ([event-sourcing RFC](rfc/implemented/architecture/2026-06-11-event-sourced-sessions.md)). +### Model Content -Durability is a plugin concern: `session/event` is synchronous, persistence backends buffer write-behind, and the loop awaits `session/flush` at every turn end. The `SessionPersistence` seam stores `SessionEvent` directly, with metadata in `SessionHeader`; JSONL and SQLite share one contract suite ([persistence RFC](rfc/implemented/architecture/2026-06-14-session-persistence.md), [write-coordinator RFC](rfc/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md)). +Messages are arrays of typed content blocks (`text`, `reasoning`, `tool-call`, `tool-result`). The union derives from the merge-extensible `ContentBlockMap`; the same pattern types `MessageSource`, `FinishReason`, `TurnTrigger`, and `TurnEndReason`. New block types are coordinated across adapters, UI bridges, compaction pricing, and persistence, so block vocabulary remains a repo-wide contract. -## Content Blocks And Streaming (dsh-llm) +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](core-data-structures/llm-streaming.md). -Messages are arrays of typed content blocks (`text`, `reasoning`, `tool-call`, `tool-result`). The union derives from the merge-extensible `ContentBlockMap`; the same pattern types `MessageSource`, `FinishReason`, `TurnTrigger`, and `TurnEndReason`. The core set is limited to blocks every shipping path honors; new block types land in one coordinated change across adapters, UI bridges, and compaction pricing ([drop-image RFC](rfc/implemented/simplification/2026-07-04-drop-image-content-block.md)). +## Extension And Composition -Streaming is a raw chunk protocol (`block-start` through `finish`) with `BlockAssembler` as the shared chunk-to-block assembler. The loop logs raw chunks for replay while assembling them for dispatch. `LlmAdapter` is the provider seam: subclass, implement `stream()`, register with `ctx.llm.registerAdapter(models, adapter)`; `dsh-llm-deepseek` and `dsh-llm-pi-ai` are deliberate design twins ([twin RFC](rfc/implemented/architecture/2026-06-13-twin-llm-adapters.md)). StreamChunk conventions live in [llm-streaming.md](core-data-structures/llm-streaming.md). +### Capability Pattern -## Capability Seams +A swappable capability usually splits into **interface / implementation / consumer**: the interface owns the `ctx` key and vocabulary; an implementation registers a backend; a consumer exposes model-facing behavior through `ctx.tools` or prompt assembly. The bash trio is the reference shape, and the [capability seam graph](capability-seams.md) shows the current package families. -A swappable capability splits into **interface / implementation / consumer**: the interface owns the `ctx` key and vocabulary; an implementation registers a backend; a consumer exposes model-facing behavior through `ctx.tools` or prompt assembly. The bash trio is the reference shape, and the mechanism is plain Cordis services plus `inject` ([capability-seams RFC](rfc/implemented/architecture/2026-06-13-capability-seams.md), [seam graph](capability-seams.md)). +Some seams bend the template deliberately. LLM keeps interface and consumer vocabulary together because adapters are the implementations. Filesystem adds policy as event gates around provider primitives. Web is one service with search and fetch provider registries, so provider swaps do not rename model tools. Subagents use a named provider registry because multiple delegation backends can coexist; `spawn` starts fresh, `fork` seeds from the parent's completed-turn prefix, and ACP can drive an out-of-process child ([subagent.md](core-data-structures/subagent.md)). -Some seams bend the template deliberately. LLM keeps interface and consumer vocabulary together because adapters are the implementations. Filesystem adds policy as an event gate: `dsh-tool-fs` dispatches `fs/write-intent`, `fs/edit-intent`, and `fs/observed`, while `dsh-fs-policy` listens without becoming a method service ([event-gate RFC](rfc/implemented/architecture/2026-06-26-file-context-as-event-gate.md)). Web is one service with search and fetch provider registries, so provider swaps do not rename model tools ([web-seam RFC](rfc/implemented/architecture/2026-06-24-web-capability-seam.md)). Subagents use a named provider registry because multiple delegation backends can coexist. +### Bundles And Apps -## Composition +`dsh-agent-core` is the default composition bundle: one plugin loading the providerless spine as code ([README](../packages/core/agent-core/README.md)). App packages compose it with a front door and own the boot `bin`: `dsh-stdio-agent` for the terminal REPL, and `dsh-acp-agent` for ACP over JSON-RPC stdio with no stdout logger ([ui/](../packages/ui/README.md)). A deployment is a thin `cordis.yml` leaf: swappable backends, one app entry, and optional product tools ([examples/](../examples/AGENTS.md), [runnable wirings](cookbook/extension-cookbook.md#runnable-wirings), [graph atlas](graph-atlas.md)). -`dsh-agent-core` is the composition bundle: one plugin loading the providerless spine as code ([README](../packages/core/agent-core/README.md)). App packages compose it with a front door and own the boot `bin`: `dsh-stdio-agent` for the terminal REPL, and `dsh-acp-agent` for ACP over JSON-RPC stdio with no stdout logger ([ui/](../packages/ui/README.md), [app-extraction RFC](rfc/implemented/architecture/2026-06-20-extract-example-app-packages.md)). A deployment is a thin `cordis.yml` leaf: swappable backends, one app entry, and optional product tools ([examples/](../examples/AGENTS.md), [runnable wirings](cookbook/extension-cookbook.md#runnable-wirings), [graph atlas](graph-atlas.md)). +### Where New Behavior Goes -## Extending The Harness - -New behavior should attach to a documented seam; changing `dsh-agent-loop` itself requires updating this map. +New behavior should attach to a documented seam; changing the shipped loop requires updating this map. | Goal | Mechanism | |---|---| diff --git a/docs/cordis-catalog/events.md b/docs/cordis-catalog/events.md index b2b2e08858..f05b156da4 100644 --- a/docs/cordis-catalog/events.md +++ b/docs/cordis-catalog/events.md @@ -9,7 +9,7 @@ This file is GENERATED from source (`scripts/gen-cordis-catalog.ts`) and verifie The **harness tier** below (the `@deepseek-ai/dsh-*` packages) is the vocabulary this repo owns, grouped by scope. The **inherited tier** at the end is the cordis-core + loader/hmr/timer event surface a plugin also sees — pinned vendor source, summarized tersely. -Dispatch modes: **emit** (fire-and-forget), **waterfall** (each listener gets `next()` and may transform or veto — see [waterfall semantics](../architecture.md#cordis-waterfall-semantics)), **parallel** (awaited fan-out; all listeners run), **serial** (awaited in registration order until one returns a bail value — anything other than `null`, `false`, or `undefined`). +Dispatch modes: **emit** (fire-and-forget), **waterfall** (each listener gets `next()` and may transform or veto — see [waterfall semantics](../cordis-primer.md#cordis-waterfall-semantics)), **parallel** (awaited fan-out; all listeners run), **serial** (awaited in registration order until one returns a bail value — anything other than `null`, `false`, or `undefined`). ## `agent/*` diff --git a/docs/cordis-primer.md b/docs/cordis-primer.md new file mode 100644 index 0000000000..15534b0ae6 --- /dev/null +++ b/docs/cordis-primer.md @@ -0,0 +1,38 @@ +# Cordis Primer + +Cordis is the vendored plugin framework underneath the DeepSeek Harness SDK. This primer teaches the Cordis ideas a harness plugin author needs before reading the generated [events](cordis-catalog/events.md) and [services](cordis-catalog/services.md) catalogs. The vendored source and sync procedure live in [vendor/README.md](../vendor/README.md). + +## Cordis In Five Ideas + +- **A plugin is a unit of behavior.** It can be a function with optional `inject` and `apply(ctx)` fields, or a `Service` subclass whose lifecycle Cordis mounts into the current context. +- **A context is the service container.** A service claims a stable `ctx.` such as `ctx.tools`, `ctx.llm`, or `ctx.sessions`; other plugins program against that key instead of importing a concrete implementation. +- **`inject` is the dependency gate.** A plugin that names required services waits until those services exist, so load order is expressed through service requirements rather than manual boot sequencing. +- **Events are typed extension seams.** Services declare event names through TypeScript declaration merging, then dispatch them as `emit`, `waterfall`, `parallel`, or `serial` depending on whether listeners observe, wrap, fan out, or run in order. +- **Registrations are disposable effects.** Prompt sections, tool schemas, adapters, providers, and listeners are installed through `ctx.effect()` or `ctx.on()` so reload and teardown unwind them predictably. + +## Dispatch Modes + +Use the mode to understand what a listener can do: + +| Mode | Shape | +|---|---| +| `emit` | synchronous notification; listeners observe but do not shape the result | +| `waterfall` | around-middleware; each listener receives `next()` and may wrap, rewrite, or veto | +| `parallel` | awaited fan-out; all listeners run and the dispatcher waits for them | +| `serial` | awaited in registration order; a non-void bail value stops the chain | + +The mode is part of the event's public contract. New harness events document it with an `@mode` tag so the generated catalog can check declarations against dispatch sites. + +## Cordis Waterfall Semantics + +`ctx.waterfall` is around-middleware, not a reducer. A listener receives `(...args, next)`. Call `next()` to delegate, optionally wrapping the result; return without `next()` to short-circuit. Values propagate through `next()`'s return value. + +Cooperative listeners usually mutate a shared request or decision object and then delegate. Returning a replacement is a takeover: downstream listeners see the replacement, and earlier mutations on the original object do not carry forward. Use `prepend: true` only when the listener must run before ordinary registrations. + +For single-decision events, short-circuiting is the design. A policy listener can return without `next()` when it owns the decision, while a listener that only annotates or observes must delegate. + +## Practical Rules + +Own vocabulary where the behavior lives: a tool pipeline event belongs to `ctx.tools`, model streaming belongs to `ctx.llm`, and live agent coordination belongs to `ctx.agents`. Prefer events for interception and policy; prefer service methods for direct capability calls. + +Every registration should have a disposer, either by returning one from `ctx.effect()` or using a Cordis helper that does it for you. If teardown order matters, keep the related work in one effect so disposal unwinds in the intended sequence. diff --git a/scripts/doc-budgets.manifest.json b/scripts/doc-budgets.manifest.json index 72e1f3d594..b43f5fa8fc 100644 --- a/scripts/doc-budgets.manifest.json +++ b/scripts/doc-budgets.manifest.json @@ -1,7 +1,8 @@ { "AGENTS.md": 1575, "docs/AGENTS.md": 1315, - "docs/architecture.md": 1840, + "docs/architecture.md": 1630, + "docs/cordis-primer.md": 550, "docs/defensive-patterns.md": 550, "docs/testing.md": 800, "examples/AGENTS.md": 610, diff --git a/scripts/gen-cordis-catalog.ts b/scripts/gen-cordis-catalog.ts index 74d4f6e00b..acb6579272 100644 --- a/scripts/gen-cordis-catalog.ts +++ b/scripts/gen-cordis-catalog.ts @@ -556,7 +556,7 @@ function renderEvents(events: EventEntry[]): string { '', 'The **harness tier** below (the `@deepseek-ai/dsh-*` packages) is the vocabulary this repo owns, grouped by scope. The **inherited tier** at the end is the cordis-core + loader/hmr/timer event surface a plugin also sees — pinned vendor source, summarized tersely.', '', - 'Dispatch modes: **emit** (fire-and-forget), **waterfall** (each listener gets `next()` and may transform or veto — see [waterfall semantics](../architecture.md#cordis-waterfall-semantics)), **parallel** (awaited fan-out; all listeners run), **serial** (awaited in registration order until one returns a bail value — anything other than `null`, `false`, or `undefined`).', + 'Dispatch modes: **emit** (fire-and-forget), **waterfall** (each listener gets `next()` and may transform or veto — see [waterfall semantics](../cordis-primer.md#cordis-waterfall-semantics)), **parallel** (awaited fan-out; all listeners run), **serial** (awaited in registration order until one returns a bail value — anything other than `null`, `false`, or `undefined`).', '', ] const scopes = [...new Set(events.map(e => e.scope))].sort() diff --git a/scripts/verify-doc-refs.ts b/scripts/verify-doc-refs.ts index 9a5714ef1d..a53399a272 100644 --- a/scripts/verify-doc-refs.ts +++ b/scripts/verify-doc-refs.ts @@ -2,7 +2,7 @@ * Doc-sync gate: verify that doc references written in TypeScript COMMENTS * resolve to a file that exists. Source comments cite docs by root-relative * prose path — `see docs/rfc/implemented/testing/2026-06-19-acp-snapshot-tests.md`, - * `docs/architecture.md § Extending The Harness`. `verify-md-links` parses Markdown + * `docs/architecture.md § Where New Behavior Goes`. `verify-md-links` parses Markdown * link AST and never sees these, so a doc rename or move could silently orphan * a `.ts` comment that points at it. The RFC classification reorg * ([the classification RFC](../docs/rfc/implemented/process/2026-06-20-rfc-classification.md)) @@ -12,7 +12,7 @@ * Detection is a token scan, NOT an AST walk: doc refs live in free prose inside * comments, not in a structured form. We match `docs/.md` tokens and * REQUIRE the `.md` extension, so extensionless prose (`docs/postmortem/0001`, - * `docs/architecture.md § Extending The Harness` — the section suffix is outside the + * `docs/architecture.md § Where New Behavior Goes` — the section suffix is outside the * token) is left alone rather than misread as a path. Each token is resolved * ROOT-RELATIVE (the way the comments are written) and must exist on disk. This * is checker, not fixer: it reports and never rewrites. @@ -43,7 +43,7 @@ const isExcluded = (p: string): boolean => * Match a `docs/…​.md` reference token. The `.md` extension is required so a * bare `docs/postmortem/0001` (no extension) does not register as a path. The * character class stops at whitespace, backticks, parens, and the section sign, - * so trailing prose (`… .md § Extending The Harness`) is not swallowed into the path. + * so trailing prose (`… .md § Where New Behavior Goes`) is not swallowed into the path. */ const DOC_REF = /\bdocs\/[A-Za-z0-9._/-]+\.md/g