Agentic context management¶
Scope: making context compression a decision the agent makes and can reverse, rather than a heuristic that fires on a threshold and destroys history. This page covers the two-tool design (manage_context and query_memory), why offload-and-retrieve behaves differently from summarise-and-discard, the dual-constraint teacher pipeline that trains an agent to compress at the right moment, and a careful read of the published results including one claim the table does not support. It is the technique-level counterpart to context and memory and an alternative to the boundary-based approach in hierarchical agent decomposition.
The numpy and stdlib block below is executed and asserted in this page (Python 3.11, numpy 2.x). It implements the two compression regimes as small classes to make the recall difference concrete, and it audits Table 2 of arXiv:2607.23809 against the claims made about it in the same paper. The benchmark numbers are the paper's; the audit conclusions are ours and are derived from those numbers in the block.
What it is¶
An agent running a long task accumulates history until something has to give. There are three established responses, and ACM is a fourth.
| Approach | Who triggers compression | What happens to the raw history |
|---|---|---|
| ReAct (none) | nobody | kept verbatim until the window ends the run |
| Summary agent | an external monitor at a usage threshold | discarded after summarising |
| Memory agent | continuous background reflection or distillation | moved to an external store, updated out of band |
| ACM | the agent itself, as a tool call | offloaded to disk, retrievable by id |
ACM adds exactly two tools:
manage_contextcompresses all messages up to the previous summary boundary using a summariser LLM, and saves the original messages to the agent's external workspace. Each summary is assigned a unique identifier mapping it to those raw messages.query_memorytakes an identifier and a query. A querier LLM receives the query plus the raw messages under that id and returns the relevant information as a tool result.
The analogy the paper draws is short-term against long-term human memory: keep what is immediately relevant in working memory, offload the rest to persistent records, retrieve on demand.
Why use it¶
- Compression stops being destructive. Under a summary agent, a detail dropped at turn 20 is gone at turn 60 when it turns out to matter. Under ACM the summary is a pointer, not a replacement.
- The agent knows when it is about to need something. A threshold monitor fires on token count, which is uncorrelated with whether the agent is mid-thought. The paper's framing is that heuristic triggers are "misaligned with the agent's evolving reasoning focus."
- It buys exploration. With compression under its own control the agent takes more turns and more tool calls before committing. On BrowseComp-Plus the post-trained agent averages 46.2 tool calls per episode against 19.5 for ReAct, more than double.
- The working context does not get bigger. As the executed block shows, the lossless version carries the same working-context size as the lossy one plus a single summary identifier. Losslessness costs disk, not window.
- It is two tools, not an architecture. Any harness with a tool loop and a filesystem can implement it.
When to use it (and when not)¶
Use it when episodes are long and multi-turn, the environment returns bulky observations (search results, file contents, stack traces), and an early detail can turn out to matter late.
Do not use it when:
- Episodes fit in the window. Then this is two extra tools for the model to misuse.
- You have no durable per-episode workspace. The offload target has to survive the whole episode and be addressable by id.
- You cannot afford the extra model calls. Both tools invoke an LLM (a summariser and a querier). That is real cost on top of the agent's own calls.
- You will not train the timing. The base (untrained) agent with these tools does help, but most of the published gain comes from the post-training that teaches when to call them. Shipping the tools alone gets you the smaller half of the benefit.
Architecture¶
flowchart TB
subgraph LOOP["Agent loop"]
A["action a_t: reasoning + tool calls"]
O["observation o_t from environment"]
A --> O --> A
end
A -->|"manage_context"| MC["summariser LLM<br/>compress up to previous<br/>summary boundary"]
MC -->|"raw messages"| STORE[("external workspace<br/>summary id -> raw messages")]
MC -->|"concise summary + id"| CTX["working context<br/>(bounded)"]
A -->|"query_memory(id, query)"| QM["querier LLM"]
STORE -->|"raw messages for that id"| QM
QM -->|"relevant content as tool result"| CTX
CTX --> A
SUM["Summary-agent baseline:<br/>monitor fires at a threshold,<br/>prior messages DISCARDED"] -.->|"contrast"| CTX
How it works, validated¶
import numpy as np
# ================= 1. lossless vs lossy compression, mechanically ============
class LossySummary:
"""Summary agent: compress, then DISCARD the raw messages."""
def __init__(self, keep):
self.keep, self.summary = keep, []
def compress(self, messages):
self.summary = list(messages[-self.keep:]) # only what fits survives
def lookup(self, fact):
return fact in self.summary
class ACM:
"""manage_context: compress AND offload raw messages to external storage,
keyed by a summary id. query_memory: retrieve from that id on demand."""
def __init__(self, keep):
self.keep, self.summary, self.store, self.next_id = keep, [], {}, 0
def manage_context(self, messages):
sid = self.next_id
self.next_id += 1
self.store[sid] = list(messages) # nothing is thrown away
self.summary = [f"<summary {sid}>"] + list(messages[-self.keep:])
return sid
def query_memory(self, sid, fact):
return fact in self.store.get(sid, [])
msgs = [f"fact_{i}" for i in range(50)]
KEEP = 5
lossy, acm = LossySummary(KEEP), ACM(KEEP)
lossy.compress(msgs)
sid = acm.manage_context(msgs)
# An early fact is unreachable for the summary agent and reachable for ACM.
assert not lossy.lookup("fact_0")
assert acm.query_memory(sid, "fact_0")
# Recall over the whole history: k/N versus 1.0.
lossy_recall = sum(lossy.lookup(m) for m in msgs) / len(msgs)
acm_recall = sum(acm.query_memory(sid, m) for m in msgs) / len(msgs)
assert abs(lossy_recall - KEEP / len(msgs)) < 1e-12
assert acm_recall == 1.0
assert acm_recall > lossy_recall
# Working context is the SAME size for both: losslessness costs disk, not window.
assert len(acm.summary) == len(lossy.summary) + 1 # +1 for the summary id
# A wrong or missing id retrieves nothing, so the id mapping is load-bearing.
assert not acm.query_memory(999, "fact_0")
# ================= 2. Table 2 of arXiv:2607.23809 ============================
# (Pass@1, avg tool calls, avg peak tokens) per benchmark.
BENCH = ["BrowseComp-Plus", "DeepSearchQA", "SWE-Bench Verified"]
T2 = {
"Qwen3.5-397B-A17B": [(0.653, 15.6, 51_000), (0.710, 28.3, 47_000), (0.682, 58.9, 38_000)],
"Gemini3-Flash": [(0.733, 22.9, 72_000), (0.619, 54.3, 121_000), (0.732, 66.7, 80_000)],
"ReAct": [(0.570, 19.5, 63_000), (0.367, 47.4, 46_000), (0.489, 74.7, 59_000)],
"ReSum": [(0.608, 24.7, 68_000), (0.371, 48.6, 79_000), (0.475, 75.2, 61_000)],
"ACON": [(0.614, 28.2, 65_000), (0.380, 51.3, 54_000), (0.480, 76.1, 57_000)],
"ACE": [(0.589, 19.8, 71_000), (0.352, 48.2, 70_000), (0.494, 75.6, 65_000)],
"ACM Base": [(0.635, 30.8, 59_000), (0.405, 88.7, 42_000), (0.508, 77.6, 46_000)],
"ACM Post-Trained": [(0.727, 46.2, 54_000), (0.425, 58.8, 41_000), (0.530, 79.3, 50_000)],
}
ACC, TOOLS, PEAK = 0, 1, 2
# -- the headline "27% / 16% / 8%" is RELATIVE improvement, not points --------
rel = [(T2["ACM Post-Trained"][i][ACC] - T2["ReAct"][i][ACC]) / T2["ReAct"][i][ACC]
for i in range(3)]
assert [round(r * 100) for r in rel] == [28, 16, 8], [round(r * 100, 1) for r in rel]
assert abs(rel[0] - 0.2754) < 1e-4 # rounds to 27% one way, 28% the other
# In absolute points the same result is far smaller, and quoting it as points
# would be a much weaker claim.
pts = [T2["ACM Post-Trained"][i][ACC] - T2["ReAct"][i][ACC] for i in range(3)]
assert [round(p * 100, 1) for p in pts] == [15.7, 5.8, 4.1]
assert all(p * 100 < r * 100 for p, r in zip(pts, rel))
# -- the "around 20% peak token reduction" claim does not hold for the ------
# -- post-trained model on this table; only one Base cell gets near it. ------
peak_red_pt = [1 - T2["ACM Post-Trained"][i][PEAK] / T2["ReAct"][i][PEAK] for i in range(3)]
peak_red_base = [1 - T2["ACM Base"][i][PEAK] / T2["ReAct"][i][PEAK] for i in range(3)]
assert all(r < 0.16 for r in peak_red_pt), peak_red_pt
assert abs(float(np.mean(peak_red_pt)) - 0.1350) < 1e-3
assert max(peak_red_base) > 0.20 # SWE-Bench, Base variant
assert abs(max(peak_red_base) - 0.2203) < 1e-3
assert float(np.mean(peak_red_base)) < 0.13 # the mean is nowhere near 20%
# -- tool calls go UP everywhere, which is the exploration claim -------------
assert all(T2["ACM Post-Trained"][i][TOOLS] > T2["ReAct"][i][TOOLS] for i in range(3))
assert T2["ACM Post-Trained"][0][TOOLS] / T2["ReAct"][0][TOOLS] > 2.3
# -- lossy context management can be NET NEGATIVE against plain ReAct -------
regressions = [(name, BENCH[i]) for name in ("ReSum", "ACON", "ACE")
for i in range(3) if T2[name][i][ACC] < T2["ReAct"][i][ACC]]
assert regressions, "expected at least one baseline to regress"
assert ("ReSum", "SWE-Bench Verified") in regressions
assert ("ACON", "SWE-Bench Verified") in regressions
assert ("ACE", "DeepSearchQA") in regressions
assert len(regressions) == 3
# Worse: several "context management" baselines RAISE peak tokens vs ReAct.
peak_up = [(n, BENCH[i]) for n in ("ReSum", "ACON", "ACE")
for i in range(3) if T2[n][i][PEAK] > T2["ReAct"][i][PEAK]]
assert len(peak_up) >= 5
assert ("ReSum", "DeepSearchQA") in peak_up # 79K vs 46K, a 72% increase
assert T2["ReSum"][1][PEAK] / T2["ReAct"][1][PEAK] > 1.7
# -- the frontier comparison generalises on ONE benchmark only --------------
beats_397b = [i for i in range(3)
if T2["ACM Post-Trained"][i][ACC] > T2["Qwen3.5-397B-A17B"][i][ACC]]
assert beats_397b == [0] # BrowseComp-Plus only
assert T2["ACM Post-Trained"][0][ACC] < T2["Gemini3-Flash"][0][ACC]
# On the other two it trails both frontier models by a wide margin.
for i in (1, 2):
assert T2["ACM Post-Trained"][i][ACC] < T2["Qwen3.5-397B-A17B"][i][ACC] - 0.14
print("all ACM assertions passed")
print(" recall after compression lossy / ACM:", lossy_recall, "/", acm_recall)
print(" relative gain vs ReAct (%):", [round(r * 100, 1) for r in rel])
print(" absolute gain vs ReAct (pts):", [round(p * 100, 1) for p in pts])
print(" peak-token reduction, post-trained (%):", [round(r * 100, 1) for r in peak_red_pt])
print(" peak-token reduction, base (%):", [round(r * 100, 1) for r in peak_red_base])
print(" baseline regressions vs plain ReAct:", regressions)
Executed output:
all ACM assertions passed
recall after compression lossy / ACM: 0.1 / 1.0
relative gain vs ReAct (%): [27.5, 15.8, 8.4]
absolute gain vs ReAct (pts): [15.7, 5.8, 4.1]
peak-token reduction, post-trained (%): [14.3, 10.9, 15.3]
peak-token reduction, base (%): [6.3, 8.7, 22.0]
baseline regressions vs plain ReAct: [('ReSum', 'SWE-Bench Verified'), ('ACON', 'SWE-Bench Verified'), ('ACE', 'DeepSearchQA')]
Losslessness costs disk, not window¶
Block 1 is deliberately small, because the property it demonstrates is structural rather than statistical. After compressing 50 messages down to a 5-message working set, the summary agent can reach 10% of its own history and ACM can reach 100%, while both carry a working context of the same size (ACM's is larger by exactly one token-sized summary identifier). The entire difference is that one wrote the raw messages to disk before shortening the prompt and the other did not.
The block also asserts the obvious failure: query_memory against a wrong id retrieves nothing. The id-to-messages mapping is the only thing making the compression reversible, so its integrity is the invariant to protect.
Reading the published results honestly¶
The paper's headline is that ACM post-training improves Qwen3.5-9B over the ReAct baseline "by 27% on BrowseComp-Plus, 16% on DeepSearchQA, and 8% on SWE-Bench Verified". Block 2 confirms these are relative improvements computed from Table 2, and reproduces them (27.5%, 15.8%, 8.4%). Stated as absolute accuracy points the same results are 15.7, 5.8, and 4.1. Both framings are honest; they are just very different-sounding, and the relative framing is the one that made the abstract.
Three further things the table shows that are worth knowing before adopting this:
The peak-token claim does not survive the arithmetic. The paper states ACM "reduces peak token usage by around 20%". Computed from Table 2, the post-trained model's peak-token reductions against ReAct are 14.3%, 10.9% and 15.3%, a mean of 13.5%. No cell reaches 20%. The only figure near 20% anywhere in the comparison is the untrained Base variant on SWE-Bench Verified at 22.0%, and that variant's own mean is under 13%. Block 2 asserts each of these. Peak-token reduction is real and useful, but budget for roughly 13 to 15%, not 20%.
Lossy context management can be worse than doing nothing. Three of the nine baseline cells regress against plain ReAct: ReSum and ACON both lose accuracy on SWE-Bench Verified (0.475 and 0.480 against 0.489), and ACE loses on DeepSearchQA (0.352 against 0.367). Worse still, five of nine baseline cells show higher peak token usage than ReAct, most dramatically ReSum on DeepSearchQA at 79K against 46K, a 72% increase. A summariser that itself produces long output, or that triggers repeatedly, can cost more context than it saves. If you are adding context management to an agent, measure peak tokens before and after; do not assume the direction.
The frontier comparison holds on one benchmark only. The 9B post-trained agent reaches 0.727 on BrowseComp-Plus, ahead of Qwen3.5-397B-A17B at 0.653 and just behind Gemini3-Flash at 0.733. That is a genuinely striking result for a 9B model. On DeepSearchQA (0.425) and SWE-Bench Verified (0.530) it trails both frontier models by more than 14 points. Context management closes a specific gap, in long-horizon search, not the general capability gap.
How to develop with it: training the timing¶
Shipping the two tools is the easy half. The published gain comes mostly from teaching the agent when to call them, and the pipeline for that is worth copying:
- Run the student twice per task, once with the ACM tools available and once without.
- A teacher model reviews each trajectory against the reference answer and applies one of two edits:
- Injection: identifies turns where context management would have helped and inserts the ACM action there, writing the first-person rationale for it.
- Correction: finds an unproductive turn and rewrites it, producing a replacement turn (for instance a novel, productive query distinct from what the student actually did).
- The student resumes from the point of feedback and completes the task, with the teacher's assessments used as soft supervision.
These are the "dual constraints" of the name: one constraint teaches when to compress, the other teaches what to do instead of a wasted turn. Together they target the timing rather than the mechanism.
Two implementation notes:
- Compress up to the previous summary boundary, not to the start. Summaries chain, so each
manage_contexthandles only what accumulated since the last one. This keeps the summariser's own input bounded. query_memoryreturns a query-conditioned extract, not the raw block. A querier LLM reads the archived messages and returns what is relevant. Returning the raw block would defeat the purpose by re-inflating the context you just compressed.
How to run it in production¶
- Meter peak tokens per episode, not mean. Peak is what determines whether an episode dies. It is also, per the audit above, the metric where published claims and tables can diverge.
- Give the workspace a lifetime. The offload store is per-episode; garbage-collect it when the episode ends, or a long-running fleet accumulates dead archives.
- Count the summariser and querier calls in your cost model. They are LLM calls that do not appear in the agent's own turn count.
- Alert on compress-loop pathologies. An agent that calls
manage_contextrepeatedly without progress is the analogue of thrashing; it burns summariser calls and shrinks its own working set to nothing. - Treat the archive as sensitive. It is a verbatim copy of everything the agent saw, written to disk. Whatever isolation and retention rules apply to agent context apply to it. See agent sandboxing and isolation.
Failure modes¶
- Broken or reused summary ids. Silent, and it converts lossless compression into lossy compression without any error.
- The querier re-inflates the context. Returning the raw archived block instead of a query-conditioned extract undoes the compression.
- Compression that costs more than it saves. Five of nine baseline cells in the published table raise peak tokens above plain ReAct. Measure.
- Tools without trained timing. The Base variant helps, but the post-training is where most of the gain is.
- Compressing mid-reasoning. The reason for agent-invoked triggering is precisely that a threshold monitor has no idea whether the agent is halfway through a thought.
- Unbounded archive growth. Per-episode stores that outlive their episodes.
- Generalising the BrowseComp-Plus result. It is the one benchmark where a 9B model reaches frontier territory; the other two do not follow.
References¶
- ACM: Agentic Context Management for Long Horizon Tasks: https://arxiv.org/abs/2607.23809
- ReAct (the no-management baseline): https://arxiv.org/abs/2210.03629
- MemGPT / Letta (external memory paging for LLM agents): https://arxiv.org/abs/2310.08560
- BrowseComp-Plus: https://arxiv.org/abs/2508.06600
- SWE-bench Verified: https://arxiv.org/abs/2310.06770
Related: Context and memory · Agent harness architecture · Hierarchical agent decomposition · Agent loop · Agent tools and function calling · Long-context reasoning · Self-improving harnesses · Agent loop economics · Agent sandboxing and isolation · Agent observability · RAG versus CAG · Prompt caching · Agent evaluation