# Client integration

> eve/client Client and ClientSession, auth policies, streaming reducers, useEveAgent React/Vue/Svelte hooks, and framework plugins (eve/next, eve/nuxt, eve/sveltekit).

- Repository: vercel/eve
- GitHub: https://github.com/vercel/eve
- Human docs: https://grok-wiki.com/public/docs/vercel-eve-759e1d74a10f
- Complete Markdown: https://grok-wiki.com/public/docs/vercel-eve-759e1d74a10f/llms-full.txt

## Source Files

- `docs/guides/client/overview.mdx`
- `docs/guides/client/streaming.mdx`
- `docs/guides/frontend/overview.mdx`
- `packages/eve/src/client/index.ts`
- `packages/eve/src/client/client.ts`
- `packages/eve/src/react/use-eve-agent.ts`

---

---
title: "Client integration"
description: "eve/client Client and ClientSession, auth policies, streaming reducers, useEveAgent React/Vue/Svelte hooks, and framework plugins (eve/next, eve/nuxt, eve/sveltekit)."
---

The `eve/client` entrypoint is the typed HTTP client for `/eve/v1/*` routes. It wraps session POSTs, NDJSON stream consumption, reconnection, and cursor advancement. Browser chat UIs add `useEveAgent` from `eve/react`, `eve/vue`, or `eve/svelte` on top of the shared `EveAgentStore`, and framework plugins (`eve/next`, `eve/nuxt`, `eve/sveltekit`) mount same-origin Eve routes so the hooks default to `host: ""`.

## Integration layers

```text
┌─────────────────────────────────────────────────────────────┐
│  Browser UI (React / Vue / Svelte)                          │
│  useEveAgent → EveAgentStore → reducer → render-ready data  │
└──────────────────────────┬──────────────────────────────────┘
                           │
┌──────────────────────────▼──────────────────────────────────┐
│  eve/client: Client → ClientSession → MessageResponse         │
│  POST turn + NDJSON stream + reconnect + SessionState cursor  │
└──────────────────────────┬──────────────────────────────────┘
                           │ fetch
┌──────────────────────────▼──────────────────────────────────┐
│  /eve/v1/session, /eve/v1/session/:id, /eve/v1/health, …      │
│  (proxied same-origin by framework plugin in dev/deploy)      │
└───────────────────────────────────────────────────────────────┘
```

| Layer | Import | Responsibility |
| --- | --- | --- |
| TypeScript client | `eve/client` | Scripts, tests, evals, custom UIs without framework state |
| Framework hooks | `eve/react`, `eve/vue`, `eve/svelte` | Session lifecycle, optimistic projection, reactive `data` |
| Route mounting | `eve/next`, `eve/nuxt`, `eve/sveltekit` | Dev proxy / production rewrite to Eve service on same origin |

<Note>
Framework hooks and the raw client share `ClientAuth`, `HeadersValue`, `SendTurnPayload`, `SessionState`, and stream event types exported from `eve/client`.
</Note>

## Client and ClientSession

### Client

`Client` binds one `host`, auth policy, header policy, and per-turn reconnection budget. One client can own many concurrent `ClientSession` handles.

```ts
import { Client } from "eve/client";

const client = new Client({
  host: "http://127.0.0.1:3000",
});
```

<ParamField body="host" type="string" required>
Origin where Eve routes are mounted. Use `""` for same-origin browser calls against `/eve/v1/*`.
</ParamField>

<ParamField body="auth" type="ClientAuth">
Bearer or Basic credentials. Values can be static strings or async functions resolved before every HTTP call, including stream reconnects.
</ParamField>

<ParamField body="headers" type="HeadersValue">
Custom headers on every request. Static map or per-request resolver (for example bypass tokens).
</ParamField>

<ParamField body="maxReconnectAttempts" type="number" default="3">
Maximum stream reconnection attempts per turn after transient disconnects.
</ParamField>

<ParamField body="preserveCompletedSessions" type="boolean" default="false">
When `true`, keeps `continuationToken` and `sessionId` after `session.completed` so the next `send()` continues the same durable conversation instead of starting fresh.
</ParamField>

