Skip to content
Markdown

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_context compresses 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_memory takes 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.

A later system keeps the reversibility argument and drops the fixed pair of tools: the history becomes an append-only event log inside a live Python kernel, and the agent writes code to build whatever working view it needs at query time instead of committing to one at compression time. That variant, and its recoverable-eviction algorithm, is programmatic context management.

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.
  • Archived history remains addressable without replaying it. The working prompt carries a summary and an identifier rather than the raw block. The identifier and query results still consume tokens, so measure their actual serialized size instead of treating the pointer as a free token.
  • 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 to use it

Validated mechanism and result audit

import re

import numpy as np

# ================= 1. summarise, archive, and retrieve =========================
TAG_PRIORITY = {"decision": 0, "constraint": 1, "error": 2, "detail": 3}


def terms(text):
    return set(re.findall(r"[a-z0-9_-]+", text.lower()))


def summarise(messages, budget):
    """Deterministic stand-in for a summariser LLM with a fixed item budget."""
    assert budget > 0
    ranked = sorted(
        enumerate(messages),
        key=lambda item: (TAG_PRIORITY[item[1].split(":", 1)[0]], item[0]),
    )
    return [message for _, message in ranked[:budget]]


def query_extract(messages, query, limit=1):
    """Deterministic stand-in for query_memory's query-conditioned LLM extract."""
    q = terms(query)
    scored = [(len(q & terms(message)), -i, message) for i, message in enumerate(messages)]
    return [message for score, _, message in sorted(scored, reverse=True)
            if score > 0][:limit]


class LossySummary:
    """Summarise, then discard the messages that the summary omitted."""

    def __init__(self, budget):
        self.budget, self.working = budget, []

    def compress(self, messages):
        self.working = summarise(messages, self.budget)

    def query(self, text):
        return query_extract(self.working, text)


class ACM:
    """Use the same summary, but archive raw messages under a unique id."""

    def __init__(self, budget):
        self.budget, self.working, self.store, self.next_id = budget, [], {}, 1

    def manage_context(self, messages):
        sid = f"memory-{self.next_id:04d}"
        self.next_id += 1
        assert sid not in self.store
        self.store[sid] = tuple(messages)
        self.working = [f"archive_id:{sid}", *summarise(messages, self.budget)]
        return sid

    def query_memory(self, sid, query):
        if sid not in self.store:
            raise KeyError(sid)
        return query_extract(self.store[sid], query)


MESSAGES = [
    "detail: checksum sha256-deadbeef",
    "decision: use nccl for collectives",
    "constraint: gpu memory limit is 80gb",
    "error: timeout occurs after 30 seconds",
    "detail: owner is platform-team",
    "detail: region is eu-central-1",
    "detail: dataset version is v7",
    "detail: retry budget is four",
]
QUERIES = ["checksum", "nccl", "80gb", "timeout", "owner", "region", "dataset", "retry"]
BUDGET = 3
lossy, acm = LossySummary(BUDGET), ACM(BUDGET)
lossy.compress(MESSAGES)
sid = acm.manage_context(MESSAGES)

# Both regimes use the same real summary; ACM adds an archive pointer.
assert acm.working[1:] == lossy.working
assert len(lossy.working) == BUDGET
assert len(acm.working) == BUDGET + 1
assert not lossy.query("checksum")
assert acm.query_memory(sid, "checksum") == [MESSAGES[0]]

lossy_recall = sum(bool(lossy.query(q)) for q in QUERIES) / len(QUERIES)
acm_recall = sum(bool(acm.query_memory(sid, q)) for q in QUERIES) / len(QUERIES)
assert lossy_recall == BUDGET / len(MESSAGES)
assert acm_recall == 1.0
assert len(acm.store[sid]) == len(MESSAGES)

# STORAGE losslessness and RETRIEVAL recall are different properties. The archive
# is byte-exact regardless of the query, so storage recall is always 1.0...
assert acm.store[sid] == tuple(MESSAGES)
storage_recall = sum(m in acm.store[sid] for m in MESSAGES) / len(MESSAGES)
assert storage_recall == 1.0
# ...but retrieval goes through a matcher, and a query that shares no surface
# term with the archived message returns nothing even though the message is
# still there. The queries above were chosen to hit; these are not.
SEMANTIC_QUERIES = ["collective communication library", "how long before it gives up",
                    "which availability zone"]
assert all(acm.query_memory(sid, q) == [] for q in SEMANTIC_QUERIES)
semantic_recall = sum(bool(acm.query_memory(sid, q)) for q in SEMANTIC_QUERIES) / len(SEMANTIC_QUERIES)
assert semantic_recall == 0.0 < storage_recall
# The facts those three queries ask for are all present in the archive.
assert any("nccl" in m for m in acm.store[sid])
assert any("timeout" in m for m in acm.store[sid])
assert any("region" in m for m in acm.store[sid])

