fix(agent): preserve runtime compatibility contracts
This commit is contained in:
@@ -139,6 +139,38 @@ def test_resolver_refresh_preserves_unchanged_active_preset() -> None:
|
||||
assert resolver.model_preset == "fast"
|
||||
|
||||
|
||||
def test_refresh_clears_preset_when_new_default_has_same_snapshot_signature() -> None:
|
||||
initial = _runtime()
|
||||
preset_provider = _provider()
|
||||
preset_snapshot = ProviderSnapshot(
|
||||
provider=preset_provider,
|
||||
model="fast-model",
|
||||
context_window_tokens=20_000,
|
||||
signature=("fast-model", "auto", "same-runtime"),
|
||||
)
|
||||
default_snapshot = ProviderSnapshot(
|
||||
provider=preset_provider,
|
||||
model="fast-model",
|
||||
context_window_tokens=20_000,
|
||||
signature=preset_snapshot.signature,
|
||||
)
|
||||
resolver = ModelRuntimeResolver(
|
||||
initial,
|
||||
model_presets={"fast": ModelPresetConfig(model="fast-model")},
|
||||
provider_snapshot_loader=lambda: default_snapshot,
|
||||
preset_snapshot_loader=lambda _name: preset_snapshot,
|
||||
)
|
||||
resolver.select_preset("fast")
|
||||
|
||||
refreshed = resolver.refresh()
|
||||
|
||||
assert refreshed is resolver.runtime
|
||||
assert refreshed is not None
|
||||
assert resolver.model_preset is None
|
||||
assert resolver.runtime.model_preset is None
|
||||
assert "_active_preset" not in resolver.__dict__
|
||||
|
||||
|
||||
def test_resolver_refreshes_provider_generation_for_next_default_turn() -> None:
|
||||
provider = _provider(temperature=0.2, max_tokens=2048)
|
||||
resolver = ModelRuntimeResolver(_runtime(provider))
|
||||
|
||||
@@ -5,7 +5,7 @@ from unittest.mock import MagicMock
|
||||
from nanobot.agent.loop import AgentLoop
|
||||
from nanobot.bus.queue import MessageBus
|
||||
from nanobot.config.loader import save_config
|
||||
from nanobot.config.schema import Config
|
||||
from nanobot.config.schema import Config, ModelPresetConfig
|
||||
from nanobot.providers.base import GenerationSettings
|
||||
from nanobot.providers.factory import ProviderSnapshot, load_provider_snapshot
|
||||
from nanobot.webui.settings_api import update_agent_settings
|
||||
@@ -99,6 +99,37 @@ def test_llm_runtime_refreshes_provider_snapshot(tmp_path: Path) -> None:
|
||||
assert not hasattr(loop.runner, "provider")
|
||||
|
||||
|
||||
def test_same_snapshot_default_clears_preset_and_publishes_update(tmp_path: Path) -> None:
|
||||
base_provider = _provider("base-model")
|
||||
fast_provider = _provider("fast-model")
|
||||
fast_snapshot = ProviderSnapshot(
|
||||
provider=fast_provider,
|
||||
model="fast-model",
|
||||
context_window_tokens=2000,
|
||||
signature=("fast-model", "auto", "same-runtime"),
|
||||
)
|
||||
published: list[tuple[str, str | None]] = []
|
||||
loop = AgentLoop(
|
||||
bus=MessageBus(),
|
||||
provider=base_provider,
|
||||
workspace=tmp_path,
|
||||
model="base-model",
|
||||
context_window_tokens=1000,
|
||||
provider_signature=("base-model", "auto", "initial"),
|
||||
provider_snapshot_loader=lambda: fast_snapshot,
|
||||
model_presets={"fast": ModelPresetConfig(model="fast-model")},
|
||||
model_preset="fast",
|
||||
preset_snapshot_loader=lambda _name: fast_snapshot,
|
||||
runtime_model_publisher=lambda model, preset: published.append((model, preset)),
|
||||
)
|
||||
|
||||
runtime = loop.llm_runtime()
|
||||
|
||||
assert runtime.model_preset is None
|
||||
assert loop.model_preset is None
|
||||
assert published == [("fast-model", None)]
|
||||
|
||||
|
||||
def test_next_turn_captures_generation_changed_after_previous_admission(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
|
||||
@@ -7,10 +7,10 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from nanobot.agent import SubagentManager
|
||||
from nanobot.agent.hook import AgentHookContext
|
||||
from nanobot.agent.runner import AgentRunResult
|
||||
from nanobot.agent.subagent import (
|
||||
SubagentManager,
|
||||
SubagentStatus,
|
||||
_SubagentHook,
|
||||
)
|
||||
@@ -98,6 +98,78 @@ class TestRuntimeOwnership:
|
||||
assert not hasattr(sm.runner, "provider")
|
||||
|
||||
|
||||
class TestLegacyCompatibility:
|
||||
def test_accepts_exported_legacy_constructor_positionally(self, tmp_path):
|
||||
provider = MagicMock(spec=LLMProvider)
|
||||
provider.generation = GenerationSettings(temperature=0.2, max_tokens=2048)
|
||||
|
||||
with pytest.warns(DeprecationWarning, match="provider"):
|
||||
sm = SubagentManager(
|
||||
provider,
|
||||
tmp_path,
|
||||
MessageBus(),
|
||||
16_000,
|
||||
"legacy-model",
|
||||
)
|
||||
|
||||
assert sm.workspace == tmp_path
|
||||
assert sm.max_tool_result_chars == 16_000
|
||||
assert not hasattr(sm, "provider")
|
||||
assert not hasattr(sm, "model")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_legacy_spawn_captures_runtime_at_admission(self, tmp_path):
|
||||
provider = MagicMock(spec=LLMProvider)
|
||||
provider.generation = GenerationSettings(temperature=0.2, max_tokens=2048)
|
||||
with pytest.warns(DeprecationWarning, match="provider"):
|
||||
sm = SubagentManager(
|
||||
provider=provider,
|
||||
workspace=tmp_path,
|
||||
bus=MessageBus(),
|
||||
max_tool_result_chars=16_000,
|
||||
model="legacy-model",
|
||||
)
|
||||
sm.runner.run = AsyncMock(return_value=AgentRunResult(
|
||||
final_content="done", messages=[], stop_reason="completed",
|
||||
))
|
||||
provider.generation = GenerationSettings(temperature=0.8, max_tokens=512)
|
||||
|
||||
with pytest.warns(DeprecationWarning, match="runtime"):
|
||||
await sm.spawn("legacy task")
|
||||
await _drain_subagent_tasks(sm)
|
||||
|
||||
runtime = sm.runner.run.await_args.args[0].runtime
|
||||
assert runtime.provider is provider
|
||||
assert runtime.model == "legacy-model"
|
||||
assert runtime.generation == GenerationSettings(0.8, 512, None)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_provider_supports_future_legacy_spawns(self, tmp_path):
|
||||
sm = _manager(tmp_path)
|
||||
provider = MagicMock(spec=LLMProvider)
|
||||
provider.generation = GenerationSettings(temperature=0.3, max_tokens=1024)
|
||||
with pytest.warns(DeprecationWarning, match="set_provider"):
|
||||
sm.set_provider(provider, "replacement-model")
|
||||
sm.runner.run = AsyncMock(return_value=AgentRunResult(
|
||||
final_content="done", messages=[], stop_reason="completed",
|
||||
))
|
||||
|
||||
with pytest.warns(DeprecationWarning, match="runtime"):
|
||||
await sm.spawn("legacy task")
|
||||
await _drain_subagent_tasks(sm)
|
||||
|
||||
runtime = sm.runner.run.await_args.args[0].runtime
|
||||
assert runtime.provider is provider
|
||||
assert runtime.model == "replacement-model"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_new_constructor_still_requires_explicit_spawn_runtime(self, tmp_path):
|
||||
sm = _manager(tmp_path)
|
||||
|
||||
with pytest.raises(TypeError, match="runtime"):
|
||||
await sm.spawn("task")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# spawn
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user