Collapse docs/adr/ and docs/rfc/ into a single docs/rfc/ with proposed/, implemented/, and rejected/ subfolders. Every file is renamed to yyyy-mm-dd-topic-title.md, where the date is when the topic was first proposed (from git history). ADRs and RFCs that covered exactly the same topic are merged (property-based testing, session persistence); the umbrella RFC 005 stays split across its three implemented decisions, and RFC 006's deferred part-3 (API extractor reports) splits into its own proposed RFC. All cross-references become machine-checkable relative links instead of bare "ADR NNNN" / "RFC NNN" prose. Add a verify-md-links doc-sync gate (scripts/verify-md-links.ts) that checks every relative Markdown cross-link resolves, wired into doc-sync alongside verify-md-wrap. This makes the reorganization self-verifying: the same change that rewrote ~forty inter-doc links adds the check that proves none dangle. Document the cross-link convention in a new docs/AGENTS.md and record the gate as an implemented RFC. doc-sync, typecheck, lint, and the full test suite (667) all pass.
1.8 KiB
RFC: Provider-neutral content-block vocabulary owned by dsh-llm
Status: implemented (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.