/* ┌──────────────────────────────────────────────────────────────────────────────┐ │ @author: Davidson Gomes │ │ @file: /app/documentation/page.tsx │ │ Developed by: Davidson Gomes │ │ Creation date: May 13, 2025 │ │ Contact: contato@evolution-api.com │ ├──────────────────────────────────────────────────────────────────────────────┤ │ @copyright © Evolution API 2025. All rights reserved. │ │ Licensed under the Apache License, Version 2.0 │ │ │ │ You may not use this file except in compliance with the License. │ │ You may obtain a copy of the License at │ │ │ │ http://www.apache.org/licenses/LICENSE-2.0 │ │ │ │ Unless required by applicable law or agreed to in writing, software │ │ distributed under the License is distributed on an "AS IS" BASIS, │ │ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │ │ See the License for the specific language governing permissions and │ │ limitations under the License. │ ├──────────────────────────────────────────────────────────────────────────────┤ │ @important │ │ For any future changes to the code in this file, it is recommended to │ │ include, together with the modification, the information of the developer │ │ who changed it and the date of modification. │ └──────────────────────────────────────────────────────────────────────────────┘ */ "use client"; import { useState, useEffect, Suspense } from "react"; import { Card, CardContent, CardHeader, CardTitle, CardDescription, } from "@/components/ui/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Send, Code, BookOpen, FlaskConical, Network, ChevronDown, ChevronUp } from "lucide-react"; import { useToast } from "@/hooks/use-toast"; import { useSearchParams } from "next/navigation"; import { Separator } from "@/components/ui/separator"; import { DocumentationSection } from "./components/DocumentationSection"; import { TechnicalDetailsSection } from "./components/TechnicalDetailsSection"; import { FrontendImplementationSection } from "./components/FrontendImplementationSection"; import { CodeBlock } from "./components/CodeBlock"; import { CodeExamplesSection } from "./components/CodeExamplesSection"; import { HttpLabForm } from "./components/HttpLabForm"; import { StreamLabForm } from "./components/StreamLabForm"; import { LabSection } from "./components/LabSection"; import { A2AComplianceCard } from "./components/A2AComplianceCard"; import { QuickStartTemplates } from "./components/QuickStartTemplates"; function DocumentationContent() { const { toast } = useToast(); const searchParams = useSearchParams(); const agentUrlParam = searchParams.get("agent_url"); const apiKeyParam = searchParams.get("api_key"); const [agentUrl, setAgentUrl] = useState(""); const [apiKey, setApiKey] = useState(""); const [message, setMessage] = useState(""); const [sessionId, setSessionId] = useState( `session-${Math.random().toString(36).substring(2, 9)}` ); const [taskId, setTaskId] = useState( `task-${Math.random().toString(36).substring(2, 9)}` ); const [callId, setCallId] = useState( `call-${Math.random().toString(36).substring(2, 9)}` ); const [response, setResponse] = useState(""); const [isLoading, setIsLoading] = useState(false); const [mainTab, setMainTab] = useState("docs"); const [labMode, setLabMode] = useState("http"); const [a2aMethod, setA2aMethod] = useState("message/send"); const [authMethod, setAuthMethod] = useState("api-key"); // Streaming states const [streamResponse, setStreamResponse] = useState(""); const [streamStatus, setStreamStatus] = useState(""); const [streamHistory, setStreamHistory] = useState([]); const [isStreaming, setIsStreaming] = useState(false); const [streamComplete, setStreamComplete] = useState(false); // Task management states const [currentTaskId, setCurrentTaskId] = useState(null); const [taskStatus, setTaskStatus] = useState(null); const [artifacts, setArtifacts] = useState([]); // Debug state const [debugLogs, setDebugLogs] = useState([]); const [showDebug, setShowDebug] = useState(false); // Files state const [attachedFiles, setAttachedFiles] = useState< { name: string; type: string; size: number; base64: string }[] >([]); // Conversation history state for multi-turn conversations const [conversationHistory, setConversationHistory] = useState([]); const [contextId, setContextId] = useState(null); // Push notifications state const [webhookUrl, setWebhookUrl] = useState(""); const [enableWebhooks, setEnableWebhooks] = useState(false); // Advanced error handling state const [showDetailedErrors, setShowDetailedErrors] = useState(true); // Lab UI visibility states const [showQuickStart, setShowQuickStart] = useState(true); const [showCorsInfo, setShowCorsInfo] = useState(false); const [showConfigStatus, setShowConfigStatus] = useState(true); // Types for A2A messages interface MessagePart { type: string; text?: string; file?: { name: string; bytes: string; }; } interface TextPart { type: "text"; text: string; } interface FilePart { type: "file"; file: { name: string; bytes: string; }; } type MessagePartType = TextPart | FilePart; useEffect(() => { if (agentUrlParam) { setAgentUrl(agentUrlParam); } if (apiKeyParam) { setApiKey(apiKeyParam); } // Generate initial UUIDs generateNewIds(); // Check for hash in URL to auto-switch to lab tab if (typeof window !== 'undefined' && window.location.hash === '#lab') { setMainTab('lab'); } }, [agentUrlParam, apiKeyParam]); // Generate UUID v4 as required by A2A spec const generateUUID = () => { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { const r = Math.random() * 16 | 0; const v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); }; // Generate new IDs const generateNewIds = () => { setTaskId(generateUUID()); setCallId(`req-${Math.random().toString(36).substring(2, 9)}`); }; // Clear conversation history const clearHistory = () => { setConversationHistory([]); setContextId(null); addDebugLog("Conversation history and contextId cleared"); toast({ title: "History Cleared", description: "Conversation context has been reset for new multi-turn conversation.", }); }; // Handle template selection const handleTemplateSelection = (template: any) => { setA2aMethod(template.method); setMessage(template.message); generateNewIds(); // Show success toast toast({ title: "Template Applied", description: `${template.name} template has been applied successfully.`, }); }; const isFilePart = (part: any): part is FilePart => { return part.type === "file" && part.file !== undefined; }; // Create A2A-compliant request based on selected method const createA2ARequest = () => { const currentMessage = { role: "user", parts: [ ...(message ? [ { type: "text", text: message, }, ] : [ { type: "text", text: "What is the A2A protocol?", }, ]), ...attachedFiles.map((file) => ({ type: "file", file: { name: file.name, mimeType: file.type, // Use mimeType as per A2A spec bytes: file.base64, }, })), ], messageId: taskId, // Use UUID as required by A2A spec }; // Include contextId for multi-turn conversations (A2A specification) const messageWithContext = contextId ? { contextId: contextId, message: currentMessage } : { message: currentMessage }; // Add push notification configuration if enabled (for message methods) const messageParamsWithPushConfig = (a2aMethod === "message/send" || a2aMethod === "message/stream") && enableWebhooks && webhookUrl ? { ...messageWithContext, pushNotificationConfig: { webhookUrl: webhookUrl, webhookAuthenticationInfo: { type: "none" } } } : messageWithContext; const baseRequest = { jsonrpc: "2.0", id: callId, method: a2aMethod, }; switch (a2aMethod) { case "message/send": case "message/stream": return { ...baseRequest, params: messageParamsWithPushConfig, }; case "tasks/get": return { ...baseRequest, params: { taskId: currentTaskId || taskId, }, }; case "tasks/cancel": return { ...baseRequest, params: { taskId: currentTaskId || taskId, }, }; case "tasks/pushNotificationConfig/set": return { ...baseRequest, params: { taskId: currentTaskId || taskId, pushNotificationConfig: enableWebhooks && webhookUrl ? { webhookUrl: webhookUrl, webhookAuthenticationInfo: { type: "none" } } : null, }, }; case "tasks/pushNotificationConfig/get": return { ...baseRequest, params: { taskId: currentTaskId || taskId, }, }; case "tasks/resubscribe": return { ...baseRequest, params: { taskId: currentTaskId || taskId, }, }; case "agent/authenticatedExtendedCard": return { ...baseRequest, params: {}, }; default: return { ...baseRequest, params: messageParamsWithPushConfig, }; } }; // Standard HTTP request const jsonRpcRequest = createA2ARequest(); // Streaming request (same as standard but with stream method) const streamRpcRequest = { ...createA2ARequest(), method: "message/stream", }; // Code examples const curlExample = `curl -X POST \\ ${agentUrl || "http://localhost:8000/api/v1/a2a/your-agent-id"} \\ -H 'Content-Type: application/json' \\ -H 'x-api-key: ${apiKey || "your-api-key"}' \\ -d '${JSON.stringify(jsonRpcRequest, null, 2)}'`; const fetchExample = `async function testA2AAgent() { const response = await fetch( '${agentUrl || "http://localhost:8000/api/v1/a2a/your-agent-id"}', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': '${apiKey || "your-api-key"}' }, body: JSON.stringify(${JSON.stringify(jsonRpcRequest, null, 2)}) } ); const data = await response.json(); console.log('Agent response:', data); }`; // Function to add debug logs const addDebugLog = (message: string) => { const timestamp = new Date().toISOString().split("T")[1].substring(0, 8); setDebugLogs((prev) => [...prev, `[${timestamp}] ${message}`]); console.log(`[DEBUG] ${message}`); }; // Standard HTTP request method const sendRequest = async () => { if (!agentUrl) { toast({ title: "Agent URL required", description: "Please enter the agent URL", variant: "destructive", }); return; } setIsLoading(true); addDebugLog("=== Starting A2A Request ==="); addDebugLog("Sending A2A request to: " + agentUrl); addDebugLog(`Method: ${a2aMethod}`); addDebugLog(`Authentication: ${authMethod}`); addDebugLog("Note: Browser may send OPTIONS preflight request first (CORS)"); addDebugLog( `Payload: ${JSON.stringify({ ...jsonRpcRequest, params: jsonRpcRequest.params && 'message' in jsonRpcRequest.params && jsonRpcRequest.params.message ? { ...jsonRpcRequest.params, message: { ...jsonRpcRequest.params.message, parts: jsonRpcRequest.params.message.parts.map((part: any) => isFilePart(part) ? { ...part, file: { ...part.file, bytes: "BASE64_DATA_TRUNCATED" }, } : part ), }, } : jsonRpcRequest.params, })}` ); if (attachedFiles.length > 0) { addDebugLog( `Sending ${attachedFiles.length} file(s): ${attachedFiles .map((f) => f.name) .join(", ")}` ); } try { // Prepare headers based on authentication method const headers: Record = { "Content-Type": "application/json", }; if (apiKey) { if (authMethod === "bearer") { headers["Authorization"] = `Bearer ${apiKey}`; } else { headers["x-api-key"] = apiKey; } } addDebugLog(`Headers: ${JSON.stringify(headers, null, 2)}`); addDebugLog("Making POST request..."); const response = await fetch(agentUrl, { method: "POST", headers, body: JSON.stringify(jsonRpcRequest), }); addDebugLog(`Response status: ${response.status} ${response.statusText}`); addDebugLog(`Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()), null, 2)}`); if (!response.ok) { const errorText = await response.text(); addDebugLog(`HTTP error: ${response.status} ${response.statusText}`); addDebugLog(`Error response: ${errorText}`); throw new Error( `HTTP error: ${response.status} ${response.statusText}` ); } const data = await response.json(); addDebugLog("Successfully received A2A response"); addDebugLog(`Response data: ${JSON.stringify(data, null, 2).substring(0, 500)}...`); // Handle A2A-specific response structure if (data.result) { const result = data.result; // Update task information if available if (result.id) { setCurrentTaskId(result.id); addDebugLog(`Task ID: ${result.id}`); } // Update task status if available if (result.status) { setTaskStatus(result.status); addDebugLog(`Task status: ${result.status.state}`); } // Update artifacts if available if (result.artifacts && Array.isArray(result.artifacts)) { setArtifacts(result.artifacts); addDebugLog(`Received ${result.artifacts.length} artifact(s)`); } // Extract contextId from response for multi-turn conversations (A2A spec) if (result.contextId) { setContextId(result.contextId); addDebugLog(`Context ID: ${result.contextId}`); } // Maintain local conversation history for UI display only if (a2aMethod === "message/send" || a2aMethod === "message/stream") { const userMessage = { role: "user", parts: [ ...(message ? [{ type: "text", text: message }] : [{ type: "text", text: "What is the A2A protocol?" }] ), ...attachedFiles.map((file) => ({ type: "file", file: { name: file.name, mimeType: file.type, bytes: file.base64, }, })), ], messageId: taskId, }; const assistantMessage = result.status?.message || { role: "assistant", parts: [{ type: "text", text: JSON.stringify(result, null, 2) }], messageId: generateUUID(), }; setConversationHistory(prev => [...prev, userMessage, assistantMessage]); addDebugLog(`Context preserved via contextId. Local history: ${conversationHistory.length + 2} messages`); } } setResponse(JSON.stringify(data, null, 2)); setAttachedFiles([]); // Clear attached files after successful request addDebugLog("=== A2A Request Completed Successfully ==="); } catch (error) { const errorMsg = error instanceof Error ? error.message : "Unknown error"; addDebugLog(`Request failed: ${errorMsg}`); addDebugLog("=== A2A Request Failed ==="); // Enhanced error handling for A2A protocol if (showDetailedErrors) { let detailedError = errorMsg; // Try to extract A2A-specific error information if (error instanceof Error && error.message.includes("HTTP error:")) { detailedError = `A2A Protocol Error: ${errorMsg}`; // Common A2A error scenarios if (errorMsg.includes("400")) { detailedError += "\n\nPossible causes:\n• Invalid JSON-RPC 2.0 format\n• Missing required A2A parameters\n• Malformed message structure"; } else if (errorMsg.includes("401")) { detailedError += "\n\nAuthentication Error:\n• Invalid API key\n• Missing authentication header\n• Expired bearer token"; } else if (errorMsg.includes("404")) { detailedError += "\n\nAgent Not Found:\n• Incorrect agent URL\n• Agent not deployed\n• Invalid endpoint path"; } else if (errorMsg.includes("500")) { detailedError += "\n\nServer Error:\n• Agent internal error\n• A2A method not implemented\n• Processing failure"; } } toast({ title: "A2A Request Failed", description: detailedError, variant: "destructive", }); } else { toast({ title: "Error sending A2A request", description: errorMsg, variant: "destructive", }); } } finally { setIsLoading(false); } }; // Function to process event stream const processEventStream = async (response: Response) => { try { const reader = response.body!.getReader(); const decoder = new TextDecoder(); let buffer = ""; addDebugLog("Starting event stream processing..."); while (true) { const { done, value } = await reader.read(); if (done) { addDebugLog("Stream finished by server"); setStreamComplete(true); setIsStreaming(false); break; } // Decode chunk of data const chunk = decoder.decode(value, { stream: true }); addDebugLog(`Received chunk (${value.length} bytes)`); // Add to buffer buffer += chunk; // Process complete events in buffer // We use regex to identify complete "data:" events // A complete SSE event ends with two newlines (\n\n) const regex = /data:\s*({.*?})\s*(?:\n\n|\r\n\r\n)/g; let match; let processedPosition = 0; // Extract all complete "data:" events while ((match = regex.exec(buffer)) !== null) { const jsonStr = match[1].trim(); addDebugLog(`Complete event found: ${jsonStr.substring(0, 50)}...`); try { // Process the JSON of the event const data = JSON.parse(jsonStr); // Add to history setStreamHistory((prev) => [...prev, jsonStr]); // Process the data obtained processStreamData(data); // Update the processed position to remove from buffer after processedPosition = match.index + match[0].length; } catch (jsonError) { addDebugLog(`Error processing JSON: ${jsonError}`); // Continue processing other events even with error in one of them } } // Remove the processed part of the buffer if (processedPosition > 0) { buffer = buffer.substring(processedPosition); } // Check if the buffer is too large (indicates invalid data) if (buffer.length > 10000) { addDebugLog("Buffer too large, clearing old data"); // Keep only the last part of the buffer that may contain a partial event buffer = buffer.substring(buffer.length - 5000); } // Remove ping events from buffer if (buffer.includes(": ping")) { addDebugLog("Ping event detected, clearing buffer"); buffer = buffer.replace(/:\s*ping.*?(?:\n\n|\r\n\r\n)/g, ""); } } } catch (streamError) { const errorMsg = streamError instanceof Error ? streamError.message : "Unknown error"; addDebugLog(`Error processing stream: ${errorMsg}`); console.error("Error processing stream:", streamError); toast({ title: "Error processing stream", description: errorMsg, variant: "destructive", }); } }; // Process a received streaming event const processStreamData = (data: any) => { // Add log to see the complete structure of the data addDebugLog( `Processing A2A stream data: ${JSON.stringify(data).substring(0, 100)}...` ); // Validate if data follows the JSON-RPC 2.0 format if (!data.jsonrpc || data.jsonrpc !== "2.0" || !data.result) { addDebugLog("Invalid A2A event format, ignoring"); return; } const result = data.result; // Handle A2A Task object structure if (result.id) { setCurrentTaskId(result.id); addDebugLog(`Task ID: ${result.id}`); } // Process A2A TaskStatus object if (result.status) { const status = result.status; const state = status.state; addDebugLog(`A2A Task status: ${state}`); setStreamStatus(state); setTaskStatus(status); // Process message from status if available if (status.message && status.message.parts) { const textParts = status.message.parts.filter( (part: any) => part.type === "text" && part.text ); if (textParts.length > 0) { const currentMessageText = textParts .map((part: any) => part.text) .join(""); addDebugLog( `A2A message text: "${currentMessageText.substring(0, 50)}${ currentMessageText.length > 50 ? "..." : "" }"` ); if (currentMessageText.trim()) { setStreamResponse(currentMessageText); } } } // Check if task is completed according to A2A spec if (state === "completed" || state === "failed" || state === "canceled") { addDebugLog(`A2A Task ${state}`); setStreamComplete(true); setIsStreaming(false); } } // Process A2A Artifact objects if (result.artifacts && Array.isArray(result.artifacts)) { addDebugLog(`Received ${result.artifacts.length} A2A artifact(s)`); setArtifacts(result.artifacts); // Extract text content from artifacts result.artifacts.forEach((artifact: any, index: number) => { if (artifact.parts && artifact.parts.length > 0) { const textParts = artifact.parts.filter( (part: any) => part.type === "text" && part.text ); if (textParts.length > 0) { const artifactText = textParts.map((part: any) => part.text).join(""); addDebugLog( `A2A Artifact ${index + 1} text: "${artifactText.substring(0, 50)}${ artifactText.length > 50 ? "..." : "" }"` ); if (artifactText.trim()) { // Update response with the artifact content setStreamResponse(prev => prev ? `${prev}\n\n${artifactText}` : artifactText); } } } }); } // Handle TaskStatusUpdateEvent and TaskArtifactUpdateEvent as per A2A spec if (result.event) { const eventType = result.event; addDebugLog(`A2A Event type: ${eventType}`); if (eventType === "task.status.update" && result.status) { // Already handled above } else if (eventType === "task.artifact.update" && result.artifact) { // Handle single artifact update addDebugLog("Processing A2A artifact update event"); if (result.artifact.parts && result.artifact.parts.length > 0) { const textParts = result.artifact.parts.filter( (part: any) => part.type === "text" && part.text ); if (textParts.length > 0) { const artifactText = textParts.map((part: any) => part.text).join(""); if (artifactText.trim()) { setStreamResponse(prev => prev ? `${prev}${artifactText}` : artifactText); // Check if this is the last chunk if (result.artifact.lastChunk === true) { addDebugLog("Last chunk of A2A artifact received"); setStreamComplete(true); setIsStreaming(false); } } } } } } // Check for final flag as per A2A spec if (result.final === true) { addDebugLog("Final A2A event received"); setStreamComplete(true); setIsStreaming(false); } }; // Stream request with EventSource const sendStreamRequestWithEventSource = async () => { if (!agentUrl) { toast({ title: "Agent URL required", description: "Please enter the agent URL", variant: "destructive", }); return; } setIsStreaming(true); setStreamResponse(""); setStreamHistory([]); setStreamStatus("submitted"); setStreamComplete(false); // Log debug info addDebugLog("Setting up A2A streaming to: " + agentUrl); addDebugLog(`Streaming method: message/stream`); addDebugLog(`Authentication: ${authMethod}`); addDebugLog( `Streaming payload: ${JSON.stringify({ ...streamRpcRequest, params: streamRpcRequest.params && 'message' in streamRpcRequest.params && streamRpcRequest.params.message ? { ...streamRpcRequest.params, message: { ...streamRpcRequest.params.message, parts: streamRpcRequest.params.message.parts.map((part: any) => isFilePart(part) ? { ...part, file: { ...part.file, bytes: "BASE64_DATA_TRUNCATED" }, } : part ), }, } : streamRpcRequest.params, })}` ); if (attachedFiles.length > 0) { addDebugLog( `Streaming with ${attachedFiles.length} file(s): ${attachedFiles .map((f) => f.name) .join(", ")}` ); } try { addDebugLog("Stream URL: " + agentUrl); // Prepare headers based on authentication method const headers: Record = { "Content-Type": "application/json", }; if (apiKey) { if (authMethod === "bearer") { headers["Authorization"] = `Bearer ${apiKey}`; } else { headers["x-api-key"] = apiKey; } } // Make initial request to start streaming session const initialResponse = await fetch(agentUrl, { method: "POST", headers, body: JSON.stringify(streamRpcRequest), }); // Verify the content-type of the response const contentType = initialResponse.headers.get("Content-Type"); addDebugLog(`Response content type: ${contentType || "not specified"}`); if (contentType && contentType.includes("text/event-stream")) { // It's an SSE (Server-Sent Events) response addDebugLog("Detected SSE response, processing stream directly"); processEventStream(initialResponse); return; } if (!initialResponse.ok) { const errorText = await initialResponse.text(); addDebugLog( `HTTP error: ${initialResponse.status} ${initialResponse.statusText}` ); addDebugLog(`Error response: ${errorText}`); throw new Error( `HTTP error: ${initialResponse.status} ${initialResponse.statusText}` ); } // Get the initial response data try { const responseText = await initialResponse.text(); // Verify if the response starts with "data:", which indicates an SSE if (responseText.trim().startsWith("data:")) { addDebugLog("Response has SSE format but wrong content-type"); // Create a synthetic response to process as stream const syntheticResponse = new Response(responseText, { headers: { "Content-Type": "text/event-stream", }, }); processEventStream(syntheticResponse); return; } // Try to process as JSON const initialData = JSON.parse(responseText); addDebugLog("Initial stream response: " + JSON.stringify(initialData)); // Display initial response displayInitialResponse(initialData); // Get task ID from response if present const responseTaskId = extractTaskId(initialData); if (responseTaskId) { addDebugLog(`Using task ID from response: ${responseTaskId}`); // Setup EventSource for streaming updates setupEventSource(agentUrl + "?taskId=" + responseTaskId); } else { setIsStreaming(false); setStreamComplete(true); setStreamStatus("completed"); addDebugLog("No task ID in response, streaming ended"); } } catch (parseError) { addDebugLog(`Error parsing response: ${parseError}`); // If we can't process as JSON or SSE, show the error setStreamResponse( `Error: Unable to process response: ${parseError instanceof Error ? parseError.message : String(parseError)}` ); setIsStreaming(false); setStreamStatus("failed"); setStreamComplete(true); } } catch (error) { const errorMsg = error instanceof Error ? error.message : "Unknown error"; setIsStreaming(false); setStreamStatus("failed"); addDebugLog(`Stream request failed: ${errorMsg}`); toast({ title: "Error setting up stream", description: errorMsg, variant: "destructive", }); } // Clear attached files after sending setAttachedFiles([]); }; const ensureTrailingSlash = (path: string) => { return path.endsWith("/") ? path : path + "/"; }; // Function to extract task ID from different response formats const extractTaskId = (data: any): string | null => { // Try different possible paths for the task ID const possiblePaths = [ data?.result?.id, data?.result?.status?.id, data?.result?.task?.id, data?.id, data?.task_id, data?.taskId, ]; for (const path of possiblePaths) { if (path && typeof path === "string") { return path; } } // If no specific ID is found, try using the request ID as fallback return taskId; }; // Configure and start the EventSource const setupEventSource = (url: string) => { addDebugLog(`Configuring EventSource for: ${url}`); // Ensure any previous EventSource is closed let eventSource: EventSource; try { // Create EventSource for streaming eventSource = new EventSource(url); addDebugLog("EventSource created and connecting..."); // For debugging all events eventSource.onopen = () => { addDebugLog("EventSource connected successfully"); }; // Process received SSE events eventSource.onmessage = (event) => { addDebugLog( `Received event [${new Date().toISOString()}]: ${event.data.substring( 0, 50 )}...` ); try { const data = JSON.parse(event.data); setStreamHistory((prev) => [...prev, event.data]); // If the event is empty or has no relevant data, ignore it if (!data || (!data.result && !data.status && !data.message)) { addDebugLog("Event without relevant data"); return; } // Process data according to the type processStreamData(data); } catch (jsonError) { const errorMsg = jsonError instanceof Error ? jsonError.message : "Unknown error"; addDebugLog(`Error processing JSON: ${errorMsg}`); console.error("Error processing JSON:", jsonError); } }; // Handle errors eventSource.onerror = (error) => { addDebugLog(`Error in EventSource: ${JSON.stringify(error)}`); console.error("EventSource error:", error); // Don't close automatically - try to reconnect unless it's a fatal error if (eventSource.readyState === EventSource.CLOSED) { addDebugLog("EventSource closed due to error"); toast({ title: "Streaming Error", description: "Connection to server was interrupted", variant: "destructive", }); setIsStreaming(false); setStreamComplete(true); } else { addDebugLog("EventSource attempting to reconnect..."); } }; const checkStreamStatus = setInterval(() => { if (streamComplete) { addDebugLog("Stream marked as complete, closing EventSource"); eventSource.close(); clearInterval(checkStreamStatus); } }, 1000); } catch (esError) { addDebugLog(`Error creating EventSource: ${esError}`); throw esError; } }; const displayInitialResponse = (data: any) => { addDebugLog("Displaying initial response without streaming"); // Try to extract text message if available try { const result = data.result || data; const status = result.status || {}; const message = status.message || result.message; if (message && message.parts) { const parts = message.parts.filter((part: any) => part.type === "text"); if (parts.length > 0) { const currentText = parts.map((part: any) => part.text).join(""); setStreamResponse(currentText); } else { // No text parts, display formatted JSON setStreamResponse(JSON.stringify(data, null, 2)); } } else { // No structured message, display formatted JSON setStreamResponse(JSON.stringify(data, null, 2)); } // Set status as completed setStreamStatus("completed"); setStreamComplete(true); } catch (parseError) { // In case of error processing, show raw JSON setStreamResponse(JSON.stringify(data, null, 2)); setStreamStatus("completed"); setStreamComplete(true); } finally { setIsStreaming(false); } }; const copyToClipboard = (text: string) => { navigator.clipboard.writeText(text); toast({ title: "Copied!", description: "Code copied to clipboard", }); }; // Render status indicator based on streaming status const renderStatusIndicator = () => { switch (streamStatus) { case "submitted": return ( Submitted ); case "working": return ( Processing ); case "completed": return ( Completed ); case "failed": return ( Failed ); case "canceled": return ( Canceled ); case "input-required": return ( Input Required ); default: return null; } }; // Typing indicator for streaming const renderTypingIndicator = () => { if (streamStatus === "working" && !streamComplete) { return (
); } return null; }; return (
{/* Modern Header */}
A2A Protocol v0.2.1

Agent2Agent Protocol

Documentation and testing lab for the official Google Agent2Agent protocol. Build, test, and validate A2A-compliant agent communications.

Documentation
Protocol specification & compliance
Testing Lab
Interactive A2A protocol testing
Code Examples
Implementation samples & snippets
{/* Documentation Tab - Only essential technical concepts and details */}
{/* A2A Compliance Card */} {/* Main Documentation */}
{/* Lab Tab */} {/* Quick Start Templates */}
setShowQuickStart(!showQuickStart)} >

