fix(agent): finalize max-iteration turns without tools

This commit is contained in:
chengyongru
2026-06-10 14:47:20 +08:00
committed by Xubin Ren
parent 99f7f371fa
commit 5d91d59cf7
9 changed files with 199 additions and 18 deletions
+2 -1
View File
@@ -64,7 +64,8 @@ async def test_loop_goal_turn_uses_standard_iteration_budget(tmp_path):
)
assert stop_reason == "max_iterations"
assert loop.provider.chat_with_retry.await_count == 2
assert loop.provider.chat_with_retry.await_count == 3
assert loop.provider.chat_with_retry.await_args_list[-1].kwargs["tools"] is None
assert final_content == (
"I reached the maximum number of tool call iterations (2) "
"without completing the task. You can try breaking the task into smaller steps."
+55
View File
@@ -101,6 +101,61 @@ async def test_runner_returns_max_iterations_fallback():
)
assert result.messages[-1]["role"] == "assistant"
assert result.messages[-1]["content"] == result.final_content
assert provider.chat_with_retry.await_count == 3
assert provider.chat_with_retry.await_args_list[-1].kwargs["tools"] is None
assert tools.execute.await_count == 2
@pytest.mark.asyncio
async def test_runner_uses_no_tools_finalization_after_max_iterations():
from nanobot.agent.runner import AgentRunner, AgentRunSpec
provider = MagicMock(spec=LLMProvider)
calls: list[dict] = []
async def chat_with_retry(*, messages, tools=None, **kwargs):
calls.append({"messages": messages, "tools": tools})
if len(calls) <= 2:
return LLMResponse(
content="still working",
tool_calls=[
ToolCallRequest(
id=f"call_{len(calls)}",
name="list_dir",
arguments={"path": "."},
)
],
)
return LLMResponse(
content="Read the directory twice. More investigation remains.",
tool_calls=[],
usage={"prompt_tokens": 10, "completion_tokens": 7},
)
provider.chat_with_retry = chat_with_retry
tools = MagicMock()
tools.get_definitions.return_value = []
tools.execute = AsyncMock(return_value="tool result")
runner = AgentRunner(provider)
result = await runner.run(AgentRunSpec(
initial_messages=[{"role": "user", "content": "inspect the repo"}],
tools=tools,
model="test-model",
max_iterations=2,
max_tool_result_chars=_MAX_TOOL_RESULT_CHARS,
))
assert result.stop_reason == "max_iterations"
assert result.final_content == "Read the directory twice. More investigation remains."
assert result.messages[-1] == {
"role": "assistant",
"content": "Read the directory twice. More investigation remains.",
}
assert len(calls) == 3
assert calls[-1]["tools"] is None
assert "tool-call budget" in calls[-1]["messages"][-1]["content"]
assert tools.execute.await_count == 2
@pytest.mark.asyncio
+1
View File
@@ -150,6 +150,7 @@ async def test_runner_goal_continue_not_limited_by_injection_cycle_cap():
max_iterations=max_iterations,
max_tool_result_chars=_MAX_TOOL_RESULT_CHARS,
goal_active_predicate=lambda: True,
finalize_on_max_iterations=False,
))
assert result.stop_reason == "max_iterations"
+13
View File
@@ -17,6 +17,7 @@ from nanobot.session.turn_continuation import (
internal_continuation_pending,
internal_continuation_run_started_at,
maybe_continue_turn,
should_finalize_on_max_iterations,
should_stream_budget_response,
)
@@ -125,3 +126,15 @@ def test_internal_continuation_requires_budget_boundary_and_queue():
pending_queue_available=False,
session_metadata=meta,
)
assert not should_finalize_on_max_iterations(
pending_queue_available=True,
session_metadata=meta,
)
assert should_finalize_on_max_iterations(
pending_queue_available=False,
session_metadata=meta,
)
assert should_finalize_on_max_iterations(
pending_queue_available=True,
session_metadata={},
)