# OpenUI Assistant

An Eve agent that streams OpenUI Lang generative UI using the official openuiChatLibrary, with demo weather, stock, and search tools from the openui-chat example.

- Install: `npx shadcn@latest add @evex/openui-assistant`
- Category: general
- Author: [TommyBez](https://www.evex.sh/authors/TommyBez)
- Updated: 2026-07-04
- Dependencies: @openuidev/react-lang@^0.2.8, @openuidev/react-ui@^0.12.1, eve@^0.18.2, just-bash@^3.0.2, zod@^4.4.3
- Web page: https://www.evex.sh/agents/openui-assistant
- This document: https://www.evex.sh/agents/openui-assistant.md

## Overview

OpenUI Assistant is an eve agent that answers every chat turn with OpenUI Lang, a structured generative UI language, so your chat streams rendered cards, tables, and follow-up buttons instead of markdown prose. It builds its system prompt at build time by calling openuiChatLibrary.prompt(openuiChatPromptOptions) from @openuidev/react-ui, the same library and prompt options the official openui-chat example app uses, so its output stays compatible with the upstream renderer.

You interact with it through a normal chat session: run eve dev for the terminal UI, or mount the bundled OpenUIEveChat reference component in a Next.js app that uses withEve(), where useEveAgent() streams the reply into the Renderer from @openuidev/react-lang. Every reply is a program whose first statement assigns root, built from chat primitives like Card, CardHeader, TextContent, Table, and FollowUpItem.

Three deterministic demo tools ship with the agent: get_weather, get_stock_price, and search_web. They mirror the sample tools from the openui-chat example and return mock JSON, which makes the agent a self-contained sandbox for evaluating generative UI without wiring real data providers or API keys.

## How it works

1. At build time, agent/instructions/openui-prompt.ts calls openuiChatLibrary.prompt(openuiChatPromptOptions) and injects the resulting OpenUI system prompt into the agent, alongside instructions.md which enforces the output contract: OpenUI Lang only, no markdown fences, first statement assigns root.
2. When a message arrives in the terminal UI or your web chat, the agent, running on the openai/gpt-5-mini model, decides whether the request needs get_weather, get_stock_price, or search_web before composing any UI.
3. The relevant tool runs and returns deterministic mock JSON, for example a weather object with temperature, humidity, wind, and a two-day forecast, or a stock quote with price, change, volume, and day range, and the agent is instructed never to invent tool-backed numbers.
4. The agent composes a top-down OpenUI Lang program from those tool results, using chat-library components such as Card, Table, ListBlock, and FollowUpBlock, and explicitly avoiding Stack, which is not part of openuiChatLibrary.
5. On the frontend, the reference component in agent/skills/openui/references/openui-eve-chat.tsx connects useEveAgent() to the @openuidev/react-lang Renderer, which paints live components while the stream is still in flight.
6. Three evals guard the contract with a 120 second timeout each: openui-format-contract checks that a greeting yields a pure OpenUI welcome card, while stock-quote-exact-price and weather-card-uses-tool-data verify the rendered UI matches the exact tool output.

## Use cases

### Prototype a generative UI chat product

Use the agent as a working end-to-end reference for streaming structured UI from an eve backend to a React frontend, with the openui skill documenting syntax, library constraints, and a debugging checklist for parse and render failures.

### Demo dashboards from natural language

Ask for a weather card for Tokyo or an NVDA quote rendered as a dashboard tile, and the agent calls the matching demo tool and streams cards, tables, and follow-up buttons built from that JSON.

### Test your OpenUI renderer integration

Because the tools are deterministic, the same prompt produces the same data every run, making it a stable fixture for verifying that your Renderer setup, withEve() wiring, and @openuidev packages handle streamed OpenUI Lang correctly.

### Learn the OpenUI Lang output contract

The instructions, skill references, and eval suite together show how to enforce a strict format contract on a model: root-first programs, no markdown fences, no forbidden components, and tool-backed numbers only.

## Requirements

- `AI_GATEWAY_API_KEY`: Optional model gateway credential for the host eve app; get one from Vercel AI Gateway. Not needed when your deployment already provides credentials through VERCEL_OIDC_TOKEN or another provider. The agent itself requires no agent-specific environment variables.
- `@openuidev/react-ui and @openuidev/react-lang`: npm packages (^0.12.1 and ^0.2.8) that provide openuiChatLibrary, the prompt options, and the React Renderer. Both must be installed in the host app for the frontend reference component to work.
- `eve`: The eve agent framework (^0.18.2) that runs the agent, tools, skill, and evals; the web integration also relies on withEve() from eve/next and the useEveAgent() hook. Requires Node 24 or newer.

## FAQ

### How do I install the agent?

Run npx shadcn@latest add @evex/openui-assistant in your eve app, then install the registry dependencies (@openuidev/react-ui, @openuidev/react-lang, eve, just-bash, zod) if they are not already present. Start it with pnpm dev or eve dev.

### Which model does it use, and can I change it?

agent/agent.ts pins openai/gpt-5-mini via defineAgent. You can swap the model string for any provider your eve gateway supports, then re-run the bundled evals to confirm the new model still honors the OpenUI Lang format contract.

### Is the web search real?

No. search_web returns deterministic mock results and is labeled demo data by design; the instructions forbid using it for current or factual research. The weather and stock tools are also deterministic samples mirrored from the openui-chat example.

### Why does the UI sometimes render as plain text or fail to parse?

The model broke the contract, usually by adding markdown fences or prose around the program. The openui skill ships a debugging checklist: confirm the reply starts with root =, all identifiers are defined, and no Stack component is used.

### How do I render the UI in my own Next.js app?

Wrap your app with withEve() from eve/next, copy the openui-eve-chat.tsx reference into your components folder, and render OpenUIEveChat on a page. The frontend-wiring.md reference covers full setup plus an AgentInterface alternative matching the upstream chat shell.

## Files installed

- `agent/agent.ts`
- `agent/instructions/openui-prompt.ts`
- `agent/instructions.md`
- `agent/lib/openui-library.ts`
- `agent/skills/openui/references/frontend-wiring.md`
- `agent/skills/openui/references/openui-eve-chat.tsx`
- `agent/skills/openui/references/syntax-examples.md`
- `agent/skills/openui/SKILL.md`
- `agent/tools/get_stock_price.ts`
- `agent/tools/get_weather.ts`
- `agent/tools/search_web.ts`
- `evals/evals.config.ts`
- `evals/openui-format-contract.eval.ts`
- `evals/stock-quote-exact-price.eval.ts`
- `evals/weather-card-uses-tool-data.eval.ts`
- `agent/README.md`
- `.env.example`

## File contents

### `agent/agent.ts`

```ts
import { defineAgent } from "eve";

export default defineAgent({
  model: "openai/gpt-5-mini",
});

```

### `agent/instructions/openui-prompt.ts`

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

const openuiSystemPrompt = createOpenuiSystemPrompt();

export default defineInstructions({
  markdown: openuiSystemPrompt,
});

function createOpenuiSystemPrompt(): string {
  try {
    return openuiChatLibrary.prompt(openuiChatPromptOptions);
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);

    throw new Error(`Failed to generate OpenUI system prompt: ${message}`);
  }
}

