You're Probably Over-Counting Your AI Agent's Context Cost
We suspected our agents were paying twice for the same files. An agent pipeline
runs a chain of stages in one session, and every stage reaches for the same
shared inputs — a manifest.json, a context document, a plan file, the reports
earlier stages wrote. Load the same content again at a new position in the token
stream and prompt caching can’t save you: a fresh copy appended later has no
earlier prefix to match, so it bills as new input. The waste looked obvious and
worth fixing.
Then we tried to size it, reached for the obvious estimate, and got a number that turned out to be wrong by more than an order of magnitude. The correction is the useful part, so this post is about the measurement, not the fix — because once we measured it honestly, we decided not to build the fix at all.
The estimate that looked alarming
The intuitive way to price duplicate loads is to multiply how many times each shared file was touched by how big the file is. Count the accesses across a history of sessions, weigh each one by the file’s size in tokens, and add it up.
Do that across a pile of real sessions and the shared files come to roughly 970,000 tokens of duplicate-load waste. Nearly a million tokens spent re-reading things the agents had already seen. That is a number that gets an optimization prioritized.
It is also wrong, and wrong in a way worth understanding, because the error is the same one that inflates most back-of-envelope agent-cost math.
Why the estimate lies
access count × file size assumes every access drops the whole file into
context. Agents don’t work that way. They slice.
Take a shared manifest.json that is about 15,000 tokens on disk. Across our
sessions it was accessed 45 times. The estimate says that is
45 × 15,000 = 675,000 tokens for that one file. But an agent almost never reads
the whole manifest — it pulls the two or three fields it needs with a jq or a
grep. The median access to that 15,000-token file returned about 136
tokens. The agent asked a question of the file and got a short answer, not a
data dump.
Two things break the estimate, and both push it the same direction:
- Agents request slices, not files. A
grep, ahead, ajqfor one key — each returns a fraction of the file. The estimate counts the whole file every time. - Harnesses truncate what is large. When a tool result is big, the harness hands the model a preview and stashes the rest behind a reference. The bytes the model actually ingests are capped; the file’s on-disk size is not what landed in context.
Worth reconciling with something our own writing says: the hidden economics of AI coding is that an agent re-sends its whole accumulated context on every turn — the growing snowball of system prompt, prior turns, and everything read so far. That is true, and it is about the conversation as a whole. It is a different level from what a single tool call costs. The turn-level story is “the accumulated context is re-sent”; the load-level story is “each individual read is usually a slice.” Both hold at once, and confusing the two is exactly how the file-size estimate goes wrong.

