mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-06-15 07:14:08 +00:00
fix(agent): keep sustained goal continuation independent
This commit is contained in:
parent
7bbd9c7103
commit
4f14f980d9
@ -16,12 +16,14 @@ from nanobot.agent.hook import AgentHook, AgentHookContext
|
|||||||
from nanobot.agent.tools.registry import ToolRegistry
|
from nanobot.agent.tools.registry import ToolRegistry
|
||||||
from nanobot.providers.base import LLMProvider, LLMResponse, ToolCallRequest
|
from nanobot.providers.base import LLMProvider, LLMResponse, ToolCallRequest
|
||||||
from nanobot.utils.file_edit_events import (
|
from nanobot.utils.file_edit_events import (
|
||||||
|
StreamingFileEditTracker,
|
||||||
build_file_edit_end_event,
|
build_file_edit_end_event,
|
||||||
build_file_edit_error_event,
|
build_file_edit_error_event,
|
||||||
build_file_edit_start_event,
|
build_file_edit_start_event,
|
||||||
prepare_file_edit_tracker as _prepare_file_edit_tracker,
|
|
||||||
prepare_file_edit_trackers,
|
prepare_file_edit_trackers,
|
||||||
StreamingFileEditTracker,
|
)
|
||||||
|
from nanobot.utils.file_edit_events import (
|
||||||
|
prepare_file_edit_tracker as _prepare_file_edit_tracker,
|
||||||
)
|
)
|
||||||
from nanobot.utils.helpers import (
|
from nanobot.utils.helpers import (
|
||||||
IncrementalThinkExtractor,
|
IncrementalThinkExtractor,
|
||||||
@ -179,16 +181,19 @@ class AgentRunner:
|
|||||||
and *iteration* are both provided) and return (True, cycles+1) so the
|
and *iteration* are both provided) and return (True, cycles+1) so the
|
||||||
caller continues the iteration loop. Otherwise return (False, cycles).
|
caller continues the iteration loop. Otherwise return (False, cycles).
|
||||||
"""
|
"""
|
||||||
if injection_cycles >= _MAX_INJECTION_CYCLES:
|
injections: list[dict[str, Any]] = []
|
||||||
return False, injection_cycles
|
real_injection = False
|
||||||
injections = await self._drain_injections(spec)
|
if injection_cycles < _MAX_INJECTION_CYCLES:
|
||||||
|
injections = await self._drain_injections(spec)
|
||||||
|
real_injection = bool(injections)
|
||||||
if not injections and allow_goal_continue and assistant_message is not None:
|
if not injections and allow_goal_continue and assistant_message is not None:
|
||||||
predicate = spec.goal_active_predicate
|
predicate = spec.goal_active_predicate
|
||||||
if predicate is not None and predicate():
|
if predicate is not None and predicate():
|
||||||
injections = [build_goal_continue_message(spec.goal_continue_message)]
|
injections = [build_goal_continue_message(spec.goal_continue_message)]
|
||||||
if not injections:
|
if not injections:
|
||||||
return False, injection_cycles
|
return False, injection_cycles
|
||||||
injection_cycles += 1
|
if real_injection:
|
||||||
|
injection_cycles += 1
|
||||||
if assistant_message is not None:
|
if assistant_message is not None:
|
||||||
messages.append(assistant_message)
|
messages.append(assistant_message)
|
||||||
if iteration is not None:
|
if iteration is not None:
|
||||||
@ -204,10 +209,13 @@ class AgentRunner:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
self._append_injected_messages(messages, injections)
|
self._append_injected_messages(messages, injections)
|
||||||
logger.info(
|
if real_injection:
|
||||||
"Injected {} follow-up message(s) {} ({}/{})",
|
logger.info(
|
||||||
len(injections), phase, injection_cycles, _MAX_INJECTION_CYCLES,
|
"Injected {} follow-up message(s) {} ({}/{})",
|
||||||
)
|
len(injections), phase, injection_cycles, _MAX_INJECTION_CYCLES,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.info("Injected sustained-goal continuation {}", phase)
|
||||||
return True, injection_cycles
|
return True, injection_cycles
|
||||||
|
|
||||||
async def _drain_injections(self, spec: AgentRunSpec) -> list[dict[str, Any]]:
|
async def _drain_injections(self, spec: AgentRunSpec) -> list[dict[str, Any]]:
|
||||||
|
|||||||
@ -129,6 +129,33 @@ async def test_runner_respects_max_iterations_even_with_active_goal():
|
|||||||
assert result.stop_reason == "max_iterations"
|
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."""
|
||||||
|
from nanobot.agent.runner import _MAX_INJECTION_CYCLES, AgentRunner, AgentRunSpec
|
||||||
|
|
||||||
|
provider = MagicMock(spec=LLMProvider)
|
||||||
|
provider.chat_with_retry = AsyncMock(return_value=LLMResponse(
|
||||||
|
content="still working", tool_calls=[], usage={},
|
||||||
|
))
|
||||||
|
tools = MagicMock()
|
||||||
|
tools.get_definitions.return_value = []
|
||||||
|
max_iterations = _MAX_INJECTION_CYCLES + 3
|
||||||
|
|
||||||
|
runner = AgentRunner(provider)
|
||||||
|
result = await runner.run(AgentRunSpec(
|
||||||
|
initial_messages=[{"role": "user", "content": "do task"}],
|
||||||
|
tools=tools,
|
||||||
|
model="test-model",
|
||||||
|
max_iterations=max_iterations,
|
||||||
|
max_tool_result_chars=_MAX_TOOL_RESULT_CHARS,
|
||||||
|
goal_active_predicate=lambda: True,
|
||||||
|
))
|
||||||
|
|
||||||
|
assert result.stop_reason == "max_iterations"
|
||||||
|
assert provider.chat_with_retry.await_count == max_iterations
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_runner_does_not_force_continue_on_error():
|
async def test_runner_does_not_force_continue_on_error():
|
||||||
"""Even with active goal, an LLM error should exit with stop_reason="error"."""
|
"""Even with active goal, an LLM error should exit with stop_reason="error"."""
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user