Agent memory
A recipe for long-term memory per user or agent - what to store, how to recall it with text, filters and vectors, and how to scope and forget it.
An agent's memory is a set of facts, preferences and past episodes it can recall later: the user's name and plan, the fix that worked last time, the file it was editing. SkimmerDB fits this shape well: a namespace per user or agent costs nothing while idle, opens in one read when it's small, and recalls by exact text, by meaning, or both.
The shape
| Choice | Recipe |
|---|---|
| Namespace | One per user (mem-<user>) or per agent (agent-<id>); per task for scratch memory (task-<id>). |
| Id | Stable and meaningful: <user>/<topic> for facts that get updated (u-17/editor), <user>/<message id> or <run>/<step> for episodes. Writing the same id again replaces it. |
text | The memory itself, in words: "Prefers TypeScript over Python for scripts". This is what exact search matches. |
meta | kind (fact, preference, episode), ts (integer seconds), source (chat, tool, import), anything you'll filter by. Flat, one type per key. |
vector | An embedding of text, if you want recall by meaning. One embedding model per namespace. |
See Data model for why each of these matters.
Remember
B=https://api.skimmerdb.com/v1
curl -s $B/namespaces/mem-u-17/docs -H "Authorization: Bearer $KEY" -d '{"docs": [
{"id": "u-17/editor", "text": "Uses Neovim; wants terminal commands, not GUI steps",
"meta": {"kind": "preference", "ts": 1760572800, "source": "chat"}},
{"id": "u-17/run-42", "text": "Fixed the E504 upstream timeout by raising the proxy read timeout to 60s",
"meta": {"kind": "episode", "ts": 1760572937, "source": "tool"}}
]}'mem = db.namespace("mem-u-17") # the first write makes it
mem.write([
{"id": "u-17/editor", "text": "Uses Neovim; wants terminal commands, not GUI steps",
"meta": {"kind": "preference", "ts": 1760572800, "source": "chat"}},
{"id": "u-17/run-42", "text": "Fixed the E504 upstream timeout by raising the proxy read timeout to 60s",
"meta": {"kind": "episode", "ts": 1760572937, "source": "tool"}},
])const mem = db.namespace("mem-u-17"); // the first write makes it
await mem.write([
{ id: "u-17/editor", text: "Uses Neovim; wants terminal commands, not GUI steps",
meta: { kind: "preference", ts: 1760572800, source: "chat" } },
{ id: "u-17/run-42", text: "Fixed the E504 upstream timeout by raising the proxy read timeout to 60s",
meta: { kind: "episode", ts: 1760572937, source: "tool" } },
]);To change a fact, write its id again (or PATCH its metadata). To keep a history instead,
give each version its own id.
Recall
Pick by what the agent knows at the moment of recall.
An exact term (a name, an error code, a file path, an id): text search. Literal, ignoring case, and it never misses a document that contains the term.
hits = mem.search("E504", limit=5).hitsconst { hits } = await mem.search("E504", { limit: 5 });A kind of memory: list with a filter. An empty q lists newest first.
prefs = mem.search("", filter={"kind": "preference"}, limit=50).hits
recent = mem.search("", filter={"ts": {"gte": 1760500000}}, limit=20).hitsconst prefs = (await mem.search("", { filter: { kind: "preference" }, limit: 50 })).hits;
const recent = (await mem.search("", { filter: { ts: { gte: 1760500000 } }, limit: 20 })).hits;Something similar in meaning: vector search on documents written with embeddings. Add q
or filter to make it hybrid: the nearest among those that contain the text and pass the
filter.
hits = mem.search(vector=embed("how did we fix the gateway timeouts?"),
filter={"kind": "episode"}, limit=5).hitsconst { hits } = await mem.search("", { vector: await embed("how did we fix the gateway timeouts?"), filter: { kind: "episode" }, limit: 5 });Hits carry the matched line and the metadata. For the whole memory, get it by id.
Tip Before a session, warm the namespace with the searches the agent is likely to run
(POST .../warm, ns.warm([...])): its first answers are then warm. See
Performance and cost.
Through MCP
An agent connected to SkimmerDB's MCP server (see Connect an agent) uses the same
operations as tools: write_documents to remember, search to recall (text, filter or
vector), get_document for one memory, delete_documents to forget, and warm_namespace
before a session. Tell the agent its namespace in its instructions ("your memory is the
namespace mem-u-17; write facts with ids u-17/<topic>").
The agent skill teaches coding agents these conventions.
Scope each agent to its own memory
Give each agent or app a key that sees only its namespaces: read or write scope, limited
to a name or a prefix. Every other namespace answers exactly like a name nobody has.
curl -s $B/keys -H "Authorization: Bearer $ADMIN_KEY" \
-d '{"name": "agent for u-17", "scope": "write", "namespaces": ["mem-u-17"]}'agent_key = db.create_key("agent for u-17", scope="write", namespaces=["mem-u-17"])const agentKey = await db.createKey("agent for u-17", { scope: "write", namespaces: ["mem-u-17"] });A prefix ("agent-7-*") covers every namespace an agent makes for itself. More in
Keys, scopes and tenants.
Forget
- One memory: delete it by id (
POST .../delete,ns.delete([...]), MCPdelete_documents). - Everything about a user: drop their namespace (
DELETE /v1/namespaces/mem-u-17,ns.drop()). Its documents are deleted from storage right after.
What it costs
Nothing runs for a namespace while nobody uses it: an idle memory costs only the bytes it stores. A small namespace's first query after a sleep opens it in a single read from object storage. Prices: skimmerdb.com/pricing.