From 0a3a60a7a472bf137aa9ae7ba345554807319f05 Mon Sep 17 00:00:00 2001 From: Xubin Ren Date: Sat, 4 Apr 2026 10:01:45 +0000 Subject: [PATCH] refactor(memory): simplify Dream config naming and rename gitstore module --- docs/MEMORY.md | 28 ++++++++---- nanobot/agent/memory.py | 2 +- nanobot/cli/commands.py | 12 +++--- nanobot/config/schema.py | 27 ++++++++++-- nanobot/utils/{git_store.py => gitstore.py} | 0 nanobot/utils/helpers.py | 2 +- tests/agent/test_git_store.py | 6 +-- tests/command/test_builtin_dream.py | 2 +- tests/config/test_dream_config.py | 48 +++++++++++++++++++++ 9 files changed, 104 insertions(+), 23 deletions(-) rename nanobot/utils/{git_store.py => gitstore.py} (100%) create mode 100644 tests/config/test_dream_config.py diff --git a/docs/MEMORY.md b/docs/MEMORY.md index ee3b91da7..414fcdca6 100644 --- a/docs/MEMORY.md +++ b/docs/MEMORY.md @@ -149,10 +149,10 @@ Dream is configured under `agents.defaults.dream`: "agents": { "defaults": { "dream": { - "cron": "0 */2 * * *", - "model": null, - "max_batch_size": 20, - "max_iterations": 10 + "intervalH": 2, + "modelOverride": null, + "maxBatchSize": 20, + "maxIterations": 10 } } } @@ -161,10 +161,22 @@ Dream is configured under `agents.defaults.dream`: | Field | Meaning | |-------|---------| -| `cron` | How often Dream runs | -| `model` | Optional model override for Dream | -| `max_batch_size` | How many history entries Dream processes per run | -| `max_iterations` | The tool budget for Dream's editing phase | +| `intervalH` | How often Dream runs, in hours | +| `modelOverride` | Optional Dream-specific model override | +| `maxBatchSize` | How many history entries Dream processes per run | +| `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 diff --git a/nanobot/agent/memory.py b/nanobot/agent/memory.py index cbaabf752..c00afaadb 100644 --- a/nanobot/agent/memory.py +++ b/nanobot/agent/memory.py @@ -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.tools.registry import ToolRegistry -from nanobot.utils.git_store import GitStore +from nanobot.utils.gitstore import GitStore if TYPE_CHECKING: from nanobot.providers.base import LLMProvider diff --git a/nanobot/cli/commands.py b/nanobot/cli/commands.py index e2b21a238..88f13215c 100644 --- a/nanobot/cli/commands.py +++ b/nanobot/cli/commands.py @@ -781,20 +781,20 @@ def gateway( 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 - if dream_cfg.model: - agent.dream.model = dream_cfg.model + if dream_cfg.model_override: + agent.dream.model = dream_cfg.model_override agent.dream.max_batch_size = dream_cfg.max_batch_size 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( id="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"), )) - console.print(f"[green]✓[/green] Dream: cron {dream_cfg.cron}") + console.print(f"[green]✓[/green] Dream: {dream_cfg.describe_schedule()}") async def run(): try: diff --git a/nanobot/config/schema.py b/nanobot/config/schema.py index e8d6db11c..0999bd99e 100644 --- a/nanobot/config/schema.py +++ b/nanobot/config/schema.py @@ -3,10 +3,12 @@ from pathlib import Path 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_settings import BaseSettings +from nanobot.cron.types import CronSchedule + class Base(BaseModel): """Base model that accepts both camelCase and snake_case keys.""" @@ -31,11 +33,30 @@ class ChannelsConfig(Base): class DreamConfig(Base): """Dream memory consolidation configuration.""" - cron: str = "0 */2 * * *" # Every 2 hours - model: str | None = None # Override model for Dream + _HOUR_MS = 3_600_000 + + 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_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): """Default agent configuration.""" diff --git a/nanobot/utils/git_store.py b/nanobot/utils/gitstore.py similarity index 100% rename from nanobot/utils/git_store.py rename to nanobot/utils/gitstore.py diff --git a/nanobot/utils/helpers.py b/nanobot/utils/helpers.py index d82037c00..93293c9e0 100644 --- a/nanobot/utils/helpers.py +++ b/nanobot/utils/helpers.py @@ -457,7 +457,7 @@ def sync_workspace_templates(workspace: Path, silent: bool = False) -> list[str] # Initialize git for memory version control try: - from nanobot.utils.git_store import GitStore + from nanobot.utils.gitstore import GitStore gs = GitStore(workspace, tracked_files=[ "SOUL.md", "USER.md", "memory/MEMORY.md", ]) diff --git a/tests/agent/test_git_store.py b/tests/agent/test_git_store.py index 285e7803b..07cfa7919 100644 --- a/tests/agent/test_git_store.py +++ b/tests/agent/test_git_store.py @@ -3,7 +3,7 @@ import pytest 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"] @@ -181,7 +181,7 @@ class TestShowCommitDiff: class TestCommitInfoFormat: 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") result = c.format(diff="some diff") assert "test commit" in result @@ -189,7 +189,7 @@ class TestCommitInfoFormat: assert "some diff" in result 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") result = c.format() assert "(no file changes)" in result diff --git a/tests/command/test_builtin_dream.py b/tests/command/test_builtin_dream.py index 215fc7a47..7b1835feb 100644 --- a/tests/command/test_builtin_dream.py +++ b/tests/command/test_builtin_dream.py @@ -7,7 +7,7 @@ import pytest from nanobot.bus.events import InboundMessage from nanobot.command.builtin import cmd_dream_log, cmd_dream_restore from nanobot.command.router import CommandContext -from nanobot.utils.git_store import CommitInfo +from nanobot.utils.gitstore import CommitInfo class _FakeStore: diff --git a/tests/config/test_dream_config.py b/tests/config/test_dream_config.py new file mode 100644 index 000000000..9266792bf --- /dev/null +++ b/tests/config/test_dream_config.py @@ -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