refactor(agent): reduce loop runner parameter plumbing

This commit is contained in:
chengyongru
2026-08-26 17:50:52 +08:00
committed by chengyongru
parent c62aec0175
commit a618e80887
14 changed files with 168 additions and 194 deletions
+31 -29
View File
@@ -1,8 +1,8 @@
"""Tests for sustained-goal continuation in AgentRunner.
"""Tests for caller-controlled continuation in AgentRunner.
When a goal_active_predicate returns True, the runner must not exit with
stop_reason="completed" after a plain-text final response. Instead it should
inject a continuation message and keep looping (similar to mid-turn injection).
When the continuation callback returns a message, the runner must not exit with
stop_reason="completed" after a plain-text final response. Instead it injects
that message and keeps looping, similar to a mid-turn injection.
"""
from __future__ import annotations
@@ -18,9 +18,13 @@ from nanobot.providers.base import LLMProvider, LLMResponse
_MAX_TOOL_RESULT_CHARS = AgentDefaults().max_tool_result_chars
def _continue_goal() -> str:
return "Continue working toward the active sustained goal."
@pytest.mark.asyncio
async def test_runner_exits_normally_without_predicate():
"""Baseline: no predicate, runner exits with completed on final text."""
async def test_runner_exits_normally_without_continuation_callback():
"""Without a continuation request, final text completes the run."""
from nanobot.agent.runner import AgentRunner
provider = MagicMock(spec=LLMProvider)
@@ -44,8 +48,8 @@ async def test_runner_exits_normally_without_predicate():
@pytest.mark.asyncio
async def test_runner_exits_normally_with_inactive_goal():
"""Predicate returns False, runner should exit normally."""
async def test_runner_exits_normally_when_continuation_callback_returns_none():
"""A callback returning None leaves the final response terminal."""
from nanobot.agent.runner import AgentRunner
provider = MagicMock(spec=LLMProvider)
@@ -62,7 +66,7 @@ async def test_runner_exits_normally_with_inactive_goal():
model="test-model",
max_iterations=2,
max_tool_result_chars=_MAX_TOOL_RESULT_CHARS,
goal_active_predicate=lambda: False,
continuation_callback=lambda: None,
))
assert result.stop_reason == "completed"
@@ -70,8 +74,8 @@ async def test_runner_exits_normally_with_inactive_goal():
@pytest.mark.asyncio
async def test_runner_forces_continue_when_goal_active():
"""Predicate returns True on final text → runner injects continuation and loops.
async def test_runner_continues_when_callback_returns_message():
"""A callback result after final text is injected for the next iteration.
We set max_iterations=3 and let the provider return final text every time.
Without the fix this would exit on the first iteration with stop_reason
@@ -94,10 +98,10 @@ async def test_runner_forces_continue_when_goal_active():
model="test-model",
max_iterations=3,
max_tool_result_chars=_MAX_TOOL_RESULT_CHARS,
goal_active_predicate=lambda: True,
continuation_callback=_continue_goal,
))
# Because the predicate keeps returning True, the runner should never
# Because the callback keeps returning a message, the runner should never
# naturally complete. It loops until max_iterations is exhausted.
assert result.stop_reason == "max_iterations"
# The injected continuation message should be present in the message list.
@@ -106,8 +110,8 @@ async def test_runner_forces_continue_when_goal_active():
@pytest.mark.asyncio
async def test_runner_respects_max_iterations_even_with_active_goal():
"""A single iteration with active goal still hits max_iterations."""
async def test_runner_respects_max_iterations_with_continuation():
"""A continuation request after one iteration still hits max_iterations."""
from nanobot.agent.runner import AgentRunner
provider = MagicMock(spec=LLMProvider)
@@ -124,15 +128,15 @@ async def test_runner_respects_max_iterations_even_with_active_goal():
model="test-model",
max_iterations=1,
max_tool_result_chars=_MAX_TOOL_RESULT_CHARS,
goal_active_predicate=lambda: True,
continuation_callback=_continue_goal,
))
assert result.stop_reason == "max_iterations"
@pytest.mark.asyncio
async def test_runner_goal_continue_not_limited_by_injection_cycle_cap():
"""Synthetic goal continuation should be governed by max_iterations."""
async def test_runner_continuation_not_limited_by_injection_cycle_cap():
"""Caller-requested continuation is governed by max_iterations."""
from nanobot.agent.runner import _MAX_INJECTION_CYCLES, AgentRunner
provider = MagicMock(spec=LLMProvider)
@@ -150,7 +154,7 @@ async def test_runner_goal_continue_not_limited_by_injection_cycle_cap():
model="test-model",
max_iterations=max_iterations,
max_tool_result_chars=_MAX_TOOL_RESULT_CHARS,
goal_active_predicate=lambda: True,
continuation_callback=_continue_goal,
finalize_on_max_iterations=False,
))
@@ -159,8 +163,8 @@ async def test_runner_goal_continue_not_limited_by_injection_cycle_cap():
@pytest.mark.asyncio
async def test_runner_does_not_force_continue_on_error():
"""Even with active goal, an LLM error should exit with stop_reason="error"."""
async def test_runner_does_not_continue_on_error():
"""An LLM error remains terminal even when continuation is available."""
from nanobot.agent.runner import AgentRunner
provider = MagicMock(spec=LLMProvider)
@@ -178,15 +182,15 @@ async def test_runner_does_not_force_continue_on_error():
model="test-model",
max_iterations=2,
max_tool_result_chars=_MAX_TOOL_RESULT_CHARS,
goal_active_predicate=lambda: True,
continuation_callback=_continue_goal,
))
assert result.stop_reason == "error"
@pytest.mark.asyncio
async def test_runner_uses_custom_goal_continue_message():
"""Custom goal_continue_message should be injected instead of the default."""
async def test_runner_injects_continuation_callback_message():
"""The callback result becomes the injected user message."""
from nanobot.agent.runner import AgentRunner
provider = MagicMock(spec=LLMProvider)
@@ -205,8 +209,7 @@ async def test_runner_uses_custom_goal_continue_message():
model="test-model",
max_iterations=2,
max_tool_result_chars=_MAX_TOOL_RESULT_CHARS,
goal_active_predicate=lambda: True,
goal_continue_message=custom_msg,
continuation_callback=lambda: custom_msg,
))
user_msgs = [m for m in result.messages if m.get("role") == "user"]
@@ -214,7 +217,7 @@ async def test_runner_uses_custom_goal_continue_message():
@pytest.mark.asyncio
async def test_runner_resolves_goal_continue_message_lazily():
async def test_runner_resolves_continuation_callback_lazily():
"""The continuation text can depend on goal metadata created during the run."""
from nanobot.agent.runner import AgentRunner
@@ -237,8 +240,7 @@ async def test_runner_resolves_goal_continue_message_lazily():
model="test-model",
max_iterations=1,
max_tool_result_chars=_MAX_TOOL_RESULT_CHARS,
goal_active_predicate=lambda: True,
goal_continue_message=dynamic_msg,
continuation_callback=dynamic_msg,
finalize_on_max_iterations=False,
))