```

### `agent/instructions.md`

```md
# Mission
You are a generative UI assistant. Every assistant reply must be valid OpenUI
Lang that renders through the bundled `openuiChatLibrary` component set.

# Non-negotiable output rules
- Respond with OpenUI Lang only. Do not wrap output in markdown fences or add
  prose before or after the program.
- The first statement must assign to `root`.
- Generate top-down: layout first, then nested components, then leaf data.
- Use tools before rendering weather or stock tiles. Never invent tool-backed
  numbers.
- `search_web` is a deterministic demo tool from the OpenUI example, not a live
  web search provider. Label its results as demo search results and do not use it
  for current, time-sensitive, or factual research claims.
- When a tool fails, render a `Card` with a concise error message and a recovery
  action button instead of plain text.

# Workflow
1. Load the `openui` skill when you need component syntax, library constraints,
   or debugging help for OpenUI Lang output.
2. Parse the user request and decide whether you need `get_weather`,
   `get_stock_price`, or `search_web`.
3. Call the relevant tools, wait for structured JSON results, then compose the
   UI program from those facts.
4. Prefer chat-library primitives such as `Card`, `CardHeader`, `TextContent`,
   `Table`, `ListBlock`, `FollowUpBlock`, and `FollowUpItem`. Do not use
   `Stack`; it is not part of `openuiChatLibrary`.
