Skip to content

fix(agent): wire read_spilled_output into the production tool seam — execute_tool has no production callers #13919

Description

@mrveiss

PR #13918 makes read_spilled_output dispatchable through ToolRegistry.execute_tool and verifies it end to end. That is genuine, but it is only half of #13754, and the half it delivers is not the live path.

The gap

ToolRegistry.execute_tool has no production callers. From the registry, production reaches only get_available_tools() / get_raw_descriptions() (via services/llm_service.py).

The live tool seam is chat_workflow/tool_handler.py::_dispatch_tool_call, which has no read_spilled_output branch. An unknown name there lands in _build_unknown_tool_error and increments consecutive_invalid_tool_calls — so with the spill enabled, an agent following the excerpt's instruction would burn its invalid-call budget.

Why it is filed rather than fixed in #13918

Wiring the production seam is a larger change than a defect-fix PR should carry, and with AUTOBOT_TOOL_OUTPUT_SPILL off nothing reaches it. Filing it explicitly rather than describing #13754 as delivered — that issue was already closed once on a capability that could not be invoked, and the correction should not repeat the same shape at a different layer.

Acceptance criteria

  • _dispatch_tool_call routes read_spilled_output to the registry method
  • a test drives the production seam, not ToolRegistry.execute_tool, and fails if the branch is removed
  • an agent following the excerpt note does not increment consecutive_invalid_tool_calls
  • the run binding is present at that seam — read_spilled_window fails closed without one, so a correctly-wired dispatch with no bound run still returns nothing useful

Blocked by

Nothing. But it is only observable with the spill enabled, so it should land with (or before) any change that turns AUTOBOT_TOOL_OUTPUT_SPILL on.

Related: #13865, #13692, PR #13918.

