Skip to content
Residual Stream
Go back

Context Engineering: Compaction, Tool Clearing, and Memory

Updated:
Tan Le
ENVI

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.

Three ways to keep tool output from filling the window no management clearing compaction memory task call result, 15K call result, 15K call result, 15K every result stays task call stub call stub call result, 15K old results become stubs; the calls stay summary call the whole transcript becomes one model-written summary session 1 /memories session 2 files the agent writes, outside the window, read by the next session

The three primitives on one transcript. Clearing and compaction shrink the window inside a session; memory carries notes into the next one.

At a glance
  • 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:

PartGrows?Lifetime
System prompt: instructions, persona, rulesNoThe whole session
Tool definitionsOnly when tools changeThe whole session
Retrieved documentsPer questionUntil the question is answered
Conversation historyEvery turnUntil summarized or dropped
Tool resultsEvery tool call, often by thousands of tokensUsually a few steps
The agent’s own reasoningEvery stepUsually the current step
Memory from earlier sessionsSlowlyAcross 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)

Full script: harness.py

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()

Full script: baseline.py

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 (90,421 tokens) file-read results, 88,317 tokens (97.7%) everything else: task, tools, notes, answer (2,104)

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.py

That 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")

Full script: kvcost.py

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

Full script: scratch.py

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"]}

Full script: clearing.py

ParameterDefaultWhat it does
trigger100,000 input tokensPrompt size at which clearing starts; can also count tool uses
keep3 tool usesHow many recent tool uses and results stay whole
clear_at_leastnoneSkip the edit unless it frees at least this many tokens
exclude_toolsnoneTools whose results are never cleared, such as memory
clear_tool_inputsfalseAlso 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.

file reads per run (the task needs 6) 0 5 10 15 20 12 16 16 10 14 12 6 keep 2 keep 4 keep 6 peak context (K tokens) 0 25 50 75 100 33 33 33 62 62 62 91 keep 2 keep 4 keep 6 three runs each for keep 2 and 4; keep 6 never frees the 5K minimum, so it clears nothing

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.py

Three 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>."))

Full script: compaction.py

ParameterDefaultWhat it does
trigger150,000 input tokens, at least 50,000Prompt size at which compaction runs
instructionsthe model’s default promptReplaces the summarization prompt entirely
pause_after_compactionfalseReturn 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

Full script: harness.py

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})

Full script: harness.py

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
Source identifiers an agent could recover from the summary alone (of 6) run 1 run 2 run 3 summary size plain corpus, default prompt 6/6 6/6 6/6 821–1,008 tokens plain corpus, instructions 6/6 6/6 6/6 893–1,116 tokens dense corpus, default prompt 0/6 0/6 0/6 1,177–1,259 tokens dense corpus, instructions 6/6 6/6 6/6 2,528–2,853 tokens Every summary kept all 6 lifespans. Plain: one identifier per document; dense: one among 29 lookalikes.

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.py

Every 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}")

Full script: harness.py

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

Full script: memory.py

Session 2, with and without session 1's 525-token notes file reads 0 2 4 6 8 6 3 empty /memories session 1's notes peak context (K tokens) 0 25 50 75 100 91.7 48.3 empty /memories session 1's notes

525 tokens of notes replaced three document reads in the next session.

Data: context-engineering-compaction-clearing-memory/memory.py

Session 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}}])

Full script: combined.py

[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
Context sent on each turn, one run each (Sonnet 5.5, 88K-token corpus) 0K 25K 50K 75K 100K 1 2 3 4 5 6 7 8 9 turn clearing trigger compaction trigger no management clearing, keep 2 compaction all three 90,421 89,615 summarized Clearing holds the context near its trigger; compaction drops it to a summary inside the request.

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/results

On 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 withWatch for
Large results, each used once (search pages, logs)ClearingRe-reads if keep is too small
Large results needed together, as in this taskCompaction, with instructionsDetails the summary drops
Results that cannot be fetched again (uploads, live APIs)Compaction over clearingThe same; probe the summary
Dialogue as the bulk of the contextCompactionSpecific figures summarized away
Work that spans sessions or daysMemoryStale notes; what the agent did not write down
Sessions that stay well under the windowNothingPaying 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_tokens and both cache fields.
  • Clearing is free in inference and costs tool calls. With keep at 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.


Written by

Tan Le

AI engineer. I write these notes to learn LLM internals and AI engineering in depth, and to keep what I learn where I can find it again.