The honest unit, and where to find it
If file size and access count both mislead, what should you measure? The bytes the model actually ingested — the content that came back in each tool result. That number is recoverable, because session transcripts already record it.
Every major agent runner writes a local transcript of each run. The path and the exact shape differ from tool to tool, but the thing you need is always there: each tool call and the result it produced, joined by an id.
| Agent | Transcript location | Pairing key |
|---|---|---|
| Claude Code | ~/.claude/projects/<project>/*.jsonl | tool_use.id → tool_result.tool_use_id |
| Codex | ~/.codex/sessions/*.jsonl | call_id → function_call_output |
| OpenCode | ~/.local/share/opencode/storage/ | part.callID → part.state.output |
| Pi | ~/.pi/agent/sessions/<project>/*.jsonl | toolCall.id → toolResult |
Most write one JSON object per line — an assistant turn with its tool calls, then
the results; OpenCode splits the same data across a message/ and a part/
store. The pairing key in the third column is what lets you attribute cost to an
individual call.
What makes per-file attribution possible is the id. When an agent calls a
tool, the harness stamps that call with a unique id and writes it into the
transcript. When the result comes back it is a separate line carrying the same
id, so you can pair a call with exactly what it returned — the third column of
the table above. Here is that pairing in a Claude Code transcript: the call is a
tool_use block with an id like toolu_01Soy…, and the result is a
tool_result whose tool_use_id is that same string.
Here is one matched pair: an agent pulling two fields out of a manifest with
jq, and what came back.
// the call — an assistant turn
{ "type": "tool_use", "id": "toolu_01Soy…", "name": "Bash",
"input": { "command": "jq '.brand.primary, .brand.font' manifest.json" } }
// the result — a later line, paired by the same id
{ "type": "tool_result", "tool_use_id": "toolu_01Soy…",
"content": [ { "type": "text", "text": "\"#0B7A75\"\n\"Fraunces\"\n" } ] }
That manifest is ~15,000 tokens on disk. What the model actually ingested from
this call is the two lines in content — about a dozen tokens, not fifteen
thousand. Size that text (a rough len(text) / 4 is close enough for tokens)
and you have the real per-load cost. The file size never entered into it.
Stack that up over a whole transcript and the method is a short script: build a map from each call’s id to the file it loaded, then for every result look up its id, size the returned content, and count how many times you have seen that path.
import json, collections
seen = collections.Counter() # path -> times loaded this session
calls = {} # tool-call id -> path it loaded
first = dup = 0
for line in open(transcript_path):
for block in json.loads(line).get("message", {}).get("content", []) or []:
if block.get("type") == "tool_use":
path = file_loaded_by(block) # a Read's file_path, or a cat/jq/grep target
if path:
calls[block["id"]] = path
elif block.get("type") == "tool_result":
path = calls.get(block.get("tool_use_id")) # <- pair call to result by id
if not path:
continue
tokens = len(text_of(block)) // 4 # bytes the model actually ingested
seen[path] += 1
if seen[path] == 1:
first += tokens # first look: unavoidable
elif tokens > 200:
dup += tokens # a real re-load: avoidable
print(first, dup)
Two helpers carry the tool-specific detail — file_loaded_by (does this call
load a file, and which one) and text_of (pull the returned text out of the
result block) — and swapping them for the Codex field names is the whole port.
That filled-in script is what produced the numbers in this post.
Run that over the same sessions and the honest figure for avoidable duplicate-load waste is about 73,000 tokens — not 970,000. The same manifest whose estimate was 675,000 tokens contributes about 2,600 tokens of real duplicate waste once you count the bytes that actually came back and only the repeat loads among them. That is the whole correction: roughly a 13× over-count, concentrated in the gap between “the file is big” and “the agent read a little of it.”
For scale, one full pipeline run already moves hundreds of millions of cache-read tokens. Against that, a few thousand tokens of avoidable fresh input per run is a rounding error.
Where the real waste actually hides
The 73,000 tokens are not spread evenly. Almost all of it lands in one pattern: read-after-write.
A stage authors a file — it builds a plan document across a series of writes, or
assembles a report through a dozen incremental edits. The full content is
already in context, because the stage just wrote it. Then, to self-verify or
fingerprint what it produced, the stage issues a whole-file read of the same
file. In the transcript it is unmistakable: a run of write and edit calls to
one path, immediately followed by a read of that same path whose result is the
entire file. That single re-read can be ~13,000 tokens of content the model
already held moments earlier. That is the purest form of the waste: not a slice, not a
first look, but a complete re-load of something already in context.
This is a sharper claim than the familiar one. Our own guide on AI coding token costs notes that an agent “re-reads constantly” and re-reads its own earlier work — true, and the general cost driver. What the measurement adds is where the avoidable part concentrates: not diffuse re-reading, but the specific act of reading back a file you just wrote. That is nameable, findable in a transcript, and — unlike most re-reading, which is the agent legitimately consulting something — genuinely redundant.
The reason it can never be cheap is prompt caching. A cache hit needs the re-loaded content to sit at the same prefix position it held before. A read-back appended after a batch of writes lands at a new position, so it bills as fresh input, not a cache read. (If that mechanic is unfamiliar, our guide on prompt caching explains why a cache is prefix-shaped, and reading a cache usage dashboard shows the read-vs-write split in practice.) Carrying the first copy forward is cheap — later turns re-send it as cache reads at a fraction of the input price. Reloading it is not.

The same trap on the other stack
Because the cause is architectural — prefix caching plus a tool-use harness, not
any one model — it should show up anywhere agents work this way. It does. The
same call-to-result transcripts turn up under OpenCode and Pi, not just the two
stacks in the table above, and one Pi run happened to be driven by an entirely
different model family — yet its tool results show the same lopsided
distribution: a roughly 60-token median with the occasional multi-thousand-token
spike. Agents slice, whatever the vendor. OpenCode makes the other half of the
trap literal — oversized tool output is spilled into a separate tool-output/
store instead of kept inline, which is exactly the truncation that makes a file’s
size lie about what the model ingested.
Codex is where checking it produced the best evidence, because we nearly got it wrong. It writes the same kind of local transcripts, with per-turn token accounting, and does automatic prefix caching for large prompts — so a re-load at a new position can’t be a cache hit there either. A first, naive pass over a pile of real Codex sessions reported ~3.7 million tokens of “duplicate-load waste.” We nearly published that number. It is inflated for the exact two reasons this post is about:
- It used each tool output’s self-reported size, which is the size before truncation. Codex caps big outputs at a preview and reports the untruncated figure separately — outputs with a reported size of 42,000 tokens where the content actually handed to the model was near 10,000.
- It counted every repeat access to a path as a duplicate. But one file was
accessed 229 times through 152 distinct commands — different
sedranges andgreppatterns, i.e. different slices, not the same content re-loaded.
Both are the file-size-and-access-count error in a different form. The honest cross-stack figure needs the same content-level care the first number got — hash the returned content, count only true identical re-loads, net out truncation — and that measurement is still worth doing carefully rather than quickly. The useful result here isn’t a rival number; it’s that the trap is real, it repeats on every stack that caches a prefix and runs tools, and it’s easy to make. (One caution if you try this on Codex: measure by reading the transcript files yourself, not by asking the agent to analyze its own session logs — that has its own runaway failure mode.)
What this buys you
The concrete deltas between stacks are small and mechanical — where the transcript lives, what the token fields are called, the exact cache-read and cache-write multipliers. Confirm those against each provider’s current docs when you act on them; the method above doesn’t change.
| What differs by tool | What to check at the source |
|---|---|
| Token-usage fields | Input / cached / output field names per provider |
| Cache pricing | Current cache-read and cache-write multipliers vs fresh input |
| Truncation behavior | Preview size and whether the untruncated size is reported separately |
The reason to bother with any of this isn’t the 73,000 tokens. It’s what measuring gives you:
- It stops you optimizing the wrong thing. A real number, gathered before you build, keeps you from spending a sprint on a fix that saves a rounding error — and from trusting headline claims that agents waste some dramatic percentage of their tokens.
- It lets you attribute cost, not just total it. A provider bill is one lump sum. Matching tool calls to their results answers which files and actions cost you, from data already on your disk.
- It names one pattern worth watching. Read-after-write is specific and findable, which is far more actionable than “reduce your token usage.”
- It works in both directions. The same measurement that justifies a fix can tell you not to build one. Here it did — and that is the honest outcome, not a failure of the investigation.
The lesson generalizes past duplicate loads: when you want to know what an agent costs, don’t reason from file sizes or access counts. Read the transcript, and measure the bytes that actually landed in context. It’s more work than an estimate, and it is regularly the difference between a scary number and a real one. That is the whole case for measuring instead of asserting — the same case the hidden economics of AI coding makes for observability, sharpened to a single number you can pull from your own sessions today. If you have watched a session open at thousands of tokens before you typed anything, the fix starts the same way: measure what actually went in.