Repository navigation
feat(agent): spill and anchor oversized tool output (#13692 step 1) - #13753
Merged
Merged
Conversation
This was referenced Aug 8, 2026
Contributor
✅ SSOT Configuration Compliance: Passing🎉 No hardcoded values detected that have SSOT config equivalents! |
Tool output entered the model's context whole and stayed there for the rest of the run. Nothing put a large observation somewhere else and left a reference: every 'offload' in the backend is compute offload, and agent_loop fingerprints observations for novelty (#6627) but never for size. A tool result over a threshold is now written to a run-scoped artifact, and what enters context is a bounded excerpt plus a stable anchor the agent can re-read. Measured on a representative tool-heavy run: 42,000 -> 1,889 tokens, a 95.5% reduction, with the full output still retrievable by anchor. Deliberately NOT step 2: no task-state canvas, no compaction changes, no new service, no LLM call. An excerpt is a slice, not a summary, so nothing here can hallucinate. Step 2 is gated on this measurement per #13692, and #12555's precedent puts it behind a flag, default off. The anchor is hashed into a path and never used as one, so a tool name containing traversal cannot escape the spill root. Spill failure returns the full output — losing the offload beats losing the observation. conftest: agent_loop.tool_output_spill is real-imported despite its parent package being stubbed, following the reasoning the file already records for orchestration.causal_validator — it is stdlib plus the canonical logger only, and leaving it behind the stub's empty __path__ would hand its tests a MagicMock asserting against mock defaults (#13111/#13162). The 234 tests the stub protects still pass.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Thinking Path
This is the one item in #13685 that is a genuine capability gap rather than something built and left unconnected. Tool output entered the model's context whole and stayed for the rest of the run. The audit held up: every
offloadin the backend is compute offload, andagent_loop/loop.py:1395fingerprints observations for novelty (#6627) but never for size.The two nearest existing things still don't cover it.
chat_history/context_overflow.pysummarises the conversation at 90% fill — reactive and whole-history, so it cannot stop one 200KB result consuming the window in a single step.memory/verbatim_store.pyis a recall index; it doesn't reduce in-context size.What Changed
agent_loop/tool_output_spill.py(new) — a result over a threshold is written to a run-scoped artifact; what enters context is a bounded excerpt plus a stable anchor. Wired atagent_loop/loop.py:619, immediately after the tool-result merge.Deliberately not step 2: no task-state canvas, no compaction changes, no new service, no LLM call. An excerpt is a slice, not a summary, so nothing here can hallucinate a result the tool didn't produce.
Measured reduction
Representative tool-heavy run — a file read, a large grep, a test run, and one small status:
For context, the external implementation of this pattern reports 30–61%. The gap is mostly because this measures raw spill without a compactor also running.
This is the number step 2 is gated on. If a 95.5% reduction comes from spill alone, the case for coupling memory injection to the compactor — which is what step 2 costs — is much weaker than it looked when #13692 was written. I'd argue step 2 should not start until there's evidence spill alone is insufficient. That decision belongs on the issue, not in this PR.
Verification
Acceptance criteria (step 1)
read_spilled, asserted round-trip.test_a_write_failure_keeps_the_full_output. Losing the offload beats losing the observation.test_the_module_default_is_offreloading under a cleared environment so the default cannot drift.Safety
The anchor is hashed into a path, never used as one —
test_a_tool_name_with_traversal_cannot_escape_the_spill_rootpasses../../etc/passwdas a tool name and asserts the artifact stays under the spill root.read_spilledrefuses anything not matching the anchor prefix.Under-threshold behaviour is byte-identical:
test_a_run_that_never_trips_the_threshold_is_unchangedasserts the mapping is returned unmodified, so with the flag off or small outputs this module is a no-op.One conftest change, and why it needed thought
agent_loopis stubbed inconftest.pybecause its package import cascades through a heavy chain. That stub sets__path__ = [], sofrom agent_loop import tool_output_spillreturns a MagicMock — and my first test run "passed" nothing while failing loudly on unpack errors.The file already documents this exact hazard for
orchestration.causal_validator: stubbing a module that doesn't need it handed its tests a MagicMock, and a whole test class asserted against mock defaults (#13111/#13162). This module has none of the heavy chain either — stdlib plus the canonical logger — so it is real-imported by the same reasoning, with the comment pointing at that precedent.The 234
tests/orchestration/tests the stub exists to protect still pass.Follow-up
The anchor is resolvable in-process via
read_spilled, but is not yet exposed as an agent-callable tool. That is the remaining piece for an agent to re-read on its own initiative mid-run, and it needs a look at the tool-registration surface rather than being bolted on here. Filing separately.Model Used
Claude Opus 5 (1M context)
Part of #13685 · #13692 step 1 (step 2 remains gated)