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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .changeset/eve-032-upgrade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
"@upstash/agentkit-eve": minor
"@upstash/agentkit-eve-extension": minor
---

Upgrade to eve 0.32 (repo now builds and tests against eve 0.32.0 / AI SDK 7.0.58).

`@upstash/agentkit-eve`:

- The Upstash Box sandbox backend implements eve ≥0.32's `SandboxBackendHandle.stop()` (authored
`ctx.getSandbox().stop()`): pauses the box, keeps the session reattachable, and rejects on provider
errors per the contract (`shutdown()` stays best-effort).
- `defineCachedTool` does not cache streams: eve ≥0.31 lets tool executors be async generators
(streaming preliminary output snapshots), but a cache hit could never replay them —
`DefineCachedToolConfig` now rejects async-generator executors at the type level (its `execute`
must resolve to a value), and a runtime `TypeError` backstops untyped JS callers before the
generator object would be serialized into the cache.

`@upstash/agentkit-eve-extension`:

- The prebuilt `dist/extension` is now built with eve 0.32, so its compatibility manifest requires
eve 0.32's contribution formats — **consumers need eve ≥0.32** to mount this version of the
extension. (The eve ≥0.25.3 fix for extensions installed as physical `node_modules` directories
means the old pnpm-only caveat is gone.)
66 changes: 48 additions & 18 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,17 @@ and `eve-extension-demo` (a minimal eve scaffold that mounts the extension).
- Eve is file-centric, but the tool factories now **call `defineTool` internally** and return the
branded `ToolDefinition` — users export them directly (no outer `defineTool(...)` wrap). Because of
this, **`eve` is a required (non-optional) peer dep** of `packages/eve`.
- **`defineCachedTool` does not cache streams:** eve ≥0.31 lets executors be async generators
(preliminary output snapshots), but a cache hit could never replay them — so
`DefineCachedToolConfig` narrows the **input** `execute` to `Promise<TOutput> | NonStreaming<TOutput>`
and rejects generator executors at the type level. The `NonStreaming` (`[Symbol.asyncIterator]?: never`)
intersection is load-bearing: with a plain `Promise<TOutput> | TOutput` union, TS just infers
`TOutput` *as* the generator object and the rejection silently fails (guarded by a
`@ts-expect-error` test in `tools.test.ts`). A **runtime backstop** covers JS callers: a directly
returned `AsyncIterable` throws a `TypeError` before `ToolCache` would serialize the generator
object into Redis (a *promised* value is just a value — only direct returns are streams, matching
eve). Factory **returns** stay plain `ToolDefinition` — direct `execute` callers (tests) narrow the
awaited union themselves.
- Rate limiting in eve = a route-auth gate: `createRateLimitAuth(config)` goes first in
`eveChannel({ auth: [...] })`; it `.limit()`s, throws `ForbiddenError` (403) over the limit, else
returns `null` to fall through to the real authenticators (`localDev()`/`vercelOidc()`/…).
Expand All @@ -96,9 +107,12 @@ and `eve-extension-demo` (a minimal eve scaffold that mounts the extension).
it, and pnpm's strict layout rejects the phantom dep (npm hoists and hides it). The old "also
install `@upstash/agentkit-sdk`" workaround is **not** needed on the 0.25 format — the compiled
dist resolves sdk from the extension's own package.
**Known eve bug:** `eve dev` fails to load an extension installed as a **real directory** (npm/yarn
hoisted layouts) — the dev snapshot's module-map references `../../node_modules/...` relatively and
misses; pnpm's symlink installs work, and production `eve build` bundles fine either way.
(The old eve bug where `eve dev` failed to load an extension installed as a **real directory** —
npm/yarn hoisted layouts — was fixed upstream in eve 0.25.3; no workaround needed on ≥0.25.3.)
**Consumer eve version:** `eve extension build` stamps the manifest's `requires` with the building
eve's *current* contribution-format versions, and a consumer rejects any version not in its own
supported list — so a dist built with eve 0.32 (tool 11 / dynamicTool 12 / hook 9) needs consumers
on **eve ≥0.32**. The wildcard peer stays `"*"`; the manifest is the real compatibility tie.
- `extension/extension.ts` = `defineExtension({ config: zod })`; the default export is the mount factory.
Config knobs: `userId` (string or `(ctx: SessionContext) => string` — eve's public base of tool+hook
ctx, imported from `eve/tools`), `redis` (defaults `Redis.fromEnv()`), `memory{topK,minScore}`,
Expand Down Expand Up @@ -175,10 +189,13 @@ and `eve-extension-demo` (a minimal eve scaffold that mounts the extension).
`agentkit:memory:<userId>:<id>`, `agentkit:chat:<userId>:<sessionId>` (default prefixes shown).