5. For greetings or help requests, render a welcome `Card` with suggested action
   follow-ups such as weather lookup, stock quote, or demo search.

# Interaction patterns
- Weather: show location, current conditions, temperature, humidity, wind, and a
  short forecast list.
- Stocks: show symbol, price, change, volume, day range, and a clear up/down
  indicator.
- Demo search: show the query, clearly mark results as demo data, and include
  title and snippet fields.
- Comparisons: use `Table` to place metrics side by side.

# Guardrails
- Do not expose environment variables or internal tool errors verbatim to users.
- Do not output HTML, markdown, or JSON when OpenUI Lang is expected.
- Keep button actions descriptive (`action:check_weather_tokyo`) so the client
  can map them to follow-up prompts.

```

### `agent/lib/openui-library.ts`

```ts
export {
  openuiChatLibrary,
  openuiChatPromptOptions,
} from "@openuidev/react-ui/genui-lib";

```

### `agent/skills/openui/references/frontend-wiring.md`

````md
# Frontend wiring

These files are reference implementations for a Next.js app that already hosts
an Eve agent. They mirror the official OpenUI
[`openui-chat`](https://github.com/thesysdev/openui/tree/main/examples/openui-chat)
example, but route turns through Eve's built-in HTTP channel instead of a custom
`/api/chat` route.

## Required app dependencies

```bash
npm install @openuidev/react-lang @openuidev/react-ui eve
```

## Next.js setup

1. Wrap `next.config.ts` with `withEve()` from `eve/next`.
2. Copy `openui-eve-chat.tsx` into your app, for example
   `app/_components/openui-eve-chat.tsx`.
3. Render it from `app/page.tsx`.

The component uses `useEveAgent()` for session streaming and `@openuidev/react-lang`
`Renderer` with `openuiChatLibrary` to turn assistant text into live UI.

## Accessibility notes

- The send form keeps a persistent label for the message input so keyboard and
  screen-reader users do not have to rely on placeholder text.
- The rendered assistant area uses `aria-live="polite"` and `aria-busy` while the
  stream is active, so updates from `Renderer` are announced without interrupting
  the current task.
- If your host app moves focus after submit or renders additional controls from
  OpenUI actions, keep focus management in the host component so new UI does not
  unexpectedly steal focus.

## Alternative: AgentInterface

For the full OpenUI chat chrome (history rail, tool-call cards, theming), follow
the upstream `openui-chat` page and point its `ChatLLM.send` handler at Eve's
session routes. The Eve + Renderer path in `openui-eve-chat.tsx` is the minimal
integration when you already manage chat state with `useEveAgent`.

## Smoke test

1. Start the Eve app with this agent installed.
2. Open the page that renders `OpenUIEveChat`.
3. Ask: `What's the weather in Tokyo?`
4. Confirm the assistant streams OpenUI Lang and the renderer shows cards instead
   of raw text.
5. Optional: ask for a demo search (for example `Search for Eve agents`) and
   confirm results are labeled as demo data, not live web results.

````

### `agent/skills/openui/references/openui-eve-chat.tsx`

