ImageBlock had no production producer and every consumer dropped it: the deepseek serializer skipped it, the pi-ai converter skipped it as unrepresentable, the ACP bridge neither advertises image prompt capability nor forwards image blocks, and compact-basic charged a flat 85-token estimate and rendered an [image] placeholder. A block constructed today would silently vanish from the wire — the vocabulary advertised a capability no path honors, the silent-data-loss shape the defensive patterns warn against. The only constructors were tests pinning the skip/estimate branches. Remove ImageBlock and its ContentBlockMap entry (its cache?: CacheHint field leaves with it; CacheHint itself and the other two cache? fields are out of scope). compact-basic loses its explicit image estimate and placeholder arms (the merge-extensible default arms absorb the case); the deepseek serializer, pi-ai converter, and ACP codec already handled image in their default arms, so only their image-naming comments change. The codec's inbound rejection of ACP-protocol image prompt content stays — that guards wire content a client can send regardless of our vocabulary. Tests that constructed harness image blocks to pin the removed branches are dropped (the 85-token estimate pin) or retargeted onto plugin-added block types / other non-text blocks, which the surviving default arms own. Docs, the type-equiv pastes, and the content-block vocabulary RFC's block list and multimodal-home consequence are updated in the same change; the RFC moves to implemented/ and the index is regenerated. A real multimodal feature reintroduces image via declaration merging together with the adapter mapping, ACP advertisement, and compaction pricing that honor it.
5.0 KiB
RFC: Prune producer-less vocabulary variants (block cache hints, the agent message source, the continuation turn trigger)
Status: proposed
Problem
The merge-extensible vocabulary maps are designed to grow by declaration merging, and the codebase already states the admission policy on TurnEndReasonMap (packages/core/session/src/types.ts): a variant like refusal is "deliberately omitted until" an adapter or loop first emits it. Three declared vocabulary items violate that policy — each has no producer and no consumer, and two have not even a test:
CacheHintand the threecache?: CacheHintfields onTextBlock/ToolResultBlock/ImageBlock(packages/llm/llm/src/types.ts). Nothing constructs a block withcache:anywhere — src, tests, and doc pastes all come up empty — and neither adapter reads.cache: DeepSeek prompt caching is automatic, so the adapters mapprompt_cache_hit_tokensOUT of responses without ever sending a hint IN. This is Anthropic-stylecache_controlsurface with no provider that can honor it.MessageSourceMap.agent({ kind: 'agent'; agentId: string }, same file). Zero constructors, tests included. Its intended producer shipped without it: the subagent backends send the parent's prompt to the child with nosource, so it logs as{ kind: 'user' }, and the generic envelope renderer interpolatessource.kindwithout ever routing on it. The variant is pasted into core.md.TurnTriggerMap.continuation(packages/core/session/src/types.ts). The loop structurally cannot emit it — continuation happens within a turn as further steps, never as a new turn — and it constructs onlymessageandinjectiontriggers. The only writer is one hand-built test fixture that needs an arbitrary non-message trigger (packages/support/llm-replay/tests/llm-replay.spec.ts); the only production trigger reader, the ACP bridge, filters onkind === 'message'. The variant is pasted into session.md.
Proposal
Delete CacheHint with its three cache? fields, the agent message-source variant, and the continuation turn-trigger variant. Switch the llm-replay fixture to an injection trigger (any non-message trigger serves its purpose). Update the type-equiv pastes in core.md and session.md (and scripts/type-equiv.manifest.json where block identity shifts) in the same change, and amend the content-block vocabulary RFC's consequence line naming cache hints as having a home, per implemented/AGENTS.md.
Each variant returns the day it gains a real producer, exactly as the maps are designed to grow: a caching feature re-adds cache together with the adapter that transmits it; subagent attribution re-adds agent together with the backend that stamps it and a consumer that routes on it; an auto-continue feature that genuinely starts new turns re-adds continuation with the plugin that emits it.
Why not keep them?
The content-block vocabulary RFC lists "cache hints … have a home" as a design consequence, and reserved slots do advertise intent. But an empty slot is contract surface every implementation and consumer must consider (must my adapter honor cache? must my renderer route agent sources?), and the sibling map's own JSDoc already rejects reservation-without-emitter — refusal and max_turn_requests are named as variants to add when something first emits them, not declared in advance. Holding already-declared dead variants to the same standard makes the vocabulary mean something: if it is in the map, something produces it.
Acceptance criteria
rgforCacheHint, theagentmessage-source spelling, and thecontinuationtrigger spelling returns only this RFC.- The core-data-structures pastes and the type-equiv manifest are in sync (
pnpm run doc-syncgreen). - The fixture asserts the same replay behavior with an
injectiontrigger; the suite is green.
Risks
None operational — nothing can construct these values today. The mirror-event removals (recorded in the boundary-mirror RFC and the stream-chunk RFC) touch only transient agent/* events, never the durable vocabulary, so there is no collision. Elsewhere in the vocabulary the admission policy already holds: rejected, prompt/blocked, and hook/invoked/hook/result each have live producers — this RFC extends the same bar to the three variants that lack one. If the image-block RFC ships first, one of the three cache? fields leaves with it; the two proposals are independent and compose in either order.