Set up
mkdir agent-memory && cd agent-memory
python -m venv .venv && source .venv/bin/activate
pip install ollama
ollama pull llama3.2
ollama pull nomic-embed-textMake sure ollama serve is running (the desktop app does this for you).
The schema
The whole design turns on one column pair: valid_from and valid_to. A fact with valid_to IS NULL is currently true. When a newer fact contradicts it, we don't delete or overwrite — we stamp valid_to on the old row and insert a new one. That gives us history for free and, more importantly, lets recall filter to "what's true now."
Create memory.py:
# memory.py
import sqlite3
DB = "memory.db"
def connect(path: str = DB) -> sqlite3.Connection:
con = sqlite3.connect(path)
con.execute(
"""
CREATE TABLE IF NOT EXISTS facts (
id INTEGER PRIMARY KEY,
subject TEXT NOT NULL, -- usually "user"
predicate TEXT NOT NULL, -- "city", "role", ...
value TEXT NOT NULL, -- "Berlin"
embedding BLOB, -- packed float32 vector
valid_from TEXT NOT NULL, -- ISO timestamp
valid_to TEXT, -- NULL = still true
session INTEGER NOT NULL
)
"""
)
con.execute(
"""
CREATE TABLE IF NOT EXISTS summaries (
session INTEGER PRIMARY KEY,
text TEXT NOT NULL
)
"""
)
con.commit()
return conWhy (subject, predicate, value)
Storing facts as triples — ("user", "city", "Berlin") — is what makes invalidation clean. "The current value of the user's city" is a precise query: WHERE subject='user' AND predicate='city' AND valid_to IS NULL. If we stored facts as free-text sentences, "moved to Berlin" and "lives in Toronto" would just be two similar strings, and we'd be back to the naive-retrieval failure the drip warns about.
You don't have to be rigid about the vocabulary — the extractor in Step 4 will invent predicates like preference or constraint as needed. The rule that matters: one predicate holds one current value per subject.
A quick look
if __name__ == "__main__":
con = connect()
con.execute(
"INSERT INTO facts(subject,predicate,value,valid_from,valid_to,session)"
" VALUES('user','city','Toronto','2026-01-01T00:00:00',NULL,1)"
)
con.commit()
for row in con.execute("SELECT subject,predicate,value,valid_to FROM facts"):
print(row)$ python memory.py
('user', 'city', 'Toronto', None)valid_to is None — Toronto is currently true. In Step 4 we'll watch the move to Berlin flip it to a timestamp.
Reference: Python sqlite3 · Temporal / bitemporal modelling · Agent Long-Term Memory (drip)