Saturday the calculator agent got short-term memory from a SQLiteSession. Today it got memory that outlives the conversation, and we built it ourselves, because most SDKs leave that part to you.
1. Two Kinds of Memory
| Holds | Lasts | |
|---|---|---|
| Short-term | The conversation: what was just said, the running state | One session |
| Long-term | Facts about the user: budgets, preferences, limits | Across sessions |
The test question: “What do you know about me?” Chat history can’t answer it once the conversation is over. Only long-term memory can.
2. Who Gives You What
| Framework | Short-term | Long-term |
|---|---|---|
| LangChain / LangGraph | Checkpointer | Store: InMemoryStore for development, a persistent store such as PostgresStore for production |
| Claude Agent SDK | Sessions | For facts your application controls, you build the tools |
| OpenAI Agents SDK | SQLiteSession | You build the tools |
The pattern: short-term memory comes free almost everywhere. Long-term memory is usually yours to design, because only you know which facts matter for your application.
3. Deciding What Deserves Remembering
This is the design decision, not the code. You decide what gets stored and when.
The simplest trigger is the system prompt. Tell the agent when to store and when to retrieve, and let the LLM spot the moments.
A caution: the model will store whatever matches your rule. For anything beyond a demo, decide what must never be stored (passwords, card numbers, health details), not just what should.
4. The Storage: A Table of Facts
import sqlite3
def initialize_memory(path="long_term_memory.db"):
db = sqlite3.connect(path)
db.execute("""
CREATE TABLE IF NOT EXISTS memories (
user_id TEXT NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
UNIQUE (user_id, key)
)
""")
db.commit()
return db
- Three columns: who it’s about, what it’s called, and what it is. One user can have many facts.
UNIQUE (user_id, key)means one value per fact per user. Without it, remembering the same key twice leaves two rows, and recall returns whichever comes first.- Call
initialize_memoryat startup, inmain. It’s setup, not a tool. The agent should never be the one deciding whether the database exists.
SQLite is the example; any database or even a file would do. The design is what matters.
5. Context: Who Is Speaking?
In a multi-user agent, every tool needs the current user’s ID. The model must not supply it. A model that can choose the user ID can read anyone’s memories.
The OpenAI Agents SDK solution is a context object:
from dataclasses import dataclass
@dataclass
class UserContext:
user_id: str
session_id: str
You pass it to the runner, and the SDK hands it to every tool. The context is never sent to the model. It stays in your program.
6. The Three Memory Tools
from agents import function_tool, RunContextWrapper
@function_tool
def remember_fact(ctx: RunContextWrapper[UserContext], key: str, value: str) -> str:
"""Save or update an important fact about the user.
Args:
key: A short name for the fact, e.g. "grocery budget".
value: The fact itself.
"""
db.execute(
"INSERT OR REPLACE INTO memories (user_id, key, value) VALUES (?, ?, ?)",
(ctx.context.user_id, key, value),
)
db.commit()
return f"Saved {key}."
@function_tool
def recall_fact(ctx: RunContextWrapper[UserContext], key: str) -> str:
"""Retrieve one saved fact about the user by its key."""
row = db.execute(
"SELECT value FROM memories WHERE user_id = ? AND key = ?",
(ctx.context.user_id, key),
).fetchone()
return row[0] if row else f"No saved fact called {key}."
@function_tool
def list_fact(ctx: RunContextWrapper[UserContext]) -> str:
"""List every saved fact about the user."""
rows = db.execute(
"SELECT key, value FROM memories WHERE user_id = ?",
(ctx.context.user_id,),
).fetchall()
return "\n".join(f"{k}: {v}" for k, v in rows) or "Nothing saved yet."
Look at what the model sees: remember_fact asks for a key and a value. There’s no user_id parameter. The ID comes from ctx.context, which the model can’t touch. That’s the whole safety argument in one signature.
7. Wiring It Into the Agent
from agents import Agent, Runner, SQLiteSession
agent = Agent[UserContext](
name="Smart Calculator",
instructions=(
"Always use the calculator tools for every calculation.\n\n"
"Memory rules:\n"
"- When the user asks you to remember something, call remember_fact "
"before confirming it's saved.\n"
"- When the user asks about something saved earlier, call recall_fact "
"or list_fact."
),
tools=[add, subtract, multiply, divide, modulus,
remember_fact, recall_fact, list_fact],
model=MODEL_NAME,
)
ctx = UserContext(user_id=os.getenv("USER_ID"), session_id=os.getenv("SESSION_ID"))
session = SQLiteSession(ctx.session_id, "calculator_memory.db")
result = await Runner.run(agent, user_input, context=ctx, session=session)
Two memories, side by side:
session=→ short-term: the conversation.context=+ the memory tools → long-term: the facts.
The memory rules in the prompt are what make the agent use the tools. Without them, it will happily say “I’ll remember that” and store nothing.
8. The Test: Both Memories in One Question
| Turn | Goes to |
|---|---|
| “I have 10 apples. I bought 2 more.” → 12 | Short-term: the session |
| “Remember: the most apples I can have is 100.” | Long-term: remember_fact |
| “How many apples do I have, and what’s my maximum?” | Both: 12 from the session, 100 from the database |
Checking the database directly confirmed one row: user 1001, key max apples, value 100.
9. Proving Users Are Isolated
Changing .env to user 102 and session_102 gave a clean slate: a new conversation, no facts, and nothing of user 1001’s visible.
Go one step further and change them one at a time:
| Change | Result |
|---|---|
| New session, same user | The conversation resets; the facts survive. That’s long-term memory working |
| New user, same session | The facts reset, but the new user sees the old user’s conversation ⚠️ |
That second row is a real bug, and an easy one to ship. Derive the session ID from the user ID (for example f"session_{user_id}") so the two can never drift apart.
10. The Bigger Picture
Every native SDK comes down to the same four steps:
- Build the agent
- Add tools
- Add short-term memory
- Add long-term memory
We’ve now done all four in Claude and in OpenAI. Next is LangChain: a full session for anyone new to it, and a quick revision for everyone else, focused on its checkpointer and store.
✅ Action Items
- Add the three memory tools to your calculator agent, with the
UNIQUE (user_id, key)constraint. - Run the apple test and inspect the database to confirm the fact was stored.
- Change user and session one at a time and reproduce both rows of the table in section 9. Then fix the second one.
- Do it all again in Google’s ADK. Agent, tools, short-term and long-term memory, on your own. This one won’t be covered in class, and that’s the point: if the four steps transfer, you’ve understood them.
⏭️ Next up: LangChain, with its checkpointer and store.