# Failure boundaries: unknown ids fail closed; irrelevant queries return no extract.
try:
    acm.query_memory("memory-9999", "checksum")
except KeyError:
    pass
else:
    raise AssertionError("unknown archive id must fail closed")
assert acm.query_memory(sid, "unmentioned-token") == []

# ================= 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("  working items  lossy / ACM:", len(lossy.working), "/", len(acm.working))
print("  archived raw messages:", len(acm.store[sid]))
print("  recall after compression  lossy / ACM:", lossy_recall, "/", acm_recall)
print("  ACM storage recall / semantic-miss recall:", storage_recall, "/", semantic_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
  working items  lossy / ACM: 3 / 4
  archived raw messages: 8
  recall after compression  lossy / ACM: 0.375 / 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')]

The archive preserves access without replaying raw history

Block 1 gives both regimes the same deterministic summariser and three-item budget. The lossy regime retains three of eight queryable facts. ACM carries those same three summary items plus an archive identifier, and retrieves all eight facts from the archived raw messages. This demonstrates the reversibility invariant without pretending that a list item equals one model token: production code must tokenize the serialized summary, identifier, and query result.

The block also asserts two failure boundaries: an unknown id raises KeyError, and a query with no matching term returns no extract. The id-to-messages mapping is the only thing making the compression reversible, so its integrity is the invariant to protect.

Lossless storage is not guaranteed retrieval

This is the distinction the word "lossless" hides, and it decides whether the technique works for you.

Storage is lossless in the strict sense. manage_context writes the raw messages to the workspace unchanged, so nothing that entered the archive is altered or dropped. The block asserts this directly: acm.store[sid] equals the original message tuple, and storage recall is 1.0.

Retrieval is a separate, lossy step. Getting a fact back requires query_memory to be called, with the right id, carrying a query that the querier LLM can match against the archived text. Each of those is a place to fail, and none of them is guaranteed by the storage property. The block makes the gap concrete: three queries phrased the way an agent might actually phrase them ("collective communication library", "how long before it gives up", "which availability zone") return nothing, while the facts they are asking for are all still sitting in the archive. Storage recall stays at 1.0; retrieval recall for those queries is 0.0.

The block's matcher is deliberately crude (surface-term overlap), and a real querier LLM would resolve at least the first of those. That is the point: the paper's system replaces a term matcher with an LLM, which changes the failure rate but not the failure mode. An LLM querier still has to be given a query that surfaces the right message, still operates on whatever slice of the archive it is shown, and still returns a summary of what it found rather than the bytes themselves.

Three consequences for anyone deploying this:

  • The reversibility claim is about the archive, not the answer. "Nothing is lost" means you retain the option to recover a detail. Whether the agent exercises that option correctly is an agent-behaviour question, and it is what the post-training stage is actually teaching.
  • Instrument retrieval, not just storage. Archive bytes written tells you nothing about whether the agent is getting its facts back. Track query count, empty-result rate, and, where you can label it, whether the retrieved extract contained the fact the agent needed.
  • A failed retrieval is worse than a failed summary. When a summary agent drops a detail, the agent has no reason to think it still has it. When ACM retrieves nothing, the agent may conclude the fact does not exist rather than that its query missed, and proceed confidently on a false negative.

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.

What the release actually ships

The paper landed on 2026-07-26 with no code. Both are now public: the repository at lixiaochuan2020/agentic-context-management (MIT, last pushed 2026-08-03, commit f06f90e) and a nine-item Hugging Face collection holding three post-trained checkpoints (acm-browsecompplus-qwen3.5-9b-opd-iter1 through iter3, 10B each) and six datasets (student rollouts and cached teacher logprobs). The repo does not tag releases; pin the commit.

The two-tool design in this page is what the code implements. Executing the released tool registry confirms it and surfaces two things the paper does not say. The block below stubs the repo's heavy HTTP and HTML dependencies, which the registry does not touch, and calls get_tools directly at that commit.

"""Execute the released ACM tool registry with stubbed heavy deps.

git clone https://github.com/lixiaochuan2020/agentic-context-management acm-repo
git -C acm-repo checkout f06f90e728af8580a4515812425c1620144145a2
"""
import sys, types, pathlib

REPO = pathlib.Path("acm-repo").resolve()
sys.path.insert(0, str(REPO))
for name in ("html2text", "lxml", "lxml.html", "lxml.etree", "pypdf", "requests", "dotenv"):
    mod = types.ModuleType(name)
    if name == "dotenv":
        mod.load_dotenv = lambda *a, **k: None
    sys.modules.setdefault(name, mod)
sys.modules["lxml"].html = sys.modules["lxml.html"]
sys.modules["lxml"].etree = sys.modules["lxml.etree"]

from src.tools import get_tools, BENCHMARK_TOOLS

names = lambda ts: [t["function"]["name"] for t in ts]

# 1. Only the two search benchmarks are registered. The paper also reports
#    DeepSearchQA and SWE-Bench Verified; neither has a tool set or a loader here.
assert sorted(BENCHMARK_TOOLS) == ["browsecomp", "browsecomp-plus"]
assert names(get_tools("browsecomp-plus")) == ["search", "get_document"]
assert names(get_tools("browsecomp-plus", use_memory_tools=True)) == [
    "manage_context", "search", "get_document", "query_memory"]

# 2. The no-query ablation is not a clean tool-set ablation: dropping
#    query_memory also rewrites manage_context's description, so the two arms
#    differ in what the agent is TOLD as well as in what it can call.
acm = get_tools("browsecomp-plus", use_memory_tools=True)
noq = get_tools("browsecomp-plus", use_memory_tools=True, disable_query_memory=True)
assert "query_memory" in names(acm) and "query_memory" not in names(noq)
desc = lambda ts: next(t for t in ts
                       if t["function"]["name"] == "manage_context")["function"]["description"]
assert desc(acm) != desc(noq)
assert "saved to disk" in desc(acm)          # offload-and-retrieve framing
assert "permanent" in desc(noq)              # summarise-and-discard framing
print("registered benchmarks:", sorted(BENCHMARK_TOOLS))
print("ACM tools    :", names(acm))
print("ablation arm :", names(noq))

Output:

registered benchmarks: ['browsecomp', 'browsecomp-plus']
ACM tools    : ['manage_context', 'search', 'get_document', 'query_memory']
ablation arm : ['manage_context', 'search', 'get_document']

Four things to know before you try to reproduce this:

  • Only the search half is reproducible from the repo. The registry and src/data_loading/ cover browsecomp and browsecomp-plus only. The shipped question sets are bcp_train_680.json (680 items), bcp_eval_150.json (150) and bcp_full.json (830), which matches the paper's n=4x150 evaluation. DeepSearchQA and SWE-Bench Verified appear in the paper's tables and nowhere in the code.
  • The on-policy distillation is run off-policy, and the README says so. Stages 4 and 5 are split because of GPU supply: the teacher's top-K logprobs are cached first, then the student trains against the cache. Once the student updates, its parameters no longer match the rollouts those logprobs were scored on. That is a straightforward and well-flagged compromise, but it means the released checkpoints are not a demonstration of true on-policy distillation.
  • The teacher is the expensive part. Qwen3.5-397B-A17B served in bf16 on 8xB200, or fp8 on 8xH100, alongside the 9B student. Retrieval defaults to local BM25 through pyserini, which needs a JDK on the path, and the BrowseComp-Plus corpus and Lucene index are cloned separately from the hltcoe/BrowseComp-Plus dataset repo.
  • Smoke first. SMOKE=1 bash scripts/run_bcp_opd_pipeline.sh runs the plumbing end to end before you commit GPUs to the full pipeline. All machine-specific paths live in scripts/pipeline_config.sh.

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:

  1. Run the student twice per task, once with the ACM tools available and once without.
  2. A teacher model reviews each trajectory against the reference answer and applies one of two edits:
  3. Injection: identifies turns where context management would have helped and inserts the ACM action there, writing the first-person rationale for it.
  4. 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).
  5. 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_context handles only what accumulated since the last one. This keeps the summariser's own input bounded.
  • query_memory returns 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 maintain it

  • Version the summariser and querier prompts with every archive record so a changed prompt does not make old evaluations irreproducible.
  • Enforce unique, unguessable ids and test archive round trips, missing-id rejection, and cross-episode isolation.
  • Track serialized summary tokens, retrieval-result tokens, archive bytes, and retrieval miss rate separately.
  • Garbage-collect archives only after the owning episode and its audit-retention window end.

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_context repeatedly 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.
  • Treating lossless storage as guaranteed recall. The archive is byte-exact, but retrieval depends on the agent issuing a query the querier can match. A miss returns nothing while the fact sits in the archive, and the agent may read that as the fact not existing. Instrument empty-result rate, not just bytes written.
  • 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
  • ACM code (MIT, commit f06f90e, pushed 2026-08-03): https://github.com/lixiaochuan2020/agentic-context-management
  • ACM checkpoints and datasets (three OPD iterations plus six rollout and logprob sets): https://huggingface.co/collections/lixiaochuan2020/acm-agentic-context-management-for-long-horizon-tasks-6a5d7d231cf4f1044dbbcae3
  • 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: Programmatic context management · When to compact · Compaction-aware RL · 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