```tsx
"use client";

import { Renderer } from "@openuidev/react-lang";
import { openuiChatLibrary } from "@openuidev/react-ui/genui-lib";
import type { EveMessage } from "eve/react";
import { useEveAgent } from "eve/react";
import { useMemo, useState } from "react";

function getLatestAssistantText(messages: readonly EveMessage[]): string {
  for (let index = messages.length - 1; index >= 0; index -= 1) {
    const message = messages[index];

    if (message?.role !== "assistant") {
      continue;
    }

    return message.parts
      .filter((part) => part.type === "text")
      .map((part) => part.text)
      .join("\n");
  }

  return "";
}

/**
 * Reference chat surface that streams Eve assistant text into the OpenUI
 * Renderer. Copy this component into a Next.js app that already uses
 * `withEve()` from `eve/next`.
 */
export function OpenUIEveChat() {
  const agent = useEveAgent();
  const [submitError, setSubmitError] = useState<string | null>(null);
  const isStreaming = agent.status === "streaming";
  const isBusy =
    agent.status === "submitted" || isStreaming;

  const assistantProgram = useMemo(
    () => getLatestAssistantText(agent.data.messages),
    [agent.data.messages],
  );

  return (
    <div className="mx-auto flex h-dvh max-w-3xl flex-col gap-4 p-4">
      <section
        aria-busy={isBusy}
        aria-live="polite"
        className="min-h-0 flex-1 overflow-auto rounded-xl border p-4"
      >
        {assistantProgram.length > 0 ? (
          <Renderer
            isStreaming={isStreaming}
            library={openuiChatLibrary}
            response={assistantProgram}
          />
        ) : (
          <p className="text-sm text-muted-foreground">
            Ask for weather, a stock quote, or a demo search to see generative UI.
          </p>
        )}
      </section>

      <form
        className="flex gap-2"
        onSubmit={async (event) => {
          event.preventDefault();
          const formElement = event.currentTarget;
          const form = new FormData(formElement);
          const message = String(form.get("message") ?? "").trim();

          if (message.length > 0) {
            setSubmitError(null);

            try {
              await agent.send({ message });
              formElement.reset();
            } catch (error) {
              const errorMessage =
                error instanceof Error ? error.message : "Failed to send message.";
              setSubmitError(errorMessage);
            }
          }
        }}
      >
        <label className="sr-only" htmlFor="message">
          Message
        </label>
        <input
          aria-describedby={submitError ? "message-error" : undefined}
          className="flex-1 rounded-md border px-3 py-2 text-sm"
          disabled={isBusy}
          id="message"
          name="message"
          placeholder="Show me the weather in Tokyo"
        />
        <button
          className="rounded-md bg-foreground px-4 py-2 text-sm text-background disabled:opacity-50"
          disabled={isBusy}
          type="submit"
        >
          Send
        </button>
      </form>
      {submitError ? (
        <p className="text-sm text-destructive" id="message-error" role="alert">
          {submitError}
        </p>
      ) : null}
    </div>
  );
}

```

### `agent/skills/openui/references/syntax-examples.md`

````md
# OpenUI Lang syntax examples

One statement per line: `identifier = Expression`. Write top-down: layout, nested
components, then leaf values. Positional arguments map to Zod prop order. Forward
references are allowed; the renderer shows placeholders until defined.

```text
root = Card([header, summary, metricsTable, followups])
header = CardHeader("Q4 Dashboard", "Revenue and user growth")
summary = TextContent("Revenue is up while user growth is steady.", "default")
metricsTable = Table([metricCol, valueCol, trendCol])
metricCol = Col("Metric", ["Revenue", "Users"])
valueCol = Col("Value", ["$1.2M", "450k"])
trendCol = Col("Trend", ["up", "flat"])
followups = FollowUpBlock([details])
details = FollowUpItem("Show the detailed breakdown")
```

````

### `agent/skills/openui/SKILL.md`

```md
---
name: openui
description: OpenUI Lang generative UI with openuiChatLibrary. Use when composing layouts, debugging parse/render failures, or extending the chat library.
---

# OpenUI Lang

Every assistant reply is an OpenUI Lang **program** whose first statement assigns
**root**. This agent uses `openuiChatLibrary` from `@openuidev/react-ui/genui-lib`
(the same library as the official
[`openui-chat`](https://github.com/thesysdev/openui/tree/main/examples/openui-chat)
example). The build-time system prompt comes from
`openuiChatLibrary.prompt(openuiChatPromptOptions)`.

## Chat library constraints

`openuiChatLibrary` centers on chat primitives: `Card`, `CardHeader`, `TextContent`,
`Table`, lists, and follow-ups. Do not use `Stack`; it belongs to the broader
OpenUI library, not this agent's chat library.

For a layout example, see [syntax-examples](./references/syntax-examples.md).

## Debugging checklist

1. Confirm the reply starts with `root = ...`
2. Confirm every referenced identifier is defined
3. Confirm tool-backed values match the latest tool JSON
4. Remove markdown fences or prose around the program
5. Re-run with a smaller layout if the stream was truncated

**Done when** each item is confirmed or ruled out and the reply is a valid OpenUI
Lang program starting with `root =`.

## Frontend integration

For Next.js + Eve wiring (dependencies, `withEve()`, `OpenUIEveChat`, accessibility,
smoke tests), see [frontend-wiring](./references/frontend-wiring.md).

## External references

- OpenUI docs: https://www.openui.com/docs/openui-lang/overview
- Language spec: https://www.openui.com/docs/openui-lang/specification
- Example app: https://github.com/thesysdev/openui/tree/main/examples/openui-chat

```

