feat: tool truncation + auto-compaction to bash harness - #2454
Conversation
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — This PR adds a stateful context-compaction workflow and a new provider-model relay, while unconditionally truncating large tool results in the shared Bash/Null loop. These changes can alter rollout context and completion behavior across production paths, so human review is appropriate. 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>
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>
# Conflicts: # verifiers/v1/harnesses/minimal/program.py
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.
Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 994a146. Configure here.
An overflow on a conversation that a compaction already reduced to [system, summary] re-raised the provider error, so the rollout failed instead of ending as a trainable sample like the in-cycle retry path. Track that a compaction happened and convert that overflow to CompactionFailed; the first-turn floor keeps raising. Also give the /v1/models relay a finite read timeout so a hung provider cannot stall threshold discovery for the rollout's whole outer timeout. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
## Summary Follow-up to #2454: the building blocks that standalone chat programs share were spread over `verifiers.v1.mcp.client`, `harnesses/standalone.py`, and a monolithic `minimal/program.py`. This PR gathers them in a new `verifiers.v1.harnesses.utils` package and removes `harnesses.minimal` entirely: - **`mcp.py`** — the bundleable MCP client helpers (`mcp_client`, `connect_mcp`, `call_mcp`, `mcp_content_to_chat_content`, `with_retry`), moved from `verifiers/v1/mcp/client.py`. - **`compaction.py`** — tool-output bounding and context compaction (markers, thresholds, `discover_threshold`, `truncate_tool_output`, `Compactor`), extracted from `minimal/program.py`. - **`core.py`** — the chat call, local tools (bash/edit/search), the interception hook, the chat loop (`run_chat_loop`), plus argument parsing, assembly, and the `__main__` entry point (inert on package import; the entry point once the module ends the bundled script), extracted from `minimal/program.py`. - **`launch.py`** — `launch_chat_program`; `bundle_program(program, *modules)`, which generalizes `inline_mcp_client` to splice any utils modules into a PEP 723 program (moved from `standalone.py`); and `CHAT_PROGRAM_SOURCE`, the shared Null/Bash program built as `bundle_program(<PEP 723 metadata>, mcp, compaction, core)` — the program text is only the metadata block. The browser_use program keeps its own program file and bundles `mcp` only. Cross-module references inside bundled code stay `TYPE_CHECKING`-guarded, resolved by the flat bundle at runtime — same pattern the programs already used. Housekeeping picked up along the way: - `verifiers.v1.mcp` keeps the host-side server infrastructure (`Toolset`, `serve*`, `SharedToolServer`) and no longer re-exports the client helpers — re-exporting from the new location would import the whole `harnesses` package during `mcp`'s own import. The one runtime consumer (`tasksets/nemo_gym/toolset.py`) imports from the new home. - Drops a duplicated `CONTEXT_OVERFLOW_MARKERS` tuple that a merge left in `minimal/program.py`. - Narrows the `.gitignore` core-dump pattern from `core.*` to `core.[0-9]*` (it swallowed `harnesses/utils/core.py`). - `run_chat_loop` returning (instead of the loop's old early `return` from `main()`) means the interception `tool_client` is now closed on the null overflow path too. No behavior changes otherwise: the bundled program is the same logic, reordered. ## Verification - `CHAT_PROGRAM_SOURCE` and the browser_use bundle compile. - The bundled chat program runs in a fresh isolated uv env (`uv run --no-project bundled.py --help` executes all spliced module-level code against the declared script deps and reaches the `__main__` entry point). - `verifiers.v1` and `tasksets.nemo_gym.toolset` import cleanly; ruff + unit tests pass. <!-- Macroscope's pull request summary starts here --> <!-- Macroscope will only edit the content between these invisible markers, and the markers themselves will not be visible in the GitHub rendered markdown. --> <!-- If you delete either of the start / end markers from your PR's description, Macroscope will append its summary at the bottom of the description. --> > [!NOTE] > ### Consolidate shared harness code into `harnesses.utils` package > - Moves MCP client, compaction utilities, and launch helpers into the new `verifiers/v1/harnesses/utils` package, deleting the old `harnesses/minimal` package > - Introduces `bundle_program` in [launch.py](https://github.com/PrimeIntellect-ai/verifiers/pull/2482/files#diff-f00f52fdf451c06e9bd80d3a7b1c0fe095bd3797ca76f6fec3a4cea166504016) to replace `inline_mcp_client`; builds a shared `CHAT_PROGRAM_SOURCE` constant used by the Bash, Null, and Browser-use harnesses > - Extracts `run_chat_loop` from `main` in [core.py](https://github.com/PrimeIntellect-ai/verifiers/pull/2482/files#diff-4641f7de541bf21c7f3e30a2d8d49f14c1179c70cf21f3dff5fa19deb9a1e206) and moves all compaction logic into [compaction.py](https://github.com/PrimeIntellect-ai/verifiers/pull/2482/files#diff-59ba1eb5314be61d1ec7c2c2f78ad8f0bbe70a855d9415f0b284f1630c2dc9dc) > - Updates all harness imports to pull from the new `utils` modules instead of `minimal` and `standalone` > - Behavioral Change: `verifiers/v1/mcp.__init__` no longer re-exports client helpers (`call_mcp`, `connect_mcp`, `mcp_client`, `mcp_content_to_chat_content`, `with_retry`); any imports of those names from `verifiers.v1.mcp` will fail at runtime — update imports to `verifiers.v1.harnesses.utils.mcp` > > <!-- Macroscope's review summary starts here --> > > <sup><a href="https://app.macroscope.com">Macroscope</a> summarized fa2e316.</sup> > <!-- Macroscope's review summary ends here --> > <!-- Macroscope's pull request summary ends here --> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## Summary - make context compaction an explicit `ExecutionPolicy.compaction` option - use an explicit threshold or discover one from the model card with 16k tokens of headroom - compact after threshold crossings, context-bound decodes, and recognized provider overflows - retry checkpoints from the latest usage-verified conversation when the full checkpoint overflows - resample empty or tool-calling checkpoints and end cleanly when recovery is exhausted - apply the same policy to root agents and recursive agents while preserving IPython state - truncate tool results over 20KB before they enter the conversation - keep compaction prompts, context detection, and truncation helpers outside the main loop Companion: [verifiers #2454](PrimeIntellect-ai/verifiers#2454). ## Breaking - `ExecutionPolicy.compaction` now controls proactive and reactive compaction. - `ExecutionPolicy.summarize_at_tokens` now defaults to `None` instead of `256_000`. - Set `compaction=true` to enable compaction. Leave `summarize_at_tokens` unset to use model-card discovery. ## Verification - `uv run ruff check .` - `uv run ruff format --check .` - `UV_PROJECT_ENVIRONMENT=/tmp/nano-rlm-merge-tests.xApDnC/.venv uv sync --group dev` - `UV_PROJECT_ENVIRONMENT=/tmp/nano-rlm-merge-tests.xApDnC/.venv uv run pytest tests/` — 157 passed <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes core LLM turn handling and default context behavior (compaction off unless configured), so long runs and training trajectories may diverge from prior releases; mistakes in overflow detection or checkpoint retry could drop history or end runs early. > > **Overview** > **Context compaction is opt-in** instead of defaulting to a 256k token threshold. Enable with `RLM_COMPACTION=1` or by setting `RLM_SUMMARIZE_AT_TOKENS`; when compaction is on but no explicit threshold is set, the engine discovers the model window via `/models` and compacts at **window minus 16k** (small windows keep at least half). Docs drop `RLM_MAX_OUTPUT` / `RLM_MAX_TOOL_OUTPUT_CHARS` in favor of a fixed **20KB** head/tail truncation on tool results before they hit the chat. > > Compaction logic moves into **`rlm.compaction`** (overflow heuristics, checkpoint prompts, truncation). The agent loop gains **`_complete`**: proactive compaction after large tool turns, reactive compact-and-retry on provider **400/413 context errors**, and on **`finish_reason=length`** when usage crosses the threshold. Checkpoints retry up to three times with **`_last_good` fallbacks**, plain-text-only summaries (no tool calls / no reasoning channel), and **`CompactionFailed`** clean stops instead of treating every oversized body as fatal. > > **`ExecutionPolicy.compaction`** and ACP/runtime snapshots expose the flag; semantic edge tracking can **release and reclaim** summary requests on resample. Tests cover overflow recovery, disabled compaction, discovered thresholds, and sub-agents. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit f300615. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## Summary - add the same optional `CompactionConfig` to the RLM harness as the bash harness carries - cross ACP with a flat policy: a `compaction` toggle plus `summarize_at_tokens` - pin merged nano-rlm compaction commit `4ef3438` Builds on merged [#2454](#2454), which adds Bash compaction and the interception `/v1/models` relay. Companion [nano-rlm #147](PrimeIntellect-ai/nano-rlm#147) is merged. ## Breaking - `RLMHarnessConfig.summarize_at_tokens` moves to `RLMHarnessConfig.compaction.summarize_at_tokens` and no longer accepts a `(lo, hi)` range. - Leave `compaction` unset to disable proactive and reactive compaction. ## Verification - `uv run pytest -q tests/v1` — passed; live E2E tests skipped without `PRIME_API_KEY` - `uv run pytest -q tests/v1/test_configs.py` — 12 passed - `uv run ruff check verifiers/v1/harnesses/rlm/harness.py` - `uv run ruff format --check verifiers/v1/harnesses/rlm/harness.py` Terminal-Bench 2 e2e: 8 tasks, local vLLM `poolside/Laguna-XS-2.1` at 32k (glm45 reasoning + glm47 tool parsers), `compaction = {}` so the engine discovers the threshold itself (`32768 − 16384 = 16384`). Trace analysis of the pinned engine: - Threshold discovery and the proactive trigger work through ACP: compaction fired on the 3 episodes whose context crossed ~17k; episodes that stayed below (0.7k-6.7k peaks) never compacted; 20KB tool truncation visible where tool output was large. - The runs surfaced and the pin fixes three integration bugs, each verified against the failing trace: `/models` discovery crashing on Python 3.10 containers (raw `cast_to` parse; now `models.list()`), and two interactions with semantic-edge bookkeeping. A failed checkpoint attempt and a resampled unusable reply each left the compaction's summary-request claim held, which killed the retry with "compaction already has a summary request". - Laguna answers checkpoint prompts entirely in the reasoning channel, so under the summaries-are-content-only rule its compactions exercise the resample-then-end-cleanly path; summary carry-over across branches was demonstrated on content-channel models (Qwen3-0.6B, deepseek-v4-flash). - A final combined verification run on a content-channel model is pending before merge. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Breaking harness configuration and ACP runtime policy shape affect rollout behavior and compaction timing; changes are localized to the RLM harness but alter long-running agent context management. > > **Overview** > **RLM harness compaction** is restructured to match the bash harness: a nested `CompactionConfig` with optional `summarize_at_tokens`, exposed over ACP as `policy.compaction` (on/off) plus the threshold when set. > > **Breaking config change:** `RLMHarnessConfig.summarize_at_tokens` is removed in favor of `compaction`; the `(lo, hi)` per-task random range and `summarize_threshold()` are dropped. **`compaction` unset** means compaction is off; an **empty** `compaction` object enables automatic thresholding (e.g. context window minus 16k when advertised). > > The pinned **nano-rlm** ref updates to **`4ef3438`** for the merged compaction engine. `_runtime_metadata` no longer takes `TaskData` since thresholds are no longer task-index–seeded. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 5f39bb2. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- Macroscope's pull request summary starts here --> <!-- Macroscope will only edit the content between these invisible markers, and the markers themselves will not be visible in the GitHub rendered markdown. --> <!-- If you delete either of the start / end markers from your PR's description, Macroscope will append its summary at the bottom of the description. --> > [!NOTE] > ### Add `CompactionConfig` to `RLMHarness` and remove per-task threshold randomization > - Introduces `CompactionConfig` (derived from `BaseConfig`) with an optional `summarize_at_tokens: PositiveInt` field, replacing the flat `summarize_at_tokens` tuple-range field on `RLMHarnessConfig`. > - Removes `RLMHarness.summarize_threshold`, which previously computed per-task randomized thresholds seeded by task index. `_runtime_metadata` now reads thresholds directly from `self.config.compaction` and emits a boolean `compaction` flag in the session policy. > - Changes the default `version` git ref for `RLMHarnessConfig` to `4ef3438`. > - Behavioral Change: `summarize_at_tokens` no longer accepts `(lo, hi)` tuple ranges; callers must provide a single `PositiveInt` inside `CompactionConfig`. The `data` parameter was removed from `RLMHarness._runtime_metadata`. > > <!-- Macroscope's review summary starts here --> > > <sup><a href="https://app.macroscope.com">Macroscope</a> summarized 5f39bb2.</sup> > <!-- Macroscope's review summary ends here --> > <!-- Macroscope's pull request summary ends here -->
## Summary - advance the `deps/verifiers` submodule from `e2103d6` to `d4a2177` - include merged Bash and RLM context compaction from verifiers #2454 and #2459 - pick up nano-rlm compaction commit `4ef3438` through the default RLM harness pin - refresh `uv.lock` for verifiers' `aiohttp>=3.14.1` requirement - migrate three RLM examples to `compaction.summarize_at_tokens`; use the former range's `98_304` midpoint - include the optional ACP semantic-edge and shared harness utility changes already on verifiers `main` Companions: [verifiers #2454](PrimeIntellect-ai/verifiers#2454), [verifiers #2459](PrimeIntellect-ai/verifiers#2459), and [nano-rlm #147](PrimeIntellect-ai/nano-rlm#147). ## Breaking - RLM harness configs must move `summarize_at_tokens` to `compaction.summarize_at_tokens`. - Leave `compaction` unset to disable proactive and reactive compaction. ## Verification - `git diff --check` - `uv lock` - `uv lock --check` - `uv run pytest -q tests/unit/test_configs.py` — 133 passed - `git submodule status deps/verifiers` — `d4a217794fc0bfd70369a8230e56653c193da8ce` - verified that `d4a2177` descends from the previous `e2103d6` pin <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Submodule bump plus a breaking harness config shape affects all RLM runs still using the old `summarize_at_tokens` field; example migrations are mechanical but custom configs must be updated. > > **Overview** > Advances the **`deps/verifiers`** submodule to pick up merged Bash/RLM **context compaction** (and related harness defaults), with **`uv.lock`** refreshed so **`aiohttp`** meets the new **`>=3.14.1`** floor. > > **Breaking for RLM harness TOML:** flat **`env.agent.harness.summarize_at_tokens`** is replaced by **`env.agent.harness.compaction.summarize_at_tokens`**. The three advanced examples (**`glm-4.5-air/search`**, **`glm-4.5-air/terminal`**, **`nemotron-3-super/swe`**) are updated accordingly—train sources that used a two-threshold list **`[65536, 131072]`** now use a single **`98304`** threshold; eval sources keep **`98304`** but under the nested **`compaction`** key. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 7429aba. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->

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
The shared agent loop now permanently truncates large tool outputs and, when compaction is enabled, replaces long histories with model-generated summaries—both can change task outcomes; the new models relay is ancillary and failures only disable auto-thresholds.
Overview
Adds an opt-in context compaction path for the bash agent:
BashHarnessConfiggainsCompactionConfig(summarize_at_tokensoptional), which forwards--compaction/--summarize-at-tokensinto the shared minimal/bash chat program.When compaction is on, the program uses a new
Compactorto compact before context blows up: it can trigger on usage crossing a threshold (explicit token limit, or auto from the model card with a 16k reserve), onfinish_reason=length, on post-tool estimated token growth, or reactively on provider 400/413 overflow. Compaction asks the model for a plain-text handoff summary (tool_choice=none), rebuilds history as system + framed summary user message, falls back to the last “good” checkpoint on overflow, and ends the loop cleanly onCompactionFailedinstead of crashing. Threshold discovery callsmodels.list()against the intercepted base URL.Tool results are always middle-truncated at ~20KB (head/tail + warning) via
bound_tool_messagebefore they enter the transcript, including interception rewrites.The interception server adds
GET /v1/modelsas an authenticated upstream relay (30s timeout) so the in-container SDK can read provider context-window fields without recording a model turn.Reviewed by Cursor Bugbot for commit e1c80ac. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add tool truncation and auto-compaction to
BashHarnessagent loopCompactionConfigtoBashHarnessConfigand passes--compactionand--summarize-at-tokensflags to the spawned program in harness.pyCompactorclass in program.py that detects context exhaustion, builds a summary checkpoint, and rebuilds the conversation; auto-discovers a token threshold from the provider's/v1/modelsendpoint when none is givenTOOL_OUTPUT_MAX_BYTES(20,000) with head/tail preservation viatruncate_tool_outputbefore they enter the conversationGET /v1/modelsthrough the interception server in server.py so the program can query the provider for context window sizechat()now returns the full completion object instead of the first message, and the main loop ends cleanly onCompactionFailedchat()signature and return type changed in program.py; any callers expecting the first message instead of the full completion will break. Tool outputs over 20,000 bytes are silently truncated mid-conversation.Macroscope summarized 27e1c91.