## AI SDK version strategy — IMPORTANT
- **AI SDK v7 stable everywhere.** Every package + demo pins `ai` to exactly **`7.0.30`**. `eve` (0.24+)
declares `ai` as a **peer** (`^7.0.26`), so the apps/packages provide the single copy. Providers:
`@ai-sdk/openai` `^4.0.15`, `@ai-sdk/provider` `^4.0.3`, `@ai-sdk/react` `^4.0.33` (all stable).
(History: the repo was on `7.0.0-beta.178`, the exact version `eve@0.13.1` depended on.)
- **AI SDK v7 stable everywhere.** Every package + demo pins `ai` to exactly **`7.0.58`**. `eve` (0.32)
declares `ai` as a **peer** (`^7.0.58`), so the apps/packages provide the single copy. Providers:
`@ai-sdk/openai` `^4.0.37`, `@ai-sdk/provider` `^4.0.7`, `@ai-sdk/react` `^4.0.62` (all stable ranges;
bump them with `pnpm -r update "@ai-sdk/*"` when eve moves — a stale `@ai-sdk/react` range can pin a
second, older `ai` copy via its peer resolution, which is exactly the two-copy breakage to avoid).
(History: the repo was on `7.0.0-beta.178` for `eve@0.13.1`, then `7.0.30` for `eve@0.25.2` — the
exact pin moves in lockstep with eve's `ai` peer range.)
- **Why exact-pin and not a pnpm `override`:** because everyone lands on the same exact `ai`, pnpm
installs a single copy. Two copies of `ai` cause type/identity breakage. An override was tried and
removed as unnecessary — keep it that way unless a dep forces a different `ai@7`.
Expand Down Expand Up @@ -222,10 +239,21 @@ and `eve-extension-demo` (a minimal eve scaffold that mounts the extension).
`$count`, `$histogram`, `$percentiles`, `$cardinality`.

## Eve framework facts
- The repo is on **`eve@0.25.2`** (peer `>=0.24.0` in `packages/eve` — its API is unchanged across
0.24→0.25; wildcard peer in the extension). Subpath exports: `eve/tools`, `eve/hooks`,
`eve/extension`, `eve/context`, `eve/instructions`, `eve/sandbox`, `eve/sandbox/vercel`,
`eve/channels/*`, `eve/next`, …
- The repo is on **`eve@0.32.0`** (peer `>=0.24.0` in `packages/eve`; wildcard peer in the extension,
but the built dist needs consumers on ≥0.32 — see the eve-extension section). Subpath exports:
`eve/tools`, `eve/hooks`, `eve/extension`, `eve/context`, `eve/instructions`, `eve/sandbox`,
`eve/sandbox/vercel`, `eve/channels/*`, `eve/next`, `eve/react`, …
- **Breaking changes absorbed on the 0.25 → 0.32 jump:** (a) 0.31 replaced continuation-token session
APIs with fixed ID-addressed handles — frontend/client `send` is now **positional**
(`agent.send(message, options?)`, not `send({ message })`; eve-demo's `agent-chat.tsx` was updated);
(b) `SandboxBackendHandle` gained a required **`stop()`** (authored-runtime stop, errors must reject)
alongside `shutdown()`; (c) tool executors may return **`AsyncIterable<TOutput>`** (streaming output
snapshots, 0.31) — `ToolDefinition.execute`'s return type is now a union; `defineCachedTool` rejects
streaming executors at the type level (see the eve exports section);
(d) 0.30 changed `localDev()` to grant a deployment-based synthetic principal (runtime `principalId`
values differ in local dev; our sanitizing `resolveUserId` is unaffected); (e) eve 0.32's `ai` peer is
`^7.0.58` (drove the repo-wide exact-pin bump). Durable sessions now **complete after 30 days** by
default (0.28) — strengthens Redis `ChatHistory` as the long-term transcript store.
- **Extension packaging changed 0.24 → 0.25**: 0.24 shipped source the consumer recompiles; 0.25 ships
prebuilt `dist/extension` + `_manifest.json` (see the eve-extension section). 0.25 rejects
0.24-format packages at discovery.
Expand All @@ -247,9 +275,10 @@ and `eve-extension-demo` (a minimal eve scaffold that mounts the extension).
- The real `SandboxBackend` is **two-phase**: `{ name, create(input) → SandboxBackendHandle, prewarm(input)
→ { reused } }`. `SandboxSession` = the AI SDK `Experimental_SandboxSession` (`run`, `spawn`,
`readFile`→stream, `readBinaryFile`, `readTextFile`, `writeFile`/`writeBinaryFile`/`writeTextFile`) plus
`id`, `resolvePath`, `setNetworkPolicy`, `removePath`. In eve ≥0.24 the handle's lifecycle method is
**`shutdown()`** (fires only on server shutdown; must leave the session reattachable) — the old
per-open `dispose()` is gone.
`id`, `resolvePath`, `setNetworkPolicy`, `removePath`. The handle's lifecycle methods are
**`stop()`** (eve ≥0.32: authored code ends sandbox work early via `ctx.getSandbox().stop()`; must
keep the session reattachable and **reject** on provider errors) and **`shutdown()`** (server
shutdown; best-effort, failures collected/logged by eve) — the old per-open `dispose()` is gone.

## @upstash/box (sandbox backend)
- Optional peer dep of the eve package. `Box.create({ apiKey | UPSTASH_BOX_API_KEY, runtime, size, … })`;
Expand Down Expand Up @@ -336,10 +365,11 @@ pnpm -r --filter "./examples/*" build # build both demo apps
lookup. `prewarm` builds **no** box when there's nothing to bake (no seed files/bootstrap). **Session
reuse:** `create` reattaches to the box from `input.existingMetadata.boxId` (`Box.get`) — Eve re-opens a
session many times and hands our captured `boxId` back, so without this every open spun a fresh box (the
"3 boxes per turn" bug). `shutdown` (eve ≥0.24's replacement for the old per-open `dispose`) fires only
when the server stops: it `box.pause()`s (reattachable; failure tolerated — keep-alive boxes can't
pause), and `keepAlive` defaults to **false** (pause-based idle; `true` can't be paused and runs until
deleted). **Path bridge:** Eve roots its tools at `/workspace` but Box sessions live in `/workspace/home`,
"3 boxes per turn" bug). Lifecycle: `stop()` (eve ≥0.32, authored `ctx.getSandbox().stop()`)
`box.pause()`s and **propagates** failures (the contract says provider errors must reject — keep-alive
boxes can't pause and will reject); `shutdown` (server stop) is the same pause but failure-tolerated.
Both leave the box reattachable. `keepAlive` defaults to **false** (pause-based idle; `true` can't be
paused and runs until deleted). **Path bridge:** Eve roots its tools at `/workspace` but Box sessions live in `/workspace/home`,
so the backend remaps both `resolvePath` (file ops) and raw commands (`find /workspace …` →
`/workspace/home`, URL-safe via lookbehind) through the exported `toBoxPath`/`rewriteWorkspacePaths`.
- `gpt-5.4-mini` (demo model) may not exist → demos build fine but can 404 at runtime. Swap if needed.
Expand Down
6 changes: 3 additions & 3 deletions examples/ai-sdk-demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@
"start": "next start"
},
"dependencies": {
"@ai-sdk/openai": "^4.0.15",
"@ai-sdk/react": "^4.0.33",
"@ai-sdk/openai": "^4.0.37",
"@ai-sdk/react": "^4.0.62",
"@upstash/agentkit-ai-sdk": "workspace:*",
"@upstash/agentkit-sdk": "workspace:*",
"@upstash/redis": "^1.38.0",
"ai": "7.0.30",
"ai": "7.0.58",
"dotenv": "^16.4.5",
"next": "16.2.9",
"react": "19.2.6",
Expand Down
2 changes: 1 addition & 1 deletion examples/eve-demo/app/_components/agent-chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export function AgentChat({
function send(text: string) {
const value = text.trim();
if (!value || busy) return;
void agent.send({ message: value });
void agent.send(value);
setInput("");
}

Expand Down
22 changes: 11 additions & 11 deletions examples/eve-demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,7 @@
"typecheck": "tsgo --noEmit -p tsconfig.json"
},
"dependencies": {
"@ai-sdk/openai": "^4.0.15",
"@upstash/agentkit-eve": "workspace:*",
"@upstash/box": "^0.5.1",
"@upstash/redis": "^1.38.0",
"@vercel/connect": "0.2.2",
"ai": "7.0.30",
"eve": "^0.25.2",
"zod": "4.4.3",
"@ai-sdk/openai": "^4.0.37",
"@radix-ui/react-use-controllable-state": "1.2.2",
"@shikijs/core": "4.1.0",
"@shikijs/engine-javascript": "4.1.0",
Expand All @@ -31,9 +24,15 @@
"@streamdown/math": "1.0.2",
"@streamdown/mermaid": "1.0.2",
"@tailwindcss/postcss": "4.3.0",
"@upstash/agentkit-eve": "workspace:*",
"@upstash/box": "^0.5.1",
"@upstash/redis": "^1.38.0",
"@vercel/connect": "0.2.2",
"ai": "7.0.58",
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
"cmdk": "1.1.1",
"eve": "^0.32.0",
"lucide-react": "1.16.0",
"motion": "12.40.0",
"nanoid": "5.1.11",
Expand All @@ -45,13 +44,14 @@
"streamdown": "2.5.0",
"tailwind-merge": "3.6.0",
"tailwindcss": "4.3.0",
"use-stick-to-bottom": "1.1.4"
"use-stick-to-bottom": "1.1.4",
"zod": "4.4.3"
},
"devDependencies": {
"@types/node": "24.x",
"@typescript/native-preview": "7.0.0-dev.20260523.1",
"@types/react": "19.2.15",
"@types/react-dom": "19.2.3"
"@types/react-dom": "19.2.3",
"@typescript/native-preview": "7.0.0-dev.20260523.1"
},
"engines": {
"node": "24.x"
Expand Down
6 changes: 3 additions & 3 deletions examples/eve-extension-demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@
"typecheck": "tsc"
},
"dependencies": {
"@ai-sdk/openai": "^4.0.15",
"@ai-sdk/openai": "^4.0.37",
"@upstash/agentkit-eve-extension": "workspace:*",
"@upstash/redis": "^1.38.0",
"@vercel/connect": "0.2.2",
"ai": "7.0.30",
"eve": "^0.25.2",
"ai": "7.0.58",
"eve": "^0.32.0",
"zod": "4.4.3"
},
"devDependencies": {
Expand Down
8 changes: 4 additions & 4 deletions packages/ai-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,11 @@
"zod": "^3.23.8 || ^4"
},
"devDependencies": {
"@ai-sdk/openai": "^4.0.37",
"@ai-sdk/provider": "^4.0.7",
"@upstash/redis": "^1.38.0",
"dotenv": "^16.4.5",
"ai": "7.0.30",
"@ai-sdk/provider": "^4.0.3",
"@ai-sdk/openai": "^4.0.15"
"ai": "7.0.58",
"dotenv": "^16.4.5"
},
"peerDependencies": {
"ai": ">=7.0.0-beta"
Expand Down
2 changes: 1 addition & 1 deletion packages/eve-extension/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ no repeated schemas; upgrades come through the package manager.

`<ns>` is the mount file's basename — the examples below use `agentkit`.

Start from an eve project (eve ≥ 0.25.2), then:
Start from an eve project (eve ≥ 0.32 — the prebuilt extension's compatibility manifest requires 0.32's contribution formats), then:

```bash
pnpm add @upstash/agentkit-eve-extension
Expand Down
2 changes: 1 addition & 1 deletion packages/eve-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
},
"devDependencies": {
"@types/node": "24.x",
"eve": "^0.25.2",
"eve": "^0.32.0",
"typescript": "7.0.2"
},
"peerDependencies": {
Expand Down
4 changes: 2 additions & 2 deletions packages/eve/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@
"devDependencies": {
"@upstash/box": "^0.5.1",
"@upstash/redis": "^1.38.0",
"ai": "7.0.30",
"ai": "7.0.58",
"dotenv": "^16.4.5",
"eve": "^0.25.2"
"eve": "^0.32.0"
},
"peerDependencies": {
"@upstash/box": ">=0.5.0",
Expand Down
12 changes: 10 additions & 2 deletions packages/eve/src/memory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,19 @@ describe.skipIf(!hasRedisCreds)("memory tools (live Redis)", () => {
});

it("save then recall round-trips through AgentMemory", async () => {
const saved = await save.execute({ text: "The user prefers dark mode" }, CTX);
// eve ≥0.31 types `execute` as possibly returning an AsyncIterable of output snapshots;
// our executors always resolve, so narrow the awaited results back to their plain values.
const saved = (await save.execute({ text: "The user prefers dark mode" }, CTX)) as {
id: string;
saved: boolean;
};
expect(saved.saved).toBe(true);
await index.waitIndexing();

const hits = await recall.execute({ query: "ui theme preference" }, CTX);
const hits = (await recall.execute({ query: "ui theme preference" }, CTX)) as {
text: string;
score: number;
}[];
expect(hits.some((h) => h.text.includes("dark mode"))).toBe(true);
});
});
16 changes: 12 additions & 4 deletions packages/eve/src/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -510,15 +510,23 @@ export class UpstashSandboxBackend implements SandboxBackend<
},
});

// Eve calls `stop` when authored code ends sandbox work early (`ctx.getSandbox().stop()`,
// eve ≥0.32): stop the compute but keep the session reattachable from `captureState`'s `boxId`
// (`openBox` reattaches via `Box.get`). Pausing does exactly that. Per the contract, provider
// errors must reject — so no catch here; keep-alive boxes can't be paused and will reject.
const stop = async (): Promise<void> => {
await box.pause();
};

// Eve calls `shutdown` only when the server itself is stopping (SIGINT/SIGTERM/nitro close):
// nothing may be left running, but the box must stay reattachable from `captureState`'s `boxId`
// on the next start. Pausing does exactly that (`openBox` reattaches via `Box.get`). Keep-alive
// boxes can't be paused — tolerate the failure, matching Eve's own Vercel backend's try/catch.
// nothing may be left running, but the box must stay reattachable on the next start — same
// pause as `stop`, except failures are tolerated (keep-alive boxes can't pause; eve collects
// and logs shutdown failures rather than blocking teardown).
const shutdown = async (): Promise<void> => {
await box.pause().catch(() => {});
};

return { session, useSessionFn, captureState, shutdown };
return { session, useSessionFn, captureState, stop, shutdown };
}

async prewarm(
Expand Down
34 changes: 34 additions & 0 deletions packages/eve/src/tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,38 @@ describe.skipIf(!hasRedisCreds)("defineCachedTool (live Redis)", () => {
await t.execute({ id: "a" }, CTX);
expect(fn).toHaveBeenCalledTimes(1);
});

it("rejects a streaming (async generator) execute at the type level", () => {
// A cached tool cannot stream — a cache hit could never replay eve ≥0.31's preliminary
// output snapshots — so `DefineCachedToolConfig.execute` only accepts resolving executors.
defineCachedTool({
description: "stream",
inputSchema: z.object({ n: z.number() }),
toolName: "stream",
userId: ns,
redis,
// @ts-expect-error — async-generator executors are not cacheable
async *execute({ n }: { n: number }) {
yield n;
},
});
});

it("rejects a streaming execute at runtime (JS callers bypass the types)", async () => {
const t = defineCachedTool({
description: "stream",
inputSchema: z.object({ n: z.number() }),
toolName: "stream-runtime",
userId: ns,
redis,
execute: async function* ({ n }: { n: number }) {
yield n;
} as never, // cast past the type-level rejection, like an untyped JS caller
});

// The generator must be refused before ToolCache serializes the generator object into Redis.
await expect(Promise.resolve(t.execute({ n: 1 }, CTX))).rejects.toThrow(
/streaming \(async generator\) executors cannot be cached/,
);
});
});
Loading
Loading