refactor(providers): move provider snapshot creation into factory

This commit is contained in:
Xubin Ren
2026-04-26 14:05:13 +00:00
parent 65b0ae81af
commit f670da6c70
6 changed files with 53 additions and 53 deletions
+17 -17
View File
@@ -18,7 +18,6 @@ from nanobot.agent.context import ContextBuilder
from nanobot.agent.hook import AgentHook, AgentHookContext, CompositeHook
from nanobot.agent.memory import Consolidator, Dream
from nanobot.agent.runner import _MAX_INJECTIONS_PER_TURN, AgentRunner, AgentRunSpec
from nanobot.agent.runtime import AgentRuntime
from nanobot.agent.skills import BUILTIN_SKILLS_DIR
from nanobot.agent.subagent import SubagentManager
from nanobot.agent.tools.ask import (
@@ -43,6 +42,7 @@ from nanobot.bus.queue import MessageBus
from nanobot.command import CommandContext, CommandRouter, register_builtin_commands
from nanobot.config.schema import AgentDefaults
from nanobot.providers.base import LLMProvider
from nanobot.providers.factory import ProviderSnapshot
from nanobot.session.manager import Session, SessionManager
from nanobot.utils.document import extract_documents
from nanobot.utils.helpers import image_placeholder_text
@@ -196,8 +196,8 @@ class AgentLoop:
unified_session: bool = False,
disabled_skills: list[str] | None = None,
tools_config: ToolsConfig | None = None,
runtime_loader: Callable[[], AgentRuntime] | None = None,
runtime_signature: tuple[object, ...] | None = None,
provider_snapshot_loader: Callable[[], ProviderSnapshot] | None = None,
provider_signature: tuple[object, ...] | None = None,
):
from nanobot.config.schema import ExecToolConfig, ToolsConfig, WebToolsConfig
@@ -206,8 +206,8 @@ class AgentLoop:
self.bus = bus
self.channels_config = channels_config
self.provider = provider
self._runtime_loader = runtime_loader
self._runtime_signature = runtime_signature
self._provider_snapshot_loader = provider_snapshot_loader
self._provider_signature = provider_signature
self.workspace = workspace
self.model = model or provider.get_default_model()
self.max_iterations = (
@@ -295,11 +295,11 @@ class AgentLoop:
self.commands = CommandRouter()
register_builtin_commands(self.commands)
def _apply_runtime(self, runtime: AgentRuntime) -> None:
def _apply_provider_snapshot(self, snapshot: ProviderSnapshot) -> None:
"""Swap model/provider for future turns without disturbing an active one."""
provider = runtime.provider
model = runtime.model
context_window_tokens = runtime.context_window_tokens
provider = snapshot.provider
model = snapshot.model
context_window_tokens = snapshot.context_window_tokens
if self.provider is provider and self.model == model:
return
old_model = self.model
@@ -317,20 +317,20 @@ class AgentLoop:
self.dream.provider = provider
self.dream.model = model
self.dream._runner.provider = provider
self._runtime_signature = runtime.signature
self._provider_signature = snapshot.signature
logger.info("Runtime model switched for next turn: {} -> {}", old_model, model)
def _refresh_runtime(self) -> None:
if self._runtime_loader is None:
def _refresh_provider_snapshot(self) -> None:
if self._provider_snapshot_loader is None:
return
try:
runtime = self._runtime_loader()
snapshot = self._provider_snapshot_loader()
except Exception:
logger.exception("Failed to refresh runtime config")
logger.exception("Failed to refresh provider config")
return
if runtime.signature == self._runtime_signature:
if snapshot.signature == self._provider_signature:
return
self._apply_runtime(runtime)
self._apply_provider_snapshot(snapshot)
def _register_default_tools(self) -> None:
"""Register the default set of tools."""
@@ -810,7 +810,7 @@ class AgentLoop:
pending_queue: asyncio.Queue | None = None,
) -> OutboundMessage | None:
"""Process a single inbound message and return the response."""
self._refresh_runtime()
self._refresh_provider_snapshot()
# System messages: parse origin from chat_id ("channel:chat_id")
if msg.channel == "system":
channel, chat_id = (
+8 -8
View File
@@ -412,7 +412,7 @@ def _make_provider(config: Config):
Routing is driven by ``ProviderSpec.backend`` in the registry.
"""
from nanobot.agent.runtime import make_provider
from nanobot.providers.factory import make_provider
try:
return make_provider(config)
@@ -597,7 +597,6 @@ def _run_gateway(
) -> None:
"""Shared gateway runtime; ``open_browser_url`` opens a tab once channels are up."""
from nanobot.agent.loop import AgentLoop
from nanobot.agent.runtime import build_agent_runtime, load_agent_runtime
from nanobot.agent.tools.cron import CronTool
from nanobot.agent.tools.message import MessageTool
from nanobot.bus.queue import MessageBus
@@ -605,6 +604,7 @@ def _run_gateway(
from nanobot.cron.service import CronService
from nanobot.cron.types import CronJob
from nanobot.heartbeat.service import HeartbeatService
from nanobot.providers.factory import build_provider_snapshot, load_provider_snapshot
from nanobot.session.manager import SessionManager
port = port if port is not None else config.gateway.port
@@ -613,11 +613,11 @@ def _run_gateway(
sync_workspace_templates(config.workspace_path)
bus = MessageBus()
try:
runtime = build_agent_runtime(config)
provider_snapshot = build_provider_snapshot(config)
except ValueError as exc:
console.print(f"[red]Error: {exc}[/red]")
raise typer.Exit(1) from exc
provider = runtime.provider
provider = provider_snapshot.provider
session_manager = SessionManager(config.workspace_path)
# Preserve existing single-workspace installs, but keep custom workspaces clean.
@@ -633,9 +633,9 @@ def _run_gateway(
bus=bus,
provider=provider,
workspace=config.workspace_path,
model=runtime.model,
model=provider_snapshot.model,
max_iterations=config.agents.defaults.max_tool_iterations,
context_window_tokens=runtime.context_window_tokens,
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,
@@ -652,8 +652,8 @@ def _run_gateway(
session_ttl_minutes=config.agents.defaults.session_ttl_minutes,
consolidation_ratio=config.agents.defaults.consolidation_ratio,
tools_config=config.tools,
runtime_loader=load_agent_runtime,
runtime_signature=runtime.signature,
provider_snapshot_loader=load_provider_snapshot,
provider_signature=provider_snapshot.signature,
)
from nanobot.agent.loop import UNIFIED_SESSION_KEY
+1 -1
View File
@@ -120,6 +120,6 @@ class Nanobot:
def _make_provider(config: Any) -> Any:
"""Create the LLM provider from config (extracted from CLI)."""
from nanobot.agent.runtime import make_provider
from nanobot.providers.factory import make_provider
return make_provider(config)
@@ -1,4 +1,4 @@
"""Runtime model/provider resolution for agent turns."""
"""Create LLM providers from config."""
from __future__ import annotations
@@ -11,7 +11,7 @@ from nanobot.providers.registry import find_by_name
@dataclass(frozen=True)
class AgentRuntime:
class ProviderSnapshot:
provider: LLMProvider
model: str
context_window_tokens: int
@@ -80,8 +80,8 @@ def make_provider(config: Config) -> LLMProvider:
return provider
def runtime_signature(config: Config) -> tuple[object, ...]:
"""Return the config fields that affect the primary LLM runtime."""
def provider_signature(config: Config) -> tuple[object, ...]:
"""Return the config fields that affect the primary LLM provider."""
model = config.agents.defaults.model
defaults = config.agents.defaults
return (
@@ -97,16 +97,16 @@ def runtime_signature(config: Config) -> tuple[object, ...]:
)
def build_agent_runtime(config: Config) -> AgentRuntime:
return AgentRuntime(
def build_provider_snapshot(config: Config) -> ProviderSnapshot:
return ProviderSnapshot(
provider=make_provider(config),
model=config.agents.defaults.model,
context_window_tokens=config.agents.defaults.context_window_tokens,
signature=runtime_signature(config),
signature=provider_signature(config),
)
def load_agent_runtime(config_path: Path | None = None) -> AgentRuntime:
def load_provider_snapshot(config_path: Path | None = None) -> ProviderSnapshot:
from nanobot.config.loader import load_config, resolve_config_env_vars
return build_agent_runtime(resolve_config_env_vars(load_config(config_path)))
return build_provider_snapshot(resolve_config_env_vars(load_config(config_path)))
+3 -3
View File
@@ -3,8 +3,8 @@ from types import SimpleNamespace
from unittest.mock import MagicMock
from nanobot.agent.loop import AgentLoop
from nanobot.agent.runtime import AgentRuntime
from nanobot.bus.queue import MessageBus
from nanobot.providers.factory import ProviderSnapshot
def _provider(default_model: str, max_tokens: int = 123) -> MagicMock:
@@ -23,7 +23,7 @@ def test_runtime_refresh_updates_loop_dependents(tmp_path: Path) -> None:
workspace=tmp_path,
model="old-model",
context_window_tokens=1000,
runtime_loader=lambda: AgentRuntime(
provider_snapshot_loader=lambda: ProviderSnapshot(
provider=new_provider,
model="new-model",
context_window_tokens=2000,
@@ -31,7 +31,7 @@ def test_runtime_refresh_updates_loop_dependents(tmp_path: Path) -> None:
),
)
loop._refresh_runtime()
loop._refresh_provider_snapshot()
assert loop.provider is new_provider
assert loop.model == "new-model"
+15 -15
View File
@@ -8,11 +8,11 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from typer.testing import CliRunner
from nanobot.agent.runtime import AgentRuntime
from nanobot.bus.events import OutboundMessage
from nanobot.cli.commands import _make_provider, app
from nanobot.config.schema import Config
from nanobot.cron.types import CronJob, CronPayload
from nanobot.providers.factory import ProviderSnapshot
from nanobot.providers.openai_codex_provider import _strip_model_prefix
from nanobot.providers.registry import find_by_name
@@ -777,8 +777,8 @@ def _stop_gateway_provider(_config) -> object:
raise _StopGatewayError("stop")
def _test_agent_runtime(provider: object, config: Config) -> AgentRuntime:
return AgentRuntime(
def _test_provider_snapshot(provider: object, config: Config) -> ProviderSnapshot:
return ProviderSnapshot(
provider=provider,
model=config.agents.defaults.model,
context_window_tokens=config.agents.defaults.context_window_tokens,
@@ -815,12 +815,12 @@ def _patch_cli_command_runtime(
provider_factory,
)
monkeypatch.setattr(
"nanobot.agent.runtime.build_agent_runtime",
lambda _config: _test_agent_runtime(provider_factory(_config), _config),
"nanobot.providers.factory.build_provider_snapshot",
lambda _config: _test_provider_snapshot(provider_factory(_config), _config),
)
monkeypatch.setattr(
"nanobot.agent.runtime.load_agent_runtime",
lambda _config_path=None: _test_agent_runtime(provider_factory(config), config),
"nanobot.providers.factory.load_provider_snapshot",
lambda _config_path=None: _test_provider_snapshot(provider_factory(config), config),
)
if message_bus is not None:
@@ -962,12 +962,12 @@ def test_gateway_cron_evaluator_receives_scheduled_reminder_context(
monkeypatch.setattr("nanobot.cli.commands.sync_workspace_templates", lambda _path: None)
monkeypatch.setattr("nanobot.cli.commands._make_provider", lambda _config: provider)
monkeypatch.setattr(
"nanobot.agent.runtime.build_agent_runtime",
lambda _config: _test_agent_runtime(provider, _config),
"nanobot.providers.factory.build_provider_snapshot",
lambda _config: _test_provider_snapshot(provider, _config),
)
monkeypatch.setattr(
"nanobot.agent.runtime.load_agent_runtime",
lambda _config_path=None: _test_agent_runtime(provider, config),
"nanobot.providers.factory.load_provider_snapshot",
lambda _config_path=None: _test_provider_snapshot(provider, config),
)
monkeypatch.setattr("nanobot.bus.queue.MessageBus", lambda: bus)
@@ -1111,12 +1111,12 @@ def test_gateway_cron_job_suppresses_intermediate_progress(
monkeypatch.setattr("nanobot.cli.commands.sync_workspace_templates", lambda _path: None)
monkeypatch.setattr("nanobot.cli.commands._make_provider", lambda _config: object())
monkeypatch.setattr(
"nanobot.agent.runtime.build_agent_runtime",
lambda _config: _test_agent_runtime(object(), _config),
"nanobot.providers.factory.build_provider_snapshot",
lambda _config: _test_provider_snapshot(object(), _config),
)
monkeypatch.setattr(
"nanobot.agent.runtime.load_agent_runtime",
lambda _config_path=None: _test_agent_runtime(object(), config),
"nanobot.providers.factory.load_provider_snapshot",
lambda _config_path=None: _test_provider_snapshot(object(), config),
)
monkeypatch.setattr("nanobot.bus.queue.MessageBus", lambda: bus)
monkeypatch.setattr("nanobot.session.manager.SessionManager", lambda _workspace: object())