Mastra Memory
Give Mastra agents long-term memory on the native processor pipeline: recalled context injected before each call, the exchange persisted after it, streaming-safe, with four ways to resolve which user the agent is acting for — plus a zero-config wrapper, five agent memory tools, and standalone helpers.
What the integration provides
| Layer | What it does |
|---|---|
createMemorySyncProcessors | An input processor (recall injected as a system message before the model call) and an output processor (the exchange persisted verbatim after it) for new Agent({ inputProcessors, outputProcessors }). |
withMemorySync | Zero-config wrapper: pass your agent config through it and both processors are merged in. Your own processors are preserved. |
| Agent tools | createMemorySyncTools() — five structured tools (add, search, list, update, delete) the model can call, which never throw. |
| Helpers | getMemoryContext, searchMemories, saveTurn — for hand-wired setups. |
| Mem0 | Supermemory | Zep | MemorySync | |
|---|---|---|---|---|
| Works on @mastra/core 1.x (current) | ✗ (peer <0.17 — predates 1.0, removed from their monorepo) | △ (alpha dependency chain) | ✓ (>=1.42) | ✓ >=1.42 <2, CI-tested on the latest 1.x |
| Pattern | 2 tools — the LLM decides | processors + wrapper | processors + 3 tools | processors + wrapper + 5 tools + helpers |
| Guaranteed persistence | ✗ — the LLM decides | ✓ | ✓ | ✓ deterministic, with an off switch |
| Identity resolution | static config | static config | static config | 4 sources: static, resolver, RequestContext, memory.resource |
| Failed-stream half-turn guard | — | — | — | ✓ a dead stream stores nothing |
Install
npm install memorysync-mastra @mastra/core
Set MEMORYSYNC_API_KEY in the environment, or pass apiKey explicitly. Supports @mastra/core 1.42+ (Node 20+ for the package; current @mastra/core itself requires Node 22+). This is a JavaScript/TypeScript surface — for Python agents use the LangChain, LangGraph or CrewAI integrations.
Add the processors
import { Agent } from "@mastra/core/agent";import { openai } from "@ai-sdk/openai";import { createMemorySyncProcessors } from "memorysync-mastra";const { inputProcessor, outputProcessor } = createMemorySyncProcessors({userId: "customer-7", // per-end-user scopingsessionId: "thread-42", // groups the stored transcript});const agent = new Agent({id: "assistant",name: "Assistant",instructions: "You are a helpful assistant.",model: openai("gpt-4o-mini"),inputProcessors: [inputProcessor],outputProcessors: [outputProcessor],});// First conversationawait agent.generate("I'm vegetarian and I fly aisle.");// Any later call — same user, any thread, any modelconst { text } = await agent.generate("Book my trip: flight plus a dinner spot.",);// The model already saw: vegetarian, aisle seat — injected from memory.
Injection happens once per generate/stream call — the processors use processInput, not the per-step hook, so multi-step tool loops never pay for the context block twice. Your agent’s own instructions stay first; the memory block is appended to the system messages after them.
Or wrap the config
import { Agent } from "@mastra/core/agent";import { openai } from "@ai-sdk/openai";import { withMemorySync } from "memorysync-mastra";const agent = new Agent(withMemorySync({id: "assistant",name: "Assistant",instructions: "You are a helpful assistant.",model: openai("gpt-4o-mini"),},{ userId: "customer-7" },));// Identical behaviour to wiring the processors yourself. Your own// input/output processors are preserved — MemorySync's run after them.
Who is the agent acting for?
One Mastra agent typically serves many end users, so the processors resolve identity per call from four sources, in priority order. Whichever you use, memories stay scoped to that user — server-side, not by convention.
import { RequestContext } from "@mastra/core/request-context";import {createMemorySyncProcessors,MASTRA_RESOURCE_ID_KEY,MASTRA_THREAD_ID_KEY,} from "memorysync-mastra";// 1. Your own resolver — wins over everything (multi-user servers).createMemorySyncProcessors({resolveIdentity: (requestContext) => ({userId: currentSession.userId,sessionId: currentSession.threadId,}),});// 2. Static ids — one agent per user (scripts, workers).createMemorySyncProcessors({ userId: "customer-7", sessionId: "thread-42" });// 3. Mastra's RequestContext — set by your server middleware.const ctx = new RequestContext();ctx.set(MASTRA_RESOURCE_ID_KEY, "customer-7");ctx.set(MASTRA_THREAD_ID_KEY, "thread-42");await agent.generate(messages, { requestContext: ctx });// 4. Mastra's own memory plumbing — the ids ride the call.await agent.generate("I moved to Lisbon.", {memory: { resource: "customer-7", thread: "thread-42" },});
Streaming
const stream = await agent.stream("Plan a dinner for my team.", {memory: { resource: "customer-7", thread: "thread-42" },});for await (const chunk of stream.textStream) {process.stdout.write(chunk);}// Persistence runs once, after the full stream — chunk latency is// untouched.
A stream that errors mid-flight persists nothing: Mastra still fires the output hook on failed runs (finishReason: "error" — verified against a real agent in our test suite), and the processor detects that and skips the write, so a dead stream never leaves a half-turn behind. The retry that follows stores the exchange once.
Recall modes and switches
// Recall shape — pick per agent:createMemorySyncProcessors({ userId, mode: "query" }); // relevant to the latest message (default)createMemorySyncProcessors({ userId, mode: "profile" }); // stable overview of the usercreateMemorySyncProcessors({ userId, mode: "full" }); // both// Read-only agent: recalls context, never writes anything.createMemorySyncProcessors({ userId, persist: false });// Write-only agent: persists exchanges, injects nothing.createMemorySyncProcessors({ userId, recall: false });// Tune recall depth and the injected block's shape.createMemorySyncProcessors({userId,k: 12,template: "What you know about this user:\n{context}",});
Agent memory tools
import { Agent } from "@mastra/core/agent";import { createMemorySyncTools } from "memorysync-mastra";const agent = new Agent({id: "assistant",name: "Assistant",instructions: "Use the memory tools to remember durable facts.",model: openai("gpt-4o-mini"),tools: { ...createMemorySyncTools({ userId: "customer-7" }) },});await agent.generate("Remember that I prefer aisle seats.");// Untrusted agents: search + list only.createMemorySyncTools({ userId: "customer-7", readOnly: true });
| Tool | What it does | Failure behaviour |
|---|---|---|
add_memory | Save one durable fact; duplicate saves answer “already stored”. | Readable error string — never throws. |
search_memory | Semantic search with relevance scores. | Readable error string. |
list_memories | Newest-first listing. | Readable error string. |
update_memory | Change tags/importance. Memory text is immutable. | Readable error string. |
delete_memory | Permanent delete by id, scoped to the configured user. | Readable error string. |
Same five operations, same response strings as the LangChain tools, the AI SDK tools and the CrewAI tools — an agent moved between frameworks keeps behaving the same way. A memory failure can never abort the agent run: tools return error strings instead of throwing.
Standalone helpers
For workflows and hand-wired setups: the same recall pipeline and the same idempotent persistence as the processors, callable directly. Mixing styles is safe — both write the same idempotency seeds, so a turn stored by the processors and again by saveTurn lands once.
import {getMemoryContext,saveTurn,searchMemories,} from "memorysync-mastra";// 1. Prompt-ready context block ("" for a new user)const context = await getMemoryContext("what should I cook?", {userId: "customer-7",});// 2. Scored raw resultsconst hits = await searchMemories("dietary preferences", {userId: "customer-7",});// [{ id: "m_123", text: "human: I'm vegetarian", score: 0.62 }, ...]// 3. Explicit persistence — THROWS on failure (an explicit call is// owed the truth), unlike the processors' reported-never-thrown.await saveTurn({ user: "I'm vegetarian", assistant: "Noted!", sessionId: "thread-42" },{ userId: "customer-7" },);
Public API
| Export | Kind | Notes |
|---|---|---|
createMemorySyncProcessors | Processor factory | Returns { inputProcessor, outputProcessor }; userId, sessionId, resolveIdentity, mode, recall, persist, k, template, onError optional. |
withMemorySync | Config wrapper | Merges both processors into an agent config; preserves your own. |
createMemorySyncTools | Tool factory | Five tools; readOnly: true returns search + list only. |
getMemoryContext | Helper | Prompt-ready context block, "" for a new user. |
searchMemories | Helper | Scored {id, text, score} results. |
saveTurn | Helper | Explicit idempotent persist — throws on failure. |
resolveCallIdentity | Function | The four-source identity resolution, directly usable. |
MASTRA_RESOURCE_ID_KEY / MASTRA_THREAD_ID_KEY | Constants | Mastra’s RequestContext keys, re-exported for middleware. |
MemorySyncContextProvider | Class | The recall pipeline behind the processors, directly usable. |
Supported versions
| Package | Registry | Requires | Runtime |
|---|---|---|---|
memorysync-mastra 1.0.0 | npm | @mastra/core >=1.42 <2 (peer) | Node 20+ (@mastra/core itself requires Node 22+) |
The CI suite drives a real @mastra/core Agent through the processors — hook timing, RequestContext keys, message shapes — on the pinned core and again on the latest 1.x release, so a breaking change in a new Mastra version fails our pipeline before it can fail your agent.