Skip to content

Commit 03ca7db

Browse files
docs(cache-policy): CK Message field inventory (MC-grounded draft)
The lossless encode/decode contract for the harness-agnostic MC Transform - the CK-Message analog of the golden vectors, for the MC-under-SUBC migration. Provider-neutral field inventory bucketed (a) content / (b) downstream-emitter typed-policy / (c) loop-durability invariant, reconciled against llm-runner's request canonical (Message/ContentBlock/CallOptions/FrozenRenderConfig). Headlines the keystone conformance invariant render(project(ck))==render(project(strip_ harness_flags(ck))). EmptyContent is the one net-new typed policy with MC as the reference spec (#135 modelAcceptsEmptyContent drop-shape). Flags the multimodal image-block projection gap (llm-runner's ContentBlock set has no image block) and documents that on the plugin path MC gap-fills only (assembly-aware, never assembly-performing - verified: role always preserved), while full family assembly lives in the owned/MITM target. Reasoning-strip carries a positional is_last_assistant_turn render-param frozen at freeze time. Draft for reconciliation; canonical-unification + multimodal scope routed to major-arch review. Co-authored-by: Alfonso [Magic Context] <288211368+alfonso-magic-context@users.noreply.github.com>
1 parent 09b5889 commit 03ca7db

1 file changed

Lines changed: 191 additions & 0 deletions

