feat(memory): restore Dream model preset override (#5107)

This commit is contained in:
chengyongru
2026-07-27 14:43:25 +08:00
committed by GitHub
parent 281b4b7f0b
commit c13df29457
8 changed files with 66 additions and 3 deletions
+2 -2
View File
@@ -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
+15
View File
@@ -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(
+5
View File
@@ -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.
+3
View File
@@ -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
+4 -1
View File
@@ -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")
+16
View File
@@ -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
+3
View File
@@ -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(
+18
View File
@@ -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": {