# 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](/guides/data-model) for why each of these matters.

## Remember

```sh title="curl"
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"}}
]}'
```

```python title="Python"
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"}},
])
```

```ts title="TypeScript"
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.

```python title="Python"
hits = mem.search("E504", limit=5).hits
```

```ts title="TypeScript"
const { hits } = await mem.search("E504", { limit: 5 });
```

**A kind of memory**: list with a filter. An empty `q` lists newest first.

```python title="Python"
prefs = mem.search("", filter={"kind": "preference"}, limit=50).hits
recent = mem.search("", filter={"ts": {"gte": 1760500000}}, limit=20).hits
```

```ts title="TypeScript"
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.

```python title="Python"
hits = mem.search(vector=embed("how did we fix the gateway timeouts?"),
                  filter={"kind": "episode"}, limit=5).hits
```

```ts title="TypeScript"
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](/guides/performance).

## Through MCP

An agent connected to SkimmerDB's MCP server (see [Connect an agent](/mcp)) 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](/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.

```sh title="curl"
curl -s $B/keys -H "Authorization: Bearer $ADMIN_KEY" \
  -d '{"name": "agent for u-17", "scope": "write", "namespaces": ["mem-u-17"]}'
```

```python title="Python"
agent_key = db.create_key("agent for u-17", scope="write", namespaces=["mem-u-17"])
```

```ts title="TypeScript"
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](/guides/keys).

## 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](https://skimmerdb.com/pricing).
