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:
@@ -0,0 +1,78 @@
|
||||
"""Dream-cycle tests: backlog sensing + a full forced pass, with LLM/embeddings
|
||||
stubbed so nothing hits a real backend."""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def lyra(tmp_path, monkeypatch):
|
||||
"""A fresh Lyra wired to a temp DB with stubbed embeddings + LLM."""
|
||||
monkeypatch.setenv("LYRA_DB_PATH", str(tmp_path / "test.db"))
|
||||
monkeypatch.setenv("SUMMARY_BACKEND", "local")
|
||||
|
||||
from lyra import llm
|
||||
# Deterministic 3-d embeddings; content-insensitive is fine for storage tests.
|
||||
monkeypatch.setattr(llm, "embed", lambda texts: [[0.1, 0.2, 0.3] for _ in texts])
|
||||
# reflect() expects JSON back; everything else just stores the text.
|
||||
monkeypatch.setattr(
|
||||
llm, "complete",
|
||||
lambda messages, backend=None, model=None:
|
||||
'{"mood":"focused","valence":0.7,"new_reflections":["I got some thinking done."]}',
|
||||
)
|
||||
|
||||
import lyra.memory as memory
|
||||
importlib.reload(memory) # drop any cached connection from another test/db
|
||||
return memory
|
||||
|
||||
|
||||
def _seed(memory, session_id, n, summarized_up_to=None):
|
||||
ids = [memory.remember(session_id, "user", f"msg {i}") for i in range(n)]
|
||||
if summarized_up_to is not None:
|
||||
memory.store_summary(session_id, "gist", ids[summarized_up_to])
|
||||
return ids
|
||||
|
||||
|
||||
def test_backlog_stats(lyra):
|
||||
memory = lyra
|
||||
_seed(memory, "s-fresh", 5) # never summarized -> ripe
|
||||
_seed(memory, "s-ripe", 25, summarized_up_to=0) # 24 new turns -> ripe
|
||||
_seed(memory, "s-clean", 3, summarized_up_to=2) # caught up -> not dirty
|
||||
|
||||
stats = memory.backlog_stats(ripe_threshold=20)
|
||||
assert stats["sessions"] == 3
|
||||
assert stats["dirty"] == 2
|
||||
assert stats["ripe"] == 2
|
||||
assert stats["max_exchange_id"] == 33
|
||||
|
||||
|
||||
def test_dream_cycle_consolidates_and_persists(lyra):
|
||||
memory = lyra
|
||||
from lyra import dream
|
||||
|
||||
# A big backlog: enough never-summarized sessions that continuity saturates
|
||||
# and the resulting fresh gists push coherence past threshold too.
|
||||
for k in range(7):
|
||||
_seed(memory, f"s{k}", 4)
|
||||
|
||||
state = dream.dream_cycle(force=False)
|
||||
|
||||
# continuity built up and fired -> sessions got summarized
|
||||
assert len(memory.list_summaries()) == 7
|
||||
acts = state["dream"]["last_actions"]
|
||||
assert any("consolidated" in a for a in acts)
|
||||
# 7 fresh gists -> coherence crossed threshold -> profile got integrated
|
||||
assert any("integrated" in a for a in acts)
|
||||
assert memory.get_profile() is not None
|
||||
|
||||
# drives + bookkeeping persisted and reload-able
|
||||
assert set(state["drives"]) == {"continuity", "coherence", "curiosity", "stability"}
|
||||
assert state["dream"]["cycle_count"] == 1
|
||||
assert memory.get_self_state()["dream"]["last_exchange_id"] == 28
|
||||
|
||||
# a second pass with no new activity should rest (continuity relieved)
|
||||
state2 = dream.dream_cycle(force=False)
|
||||
assert state2["dream"]["cycle_count"] == 2
|
||||
assert state2["drives"]["continuity"] == 0.0
|
||||
Reference in New Issue
Block a user