---
title: "Overview"
description: "Put an eve agent behind a browser chat UI with useEveAgent."
---

Use this guide when you already have a browser application and need to connect it to an eve agent. `useEveAgent()` opens a durable session, sends turns, streams the reply back, and turns the raw event stream into render-ready state. React is the reference implementation; [Vue](./use-eve-agent-vue) and [Svelte](./use-eve-agent-svelte) ship the same surface.

## The integration model

A browser UI is a client of the agent's HTTP routes (the [eve channel](../../channels/overview)). Two layers wire it up:

- **Routing** makes the agent reachable from the browser. A framework integration can mount eve on your app's origin: [Next.js](./nextjs) (`withEve`), [Nuxt](./nuxt) (the `eve/nuxt` module), [SvelteKit](./sveltekit) (the `eveSvelteKit` Vite plugin), or [TanStack Start](./tanstack) (the `eveTanStack` Vite plugin). If the frontend and agents are peer services, use your deployment platform or reverse proxy instead. The hook works with either setup; pass `host` for a different origin or a named path mount.
- **The hook** (`useEveAgent`) holds the session state, streaming, errors, and composer status. It defaults to same-origin eve routes such as `/eve/v1/session`.

The framework pages walk through the wiring step by step: [Next.js](./nextjs), [Nuxt](./nuxt), [SvelteKit](./sveltekit), and [TanStack Start](./tanstack). See [Project Structure](../../concepts/project-structure#configure-a-web-deployment) to choose between a framework integration and peer services.

For scripts, server-to-server calls, evals, tests, or custom clients that do not need framework UI state, use the [Client SDK](../client/overview) directly.

## Authenticate browser requests

A same-origin framework integration sends your application cookies with every eve request. For bearer tokens or another non-cookie scheme, pass `auth` or `headers` to `useEveAgent`.

The default eve channel fails closed. Without an authored `agent/channels/eve.ts`, production browser traffic receives `401` from the default `[vercelOidc(), localDev(), placeholderAuth()]` policy. Add the channel file with an `AuthFn` that verifies your application session or token.

For a public demo, use `none()` from `eve/channels/auth` to admit anonymous requests explicitly. Do not use `none()` for an agent that handles private or production data. See [Authentication](../auth-and-route-protection) for application-session examples, token verifiers, and the default policy.

## Basic chat (React)

The hook lives in `eve/react`. Render `data.messages`, use `status` to steer follow-ups during an active turn, and send text with `send`:

```tsx
"use client";

import { useEveAgent } from "eve/react";

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

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

## Returned state

`useEveAgent()` returns the current UI state plus commands:

| Field          | What it is                                                                                |
| -------------- | ----------------------------------------------------------------------------------------- |
| `data`         | The selected projection: `ConversationState` by default, or your custom reducer's state.  |
| `conversation` | Canonical conversation state, including messages, root turns and inputs, and child state. |
| `status`       | `"ready"`, `"resuming"`, `"submitted"`, `"streaming"`, or `"error"`. Drives the composer. |
| `error`        | The latest session creation, stream, resume, or turn failure.                             |
| `events`       | Raw eve stream events for this session.                                                   |
| `session`      | Serializable fixed session cursor (`sessionId`, `streamIndex`).                           |
| `prewarm`      | Create the durable session without starting a turn.                                       |
| `send`         | Send text or a multi-part message, with per-turn options.                                 |
| `respond`      | Answer pending HITL input requests, with per-turn options.                                |
| `resume`       | Replay an attached session and follow its in-flight turn.                                 |
| `cancel`       | Request durable cancellation of the active turn.                                          |
| `reset`        | Clear local events, data, errors, and the local session cursor.                           |

Most chat UIs only need `data.messages` and `status` with the default reducer. Use `conversation` for session state when a custom reducer shapes `data`. Drop down to `events` when you need the authoritative wire events directly, for example to persist an audit log or build a custom projection.

Pass `prewarm: true` to start a workflow when the chat mounts and after each reset:

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

export function Chat() {
  const agent = useEveAgent({ prewarm: true });

  return <button onClick={agent.reset}>New chat</button>;
}
```

In React, `prewarm` is a live boolean. Use component state to wait until the user starts
composing a message:

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

export function Chat() {
  const [message, setMessage] = useState("");
  const agent = useEveAgent({ prewarm: message.length > 0 });

  return (
    <>
      <input value={message} onChange={(event) => setMessage(event.currentTarget.value)} />
      <button
        onClick={() => {
          setMessage("");
          agent.reset();
        }}
        type="button"
      >
        New chat
      </button>
    </>
  );
}
```

An initial `true` value or a later `false` to `true` change prepares the current owned session.
Changing `prewarm` back to `false` does not abort creation or discard an existing session. If
a reset and state update happen in the same event, React applies both before deciding whether
to prepare the next session. Vue and Svelte read `prewarm` when their binding is created; call
their returned `prewarm()` method for interaction-driven creation.

Prewarming defaults to `false`: the first send creates the session. Set it to `true` only after
your application's auth, headers, and any chat-record binding are ready. Applications that
prepare those in the submit handler should keep the default until then.

Prewarming starts the workflow and establishes its inbox, then parks before session hooks,
dynamic definitions, or sandbox setup run. The first message passes through the channel's
`onMessage` hook and supplies the identity and context for initialization and `turn_0`.

The explicit `prewarm()` method resolves on `202 Accepted`, as does `client.sessions.create()`.
It does not wait for an initialization event; concurrent calls share the same create request.
The composer stays `"ready"`, meaning it can accept input, and prewarming does not run
`prepareSend`. A concurrent `send()` waits only for acceptance, then posts the message.
If the inbox is still starting, the client retries `409 session_not_ready` with bounded
exponential backoff for up to 20 seconds. The caller's abort signal cancels the wait. Stream
events are not a prerequisite for sending.

A failed create sets `error` and calls `onError` when no send is waiting. A waiting send can
fall back to creating a session with its message. After creation succeeds, initialization
failures arrive on the stream during the first turn. The configured session timeout starts
at creation, so an unused prewarmed session can expire before its first message. React does not
retry a failed prewarm on unrelated renders while the option remains `true`; call `prewarm()`,
change the option from `false` to `true` again, reset, or send a message to recover.

The hook keeps consuming one session stream across turns and idle periods. Replies,
authorization updates, and turns started elsewhere all update the same projection. Separately
delivered participant messages remain separate bubbles before that turn's assistant response, even
when they arrive after the response begins. A single event that coalesces multiple deliveries
remains one bubble. A turn settling does not close the connection. The transport reconnects from
its cursor after a disconnect or the
server's renewable 60-second lease; lease renewal does not start a turn or clear UI state.
Unmounting or resetting closes the local stream without cancelling the old durable session.

`reset()` reuses the hook instance and clears the conversation. When `prewarm` is true after
the reset, it prepares the next owned session; otherwise, the next send creates it. An explicitly
supplied `session` remains externally owned and is never replaced by prewarming or reset. Use
`initialSession` with `resume: true` to restore a saved chat.

`data.messages` are eve-owned `EveMessage[]`. Assistant text and reasoning parts have a stable `id` from the event that created the part. Appends and completion retain that ID. Use `part.id` to key streamed assistant content, including successive text parts within one step. Older events without an ID get a best-effort fallback. User text parts may not have an ID.

Common text, reasoning, file, and dynamic-tool parts follow the [AI SDK `UIMessage`](https://ai-sdk.dev/docs/reference/ai-sdk-core/ui-message) rendering convention, but the types are not interchangeable. eve also exposes authorization and HITL metadata, and a file part's URL can be absent. A tool part's `toolMetadata.eve.label` is how the call reads to people, the same text eve's channels show: the tool's [label](/docs/tools#label-tool-activity) or a readable form of its name, replaced by its completion label once the call settles. Show it instead of `toolName`, which can be an internal name such as `eve:subagent:researcher`. A skill load's part has the `toolName` `eve__skill`, and `toolMetadata.eve.name` holds the skill's name. Adapt those parts before passing messages to an API typed as `UIMessage[]`.

A tool part's `state` comes from the shared lifecycle interpretation of the event stream. Its events supply input, output, and error text. A call waiting on an approval or a question reads `approval-requested` until the stream settles the request. An answer you submit doesn't change the part until then. `toolCallState(conversation, callId)` reads tool-call status, including task cancellation and calls interrupted without a result. The full event projection remains internal; `ConversationState` retains its messages, turns, inputs, tasks, and agent sessions.

When the root agent delegates, its stream emits `task.started` for the call, `agent.started` with the child's `sessionId`, and `task.settled` with the call's result, failure, or cancellation. The default reducer keeps the subagent's tool part running until `task.settled` arrives and then settles it with the call's outcome; the start receipt never settles it. `data.tasks[taskId].calls[callId]` records each call's `status`, `output`, and `error`, and `data.agents[sessionId]` records each session a call opened, with its `taskId` and opening `callId`. These records come from the root stream alone.

Detailed child progress lives on the child session's stream, not in the root `data.messages`. Pass `followSubagents: true` to follow the session each subagent call reaches while the component is mounted; following is off by default. Narrow `data.agents[sessionId].observation.status` before reading its `conversation`: `"not-followed"` has no child detail, `"following"` has live detail, and `"idle"` has everything the calls so far produced and resumes when another call reaches the task. `"unavailable"` means the stream failed, not that the child failed. Calls that continue a task with its `taskId` reach the same session, so one session's `conversation.messages`, `inputs`, and `turns` span all of them. Sessions that a custom task opens with `ctx.agent` are recorded but not followed.

If you manage your own session rather than using the hook, pass an `agent.started` event from `events` to [`session.agent()`](../client/streaming#follow-a-subagent) and follow the child with `stream()`. It follows local and remote children through the parent, so the browser never calls a remote deployment directly. See [What the parent sees](../../subagents#what-the-parent-sees) for the complete contract.

## Sending and streaming

Pass the message first and optional per-turn settings second. Use `respond()` for HITL answers:

```tsx
await agent.send("Summarize this session.");

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

Assistant text, reasoning, tool calls, and tool results stream into `data` as they arrive, and a new turn moves `status` from `ready` to `submitted` to `streaming` and back. Resuming an attached session uses `resuming` while eve performs bounded catch-up. Disable message and HITL submission in that state. A settled tail moves directly to `ready`; an in-flight tail moves to `streaming` before eve follows it.

A turn stays open while tasks it started are working. `status` stays `streaming` until the turn ends, and `data.turns[turnId].waiting` is `true` while the turn is parked, until its next step starts. When a call the turn runs asks a question, `send()` resolves at that point, as `session.send().result()` does; answer with `respond()` while the turn stays open.

To steer an active turn with a follow-up, send the message with `turnPolicy: "steer"`. Before answer output or local tool execution begins, eve interrupts pending model generation and continues the same turn with the correction. Executing tools finish and checkpoint their results first; after answer output starts, steering waits for the next committed workflow boundary. Reasoning and search progress do not count as answer output. The hook stays attached through completion:

```tsx
await agent.send(message, { turnPolicy: "steer" });
```

Other message sends reject while the hook is processing a turn. Answers to open input requests are accepted at any time; see [Human-in-the-loop prompts](#human-in-the-loop-prompts). The default `data.inputs` ledger keeps each request's originating `turnId` and `stepIndex`, response and settlement status, including requests left open after their originating turn ends; `data.activeTurnId` tracks the current root turn when known. A request from a task's run also carries that task's `taskId`. Each followed session keeps its own ledger in `data.agents[sessionId].observation.conversation`. Call `cancel()` to stop the durable server-side turn without replacing it, and `reset()` to clear local state so the next send starts a fresh durable session.

`cancel()` can be called as soon as `status` is `"submitted"`; the hook waits for the active response to identify its turn when necessary, sends one guarded cancellation request, and keeps the event stream attached. The promise resolves when eve accepts the request or reports that no turn is active, and rejects if the cancellation request fails. The turn then settles on the same stream as `turn.cancelled` followed by `session.waiting`, so the session is safe to continue.

```tsx
if (agent.status === "submitted" || agent.status === "streaming") {
  await agent.cancel();
}
```

Unmounting the component or closing the page disconnects the local stream but does not cancel server execution. Call `cancel()` before detaching when the user intends to stop the durable turn.

After eve confirms an attachment turn with `message.received`, the default reducer projects each
received attachment as a `file` part on the user message. The part includes `mediaType`, optional
`filename` and `size`, and a `url` only when the original attachment was browser-resolvable.

## Human-in-the-loop prompts

Tools opt into approval with `approval`, and workflow tools such as the opt-in `ask_question` tool ask the user a question with `ctx.ask()` — see [Human-in-the-loop](/docs/human-in-the-loop) for the server-side model. Either way the stream emits an `input.requested` event. The default projection keeps open requests in `data.inputs`, including a request that sits on an earlier message than the latest one. Render from that ledger and answer through the same session with `respond()`. The request also appears on its originating `dynamic-tool` part at `part.toolMetadata?.eve?.inputRequest` for inline display:

```tsx
import { openConversationInputs } from "eve/react";

const pendingRequests = openConversationInputs(agent.data).map((input) => input.request);

const inputDisabled = agent.status === "resuming";

return pendingRequests.map((request) => (
  <fieldset disabled={inputDisabled} key={request.requestId}>
    <legend>
      {request.kind === "tool-approval"
        ? "Approval required"
        : request.kind === "question"
          ? "Question"
          : "Session limit"}
    </legend>
    <p>{request.prompt}</p>
    {request.options?.map((option) => (
      <button
        key={option.id}
        onClick={() => void agent.respond([{ requestId: request.requestId, optionId: option.id }])}
        type="button"
      >
        {option.label}
      </button>
    ))}
  </fieldset>
));
```

For a question with `allowFreeform`, render a text input and send `{ requestId, text }`. You can answer while other work runs. For example, when one model step asks to approve two tools, the first approval settles on its own and the second stays answerable while the batch waits for it. `respond()` marks each answered request `responded` in `data.inputs` until the server settles it through `approval.settled` or `input.resolved`, so `openConversationInputs()` no longer returns it. Answering a request that is no longer open rejects without contacting the server. A refused approval attempt, or a response that never reaches the server, reopens the request. Approved tools update again when eve streams their result.

## Authorization prompts

Connections and tools that need OAuth or another grant emit `authorization.required`. The default reducer projects that into an `authorization` message part with the display name, instructions, device code, and user-facing sign-in URL. When multiple people authorize the same connection in one session, the client matches completion to the corresponding `attemptId`, not merely the connection name. The part stays `required` until eve reports the outcome, then becomes `completed`. `awaitsCallback` is `true` when the sign-in's callback settles the attempt and resumes the parked turn. Render the part as a normal chat message and keep the session cursor so eve can resume the parked turn:

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

function AuthorizationPrompt({ part }: { part: EveMessagePart }) {
  if (part.type !== "authorization") return null;

  if (part.state === "completed") {
    return (
      <p>
        {part.outcome === "authorized"
          ? `${part.displayName} connected.`
          : `${part.displayName} authorization ${part.outcome}.`}
      </p>
    );
  }

  return (
    <section>
      <p>{part.description}</p>
      {part.authorization?.userCode ? <code>{part.authorization.userCode}</code> : null}
      {part.authorization?.url ? <a href={part.authorization.url}>Sign in</a> : null}
    </section>
  );
}
```

With a custom reducer, authorization attempts remain available in `conversation.messages`; `authorization.required` and `authorization.completed` are also available on `events` and `onEvent`.

## Attach page context per turn

`clientContext` adds ephemeral context for the current turn. Strings (or an array of strings) become user-role context messages; an object is JSON-serialized into one. The context remains available to every model call in the turn, then disappears before the next turn. It rides along with a message or HITL response, so it never dispatches a turn on its own and never lands in durable session history. Pass it in the second argument to `send()` or `respond()`:

```tsx
await agent.send("What should I do on this screen?", {
  clientContext: { route: "/billing", plan: "pro", seatsUsed: 4 },
});
```

To attach the same context to every turn without threading it through each call site, use `prepareSend`. It runs right before each send and returns the (possibly augmented) turn:

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

## Lifecycle callbacks

The hook accepts these lifecycle callbacks:

- `onEvent(event)`: fires for each eve stream event as it arrives.
- `onError(error)`: fires when session creation, streaming, resume, or a turn fails. A failed prewarm reports here when no send is waiting to retry creation.
- `onFinish(snapshot)`: fires with the final `{ data, status, session, ... }` snapshot once a turn settles.
- `onSessionChange(session)`: fires when a session is created, its cursor advances, or reset clears it. Persist the session state to resume across reloads.

```tsx
const agent = useEveAgent({
  onEvent: (event) => console.debug(event.type),
  onError: (error) => toast.error(error.message),
  onFinish: (snapshot) => console.log(snapshot.status),
});
```

The `optimistic` option (default `true`) projects submitted user messages into `data` before eve confirms them with a `message.received` event. Steered follow-ups appear before the active turn's assistant response once the turn is known; if the send fails, the failed placeholder stays in place. These are reducer-facing projection events only. `events` stays the authoritative eve stream. For an existing session, the hook reconciles each placeholder with the server's delivery ID, so an unrelated or identical message cannot consume it.

## Custom reducer

The default hook uses `conversationReducer`, which includes the `EveMessageData` message fields plus turn and input state (`ConversationState`). When you want `data` shaped differently, pass a `reducer` implementing `EveAgentReducer<TData>`:

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

interface ToolLog {
  readonly toolCalls: number;
}

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

const agent = useEveAgent({ reducer: toolCounter });
// agent.data is ToolLog; agent.conversation remains ConversationState.
```

`reduce(data, event)` receives authoritative eve stream events and client projection events (`client.message.submitted`, `client.message.failed`, `client.input.responded`). The custom reducer only shapes `data`: the client maintains `conversation` separately for authorization, input requests, turns, and child following. Agent-session observations update `conversation.agents`, not the custom reducer. The built-in reducer makes `data` and `conversation` the same state. `client.input.responded` tells a custom reducer an answer was submitted, not that the server accepted it; the durable `input.resolved` event confirms settlement. Return `data` unchanged for events your reducer does not handle.

## Resumable sessions

The browser conversation lives durably on the server. Persist both the rendered event log and the `session` cursor to pick it back up after a reload:

```tsx
import type { ClientSessionState, MessageStreamEvent } from "eve/client";

type SavedEveChat = {
  events?: readonly MessageStreamEvent[];
  session?: ClientSessionState;
};

const [saved] = useState<SavedEveChat>(() => {
  const raw = localStorage.getItem("eve-chat");
  return raw ? JSON.parse(raw) : {};
});

const agent = useEveAgent({
  initialEvents: saved.events ?? [],
  initialSession: saved.session,
  resume: saved.session !== undefined,
  onFinish(snapshot) {
    localStorage.setItem(
      "eve-chat",
      JSON.stringify({
        events: snapshot.events,
        session: snapshot.session,
      }),
    );
  },
});
```

Store the full `session` object (`sessionId`, `streamIndex`). The session cursor
lets eve continue the exact durable conversation; the event log lets your UI
render historical messages without replaying the whole stream. A database-backed
chat app should usually persist stream events as they arrive with `onEvent` and
then save a final snapshot in `onFinish`.

`initialEvents` must be an ordered prefix of the same session's stream, but its endpoint does not have to line up exactly with where the stream resumes. When the event count matches `initialSession.streamIndex`, catch-up continues from that cursor. A partial or overlapping saved log falls back to index `0`. Every event carries a stable [`meta.id`](/docs/concepts/sessions-runs-and-streaming#the-event-envelope), and the store drops any event whose id it has already applied, so an overlapping replay renders once and `onEvent` only fires for events your UI has not seen.

For multiple chat threads, keep one saved event log and session cursor per thread. `agent`, `host`, `reducer`, `session`, `initialEvents`, `initialSession`, `auth`, `headers`, `optimistic`, and `resume` are read when the hook creates its store, so remount the chat component when switching threads, for example with `key={chat.id}`. React reads `prewarm` on every render.

Pass `resume: true` with `initialSession` to rebuild the projection from the durable stream after mount. While `status` is `"resuming"`, render hydrated `data` but disable message and HITL submission; do not present cancellation or active-turn progress controls. If catch-up finds an in-flight turn, `status` changes to `"streaming"` before the binding follows it to a boundary. The stream reports its initial durable tail so catch-up can finish without closing the connection or waiting for an idle timeout. A settled tail changes directly to `"ready"`; a newer turn or pending authorization remains `"streaming"`. The connection stays open to receive later events. A terminal session failure changes to `"error"`.

```tsx
const agent = useEveAgent({
  initialSession: { sessionId, streamIndex: 0 },
  resume: true,
});
```

If the user can refresh or navigate immediately after pressing send, create your app-level chat row before calling `send()`, then persist the session ID from `onSessionChange`. This lets the reloaded UI mount with that ID and pass `resume: true` while the durable turn is still running.

## Custom hosts and headers

Pass `host` when the eve server isn't same-origin, and pass `auth` or `headers` when the channel needs credentials. Function values are re-resolved before every HTTP request, reconnects included:

```tsx
const agent = useEveAgent({
  host: "https://agent.example.com",
  auth: {
    bearer: async () => await getAccessToken(),
  },
});
```

Headers supplied to `send()` apply to that turn request and become the credentials used by the
continuous session stream on its next reconnect. A later send replaces them with that turn's
headers.

When a framework integration mounts multiple named agents, pass `agent` instead of `host`:

```tsx
const support = useEveAgent({ agent: "support" });
```

## Per-framework integration

| Framework      | Integration                          | Hook                                             |
| -------------- | ------------------------------------ | ------------------------------------------------ |
| Next.js        | [`withEve`](./nextjs)                | [`useEveAgent` (React)](#basic-chat-react)       |
| Nuxt           | [`eve/nuxt` module](./nuxt)          | [`useEveAgent` (Vue)](./use-eve-agent-vue)       |
| SvelteKit      | [`eveSvelteKit` plugin](./sveltekit) | [`useEveAgent` (Svelte)](./use-eve-agent-svelte) |
| TanStack Start | [`eveTanStack` plugin](./tanstack)   | [`useEveAgent` (React)](#basic-chat-react)       |
| Any React      | same-origin or `host`                | [`useEveAgent` (React)](#basic-chat-react)       |

## What to read next

- [Sessions, runs & streaming](../../concepts/sessions-runs-and-streaming): the event stream and session cursor
- [Channels](../../channels/overview): the HTTP routes the hook talks to
- [Client SDK](../client/overview): the lower-level client underneath the frontend hooks
- [Next.js](./nextjs): step-by-step setup for wiring eve into a Next.js app
