Agent chat guide

Build a streaming agent chat with tool calls, reasoning, approvals and sources on the Vercel AI SDK, from free components.

Conversation shell · free, MIT

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

  1. 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 zod

    Then 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.json

    They land in components/nextui/, so the code is yours to change.

  2. 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";
    4
    5export async function POST(req: Request) {
    6 const { messages }: { messages: UIMessage[] } = await req.json();
    7
    8 const result = streamText({
    9 // Any AI SDK model works here; swap the provider and model id for yours.
    10 model: anthropic("your-model-id"),
    11 system: "You are a helpful agent. Use tools when they help.",
    12 messages: await convertToModelMessages(messages),
    13 stopWhen: stepCountIs(5),
    14 tools: {
    15 get_weather: tool({
    16 description: "Current weather for a city",
    17 inputSchema: z.object({ city: z.string() }),
    18 execute: async ({ city }) => ({ city, tempC: 29, sky: "clear" }),
    19 }),
    20 },
    21 });
    22
    23 // Reasoning and sources reach the browser only when you ask for them.
    24 return result.toUIMessageStreamResponse({ sendReasoning: true, sendSources: true });
    25}
  3. Render the chat

    useChat keeps the messages; ConversationShell gives you the frame, the composer and the scroll that follows new text. Each message part maps to a primitive: text to StreamingMessage, tool parts to ToolCall.

    1"use client";
    2
    3import { 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";
    7
    8export default function ChatPage() {
    9 // Posts to /api/chat by default.
    10 const { messages, sendMessage, status, stop } = useChat();
    11
    12 return (
    13 <ConversationShell
    14 className="h-dvh"
    15 title="New chat"
    16 suggestions={["What's the weather in Hanoi?", "Plan my week"]}
    17 busy={status === "submitted" || status === "streaming"}
    18 onStop={stop}
    19 onSend={(text) => sendMessage({ text })}
    20 messages={messages.map((m) => ({
    21 id: m.id,
    22 role: m.role === "user" ? "user" : "assistant",
    23 content: m.parts.map((part, i) =>
    24 part.type === "text" ? (
    25 m.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 /chat and 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";
2
3// Inside the parts map, before the text branch:
4part.type === "reasoning" ? (
5 <Reasoning key={i} part={part} />
6) : part.type === "text" ? (
7 // ...as before
Reasoning · free, MIT

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({
2 description: "Send an email from the user's account",
3 inputSchema: 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.)
6 needsApproval: true,
7 execute: 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";
2
3const { messages, sendMessage, status, stop, addToolApprovalResponse } = useChat();
4
5// Inside the parts map, before the generic tool branch:
6part.type === "tool-send_email" && part.state === "approval-requested" ? (
7 <ToolApproval
8 key={i}
9 title="Send this email?"
10 toolName="send_email"
11 details={{ To: part.input.to, Subject: part.input.subject }}
12 onApprove={() => addToolApprovalResponse({ id: part.approval.id, approved: true })}
13 onDeny={() => addToolApprovalResponse({ id: part.approval.id, approved: false })}
14 />
15) : part.type.startsWith("tool-") ? (
16 // ...as before
Tool approval · free, MIT

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";
3
4// Ask the model to cite with [1], [2] in the order of the sources it used.
5function Answer({ message }: { message: UIMessage }) {
6 const text = message.parts.flatMap((p) => (p.type === "text" ? [p.text] : [])).join("");
7 const sources = message.parts.flatMap((p) => (p.type === "source-url" ? [{ url: p.url, title: p.title }] : []));
8 return 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".