import { Suspense, useCallback, useEffect, useRef, useState, type ReactNode } from "react"; import { useAgent } from "agents/react"; import { useAgentChat } from "@cloudflare/ai-chat/react"; import { isToolUIPart, getToolName } from "ai"; import { Button, Surface, Text, InputArea, Empty, Badge } from "@cloudflare/kumo"; import { PaperPlaneRightIcon, TrashIcon, GearIcon, WrenchIcon, GlobeIcon, MonitorIcon, CheckCircleIcon, XCircleIcon, LightningIcon, ShieldCheckIcon, BrowserIcon } from "@phosphor-icons/react"; import { Streamdown } from "streamdown"; import { DemoWrapper } from "../../layout"; import { ConnectionStatus, CodeExplanation, type CodeSection } from "../../components"; import { useUserId } from "../../hooks"; const codeSections: CodeSection[] = [ { title: "Define tools for the AI to call", description: "Use the AI SDK's tool() function to define tools with Zod schemas. Tools can run server-side (in the agent) or client-side (in the browser) via executions.", code: `import { tool } from "ai"; import { z } from "zod"; const tools = { getWeather: tool({ description: "Get the current weather for a location", parameters: z.object({ city: z.string().describe("City name"), }), execute: async ({ city }) => { return { temperature: 72, condition: "sunny", city }; }, }), };` }, { title: "Client-side tool execution", description: "Some tools need to run in the browser — accessing the DOM, camera, or user interactions. Mark them with executions and handle them on the client with useAgentChat.", code: `const { messages, addToolResult } = useAgentChat(agent, { // Handle tool calls that need client-side execution onToolCall: async ({ toolCall }) => { if (toolCall.toolName === "getUserLocation") { const position = await navigator.geolocation.getCurrentPosition(); return { lat: position.coords.latitude, lng: position.coords.longitude }; } }, });` } ]; const TOOL_META: Record< string, { icon: ReactNode; label: string; type: string } > = { getWeather: { icon: , label: "getWeather", type: "Server" }, rollDice: { icon: , label: "rollDice", type: "Server" }, getUserTimezone: { icon: , label: "getUserTimezone", type: "Client" }, getScreenSize: { icon: , label: "getScreenSize", type: "Client" }, calculate: { icon: , label: "calculate", type: "Approval" }, deleteFile: { icon: , label: "deleteFile", type: "Approval" } }; function typeBadgeVariant( type: string ): "secondary" | "primary" | "destructive" { if (type === "Server") return "secondary"; if (type === "Client") return "primary"; return "destructive"; } function MessageBubble({ align, variant, children }: { align: "left" | "right"; variant: "user" | "assistant"; children: ReactNode; }) { const base = "max-w-[80%] rounded-2xl overflow-hidden"; const userStyle = `${base} rounded-br-md bg-kumo-contrast text-kumo-inverse`; const assistantStyle = `${base} rounded-bl-md ring ring-kumo-line`; return (
{variant === "user" ? (
{children}
) : ( {children} )}
); } function ToolCard({ toolName, state, input: toolInput, output, approvalId, onApprove, onReject }: { toolName: string; state: string; input?: unknown; output?: unknown; approvalId?: string; onApprove?: (id: string) => void; onReject?: (id: string) => void; }) { const meta = TOOL_META[toolName] ?? { icon: , label: toolName, type: "Unknown" }; const isApproval = state === "approval-requested"; const isDone = state === "output-available"; const isDenied = state === "output-denied"; const isError = state === "output-error"; const isRunning = state === "input-available" || state === "input-streaming"; return (
{meta.icon} {meta.label} {meta.type} {isDone && ( Done )} {isRunning && ( Running )} {isApproval && Needs Approval} {isDenied && ( Denied )} {isError && ( Error )}
{toolInput != null && (
            {JSON.stringify(toolInput, null, 2)}
          
)} {isDone && output != null && (
            {JSON.stringify(output, null, 2)}
          
)} {isApproval && approvalId && onApprove && onReject && (
)}
); } function ToolsUI() { const userId = useUserId(); const [connectionStatus, setConnectionStatus] = useState< "connected" | "connecting" | "disconnected" >("connecting"); const [input, setInput] = useState(""); const messagesContainerRef = useRef(null); const agent = useAgent({ agent: "ToolsAgent", name: `tools-demo-${userId}`, onOpen: useCallback(() => setConnectionStatus("connected"), []), onClose: useCallback(() => setConnectionStatus("disconnected"), []), onError: useCallback(() => setConnectionStatus("disconnected"), []) }); const { messages, sendMessage, clearHistory, addToolApprovalResponse, status } = useAgentChat({ agent, onToolCall: async ({ toolCall, addToolOutput }) => { if (toolCall.toolName === "getUserTimezone") { addToolOutput({ toolCallId: toolCall.toolCallId, output: { timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, localTime: new Date().toLocaleTimeString() } }); } if (toolCall.toolName === "getScreenSize") { addToolOutput({ toolCallId: toolCall.toolCallId, output: { width: window.innerWidth, height: window.innerHeight, devicePixelRatio: window.devicePixelRatio } }); } } }); const isStreaming = status === "streaming"; const isConnected = connectionStatus === "connected"; useEffect(() => { const el = messagesContainerRef.current; if (el) el.scrollTop = el.scrollHeight; }, [messages]); const send = useCallback(() => { const text = input.trim(); if (!text || isStreaming) return; setInput(""); sendMessage({ role: "user", parts: [{ type: "text", text }] }); }, [input, isStreaming, sendMessage]); const handleApprove = useCallback( (id: string) => addToolApprovalResponse({ id, approved: true }), [addToolApprovalResponse] ); const handleReject = useCallback( (id: string) => addToolApprovalResponse({ id, approved: false }), [addToolApprovalResponse] ); return ( AI agents can use tools — functions the model calls during a conversation. Tools can run server-side (inside the agent), client-side (in the browser, e.g. geolocation), or require human approval before executing. Define them with Zod schemas for type-safe argument validation. } statusIndicator={} >
{/* Tool legend */}
Server Auto-executed on server
Client Runs in your browser
Approval Needs your confirmation
{/* Messages area */}
{messages.length === 0 && ( } title="Try the tools" description={ 'Try "What\'s the weather in Tokyo?", "Roll 3d20", ' + '"What timezone am I in?", "What\'s my screen size?", ' + '"What is 42 * 38?", "What is 5000 + 3000?", or "Delete /tmp/old.log"' } /> )} {messages.map((message, index) => { const isUser = message.role === "user"; const isLastAssistant = message.role === "assistant" && index === messages.length - 1; return (
{message.parts.map((part, partIdx) => { if (part.type === "text") { if (!part.text || part.text.trim() === "") return null; if (isUser) { return ( {part.text} ); } return ( {part.text} ); } if (part.type === "reasoning") { if (!part.text || part.text.trim() === "") return null; return (
Thinking: {part.text}
); } if (isToolUIPart(part)) { const toolName = getToolName(part); const approvalId = "approval" in part ? (part.approval as { id?: string })?.id : undefined; return ( ); } return null; })}
); })}
{/* Input area */}
{ e.preventDefault(); send(); }} >
{ if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } }} placeholder={ isConnected ? 'Try "What\'s the weather in Tokyo?" or "What is 5000 + 3000?"' : "Connecting to agent..." } disabled={!isConnected || isStreaming} rows={2} className="flex-1 ring-0! focus:ring-0! shadow-none! bg-transparent! outline-none!" />
); } export function ToolsDemo() { return (
Loading tools demo...
} >
); }