The value the model never held.
A user asks our agent for a status report: filter a project’s work items to the open ones and produce a PDF summary. The items export is 2.1 MB of JSON. In a naive harness that value enters the model’s context twice: once when the query tool returns it (~500k tokens of input — if it fits at all), and again when the model passes the rows to the PDF tool, because the only way to hand a value to the next tool is to write it out token by token. That second copy is the expensive kind — output tokens — and the model is now a lossy, non-deterministic wire: it will truncate, “fix,” or hallucinate rows in transit.
Part 1 kept the domain model out of standing context; part 2 kept the tool catalog out. Both were fixed-size wins. This one is unbounded: the data never enters context at all. The model works with references, and the harness resolves them server-side at the moment a tool actually needs the bytes.
The vocabulary
A reference is not a URI or an opaque handle. It’s a small typed JSON object the model can read, and — in two of four cases — write:
Any top-level property of any tool call can be replaced by
{ "$ref": … }, or the entire input can be one. A Message reference
addresses a part of the transcript itself — message 42, part 2, field
result.value — which means the transcript doubles as an addressable store:
anything a tool ever returned is a coordinate, not a copy. A Blob reference
addresses stored bytes by key or through another reference. The two derived
forms — JsonPointer (one node of a JSON document) and JsonQuery (an array
filtered by field equality or substring) — are recipes: the model can
forward one it received, but the input parser rejects any it tries to
author. That asymmetry closes an injection surface — the model cannot
fabricate a pointer into data it was never shown — and it keeps recipes an
implementation detail the harness is free to change.
How values leave context
The interesting design choice: there is no size threshold. Whether a result passes by reference is decided by type, at two chokepoints.
First, any Blob in a tool result is intercepted on the way out. The bytes are
streamed to an object store under agent/<agentId>/<key> — chunk by chunk,
never materialized — and the result field is rewritten in place to a typed
marker. Second, when the provider serializes the result for the model, the
marker becomes:
{
"opaque": true,
"value": { "$ref": { "type": "Message", "part": 0 } },
"size": 2214731,
"format": { "mediaType": "application/json" }
}
Metadata, and an address. Never bytes, never even the storage key. A 2 MB file and a 2 KB file cost the same ~120 characters of context.
Structured results — plain objects — inline as-is, which sounds like the loophole
that ruins everything, until you look at how the file-reading tools are shaped.
None of them can return an unbounded value. read_json and query_json
return a preview + reference pair: the preview is paginated and capped —
long strings elided, child and item counts bounded — and alongside it comes a derived
reference to the complete, exact value — {type: "JsonPointer", path: "/items"} or {type: "JsonQuery", path, filter}. Even that recipe is hidden:
the model sees a Message reference pointing back at the result’s own value
field. The test suite asserts the string "JsonPointer" never appears in
what the model reads.
So the model always holds enough to decide — shapes, counts, first rows — and an address for the whole thing, evaluated only if some tool eventually resolves it.
The flow, end to end
The step that still surprises me is the third arrow: when generate_pdf
receives rows: {$ref}, the resolver walks Message → result field → recovers
the stored JsonQuery recipe → re-reads the blob → re-applies the filter — and
hands the tool the full 148-row array. The recipe is lazy: it was never
evaluated when query_json returned, only now, at the moment of use. The model
brokered a value it never held. Resolution is recursive with two independent
caps — one at validation, a tighter one at resolution — so
Blob(source: JsonQuery(source: Message)) chains are legal but bounded.
Cheaper is also more correct
The token arithmetic gets the headlines, but the correctness argument is what makes references non-negotiable. When values travel tool-to-tool through the model, the model is part of the data path: every row is re-generated token by token by a sampler. It abbreviates long fields, “fixes” values that look inconsistent, rounds numbers, drops a row and keeps going — none of which is a bug in the model. That is simply what generative transport is.
By reference, the model is only the control path. It routes addresses; the
data path is deterministic server code. The 148 rows that reach generate_pdf
are byte-for-byte the filter’s output, not the model’s recollection of it. And
the failure modes improve along with the fidelity:
- A stale pointer throws — “Derived JSON path … no longer exists.” — loudly and immediately, instead of silently shipping plausible-looking rows.
- The model cannot author a derivation recipe, so it cannot point a tool at data it was never shown; a fabricated reference fails validation instead of fabricating data.
- The preview and the reference travel together, describing the same exact value — there is no gap between what the model inspected and what the next tool receives.
Token savings you can buy with a bigger context window. This property you can’t: the only way the model corrupts a value in transit is if the value transits the model.
The plumbing that makes it declarable
For the model to pass a reference anywhere, every tool’s schema must say so.
The harness rewrites each tool’s input schema so that every top-level property
accepts anyOf: [original, $ref]. Two details echo part 2’s discipline:
- The alternative is attached per property, never at the schema root —
the Anthropic API rejects
oneOf/anyOfat the top level ofinput_schema, so the envelope design bends around a provider constraint. - The rewritten schema is cached in a
WeakMapkeyed by the original parameters object — so the declaration bytes are stable across turns, and the prefix cache keeps hitting.
Tools that want laziness inside their implementation opt out of eager
resolution: read_json’s ref parameter receives the reference object itself
rather than resolved bytes, so the tool can stream from the store on its own
terms. Everything else gets its $ref arguments resolved before execute
runs — tool authors mostly never know references exist.
One envelope, one union, one resolver
This machinery is the product of a deliberate unification. Before it, the
codebase had accumulated three ways to say “that value, over there”: bare {seq, part} objects for message parts, {key, ...metadata}
markers for staged blobs, and positional seq="9" part="2" attributes in
attachment tags. Each had its own parser and its own resolution path. The
unification collapsed them into the single $ref envelope over the single
typed union, resolved by one recursive resolver — and the boundary now
rejects the legacy shapes, with tests pinning the rejection. The old forms
survive only deep inside resolution for backward compatibility, guarded by a
pointed comment: generic value resolution must not reinterpret an ordinary
domain object merely because it happens to contain a key field.
That’s the unglamorous part of harness engineering: the token win was available with any of the three ad-hoc shapes. The unification is what makes the mechanism explainable to the model in one paragraph of protocol prompt — and models are much better at one rule than at three.
That closes the loop this part opened: the model routes data by address, the harness moves bytes the model never sees, and the token bill and the error rate drop together. But the standing context still has rows we haven’t examined — and they’re different in kind. The ontology, the catalog and the data are all knowledge we shipped. The agent also picks things up along the way: the user’s preferences, a project’s quirks, corrections it shouldn’t need twice. That accumulating knowledge needs the index-and-payload treatment more than anything else — it grows without bound by design — and it’s the subject of part 4. What users deliberately teach the agent is a different mechanism again, and it closes the series in part 5.