3567d000c4
The vocabulary maps grow by declaration merging, and the admission policy stated on TurnEndReasonMap is that a variant lands with its first emitter. Three declared items had no producer and no consumer: - CacheHint and the cache?: CacheHint fields on TextBlock/ToolResultBlock: nothing constructs a block with cache:, and neither adapter reads .cache — DeepSeek prompt caching is automatic (hints map OUT of responses, never IN). - MessageSourceMap.agent: zero constructors; the subagent backends send the parent prompt with no source (logs as user), and the envelope renderer interpolates source.kind without routing on it. - TurnTriggerMap.continuation: the loop structurally cannot emit it — continuation is further steps within a turn, never a new turn — and its only writer was an llm-replay test fixture that needed any non-message trigger (now an injection trigger). Each variant returns the day it gains a real producer, via the same merge-extensible maps. Docs updated in the same change: the MessageSourceMap paste in core.md, the TurnTriggerMap paste in session.md (manifest untouched — both symbols survive), the content-block vocabulary RFC's cache-hints consequence line, and the RFC moved to implemented/ and amended to shipped reality (the image block's own cache field had already left with the drop-image RFC).
22 lines
2.7 KiB
Markdown
22 lines
2.7 KiB
Markdown
# RFC: Provider-neutral content-block vocabulary owned by dsh-llm
|
|
|
|
Status: implemented (accepted 2026-06-11)
|
|
|
|
<!-- XXX: legacy ADR/RFC body format, not yet normalized to a unified RFC template. -->
|
|
|
|
## 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`), 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. Live-adapter review has since validated the tagged-envelope rendering against current DeepSeek behavior; a future provider-specific mismatch should be handled in that adapter rather than by adding a new role to the canonical content vocabulary.
|
|
|
|
## Consequences
|
|
|
|
- Reasoning and prefill have a home without provider contortions. Multimodal content deliberately has NO core block type: the core set is limited to blocks every shipping path honors, and a multimodal feature adds its block type through the merge-extensible map in the same coordinated change that maps it in the adapters, surfaces it in the UI bridges, and prices it in compaction — see [the drop-image RFC](../simplification/2026-07-04-drop-image-content-block.md). Block cache hints likewise have no core field: DeepSeek prompt caching is automatic, so no shipping adapter can transmit a hint; a caching feature adds a `cache` field together with the adapter that honors it — see [the producer-less-variants RFC](../simplification/2026-07-04-prune-producerless-vocabulary-variants.md).
|
|
- Every adapter pays a translation cost; the first real adapters have since validated the streaming protocol, and new adapters should continue proving their provider-specific mapping in adapter-local tests.
|
|
- IDs that cross package boundaries are branded (`CallId`, `SessionId`, `AgentId`) — nominal typing at zero runtime cost.
|