feat: tool truncation + auto-compaction to bash harness - #2454
feat: tool truncation + auto-compaction to bash harness#2454mikasenghaas wants to merge 14 commits into
Conversation
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — This PR adds a substantial context-management workflow to the bash harness, including truncation of existing tool outputs, model discovery, summarization retries, and a new authenticated relay route. Its runtime behavior and unresolved overflow-recovery concern warrant human review. You can add or adjust custom eligibility rules. Learn more. |
ca40cbf to
fd382c5
Compare
## Summary Hard-caps any single tool result entering the conversation at **10KB**, middle-out (keep head+tail), with a warning header naming the original size and line count: ``` Warning: truncated output (original token count: 12500) Total output lines: 10001 <first 5KB> [... 40000 bytes truncated ...] <last 5KB> ``` Applies at the engine tool-result boundary — so it covers **all tools** (`bash`, `edit`, and `ipython` cell output) uniformly. ## Scope: context only — tools yes, skills no - The **session log keeps the full output** (audit trail unchanged). - **Skill return values inside the kernel stay uncapped**: `out = await bash(...)` holds the complete string for in-cell filtering; only what the cell *prints* (the tool result) is subject to the cap. Capture-then-filter workflows keep full fidelity — the cap only protects the context window. ## Compatibility The function is byte-compatible with the truncation in [verifiers#2454](PrimeIntellect-ai/verifiers#2454) (bash harness) and the copy bundled inside [#147](#147) (auto-compaction) — extracted standalone here so the cap can ship independently; #147 can relocate/dedupe it when it lands. Motivation from eval traces: unclipped tool results produced single-turn context blowups (observed up to ~690k tokens from one `cat` on a large file), which no compaction threshold can save you from after the fact. Tests: truncation unit test added; 139 passed (6 pre-existing `test_acp.py` env failures, same as clean main); ruff clean.
Optional CompactionConfig on both in-house harnesses: compact into a handoff summary at summarize_at_tokens, or at 90% of the model context window when the provider advertises one. The threshold is discovered by the agent loops themselves (the bash program reads the provider's /models card; nano-rlm's engine already does), so the interception server gains a stateless GET /v1/models relay serving every dialect. On a provider overflow error the loops compact and retry once, learning the threshold from the error message; an oversized checkpoint request drops the newest tool results one at a time until it fits. RLM's summarize_at_tokens moves into the compaction config and crosses ACP flat; nano-rlm pinned at f5c14aa. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Compact when 16k tokens remain below the context window instead of at 90% of it - a fixed reserve keeps constant headroom on any window size (small windows keep at least half). Truncate a tool result over 10KB middle-out before it enters the conversation, with a warning naming the original token count and line count, so one giant output can never leap past the reserve and the model knows what was cut. Matches Codex's output policy; the threshold matches pi's reserve design. Pin nano-rlm 4fd3fa2 with the same changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Each marker now names the API whose error wording it matches, and the unattributable generics are gone - "too many tokens" also matches Bedrock throttling, and bare "context length"/"context window" substrings matched more than they targeted. Pin nano-rlm 3b97900 with the same map. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 413 markers could never fire: a 413 arrives as a plain APIStatusError, not BadRequestError. Catch APIStatusError at the compaction sites and gate overflow detection on a deterministic status (400 or 413) so marker-shaped text in a transient failure never triggers a compaction. Pin nano-rlm 4bb5f48 with the same fix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Final compaction design: proactive at a fixed reserve below a known context window, reactive on attributed 400/413 overflow errors. A rejected checkpoint no longer sheds tool results - it falls back to the last state that passed a threshold check, which by definition holds a full reserve of room; an empty or tool-calling reply is resampled, and after three failed attempts the program ends the run cleanly as a trainable sample instead of crashing. An overflow with no history beyond the task propagates. Tool truncation grows to 20KB and the threshold-learning regexes go away - compaction now requires a known window. Pin nano-rlm b928097 with the same design. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The RLM harness wiring moves to a stacked PR so this one can merge before the nano-rlm companion lands. Also discover the context window via models.list - the raw cast_to parse breaks on one Python version or another (a bare dict cannot be constructed on 3.13, and a parameterized dict trips inspect.isclass on 3.10). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
9824e5b to
978e650
Compare
A reasoning-parsed model (observed: Laguna via the glm45 parser) can put the entire checkpoint reply in reasoning_content, leaving content empty - every attempt then fails and the run ends as compaction-failed despite a perfectly good summary. The checkpoint asked for a summary, so when content is empty accept the reasoning text as the summary. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
vLLM 0.26 names the field "reasoning" and the SDK only keeps it in model_extra, so the attribute lookup never saw it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit f07f60a. Configure here.
| or not is_context_overflow(error) | ||
| or not compactable(messages) | ||
| ): | ||
| raise |
There was a problem hiding this comment.
Overflow recovery requires a threshold
Medium Severity
Reactive compaction on a 400/413 overflow is skipped whenever threshold is None. An empty compaction config that cannot discover a window therefore never recovers from overflow, even though that recovery is described as independent of the proactive threshold.
Reviewed by Cursor Bugbot for commit f07f60a. Configure here.
There was a problem hiding this comment.
By design: compaction requires a known context window (discovered or explicit); without one the overflow propagates. Documented scoping - training always knows the window.
A checkpoint reply that lives entirely in the reasoning channel is resampled like an empty one - reasoning never enters the summary. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review: last_good started at zero, so a first-turn checkpoint rejection retried over an empty base - a summary of nothing with the task gone; the initial conversation is now the floor. And a multimodal MCP result is a content-part list, which the byte truncation crashed on - only plain text is truncated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review: interception hook rewrites entered the conversation unbounded, sidestepping the 20KB tool-output limit. Every message entering as a tool result now passes the same bound, rewrites included. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review: an overflow on the post-compaction work call propagated out of the loop and crashed the rollout. The rebuilt conversation is sized to fit by construction, so if it still overflows there are no moves left - convert it to the compaction-failed clean ending. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review: the post-tool snapshot was taken on a chars/4 estimate, which can undercount dense content severalfold - the "good" snapshot could itself be oversized, making the fallback identical to the overflowing request. A state now becomes the fallback only when the provider accepted that exact prompt with real usage below the threshold, which lands the fallback before the tool results, as designed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>


