Journal

Agent Systems · 31 Mar 2026 · 7 min read

Context engineering for agent fleets

A long-running agent's context window is a resource with an allocation policy, an eviction strategy, and a failure mode. Treating it like a scratch buffer is why long runs degrade.

Multi-agentLLMContext

The context window is memory management. Everything the operating-systems literature says about working sets, eviction, and thrashing applies, and most agent frameworks are running without a policy.

An agent that works well for ten steps and poorly for eighty has not become less intelligent. Its working set has exceeded what it can attend to usefully, and the signal it needs is now buried under eighty steps of transcript.

Bigger windows moved the cliff; they did not remove it. Attention over a million tokens is not uniform, retrieval accuracy varies with position, and cost scales with what you actually put in there. The window is a budget, and budgets need policies.

Anatomy of a context#

For any long-running agent the window decomposes into five regions with very different economics.

| Region | Typical share | Volatility | Eviction policy | | --- | --- | --- | --- | | System / role | 3% | Never changes | Pinned | | Task statement | 2% | Never changes | Pinned, and repeated at the tail | | Tool definitions | 10–25% | Changes with phase | Swap by phase | | Working memory | 40–60% | Every step | Compact and evict | | Retrieved material | 15–30% | Every step | Evict on use |

Two of those rows are where the wins are.

Tool definitions are quietly enormous. Thirty tools with thorough JSON schemas is easily 8,000 tokens paid on every single call, and a model choosing among thirty options also chooses worse than one choosing among six. Load tools by phase: a research phase does not need the deployment tools in scope.

Working memory is where the degradation happens, and where a policy is worth the most.

Pin the task statement at both the head and the tail of the window. It is the cheapest single intervention in agent engineering, and it measurably reduces drift on long runs.

Compaction, done in tiers#

The naive approach — summarize the transcript when it gets long — loses exactly the things you need: identifiers, file paths, error strings, numbers. A tiered scheme keeps the recoverable parts recoverable.

code
class TieredMemory:
    """Recent turns verbatim, older turns compressed, facts extracted and kept.

    The invariant: anything with a stable identifier survives compaction, because
    identifiers are what let the agent re-fetch detail it discarded.
    """

    def __init__(self, verbatim_turns=6, budget=24_000):
        self.verbatim = deque(maxlen=verbatim_turns)   # tier 1: last N turns
        self.compressed: list[str] = []                # tier 2: summarized spans
        self.facts: dict[str, str] = {}                # tier 3: extracted, pinned
        self.budget = budget

    def append(self, turn):
        if len(self.verbatim) == self.verbatim.maxlen:
            evicted = self.verbatim[0]
            self.facts.update(extract_identifiers(evicted))   # ids, paths, versions
            self.compressed.append(compress(evicted))
        self.verbatim.append(turn)
        self._enforce()

    def _enforce(self):
        while self.tokens() > self.budget and len(self.compressed) > 1:
            # fold the two oldest summaries into one — lossy, but bounded
            a, b = self.compressed.pop(0), self.compressed.pop(0)
            self.compressed.insert(0, compress_pair(a, b))

    def render(self) -> str:
        return "\n\n".join([
            f"## Established facts\n{fmt(self.facts)}",
            f"## Earlier (summarized)\n{chr(10).join(self.compressed)}",
            f"## Recent\n{fmt_turns(self.verbatim)}",
        ])

extract_identifiers is the load-bearing function. A summary that says "explored the authentication module" is nearly useless; one that says "explored src/auth/session.ts, found validateToken at line 88, which throws TokenExpiredError" lets the agent go back. Identifiers are pointers, and pointers survive compression at almost no cost.

Tool results are the biggest offender#

A single directory listing, database query, or file read can be tens of thousands of tokens, and it is usually 95% irrelevant to the decision being made.

Three rules, applied at the tool boundary rather than in the prompt:

Truncate at the source

Every tool declares a maximum result size and truncates deterministically — head plus tail, with an explicit marker and a handle to fetch the rest. Never let a tool return unbounded output into a context window.

Return handles, not payloads

A search tool returns ids and one-line snippets. A read tool fetches one document by id. This turns one 40k-token result into a 400-token result plus an optional 4k-token follow-up the agent only pays for if it needs it.

Evict on use

Once a retrieved document has produced a conclusion, replace it in the context with the conclusion plus its id. The agent keeps the finding and the pointer, and drops the body.

That third one is where most of the savings live in long runs, and almost no framework does it by default.

Fleet-level context#

With multiple agents, context becomes a distributed systems problem, and the question is what crosses the boundary.

Sub-agent to parent: conclusions only. A sub-agent that returns its transcript has defeated the purpose of being a sub-agent. Enforce this with a schema — the return type is a structured result object, not a string — and the temptation disappears.

code
class SubAgentResult(BaseModel):
    finding: str = Field(max_length=1200)     # what the parent needs to know
    confidence: Literal["high", "medium", "low"]
    sources: list[str]                        # ids, so the parent can re-fetch
    unresolved: list[str] = []                # what it could not determine

The unresolved field earns its place: the most common multi-agent failure is a sub-agent quietly not doing part of its job and returning a confident summary of the part it did.

Shared state: explicit, versioned, small. If agents genuinely need to see each other's work, put it in a store with an interface, not in a shared prompt. Versioned, attributable writes; agents read what they query for. This makes the "who changed this" question answerable, which it is not when the shared state is a growing string.

Nothing implicit crosses. Every piece of shared context should be traceable to a specific call. Ambient state in a multi-agent system is where the non-reproducible bugs live.

Failure modes and their signatures#

Thrashing. The agent repeatedly re-fetches material it already had, because the compaction evicted the content and did not keep the identifier. Signature: duplicate tool calls with identical arguments. Instrument this — it is a one-line check and a reliable alarm on a broken memory policy.

Instruction drift. Around step forty, the agent starts optimizing for something adjacent to the task. Signature: outputs that are locally sensible and globally off-target. Fix: pin the task at the tail, and periodically re-inject an explicit "the goal is still X" turn.

Poisoned memory. An early wrong conclusion gets extracted into the facts tier and is thereafter treated as ground truth, with the evidence long since evicted. This is the nastiest one, because compaction has removed the ability to re-litigate it. Mitigation: keep provenance on every extracted fact, and mark facts derived from inference rather than observation as revisable.

Silent truncation. The context exceeds the window, the framework drops the middle, and nothing tells you. Fix: count tokens before every call and fail loudly rather than truncating implicitly.

Instrumentation#

Four numbers, on a dashboard, per run:

  • Token utilization by region. If tool definitions are 30% of your window, that is a finding.
  • Compaction ratio over time. Rising ratios mean the agent is losing more detail per step; a run that ends at 40:1 compression has effectively forgotten its beginning.
  • Re-fetch rate. Duplicate tool calls per run. The thrashing alarm.
  • Steps to completion. Distribution, not mean. A long tail is where context degradation shows up before it shows up in success rate.

Log the full rendered context for a sample of runs — not the message list, the actual string sent to the model. Reading three of those end to end teaches you more about your agent's problems than a week of staring at metrics.

The short version#

Treat the window as a managed resource. Pin what must never be lost. Load tools by phase. Truncate at the tool boundary and return handles rather than payloads. Compact in tiers and always keep identifiers. Cross agent boundaries with typed conclusions, never transcripts. Measure utilization, compaction, and re-fetch.

None of this is exotic. It is memory management, and it is the difference between an agent that works for ten steps and one that works for a hundred.

Let's build

Building something in this space?

If this is the kind of problem your team is working on, we'd like to hear about it — especially the parts that aren't working yet.