mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-06-14 06:43:53 +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",
|
"read_file", "exec", "grep", "find_files",
|
||||||
"web_search", "web_fetch", "list_dir", "list_exec_sessions",
|
"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]"
|
_BACKFILL_CONTENT = "[Tool result unavailable — call was interrupted or lost]"
|
||||||
|
|
||||||
# Backward-compatible module attribute for tests/extensions that monkeypatch
|
# Backward-compatible module attribute for tests/extensions that monkeypatch
|
||||||
@ -1114,6 +1116,9 @@ class AgentRunner:
|
|||||||
result: Any,
|
result: Any,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
result = ensure_nonempty_tool_result(tool_name, result)
|
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:
|
try:
|
||||||
content = maybe_persist_tool_result(
|
content = maybe_persist_tool_result(
|
||||||
spec.workspace,
|
spec.workspace,
|
||||||
|
|||||||
@ -123,6 +123,54 @@ def test_persist_tool_result_logs_cleanup_failures(monkeypatch, tmp_path):
|
|||||||
|
|
||||||
assert "[tool output persisted]" in persisted
|
assert "[tool output persisted]" in persisted
|
||||||
assert warnings and "Failed to clean stale tool result buckets" in warnings[0]
|
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():
|
async def test_runner_keeps_going_when_tool_result_persistence_fails():
|
||||||
from nanobot.agent.runner import AgentRunSpec, AgentRunner
|
from nanobot.agent.runner import AgentRunSpec, AgentRunner
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user