Skip to content

feat(context): spill oversized tool output into the sandbox workspace#882

Merged
0xallam merged 14 commits into
mainfrom
agent/tool-output-spill
Jul 26, 2026
Merged

feat(context): spill oversized tool output into the sandbox workspace#882
0xallam merged 14 commits into
mainfrom
agent/tool-output-spill

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

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 spills the full output durably into the agent's own sandbox and gives it a bounded preview pointing at the file, so history stays bounded but nothing is lost.

Builds on #880 (per-tool bounding) and #881 (compaction), both merged. Rebased onto main.

Workspace spill only

Every agent run always has a sandbox (SandboxAgent's shell/filesystem require one; the runner always creates it), so the full output is written inside the agent's own sandbox at /workspace/.strix/tool-output/<id>.txt and the agent reads it back with its normal file tools (exec_command / sed / grep / cat) — no dedicated retrieval tool, no host-side store.

async def bound_and_store(text, *, max_lines, max_bytes) -> str:
    parts = _head_tail(text, max_lines, max_bytes)
    if parts is None: return text                       # small: unchanged
    writer = _spill.get("writer")
    if writer is not None:
        path = await writer(uuid4().hex, text)          # -> /workspace/.strix/tool-output/<id>.txt
        if path is not None:
            return _join(head, tail, workspace_notice(path))  # "read it with exec_command ..."
    return _join(head, tail, plain_notice(...))         # write failed: plain preview, bounded, no dangling pointer

Wiring

  • strix/core/runner.py: once the sandbox exists, installs a writer via configure_spill_writer(_spill_to_workspace) that does sandbox_session.write(/workspace/.strix/tool-output/<id>.txt, ...), cleared in finally so a later scan can't use a stale session.
  • The read_tool_output tool (strix/tools/tool_output/) and the host-side output store (store_full_output / read_stored_output / configure_output_store) are removed entirely.
  • _head_tail reserves bytes for the longest possible notice (workspace path or plain) so the joined preview always fits tool_output_max_bytes, and recomputes dropped-line counts after the byte-trim pass.

Test plan

tests/test_output_store.py: small output not spilled; oversized output spilled to the workspace (full text to the writer, notice points at the workspace path, a buried line absent from the preview is present in the spill); multibyte-safe preview within the byte ceiling; writer-failure / no-writer degrade to a plain preview; dropped-line count accounts for byte trimming. Validated live in a real Kali Docker sandbox (215KB output → spill file, agent grep/cat recovers it byte-identical) and in the full integration scan. uv run pytest green; ruff + mypy + bandit pass.

Link to Devin session: https://app.devin.ai/sessions/3461e7dce476402aa2091e4af3f55c00
Requested by: @0xallam

@0xallam 0xallam self-assigned this Jul 25, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@greptile-apps

greptile-apps Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Replaces host-side paginated tool-output retrieval with sandbox-local spill files.

  • Wraps oversized string results with asynchronous bounding and persistence.
  • Installs a per-scan workspace writer and clears it during runner cleanup.
  • Adds coverage for full-output spills, fallback behavior, path notices, and byte bounds.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
strix/tools/output_store.py Adds bounded previews backed by full-output spill files in the agent sandbox, eliminating the previously reported pagination defects.
strix/core/runner.py Configures the sandbox-specific spill writer for the scan lifecycle and clears it during cleanup.
strix/agents/factory.py Converts result bounding to an asynchronous path so every string-valued tool result can be spilled when oversized.
tests/test_output_store.py Covers spill persistence, bounded notices, multibyte output, and no-writer or failed-writer fallbacks.
tests/test_agent_factory_shell.py Verifies that assembled function tools retain the result-bounding wrapper.

Reviews (15): Last reviewed commit: "refactor(context): workspace spill only;..." | Re-trigger Greptile

Comment thread strix/agents/factory.py Outdated
Comment thread strix/tools/output_store.py Outdated
5hy7xz92nd-oss

This comment was marked as spam.

5hy7xz92nd-oss

This comment was marked as spam.

@devin-ai-integration
devin-ai-integration Bot force-pushed the agent/context-compaction branch from f6e2827 to 5433bfc Compare July 25, 2026 22:35
@devin-ai-integration
devin-ai-integration Bot force-pushed the agent/tool-output-spill branch from ed423a8 to 72041ab Compare July 25, 2026 22:41
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

@greptile

Comment thread strix/tools/output_store.py Outdated
@devin-ai-integration
devin-ai-integration Bot force-pushed the agent/context-compaction branch from 5433bfc to bae1f9a Compare July 25, 2026 22:51
@devin-ai-integration
devin-ai-integration Bot force-pushed the agent/tool-output-spill branch from 72041ab to 029d8b2 Compare July 25, 2026 22:52
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

@greptile

Comment thread strix/tools/output_store.py Outdated
@devin-ai-integration
devin-ai-integration Bot force-pushed the agent/context-compaction branch from bae1f9a to a3e2e3f Compare July 25, 2026 23:06
@devin-ai-integration
devin-ai-integration Bot force-pushed the agent/tool-output-spill branch 2 times, most recently from 83659b4 to 97919a4 Compare July 25, 2026 23:13
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

@greptile

@devin-ai-integration
devin-ai-integration Bot force-pushed the agent/tool-output-spill branch from 97919a4 to d2797af Compare July 25, 2026 23:25
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

@greptile

@devin-ai-integration
devin-ai-integration Bot force-pushed the agent/context-compaction branch from 771284e to 16cbd51 Compare July 25, 2026 23:49
@devin-ai-integration
devin-ai-integration Bot force-pushed the agent/tool-output-spill branch from 2c83e88 to 189e375 Compare July 25, 2026 23:51
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

@greptile

Comment thread strix/tools/output_store.py Outdated
@devin-ai-integration
devin-ai-integration Bot force-pushed the agent/tool-output-spill branch from 189e375 to 028a854 Compare July 26, 2026 00:05
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

@greptile

@devin-ai-integration
devin-ai-integration Bot force-pushed the agent/tool-output-spill branch from 028a854 to e5a46b9 Compare July 26, 2026 00:16
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

@greptile

Comment thread strix/tools/output_store.py Outdated
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

@greptile

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

@greptile

1 similar comment
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

@greptile

5hy7xz92nd-oss

This comment was marked as spam.

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

@greptile

2 similar comments
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

@greptile

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

@greptile

@devin-ai-integration
devin-ai-integration Bot force-pushed the agent/context-compaction branch from 4eedfc6 to d4f72e6 Compare July 26, 2026 19:29
@devin-ai-integration
devin-ai-integration Bot force-pushed the agent/tool-output-spill branch 3 times, most recently from 0123e3b to c91908a Compare July 26, 2026 21:09
Base automatically changed from agent/context-compaction to main July 26, 2026 21:39
0xallam added 14 commits July 26, 2026 21:40
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.
…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.
Prefer writing an oversized tool result into the agent's own sandbox at
/workspace/.strix/tool-output/<id>.txt so the agent reads it back with its
normal file tools (exec_command etc.) instead of a dedicated retrieval tool.
The host-side store + read_tool_output remain as a fallback when no sandbox
writer is configured (unit tests, chat mode) or a workspace write fails.

Only sandbox-origin tools (shell/filesystem capabilities) may spill into the
workspace; orchestrator-side tool output is never written into the hostile
sandbox and always uses the host-side store.
…pace

Drop the workspace_spill toggle and the orchestrator-vs-sandbox origin guard.
Every oversized tool result now prefers the sandbox-workspace writer; the
host-side store + read_tool_output remains only as the fallback when no writer
is configured (unit tests, chat mode) or the workspace write fails.
…output

Every agent run always has a sandbox, so the host-side output store and the
read_tool_output retrieval tool were only ever a redundant fallback. Remove
them entirely: oversized tool output always spills to
/workspace/.strix/tool-output/<id>.txt and the agent reads it with its own
file tools. If the sandbox write fails, degrade to a plain head+tail preview.
@devin-ai-integration
devin-ai-integration Bot force-pushed the agent/tool-output-spill branch from c91908a to f957a32 Compare July 26, 2026 21:40
@devin-ai-integration devin-ai-integration Bot changed the title feat(context): spill oversized tool output to disk with a retrieval tool feat(context): spill oversized tool output into the sandbox workspace Jul 26, 2026
@0xallam
0xallam merged commit 3b79e97 into main Jul 26, 2026
@0xallam
0xallam deleted the agent/tool-output-spill branch July 26, 2026 21:42
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.

2 participants