refactor(agent): make runner consume required runtime
This commit is contained in:
@@ -8,6 +8,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from agent.runner_helpers import make_run_spec
|
||||
from nanobot.config.schema import AgentDefaults
|
||||
from nanobot.providers.base import LLMResponse, ToolCallRequest
|
||||
|
||||
@@ -42,13 +43,13 @@ def _make_loop(tmp_path):
|
||||
@pytest.mark.asyncio
|
||||
async def test_drain_injections_returns_empty_when_no_callback():
|
||||
"""No injection_callback → empty list."""
|
||||
from nanobot.agent.runner import AgentRunner, AgentRunSpec
|
||||
from nanobot.agent.runner import AgentRunner
|
||||
|
||||
provider = MagicMock()
|
||||
runner = AgentRunner(provider)
|
||||
runner = AgentRunner()
|
||||
tools = MagicMock()
|
||||
tools.get_definitions.return_value = []
|
||||
spec = AgentRunSpec(
|
||||
spec = make_run_spec(provider,
|
||||
initial_messages=[], tools=tools, model="m",
|
||||
max_iterations=1, max_tool_result_chars=1000,
|
||||
injection_callback=None,
|
||||
@@ -60,11 +61,11 @@ async def test_drain_injections_returns_empty_when_no_callback():
|
||||
@pytest.mark.asyncio
|
||||
async def test_drain_injections_extracts_content_from_inbound_messages():
|
||||
"""Should extract .content from InboundMessage objects."""
|
||||
from nanobot.agent.runner import AgentRunner, AgentRunSpec
|
||||
from nanobot.agent.runner import AgentRunner
|
||||
from nanobot.bus.events import InboundMessage
|
||||
|
||||
provider = MagicMock()
|
||||
runner = AgentRunner(provider)
|
||||
runner = AgentRunner()
|
||||
tools = MagicMock()
|
||||
tools.get_definitions.return_value = []
|
||||
|
||||
@@ -76,7 +77,7 @@ async def test_drain_injections_extracts_content_from_inbound_messages():
|
||||
async def cb():
|
||||
return msgs
|
||||
|
||||
spec = AgentRunSpec(
|
||||
spec = make_run_spec(provider,
|
||||
initial_messages=[], tools=tools, model="m",
|
||||
max_iterations=1, max_tool_result_chars=1000,
|
||||
injection_callback=cb,
|
||||
@@ -91,11 +92,11 @@ async def test_drain_injections_extracts_content_from_inbound_messages():
|
||||
@pytest.mark.asyncio
|
||||
async def test_drain_injections_passes_limit_to_callback_when_supported():
|
||||
"""Limit-aware callbacks can preserve overflow in their own queue."""
|
||||
from nanobot.agent.runner import _MAX_INJECTIONS_PER_TURN, AgentRunner, AgentRunSpec
|
||||
from nanobot.agent.runner import _MAX_INJECTIONS_PER_TURN, AgentRunner
|
||||
from nanobot.bus.events import InboundMessage
|
||||
|
||||
provider = MagicMock()
|
||||
runner = AgentRunner(provider)
|
||||
runner = AgentRunner()
|
||||
tools = MagicMock()
|
||||
tools.get_definitions.return_value = []
|
||||
seen_limits: list[int] = []
|
||||
@@ -109,7 +110,7 @@ async def test_drain_injections_passes_limit_to_callback_when_supported():
|
||||
seen_limits.append(limit)
|
||||
return msgs[:limit]
|
||||
|
||||
spec = AgentRunSpec(
|
||||
spec = make_run_spec(provider,
|
||||
initial_messages=[], tools=tools, model="m",
|
||||
max_iterations=1, max_tool_result_chars=1000,
|
||||
injection_callback=cb,
|
||||
@@ -126,11 +127,11 @@ async def test_drain_injections_passes_limit_to_callback_when_supported():
|
||||
@pytest.mark.asyncio
|
||||
async def test_drain_injections_skips_empty_content():
|
||||
"""Messages with blank content should be filtered out."""
|
||||
from nanobot.agent.runner import AgentRunner, AgentRunSpec
|
||||
from nanobot.agent.runner import AgentRunner
|
||||
from nanobot.bus.events import InboundMessage
|
||||
|
||||
provider = MagicMock()
|
||||
runner = AgentRunner(provider)
|
||||
runner = AgentRunner()
|
||||
tools = MagicMock()
|
||||
tools.get_definitions.return_value = []
|
||||
|
||||
@@ -143,7 +144,7 @@ async def test_drain_injections_skips_empty_content():
|
||||
async def cb():
|
||||
return msgs
|
||||
|
||||
spec = AgentRunSpec(
|
||||
spec = make_run_spec(provider,
|
||||
initial_messages=[], tools=tools, model="m",
|
||||
max_iterations=1, max_tool_result_chars=1000,
|
||||
injection_callback=cb,
|
||||
@@ -155,10 +156,10 @@ async def test_drain_injections_skips_empty_content():
|
||||
@pytest.mark.asyncio
|
||||
async def test_drain_injections_filters_empty_dict_payloads():
|
||||
"""Pre-normalized dict injections should obey the same empty-content guard."""
|
||||
from nanobot.agent.runner import AgentRunner, AgentRunSpec
|
||||
from nanobot.agent.runner import AgentRunner
|
||||
|
||||
provider = MagicMock()
|
||||
runner = AgentRunner(provider)
|
||||
runner = AgentRunner()
|
||||
tools = MagicMock()
|
||||
tools.get_definitions.return_value = []
|
||||
|
||||
@@ -176,7 +177,7 @@ async def test_drain_injections_filters_empty_dict_payloads():
|
||||
async def cb():
|
||||
return msgs
|
||||
|
||||
spec = AgentRunSpec(
|
||||
spec = make_run_spec(provider,
|
||||
initial_messages=[], tools=tools, model="m",
|
||||
max_iterations=1, max_tool_result_chars=1000,
|
||||
injection_callback=cb,
|
||||
@@ -193,10 +194,10 @@ async def test_drain_injections_skips_objects_with_none_content():
|
||||
"""Objects exposing content=None should be skipped rather than stringified."""
|
||||
from types import SimpleNamespace
|
||||
|
||||
from nanobot.agent.runner import AgentRunner, AgentRunSpec
|
||||
from nanobot.agent.runner import AgentRunner
|
||||
|
||||
provider = MagicMock()
|
||||
runner = AgentRunner(provider)
|
||||
runner = AgentRunner()
|
||||
tools = MagicMock()
|
||||
tools.get_definitions.return_value = []
|
||||
|
||||
@@ -207,7 +208,7 @@ async def test_drain_injections_skips_objects_with_none_content():
|
||||
SimpleNamespace(content="valid"),
|
||||
]
|
||||
|
||||
spec = AgentRunSpec(
|
||||
spec = make_run_spec(provider,
|
||||
initial_messages=[], tools=tools, model="m",
|
||||
max_iterations=1, max_tool_result_chars=1000,
|
||||
injection_callback=cb,
|
||||
@@ -219,17 +220,17 @@ async def test_drain_injections_skips_objects_with_none_content():
|
||||
@pytest.mark.asyncio
|
||||
async def test_drain_injections_handles_callback_exception():
|
||||
"""If the callback raises, return empty list (error is logged)."""
|
||||
from nanobot.agent.runner import AgentRunner, AgentRunSpec
|
||||
from nanobot.agent.runner import AgentRunner
|
||||
|
||||
provider = MagicMock()
|
||||
runner = AgentRunner(provider)
|
||||
runner = AgentRunner()
|
||||
tools = MagicMock()
|
||||
tools.get_definitions.return_value = []
|
||||
|
||||
async def cb():
|
||||
raise RuntimeError("boom")
|
||||
|
||||
spec = AgentRunSpec(
|
||||
spec = make_run_spec(provider,
|
||||
initial_messages=[], tools=tools, model="m",
|
||||
max_iterations=1, max_tool_result_chars=1000,
|
||||
injection_callback=cb,
|
||||
@@ -241,7 +242,7 @@ async def test_drain_injections_handles_callback_exception():
|
||||
@pytest.mark.asyncio
|
||||
async def test_checkpoint1_injects_after_tool_execution():
|
||||
"""Follow-up messages are injected after tool execution, before next LLM call."""
|
||||
from nanobot.agent.runner import AgentRunner, AgentRunSpec
|
||||
from nanobot.agent.runner import AgentRunner
|
||||
from nanobot.bus.events import InboundMessage
|
||||
|
||||
provider = MagicMock()
|
||||
@@ -272,8 +273,8 @@ async def test_checkpoint1_injects_after_tool_execution():
|
||||
InboundMessage(channel="cli", sender_id="u", chat_id="c", content="follow-up question")
|
||||
)
|
||||
|
||||
runner = AgentRunner(provider)
|
||||
result = await runner.run(AgentRunSpec(
|
||||
runner = AgentRunner()
|
||||
result = await runner.run(make_run_spec(provider,
|
||||
initial_messages=[{"role": "user", "content": "hello"}],
|
||||
tools=tools,
|
||||
model="test-model",
|
||||
@@ -295,7 +296,7 @@ async def test_checkpoint1_injects_after_tool_execution():
|
||||
async def test_checkpoint2_injects_after_final_response_with_resuming_stream():
|
||||
"""After final response, if injections exist, stream_end should get resuming=True."""
|
||||
from nanobot.agent.hook import AgentHook, AgentHookContext
|
||||
from nanobot.agent.runner import AgentRunner, AgentRunSpec
|
||||
from nanobot.agent.runner import AgentRunner
|
||||
from nanobot.bus.events import InboundMessage
|
||||
|
||||
provider = MagicMock()
|
||||
@@ -330,8 +331,8 @@ async def test_checkpoint2_injects_after_final_response_with_resuming_stream():
|
||||
InboundMessage(channel="cli", sender_id="u", chat_id="c", content="quick follow-up")
|
||||
)
|
||||
|
||||
runner = AgentRunner(provider)
|
||||
result = await runner.run(AgentRunSpec(
|
||||
runner = AgentRunner()
|
||||
result = await runner.run(make_run_spec(provider,
|
||||
initial_messages=[{"role": "user", "content": "hello"}],
|
||||
tools=tools,
|
||||
model="test-model",
|
||||
@@ -353,7 +354,7 @@ async def test_checkpoint2_injects_after_final_response_with_resuming_stream():
|
||||
@pytest.mark.asyncio
|
||||
async def test_checkpoint2_preserves_final_response_in_history_before_followup():
|
||||
"""A follow-up injected after a final answer must still see that answer in history."""
|
||||
from nanobot.agent.runner import AgentRunner, AgentRunSpec
|
||||
from nanobot.agent.runner import AgentRunner
|
||||
from nanobot.bus.events import InboundMessage
|
||||
|
||||
provider = MagicMock()
|
||||
@@ -378,8 +379,8 @@ async def test_checkpoint2_preserves_final_response_in_history_before_followup()
|
||||
InboundMessage(channel="cli", sender_id="u", chat_id="c", content="follow-up question")
|
||||
)
|
||||
|
||||
runner = AgentRunner(provider)
|
||||
result = await runner.run(AgentRunSpec(
|
||||
runner = AgentRunner()
|
||||
result = await runner.run(make_run_spec(provider,
|
||||
initial_messages=[{"role": "user", "content": "hello"}],
|
||||
tools=tools,
|
||||
model="test-model",
|
||||
@@ -530,7 +531,7 @@ async def test_subagent_pending_injection_is_hidden_history_and_not_merged(tmp_p
|
||||
@pytest.mark.asyncio
|
||||
async def test_runner_merges_multiple_injected_user_messages_without_losing_media():
|
||||
"""Multiple injected follow-ups should not create lossy consecutive user messages."""
|
||||
from nanobot.agent.runner import AgentRunner, AgentRunSpec
|
||||
from nanobot.agent.runner import AgentRunner
|
||||
|
||||
provider = MagicMock()
|
||||
call_count = {"n": 0}
|
||||
@@ -561,8 +562,8 @@ async def test_runner_merges_multiple_injected_user_messages_without_losing_medi
|
||||
]
|
||||
return []
|
||||
|
||||
runner = AgentRunner(provider)
|
||||
result = await runner.run(AgentRunSpec(
|
||||
runner = AgentRunner()
|
||||
result = await runner.run(make_run_spec(provider,
|
||||
initial_messages=[{"role": "user", "content": "hello"}],
|
||||
tools=tools,
|
||||
model="test-model",
|
||||
@@ -593,7 +594,7 @@ async def test_runner_merges_multiple_injected_user_messages_without_losing_medi
|
||||
@pytest.mark.asyncio
|
||||
async def test_injection_cycles_capped_at_max():
|
||||
"""Injection cycles should be capped at _MAX_INJECTION_CYCLES."""
|
||||
from nanobot.agent.runner import _MAX_INJECTION_CYCLES, AgentRunner, AgentRunSpec
|
||||
from nanobot.agent.runner import _MAX_INJECTION_CYCLES, AgentRunner
|
||||
from nanobot.bus.events import InboundMessage
|
||||
|
||||
provider = MagicMock()
|
||||
@@ -616,8 +617,8 @@ async def test_injection_cycles_capped_at_max():
|
||||
return [InboundMessage(channel="cli", sender_id="u", chat_id="c", content=f"msg-{drain_count['n']}")]
|
||||
return []
|
||||
|
||||
runner = AgentRunner(provider)
|
||||
result = await runner.run(AgentRunSpec(
|
||||
runner = AgentRunner()
|
||||
result = await runner.run(make_run_spec(provider,
|
||||
initial_messages=[{"role": "user", "content": "start"}],
|
||||
tools=tools,
|
||||
model="test-model",
|
||||
@@ -634,7 +635,7 @@ async def test_injection_cycles_capped_at_max():
|
||||
@pytest.mark.asyncio
|
||||
async def test_no_injections_flag_is_false_by_default():
|
||||
"""had_injections should be False when no injection callback or no messages."""
|
||||
from nanobot.agent.runner import AgentRunner, AgentRunSpec
|
||||
from nanobot.agent.runner import AgentRunner
|
||||
|
||||
provider = MagicMock()
|
||||
|
||||
@@ -645,8 +646,8 @@ async def test_no_injections_flag_is_false_by_default():
|
||||
tools = MagicMock()
|
||||
tools.get_definitions.return_value = []
|
||||
|
||||
runner = AgentRunner(provider)
|
||||
result = await runner.run(AgentRunSpec(
|
||||
runner = AgentRunner()
|
||||
result = await runner.run(make_run_spec(provider,
|
||||
initial_messages=[{"role": "user", "content": "hi"}],
|
||||
tools=tools,
|
||||
model="test-model",
|
||||
@@ -1089,7 +1090,7 @@ async def test_dispatch_republishes_leftover_queue_messages(tmp_path):
|
||||
@pytest.mark.asyncio
|
||||
async def test_drain_injections_on_fatal_tool_error():
|
||||
"""Pending injections should be drained even when a fatal tool error occurs."""
|
||||
from nanobot.agent.runner import AgentRunner, AgentRunSpec
|
||||
from nanobot.agent.runner import AgentRunner
|
||||
from nanobot.bus.events import InboundMessage
|
||||
|
||||
provider = MagicMock()
|
||||
@@ -1118,8 +1119,8 @@ async def test_drain_injections_on_fatal_tool_error():
|
||||
InboundMessage(channel="cli", sender_id="u", chat_id="c", content="follow-up after error")
|
||||
)
|
||||
|
||||
runner = AgentRunner(provider)
|
||||
result = await runner.run(AgentRunSpec(
|
||||
runner = AgentRunner()
|
||||
result = await runner.run(make_run_spec(provider,
|
||||
initial_messages=[{"role": "user", "content": "hello"}],
|
||||
tools=tools,
|
||||
model="test-model",
|
||||
@@ -1142,7 +1143,7 @@ async def test_drain_injections_on_fatal_tool_error():
|
||||
@pytest.mark.asyncio
|
||||
async def test_drain_injections_on_llm_error():
|
||||
"""Pending injections should be drained when the LLM returns an error finish_reason."""
|
||||
from nanobot.agent.runner import AgentRunner, AgentRunSpec
|
||||
from nanobot.agent.runner import AgentRunner
|
||||
from nanobot.bus.events import InboundMessage
|
||||
|
||||
provider = MagicMock()
|
||||
@@ -1171,8 +1172,8 @@ async def test_drain_injections_on_llm_error():
|
||||
InboundMessage(channel="cli", sender_id="u", chat_id="c", content="follow-up after LLM error")
|
||||
)
|
||||
|
||||
runner = AgentRunner(provider)
|
||||
result = await runner.run(AgentRunSpec(
|
||||
runner = AgentRunner()
|
||||
result = await runner.run(make_run_spec(provider,
|
||||
initial_messages=[
|
||||
{"role": "user", "content": "hello"},
|
||||
{"role": "assistant", "content": "previous response"},
|
||||
@@ -1197,7 +1198,7 @@ async def test_drain_injections_on_llm_error():
|
||||
@pytest.mark.asyncio
|
||||
async def test_drain_injections_on_empty_final_response():
|
||||
"""Pending injections should be drained when the runner exits due to empty response."""
|
||||
from nanobot.agent.runner import _MAX_EMPTY_RETRIES, AgentRunner, AgentRunSpec
|
||||
from nanobot.agent.runner import _MAX_EMPTY_RETRIES, AgentRunner
|
||||
from nanobot.bus.events import InboundMessage
|
||||
|
||||
provider = MagicMock()
|
||||
@@ -1221,8 +1222,8 @@ async def test_drain_injections_on_empty_final_response():
|
||||
InboundMessage(channel="cli", sender_id="u", chat_id="c", content="follow-up after empty")
|
||||
)
|
||||
|
||||
runner = AgentRunner(provider)
|
||||
result = await runner.run(AgentRunSpec(
|
||||
runner = AgentRunner()
|
||||
result = await runner.run(make_run_spec(provider,
|
||||
initial_messages=[
|
||||
{"role": "user", "content": "hello"},
|
||||
{"role": "assistant", "content": "previous response"},
|
||||
@@ -1252,7 +1253,7 @@ async def test_drain_injections_on_max_iterations():
|
||||
injections are appended to messages but not processed by the LLM.
|
||||
The key point is they are consumed from the queue to prevent re-publish.
|
||||
"""
|
||||
from nanobot.agent.runner import AgentRunner, AgentRunSpec
|
||||
from nanobot.agent.runner import AgentRunner
|
||||
from nanobot.bus.events import InboundMessage
|
||||
|
||||
provider = MagicMock()
|
||||
@@ -1278,8 +1279,8 @@ async def test_drain_injections_on_max_iterations():
|
||||
InboundMessage(channel="cli", sender_id="u", chat_id="c", content="follow-up after max iters")
|
||||
)
|
||||
|
||||
runner = AgentRunner(provider)
|
||||
result = await runner.run(AgentRunSpec(
|
||||
runner = AgentRunner()
|
||||
result = await runner.run(make_run_spec(provider,
|
||||
initial_messages=[{"role": "user", "content": "hello"}],
|
||||
tools=tools,
|
||||
model="test-model",
|
||||
@@ -1304,7 +1305,7 @@ async def test_drain_injections_on_max_iterations():
|
||||
async def test_drain_injections_set_flag_when_followup_arrives_after_last_iteration():
|
||||
"""Late follow-ups drained in max_iterations should still flip had_injections."""
|
||||
from nanobot.agent.hook import AgentHook
|
||||
from nanobot.agent.runner import AgentRunner, AgentRunSpec
|
||||
from nanobot.agent.runner import AgentRunner
|
||||
from nanobot.bus.events import InboundMessage
|
||||
|
||||
provider = MagicMock()
|
||||
@@ -1342,8 +1343,8 @@ async def test_drain_injections_set_flag_when_followup_arrives_after_last_iterat
|
||||
)
|
||||
)
|
||||
|
||||
runner = AgentRunner(provider)
|
||||
result = await runner.run(AgentRunSpec(
|
||||
runner = AgentRunner()
|
||||
result = await runner.run(make_run_spec(provider,
|
||||
initial_messages=[{"role": "user", "content": "hello"}],
|
||||
tools=tools,
|
||||
model="test-model",
|
||||
@@ -1366,7 +1367,7 @@ async def test_drain_injections_set_flag_when_followup_arrives_after_last_iterat
|
||||
@pytest.mark.asyncio
|
||||
async def test_injection_cycle_cap_on_error_path():
|
||||
"""Injection cycles should be capped even when every iteration hits an LLM error."""
|
||||
from nanobot.agent.runner import _MAX_INJECTION_CYCLES, AgentRunner, AgentRunSpec
|
||||
from nanobot.agent.runner import _MAX_INJECTION_CYCLES, AgentRunner
|
||||
from nanobot.bus.events import InboundMessage
|
||||
|
||||
provider = MagicMock()
|
||||
@@ -1393,8 +1394,8 @@ async def test_injection_cycle_cap_on_error_path():
|
||||
return [InboundMessage(channel="cli", sender_id="u", chat_id="c", content=f"msg-{drain_count['n']}")]
|
||||
return []
|
||||
|
||||
runner = AgentRunner(provider)
|
||||
result = await runner.run(AgentRunSpec(
|
||||
runner = AgentRunner()
|
||||
result = await runner.run(make_run_spec(provider,
|
||||
initial_messages=[
|
||||
{"role": "user", "content": "hello"},
|
||||
{"role": "assistant", "content": "previous"},
|
||||
|
||||
Reference in New Issue
Block a user