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 |
|
| `intervalH` | How often Dream runs, in hours |
|
||||||
| `cron` | Cron expression override (takes precedence over `intervalH`) |
|
| `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:
|
In practical terms:
|
||||||
|
|
||||||
- `intervalH` is the normal way to configure Dream frequency. Internally it runs as an `every` schedule.
|
- `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 * * *`).
|
- `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
|
## In Practice
|
||||||
|
|
||||||
|
|||||||
@@ -220,6 +220,12 @@ class AgentLoop:
|
|||||||
self._publish_runtime_selection(runtime)
|
self._publish_runtime_selection(runtime)
|
||||||
return 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"
|
_RUNTIME_CHECKPOINT_KEY = "runtime_checkpoint"
|
||||||
_PENDING_USER_TURN_KEY = "pending_user_turn"
|
_PENDING_USER_TURN_KEY = "pending_user_turn"
|
||||||
|
|
||||||
@@ -257,6 +263,7 @@ class AgentLoop:
|
|||||||
model_presets: dict[str, ModelPresetConfig] | None = None,
|
model_presets: dict[str, ModelPresetConfig] | None = None,
|
||||||
preset_catalog_loader: preset_helpers.PresetCatalogLoader | None = None,
|
preset_catalog_loader: preset_helpers.PresetCatalogLoader | None = None,
|
||||||
model_preset: str | None = None,
|
model_preset: str | None = None,
|
||||||
|
dream_model_preset: str | None = None,
|
||||||
preset_snapshot_loader: preset_helpers.PresetSnapshotLoader | None = None,
|
preset_snapshot_loader: preset_helpers.PresetSnapshotLoader | None = None,
|
||||||
runtime_events: RuntimeEventBus | None = None,
|
runtime_events: RuntimeEventBus | None = None,
|
||||||
turn_delivery_factory: TurnDeliveryFactory | None = None,
|
turn_delivery_factory: TurnDeliveryFactory | None = None,
|
||||||
@@ -311,6 +318,7 @@ class AgentLoop:
|
|||||||
provider_snapshot_loader=provider_snapshot_loader,
|
provider_snapshot_loader=provider_snapshot_loader,
|
||||||
preset_snapshot_loader=preset_snapshot_loader,
|
preset_snapshot_loader=preset_snapshot_loader,
|
||||||
)
|
)
|
||||||
|
self.dream_model_preset = dream_model_preset
|
||||||
self.context_block_limit = context_block_limit
|
self.context_block_limit = context_block_limit
|
||||||
self.max_tool_result_chars = (
|
self.max_tool_result_chars = (
|
||||||
max_tool_result_chars
|
max_tool_result_chars
|
||||||
@@ -474,6 +482,7 @@ class AgentLoop:
|
|||||||
tools_config=config.tools,
|
tools_config=config.tools,
|
||||||
model_presets=preset_helpers.configured_model_presets(config),
|
model_presets=preset_helpers.configured_model_presets(config),
|
||||||
model_preset=defaults.model_preset,
|
model_preset=defaults.model_preset,
|
||||||
|
dream_model_preset=defaults.dream.model_override,
|
||||||
restart_mode=config.gateway.restart_mode,
|
restart_mode=config.gateway.restart_mode,
|
||||||
provider_snapshot_loader=provider_snapshot_loader,
|
provider_snapshot_loader=provider_snapshot_loader,
|
||||||
preset_snapshot_loader=preset_snapshot_loader,
|
preset_snapshot_loader=preset_snapshot_loader,
|
||||||
@@ -1573,6 +1582,12 @@ class AgentLoop:
|
|||||||
if runtime is None:
|
if runtime is None:
|
||||||
runtime = self.runtime_for_session(ctx.session)
|
runtime = self.runtime_for_session(ctx.session)
|
||||||
ctx.runtime = runtime
|
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:
|
if ctx.on_runtime_admitted is not None:
|
||||||
await ctx.on_runtime_admitted(runtime)
|
await ctx.on_runtime_admitted(runtime)
|
||||||
replay_max_messages = replay_max_messages_for_context(
|
replay_max_messages = replay_max_messages_for_context(
|
||||||
|
|||||||
@@ -1820,12 +1820,17 @@ def _run_gateway(
|
|||||||
return None
|
return None
|
||||||
prompt, last_cursor = result
|
prompt, last_cursor = result
|
||||||
key = dream_session_key()
|
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(
|
resp = await agent.process_direct(
|
||||||
prompt,
|
prompt,
|
||||||
session_key=key,
|
session_key=key,
|
||||||
ephemeral=True,
|
ephemeral=True,
|
||||||
tools=store.build_dream_tools(),
|
tools=store.build_dream_tools(),
|
||||||
on_progress=progress,
|
on_progress=progress,
|
||||||
|
runtime=dream_runtime,
|
||||||
)
|
)
|
||||||
# The real file delta grounds the audit record; clean completion
|
# The real file delta grounds the audit record; clean completion
|
||||||
# decides whether this history batch has finished processing.
|
# decides whether this history batch has finished processing.
|
||||||
|
|||||||
@@ -427,12 +427,15 @@ async def cmd_dream(ctx: CommandContext) -> OutboundMessage:
|
|||||||
return
|
return
|
||||||
prompt, last_cursor = result
|
prompt, last_cursor = result
|
||||||
key = dream_session_key()
|
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(
|
resp = await loop.process_direct(
|
||||||
prompt,
|
prompt,
|
||||||
session_key=key,
|
session_key=key,
|
||||||
ephemeral=True,
|
ephemeral=True,
|
||||||
tools=store.build_dream_tools(),
|
tools=store.build_dream_tools(),
|
||||||
on_progress=progress,
|
on_progress=progress,
|
||||||
|
runtime=dream_runtime,
|
||||||
)
|
)
|
||||||
elapsed = time.monotonic() - t0
|
elapsed = time.monotonic() - t0
|
||||||
# The real file delta grounds the audit record; clean completion
|
# The real file delta grounds the audit record; clean completion
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ class DreamConfig(Base):
|
|||||||
model_override: str | None = Field(
|
model_override: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
validation_alias=AliasChoices("modelOverride", "model", "model_override"),
|
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:
|
def build_schedule(self, timezone: str) -> CronSchedule:
|
||||||
"""Build the runtime schedule, preferring the legacy cron override if present."""
|
"""Build the runtime schedule, preferring the legacy cron override if present."""
|
||||||
@@ -436,6 +436,9 @@ class Config(BaseSettings):
|
|||||||
name = self.agents.defaults.model_preset
|
name = self.agents.defaults.model_preset
|
||||||
if name and name != "default" and name not in self.model_presets:
|
if name and name != "default" and name not in self.model_presets:
|
||||||
raise ValueError(f"model_preset {name!r} not found in 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:
|
for fallback in self.agents.defaults.fallback_models:
|
||||||
if isinstance(fallback, str) and fallback not in self.model_presets:
|
if isinstance(fallback, str) and fallback not in self.model_presets:
|
||||||
raise ValueError(f"fallback_models entry {fallback!r} not found in model_presets")
|
raise ValueError(f"fallback_models entry {fallback!r} not found in model_presets")
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from nanobot.agent.memory import MemoryStore
|
from nanobot.agent.memory import MemoryStore
|
||||||
|
from nanobot.config.schema import ModelPresetConfig
|
||||||
from nanobot.providers.base import LLMResponse
|
from nanobot.providers.base import LLMResponse
|
||||||
from nanobot.security.workspace_access import (
|
from nanobot.security.workspace_access import (
|
||||||
bind_workspace_scope,
|
bind_workspace_scope,
|
||||||
@@ -385,6 +386,21 @@ class TestEphemeralDirect:
|
|||||||
|
|
||||||
return loop, store
|
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):
|
async def test_ephemeral_skips_raw_archive(self, tmp_path, _make_loop):
|
||||||
"""When ephemeral=True, raw_archive must not be called."""
|
"""When ephemeral=True, raw_archive must not be called."""
|
||||||
from unittest.mock import patch
|
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 = tmp_path / "sessions"
|
||||||
sessions_dir.mkdir()
|
sessions_dir.mkdir()
|
||||||
|
dream_runtime = object()
|
||||||
loop = SimpleNamespace(
|
loop = SimpleNamespace(
|
||||||
bus=bus,
|
bus=bus,
|
||||||
context=SimpleNamespace(memory=store, timezone="UTC"),
|
context=SimpleNamespace(memory=store, timezone="UTC"),
|
||||||
sessions=SimpleNamespace(sessions_dir=sessions_dir),
|
sessions=SimpleNamespace(sessions_dir=sessions_dir),
|
||||||
process_direct=process_direct,
|
process_direct=process_direct,
|
||||||
|
dream_runtime=lambda: dream_runtime,
|
||||||
)
|
)
|
||||||
ctx = CommandContext(msg=msg, session=None, key=msg.session_key, raw="/dream", args="", loop=loop)
|
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 len(calls) == 1
|
||||||
assert callable(calls[0][1]["on_progress"])
|
assert callable(calls[0][1]["on_progress"])
|
||||||
|
assert calls[0][1]["runtime"] is dream_runtime
|
||||||
|
|
||||||
|
|
||||||
def _build_runnable_dream(
|
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:
|
def test_model_preset_accepts_explicit_default_name() -> None:
|
||||||
config = Config.model_validate({
|
config = Config.model_validate({
|
||||||
"agents": {
|
"agents": {
|
||||||
|
|||||||
Reference in New Issue
Block a user