Agent chat guide
Build a streaming agent chat with tool calls, reasoning, approvals and sources on the Vercel AI SDK, from free components.
By the end you have a chat that streams answers word by word, shows each tool the agent runs, folds its reasoning away, asks before it does anything risky and cites what it read. Every component here is free and MIT, and installs without an account.
Build itStart here
Install the AI SDK and the components
In a Next.js app (or start one), add the AI SDK, a model provider and zod:
1npm i ai @ai-sdk/react @ai-sdk/anthropic zodThen add the six primitives straight from their registry URLs:
$pnpm dlx shadcn@latest add https://nextui.dev/r/conversation-shell.json https://nextui.dev/r/streaming-message.json https://nextui.dev/r/tool-call.json https://nextui.dev/r/reasoning.json https://nextui.dev/r/tool-approval.json https://nextui.dev/r/sources.jsonThey land in
components/nextui/, so the code is yours to change.Stream from a route
One route handler streams the model's reply and runs its tools. Put your provider key in
.env.local(for Anthropic,ANTHROPIC_API_KEY).1import { anthropic } from "@ai-sdk/anthropic";2import { convertToModelMessages, stepCountIs, streamText, tool, type UIMessage } from "ai";3import { z } from "zod";45export async function POST(req: Request) {6const { messages }: { messages: UIMessage[] } = await req.json();78const result = streamText({9// Any AI SDK model works here; swap the provider and model id for yours.10model: anthropic("your-model-id"),11system: "You are a helpful agent. Use tools when they help.",12messages: await convertToModelMessages(messages),13stopWhen: stepCountIs(5),14tools: {15get_weather: tool({16description: "Current weather for a city",17inputSchema: z.object({ city: z.string() }),18execute: async ({ city }) => ({ city, tempC: 29, sky: "clear" }),19}),20},21});2223// Reasoning and sources reach the browser only when you ask for them.24return result.toUIMessageStreamResponse({ sendReasoning: true, sendSources: true });25}Render the chat
useChatkeeps the messages;ConversationShellgives you the frame, the composer and the scroll that follows new text. Each message part maps to a primitive: text toStreamingMessage, tool parts toToolCall.1"use client";23import { useChat } from "@ai-sdk/react";4import { ConversationShell } from "@/components/nextui/conversation-shell";5import { StreamingMessage } from "@/components/nextui/streaming-message";6import { ToolCall } from "@/components/nextui/tool-call";78export default function ChatPage() {9// Posts to /api/chat by default.10const { messages, sendMessage, status, stop } = useChat();1112return (13<ConversationShell14className="h-dvh"15title="New chat"16suggestions={["What's the weather in Hanoi?", "Plan my week"]}17busy={status === "submitted" || status === "streaming"}18onStop={stop}19onSend={(text) => sendMessage({ text })}20messages={messages.map((m) => ({21id: m.id,22role: m.role === "user" ? "user" : "assistant",23content: m.parts.map((part, i) =>24part.type === "text" ? (25m.role === "user" ? part.text : <StreamingMessage key={i} part={part} />26) : part.type.startsWith("tool-") ? (27<ToolCall key={i} part={part as never} />28) : null,29),30}))}31/>32);33}Run
npm run dev, open/chatand ask about the weather: the tool card shows its arguments streaming in, then the result.
Show the model thinking
With a model that reasons and sendReasoning: true on the route, reasoning arrives as its own parts. Reasoning shows it live with a timer, then folds it into "Thought for Ns" so the answer stays in focus.
1import { Reasoning } from "@/components/nextui/reasoning";23// Inside the parts map, before the text branch:4part.type === "reasoning" ? (5<Reasoning key={i} part={part} />6) : part.type === "text" ? (7// ...as before
Ask before acting
For anything hard to undo (sending, paying, deleting), mark the tool as needing approval. The run pauses and the browser gets a part in the approval-requested state.
1send_email: tool({2description: "Send an email from the user's account",3inputSchema: z.object({ to: z.string(), subject: z.string(), body: z.string() }),4// Pauses the run until the user answers in the browser.5// (AI SDK 7 names this option toolApproval.)6needsApproval: true,7execute: async (email) => sendEmail(email),8}),
Answer it with ToolApproval, which shows the exact arguments, so people approve what will actually run:
1import { ToolApproval } from "@/components/nextui/tool-approval";23const { messages, sendMessage, status, stop, addToolApprovalResponse } = useChat();45// Inside the parts map, before the generic tool branch:6part.type === "tool-send_email" && part.state === "approval-requested" ? (7<ToolApproval8key={i}9title="Send this email?"10toolName="send_email"11details={{ To: part.input.to, Subject: part.input.subject }}12onApprove={() => addToolApprovalResponse({ id: part.approval.id, approved: true })}13onDeny={() => addToolApprovalResponse({ id: part.approval.id, approved: false })}14/>15) : part.type.startsWith("tool-") ? (16// ...as before
Cite your sources
When the agent searches or reads pages, send them with sendSources: true and render the answer with Sources: [1] markers become chips that show the page on hover.
1import type { UIMessage } from "ai";2import { Sources } from "@/components/nextui/sources";34// Ask the model to cite with [1], [2] in the order of the sources it used.5function Answer({ message }: { message: UIMessage }) {6const text = message.parts.flatMap((p) => (p.type === "text" ? [p.text] : [])).join("");7const sources = message.parts.flatMap((p) => (p.type === "source-url" ? [{ url: p.url, title: p.title }] : []));8return sources.length ? <Sources answer={text} sources={sources} /> : null;9}
Go further
- Agent starter: this whole guide as a Next.js app, with every primitive wired in. It runs a demo model until you add a key.
- Agent templates: research, support and coding apps built from the same parts.
- All agent components: timelines, artifact panels, model pickers, context meters, sub-agents and more.
- Prefer to ask your editor? Set up MCP and tell Claude Code or Cursor to "add the nextui tool-call to my chat".