Summary
CompactionConfigto the bash harness: compact into a handoff summary atsummarize_at_tokens, or when 16k tokens remain below the model context window/modelscard (viamodels.list()), so the interception server gains a statelessGET /v1/modelsrelay (one route serves every dialect)The RLM harness gets the same policy in #2459, stacked on this PR so this one can merge before nano-rlm does. Together with #2453 (merged) and #2459 this supersedes #2448.
Breaking
compactionis a new optional field, unset by default.Verification
uv run pytest -q tests/v1— passed; live E2E tests skipped withoutPRIME_API_KEYTool truncation checked by exact match: a synthetic 55KB output produces the byte-identical expected head/tail + warning header; short outputs pass through untouched.
Terminal-Bench 2 e2e: 8 tasks, local vLLM
poolside/Laguna-XS-2.1at 32k (glm45 reasoning + glm47 tool parsers),compaction = {}so the threshold auto-discovers to32768 − 16384 = 16384. Trace analysis:8/8 rollouts end
ok=truewith scoring run (1 solved); no harness errors, no truncation stops.The threshold triggers exactly where designed: 7/8 episodes compacted with peak contexts of 17.0k-23.4k tokens; the one episode that stayed at 15.4k never compacted.
20KB tool truncation visible in 3 episodes' traces (
Warning: truncated outputwith original size).Laguna puts its entire checkpoint reply in the reasoning channel, so under the summaries-are-content-only rule every compaction on this model exercises the failure path end to end: three resampled checkpoint attempts, then the program ends the run cleanly - the rollout stays a trainable sample. Summary carry-over itself was demonstrated on models that answer in the content channel (Qwen3-0.6B at 4k/8k, deepseek-v4-flash on tb2): every non-final branch ends with the checkpoint prompt followed by the summary, and the next branch opens with
[system, framed summary].A final combined verification run on a content-channel model is pending.
🤖 Generated with Claude Code
Note
Medium Risk
Opt-in compaction rewrites conversation history and adds a new upstream relay route; mistakes could affect long agent rollouts or model discovery, but default behavior is unchanged when compaction is unset.
Overview
Adds optional context compaction to the bash harness agent loop so long rollouts can keep going without hitting the model window.
BashHarnessConfiggainsCompactionConfig(optionalsummarize_at_tokens); enabling compaction forwards--compaction/--summarize-at-tokensinto the uv program.When compaction is on, the program uses a
Compactorto trigger on token usage (default threshold: model context window minus 16k, discovered viamodels.list()), onfinish_reason == length, or on provider 400/413 overflow errors. It replaces history with a handoff summary (checkpoint prompt,tool_choice=none, up to three retries with fallback to the last “good” snapshot), then continues; unrecoverable compaction ends the loop cleanly instead of crashing.Tool results over 20KB are middle-truncated (head/tail + warning) before they enter the transcript, including interception rewrites.
The interception server adds
GET /v1/modelsas a dialect-aware relay to upstream so the bash program can read context-window fields from model cards when no explicit threshold is set.Reviewed by Cursor Bugbot for commit cad252f. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add tool truncation and auto-compaction to bash harness
CompactionConfigtoBashHarnessConfigso callers can enable compaction and set asummarize_at_tokensthreshold; the harness passes--compactionand--summarize-at-tokensCLI flags to the programCompactorclass in the bash program that summarizes conversation history when token thresholds are hit or context overflow is detected, rebuilding to a compacted system+user state; raisesCompactionFailedafter 3 failed checkpoint attempts to terminate cleanlyTOOL_OUTPUT_MAX_BYTES(20000) before inserting into the conversation, preserving head and tail with a warning/v1/modelsendpoint inInterceptionServerchat()now returns the raw completion object instead of a pre-extracted message, and acceptstool_choice; tool outputs exceeding 20000 bytes are truncated before entering the conversationMacroscope summarized cad252f.