From 9b8fccc6f9f133f3b400e6f196db14167bcf4021 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Thu, 11 Jun 2026 15:24:14 +0800 Subject: [PATCH] Backfill architecture decision records Seven ADRs capturing the why behind decisions already made: vendoring Cordis as source with a guarded manifest; the microkernel event taxonomy with one swappable concrete loop; event-sourced sessions with derived history and the append-before-emit ordering contract; the provider-neutral content-block vocabulary (and why not OpenAI/Anthropic shapes); the custom tool-schema DSL over schemastery; tool schemas living in the prompt assembly; and mechanical quality gates over prose guidelines (the agents-write-the-code rationale). --- docs/adr/0001-vendor-cordis-as-source.md | 34 +++++++++++++++++ docs/adr/0002-microkernel-event-taxonomy.md | 38 +++++++++++++++++++ docs/adr/0003-event-sourced-sessions.md | 38 +++++++++++++++++++ docs/adr/0004-own-content-block-vocabulary.md | 37 ++++++++++++++++++ ...0005-custom-schema-dsl-over-schemastery.md | 35 +++++++++++++++++ .../0006-tool-schemas-in-prompt-assembly.md | 31 +++++++++++++++ docs/adr/0007-quality-gates.md | 36 ++++++++++++++++++ docs/adr/README.md | 19 ++++++++++ 8 files changed, 268 insertions(+) create mode 100644 docs/adr/0001-vendor-cordis-as-source.md create mode 100644 docs/adr/0002-microkernel-event-taxonomy.md create mode 100644 docs/adr/0003-event-sourced-sessions.md create mode 100644 docs/adr/0004-own-content-block-vocabulary.md create mode 100644 docs/adr/0005-custom-schema-dsl-over-schemastery.md create mode 100644 docs/adr/0006-tool-schemas-in-prompt-assembly.md create mode 100644 docs/adr/0007-quality-gates.md create mode 100644 docs/adr/README.md diff --git a/docs/adr/0001-vendor-cordis-as-source.md b/docs/adr/0001-vendor-cordis-as-source.md new file mode 100644 index 0000000000..28d6cdf83d --- /dev/null +++ b/docs/adr/0001-vendor-cordis-as-source.md @@ -0,0 +1,34 @@ +# ADR 0001: Vendor Cordis as source, not npm dependencies + +Status: accepted (2026-06-11) + +## Context + +DeepSeek Code is built on the Cordis framework. Cordis core was at 4.0.0-rc.6 +(a release candidate) when this repo started; the harness depends on framework +internals (fiber lifecycle, effect disposal, waterfall dispatch) whose exact +behavior matters to the agent loop's correctness guarantees. + +## Decision + +Copy the needed Cordis packages (core, loader, include, group, timer, hmr, +logger-console) and the cordiverse foundation libraries (cosmokit, +schemastery) into `vendor/` as source, flattened, keeping their original npm +names so workspace resolution is transparent. Truly third-party dependencies +(js-yaml, chokidar, @standard-schema/spec, …) stay on npm. + +`vendor/README.md` is the manifest: upstream repo + commit SHA per package and +an exhaustive local-modification log. A pre-commit guard +(`scripts/check-vendor-manifest.sh`) rejects vendored-source changes that +don't update the manifest in the same commit. + +## Consequences + +- The harness fully owns its framework layer: auditable, patchable, pinned — + an RC upstream can't break us, and we can fix framework bugs in-tree. +- Upstream sync is manual (documented procedure in the manifest). The + modification log keeps the diff surface known. +- Vendored packages keep upstream code style; lint/strictness gates exclude + them (their tsconfigs relax our newer compiler flags locally). +- One local patch exists from day one: hmr's locale-YAML imports removed (the + runtime YAML import hook isn't vendored). diff --git a/docs/adr/0002-microkernel-event-taxonomy.md b/docs/adr/0002-microkernel-event-taxonomy.md new file mode 100644 index 0000000000..0ddf1f4b6c --- /dev/null +++ b/docs/adr/0002-microkernel-event-taxonomy.md @@ -0,0 +1,38 @@ +# ADR 0002: Microkernel — extension via Cordis event taxonomy, one concrete loop + +Status: accepted (2026-06-11) + +## Context + +The product principle (see the 微内核Harness实现思路 design doc) is +"everything is a plugin": hooks, /goal, /loop, dynamic workflows, compaction, +sandboxing, permissions, UI, persistence, MCP, skills must all be writable as +plugins without modifying the core. Candidate mechanisms considered: a +purpose-built middleware stack (koa-compose style), an explicit phase state +machine plugins can insert into, or Cordis's native event system. + +## Decision + +Pure Cordis event taxonomy. The loop's extension seams are typed events with +deliberate dispatch modes: + +- **waterfall** (around-middleware) where plugins mutate or veto: + `agent/request`, `agent/step-result`, `agent/turn-continuation`, + `tools/execute`, `llm/stream`, `llm/generate`, `system-prompt/assemble`. +- **emit** (sync fire-and-forget) for notifications: turn/step boundaries, + stream chunks, lifecycle, errors. +- **parallel** (awaited) for the one durability checkpoint: `session/flush`. + +The event vocabulary lives in interface packages (dsh-agent declares the +agent/* events); `@deepseek-ai/dsh-agent-loop` is the only concrete plugin and +is itself swappable — nothing outside it may depend on it. + +## Consequences + +- Every MVP feature maps to a listener (the "plugin sanity checklist" in + docs/architecture.md is the proof obligation, kept current). +- HMR and disposal come free: listeners and registrations are Cordis effects. +- Waterfall semantics (call `next()` or short-circuit) are non-obvious and + must be taught — documented in AGENTS.md and covered by composition tests. +- The loop must be defensive: plugin exceptions are contained at turn level, + steering from any seam is never stranded (regression-tested). diff --git a/docs/adr/0003-event-sourced-sessions.md b/docs/adr/0003-event-sourced-sessions.md new file mode 100644 index 0000000000..e8046dc034 --- /dev/null +++ b/docs/adr/0003-event-sourced-sessions.md @@ -0,0 +1,38 @@ +# ADR 0003: Event-sourced sessions with derived message history + +Status: accepted (2026-06-11) + +## Context + +The MVP requires strict event-based tracing with fully replayable sessions +(严格的基于事件的trace、logging系统,session完全可回放). Two models were +considered: a mutable message array with events fired as notifications +(simpler, but state and log can diverge), or event-sourcing where the log IS +the state. + +## Decision + +A `Session` is an append-only log of typed `SessionEvent`s — the single +source of truth. The LLM message history is *derived* from the log +(`deriveMessages()`); raw stream chunks are logged for token-level replay +fidelity while the assembled `assistant/message` event is authoritative for +derivation. Replay/fork = seed a new session with an existing log. + +Appends are synchronous (the hot path never blocks on I/O); `session/event` +is a sync notification; persistence plugins buffer write-behind and drain at +the awaited `session/flush` checkpoint fired at every turn end. + +Ordering contract: the loop appends to the session *before* emitting the +corresponding Cordis event, and the `agent/step-result` waterfall runs before +the `assistant/message` append so the log records what tool dispatch actually +used (post-review fix; regression-tested). + +## Consequences + +- Replay, trace, and telemetry are structurally guaranteed, not bolted on. +- Persistence stays a plugin concern; the in-memory store ships in dsh-session. +- The event vocabulary is merge-extensible (plugins add e.g. compaction + events); it carries a TODO(review) marker until the first persistence + plugin and real adapter exercise it. +- Derivation cost grows with log length — compaction (future plugin) is the + intended mitigation, not log mutation. diff --git a/docs/adr/0004-own-content-block-vocabulary.md b/docs/adr/0004-own-content-block-vocabulary.md new file mode 100644 index 0000000000..f566081e7e --- /dev/null +++ b/docs/adr/0004-own-content-block-vocabulary.md @@ -0,0 +1,37 @@ +# ADR 0004: Provider-neutral content-block vocabulary owned by dsh-llm + +Status: accepted (2026-06-11) + +## Context + +The harness needs one internal language for messages that the loop, session +log, and all plugins speak. Options: mirror the DeepSeek/OpenAI +chat-completions shape (zero mapping for the first provider, awkward for rich +content), adopt Anthropic's Messages block structure verbatim (battle-tested, +but our canonical types would mirror a third-party API we don't target +first), or own a vocabulary. + +## Decision + +Own it: messages are arrays of typed content blocks (`text`, `reasoning`, +`tool-call`, `tool-result`, `image`), with the union derived from the +merge-extensible `ContentBlockMap` so plugins add block types via declaration +merging. The same merge-extensible-map pattern types every "stringly" field +(`MessageSource`, `FinishReason`, `TurnTrigger`, `TurnEndReason`). Streaming +is a raw chunk protocol; `BlockAssembler` is the single shared assembly +implementation. Adapters translate to provider wire formats — mapping cost +lives in adapters, where it belongs. + +In-session context injection (`context/message`, `steering/message`) renders +as tagged user-role envelopes (the system-reminder pattern) rather than a new +role, so adapters carry zero burden. TODO(review): revisit once the DeepSeek +V4 adapter exists. + +## Consequences + +- Reasoning, prefill, cache hints, and multimodal content all have a home + without provider contortions. +- Every adapter pays a translation cost; the streaming protocol carries a + TODO(review) marker until the first real adapter validates it. +- IDs that cross package boundaries are branded (`CallId`, `SessionId`, + `AgentId`) — nominal typing at zero runtime cost. diff --git a/docs/adr/0005-custom-schema-dsl-over-schemastery.md b/docs/adr/0005-custom-schema-dsl-over-schemastery.md new file mode 100644 index 0000000000..9d7011a6db --- /dev/null +++ b/docs/adr/0005-custom-schema-dsl-over-schemastery.md @@ -0,0 +1,35 @@ +# ADR 0005: Custom typed tool-schema DSL instead of schemastery + +Status: accepted (2026-06-11) + +## Context + +Tool parameters must reach the model as standard JSON Schema (the wire +format), and tool authors deserve typed `execute(args)` without casts. The +repo already vendors schemastery (used for plugin Config), so reusing it was +the obvious candidate. The user also explicitly preferred per-property +`required: true` booleans over JSON Schema's separate `required` array. + +## Decision + +A small custom DSL in dsh-tools: `SchemaSpec` (per-property specs with +`required: true` booleans), type-level `InferArgs` mapping a spec to the +argument type (required keys non-optional, others genuinely optional via `?`), +a runtime `schemaSpecToJsonSchema()` converter, and `defineTool()` tying them +together. Raw JSON-Schema `ToolDefinition`s remain accepted by +`ToolRegistry.register()` — that's how MCP-sourced tools arrive. + +Schemastery was evaluated and rejected for this use: it targets validation / +transformation against StandardSchema, not JSON Schema *generation*, so it +would add indirection without producing the wire format cleanly. + +## Consequences + +- First-party tool authors get zero-cast typed args; the type gymnastics cost + stays inside the core package (sanctioned by the AGENTS.md type-safety + policy). +- The DSL is deliberately small (string/number/boolean/object/array, enum, + default, nested properties/items). Gaps vs full JSON Schema (unions, + formats, constraints) are accepted until real tools demand them. +- The InferArgs mapping is regression-tested at the type level (expectTypeOf) + after an early optionality bug shipped and was caught by review. diff --git a/docs/adr/0006-tool-schemas-in-prompt-assembly.md b/docs/adr/0006-tool-schemas-in-prompt-assembly.md new file mode 100644 index 0000000000..b6158a66ec --- /dev/null +++ b/docs/adr/0006-tool-schemas-in-prompt-assembly.md @@ -0,0 +1,31 @@ +# ADR 0006: Tool schemas are part of the system-prompt assembly + +Status: accepted (2026-06-11) + +## Context + +On the wire, tool schemas travel in a dedicated `tools` field of the model +request, not in prompt text. Architecturally, though, "what the model is told +it can do" is one coherent concern: prompt sections and the tool list are +assembled from the same plugin contributions and consumed at the same moment. +The alternative — the loop querying the tool registry separately from the +prompt service — splits one concern across two seams. + +## Decision + +`PromptAssembly { sections, tools }`: the system-prompt service collects +ordered text sections AND tool schemas (the tool registry auto-contributes a +provider). The loop consumes one assembly per step; adapters map `sections` +to the provider's system slot and `tools` to the wire `tools` field. The +`system-prompt/assemble` waterfall is therefore a single interception point +for everything the model is told up front — tool filtering (ToolSearch / +progressive disclosure) is an assembly rewrite, same as prompt edits. + +## Consequences + +- One waterfall governs the model's standing context; plugins like plan mode + can swap prompt text and visible tools in one listener. +- The assembly interface is merge-extensible for future slots (no untyped + `extras` bag — extension is declaration merging). +- Slight conceptual surprise (schemas in a "prompt" service) is documented + here and in the package README. diff --git a/docs/adr/0007-quality-gates.md b/docs/adr/0007-quality-gates.md new file mode 100644 index 0000000000..357b34177c --- /dev/null +++ b/docs/adr/0007-quality-gates.md @@ -0,0 +1,36 @@ +# ADR 0007: Mechanical quality gates over prose guidelines + +Status: accepted (2026-06-11) + +## Context + +This codebase is developed primarily by coding agents. Agents follow enforced +gates far more reliably than prose conventions, and "a lot of work" is not a +cost argument when agents do the labor. Early evidence: tests that didn't +typecheck shipped (vitest doesn't typecheck) and were only caught by a review. + +## Decision + +Every AGENTS.md promise gets a command that exits non-zero, wired into git +hooks and CI both calling the same package.json scripts: + +- Max-strict TypeScript (`noUncheckedIndexedAccess`, + `exactOptionalPropertyTypes`, …); tests and examples typecheck in CI via + `tsconfig.typecheck.json` (vendored packages resolve as built declarations). +- ESLint strict-type-checked + @stylistic (the house style, enforced); + vendored code excluded. +- Per-file 100% coverage on `packages/*/src` (v8); unreachable defensive + guards carry `/* v8 ignore */ ` with stated reasons instead of deletion. +- knip (dead code/deps), publint (package correctness), yarn constraints + (workspace rules: private, cordis peer+dev, uniform version, ESM). +- lefthook pre-commit (lint staged, typecheck, vendor-manifest guard) and + pre-push (tests, hygiene); CI runs the full matrix on node 24/26 plus a + demo smoke test driving the echo-agent end to end. + +## Consequences + +- Conventions survive agent turnover; violations fail fast and locally. +- The gates themselves are code to maintain; config changes are reviewed like + any change. +- 100%-coverage pressure can produce assertion-free tests — mutation testing + is the planned counterweight (see RFC 002). diff --git a/docs/adr/README.md b/docs/adr/README.md new file mode 100644 index 0000000000..607780a3f7 --- /dev/null +++ b/docs/adr/README.md @@ -0,0 +1,19 @@ +# Architecture Decision Records + +Short, immutable records of the *why* behind decisions that shape this +codebase. Code and docs say what the system does; ADRs say why it does it +that way and what we gave up. + +Format: one file per decision, numbered, with Status / Context / Decision / +Consequences. An ADR is never edited into a different decision — supersede it +with a new one and cross-link. + +| # | Title | Status | +|---|---|---| +| [0001](0001-vendor-cordis-as-source.md) | Vendor Cordis as source, not npm dependencies | accepted | +| [0002](0002-microkernel-event-taxonomy.md) | Microkernel: extension via Cordis event taxonomy, one concrete loop | accepted | +| [0003](0003-event-sourced-sessions.md) | Event-sourced sessions with derived message history | accepted | +| [0004](0004-own-content-block-vocabulary.md) | Provider-neutral content-block vocabulary owned by dsh-llm | accepted | +| [0005](0005-custom-schema-dsl-over-schemastery.md) | Custom typed tool-schema DSL instead of schemastery | accepted | +| [0006](0006-tool-schemas-in-prompt-assembly.md) | Tool schemas are part of the system-prompt assembly | accepted | +| [0007](0007-quality-gates.md) | Mechanical quality gates over prose guidelines | accepted |