Quick Start Templates (4 templates available)

{showQuickStart ? 'Hide templates' : 'Show templates'} {showQuickStart ? ( ) : ( )}
{showQuickStart && ( )}
{/* CORS Information */}
setShowCorsInfo(!showCorsInfo)} >

⚠️ CORS & Browser Requests (Important for cross-origin testing)

{showCorsInfo ? 'Hide info' : 'Show info'} {showCorsInfo ? ( ) : ( )}
{showCorsInfo && (

Note: If you see OPTIONS requests in logs, this is normal browser behavior.

  • • Browser sends OPTIONS preflight for cross-origin requests
  • • OPTIONS request checks CORS permissions before actual POST
  • • Your A2A server must handle OPTIONS and return proper CORS headers
  • • The actual POST request with your data comes after OPTIONS
Server CORS Headers needed:
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, x-api-key, Authorization
)}
{/* A2A Configuration Status */}
setShowConfigStatus(!showConfigStatus)} >

🔧 Active A2A Configuration (Real-time feature status)

{showConfigStatus ? 'Hide config' : 'Show config'} {showConfigStatus ? ( ) : ( )}
{showConfigStatus && (

Multi-turn Conversations

{contextId ? '✅ Active' : '⏸️ Waiting for contextId'}
{contextId && (
contextId: {contextId.substring(0, 8)}...
)} {!contextId && (
Server will provide contextId if supported
)}

Push Notifications

{enableWebhooks ? '✅ Enabled' : '❌ Disabled'}
{enableWebhooks && webhookUrl && (
{webhookUrl}
)} {enableWebhooks && !webhookUrl && (
URL not configured
)}

Debug Logging

{showDetailedErrors ? '🔍 Detailed' : '⚡ Basic'}
{showDetailedErrors ? 'Enhanced debug logs' : 'Standard logs only'}
)}
A2A Testing Lab Test your A2A agent with different communication methods. Fully compliant with A2A v0.2.1 specification. HTTP Request Streaming {response && labMode === "http" && ( Response
)} {/* Show message if no response yet but in HTTP mode */} {!response && labMode === "http" && !isLoading && (

Click "Send" to test your A2A agent and see the response here.

)} {/* Debug Logs Section */} {debugLogs.length > 0 && (
Debug Logs
Detailed request flow - includes CORS preflight and actual requests
{showDebug && (
                      {debugLogs.join('\n')}
                    
)}
)}
{/* Footer with additional resources */}

Implementation Status

✅ A2A v0.2.1 Compliant
✅ All Core Methods Supported
✅ Enterprise Security Ready

Need Help?

📖 Check the documentation tab
🧪 Test with the lab interface
💡 View code examples for implementation
Built with ❤️ for the Agent2Agent community • Evolution API © 2025
); } export default function DocumentationPage() { return (
} >
); }