Utility methods:

| Method | Route | Purpose |
| --- | --- | --- |
| `health()` | `GET /eve/v1/health` | Fail fast before creating a session |
| `info()` | `GET /eve/v1/info` | Agent inspection payload |
| `fetch(path, init)` | Any path on `host` | Authenticated escape hatch for framework-owned routes |
| `session(state?)` | — | Create or resume a `ClientSession` |

Non-2xx responses throw `ClientError` with `status` and `body`.

### ClientSession

`ClientSession` tracks one conversation's `continuationToken`, `sessionId`, and `streamIndex`. Read `session.state` after each turn and serialize it to resume later.

```ts
const session = client.session();

// Resume from persisted cursor
const resumed = client.session({
  continuationToken: "eve:…",
  sessionId: "wrun_…",
  streamIndex: 10,
});

// Shorthand: continuation token only
const fromToken = client.session("eve:…");
```

`send()` posts to `POST /eve/v1/session` on the first turn, then `POST /eve/v1/session/:sessionId` for follow-ups. It returns a `MessageResponse` as soon as the POST completes.

<ParamField body="message" type="string | UserContent">
User turn text or multi-part content (text, file attachments as data URLs).
</ParamField>

<ParamField body="inputResponses" type="InputResponse[]">
HITL responses for pending approvals or `ask_question` prompts.
</ParamField>

<ParamField body="clientContext" type="string | string[] | JsonObject">
Ephemeral page context for the next model call only. Never persisted to durable session history.
</ParamField>

<ParamField body="outputSchema" type="StandardJSONSchemaV1 | JsonObject">
Structured output schema; the client lowers Standard Schema implementations to JSON Schema before send.
</ParamField>

<ParamField body="signal" type="AbortSignal">
Cancels the POST and stream. Arm timeouts before `await send()` so they cover both phases.
</ParamField>

<ParamField body="headers" type="Record<string, string>">
Per-turn headers merged on top of client-level headers.
</ParamField>

HITL delivery retries: when `inputResponses` is non-empty, the client retries POST delivery up to 10 times on `500` responses whose body matches `target session was not found`.

`stream(options?)` attaches to the existing NDJSON stream for the current `sessionId`, resuming from `streamIndex` unless `startIndex` overrides it. Throws if no message has been sent yet.

## Authentication

Client auth is transport-level. Route auth is enforced server-side on the Eve channel (`agent/channels/eve.ts`). Both must align for production browser integrations.

### Client-side credentials

<CodeGroup>
```ts Bearer
const client = new Client({
  host: "https://agent.example.com",
  auth: {
    bearer: async () => await getAccessToken(),
  },
});
```

```ts Basic
const client = new Client({
  host: "https://agent.example.com",
  auth: {
    basic: {
      username: "agent-client",
      password: async () => await getRotatingSecret(),
    },
  },
});
```

```ts Custom headers
const client = new Client({
  host: "https://agent.example.com",
  headers: async () => ({
    "x-vercel-protection-bypass": await getBypassToken(),
  }),
});
```
</CodeGroup>

Bearer tokens that resolve to an empty string omit the `Authorization` header entirely rather than sending `Bearer `.

### Server-side route auth

The default Eve channel is fail-closed: without an authored `agent/channels/eve.ts`, Eve registers `eveChannel({ auth: [localDev(), vercelOidc()] })`. Same-origin framework integrations inherit session cookies automatically; for token schemes, pass matching `auth` or `headers` to `Client` or `useEveAgent`.

<Warning>
Client credentials do not bypass server route auth. A `401` from `/eve/v1/session` means the channel rejected the request, not that the client misconfigured `host`.
</Warning>

## Streaming and MessageResponse

Every `ClientSession.send()` yields a `MessageResponse` that is both metadata (immediate `sessionId`, `continuationToken`) and an async iterable of NDJSON events.

### Aggregate a turn

```ts
const response = await session.send("Summarize the forecast.");
const result = await response.result();

console.log(result.status);   // "waiting" | "completed" | "failed"
console.log(result.message);  // final assistant text
console.log(result.events);   // all events this turn
```

