414 lines
13 KiB
Python
414 lines
13 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from opensquilla.engine.tool_result_store import (
|
|
TOOL_RESULT_COMPRESSED_CONTENT_NAME,
|
|
ToolResultStore,
|
|
)
|
|
from opensquilla.engine.types import ToolCall
|
|
from opensquilla.tools import get_default_registry
|
|
from opensquilla.tools.builtin import tool_results
|
|
from opensquilla.tools.dispatch import build_tool_handler
|
|
from opensquilla.tools.types import CallerKind, SafeToolError, ToolContext, current_tool_context
|
|
|
|
|
|
def _write_record(root: Path, *, session_id: str = "session-a") -> str:
|
|
content = "\n".join(
|
|
[
|
|
"alpha",
|
|
"beta context",
|
|
"ERROR target line",
|
|
"delta context",
|
|
"epsilon",
|
|
]
|
|
)
|
|
record = ToolResultStore(root).write(
|
|
content,
|
|
tool_use_id="call-1",
|
|
tool_name="exec_command",
|
|
session_id=session_id,
|
|
session_key="agent:main:test",
|
|
agent_id="main",
|
|
)
|
|
return record.handle
|
|
|
|
|
|
def _ctx(
|
|
root: Path,
|
|
*,
|
|
session_id: str = "session-a",
|
|
allowed: bool = False,
|
|
) -> ToolContext:
|
|
return ToolContext(
|
|
is_owner=True,
|
|
caller_kind=CallerKind.AGENT,
|
|
session_key="agent:main:test",
|
|
agent_id="main",
|
|
tool_result_store_dir=str(root),
|
|
tool_result_store_session_id=session_id,
|
|
allowed_tools={"retrieve_tool_result"} if allowed else None,
|
|
)
|
|
|
|
|
|
def test_tool_result_store_compresses_large_snapshot_over_raw_limit(tmp_path: Path) -> None:
|
|
content = "\n".join("ERROR repeated diagnostic line" for _ in range(20_000))
|
|
|
|
record = ToolResultStore(tmp_path).write(
|
|
content,
|
|
tool_use_id="call-large",
|
|
tool_name="grep_search",
|
|
session_id="session-a",
|
|
session_key="agent:main:test",
|
|
agent_id="main",
|
|
max_bytes=8_000,
|
|
)
|
|
|
|
assert record.size_bytes > 8_000
|
|
assert record.stored_size_bytes is not None
|
|
assert record.stored_size_bytes <= 8_000
|
|
assert record.storage_encoding == "gzip+utf-8"
|
|
assert list(tmp_path.rglob(TOOL_RESULT_COMPRESSED_CONTENT_NAME))
|
|
|
|
reread = ToolResultStore(tmp_path).read(record.handle, session_id="session-a")
|
|
assert reread.content == content
|
|
assert reread.sha256 == record.sha256
|
|
assert reread.stored_size_bytes == record.stored_size_bytes
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_retrieve_tool_result_metadata_reads_current_session_record(tmp_path: Path) -> None:
|
|
handle = _write_record(tmp_path)
|
|
token = current_tool_context.set(_ctx(tmp_path))
|
|
try:
|
|
result = await tool_results.retrieve_tool_result(handle, mode="metadata")
|
|
finally:
|
|
current_tool_context.reset(token)
|
|
|
|
assert result.startswith("[tool_result_retrieval]")
|
|
metadata = json.loads(result.split("---\n", 1)[1])
|
|
assert metadata["handle"] == handle
|
|
assert metadata["tool_name"] == "exec_command"
|
|
assert metadata["line_count"] == 5
|
|
assert metadata["storage_encoding"] == "utf-8"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_retrieve_tool_result_slice_returns_bounded_numbered_lines(tmp_path: Path) -> None:
|
|
handle = _write_record(tmp_path)
|
|
token = current_tool_context.set(_ctx(tmp_path))
|
|
try:
|
|
result = await tool_results.retrieve_tool_result(
|
|
handle,
|
|
mode="slice",
|
|
start_line=2,
|
|
end_line=4,
|
|
)
|
|
finally:
|
|
current_tool_context.reset(token)
|
|
|
|
assert "mode: slice" in result
|
|
assert "2| beta context" in result
|
|
assert "3| ERROR target line" in result
|
|
assert "4| delta context" in result
|
|
assert "1| alpha" not in result
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_retrieve_tool_result_grep_returns_match_context(tmp_path: Path) -> None:
|
|
handle = _write_record(tmp_path)
|
|
token = current_tool_context.set(_ctx(tmp_path))
|
|
try:
|
|
result = await tool_results.retrieve_tool_result(
|
|
handle,
|
|
mode="grep",
|
|
pattern="ERROR",
|
|
context_lines=1,
|
|
)
|
|
finally:
|
|
current_tool_context.reset(token)
|
|
|
|
assert "[lines 2-4]" in result
|
|
assert "2| beta context" in result
|
|
assert "3| ERROR target line" in result
|
|
assert "4| delta context" in result
|
|
assert "5| epsilon" not in result
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_retrieve_tool_result_query_supports_line_refs(tmp_path: Path) -> None:
|
|
handle = _write_record(tmp_path)
|
|
token = current_tool_context.set(_ctx(tmp_path))
|
|
try:
|
|
result = await tool_results.retrieve_tool_result(handle, query="L3", context_lines=0)
|
|
finally:
|
|
current_tool_context.reset(token)
|
|
|
|
assert "mode: query" in result
|
|
assert "3| ERROR target line" in result
|
|
assert "2| beta context" not in result
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_retrieve_tool_result_query_extracts_embedded_line_refs(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
handle = _write_record(tmp_path)
|
|
token = current_tool_context.set(_ctx(tmp_path))
|
|
try:
|
|
result = await tool_results.retrieve_tool_result(
|
|
handle,
|
|
query="show L2 and line 4 from search_hints",
|
|
context_lines=0,
|
|
)
|
|
finally:
|
|
current_tool_context.reset(token)
|
|
|
|
assert "mode: query" in result
|
|
assert "[lines 2-2]" in result
|
|
assert "2| beta context" in result
|
|
assert "[lines 4-4]" in result
|
|
assert "4| delta context" in result
|
|
assert "3| ERROR target line" not in result
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_retrieve_tool_result_query_extracts_embedded_line_ranges(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
handle = _write_record(tmp_path)
|
|
token = current_tool_context.set(_ctx(tmp_path))
|
|
try:
|
|
result = await tool_results.retrieve_tool_result(
|
|
handle,
|
|
query="diagnostics around L2-L4",
|
|
context_lines=0,
|
|
)
|
|
finally:
|
|
current_tool_context.reset(token)
|
|
|
|
assert "[lines 2-4]" in result
|
|
assert "2| beta context" in result
|
|
assert "3| ERROR target line" in result
|
|
assert "4| delta context" in result
|
|
assert "5| epsilon" not in result
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_retrieve_tool_result_query_supports_text_search(tmp_path: Path) -> None:
|
|
handle = _write_record(tmp_path)
|
|
token = current_tool_context.set(_ctx(tmp_path))
|
|
try:
|
|
result = await tool_results.retrieve_tool_result(handle, query="ERROR", context_lines=1)
|
|
finally:
|
|
current_tool_context.reset(token)
|
|
|
|
assert "mode: query" in result
|
|
assert "[lines 2-4]" in result
|
|
assert "3| ERROR target line" in result
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_retrieve_tool_result_normalizes_qwen_style_mixed_arguments(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
handle = _write_record(tmp_path)
|
|
token = current_tool_context.set(_ctx(tmp_path))
|
|
try:
|
|
grep_result = await tool_results.retrieve_tool_result(
|
|
handle,
|
|
mode="not_a_mode",
|
|
pattern="ERROR",
|
|
limit=1000,
|
|
)
|
|
query_result = await tool_results.retrieve_tool_result(
|
|
handle,
|
|
mode="grep",
|
|
query="ERROR",
|
|
max_chars=1000,
|
|
)
|
|
slice_result = await tool_results.retrieve_tool_result(
|
|
handle,
|
|
start_line=2,
|
|
end_line=3,
|
|
)
|
|
finally:
|
|
current_tool_context.reset(token)
|
|
|
|
assert "mode: grep" in grep_result
|
|
assert "3| ERROR target line" in grep_result
|
|
assert "mode: query" in query_result
|
|
assert "3| ERROR target line" in query_result
|
|
assert "mode: slice" in slice_result
|
|
assert "2| beta context" in slice_result
|
|
assert "3| ERROR target line" in slice_result
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_retrieve_tool_result_accepts_handle_from_projection_line(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
handle = _write_record(tmp_path)
|
|
token = current_tool_context.set(_ctx(tmp_path))
|
|
try:
|
|
result = await tool_results.retrieve_tool_result(
|
|
f"tool_result_handle: {handle}",
|
|
mode="metadata",
|
|
)
|
|
finally:
|
|
current_tool_context.reset(token)
|
|
|
|
assert "mode: metadata" in result
|
|
assert f'"handle": "{handle}"' in result
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_retrieve_tool_result_raw_slice_returns_continuation(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
handle = _write_record(tmp_path)
|
|
token = current_tool_context.set(_ctx(tmp_path))
|
|
try:
|
|
result = await tool_results.retrieve_tool_result(
|
|
handle,
|
|
mode="raw_slice",
|
|
offset=0,
|
|
limit=12,
|
|
)
|
|
finally:
|
|
current_tool_context.reset(token)
|
|
|
|
assert "mode: raw_slice" in result
|
|
assert "offset: 0" in result
|
|
assert "returned_chars: 12" in result
|
|
assert "continuation.next_call_strategy: raw_slice_offset" in result
|
|
assert '"offset": 12' in result
|
|
assert '"mode": "raw_slice"' in result
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_retrieve_tool_result_query_clip_includes_larger_retry_continuation(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
content = "\n".join(f"ERROR repeated diagnostic line {index}" for index in range(80))
|
|
record = ToolResultStore(tmp_path).write(
|
|
content,
|
|
tool_use_id="call-large-query",
|
|
tool_name="exec_command",
|
|
session_id="session-a",
|
|
session_key="agent:main:test",
|
|
agent_id="main",
|
|
)
|
|
token = current_tool_context.set(_ctx(tmp_path))
|
|
try:
|
|
result = await tool_results.retrieve_tool_result(
|
|
record.handle,
|
|
query="ERROR",
|
|
context_lines=1,
|
|
max_chars=500,
|
|
)
|
|
finally:
|
|
current_tool_context.reset(token)
|
|
|
|
assert len(result) <= 500
|
|
assert "truncated" in result
|
|
assert "continuation.next_call_strategy: same_query_larger_max_chars" in result
|
|
assert '"query": "ERROR"' in result
|
|
assert '"max_chars": 2000' in result
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_retrieve_tool_result_clips_large_output(tmp_path: Path) -> None:
|
|
handle = _write_record(tmp_path)
|
|
token = current_tool_context.set(_ctx(tmp_path))
|
|
try:
|
|
result = await tool_results.retrieve_tool_result(
|
|
handle,
|
|
mode="head_tail",
|
|
max_chars=90,
|
|
)
|
|
finally:
|
|
current_tool_context.reset(token)
|
|
|
|
assert len(result) <= 90
|
|
assert "truncated" in result
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_retrieve_tool_result_rejects_cross_session_handle(tmp_path: Path) -> None:
|
|
handle = _write_record(tmp_path, session_id="session-a")
|
|
token = current_tool_context.set(_ctx(tmp_path, session_id="session-b"))
|
|
try:
|
|
with pytest.raises(SafeToolError) as exc_info:
|
|
await tool_results.retrieve_tool_result(handle)
|
|
finally:
|
|
current_tool_context.reset(token)
|
|
|
|
assert "current session" in exc_info.value.user_message
|
|
|
|
|
|
def test_retrieve_tool_result_is_hidden_by_default_but_explicitly_allowable() -> None:
|
|
registry = get_default_registry()
|
|
assert registry.get("retrieve_tool_result") is not None
|
|
|
|
default_names = {
|
|
tool.name
|
|
for tool in registry.to_tool_definitions(
|
|
ToolContext(is_owner=True, caller_kind=CallerKind.AGENT)
|
|
)
|
|
}
|
|
assert "retrieve_tool_result" not in default_names
|
|
|
|
allowed_names = {
|
|
tool.name
|
|
for tool in registry.to_tool_definitions(
|
|
ToolContext(
|
|
is_owner=True,
|
|
caller_kind=CallerKind.AGENT,
|
|
allowed_tools={"retrieve_tool_result"},
|
|
)
|
|
)
|
|
}
|
|
assert allowed_names == {"retrieve_tool_result"}
|
|
|
|
|
|
def test_retrieve_tool_result_schema_guides_projected_output_recovery() -> None:
|
|
definitions = get_default_registry().to_tool_definitions(
|
|
ToolContext(
|
|
is_owner=True,
|
|
caller_kind=CallerKind.AGENT,
|
|
allowed_tools={"retrieve_tool_result"},
|
|
)
|
|
)
|
|
|
|
definition = definitions[0]
|
|
assert definition.name == "retrieve_tool_result"
|
|
assert "aggregate_tool_result_compacted" in definition.description
|
|
assert "before acting on a projected result" in definition.description
|
|
properties = definition.input_schema.properties
|
|
assert "Prefer query" in properties["mode"]["description"]
|
|
assert "raw_slice" in properties["mode"]["description"]
|
|
assert "failing test name" in properties["query"]["description"]
|
|
assert "offset" in properties
|
|
assert "limit" in properties
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_retrieve_tool_result_dispatch_uses_tool_context(tmp_path: Path) -> None:
|
|
handle = _write_record(tmp_path)
|
|
handler = build_tool_handler(get_default_registry(), _ctx(tmp_path, allowed=True))
|
|
|
|
result = await handler(
|
|
ToolCall(
|
|
tool_use_id="call-dispatch",
|
|
tool_name="retrieve_tool_result",
|
|
arguments={"handle": handle, "mode": "grep", "pattern": "target"},
|
|
)
|
|
)
|
|
|
|
assert result.is_error is False
|
|
assert "ERROR target line" in result.content
|