Vercel AI SDK Integration
Add persistent, decay-weighted memory to any Vercel AI SDK app. @dakera-ai/ai-sdk plugs into the SDK's two standard extension points — language model middleware and tools — so cross-session memory drops in without changing your model or provider code.
Overview
The Vercel AI SDK gives you a clean, provider-agnostic surface — generateText, streamText, tools — but every call is stateless. Close the process and the model forgets everything a user told it. @dakera-ai/ai-sdk backs that surface with a self-hosted Dakera memory server so relevant facts from earlier sessions are recalled automatically, and new exchanges are written back — with importance-scored, decay-weighted recall so stale context stops competing with fresh, relevant facts.
The package exports exactly two entry points, mirroring the AI SDK's two extension points:
createDakeraMemoryMiddleware— language model middleware for transparent recall + write-back. Wrap once, then keep callinggenerateText/streamTextas before.createDakeraTools— aToolSetofrecallMemory/storeMemorytools that hand memory control to the model, so it decides when to look something up or persist a fact.
When to use which
| Use case | Reach for |
|---|---|
| Continuity should "just work" with no prompt changes | Middleware |
| Every turn should recall context and store the exchange | Middleware |
| The model should decide when a fact is worth remembering | Tools |
| Agentic / multi-step flows where explicit recall improves quality | Tools |
| Transparent recall, but deliberate writes | Both (middleware with store: false + tools) |
With the middleware, recall happens in transformParams and write-back in wrapGenerate — so generateText and streamText gain cross-session memory with no change to your provider code.
Quick Start
Run Dakera
Dakera is a self-hosted memory server with zero external dependencies. The Docker Compose setup brings up the server plus MinIO object storage, listening on :3000:
git clone https://github.com/dakera-ai/dakera-deploy
cd dakera-deploy
docker compose up -d
# server + MinIO object storage, listening on :3000
See dakera-ai/dakera-deploy for the full Compose configuration.
Install
npm install @dakera-ai/ai-sdk ai @dakera-ai/dakera zod
Requirements: Node ≥ 20, and the peer dependencies ai ≥ 6.0.0, @dakera-ai/dakera ≥ 0.11.54, and zod ≥ 3.23.0. Bring your own model provider (e.g. @ai-sdk/openai).
Configure
Both helpers resolve their connection from options first, then environment variables, then a local default (http://localhost:3000):
export DAKERA_URL=http://localhost:3000 # default — can omit
export DAKERA_API_KEY=dk-your-key-here
How it works
The integration hooks into the two documented AI SDK extension points and nothing else — no forked provider, no patched runtime.
The middleware (transparent path)
createDakeraMemoryMiddleware returns a language model middleware object (specificationVersion: "v3") with two hooks:
transformParams— before the model runs, it reads the text of the most recentusermessage and callsrecall(agentId, query, { top_k: recallK, min_importance: minImportance }). If any memories come back, it prepends a singlesystemmessage to the prompt: a header line followed by one- <content>bullet per memory. If there's no user text, or recall returns nothing, the prompt is passed through untouched. This hook runs for bothgenerateTextandstreamText.wrapGenerate— after generation completes, it writes the exchange back:User: <the user's message>andAssistant: <the reply>, each stored at the configuredimportance. This hook runs forgenerateText-style generate calls.
transformParams, so it applies to streamText too. Automatic write-back lives in wrapGenerate, which covers generate calls — to persist streamed turns, use the tools pattern or store explicitly (see below).The tools (model-driven path)
createDakeraTools returns an AI SDK ToolSet with two zod-typed tools the model can call:
recallMemory({ query, topK? })→ runsrecalland returns an array of{ content, importance, score }.storeMemory({ content, importance? })→ persists a fact and returns{ id, status: "stored" }.
Pattern 1 — Memory middleware (transparent)
createDakeraMemoryMiddleware wraps any language model with wrapLanguageModel. On every call it recalls the most relevant memories, injects them as a system message, then stores the exchange. Nothing about your generation code changes.
import { generateText, wrapLanguageModel } from "ai";
import { openai } from "@ai-sdk/openai";
import { createDakeraMemoryMiddleware } from "@dakera-ai/ai-sdk";
const model = wrapLanguageModel({
model: openai("gpt-4o-mini"),
middleware: createDakeraMemoryMiddleware({
agentId: "example-user-1", // Scope memories to a user or agent ID
recallK: 5, // Recall up to 5 relevant memories per call
importance: 0.8, // Importance assigned to stored exchanges
}),
});
// Session 1 — the exchange is stored automatically
await generateText({
model,
prompt: "My name is Alex and I'm building a KV store called Kestrel in Go.",
});
// Session 2 — a later run recalls the earlier context, no prompt hints
const { text } = await generateText({
model,
prompt: "What do you know about my current project?",
});
// → references Kestrel and Alex without any context in the prompt
What gets injected
When recall returns memories, the middleware prepends one system message shaped like this (the header is configurable via the header option):
# injected as a leading system message
Relevant memories from previous sessions (use them if helpful):
- User: My name is Alex and I'm building a KV store called Kestrel in Go.
- Assistant: Nice — a Go KV store called Kestrel. Want help with the storage engine?
Streaming with streamText
The wrapped model streams exactly like an unwrapped one — recall injection runs transparently in transformParams before the first token. Because automatic write-back runs in wrapGenerate, pair streaming with the tools pattern (or a manual storeMemory call) when you want streamed turns persisted.
import { streamText, wrapLanguageModel } from "ai";
import { openai } from "@ai-sdk/openai";
import { createDakeraMemoryMiddleware } from "@dakera-ai/ai-sdk";
const model = wrapLanguageModel({
model: openai("gpt-4o-mini"),
middleware: createDakeraMemoryMiddleware({ agentId: "example-user-1" }),
});
const result = streamText({
model,
prompt: "Remind me what I'm working on, then suggest a next step.",
});
for await (const chunk of result.textStream) {
process.stdout.write(chunk); // recall already applied before streaming began
}
Pattern 2 — Memory tools (model-driven)
createDakeraTools returns an AI SDK ToolSet with two tools — recallMemory and storeMemory — that the model decides when to call. Spread the result straight into the tools field.
import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";
import { createDakeraTools } from "@dakera-ai/ai-sdk";
const tools = createDakeraTools({
agentId: "example-user-2",
recallK: 5, // default topK when the model omits it
importance: 0.8, // default importance when the model omits it
});
// The model calls storeMemory(...) to persist the preference
await generateText({
model: openai("gpt-4o-mini"),
tools,
maxSteps: 3,
prompt: "Please remember that I always want code examples in TypeScript.",
});
// A later call: the model calls recallMemory(...) before answering
await generateText({
model: openai("gpt-4o-mini"),
tools,
maxSteps: 3,
system: "Always check memory for user preferences before answering.",
prompt: "Show me a quick example of how to read a file.",
});
The tools return structured results the model can reason over. recallMemory yields [{ content, importance, score }, …]; storeMemory yields { id, status: "stored" }. Provide a topK to a recallMemory call to override the default per-lookup, or an importance (0–1) to a storeMemory call to weight a critical fact.
Pattern 3 — Combined (transparent + explicit)
Use the middleware for automatic continuity and the tools for deliberate writes in the same call. Set store: false on the middleware so it only recalls, and let the tools own persistence. Share one DakeraClient so both hit the same connection.
import { generateText, wrapLanguageModel } from "ai";
import { openai } from "@ai-sdk/openai";
import { createDakeraMemoryMiddleware, createDakeraTools } from "@dakera-ai/ai-sdk";
import { DakeraClient } from "@dakera-ai/dakera";
const client = new DakeraClient({ baseUrl: process.env.DAKERA_URL! });
const agentId = "user-1234";
const model = wrapLanguageModel({
model: openai("gpt-4o"),
middleware: createDakeraMemoryMiddleware({ client, agentId, store: false }),
});
const tools = createDakeraTools({ client, agentId });
// Middleware recalls context; the model writes back only what matters
await generateText({ model, tools, maxSteps: 6, prompt: "My deadline for Velox is Friday the 11th." });
Sharing a pre-built client
Pass a DakeraClient via the client option to reuse one connection across many agents — or to avoid repeating credentials. When client is set, apiUrl / apiKey are ignored and the client is used directly, on both the middleware and the tools.
const client = new DakeraClient({
baseUrl: "http://my-dakera.internal:3000",
apiKey: process.env.DAKERA_API_KEY,
});
// Every agent shares the same connection, scoped by its own agentId
const supportMiddleware = createDakeraMemoryMiddleware({ client, agentId: "support-bot" });
const salesTools = createDakeraTools({ client, agentId: "sales-bot" });
Error safety
Persisting memory must never break generation. In wrapGenerate, the model runs first via doGenerate(); only afterwards does the middleware attempt to write the exchange, and that write is wrapped in a try / catch that swallows storage errors. A memory-server outage degrades a call to a plain, stateless generation — never a crash or a 500.
// Simplified from src/middleware.ts — write-back is best-effort
const result = await doGenerate();
if (!shouldStore) return result;
try {
// store "User: …" and "Assistant: …" at the configured importance
await client.storeMemory(agentId, { content, importance });
} catch {
// Swallow storage errors: memory is best-effort, generation is not.
}
return result;
Options
Middleware — createDakeraMemoryMiddleware
| Option | Type | Default | Description |
|---|---|---|---|
agentId | string | — | Scopes stored/recalled memories to a user or agent (required) |
recallK | number | 5 | Memories recalled and injected per call (top_k) |
minImportance | number | 0 | Minimum importance threshold for recall (min_importance) |
importance | number | 0.7 | Importance assigned to memories stored after each call |
store | boolean | true | Whether to write the exchange back after generation |
header | string | see note | Header line prepended to the injected memory block |
client | DakeraClient | — | Pre-built client; overrides apiUrl/apiKey |
apiUrl | string | $DAKERA_URL / http://localhost:3000 | Dakera server base URL |
apiKey | string | $DAKERA_API_KEY | Dakera API key (dk-…) |
Default header: "Relevant memories from previous sessions (use them if helpful):"
Tools — createDakeraTools
| Option | Type | Default | Description |
|---|---|---|---|
agentId | string | — | Scopes stored/recalled memories to a user or agent (required) |
recallK | number | 5 | Default number of memories returned by recallMemory |
importance | number | 0.7 | Default importance for storeMemory when the model omits it |
client | DakeraClient | — | Pre-built client; overrides apiUrl/apiKey |
apiUrl | string | $DAKERA_URL / http://localhost:3000 | Dakera server base URL |
apiKey | string | $DAKERA_API_KEY | Dakera API key (dk-…) |
Tool call inputs
| Tool | Input | Type | Notes |
|---|---|---|---|
recallMemory | query | string | What to search for in memory (required) |
recallMemory | topK | number | Positive integer; overrides recallK for this call (optional) |
storeMemory | content | string | The fact to remember (required) |
storeMemory | importance | number | Weight from 0 (trivial) to 1 (critical); overrides the default (optional) |
Environment variables
Connection resolves in this order: an explicit client, then apiUrl/apiKey options, then these environment variables, then the local default URL.
| Variable | Default | Description |
|---|---|---|
DAKERA_URL | http://localhost:3000 | Dakera server base URL (can be omitted) |
DAKERA_API_KEY | — | API key for the Dakera server (dk-…) |
Next.js & edge
The integration is plain server-side TypeScript, so it drops into Next.js Route Handlers, Server Actions, and server components. Read DAKERA_URL / DAKERA_API_KEY from the server environment and keep the wrapped model (or tools) on the server — never ship your API key to the browser.
// app/api/chat/route.ts
import { streamText, wrapLanguageModel } from "ai";
import { openai } from "@ai-sdk/openai";
import { createDakeraMemoryMiddleware } from "@dakera-ai/ai-sdk";
export async function POST(req: Request) {
const { messages, userId } = await req.json();
const model = wrapLanguageModel({
model: openai("gpt-4o-mini"),
middleware: createDakeraMemoryMiddleware({ agentId: userId }),
});
const result = streamText({ model, messages });
return result.toUIMessageStreamResponse();
}
Related integrations
Links
- GitHub — dakera-ai-sdk
- npm — @dakera-ai/ai-sdk
- Vercel AI SDK
- Dakera deploy — Docker Compose setup
- All integrations
Troubleshooting
| Symptom | Fix |
|---|---|
| Recall is empty on the first call | Expected — the first call has no history. After the first exchange stores, later calls recall. |
| A Dakera outage breaks generation | It won't. wrapGenerate catches all storage errors, so an outage degrades to a stateless call, never a crash. |
| Streamed turns aren't being remembered | Write-back runs in wrapGenerate (generate calls). For streamText, add the tools pattern or call storeMemory explicitly. |
| Recalled memories look stale or off-topic | Raise minImportance to filter low-importance recall, or lower recallK so fewer, more relevant memories are injected. |
| Server returns 401 | Verify DAKERA_API_KEY matches the server's key. Keys look like dk-…. |
Cannot find name 'process' | Add "types": ["node"] to your tsconfig.json compilerOptions. |
| Peer dependency warnings on install | Ensure ai ≥ 6.0.0, @dakera-ai/dakera ≥ 0.11.54, and zod ≥ 3.23.0 are installed alongside the package. |
Frequently Asked Questions
How do I add memory to the Vercel AI SDK?
Install @dakera-ai/ai-sdk, run a self-hosted Dakera server, then wrap your model with createDakeraMemoryMiddleware or add createDakeraTools to your generateText / streamText call. No model or provider changes required.
Does Dakera work with streaming?
Yes. Recall injection runs in transformParams, which the SDK applies to both generateText and streamText, so streamed calls recall context transparently. Automatic write-back runs in wrapGenerate (generate calls); to persist streamed turns, use the tools pattern or call storeMemory explicitly.
What happens if the Dakera server is down?
Generation still succeeds. The middleware runs the model first, then attempts write-back inside a try / catch that swallows storage errors — so an outage degrades to a plain stateless call instead of an exception.
What is the difference between the middleware and the tools?
The middleware recalls and stores transparently on every call with no prompt changes. The tools (recallMemory / storeMemory) hand control to the model, so it decides when to look something up or persist a fact. They compose — run the middleware with store: false for automatic recall and let the tools own writes.
What does Dakera add to the AI SDK?
Persistent cross-session memory with decay-weighted recall — importance-scored memories that decay over time, so stale context stops competing with fresh, relevant facts. The same engine scores 88.2% Recall@20 on the 1,540-question LoCoMo benchmark.
Ready to ship?
Deploy Dakera in minutes — self-hosted and free. Cloud managed hosting coming soon.
Give your AI agents persistent memory
Self-host free today — or join the Dakera Cloud waitlist for managed hosting, SLA & founder pricing.