feat: era-rollup + narrative engine (consolidation steps 3-4)

Complete the consolidation pipeline: summaries -> profile + eras -> narrative.

- memory: eras table (per-month digests) + Era, summaries_by_month, store_era,
  list_eras, recall_eras; narrative table + set/get_narrative
- lyra/era.py (lyra-era): groups session gists by the month the session occurred
  (real timestamps) and map-reduces each month into a "what was happening" digest
- lyra/narrative.py (lyra-narrative): distills profile + recent eras into the
  current arc/trends/callbacks ("remember when…", "you're trending toward…")
- chat.build_messages injects the narrative alongside the profile

Verified on the real corpus: 17 monthly eras (Dec 2024-Jun 2026) + a narrative
that surfaces specific callbacks (the $573 Hollywood session, 4 years sober).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-16 19:28:01 +00:00
parent d7e2fce694
commit bfb81428ab
5 changed files with 277 additions and 0 deletions
+66
View File
@@ -0,0 +1,66 @@
"""Narrative engine (consolidation step 4): the current arc, trends, callbacks.
Where the profile is timeless ("who Brian is"), the narrative is time-aware
("what's going on lately, where things are trending"). It distills the profile
plus the most recent monthly era digests into the current story — recent focus,
notable trends or changes, mood/arc, and a few specific callbacks worth
referencing. Injected into chat so Lyra follows along like a friend who's been
paying attention. Runs on the consolidation backend (MI50 in steady state).
"""
from __future__ import annotations
from lyra import config, llm, logbus, memory
from lyra.llm import Backend, Message
RECENT_ERAS = 4
_PROMPT = """You are distilling the CURRENT narrative about Brian — what a close \
friend who has been following along would keep in mind right now. From his profile \
and recent monthly digests below, write: what he's been focused on lately, any \
notable trends or changes (improving, slipping, new patterns), his current arc and \
mood, and 2-4 specific things worth referencing back to him ("remember when…"). \
Third person, referring to him as "Brian". 6-10 sentences. This is a memory note, \
not a reply. No preamble."""
def rebuild_narrative(backend: Backend | None = None) -> str | None:
"""(Re)derive the current narrative from the profile + recent era digests."""
backend = backend or config.load().summary_backend
profile = memory.get_profile()
eras = memory.list_eras()
if not profile and not eras:
return None
parts = []
if profile:
parts.append("PROFILE (timeless):\n" + profile)
recent = eras[-RECENT_ERAS:]
if recent:
parts.append(
"RECENT MONTHS (oldest first):\n"
+ "\n\n".join(f"[{e.month}]\n{e.content}" for e in recent)
)
body = "\n\n".join(parts)
messages: list[Message] = [
{"role": "system", "content": _PROMPT},
{"role": "user", "content": body},
]
narrative = llm.complete(messages, backend=backend)
memory.set_narrative(narrative)
logbus.log("info", "narrative rebuilt", chars=len(narrative), eras=len(recent))
return narrative
def main() -> int:
narrative = rebuild_narrative()
if narrative is None:
print("Need a profile and/or eras first — run lyra-profile and lyra-era.")
return 1
print(narrative)
return 0
if __name__ == "__main__":
raise SystemExit(main())