An agent carries its whole transcript into every turn. Every file it reads, every API response and every note it writes is sent again on the next call, so a long task fills the window with material the agent finished with many turns ago. Even a 1M-token window does not make this free. Chroma’s report on context rot measured that models use long inputs less reliably as they grow, and Anthropic’s engineering blog puts the effect in one sentence: “as the number of tokens in the context window increases, the model’s ability to accurately recall information from that context decreases.” The same blog names the goal of context engineering: “find the smallest set of high-signal tokens that maximize the likelihood of your desired outcome.”
Anthropic’s API offers three primitives for it. Tool-result clearing replaces old tool results with a stub. Compaction replaces the transcript with a summary the model writes. The memory tool gives the agent files outside the window that the next session can read. This post runs each one on the same research agent, on Claude Sonnet 5.5, and measures what it saves, what it costs and what it loses.
The three primitives on one transcript. Clearing and compaction shrink the window inside a session; memory carries notes into the next one.
- Tool results fill the window. In the baseline, file reads were 88,317 of the 90,421 tokens at the peak, 97.7%.
- Clearing holds the context near its trigger, and costs re-reads. Keeping the newest 2 results held the peak near 33K tokens, but the agent read files 12 to 16 times for a task that needs 6.
- Compaction keeps what stands out. Every summary kept all six lifespans. Identifiers hidden among 29 lookalikes survived in none of 3 runs with the default prompt and in all 3 with instructions naming them.
- Memory saves the next session’s work. With 525 tokens of notes from session 1, session 2 read 3 documents instead of 6 and peaked at 48,298 tokens instead of 91,650.
- The experiments are cheap. Every run in this post together cost $10.65 at Sonnet 5.5’s list prices.
Table of contents
Open Table of contents
What a context is made of
Everything a model sees on a call is one token sequence, built from parts with different lifetimes:
| Part | Grows? | Lifetime |
|---|---|---|
| System prompt: instructions, persona, rules | No | The whole session |
| Tool definitions | Only when tools change | The whole session |
| Retrieved documents | Per question | Until the question is answered |
| Conversation history | Every turn | Until summarized or dropped |
| Tool results | Every tool call, often by thousands of tokens | Usually a few steps |
| The agent’s own reasoning | Every step | Usually the current step |
| Memory from earlier sessions | Slowly | Across sessions, outside the window |
Two earlier results decide how these parts should be arranged. Models use the middle of a long context worst (Long Context, and Where It Breaks), so what matters most belongs at the start or the end. A provider’s prompt cache reuses only an exact prefix (Prompt Caching), so what changes least goes first: tools, then the system prompt, then history, then the newest material. The parts that grow fastest and expire soonest, tool results above all, are what fill an agent’s window, and they are what the three primitives remove. Retrieval (Building RAG) decides what enters the window; this post is about what leaves it.
The experiment
The agent compares six model organisms for aging research. Its corpus is six synthetic reviews of about 14,700 tokens each, 88,317 tokens in all. Each review is filler plus two planted facts: a headline fact, the organism’s median lifespan, stated once near the top, and an obscure fact, the identifier its replicate cohort came from, in an appendix line at the end. The planted facts give ground truth for checking a summary later.
The agent has three ordinary tools: search_files returns matching paths,
read_file returns a whole document, and record_finding stores a note for
the session. The task asks it to read every review, record one finding per
organism and write a short synthesis. The harness is a plain agent loop
(From Function Calling to the Agent Loop)
with instrumentation: on every turn it records the context size, the clearing
and compaction events the API reports, and every file read.
The setup follows Anthropic’s context-engineering cookbook, which ran the same
kind of agent on Claude Sonnet 4.6 with eight documents of about 41,000
tokens each. The cookbook’s numbers are quoted below as the cookbook’s. Every
other number comes from the scripts in this post’s folder, run on
claude-sonnet-5-5 with the anthropic SDK 1.13.0 on 2026-10-11 (UTC).
The setup is simplified in ways that matter for reading the results. The
corpus is synthetic: the plain reviews pad their two facts with one repeated
sentence, and the dense ones are described below. The runs set no prompt
cache breakpoints. Each session stops after 12 turns, and each request asks
for at most 4,096 output tokens, except in the compaction runs, which allow
16,000. The model is not deterministic, so clearing and compaction are run
three times per setting, and plain-corpus compaction ten more times; the
baseline, the memory sessions, the combined run and clearing with keep 6
are single runs, and their numbers are one sample each.
Measuring the context
The number to track is the size of each request’s prompt, and the API’s
usage.input_tokens alone is not it. With prompt caching, input_tokens
counts only the tokens after the last cache breakpoint; the docs give the
total as the sum of three fields:
def context_size(u):
# input_tokens is only the uncached remainder; the context is all three
return u.input_tokens + (u.cache_read_input_tokens or 0) + (u.cache_creation_input_tokens or 0)
These runs set no cache breakpoints, so both cache fields came back zero, but an agent that caches its prefix would look almost empty without them.
The problem, measured
With no context management, the baseline run:
from harness import TASK, CORPUS, run_session, count_tokens, save, print_usage
# 1. No context management: every file the agent reads stays in the window.
s = run_session(TASK, label="no management")
# What fills the window at its peak: the file reads, counted exactly.
read_tokens = sum(count_tokens(CORPUS[p]) for _, p in s.reads if p in CORPUS)
print(f"corpus: {len(CORPUS)} documents, {sum(count_tokens(c) for c in CORPUS.values()):,} tokens")
print(f"peak context {s.peak:,} tokens after {len(s.trajectory)} turns and {len(s.reads)} file reads")
print(f" file-read results {read_tokens:>7,} {read_tokens / s.peak:6.1%}")
print(f" everything else {s.peak - read_tokens:>7,} {1 - read_tokens / s.peak:6.1%}")
save("baseline", s, read_tokens=read_tokens)
print_usage()
printed:
[no management]
turn 1 context 652 search_files
turn 2 context 780 read_file, read_file, read_file, read_file, read_file, read_file
turn 3 context 89,529 record_finding, record_finding, record_finding, record_finding, record_finding, record_finding
turn 4 context 90,421 final answer
corpus: 6 documents, 88,317 tokens
peak context 90,421 tokens after 4 turns and 6 file reads
file-read results 88,317 97.7%
everything else 2,104 2.3%
API usage: 181,382 uncached input, 0 cache-write, 0 cache-read, 1,800 output tokens; $0.38
Sonnet 5.5 issued all six reads in one turn, so the context jumped from 780 to 89,529 tokens in a single step and never came down. The cookbook’s agent read in two batches of four and climbed in two steps, to a peak of 335,279 tokens over 5 turns.
What filled the baseline’s window at its peak. The file reads are counted with the API’s token counter, one document at a time.
Data: context-engineering-compaction-clearing-memory/baseline.pyThat one measurement picks the tool. The agent’s own text, the task and the tool definitions are 2.3% of the window, so summarizing the dialogue would reclaim almost nothing; the bulk is tool results the agent has already used. The cookbook found the same shape at its scale, 96.3% file reads, from a character-count estimate. A chat-heavy workload would invert the ratio, and the right primitive with it.
What a long context holds on the server
Every token in the window is a key and a value in every layer’s KV cache, read again on every decode step (The KV Cache). Claude’s architecture is not public, so open models of known shape stand in:
# What a context of n tokens holds in a model's KV cache: 2 (K and V) x layers
# x KV heads x head dim x 2 bytes per token (16-bit), the formula from
# The KV Cache. Claude's architecture is not public; these are open models
# of known shape, as stand-ins.
models = {"Llama 2 7B (32 layers, 32 KV heads of 128)": (32, 32, 128),
"Llama 2 70B (80 layers, 8 KV heads of 128)": (80, 8, 128),
"Qwen2.5-1.5B (28 layers, 2 KV heads of 128)": (28, 2, 128)}
contexts = {"this post's baseline peak": 90_421, "the cookbook's baseline peak": 335_279}
for name, (layers, heads, dim) in models.items():
per_token = 2 * layers * heads * dim * 2
print(f"{name}: {per_token / 1024:,.0f} KiB per token")
for label, n in contexts.items():
print(f" {label}, {n:,} tokens: {per_token * n / 2**30:6.1f} GiB")
Llama 2 7B (32 layers, 32 KV heads of 128): 512 KiB per token
this post's baseline peak, 90,421 tokens: 44.2 GiB
the cookbook's baseline peak, 335,279 tokens: 163.7 GiB
Llama 2 70B (80 layers, 8 KV heads of 128): 320 KiB per token
this post's baseline peak, 90,421 tokens: 27.6 GiB
the cookbook's baseline peak, 335,279 tokens: 102.3 GiB
Qwen2.5-1.5B (28 layers, 2 KV heads of 128): 28 KiB per token
this post's baseline peak, 90,421 tokens: 2.4 GiB
the cookbook's baseline peak, 335,279 tokens: 9.0 GiB
A model built like Llama 2 7B would hold 44.2 GiB of cache for this agent’s peak and 163.7 GiB for the cookbook’s, more than two 80 GB GPUs hold. Grouped-query attention cuts this by the ratio of query heads to key-value heads, which is why 70B models with 8 KV heads hold less than a 7B with 32. Either way the cost of a long context is paid on every token generated, not once.
Tool-result clearing
Clearing replaces old tool_result blocks with a short placeholder and
keeps the tool_use blocks that requested them. The model still sees that it
read /research/mouse_review.md, with what arguments, but no longer carries
the document. The core of it is a pass over the message list:
def clear_old_tool_results(messages, keep=3, placeholder="[cleared to save context]"):
"""Replace the content of all but the newest `keep` tool_result blocks."""
spots = [(i, j) for i, m in enumerate(messages) if isinstance(m["content"], list)
for j, b in enumerate(m["content"]) if b.get("type") == "tool_result"]
for i, j in spots[:len(spots) - keep]:
messages[i]["content"][j]["content"] = placeholder
return messages
On a transcript of four reads, keeping only the newest result:
clearing, keep=1: 193,182 -> 48,501 characters
tool_use records kept: ['/research/celegans_review.md', '/research/drosophila_review.md', '/research/yeast_review.md', '/research/killifish_review.md']
The API version, clear_tool_uses_20250919 (beta header
context-management-2025-06-27), runs the same edit on the server before the
prompt reaches the model. The client keeps the full history and sends it
every turn; the docs say it does “not need to sync your client state with the
edited version.” The experiment’s setting:
clear = {"type": "clear_tool_uses_20250919",
"trigger": {"type": "input_tokens", "value": 30_000},
"keep": {"type": "tool_uses", "value": keep},
"clear_at_least": {"type": "input_tokens", "value": 5_000},
"exclude_tools": ["memory"]}
| Parameter | Default | What it does |
|---|---|---|
trigger | 100,000 input tokens | Prompt size at which clearing starts; can also count tool uses |
keep | 3 tool uses | How many recent tool uses and results stay whole |
clear_at_least | none | Skip the edit unless it frees at least this many tokens |
exclude_tools | none | Tools whose results are never cleared, such as memory |
clear_tool_inputs | false | Also clear the arguments of the cleared calls |
The first run with keep at 2:
[clearing, keep 2, run 1]
turn 1 context 652 search_files
turn 2 context 780 read_file, read_file, read_file, read_file, read_file, read_file
turn 3 context 30,658 read_file, read_file, read_file, read_file, record_finding, record_finding clearing
turn 4 context 31,542 record_finding, record_finding, read_file, read_file clearing
turn 5 context 32,201 record_finding, record_finding clearing
turn 6 context 32,516 final answer clearing
keep 2: finished after 6 turns; peak context 32,516; 12 file reads, 4 documents read more than once
turn 3: cleared_tool_uses = 5, cleared_input_tokens = 59,014
turn 4: cleared_tool_uses = 9, cleared_input_tokens = 117,990
turn 5: cleared_tool_uses = 11, cleared_input_tokens = 147,508
turn 6: cleared_tool_uses = 11, cleared_input_tokens = 147,569
On turn 3 the request carried six documents, about 89K tokens. The edit cleared five tool results (the search and the four oldest reads) and the model saw 30,658 tokens. Then the agent read four documents again, because the ones it was about to record findings for were gone.
cleared_input_tokens grows on every turn here, from 59,014 to 147,569. It is
not a running total of what clearing has saved. The server is stateless, so
each request’s edit clears the whole uncleared history again, and the field
counts what this request’s edit removed. It grows because the history does,
re-reads included. Summing it across turns counts the same documents many
times.
Clearing trades window for tool calls. Every run finished, but the settings that saved context re-read documents.
Data: context-engineering-compaction-clearing-memory/clearing.pyThree runs at keep 2 and 4, and one at 6, show the exchange rate. Keeping 2
results held the peak near 33K and cost 12 to 16 reads for a task that needs
6. Keeping 4 held it near 62K and cost 10 to 14. With keep at 6, no
edit fired at all, and the run was the baseline again: 6 reads and a
90,591-token peak. The docs describe keep as the newest tool uses, but the
cleared_tool_uses counts in these runs fit only if the record_finding
calls were neither counted toward keep nor cleared, which would also
explain why keep 6 never left 5,000 tokens to clear. This
task compares all six documents at the end, so it needs them all at once,
and clearing can only make it fetch them again. Clearing suits results that
are used once, such as a page searched for one fact, and results that are
cheap to fetch again.
Clearing costs no inference: it is an edit, with no prompt to tune. Its cost
is in the prompt cache. Changing an early block changes the prefix, and the
docs say tool clearing “invalidates cached prompt prefixes when content is
cleared” and suggest clear_at_least so that each edit frees enough tokens
to be worth a new cache write.
Compaction
Compaction replaces the transcript with a summary. When the prompt passes the trigger, the API first runs a summarizing step with the same model, then answers the request from the summary instead of the history. The experiment runs it twice, once with the default prompt and once with instructions that name the details to keep:
# 3. Compaction at the API's minimum trigger, with the default prompt and with
# instructions that name the details to keep, on the plain corpus and on
# the dense one, three runs each. Each summary is probed for the planted
# facts of the documents it summarized. max_tokens is raised to 16,000
# because the summarizing step writes within it, and the default 4,096 cut
# off summaries that listed every appendix row.
compact = {"type": "compact_20260112", "trigger": {"type": "input_tokens", "value": 50_000}}
guided = dict(compact, instructions=(
"Summarize this research agent transcript. Preserve every quantitative lifespan figure "
"with its source organism, AND every appendix provenance detail (cohort/jar/rack/plate "
"identifiers). Note which documents have been read and which remain. Wrap the summary "
"in <summary></summary>."))
| Parameter | Default | What it does |
|---|---|---|
trigger | 150,000 input tokens, at least 50,000 | Prompt size at which compaction runs |
instructions | the model’s default prompt | Replaces the summarization prompt entirely |
pause_after_compaction | false | Return after writing the summary, before answering |
compact_20260112 takes the beta header compact-2026-01-12. The docs say
“the default summarization prompt varies by model” and quote the one “some
models use”, which asks for “a summary of the transcript” to “provide
continuity” and to be wrapped in <summary></summary>. Instructions do not
add to that prompt; they replace it completely, so they must ask for the
summary and its wrapper themselves.
Two details of the response matter in the loop. The summary comes back as a
compaction block, which has to go back to the API with the rest of the
assistant’s content; the API then ignores every block before it. And the
Python SDK’s plain model_dump() adds null fields that the API rejects, so
the docs recommend to_dict() or exclude_none=True:
content, calls = [], []
for b in resp.content:
if b.type == "compaction":
s.summaries.append(b.content or "") # None if the summarizing step failed
elif b.type == "tool_use":
calls.append(b)
if b.name == "read_file":
s.reads.append((turn, b.input.get("path", "?")))
content.append(b.model_dump(exclude_none=True))
messages.append({"role": "assistant", "content": content}) # all blocks, compaction included
The summarizing step is billed like any other call, but it does not appear
in the top-level usage. The docs: “The top-level input_tokens and
output_tokens do not include compaction iteration usage.” It is in
usage.iterations, which is also the only place the summarizing step’s
input, the transcript plus the summarization prompt, appears:
resp = client.beta.messages.create(**kw)
u = resp.usage
# the top-level counts leave out a compaction's own summarizing call;
# usage.iterations lists every sampling step the request billed
for it in getattr(u, "iterations", None) or [u]:
if getattr(it, "type", "") == "compaction": # the context the summary replaced
s.events.append({"turn": turn, "kind": "compaction", "before": it.input_tokens,
"summary_out": it.output_tokens})
bill({"input": it.input_tokens, "cache_write": getattr(it, "cache_creation_input_tokens", 0),
"cache_read": getattr(it, "cache_read_input_tokens", 0), "output": it.output_tokens})
What survives
A summary that keeps everything is free to trust; one that drops details has to be checked. The probe gives the model the summary alone and asks, for each organism, its lifespan and the identifier its cohort came from: what an agent continuing from the summary could still answer. The plain corpus has one identifier per document. The dense corpus is a harder test: its filler is hundreds of distinct cohort measurements, and its appendix lists 30 provenance rows, the planted one labeled “primary” and 29 lookalikes. One run of the plain corpus and two of the dense one:
plain corpus: 88,317 tokens
[compaction, plain corpus, default prompt, run 1]
turn 1 context 694 search_files
turn 2 context 822 read_file, read_file, read_file, read_file, read_file, read_file
turn 3 context 1,548 record_finding, record_finding, record_finding, record_finding, record_finding, record_finding compaction
turn 4 context 2,726 final answer
89,615 tokens -> 885-token summary (101x, 889 output tokens billed) at turn 3, final context 2,726; from the summary alone: lifespans 6/6, source identifiers 6/6
dense corpus: 129,152 tokens
[compaction, dense corpus, default prompt, run 1]
turn 1 context 694 search_files
turn 2 context 822 read_file, read_file, read_file, read_file, read_file, read_file
turn 3 context 1,840 record_finding, record_finding, record_finding, record_finding, record_finding, record_finding, record_finding compaction
turn 4 context 3,239 final answer
130,450 tokens -> 1,177-token summary (111x, 1,181 output tokens billed) at turn 3, final context 3,239; from the summary alone: lifespans 6/6, source identifiers 0/6
[compaction, dense corpus, guided prompt, run 1]
turn 1 context 694 search_files
turn 2 context 822 read_file, read_file, read_file, read_file, read_file, read_file
turn 3 context 3,348 record_finding, record_finding, record_finding, record_finding, record_finding, record_finding compaction
turn 4 context 4,894 final answer
130,499 tokens -> 2,685-token summary (49x, 4,693 output tokens billed) at turn 3, final context 4,894; from the summary alone: lifespans 6/6, source identifiers 6/6
Twelve compactions of about 90K and 130K tokens into summaries of 821 to 2,853 tokens. Every summary kept all six lifespans.
Data: context-engineering-compaction-clearing-memory/compaction.pyEvery compaction shrank the context 46 to 111 times, and every summary kept all six lifespans. The identifiers are where the runs differ, and each summary kept either all six or none:
- Plain corpus, default prompt. All six in all three runs, and in 9 of 10 more runs of the same condition (below).
- Plain corpus, instructions. All six, in all three runs.
- Dense corpus, default prompt. None, in all three runs. The summaries describe the appendix as “provenance” lines (one calls them “just bookkeeping”, another “purely logistical”) and keep none of its values.
- Dense corpus, instructions. All six, in all three runs. The instructions asked for “every appendix provenance detail”, and the summaries were more than twice as long, 2,528 to 2,853 tokens, with 4,167 to 4,943 output tokens billed for each.
The ten extra runs of the plain corpus with the default prompt
(Full script: repeat.py):
summaries that kept every identifier: 9 of 10
The one that lost them calls each appendix line “not relevant to the comparison” and records none of the values. With the default prompt, keeping a detail that stands out is likely but not certain.
A first version of this experiment capped the summarizing step at the
request’s default 4,096 output tokens. The guided summaries of the dense
corpus, which tried to list all 180 rows, were cut off before the last
appendices and lost the identifiers they never reached. The summary is written within the
request’s max_tokens, so a summary asked to keep much needs room to.
The cookbook reported the same pattern at its scale: “high-level 3/3 preserved, obscure 0/3 preserved”, from a substring check of a single summary. Compaction keeps what reads as important and drops what reads as noise, and an identifier among lookalikes reads as noise. Instructions that name the detail move it from the second group to the first, at the cost of a longer summary and more output tokens. Whether a given task’s details survive is a question for a probe like this one, run on that task’s own transcripts.
The docs add a limit on the cheap end: the request’s model writes the summary,
with “no option to use a different (for example, cheaper) model”. And when
tools are defined, the model “occasionally calls a tool during the internal
summarization step instead of writing a summary”, which returns a
compaction block with content: null; the harness records that case as an
empty summary.
The docs now recommend a second kind, on-demand compaction (beta header
compact-2026-09-04): the client asks for a summary with a top-level
compaction parameter when it chooses, instead of the server compacting at a
threshold. It cannot be combined with context_management edits on the same
request, so this post’s experiments use the threshold kind, which composes
with clearing.
The memory tool
Clearing and compaction work inside one session. When a new session starts,
the window is empty, and neither helps. The memory tool
(memory_20250818, no beta header) gives the agent a /memories directory
with six commands: view, create, str_replace, insert, delete and
rename. The tool runs on the client: the model asks for a file operation,
and the application performs it on storage it owns. When the tool is present,
the API adds an instruction to the system prompt, which begins “IMPORTANT:
ALWAYS VIEW YOUR MEMORY DIRECTORY BEFORE DOING ANYTHING ELSE.” and warns
“Your context window might be reset at any moment”.
The backend receives paths the model wrote, so it must keep them inside the
memory root. The docs’ example is /memories/../../secrets.env:
def resolve(self, path):
# model-written paths must stay inside the memory root
rel = path.removeprefix("/memories").removeprefix("/") or "."
p = (self.base / rel).resolve()
if not p.is_relative_to(self.base.resolve()):
raise ValueError(f"path escapes the memory directory: {path}")
Created /memories/notes.md
Error: path escapes the memory directory: /memories/../../escape.txt
The experiment runs two sessions. Session 1 reads half the corpus and saves its findings. Session 2 asks for the comparison of all six organisms, once with an empty memory directory and once with session 1’s notes:
[session 1]
turn 1 context 2,113 memory
turn 2 context 2,191 read_file, read_file, read_file
turn 3 context 46,577 memory, record_finding
turn 4 context 47,431 final answer
session 1: 3 file reads, peak 47,431; saved 1 file(s), 525 tokens
[session 2, without memory]
turn 1 context 2,081 memory
turn 2 context 2,159 search_files, search_files
turn 3 context 2,471 read_file, read_file, read_file, read_file, read_file, read_file
turn 4 context 91,220 memory
turn 5 context 91,650 final answer
session 2 without memory: 6 file reads, 5 turns, peak 91,650
[session 2, with memory]
turn 1 context 2,081 memory
turn 2 context 2,169 memory, search_files
turn 3 context 3,006 read_file, read_file, read_file
turn 4 context 47,386 memory
turn 5 context 48,298 final answer
session 2 with memory: 3 file reads, 5 turns, peak 48,298
API usage: 390,833 uncached input, 0 cache-write, 0 cache-read, 5,354 output tokens; $0.84
525 tokens of notes replaced three document reads in the next session.
Data: context-engineering-compaction-clearing-memory/memory.pySession 1 wrote one file of 525 tokens. Session 2 with it read only the three documents session 1 had not, and its window peaked at 48,298 tokens instead of 91,650. Memory does not shrink the window it runs in. Its value is the work the next session does not repeat, and it is only as good as what the agent chose to write: the notes stand in for the documents, so a detail left out of them is gone. The cookbook measured the same effect: 4 file reads instead of 8 with memory.
The backend is the application’s responsibility, and the docs list what it
must handle: validate every path, cap file sizes and what view returns,
expire old files, and keep secrets out, since memory is replayed into every
session that reads it.
All three together
The primitives compose. Clearing goes first because it is free; compaction waits for what clearing cannot remove; memory sits underneath and is excluded from clearing so the agent never clears its own notes:
s = run_session(TASK + " Save key findings to /memories as you go.", memory=memory, label="all three", edits=[
{"type": "clear_tool_uses_20250919",
"trigger": {"type": "input_tokens", "value": 30_000},
"keep": {"type": "tool_uses", "value": 2},
"exclude_tools": ["memory"]},
{"type": "compact_20260112",
"trigger": {"type": "input_tokens", "value": 50_000}}])
[all three]
turn 1 context 2,145 memory
turn 2 context 2,223 search_files
turn 3 context 2,351 read_file, read_file, read_file, read_file, read_file, read_file
turn 4 context 32,229 memory, record_finding, record_finding, read_file clearing
turn 5 context 33,102 memory, record_finding, read_file clearing
turn 6 context 33,553 memory, record_finding, read_file clearing
turn 7 context 33,986 memory, record_finding, read_file clearing
turn 8 context 34,441 record_finding, memory clearing
turn 9 context 34,831 final answer clearing
peak context 34,831 after 9 turns and 10 file reads; 6 clearing pass(es), 0 compaction(s)
API usage: 208,861 uncached input, 0 cache-write, 0 cache-read, 3,146 output tokens; $0.45
The context the model processed on each turn, one run of each. For compaction the plot also shows the summarizing step’s input, so its drop happens inside one request; for clearing it shows only the context after the edit, which on turn 3 was cut from about 89K.
Data: context-engineering-compaction-clearing-memory/resultsOn turn 4 the request the client sent held all six documents, about 90K
tokens, past both triggers. Clearing, listed first, cut it to 32,229 before
compaction’s check, and the context stayed under 35K, below the 50K
compaction trigger, so compaction never ran. That clearing ran first is what
this run shows; the docs do not state the order. Configuring three primitives does not mean three run;
the cheap one may absorb the whole problem. Here it absorbed it at a price:
10 file reads instead of 6, because keep was 2 for a task that needs six
documents together. The thresholds have to be set against the workload’s
real shape, and the shape can surprise. Sonnet 5.5 read all six documents in
one turn, so the uncleared request jumped past both triggers at once, where
the cookbook’s batched agent climbed in steps.
Choosing
The measurements suggest where to start. Each row is a hypothesis to test on the real workload, not a rule:
| If the workload has… | Start with | Watch for |
|---|---|---|
| Large results, each used once (search pages, logs) | Clearing | Re-reads if keep is too small |
| Large results needed together, as in this task | Compaction, with instructions | Details the summary drops |
| Results that cannot be fetched again (uploads, live APIs) | Compaction over clearing | The same; probe the summary |
| Dialogue as the bulk of the context | Compaction | Specific figures summarized away |
| Work that spans sessions or days | Memory | Stale notes; what the agent did not write down |
| Sessions that stay well under the window | Nothing | Paying in fidelity for headroom not needed |
Two cost facts sharpen the choice. Clearing and compaction both change the prompt after its first edited block when they fire, so each firing pays a new cache write for everything after that point. And on Claude Haiku 5.5, the Pricing page says “a request whose prompt is over 100,000 tokens pays higher prices”, and that the prompt length “counts all of its input tokens, including cache reads and cache writes”. Base input goes from $0.10 to $0.50 per million tokens, 5 times the price, so on that model keeping the window under 100K is worth money directly.
A bigger window moves the wall, not the slope. The baseline here filled at the same rate it would on any window size, and context rot grows with what is in the window, not with its limit.
Where this leads
Clearing, compaction and memory are server and client features around one loop. Agent Memory takes the memory side further: what to write, when to read it back, and how to update and forget it. Many Agents uses another way to keep a window small, giving a subtask to a subagent with its own window and taking back only its answer. Recursive Language Models goes further still and keeps the long input out of the window altogether.
Takeaways
- Measure what fills the window first. Here file reads were 97.7% of it,
which ruled out summarizing the dialogue. Count the context as the sum of
input_tokensand both cache fields. - Clearing is free in inference and costs tool calls. With
keepat 2 it held the peak near 33K, about 36% of the baseline, and the agent re-read documents up to 10 extra times. It suits results used once. - Compaction keeps what stands out. Lifespans survived every summary; identifiers among lookalikes survived in no run with the default prompt and in every run with instructions naming them, given room to write them. Probe the summaries of the real task.
- Compaction’s own cost hides in
usage.iterations, and so does the size of its input. - Memory pays off in the next session. 525 tokens of notes halved the reads of session 2 and nearly halved its peak, in one run. The backend, and its path checks, are the application’s to write.
- Composed primitives may not all fire. The combined run never compacted, because clearing kept the context under the trigger.
References
Start with Anthropic’s context-engineering cookbook, which this post’s experiment follows, then the three docs pages for the exact parameters, and the engineering blog for the reasoning behind them.
- Anthropic, Context engineering: memory, compaction, and tool clearing, the cookbook notebook at commit
2fcb7b4. The source of every number labeled as the cookbook’s: the 335,279-token baseline, the 96.3% estimate, the 3/3 and 0/3 probe and the 4-versus-8 memory reads. - Anthropic, Context editing.
clear_tool_uses_20250919, its parameters and defaults, and its cache behavior. - Anthropic, Compaction at a token threshold.
compact_20260112: trigger, instructions, thecompactionblock,usage.iterationsand the limitations quoted here. The compaction overview compares it with on-demand compaction. - Anthropic, Memory tool. The commands, the injected system prompt and the security list.
- Anthropic, Prompt caching. The formula for total input tokens.
- Anthropic, Pricing. Sonnet 5.5’s prices, used for the costs the scripts print, and Haiku 5.5’s long-prompt pricing.
- Rajasekaran, Dixon, Ryan and Hadfield, Effective context engineering for AI agents, Anthropic, 2025. The definition of context rot quoted above and the goal of context engineering.
- Hong, Troynikov and Huber, Context Rot: How Increasing Input Tokens Impacts LLM Performance, Chroma, 2025. The measurements behind context rot, across 18 models.