Tool UI is in active development, and APIs are still coalescing as we refine discover better patterns for conversation-native UIs.
The assistant calls a tool, the tool returns JSON matching a schema, and the UI renders inline.
import { streamText, tool, convertToModelMessages } from "ai"; import { openai } from "@ai-sdk/openai"; import { z } from "zod"; import { SerializableLinkPreviewSchema } from "@/components/tool-ui/link-preview/schema"; export async function POST(req: Request) { const { messages } = await req.json(); const result = streamText({ model: openai("gpt-4o"), messages: convertToModelMessages(messages), tools: { previewLink: tool({ description: "Show a preview card for a URL", inputSchema: z.object({ url: z.url() }), outputSchema: SerializableLinkPreviewSchema, async execute({ url }) { return { id: "link-preview-1", href: url, title: "Example Site", description: "A description of the linked content", image: "https://example.com/image.jpg", }; }, }), }, }); return result.toUIMessageStreamResponse(); }
"use client"; import { AssistantRuntimeProvider, makeAssistantToolUI, } from "@assistant-ui/react"; import { useChatRuntime, AssistantChatTransport, } from "@assistant-ui/react-ai-sdk"; import { LinkPreview, parseSerializableLinkPreview, } from "@/components/tool-ui/link-preview"; const PreviewLinkUI = makeAssistantToolUI({ toolName: "previewLink", render: ({ result }) => { const preview = parseSerializableLinkPreview(result); return <LinkPreview {...preview} maxWidth="420px" />; }, }); export default function App() { const runtime = useChatRuntime({ transport: new AssistantChatTransport({ api: "/api/chat" }), }); return ( <AssistantRuntimeProvider runtime={runtime}> <PreviewLinkUI /> {/* your <Thread /> component here */} </AssistantRuntimeProvider> ); }
Every Tool UI surface is addressable and reconstructable. Here's the common schema structure:
{ id: string; // stable identifier for this rendering role: "information"|"decision"|"control"|"state"|"composite"; actions?: Array<{ id: string; label: string; sentence: string }>; // optional receipt after side effects: receipt?: { outcome: "success"|"partial"|"failed"|"cancelled"; summary: string; identifiers?: Record<string, string>; at: string; // ISO timestamp }; }