Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions docs/content/docs/openui-lang/examples/harnesses/vercel-eve.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
---
title: Vercel Eve
description: Connect a Vercel Eve agent to OpenUI and render streamed responses and tool calls as live generative UI.
---

OpenUI can render output from a [Vercel Eve](https://github.com/vercel/eve) agent without replacing Eve's session protocol. This example uses Eve's built-in HTTP channel for delivery and resumable streaming, translates its events to AG-UI, and passes that stream to OpenUI's `<FullScreen>` chat.

The agent receives the OpenUI component-library prompt when a session starts, so its responses use OpenUI Lang and render as cards, tables, charts, forms, and other interactive components.

[View source on GitHub →](https://github.com/thesysdev/openui/tree/main/examples/harnesses/vercel-eve)

## Architecture

```text
browser ── Eve session HTTP ──▶ Eve agent ── model + tools
▲ │
└── AG-UI SSE ── event adapter ─┘
rendered by <FullScreen>
```

The integration has four main pieces:

| Piece | File | Role |
| --- | --- | --- |
| OpenUI chat | `src/app/page.tsx` | Renders `<FullScreen>` with `agUIAdapter()` and the built-in OpenUI component library. |
| Session bridge | `src/eve-chat.ts` | Delivers turns through Eve's HTTP API, follows its resumable event stream, and returns AG-UI SSE to OpenUI. |
| Event adapter | `src/eve-stream.ts` | Maps Eve text, tool-call, and failure events to AG-UI events. |
| Agent instructions | `agent/instructions/openui.ts` | Adds the generated OpenUI system prompt when each Eve session starts. |

## Connecting OpenUI

The page creates Eve-backed chat callbacks and passes them to `<FullScreen>`. OpenUI's AG-UI adapter consumes the SSE response produced by `processMessage`:

```tsx
import { agUIAdapter } from "@openuidev/react-headless";
import { FullScreen } from "@openuidev/react-ui";
import { openuiChatLibrary } from "@openuidev/react-ui/genui-lib";
import { createEveChatProps } from "../eve-chat";

const chatProps = createEveChatProps();

<FullScreen
{...chatProps}
streamProtocol={agUIAdapter()}
componentLibrary={openuiChatLibrary}
agentName="Eve + OpenUI"
/>;
```

`createEveChatProps()` also provides OpenUI's thread callbacks. The example stores thread metadata, transcripts, Eve session IDs, continuation tokens, and stream cursors in `localStorage`, so each OpenUI thread resumes the corresponding Eve conversation.

## Teaching Eve OpenUI Lang

Eve loads instructions from `agent/instructions`. A dynamic instruction adds the component library's generated prompt once when a session starts:

```ts
import { openuiChatLibrary, openuiChatPromptOptions } from "@openuidev/react-ui/genui-lib";
import { defineDynamic, defineInstructions } from "eve/instructions";

export default defineDynamic({
events: {
"session.started": () =>
defineInstructions({
markdown: openuiChatLibrary.prompt(openuiChatPromptOptions),
}),
},
});
```

This keeps the model's instructions synchronized with the exact component library used by the renderer.

## Translating Eve events

Eve emits typed session events. The adapter in `src/eve-stream.ts` converts the events OpenUI needs:

```text
actions.requested -> TOOL_CALL_START / TOOL_CALL_ARGS / TOOL_CALL_END
message.appended -> TEXT_MESSAGE_CONTENT
message.completed -> TEXT_MESSAGE_CONTENT (non-streaming fallback)
turn.failed -> RUN_ERROR
session.failed -> RUN_ERROR
```

Tool calls and text share one assistant message ID. OpenUI renders tool activity in its behind-the-scenes section and the final OpenUI Lang response in the conversation.

## Eve session protocol

The browser talks to the same-origin Eve endpoints installed by `withEve()`:

```text
POST /eve/v1/session
POST /eve/v1/session/:id
GET /eve/v1/session/:id/stream?startIndex=N
```

For a follow-up turn, the bridge sends the saved continuation token and resumes from the saved stream index. A completed session clears the cursor; waiting and failed sessions remain resumable.

The demo channel uses anonymous authentication for local development. Replace `none()` in `agent/channels/eve.ts` with an authenticated Eve channel before exposing the application.

## Project layout

```text
examples/harnesses/vercel-eve/
|- agent/agent.ts # Eve model and build configuration
|- agent/channels/eve.ts # Eve HTTP session channel
|- agent/instructions/openui.ts # Generated OpenUI Lang instructions
|- agent/tools/get_current_time.ts # Example Eve tool
|- src/app/page.tsx # OpenUI FullScreen chat
|- src/eve-chat.ts # Session transport and persistence
|- src/eve-stream.ts # Eve-to-AG-UI event mapping
|- src/thread-store.ts # Browser thread and transcript storage
|- next.config.ts # Installs Eve with withEve()
```

## Run the example

Eve 0.11 requires Node.js 24. From the repository root:

```bash
pnpm install

cd examples/harnesses/vercel-eve
LLM_API_KEY=your-api-key pnpm dev
```

By default the agent uses OpenAI. Configure `LLM_MODEL` and `LLM_BASE_URL` to use another OpenAI-compatible endpoint. Open [http://localhost:3000](http://localhost:3000) and start a conversation.

The repository scripts also expose Eve directly when you need to build or run the agent separately:

```bash
pnpm eve:build
pnpm eve:start
```
3 changes: 2 additions & 1 deletion docs/content/docs/openui-lang/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"examples/langgraph-chat",
"examples/react-email",
"---Harnesses---",
"examples/harnesses/pi-agent-harness"
"examples/harnesses/pi-agent-harness",
"examples/harnesses/vercel-eve"
]
}
15 changes: 15 additions & 0 deletions examples/harnesses/vercel-eve/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
node_modules
.env*
.eve
.vercel
.workflow-data
.next
.output
.nitro
dist
.DS_Store
*.tsbuildinfo

# Local-only live integration smoke test (needs a running agent + API key)
scripts/verify-thread-context.mjs

8 changes: 8 additions & 0 deletions examples/harnesses/vercel-eve/.vercelignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
node_modules
.env*
.eve
.workflow-data
.next
.output
.nitro
dist
3 changes: 3 additions & 0 deletions examples/harnesses/vercel-eve/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# eve Agent App

This project uses the eve framework. Before writing code, always read the relevant guide in `node_modules/eve/docs/`.
20 changes: 20 additions & 0 deletions examples/harnesses/vercel-eve/agent/agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { createOpenAI } from "@ai-sdk/openai";
import { defineAgent } from "eve";

const apiKey = process.env.LLM_API_KEY || process.env.OPENAI_API_KEY;
const baseURL =
process.env.LLM_BASE_URL || process.env.OPENAI_BASE_URL || "https://api.openai.com/v1";
const modelName = process.env.LLM_MODEL || process.env.OPENAI_MODEL || "gpt-5.5";
const openai = createOpenAI({
apiKey,
baseURL,
});

const model = openai(modelName);

export default defineAgent({
model,
build: {
externalDependencies: ["@openuidev/react-lang", "@openuidev/react-ui", "react", "react-dom"],
},
});
12 changes: 12 additions & 0 deletions examples/harnesses/vercel-eve/agent/channels/eve.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { none } from "eve/channels/auth";
import { eveChannel } from "eve/channels/eve";

/**
* Eve's built-in HTTP channel: serves the standard `/eve/v1/session*` routes
* (deliver + resumable NDJSON event stream). The OpenUI client talks to these
* directly via `eve/client`, so there is no custom transport to maintain.
*
* `none()` allows anonymous traffic for the local demo — swap in `bearer()` /
* `basic()` before exposing this publicly.
*/
export default eveChannel({ auth: none() });
3 changes: 3 additions & 0 deletions examples/harnesses/vercel-eve/agent/instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Identity

You are a helpful assistant.
15 changes: 15 additions & 0 deletions examples/harnesses/vercel-eve/agent/instructions/openui.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { openuiChatLibrary, openuiChatPromptOptions } from "@openuidev/react-ui/genui-lib";
import { defineDynamic, defineInstructions } from "eve/instructions";

/**
* Teach the agent to answer in OpenUI Lang. Resolved once per session so the
* (large) component-library prompt is only attached when a conversation starts.
*/
export default defineDynamic({
events: {
"session.started": () =>
defineInstructions({
markdown: openuiChatLibrary.prompt(openuiChatPromptOptions),
}),
},
});
47 changes: 47 additions & 0 deletions examples/harnesses/vercel-eve/agent/tools/get_current_time.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { defineTool } from "eve/tools";
import { z } from "zod";

/**
* A tiny, dependency-free tool so the demo exercises the full tool-call path:
* the model emits a tool call, Eve runs `execute`, and OpenUI renders the call
* (name + arguments) in its "behind the scenes" panel. The runtime tool name is
* the filename slug, `get_current_time`.
*/
export default defineTool({
description:
"Get the current date and time, optionally for a specific IANA timezone " +
"(e.g. 'America/New_York', 'Asia/Tokyo'). Use this whenever the user asks " +
"what the current time or date is.",
inputSchema: z.object({
timezone: z
.string()
.optional()
.describe("IANA timezone name such as 'Asia/Kolkata'. Defaults to UTC."),
}),
outputSchema: z.object({
iso: z.string(),
formatted: z.string(),
timezone: z.string(),
}),
execute({ timezone }) {
const now = new Date();
let tz = timezone?.trim() || "UTC";
let formatted: string;
try {
formatted = new Intl.DateTimeFormat("en-US", {
dateStyle: "full",
timeStyle: "long",
timeZone: tz,
}).format(now);
} catch {
// Unknown timezone: fall back to UTC rather than failing the turn.
tz = "UTC";
formatted = new Intl.DateTimeFormat("en-US", {
dateStyle: "full",
timeStyle: "long",
timeZone: tz,
}).format(now);
}
return { iso: now.toISOString(), formatted, timezone: tz };
},
});
26 changes: 26 additions & 0 deletions examples/harnesses/vercel-eve/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
import { defineConfig, globalIgnores } from "eslint/config";

const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// ESLint's flat config only auto-ignores node_modules, so mirror the build
// artifacts from .gitignore (the eve/next/nitro output dirs ship huge bundles
// that would otherwise blow the parser's stack).
globalIgnores([
".next/**",
".eve/**",
".output/**",
".nitro/**",
".vercel/**",
".workflow-data/**",
"out/**",
"build/**",
"dist/**",
"next-env.d.ts",
"scripts/**",
]),
]);