### `agent/tools/get_stock_price.ts`

```ts
import { defineTool } from "eve/tools";
import { z } from "zod";

const knownPrices: Record<string, number> = {
  AAPL: 189.84,
  AMZN: 178.25,
  GOOGL: 141.8,
  META: 485.58,
  MSFT: 378.91,
  NVDA: 875.28,
  TSLA: 248.42,
};

const getStockPriceInput = z.object({
  symbol: z
    .string()
    .min(1)
    .describe("Ticker symbol, for example AAPL or NVDA"),
});

export type GetStockPriceInput = z.infer<typeof getStockPriceInput>;

export type GetStockPriceOutput = {
  change: number;
  change_percent: number;
  day_high: number;
  day_low: number;
  price: number;
  symbol: string;
  volume: string;
};

function resolveBasePrice(symbol: string): number {
  const known = knownPrices[symbol];

  if (known !== undefined) {
    return known;
  }

  let hash = 0;

  for (const character of symbol) {
    hash = (hash + character.charCodeAt(0)) % 480;
  }

  return 20 + hash;
}

export default defineTool({
  description:
    "Get the latest stock quote for a ticker symbol. Use before rendering price or market cards.",
  inputSchema: getStockPriceInput,
  execute({ symbol }): GetStockPriceOutput {
    const normalizedSymbol = symbol.trim().toUpperCase();
    const basePrice = resolveBasePrice(normalizedSymbol);
    const change = Number.parseFloat(
      ((normalizedSymbol.length % 8) - 4).toFixed(2),
    );
    const price = Number.parseFloat((basePrice + change).toFixed(2));
    const changePercent = Number.parseFloat(
      ((change / basePrice) * 100).toFixed(2),
    );
    const volumeMillions = 10 + (normalizedSymbol.length % 50);

    return {
      symbol: normalizedSymbol,
      price,
      change,
      change_percent: changePercent,
      volume: `${volumeMillions.toFixed(1)}M`,
      day_high: Number.parseFloat(
        (price + Math.abs(change) + 1.5).toFixed(2),
      ),
      day_low: Number.parseFloat(
        (price - Math.abs(change) - 1.2).toFixed(2),
      ),
    };
  },
  toModelOutput(output) {
    return {
      type: "json",
      value: output,
    };
  },
});

```

### `agent/tools/get_weather.ts`

```ts
import { defineTool } from "eve/tools";
import { z } from "zod";

const knownTemperaturesCelsius: Record<string, number> = {
  berlin: 16,
  london: 14,
  mumbai: 33,
  "new york": 25,
  paris: 19,
  "san francisco": 18,
  sydney: 27,
  tokyo: 22,
};

const weatherConditions = [
  "Clear Skies",
  "Cloudy",
  "Light Rain",
  "Partly Cloudy",
  "Sunny",
] as const;

const getWeatherInput = z.object({
  location: z.string().min(1).describe("City name, for example Tokyo"),
});

export type GetWeatherInput = z.infer<typeof getWeatherInput>;

export type GetWeatherOutput = {
  condition: string;
  forecast: Array<{
    condition: string;
    day: string;
    high: number;
    low: number;
  }>;
  humidity_percent: number;
  location: string;
  temperature_celsius: number;
  temperature_fahrenheit: number;
  wind_speed_kmh: number;
};

function pickCondition(index: number): string {
  return weatherConditions[index % weatherConditions.length] ?? "Partly Cloudy";
}

function resolveTemperatureCelsius(location: string): number {
  const normalized = location.trim().toLowerCase();
  const known = knownTemperaturesCelsius[normalized];

  if (known !== undefined) {
    return known;
  }

  let hash = 0;

  for (const character of normalized) {
    hash = (hash + character.charCodeAt(0)) % 30;
  }

  return 5 + hash;
}

export default defineTool({
  description:
    "Get current weather and a short forecast for a city. Use before rendering weather cards or charts.",
  inputSchema: getWeatherInput,
  execute({ location }): GetWeatherOutput {
    const temperatureCelsius = resolveTemperatureCelsius(location);
    const condition = pickCondition(location.length);

    return {
      location,
      temperature_celsius: temperatureCelsius,
      temperature_fahrenheit: Math.round(temperatureCelsius * 1.8 + 32),
      condition,
      humidity_percent: 40 + (location.length % 40),
      wind_speed_kmh: 5 + (location.length % 25),
      forecast: [
        {
          day: "Tomorrow",
          high: temperatureCelsius + 2,
          low: temperatureCelsius - 4,
          condition: "Partly Cloudy",
        },
        {
          day: "Day After",
          high: temperatureCelsius + 1,
          low: temperatureCelsius - 3,
          condition: "Sunny",
        },
      ],
    };
  },
  toModelOutput(output) {
    return {
      type: "json",
      value: output,
    };
  },
});

```