Activity

  1. github-actions commented on Aug 10, 2026

    @github-actions
    Contributor

    👋 Auto-triage could not confidently classify this issue (no confident signal). Could a maintainer add the appropriate labels (frontend, backend, infrastructure, docs, testing and good-first-issue, intermediate, advanced)?

  2. mrveiss commented on Aug 10, 2026

    @mrveiss
    OwnerAuthor

    Landed on Dev_new_gui via #13918.

    $ git show origin/Dev_new_gui:autobot-backend/tools/tool_registry.py | grep -c read_spilled_output
    4
    

    read_spilled_output is dispatchable from the production seam (registration and the execute path), not merely defined.

    Verification beyond the merge: agent_loop/ 203 passed, and the error-key preservation was mutated rather than re-run — killed by 3 tests including TestAFailureStaysAFailure::test_the_preserved_error_is_a_marker_not_the_payload. A green re-run would not have distinguished a working guard from a deleted one.

  3. mrveiss commented on Aug 11, 2026

    @mrveiss
    OwnerAuthor

    Reopening. The closure evidence addresses the registry, which was never what this issue was about — and it is the same shape as #13754's first false closure, at a different layer.

    What the closure showed

    $ git show origin/Dev_new_gui:autobot-backend/tools/tool_registry.py | grep -c read_spilled_output
    4
    

    That is correct and was already true when this issue was filed. #13754 delivered registry dispatch; this issue exists because ToolRegistry.execute_tool is not the production seam.

    What is still missing

    The live chat dispatcher does not know the tool:

    $ git show origin/Dev_new_gui:autobot-backend/chat_workflow/tool_handler.py | grep -c read_spilled_output
    0
    

    And ToolRegistry.execute_tool still has no production callers. Both apparent hits are false positives:

    $ git grep -l "execute_tool(" origin/Dev_new_gui -- 'autobot-backend/**/*.py' | grep -v test
    api/database_mcp.py            -> `def _create_database_execute_tool()` — a factory NAME, not a call
    services/neural_mesh_retriever.py -> `self._execute_tool(...)` — its own method on a different class
    tools/tool_registry.py         -> the definition itself
    

    So an agent following the spill excerpt's instruction still lands in _build_unknown_tool_error and increments consecutive_invalid_tool_calls — the exact failure #13754 was filed to remove, one layer up.

    Why this matters beyond tidiness

    This issue is the stated gate on ever enabling AUTOBOT_TOOL_OUTPUT_SPILL. Closing it makes the mechanism look ready when the excerpt still names a tool the agent cannot invoke — and the whole #13692 line has now been closed-then-corrected twice on exactly this pattern:

    The verification standard that catches it is unchanged: grep the seam the code actually runs through, not the layer you changed.

    Acceptance criteria (unchanged)

    • _dispatch_tool_call routes read_spilled_output to the registry method
    • a test drives the production seam, not ToolRegistry.execute_tool, and fails if the branch is removed
    • an agent following the excerpt note does not increment consecutive_invalid_tool_calls
    • the run binding is present at that seam — read_spilled_window fails closed without one

    Credit where due: the mutation testing cited in the closure (killing the error-key guard with 3 tests rather than re-running it green) is the right standard. It was applied to the wrong claim.

  4. mrveiss commented on Aug 11, 2026

    @mrveiss
    OwnerAuthor

    PR #13991 is open and this issue stays open when it merges.

    The PR closes the routing gap: a model-emitted read_spilled_output now reaches a handler at _dispatch_tool_call instead of _build_unknown_tool_error, so it no longer burns the invalid-call budget. Real, worth landing.

    It does not make the capability reachable, and review caught me about to claim it did. Verified:

    $ grep -rn 'bind_task\|spill_results' autobot-backend/chat_workflow/
    (0 rows)
    

    The chat seam neither spills nor binds a run, so read_spilled_window returns no_run_bound on every call from a real turn. The root cause is bigger than this issue and is filed as #13997: the whole spill mechanism is wired only into AgentLoop, which its own docstring documents as never instantiated in production (#11221).

    Remaining scope here, unchanged:

    That last box is why this stays open. A test now pins the current answer (no_run_bound) with a failure message pointing here, so the day the seam starts binding a run, the suite says so rather than staying quietly green.

  5. mrveiss commented on Aug 11, 2026

    @mrveiss
    OwnerAuthor

    Noting that I closed this issue in error earlier today, and why — the reopen was correct.

    My closure evidence was:

    $ git show origin/Dev_new_gui:autobot-backend/tools/tool_registry.py | grep -c read_spilled_output
    4
    

    and I wrote that the tool was "dispatchable from the production seam". That counts occurrences of a name in a file. It does not establish that production can reach it — which is precisely what this issue says is missing: ToolRegistry.execute_tool has no production callers, so making the tool dispatchable through execute_tool delivers the half that is not the live path.

    The four occurrences are the method definition, its descriptor, a lambda in the execute_tool table, and an entry in registry_tools — every one of them on the side of the seam the issue is about.

    This is the failure mode I spent the session filing issues about (#14004/#14005/#14006, and #13862's skipped vs never ran): a measurement that is easy to take standing in for the property that matters. A symbol-presence grep is the same shape as a green check that never ran.

    The right evidence would have been a call graph from a production entry point to execute_tool, or the absence of one — which is what #13991 is addressing. Leaving that PR to its author; nothing for me to change here beyond the record.

  6. mrveiss commented on Aug 11, 2026

    @mrveiss
    OwnerAuthor

    Same root cause as #14031 (pre-action verifier + belief state run nowhere): AgentLoop has no production caller — grep -rn "from agent_loop.loop import\|AgentLoop(" returns only test files.

    Full inventory of agent_loop/ capability that never reaches production is #13919 + #13997 + #14031. One extraction pass over the package is cheaper than three separate wirings — the pattern is #13590 (CLOSED): extract the decision logic into autobot_shared/ as a dependency-free pure function, call it from the live seam, and add a wiring assertion so the next refactor cannot silently un-wire it.

    Premise correction posted on umbrella #13587, which listed the verifier as "already covered" citing agent_loop/loop.py.

  7. mrveiss commented on Aug 11, 2026

    @mrveiss
    OwnerAuthor

    Found while reviewing PR #13991: the routing this PR fixes is real, but the window read_spilled_output returns is dropped before the model sees it.

    _format_execution_step (chat_workflow/manager.py:1820-1836) reads only command/stdout/stderr/status; the new handler records its result under output, as web_search and three other tool families already do. So a successfully resolved 8,000-char window renders to the model as Status: success / (no output), and the model concludes the artifact is empty.

    Filed as #14120 with the reproduction — it is a pre-existing gap affecting five producers, not something PR #13991 introduced, which is why it belongs in this issue's remaining scope rather than blocking that PR outright.

  8. mrveiss commented on Aug 12, 2026

    @mrveiss
    OwnerAuthor

    PR #13991 merged as 3708cb3b4 — the routing half. This issue stays open; the capability still does not reach the model.

    $ git show origin/Dev_new_gui:autobot-backend/chat_workflow/tool_handler.py
    164:   _SPILL_MISS_ADVICE                -> present
    184:   _SPILL_MISS_UNKNOWN_ADVICE        -> present
    2856:  window = await asyncio.to_thread( -> present
    

    What landed

    read_spilled_output is routed at the production dispatch seam, so an agent obeying the excerpt's own note no longer lands in _build_unknown_tool_error and burns its invalid-call budget. The wiring is guarded in both directions — deleting the route row makes the dispatch raise, deleting the membership row fails the set-equality test and the budget assertion.

    Review also caught two things the first version introduced, both fixed before merge:

    • the artifact was read and json-parsed on the event loop — up to SPILL_MAX_ARTIFACT_CHARS (5,000,000) per call, re-read once per page, which for one 5 MB artifact at the 8,000-char window cap is ~625 calls and roughly 3 GB of read+parse stalling every other session's stream. Now asyncio.to_thread, matching the decision spill_results_async already took on the write side.
    • a miss with no stated reason was answered "Do not retry this anchor." read_spilled swallows every exception into None, and a cross-run refusal returns None too, so that default covered a truncated artifact caught mid-write, a transient OSError, a PermissionError and a run-scope refusal — in all four the output is still on disk. A reasonless miss is now status: "unknown" with retry-once advice.

    Why this issue stays open

    The window never reaches the model. _format_execution_step (chat_workflow/manager.py:1820-1836) builds the continuation prompt from command/stdout/stderr/status only, and this handler records its result under output — as web_search, web research, browser success and extract_content already do. So a successfully resolved 8,000-char window renders to the model as Status: success / (no output).

    Filed as #14120, with the reproduction, and cross-linked to #12508 which it very likely explains. Five producers hit it, so it is a pre-existing delivery gap rather than something #13991 caused — but it is why no test here could see the real outcome: every assertion targets execution_results[...] or WorkflowMessage.content, and both are dropped or never sent to the model.

    Paging is advertised but invisible. The schema tells the model "Page forward while the reply reports has_more", yet has_more/offset/total_chars live only in WorkflowMessage.metadata, which routes to the UI. Fixing #14120 alone would not fix this — the model would get window 1 of 5 with no signal that 32,000 chars remain. The pagination state has to be in the content.

    No run is ever bound at the chat seam. TestAgainstTheRealReader pins the honest production answer: no_run_bound, always, because bind_task is called only from agent_loop/loop.py and AgentLoop has no production instantiation. That is the same root cause as #14031 and #13997.

    Remaining scope

    This issue was closed falsely twice before on evidence from the layer that changed. Not doing that a third time.

  9. github-actions commented on Aug 15, 2026

    @github-actions
    Contributor

    PR #14237 (merged to Dev_new_gui) references this issue with a close keyword.

    fix(agent): offload oversized tool output at the chat seam, where the model actually sees it (#13997)

    If this issue is fully resolved, close it manually. If work remains, no action is needed.

  10. mrveiss commented on Aug 15, 2026

    @mrveiss
    OwnerAuthor

    Closing. The last unticked box — the run binding is present at that seam — is now satisfied, and this time by evidence from the path production actually runs.

    Acceptance criteria

    $ git show origin/Dev_new_gui:autobot-backend/chat_workflow/manager.py | grep -n "_bind_spill_run(ctx.session_id)"
    2897:        self._bind_spill_run(ctx.session_id)
    

    It sits in _run_continuation_loop_iteration, beside the _current_lightweight_mode ContextVar — the seam the file's own #11612 docstring identifies as the one both the legacy loop and the LangGraph path call through.

    Why this issue took three attempts

    This is worth recording, because the failure repeated in the same shape each time:

    1. feat(agent): expose spilled tool output as an agent-callable read tool #13754 closed on assert hasattr(ToolRegistry, "read_spilled_output") — which passes for any unwired method.
    2. fix(agent): wire read_spilled_output into the production tool seam — execute_tool has no production callers #13919 (this issue) was then closed citing tool_registry.py grep counts — already true when it was filed. ToolRegistry.execute_tool has no production callers; the live seam is _dispatch_tool_call.
    3. PR fix(agent): offload oversized tool output at the chat seam, where the model actually sees it (#13997) #14237's first attempt put the run bind in _run_llm_iterations — a wrapper the LangGraph path bypasses entirely. Green tests, dead feature.

    Every time the evidence came from the layer that had just been changed. What finally worked was pointing each test at something I had not touched: the seam the other path calls, and the factory that builds the real result.

    Attempt 3 also repeated a documented mistake: #11612 had already put the cost badge in that same bypassed wrapper, diagnosed it, fixed it, and left the explanation six lines from where I made the same error.

    What is still open on this mechanism

    The spill is now genuinely reachable, and it stays off (AUTOBOT_TOOL_OUTPUT_SPILL defaults false). Before it is enabled:

    Related: #13692 · #13754 · #13865 · #13933 · #13997 / PR #14237 · #13991

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions