Vercel AI SDK Memory
Give any AI SDK app long-term memory with one wrapLanguageModel call: recalled context injected before generation, the exchange persisted after it, streaming-safe, on AI SDK v6 and v7 — plus standalone helpers and five agent memory tools.
What the integration provides
| Layer | What it does |
|---|---|
createMemorySyncMiddleware | Wrap a model once: recall runs before each generation and injects a system context block; the exchange persists verbatim after it. Works with generateText, streamText, and every provider. |
| Helpers | getMemoryContext, searchMemories, saveTurn, createMemorySyncOnFinish — for apps that wire memory around calls themselves. |
| Agent tools | memorySyncTools() — five structured tools (add, search, list, update, delete) the model can call, which never throw. |
| Mem0 | Supermemory | Zep | MemorySync | |
|---|---|---|---|---|
| AI SDK v7 (current) | ✗ (v6, bundled as a hard dep) | ✗ (v5 — two majors behind) | ✗ (v6) | ✓ v6 AND v7, one build |
| Pattern | custom provider wrapping 5 vendors | tools only | middleware | middleware + helpers + tools |
| Guaranteed persistence | ✓ (opaque) | ✗ — the LLM decides | ✓ | ✓ deterministic, with an off switch |
| Extra LLM provider deps | 5 bundled | 0 | 0 | 0 — provider-agnostic |
Install
npm install memorysync-ai-sdk ai
Set MEMORYSYNC_API_KEY in the environment, or pass apiKey explicitly. Supports ai 6.x and 7.x (Node 18+ for the package; the ai package itself requires Node 22+ on v7). This is a JavaScript/TypeScript surface — for Python agents use the LangChain, LangGraph or CrewAI integrations.
Wrap the model once
import { generateText, wrapLanguageModel } from "ai";import { openai } from "@ai-sdk/openai";import { createMemorySyncMiddleware } from "memorysync-ai-sdk";const model = wrapLanguageModel({model: openai("gpt-4o-mini"),middleware: createMemorySyncMiddleware({userId: "customer-7", // per-end-user scoping — requiredsessionId: "thread-42", // groups the stored transcript}),});// First conversationawait generateText({ model, prompt: "I'm vegetarian and I fly aisle." });// Any later call — same user, any process, any providerconst { text } = await generateText({model,prompt: "Book my trip: flight plus a dinner spot.",});// The model already saw: vegetarian, aisle seat — injected from memory.
Injection happens once per user turn — on tool-loop continuation steps (where the last message is a tool result, not the user) the prompt passes through untouched, so multi-step agents never pay for the context block twice. Your app’s own system prompt stays first; the memory block is inserted after it.
Streaming and Next.js
// app/api/chat/route.tsimport { convertToModelMessages, streamText, wrapLanguageModel } from "ai";import { openai } from "@ai-sdk/openai";import { createMemorySyncMiddleware } from "memorysync-ai-sdk";export async function POST(req: Request) {const { messages, userId, sessionId } = await req.json();const model = wrapLanguageModel({model: openai("gpt-4o-mini"),middleware: createMemorySyncMiddleware({ userId, sessionId }),});const result = streamText({model,messages: convertToModelMessages(messages),});return result.toUIMessageStreamResponse();}
Persistence accumulates the text deltas and writes once on the stream’s finish part — chunk latency is untouched, and the write completes before the stream closes, which is what guarantees it happened before a serverless runtime freezes the sandbox. A stream that errors or is aborted never reaches its finish, so nothing partial is ever stored.
Recall-only and persist-only
// Read-only wrap: recalls context, never writes anything.createMemorySyncMiddleware({ userId, persist: false });// Write-only wrap: persists exchanges, injects nothing.createMemorySyncMiddleware({ userId, recall: false });// Tune recall depth and the injected block's shape.createMemorySyncMiddleware({userId,k: 12,template: "What you know about this user:\n{context}",});
Standalone helpers
For apps that keep their model calls unwrapped: the same recall pipeline and the same idempotent persistence as the middleware, callable directly. Mixing styles is safe — both write the same idempotency seeds, so a turn stored by the middleware and again by saveTurn lands once.
import {createMemorySyncOnFinish,getMemoryContext,saveTurn,searchMemories,} from "memorysync-ai-sdk";// 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 middleware's reported-never-thrown.await saveTurn({ user: "I'm vegetarian", assistant: "Noted!", sessionId: "thread-42" },{ userId: "customer-7" },);// 4. Or persist from onFinish — fires once after the full tool loop.const result = streamText({model,prompt,onFinish: createMemorySyncOnFinish({ userId: "customer-7", user: prompt }),});
Agent memory tools
import { generateText, stepCountIs } from "ai";import { memorySyncTools } from "memorysync-ai-sdk";const { text } = await generateText({model: openai("gpt-4o-mini"),tools: { ...memorySyncTools({ userId: "customer-7" }) },stopWhen: stepCountIs(5),prompt: "Remember that I prefer aisle seats.",});// Untrusted agents: search + list only.memorySyncTools({ userId: "customer-7", readOnly: true });
| Tool | What it does | Failure behaviour |
|---|---|---|
addMemory | Save one durable fact; duplicate saves answer “already stored”. | Readable error string — never throws. |
searchMemory | Semantic search with relevance scores. | Readable error string. |
listMemories | Newest-first listing. | Readable error string. |
updateMemory | Change tags/importance. Memory text is immutable. | Readable error string. |
deleteMemory | Permanent delete by id, scoped to the configured user. | Readable error string. |
Same five operations, same response strings as the LangChain tools and the CrewAI tools — an agent moved between frameworks keeps behaving the same way. A memory failure can never abort the tool loop: tools return error strings instead of throwing.
See which memories were used
const result = await generateText({ model, prompt });const memories = result.providerMetadata?.memorysync?.memories;// [{ id: "m_812", text: "human: I'm vegetarian", score: 0.62 }, ...]// Render "why this answer" UI, log for evals, or audit recalls.// Streaming: the same field on the awaited stream result.const streamed = streamText({ model, prompt });const meta = (await streamed.providerMetadata)?.memorysync;
Public API
| Export | Kind | Notes |
|---|---|---|
createMemorySyncMiddleware | Middleware factory | userId required; sessionId, recall, persist, k, template, specificationVersion, onError optional. |
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. |
createMemorySyncOnFinish | Callback factory | onFinish persist for generateText/streamText — never throws. |
memorySyncTools | Tool factory | Five tools; readOnly: true returns search + list only. |
MemorySyncContextProvider | Class | The recall pipeline behind the middleware, directly usable. |
Supported versions
| Package | Registry | Requires | Runtime |
|---|---|---|---|
memorysync-ai-sdk 1.0.0 | npm | ai >=6 <8 (peer) | Node 18+ (ai@7 itself requires Node 22+) |
One build serves both majors: the package touches only fields that are identical across the v3 and v4 provider specs and imports nothing from ai at runtime. The CI suite runs identically against ai@6 + zod 3 and ai@7 + zod 4. Edge-safe by construction — no Node-only imports, so it runs on Vercel Edge and Cloudflare Workers.