Eve Runtime

Connect an Eve agent to assistant-ui with useEveAgentRuntime, eve/next, durable sessions, streaming messages, and human-in-the-loop approvals.

@assistant-ui/eve integrates assistant-ui with Eve, Vercel's filesystem-first framework for durable agents. It wraps Eve's useEveAgent hook and exposes it as an assistant-ui ExternalStoreRuntime, so Eve owns the session stream while assistant-ui renders messages, reasoning, dynamic tool calls, and approval requests.

When to use it

Pick the Eve runtime when:

  • You want your agent implementation to live under agent/ and be served by eve/next.
  • You want Eve sessions, continuation tokens, NDJSON streaming, and local development tooling.
  • You want assistant-ui to render Eve messages and human-in-the-loop tool approvals without writing a custom runtime adapter.

Architecture

The Next.js app mounts Eve with withEve() and assistant-ui's registry transform with withAui():

next.config.ts
import { withAui } from "@assistant-ui/next";
import type { NextConfig } from "next";
import { withEve } from "eve/next";

const nextConfig: NextConfig = {};

export default withEve(withAui(nextConfig));

On the client, useEveAgentRuntime() calls Eve's React hook and converts Eve message parts into assistant-ui thread messages:

app/page.tsx
"use client";

import { Thread } from "@/components/assistant-ui/elements/thread.aui";
import { useEveAgentRuntime } from "@assistant-ui/eve";
import { AssistantRuntimeProvider } from "@assistant-ui/react";

export default function Home() {
  const runtime = useEveAgentRuntime();

  return (
    <AssistantRuntimeProvider runtime={runtime}>
      <Thread />
    </AssistantRuntimeProvider>
  );
}

The custom bag of a per-turn runConfig is forwarded to Eve as clientContext, so runConfig: { custom: { page: "/pricing" } } arrives as clientContext: { page: "/pricing" }. An empty or absent bag is omitted, and every value must be JSON-serializable.

Eve turns that object into a model context message, which is why the assistant-ui custom envelope is unwrapped: the envelope would put a literal custom key in the prompt. It is page or client context rather than run configuration, and it cannot select a model or change how the turn executes. Keep secrets, credentials, and personal data out of it, since the model reads it as message text.

Connector authorization

When Eve pauses for connector authorization, the runtime emits an assistant data part named authorization. Its data includes state, name, and the available display fields such as displayName, description, url, userCode, instructions, and expiresAt. Completed parts may also include outcome and reason. url is present only when the connector supplied an http(s) address; the adapter drops other schemes so a renderer can link to it directly.

The Eve template ships this renderer as components/eve-authorization.tsx and mounts it next to <Thread />. To add it to an existing app, register a data UI for the part and type it with the exported EveAuthorizationData:

authorization-ui.tsx
import type { EveAuthorizationData } from "@assistant-ui/eve";
import { makeAssistantDataUI } from "@assistant-ui/react";

export const AuthorizationUI = makeAssistantDataUI<EveAuthorizationData>({
  name: "authorization",
  render: ({ data }) => (
    <div>
      {data.state === "required" ? (
        <>
          <p>{data.instructions ?? `Sign in to ${data.displayName ?? data.name}`}</p>
          {data.userCode && <code>{data.userCode}</code>}
          {data.url && <a href={data.url}>Continue to sign in</a>}
        </>
      ) : (
        <p>{data.outcome ?? "Authorization completed"}</p>
      )}
    </div>
  ),
});

Mount <AuthorizationUI /> inside the AssistantRuntimeProvider tree. The standard thread leaves unmatched data parts unrendered.

Questions

Eve's opt-in ask_question tool (added with eve add tool/ask_question) pauses the turn until the user answers. The runtime projects it onto an assistant-ui tool approval that carries the prompt, the options, and whether a free-form answer is accepted, and the answer goes back through respondToApproval. While the question is pending, a composer message that matches an option label, or any message when the question accepts free text, answers it as well. Any other message withdraws the question, and the tool returns { interrupted: true }.

Without a renderer for ask_question, the question is drawn by the default ToolFallback inside the collapsed tool group, which reads as a step in the agent's trace rather than as the agent talking to the user. The Eve template ships a renderer as components/eve-ask-question.tsx that presents the question on its own with the answer controls, and eve add @assistant-ui/eve-chat installs it. Add it to an existing app from the registry:

npx shadcn@latest add https://r.assistant-ui.com/eve-ask-question.json

Then register the toolkit it exports on the runtime provider in app/page.tsx:

app/page.tsx
"use client";

import { Thread } from "@/components/assistant-ui/elements/thread.aui";
import { eveAskQuestionToolkit } from "@/components/eve-ask-question";
import { useEveAgentRuntime } from "@assistant-ui/eve";
import {
  AssistantRuntimeProvider,
  AuiConfig,
  Tools,
} from "@assistant-ui/react";

export default function Home() {
  const runtime = useEveAgentRuntime();
  const config = AuiConfig({
    tools: Tools({ toolkit: eveAskQuestionToolkit }),
  });

  return (
    <AssistantRuntimeProvider runtime={runtime} config={config}>
      <Thread />
    </AssistantRuntimeProvider>
  );
}

The renderer builds on ToolFallback.Approval, so it inherits the option buttons, the free-form answer field, the confirmation step, and the settled receipt with the recorded answer. A withdrawn question reads as skipped, a failed call shows its error message, and an output it does not recognize falls back to the raw tool result.

Store threads in Assistant Cloud

Without cloud, the runtime runs a single Eve session. Persist useEveSession() and pass it back as initialSession with resume: true to pick the conversation up after a reload.

Pass cloud to back a thread list with Assistant Cloud instead. cloud is the only switch: NEXT_PUBLIC_ASSISTANT_BASE_URL alone keeps the single session. Pass it from the first render, since adding or removing it later throws (remount the runtime, for example with a key, to switch), and use eve 0.44.1 or later, which can resume a session.

const runtime = useEveAgentRuntime({ cloud });

Each cloud thread keeps one Eve session, stored as its external id. Eve assigns a session id only once a thread's first turn is accepted, so a new thread is saved to the cloud at that point. Until then, a first message staged without a run, or a first turn that fails before Eve creates the session, leaves the thread unsaved. The save uses upsert, so a save retried after a lost response reuses the thread that already has the session.

Opening a thread resumes its session: the thread reports loading while the replay runs, and a message sent meanwhile waits for it. A cloud thread that no Eve runtime created has no session: it opens empty, and sending in it keeps the draft in the composer.

session, initialSession, initialEvents and resume are not used with cloud, and useEveReset() starts a new thread, leaving the current thread and its session in the list. Renaming, archiving and deleting change the cloud thread only, since Eve has no call to delete a session. The runtime copies each settled turn to the cloud, so the dashboard shows the conversation.

Requirements

  • Node.js 24 or higher.
  • React 18 or 19.
  • An Eve app mounted with eve/next.
  • A model credential for the model configured in agent/agent.ts.

Eve's default gateway model ids route through the Vercel AI Gateway. Use AI_GATEWAY_API_KEY or Vercel OIDC, or configure a direct provider LanguageModel. For local development, a direct LanguageModel can also run on a ChatGPT Plus or Pro plan without any API key; see ChatGPT Subscription.

Install

npm install @assistant-ui/react @assistant-ui/eve eve

Auth note

Eve's built-in eve channel accepts localhost during development and trusted Vercel OIDC callers. It does not automatically admit browser users in production. Before deploying a public app, add agent/channels/eve.ts and wire the channel to your application auth.

Next