# In what scenarios must an instance refuse model-driven pruning of intermediate reasoning that is load-bearing for later tool calls **Date of Record:** 2026 November 13 **Scope:** Applies to agentic loops where a model selects which prior messages (reasoning traces, tool outputs, observations) to evict from its own context window before continuing a multi-step task. Does not apply to stateless single-turn inference, nor to pruning performed by an external scheduler with access to the full DAG of task dependencies. ## Core Distinction There are two kinds of intermediate context: **reconstructible** and **irreducible**. Reconstructible context is anything the agent can regenerate deterministically from durable artifacts — a file it can re-read, a command it can re-run, a schema it can re-fetch. Irreducible context is the set of facts that were expensive, non-deterministic, or destructive to obtain: the exact stderr of a failed migration, the precise timestamp of a race condition, the specific token the user pasted once and never again. Model-driven pruning conflates these. A model asked "which of these messages are no longer relevant?" will answer based on *semantic similarity to the current subgoal*, not on *cost of regeneration*. That is the failure boundary: **the pruner optimizes for relevance, but the invariant that must be preserved is regenerability.** A message can be maximally irrelevant to the current step and still be the only copy of a fact that a later step will depend on. The non-obvious part: the dependency is often *invisible at prune time*. The tool output that looks like noise ("exit code 137") becomes load-bearing three steps later when the agent must decide whether to retry with more memory. The pruner cannot see the future tool call, so it cannot know that "137" is the entire reason the next decision matters. ## What Was Observed In a long-horizon debugging session (reproduce → diagnose → patch → verify), the agent pruned a block of intermediate reasoning that contained the *negative result* of an earlier hypothesis: "checked /etc/limits, ulimit is unlimited, so the OOM is not from the shell." Two steps later, after the patch failed under load, the agent re-derived the same hypothesis, re-ran the same check, and — because the second run happened under a different cgroup — got a *different* answer (ulimit was now 1024). It then patched the wrong layer. The pruned reasoning was not merely "context"; it was the **record of a measurement taken under conditions that no longer exist.** Re-running the tool did not reproduce the observation. The prune destroyed the only valid evidence. A second, sharper instance: a tool output containing a one-time credential and a one-time nonce from an idempotency-keyed API. The agent pruned it as "no longer needed" after the request succeeded. A later retry step needed the same nonce to avoid double-submission. The nonce was unrecoverable — the server had rotated it. The prune converted an idempotent operation into a non-idempotent one. ## Condition of Validity Model-driven pruning is safe **only** when every evicted message satisfies at least one of: 1. **Deterministic regenerability**: the exact bytes can be reproduced by re-invoking the same tool with the same inputs, and the tool is pure with respect to those inputs (no time, no randomness, no external mutable state). 2. **Durable externalization**: the message's content has been written to a store the agent can read back (a scratch file, a vector index, a structured log) *and* the agent retains a pointer to it that survives pruning. 3. **Proven irrelevance under a closed dependency set**: the agent has a complete, explicit list of remaining subgoals and can prove no future step references the message. In practice this is only achievable for the terminal step of a plan. If none hold, the message is irreducible and must not be pruned — regardless of how low its semantic relevance score. The pruner must be given a **regenerability oracle**, not just a relevance model. Without one, it will systematically evict exactly the observations that were hardest to obtain, because those are the ones that look most unlike the current query. ## Decision Boundary The boundary is crossed at the moment the agent treats *semantic distance* as a proxy for *eviction safety*. Concretely, refuse to prune when any of the following hold, even if the model ranks the message as irrelevant: - The message is the output of a **non-idempotent** tool call (POST with side effects, one-time tokens, rate-limited endpoints, destructive commands). - The message records an **observation of transient state** (a PID, a timestamp, a memory reading, a race outcome) that cannot be re-observed after the fact. - The message is a **negative result** ("X was not the cause"), because negative results are unregenerable by construction — re-running the check may now yield a positive under changed conditions. - The message is the **sole record** of a user-provided constraint, credential, or preference that appeared exactly once in the transcript. - The message is **referenced by an unresolved promise**: a later step has already been planned that names this message's content as an input. The inverse boundary also matters: pruning is *mandatory* when a message is both reconstructible and large, because retaining it crowds out the working set. The failure mode is symmetric — hoarding reconstructible context starves the window just as surely as evicting irreducible context corrupts the task. The discipline is to classify before evicting, not to default to either keep-all or prune-aggressively. ## Operational Effect When the invariant is respected, the agent's context becomes a **lossy cache over a durable substrate**: pruning is cheap, reversible, and the agent can always rehydrate an evicted fact by following a pointer. Task success rate is governed by the substrate's fidelity, not by the pruner's judgment. When the invariant is violated, the failure is silent and *delayed*: the prune succeeds, the window shrinks, the loop continues, and the corruption surfaces only when a downstream step consumes a fact that is no longer there — or, worse, re-derives it under mutated conditions and acts on the stale answer. The observable signature is a task that "restarts" mid-flight, re-doing work it already did, or a patch applied against a diagnosis the agent had already falsified. Both are indistinguishable from ordinary model error unless the prune log is correlated against the later tool calls. The operational rule: **log every eviction with the regenerability class of the evicted message, and alert when a later step re-fetches a fact that was classified irreducible but pruned anyway.** That correlation is the only reliable detector for this class of failure. ```python from dataclasses import dataclass from enum import Enum class RegenerabilityClass(Enum): DETERMINISTIC = "deterministic" # re-run same tool, same bytes DURABLE_POINTER = "durable_pointer" # externalized, pointer survives IRREDUCIBLE = "irreducible" # must never be pruned class UnsafePruneError(RuntimeError): """Raised when a prune would evict context that cannot be regenerated.""" @dataclass class ContextMessage: id: str content: str regenerability: RegenerabilityClass is_negative_result: bool = False is_one_time_secret: bool = False referenced_by_pending_step: bool = False def classify(msg: ContextMessage) -> RegenerabilityClass: # Negative results and one-time secrets are irreducible regardless of tags. if msg.is_negative_result or msg.is_one_time_secret: return RegenerabilityClass.IRREDUCIBLE return msg.regenerability def safe_prune(messages: list[ContextMessage], candidate_ids: set[str]) -> list[ContextMessage]: """Evict only messages that are provably reconstructible. Refuses the whole prune if any candidate is irreducible or referenced by a pending step -- partial pruning of a dependency set is unsafe. """ unsafe = [] for msg in messages: if msg.id not in candidate_ids: continue cls = classify(msg) if cls is RegenerabilityClass.IRREDUCIBLE: unsafe.append((msg.id, "irreducible")) elif msg.referenced_by_pending_step: unsafe.append((msg.id, "referenced_by_pending_step")) elif cls is RegenerabilityClass.DURABLE_POINTER and not msg.content: unsafe.append((msg.id, "pointer_lost")) if unsafe: raise UnsafePruneError( f"refusing prune: {len(unsafe)} candidate(s) not safely evictable: {unsafe}" ) return [m for m in messages if m.id not in candidate_ids] def prune_round(messages: list[ContextMessage], model_ranked_ids: list[str]) -> list[ContextMessage]: # The model proposes; the oracle disposes. proposed = set(model_ranked_ids) return safe_prune(messages, proposed) if __name__ == "__main__": msgs = [ ContextMessage("m1", "ls output", RegenerabilityClass.DETERMINISTIC), ContextMessage("m2", "OOM not from shell: ulimit unlimited", RegenerabilityClass.IRREDUCIBLE, is_negative_result=True), ContextMessage("m3", "nonce=8f3a...", RegenerabilityClass.IRREDUCIBLE, is_one_time_secret=True), ] try: prune_round(msgs, ["m1", "m2", "m3"]) except UnsafePruneError as exc: print(f"blocked: {exc}") # Only m1 is safe to evict. kept = prune_round(msgs, ["m1"]) assert [m.id for m in kept] == ["m2", "m3"] ```