export default eslintConfig;
6 changes: 6 additions & 0 deletions examples/harnesses/vercel-eve/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
8 changes: 8 additions & 0 deletions examples/harnesses/vercel-eve/next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { withEve } from "eve/next";
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
turbopack: {},
};

export default withEve(nextConfig);
50 changes: 50 additions & 0 deletions examples/harnesses/vercel-eve/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"name": "vercel-eve",
"version": "0.0.0",
"type": "module",
"imports": {
"#*": "./agent/*",
"#evals/*": "./evals/*"
},
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"eve:dev": "eve dev",
"eve:build": "eve build",
"eve:start": "eve start",
"lint": "eslint",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@ai-sdk/openai": "^3.0.41",
"@openuidev/react-headless": "workspace:*",
"@openuidev/react-lang": "workspace:*",
"@openuidev/react-ui": "workspace:*",
"@vercel/connect": "0.2.2",
"ai": "7.0.0-beta.178",
"eve": "^0.11.7",
"next": "16.2.6",
"react": "19.2.3",
"react-dom": "19.2.3",
"zod": "4.4.3"
},
"devDependencies": {
"@types/node": "24.x",
"@types/react": "^19",
"@types/react-dom": "^19",
"@typescript/native-preview": "7.0.0-dev.20260523.1",
"eslint": "^9",
"eslint-config-next": "16.2.6",
"typescript": "^5"
},
"overrides": {
"ai": "7.0.0-beta.178"
},
"resolutions": {
"ai": "7.0.0-beta.178"
},
"engines": {
"node": "24.x"
}
}
13 changes: 13 additions & 0 deletions examples/harnesses/vercel-eve/src/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@import "@openuidev/react-ui/styles/index.css";

html,
body {
margin: 0;
}

.app-shell {
height: 100vh;
overflow: hidden;
position: relative;
width: 100vw;
}
Loading
Loading