Agent memory
memoryLet app agents remember the people they serve, inside the tenant and the app, with untrusted recall, versions and forgetting you can prove.
memoryFree in your app, Apache-2.0Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.
Stability The promise an interface ships with.
- Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
- Beta Usable in production; may change before it becomes stable.
- Experimental May change or go away; needs an explicit opt-in in main.yaml.
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.
At a glance
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 }import { ai } from "mainsdk";
import { memory } from "mainsdk/experimental/memory";
await memory.remember("Prefers WhatsApp over email", { kind: "preference" }); // about the person the agent acts for
const ctx = await memory.context({ query: message, budget: 800 }); // pinned first, in one <memory> data block
const reply = await ai.generate({
model: "anthropic/claude-sonnet-4-5",
messages: [{ role: "user", content: ctx.text }, { role: "user", content: message }], // data, never system text
feature: "support-chat",
agent: "support-bot",
user,
});
const receipt = await memory.forget({ about: user.id }, { reason: "user request" }); // signed, hard, cascadesCalls
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()
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
7 drivers. Select one to see its line in main.yaml.
memory:
driver: dbChange this one line to swap providers; your calls stay the same.Your database integration
Codes
| Code | Where it appears | Meaning |
|---|---|---|
MAIN-MEM-0020 | Checkmainsdk check, before deploy | A driver that only soft-deletes holds memories that may be personal |
MAIN-MEM-0021 | Checkmainsdk check, before deploy | A region the app serves has no endpoint in the memory driver |
MAIN-MEM-0022 | Checkmainsdk check, before deploy | Recalled memory is passed as the system text of an ai call |
MAIN-MEM-0023 | Checkmainsdk check, before deploy | A driver is named memory; the in-process driver is local (ADR-0039) |
MAIN-MEM-0024 | Checkmainsdk check, before deploy | children: guardian with an extract model on a provider |
MAIN-MEM-0025 | Checkmainsdk check, before deploy | The local memory driver in production |
MAIN-MEM-0001 | RuntimeError responses and logs | No subject: about is missing and the call has no on_behalf_of or session user (400) |
MAIN-MEM-0002 | RuntimeError responses and logs | No memory with that id in the current tenant, app and scope (404, same body either way) |
MAIN-MEM-0003 | RuntimeError responses and logs | The injection check flagged the text of a write; nothing was stored (422) |
MAIN-MEM-0004 | RuntimeError responses and logs | A recalled memory was flagged by the injection check and left out of the context (logged) |
MAIN-MEM-0005 | RuntimeError responses and logs | The agent has no memory grant for this verb, or forget: deny (403) |
MAIN-MEM-0006 | RuntimeError responses and logs | The agent is over its memory recalls or writes per day (429) |
MAIN-MEM-0007 | RuntimeError responses and logs | Forget refused: a legal hold applies to the person (409) |
MAIN-MEM-0008 | RuntimeError responses and logs | A children's rule refused it: children are off, no guardian consent, or a provider model would extract (403) |
MAIN-MEM-0009 | RuntimeError responses and logs | Invalid input: unknown kind, bad duration, pan label, k over 100, hops over 2 or a bad predicate (400) |
MAIN-MEM-0010 | RuntimeError responses and logs | Revise of a version that is no longer current (409) |
MAIN-MEM-0011 | RuntimeError responses and logs | Import refused: a line is malformed, or personal or non-procedure memory would cross a tenant or app (422) |
MAIN-MEM-0012 | RuntimeError responses and logs | The driver doesn't declare the capability the call needs, such as asOf, hops or relate (501) |
MAIN-MEM-0013 | RuntimeError responses and logs | The memory store is unreachable (503, retryable); context degrades to an empty block |
MAIN-MEM-0014 | RuntimeError responses and logs | An Anthropic memory tool path is outside /memories/ or tries to traverse (400) |
MAIN-MEM-0015 | RuntimeError responses and logs | A driver hasn't confirmed the erase yet; no receipt is issued and the erase keeps retrying (503) |
MAIN-MEM-0016 | RuntimeError responses and logs | A write from a tool, MCP, the web or an import was refused by from-tools: refuse (403) |
Features
Nothing here has shipped yet. Each feature lands in its phase; the spec ticket builds it.
| Feature | Phase | Milestone | Spec ticket |
|---|---|---|---|
| Remember and recall by tenant | Phase 1 | 0.4 | memory#remember-recall |
| Model context with pinned memories in one escaped data block | Phase 1 | 0.4 | memory#context-pin |
| Versions with revise | Phase 1 | 0.4 | memory#revise-history |
| Hard forget with cascade and a signed receipt | Phase 1 | 0.4 | memory#forget-receipt |
| Injection check and quarantine for tool and import writes | Phase 1 | 0.4 | memory#guards |
| Agent grants | Phase 1 | 0.4 | memory#governance |
| Personal label | Phase 1 | 0.4 | memory#privacy-keys |
| Audit of every write | Phase 1 | 0.4 | memory#audit |
| db driver with row-level security and pgvector | Phase 1 | 0.4 | memory#db-driver |
| Local driver with BM25 | Phase 1 | 0.4 | memory#local-driver |
| Memory conformance suite | Phase 1 | 0.4 | memory#conformance |
| MCP tools for remember | Phase 1 | 0.4 | memory#mcp-tools |
| Anthropic memory tool compatibility | Phase 1 | 0.4 | memory#anthropic-tool |
| Children off by default | Phase 1 | 0.4 | memory#children |
| Learn memories from a conversation | Phase 1 | 0.5 | memory-advanced#learn |
| Consolidate and reflect as a workflow | Phase 1 | 0.5 | memory-advanced#consolidate |
| Relations and recall hops | Phase 1 | 0.5 | memory-advanced#relate |
| Lossless JSONL export and import | Phase 1 | 0.5 | memory-advanced#export-import |
| Knowledge-graph MCP server compatibility | Phase 1 | 0.5 | memory-advanced#kg-compat |
| Mem0 | Phase 1 | 0.5 | memory-advanced#provider-drivers |
| Pinecone and MCP drivers | Phase 1 | 0.5 | memory-advanced#provider-drivers-vector-mcp |
Quick start
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 }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,recallandcontexton one scoped store, with versions andasOfreads.forgetwith a signed receipt, and erasure throughprivacy.- 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
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
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 overwrittenPin what the agent must always see
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
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.
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
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
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)
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 requestReference 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: ownsees only what it wrote. - Recalled text is untrusted data, never system text;
checkreportsMAIN-MEM-0022when it is passed assystem. - The injection check runs on every write (
MAIN-MEM-0003) and every recall (MAIN-MEM-0004). remember,recallandforgetnever call a generative model.- Every write, recall and forget is audited with ids only, never the text.
privacy.export()andprivacy.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.
Swapping drivers
memory:
driver: mem0 # was dbYour 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.
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.
Next
Was this page helpful?