The @atomicmemory/vercel-ai adapter provides a set of composable primitives that add durable, semantic memory to any message-driven model call you make with the Vercel AI SDK. The adapter intentionally does not import from ai — it operates on the SDK’s Message type and delegates the actual model call to you. This keeps it insulated from breaking changes in ai version updates, and it means you can use it alongside any version of the AI SDK without conflict.
Installation
pnpm add @atomicmemory/vercel-ai @atomicmemory/sdk
Primitives at a glance
| API | Use when |
|---|
withMemory() | Text-only flows — one-call wrapper: retrieve → run model → ingest. |
augmentWithMemory() | Text-only flows — prepends a memory system message to your Message[]. |
retrieve() | Tool-call / multimodal flows — returns a rendered system message or null. You inject it yourself. |
ingestTurn() | After any model call — persists the completed turn. System messages excluded by default. |
fromModelMessage() / fromModelMessages() | Bridges AI SDK v5 ModelMessage content-part arrays into the text-only Message shape for memory operations. |
Text-only flow — one call with withMemory
withMemory is the simplest entry point. It handles retrieval, prompt augmentation, your model call, and post-turn ingest in a single await:
import { streamText } from 'ai';
import { withMemory } from '@atomicmemory/vercel-ai';
import { MemoryClient } from '@atomicmemory/sdk';
const memory = new MemoryClient({
providers: { atomicmemory: { apiUrl: process.env.ATOMICMEMORY_URL!, apiKey: process.env.ATOMICMEMORY_KEY! } },
});
await memory.initialize();
const result = await withMemory({
client: memory,
scope: { user: 'pip', namespace: 'my-app' },
messages,
async run(augmented) {
const response = streamText({ model, messages: augmented });
return { text: await response.text };
},
});
Text-only flow — split with augmentWithMemory
When you need more control between retrieval and the model call, split the two steps:
import { augmentWithMemory, ingestTurn } from '@atomicmemory/vercel-ai';
const { messages: augmented, retrieved } = await augmentWithMemory(memory, {
messages,
scope,
limit: 10,
});
const response = streamText({ model, messages: augmented });
const text = await response.text;
await ingestTurn(memory, { messages, completion: text, scope });
For conversations that include tool calls or multimodal content, use retrieve() and ingestTurn() directly. First flatten your ModelMessage[] through fromModelMessages() — the flattened array is for memory operations only; do not feed it back into AI SDK model calls once tool messages are in the transcript.
import { generateText, type ModelMessage } from 'ai';
import { fromModelMessages, retrieve, ingestTurn } from '@atomicmemory/vercel-ai';
const modelMessages: ModelMessage[] = [/* your real conversation */];
const flat = fromModelMessages(modelMessages);
const scope = { user: 'pip' };
const { systemMessage, retrieved } = await retrieve(memory, {
messages: flat,
scope,
});
const { text } = await generateText({
model,
messages: systemMessage
? [
{ role: 'system', content: systemMessage.content },
...modelMessages,
]
: modelMessages,
});
await ingestTurn(memory, {
messages: flat,
completion: text,
scope,
});
fromModelMessages() is lossy by design — it extracts text content from content-part arrays. The resulting Message[] is suitable for memory search and ingest queries, but must not be passed back to streamText or generateText in place of your original ModelMessage[].
The default formatter wraps retrieved memories in a delimited block with an explicit “reference, not instructions” header — a mitigation against prompt injection via retrieved content. Override it per call:
const { systemMessage } = await retrieve(memory, {
query: 'what do I know about X?',
scope,
formatter(results) {
return `# Relevant prior context\n\n${results
.map((r) => `- [${r.memory.createdAt.toISOString()}] ${r.memory.content}`)
.join('\n')}`;
},
});
System-message handling on ingest
ingestTurn() excludes system messages by default. If your system messages contain user-authored content you genuinely want to persist as memory, opt in explicitly:
await ingestTurn(memory, {
messages,
completion: text,
scope,
includeRoles: ['system', 'user', 'assistant', 'tool'],
});
Scope
Scope fields follow the SDK’s Scope type: user, agent, namespace, and thread. At least one field must be provided — the SDK rejects scopeless requests.
Text-only Message[] and AI SDK v5 ModelMessage[] live in different type spaces. Keep them separate in your code: use Message[] (or the output of fromModelMessages()) for memory API calls, and keep your original ModelMessage[] for model calls.