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-text

Make 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 con

Why (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)