1212from collections .abc import Callable
1313
1414from brain import __version__
15+ from brain .bridge .provider import get_provider
16+ from brain .engines .dream import DreamEngine
17+ from brain .memory .hebbian import HebbianMatrix
18+ from brain .memory .store import MemoryStore
1519from brain .migrator .cli import build_parser as _build_migrate_parser
20+ from brain .paths import get_persona_dir
1621
1722# Subcommands the framework plans to ship. Each is a stub in Week 1;
1823# filled in across Weeks 2-8 as respective modules come online.
1924_STUB_COMMANDS : tuple [str , ...] = (
2025 "supervisor" ,
21- "dream" ,
2226 "heartbeat" ,
2327 "reflex" ,
2428 "status" ,
@@ -47,6 +51,52 @@ def _handler(args: argparse.Namespace) -> int:
4751 return _handler
4852
4953
54+ def _dream_handler (args : argparse .Namespace ) -> int :
55+ """Dispatch `nell dream` to the DreamEngine."""
56+ persona_dir = get_persona_dir (args .persona )
57+ if not persona_dir .exists ():
58+ raise FileNotFoundError (
59+ f"No persona directory at { persona_dir } — "
60+ f"run `nell migrate --install-as { args .persona } ` first."
61+ )
62+ # Nested try/finally so a HebbianMatrix open failure still closes the
63+ # already-open MemoryStore connection. Inline contextmanager would be
64+ # prettier but stores don't implement __enter__/__exit__ yet.
65+ store = MemoryStore (db_path = persona_dir / "memories.db" )
66+ try :
67+ hebbian = HebbianMatrix (db_path = persona_dir / "hebbian.db" )
68+ try :
69+ provider = get_provider (args .provider )
70+ engine = DreamEngine (
71+ store = store ,
72+ hebbian = hebbian ,
73+ embeddings = None ,
74+ provider = provider ,
75+ log_path = persona_dir / "dreams.log.jsonl" ,
76+ )
77+ result = engine .run_cycle (
78+ seed_id = args .seed ,
79+ lookback_hours = args .lookback ,
80+ depth = args .depth ,
81+ decay_per_hop = args .decay ,
82+ neighbour_limit = args .limit ,
83+ dry_run = args .dry_run ,
84+ )
85+ finally :
86+ hebbian .close ()
87+ finally :
88+ store .close ()
89+
90+ if args .dry_run :
91+ print ("Dry run — no writes." )
92+ print (f"Seed: { result .seed .id } ({ result .seed .content [:80 ]} )" )
93+ print (f"Neighbours: { len (result .neighbours )} " )
94+ print (f"Prompt preview:\n { result .prompt [:400 ]} " )
95+ else :
96+ print (result .dream_text or "" )
97+ return 0
98+
99+
50100def _build_parser () -> argparse .ArgumentParser :
51101 """Construct the top-level argparse parser with all stub subcommands."""
52102 parser = argparse .ArgumentParser (
@@ -69,6 +119,32 @@ def _build_parser() -> argparse.ArgumentParser:
69119
70120 _build_migrate_parser (subparsers )
71121
122+ dream_sub = subparsers .add_parser (
123+ "dream" ,
124+ help = "Run one dream cycle against a persona's memory store." ,
125+ )
126+ dream_sub .add_argument ("--persona" , default = "nell" , help = "Persona name (default: nell)." )
127+ dream_sub .add_argument (
128+ "--seed" , default = None , help = "Explicit seed memory id (default: auto-select)."
129+ )
130+ dream_sub .add_argument (
131+ "--provider" ,
132+ default = "claude-cli" ,
133+ help = "LLM provider: claude-cli (default), fake, ollama." ,
134+ )
135+ dream_sub .add_argument ("--dry-run" , action = "store_true" , help = "Skip LLM call and store writes." )
136+ dream_sub .add_argument (
137+ "--lookback" , type = int , default = 24 , help = "Hours of history to consider (default: 24)."
138+ )
139+ dream_sub .add_argument (
140+ "--depth" , type = int , default = 2 , help = "Spreading-activation depth (default: 2)."
141+ )
142+ dream_sub .add_argument ("--decay" , type = float , default = 0.5 , help = "Per-hop decay (default: 0.5)." )
143+ dream_sub .add_argument (
144+ "--limit" , type = int , default = 8 , help = "Max neighbours in prompt (default: 8)."
145+ )
146+ dream_sub .set_defaults (func = _dream_handler )
147+
72148 return parser
73149
74150
0 commit comments