Persisting memory with SQLite
Give your agent durable, cross-restart memory by swapping the in-RAM store for MemorySQLite, including FTS5-backed fact recall.
What you'll build
By default an agent keeps everything in process memory: the conversation so far and any facts it has chosen to remember. That is fine for a single run, but the moment the process exits, all of it is gone. This guide swaps the default in-RAM store for MemorySQLite, so an agent can write a fact in one run and read it back in a completely separate process.
Two stores satisfy the same MemoryProto protocol, so they are drop-in interchangeable:
In-RAM. Zero setup, fastest, but lost on restart. The default when you pass nothing.
File-backed. Messages and facts survive restarts; fact search uses SQLite FTS5 when available.
Both implement the same async surface — get_messages, add_messages, clear_messages, remember, recall, forget — so everything below works against either. See Memory concepts for the model, and the memory reference for the full API.
Attach a store to an agent
Pass a store instance to Agent(memory=...). With nothing passed, the agent uses an in-RAM Memory.
from agentroute import Agent, Memory
# Explicit in-RAM store (this is also the default).
agent = Agent(
name="assistant",
model="claude-sonnet-4",
instructions="You are a helpful assistant.",
memory=Memory(),
)To make memory durable, swap in MemorySQLite with a file path. The database file is created on first use, so there is no migration step.
from agentroute import Agent, MemorySQLite
agent = Agent(
name="assistant",
model="claude-sonnet-4",
instructions="You are a helpful assistant.",
memory=MemorySQLite("agent.db"),
)MemorySQLite opens the database lazily on first access and uses WAL journaling. The schema (a messages table plus a facts table) is created automatically.
Remember and recall across restarts
The point of a file-backed store is that what you write in one process is there in the next. The two scripts below demonstrate this — run the first, let it exit, then run the second against the same agent.db.
remember(key, value) upserts a fact keyed by key. Run this script and let it exit completely.
import asyncio
from agentroute import MemorySQLite
async def main() -> None:
mem = MemorySQLite("agent.db")
await mem.remember("user_name", "Ada")
await mem.remember("favorite_language", "Python")
await mem.close()
print("Stored two facts in agent.db")
asyncio.run(main())A brand-new process, no shared state — only the agent.db file on disk. recall(query, limit=10) returns matching fact values.
import asyncio
from agentroute import MemorySQLite
async def main() -> None:
mem = MemorySQLite("agent.db")
hits = await mem.recall("language")
print(hits) # ['Python']
await mem.close()
asyncio.run(main())Run them in sequence:
python session_one.py # Stored two facts in agent.db
python session_two.py # ['Python']If you swap MemorySQLite("agent.db") for Memory() in both scripts, the second one prints [] — the in-RAM store starts empty every time.
remember is an upsert: calling it again with the same key overwrites the value. Use forget(key) to drop a single fact, and clear_messages() to wipe the conversation history without touching facts.
Let a tool read and write memory
A tool that takes a ctx: Context parameter gets the agent's Context auto-injected (and hidden from the model). ctx.memory is the same store you attached to the agent, so tools can persist facts during a run and surface them later.
import asyncio
from agentroute import Agent, Context, MemorySQLite
agent = Agent(
name="notetaker",
model="claude-sonnet-4",
instructions=(
"You help the user keep notes. Save durable facts with save_note "
"and look things up with find_notes before answering from memory."
),
memory=MemorySQLite("notes.db"),
)
@agent.tool
async def save_note(ctx: Context, key: str, note: str) -> str:
"""Persist a note under a short key for later recall."""
await ctx.memory.remember(key, note)
return f"Saved note '{key}'."
@agent.tool
async def find_notes(ctx: Context, query: str) -> list[str]:
"""Search saved notes for anything matching the query."""
return await ctx.memory.recall(query, limit=5)
async def main() -> None:
# First run: the agent saves something.
r1 = await agent.arun("Remember that the deploy window is Friday 2pm UTC.")
print(r1)
# A later run — even a later process — can recall it.
r2 = await agent.arun("When is the deploy window?")
print(r2)
asyncio.run(main())Because notes.db is on disk, restarting the program and asking "When is the deploy window?" again still works — the find_notes tool reads the fact straight back out of SQLite.
ctx.memory is whatever you passed to Agent(memory=...). With no store configured it is an in-RAM Memory; tools written against ctx.memory keep working unchanged when you upgrade to MemorySQLite. See Context for everything else on the context object.
How recall search works
recall(query, limit=10) searches the stored facts and returns their values, ordered by the underlying store.
- An empty or whitespace-only query returns up to
limitfacts unfiltered. Memory(in-RAM) does a case-insensitive substring match over keys and values.MemorySQLiteuses an FTS5 virtual table for full-text matching when the local SQLite build supports it, and transparently falls back to aLIKEsubstring scan otherwise (including when an FTS query string is malformed).
This means a query like "deploy window" is matched as FTS terms against MemorySQLite, so word-level matches work without you writing any SQL. Because the fallback is automatic, the same recall call behaves sensibly regardless of how SQLite was compiled.
FTS5 availability depends on how SQLite was built into your Python. If it is missing, MemorySQLite still works — recall just degrades to substring matching. Both paths return list[str] of fact values, so your code does not need to branch.
Choosing a store
Runs are short-lived, you do not need persistence, or you are writing a test. It is the default — pass nothing.
Facts or conversation must outlive the process: assistants with long-term context, multi-session workflows, anything that should "remember" between restarts.
Next steps
The mental model behind messages, facts, and recall.
Full API for Memory, MemorySQLite, and the MemoryProto protocol.
How a ctx: Context parameter gets injected into a tool.
Compact long conversations with sliding-window, truncate, or summarize strategies.