feat(memory): restore Dream model preset override (#5107)
This commit is contained in:
+2
-2
@@ -197,13 +197,13 @@ Dream is configured under `agents.defaults.dream`:
|
||||
|-------|---------|
|
||||
| `intervalH` | How often Dream runs, in hours |
|
||||
| `cron` | Cron expression override (takes precedence over `intervalH`) |
|
||||
| `modelOverride` | Optional Dream-specific model override *(pending implementation)* |
|
||||
| `modelOverride` | Optional model preset name used for Dream |
|
||||
|
||||
In practical terms:
|
||||
|
||||
- `intervalH` is the normal way to configure Dream frequency. Internally it runs as an `every` schedule.
|
||||
- `cron` overrides `intervalH` when set, allowing precise cron expressions (e.g. `0 */4 * * *`).
|
||||
- `modelOverride` is reserved for a future release. Currently Dream uses the same model as the main agent.
|
||||
- `modelOverride` selects a named entry from `model_presets` for Dream. It accepts preset names only; raw model identifiers are not supported. If omitted, Dream uses the main agent's selected runtime.
|
||||
|
||||
## In Practice
|
||||
|
||||
|
||||
@@ -220,6 +220,12 @@ class AgentLoop:
|
||||
self._publish_runtime_selection(runtime)
|
||||
return runtime
|
||||
|
||||
def dream_runtime(self) -> LLMRuntime | None:
|
||||
"""Resolve the optional preset used for Dream without changing defaults."""
|
||||
if not self.dream_model_preset:
|
||||
return None
|
||||
return self.runtime_resolver.resolve_preset(self.dream_model_preset)
|
||||
|
||||
_RUNTIME_CHECKPOINT_KEY = "runtime_checkpoint"
|
||||
_PENDING_USER_TURN_KEY = "pending_user_turn"
|
||||
|
||||
@@ -257,6 +263,7 @@ class AgentLoop:
|
||||
model_presets: dict[str, ModelPresetConfig] | None = None,
|
||||
preset_catalog_loader: preset_helpers.PresetCatalogLoader | None = None,
|
||||
model_preset: str | None = None,
|
||||
dream_model_preset: str | None = None,
|
||||
preset_snapshot_loader: preset_helpers.PresetSnapshotLoader | None = None,
|
||||
runtime_events: RuntimeEventBus | None = None,
|
||||
turn_delivery_factory: TurnDeliveryFactory | None = None,
|
||||
@@ -311,6 +318,7 @@ class AgentLoop:
|
||||
provider_snapshot_loader=provider_snapshot_loader,
|
||||
preset_snapshot_loader=preset_snapshot_loader,
|
||||
)
|
||||
self.dream_model_preset = dream_model_preset
|
||||
self.context_block_limit = context_block_limit
|
||||
self.max_tool_result_chars = (
|
||||
max_tool_result_chars
|
||||
@@ -474,6 +482,7 @@ class AgentLoop:
|
||||
tools_config=config.tools,
|
||||
model_presets=preset_helpers.configured_model_presets(config),
|
||||
model_preset=defaults.model_preset,
|
||||
dream_model_preset=defaults.dream.model_override,
|
||||
restart_mode=config.gateway.restart_mode,
|
||||
provider_snapshot_loader=provider_snapshot_loader,
|
||||
preset_snapshot_loader=preset_snapshot_loader,
|
||||
@@ -1573,6 +1582,12 @@ class AgentLoop:
|
||||
if runtime is None:
|
||||
runtime = self.runtime_for_session(ctx.session)
|
||||
ctx.runtime = runtime
|
||||
if ctx.session_key.startswith("dream:"):
|
||||
logger.info(
|
||||
"Dream run using model={} (preset={})",
|
||||
runtime.model,
|
||||
runtime.model_preset or "default",
|
||||
)
|
||||
if ctx.on_runtime_admitted is not None:
|
||||
await ctx.on_runtime_admitted(runtime)
|
||||
replay_max_messages = replay_max_messages_for_context(
|
||||
|
||||
@@ -1820,12 +1820,17 @@ def _run_gateway(
|
||||
return None
|
||||
prompt, last_cursor = result
|
||||
key = dream_session_key()
|
||||
resolve_dream_runtime = getattr(agent, "dream_runtime", None)
|
||||
dream_runtime = (
|
||||
resolve_dream_runtime() if callable(resolve_dream_runtime) else None
|
||||
)
|
||||
resp = await agent.process_direct(
|
||||
prompt,
|
||||
session_key=key,
|
||||
ephemeral=True,
|
||||
tools=store.build_dream_tools(),
|
||||
on_progress=progress,
|
||||
runtime=dream_runtime,
|
||||
)
|
||||
# The real file delta grounds the audit record; clean completion
|
||||
# decides whether this history batch has finished processing.
|
||||
|
||||
@@ -427,12 +427,15 @@ async def cmd_dream(ctx: CommandContext) -> OutboundMessage:
|
||||
return
|
||||
prompt, last_cursor = result
|
||||
key = dream_session_key()
|
||||
resolve_dream_runtime = getattr(loop, "dream_runtime", None)
|
||||
dream_runtime = resolve_dream_runtime() if callable(resolve_dream_runtime) else None
|
||||
resp = await loop.process_direct(
|
||||
prompt,
|
||||
session_key=key,
|
||||
ephemeral=True,
|
||||
tools=store.build_dream_tools(),
|
||||
on_progress=progress,
|
||||
runtime=dream_runtime,
|
||||
)
|
||||
elapsed = time.monotonic() - t0
|
||||
# The real file delta grounds the audit record; clean completion
|
||||
|
||||
@@ -63,7 +63,7 @@ class DreamConfig(Base):
|
||||
model_override: str | None = Field(
|
||||
default=None,
|
||||
validation_alias=AliasChoices("modelOverride", "model", "model_override"),
|
||||
) # Override model for Dream sessions (pending implementation)
|
||||
) # Model preset name for Dream sessions
|
||||
|
||||
def build_schedule(self, timezone: str) -> CronSchedule:
|
||||
"""Build the runtime schedule, preferring the legacy cron override if present."""
|
||||
@@ -436,6 +436,9 @@ class Config(BaseSettings):
|
||||
name = self.agents.defaults.model_preset
|
||||
if name and name != "default" and name not in self.model_presets:
|
||||
raise ValueError(f"model_preset {name!r} not found in model_presets")
|
||||
dream_name = self.agents.defaults.dream.model_override
|
||||
if dream_name and dream_name != "default" and dream_name not in self.model_presets:
|
||||
raise ValueError(f"Dream model preset {dream_name!r} not found in model_presets")
|
||||
for fallback in self.agents.defaults.fallback_models:
|
||||
if isinstance(fallback, str) and fallback not in self.model_presets:
|
||||
raise ValueError(f"fallback_models entry {fallback!r} not found in model_presets")
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import pytest
|
||||
|
||||
from nanobot.agent.memory import MemoryStore
|
||||
from nanobot.config.schema import ModelPresetConfig
|
||||
from nanobot.providers.base import LLMResponse
|
||||
from nanobot.security.workspace_access import (
|
||||
bind_workspace_scope,
|
||||
@@ -385,6 +386,21 @@ class TestEphemeralDirect:
|
||||
|
||||
return loop, store
|
||||
|
||||
def test_dream_runtime_uses_preset_without_changing_default(self, _make_loop):
|
||||
loop, _ = _make_loop
|
||||
loop.runtime_resolver._model_presets = {
|
||||
"dream": ModelPresetConfig(model="dream-model"),
|
||||
}
|
||||
loop.dream_model_preset = "dream"
|
||||
|
||||
runtime = loop.dream_runtime()
|
||||
|
||||
assert runtime is not None
|
||||
assert runtime.model == "dream-model"
|
||||
assert runtime.model_preset == "dream"
|
||||
assert loop.model == "test-model"
|
||||
assert loop.model_preset is None
|
||||
|
||||
async def test_ephemeral_skips_raw_archive(self, tmp_path, _make_loop):
|
||||
"""When ephemeral=True, raw_archive must not be called."""
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -171,11 +171,13 @@ async def test_dream_internal_run_silences_progress(tmp_path) -> None:
|
||||
|
||||
sessions_dir = tmp_path / "sessions"
|
||||
sessions_dir.mkdir()
|
||||
dream_runtime = object()
|
||||
loop = SimpleNamespace(
|
||||
bus=bus,
|
||||
context=SimpleNamespace(memory=store, timezone="UTC"),
|
||||
sessions=SimpleNamespace(sessions_dir=sessions_dir),
|
||||
process_direct=process_direct,
|
||||
dream_runtime=lambda: dream_runtime,
|
||||
)
|
||||
ctx = CommandContext(msg=msg, session=None, key=msg.session_key, raw="/dream", args="", loop=loop)
|
||||
|
||||
@@ -184,6 +186,7 @@ async def test_dream_internal_run_silences_progress(tmp_path) -> None:
|
||||
|
||||
assert len(calls) == 1
|
||||
assert callable(calls[0][1]["on_progress"])
|
||||
assert calls[0][1]["runtime"] is dream_runtime
|
||||
|
||||
|
||||
def _build_runnable_dream(
|
||||
|
||||
@@ -267,6 +267,24 @@ def test_validator_rejects_unknown_preset() -> None:
|
||||
})
|
||||
|
||||
|
||||
def test_validator_accepts_dream_model_preset() -> None:
|
||||
config = Config.model_validate({
|
||||
"modelPresets": {
|
||||
"dream": {"model": "anthropic/claude-haiku-4-5", "provider": "anthropic"},
|
||||
},
|
||||
"agents": {"defaults": {"dream": {"modelOverride": "dream"}}},
|
||||
})
|
||||
|
||||
assert config.agents.defaults.dream.model_override == "dream"
|
||||
|
||||
|
||||
def test_validator_rejects_unknown_dream_model_preset() -> None:
|
||||
with pytest.raises(ValueError, match="Dream model preset 'unknown' not found"):
|
||||
Config.model_validate({
|
||||
"agents": {"defaults": {"dream": {"modelOverride": "unknown"}}},
|
||||
})
|
||||
|
||||
|
||||
def test_model_preset_accepts_explicit_default_name() -> None:
|
||||
config = Config.model_validate({
|
||||
"agents": {
|
||||
|
||||
Reference in New Issue
Block a user