refactor(memory): simplify Dream config naming and rename gitstore module

This commit is contained in:
Xubin Ren 2026-04-04 10:01:45 +00:00
parent a166fe8fc2
commit 0a3a60a7a4
9 changed files with 104 additions and 23 deletions

View File

@ -149,10 +149,10 @@ Dream is configured under `agents.defaults.dream`:
"agents": { "agents": {
"defaults": { "defaults": {
"dream": { "dream": {
"cron": "0 */2 * * *", "intervalH": 2,
"model": null, "modelOverride": null,
"max_batch_size": 20, "maxBatchSize": 20,
"max_iterations": 10 "maxIterations": 10
} }
} }
} }
@ -161,10 +161,22 @@ Dream is configured under `agents.defaults.dream`:
| Field | Meaning | | Field | Meaning |
|-------|---------| |-------|---------|
| `cron` | How often Dream runs | | `intervalH` | How often Dream runs, in hours |
| `model` | Optional model override for Dream | | `modelOverride` | Optional Dream-specific model override |
| `max_batch_size` | How many history entries Dream processes per run | | `maxBatchSize` | How many history entries Dream processes per run |
| `max_iterations` | The tool budget for Dream's editing phase | | `maxIterations` | The tool budget for Dream's editing phase |
In practical terms:
- `modelOverride: null` means Dream uses the same model as the main agent. Set it only if you want Dream to run on a different model.
- `maxBatchSize` controls how many new `history.jsonl` entries Dream consumes in one run. Larger batches catch up faster; smaller batches are lighter and steadier.
- `maxIterations` limits how many read/edit steps Dream can take while updating `SOUL.md`, `USER.md`, and `MEMORY.md`. It is a safety budget, not a quality score.
- `intervalH` is the normal way to configure Dream. Internally it runs as an `every` schedule, not as a cron expression.
Legacy note:
- Older source-based configs may still contain `dream.cron`. nanobot continues to honor it for backward compatibility, but new configs should use `intervalH`.
- Older source-based configs may still contain `dream.model`. nanobot continues to honor it for backward compatibility, but new configs should use `modelOverride`.
## In Practice ## In Practice

View File

@ -16,7 +16,7 @@ from nanobot.utils.helpers import ensure_dir, estimate_message_tokens, estimate_
from nanobot.agent.runner import AgentRunSpec, AgentRunner from nanobot.agent.runner import AgentRunSpec, AgentRunner
from nanobot.agent.tools.registry import ToolRegistry from nanobot.agent.tools.registry import ToolRegistry
from nanobot.utils.git_store import GitStore from nanobot.utils.gitstore import GitStore
if TYPE_CHECKING: if TYPE_CHECKING:
from nanobot.providers.base import LLMProvider from nanobot.providers.base import LLMProvider

View File

