Przeglądaj źródła

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).
Tianyi Cui 4 miesięcy temu
rodzic
commit
9b8fccc6f9

+ 34 - 0
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).

+ 38 - 0
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).

+ 38 - 0
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.

+ 37 - 0
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.

+ 35 - 0
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<S>` 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.

+ 31 - 0
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.

+ 36 - 0
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).

+ 19 - 0
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 |