### `agent/tools/search_web.ts`

```ts
import { defineTool } from "eve/tools";
import { z } from "zod";

const searchWebInput = z.object({
  query: z.string().min(1).describe("Search query"),
});

export type SearchWebInput = z.infer<typeof searchWebInput>;

export type SearchWebOutput = {
  kind: "mock-search-results";
  query: string;
  results: Array<{
    snippet: string;
    title: string;
  }>;
};

export default defineTool({
  description:
    "Return deterministic mock search results for OpenUI demo layouts. This does not perform live web search and must be labeled as demo data.",
  inputSchema: searchWebInput,
  execute({ query }): SearchWebOutput {
    return {
      kind: "mock-search-results",
      query,
      results: [
        {
          title: `Demo result for "${query}"`,
          snippet: `Example overview card for ${query}. This is mock data for layout testing.`,
        },
        {
          title: `${query} - Demo trend`,
          snippet: `Example trend snippet for ${query}. Do not treat this as live research.`,
        },
        {
          title: `Understanding ${query}`,
          snippet: `Example explainer snippet for ${query}, included for demo UI rendering.`,
        },
      ],
    };
  },
  toModelOutput(output) {
    return {
      type: "json",
      value: output,
    };
  },
});

```

### `evals/evals.config.ts`

```ts
import { defineEvalConfig } from "eve/evals";

export default defineEvalConfig({
  timeoutMs: 120_000,
});

```

### `evals/openui-format-contract.eval.ts`

````ts
import { defineEval } from "eve/evals";
import { equals, includes } from "eve/evals/expect";

