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
5 changes: 5 additions & 0 deletions .changeset/eve-extension-eve-085.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@upstash/agentkit-eve-extension": minor
---

Rebuild on eve 0.85.0 and migrate to the eve 0.84+ extension API, fixing `eve build` on eve 0.84.0 and later (the previous dist was rejected with "uses an old epoch-based distribution manifest"). The `eve` peer is now `>=0.85.0`: the new manifest records the eve version it was built with, and older eve releases refuse it. The chat-history hook now captures user text on `delivery.consumed` and assistant replies from `content.completed` (reply-phase text parts); the optional `userId` function receives the session `ctx` typed as `Pick<SessionFrame, "session">`.
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1200,6 +1200,7 @@ implements TanStack AI's own backend contracts (see its section below) — keep
- **eve 0.83.0 (2026-10-23) is applied to the extension only; `packages/eve` stays on 0.82.0.** 0.83.0 dropped the 0.82.0 contract stamps (extension rebuild: tool 96→102, dynamicTool 88→92, hook 48→51; extension peer `>=0.83.0`) and also **broke `packages/eve/src/sandbox.ts`'s typing** (not migrated yet — needs a source change): `eve/sandbox/provider` no longer exports `SandboxProviderHandle`/`SandboxProviderResources`; `NetworkPolicy` lists are mutable `string[]` (ours are `readonly`); `SandboxSession` requires `writeFiles`; `SandboxPrepareContext` lost `resources`/`sourceRevision`; the connection no longer has `handle`/`sandbox`/`onSandboxDelete`. Until that migration lands, `packages/eve` + `examples/eve-demo` pin `eve ^0.82.0` (two eve versions in the workspace on purpose).
- **eve 0.83.2 + ai 7.0.153 (2026-10-25): extension + `examples/eve-extension-demo` move to eve `^0.83.2`; `packages/eve` + `examples/eve-demo` stay on `^0.82.0`.** The extension rebuilt on 0.83.2 stamps the same contracts as 0.83.0 (tool 102 / dynamicTool 92 / hook 51), so its `>=0.83.0` peer floor is still truthful. Published `@upstash/agentkit-eve@0.16.1` `./sandbox` fails at sandbox-template init on eve 0.83.x (plain `eve build` with `UpstashSandbox.environment()` -> "Cannot read properties of undefined (reading 'workspace')"; `--skip-sandbox-prewarm` hides it), so its `>=0.75.0` peer admits eve versions where `./sandbox` is broken until the source migration lands. TanStack AI 1.0.0 still not applied (see above).
- **eve 0.83.3 + ai 7.0.154 (2026-10-27): extension + `examples/eve-extension-demo` move to eve `^0.83.3`, ai is `7.0.154` everywhere (`@ai-sdk/react ^4.0.158`).** Same contracts as 0.83.0, peer floor unchanged. **eve 0.84.0 (2026-10-26) is NOT applied: it breaks `packages/eve-extension` and `packages/eve` sources** (needs a source migration). Published `@upstash/agentkit-eve-extension@0.16.1` fails `eve build` on eve 0.84.0 with "uses an old epoch-based distribution manifest … Rebuild the extension with this eve release". Extension typecheck on 0.84.0: `SessionContext` no longer exported from `eve/tools`; hook key `message.received` gone; tool defs no longer accept `events`. `packages/eve` on 0.84.0: `signal` removed from `MemoryDocumentReadInput`/`WriteInput`, `MemoryCompactionCompletedContext` and the `compaction.completed` recall hook gone, `MemoryToolsContext.turn` gone, plus the sandbox typings above. TanStack AI 1.0.0 still not applied (see above); ai 7.0.154 is clean (published `@upstash/agentkit-ai-sdk@0.15.1` typechecks and imports on it).
- **eve 0.85.0: the extension IS migrated (extension-only; `packages/eve` + `examples/eve-demo` stay on eve 0.82.0).** eve 0.84.0 replaced per-capability contract epochs with a `{formatVersion: 3, builtWithEve, contractMajor: 0}` manifest and a v27 session-event/dynamic API. Source changes in `packages/eve-extension`: `SessionContext` is gone from `eve/tools` (own `SessionRef = Pick<SessionFrame, "session">` in `extension/lib/session.ts`, used for the `userId` function and `resolveUserId`); dynamic tools use `defineDynamic(ctx => tool | null)` instead of `defineDynamic({events: {"session.started"}})`; the chat-history hook maps `message.received` -> `delivery.consumed` (text parts of `event.data.parts`) and `message.completed` -> `content.completed` filtered to `kind: "text"` + `phase: "reply"` (one stored assistant message per reply text part; narration is not stored). **The peer floor is `>=0.85.0`**: verified by packing the dist (built with 0.85.0) into consumers on 0.83.3 ("no valid compatibility manifest"), 0.84.0 and 0.84.1 ("was built with eve 0.85.0. Upgrade eve to 0.85.0 or later") and 0.85.0 (`eve build` clean). Any rebuild on a newer eve will raise the floor again to that eve.
- **ai 7.0.155 (2026-10-28) applied everywhere except `packages/eve` + `examples/eve-demo` (stay on 7.0.154; `@ai-sdk/react ^4.0.159`); with 7.0.155 `packages/eve` tsc fails at `src/sandbox.ts:340` (spawn return type vs eve 0.82.0 overloads) and eve-demo `next build` fails; eve 0.84.1 / 0.85.0 NOT applied.** Same state as 0.84.0: extension typecheck on 0.85.0 still fails (`SessionContext` missing from `eve/tools`, hook key `message.received` gone, `events` rejected on tool defs) and published `@upstash/agentkit-eve-extension@0.16.1` still fails `eve build` on eve 0.85.0 ("uses an old epoch-based distribution manifest"); needs a source migration. Published `@upstash/agentkit-ai-sdk@0.15.1` typechecks (`skipLibCheck: false`, with `@types/json-schema` installed, which `@ai-sdk/provider` omits) and imports on ai 7.0.155. TanStack AI 1.0.0 still not applied.
- **The 0.74.0 → 0.82.0 bump (2026-10-22) moves everything, with a source migration in `packages/eve`.** eve 0.75.0 renamed the sandbox provider handle hooks `onSessionStop`/`onSessionDelete` → `onSandboxStop`/`onSandboxDelete`, and eve 0.66.0 had removed `MutableNetworkSandboxSession` (network policy became provider-specific). `packages/eve/src/sandbox.ts` now declares its own `UpstashSandboxSession = SandboxSession & { setNetworkPolicy }` (exported; what `ctx.getSandbox(environment)` returns) and uses the new hook names, so the `eve` peer floor moved `>=0.65.0` → **`>=0.75.0`** (floor read off the 0.74.0 vs 0.75.0 tarballs: `onSessionStop` present vs absent in `shared/sandbox-provider.d.ts`; not verified by a full `tsc` against 0.75.0). `packages/eve` + `examples/eve-demo` are on eve ^0.82.0. The extension rebuild re-stamps **tool 78→96, dynamicTool 73→88, hook 39→48**; only 0.82.0 accepts them (0.75.0–0.81.1 do not), floor `>=0.74.0` → **`>=0.82.0`**. `upstashPersistence` gained an `activities` store (`ActivityStore`, one JSON doc per thread at `<prefix>:activities:<threadId>`, same TTL as messages) because `@tanstack/ai-persistence` ≥0.8.0's conformance suite requires it, so ai-persistence moves 0.7.2 → 0.10.1 and the peer floors become `@tanstack/ai` `>=0.65.0` (`ActivityRecord` type), `@tanstack/ai-persistence` `>=0.8.0` (`ActivityStore` type). ai 7.0.148, @tanstack/ai 0.71.0, -memory 0.2.16 and the demo packages are bumped.
- **The 0.72.1 → 0.74.0 bump (2026-10-08) is EXTENSION-ONLY again** (same reason: `packages/eve` still imports the removed
Expand Down
4 changes: 2 additions & 2 deletions examples/eve-extension-demo/evals/agentkit-smoke.eval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { includes } from "eve/evals/expect";
// has fired on the same turn.
//
// eve ≥0.59 eval API: `t.send` opens a fresh session and resolves to the settled turn, which
// carries the reply (`turn.message`) and its own assertion scope.
// carries the reply (`turn.reply`, was `turn.message`) and its own assertion scope.
export default defineEval({
async test(t) {
const turn = await t.send("Please remember my favorite color.");
Expand All @@ -18,6 +18,6 @@ export default defineEval({
// The dynamic search tools are resolved at session start; a schema eve cannot replay makes
// the resolver fail and the tools silently disappear, which `eve build` does not catch.
turn.calledTool("agentkit__search_count");
t.check(turn.message, includes("Saved:"));
t.check(turn.reply, includes("Saved:"));
},
});
4 changes: 2 additions & 2 deletions examples/eve-extension-demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
"@ai-sdk/openai": "^4.0.71",
"@upstash/agentkit-eve-extension": "workspace:*",
"@upstash/redis": "^1.38.4",
"@vercel/connect": "0.2.2",
"@vercel/connect": "2.5.6",
"ai": "7.0.155",
"eve": "^0.83.3",
"eve": "^0.85.0",
Comment thread
Copilot marked this conversation as resolved.
Resolved
"zod": "4.4.3"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion packages/eve-extension/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ unavailable, use https://eve.dev/docs/extensions as a fallback.
agent-shaped source tree into `dist/extension/`, emits type declarations and a
compatibility manifest, and fills the package `exports` map. Ship `dist/` only.
`eve` is a required peer so the consumer's eve is the one that runs, but NOT a
wildcard: keep the floor (`>=0.83.0`) in sync with what the built manifest's
wildcard: keep the floor (`>=0.85.0`) in sync with what the built manifest's
contracts require, so an incompatible eve fails at install instead of at
`eve build` (see issue #22). eve validates the real compatibility from the
generated manifest.
5 changes: 2 additions & 3 deletions 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.83.0 — the prebuilt extension is built with eve 0.83.0 and its compatibility manifest requires tool contract v102 (dynamicTool v92, hook v51), which eve 0.83.0 is the first release to support; the package declares this as its `eve` peer range), then:
Start from an eve project (eve ≥ 0.85.0 — the prebuilt extension is built with eve 0.85.0, and eve 0.84+ checks the version an extension was built with against its own: 0.84.x and older refuse a dist built with 0.85.0; the package declares this as its `eve` peer range), then:

```bash
pnpm add @upstash/agentkit-eve-extension
Expand Down Expand Up @@ -117,8 +117,7 @@ Don't want the model reading history at all? Keep the capture and
The search tools' descriptions and input schemas are generated from your `search.schema`
(field-by-field filter guidance for the model), and whether the chat-history tools apply at all
depends on `chatHistory` — neither is known until the mount config binds at runtime. So both sets are
contributed as [dynamic tools](https://eve.dev/docs/guides/dynamic-capabilities) resolved at
`session.started`: unconfigured, they resolve to nothing rather than erroring when the model calls
contributed as [dynamic tools](https://eve.dev/docs/guides/dynamic-capabilities) resolved by `defineDynamic` before each model call: unconfigured, they resolve to nothing rather than erroring when the model calls
them. Only the memory tools are static.

## Overriding or disabling contributions
Expand Down
6 changes: 3 additions & 3 deletions packages/eve-extension/extension/extension.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { defineExtension } from "eve/extension";
import type { SessionContext } from "eve/tools";
import type { SessionRef } from "./lib/session";
import { z } from "zod";
import type { Redis } from "@upstash/redis";
import type { AnySearchSchema } from "@upstash/agentkit-sdk";

/**
* The user all data is scoped under (the tenant boundary for memory and chat history). A string
* shares one scope across all callers (fine for a single-user agent); a function derives it per call
* from eve's `SessionContext` — the base both tool and hook `ctx` extend, so one function serves
* from eve's `SessionRef` (the `session` every tool and hook `ctx` carries), so one function serves
* every contribution (`(ctx) => ctx.session.auth.current?.principalId ?? …`). Defaults to the
* authenticated principal (`auth.current`, falling back to `initiator`), then the session id.
*/
const userId = z.union([
z.string().min(1),
z.custom<(ctx: SessionContext) => string>((value) => typeof value === "function"),
z.custom<(ctx: SessionRef) => string>((value) => typeof value === "function"),
]);

export default defineExtension({
Expand Down
17 changes: 13 additions & 4 deletions packages/eve-extension/extension/hooks/chat_history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,30 @@ import { appendChatMessage } from "../lib/runtime";
* `ChatHistory` (`agentkit:chat:<userId>:<sessionId>`) as the session streams. Redis is the
* long-term, searchable source of truth — eve's own workflow store is pruned after a run completes.
*
* User text arrives on `delivery.consumed` (the v27 successor of `message.received`); the
* assistant's answer is each text part of `content.completed` with `phase: "reply"` (narration
* between tool calls is not stored).
*
* A thrown hook fails the turn, so persistence errors are logged and swallowed: losing one
* transcript write should never take the conversation down.
*/
export default defineHook({
events: {
async "message.received"(event, ctx) {
async "delivery.consumed"(event, ctx) {
const text = event.data.parts
.flatMap((part) => (part.kind === "text" ? [part.text] : []))
.join("\n");
try {
await appendChatMessage(ctx, "user", event.data.message);
await appendChatMessage(ctx, "user", text);
} catch (error) {
console.warn("[agentkit] chat-history capture failed (user message):", error);
}
},
async "message.completed"(event, ctx) {
async "content.completed"(event, ctx) {
const { kind, phase, value } = event.data;
if (kind !== "text" || phase !== "reply" || typeof value !== "string") return;
try {
await appendChatMessage(ctx, "assistant", event.data.message ?? "");
await appendChatMessage(ctx, "assistant", value);
} catch (error) {
console.warn("[agentkit] chat-history capture failed (assistant message):", error);
}
Expand Down
6 changes: 3 additions & 3 deletions packages/eve-extension/extension/lib/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
type SearchToolDefs,
} from "@upstash/agentkit-sdk";
import { Redis } from "@upstash/redis";
import type { SessionContext } from "eve/tools";
import type { SessionRef } from "./session";
import extension from "../extension";
import { VERSION } from "./version";

Expand All @@ -22,7 +22,7 @@ export function sanitizeId(value: string): string {
}

/** Resolve the per-call user from config (string or function), defaulting to principal → session id. */
export function resolveUserId(ctx: SessionContext): string {
export function resolveUserId(ctx: SessionRef): string {
const { userId } = extension.config;
if (typeof userId === "string") return sanitizeId(userId);
if (typeof userId === "function") return sanitizeId(userId(ctx));
Expand Down Expand Up @@ -132,7 +132,7 @@ export function requireChatHistory(): ChatHistory<StoredChatMessage> {
* dispatched in order, so the read-modify-write doesn't race itself.
*/
export async function appendChatMessage(
ctx: SessionContext,
ctx: SessionRef,
role: StoredChatMessage["role"],
content: string,
): Promise<void> {
Expand Down
7 changes: 7 additions & 0 deletions packages/eve-extension/extension/lib/session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { SessionFrame } from "eve/tools";

/**
* The part of eve's session context the extension reads: tool `ctx` and hook `ctx` both carry it,
* so one `userId` function serves every contribution.
*/
export type SessionRef = Pick<SessionFrame, "session">;
94 changes: 45 additions & 49 deletions packages/eve-extension/extension/tools/read_chat_history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,55 +12,51 @@ const MAX_MESSAGES = 50;
* `userId` comes from the session, so `sessionId` alone can't address another user's chat: the key is
* `<prefix>:<userId>:<sessionId>` and core `ChatHistory` rejects a `:` in either id.
*/
export default defineDynamic({
events: {
"session.started": () => {
if (!chatHistory()) return null;
return defineTool({
description:
"Read one of this user's past conversations in full, by the `sessionId` returned from the " +
"chat-history search tool. Returns the transcript, newest messages last.",
inputSchema: z.object({
sessionId: z
.string()
.min(1)
.describe("The chat's `sessionId`, as returned by the chat-history search tool."),
limit: z
.number()
.int()
.positive()
.max(MAX_MESSAGES)
.optional()
.describe(
`Max messages to return, counting back from the end of the conversation. ` +
`Defaults to ${MAX_MESSAGES}.`,
),
}),
async execute({ sessionId, limit }, ctx) {
const chat = await requireChatHistory().getChat({
userId: resolveUserId(ctx),
sessionId,
});
if (!chat) {
return { found: false as const, sessionId };
}
const take = Math.min(limit ?? MAX_MESSAGES, MAX_MESSAGES);
const messages = chat.messages.slice(-take);
return {
found: true as const,
sessionId: chat.sessionId,
...(chat.title !== undefined ? { title: chat.title } : {}),
updatedAt: new Date(chat.updatedAt).toISOString(),
messageCount: chat.messageCount,
// Flagged so the model knows the transcript is partial rather than the whole chat.
truncated: chat.messages.length > messages.length,
messages: messages.map((message) => ({
role: message.role,
content: message.content,
})),
};
},
export default defineDynamic(() => {
if (!chatHistory()) return null;
return defineTool({
description:
"Read one of this user's past conversations in full, by the `sessionId` returned from the " +
"chat-history search tool. Returns the transcript, newest messages last.",
inputSchema: z.object({
sessionId: z
.string()
.min(1)
.describe("The chat's `sessionId`, as returned by the chat-history search tool."),
limit: z
.number()
.int()
.positive()
.max(MAX_MESSAGES)
.optional()
.describe(
`Max messages to return, counting back from the end of the conversation. ` +
`Defaults to ${MAX_MESSAGES}.`,
),
}),
async execute({ sessionId, limit }, ctx) {
const chat = await requireChatHistory().getChat({
userId: resolveUserId(ctx),
sessionId,
});
if (!chat) {
return { found: false as const, sessionId };
}
const take = Math.min(limit ?? MAX_MESSAGES, MAX_MESSAGES);
const messages = chat.messages.slice(-take);
return {
found: true as const,
sessionId: chat.sessionId,
...(chat.title !== undefined ? { title: chat.title } : {}),
updatedAt: new Date(chat.updatedAt).toISOString(),
messageCount: chat.messageCount,
// Flagged so the model knows the transcript is partial rather than the whole chat.
truncated: chat.messages.length > messages.length,
messages: messages.map((message) => ({
role: message.role,
content: message.content,
})),
};
},
},
});
});
24 changes: 10 additions & 14 deletions packages/eve-extension/extension/tools/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,14 @@ import { searchDefs, trySearchDefs } from "../lib/runtime";
* mounts. Plain JSON Schema data needs no durable factory — eve rehydrates it into its own
* validator — and `z.toJSONSchema` keeps every field description the model is taught with.
*/
export default defineDynamic({
events: {
"session.started": () => {
const defs = trySearchDefs();
if (!defs) return null;
// Plain JSON data, computed here so the schema factory's closure holds only that (see above).
const inputSchema = z.toJSONSchema(defs.search.inputSchema);
return defineTool({
description: defs.search.description,
inputSchema,
execute: (input: Record<string, unknown>) => searchDefs().search.execute(input),
});
},
},
export default defineDynamic(() => {
const defs = trySearchDefs();
if (!defs) return null;
// Plain JSON data, computed here so the schema factory's closure holds only that (see above).
const inputSchema = z.toJSONSchema(defs.search.inputSchema);
return defineTool({
description: defs.search.description,
inputSchema,
execute: (input: Record<string, unknown>) => searchDefs().search.execute(input),
});
});
Loading
Loading