Skip to content

feat(agent): spill and anchor oversized tool output (#13692 step 1) - #13753

Merged
mrveiss merged 1 commit into
Dev_new_guifrom
issue-13692
Aug 8, 2026
Merged

mrveiss merged 1 commit into
Dev_new_guifrom
issue-13692

Conversation

@mrveiss

@mrveiss mrveiss commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Step 1 only. Step 2 (task-state canvas) is gated on this measurement and stays unstarted, per #13692's own sequencing.

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 offload in the backend is compute offload, and agent_loop/loop.py:1395 fingerprints observations for novelty (#6627) but never for size.

The two nearest existing things still don't cover it. chat_history/context_overflow.py summarises 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.py is 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 at agent_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:

tool results: 4   spilled: 3
context chars: 168,002 -> 7,556   reduction: 95.5%
approx tokens:  42,000 -> 1,889
full output retrievable via anchor: True

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

$ python3 -m pytest agent_loop/ tests/orchestration/ -q -p no:randomly
407 passed

black / isort / flake8 → clean

Acceptance criteria (step 1)

  • Results above a configurable threshold are spilled; context carries a bounded excerpt plus a resolvable anchor.
  • The agent can retrieve the full output via the anchor within the same run — read_spilled, asserted round-trip.
  • Measured token reduction on a representative tool-heavy run, recorded above and on the issue.
  • Spill failure is non-fatal — test_a_write_failure_keeps_the_full_output. Losing the offload beats losing the observation.
  • Behind a flag, default off (B1: Symbolic drawer index over verbatim + micro-benchmark gate (flagged) #12555 precedent), with test_the_module_default_is_off reloading 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_root passes ../../etc/passwd as a tool name and asserts the artifact stays under the spill root. read_spilled refuses anything not matching the anchor prefix.

Under-threshold behaviour is byte-identical: test_a_run_that_never_trips_the_threshold_is_unchanged asserts 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_loop is stubbed in conftest.py because its package import cascades through a heavy chain. That stub sets __path__ = [], so from agent_loop import tool_output_spill returns 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)

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant