Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
208 changes: 165 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,44 @@
# @dakera-ai/ai-sdk

Vercel AI SDK integration for [Dakera](https://dakera.ai) — a self-hosted memory
server that adds **persistent, decay-weighted vector recall** across sessions.
Memories are importance-scored and decay over time, so stale context stops
competing with fresh, relevant facts.
[![npm version](https://img.shields.io/npm/v/@dakera-ai/ai-sdk.svg)](https://www.npmjs.com/package/@dakera-ai/ai-sdk)
[![CI](https://github.com/dakera-ai/dakera-ai-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/dakera-ai/dakera-ai-sdk/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![TypeScript](https://img.shields.io/badge/TypeScript-5%2B-blue.svg)](https://www.typescriptlang.org/)

It plugs into the AI SDK's two standard extension points — **language model
middleware** and **tools** — so you can add cross-session memory to any AI SDK
app without changing your model or provider code.
Vercel AI SDK integration for [Dakera](https://dakera.ai) — a self-hosted memory server that adds **persistent, decay-weighted vector recall** across agent sessions.

## Install
Memories are importance-scored and decay over time, so stale context stops competing with fresh, relevant facts. The integration plugs into the AI SDK's two standard extension points — **language model middleware** and **tools** — so you can add cross-session memory to any AI SDK app without changing your model or provider code.

## Quick start

```bash
npm install @dakera-ai/ai-sdk ai @dakera-ai/dakera zod
```

Run a Dakera server first (self-hosted, no external dependencies):
see [`dakera-ai/dakera-deploy`](https://github.com/dakera-ai/dakera-deploy) for
the Docker Compose setup (server + MinIO). Point the integration at it with
`DAKERA_URL` (default `http://localhost:3000`) and `DAKERA_API_KEY`.
Run a Dakera server first (self-hosted, zero external dependencies):

```bash
# Docker Compose — server + MinIO object storage
curl -sSL https://raw.githubusercontent.com/dakera-ai/dakera-deploy/main/docker-compose.yml | \
docker compose -f - up -d
```

Then set two environment variables:

```bash
export DAKERA_URL=http://localhost:3000 # default — can omit
export DAKERA_API_KEY=dk-your-key-here
```

## Pattern 1 — Memory middleware (transparent)

`createDakeraMemoryMiddleware` wraps a model so that relevant memories are
recalled and injected as system context before every call, and the new exchange
is stored afterwards. No changes to your generation code.
`createDakeraMemoryMiddleware` wraps any language model. On every call it:

1. Recalls the most relevant memories for the current prompt
2. Injects them as a system message before generation
3. Stores the new exchange back into Dakera

Your generation code stays unchanged.

```typescript
import { generateText, wrapLanguageModel } from "ai";
Expand All @@ -34,41 +48,66 @@ import { createDakeraMemoryMiddleware } from "@dakera-ai/ai-sdk";
const model = wrapLanguageModel({
model: openai("gpt-4o"),
middleware: createDakeraMemoryMiddleware({
apiUrl: "http://localhost:3000",
apiKey: process.env.DAKERA_API_KEY,
agentId: "user-1234",
agentId: "user-1234", // scopes memories to this agent / user
recallK: 5, // inject up to 5 relevant memories per call
}),
});

// First session
await generateText({ model, prompt: "I'm building a Rust vector database." });
// Session 1
await generateText({
model,
prompt: "I'm building a Rust vector database called Velox.",
});

// A later session — the model recalls the earlier context automatically
const { text } = await generateText({ model, prompt: "What am I working on?" });
// → "You're building a Rust vector database."
// Session 2 — days later, different process
const { text } = await generateText({
model,
prompt: "What am I working on?",
});
// → "You're building Velox, a Rust vector database."
```

Under the hood the middleware uses `transformParams` to prepend recalled
memories as a system message, and `wrapGenerate` to persist the exchange.
Storage is best-effort: a memory-server error never breaks generation.
### Streaming

The middleware works with `streamText` unchanged:

```typescript
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"),
middleware: createDakeraMemoryMiddleware({ agentId: "user-1234" }),
});

const { textStream } = streamText({
model,
prompt: "Summarise what I've told you about my project.",
});

for await (const chunk of textStream) {
process.stdout.write(chunk);
}
```

### Options
### Middleware options

| Option | Default | Description |
| --- | --- | --- |
| `agentId` | — | Identifier that scopes stored/recalled memories (required) |
| `apiUrl` | `$DAKERA_URL` / `http://localhost:3000` | Dakera server URL |
| `apiKey` | `$DAKERA_API_KEY` | Dakera API key (`dk-...`) |
| `client` | — | A pre-built `DakeraClient` (overrides `apiUrl`/`apiKey`) |
| `recallK` | `5` | Memories to recall per call |
| `minImportance` | `0` | Minimum importance to recall |
| `importance` | `0.7` | Importance assigned to stored memories |
| `store` | `true` | Store the exchange after generation |
| Option | Type | Default | Description |
|---|---|---|---|
| `agentId` | `string` | — | Scopes stored/recalled memories **(required)** |
| `apiUrl` | `string` | `$DAKERA_URL` / `http://localhost:3000` | Dakera server URL |
| `apiKey` | `string` | `$DAKERA_API_KEY` | API key (`dk-...`) |
| `client` | `DakeraClient` | — | Pre-built client (overrides `apiUrl`/`apiKey`) |
| `recallK` | `number` | `5` | Memories to recall and inject per call |
| `minImportance` | `number` | `0` | Minimum importance score to recall (0 – 1) |
| `importance` | `number` | `0.7` | Importance assigned to stored memories (0 – 1) |
| `store` | `boolean` | `true` | Persist the exchange after generation |
| `header` | `string` | `"Relevant memories…"` | System-message header prepended to the memory block |

## Pattern 2 — Memory tools (model-driven)

`createDakeraTools` gives the model explicit `recallMemory` and `storeMemory`
tools, so it decides when to look something up or remember it.
`createDakeraTools` returns `recallMemory` and `storeMemory` tools. The model decides when to look something up or persist a new fact — useful for agentic workflows where explicit memory control improves quality.

```typescript
import { generateText } from "ai";
Expand All @@ -81,13 +120,96 @@ const { text } = await generateText({
model: openai("gpt-4o"),
tools,
maxSteps: 4,
prompt: "Remember that I prefer metric units, then convert 5 miles to km.",
prompt: "Remember that I prefer metric units. Then convert 5 miles to km.",
});
// Model calls storeMemory("User prefers metric units", importance 0.8),
// then answers: "5 miles = 8.047 km"
```

The two patterns compose — use the middleware for automatic continuity and the
tools when you want the model to manage memory deliberately.
### Tool options

| Option | Type | Default | Description |
|---|---|---|---|
| `agentId` | `string` | — | Scopes stored/recalled memories **(required)** |
| `apiUrl` | `string` | `$DAKERA_URL` / `http://localhost:3000` | Dakera server URL |
| `apiKey` | `string` | `$DAKERA_API_KEY` | API key (`dk-...`) |
| `client` | `DakeraClient` | — | Pre-built client (overrides `apiUrl`/`apiKey`) |
| `recallK` | `number` | `5` | Default `topK` for the recall tool |
| `importance` | `number` | `0.7` | Default importance for the store tool |

## Pattern 3 — Combined (transparent + explicit)

Use the middleware for automatic continuity and the tools when you want the model to manage memory deliberately in the same call:

```typescript
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";

// Share one client instance across middleware and tools
const client = new DakeraClient({ baseUrl: process.env.DAKERA_URL! });
const agentId = "user-1234";

const model = wrapLanguageModel({
model: openai("gpt-4o"),
// Recall automatically, but let tools handle persistence
middleware: createDakeraMemoryMiddleware({ client, agentId, store: false }),
});

const tools = createDakeraTools({ client, agentId });

const { text } = await generateText({
model,
tools,
maxSteps: 6,
system:
"You are a helpful assistant. Use storeMemory for facts the user will want remembered across sessions.",
prompt: "My deadline for the Velox project is Friday the 11th.",
});
```

## Using a pre-built client

Share connection config across multiple agents or avoid repeating credentials:

```typescript
import { DakeraClient } from "@dakera-ai/dakera";
import { createDakeraMemoryMiddleware, createDakeraTools } from "@dakera-ai/ai-sdk";

const client = new DakeraClient({
baseUrl: "http://my-dakera.internal:3000",
apiKey: process.env.DAKERA_API_KEY,
});

// All agents share the same connection
const supportMiddleware = createDakeraMemoryMiddleware({ client, agentId: "support-bot" });
const salesTools = createDakeraTools({ client, agentId: "sales-bot" });
```

## Running the examples

```bash
git clone https://github.com/dakera-ai/dakera-ai-sdk
cd dakera-ai-sdk/examples
npm install
DAKERA_URL=http://localhost:3000 DAKERA_API_KEY=dk-dev npx tsx 01-middleware.ts
```

See [`examples/README.md`](examples/README.md) for all examples and prerequisites.

## Troubleshooting

**`Cannot find name 'process'`** — add `"types": ["node"]` to your `tsconfig.json` `compilerOptions`.

**Memory recall is empty on first call** — expected. The first call has no history. After the first exchange the store step runs, and subsequent calls will recall.

**Storage errors break my app** — they won't. The middleware catches all storage errors so a Dakera outage never interrupts generation.

**Getting 401 from the server** — verify `DAKERA_API_KEY` matches the server's env. Keys look like `dk-...` when generated via the Dakera admin API, but any shared string works.

**Using with Next.js / edge runtime** — set `DAKERA_URL` and `DAKERA_API_KEY` as Next.js environment variables and import from `@dakera-ai/ai-sdk` in server components or API routes.

## License

MIT © Dakera
MIT © [Dakera](https://dakera.ai)
44 changes: 44 additions & 0 deletions examples/01-middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* Example 1 — Memory middleware (transparent)
*
* The middleware wraps a language model and automatically recalls relevant
* memories before each call, then stores the new exchange afterwards.
* Your generation code needs no changes.
*
* Run:
* OPENAI_API_KEY=sk-... DAKERA_API_KEY=dk-dev npx tsx 01-middleware.ts
*/

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({
// Scope memories to a user or agent ID
agentId: "example-user-1",
// Recall up to 5 relevant memories per call
recallK: 5,
// Assign importance 0.8 to stored exchanges
importance: 0.8,
}),
});

console.log("=== Session 1: storing a fact ===");
const { text: reply1 } = await generateText({
model,
prompt:
"My name is Alex and I'm building a distributed key-value store called Kestrel in Go.",
});
console.log("Assistant:", reply1);

console.log("\n=== Session 2: recalling across a simulated restart ===");
// In a real app this would be a separate process or later request.
// The memory persists in Dakera between sessions.
const { text: reply2 } = await generateText({
model,
prompt: "What do you know about my current project?",
});
console.log("Assistant:", reply2);
// → Should reference Kestrel and Alex without any explicit context in the prompt
44 changes: 44 additions & 0 deletions examples/02-tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* Example 2 — Memory tools (model-driven)
*
* `createDakeraTools` gives the model explicit `recallMemory` and `storeMemory`
* tools. The model decides when to look something up or when to persist a fact.
* Useful for agents that need deliberate memory management.
*
* Run:
* OPENAI_API_KEY=sk-... DAKERA_API_KEY=dk-dev npx tsx 02-tools.ts
*/

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,
importance: 0.8,
});

console.log("=== Step 1: storing a preference ===");
const { text: reply1, steps: steps1 } = await generateText({
model: openai("gpt-4o-mini"),
tools,
maxSteps: 3,
prompt:
"Please remember that I always want code examples in TypeScript, not JavaScript.",
});
console.log("Assistant:", reply1);
console.log("Tool calls:", steps1.flatMap((s) => s.toolCalls).map((c) => c.toolName));

console.log("\n=== Step 2: recalling the preference ===");
const { text: reply2, steps: steps2 } = await generateText({
model: openai("gpt-4o-mini"),
tools,
maxSteps: 3,
system:
"You are a helpful coding assistant. Always check memory for user preferences before answering.",
prompt: "Show me a quick example of how to read a file.",
});
console.log("Assistant:", reply2);
console.log("Tool calls:", steps2.flatMap((s) => s.toolCalls).map((c) => c.toolName));
// → Model should call recallMemory, find the TypeScript preference, and answer in TS
54 changes: 54 additions & 0 deletions examples/03-combined.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* Example 3 — Combined middleware + tools
*
* Use the middleware for automatic recall (so the model always has relevant
* context) and the tools when you want the model to persist facts explicitly.
* A single DakeraClient is shared to avoid duplicate connections.
*
* Run:
* OPENAI_API_KEY=sk-... DAKERA_API_KEY=dk-dev npx tsx 03-combined.ts
*/

import { generateText, wrapLanguageModel } from "ai";
import { openai } from "@ai-sdk/openai";
import { DakeraClient } from "@dakera-ai/dakera";
import { createDakeraMemoryMiddleware, createDakeraTools } from "@dakera-ai/ai-sdk";

// Share one DakeraClient between middleware and tools
const client = new DakeraClient({
baseUrl: process.env["DAKERA_URL"] ?? "http://localhost:3000",
apiKey: process.env["DAKERA_API_KEY"],
});
const agentId = "example-user-3";

// Middleware handles recall automatically; store: false lets the model
// decide what's worth persisting via the storeMemory tool
const model = wrapLanguageModel({
model: openai("gpt-4o-mini"),
middleware: createDakeraMemoryMiddleware({ client, agentId, store: false }),
});

const tools = createDakeraTools({ client, agentId });

console.log("=== Turn 1: model decides what to remember ===");
const { text: reply1, steps: steps1 } = await generateText({
model,
tools,
maxSteps: 4,
system:
"You are a helpful assistant. Use storeMemory only for facts the user will want in future sessions (not transient conversation details).",
prompt:
"I'm Jordan. My preferred stack is Next.js + Postgres and my timezone is UTC+2.",
});
console.log("Assistant:", reply1);
console.log("Stored:", steps1.flatMap((s) => s.toolCalls).filter((c) => c.toolName === "storeMemory").length, "memories");

console.log("\n=== Turn 2: middleware injects recalled memories automatically ===");
const { text: reply2 } = await generateText({
model,
tools,
maxSteps: 2,
prompt: "What stack should I use for my next project?",
});
console.log("Assistant:", reply2);
// → Should reference Next.js + Postgres from memory without any explicit context
Loading