---
title: "Agent memory"
description: "Let app agents remember the people they serve, inside the tenant and the app, with untrusted recall, versions and forgetting you can prove."
section: "interfaces"
status: "Building for 0.4 · ships as a experimental API"
spec: "memory"
canonical: "https://mainsdk.dev/docs/interfaces/memory"
updated: "2026-10-28"
---

# Agent memory

> Let app agents remember the people they serve, inside the tenant and the app, with untrusted recall, versions and forgetting you can prove.

Status: Building for 0.4 · ships as a experimental API · Canonical: https://mainsdk.dev/docs/interfaces/memory · Updated: 2026-10-28

`memory` lets your app's agents remember the people they serve, scoped to the tenant and app, with untrusted recall and a `forget` you can prove. It is free and runs in your app.

Every memory is about one person (or the whole tenant), labelled `personal` by default and sealed with that person's key. Recalled text goes to a model only as data inside one escaped `<memory>` block. `forget` deletes for real, takes everything derived with it and returns a signed receipt. Spec `memory` is `experimental`, so the shape may change. Import it from `mainsdk/experimental/memory`, not from `mainsdk`.

> **Note:** Built, with the `db` and `local` drivers and the 0.5 calls (`learn`, `consolidate`, `relate`, `export`, `import`, the knowledge-graph layer and the mem0, Zep, Letta, Pinecone and MCP provider drivers). Provider endpoints are tested against in-repo fakes, not live accounts. There is no pgvector yet: similarity is computed in process.

## At a glance

- Status: Building for 0.4 · ships as a experimental API
- Phase: 1 (Core, public beta)
- Milestone: 0.4 Hardening
- Plan: free in your app (Apache-2.0)
- Spec: https://github.com/hubxero/mainsdk/blob/develop/docs/specs/memory.md
- Category: AI and agents

Calls:

- `memory.remember(input, { about, kind, ttl, label, source, key, session })`
- `memory.recall(query, { about, kinds, k, asOf, halfLife, hops, session })`
- `memory.context({ about, query, budget })`
- `memory.revise(id, next, { reason })`
- `memory.forget(target, { reason, dryRun })`
- `memory.pin(id)`
- `memory.unpin(id)`
- `memory.history(id)`
- `memory.learn(messages, { about, kinds, maxCost })`
- `memory.relate({ from, predicate, to })`
- `memory.consolidate({ about, reflect })`
- `memory.export({ about, kinds })`
- `memory.import(lines, { about, session })`
- `memory.mcp({ compat })`
- `memory.anthropicTool()`

In main.yaml:

```yaml
memory:
  driver: db                       # db | local | mem0 | zep | letta | pinecone | mcp
  embed: { model: openai/text-embedding-3-small, feature: memory-embed }
  extract: { model: anthropic/claude-haiku-4-5, feature: memory-learn }
  ttl: { event: 90d }
  from-tools: quarantine           # quarantine | allow | refuse
  children: off                    # off | guardian
agents:
  support-bot:
    memory: { read: own, write: true, forget: approve, recalls: 500/day, writes: 100/day }
```

Guarantees, whatever the driver:

- Memories never cross tenants or apps; the tenant comes from tenant.current()
- Recalled text reaches the model only as escaped data, never as system text
- The injection check runs on every write and every recall
- Tool, MCP, web and import writes stay in their session until a person confirms them
- Revisions make new versions; nothing is overwritten
- Forget is a hard delete that cascades and returns a signed receipt
- Labelled personal by default, sealed with the person's key, kept only as long as retention allows
- Every write, recall and forget audited with ids only, never the text
- remember, recall and forget never call a generative model


## Drivers

Pick one in main.yaml with `memory.driver`.

