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

# Overview



The frontend helpers put a browser chat or agent UI on top of 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:

* **The framework integration** mounts the eve routes on your app's origin, so the browser never crosses a CORS boundary or reads an env var to find the agent. Pick yours: [Next.js](./nextjs) (`withEve`), [Nuxt](./nuxt) (the `eve/nuxt` module), or [SvelteKit](./sveltekit) (the `eveSvelteKit` Vite plugin). On any other stack the hook talks to same-origin `/eve/v1/*` routes directly, or you pass an explicit `host`.
* **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 per-framework pages below walk through the wiring step by step: [Next.js](./nextjs), [Nuxt](./nuxt), and [SvelteKit](./sveltekit).

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={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`    | Projected UI state from the reducer. Defaults to `{ messages }`.                          |
| `status`  | `"ready"`, `"resuming"`, `"submitted"`, `"streaming"`, or `"error"`. Drives the composer. |
| `error`   | The last `Error` thrown, if any.                                                          |
| `events`  | Raw eve stream events for this session.                                                   |
| `session` | Serializable fixed session cursor (`sessionId`, `streamIndex`).                           |
| `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`. Drop down to `events` when you need the authoritative wire events directly, for example to persist an audit log or build a custom projection.

`data.messages` are eve-owned `EveMessage[]`. 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. Adapt those parts before passing messages to an API typed as `UIMessage[]`.

When the root agent delegates, its stream emits `subagent.called` with the child's `childSessionId`, then `subagent.completed` after admission with a working task receipt. Later task notifications wake the parent with completion, failure, or cancellation. Detailed child progress lives on the child session's stream instead of being flattened into the root `data.messages`. Use the lower-level [TypeScript client](../client/overview#sessions) to attach to that ID when your UI needs live subagent activity. 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. To replace an active turn with a follow-up, send the message with `turnPolicy: "steer"`. eve accepts the message through the durable session, cancels the active turn, and keeps the hook attached through the replacement turn:

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

Other message sends and HITL responses reject while the hook is processing a turn. 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 the model can also ask a question with `ask_question` — see [Human-in-the-loop](/docs/human-in-the-loop) for the server-side model. Either way the stream emits an `input.requested` event, and the pending request rides on a `dynamic-tool` part at `part.toolMetadata?.eve?.inputRequest`. Scan every message because an unrelated turn can add newer messages while an approval stays open, then answer through the same session with `respond()`:

```tsx
const pendingRequests = agent.data.messages
  .flatMap((message) => message.parts)
  .flatMap((part) => {
    if (part.type !== "dynamic-tool" || part.state !== "approval-requested") return [];
    const request = part.toolMetadata?.eve?.inputRequest;
    return request ? [request] : [];
  });

return pendingRequests.map((request) => (
  <fieldset 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 }`. The default reducer marks each matching part as responded immediately. 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. Render that part as a normal chat message, then keep the session cursor; eve resumes the parked turn when the callback completes and updates the part after `authorization.completed`:

```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>
  );
}
```

For fully custom state machines, `authorization.required` and `authorization.completed` are still 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 with the last `Error` when a turn fails.
* `onFinish(snapshot)`: fires with the final `{ data, status, session, ... }` snapshot once a turn settles.
* `onSessionChange(session)`: fires when the session cursor advances. Persist it 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. These are reducer-facing projection events only. `events` stays the authoritative eve stream.

## Custom reducer

The default reducer projects events into `{ messages }` (`EveMessageData`). 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
```

`reduce(data, event)` receives both authoritative eve stream events and client projection events (`client.message.submitted`, `client.message.failed`, `client.input.responded`). `client.input.responded` updates the submitting UI immediately; the durable `input.resolved` event later confirms the server-accepted outcome and lets replayed history rebuild the same HITL state. 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}`.

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. A settled tail changes directly to `"ready"` after a bounded catch-up check, without waiting for the live stream idle timeout. If that check finds a newly started turn or pending authorization, eve keeps following it. 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(),
  },
});
```

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) |
| 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


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)