mirror of
https://github.com/usestrix/strix.git
synced 2026-08-24 11:52:38 +02:00
feat(context): spill oversized tool output into the sandbox workspace (#882)
Co-authored-by: Ahmed Allam <ahmed39652003@gmail.com>
This commit is contained in:
co-authored by
Ahmed Allam
parent
66a283b71b
commit
3b79e97f00
@@ -108,3 +108,10 @@ async def test_chat_completions_filesystem_custom_tool_becomes_function_tool() -
|
||||
factory._configure_filesystem_tools(toolset, chat_completions=True)
|
||||
|
||||
assert isinstance(toolset.read_file, FunctionTool)
|
||||
|
||||
|
||||
def test_function_tools_are_result_bounded() -> None:
|
||||
agent = factory.build_strix_agent(is_root=True)
|
||||
by_name = {t.name: t for t in agent.tools}
|
||||
|
||||
assert getattr(by_name["think"], "_strix_bounded", False) is True
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
"""Tests for per-tool-output bounding."""
|
||||
"""Tests for per-tool-output bounding and sandbox-workspace spill."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from strix.tools.output_store import bound_text
|
||||
import pytest
|
||||
|
||||
from strix.tools.output_store import (
|
||||
WORKSPACE_SPILL_DIR,
|
||||
bound_and_store,
|
||||
bound_text,
|
||||
configure_spill_writer,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clear_spill_writer() -> None:
|
||||
configure_spill_writer(None)
|
||||
|
||||
|
||||
def test_small_output_passes_through_unchanged() -> None:
|
||||
@@ -58,3 +70,87 @@ def test_dropped_line_count_accounts_for_byte_trimming() -> None:
|
||||
kept = [ln for ln in bounded.splitlines() if ln and "truncated" not in ln]
|
||||
assert dropped == 200 - len(kept)
|
||||
assert dropped > 200 - 20
|
||||
|
||||
|
||||
async def test_bound_and_store_small_output_not_spilled() -> None:
|
||||
written: dict[str, str] = {}
|
||||
|
||||
async def writer(output_id: str, text: str) -> str | None:
|
||||
written[output_id] = text
|
||||
return f"{WORKSPACE_SPILL_DIR}/{output_id}.txt"
|
||||
|
||||
configure_spill_writer(writer)
|
||||
text = "just a few lines\nsecond line"
|
||||
assert await bound_and_store(text, max_lines=100, max_bytes=10_000) == text
|
||||
assert written == {}
|
||||
|
||||
|
||||
async def test_bound_and_store_spills_full_output_to_workspace() -> None:
|
||||
written: dict[str, str] = {}
|
||||
|
||||
async def writer(output_id: str, text: str) -> str | None:
|
||||
written[output_id] = text
|
||||
return f"{WORKSPACE_SPILL_DIR}/{output_id}.txt"
|
||||
|
||||
configure_spill_writer(writer)
|
||||
text = "\n".join(f"secret-line-{i}" for i in range(1000))
|
||||
bounded = await bound_and_store(text, max_lines=10, max_bytes=1_000_000)
|
||||
|
||||
assert WORKSPACE_SPILL_DIR in bounded
|
||||
assert "exec_command" in bounded
|
||||
assert "read_tool_output" not in bounded
|
||||
assert len(bounded.encode("utf-8")) <= 1_000_000
|
||||
assert list(written.values()) == [text]
|
||||
stored = next(iter(written.values()))
|
||||
assert stored.splitlines() == text.splitlines()
|
||||
# A buried line elided from the preview is still present in the spilled file.
|
||||
assert "secret-line-500" not in bounded
|
||||
assert "secret-line-500" in stored
|
||||
|
||||
|
||||
async def test_workspace_notice_carries_the_returned_path() -> None:
|
||||
async def writer(output_id: str, _text: str) -> str | None:
|
||||
return f"{WORKSPACE_SPILL_DIR}/{output_id}.txt"
|
||||
|
||||
configure_spill_writer(writer)
|
||||
text = "\n".join(f"line-{i}" for i in range(1000))
|
||||
bounded = await bound_and_store(text, max_lines=10, max_bytes=1_000_000)
|
||||
|
||||
match = re.search(rf"{re.escape(WORKSPACE_SPILL_DIR)}/([0-9a-f]{{32}})\.txt", bounded)
|
||||
assert match is not None, bounded
|
||||
|
||||
|
||||
async def test_no_writer_degrades_to_plain_preview() -> None:
|
||||
text = "\n".join(f"line-{i}" for i in range(1000))
|
||||
bounded = await bound_and_store(text, max_lines=10, max_bytes=1_000_000)
|
||||
|
||||
assert "truncated" in bounded
|
||||
assert WORKSPACE_SPILL_DIR not in bounded
|
||||
assert "read_tool_output" not in bounded
|
||||
assert len(bounded.encode("utf-8")) <= 1_000_000
|
||||
|
||||
|
||||
async def test_writer_failure_degrades_to_plain_preview() -> None:
|
||||
async def failing_writer(_output_id: str, _text: str) -> str | None:
|
||||
return None
|
||||
|
||||
configure_spill_writer(failing_writer)
|
||||
text = "\n".join(f"line-{i}" for i in range(1000))
|
||||
bounded = await bound_and_store(text, max_lines=10, max_bytes=1_000_000)
|
||||
|
||||
assert "truncated" in bounded
|
||||
assert WORKSPACE_SPILL_DIR not in bounded
|
||||
assert len(bounded.encode("utf-8")) <= 1_000_000
|
||||
|
||||
|
||||
async def test_workspace_preview_honours_byte_budget() -> None:
|
||||
# The workspace notice is longer than a plain notice; the preview reserves for it.
|
||||
async def writer(output_id: str, _text: str) -> str | None:
|
||||
return f"{WORKSPACE_SPILL_DIR}/{output_id}.txt"
|
||||
|
||||
configure_spill_writer(writer)
|
||||
text = "\n".join("x" * 500 for _ in range(200))
|
||||
bounded = await bound_and_store(text, max_lines=2_000, max_bytes=2_000)
|
||||
|
||||
assert WORKSPACE_SPILL_DIR in bounded
|
||||
assert len(bounded.encode("utf-8")) <= 2_000
|
||||
|
||||
Reference in New Issue
Block a user