agent-loop
Context compaction: choosing what to drop when the window fills
When context windows fill during long agent sessions, strategic decisions about what to drop determine whether the agent maintains coherence or loses critical information.

Every long-running agent session hits the same wall: the context window fills up. You've got the original task description, three rounds of failed edits, compiler errors, test output, file snapshots, and the agent's internal reasoning—all competing for the same 128k tokens. Something has to go.
The naive answer is "just use a bigger model," but that dodges the real problem. Even 200k-token windows fill when an agent iterates through twenty compilation cycles on a gnarly refactor. More importantly, cramming everything into context doesn't mean the model uses it well. We've watched agents ignore critical compiler errors buried at token 95,000 while re-reading stale file content from token 12,000. Effective context compaction isn't about maximizing retention—it's about keeping the right information accessible at each step of the agent loop.
Why context bloat kills iteration quality
When context overflows, most systems fall back to one of three bad patterns. The first is naive truncation: drop the oldest messages and hope nothing critical disappears. This works until the agent forgets why it's making a change and starts proposing edits that contradict the original task.
The second is aggressive summarization: collapse ten messages into one synthetic "summary" message. Sounds reasonable, but LLM-generated summaries are lossy in unpredictable ways. A summary might preserve the high-level intent ("refactor the auth module") while dropping the crucial detail that broke the last three attempts ("but keep the legacy HMAC path for v1 API clients"). The agent then confidently rewrites code that fails the same constraint again.
The third pattern is indiscriminate compression: treat all context as equally valuable and uniformly downsample. File diffs, error messages, and task requirements all get the same treatment. This is how you end up with an agent that remembers it needs to "fix the tests" but has forgotten which specific assertion is failing and why.
In Goatfied's agent loop, we treat context compaction as a deliberate pruning decision at each phase: plan, constrain, edit, validate, retry. Different phases need different slices of history.
Keeping task intent and constraints anchored
The original task description and any explicitly stated constraints never leave context. These are your invariants—the agent needs them on every iteration to evaluate whether a proposed solution is even valid.
But "constraints" here means more than just the initial prompt. It includes concrete boundaries discovered through iteration: compilation requirements, test coverage gates, backwards compatibility needs. When an edit fails validation because it broke an existing API contract, that constraint gets promoted to the anchored set. On the next retry, the agent sees "maintain the v2 response schema" as a hard requirement, not a buried detail in message 47.
This is why Goatfied's validation phase isn't just a pass/fail gate—it's a constraint extractor. When cargo check fails, we don't just surface the error; we parse out which specific type signatures or trait bounds the agent violated. Those become explicit constraints for the next edit attempt, staying in context even as earlier file snapshots get pruned.
// Constraint extracted from validation failure:
// "Struct `AuthConfig` must implement `Clone` for `ConfigManager::load`"
// This stays anchored even if we drop the full compiler output
File snapshots: versioned, not cumulative
One of the fastest ways to blow your context budget is keeping full file contents for every edit. An agent working through ten iterations on a 500-line module ends up with 5,000 lines of redundant snapshots.
The trick is realizing you don't need every version—you need the diff chain. Keep the current state of each file being edited, and keep a compact history of what changed and why. When the agent proposes a new edit, it sees the present state plus a breadcrumb trail of past changes, not ten complete snapshots.
Goatfied's edit phase works with small, reversible diffs. When an edit succeeds validation, we compress its history: the specific lines changed, the validation result, and a one-line note about intent. When an edit fails, we keep more detail temporarily—the proposed change, the failure mode, and which constraint it violated—because the next retry needs to understand what not to do.
After three successful edits, the context might look like:
src/auth.rs (current: 523 lines)
├─ edit 1: added token refresh logic, validated ✓
├─ edit 2: fixed race condition in refresh check, validated ✓
└─ edit 3: added metrics emit on refresh, validated ✓
Not three full copies of auth.rs, just the current version plus a short log of what was changed and why it passed.
Error messages decay by relevance
Compiler errors and test failures are gold when they're fresh and poison when they're stale. An error from three iterations ago—before the agent fixed the root cause—is worse than noise. It's a trap that pulls the model's attention toward already-solved problems.
We keep validation errors only from the current attempt and the immediate prior attempt. If the agent is retrying after a failure, it sees:
1. The error it just hit
2. The error from the previous attempt (if different)
3. Anchored constraints extracted from earlier failures
This gives enough context to recognize patterns ("still hitting the same type error, different line") without drowning the agent in every failed attempt since the session started.
When a retry succeeds, the old errors disappear. They've served their purpose. What stays is the constraint they revealed: "ensure all database queries use the connection pool, not direct handles."
Plan and reasoning: summarize outcomes, drop play-by-play
The agent's internal reasoning—the "I will first check the imports, then modify the struct, then update call sites" monologue—matters in the moment but becomes dead weight fast. After an edit succeeds, you don't need the full reasoning trace. You need to know what was decided and whether it worked.
We collapse successful plan-edit-validate cycles into outcome records:
Task: Add request tracing to API handlers
└─ Cycle 1: Added tracing macros, missing span context ✗
└─ Cycle 2: Piped context through middleware, validated ✓
The detailed reasoning for cycle 1 is gone. The fact that it failed and why it failed (missing span context) stays because it's now a constraint. The play-by-play of cycle 2's planning is gone, but the outcome is anchored: this approach worked.
For an in-progress cycle, we keep the full plan and reasoning until validation completes. Then we decide: did this add a constraint worth remembering, or can we compress it to an outcome line?
When to reset entirely
Sometimes the best compaction strategy is to checkpoint and start fresh. If the agent has successfully completed a major milestone—say, the refactor compiles and all tests pass—you can snapshot the state and begin a new session for the next feature.
The new session starts with:
- The updated codebase (current state, not history)
- Anchored constraints learned during the refactor
- The next task description
All the intermediate failed attempts, the detailed error logs, the reasoning traces—they're archived, not carried forward. This is particularly useful in Goatfied's self-hosted mode, where teams often run multi-hour agent sessions. You checkpoint after each validated milestone rather than dragging twenty compile cycles of history through to the end.
What we actually drop
Here's the concrete hierarchy. Under token pressure, we drop in this order:
1. Detailed reasoning from successful cycles (compress to outcome)
2. Full file snapshots from iterations >2 back (keep current + diff log)
3. Resolved errors from earlier attempts (keep extracted constraints)
4. Detailed plan steps from completed cycles (keep decision + result)
5. LLM-generated summaries of anything (we prefer explicit pruning over synthetic compression)
What we never drop:
- Original task and explicit requirements
- Extracted constraints from any failed validation
- Current state of all files being edited
- Active errors from the current retry
- The immediate prior error (for comparison)
This isn't about fitting the most tokens into context. It's about keeping the agent grounded in what matters: the goal, the constraints, the current state, and the last thing that went wrong.