feat(context): spill oversized tool output to disk with a retrieval tool#882
feat(context): spill oversized tool output to disk with a retrieval tool#882devin-ai-integration[bot] wants to merge 10 commits into
Conversation
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
Greptile SummaryThis revision adds durable, bounded retrieval for oversized tool results and addresses the previously reported pagination defects.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains; the current code fixes the previously reported retrieval re-truncation, full-file pagination, skipped content, oversized pages, UTF-8 corruption, and stalled-offset behaviors. Important Files Changed
Reviews (12): Last reviewed commit: "fix(context): honor the caller's retriev..." | Re-trigger Greptile |
f6e2827 to
5433bfc
Compare
ed423a8 to
72041ab
Compare
5433bfc to
bae1f9a
Compare
72041ab to
029d8b2
Compare
bae1f9a to
a3e2e3f
Compare
83659b4 to
97919a4
Compare
97919a4 to
d2797af
Compare
771284e to
16cbd51
Compare
2c83e88 to
189e375
Compare
189e375 to
028a854
Compare
Truncating a large tool result to a head+tail preview loses the middle, which may hold the one line that matters (a buried match, a stack frame, a credential). Instead of dropping it, persist the full output and let the agent page back to it on demand. - output_store.py: bound_and_store() writes the complete output to a per-scan store and embeds an output_id in the truncation notice; read_stored_output() serves it back in validated, paginated chunks (output_id is a 32-char hex token, guarding against path traversal). - read_tool_output tool: lets the agent retrieve any elided output by id, paging via offset/limit. - factory.py: bounded tool results now spill via bound_and_store; the new tool is registered for every scan agent. - runner.py: point the store at the run's .state/tool-output directory so spilled output lives beside the rest of the scan state. Falls back to a plain head+tail preview if the spill write fails.
Exempt read_tool_output from result-bounding so paging a large stored output isn't re-spilled under a new id, byte-cap each page in place, and stream the requested window with islice instead of loading the whole file per page.
Bounding a retrieval page with a destructive head+tail preview and then advancing the offset past those lines could permanently lose the elided output the store exists to preserve. Instead honour the page byte budget by returning fewer whole lines and advancing the offset only by lines actually returned, so paging forward reconstructs the full output.
bound_and_store's notice carries the output_id and is longer than the plain truncation notice, so reserve for it explicitly and cover that the spilled preview stays within max_bytes.
…rflows The line-based pager always kept at least one whole line, so a stored line larger than the page byte ceiling was returned in full and could overflow history (retrieval bypasses the result-bounding wrapper). Page by byte offset instead: every page is bounded by the byte budget even inside one oversized line, a partial UTF-8 char at the window edge is dropped and re-read on the next page, and paging forward reconstructs the output byte-for-byte.
read_tool_output accepts arbitrary byte offsets; one landing inside a multi-byte character previously decoded to a replacement character. Trim the orphaned leading continuation bytes (their lead byte is on an earlier page) so every accepted offset returns valid UTF-8, without affecting forward paging offsets.
028a854 to
e5a46b9
Compare
…ge ceiling A full non-final retrieval page consumed the entire byte budget and then appended the pagination hint, so the returned result (which bypasses the result-bounding wrapper) exceeded the ceiling. Reserve the hint's bytes out of the page budget so content plus hint always fits.
An arbitrary offset landing inside a multi-byte character is now advanced to the next character boundary before reading, instead of trimming leading continuation bytes after the read. This keeps every page on a real boundary (no replacement characters) and guarantees forward progress, so an offset inside the final character can't yield an empty page with an unchanged continuation offset.
…imit The continuation hint was reserved only against the global page cap, so a small caller-supplied limit bounded content alone and the appended hint pushed the complete response past the requested maximum. Reserve the hint out of the effective ceiling (min of limit and the global cap) so content plus hint always honours the caller's limit, flooring content so pages still make progress.
A final page carries no continuation hint, so it uses the whole budget and honors any limit exactly. A non-final page's content plus hint must fit the limit; a limit too small to hold a progressing page and its hint is now rejected with a clear message rather than silently exceeding the documented maximum.
5hy7xz92nd-oss
left a comment
There was a problem hiding this comment.
Deep research REALITY INGESTION LAYER GitHub Manus Base44 Supabase Notion APIs Financial Systems CRM Banking Data | v ENTITY GRAPH "What exists?" Company Person Product Customer Repository Document Financial Record AI Agent | v BEHAVIOR GRAPH "What is happening?" Events Actions Patterns Velocity Trends Changes Interactions Outcomes | v EVIDENCE GRAPH "Why believe it?" Claims Sources Documents Transactions Verification Provenance | v CONFIDENCE ENGINE "How certain are we?" Source Reliability Data Freshness Conflict Detection Historical Accuracy Confidence Score | v AI REASONING ENGINE "What does it mean?" Context Analysis Risk Detection Scenario Modeling Forecasting Opportunity Discovery | v DECISION ENGINE Funding Investment Credit M&A Risk Operations Automation | v HUMAN GOVERNANCE Review Approval Override Compliance | v OUTCOME LEARNING Repayment Growth Failure Performance Decision Accuracy | ↺ UPDATED INTELLIGENCE MODEL@Manus @datacamp @call-e REALITY INGESTION LAYER GitHub Manus Base44 Supabase Notion APIs Financial Systems CRM Banking Data | v ENTITY GRAPH "What exists?" Company Person Product Customer Repository Document Financial Record AI Agent | v BEHAVIOR GRAPH "What is happening?" Events Actions Patterns Velocity Trends Changes Interactions Outcomes | v EVIDENCE GRAPH "Why believe it?" Claims Sources Documents Transactions Verification Provenance | v CONFIDENCE ENGINE "How certain are we?" Source Reliability Data Freshness Conflict Detection Historical Accuracy Confidence Score | v AI REASONING ENGINE "What does it mean?" Context Analysis Risk Detection Scenario Modeling Forecasting Opportunity Discovery | v DECISION ENGINE Funding Investment Credit M&A Risk Operations Automation | v HUMAN GOVERNANCE Review Approval Override Compliance | v OUTCOME LEARNING Repayment Growth Failure Performance Decision Accuracy | ↺ UPDATED INTELLIGENCE MODEL@GitHub @Malwarebytes @heygen
Summary
Completes the per-tool-output story from #880. Today an oversized tool result is truncated to a head+tail preview and the middle is discarded — which can drop the one line that matters (a buried grep hit, a stack frame, a leaked credential). This persists the full output and gives the agent a way to page back to it, so history stays bounded but nothing is lost.
strix/tools/output_store.pyoutput_idis a 32-char hex token, validated with^[0-9a-f]{32}$before touching the filesystem, so a crafted id can't escape the store dir.read_stored_outputserves validated, paginated chunks and appends a "call again withoffset=…" hint when more remains.read_tool_outputtool (strix/tools/tool_output/)A new
@function_toolregistered for every scan agent. The agent callsread_tool_output(output_id, offset, limit)to retrieve any elided output by id and page through it.Wiring
strix/agents/factory.py:_bound_resultnow spills viabound_and_store(wasbound_text);read_tool_outputadded to_BASE_TOOLS. Error strings still use the non-spillingbound_text(they're short).strix/core/runner.py:configure_output_store(state_dir / "tool-output")so spilled files live beside the rest of the run's.stateand are cleaned up with it. Defaults to~/.strix/tool-outputif unconfigured.The store dir is held in a module-level dict so it can be swapped per scan without a
globalrebind.Test plan
tests/test_output_store.pyadds: small output not spilled, oversized output spilled + fully retrievable (incl. a middle line absent from the preview but present in the retrieved full text), pagination hint/offset, path-traversal rejection, and missing-id handling.uv run pytestgreen (same 2 pre-existing unrelatedtest_runner_root_prompt.pyfailures asmain); ruff + mypy + bandit pass.Link to Devin session: https://app.devin.ai/sessions/3461e7dce476402aa2091e4af3f55c00
Requested by: @0xallam