Eve

Add long-term agent memory to Vercel Eve agents with the Zep SDK

Eve is Vercel’s framework for building production agents. This guide shows how to wire Zep into an Eve agent with the Zep TypeScript SDK. The pattern uses hooks for persistence and authored tools for retrieval.

Keep retrieved context out of privileged instructions

Zep context can include content that your users, documents, or tools supplied. A system or developer message gives that content higher instruction priority than ordinary input. Some convenience integrations use system-message injection. Use direct SDK retrieval or an actual retrieval tool call unless all stored content is application-authored and trusted. Follow Memory security best practices for provider-specific placement.

Why this pattern

Eve hooks are observe-only. They can persist side effects, but they cannot add model input. Authored tools preserve the distinction between application instructions and retrieved data.

ConcernEve primitiveZep API
Persist turnsHook (message.received / message.completed)thread.addMessages
Warm cacheHook (session.started)user.warm (fire-and-forget)
RecallAuthored toolgraph.search (scope: "auto")

Do not use Eve dynamic instructions or channel context for retrieved memory. Dynamic instructions enter a privileged channel. Channel context enters durable session history and accumulates.

Identity mapping stays outside the model:

  • Eve session.id → Zep threadId (for example eve-<sessionId>)
  • Eve session auth principal (or your app’s user id) → Zep userId

Never accept userId or threadId from the model.

Architecture

Eve HTTP message
├─ session.started → ensure user/thread, fire-and-forget warm
├─ message.received / message.completed → thread.addMessages
└─ authored tools → graph.search on user or standalone graph

An authored tool passes its current query directly to graph.search. Pin the user or graph identity from authenticated session state.

Setup

$npm install @getzep/zep-cloud eve

Requires Node.js 24+ (Eve), @getzep/zep-cloud, and a Zep Cloud API key from app.getzep.com.

$export ZEP_API_KEY="your-zep-api-key"

Provision users and threads with your own create-then-catch-conflict helper, shown here as ensureZepUserAndThread, so repeats are safe. After the helper returns, warm the user cache as fire-and-forget:

1await ensureZepUserAndThread({ userId, userName, threadId });
2void zep.user.warm(userId).catch(() => {});

Automatic message capture

Persist each turn with a hook. Skip interim narration before tool calls (finishReason === "tool-calls"); persist other completions (stop, length, and similar):

1export default defineHook({
2 events: {
3 async "session.started"(_event, ctx) {
4 const identity = resolveZepIdentity(ctx);
5 await ensureZepUserAndThread(identity);
6 void zep.user.warm(identity.userId).catch(() => {});
7 },
8
9 async "message.received"(event, ctx) {
10 const text = event.data.message?.trim()?.slice(0, 4000);
11 if (!text) return;
12 const { threadId, userName } = resolveZepIdentity(ctx);
13 await zep.thread.addMessages(threadId, {
14 messages: [{ role: "user", name: userName, content: text }],
15 });
16 },
17
18 async "message.completed"(event, ctx) {
19 if (event.data.finishReason === "tool-calls") return;
20 const text = event.data.message?.trim()?.slice(0, 4000);
21 if (!text) return;
22 const { threadId } = resolveZepIdentity(ctx);
23 await zep.thread.addMessages(threadId, {
24 messages: [{ role: "assistant", name: "Eve Agent", content: text }],
25 });
26 },
27 },
28});

Wrap each Zep call in try/catch so a Zep outage never fails the Eve turn.

Zep indexes knowledge asynchronously. Facts from a turn are not reliably searchable until processing finishes — often tens of seconds. Confirm facts in the Zep app before expecting preference recall in a new session.

On-demand search tools

Expose graph.search as authored tools when the turn’s memory section is incomplete. Pin userId / graphId from session auth or config — never from the model:

1// User graph
2await zep.graph.search({
3 userId,
4 query: query.slice(0, 400), // Zep rejects queries over 400 chars
5 scope: "auto",
6 maxCharacters: 4000,
7});
8
9// Standalone company graph (seed once outside Eve, then search)
10await zep.graph.search({
11 graphId: process.env.ZEP_COMPANY_GRAPH_ID || "eve-demo-company",
12 query: query.slice(0, 400), // Zep rejects queries over 400 chars
13 scope: "auto",
14 maxCharacters: 4000,
15});

For shared organization knowledge, create and seed a standalone graph with the Zep SDK. Wait for episodes to process before the tool searches the graph.

Production notes

  • Replace demo identity — resolve userId from real auth in multi-tenant production.
  • Idempotent ensure — catch “already exists” on user and thread create.
  • Message size — Zep rejects thread messages over 4,096 characters; truncate to ~4,000 before thread.addMessages.
  • Search query size — Zep rejects graph.search queries over 400 characters; truncate before calling.
  • Async indexing — do not expect read-after-write within the same turn.

Learn more