Long-Term Memory Basics
8 examples to get you started with long-term memory - 5 basic and 3 intermediate.
You will save a user preference to a small store, start a new session with empty chat history, reload that preference, and use it in the next reply.
Prerequisites
- Python 3.11+ recommended
- Comfort with JSON files and simple functions
- Optional: an OpenAI-compatible API key if you want a live model call in example 8
python -m venv .venv && source .venv/bin/activate
pip install pydantic openai
# Optional for example 8:
# export OPENAI_API_KEY="sk-..."Basic Examples
1. Model a Memory Record
Start with a typed fact, not a free-form blob.
from datetime import datetime, timezone
from pydantic import BaseModel, Field
class MemoryRecord(BaseModel):
user_id: str
key: str = Field(description="Stable field name, e.g. preferred_language")
value: str
source: str = "explicit_user" # explicit_user | extracted | system
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))- Keys keep updates idempotent (
preferred_languageoverwrites cleanly). sourceand timestamps make later conflict handling possible.- Typed records beat dumping whole transcripts into a "memory" file.
Related: Long-Term Memory: What Should Survive Between Agent Sessions
2. Save a Preference to a JSON Store
Use a file store for the learning path; swap to a database later without changing the idea.
import json
from pathlib import Path
STORE = Path("memory_store.json")
def load_all() -> dict:
if not STORE.exists():
return {"memories": []}
return json.loads(STORE.read_text(encoding="utf-8"))
def upsert_memory(record: MemoryRecord) -> None:
data = load_all()
rows = data["memories"]
for i, row in enumerate(rows):
if row["user_id"] == record.user_id and row["key"] == record.key:
payload = record.model_dump(mode="json")
payload["created_at"] = row["created_at"]
rows[i] = payload
break
else:
rows.append(record.model_dump(mode="json"))
STORE.write_text(json.dumps(data, indent=2), encoding="utf-8")
upsert_memory(
MemoryRecord(user_id="user-42", key="preferred_language", value="Portuguese")
)
print(STORE.read_text(encoding="utf-8"))- Upsert by
(user_id, key)so repeats do not create duplicates. - File IO is fine for demos; production needs locking or a real DB.
- You have now created durable state outside any chat session.
3. Load Profile Memory for a User
Session start should rehydrate a small profile, not the entire store.
def get_profile(user_id: str) -> dict[str, str]:
data = load_all()
return {
row["key"]: row["value"]
for row in data["memories"]
if row["user_id"] == user_id
}
profile = get_profile("user-42")
print(profile) # {'preferred_language': 'Portuguese'}- Profile maps are cheap to inject every turn when small.
- Filter by
user_idalways - never load global rows by accident. - Empty profile is valid; agents must tolerate first-time users.
4. Simulate Two Separate Sessions
Prove that chat history is gone while long-term memory remains.
def session_a_save_preference():
# Session A: user states a preference; chat history will be discarded after.
chat = [{"role": "user", "content": "Please answer me in Portuguese from now on."}]
upsert_memory(
MemoryRecord(user_id="user-42", key="preferred_language", value="Portuguese")
)
return "saved", chat
def session_b_new_thread(user_id: str, user_text: str) -> list[dict]:
# Session B: brand-new chat; only LTM crosses the boundary.
profile = get_profile(user_id)
system = "You are a helpful assistant."
if lang := profile.get("preferred_language"):
system += f" Prefer responding in {lang}."
return [
{"role": "system", "content": system},
{"role": "user", "content": user_text},
]
session_a_save_preference()
messages = session_b_new_thread("user-42", "How do I reset my password?")
assert not any("from now on" in m.get("content", "") for m in messages if m["role"] == "user")
print(messages[0]["content"])- Long-term memory is not "the old transcript reattached."
- New sessions start with empty short-term history plus a compact profile.
- This is the core LTM teaching demo.
5. Expose Save and Load as Agent Tools
Agents should call explicit tools rather than silently rewriting storage mid-thought without a contract.
from typing import Callable
ToolFn = Callable[..., str]
def save_preference(user_id: str, key: str, value: str) -> str:
upsert_memory(MemoryRecord(user_id=user_id, key=key, value=value))
return f"saved {key}={value}"
def load_preferences(user_id: str) -> str:
return json.dumps(get_profile(user_id), ensure_ascii=False)
TOOLS: dict[str, ToolFn] = {
"save_preference": lambda key, value, user_id="user-42": save_preference(user_id, key, value),
"load_preferences": lambda user_id="user-42": load_preferences(user_id),
}
print(TOOLS["save_preference"]("timezone", "America/Sao_Paulo"))
print(TOOLS["load_preferences"]())- Tool boundaries make write policy auditable.
- Keep
user_idfrom auth context in production, not from model-supplied args alone. - Descriptions for the model should say when to save (stable prefs), not every utterance.
Intermediate Examples
6. Cap Always-On Profile Size
Unbounded profiles quietly become a second transcript.
ALWAYS_ON_KEYS = {"preferred_language", "timezone", "tone"}
MAX_VALUE_LEN = 80
def profile_for_prompt(user_id: str) -> str:
profile = get_profile(user_id)
lines = []
for key in sorted(ALWAYS_ON_KEYS):
if key in profile:
val = profile[key][:MAX_VALUE_LEN]
lines.append(f"- {key}: {val}")
return "Known preferences:\n" + "\n".join(lines) if lines else "No saved preferences."
print(profile_for_prompt("user-42"))- Always-on memory is a whitelist, not "everything we ever saved."
- Longer episodic facts belong in retrieval, covered later in this section.
- Size caps protect latency and cost.
7. Forget and Correct
Users must be able to undo bad memories.
def delete_memory(user_id: str, key: str) -> bool:
data = load_all()
before = len(data["memories"])
data["memories"] = [
row for row in data["memories"]
if not (row["user_id"] == user_id and row["key"] == key)
]
STORE.write_text(json.dumps(data, indent=2), encoding="utf-8")
return len(data["memories"]) < before
def correct_memory(user_id: str, key: str, value: str) -> None:
upsert_memory(MemoryRecord(user_id=user_id, key=key, value=value, source="user_correction"))
correct_memory("user-42", "preferred_language", "English")
delete_memory("user-42", "timezone")
print(get_profile("user-42"))- Correction is an upsert with a clearer
source. - Deletion is a product feature and often a compliance requirement.
- Log who changed what when you leave the file-store stage.
8. Wire Profile Into a Single Model Call
Optional live call: empty chat history, non-empty long-term preference.
import os
from openai import OpenAI
client = OpenAI() # set OPENAI_BASE_URL for gateways such as OpenRouter
def reply_with_ltm(user_id: str, user_text: str) -> str:
messages = session_b_new_thread(user_id, user_text)
# Demo-only: skip if no key configured
if not os.environ.get("OPENAI_API_KEY"):
return f"[dry-run] system={messages[0]['content']!r} user={user_text!r}"
resp = client.chat.completions.create(
model="gpt-4o-mini", # verify at build
messages=messages,
)
return resp.choices[0].message.content or ""
print(reply_with_ltm("user-42", "Give me a one-line greeting."))- The model never saw session A's transcript - only the injected preference.
- Dry-run mode keeps the example useful offline.
- Next pages replace the JSON file with databases and retrieval.
Related: File-Based and Database-Backed Long-Term Memory Stores
Related
- Long-Term Memory: What Should Survive Between Agent Sessions - what to persist
- File-Based and Database-Backed Long-Term Memory Stores - stronger stores
- Memory Extraction: Deciding What's Worth Remembering - automatic writes
- Memory Retrieval: Surfacing the Right Fact at the Right Time - selective reads
- Long-Term Memory Best Practices - section checklist
- Short-Term Memory Basics - in-session history
Stack versions: Pins from the category manifest (verify at build): OpenRouter (~315+ models, July 2026 pricing/fees); LangGraph 1.0+; CrewAI 1.14+; Microsoft Agent Framework 1.0; Vercel AI SDK 6; Pydantic AI (latest); LlamaIndex (latest); OpenAI Agents SDK (latest + MCP); MCP (Linux Foundation governance); A2A (HTTP+SSE+JSON-RPC 2.0); Solana
@solana/web3.js+@solana/spl-token.