Guides Markdown

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

ChoiceRecipe
NamespaceOne per user (mem-<user>) or per agent (agent-<id>); per task for scratch memory (task-<id>).
IdStable 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.
textThe memory itself, in words: "Prefers TypeScript over Python for scripts". This is what exact search matches.
metakind (fact, preference, episode), ts (integer seconds), source (chat, tool, import), anything you'll filter by. Flat, one type per key.
vectorAn 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).hits
const { 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).hits
const 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).hits
const { 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([...]), MCP delete_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.