Skip to content

AI SDK integration

Memory Soda ships a subpath for the Vercel AI SDK:

import {
memoryMiddleware,
memoryTool,
toMemoryMessages,
} from '@memory-soda/sdk/ai';

It is a separate entry point, so importing the client never pulls in code that only makes sense alongside ai.


wrapLanguageModel gives a memory layer exactly the two seams it needs: transformParams runs before the provider call and can add to the prompt, and wrapGenerate / wrapStream see the finished turn and can write it down. Wrapping the model instead of every call site means memory behaves the same in generateText, streamText, and any agent loop built on them.

import { google } from '@ai-sdk/google';
import { generateText, wrapLanguageModel } from 'ai';
import { MemorySoda } from '@memory-soda/sdk';
import { memoryMiddleware } from '@memory-soda/sdk/ai';
const memory = new MemorySoda();
const { threadId, dataset } = await memory.createThread({ dataset: 'u_42' });
const model = wrapLanguageModel({
model: google('gemini-2.5-flash'),
middleware: memoryMiddleware({ memory, dataset, threadId }),
});
// Recalls before the call, records the turn after it.
const { text } = await generateText({ model, prompt: 'What should I cook?' });
Option Default What it does
memory , The client to use. Required.
dataset , Whose memory this is. Required.
threadId , Thread to append to. A string, or a function resolving one per call. Omit it and the middleware only reads.
recall true Inject recalled context into the system prompt.
write true when a thread is available Record the finished turn.
limit project setting Facts to retrieve.
includeEpisodes false Also inject cross-thread episode summaries.
recallTimeoutMs 2000 Cap on how long recall may delay a model call.
onError console.warn Called when recall or the write-back fails.

Both matter, because breaking either makes the middleware worse than no memory at all.

A recall failure never fails the call. If memory is down, slow, or empty, transformParams returns the prompt untouched and the model answers without context. The timeout is part of this: past recallTimeoutMs the call proceeds regardless, because a slow answer with context is worse than a fast one without.

The write-back never blocks the response. It is fired and forgotten, with failures routed to onError. On streamText only the prompt is recorded at call time, the assistant’s reply arrives on the next turn’s prompt, so the write stays off the streaming path entirely.

Memory is written through threads, so writing needs one. For a chat app you usually already have a thread per conversation. For anything that starts without one, pass a function:

let threadId: string | undefined;
memoryMiddleware({
memory,
dataset,
threadId: async () => {
threadId ??= (await memory.createThread({ dataset })).threadId;
return threadId;
},
});

Cache it as shown, a function that creates a thread on every call produces one thread per turn, and each thread’s episode then sees a single message.


The middleware recalls on every turn, which is right for a chat assistant and wasteful for an agent that mostly runs tools. A tool inverts it: the model decides when a lookup is worth a round trip, and says what it is looking for, usually a better retrieval query than the raw user message.

import { memoryTool } from '@memory-soda/sdk/ai';
const { text } = await generateText({
model: google('gemini-2.5-flash'),
tools: { recallMemory: memoryTool({ memory, dataset }) },
prompt: 'Book me the usual table.',
});

The tool returns the rendered context block as a string. When nothing matches it returns a plain sentence saying so, rather than an error, “nothing known” is an answer the model should act on, not a failure it should retry.

You can use both: the middleware for ambient personalisation, the tool for deliberate lookups.


The AI SDK models a message as a role plus an array of parts. Memory stores text. toMemoryMessages flattens one into the other:

import { toMemoryMessages } from '@memory-soda/sdk/ai';
await memory.addMessages(threadId, toMemoryMessages(messages));

The middleware uses this internally; call it directly when you are writing to memory yourself, from an onFinish handler, say.

Tool calls and results are kept, not dropped. A turn where the agent looked something up and got an answer back is often the only durable fact in it, and an agent that silently forgets its own tool use is the failure this package exists to prevent. They render as [called getProfile({"id":"42"})] and [getProfile returned {"tier":"gold"}].

Messages that flatten to nothing, a bare file attachment, an empty assistant turn, are skipped rather than stored blank, and roles memory does not model are skipped rather than coerced.


The middleware is a convenience. Under it, the loop is two calls:

const { context } = await memory.recall({ dataset, query: userInput });
const { text } = await generateText({
model: google('gemini-2.5-flash'),
system: context ? `What you know about this user:\n${context}` : undefined,
prompt: userInput,
});
await memory.addMessages(threadId, [
{ role: 'user', content: userInput },
{ role: 'assistant', content: text },
]);

Reach for this when you want the context block somewhere other than the system prompt, or when you are not using the AI SDK at all.