Memory, Chat History, RAG, Rate Limiting & Sandboxes for the Vercel Eve Agent Framework
Add long-term memory, searchable chat history, RAG, rate limiting, tool caching, and sandboxes to Vercel's Eve agent framework with Upstash Redis — no separate vector database.
Upstash AgentKit builds AI agents on Upstash Redis: memory,
conversation history, caching, and RAG, with no separate vector database. The semantic features run on
Upstash Redis Search and its $smart fuzzy operator.
Two packages bring AgentKit to Eve, the Vercel agent framework. They work together in one agent:
| Package | What it is |
|---|---|
@upstash/agentkit-eve-extension | An Eve extension: one mount file adds memory, searchable chat history, and RAG. |
@upstash/agentkit-eve | Per-file building blocks, plus the two things an extension cannot contribute: rate limiting and a sandbox backend. |
Mount the extension for the bundled setup. Add the package when you need a rate-limit gate, an Upstash Box sandbox, or control over an individual tool file.
Start from an eve project (0.25.2 or later). Scaffold one, which installs eve and an AI-SDK provider
for you:
npx eve@latest init my-agent# or, to start with a Next.js app:npx eve@latest init my-agent --channel-web-nextjsAgentKit reads UPSTASH_REDIS_REST_URL / UPSTASH_REDIS_REST_TOKEN from the environment by default.
Pass your own @upstash/redis client as redis to any helper to override.
The Eve extension: memory, chat history, and RAG
Everything in this part comes from @upstash/agentkit-eve-extension, configured in one mount file.
How to mount the Upstash extension in Vercel Eve
npm install @upstash/agentkit-eve-extensionOne file under agent/extensions/ mounts everything. Every config field is optional, and the smallest
mount gives the model long-term memory:
// agent/extensions/agentkit.tsimport agentkit from "@upstash/agentkit-eve-extension";export default agentkit();The filename supplies the namespace, so contributions compose as agentkit__recall_memory,
agentkit__save_memory, and so on. The extension also merges a short instructions fragment into your
system prompt telling the model when to save and recall.
The three feature groups below are independent of each other. Memory holds facts the model decided to keep. Chat history is the transcript of what was said. Search is retrieval over documents you write into your own index. Each has its own Redis keyspace and its own search index.
How to add memory to Vercel Eve
Long-term memory the model reads and writes itself. A bare mount already has it; the memory field
only tunes recall:
// agent/extensions/agentkit.tsimport agentkit from "@upstash/agentkit-eve-extension";export default agentkit({ memory: { topK: 5, minScore: 1 },});Tools, options, and the userId tenant boundary
agentkit__recall_memory— searches the user's memories; called with noqueryit returns all of them.agentkit__save_memory— stores one durable fact about the user.memory.topK— max memories a recall returns.memory.minScore— relevance floor. Scores are unbounded BM25 values, not[0,1].
userId is the only tenant boundary. It defaults to Eve's verified session auth
(auth.current?.principalId, then auth.initiator?.principalId, then the session id), so
configure a real authenticator (vercelOidc(), an OIDC/JWT provider like Clerk, …) if you want the
principal to be trustworthy. You can also set it to a string, which puts every caller in one shared
scope, or derive it per call:
export default agentkit({ userId: (ctx) => ctx.session.auth.current?.principalId ?? ctx.session.id,});Memories are stored at agentkit:memory:<userId>:<id>.
How to add searchable chat history to Vercel Eve
chatHistory: true persists every user and assistant message to Redis as the session streams, and
gives the model two tools over that store. A user can then ask about something settled in a previous
conversation:
// agent/extensions/agentkit.tsimport agentkit from "@upstash/agentkit-eve-extension";export default agentkit({ chatHistory: true });agentkit__search_chat_historyruns a$smart(typo-tolerant) search over what was said and returns the matching chats as summaries:sessionId,title,updatedAt,messageCount,score. The current conversation is excluded, since it's already in context.agentkit__read_chat_historyreads one of those chats back bysessionId, newest messages last.
Both tools take userId from the session, so the model cannot widen a lookup past the current user's
own transcripts.
Your own code reads the same store with ChatHistory from @upstash/agentkit-sdk, which is how you'd
build a history sidebar or run evals over past sessions:
npm install @upstash/agentkit-sdk @upstash/redis// app/api/chats/route.ts — server-side only; `userId` comes from your session, never the clientimport { Redis } from "@upstash/redis";import { ChatHistory } from "@upstash/agentkit-sdk";// `ChatHistory` is generic over the message type, and the extension doesn't export the shape its// hook writes — declare it to get a typed `messages` array back.type StoredMessage = { role: "user" | "assistant"; content: string; createdAt: number };// Pass the same prefix / indexName / ttlSeconds you set on `chatHistory` in the mount file.const history = new ChatHistory<StoredMessage>({ redis: Redis.fromEnv() });// List: summaries only (no messages), newest-updated first.const chats = await history.listChats({ userId, limit: 50 });// Search: `$smart` fuzzy match, summaries plus a BM25 `score`. target: "user" | "model" | "both".const hits = await history.searchChats({ userId, query: "the retry schema", target: "both", limit: 20 });// Read: one full transcript, or null if this user has no chat with that sessionId.const chat = await history.getChat({ userId, sessionId: hits[0].sessionId });chat?.messages.forEach((m) => console.log(m.role, m.content));userId must be the same value the extension resolved for those sessions, and both ids are Redis key
parts, so : in a derived value is replaced with _. deleteChat({ userId, sessionId }) removes a
chat and its index entry.
Options and storage
Pass an object in place of true to tune storage:
chatHistory.prefix— base key prefix (defaultagentkit:chat).chatHistory.indexName— Redis Search index name (defaults to the identifier-safeprefix).chatHistory.ttlSeconds— per-chat TTL. Omit for no expiry.
Each session is one JSON document at agentkit:chat:<userId>:<sessionId>, holding the raw transcript
plus $smart-indexed user and model text. A search returns summaries, and a read is capped at 50
messages per call with a truncated flag, so neither can flood the context window.
Redis is the durable record here, since Eve's own workflow store is pruned after a run completes.
These tools look history up on demand. They don't resume a session: Eve does that through its own session cursor.
How to add RAG to Vercel Eve
Point the extension at an Upstash Redis Search index and the model gets
search, search_aggregate, and search_count tools over it. You build the schema with s from
@upstash/redis, so your mount file imports it. Add the package to your app:
npm install @upstash/redis// agent/extensions/agentkit.tsimport { s } from "@upstash/redis";import agentkit from "@upstash/agentkit-eve-extension";export default agentkit({ search: { schema: s.object({ title: s.string(), author: s.string().noTokenize(), year: s.number() }), indexName: "books", },});Options and how the tools are described to the model
search.schema(required) — built withsfrom@upstash/redis.search.indexName— defaults to"agentkit:search"; ties all three tools to one index.search.prefix— key prefix for indexed JSON docs (defaults to"<indexName>:").search.defaultLimit— default page size forsearch(10).
Tool descriptions are generated from your schema (field names, types, and the filter operators that
apply to each), so the model learns the index without any prompt text from you. You write the
documents yourself with redis.json.set under the prefix, and the index is created on first read.
Omit search and these three tools don't exist at all. Like the chat-history tools, they resolve at
session start, which is why they don't appear in a static tool listing.
Extension configuration reference
// agent/extensions/agentkit.tsimport { s } from "@upstash/redis";import agentkit from "@upstash/agentkit-eve-extension";export default agentkit({ // optional: string, or (ctx) => string. Defaults to the verified principal, then the session id. userId: (ctx) => ctx.session.auth.current?.principalId ?? ctx.session.id, // optional: an explicit client; defaults to Redis.fromEnv() // redis: new Redis({ url, token }), // optional: tune memory recall memory: { topK: 5, minScore: 1 }, // optional: omit and the search tools don't exist search: { schema: s.object({ title: s.string(), author: s.string().noTokenize(), year: s.number() }), indexName: "books", prefix: "books:", // optional defaultLimit: 10, // optional }, // optional: off by default. `true`, or an object to tune storage chatHistory: { ttlSeconds: 60 * 60 * 24 * 30 },});Dropping or overriding a contribution
Mount as a directory and override a slot by filename. This is how you drop a tool you don't want, for example to capture chat history without letting the model read it back:
agent/extensions/agentkit/ extension.ts # the mount: export default agentkit({ ... }) tools/read_chat_history.ts # your override for agentkit__read_chat_history// agent/extensions/agentkit/tools/read_chat_history.tsimport { disableTool } from "eve/tools";export default disableTool();You can also re-define the memory tools, say to gate saves behind approval, by importing them from
@upstash/agentkit-eve-extension/tools and spreading them into your own defineTool.
@upstash/agentkit-eve below offers memory and RAG as standalone tool files too. Use those when you
want to configure one tool at a time, and use the package alongside the extension for rate limiting
and sandboxes.
The Eve package: rate limiting, sandboxes, and tool files
Everything in this part comes from @upstash/agentkit-eve, written as individual agent/ files.
Rate limiting and the sandbox backend live here because an extension cannot contribute a channel or
a sandbox.
How to add rate limiting to Vercel Eve
npm install @upstash/agentkit-eve @upstash/rediscreateRateLimitAuth is a ready AuthFn that throttles inbound requests. Drop it into your channel's
auth walk ahead of your real authenticators.
// agent/channels/eve.tsimport { createRateLimitAuth, Ratelimit } from "@upstash/agentkit-eve";import { localDev, vercelOidc } from "eve/channels/auth";import { eveChannel } from "eve/channels/eve";export default eveChannel({ auth: [ createRateLimitAuth({ limiter: Ratelimit.slidingWindow(20, "1 m"), identifier: (req) => req.headers.get("x-forwarded-for") ?? "anonymous", }), localDev(), vercelOidc(), ],});Options and the required identifier
limiter(required) — e.g.Ratelimit.slidingWindow(20, "1 m")orfixedWindow(...).identifier(required) — a string, or(request) => string. There's no implicit"global": one shared bucket lets a single abusive caller exhaust the window for everyone, so derive it per request (an auth user id, an API key, orx-forwarded-forfor per-IP).prefix— base key prefix; keys are<prefix>:<identifier>(defaultagentkit:rateLimit).message— 403 body when over the limit.redis— defaults toRedis.fromEnv().
It's a gate: under the limit it returns null to fall through to the next AuthFn; over it throws
a 403.
Why only POST requests are counted
Eve runs each turn as two authenticated requests: the message POST (which invokes the model) and a
follow-up GET …/stream that opens the reply stream. The auth walk runs on both, so counting both
would charge every turn twice. createRateLimitAuth counts only the POSTs, so one turn costs one
token: a Ratelimit.slidingWindow(20, "1 m") allows 20 turns per minute, not 10. The session-read
GETs pass through unthrottled.
How to add a sandbox to Vercel Eve
A drop-in replacement for Eve's vercel() backend, powered by
Upstash Box. Swap the import and keep the rest of your
sandbox file the same.
npm install @upstash/box// agent/sandbox.tsimport { defineSandbox } from "eve/sandbox";import { upstash } from "@upstash/agentkit-eve/sandbox"; // was: eve/sandbox/vercelexport default defineSandbox({ backend: upstash({ runtime: "node", size: "medium" }), revalidationKey: () => "repo-bootstrap-v1", async bootstrap({ use }) { const sandbox = await use({ networkPolicy: "allow-all" }); // open egress to install packages await sandbox.run({ command: "apt-get install -y jq" }); }, async onSession({ use }) { await use(); // inherits the secure deny-all default },});Config: Box's BoxConfig
upstash(config) takes the @upstash/box BoxConfig verbatim, meaning whatever you'd pass to
Box.create({...}): runtime, size, apiKey (defaults to UPSTASH_BOX_API_KEY), keepAlive,
initCommand, env, skills, mcpServers, timeout, and so on. It also takes an optional
redis (defaults to Redis.fromEnv()). networkPolicy is not a config knob (see below).
@upstash/box is an optional peer dependency, needed only when you import
@upstash/agentkit-eve/sandbox.
Security: network egress is deny-all by default
The sandbox runs untrusted, model-generated code, so open egress would mean SSRF / data
exfiltration / reaching your own infrastructure from inside the box. Open it per-session, in
bootstrap's use(...) or the session use(...), and never as a config knob. Note that env passed
to upstash({ env }) is readable by code running in the box; don't pass secrets you wouldn't want
it to see.
Brokering credentials (injecting headers)
Box network policies are plain domain/CIDR allow-lists. Eve's per-domain firewall rules (transform
header injection, forwardURL) have no Box equivalent, so passing them in use({ networkPolicy })
throws rather than silently sending the request unauthenticated:
// ❌ throws — Box can't inject headers via a per-session policyexport default defineSandbox({ backend: upstash({ runtime: "node" }), async onSession({ use }) { await use({ networkPolicy: { allow: { "api.example.com": [{ transform: [{ headers: { authorization: "Bearer …" } }] }] }, }, }); },});Broker credentials with Box's attachHeaders instead (set at backend creation; a proxy on the box
injects them), and open the domain with a plain allow-list:
// ✅ headers injected at the firewall; the secret never enters the boxexport default defineSandbox({ backend: upstash({ runtime: "node", attachHeaders: { "api.example.com": { Authorization: "Bearer …" } }, }), async onSession({ use }) { await use({ networkPolicy: { allow: ["api.example.com"] } }); },});Lifecycle: one box per conversation
Reuse. Eve re-opens a session several times per turn, and the backend reattaches to the same Box
instead of creating a new one each time. Boxes default to Box's pause-based idle lifecycle
(keepAlive: false): auto-paused when idle, resumed on reattach, reaped by Box. Pass
keepAlive: true only for an always-running box you manage yourself.
Template registry. Eve builds your template (seed files + bootstrap) at build/startup, but
session creation runs per request in a different process, so the snapshot id is stored in a durable
Redis registry (redis, defaulting to Redis.fromEnv()). Eve roots its tools at /workspace while
a Box session lives at /workspace/home; the backend bridges the two automatically.
How to cache tools in Vercel Eve
Like Eve's defineTool, but the execute result is memoized in Redis.
// agent/tools/get_weather.tsimport { z } from "zod";import { defineCachedTool } from "@upstash/agentkit-eve";export default defineCachedTool({ description: "Get the current weather for a city.", inputSchema: z.object({ city: z.string() }), execute: async ({ city }) => fetchWeather(city), toolName: "get_weather", userId: (_, ctx) => ctx.session.auth.current?.principalId ?? ctx.session.id,});Options
description/inputSchema/execute— the usualdefineToolfields;execute's result is memoized.toolName(required) — the tool segment of the cache key.userId(required) — a string, or(input, ctx) => string; scopes the cache per user.ttlSeconds— per-result TTL (default: no expiry).redis— defaults toRedis.fromEnv().
Keys are agentkit:toolCache:<userId>:<toolName>:<hash>.
Memory and RAG as individual tool files
The same two features the extension mounts, written as standalone agent/tools/ files. Use these when
you want to configure each tool on its own.
Memory takes one file per tool:
// agent/tools/recall_memory.tsimport { defineMemoryRecallTool } from "@upstash/agentkit-eve";export default defineMemoryRecallTool({ userId: (_, ctx) => ctx.session.auth.current?.principalId ?? ctx.session.id,});// agent/tools/save_memory.tsimport { defineMemorySaveTool } from "@upstash/agentkit-eve";export default defineMemorySaveTool({ userId: (_, ctx) => ctx.session.auth.current?.principalId ?? ctx.session.id,});RAG is defineSearchTools, the counterpart to the
AI SDK adapter's createSearchTools:
// agent/tools/search_books.tsimport { s } from "@upstash/redis";import { defineSearchTools } from "@upstash/agentkit-eve";export default defineSearchTools({ schema: s.object({ title: s.string(), author: s.string().noTokenize(), year: s.number() }), indexName: "books",}).search; // aggregate_books.ts → .aggregate, count_books.ts → .countOptions and the one-file-per-tool rule
defineMemoryRecallTool / defineMemorySaveTool take a required userId (string or
(input, ctx) => string), plus topK, minScore, and redis. defineSearchTools takes a required
schema, plus indexName, prefix, defaultLimit, and redis.
Each tool file must be self-contained, so call defineSearchTools in each one and export the member
you want, repeating the same schema and indexName across search_books.ts, aggregate_books.ts,
and count_books.ts. The index is created on first use, and every returned tool is already
defineTool-branded.
This package has no chat history. It comes from the extension, or from
ChatHistory in @upstash/agentkit-sdk if
you're writing the code yourself.
Working with Eve's agent/ files
Eve's runtime snapshots each tool/channel/hook file and resolves only package imports from it. It
does not include shared agent/-source modules such as an agent/lib/redis.ts. So inside agent/:
- Import only from packages, never from other
agent/files. - Lean on the defaults.
redisfalls back toRedis.fromEnv()in every helper, so you almost never pass it. - Repeat config (schema, names) in each file instead of sharing a module.
Shared app code, like a seeder a page calls, belongs in your project lib/ and is imported by the app,
not by agent/ files. Extensions are exempt from all of this. The extension package ships as one
compiled unit, which is why its whole configuration fits in a single mount file.
How to run the Vercel Eve example apps
Two complete eve apps live in the AgentKit repo:
examples/eve-extension-demois a minimal agent whose whole configuration is one extension mount, with memory, chat history, and book search turned on.examples/eve-demouses the file-by-file package (memory, search, cached tools, a rate-limit gate, and an Upstash Box sandbox), with a chat UI that renders tool calls inline.