This document is the contract for how ash shapes its responses so an
agent's conversation transcript can hit the Anthropic prompt cache. It
is the cache-shape companion to vocab/inventory.md:
inventory pins per-token cost on every output literal; this pins where
those literals are positioned in the encoded bytes so the cache
prefix matches across consecutive identical calls.
The Anthropic prompt cache charges roughly 10× less on cached input tokens (5-minute TTL) than on uncached. The 2026-05-13 encoding measurement also found Claude tokenizes ash output ~19% heavier than the cl100k_base proxy in the ledger (see encoding-results.md) and the 2026-05-15 wire-cost measurement found MCP-emitted envelopes are ~3.4× the CLI cost (see mcp/wire-cost.md). Restructuring the response envelope so volatile fields (random request IDs, per-call timings) ride at the tail of the encoded bytes lets two consecutive identical calls share a long matching prefix that the cache can latch onto. The win compounds across a conversation: every grep+read+stat sequence the agent runs becomes mostly-cached input after the first run.
Every ash response — over every surface — has three zones, in order:
- Stable prefix. Bytes that are identical for two calls with identical inputs. The protocol version, the OK flag, and the verb- specific result data live here. This is the part the cache latches onto.
- Bounded-stable middle. Verb data that depends on the world (file contents, file lists, git output) but not on the call's instrumentation. Stable across calls when the world doesn't change.
- Volatile suffix. Per-call telemetry that cannot match across calls by construction: the random request ID, latency microseconds, tokens-in/-out counts, sub-phase breakdowns. This goes at the end of the encoded bytes, never in the middle.
If a field is volatile, it must come after every stable field. A volatile field in the middle of the envelope wrecks the cache contract — the matching prefix ends at the first volatile byte.
The table below classifies every field across the three encoded surfaces an agent might consume. Fields are listed in the order they appear in encoded output.
| Field | Zone | Why |
|---|---|---|
V |
stable prefix | protocol version constant |
OK |
stable prefix | per-call outcome, stable for matching inputs |
Data |
bounded-stable | verb result; deterministic for fixed file state |
Err |
stable prefix | error code + msg; stable for matching inputs |
ID |
volatile suffix | random uint64 per request |
Metrics |
volatile suffix | latencies, token counts, ledger errors — vary per call |
This is the internal UDS frame and not directly agent-visible, but its struct-field order is the source of truth for the JSON and MCP surfaces below.
| Field | Zone | Notes |
|---|---|---|
v |
stable prefix | mirrors proto.Response.V |
ok |
stable prefix | |
data |
bounded-stable | decoded verb result, not raw bytes |
err |
stable prefix | |
id |
volatile suffix | at the tail so the JSON prefix matches cross-call |
metrics |
volatile suffix | at the very end |
| Field | Zone | Notes |
|---|---|---|
Content[].TextContent.Text |
bounded-stable | proto.MCPEnvelope(rsp) — JSON of rsp.Data only (no envelope wrap) |
StructuredContent |
bounded-stable | decoded rsp.Data for harnesses that validate against outputSchema |
IsError |
stable prefix | mirrors !rsp.OK |
Meta["ash"]["metrics"] |
volatile, OUT | metrics ride in MCP _meta, not in model-visible content |
Meta["ash"]["truncated"] |
volatile, OUT | truncation hint, also out of model context |
The MCP path is the strictest realization of the contract: everything
the model sees is in the bounded-stable middle. Per-call telemetry
goes through _meta, which MCP-aware harnesses keep out of the
agent's conversation context. This is the surface most adoption sits
on (ASH-104) and the one prompt caching helps most.
| Stream | Zone | Notes |
|---|---|---|
| stdout | stable prefix + bounded-stable | verb pretty body — header (§<verb>: …) then result rendering |
| stdout | stable prefix | error responses are err <code>\n<msg> only |
| stderr | volatile | [ash bi=… bo=… ti=… to=… us=…] metrics line on the trailing line |
Pretty already keeps timing on stderr; the stdout body is cache-stable for identical inputs.
Three rules. Read them as a checklist before touching response shape.
- No volatile field in the middle of the envelope. If a new field
varies per call (random IDs, wall-clock timestamps, latency
microseconds), it must come after every stable field in struct
declaration order. Both
msgpack/v5andencoding/jsonpreserve field order, so struct ordering is the wire ordering. - Metrics ride in a separate channel. For pretty, stderr. For
MCP,
_meta(protocol-reserved metadata, out of model context). For JSON-formatted CLI output, last in the envelope. The agent should never have to read the metrics dict to reach the next verb's response. - Verb result data is deterministic for fixed inputs. If a verb
reaches into the world it must reach deterministically: sorted
results, stable ordering, no timestamps inside the data payload.
Pretty headers (
§<verb>: …) and ASCII-only literals do not change across calls. Anything that would change (path-prefix tax, truncation hint) is structured (proto.TruncInfo) so it can move to_metaor be omitted.
The ledger has two columns reserved for prompt-cache accounting:
tokens_cache_hit— Anthropic'susage.cache_read_input_tokensfor the message that included this call.tokens_cache_miss—usage.cache_creation_input_tokensfor the same message.
Both default to zero. Daemon-originated rows leave them at zero
because the daemon does not see Anthropic API responses. The
populator is ash usage (ASH-134): the agent calls
ash usage --hit <cache_read_input_tokens> --miss <cache_creation_input_tokens>
after observing the numbers in an Anthropic API response, and the
daemon UPDATEs tokens_cache_hit / tokens_cache_miss on the most
recent non-usage row in the current session. --for <request_id>
overrides the default "most recent" target to annotate a specific
prior call. We chose this explicit-verb path over an
ashmcp-reads-_meta path because (a) it works regardless of
harness (CLI/bash invocation gets the same surface as MCP), (b) it
keeps the daemon free of a back-channel control verb, and (c) it
is straightforward to test end-to-end.
When at least one row in the report window has non-zero cache
numbers, ash report adds a cache: line with the per-window
hit ratio. ash metrics widens the per-row table with ch / cm
columns under the same gate. CLI-only sessions today produce
byte-identical pretty output to the pre-ASH-108 surface.
internal/proto/proto_test.go carries TestResponse_CacheStableWirePrefix,
which encodes two Response values that differ only in ID and
Metrics and asserts that the encoded byte sequences share a long
common prefix. Any field reordering that pushes a volatile field
earlier shortens the matching prefix and the test fails — surfacing
the cache-contract break at PR review time.
- Wire path for harness-reported cache stats. Resolved by
ASH-134: see the
ash usagedescription in "Ledger telemetry" above. - Measuring the actual cache-hit % on real Claude Code sessions.
That needs MCP-side hookup plus replay
(ASH-112) to A/B the
same task with and without cache-aware envelope. The structural
shape change is the prerequisite; the empirical scoreboard is the
next ticket. Update (ASH-135): the structural A/B is now
available via
ash replay --cache_prefix true— it encodes each replayed response under both the current envelope and a struct mirroring the pre-ASH-108 ordering, then reports the average matching byte-prefix per verb. Measured against a real session of 376 stable consecutive same-verb pairs: legacy ordering shared 8 bytes per pair on average; current ordering shared 775 bytes — a +767 byte cache-prefix win per call. Per-verb wins:help+14084,read+4965,grep+2337,report+2343,find+1352,git+408,stat+93,hook+46. The empirical scoreboard matches the structural claim. See the ASH-135 measurement for the methodology and full per-run numbers. proto.Metrics.CacheReadTokens/CacheCreationTokensexist on the wire so a producer can ship telemetry through_metawithout a protocol revision.ash usage(ASH-134) writes to the ledger rather than this in-response channel — the in-response path stays reserved for a future producer that has the numbers at request time.