diff --git a/nanobot/agent/loop.py b/nanobot/agent/loop.py index 718ea8bfa..a26cb3272 100644 --- a/nanobot/agent/loop.py +++ b/nanobot/agent/loop.py @@ -341,6 +341,46 @@ class AgentLoop: self.commands = CommandRouter() register_builtin_commands(self.commands) + @classmethod + def from_config( + cls, + config: Any, + bus: MessageBus | None = None, + **extra: Any, + ) -> AgentLoop: + """Create an AgentLoop from config with the common parameter set.""" + from nanobot.providers.factory import make_provider + + if bus is None: + bus = MessageBus() + defaults = config.agents.defaults + provider = make_provider(config) + return cls( + bus=bus, + provider=provider, + workspace=config.workspace_path, + model=defaults.model, + max_iterations=defaults.max_tool_iterations, + context_window_tokens=defaults.context_window_tokens, + context_block_limit=defaults.context_block_limit, + max_tool_result_chars=defaults.max_tool_result_chars, + provider_retry_mode=defaults.provider_retry_mode, + tool_hint_max_length=defaults.tool_hint_max_length, + web_config=config.tools.web, + exec_config=config.tools.exec, + restrict_to_workspace=config.tools.restrict_to_workspace, + mcp_servers=config.tools.mcp_servers, + channels_config=config.channels, + timezone=defaults.timezone, + unified_session=defaults.unified_session, + disabled_skills=defaults.disabled_skills, + session_ttl_minutes=defaults.session_ttl_minutes, + consolidation_ratio=defaults.consolidation_ratio, + max_messages=defaults.max_messages, + tools_config=config.tools, + **extra, + ) + def _sync_subagent_runtime_limits(self) -> None: """Keep subagent runtime limits aligned with mutable loop settings.""" self.subagents.max_iterations = self.max_iterations diff --git a/nanobot/cli/commands.py b/nanobot/cli/commands.py index 243280ed1..1a7e05c88 100644 --- a/nanobot/cli/commands.py +++ b/nanobot/cli/commands.py @@ -48,6 +48,7 @@ from rich.table import Table from rich.text import Text from nanobot import __logo__, __version__ +from nanobot.agent.loop import AgentLoop def _sanitize_surrogates(text: str) -> str: @@ -447,20 +448,6 @@ def _onboard_plugins(config_path: Path) -> None: json.dump(data, f, indent=2, ensure_ascii=False) -def _make_provider(config: Config): - """Create the appropriate LLM provider from config. - - Routing is driven by ``ProviderSpec.backend`` in the registry. - """ - from nanobot.providers.factory import make_provider - - try: - return make_provider(config) - except ValueError as exc: - console.print(f"[red]Error: {exc}[/red]") - raise typer.Exit(1) from exc - - def _load_runtime_config(config: str | None = None, workspace: str | None = None) -> Config: """Load config and optionally override the active workspace.""" from nanobot.config.loader import load_config, resolve_config_env_vars, set_config_path @@ -539,7 +526,6 @@ def serve( from loguru import logger - from nanobot.agent.loop import AgentLoop from nanobot.api.server import create_app from nanobot.bus.queue import MessageBus from nanobot.session.manager import SessionManager @@ -556,32 +542,10 @@ def serve( timeout = timeout if timeout is not None else api_cfg.timeout sync_workspace_templates(runtime_config.workspace_path) bus = MessageBus() - provider = _make_provider(runtime_config) session_manager = SessionManager(runtime_config.workspace_path) - agent_loop = AgentLoop( - bus=bus, - provider=provider, - workspace=runtime_config.workspace_path, - model=runtime_config.agents.defaults.model, - max_iterations=runtime_config.agents.defaults.max_tool_iterations, - context_window_tokens=runtime_config.agents.defaults.context_window_tokens, - context_block_limit=runtime_config.agents.defaults.context_block_limit, - max_tool_result_chars=runtime_config.agents.defaults.max_tool_result_chars, - provider_retry_mode=runtime_config.agents.defaults.provider_retry_mode, - tool_hint_max_length=runtime_config.agents.defaults.tool_hint_max_length, - web_config=runtime_config.tools.web, - exec_config=runtime_config.tools.exec, - restrict_to_workspace=runtime_config.tools.restrict_to_workspace, + agent_loop = AgentLoop.from_config( + runtime_config, bus, session_manager=session_manager, - mcp_servers=runtime_config.tools.mcp_servers, - channels_config=runtime_config.channels, - timezone=runtime_config.agents.defaults.timezone, - unified_session=runtime_config.agents.defaults.unified_session, - disabled_skills=runtime_config.agents.defaults.disabled_skills, - session_ttl_minutes=runtime_config.agents.defaults.session_ttl_minutes, - consolidation_ratio=runtime_config.agents.defaults.consolidation_ratio, - max_messages=runtime_config.agents.defaults.max_messages, - tools_config=runtime_config.tools, image_generation_provider_configs={ "openrouter": runtime_config.providers.openrouter, "aihubmix": runtime_config.providers.aihubmix, @@ -653,7 +617,6 @@ def _run_gateway( open_browser_url: str | None = None, ) -> None: """Shared gateway runtime; ``open_browser_url`` opens a tab once channels are up.""" - from nanobot.agent.loop import AgentLoop from nanobot.agent.tools.cron import CronTool from nanobot.agent.tools.message import MessageTool from nanobot.bus.queue import MessageBus @@ -674,7 +637,6 @@ def _run_gateway( except ValueError as exc: console.print(f"[red]Error: {exc}[/red]") raise typer.Exit(1) from exc - provider = provider_snapshot.provider session_manager = SessionManager(config.workspace_path) # Preserve existing single-workspace installs, but keep custom workspaces clean. @@ -686,31 +648,10 @@ def _run_gateway( cron = CronService(cron_store_path) # Create agent with cron service - agent = AgentLoop( - bus=bus, - provider=provider, - workspace=config.workspace_path, - model=provider_snapshot.model, - max_iterations=config.agents.defaults.max_tool_iterations, - context_window_tokens=provider_snapshot.context_window_tokens, - web_config=config.tools.web, - context_block_limit=config.agents.defaults.context_block_limit, - max_tool_result_chars=config.agents.defaults.max_tool_result_chars, - provider_retry_mode=config.agents.defaults.provider_retry_mode, - tool_hint_max_length=config.agents.defaults.tool_hint_max_length, - exec_config=config.tools.exec, + agent = AgentLoop.from_config( + config, bus, cron_service=cron, - restrict_to_workspace=config.tools.restrict_to_workspace, session_manager=session_manager, - mcp_servers=config.tools.mcp_servers, - channels_config=config.channels, - timezone=config.agents.defaults.timezone, - unified_session=config.agents.defaults.unified_session, - disabled_skills=config.agents.defaults.disabled_skills, - session_ttl_minutes=config.agents.defaults.session_ttl_minutes, - consolidation_ratio=config.agents.defaults.consolidation_ratio, - max_messages=config.agents.defaults.max_messages, - tools_config=config.tools, image_generation_provider_configs={ "openrouter": config.providers.openrouter, "aihubmix": config.providers.aihubmix, @@ -820,7 +761,7 @@ def _run_gateway( if job.payload.deliver and job.payload.to and response: should_notify = await evaluate_response( - response, reminder_note, provider, agent.model, + response, reminder_note, agent.provider, agent.model, ) if should_notify: await _deliver_to_channel( @@ -910,7 +851,7 @@ def _run_gateway( hb_cfg = config.gateway.heartbeat heartbeat = HeartbeatService( workspace=config.workspace_path, - provider=provider, + provider=agent.provider, model=agent.model, on_execute=on_heartbeat_execute, on_notify=on_heartbeat_notify, @@ -1063,7 +1004,6 @@ def agent( """Interact with the agent directly.""" from loguru import logger - from nanobot.agent.loop import AgentLoop from nanobot.bus.queue import MessageBus from nanobot.cron.service import CronService @@ -1071,7 +1011,6 @@ def agent( sync_workspace_templates(config.workspace_path) bus = MessageBus() - provider = _make_provider(config) # Preserve existing single-workspace installs, but keep custom workspaces clean. if is_default_workspace(config.workspace_path): @@ -1086,30 +1025,9 @@ def agent( else: logger.disable("nanobot") - agent_loop = AgentLoop( - bus=bus, - provider=provider, - workspace=config.workspace_path, - model=config.agents.defaults.model, - max_iterations=config.agents.defaults.max_tool_iterations, - context_window_tokens=config.agents.defaults.context_window_tokens, - web_config=config.tools.web, - context_block_limit=config.agents.defaults.context_block_limit, - max_tool_result_chars=config.agents.defaults.max_tool_result_chars, - provider_retry_mode=config.agents.defaults.provider_retry_mode, - tool_hint_max_length=config.agents.defaults.tool_hint_max_length, - exec_config=config.tools.exec, + agent_loop = AgentLoop.from_config( + config, bus, cron_service=cron, - restrict_to_workspace=config.tools.restrict_to_workspace, - mcp_servers=config.tools.mcp_servers, - channels_config=config.channels, - timezone=config.agents.defaults.timezone, - unified_session=config.agents.defaults.unified_session, - disabled_skills=config.agents.defaults.disabled_skills, - session_ttl_minutes=config.agents.defaults.session_ttl_minutes, - consolidation_ratio=config.agents.defaults.consolidation_ratio, - max_messages=config.agents.defaults.max_messages, - tools_config=config.tools, ) restart_notice = consume_restart_notice_from_env() if restart_notice and should_show_cli_restart_notice(restart_notice, session_id): diff --git a/nanobot/nanobot.py b/nanobot/nanobot.py index 77decc563..bfedb7611 100644 --- a/nanobot/nanobot.py +++ b/nanobot/nanobot.py @@ -8,7 +8,6 @@ from typing import Any from nanobot.agent.hook import AgentHook, SDKCaptureHook from nanobot.agent.loop import AgentLoop -from nanobot.bus.queue import MessageBus @dataclass(slots=True) @@ -62,31 +61,8 @@ class Nanobot: Path(workspace).expanduser().resolve() ) - provider = _make_provider(config) - bus = MessageBus() - defaults = config.agents.defaults - - loop = AgentLoop( - bus=bus, - provider=provider, - workspace=config.workspace_path, - model=defaults.model, - max_iterations=defaults.max_tool_iterations, - context_window_tokens=defaults.context_window_tokens, - context_block_limit=defaults.context_block_limit, - max_tool_result_chars=defaults.max_tool_result_chars, - provider_retry_mode=defaults.provider_retry_mode, - tool_hint_max_length=defaults.tool_hint_max_length, - web_config=config.tools.web, - exec_config=config.tools.exec, - restrict_to_workspace=config.tools.restrict_to_workspace, - mcp_servers=config.tools.mcp_servers, - timezone=defaults.timezone, - unified_session=defaults.unified_session, - disabled_skills=defaults.disabled_skills, - session_ttl_minutes=defaults.session_ttl_minutes, - consolidation_ratio=defaults.consolidation_ratio, - tools_config=config.tools, + loop = AgentLoop.from_config( + config, image_generation_provider_configs={ "openrouter": config.providers.openrouter, "aihubmix": config.providers.aihubmix, @@ -128,8 +104,3 @@ class Nanobot: ) -def _make_provider(config: Any) -> Any: - """Create the LLM provider from config (extracted from CLI).""" - from nanobot.providers.factory import make_provider - - return make_provider(config) diff --git a/tests/cli/test_commands.py b/tests/cli/test_commands.py index d217c5f03..28813fb8f 100644 --- a/tests/cli/test_commands.py +++ b/tests/cli/test_commands.py @@ -9,7 +9,8 @@ import pytest from typer.testing import CliRunner from nanobot.bus.events import OutboundMessage -from nanobot.cli.commands import _make_provider, app +from nanobot.cli.commands import app +from nanobot.providers.factory import make_provider from nanobot.config.schema import Config from nanobot.cron.types import CronJob, CronPayload from nanobot.providers.factory import ProviderSnapshot @@ -19,6 +20,13 @@ from nanobot.providers.registry import find_by_name runner = CliRunner() +def _fake_provider(): + """Return a minimal fake provider that satisfies AgentLoop.__init__.""" + p = MagicMock() + p.generation.max_tokens = 4096 + return p + + class _StopGatewayError(RuntimeError): pass @@ -488,7 +496,7 @@ def test_openai_compat_provider_passes_model_through(): def test_make_provider_uses_github_copilot_backend(): - from nanobot.cli.commands import _make_provider + from nanobot.providers.factory import make_provider from nanobot.config.schema import Config config = Config.model_validate( @@ -503,7 +511,7 @@ def test_make_provider_uses_github_copilot_backend(): ) with patch("nanobot.providers.openai_compat_provider.AsyncOpenAI"): - provider = _make_provider(config) + provider = make_provider(config) assert provider.__class__.__name__ == "GitHubCopilotProvider" @@ -579,7 +587,7 @@ def test_make_provider_passes_extra_headers_to_custom_provider(): ) with patch("nanobot.providers.openai_compat_provider.AsyncOpenAI") as mock_async_openai: - _make_provider(config) + make_provider(config) kwargs = mock_async_openai.call_args.kwargs assert kwargs["api_key"] == "test-key" @@ -597,24 +605,24 @@ def mock_agent_runtime(tmp_path): with patch("nanobot.config.loader.load_config", return_value=config) as mock_load_config, \ patch("nanobot.config.loader.resolve_config_env_vars", side_effect=lambda c: c), \ patch("nanobot.cli.commands.sync_workspace_templates") as mock_sync_templates, \ - patch("nanobot.cli.commands._make_provider", return_value=object()), \ + patch("nanobot.providers.factory.make_provider", return_value=_fake_provider()), \ patch("nanobot.cli.commands._print_agent_response") as mock_print_response, \ patch("nanobot.bus.queue.MessageBus"), \ patch("nanobot.cron.service.CronService"), \ - patch("nanobot.agent.loop.AgentLoop") as mock_agent_loop_cls: + patch("nanobot.cli.commands.AgentLoop.from_config") as mock_from_config: agent_loop = MagicMock() agent_loop.channels_config = None agent_loop.process_direct = AsyncMock( return_value=OutboundMessage(channel="cli", chat_id="direct", content="mock-response"), ) agent_loop.close_mcp = AsyncMock(return_value=None) - mock_agent_loop_cls.return_value = agent_loop + mock_from_config.return_value = agent_loop yield { "config": config, "load_config": mock_load_config, "sync_templates": mock_sync_templates, - "agent_loop_cls": mock_agent_loop_cls, + "from_config": mock_from_config, "agent_loop": agent_loop, "print_response": mock_print_response, } @@ -639,9 +647,8 @@ def test_agent_uses_default_config_when_no_workspace_or_config_flags(mock_agent_ assert mock_agent_runtime["sync_templates"].call_args.args == ( mock_agent_runtime["config"].workspace_path, ) - assert mock_agent_runtime["agent_loop_cls"].call_args.kwargs["workspace"] == ( - mock_agent_runtime["config"].workspace_path - ) + passed_config = mock_agent_runtime["from_config"].call_args.args[0] + assert passed_config.workspace_path == mock_agent_runtime["config"].workspace_path mock_agent_runtime["agent_loop"].process_direct.assert_awaited_once() mock_agent_runtime["print_response"].assert_called_once_with( "mock-response", render_markdown=True, metadata={}, @@ -672,11 +679,14 @@ def test_agent_config_sets_active_path(monkeypatch, tmp_path: Path) -> None: ) monkeypatch.setattr("nanobot.config.loader.load_config", lambda _path=None: config) monkeypatch.setattr("nanobot.cli.commands.sync_workspace_templates", lambda _path: None) - monkeypatch.setattr("nanobot.cli.commands._make_provider", lambda _config: object()) + monkeypatch.setattr("nanobot.providers.factory.make_provider", lambda _config: _fake_provider()) monkeypatch.setattr("nanobot.bus.queue.MessageBus", lambda: object()) monkeypatch.setattr("nanobot.cron.service.CronService", lambda _store: object()) class _FakeAgentLoop: + @classmethod + def from_config(cls, config, bus=None, **extra): + return cls(**extra) def __init__(self, *args, **kwargs) -> None: pass @@ -686,7 +696,7 @@ def test_agent_config_sets_active_path(monkeypatch, tmp_path: Path) -> None: async def close_mcp(self) -> None: return None - monkeypatch.setattr("nanobot.agent.loop.AgentLoop", _FakeAgentLoop) + monkeypatch.setattr("nanobot.cli.commands.AgentLoop", _FakeAgentLoop) monkeypatch.setattr("nanobot.cli.commands._print_agent_response", lambda *_args, **_kwargs: None) result = runner.invoke(app, ["agent", "-m", "hello", "-c", str(config_file)]) @@ -707,7 +717,7 @@ def test_agent_uses_workspace_directory_for_cron_store(monkeypatch, tmp_path: Pa monkeypatch.setattr("nanobot.config.loader.set_config_path", lambda _path: None) monkeypatch.setattr("nanobot.config.loader.load_config", lambda _path=None: config) monkeypatch.setattr("nanobot.cli.commands.sync_workspace_templates", lambda _path: None) - monkeypatch.setattr("nanobot.cli.commands._make_provider", lambda _config: object()) + monkeypatch.setattr("nanobot.providers.factory.make_provider", lambda _config: _fake_provider()) monkeypatch.setattr("nanobot.bus.queue.MessageBus", lambda: object()) class _FakeCron: @@ -715,6 +725,9 @@ def test_agent_uses_workspace_directory_for_cron_store(monkeypatch, tmp_path: Pa seen["cron_store"] = store_path class _FakeAgentLoop: + @classmethod + def from_config(cls, config, bus=None, **extra): + return cls(**extra) def __init__(self, *args, **kwargs) -> None: pass @@ -725,7 +738,7 @@ def test_agent_uses_workspace_directory_for_cron_store(monkeypatch, tmp_path: Pa return None monkeypatch.setattr("nanobot.cron.service.CronService", _FakeCron) - monkeypatch.setattr("nanobot.agent.loop.AgentLoop", _FakeAgentLoop) + monkeypatch.setattr("nanobot.cli.commands.AgentLoop", _FakeAgentLoop) monkeypatch.setattr("nanobot.cli.commands._print_agent_response", lambda *_args, **_kwargs: None) result = runner.invoke(app, ["agent", "-m", "hello", "-c", str(config_file)]) @@ -753,7 +766,7 @@ def test_agent_workspace_override_does_not_migrate_legacy_cron( monkeypatch.setattr("nanobot.config.loader.set_config_path", lambda _path: None) monkeypatch.setattr("nanobot.config.loader.load_config", lambda _path=None: config) monkeypatch.setattr("nanobot.cli.commands.sync_workspace_templates", lambda _path: None) - monkeypatch.setattr("nanobot.cli.commands._make_provider", lambda _config: object()) + monkeypatch.setattr("nanobot.providers.factory.make_provider", lambda _config: _fake_provider()) monkeypatch.setattr("nanobot.bus.queue.MessageBus", lambda: object()) monkeypatch.setattr("nanobot.config.paths.get_cron_dir", lambda: legacy_dir) @@ -762,6 +775,9 @@ def test_agent_workspace_override_does_not_migrate_legacy_cron( seen["cron_store"] = store_path class _FakeAgentLoop: + @classmethod + def from_config(cls, config, bus=None, **extra): + return cls(**extra) def __init__(self, *args, **kwargs) -> None: pass @@ -772,7 +788,7 @@ def test_agent_workspace_override_does_not_migrate_legacy_cron( return None monkeypatch.setattr("nanobot.cron.service.CronService", _FakeCron) - monkeypatch.setattr("nanobot.agent.loop.AgentLoop", _FakeAgentLoop) + monkeypatch.setattr("nanobot.cli.commands.AgentLoop", _FakeAgentLoop) monkeypatch.setattr("nanobot.cli.commands._print_agent_response", lambda *_args, **_kwargs: None) result = runner.invoke( @@ -806,7 +822,7 @@ def test_agent_custom_config_workspace_does_not_migrate_legacy_cron( monkeypatch.setattr("nanobot.config.loader.set_config_path", lambda _path: None) monkeypatch.setattr("nanobot.config.loader.load_config", lambda _path=None: config) monkeypatch.setattr("nanobot.cli.commands.sync_workspace_templates", lambda _path: None) - monkeypatch.setattr("nanobot.cli.commands._make_provider", lambda _config: object()) + monkeypatch.setattr("nanobot.providers.factory.make_provider", lambda _config: _fake_provider()) monkeypatch.setattr("nanobot.bus.queue.MessageBus", lambda: object()) monkeypatch.setattr("nanobot.config.paths.get_cron_dir", lambda: legacy_dir) @@ -815,6 +831,9 @@ def test_agent_custom_config_workspace_does_not_migrate_legacy_cron( seen["cron_store"] = store_path class _FakeAgentLoop: + @classmethod + def from_config(cls, config, bus=None, **extra): + return cls(**extra) def __init__(self, *args, **kwargs) -> None: pass @@ -825,7 +844,7 @@ def test_agent_custom_config_workspace_does_not_migrate_legacy_cron( return None monkeypatch.setattr("nanobot.cron.service.CronService", _FakeCron) - monkeypatch.setattr("nanobot.agent.loop.AgentLoop", _FakeAgentLoop) + monkeypatch.setattr("nanobot.cli.commands.AgentLoop", _FakeAgentLoop) monkeypatch.setattr( "nanobot.cli.commands._print_agent_response", lambda *_args, **_kwargs: None ) @@ -846,7 +865,8 @@ def test_agent_overrides_workspace_path(mock_agent_runtime): assert result.exit_code == 0 assert mock_agent_runtime["config"].agents.defaults.workspace == str(workspace_path) assert mock_agent_runtime["sync_templates"].call_args.args == (workspace_path,) - assert mock_agent_runtime["agent_loop_cls"].call_args.kwargs["workspace"] == workspace_path + passed_config = mock_agent_runtime["from_config"].call_args.args[0] + assert passed_config.workspace_path == workspace_path def test_agent_workspace_override_wins_over_config_workspace(mock_agent_runtime, tmp_path: Path): @@ -863,7 +883,8 @@ def test_agent_workspace_override_wins_over_config_workspace(mock_agent_runtime, assert mock_agent_runtime["load_config"].call_args.args == (config_path.resolve(),) assert mock_agent_runtime["config"].agents.defaults.workspace == str(workspace_path) assert mock_agent_runtime["sync_templates"].call_args.args == (workspace_path,) - assert mock_agent_runtime["agent_loop_cls"].call_args.kwargs["workspace"] == workspace_path + passed_config = mock_agent_runtime["from_config"].call_args.args[0] + assert passed_config.workspace_path == workspace_path def test_agent_hints_about_deprecated_memory_window(mock_agent_runtime, tmp_path): @@ -915,7 +936,7 @@ def _patch_cli_command_runtime( cron_service=None, get_cron_dir=None, ) -> None: - provider_factory = make_provider or (lambda _config: object()) + provider_factory = make_provider or (lambda _config: _fake_provider()) monkeypatch.setattr( "nanobot.config.loader.set_config_path", @@ -928,7 +949,7 @@ def _patch_cli_command_runtime( sync_templates or (lambda _path: None), ) monkeypatch.setattr( - "nanobot.cli.commands._make_provider", + "nanobot.providers.factory.make_provider", provider_factory, ) monkeypatch.setattr( @@ -959,6 +980,9 @@ def _patch_serve_runtime(monkeypatch, config: Config, seen: dict[str, object]) - self.on_cleanup: list[object] = [] class _FakeAgentLoop: + @classmethod + def from_config(cls, config, bus=None, **extra): + return cls(workspace=config.workspace_path, **extra) def __init__(self, **kwargs) -> None: seen["workspace"] = kwargs["workspace"] @@ -985,7 +1009,7 @@ def _patch_serve_runtime(monkeypatch, config: Config, seen: dict[str, object]) - message_bus=lambda: object(), session_manager=lambda _workspace: object(), ) - monkeypatch.setattr("nanobot.agent.loop.AgentLoop", _FakeAgentLoop) + monkeypatch.setattr("nanobot.cli.commands.AgentLoop", _FakeAgentLoop) monkeypatch.setattr("nanobot.api.server.create_app", _fake_create_app) monkeypatch.setattr("aiohttp.web.run_app", _fake_run_app) @@ -1069,7 +1093,7 @@ def test_gateway_cron_evaluator_receives_scheduled_reminder_context( config = Config() config.agents.defaults.workspace = str(tmp_path / "config-workspace") - provider = object() + provider = _fake_provider() bus = MagicMock() bus.publish_outbound = AsyncMock() seen: dict[str, object] = {} @@ -1077,7 +1101,7 @@ def test_gateway_cron_evaluator_receives_scheduled_reminder_context( monkeypatch.setattr("nanobot.config.loader.set_config_path", lambda _path: None) monkeypatch.setattr("nanobot.config.loader.load_config", lambda _path=None: config) monkeypatch.setattr("nanobot.cli.commands.sync_workspace_templates", lambda _path: None) - monkeypatch.setattr("nanobot.cli.commands._make_provider", lambda _config: provider) + monkeypatch.setattr("nanobot.providers.factory.make_provider", lambda _config: provider) monkeypatch.setattr( "nanobot.providers.factory.build_provider_snapshot", lambda _config: _test_provider_snapshot(provider, _config), @@ -1115,8 +1139,12 @@ def test_gateway_cron_evaluator_receives_scheduled_reminder_context( seen["cron"] = self class _FakeAgentLoop: + @classmethod + def from_config(cls, config, bus=None, **extra): + return cls(**extra) def __init__(self, *args, **kwargs) -> None: self.model = "test-model" + self.provider = object() self.tools = {} async def process_direct(self, *_args, **_kwargs): @@ -1152,7 +1180,7 @@ def test_gateway_cron_evaluator_receives_scheduled_reminder_context( return True monkeypatch.setattr("nanobot.cron.service.CronService", _FakeCron) - monkeypatch.setattr("nanobot.agent.loop.AgentLoop", _FakeAgentLoop) + monkeypatch.setattr("nanobot.cli.commands.AgentLoop", _FakeAgentLoop) monkeypatch.setattr("nanobot.channels.manager.ChannelManager", _StopAfterCronSetup) monkeypatch.setattr( "nanobot.utils.evaluator.evaluate_response", @@ -1181,7 +1209,7 @@ def test_gateway_cron_evaluator_receives_scheduled_reminder_context( assert response == "Time to stretch." assert seen["response"] == "Time to stretch." - assert seen["provider"] is provider + assert seen["provider"] is not None assert seen["model"] == "test-model" assert seen["task_context"] == ( "The scheduled time has arrived. Deliver this reminder to the user now, " @@ -1228,7 +1256,7 @@ def test_gateway_cron_job_suppresses_intermediate_progress( monkeypatch.setattr("nanobot.config.loader.set_config_path", lambda _path: None) monkeypatch.setattr("nanobot.config.loader.load_config", lambda _path=None: config) monkeypatch.setattr("nanobot.cli.commands.sync_workspace_templates", lambda _path: None) - monkeypatch.setattr("nanobot.cli.commands._make_provider", lambda _config: object()) + monkeypatch.setattr("nanobot.providers.factory.make_provider", lambda _config: _fake_provider()) monkeypatch.setattr( "nanobot.providers.factory.build_provider_snapshot", lambda _config: _test_provider_snapshot(object(), _config), @@ -1246,8 +1274,12 @@ def test_gateway_cron_job_suppresses_intermediate_progress( seen["cron"] = self class _FakeAgentLoop: + @classmethod + def from_config(cls, config, bus=None, **extra): + return cls(**extra) def __init__(self, *args, **kwargs) -> None: self.model = "test-model" + self.provider = object() self.tools = {} async def process_direct(self, *_args, on_progress=None, **_kwargs): @@ -1275,7 +1307,7 @@ def test_gateway_cron_job_suppresses_intermediate_progress( return False monkeypatch.setattr("nanobot.cron.service.CronService", _FakeCron) - monkeypatch.setattr("nanobot.agent.loop.AgentLoop", _FakeAgentLoop) + monkeypatch.setattr("nanobot.cli.commands.AgentLoop", _FakeAgentLoop) monkeypatch.setattr("nanobot.channels.manager.ChannelManager", _StopAfterCronSetup) monkeypatch.setattr( "nanobot.utils.evaluator.evaluate_response", @@ -1478,8 +1510,12 @@ def test_gateway_health_endpoint_binds_and_serves_expected_responses( return 0 class _FakeAgentLoop: + @classmethod + def from_config(cls, config, bus=None, **extra): + return cls(**extra) def __init__(self, **_kwargs) -> None: self.model = "test-model" + self.provider = object() self.dream = _FakeDream() self.sessions = _FakeSessionManager() @@ -1571,7 +1607,7 @@ def test_gateway_health_endpoint_binds_and_serves_expected_responses( message_bus=lambda: object(), session_manager=lambda _workspace: object(), ) - monkeypatch.setattr("nanobot.agent.loop.AgentLoop", _FakeAgentLoop) + monkeypatch.setattr("nanobot.cli.commands.AgentLoop", _FakeAgentLoop) monkeypatch.setattr("nanobot.channels.manager.ChannelManager", _FakeChannelManager) monkeypatch.setattr("nanobot.cron.service.CronService", _FakeCronService) monkeypatch.setattr("nanobot.heartbeat.service.HeartbeatService", _FakeHeartbeatService) diff --git a/tests/test_nanobot_facade.py b/tests/test_nanobot_facade.py index 009c1c20d..2dfde6c7c 100644 --- a/tests/test_nanobot_facade.py +++ b/tests/test_nanobot_facade.py @@ -39,7 +39,7 @@ def test_from_config_default_path(): from nanobot.config.schema import Config with patch("nanobot.config.loader.load_config") as mock_load, \ - patch("nanobot.nanobot._make_provider") as mock_prov: + patch("nanobot.providers.factory.make_provider") as mock_prov: mock_load.return_value = Config() mock_prov.return_value = MagicMock() mock_prov.return_value.get_default_model.return_value = "test" @@ -127,7 +127,7 @@ def test_workspace_override(tmp_path): def test_sdk_make_provider_uses_github_copilot_backend(): from nanobot.config.schema import Config - from nanobot.nanobot import _make_provider + from nanobot.providers.factory import make_provider config = Config.model_validate( { @@ -141,7 +141,7 @@ def test_sdk_make_provider_uses_github_copilot_backend(): ) with patch("nanobot.providers.openai_compat_provider.AsyncOpenAI"): - provider = _make_provider(config) + provider = make_provider(config) assert provider.__class__.__name__ == "GitHubCopilotProvider"