`result()` consumes until a turn boundary: `session.waiting`, `session.completed`, or `session.failed`.

### Stream events live

```ts
const response = await session.send("Draft a plan.");

for await (const event of response) {
  if (event.type === "message.appended") {
    process.stdout.write(event.data.messageDelta);
  }
}
```

Each `MessageResponse` can only be consumed once. After abort, start a new `send()` for the next turn.

### Common stream events

| Event | UI use |
| --- | --- |
| `message.received` | Confirm user message landed |
| `reasoning.appended` | Render reasoning deltas |
| `message.appended` | Render assistant text deltas |
| `actions.requested` | Show tool calls |
| `action.result` | Show tool results |
| `input.requested` | Pause for HITL approval or question |
| `result.completed` | Structured output when `outputSchema` was set |
| `session.waiting` | Enable composer for next turn |
| `session.completed` | Terminal conversation |
| `session.failed` | Terminal failure |

Import helpers from `eve/client`:

```ts
import { isCurrentTurnBoundaryEvent, isTurnFailureEvent } from "eve/client";
import type { HandleMessageStreamEvent } from "eve/client";
```

### Reconnection

On transient socket disconnect, the client resumes from the number of events already consumed in the current turn, up to `maxReconnectAttempts` (default `3`). Caller-initiated `AbortSignal` aborts without reconnecting.

### Session cursor advancement

After each streamed turn, `advanceSession` updates `SessionState`:

- `session.waiting` → preserve `sessionId`, `continuationToken`, advance `streamIndex`
- `session.completed` or `session.failed` → reset to fresh state unless `preserveCompletedSessions: true` on `session.completed`

## Reducers and EveAgentStore

`EveAgentStore` is the framework-agnostic state machine behind all `useEveAgent` bindings. It drives one turn at a time (`send` rejects while `submitted` or `streaming`), manages optimistic projection, and notifies subscribers.

### Reducer contract

```ts
import type { EveAgentReducer } from "eve/client";

interface EveAgentReducer<TData> {
  initial(): TData;
  reduce(data: TData, event: EveAgentReducerEvent): TData;
}
```

`EveAgentReducerEvent` is either an authoritative Eve stream event or a client projection event:

| Client projection event | When emitted |
| --- | --- |
| `client.message.submitted` | Optimistic user message before `message.received` |
| `client.message.failed` | Submitted message failed before confirmation |
| `client.input.responded` | HITL responses sent via `send()` |

`events` on the store snapshot is the raw server stream only. Projection events feed `data` through the reducer but never appear in `events`.

### Default message reducer

`defaultMessageReducer()` projects into `{ messages: EveMessage[] }` following the AI SDK `UIMessage` `messages[].parts[]` convention. Parts include user text, assistant text, reasoning, tool calls, tool results, and `dynamic-tool` parts with HITL metadata at `part.toolMetadata?.eve?.inputRequest`.

Custom reducers receive the same event union:

```tsx
import { useEveAgent } from "eve/react";
import type { EveAgentReducer } from "eve/react";

const toolCounter: EveAgentReducer<{ toolCalls: number }> = {
  initial: () => ({ toolCalls: 0 }),
  reduce: (data, event) =>
    event.type === "actions.requested" ? { toolCalls: data.toolCalls + 1 } : data,
};

const agent = useEveAgent({ reducer: toolCounter });
```

## useEveAgent hooks

All three framework bindings wrap `EveAgentStore` with framework-specific reactivity. Session-shaping options (`host`, `reducer`, `session`, `initialSession`, `auth`, `headers`, `maxReconnectAttempts`, `optimistic`) are read once at store creation; remount to change them. Lifecycle callbacks refresh every render.

| Framework | Import | Return shape |
| --- | --- | --- |
| React | `eve/react` | Snapshot object + `send`, `stop`, `reset` via `useSyncExternalStore` |
| Vue | `eve/vue` | Reactive `ComputedRef`s + commands; auto-imported by `eve/nuxt` |
| Svelte | `eve/svelte` | Rune-friendly getters on a reactive binding |