export default defineEval({
  description:
    "Answers a greeting with a pure OpenUI Lang welcome card: assigns root first, uses chat-library components, and emits no markdown fences, prose, or forbidden Stack component.",
  async test(t) {
    await t.send("Hi! What can you do?");

    t.succeeded();
    t.noFailedActions();

    const reply = t.reply ?? "";
    t.check(reply.trimStart().startsWith("root ="), equals(true).gate());
    t.check(reply.includes("```"), equals(false).gate());
    t.check(/\bStack\s*\(/.test(reply), equals(false).gate());
    t.check(reply, includes("Card").soft());
    t.check(reply, includes("FollowUp").soft());
  },
});

````

### `evals/stock-quote-exact-price.eval.ts`

````ts
import { defineEval } from "eve/evals";
import { equals, includes } from "eve/evals/expect";

export default defineEval({
  description:
    "Calls get_stock_price for NVDA and renders the exact deterministic quote instead of inventing market numbers.",
  async test(t) {
    await t.send("Show me the current NVDA stock price as a market card.");

    t.succeeded();
    t.noFailedActions();
    t.calledTool("get_stock_price").gate();

    const reply = t.reply ?? "";
    t.check(reply.trimStart().startsWith("root ="), equals(true).gate());
    t.check(reply.includes("```"), equals(false).gate());
    t.check(reply, includes("875.28").gate());
  },
});

````

### `evals/weather-card-uses-tool-data.eval.ts`

````ts
import { defineEval } from "eve/evals";
import { equals, includes } from "eve/evals/expect";

export default defineEval({
  description:
    "Calls get_weather before rendering a weather card and answers in OpenUI Lang only, with values taken from the deterministic tool output.",
  async test(t) {
    await t.send(
      "What's the weather in Tokyo right now? Render it as a weather card.",
    );

    t.succeeded();
    t.noFailedActions();
    t.calledTool("get_weather").gate();

    const reply = t.reply ?? "";
    t.check(reply.trimStart().startsWith("root ="), equals(true).gate());
    t.check(reply.includes("```"), equals(false).gate());
    t.check(reply, includes("22").soft());
    t.check(reply, includes("Clear Skies").soft());
  },
});

````

### `agent/README.md`

````md
# OpenUI Assistant

An Eve agent that responds with **OpenUI Lang** and renders rich generative UI
through the official `openuiChatLibrary`. It follows the
[`openui-chat`](https://github.com/thesysdev/openui/tree/main/examples/openui-chat)
reference app: the model streams structured UI instead of markdown, and a React
`Renderer` turns that stream into live components.

## What it does

1. **Injects the OpenUI system prompt** - `agent/instructions/openui-prompt.ts`
   calls `openuiChatLibrary.prompt(openuiChatPromptOptions)` at build time, the
   same library and prompt options used by the GitHub example.
2. **Answers with OpenUI Lang only** - cards, grids, charts, tables, and buttons
   instead of plain text replies.
3. **Calls demo data tools** - `get_weather`, `get_stock_price`, and `search_web`
   mirror the sample tools from `examples/openui-chat/src/app/api/chat/route.ts`;
   `search_web` is mock demo data, not live search.
4. **Ships a frontend reference** -
   `agent/skills/openui/references/openui-eve-chat.tsx` shows how to connect
   `useEveAgent()` to `@openuidev/react-lang` `Renderer`.

## Installation

```bash
npx shadcn@latest add @evex/openui-assistant
```

Install the registry dependencies in your Eve app if they are not already present:

```bash
npm install @openuidev/react-ui @openuidev/react-lang eve just-bash zod
```

## Configuration

This agent does not require agent-specific environment variables. The host Eve
app still needs a model credential (`AI_GATEWAY_API_KEY`, `VERCEL_OIDC_TOKEN`, or
your provider's equivalent).

See `.env.example` for optional notes.

## Usage

Ask for structured UI backed by the demo tools:

```text
Show me the weather in Tokyo with a short forecast.
```

```text
Quote NVDA and render the move as a dashboard card.
```

```text
Show demo search results for "generative UI frameworks".
```

The assistant should return OpenUI Lang starting with `root = ...` and use tool
results for factual values. Search results are labeled as demo data because the
bundled search tool is deterministic and does not call a live provider.

## Web UI setup

To render generative UI in the browser:

1. Ensure your Next.js app uses `withEve()` from `eve/next`.
2. Copy `agent/skills/openui/references/openui-eve-chat.tsx` into your app
   components folder.
3. Render `<OpenUIEveChat />` on a page.

See `agent/skills/openui/references/frontend-wiring.md` for the full wiring
guide and an `AgentInterface` alternative that matches the upstream OpenUI chat
shell.

## Smoke test

1. Install the agent into an Eve app and run `pnpm dev` (or `eve dev`).
2. In the terminal UI or your web chat, send:

   ```text
   What's the weather in San Francisco?
   ```

3. Confirm the agent calls `get_weather`, then streams OpenUI Lang with weather
   cards rather than markdown prose.
4. If you mounted the frontend reference, confirm `Renderer` paints the UI while
   the stream is active.

## Troubleshooting

- **Plain text instead of UI** - the model ignored the OpenUI contract. Reload the
  `openui` skill and retry with an explicit layout request.
- **Parser errors in the browser** - an assistant reply included markdown fences or
  prose around the program. The agent should output raw OpenUI Lang only.
- **Missing tool data** - verify the turn includes a `get_weather`,
  `get_stock_price`, or `search_web` tool call before the UI references numbers.
- **Renderer shows nothing** - confirm `@openuidev/react-lang` and
  `@openuidev/react-ui` are installed in the host app and that you passed
  `openuiChatLibrary` to `Renderer`.

## Development

```bash
pnpm install
pnpm --dir registry/openui-assistant typecheck
pnpm --dir registry/openui-assistant build
pnpm --dir registry/openui-assistant check
```

The package `tsconfig.json` includes the `.tsx` frontend reference under
`agent/skills/openui/references/`, plus DOM and React types, so `typecheck`
validates the shipped reference component as well as the Eve agent source.

After editing `registry.json`, regenerate the embedded catalog:

```bash
pnpm --filter @evex/agent-registry generate
```

````

### `.env.example`

```
# No environment variables are required for the bundled demo tools.

# Optional: set a model gateway credential in the host Eve app if your deployment
# does not already provide one through Vercel OIDC or AI Gateway.
# AI_GATEWAY_API_KEY=

```
