mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-06-13 22:34:06 +00:00
* refactor(dream): replace two-phase Dream class with simple cron + process_direct - Remove the heavyweight Dream class (AgentRunner-based two-phase system) from nanobot/agent/memory.py - Delete dream_phase1.md and dream_phase2.md templates - New dream.md template serves as the consolidation prompt - Cron callback uses agent.process_direct(prompt, session_key=\"dream\") instead of agent.dream.run() - Always performs git auto_commit after execution - /dream command updated to use process_direct + git commit - DreamConfig kept for backward compatibility; deprecated fields (model_override, max_batch_size, max_iterations, annotate_line_ages) are ignored but accepted in config - interval_h remains configurable via agents.defaults.dream.interval_h - Update tests and webui settings to match new architecture * feat(loop): add ephemeral mode to process_direct, skip history writes for Dream When ephemeral=True, _state_save skips enforce_file_cap (which calls raw_archive -> append_history) and consolidator.maybe_consolidate_by_tokens. This prevents Dream sessions from creating a positive feedback loop where they process their own output. The session IS still saved to disk. * fix(loop): skip extra hooks for ephemeral sessions (Dream) * feat(dream): per-run timestamped sessions with rotation for WebUI * test(config): restore DreamConfig schedule and alias tests * fix(dream): include LLM response summary in git auto-commit message The old two-phase Dream class included the Phase 1 analysis in the git commit message body. The new single-phase version lost this. Restore it by extracting resp.content from the process_direct return value and appending it to the commit message in both the cron handler and the /dream command. * fix(test): accept ephemeral kwarg in test_openai_api fake_process * refactor(dream): merge dream_session.py into MemoryStore The standalone dream_session.py module only contained three small helpers that all revolve around MemoryStore concerns (session keys, commit messages, file pruning). Fold them into MemoryStore as @staticmethod to reduce indirection and avoid a 35-line module with no independent reason to exist. * fix(test): address code review — patch correct instance, use actual function - Fix test_ephemeral_skips_raw_archive to patch loop.context.memory instead of the fixture's separate MemoryStore instance - Fix TestDreamCommitMessage to call MemoryStore.build_dream_commit_message instead of reimplementing the logic inline - Move Dream helpers in memory.py above the Consolidator section comment to avoid misleading visual boundary * fix(dream): gate cursor advancement and restrict tools maintainer edit: Dream now processes backlog from the oldest unprocessed entries, only advances the cursor after a completed ephemeral run, and uses a restricted file-only tool registry for background consolidation. * fix(dream): skip idle compact for dream sessions Dream runs use internal dream:* sessions that are pruned by Dream retention. Exclude them from AutoCompact scheduling, archive execution, and summary injection so idle-session compaction cannot truncate Dream transcripts. * fix(dream): keep batched history isolated * feat(dream): tag archived memory for single-phase Dream --------- Co-authored-by: Xubin Ren <52506698+Re-bin@users.noreply.github.com>
102 lines
3.6 KiB
Python
102 lines
3.6 KiB
Python
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from unittest.mock import MagicMock
|
|
|
|
from nanobot.agent.loop import AgentLoop
|
|
from nanobot.bus.queue import MessageBus
|
|
from nanobot.config.loader import save_config
|
|
from nanobot.config.schema import Config
|
|
from nanobot.providers.factory import ProviderSnapshot, load_provider_snapshot
|
|
from nanobot.webui.settings_api import update_agent_settings
|
|
|
|
|
|
def _provider(default_model: str, max_tokens: int = 123) -> MagicMock:
|
|
provider = MagicMock()
|
|
provider.get_default_model.return_value = default_model
|
|
provider.generation = SimpleNamespace(max_tokens=max_tokens)
|
|
return provider
|
|
|
|
|
|
def test_provider_refresh_updates_all_model_dependents(tmp_path: Path) -> None:
|
|
old_provider = _provider("old-model")
|
|
new_provider = _provider("new-model", max_tokens=456)
|
|
loop = AgentLoop(
|
|
bus=MessageBus(),
|
|
provider=old_provider,
|
|
workspace=tmp_path,
|
|
model="old-model",
|
|
context_window_tokens=1000,
|
|
provider_snapshot_loader=lambda: ProviderSnapshot(
|
|
provider=new_provider,
|
|
model="new-model",
|
|
context_window_tokens=2000,
|
|
signature=("new-model",),
|
|
),
|
|
)
|
|
|
|
loop._refresh_provider_snapshot()
|
|
|
|
assert loop.provider is new_provider
|
|
assert loop.model == "new-model"
|
|
assert loop.context_window_tokens == 2000
|
|
assert loop.runner.provider is new_provider
|
|
assert loop.subagents.provider is new_provider
|
|
assert loop.subagents.model == "new-model"
|
|
assert loop.subagents.runner.provider is new_provider
|
|
assert loop.consolidator.provider is new_provider
|
|
assert loop.consolidator.model == "new-model"
|
|
assert loop.consolidator.context_window_tokens == 2000
|
|
assert loop.consolidator.max_completion_tokens == 456
|
|
|
|
|
|
def test_llm_runtime_refreshes_provider_snapshot(tmp_path: Path) -> None:
|
|
old_provider = _provider("old-model")
|
|
new_provider = _provider("new-model", max_tokens=456)
|
|
loop = AgentLoop(
|
|
bus=MessageBus(),
|
|
provider=old_provider,
|
|
workspace=tmp_path,
|
|
model="old-model",
|
|
context_window_tokens=1000,
|
|
provider_snapshot_loader=lambda: ProviderSnapshot(
|
|
provider=new_provider,
|
|
model="new-model",
|
|
context_window_tokens=2000,
|
|
signature=("new-model",),
|
|
),
|
|
)
|
|
|
|
runtime = loop.llm_runtime()
|
|
|
|
assert runtime.provider is new_provider
|
|
assert runtime.model == "new-model"
|
|
assert loop.provider is new_provider
|
|
assert loop.runner.provider is new_provider
|
|
|
|
|
|
def test_settings_context_window_refreshes_runtime_state(
|
|
tmp_path: Path,
|
|
monkeypatch,
|
|
) -> None:
|
|
config_path = tmp_path / "config.json"
|
|
config = Config()
|
|
config.agents.defaults.workspace = str(tmp_path / "workspace")
|
|
config.agents.defaults.model = "openai/gpt-4o"
|
|
config.agents.defaults.provider = "openai"
|
|
config.agents.defaults.context_window_tokens = 65_536
|
|
config.providers.openai.api_key = "sk-test"
|
|
save_config(config, config_path)
|
|
monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)
|
|
|
|
def loader(*, preset_name: str | None = None) -> ProviderSnapshot:
|
|
return load_provider_snapshot(config_path, preset_name=preset_name)
|
|
|
|
loop = AgentLoop.from_config(config, provider_snapshot_loader=loader)
|
|
|
|
payload = update_agent_settings({"context_window_tokens": ["262144"]})
|
|
loop._refresh_provider_snapshot()
|
|
|
|
assert payload["requires_restart"] is False
|
|
assert loop.context_window_tokens == 262_144
|
|
assert loop.consolidator.context_window_tokens == 262_144
|