mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-09-04 02:01:48 +03:00
refactor(agent): reduce loop runner parameter plumbing
This commit is contained in:
@@ -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,
|
||||
))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user