Vercel AI SDK
The AI SDK is the
TypeScript toolkit for building AI applications:
generateText, streamText, agents, and UI hooks
for React, Vue and Svelte. Korely adds a memory to it through the
korely-memory Node package, or through plain fetch
calls. You define small tool() wrappers around Korely's REST
endpoints and pass them to the model. The model can then search
memories, read
facts and save new memories, all inside
a normal generateText call.
What you get
Each tool wrapper calls one REST endpoint. The model receives JSON and
decides when to use memory. For reading, the examples give the model
search and get context, which returns the
facts that are true now plus the best-matching memories. For writing, they
give it a tool that adds a memory. The same pattern wraps any other
endpoint (GET /v1/memories/{id},
GET /v1/facts, GET /v1/profile,
DELETE /v1/memories/{id}) in a few lines.
Install
You need the AI SDK, the Korely Node package, and zod, which
the examples use for the tools' input schemas. The examples were run with
ai 7.0.159 (it needs Node 22 or later),
korely-memory 0.1.17 and zod 4.6.7, with a fake model and the Korely calls answered by a local test server, so no model provider was called. The calls to the Korely Cloud
that the ready-made tools make were also checked once against the real
service.
npm install ai korely-memory zod
You also need a Korely API key. Copy it from
API Keys in the dashboard at agent.korely.ai and export it.
The examples name the model as a string
('anthropic/claude-sonnet-4.5'). The AI SDK sends such a
string through the Vercel AI Gateway, so you also need an AI Gateway key.
To call a provider directly, install its package (for example
@ai-sdk/anthropic) and pass its model object. We did not call
a real model, so the model name is yours to check.
export KORELY_API_KEY="kor_live_..."export AI_GATEWAY_API_KEY="..." # for the 'provider/model' strings belowAuthentication
Every request to https://api.korely.ai/v1 carries the key in
the Authorization header:
const headers = { Authorization: 'Bearer ' + process.env.KORELY_API_KEY, 'Content-Type': 'application/json',};Complete example
A single-file agent with two memory tools. The model decides when to use
them. stopWhen: stepCountIs(5) lets the model chain up to five
steps (for example search, then answer) before it must produce the final
text.
import { generateText, stepCountIs, tool } from 'ai';import { z } from 'zod';
const KORELY_API = 'https://api.korely.ai/v1';const headers = { Authorization: 'Bearer ' + process.env.KORELY_API_KEY, 'Content-Type': 'application/json',};
const tools = { search_memory: tool({ description: 'Search what has been remembered so far.', inputSchema: z.object({ query: z.string().describe('Keyword-style query, 1 to 5 words'), limit: z.number().optional().default(5), }), execute: async ({ query, limit }) => { const res = await fetch(KORELY_API + '/memories/search', { method: 'POST', headers, body: JSON.stringify({ query, limit }), }); return res.json(); }, }), save_memory: tool({ description: 'Save something worth remembering.', inputSchema: z.object({ content: z.string() }), execute: async ({ content }) => { const res = await fetch(KORELY_API + '/memories', { method: 'POST', headers, body: JSON.stringify({ content }), }); return res.json(); }, }),};
const { text } = await generateText({ model: 'anthropic/claude-sonnet-4.5', tools, stopWhen: stepCountIs(5), prompt: 'What do my notes say about the Lisbon offsite? ' + 'Search memory before answering.',});
console.log(text);
When you run it, the model calls search_memory on its own. The
output below is an example, not yours:
$ npx tsx agent.ts
→ tool call search_memory(query="lisbon offsite", limit=5)
← JSON from /v1/memories/search:
{ "results": [
{ "id": "mem_...", "score": 0.94, "snippet": "Three days, June 24 to 26.
Day 1 product review, day 2 strategy workshop, day 3 free morning +
flights home. Hotel booked near Cais do Sodré.",
"user_id": "...", "agent_id": null, "metadata": {} },
{ "id": "mem_...", "score": 0.81, "snippet": "Flights confirmed for the
23rd evening. Bring the demo laptop, HDMI dongle, offsite agenda
printouts...", "user_id": "...", "agent_id": null, "metadata": {} }
] }
Agent › The Lisbon offsite runs June 24 to 26: product review on
day 1, a strategy workshop on day 2, and a free morning on day 3
before flights home. You fly in the evening of the 23rd, the hotel
is near Cais do Sodré, and your checklist flags the demo laptop,
an HDMI dongle and printed agendas. /v1/memories/search compares the meaning of your query with
the saved memories and returns the closest ones. No language model runs on
Korely's side when you read; your model does the reasoning. Reads are cheap
for us, so every plan allows at least 100 times more queries than writes
per month (see the pricing page).
Prefer get_context for recall. Search
returns matching snippets. GET /v1/context returns a
context block: the facts that
are true now ("Luca upgraded to Pro", "prefers async standups"), with
replaced facts left out, plus the best-matching memories. Give the model
a get_context tool when you want it to know the user, and
keep search for "find the exact memory that mentioned X".
Using the korely-memory Node SDK
If you prefer a typed client to raw fetch, the
korely-memory package exports a Korely class that
calls the same endpoints. It reads KORELY_API_KEY from the
environment when you pass no key. This snippet reuses tool and
z from the example above:
import { Korely } from 'korely-memory';
const korely = new Korely({ apiKey: process.env.KORELY_API_KEY });const AGENT_ID = 'ai-sdk-agent'; // whose memory this is: a write names a user_id or an agent_id
// Recall: the facts that are true now. Reach for this first.const recall = tool({ description: 'Recall settled facts and relevant memories about this user.', inputSchema: z.object({ query: z.string() }), execute: async ({ query }) => korely.getContext({ query, agent_id: AGENT_ID }),});
const search_memory = tool({ description: 'Find the exact memory that mentioned something.', inputSchema: z.object({ query: z.string() }), execute: async ({ query }) => korely.search(query, { agent_id: AGENT_ID, limit: 5 }),});
const save_memory = tool({ description: 'Save something worth remembering.', inputSchema: z.object({ content: z.string() }), execute: async ({ content }) => korely.add(content, { agent_id: AGENT_ID }),});Ready-made: korely-memory/ai-sdk
The korely-memory package also ships
korely-memory/ai-sdk.
withKorelyMemory(model, { userId }) reads the memory
before each call (it calls GET /v1/context for the latest user
message and adds the result as system messages) and stores the turn after
it. korelyTools({ userId }) gives the model
searchMemory and addMemory tools; they never
take the user from the model. Pass userId from your own login.
The hand-written tool() examples on this page are the
do-it-yourself path.
import { generateText } from 'ai';import { withKorelyMemory, korelyTools } from 'korely-memory/ai-sdk';
const { text } = await generateText({ model: withKorelyMemory('anthropic/claude-sonnet-4.5', { userId }), tools: korelyTools({ userId }), prompt: 'What plan am I on?',}); withKorelyMemory writes by default. After a
call that ends the turn, it stores the user message and the reply as one
memory (for example "user: What plan am I on?" followed by
"assistant: ..."). That counts as a write against your monthly quota, on
every turn. Pass remember: false if your app writes its own
memories or has no consent to store the conversation.
Streaming
With streamText the call returns before the model finishes,
and you read the text as it arrives. The tools are plain HTTP calls, so
there is nothing to close. This example reuses the workspace-scoped
tools defined above (the streaming loop itself was not run
against a model):
import { streamText, stepCountIs } from 'ai';
const result = streamText({ model: 'anthropic/claude-sonnet-4.5', tools, stopWhen: stepCountIs(5), prompt: 'Summarize what you know about my current apartment lease.',});
for await (const chunk of result.textStream) { process.stdout.write(chunk);} Serverless route handlers: define tools that carry no
end-user id at module scope, so they are not rebuilt on every request.
Create tools that carry a user_id
(korelyTools({ userId }),
withKorelyMemory, or the factory below) on every request,
because the user_id must come from that request's session.
Each execute is just a fetch call, so there is
no connection to open or close.
One memory per end user
The examples above read and write one memory, the agent's own, named by
agent_id (every write names an owner),
and a read without a user_id covers every
end user. When each person who uses
your product needs a private memory, label every read and write. The three
labels are user_id (the
end user; unlimited on every plan; you choose the string),
agent_id (your
application) and run_id
(one session).
Take user_id from your session on the server, so the model
cannot reach another person's memory whatever the conversation says:
import { generateText, stepCountIs, tool } from 'ai';import { z } from 'zod';
const KORELY_API = 'https://api.korely.ai/v1';const headers = { Authorization: 'Bearer ' + process.env.KORELY_API_KEY, 'Content-Type': 'application/json',};
// user_id is bound here, server-side. The model never sees or sets it.function korelyMemoryTools(userId: string) { return { search_memory: tool({ description: 'Search everything known about this user.', inputSchema: z.object({ query: z.string().describe('Keyword-style query, 1 to 5 words'), }), execute: async ({ query }) => { const res = await fetch(KORELY_API + '/memories/search', { method: 'POST', headers, body: JSON.stringify({ query, user_id: userId, limit: 5 }), }); return res.json(); }, }), save_memory: tool({ description: 'Save something worth remembering about this user.', inputSchema: z.object({ content: z.string(), }), execute: async ({ content }) => { const res = await fetch(KORELY_API + '/memories', { method: 'POST', headers, body: JSON.stringify({ content, user_id: userId, agent_id: 'support-bot', }), }); return res.json(); }, }), };}
// Per chat request:const { text } = await generateText({ model: 'anthropic/claude-sonnet-4.5', tools: korelyMemoryTools(session.userId), stopWhen: stepCountIs(5), messages,});
A read that carries a user_id returns only that user's data.
When a user asks you to wipe what the bot knows, one call erases every
memory for that user
(DELETE /v1/users/{userId}/memories, see the
API reference). The
chatbot that
remembers cookbook builds this pattern out end to end.
Troubleshooting
| Symptom | Fix |
|---|---|
401 from api.korely.ai |
Key missing or malformed. Check that KORELY_API_KEY
starts with kor_live_ and is exported in the process
environment where the agent runs.
|
| The tools load but the model never calls them | Add a line to the prompt or the system message ("search memory before answering"). The model chooses its tools, and smaller models need the hint more than larger ones. |
| 429 on writes |
Read the error's code. quota_exceeded:
you have used the month's writes for your plan (hobby:
250 writes / month, plus a 10% grace); upgrade on the
pricing page or deduplicate before saving.
agent_cap_exceeded: the plan's named agents are taken;
reuse an agent_id or delete one.
rate_limit_exceeded: slow down and retry after the
Retry-After header.
|
| One slow tool call stalls the whole turn |
Cap chains with stopWhen: stepCountIs(n) and keep
limit low on search calls; 5 results is usually
plenty for a chat turn.
|
For the full SDK method surface, see the SDK reference. For how facts, supersession and time work, see temporal facts.
Something not working? Email
[email protected] with your
ai and korely-memory versions and the error
output. We read every message.