Files
project-lyra/lyra/summary.py
T
serversdown d7c258eba0 feat: tiered, compacting memory (phase 1.5)
Older sessions fade to a general idea; details stay retrievable.

- memory: summaries table (one compacted gist per session, embedded), plus
  store_summary/get_summary/recall_summaries and unsummarized_count (tracks
  exchanges newer than the current summary)
- lyra/summary.py: summarize_session compacts a session's raw turns into a
  third-person gist (default SUMMARY_BACKEND=local, so compaction is free);
  maybe_summarize re-summarizes once SUMMARIZE_AFTER new turns accumulate
- chat.build_messages now layers context in tiers: persona -> gists of other
  sessions -> a few sharp raw cross-session details -> current session raw
  turns -> new message; respond() compacts the session after each turn
- web: POST /sessions/{id}/summarize to compact on demand
- summarization activity surfaces in the live log

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 18:52:58 +00:00

57 lines
2.2 KiB
Python

"""Session summarization: compact a session's raw exchanges into a stored gist.
This is the compaction half of the tiered memory. Raw exchanges stay for detail
recall; the summary is what surfaces when an *older* session is recalled later —
"a month ago is a general idea," per the design.
"""
from __future__ import annotations
from lyra import config, llm, logbus, memory
from lyra.llm import Backend
# Re-summarize a session once it has accumulated this many new raw exchanges
# beyond what its current summary covers.
SUMMARIZE_AFTER = 20
_PROMPT = """You are compacting a conversation into a long-term memory record \
(not replying to anyone). Write a concise gist of the session below: what was \
discussed, key decisions or outcomes, concrete specifics worth keeping (names, \
places, numbers, hands), and the user's apparent mood/state. Third person, \
referring to the user as "Brian". 4-8 sentences. No preamble."""
def _transcript(exchanges: list[memory.Exchange]) -> str:
return "\n".join(f"{ex.role}: {ex.content}" for ex in exchanges)
def summarize_session(session_id: str, backend: Backend | None = None) -> str | None:
"""(Re)generate and store the gist for a session. Returns the summary text.
Returns None if the session has no exchanges. The summarizer defaults to the
local backend so routine compaction stays free.
"""
exchanges = memory.history(session_id)
if not exchanges:
return None
backend = backend or config.load().summary_backend
messages = [
{"role": "system", "content": _PROMPT},
{"role": "user", "content": _transcript(exchanges)},
]
gist = llm.complete(messages, backend=backend)
last_id = exchanges[-1].id
memory.store_summary(session_id, gist, last_id)
logbus.log(
"info", "summarized session", session=session_id,
exchanges=len(exchanges), backend=backend,
)
return gist
def maybe_summarize(session_id: str, backend: Backend | None = None) -> None:
"""Summarize the session if enough new turns have accumulated since last time."""
if memory.unsummarized_count(session_id) >= SUMMARIZE_AFTER:
summarize_session(session_id, backend=backend)