| Driver | Id in main.yaml | Default | Note |
| --- | --- | --- | --- |
| Your database (Postgres with pgvector | `db` | yes |  |
| Local (SQLite or JSONL) | `local` |  |  |
| Mem0 | `mem0` |  |  |
| Zep | `zep` |  |  |
| Letta | `letta` |  |  |
| Pinecone | `pinecone` |  |  |
| MCP memory server | `mcp` |  |  |

## Codes

| Code | Where | Meaning |
| --- | --- | --- |
| `MAIN-MEM-0020` | check | A driver that only soft-deletes holds memories that may be personal |
| `MAIN-MEM-0021` | check | A region the app serves has no endpoint in the memory driver |
| `MAIN-MEM-0022` | check | Recalled memory is passed as the `system` text of an `ai` call |
| `MAIN-MEM-0023` | check | A driver is named `memory`; the in-process driver is `local` (ADR-0039) |
| `MAIN-MEM-0024` | check | `children: guardian` with an extract model on a provider |
| `MAIN-MEM-0025` | check | The `local` memory driver in production |
| `MAIN-MEM-0001` | runtime | No subject: `about` is missing and the call has no `on_behalf_of` or session user (400) |
| `MAIN-MEM-0002` | runtime | No memory with that id in the current tenant, app and scope (404, same body either way) |
| `MAIN-MEM-0003` | runtime | The injection check flagged the text of a write; nothing was stored (422) |
| `MAIN-MEM-0004` | runtime | A recalled memory was flagged by the injection check and left out of the context (logged) |
| `MAIN-MEM-0005` | runtime | The agent has no memory grant for this verb, or `forget: deny` (403) |
| `MAIN-MEM-0006` | runtime | The agent is over its memory `recalls` or `writes` per day (429) |
| `MAIN-MEM-0007` | runtime | Forget refused: a legal hold applies to the person (409) |
| `MAIN-MEM-0008` | runtime | A children's rule refused it: children are off, no guardian consent, or a provider model would extract (403) |
| `MAIN-MEM-0009` | runtime | Invalid input: unknown kind, bad duration, `pan` label, `k` over 100, `hops` over 2 or a bad predicate (400) |
| `MAIN-MEM-0010` | runtime | Revise of a version that is no longer current (409) |
| `MAIN-MEM-0011` | runtime | Import refused: a line is malformed, or personal or non-procedure memory would cross a tenant or app (422) |
| `MAIN-MEM-0012` | runtime | The driver doesn't declare the capability the call needs, such as `asOf`, `hops` or `relate` (501) |
| `MAIN-MEM-0013` | runtime | The memory store is unreachable (503, retryable); `context` degrades to an empty block |
| `MAIN-MEM-0014` | runtime | An Anthropic memory tool path is outside `/memories/` or tries to traverse (400) |
| `MAIN-MEM-0015` | runtime | A driver hasn't confirmed the erase yet; no receipt is issued and the erase keeps retrying (503) |
| `MAIN-MEM-0016` | runtime | A write from a tool, MCP, the web or an import was refused by `from-tools: refuse` (403) |

## Features

| Feature | Phase | Milestone | Spec ticket |
| --- | --- | --- | --- |
| Remember and recall by tenant | 1 | 0.4 | `memory#remember-recall` |
| Model context with pinned memories in one escaped data block | 1 | 0.4 | `memory#context-pin` |
| Versions with revise | 1 | 0.4 | `memory#revise-history` |
| Hard forget with cascade and a signed receipt | 1 | 0.4 | `memory#forget-receipt` |
| Injection check and quarantine for tool and import writes | 1 | 0.4 | `memory#guards` |
| Agent grants | 1 | 0.4 | `memory#governance` |
| Personal label | 1 | 0.4 | `memory#privacy-keys` |
| Audit of every write | 1 | 0.4 | `memory#audit` |
| db driver with row-level security and pgvector | 1 | 0.4 | `memory#db-driver` |
| Local driver with BM25 | 1 | 0.4 | `memory#local-driver` |
| Memory conformance suite | 1 | 0.4 | `memory#conformance` |
| MCP tools for remember | 1 | 0.4 | `memory#mcp-tools` |
| Anthropic memory tool compatibility | 1 | 0.4 | `memory#anthropic-tool` |
| Children off by default | 1 | 0.4 | `memory#children` |
| Learn memories from a conversation | 1 | 0.5 | `memory-advanced#learn` |
| Consolidate and reflect as a workflow | 1 | 0.5 | `memory-advanced#consolidate` |
| Relations and recall hops | 1 | 0.5 | `memory-advanced#relate` |
| Lossless JSONL export and import | 1 | 0.5 | `memory-advanced#export-import` |
| Knowledge-graph MCP server compatibility | 1 | 0.5 | `memory-advanced#kg-compat` |
| Mem0 | 1 | 0.5 | `memory-advanced#provider-drivers` |
| Pinecone and MCP drivers | 1 | 0.5 | `memory-advanced#provider-drivers-vector-mcp` |

## MainSDK Cloud

Everything on this page is free in your app.


## Quick start

```yaml title="main.yaml"
memory:
  driver: db                       # db | local | mem0 | zep | letta | pinecone | mcp
  embed: { model: openai/text-embedding-3-small, feature: memory-embed }
  ttl: { event: 90d }
agents:
  support-bot:
    memory: { read: own, write: true, forget: approve, recalls: 500/day, writes: 100/day }
```

```ts title="lib/support-reply.ts"
import { ai } from "mainsdk";
import { memory } from "mainsdk/experimental/memory";

export async function reply(user: { id: string }, message: string) {
  const ctx = await memory.context({ query: message, budget: 800 });
  return ai.generate({
    model: "anthropic/claude-sonnet-4-5",
    messages: [
      { role: "user", content: ctx.text },   // memories as data, never system text
      { role: "user", content: message },
    ],
    feature: "support-chat",
    agent: "support-bot",
    user,
  });
}
```

You never pass a tenant: it comes from `tenant.current()`. `about` defaults to the person the agent acts for.

## Configuration

| Key | What it does |
| --- | --- |
| `memory.driver` | Where memories live. `db` (default) uses your database with row-level security; `local` keeps them under `.mainsdk/memory/` for development |
| `memory.embed` | The model and feature for embeddings, made in a background job through `ai` |
| `memory.extract` | The model and feature `learn` and `consolidate` use (0.5) |
| `memory.ttl.<kind>` | How long a kind lives. The label's retention under `privacy.retention` caps it |
| `memory.from-tools` | `quarantine` (default), `allow` or `refuse` for writes from tools, MCP, the web and imports |
| `memory.children` | `off` (default) or `guardian` |
| `agents.<name>.memory.read` | `own`, `all` or `none` |
| `agents.<name>.memory.write` | `true` lets the agent write |
| `agents.<name>.memory.forget` | `approve` (default) waits for a person; `deny` refuses |
| `agents.<name>.memory.recalls`, `writes` | Daily limits per agent |

## What you get

- `remember`, `recall` and `context` on one scoped store, with versions and `asOf` reads.
- `forget` with a signed receipt, and erasure through `privacy`.
- Untrusted-by-default recall, the injection check and an audit trail without text.
- MCP tools and the Claude memory tool, so agents use it without glue code.

## Examples

### Remember and recall

```ts title="lib/preferences.ts"
import { memory } from "mainsdk/experimental/memory";
import { s } from "mainsdk/schema";

await memory.remember("Prefers WhatsApp over email", { kind: "preference" });
await memory.remember(
  { schema: s.object({ city: s.string() }), value: { city: "Pune" } },
  { key: "home-city" },                      // a second remember with this key makes a new version
);

const found = await memory.recall("how to contact", { kinds: ["preference"], k: 5 });
```

### Revise, read the past and see the history

```ts title="lib/moved.ts"
import { memory } from "mainsdk/experimental/memory";

declare const id: string;   // a memory id from remember or recall

const next = await memory.revise(id, "Lives in Mumbai", { reason: "told support on 27 Oct" });
const then = await memory.recall("where does she live", { asOf: "2026-10-01T00:00:00Z" });
const versions = await memory.history(id);   // oldest first; nothing is overwritten
```

### Pin what the agent must always see

```ts title="lib/pin.ts"
import { memory } from "mainsdk/experimental/memory";

declare const allergyId: string;
declare const oldNoteId: string;

await memory.pin(allergyId);     // pinned memories come first in memory.context()
await memory.unpin(oldNoteId);
```

### Forget, with a receipt

```ts title="app/api/memory/forget/route.ts"
import { auth } from "mainsdk";
import { memory } from "mainsdk/experimental/memory";

export async function POST(req: Request) {
  const session = await auth.require(req);
  const preview = await memory.forget({ about: session.user }, { reason: "user request", dryRun: true });
  const receipt = await memory.forget({ about: session.user }, { reason: "user request" });
  if ("pending" in receipt) return Response.json({ approvalId: receipt.approvalId }, { status: 202 });   // an agent's forget waits for a person
  return Response.json({ deleted: receipt.ids.length + receipt.cascaded.length, receipt, preview });
}
```

The receipt lists every id, everything derived from them and each driver's confirmation, and is signed by your project key. Forgetting everything about a person also destroys their memory key, so backups can't be read.

### Confirm a quarantined memory

A memory written from a tool, MCP, the web or an import is returned only in its own session until a person confirms it.

```ts title="app/api/memory/confirm/route.ts"
import { auth } from "mainsdk";
import { memory } from "mainsdk/experimental/memory";

export async function POST(req: Request) {
  await auth.require(req);
  const { id } = (await req.json()) as { id: string };
  return Response.json(await memory.revise(id, { trust: "confirmed" }, { reason: "person confirmed" }));
}
```

### Give agents memory over MCP

```ts title="app/mcp/route.ts"
import { mcpServer } from "mainsdk";
import { memory } from "mainsdk/experimental/memory";

export const POST = mcpServer({ actions: [...memory.mcp()] });   // remember (write), recall (read), forget (destructive)
```

### Use Claude's memory tool

```ts title="lib/claude-notes.ts"
import { ai } from "mainsdk";
import { memory } from "mainsdk/experimental/memory";

declare const user: string;

const out = await ai.run({
  model: "anthropic/claude-sonnet-4-5",
  goal: "Plan my week from my notes",
  tools: [memory.anthropicTool()],   // memory_20250818 commands on note memories under /memories/
  feature: "planner",
  agent: "planner-bot",
  user,
});
```

### Learn from a conversation (0.5)

```ts title="jobs/after-chat.ts"
import { memory } from "mainsdk/experimental/memory";

declare const messages: { role: "user" | "assistant"; content: string }[];
declare const userId: string;

const { changes } = await memory.learn(messages, { maxCost: "$0.05" });   // ADD, REVISE, INVALIDATE or NOOP
const { runId } = await memory.consolidate({ about: userId, reflect: true });   // a workflow, never in the request
```

## Reference for 0.5 calls

These calls are specified in spec `memory-advanced`.

| Call | What it does |
| --- | --- |
| `memory.relate({ from, predicate, to })` | Links two memories with a lowercase snake_case predicate such as `works_at`, so `recall` can follow `hops` of 0, 1 or 2. A driver without the `graph` capability fails with `MAIN-MEM-0012`. |
| `memory.export({ about?, kinds? })` | Streams the scope's memories as JSONL (`mainsdk-memory/1`), with every version, pin and relation. |
| `memory.import(lines, { about?, session? })` | Reads that JSONL back. Imported memories start `untrusted` and quarantined to the import session. Into another app, only `procedure` memories labelled `none` import (`MAIN-MEM-0011`). |
| `memory.mcp({ compat? })` | Returns the MCP actions: `remember`, `recall` and `forget`. With `compat: "kg"` (0.5) it serves the knowledge-graph server's tool names instead. |

## Guarantees

- Memories never cross tenants or apps. An agent with `read: own` sees only what it wrote.
- Recalled text is untrusted data, never system text; `check` reports `MAIN-MEM-0022` when it is passed as `system`.
- The injection check runs on every write (`MAIN-MEM-0003`) and every recall (`MAIN-MEM-0004`).
- `remember`, `recall` and `forget` never call a generative model.
- Every write, recall and forget is audited with ids only, never the text.
- `privacy.export()` and `privacy.erase()` include memory.

## Errors and checks

| Code | When |
| --- | --- |
| `MAIN-MEM-0001` | No `about`, and no person in context |
| `MAIN-MEM-0002` | No memory with that id in this tenant, app and scope |
| `MAIN-MEM-0005` | The agent has no memory grant for this verb |
| `MAIN-MEM-0013` | The store is down; `memory.context()` returns an empty block with `degraded: true` instead |

The check codes `MAIN-MEM-0020` to `MAIN-MEM-0025` and the rest are in [check codes](https://mainsdk.dev/docs/reference/check-codes).

## Swapping drivers

```yaml title="main.yaml"
memory:
  driver: mem0   # was db
```

Your calls stay the same. A provider that keeps readable text stores it as the provider's own, so the SDK does not seal it; a driver without `graph` fails `relate` and `hops` with `MAIN-MEM-0012`. See [Swap a driver](https://mainsdk.dev/docs/guides/swap-a-driver).

## Testing

`memory` runs on the `local` driver in tests, and the fake clock expires a memory without waiting. `runMemoryConformance` checks a driver and `fakeProvider` stands in for a provider, both from `mainsdk/testing`. See [the testing kit](https://mainsdk.dev/docs/interfaces/testing).

## Next

- [Agent memory, the concept](https://mainsdk.dev/docs/concepts/agent-memory)
- [Give an app agent memory](https://mainsdk.dev/docs/guides/give-an-agent-memory)
- [App agents](https://mainsdk.dev/docs/interfaces/agents)