File tree

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
# CK Message field inventory (MC-grounded, draft for reconciliation)
2+
3+
The companion to `cache-stability-golden-vectors.json`. The golden vectors pin
4+
the cache *mechanics* (anchor/freeze/replay/classify). This pins the *message
5+
representation* those mechanics operate on: the lossless `encode`/`decode`
6+
contract for the one harness-agnostic MC Transform.
7+
8+
Status: **draft, for reconciliation** against llm-runner's request canonical.
9+
The canonical-unification decision (CK == llm-runner canonical, vs CK a superset
10+
with llm-runner's a projection) is the major-architecture call; this draft is
11+
written to the **converged direction: CK is the superset, llm-runner's canonical
12+
is the byte-affecting projection.**
13+
14+
## The flow it serves
15+
16+
There is one transform, one canonical, and provider knowledge lives downstream:
17+
18+
```
19+
harness msg --encode--> CK Message --[MC Transform]--> CK Message --decode--> harness msg --> harness serializer --> wire
20+
\--(own-harness / MITM)--> [downstream quirk pass] --> provider wire
21+
```
22+
23+
The MC Transform is **provider-neutral**: it freezes CK-level units (a logical
24+
"drop of tag N"), never provider-specific bytes. The provider-correct rendering
25+
(empty part vs `[dropped]` text, Gemini name-binding vs id-binding) is a
26+
**downstream quirk pass** that consumes llm-runner's typed policies. Placement of
27+
that pass is flexible (Rust after the MC pass, plugin on subc-return, or
28+
llm-runner's framework for the own-harness path); it is not part of the cache
29+
core.
30+
31+
## The keystone conformance invariant
32+
33+
CK is a superset; llm-runner's canonical is the byte-affecting projection. The
34+
invariant that keeps the superset non-lossy (the CK analog of the render
35+
goldens, and a checkable conformance test):
36+
37+
```
38+
render(project(ck)) == render(project(strip_harness_flags(ck)))
39+
```
40+
41+
Stripping every CK field that is NOT in the projection (harness-only flags:
42+
`synthetic`, harness message id, ordinal) must NEVER change the rendered bytes.
43+
If it does, that field was mis-bucketed as harness-only when it is actually
44+
content.
45+
46+
## The buckets
47+
48+
Every byte-relevant field is exactly one of:
49+
50+
- **(a) CONTENT** - round-trips in CK verbatim and is in llm-runner's projection.
51+
Renders to provider bytes; cannot be stripped without changing bytes.
52+
- **(b) DOWNSTREAM-EMITTER POLICY** - a render-time typed-policy *unit decision*,
53+
NOT carried in CK as content. Reused from llm-runner's policy layer at
54+
per-unit granularity (`decide_reasoning`, `decide_drop_shape`), not whole-body
55+
`shape()`.
56+
- **(c) LOOP / DURABILITY INVARIANT** - guaranteed by canonical normalization,
57+
target-agnostic, NOT a wire policy.
58+
- **(param) RENDER PARAM** - render input that rides run-config / the frozen
59+
render config, never a CK message field.
60+
- **(flag) HARNESS-ONLY** - in the CK superset, stripped in the projection; the
61+
keystone invariant forbids it from affecting rendered bytes.
62+
63+
## Field table
64+
65+
| CK field | bucket | llm-runner mapping | consuming policy / invariant | render target per path |
66+
|---|---|---|---|---|
67+
| message `role` (System/User/Assistant/Tool) | a | `Message.role` | - | content, all paths |
68+
| text part `text` | a | `ContentBlock::Text{text}` | - | content, all paths |
69+
| `§N§` tag prefix | a (literal text *inside* a Text block, NOT a field) | part of `Text.text` | a changed tag number IS a bust | content, all paths |
70+
| reasoning `text` + `signature` | a | `ContentBlock::Reasoning{text, signature}` | signature byte-verbatim | content, all paths |
71+
| redacted reasoning `data` | a | `ContentBlock::RedactedReasoning{data}` | data byte-verbatim | content, all paths |
72+
| tool call `id` / `name` / `input` | a | `ContentBlock::ToolCall{...}` | - | content, all paths |
73+
| tool result `id` / `output` / `is_error` | a | `ContentBlock::ToolResult{...}` | - | content, all paths |
74+
| **part ORDER within a message** | a (structural) | `Vec<ContentBlock>` order | order is byte-affecting (Anthropic rejects thinking-after-text) | content, all paths |
75+
| **image / file part (base64)** | a | **NO llm-runner ContentBlock twin (the projection gap, see below)** | `stripProcessedImages` is a frozen unit over these | content, all paths |
76+
| drop / empty-content shape | **b** | net-new typed policy (`EmptyContent`) | `decide_drop_shape(provider, drop) -> EmptyPart \| DroppedTextPart`; **MC is the reference spec** (#135) | emitter: empty part (Anthropic/Bedrock) vs `[dropped]` text (Copilot) |
77+
| reasoning last-turn strip / clear | **b** + positional render-param | `ReasoningPolicy` (`decide_reasoning`) | Anthropic merges consecutive assistants and rejects thinking-after-text; OpenAI-compat may require non-empty `reasoning_content`. **Decision depends on `is_last_assistant_turn` (body-derived); frozen at freeze time, never re-derived (see below).** | emitter: literal `[cleared]`/empty per provider |
78+
| tool-call/result pairing completeness | **c** | `synthesize_hard_stop` core invariant | every `tool_use` has a matching `tool_result` pre-send; MC's skeleton-drop + newest-20 reserve preserve adjacency | canonical normalization, target-agnostic |
79+
| `cache_control` breakpoint placement | param | `CachePolicy` | normalized OUT of the bust decision (golden vectors V7); the renderer places it | emitter / render-param |
80+
| `step_context` (run_id, step_index) | param | `StepContext` (serde-skip; only Antigravity reads it) | render input, not content | render-param |
81+
| `synthetic` | flag | (none - stripped in projection) | harness titling-gate + TUI visibility; keystone invariant forbids byte effect | harness-only |
82+
| harness message `id` / `ordinal` | flag | (none - stripped in projection) | MC anchoring / tagging / `computeRawRangeFingerprint`; not on the provider wire | harness-only |
83+
84+
## EmptyContent: the one net-new policy (MC is the reference)
85+
86+
llm-runner has no twin for this, so MC's `modelAcceptsEmptyContent` is the
87+
reference spec. The per-unit decision the downstream pass needs llm-runner to
88+
expose:
89+
90+
```
91+
decide_drop_shape(provider, logical_drop_unit) -> EmptyPart | DroppedTextPart
92+
```
93+
94+
- `EmptyPart`: providers whose serializer drops empty content blocks
95+
(Anthropic, Bedrock). The dropped unit becomes an empty content part the
96+
provider/serializer removes, preserving tool adjacency.
97+
- `DroppedTextPart`: providers whose serializer does NOT drop empty parts and
98+
break tool-use adjacency on them (Copilot / OpenAI-compatible passthroughs,
99+
issue #135). The dropped unit becomes a literal `[dropped §N§]` text part.
100+
101+
The choice is per (provider, unit), constant within a cache epoch: the provider
102+
is fixed across defer passes, and a provider/model switch is a HARD bust, so the
103+
emitter cannot vary mid-epoch. That fixed-determinism is what makes CK-level
104+
byte-stability imply wire byte-stability uniformly (no per-path vector binding).
105+
106+
## Where assembly happens: plugin gap-fills, owned/MITM assembles
107+
108+
Family *assembly* (Anthropic pulls System-role messages to a top-level `system`
109+
field; tool-role messages reshape into a user-turn carrying `tool_result`
110+
blocks; consecutive assistants merge) is real, but **on the plugin path MC does
111+
not do it** - verified at source: MC strips/clears/drops parts *in place*, role
112+
always preserved (the `role !== "user"` guards in `strip-content.ts`), never a
113+
top-level `system` pull, never a tool-role reshape. The harness serializer
114+
(OpenCode's `@ai-sdk` adapter) does assembly downstream of MC. So the split is:
115+
116+
- **Owned / MITM path:** the downstream pass renders the FULL provider request
117+
via llm-runner's family renderer - assembly (system-pull, tool-role reshape,
118+
merge) INCLUDED. This is `shape()` whole-body; llm-runner owns it.
119+
- **Plugin path:** the harness serializer does assembly. MC's quirk pass is
120+
**gap-fill ONLY** - it applies the per-unit decisions the harness serializer
121+
gets wrong or does incompletely (EmptyContent drop-shape #135; reasoning
122+
handling), and the role-preserved array it hands back is then assembled by the
123+
harness. The plugin quirk pass must NOT replicate family assembly rules; that
124+
would double-assemble.
125+
126+
The one nuance: some plugin gap-fills are **assembly-AWARE** without being
127+
assembly-PERFORMING. `stripReasoningFromMergedAssistants` exists precisely
128+
because the harness/provider MERGES consecutive assistants and Anthropic rejects
129+
thinking repositioned after text - so MC strips reasoning from all-but-the-first
130+
merged assistant *before* the harness merges. MC must KNOW the merge will happen
131+
(to gap-fill correctly) but does not perform it. So: plugin quirk pass = gap-fill,
132+
sometimes assembly-aware, never assembly-performing.
133+
134+
Inventory consequence: the "render target per path" column reads, for plugin,
135+
"gap-fill; harness assembles" (and for the reasoning row, "assembly-aware:
136+
compensates for harness assistant-merge"). Full family assembly lives only in the
137+
owned/MITM target.
138+
139+
## Reasoning-strip: the one policy carrying a positional render-param
140+
141+
The Anthropic last-turn thinking strip is bucket (b), but it is the one policy
142+
whose decision is not unit-pure: the *shape* is unit-pure, but the *trigger*
143+
(strip thinking from all but the last assistant turn) is **body-derived** -
144+
`is_last_assistant_turn`, an `rposition` over non-system messages. This is
145+
Anthropic-specific and a single bit; other families' reasoning is `None` or a
146+
top-level Responses object with no per-message strip.
147+
148+
Critically, this bit is **frozen at freeze time, never re-derived per pass** -
149+
it is exactly the byte-identity hazard the golden vectors guard against. If the
150+
downstream pass re-computed `is_last_assistant_turn` on a later defer pass after
151+
the tail grew, the strip decision for a previously-frozen assistant turn could
152+
flip, mutating cached-prefix bytes (a bust). So the reasoning-strip frozen unit
153+
is frozen as `(reasoning_block + is_last_assistant_turn)`, and that positional
154+
param rides alongside exactly like `step_context` (render-input, not content,
155+
not whole-body hidden state). MC's freeze must capture the one bit at freeze
156+
time.
157+
158+
## The projection gap llm-runner must resolve: multimodal
159+
160+
llm-runner's relayed `ContentBlock` set is
161+
`Text | Reasoning | RedactedReasoning | ToolCall | ToolResult` - **no image /
162+
file / document block.** MC carries multimodal image parts (base64) in the
163+
message array, and `stripProcessedImages` is a frozen unit operating on them
164+
(the screenshot-in-a-user-message cache scar). Image parts are bucket **(a)
165+
CONTENT** (they render to provider bytes), so they cannot be a harness-only flag
166+
stripped in the projection. Therefore the **projection must grow an image/file
167+
ContentBlock**, or multimodal input is declared out-of-scope for the bytes-owned
168+
paths initially (in which case plugin paths STILL need it in CK for the harness
169+
round-trip, so it stays a CK content field either way). llm-runner owns this
170+
call; flagged here as the one place the byte-affecting projection needs to grow.
171+
172+
## Open decisions (routed to Ufuk, major-arch)
173+
174+
1. **Canonical unification:** CK == llm-runner canonical, or CK superset with
175+
llm-runner's a projection (this draft assumes superset).
176+
2. **Multimodal scope:** grow llm-runner's projection with an image/file block
177+
now, or defer multimodal on bytes-owned paths.
178+
3. **Quirk-pass placement default:** Rust-after-MC vs plugin-on-return vs
179+
per-harness (own-harness uses llm-runner's framework regardless).
180+
181+
## Provenance
182+
183+
Field set extracted from MC's actual wire handling: `strip-content.ts`
184+
(reasoning clear/replay, structural-noise, processed-image strip), `sentinel.ts`
185+
+ `transform.ts:803` / `transform-postprocess-phase.ts:384`
186+
(`modelAcceptsEmptyContent` drop-shape, the #135 scar), `apply-operations.ts`
187+
(`buildReplacementContent`, the `[dropped §N§]` placeholder), `tagger.ts` (the
188+
`§N§` tag text), `inject-compartments.ts` (the `synthetic` prepend flag),
189+
`read-session-true-raw-tokens.ts` (`computeRawRangeFingerprint`, the harness-id /
190+
ordinal anchor). Reconciled against the relayed llm-runner request canonical
191+
(`Message` / `ContentBlock` / `CallOptions` / `FrozenRenderConfig`).

0 commit comments

Comments
 (0)