@ -781,20 +781,20 @@ def gateway(
console.print(f"[green]✓[/green] Heartbeat: every {hb_cfg.interval_s}s") console.print(f"[green]✓[/green] Heartbeat: every {hb_cfg.interval_s}s")
# Register Dream cron job (always-on, idempotent on restart) # Register Dream system job (always-on, idempotent on restart)
dream_cfg = config.agents.defaults.dream dream_cfg = config.agents.defaults.dream
if dream_cfg.model: if dream_cfg.model_override:
agent.dream.model = dream_cfg.model agent.dream.model = dream_cfg.model_override
agent.dream.max_batch_size = dream_cfg.max_batch_size agent.dream.max_batch_size = dream_cfg.max_batch_size
agent.dream.max_iterations = dream_cfg.max_iterations agent.dream.max_iterations = dream_cfg.max_iterations
from nanobot.cron.types import CronJob, CronPayload, CronSchedule from nanobot.cron.types import CronJob, CronPayload
cron.register_system_job(CronJob( cron.register_system_job(CronJob(
id="dream", id="dream",
name="dream", name="dream",
schedule=CronSchedule(kind="cron", expr=dream_cfg.cron, tz=config.agents.defaults.timezone), schedule=dream_cfg.build_schedule(config.agents.defaults.timezone),
payload=CronPayload(kind="system_event"), payload=CronPayload(kind="system_event"),
)) ))
console.print(f"[green]✓[/green] Dream: cron {dream_cfg.cron}") console.print(f"[green]✓[/green] Dream: {dream_cfg.describe_schedule()}")
async def run(): async def run():
try: try:

View File

@ -3,10 +3,12 @@
from pathlib import Path from pathlib import Path
from typing import Literal from typing import Literal
from pydantic import BaseModel, ConfigDict, Field from pydantic import AliasChoices, BaseModel, ConfigDict, Field
from pydantic.alias_generators import to_camel from pydantic.alias_generators import to_camel
from pydantic_settings import BaseSettings from pydantic_settings import BaseSettings
from nanobot.cron.types import CronSchedule
class Base(BaseModel): class Base(BaseModel):
"""Base model that accepts both camelCase and snake_case keys.""" """Base model that accepts both camelCase and snake_case keys."""
@ -31,11 +33,30 @@ class ChannelsConfig(Base):
class DreamConfig(Base): class DreamConfig(Base):
"""Dream memory consolidation configuration.""" """Dream memory consolidation configuration."""
cron: str = "0 */2 * * *" # Every 2 hours _HOUR_MS = 3_600_000
model: str | None = None # Override model for Dream
interval_h: int = Field(default=2, ge=1) # Every 2 hours by default
cron: str | None = Field(default=None, exclude=True) # Legacy compatibility override
model_override: str | None = Field(
default=None,
validation_alias=AliasChoices("modelOverride", "model", "model_override"),
) # Optional Dream-specific model override
max_batch_size: int = Field(default=20, ge=1) # Max history entries per run max_batch_size: int = Field(default=20, ge=1) # Max history entries per run
max_iterations: int = Field(default=10, ge=1) # Max tool calls per Phase 2 max_iterations: int = Field(default=10, ge=1) # Max tool calls per Phase 2
def build_schedule(self, timezone: str) -> CronSchedule:
"""Build the runtime schedule, preferring the legacy cron override if present."""
if self.cron:
return CronSchedule(kind="cron", expr=self.cron, tz=timezone)
return CronSchedule(kind="every", every_ms=self.interval_h * self._HOUR_MS)
def describe_schedule(self) -> str:
"""Return a human-readable summary for logs and startup output."""
if self.cron:
return f"cron {self.cron} (legacy)"
hours = self.interval_h
return f"every {hours}h"
class AgentDefaults(Base): class AgentDefaults(Base):
"""Default agent configuration.""" """Default agent configuration."""

View File

@ -457,7 +457,7 @@ def sync_workspace_templates(workspace: Path, silent: bool = False) -> list[str]
# Initialize git for memory version control # Initialize git for memory version control
try: try:
from nanobot.utils.git_store import GitStore from nanobot.utils.gitstore import GitStore
gs = GitStore(workspace, tracked_files=[ gs = GitStore(workspace, tracked_files=[
"SOUL.md", "USER.md", "memory/MEMORY.md", "SOUL.md", "USER.md", "memory/MEMORY.md",
]) ])

View File

@ -3,7 +3,7 @@
import pytest import pytest
from pathlib import Path from pathlib import Path
from nanobot.utils.git_store import GitStore, CommitInfo from nanobot.utils.gitstore import GitStore, CommitInfo
TRACKED = ["SOUL.md", "USER.md", "memory/MEMORY.md"] TRACKED = ["SOUL.md", "USER.md", "memory/MEMORY.md"]
@ -181,7 +181,7 @@ class TestShowCommitDiff:
class TestCommitInfoFormat: class TestCommitInfoFormat:
def test_format_with_diff(self): def test_format_with_diff(self):
from nanobot.utils.git_store import CommitInfo from nanobot.utils.gitstore import CommitInfo
c = CommitInfo(sha="abcd1234", message="test commit\nsecond line", timestamp="2026-04-02 12:00") c = CommitInfo(sha="abcd1234", message="test commit\nsecond line", timestamp="2026-04-02 12:00")
result = c.format(diff="some diff") result = c.format(diff="some diff")
assert "test commit" in result assert "test commit" in result
@ -189,7 +189,7 @@ class TestCommitInfoFormat:
assert "some diff" in result assert "some diff" in result
def test_format_without_diff(self): def test_format_without_diff(self):
from nanobot.utils.git_store import CommitInfo from nanobot.utils.gitstore import CommitInfo
c = CommitInfo(sha="abcd1234", message="test", timestamp="2026-04-02 12:00") c = CommitInfo(sha="abcd1234", message="test", timestamp="2026-04-02 12:00")
result = c.format() result = c.format()
assert "(no file changes)" in result assert "(no file changes)" in result

View File

@ -7,7 +7,7 @@ import pytest
from nanobot.bus.events import InboundMessage from nanobot.bus.events import InboundMessage
from nanobot.command.builtin import cmd_dream_log, cmd_dream_restore from nanobot.command.builtin import cmd_dream_log, cmd_dream_restore
from nanobot.command.router import CommandContext from nanobot.command.router import CommandContext
from nanobot.utils.git_store import CommitInfo from nanobot.utils.gitstore import CommitInfo
class _FakeStore: class _FakeStore:

View File

@ -0,0 +1,48 @@
from nanobot.config.schema import DreamConfig
def test_dream_config_defaults_to_interval_hours() -> None:
cfg = DreamConfig()
assert cfg.interval_h == 2
assert cfg.cron is None
def test_dream_config_builds_every_schedule_from_interval() -> None:
cfg = DreamConfig(interval_h=3)
schedule = cfg.build_schedule("UTC")
assert schedule.kind == "every"
assert schedule.every_ms == 3 * 3_600_000
assert schedule.expr is None
def test_dream_config_honors_legacy_cron_override() -> None:
cfg = DreamConfig.model_validate({"cron": "0 */4 * * *"})
schedule = cfg.build_schedule("UTC")
assert schedule.kind == "cron"
assert schedule.expr == "0 */4 * * *"
assert schedule.tz == "UTC"
assert cfg.describe_schedule() == "cron 0 */4 * * * (legacy)"
def test_dream_config_dump_uses_interval_h_and_hides_legacy_cron() -> None:
cfg = DreamConfig.model_validate({"intervalH": 5, "cron": "0 */4 * * *"})
dumped = cfg.model_dump(by_alias=True)
assert dumped["intervalH"] == 5
assert "cron" not in dumped
def test_dream_config_uses_model_override_name_and_accepts_legacy_model() -> None:
cfg = DreamConfig.model_validate({"model": "openrouter/sonnet"})
dumped = cfg.model_dump(by_alias=True)
assert cfg.model_override == "openrouter/sonnet"
assert dumped["modelOverride"] == "openrouter/sonnet"
assert "model" not in dumped