Repository navigation
fix(agent): offload oversized tool output at the chat seam, where the model actually sees it (#13997) - #14237
Conversation
… model actually sees it (#13997)
✅ SSOT Configuration Compliance: Passing🎉 No hardcoded values detected that have SSOT config equivalents! |
Review response — all three fixed. The first two were fatal and you were right to call it a fourth instance.1. The bind was in a wrapper the production path bypassesConfirmed, and the warning was in the function I edited, six lines from my change:
So this exact mistake had already been made once, diagnosed, fixed, and documented in place — and I made it again one layer up. The bind now sits beside
2. The adapter missed shell output entirelyConfirmed. Fixed: The tests now build their fixture with 3. Sync write on the event loopConfirmed, and it is the worse kind of miss — I added 4. The approval-gate path — stated, not silently narrowedYou are right that VerificationThe patternFour times now: |
Code reviewReviewed from a sweep of green-but-unreviewed PRs, not as the author. Two findings; the wiring 1. The reduction metric reads a key shell results do not have — so the one measurement this PR before = sum(len(str(r.get("output", ""))) for r in results)
after = sum(len(str(r.get("output", ""))) for r in rewritten)
logger.info("Offloaded %d oversized tool result(s): %d -> %d chars (#13997)", ...)
{"command", "host", "stdout", "stderr", "return_code", "status", "approved"}No This matters because the PR states the chat-path equivalent of #13692's 95.5% figure "will come 2. A second dispatch path is not covered by "the chat seam".
Verified as correct, not findings: the binding at |
Re-review: clean, no blockersVerdict was no blockers, clean to merge, with all three fixes verified on the property rather than the instance — traced end to end and mutation-tested. The one I most wanted checked came back sound: the run bind is genuinely on the production path. Traced Key list confirmed complete by enumeration rather than assertion: all 38 Two residualsFixed here, because it was mine and wrong: the reduction log summed only Filed as #14284, not fixed here: StateGreen, re-reviewed clean, ready to merge. When it lands, #13919's last unticked box — the run binding is present at that seam — can finally be ticked with evidence from a real dispatch rather than a grep of the layer that changed. |
Applied Black, isort, and autoflake to match code-quality checks. Triggered by workflow auto-fix-formatting.yml.
Thinking Path
#13692 built the tool-output spill and wired it into
AgentLoop— a class whose own docstring says it is not instantiated anywhere in production. So on the live chat path nothing ever spilled, no run was ever bound, and the read tool #13919 wired into the dispatch seam returnedno_run_boundon every call. Three PRs had made the mechanism safe, dispatchable and correct while it remained unreachable.Owner decision on #13997 was to wire the live seam rather than park or remove it.
Before building, I checked whether chat already solved this — a second mechanism would be a fork, not a fix. It does not:
execute_commandweb_searchSo today the chat path either loses the overflow or does not bound it. The spill is non-lossy: the excerpt carries an anchor and the full output stays retrievable.
What Changed
manager.py::_handle_execution_summary— the point where results extendexecution_history, which is what the model sees. One call added before that.The run binding is in
_run_llm_iterations, deliberately._current_task_idis aContextVarand asyncio copies the context at Task creation, so a bind in a parent or sibling Task is invisible to the dispatch. That coroutine and every tool call it drives share one Task. Binding "earlier in the request" would have reproduced the current always-no_run_boundbehaviour with more code — the trap recorded on #13997 before starting.Scoped to
session_id, so an anchor leaked into another conversation cannot resolve there.spill_execution_results— a shape adapter, not a second mechanism. The agent loop passes{name: result}; the chat seam collects a list of{"tool", "status", "output"}envelopes. Only the payload field is replaced:statusdecides error handling downstream (tool_handlercounts== "success", delegation checks== "error"), and swapping the whole dict for an excerpt would drop it;outputstays astr—_as_output_textandtool_handlerboth read it as one;spill_if_oversized, so the measured smaller-than-what-it-replaced guard, the artifact cap and the flag all apply unchanged.Verification
Eleven new tests, using the real spill module against a temp directory — no mocking of the reader, since a mocked reader is precisely what let the unreachability survive two "deliveries".
Mutation-tested:
Mutation B initially passed, and that is worth stating: my first ten tests called
_bind_spill_rundirectly, so deleting the bind from_run_llm_iterationschanged nothing they observed. The helper was covered and the wiring was not — the same shape of gap that let this feature ship unreachable in the first place.TestTheTurnActuallyBindsTheRunnow drives_run_llm_iterationsitself and is the test that fails.Scope
This does not close #13919. Its last box — "the run binding is present at that seam" — is what this PR supplies, so that issue closes on this landing; I am not claiming it in the commit because the two are separate concerns and #13919 has been falsely closed twice already.
AUTOBOT_TOOL_OUTPUT_SPILLstays off. The reduction is now logged per turn, so the chat-path equivalent of #13692's 95.5% figure will come from real traffic rather than a harness against a dormant class.Model Used
Opus 5
Closes #13997