import "./styles.css"; import { createRoot } from "react-dom/client"; import { Suspense, useCallback, useEffect, useRef, useState } from "react"; import type { AgentCard, Message, Task, TaskStatusUpdateEvent } from "@a2a-js/sdk"; import { Button, Badge, Empty, InputArea, Surface, Text } from "@cloudflare/kumo"; import { ModeToggle, PoweredByAgents } from "@cloudflare/agents-ui"; import { ThemeProvider } from "@cloudflare/agents-ui/hooks"; import { InfoIcon, PaperPlaneRightIcon, RobotIcon, SpinnerIcon } from "@phosphor-icons/react"; // -- Lightweight A2A client using fetch (no SDK bundling needed) -- type A2AEvent = Message | Task | TaskStatusUpdateEvent; async function fetchAgentCard(baseUrl: string): Promise { const res = await fetch(`${baseUrl}/.well-known/agent-card.json`); if (!res.ok) throw new Error(`Failed to fetch agent card: ${res.status}`); return res.json(); } async function sendMessage( url: string, message: Message ): Promise { const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", id: crypto.randomUUID(), method: "message/send", params: { message } }) }); const json = (await res.json()) as { error?: { message: string }; result: Message | Task; }; if (json.error) throw new Error(json.error.message); return json.result; } async function* streamMessage( url: string, message: Message ): AsyncGenerator { const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", id: crypto.randomUUID(), method: "message/stream", params: { message } }) }); if (!res.ok) throw new Error(`Stream request failed: ${res.status}`); const reader = res.body!.getReader(); const decoder = new TextDecoder(); let buffer = ""; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const parts = buffer.split("\n\n"); buffer = parts.pop()!; for (const part of parts) { for (const line of part.split("\n")) { if (line.startsWith("data: ")) { const data = JSON.parse(line.slice(6)); if (data.result) yield data.result as A2AEvent; } } } } } // -- UI types -- interface ChatEntry { id: string; role: "user" | "agent"; text: string; taskId?: string; status?: string; } // -- Components -- function getTextFromMessage(msg: Message): string { return msg.parts .filter((p) => p.kind === "text") .map((p) => (p as { kind: "text"; text: string }).text) .join(""); } function StatusBadge({ state }: { state: string }) { const variant = state === "completed" ? "primary" : state === "failed" || state === "canceled" ? "destructive" : "secondary"; return {state}; } function Chat() { const [agentCard, setAgentCard] = useState(null); const [messages, setMessages] = useState([]); const [input, setInput] = useState(""); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const messagesEndRef = useRef(null); const contextIdRef = useRef(crypto.randomUUID()); // Discover agent on mount useEffect(() => { fetchAgentCard(window.location.origin) .then(setAgentCard) .catch((err) => setError(`Agent discovery failed: ${err.message}`)); }, []); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); const send = useCallback(async () => { const text = input.trim(); if (!text || isLoading || !agentCard) return; setInput(""); setError(null); const userEntry: ChatEntry = { id: crypto.randomUUID(), role: "user", text }; setMessages((prev) => [...prev, userEntry]); setIsLoading(true); const userMessage: Message = { contextId: contextIdRef.current, kind: "message", messageId: crypto.randomUUID(), parts: [{ kind: "text", text }], role: "user" }; try { if (agentCard.capabilities.streaming) { // Use streaming let agentText = ""; let taskStatus = ""; let taskId = ""; const agentEntryId = crypto.randomUUID(); // Add placeholder setMessages((prev) => [ ...prev, { id: agentEntryId, role: "agent", text: "", status: "working" } ]); for await (const event of streamMessage(agentCard.url, userMessage)) { if (event.kind === "message" && (event as Message).role === "agent") { agentText = getTextFromMessage(event as Message); taskId = (event as Message).taskId || taskId; } else if (event.kind === "status-update") { const update = event as TaskStatusUpdateEvent; taskStatus = update.status.state; taskId = update.taskId || taskId; if (update.status.message) { agentText = getTextFromMessage(update.status.message); } } else if (event.kind === "task") { taskId = (event as Task).id; taskStatus = (event as Task).status.state; } setMessages((prev) => prev.map((m) => m.id === agentEntryId ? { ...m, text: agentText, status: taskStatus, taskId } : m ) ); } } else { // Non-streaming fallback const result = await sendMessage(agentCard.url, userMessage); let agentText = ""; let taskId = ""; let status = ""; if (result.kind === "message") { agentText = getTextFromMessage(result); } else if (result.kind === "task") { const task = result as Task; taskId = task.id; status = task.status.state; if (task.status.message) { agentText = getTextFromMessage(task.status.message); } } setMessages((prev) => [ ...prev, { id: crypto.randomUUID(), role: "agent", text: agentText, taskId, status } ]); } } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setIsLoading(false); } }, [input, isLoading, agentCard]); return (
{/* Header */}

{agentCard?.name || "A2A Agent"}

A2A Protocol
{/* Messages */}
{/* Explainer */}
Agent-to-Agent (A2A) Protocol This demo exposes a Cloudflare Agent as an A2A-compliant server. The browser acts as an A2A client, discovering the agent via its Agent Card and communicating over JSON-RPC with SSE streaming. Any A2A client can connect at{" "} /.well-known/agent-card.json
{/* Agent card info */} {agentCard && (
v{agentCard.protocolVersion} {agentCard.capabilities.streaming && ( Streaming )} {agentCard.skills?.map((skill) => ( {skill.name} ))} {agentCard.url}
)} {messages.length === 0 && ( } title="Start a conversation" description='This AI agent communicates via the A2A protocol. Try "Explain A2A in simple terms" or "Write a haiku about cloud computing"' /> )} {messages.map((entry) => { if (entry.role === "user") { return (
{entry.text}
); } return (
{entry.text ? (
{entry.text} {isLoading && entry.status === "working" && ( )}
) : (
Thinking...
)}
{entry.status && (
{entry.taskId && ( {entry.taskId} )}
)}
); })} {error && ( {error} )}
{/* Input */}
{ e.preventDefault(); send(); }} className="max-w-3xl mx-auto px-5 py-4" >
{ if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } }} placeholder={ agentCard ? "Send a message via A2A protocol..." : "Discovering agent..." } disabled={!agentCard || isLoading} rows={2} className="flex-1 !ring-0 focus:!ring-0 !shadow-none !bg-transparent !outline-none" />
); } function App() { return ( Loading...
} > ); } const root = document.getElementById("root")!; createRoot(root).render();