### Returned state

| Field | Type | Purpose |
| --- | --- | --- |
| `data` | `TData` | Reducer projection (default: `{ messages }`) |
| `status` | `"ready" \| "submitted" \| "streaming" \| "error"` | Composer gating |
| `error` | `Error \| undefined` | Last failure |
| `events` | `HandleMessageStreamEvent[]` | Authoritative server stream |
| `session` | `SessionState` | Serializable cursor |
| `send` | `(SendTurnPayload) => Promise<void>` | Dispatch turn |
| `stop` | `() => void` | Abort in-flight turn |
| `reset` | `() => void` | Clear local state, new session |

### Basic React chat

```tsx
"use client";

import { useEveAgent } from "eve/react";

export function Chat() {
  const agent = useEveAgent();
  const isBusy = agent.status === "submitted" || agent.status === "streaming";

  return (
    <form
      onSubmit={(event) => {
        event.preventDefault();
        const form = new FormData(event.currentTarget);
        const message = String(form.get("message") ?? "").trim();
        if (message.length > 0) void agent.send({ message });
      }}
    >
      {agent.data.messages.map((message) => (
        <article key={message.id}>
          <header>{message.role}</header>
          {message.parts.map((part, index) =>
            part.type === "text" ? <p key={index}>{part.text}</p> : null,
          )}
        </article>
      ))}
      <input name="message" disabled={isBusy} />
      <button disabled={isBusy} type="submit">Send</button>
    </form>
  );
}
```

### Sending turns

```tsx
await agent.send({ message: "Summarize this session." });

await agent.send({
  message: [
    { type: "text", text: "What is in this file?" },
    { type: "file", data: fileDataUrl, mediaType: "application/pdf", filename: "report.pdf" },
  ],
});
```

Use `createDataUrlFilePart` and `createTextWithFileContent` from `eve/client` to build file parts from bytes.

### Human-in-the-loop

When the stream emits `input.requested`, read the pending request from the latest message's `dynamic-tool` part:

```tsx
const request = agent.data.messages
  .at(-1)
  ?.parts.find((part) => part.type === "dynamic-tool" && part.toolMetadata?.eve?.inputRequest)
  ?.toolMetadata?.eve?.inputRequest;

if (request) {
  await agent.send({
    inputResponses: [{ requestId: request.requestId, optionId: "approve" }],
  });
}
```

### Client context per turn

`clientContext` adds ephemeral context for one model call. Pass it on `send()` or attach globally via `prepareSend`:

```tsx
const agent = useEveAgent({
  prepareSend: (input) => ({
    ...input,
    clientContext: { route: location.pathname },
  }),
});
```

### Lifecycle callbacks

<ParamField body="onEvent" type="(event) => void">
Fires for each authoritative stream event.
</ParamField>

<ParamField body="onError" type="(error) => void">
Fires on turn failure.
</ParamField>

<ParamField body="onFinish" type="(snapshot) => void">
Fires when a turn settles with final snapshot.
</ParamField>

<ParamField body="onSessionChange" type="(session) => void">
Fires when `SessionState` advances; use to persist cursor.
</ParamField>

<ParamField body="prepareSend" type="PrepareSend">
Runs immediately before each `send`; return modified `SendTurnPayload`.
</ParamField>

<ParamField body="optimistic" type="boolean" default="true">
Project submitted user messages before `message.received`.
</ParamField>

### Resumable sessions

Persist the full `session` object (`sessionId`, `continuationToken`, `streamIndex`):

```tsx
const [initialSession] = useState(() => {
  const raw = localStorage.getItem("eve-session");
  return raw ? JSON.parse(raw) : undefined;
});

const agent = useEveAgent({
  initialSession,
  onSessionChange(session) {
    localStorage.setItem("eve-session", JSON.stringify(session));
  },
});
```

## Framework plugins

Framework plugins proxy `/eve/v1/*` to a local Eve dev server in development and to a private Eve Vercel service in production, keeping browser requests same-origin.

### Next.js — `eve/next`

Wrap `next.config.ts` with `withEve()`:

```ts
import type { NextConfig } from "next";
import { withEve } from "eve/next";

const nextConfig: NextConfig = {};
export default withEve(nextConfig);
```

| Option | Default | Purpose |
| --- | --- | --- |
| `eveRoot` | Next.js app root | Path to Eve agent directory |
| `eveBuildCommand` | `"eve build"` | Eve Vercel service build command |
| `configureVercelOutput` | `true` | Write `experimentalServices` to `.vercel/output/config.json` |
| `servicePrefix` | `"/_eve_internal/eve"` | Private Vercel route namespace |
| `devServerTimeoutMs` | `180000` | Dev server startup timeout |

Local production: `next build && next start` serves built Eve output on port `4274` (override with `EVE_NEXT_PRODUCTION_PORT`). Non-Vercel hosts: set `EVE_NEXT_PRODUCTION_ORIGIN`.

### Nuxt — `eve/nuxt`

```ts
export default defineNuxtConfig({
  modules: ["eve/nuxt"],
  eve: {
    eveRoot: "../my-agent",       // optional
    eveBuildCommand: "npm run build:eve",  // optional
  },
});
```

Auto-imports `useEveAgent` from `eve/vue`. Production overrides: `EVE_NUXT_PRODUCTION_ORIGIN`, `EVE_NUXT_PRODUCTION_PORT`.

### SvelteKit — `eve/sveltekit`

Register `eveSvelteKit()` before `sveltekit()` in `vite.config.ts`:

```ts
import { sveltekit } from "@sveltejs/kit/vite";
import { eveSvelteKit } from "eve/sveltekit";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [eveSvelteKit(), sveltekit()],
});
```

Dev proxy resolves `EVE_BASE_URL`, a healthy shared dev server, or spawns `eve dev --no-ui --port 0`. For separate-origin Eve in production, pass `host` to `useEveAgent`.

<Steps>
<Step title="Install eve and scaffold agent">
Install `eve`, run `eve init` if needed, and confirm `agent/` exists at the app root or set `eveRoot`.
</Step>
<Step title="Mount routes with framework plugin">
Add `withEve`, `eve/nuxt`, or `eveSvelteKit` so `/eve/v1/*` is same-origin.
</Step>
<Step title="Wire the hook or client">
Use `useEveAgent` for browser UIs or `Client` + `ClientSession` for scripts and custom integrations.
</Step>
<Step title="Configure auth on both sides">
Match client `auth`/`headers` to the Eve channel policy in `agent/channels/eve.ts`.
</Step>
<Step title="Persist session cursor">
Store `agent.session` via `onSessionChange` for reload survival.
</Step>
</Steps>

## When to use which surface

| Use case | Surface |
| --- | --- |
| Browser chat UI with streaming state | `useEveAgent` + framework plugin |
| Backend job, eval, test harness | `Client` + `ClientSession` |
| Custom UI projection shape | `useEveAgent({ reducer })` or manual `for await` over `MessageResponse` |
| Attach to in-progress stream after reload | `client.session(persistedState).stream()` |
| Agent discovery from a script | `client.info()` |

<Tip>
`EveMessage[]` from the default reducer follow AI SDK `UIMessage` conventions and drop into AI SDK UI primitives that accept `UIMessage[]`.
</Tip>

## Related pages

<CardGroup>
<Card title="Sessions and streaming" href="/sessions-and-streaming">
continuationToken vs sessionId, POST routes, NDJSON vocabulary, and reconnect semantics.
</Card>
<Card title="HTTP API reference" href="/http-api">
Stable `/eve/v1` routes, request/response shapes, and event vocabulary.
</Card>
<Card title="Auth and deployment" href="/auth-and-deployment">
Route auth on the Eve channel, env vars, and production verification.
</Card>
<Card title="Security model" href="/security-model">
Fail-closed route auth defaults and trust boundaries.
</Card>
<Card title="TypeScript API reference" href="/typescript-api">
`define*` helpers, client entrypoints, and exported types.
</Card>
<Card title="Quickstart" href="/quickstart">
Shortest path from `eve init` to a running HTTP session.
</Card>
</CardGroup>
