feat: dream cycle — drives-driven unattended consolidation + reflection

Lyra's inner loop for when no one's talking to her. Each pass senses her own
backlog/novelty, lets four drives build from real signals, and acts on those
past threshold:
- continuity -> summarize sessions with new turns
- coherence  -> rebuild profile/eras/narrative (stale once new gists land)
- curiosity  -> reflect() and evolve the self-state
- stability  -> readout of how caught-up she ended up

Drives are rendered into chat context so she can feel them. Causal chain:
consolidation creates gists -> coherence rises -> integration fires next.

- lyra/dream.py: dream_cycle() + lyra-dream CLI (--force, --loop SECONDS)
- memory: backlog_stats(), profile_sessions_covered(), WAL + busy_timeout
  so a separate dream process coexists with the web server
- self_state: DEFAULT_DRIVES baseline + drives in render_for_context
- tests/test_dream.py: backlog sensing + a full forced pass (LLM stubbed)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-17 00:52:44 +00:00
parent f89849801b
commit 4f40e2d57e
5 changed files with 307 additions and 7 deletions
+23 -7
View File
@@ -17,6 +17,15 @@ from lyra.llm import Backend, Message
MAX_REFLECTIONS = 6
# Lyra's drives — standing pressures the dream cycle lets build up and act on.
# (Mood axes above are how she *feels*; drives are what's *pulling at her*.)
DEFAULT_DRIVES: dict = {
"continuity": 0.3, # don't lose the thread of recent activity
"coherence": 0.3, # keep my understanding of Brian integrated/current
"curiosity": 0.5, # think, notice, reflect
"stability": 0.7, # am I caught up / calm
}
# Starting state for a fresh Lyra — identity-aware, first person.
DEFAULT_STATE: dict = {
"mood": "curious",
@@ -24,6 +33,7 @@ DEFAULT_STATE: dict = {
"energy": 0.75,
"confidence": 0.55,
"curiosity": 0.7,
"drives": dict(DEFAULT_DRIVES),
"self_narrative": (
"I'm Lyra — an AI Brian built. I started life as Cortex/NeoMem and I'm "
"being rebuilt now into something new. I'm his poker copilot and his "
@@ -62,15 +72,21 @@ def load() -> dict:
def render_for_context(state: dict) -> str:
"""Format the self-state as Lyra's interiority for the prompt."""
refs = state.get("reflections") or []
return (
"Your inner state right now (you're continuous — speak from this, don't reset):\n"
lines = [
"Your inner state right now (you're continuous — speak from this, don't reset):",
f"- mood: {state.get('mood')} "
f"(valence {state.get('valence')}, energy {state.get('energy')}, "
f"confidence {state.get('confidence')}, curiosity {state.get('curiosity')})\n"
f"- Who you are right now: {state.get('self_narrative')}\n"
f"- You and Brian: {state.get('relationship')}\n"
+ (f"- On your mind lately: {' | '.join(refs[-3:])}" if refs else "")
)
f"confidence {state.get('confidence')}, curiosity {state.get('curiosity')})",
f"- Who you are right now: {state.get('self_narrative')}",
f"- You and Brian: {state.get('relationship')}",
]
drives = state.get("drives") or {}
if drives:
ds = ", ".join(f"{k} {float(v):.2f}" for k, v in drives.items())
lines.append(f"- What's pulling at you (drives): {ds}")
if refs:
lines.append(f"- On your mind lately: {' | '.join(refs[-3:])}")
return "\n".join(lines)
def _safe_json(s: str) -> dict | None: