mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-06-13 22:34:06 +00:00
fix(runner): prevent read_file offload loop
This commit is contained in:
parent
f382133bb4
commit
92fe40a690
@ -69,6 +69,8 @@ _COMPACTABLE_TOOLS = frozenset({
|
||||
"read_file", "exec", "grep", "find_files",
|
||||
"web_search", "web_fetch", "list_dir", "list_exec_sessions",
|
||||
})
|
||||
# read_file is the recovery path for persisted results; exempting it prevents persist->read->persist loops.
|
||||
_TOOL_RESULT_OFFLOAD_EXEMPT_TOOLS = frozenset({"read_file"})
|
||||
_BACKFILL_CONTENT = "[Tool result unavailable — call was interrupted or lost]"
|
||||
|
||||
# Backward-compatible module attribute for tests/extensions that monkeypatch
|
||||
@ -1114,6 +1116,9 @@ class AgentRunner:
|
||||
result: Any,
|
||||
) -> Any:
|
||||
result = ensure_nonempty_tool_result(tool_name, result)
|
||||
if tool_name in _TOOL_RESULT_OFFLOAD_EXEMPT_TOOLS:
|
||||
# Exempt tools bound their own output; skip generic offload and truncation.
|
||||
return result
|
||||
try:
|
||||
content = maybe_persist_tool_result(
|
||||
spec.workspace,
|
||||
|
||||
@ -123,6 +123,54 @@ def test_persist_tool_result_logs_cleanup_failures(monkeypatch, tmp_path):
|
||||
|
||||
assert "[tool output persisted]" in persisted
|
||||
assert warnings and "Failed to clean stale tool result buckets" in warnings[0]
|
||||
|
||||
|
||||
async def test_read_file_result_is_not_offloaded(tmp_path):
|
||||
"""read_file must not trigger generic offloading (prevents persist->read->persist loops)."""
|
||||
from nanobot.agent.runner import AgentRunner, AgentRunSpec
|
||||
|
||||
provider = MagicMock()
|
||||
captured_second_call: list[dict] = []
|
||||
call_count = {"n": 0}
|
||||
|
||||
async def chat_with_retry(*, messages, **kwargs):
|
||||
call_count["n"] += 1
|
||||
if call_count["n"] == 1:
|
||||
return LLMResponse(
|
||||
content="reading",
|
||||
tool_calls=[ToolCallRequest(id="call_rf", name="read_file", arguments={"path": "big.txt"})],
|
||||
usage={"prompt_tokens": 5, "completion_tokens": 3},
|
||||
)
|
||||
captured_second_call[:] = messages
|
||||
return LLMResponse(content="done", tool_calls=[], usage={})
|
||||
|
||||
provider.chat_with_retry = chat_with_retry
|
||||
tools = MagicMock()
|
||||
tools.get_definitions.return_value = []
|
||||
tools.execute = AsyncMock(return_value="x" * 20_000)
|
||||
|
||||
runner = AgentRunner(provider)
|
||||
result = await runner.run(AgentRunSpec(
|
||||
initial_messages=[{"role": "user", "content": "read big file"}],
|
||||
tools=tools,
|
||||
model="test-model",
|
||||
max_iterations=2,
|
||||
workspace=tmp_path,
|
||||
session_key="test:runner",
|
||||
max_tool_result_chars=2048,
|
||||
))
|
||||
|
||||
assert result.final_content == "done"
|
||||
tool_message = next(msg for msg in captured_second_call if msg.get("role") == "tool")
|
||||
# read_file result must NOT be offloaded to a file
|
||||
assert "[tool output persisted]" not in tool_message["content"]
|
||||
# read_file manages its own size; generic truncation must NOT apply
|
||||
assert len(tool_message["content"]) == 20_000
|
||||
# no file should have been written for this read_file call
|
||||
offload_dir = tmp_path / ".nanobot" / "tool-results"
|
||||
assert not any(offload_dir.rglob("call_rf.txt")) if offload_dir.exists() else True
|
||||
|
||||
|
||||
async def test_runner_keeps_going_when_tool_result_persistence_fails():
|
||||
from nanobot.agent.runner import AgentRunSpec, AgentRunner
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user