From 3f4717e30886384d831e0746ecac7cb40840ec68 Mon Sep 17 00:00:00 2001 From: Arda Oz Date: Wed, 24 Jun 2026 14:11:01 +0300 Subject: [PATCH 1/4] docs: simplify package READMEs (lean intros, details in toggles) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restructure the sdk / ai-sdk / eve package READMEs for readability: - Each feature leads with a one-line "what it does" + a minimal-comment snippet. - Options, storage details, and security/behavior notes move into collapsible
toggles below the snippet, instead of heavy inline comments and stacked blockquotes. - The eve "keep each agent/ file self-contained" warning moves from the top of the README to a dedicated section at the bottom. Content is unchanged — same options, same guarantees — just reorganized. --- packages/ai-sdk/README.md | 206 +++++++++++++++-------------- packages/eve/README.md | 253 ++++++++++++++++++++---------------- packages/sdk/README.md | 264 +++++++++++++++++++------------------- 3 files changed, 386 insertions(+), 337 deletions(-) diff --git a/packages/ai-sdk/README.md b/packages/ai-sdk/README.md index 8a9c9aa..45eb98c 100644 --- a/packages/ai-sdk/README.md +++ b/packages/ai-sdk/README.md @@ -1,9 +1,9 @@ # @upstash/agentkit-ai-sdk [Vercel AI SDK](https://ai-sdk.dev) adapter for [Upstash AgentKit](https://www.npmjs.com/package/@upstash/agentkit-sdk). -Everything is a drop-in for `generateText` / `streamText`: durable chat history, ready-made memory + -Redis-Search tools, a rate limiter you call before the model, and self-contained cached tools. -`redis` defaults to `Redis.fromEnv()` everywhere, so you import only from this package. +Drop-ins for `generateText` / `streamText`: durable chat history, ready-made memory + Redis-Search +tools, a rate limiter, and self-contained cached tools. `redis` defaults to `Redis.fromEnv()` +everywhere, so you import only from this package. ```bash pnpm add @upstash/agentkit-ai-sdk @upstash/redis ai @@ -11,94 +11,98 @@ pnpm add @upstash/agentkit-ai-sdk @upstash/redis ai ## Chat history -`createChatHistory` returns a Redis-backed `ChatHistory` — the durable source of truth for -your conversations. Each chat is one JSON doc at `agentkit:chat::` (keyed per user, -so two users can't collide on a `sessionId`), indexed over `userId` + -`sessionId` (filters) and `userMessages` + `modelMessages` (`$smart` fuzzy text); the raw `messages` -array rides along **unindexed**. - -```ts -import { createChatHistory } from "@upstash/agentkit-ai-sdk"; - -const history = createChatHistory({ - redis, // optional: Upstash Redis client (defaults to Redis.fromEnv()) - prefix: "agentkit:chat", // optional: base key prefix (defaults to "agentkit:chat") - indexName: "agentkit_chat", // optional: Redis Search index name (defaults to the prefix) - ttlSeconds: 60 * 60 * 24 * 30, // optional: per-chat TTL in seconds (default: no expiry) -}); -``` - -Every method takes a single object; `userId` is **required, non-empty, and may not contain `:`**. It's -the tenant boundary, so **derive it from a verified server-side auth source** — the subject/user id -from your auth provider (Clerk, Auth.js/NextAuth, Supabase Auth, Auth0, …) — and **never from a -client-supplied header, query param, or body** (e.g. read it from the session in your route, not from -the request the browser controls). A chat can't be read or overwritten under a different `userId`. -`saveChat` overwrites the **whole** message array — `useChat` sends the full -conversation, so there's no transport trimming and no delta to merge. Persist from your route's -`onFinish`: +A Redis-backed `ChatHistory` — the durable source of truth for your conversations. Persist +the full transcript from your route's `onFinish`: ```ts // app/api/chat/route.ts import { createUIMessageStreamResponse, streamText, toUIMessageStream } from "ai"; +import { createChatHistory } from "@upstash/agentkit-ai-sdk"; +const history = createChatHistory(); const result = streamText({ model, messages: convertToModelMessages(messages) }); return createUIMessageStreamResponse({ stream: toUIMessageStream({ stream: result.stream, - originalMessages: messages, // so onFinish receives the full UIMessage[] (request + reply) - onFinish: ({ messages }) => history.saveChat({ userId, sessionId: chatId, messages, title }), // overwrite the whole array + originalMessages: messages, + onFinish: ({ messages }) => history.saveChat({ userId, sessionId: chatId, messages, title }), }), }); ``` -Seed `useChat` with the stored transcript when loading a chat, and use `listChats` / `searchChats` -for a sidebar: +Load a transcript and list/search chats for a sidebar: ```ts -// page loader (server) const chat = await history.getChat({ userId, sessionId: chatId }); // full transcript, or null -const chats = await history.listChats({ userId, limit: 50 }); // sidebar: summaries, no messages -const hits = await history.searchChats({ userId, query: "headphones", target: "both", limit: 20, minScore: 0 }); +const chats = await history.listChats({ userId, limit: 50 }); // summaries, no messages +const hits = await history.searchChats({ userId, query: "headphones", target: "both", limit: 20 }); +// then: useChat({ id: chatId, messages: chat?.messages ?? [] }) +``` + +
+Config & how it's stored -// client — hand the stored messages straight to useChat -// const { messages } = useChat({ id: chatId, messages: chat?.messages ?? [] }); +```ts +createChatHistory({ + redis, // optional: defaults to Redis.fromEnv() + prefix: "agentkit:chat", // optional: base key prefix + indexName: "agentkit_chat", // optional: Redis Search index name (defaults to the prefix) + ttlSeconds: 60 * 60 * 24 * 30, // optional: per-chat TTL (default: no expiry) +}); ``` -Other methods: `getChat({ userId, sessionId })`, `deleteChat({ userId, sessionId })`. +Each chat is one JSON doc at `agentkit:chat::` (keyed per user, so two users can't +collide on a `sessionId`), indexed over `userId` + `sessionId` (filters) and `userMessages` + +`modelMessages` (`$smart` fuzzy text); the raw `messages` array rides along unindexed. `saveChat` +overwrites the **whole** array (no delta merge) — `useChat` sends the full conversation. Other methods: +`getChat` / `deleteChat` (`{ userId, sessionId }`), `listChats` / `searchChats` (`{ userId }`). + +
+ +
+Security — userId is the tenant boundary + +Every method takes a single object; `userId` is **required, non-empty, and may not contain `:`**. +**Derive it from a verified server-side auth source** — the subject/user id from your auth provider +(Clerk, Auth.js/NextAuth, Supabase Auth, Auth0, …) — and **never from a client-supplied header, query +param, or body** (read it from the session in your route, not the request the browser controls). A chat +can't be read or overwritten under a different `userId`. + +
## Agent memory -`createMemoryTools` returns `recall_memory` and `save_memory` tools so the model can read and write -long-term memory itself. Memories are stored at `agentkit:memory::`. +`recall_memory` and `save_memory` tools so the model reads and writes its own long-term memory. ```ts import { createMemoryTools } from "@upstash/agentkit-ai-sdk"; import { generateText, stepCountIs } from "ai"; -const tools = createMemoryTools({ - userId, // required, non-empty: the user the memory belongs to (a string, or (input, options) => string) - redis, // optional: Upstash Redis client (defaults to Redis.fromEnv()) - topK: 5, // optional: max memories the recall tool returns - minScore: 0, // optional: BM25 relevance floor for recall - recallToolName: "recall_memory", // optional: override the recall tool's name - saveToolName: "save_memory", // optional: override the save tool's name -}); +const tools = createMemoryTools({ userId }); await generateText({ model, tools, stopWhen: stepCountIs(5), prompt: "What do you know about me?" }); ``` -> **`userId` is required, non-empty, and may not contain `:`** — it's the only tenant boundary for -> memory. **Derive it from a verified server-side auth source** (the subject/user id from Clerk, -> Auth.js/NextAuth, Supabase Auth, Auth0, …), passed as a string or a `(input, options) => string`; -> **never trust a client-supplied value.** An empty/separator-bearing value throws. +
+Options & the userId tenant boundary + +- **`userId`** _(required)_ — a string, or `(input, options) => string`. +- `redis` — defaults to `Redis.fromEnv()`. +- `topK` — max memories `recall` returns. +- `minScore` — BM25 relevance floor. +- `recallToolName` / `saveToolName` — override the tool names. + +`userId` is the only tenant boundary (required, non-empty, no `:`). **Derive it from a verified +server-side auth source** (Clerk, Auth.js/NextAuth, Supabase Auth, Auth0, …) — never a client-supplied +value. Memories are stored at `agentkit:memory::`. + +
## Search tools -`createSearchTools` returns `search` / `aggregate` / `count` tools over an Upstash Redis Search index. -The tool descriptions are generated from your `s.object(...)` schema, so the model learns the fields, -their types, and which filter operators (`$smart`, `$lt`, `$in`, `$and`, …) apply. The index is -created (and `waitIndexing`-ed) **reactively** on first use — no setup step. +`search` / `aggregate` / `count` over an Upstash Redis Search index; the model-facing descriptions are +generated from your schema. Use these over your own documents for RAG. ```ts import { s } from "@upstash/redis"; @@ -106,53 +110,53 @@ import { createSearchTools } from "@upstash/agentkit-ai-sdk"; import { generateText, stepCountIs } from "ai"; const schema = s.object({ name: s.string(), age: s.number(), city: s.string().noTokenize() }); +const tools = createSearchTools({ schema, indexName: "users" }); -const tools = createSearchTools({ - schema, // the Upstash Redis Search schema (built with `s` from @upstash/redis) - redis, // optional: Upstash Redis client (defaults to Redis.fromEnv()) - indexName: "users", // optional: index name (defaults to "agentkit:search") - prefix: "users:", // optional: key prefix for indexed JSON docs (defaults to ":") - defaultLimit: 10, // optional: default page size for the `search` tool (defaults to 10) -}); - -await generateText({ - model, - tools, - stopWhen: stepCountIs(5), - prompt: "How many users named Ada live in London?", -}); +await generateText({ model, tools, stopWhen: stepCountIs(5), prompt: "How many users named Ada live in London?" }); ``` +
+Options + +- **`schema`** _(required)_ — built with `s` from `@upstash/redis`. +- `redis` — defaults to `Redis.fromEnv()`. +- `indexName` — defaults to `"agentkit:search"`. +- `prefix` — key prefix for indexed JSON docs (defaults to `":"`). +- `defaultLimit` — default page size for `search` (10). + +The index is created (and `waitIndexing`-ed) reactively on first use — no setup step. + +
+ ## Rate limiting -`createRateLimit` returns a configured [Upstash Ratelimit](https://github.com/upstash/ratelimit-js) -`Ratelimit` with AgentKit defaults. There is no model wrapper — call `.limit(identifier)` yourself -before `generateText` and short-circuit when you're over the limit. Keys are -`agentkit:rateLimit:`. +A configured [Upstash Ratelimit](https://github.com/upstash/ratelimit-js) — call `.limit(identifier)` +before the model and short-circuit when over the limit. ```ts -import { openai } from "@ai-sdk/openai"; -import { generateText } from "ai"; import { createRateLimit, Ratelimit } from "@upstash/agentkit-ai-sdk"; -const ratelimit = createRateLimit({ - redis, // the Upstash Redis client backing the limiter - limiter: Ratelimit.slidingWindow(20, "1 m"), // required: the limiter algorithm (or fixedWindow, …) - prefix: "agentkit:rateLimit", // optional: base key prefix; keys are `:` -}); +const ratelimit = createRateLimit({ redis, limiter: Ratelimit.slidingWindow(20, "1 m") }); -const { success } = await ratelimit.limit(userId); // pass a per-user identifier to limit by user +const { success } = await ratelimit.limit(userId); if (!success) throw new Error("rate limited"); // or return a 429 from your route - -await generateText({ model: openai("gpt-5.4-mini"), prompt: "..." }); ``` +
+Options + +- **`limiter`** _(required)_ — e.g. `Ratelimit.slidingWindow(20, "1 m")` or `fixedWindow(...)`. +- `redis` — the Upstash Redis client backing the limiter. +- `prefix` — base key prefix; keys are `:` (default `agentkit:rateLimit`). + +There's no model wrapper — pass a per-user `identifier` to `.limit()` to throttle per user. + +
+ ## Tool cache -`cachedTools` memoizes a map of AI SDK tools' results in Redis. Pass tools built with the AI SDK's -`tool()` (so each keeps full input/output inference) — each is cached under **its map key as the tool -name** (so you don't pass a name yourself), scoped to `userId`. Cache keys are -`agentkit:toolCache:::`. +Memoize a map of AI SDK tools' results in Redis — each tool is cached under its map key, scoped to +`userId`. ```ts import { z } from "zod"; @@ -164,23 +168,33 @@ const tools = cachedTools( getWeather: tool({ description: "Get the weather for a city", inputSchema: z.object({ city: z.string() }), - execute: async ({ city }) => fetchWeather(city), // cached under "getWeather" + execute: async ({ city }) => fetchWeather(city), }), }, - { - userId, // required: scope every entry to this user (a string, or (input, options) => string) - redis, // optional: Upstash Redis client shared by every tool (defaults to Redis.fromEnv()) - ttlSeconds: 600, // optional: default per-result TTL in seconds for every tool - }, + { userId }, ); await generateText({ model, tools, prompt: "What's the weather in Paris?" }); ``` +
+Options + +Pass tools built with the AI SDK's `tool()` (so each keeps full input/output inference). Second arg: + +- **`userId`** _(required)_ — a string, or `(input, options) => string`; scopes every entry to this user. +- `redis` — defaults to `Redis.fromEnv()`. +- `ttlSeconds` — default per-result TTL for every tool. + +Cache keys are `agentkit:toolCache:::` — the `toolName` is the map key, +so you never pass a name yourself. + +
+ ## Testing -Tests run against a **real Upstash Redis** (only LLM calls are mocked). Set -`UPSTASH_REDIS_REST_URL` / `UPSTASH_REDIS_REST_TOKEN` (suites skip when absent). +Tests run against a **real Upstash Redis** (only LLM calls are mocked). Set `UPSTASH_REDIS_REST_URL` / +`UPSTASH_REDIS_REST_TOKEN` (suites skip when absent). ## License diff --git a/packages/eve/README.md b/packages/eve/README.md index 25bdeaa..531b2ad 100644 --- a/packages/eve/README.md +++ b/packages/eve/README.md @@ -1,10 +1,8 @@ # @upstash/agentkit-eve -Adapter that brings [Upstash AgentKit](https://upstash.com/) to **Eve, the Vercel agent framework**. -Eve is file-centric, so this package ships small pieces you drop into your `agent/` tree: long-term -memory tools, schema-driven Redis-Search tools, a rate limiter you drive from an eve `AuthFn`, a real -code-execution **sandbox backend** powered by [Upstash Box](https://github.com/upstash/box), and -cached tools. +[Upstash AgentKit](https://upstash.com/) for **Eve, the Vercel agent framework**. Drop-in pieces for +your `agent/` tree: memory tools, Redis-Search tools, a rate-limit gate, an +[Upstash Box](https://github.com/upstash/box) sandbox backend, and cached tools. ```bash pnpm add @upstash/agentkit-eve @upstash/redis @@ -12,29 +10,16 @@ pnpm add @upstash/agentkit-eve @upstash/redis pnpm add eve @ai-sdk/openai @upstash/box ``` -> **Keep each `agent/` file self-contained.** eve's runtime snapshots each tool/channel/hook file and -> resolves only **package** imports from it — it does **not** include shared `agent/`-source modules -> (e.g. a `agent/lib/redis.ts` or a shared tool-set module). So import only from packages inside -> `agent/`, and lean on the defaults: **`redis` defaults to `Redis.fromEnv()`** in every helper, so you -> almost never pass it. (Shared app code — e.g. a seeder you call from a page — lives in your project -> `lib/`, imported by the app, not by `agent/` files.) +## Memory tools -## Memory tools (`agent/tools/*.ts`) - -`defineMemoryRecallTool` and `defineMemorySaveTool` are ready eve tools — they call `defineTool` -internally, so you export them directly (no extra wrapping). One file each, one import. Pass a -`userId` (a string shared across users, or a function deriving it from the context); `redis` -defaults to env. Memories are stored at `agentkit:memory::`. +Long-term memory the model reads and writes itself — `recall_memory` and `save_memory`, one file each. ```ts // agent/tools/recall_memory.ts import { defineMemoryRecallTool } from "@upstash/agentkit-eve"; export default defineMemoryRecallTool({ - userId: (_, ctx) => ctx.session.auth.current?.principalId ?? ctx.session.id, // the user — a string, or (input, ctx) => string - topK: 5, // optional: max memories to return - minScore: 0, // optional: BM25 relevance floor for recall - // redis, // optional: Upstash Redis client — omit to default to Redis.fromEnv() + userId: (_, ctx) => ctx.session.auth.current?.principalId ?? ctx.session.id, }); ``` @@ -43,26 +28,30 @@ export default defineMemoryRecallTool({ import { defineMemorySaveTool } from "@upstash/agentkit-eve"; export default defineMemorySaveTool({ - userId: (_, ctx) => ctx.session.auth.current?.principalId ?? ctx.session.id, // the user — a string, or (input, ctx) => string + userId: (_, ctx) => ctx.session.auth.current?.principalId ?? ctx.session.id, }); ``` -> **`userId` is the tenant boundary** (required, non-empty, no `:`). Derive it from eve's **verified -> session auth** — `ctx.session.auth.current?.principalId` (the authenticated principal, gated by your -> channel's `auth` walk) — as above, not from anything the client supplies. Configure a real -> authenticator (`vercelOidc()`, an OIDC/JWT provider like Clerk, …) so `principalId` is trustworthy; -> the `?? ctx.session.id` fallback only applies when a request is unauthenticated. +
+Options & the userId tenant boundary + +- **`userId`** _(required)_ — a string, or `(input, ctx) => string`. +- `topK` — max memories `recall` returns. +- `minScore` — BM25 relevance floor. +- `redis` — defaults to `Redis.fromEnv()`. + +`userId` is the only tenant boundary (required, non-empty, no `:`). Derive it from eve's **verified +session auth** — `ctx.session.auth.current?.principalId` — not from anything the client supplies. +Configure a real authenticator (`vercelOidc()`, an OIDC/JWT provider like Clerk, …) so `principalId` +is trustworthy; the `?? ctx.session.id` fallback only applies to unauthenticated requests. Memories +are stored at `agentkit:memory::`. -## Search tools (`agent/tools/*.ts`) +
-`defineSearchTools` builds `search` / `aggregate` / `count` eve tools over an Upstash Redis Search -index — the eve counterpart to the ai-sdk adapter's `createSearchTools`. The tool descriptions are -generated from your `s.object(...)` schema (fields, types, applicable filter operators), and the index -is created **reactively** on first use. Each returned tool is already `defineTool`-branded. +## Search tools -eve is file-centric (filename = tool name) and each file must be self-contained, so call -`defineSearchTools` in each tool file and export the member you want (the same `name` ties them to one -index). Keep the `schema` + `name` identical across the files: +`search` / `aggregate` / `count` over an Upstash Redis Search index; the model-facing descriptions are +generated from your schema. ```ts // agent/tools/search_books.ts @@ -70,26 +59,34 @@ import { 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() }), // the schema (built with `s`) - indexName: "books", // optional: index name (defaults to "agentkit:search"); ties all three tools to one index - // prefix: "books:", // optional: key prefix for indexed JSON docs (defaults to ":") - // defaultLimit: 10, // optional: default page size for the `search` tool (defaults to 10) - // redis, // optional: omit to default to Redis.fromEnv() -}).search; // and: aggregate_books.ts → .aggregate, count_books.ts → .count (repeat schema + name) + schema: s.object({ title: s.string(), author: s.string().noTokenize(), year: s.number() }), + indexName: "books", +}).search; // aggregate_books.ts → .aggregate, count_books.ts → .count ``` -## Rate limiting (`agent/channels/eve.ts`) +
+Options & the one-file-per-tool rule + +- **`schema`** _(required)_ — built with `s` from `@upstash/redis`. +- `indexName` — defaults to `"agentkit:search"`; ties all three tools to one index. +- `prefix` — key prefix for indexed JSON docs (defaults to `":"`). +- `defaultLimit` — default page size for `search` (10). +- `redis` — defaults to `Redis.fromEnv()`. + +Each tool file must be self-contained, so call `defineSearchTools` in each one and export the member +you want — repeat the same `schema` + `indexName` across `search_books.ts` / `aggregate_books.ts` / +`count_books.ts`. The index is created reactively on first use, and each returned tool is already +`defineTool`-branded. + +
-Eve gates inbound HTTP routes with an ordered [auth walk](https://eve.dev/docs/guides/auth-and-route-protection): -each `AuthFn` accepts (returns a `SessionAuthContext`), skips (returns `null`/`undefined`), or rejects -(throws). `createRateLimitAuth` returns a ready `AuthFn` — drop it into the walk ahead of your real -authenticators. It's a _gate_: it throttles, then returns `null` to fall through (over the limit it -throws a 403). Backed by [Upstash Ratelimit](https://github.com/upstash/ratelimit-js); keys are -`agentkit:rateLimit:`. +## Rate limiting + +A ready `AuthFn` that throttles inbound requests — drop it into your channel's `auth` walk ahead of +your real authenticators. ```ts -// agent/channels/eve.ts — import only packages here; eve's per-channel bundle does NOT include -// other agent-source files, so don't import a shared `../lib/redis` into a channel. +// agent/channels/eve.ts import { createRateLimitAuth, Ratelimit } from "@upstash/agentkit-eve"; import { localDev, vercelOidc } from "eve/channels/auth"; import { eveChannel } from "eve/channels/eve"; @@ -97,87 +94,97 @@ import { eveChannel } from "eve/channels/eve"; export default eveChannel({ auth: [ createRateLimitAuth({ - // redis, // optional: omit to default to Redis.fromEnv() (don't import a shared client here) - limiter: Ratelimit.slidingWindow(20, "1 m"), // required: the limiter algorithm (or fixedWindow, …) - prefix: "agentkit:rateLimit", // optional: base key prefix; keys are `:` - identifier: (req) => req.headers.get("x-forwarded-for") ?? "anonymous", // required: who to limit — a string, or (request) => string - message: "Rate limit exceeded.", // optional: message in the 403 body when over the limit + limiter: Ratelimit.slidingWindow(20, "1 m"), + identifier: (req) => req.headers.get("x-forwarded-for") ?? "anonymous", }), - localDev(), // throttle first, then authenticate + localDev(), vercelOidc(), ], }); ``` -> **`identifier` is required** — there is no implicit `"global"` default. A single shared bucket means -> one abusive caller can exhaust the window for everyone, so for per-user limiting derive it per -> request (an authenticated user id, an API key, or `x-forwarded-for` for per-IP). +
+Options, the required identifier & POST-only counting + +- **`limiter`** _(required)_ — e.g. `Ratelimit.slidingWindow(20, "1 m")` or `fixedWindow(...)`. +- **`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, or `x-forwarded-for` for per-IP). +- `prefix` — base key prefix; keys are `:` (default `agentkit:rateLimit`). +- `message` — 403 body when over the limit. +- `redis` — defaults to `Redis.fromEnv()`. + +It's a _gate_: under the limit it returns `null` to fall through to the next `AuthFn`; over it throws a +403. **Only `POST` requests are counted** — eve runs each turn as a message `POST` plus a follow-up +`GET …/stream`, and the auth walk runs on both, so counting only the `POST`s means one turn costs one +token (a `slidingWindow(20, "1 m")` allows 20 turns/min, not 10). -> **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 — and the -> `auth` walk runs on both. `createRateLimitAuth` throttles only the `POST`s, so **one turn costs one -> token** of your limiter (a `Ratelimit.slidingWindow(20, "1 m")` allows 20 turns/min, not 10); the -> session-read `GET`s fall through unthrottled. +
-## Code-execution sandbox (`agent/sandbox.ts`) +## Code-execution sandbox -`upstash()` is a drop-in replacement for Eve's `vercel()` backend, powered by Upstash Box. Swap the -backend import and keep the rest of your [sandbox file](https://eve.dev/docs/sandbox) the same. +A drop-in replacement for Eve's `vercel()` backend, powered by Upstash Box — swap the import and keep +the rest of your [sandbox file](https://eve.dev/docs/sandbox) the same. ```ts // agent/sandbox.ts import { defineSandbox } from "eve/sandbox"; -import { upstash } from "@upstash/agentkit-eve/sandbox"; // was: import { vercel } from "eve/sandbox/vercel" +import { upstash } from "@upstash/agentkit-eve/sandbox"; // was: eve/sandbox/vercel export default defineSandbox({ - backend: upstash({ - // The Upstash Box `BoxConfig`, verbatim — whatever you'd pass to `Box.create({...})`: - runtime: "node", // the Box runtime (node | python | golang | ruby | rust) - size: "medium", // optional: Box resource size (small | medium | large) - // env: { ... }, initCommand, keepAlive, skills, mcpServers, timeout, … — all BoxConfig fields - // apiKey, // optional: Upstash Box API key (defaults to UPSTASH_BOX_API_KEY) - // (networkPolicy is NOT a config knob — egress is deny-all by default, opened per-session below) - }), + backend: upstash({ runtime: "node", size: "medium" }), revalidationKey: () => "repo-bootstrap-v1", async bootstrap({ use }) { - // Egress is denied by default — open it here because installing a package needs the network. - const sandbox = await use({ networkPolicy: "allow-all" }); + 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(); // sessions inherit the secure default (deny-all); pass a networkPolicy to open egress + await use(); // inherits the secure deny-all default }, }); ``` -> **Network egress is denied 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(...)` — 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 that code to see. +
+Config — it's Box's BoxConfig + +`upstash(config)` takes the `@upstash/box` `BoxConfig` verbatim — whatever you'd pass to +`Box.create({...})`: `runtime`, `size`, `apiKey` (defaults to `UPSTASH_BOX_API_KEY`), `keepAlive`, +`initCommand`, `env`, `skills`, `mcpServers`, `timeout`, … — plus an optional `redis` (defaults to +`Redis.fromEnv()`). `networkPolicy` is **not** a config knob (see below). + +`@upstash/box` is an optional peer dependency — only needed when you import +`@upstash/agentkit-eve/sandbox`. + +
-Set `UPSTASH_BOX_API_KEY` (or pass `apiKey`). `@upstash/box` is an optional peer dependency — only -needed when you import `@upstash/agentkit-eve/sandbox`. +
+Security — network egress is deny-all by default -> **Template registry uses Redis.** Eve builds your sandbox template (seed files + `bootstrap`) at -> build/startup via `prewarm`, but `create` runs per request in a different process — so the built -> snapshot's id is stored in a durable Redis registry (`redis` defaults to `Redis.fromEnv()`; override -> with `upstash({ redis })`). Without it, `create` couldn't find the prewarmed snapshot and would spin a -> fresh, empty box every time (Box has no cross-process snapshot lookup). Eve roots its file/`find`/`grep` -> tools at `/workspace`; a Box session lives at `/workspace/home`, and the backend bridges the two -> automatically. +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(...)` — 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. -> **One box per conversation.** Eve re-opens a sandbox session several times per turn; the backend -> reattaches to the same Box (via the box id it captured) 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 — so nothing leaks. Pass `upstash({ keepAlive: true })` only if you want an -> always-running box you manage yourself. +
-## Cached tools (`agent/tools/*.ts`) +
+Lifecycle — one box per conversation, Redis template registry -`defineCachedTool` is like Eve's `defineTool`, but its result is memoized — pass a `toolName` and a -`userId` (a string, or a function of the input + context). It calls `defineTool` internally, so you -export it directly. `redis` defaults to env. Keys are `agentkit:toolCache:::`. +**Reuse** — eve re-opens a session several times per turn; 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. + +
+ +## Cached tools + +Like Eve's `defineTool`, but the `execute` result is memoized in Redis. ```ts // agent/tools/get_weather.ts @@ -185,21 +192,43 @@ import { z } from "zod"; import { defineCachedTool } from "@upstash/agentkit-eve"; export default defineCachedTool({ - description: "Get the current weather for a city.", // (defineTool field) shown to the model - inputSchema: z.object({ city: z.string() }), // (defineTool field) zod schema for the input - execute: async ({ city }) => fetchWeather(city), // (defineTool field) memoized - toolName: "get_weather", // the toolName segment of the cache key - userId: (_, ctx) => ctx.session.auth.current?.principalId ?? ctx.session.id, // scope per user - ttlSeconds: 600, // optional: per-result TTL in seconds (default: no expiry) - // redis, // optional: omit to default to Redis.fromEnv() + 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 usual `defineTool` fields; `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 to `Redis.fromEnv()`. + +Keys are `agentkit:toolCache:::`. + +
+ +## 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 (e.g. a `agent/lib/redis.ts`). So inside `agent/`: + +- Import only from packages, never from other `agent/` files. +- Lean on the defaults — **`redis` defaults to `Redis.fromEnv()`** in every helper, so you almost never pass it. +- Repeat config (schema, names) per file rather than sharing a module. + +Shared app code (e.g. a seeder a page calls) lives in your project `lib/`, imported by the app — not by +`agent/` files. + ## Testing -Tests run against a **real Upstash Redis** (and a real Box when `UPSTASH_BOX_API_KEY` is set); only -LLM calls are mocked. Set `UPSTASH_REDIS_REST_URL` / `UPSTASH_REDIS_REST_TOKEN` (suites skip when -absent). +Tests run against a **real Upstash Redis** (and a real Box when `UPSTASH_BOX_API_KEY` is set); only LLM +calls are mocked. Set `UPSTASH_REDIS_REST_URL` / `UPSTASH_REDIS_REST_TOKEN` (suites skip when absent). ## License diff --git a/packages/sdk/README.md b/packages/sdk/README.md index 8cf4f90..1785a1d 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -2,204 +2,210 @@ Core, framework-agnostic primitives for building AI agents — entirely on [Upstash Redis](https://upstash.com/). No vector database required: the "semantic" features (memory -recall, search) are powered by [Upstash Redis Search](https://upstash.com/docs/redis/search/introduction) -and its `$smart` fuzzy operator (layered phrase / term / fuzzy / prefix matching, BM25-scored). +recall, search) run on [Upstash Redis Search](https://upstash.com/docs/redis/search/introduction) and +its `$smart` fuzzy operator (layered phrase / term / fuzzy / prefix matching, BM25-scored). ```bash pnpm add @upstash/agentkit-sdk @upstash/redis ``` -## Wiring up - -Every feature takes the `@upstash/redis` client and nothing else — the search-backed features create -and own their Redis Search index internally: +Every feature takes only the `@upstash/redis` client — the search-backed ones create and own their +Redis Search index internally: ```ts import { Redis } from "@upstash/redis"; import { AgentMemory, ChatHistory, ToolCache } from "@upstash/agentkit-sdk"; const redis = Redis.fromEnv(); - const history = new ChatHistory({ redis }); const memory = new AgentMemory({ redis }); -const tools = new ToolCache({ redis }); +const cache = new ToolCache({ redis }); ``` -The raw search index handle is exposed for advanced use (`describe`, `count`, `waitIndexing`, `drop`): +## Chat history -```ts -await memory.searchIndex.waitIndexing(); -const info = await memory.searchIndex.describe(); -``` +Durable conversation transcripts on Redis Search — the source of truth for a chat. `saveChat` replaces +the whole message array; `getChat` / `listChats` / `searchChats` read it back. -## Features +```ts +await history.saveChat({ userId: "user-123", sessionId: "session-abc", messages, title: "Trip planning" }); -### Chat history +const chat = await history.getChat({ userId: "user-123", sessionId: "session-abc" }); // or null +const chats = await history.listChats({ userId: "user-123", limit: 50 }); +const hits = await history.searchChats({ userId: "user-123", query: "wireless headphones", target: "both" }); +await history.deleteChat({ userId: "user-123", sessionId: "session-abc" }); +``` -Durable conversation transcripts backed by Upstash Redis Search — the source of truth for a chat. -`ChatHistory` is generic over the message type (the ai-sdk adapter specializes it to -`UIMessage`, eve to `EveMessage`). Each chat is one JSON doc at `agentkit:chat::` -(keyed per user, so two users can't collide on a `sessionId`), indexed -over `userId` + `sessionId` (exact-match filters) and `userMessages` + `modelMessages` (`$smart` -fuzzy text); the raw `messages` array rides along **unindexed**. So you can filter by -`userId` to list a user's chats and `$smart`-search within what the user or the model said. +
+Config, method options & how it's stored ```ts -const history = new ChatHistory({ - redis, // the Upstash Redis client (the search index is created/managed internally) - prefix: "agentkit:chat", // optional: base key prefix (defaults to "agentkit:chat") +new ChatHistory({ + redis, + prefix: "agentkit:chat", // optional: base key prefix indexName: "agentkit_chat", // optional: Redis Search index name (defaults to the prefix) - ttlSeconds: 60 * 60 * 24 * 30, // optional: per-chat TTL in seconds (default: no expiry) - extractText: (messages) => ({ userMessages: "...", modelMessages: "..." }), // optional: override how text is pulled into the two indexed fields (defaults to the UIMessage/EveMessage convention) + ttlSeconds: 60 * 60 * 24 * 30, // optional: per-chat TTL (default: no expiry) + extractText: (messages) => ({ userMessages: "...", modelMessages: "..." }), // optional: override text extraction }); +``` -// Every method takes a single object. `userId` is **required** and must be **unique per user** — it's -// the tenant boundary: chats are keyed per user, so one user can never read or overwrite another's. -// `saveChat` REPLACES the whole message array (overwrite, not append) — pass the complete transcript, -// typically server-side once a turn finishes (e.g. the AI SDK route's `onFinish`). -await history.saveChat({ - userId: "user-123", // required, non-empty, unique per user (the owner of the chat) - sessionId: "session-abc", // required, non-empty (the chat/session id) - messages, // the full transcript - title: "Trip planning", // optional: human-readable title -}); +- `searchChats` also takes `limit`, `minScore` (BM25 floor, default 0), and `target` (`"user"` | `"model"` | `"both"`). +- `saveChat` is an overwrite, not an append — pass the complete transcript (typically from the route's `onFinish`). -const chat = await history.getChat({ userId: "user-123", sessionId: "session-abc" }); // full transcript, or null -const chats = await history.listChats({ - userId: "user-123", // required - limit: 50, // optional: max chats to return (newest-updated first) -}); +`ChatHistory` is generic over the message type (ai-sdk → `UIMessage`, eve → `EveMessage`). +Each chat is one JSON doc at `agentkit:chat::` (keyed per user, so two users can't +collide on a `sessionId`), indexed over `userId` + `sessionId` (filters) and `userMessages` + +`modelMessages` (`$smart` text); the raw `messages` array rides along unindexed. -const hits = await history.searchChats({ - userId: "user-123", // required - query: "wireless headphones", - target: "both", // optional: which side to match — "user" | "model" | "both" (defaults to "both") - limit: 20, // optional: max hits to return - minScore: 0, // optional: BM25 relevance floor (defaults to 0) -}); +
-await history.deleteChat({ userId: "user-123", sessionId: "session-abc" }); // delete (also de-indexes it) -``` +
+Security — userId / sessionId are the tenant boundary -> **`userId` and `sessionId` are required, non-empty, and may not contain `:`** (the key separator) — -> they are the only tenant boundary, so an empty or separator-bearing value throws rather than -> silently mis-scoping a chat. **Derive `userId` from a verified server-side auth source** — the -> subject/user id from your auth provider (Clerk, Auth.js/NextAuth, Supabase Auth, Auth0, …) — and -> **never from a client-supplied header, query param, or request body**, or a caller can impersonate -> any user. A chat can't be read or overwritten under a different `userId`. +Both are **required, non-empty, and may not contain `:`** (the key separator) — they're the only tenant +boundary, so an empty or separator-bearing value throws rather than silently mis-scoping a chat. +**Derive `userId` from a verified server-side auth source** (Clerk, Auth.js/NextAuth, Supabase Auth, +Auth0, …) — **never from a client-supplied header, query param, or body**, or a caller can impersonate +any user. A chat can't be read or overwritten under a different `userId`. -### Agent memory +
-Long-term, fuzzily-recalled memory scoped per user. Stored at `agentkit:memory::`. +## Agent memory + +Long-term, fuzzily-recalled memory scoped per user. + +```ts +await memory.add({ text: "The user prefers TypeScript", userId: "user-123" }); + +const hits = await memory.recall({ query: "typescript preference", userId: "user-123", topK: 5 }); +await memory.forget("pref-lang", { userId: "user-123" }); +``` + +
+Config, method options & the tenant boundary ```ts -const memory = new AgentMemory({ - redis, // the Upstash Redis client (the search index is created/managed internally) - prefix: "agentkit:memory", // optional: base key prefix (defaults to "agentkit:memory") +new AgentMemory({ + redis, + prefix: "agentkit:memory", // optional: base key prefix indexName: "agentkit_memory", // optional: Redis Search index name (defaults to the prefix) minScore: 0, // optional: default BM25 relevance floor for recall }); +``` -await memory.add({ - text: "The user prefers TypeScript", - userId: "user-123", // required, non-empty: the user the memory belongs to - id: "pref-lang", // optional: stable id (generated when omitted) -}); - -const hits = await memory.recall({ - query: "typescript preference", // optional: omit (or "") to return everything for the user - userId: "user-123", // required, non-empty: the user to recall for - topK: 5, // optional: max memories to return (defaults to 5) - minScore: 0, // optional: BM25 relevance floor (defaults to the constructor's minScore) -}); +- `add` takes an optional `id` (a stable id; generated when omitted). +- `recall` takes `topK` (default 5), `minScore`, and an optional `query` — omit it (or pass `""`) to return everything for the user. +- Stored at `agentkit:memory::`. -await memory.forget("pref-lang", { userId: "user-123" }); // required, non-empty userId -``` +`userId` is **required, non-empty, and may not contain `:`** on every method — the only tenant boundary +for memory. **Derive it from a verified server-side auth source** (Clerk, Auth.js/NextAuth, Supabase +Auth, Auth0, …) — never a client-supplied value. -> **`userId` is required, non-empty, and may not contain `:`** on every method — it's the only tenant -> boundary for memory, so an empty or separator-bearing value throws rather than collapsing or -> colliding callers. **Derive it from a verified server-side auth source** (the subject/user id from -> Clerk, Auth.js/NextAuth, Supabase Auth, Auth0, …) — **never from a client-supplied value** — to keep -> each user's memories isolated. +
-### Search tools +## Search tools Framework-agnostic `search` / `aggregate` / `count` tool **definitions** over an Upstash Redis Search -index. `createSearchToolDefs` returns `{ description, inputSchema, execute }` triples — the ai-sdk -adapter wraps them with `tool()`, the eve adapter with `defineTool()`. The descriptions are generated -from your `s.object(...)` schema (fields, types, applicable filter operators). The index is created -**reactively** on first use (no setup step). +index — this is how you do **RAG** (over your own documents, no dedicated primitive). The descriptions +are generated from your schema. ```ts import { s } from "@upstash/redis"; import { createSearchToolDefs } from "@upstash/agentkit-sdk"; const defs = createSearchToolDefs({ - schema: s.object({ name: s.string(), age: s.number(), city: s.string().noTokenize() }), // the Upstash Redis Search schema (built with `s`) - redis, // the Upstash Redis client - indexName: "users", // optional: index name (defaults to "agentkit:search") - prefix: "users:", // optional: key prefix for indexed JSON docs (defaults to ":") - defaultLimit: 10, // optional: default page size for the `search` tool (defaults to 10) + schema: s.object({ name: s.string(), age: s.number(), city: s.string().noTokenize() }), + redis, + indexName: "users", }); - -// defs.search / defs.aggregate / defs.count — each `{ description, inputSchema, execute }`. +// defs.search / defs.aggregate / defs.count — each { description, inputSchema, execute } ``` -> **RAG?** There's no dedicated RAG primitive — use the **search tools** above over your own -> documents. Index your docs as JSON under one prefix with a schema you control, then give the agent -> the generated `search`/`aggregate`/`count` tools (typo-tolerant `$smart` retrieval, BM25-ranked). +
+Options & how the adapters use it -### Rate limiting +- **`schema`** _(required)_ — built with `s`. +- **`redis`** _(required)_ — the Upstash Redis client. +- `indexName` — defaults to `"agentkit:search"`. +- `prefix` — key prefix for indexed JSON docs (defaults to `":"`). +- `defaultLimit` — default page size for `search` (10). -`createRateLimit` returns a configured [Upstash Ratelimit](https://github.com/upstash/ratelimit-js) -`Ratelimit` with AgentKit defaults. There's no model wrapper — call `.limit(identifier)` yourself -before doing work (e.g. before calling a model) and short-circuit when over the limit. Keys are -`agentkit:rateLimit:`. +Each def is `{ description, inputSchema, execute }` — the ai-sdk adapter wraps them with `tool()`, the +eve adapter with `defineTool()`. The index is created reactively on first use (no setup step). For RAG, +index your docs as JSON under one prefix and hand the agent these tools (typo-tolerant `$smart` +retrieval, BM25-ranked). + +
+ +## Rate limiting + +A configured [Upstash Ratelimit](https://github.com/upstash/ratelimit-js) — call `.limit(identifier)` +before doing work and short-circuit when over the limit. ```ts import { createRateLimit, Ratelimit } from "@upstash/agentkit-sdk"; -const ratelimit = createRateLimit({ - redis, // the Upstash Redis client backing the limiter - limiter: Ratelimit.slidingWindow(20, "1 m"), // required: the limiter algorithm (or fixedWindow, …) - prefix: "agentkit:rateLimit", // optional: base key prefix; keys are `:` -}); +const ratelimit = createRateLimit({ redis, limiter: Ratelimit.slidingWindow(20, "1 m") }); -const { success } = await ratelimit.limit("user-123"); // pass a per-user identifier to limit by user +const { success } = await ratelimit.limit("user-123"); if (!success) throw new Error("rate limited"); ``` -### Tool cache +
+Options -Memoize deterministic tool results in Redis, keyed by user, then tool, then a stable hash of the -arguments. Keys are `agentkit:toolCache:::`. +- **`limiter`** _(required)_ — e.g. `Ratelimit.slidingWindow(20, "1 m")` or `fixedWindow(...)`. +- `redis` — the Upstash Redis client backing the limiter. +- `prefix` — base key prefix; keys are `:` (default `agentkit:rateLimit`). -```ts -const tools = new ToolCache({ - redis, // the Upstash Redis client - prefix: "agentkit:toolCache", // optional: base key prefix (defaults to "agentkit:toolCache") - ttlSeconds: 600, // optional: default TTL in seconds for cached results (default: no expiry) -}); +There's no model wrapper — pass a per-user `identifier` to `.limit()` to throttle per user. +
+ +## Tool cache + +Memoize deterministic tool results in Redis, keyed by user, then tool, then a hash of the arguments. + +```ts // `wrap` returns a memoized version of your execute, keyed by userId + "getWeather" + the args hash. -const getWeather = tools.wrap( - "user-123", // required, non-empty: the user the cache entry is scoped to - "getWeather", // required, non-empty: the tool name - (args) => fetchWeather(args), // the function to memoize - { ttlSeconds: 600 }, // optional: per-result TTL (overrides the constructor default) -); +const getWeather = tools.wrap("user-123", "getWeather", (args) => fetchWeather(args)); +``` + +
+Config, the low-level API & the key parts + +```ts +new ToolCache({ + redis, + prefix: "agentkit:toolCache", // optional: base key prefix + ttlSeconds: 600, // optional: default TTL (default: no expiry) +}); ``` -> **`userId` and `toolName` are both required, non-empty, and may not contain `:`** (`get`/`set`/ -> `invalidate`/`wrap` all throw otherwise). The entry is scoped to the user first, so one user's cached -> result is never served to another — provided `userId` comes from a verified auth source, not a -> client-supplied value. +`wrap(userId, toolName, execute, { ttlSeconds? })` is the high-level helper; `get` / `set` / +`invalidate` are the low-level API. Keys are `agentkit:toolCache:::`. + +`userId` and `toolName` are both **required, non-empty, and may not contain `:`** (all methods throw +otherwise). The entry is scoped to the user first, so one user's cached result is never served to +another — provided `userId` comes from a verified auth source, not a client-supplied value. + +
+ +## Advanced — the raw search index + +The search-backed features expose their Redis Search index handle (`describe`, `count`, `waitIndexing`, +`drop`): + +```ts +await memory.searchIndex.waitIndexing(); +const info = await memory.searchIndex.describe(); +``` ## Testing -The SDK is tested against a **real Upstash Redis** instance (no Redis mock) — only LLM calls are -mocked. Set `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN` (the suites skip themselves when -these are absent). Each suite uses a unique key prefix and cleans up its index/keys afterwards. +Tested against a **real Upstash Redis** instance (no Redis mock) — only LLM calls are mocked. Set +`UPSTASH_REDIS_REST_URL` / `UPSTASH_REDIS_REST_TOKEN` (suites skip when absent). Each suite uses a +unique key prefix and cleans up afterwards. ## License From f49dbfb5a60e88cafc695bb7bb166cd43aa239ac Mon Sep 17 00:00:00 2001 From: Arda Oz Date: Wed, 24 Jun 2026 14:21:33 +0300 Subject: [PATCH 2/4] fix: add license files --- LICENSE | 21 +++++++++++++++++++++ packages/ai-sdk/LICENSE | 21 +++++++++++++++++++++ packages/eve/LICENSE | 21 +++++++++++++++++++++ packages/sdk/LICENSE | 21 +++++++++++++++++++++ 4 files changed, 84 insertions(+) create mode 100644 LICENSE create mode 100644 packages/ai-sdk/LICENSE create mode 100644 packages/eve/LICENSE create mode 100644 packages/sdk/LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e748b2f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2026 Upstash, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/packages/ai-sdk/LICENSE b/packages/ai-sdk/LICENSE new file mode 100644 index 0000000..e748b2f --- /dev/null +++ b/packages/ai-sdk/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2026 Upstash, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/packages/eve/LICENSE b/packages/eve/LICENSE new file mode 100644 index 0000000..e748b2f --- /dev/null +++ b/packages/eve/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2026 Upstash, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/packages/sdk/LICENSE b/packages/sdk/LICENSE new file mode 100644 index 0000000..e748b2f --- /dev/null +++ b/packages/sdk/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2026 Upstash, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file From dd02a05a6b225e4e9e01260e013655fe58fd8336 Mon Sep 17 00:00:00 2001 From: Arda Oz Date: Wed, 24 Jun 2026 14:44:03 +0300 Subject: [PATCH 3/4] docs: add package metadata, clarify chat-history ids, de-AI the prose MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - package.json (root + 3 packages): add repository, homepage, and bugs links; homepages point to the docs pages (ai-sdk, eve) and the GitHub repo (root, core sdk). Refresh stale descriptions (drop telemetry / model cache / sandbox from core, etc.). - Chat history (README + docs): show where userId and chatId come from — userId from the auth session, chatId from the useChat id the client posts — in a full POST handler instead of bare variables. - eve install: only the AgentKit packages (@upstash/agentkit-eve, @upstash/redis, and @upstash/box for the sandbox); don't tell users to install eve or an AI-SDK provider, which an eve project already has. - Rate limiting: split the POST-only-counting explanation into its own toggle. - Prose cleanup per the "signs of AI writing" guide: cut puffery (drop-in/ready-made/self-contained/"production"), reduce em-dash density in section leads and toggle titles. --- package.json | 10 +++++++- packages/ai-sdk/README.md | 50 +++++++++++++++++++++--------------- packages/ai-sdk/package.json | 11 +++++++- packages/eve/README.md | 39 ++++++++++++++++++---------- packages/eve/package.json | 11 +++++++- packages/sdk/README.md | 20 +++++++-------- packages/sdk/package.json | 11 +++++++- 7 files changed, 103 insertions(+), 49 deletions(-) diff --git a/package.json b/package.json index 913d876..33f2d65 100644 --- a/package.json +++ b/package.json @@ -2,8 +2,16 @@ "name": "redis-agentkit", "version": "0.0.0", "private": true, - "description": "Redis AgentKit — agent memory, model & tool caching, rate limiting, search tools, sandbox, and RAG primitives for building AI agents on Upstash Redis.", + "description": "AgentKit primitives for building AI agents on Upstash Redis: chat history, memory, search tools (RAG), tool caching, rate limiting, and an Eve sandbox backend.", "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/upstash/agentkit.git" + }, + "homepage": "https://github.com/upstash/agentkit", + "bugs": { + "url": "https://github.com/upstash/agentkit/issues" + }, "type": "module", "scripts": { "build": "pnpm -r --filter \"./packages/*\" build", diff --git a/packages/ai-sdk/README.md b/packages/ai-sdk/README.md index 45eb98c..864952b 100644 --- a/packages/ai-sdk/README.md +++ b/packages/ai-sdk/README.md @@ -1,9 +1,9 @@ # @upstash/agentkit-ai-sdk [Vercel AI SDK](https://ai-sdk.dev) adapter for [Upstash AgentKit](https://www.npmjs.com/package/@upstash/agentkit-sdk). -Drop-ins for `generateText` / `streamText`: durable chat history, ready-made memory + Redis-Search -tools, a rate limiter, and self-contained cached tools. `redis` defaults to `Redis.fromEnv()` -everywhere, so you import only from this package. +It adds chat history, agent memory, Redis-Search tools, rate limiting, and tool caching to +`generateText` / `streamText`. `redis` defaults to `Redis.fromEnv()`, so you import only from this +package. ```bash pnpm add @upstash/agentkit-ai-sdk @upstash/redis ai @@ -11,33 +11,41 @@ pnpm add @upstash/agentkit-ai-sdk @upstash/redis ai ## Chat history -A Redis-backed `ChatHistory` — the durable source of truth for your conversations. Persist -the full transcript from your route's `onFinish`: +A Redis-backed `ChatHistory`, the durable source of truth for your conversations. `userId` +comes from your auth session; `chatId` is the `useChat` id that the client posts. Save the full +transcript from your route's `onFinish`: ```ts // app/api/chat/route.ts -import { createUIMessageStreamResponse, streamText, toUIMessageStream } from "ai"; +import { convertToModelMessages, createUIMessageStreamResponse, streamText, toUIMessageStream } from "ai"; import { createChatHistory } from "@upstash/agentkit-ai-sdk"; const history = createChatHistory(); -const result = streamText({ model, messages: convertToModelMessages(messages) }); - -return createUIMessageStreamResponse({ - stream: toUIMessageStream({ - stream: result.stream, - originalMessages: messages, - onFinish: ({ messages }) => history.saveChat({ userId, sessionId: chatId, messages, title }), - }), -}); + +export async function POST(req: Request) { + const userId = await getSessionUserId(req); // your auth session — never trust a client-sent id + const { id: chatId, messages } = await req.json(); // useChat posts its chat id + the full transcript + + const result = streamText({ model, messages: convertToModelMessages(messages) }); + + return createUIMessageStreamResponse({ + stream: toUIMessageStream({ + stream: result.stream, + originalMessages: messages, + onFinish: ({ messages }) => + history.saveChat({ userId, sessionId: chatId, messages, title: "New chat" }), + }), + }); +} ``` -Load a transcript and list/search chats for a sidebar: +To load a chat, take `chatId` from the page route and `userId` from the session, then seed `useChat`: ```ts const chat = await history.getChat({ userId, sessionId: chatId }); // full transcript, or null const chats = await history.listChats({ userId, limit: 50 }); // summaries, no messages const hits = await history.searchChats({ userId, query: "headphones", target: "both", limit: 20 }); -// then: useChat({ id: chatId, messages: chat?.messages ?? [] }) +// client: useChat({ id: chatId, messages: chat?.messages ?? [] }) ```
@@ -61,7 +69,7 @@ overwrites the **whole** array (no delta merge) — `useChat` sends the full con
-Security — userId is the tenant boundary +Security: userId is the tenant boundary Every method takes a single object; `userId` is **required, non-empty, and may not contain `:`**. **Derive it from a verified server-side auth source** — the subject/user id from your auth provider @@ -130,7 +138,7 @@ The index is created (and `waitIndexing`-ed) reactively on first use — no setu ## Rate limiting -A configured [Upstash Ratelimit](https://github.com/upstash/ratelimit-js) — call `.limit(identifier)` +A configured [Upstash Ratelimit](https://github.com/upstash/ratelimit-js). Call `.limit(identifier)` before the model and short-circuit when over the limit. ```ts @@ -149,13 +157,13 @@ if (!success) throw new Error("rate limited"); // or return a 429 from your rout - `redis` — the Upstash Redis client backing the limiter. - `prefix` — base key prefix; keys are `:` (default `agentkit:rateLimit`). -There's no model wrapper — pass a per-user `identifier` to `.limit()` to throttle per user. +There is no model wrapper; pass a per-user `identifier` to `.limit()` to throttle per user.
## Tool cache -Memoize a map of AI SDK tools' results in Redis — each tool is cached under its map key, scoped to +Memoize a map of AI SDK tools' results in Redis. Each tool is cached under its map key, scoped to `userId`. ```ts diff --git a/packages/ai-sdk/package.json b/packages/ai-sdk/package.json index c81933b..673e622 100644 --- a/packages/ai-sdk/package.json +++ b/packages/ai-sdk/package.json @@ -1,8 +1,17 @@ { "name": "@upstash/agentkit-ai-sdk", "version": "0.1.0", - "description": "Vercel AI SDK adapter for Upstash AgentKit: model response cache, rate limiting, cached tools, and memory + Redis-Search tools.", + "description": "Upstash AgentKit adapter for the Vercel AI SDK: chat history, memory tools, Redis-Search tools, rate limiting, and cached tools.", "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/upstash/agentkit.git", + "directory": "packages/ai-sdk" + }, + "homepage": "https://upstash.com/docs/redis/integrations/agentkit/ai-sdk", + "bugs": { + "url": "https://github.com/upstash/agentkit/issues" + }, "type": "module", "main": "./dist/index.js", "module": "./dist/index.js", diff --git a/packages/eve/README.md b/packages/eve/README.md index 531b2ad..48a4d16 100644 --- a/packages/eve/README.md +++ b/packages/eve/README.md @@ -1,18 +1,20 @@ # @upstash/agentkit-eve -[Upstash AgentKit](https://upstash.com/) for **Eve, the Vercel agent framework**. Drop-in pieces for +[Upstash AgentKit](https://upstash.com/) for **Eve, the Vercel agent framework**. You drop these into your `agent/` tree: memory tools, Redis-Search tools, a rate-limit gate, an [Upstash Box](https://github.com/upstash/box) sandbox backend, and cached tools. ```bash pnpm add @upstash/agentkit-eve @upstash/redis -# in your app (Eve + the OpenAI provider, plus Box only if you use /sandbox): -pnpm add eve @ai-sdk/openai @upstash/box +# only if you use the sandbox backend: +pnpm add @upstash/box ``` +Add these to your existing eve project; `eve` and your AI-SDK provider are already installed there. + ## Memory tools -Long-term memory the model reads and writes itself — `recall_memory` and `save_memory`, one file each. +Long-term memory the model reads and writes itself: `recall_memory` and `save_memory`, one file each. ```ts // agent/tools/recall_memory.ts @@ -82,7 +84,7 @@ you want — repeat the same `schema` + `indexName` across `search_books.ts` / ` ## Rate limiting -A ready `AuthFn` that throttles inbound requests — drop it into your channel's `auth` walk ahead of +A ready `AuthFn` that throttles inbound requests. Drop it into your channel's `auth` walk ahead of your real authenticators. ```ts @@ -104,7 +106,7 @@ export default eveChannel({ ```
-Options, the required identifier & POST-only counting +Options and the required identifier - **`limiter`** _(required)_ — e.g. `Ratelimit.slidingWindow(20, "1 m")` or `fixedWindow(...)`. - **`identifier`** _(required)_ — a string, or `(request) => string`. There's no implicit `"global"`: @@ -114,16 +116,25 @@ export default eveChannel({ - `message` — 403 body when over the limit. - `redis` — defaults to `Redis.fromEnv()`. -It's a _gate_: under the limit it returns `null` to fall through to the next `AuthFn`; over it throws a -403. **Only `POST` requests are counted** — eve runs each turn as a message `POST` plus a follow-up -`GET …/stream`, and the auth walk runs on both, so counting only the `POST`s means one turn costs one -token (a `slidingWindow(20, "1 m")` allows 20 turns/min, not 10). +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 `POST`s, so one turn costs one +token: a `Ratelimit.slidingWindow(20, "1 m")` allows 20 turns per minute, not 10. The session-read +`GET`s pass through unthrottled.
## Code-execution sandbox -A drop-in replacement for Eve's `vercel()` backend, powered by Upstash Box — swap the import and keep +A drop-in replacement for Eve's `vercel()` backend, powered by Upstash Box. Swap the import and keep the rest of your [sandbox file](https://eve.dev/docs/sandbox) the same. ```ts @@ -145,7 +156,7 @@ export default defineSandbox({ ```
-Config — it's Box's BoxConfig +Config: Box's BoxConfig `upstash(config)` takes the `@upstash/box` `BoxConfig` verbatim — whatever you'd pass to `Box.create({...})`: `runtime`, `size`, `apiKey` (defaults to `UPSTASH_BOX_API_KEY`), `keepAlive`, @@ -158,7 +169,7 @@ export default defineSandbox({
-Security — network egress is deny-all by default +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(...)` @@ -168,7 +179,7 @@ readable by code running in the box; don't pass secrets you wouldn't want it to
-Lifecycle — one box per conversation, Redis template registry +Lifecycle: one box per conversation **Reuse** — eve re-opens a session several times per turn; the backend reattaches to the same Box instead of creating a new one each time. Boxes default to Box's pause-based idle lifecycle diff --git a/packages/eve/package.json b/packages/eve/package.json index 11b6e85..a039438 100644 --- a/packages/eve/package.json +++ b/packages/eve/package.json @@ -1,8 +1,17 @@ { "name": "@upstash/agentkit-eve", "version": "0.1.0", - "description": "Vercel Eve framework adapter for Upstash AgentKit: cached tools, long-term memory tools, and an Upstash Box code-execution sandbox backend.", + "description": "Upstash AgentKit adapter for the Vercel Eve agent framework: memory tools, Redis-Search tools, a rate-limit gate, an Upstash Box sandbox backend, and cached tools.", "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/upstash/agentkit.git", + "directory": "packages/eve" + }, + "homepage": "https://upstash.com/docs/redis/integrations/agentkit/eve", + "bugs": { + "url": "https://github.com/upstash/agentkit/issues" + }, "type": "module", "main": "./dist/index.js", "module": "./dist/index.js", diff --git a/packages/sdk/README.md b/packages/sdk/README.md index 1785a1d..4c53c0c 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -1,6 +1,6 @@ # @upstash/agentkit-sdk -Core, framework-agnostic primitives for building AI agents — entirely on +Core, framework-agnostic primitives for building AI agents on [Upstash Redis](https://upstash.com/). No vector database required: the "semantic" features (memory recall, search) run on [Upstash Redis Search](https://upstash.com/docs/redis/search/introduction) and its `$smart` fuzzy operator (layered phrase / term / fuzzy / prefix matching, BM25-scored). @@ -9,7 +9,7 @@ its `$smart` fuzzy operator (layered phrase / term / fuzzy / prefix matching, BM pnpm add @upstash/agentkit-sdk @upstash/redis ``` -Every feature takes only the `@upstash/redis` client — the search-backed ones create and own their +Every feature takes only the `@upstash/redis` client. The search-backed ones create and own their Redis Search index internally: ```ts @@ -24,7 +24,7 @@ const cache = new ToolCache({ redis }); ## Chat history -Durable conversation transcripts on Redis Search — the source of truth for a chat. `saveChat` replaces +Durable conversation transcripts on Redis Search, the source of truth for a chat. `saveChat` replaces the whole message array; `getChat` / `listChats` / `searchChats` read it back. ```ts @@ -60,7 +60,7 @@ collide on a `sessionId`), indexed over `userId` + `sessionId` (filters) and `us
-Security — userId / sessionId are the tenant boundary +Security: userId / sessionId are the tenant boundary Both are **required, non-empty, and may not contain `:`** (the key separator) — they're the only tenant boundary, so an empty or separator-bearing value throws rather than silently mis-scoping a chat. @@ -106,8 +106,8 @@ Auth, Auth0, …) — never a client-supplied value. ## Search tools Framework-agnostic `search` / `aggregate` / `count` tool **definitions** over an Upstash Redis Search -index — this is how you do **RAG** (over your own documents, no dedicated primitive). The descriptions -are generated from your schema. +index. This is how you do **RAG**: index your own documents, then hand the agent these tools (there's +no dedicated RAG primitive). The descriptions are generated from your schema. ```ts import { s } from "@upstash/redis"; @@ -130,7 +130,7 @@ const defs = createSearchToolDefs({ - `prefix` — key prefix for indexed JSON docs (defaults to `":"`). - `defaultLimit` — default page size for `search` (10). -Each def is `{ description, inputSchema, execute }` — the ai-sdk adapter wraps them with `tool()`, the +Each def is `{ description, inputSchema, execute }`; the ai-sdk adapter wraps them with `tool()`, the eve adapter with `defineTool()`. The index is created reactively on first use (no setup step). For RAG, index your docs as JSON under one prefix and hand the agent these tools (typo-tolerant `$smart` retrieval, BM25-ranked). @@ -139,7 +139,7 @@ retrieval, BM25-ranked). ## Rate limiting -A configured [Upstash Ratelimit](https://github.com/upstash/ratelimit-js) — call `.limit(identifier)` +A configured [Upstash Ratelimit](https://github.com/upstash/ratelimit-js). Call `.limit(identifier)` before doing work and short-circuit when over the limit. ```ts @@ -158,7 +158,7 @@ if (!success) throw new Error("rate limited"); - `redis` — the Upstash Redis client backing the limiter. - `prefix` — base key prefix; keys are `:` (default `agentkit:rateLimit`). -There's no model wrapper — pass a per-user `identifier` to `.limit()` to throttle per user. +There is no model wrapper; pass a per-user `identifier` to `.limit()` to throttle per user.
@@ -203,7 +203,7 @@ const info = await memory.searchIndex.describe(); ## Testing -Tested against a **real Upstash Redis** instance (no Redis mock) — only LLM calls are mocked. Set +Tested against a **real Upstash Redis** instance (no Redis mock); only LLM calls are mocked. Set `UPSTASH_REDIS_REST_URL` / `UPSTASH_REDIS_REST_TOKEN` (suites skip when absent). Each suite uses a unique key prefix and cleans up afterwards. diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 8bbc3f5..821707c 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,8 +1,17 @@ { "name": "@upstash/agentkit-sdk", "version": "0.1.0", - "description": "Core primitives for building AI agents on Upstash Redis: memory, chat history, semantic & tool caching, telemetry, sandbox, and RAG.", + "description": "Core, framework-agnostic primitives for AI agents on Upstash Redis: chat history, long-term memory, schema-driven search tools (RAG), tool caching, and rate limiting.", "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/upstash/agentkit.git", + "directory": "packages/sdk" + }, + "homepage": "https://github.com/upstash/agentkit/tree/main/packages/sdk", + "bugs": { + "url": "https://github.com/upstash/agentkit/issues" + }, "type": "module", "main": "./dist/index.js", "module": "./dist/index.js", From 64769e82198ad17b9bf9ede4da08d4b7c6dcc6c0 Mon Sep 17 00:00:00 2001 From: Arda Oz Date: Wed, 24 Jun 2026 14:57:17 +0300 Subject: [PATCH 4/4] chore: point ai-sdk/eve package homepages to redis/sdks/agentkit docs --- packages/ai-sdk/package.json | 2 +- packages/eve/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ai-sdk/package.json b/packages/ai-sdk/package.json index 673e622..19dd7c1 100644 --- a/packages/ai-sdk/package.json +++ b/packages/ai-sdk/package.json @@ -8,7 +8,7 @@ "url": "git+https://github.com/upstash/agentkit.git", "directory": "packages/ai-sdk" }, - "homepage": "https://upstash.com/docs/redis/integrations/agentkit/ai-sdk", + "homepage": "https://upstash.com/docs/redis/sdks/agentkit/ai-sdk", "bugs": { "url": "https://github.com/upstash/agentkit/issues" }, diff --git a/packages/eve/package.json b/packages/eve/package.json index a039438..c385398 100644 --- a/packages/eve/package.json +++ b/packages/eve/package.json @@ -8,7 +8,7 @@ "url": "git+https://github.com/upstash/agentkit.git", "directory": "packages/eve" }, - "homepage": "https://upstash.com/docs/redis/integrations/agentkit/eve", + "homepage": "https://upstash.com/docs/redis/sdks/agentkit/eve", "bugs": { "url": "https://github.com/upstash/agentkit/issues" },