Spaces:
Sleeping
Sleeping
yc1838 commited on
Commit ·
b04f018
1
Parent(s): f2975c9
docs(superpowers): add LangMem persistent memory design and plan
Browse files
docs/superpowers/plans/2026-04-26-langmem-persistent-memory-plan.md
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# LangMem Persistent Memory Implementation Plan
|
| 2 |
+
|
| 3 |
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
| 4 |
+
|
| 5 |
+
**Goal:** Implement a three-tiered persistent memory system (Short-term, Semantic, Episodic) for the Lilith Agent using LangGraph's SqliteSaver and LangMem.
|
| 6 |
+
|
| 7 |
+
**Architecture:**
|
| 8 |
+
1. Replace in-memory `MemorySaver` with `SqliteSaver` pointing to `.lilith/threads.sqlite` for short-term thread persistence.
|
| 9 |
+
2. Integrate `langmem` to extract facts (Semantic) and task summaries (Episodic) in the background at the end of runs, compressing/merging them to prevent bloat.
|
| 10 |
+
3. Query the LangMem vector store at the start of new tasks and inject relevant context into the initial `SystemMessage`.
|
| 11 |
+
|
| 12 |
+
**Tech Stack:** `langgraph-checkpoint-sqlite`, `langmem`, `sqlite3`, `chromadb` (or `langmem` default local storage)
|
| 13 |
+
|
| 14 |
+
---
|
| 15 |
+
|
| 16 |
+
### Task 1: Setup Dependencies and Short-Term Thread Persistence
|
| 17 |
+
|
| 18 |
+
**Files:**
|
| 19 |
+
- Modify: `pyproject.toml:10-33`
|
| 20 |
+
- Modify: `src/lilith_agent/app.py`
|
| 21 |
+
- Modify: `src/lilith_agent/tui.py`
|
| 22 |
+
- Create: `tests/test_memory_persistence.py`
|
| 23 |
+
|
| 24 |
+
- [ ] **Step 1: Add new dependencies**
|
| 25 |
+
|
| 26 |
+
```bash
|
| 27 |
+
# Update pyproject.toml to include dependencies
|
| 28 |
+
sed -i '' '/"langgraph>=1.0,<2.0",/a \
|
| 29 |
+
"langgraph-checkpoint-sqlite>=1.0,<2.0",\
|
| 30 |
+
"langmem>=0.0.1",
|
| 31 |
+
' pyproject.toml
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
- [ ] **Step 2: Write test for SqliteSaver initialization**
|
| 35 |
+
|
| 36 |
+
```python
|
| 37 |
+
# tests/test_memory_persistence.py
|
| 38 |
+
import pytest
|
| 39 |
+
from pathlib import Path
|
| 40 |
+
from lilith_agent.config import Config
|
| 41 |
+
from lilith_agent.app import build_react_agent
|
| 42 |
+
|
| 43 |
+
def test_build_react_agent_uses_sqlite_saver(tmp_path):
|
| 44 |
+
cfg = Config(
|
| 45 |
+
cheap_provider="google",
|
| 46 |
+
cheap_model="gemini-3-flash-preview",
|
| 47 |
+
strong_provider="google",
|
| 48 |
+
strong_model="gemini-3-flash-preview",
|
| 49 |
+
extra_strong_provider="google",
|
| 50 |
+
extra_strong_model="gemini-3-flash-preview"
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
# Temporarily override where the agent looks for the .lilith dir
|
| 54 |
+
import os
|
| 55 |
+
os.environ["LILITH_HOME"] = str(tmp_path / ".lilith")
|
| 56 |
+
|
| 57 |
+
agent = build_react_agent(cfg)
|
| 58 |
+
|
| 59 |
+
assert agent.checkpointer is not None
|
| 60 |
+
assert type(agent.checkpointer).__name__ == "SqliteSaver"
|
| 61 |
+
|
| 62 |
+
# Check if DB file was created
|
| 63 |
+
db_path = tmp_path / ".lilith" / "threads.sqlite"
|
| 64 |
+
assert db_path.exists()
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
- [ ] **Step 3: Run the failing test**
|
| 68 |
+
|
| 69 |
+
Run: `pytest tests/test_memory_persistence.py -v`
|
| 70 |
+
Expected: FAIL
|
| 71 |
+
|
| 72 |
+
- [ ] **Step 4: Implement SqliteSaver in app.py**
|
| 73 |
+
|
| 74 |
+
Modify `src/lilith_agent/app.py`:
|
| 75 |
+
|
| 76 |
+
```python
|
| 77 |
+
# Add imports
|
| 78 |
+
import os
|
| 79 |
+
from pathlib import Path
|
| 80 |
+
from langgraph.checkpoint.sqlite import SqliteSaver
|
| 81 |
+
import sqlite3
|
| 82 |
+
|
| 83 |
+
# Modify build_react_agent
|
| 84 |
+
def build_react_agent(cfg: Config):
|
| 85 |
+
# ... existing code ...
|
| 86 |
+
|
| 87 |
+
graph = StateGraph(AgentState)
|
| 88 |
+
# ... existing graph node/edge setup ...
|
| 89 |
+
|
| 90 |
+
# Setup SQLite Saver
|
| 91 |
+
lilith_home = Path(os.getenv("LILITH_HOME", ".lilith"))
|
| 92 |
+
lilith_home.mkdir(parents=True, exist_ok=True)
|
| 93 |
+
db_path = lilith_home / "threads.sqlite"
|
| 94 |
+
|
| 95 |
+
# Note: in a production app you might manage connection lifecycle differently,
|
| 96 |
+
# but for CLI a persistent connection during the app lifetime works.
|
| 97 |
+
conn = sqlite3.connect(str(db_path), check_same_thread=False)
|
| 98 |
+
memory_saver = SqliteSaver(conn)
|
| 99 |
+
|
| 100 |
+
compiled = graph.compile(checkpointer=memory_saver)
|
| 101 |
+
return compiled.with_config({"recursion_limit": cfg.recursion_limit})
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
- [ ] **Step 5: Run the test again to verify it passes**
|
| 105 |
+
|
| 106 |
+
Run: `pytest tests/test_memory_persistence.py -v`
|
| 107 |
+
Expected: PASS
|
| 108 |
+
|
| 109 |
+
- [ ] **Step 6: Commit**
|
| 110 |
+
|
| 111 |
+
```bash
|
| 112 |
+
git add pyproject.toml src/lilith_agent/app.py tests/test_memory_persistence.py
|
| 113 |
+
git commit -m "feat: replace MemorySaver with SqliteSaver for thread persistence"
|
| 114 |
+
```
|
| 115 |
+
|
| 116 |
+
---
|
| 117 |
+
|
| 118 |
+
### Task 2: Implement Semantic Memory Extraction (LangMem Background)
|
| 119 |
+
|
| 120 |
+
**Files:**
|
| 121 |
+
- Create: `src/lilith_agent/memory.py`
|
| 122 |
+
- Modify: `src/lilith_agent/app.py`
|
| 123 |
+
|
| 124 |
+
- [ ] **Step 1: Create memory.py module with extraction logic**
|
| 125 |
+
|
| 126 |
+
```python
|
| 127 |
+
# src/lilith_agent/memory.py
|
| 128 |
+
import os
|
| 129 |
+
import logging
|
| 130 |
+
from pathlib import Path
|
| 131 |
+
import langmem
|
| 132 |
+
from typing import List, Dict, Any
|
| 133 |
+
from langchain_core.messages import BaseMessage, AIMessage, HumanMessage
|
| 134 |
+
|
| 135 |
+
log = logging.getLogger(__name__)
|
| 136 |
+
|
| 137 |
+
# Initialize local langmem client
|
| 138 |
+
lilith_home = Path(os.getenv("LILITH_HOME", ".lilith"))
|
| 139 |
+
langmem.init(local_dir=str(lilith_home / "memory"))
|
| 140 |
+
|
| 141 |
+
def extract_and_compress_facts(messages: List[BaseMessage], model) -> None:
|
| 142 |
+
"""
|
| 143 |
+
Extracts new facts from the conversation and merges/compresses them
|
| 144 |
+
with existing semantic memory to prevent bloat.
|
| 145 |
+
"""
|
| 146 |
+
log.info("[memory] Extracting semantic facts from thread...")
|
| 147 |
+
try:
|
| 148 |
+
# Convert messages to dict format expected by some extraction prompts
|
| 149 |
+
conv_str = "\n".join([f"{m.type}: {m.content}" for m in messages if m.content])
|
| 150 |
+
|
| 151 |
+
# We use a simple prompt to extract facts and deduplicate.
|
| 152 |
+
# In a full langmem setup, we'd use their create_memory_manager or memory schemas.
|
| 153 |
+
# For this local implementation, we'll use a direct extraction approach:
|
| 154 |
+
|
| 155 |
+
prompt = f"""
|
| 156 |
+
Extract any persistent facts, preferences, or knowledge about the user, the project,
|
| 157 |
+
or the environment from this conversation.
|
| 158 |
+
Focus ONLY on static knowledge (e.g., 'User prefers Python', 'API Key is X').
|
| 159 |
+
Ignore dynamic reasoning or temporary states.
|
| 160 |
+
|
| 161 |
+
Conversation:
|
| 162 |
+
{conv_str}
|
| 163 |
+
|
| 164 |
+
Output as a JSON list of strings. If no facts, output [].
|
| 165 |
+
"""
|
| 166 |
+
|
| 167 |
+
response = model.invoke(prompt)
|
| 168 |
+
# Parse JSON and save via langmem (Implementation detail depends on langmem API version)
|
| 169 |
+
# Placeholder for actual langmem SDK call:
|
| 170 |
+
# facts = json.loads(response.content)
|
| 171 |
+
# for fact in facts:
|
| 172 |
+
# langmem.save_fact(content=fact, namespace="lilith_semantic")
|
| 173 |
+
|
| 174 |
+
log.info("[memory] Extraction complete.")
|
| 175 |
+
except Exception as e:
|
| 176 |
+
log.error(f"[memory] Failed to extract facts: {e}")
|
| 177 |
+
|
| 178 |
+
```
|
| 179 |
+
|
| 180 |
+
- [ ] **Step 2: Hook up extraction in the graph (End of run)**
|
| 181 |
+
|
| 182 |
+
Modify `src/lilith_agent/app.py` to trigger this after the final answer.
|
| 183 |
+
(Since the current graph just returns END when there are no tool calls, we can wrap the invocation or add an `extract_memory` node that runs before END).
|
| 184 |
+
|
| 185 |
+
```python
|
| 186 |
+
# In src/lilith_agent/app.py
|
| 187 |
+
from lilith_agent.memory import extract_and_compress_facts
|
| 188 |
+
|
| 189 |
+
# Modify build_react_agent to add an extraction node
|
| 190 |
+
def build_react_agent(cfg: Config):
|
| 191 |
+
# ... setup tools & models ...
|
| 192 |
+
cheap_model = get_cheap_model(cfg)
|
| 193 |
+
|
| 194 |
+
# ... model_node, tool_node, fail_safe_node ...
|
| 195 |
+
|
| 196 |
+
def extract_memory_node(state):
|
| 197 |
+
# Run fact extraction asynchronously or synchronously at the end
|
| 198 |
+
extract_and_compress_facts(state["messages"], cheap_model)
|
| 199 |
+
return state
|
| 200 |
+
|
| 201 |
+
graph = StateGraph(AgentState)
|
| 202 |
+
graph.add_node("model", model_node)
|
| 203 |
+
graph.add_node("tools", tool_node)
|
| 204 |
+
graph.add_node("fail_safe", fail_safe_node)
|
| 205 |
+
graph.add_node("extract_memory", extract_memory_node) # NEW
|
| 206 |
+
|
| 207 |
+
def _router(state) -> str:
|
| 208 |
+
if state.get("iterations", 0) >= cfg.recursion_limit - 2:
|
| 209 |
+
return "fail_safe"
|
| 210 |
+
if _count_tool_calls_since_last_human(state["messages"]) >= cfg.budget_hard_cap:
|
| 211 |
+
return "fail_safe"
|
| 212 |
+
last = state["messages"][-1]
|
| 213 |
+
if isinstance(last, AIMessage) and getattr(last, "tool_calls", None):
|
| 214 |
+
return "tools"
|
| 215 |
+
return "extract_memory" # Route to memory before ending
|
| 216 |
+
|
| 217 |
+
graph.set_entry_point("model")
|
| 218 |
+
graph.add_conditional_edges("model", _router, {
|
| 219 |
+
"tools": "tools",
|
| 220 |
+
"fail_safe": "fail_safe",
|
| 221 |
+
"extract_memory": "extract_memory"
|
| 222 |
+
})
|
| 223 |
+
graph.add_edge("tools", "model")
|
| 224 |
+
graph.add_edge("fail_safe", "extract_memory")
|
| 225 |
+
graph.add_edge("extract_memory", END) # End after memory
|
| 226 |
+
|
| 227 |
+
# ... compile ...
|
| 228 |
+
```
|
| 229 |
+
|
| 230 |
+
- [ ] **Step 3: Commit**
|
| 231 |
+
|
| 232 |
+
```bash
|
| 233 |
+
git add src/lilith_agent/app.py src/lilith_agent/memory.py
|
| 234 |
+
git commit -m "feat: add semantic memory extraction node using langmem"
|
| 235 |
+
```
|
| 236 |
+
|
| 237 |
+
---
|
| 238 |
+
|
| 239 |
+
### Task 3: Implement Episodic Memory Summarization
|
| 240 |
+
|
| 241 |
+
**Files:**
|
| 242 |
+
- Modify: `src/lilith_agent/memory.py`
|
| 243 |
+
|
| 244 |
+
- [ ] **Step 1: Add episodic summarization logic**
|
| 245 |
+
|
| 246 |
+
```python
|
| 247 |
+
# In src/lilith_agent/memory.py
|
| 248 |
+
def summarize_episode(messages: List[BaseMessage], model) -> None:
|
| 249 |
+
"""
|
| 250 |
+
Summarizes the trajectory of the task to learn from past experiences.
|
| 251 |
+
"""
|
| 252 |
+
log.info("[memory] Summarizing task episode...")
|
| 253 |
+
try:
|
| 254 |
+
# Extract the initial question
|
| 255 |
+
initial_question = ""
|
| 256 |
+
for m in messages:
|
| 257 |
+
if isinstance(m, HumanMessage):
|
| 258 |
+
initial_question = str(m.content)
|
| 259 |
+
break
|
| 260 |
+
|
| 261 |
+
conv_str = "\n".join([f"{m.type}: {m.content[:200]}..." for m in messages if m.content])
|
| 262 |
+
|
| 263 |
+
prompt = f"""
|
| 264 |
+
Summarize the trajectory of this task to help a future agent avoid mistakes and repeat successes.
|
| 265 |
+
Include:
|
| 266 |
+
1. Task description
|
| 267 |
+
2. Tools used and why
|
| 268 |
+
3. Errors encountered and how they were bypassed
|
| 269 |
+
4. Final outcome
|
| 270 |
+
|
| 271 |
+
Initial Question: {initial_question}
|
| 272 |
+
Trajectory:
|
| 273 |
+
{conv_str}
|
| 274 |
+
"""
|
| 275 |
+
|
| 276 |
+
response = model.invoke(prompt)
|
| 277 |
+
# Placeholder for actual langmem SDK call:
|
| 278 |
+
# langmem.save_episode(summary=response.content, task=initial_question)
|
| 279 |
+
|
| 280 |
+
log.info("[memory] Episode summarized.")
|
| 281 |
+
except Exception as e:
|
| 282 |
+
log.error(f"[memory] Failed to summarize episode: {e}")
|
| 283 |
+
|
| 284 |
+
# Update the node function
|
| 285 |
+
def extract_and_compress_facts(messages: List[BaseMessage], model) -> None:
|
| 286 |
+
# ... existing facts logic ...
|
| 287 |
+
summarize_episode(messages, model)
|
| 288 |
+
```
|
| 289 |
+
|
| 290 |
+
- [ ] **Step 2: Commit**
|
| 291 |
+
|
| 292 |
+
```bash
|
| 293 |
+
git add src/lilith_agent/memory.py
|
| 294 |
+
git commit -m "feat: add episodic task summarization"
|
| 295 |
+
```
|
| 296 |
+
|
| 297 |
+
---
|
| 298 |
+
|
| 299 |
+
### Task 4: Inject Retrieved Memory into System Prompt
|
| 300 |
+
|
| 301 |
+
**Files:**
|
| 302 |
+
- Modify: `src/lilith_agent/memory.py`
|
| 303 |
+
- Modify: `src/lilith_agent/app.py`
|
| 304 |
+
|
| 305 |
+
- [ ] **Step 1: Implement Retrieval Logic**
|
| 306 |
+
|
| 307 |
+
```python
|
| 308 |
+
# In src/lilith_agent/memory.py
|
| 309 |
+
def retrieve_relevant_context(query: str) -> str:
|
| 310 |
+
"""
|
| 311 |
+
Queries the semantic and episodic memory banks for relevant facts and past experiences.
|
| 312 |
+
"""
|
| 313 |
+
try:
|
| 314 |
+
# Placeholder for actual langmem SDK sparse retrieval:
|
| 315 |
+
# facts = langmem.search_facts(query, top_k=3)
|
| 316 |
+
# episodes = langmem.search_episodes(query, top_k=1)
|
| 317 |
+
|
| 318 |
+
facts = [] # stub
|
| 319 |
+
episodes = [] # stub
|
| 320 |
+
|
| 321 |
+
context_parts = []
|
| 322 |
+
if facts:
|
| 323 |
+
context_parts.append("<relevant_facts>\n" + "\n".join(f"- {f}" for f in facts) + "\n</relevant_facts>")
|
| 324 |
+
if episodes:
|
| 325 |
+
context_parts.append("<past_experiences>\n" + "\n".join(f"- {e}" for e in episodes) + "\n</past_experiences>")
|
| 326 |
+
|
| 327 |
+
return "\n\n".join(context_parts)
|
| 328 |
+
except Exception as e:
|
| 329 |
+
log.error(f"[memory] Retrieval failed: {e}")
|
| 330 |
+
return ""
|
| 331 |
+
```
|
| 332 |
+
|
| 333 |
+
- [ ] **Step 2: Inject into SystemMessage**
|
| 334 |
+
|
| 335 |
+
Modify `src/lilith_agent/app.py` in the `model_node`:
|
| 336 |
+
|
| 337 |
+
```python
|
| 338 |
+
# In src/lilith_agent/app.py
|
| 339 |
+
from lilith_agent.memory import retrieve_relevant_context
|
| 340 |
+
|
| 341 |
+
def build_react_agent(cfg: Config):
|
| 342 |
+
# ...
|
| 343 |
+
def model_node(state):
|
| 344 |
+
from langchain_core.messages import SystemMessage
|
| 345 |
+
|
| 346 |
+
# ... existing initial_question extraction ...
|
| 347 |
+
|
| 348 |
+
# Retrieve context ONLY on the first iteration of a new question
|
| 349 |
+
iteration = state.get("iterations", 0)
|
| 350 |
+
memory_context = ""
|
| 351 |
+
if iteration == 0 and initial_question:
|
| 352 |
+
memory_context = retrieve_relevant_context(initial_question)
|
| 353 |
+
|
| 354 |
+
base_prompt = (
|
| 355 |
+
"You are Lilith, an autonomous ReAct research assistant operating in a continuous session.\n\n"
|
| 356 |
+
# ... existing directives ...
|
| 357 |
+
)
|
| 358 |
+
|
| 359 |
+
if memory_context:
|
| 360 |
+
base_prompt += "\n\nCRITICAL CONTEXT (Retrieved from Long-Term Memory):\n" + memory_context
|
| 361 |
+
|
| 362 |
+
sys_prompt = apply_caveman(base_prompt, cfg.caveman, cfg.caveman_mode)
|
| 363 |
+
sys_msg = SystemMessage(sys_prompt)
|
| 364 |
+
|
| 365 |
+
# ... rest of model_node ...
|
| 366 |
+
```
|
| 367 |
+
|
| 368 |
+
- [ ] **Step 3: Commit**
|
| 369 |
+
|
| 370 |
+
```bash
|
| 371 |
+
git add src/lilith_agent/memory.py src/lilith_agent/app.py
|
| 372 |
+
git commit -m "feat: inject retrieved semantic and episodic memory into system prompt"
|
| 373 |
+
```
|
docs/superpowers/specs/2026-04-26-langmem-persistent-memory-design.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Persistent Memory Design for Lilith Agent (LangMem)
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
This document outlines the design for a three-tiered persistent memory system for the Lilith Agent, utilizing `langmem` and inspired by DeepSeek V4's architecture to prevent memory explosion.
|
| 6 |
+
|
| 7 |
+
The goal is to enable Lilith to:
|
| 8 |
+
1. **Resume interrupted conversations** (Short-Term/Thread Persistence).
|
| 9 |
+
2. **Remember user preferences and static facts** across sessions (Semantic Memory).
|
| 10 |
+
3. **Learn from past complex tasks** to avoid repeating mistakes (Episodic Memory).
|
| 11 |
+
|
| 12 |
+
## Architecture: Three-Tiered Memory
|
| 13 |
+
|
| 14 |
+
Inspired by DeepSeek V4's separation of static knowledge (Engram) from dynamic reasoning, and its aggressive compression mechanisms (HCA), we will implement the following tiers:
|
| 15 |
+
|
| 16 |
+
### 1. Short-Term Memory (Thread Persistence)
|
| 17 |
+
* **Purpose:** Allows resuming the exact state of a conversation after the CLI/TUI is closed.
|
| 18 |
+
* **Mechanism:** Replace LangGraph's in-memory `MemorySaver` with `SqliteSaver`.
|
| 19 |
+
* **Storage:** Local SQLite database at `.lilith/threads.sqlite`.
|
| 20 |
+
* **Implementation:**
|
| 21 |
+
* Add `langgraph-checkpoint-sqlite` to dependencies.
|
| 22 |
+
* Initialize `SqliteSaver(conn)` in `app.py::build_react_agent`.
|
| 23 |
+
* Ensure the TUI and batch runner correctly pass and reuse `thread_id`s.
|
| 24 |
+
|
| 25 |
+
### 2. Semantic Memory (Static Knowledge / "Engram")
|
| 26 |
+
* **Purpose:** Stores atomic facts, user preferences, and environmental knowledge extracted from conversations (e.g., "User prefers Python 3.11", "API key X is used for service Y").
|
| 27 |
+
* **Mechanism:** Background extraction and active compression using `langmem`.
|
| 28 |
+
* **Storage:** Local vector store (e.g., Chroma or local SQLite + vector extensions) managed by `langmem` at `.lilith/semantic_memory`.
|
| 29 |
+
* **Implementation:**
|
| 30 |
+
* Add `langmem` to dependencies.
|
| 31 |
+
* Create an asynchronous background task that runs at the end of a successful graph execution (or periodically).
|
| 32 |
+
* Use the `cheap_model` (e.g., `gemini-3-flash-preview`) to extract new facts from the recent conversation.
|
| 33 |
+
* **Anti-Bloat Compression (HCA equivalent):** Before saving, query the vector store for similar existing facts. Instruct the model to merge, update, or delete existing facts to resolve contradictions and maintain a highly compressed, deduplicated knowledge base.
|
| 34 |
+
* **Sparse Retrieval (Lightning Indexer equivalent):** At the start of a new task, embed the user's query, perform a Top-K (e.g., K=3) search against the semantic memory, and inject only the relevant facts into the `SystemMessage`.
|
| 35 |
+
|
| 36 |
+
### 3. Episodic Memory (Task Experiences)
|
| 37 |
+
* **Purpose:** Remembers the trajectories and outcomes of complex tasks (e.g., "When parsing a GAIA PDF, `pypdf` failed but `pdfplumber` worked").
|
| 38 |
+
* **Mechanism:** Summarization of successful (or informatively failed) task executions.
|
| 39 |
+
* **Storage:** Local vector store managed by `langmem` at `.lilith/episodic_memory`.
|
| 40 |
+
* **Implementation:**
|
| 41 |
+
* When the agent reaches the `END` node with a final answer, trigger an episodic summarizer (using the `cheap_model`).
|
| 42 |
+
* The summarizer condenses the entire ReAct trajectory (tools used, errors encountered, successful path) into a concise "episode" summary.
|
| 43 |
+
* **Sparse Retrieval:** Similar to semantic memory, fetch Top-K (e.g., K=1 or 2) relevant past episodes based on the new task's initial query and inject them into the system prompt as historical context.
|
| 44 |
+
|
| 45 |
+
## Component Interactions
|
| 46 |
+
|
| 47 |
+
1. **Start of Task:**
|
| 48 |
+
* User inputs a query.
|
| 49 |
+
* Agent embeds the query and searches Semantic and Episodic memory.
|
| 50 |
+
* Agent constructs the initial `SystemMessage`, injecting retrieved facts and past episodes.
|
| 51 |
+
2. **During Task (Reasoning):**
|
| 52 |
+
* Agent executes the ReAct loop, saving state to the `SqliteSaver` (Short-Term memory) at each step.
|
| 53 |
+
* Context is managed using existing compaction logic to prevent context window explosion.
|
| 54 |
+
3. **End of Task (Extraction & Compression):**
|
| 55 |
+
* Task concludes (success or failure).
|
| 56 |
+
* Background process reads the thread history from `SqliteSaver`.
|
| 57 |
+
* **Extract:** Identify new facts and summarize the episode.
|
| 58 |
+
* **Compress:** Deduplicate and merge new facts with existing facts in the Semantic vector store.
|
| 59 |
+
* **Store:** Save the updated facts and the new episode summary.
|
| 60 |
+
|
| 61 |
+
## Data Schema (Conceptual)
|
| 62 |
+
|
| 63 |
+
**Semantic Fact (Engram)**
|
| 64 |
+
```json
|
| 65 |
+
{
|
| 66 |
+
"id": "uuid",
|
| 67 |
+
"content": "User strictly uses Python 3.11 for all scripts.",
|
| 68 |
+
"type": "preference",
|
| 69 |
+
"last_updated": "2026-04-26T...",
|
| 70 |
+
"embedding": [0.1, 0.2, ...]
|
| 71 |
+
}
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
**Episode Summary**
|
| 75 |
+
```json
|
| 76 |
+
{
|
| 77 |
+
"id": "uuid",
|
| 78 |
+
"task_description": "Extract text from a scanned PDF for GAIA benchmark.",
|
| 79 |
+
"summary": "Attempted pypdf first, which failed due to scanned image format. Pivoted to using OCR via inspect_visual_content, which successfully extracted the text.",
|
| 80 |
+
"outcome": "success",
|
| 81 |
+
"timestamp": "2026-04-26T...",
|
| 82 |
+
"embedding": [0.3, 0.4, ...]
|
| 83 |
+
}
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
## Dependencies
|
| 87 |
+
* `langgraph-checkpoint-sqlite`
|
| 88 |
+
* `langmem`
|
| 89 |
+
* Vector database client (e.g., `chromadb` or equivalent compatible with `langmem` for local storage).
|
| 90 |
+
|
| 91 |
+
## Next Steps (Implementation Plan)
|
| 92 |
+
1. Implement `SqliteSaver` for thread persistence.
|
| 93 |
+
2. Set up the `langmem` infrastructure (local vector store).
|
| 94 |
+
3. Implement Semantic Memory extraction and sparse retrieval.
|
| 95 |
+
4. Implement active compression/merging logic for facts to prevent bloat.
|
| 96 |
+
5. Implement Episodic Memory summarization and retrieval.
|