From dd7e7646e210c3e4df1ee461f4e29804101f3551 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Br=C3=BCderl?= Date: Tue, 27 Jan 2026 18:29:01 +0100 Subject: [PATCH] a2a: display JSON-RPC errors with full details The previous error handling was useless - it showed a generic "Sorry, I encountered an error" message that told users nothing about what actually went wrong. Now when an A2A JSON-RPC error occurs, we display a proper error block showing code, message, and data fields. Error codes are mapped to human-readable names based on a2a-go/a2a/errors.go. The duplicate error message creation in use-chat-actions.ts is removed since use-message-streaming.ts now handles error display properly via the onMessageUpdate callback. --- .../a2a/chat/components/chat-message.tsx | 3 + .../message-blocks/a2a-error-block.tsx | 100 ++++++++++++++++ .../a2a/chat/hooks/use-chat-actions.ts | 11 +- .../a2a/chat/hooks/use-message-streaming.ts | 108 ++++++++++++++++-- .../pages/agents/details/a2a/chat/types.ts | 7 +- 5 files changed, 211 insertions(+), 18 deletions(-) create mode 100644 frontend/src/components/pages/agents/details/a2a/chat/components/message-blocks/a2a-error-block.tsx diff --git a/frontend/src/components/pages/agents/details/a2a/chat/components/chat-message.tsx b/frontend/src/components/pages/agents/details/a2a/chat/components/chat-message.tsx index 88074e944f..b60619fd65 100644 --- a/frontend/src/components/pages/agents/details/a2a/chat/components/chat-message.tsx +++ b/frontend/src/components/pages/agents/details/a2a/chat/components/chat-message.tsx @@ -12,6 +12,7 @@ import { Message, MessageBody, MessageContent, MessageMetadata } from 'components/ai-elements/message'; import { ChatMessageActions } from './chat-message-actions'; +import { A2AErrorBlock } from './message-blocks/a2a-error-block'; import { ArtifactBlock } from './message-blocks/artifact-block'; import { TaskStatusUpdateBlock } from './message-blocks/task-status-update-block'; import { ToolBlock } from './message-blocks/tool-block'; @@ -101,6 +102,8 @@ export const ChatMessage = ({ message, isLoading: _isLoading }: ChatMessageProps timestamp={block.timestamp} /> ); + case 'a2a-error': + return ; default: return null; } diff --git a/frontend/src/components/pages/agents/details/a2a/chat/components/message-blocks/a2a-error-block.tsx b/frontend/src/components/pages/agents/details/a2a/chat/components/message-blocks/a2a-error-block.tsx new file mode 100644 index 0000000000..3466b32d41 --- /dev/null +++ b/frontend/src/components/pages/agents/details/a2a/chat/components/message-blocks/a2a-error-block.tsx @@ -0,0 +1,100 @@ +/** + * Copyright 2025 Redpanda Data, Inc. + * + * Use of this software is governed by the Business Source License + * included in the file https://github.com/redpanda-data/redpanda/blob/dev/licenses/bsl.md + * + * As of the Change Date specified in that file, in accordance with + * the Business Source License, use of this software will be governed + * by the Apache License, Version 2.0 + */ + +import type { JSONRPCError } from '@a2a-js/sdk'; +import { Alert, AlertDescription, AlertTitle } from 'components/redpanda-ui/components/alert'; +import { Text } from 'components/redpanda-ui/components/typography'; +import { AlertCircleIcon } from 'lucide-react'; + +type A2AErrorBlockProps = { + error: JSONRPCError; + timestamp: Date; +}; + +/** + * Map JSON-RPC error codes to human-readable names + */ +/** + * Map JSON-RPC/A2A error codes to human-readable names + * Based on a2a-go/a2a/errors.go codeToError mapping + */ +const getErrorCodeName = (code: number): string => { + const errorCodes: Record = { + // Standard JSON-RPC 2.0 errors + [-32_700]: 'Parse Error', + [-32_600]: 'Invalid Request', + [-32_601]: 'Method Not Found', + [-32_602]: 'Invalid Params', + [-32_603]: 'Internal Error', + [-32_000]: 'Server Error', + // A2A-specific errors + [-32_001]: 'Task Not Found', + [-32_002]: 'Task Not Cancelable', + [-32_003]: 'Push Notifications Not Supported', + [-32_004]: 'Unsupported Operation', + [-32_005]: 'Content Type Not Supported', + [-32_006]: 'Invalid Agent Response', + [-32_007]: 'Authenticated Extended Card Not Configured', + [-32_008]: 'Authentication Failed', + [-32_009]: 'Forbidden', + }; + + return errorCodes[code] || `Error ${code}`; +}; + +/** + * A2A Error Block - displays JSON-RPC errors with full details + */ +export const A2AErrorBlock = ({ error, timestamp }: A2AErrorBlockProps) => { + const time = timestamp.toLocaleTimeString(undefined, { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + fractionalSecondDigits: 3, + }); + + const errorCodeName = getErrorCodeName(error.code); + const hasData = error.data && Object.keys(error.data).length > 0; + + return ( + } variant="destructive"> + {errorCodeName} + +
+ + {error.message} + + +
+
+ code: + {error.code} +
+
+ message: + {error.message} +
+ {Boolean(hasData) && ( +
+ data: +
{JSON.stringify(error.data, null, 2)}
+
+ )} +
+ time: + {time} +
+
+
+
+
+ ); +}; diff --git a/frontend/src/components/pages/agents/details/a2a/chat/hooks/use-chat-actions.ts b/frontend/src/components/pages/agents/details/a2a/chat/hooks/use-chat-actions.ts index e3ac168063..58b3ce20f5 100644 --- a/frontend/src/components/pages/agents/details/a2a/chat/hooks/use-chat-actions.ts +++ b/frontend/src/components/pages/agents/details/a2a/chat/hooks/use-chat-actions.ts @@ -18,7 +18,7 @@ import { streamMessage } from './use-message-streaming'; import type { ChatMessage } from '../types'; import { createA2AClient } from '../utils/a2a-client'; import { clearChatHistory, deleteMessages, saveMessage } from '../utils/database-operations'; -import { createErrorMessage, createUserMessage } from '../utils/message-converter'; +import { createUserMessage } from '../utils/message-converter'; type UseChatActionsParams = { agentId: string; @@ -91,7 +91,7 @@ export const useChatActions = ({ await saveMessage(userMessage, agentId); // Stream assistant response - const result = await streamMessage({ + await streamMessage({ prompt: prompt || 'Sent with attachments', agentId, agentCardUrl, @@ -108,13 +108,6 @@ export const useChatActions = ({ }, }); - // Handle error if streaming failed - if (!result.success) { - const errorMessage = createErrorMessage(contextId); - setMessages((prev) => [...prev, errorMessage]); - await saveMessage(errorMessage, agentId, { failure: true }); - } - setIsLoading(false); }, [agentId, agentCardUrl, contextId, editingMessageId, messages, model, setMessages] diff --git a/frontend/src/components/pages/agents/details/a2a/chat/hooks/use-message-streaming.ts b/frontend/src/components/pages/agents/details/a2a/chat/hooks/use-message-streaming.ts index f2578524af..2bf37fd144 100644 --- a/frontend/src/components/pages/agents/details/a2a/chat/hooks/use-message-streaming.ts +++ b/frontend/src/components/pages/agents/details/a2a/chat/hooks/use-message-streaming.ts @@ -9,12 +9,9 @@ * by the Apache License, Version 2.0 */ -import type { Task, TaskArtifactUpdateEvent, TaskStatusUpdateEvent } from '@a2a-js/sdk'; -import { ConnectError } from '@connectrpc/connect'; +import type { JSONRPCError, Task, TaskArtifactUpdateEvent, TaskStatusUpdateEvent } from '@a2a-js/sdk'; import { streamText } from 'ai'; import { config } from 'config'; -import { toast } from 'sonner'; -import { formatToastErrorMessageGRPC } from 'utils/toast.utils'; import { handleArtifactUpdateEvent, @@ -26,10 +23,77 @@ import { import { buildMessageWithContentBlocks, closeActiveTextBlock } from './message-builder'; import type { ResponseMetadataEvent, StreamChunk, StreamingState, TextDeltaEvent } from './streaming-types'; import { a2a } from '../../a2a-provider'; -import type { ChatMessage } from '../types'; +import type { ChatMessage, ContentBlock } from '../types'; import { saveMessage, updateMessage } from '../utils/database-operations'; import { createAssistantMessage } from '../utils/message-converter'; +/** + * Regex patterns for parsing JSON-RPC error details from error messages. + * + * Why regex? The a2a-js SDK throws plain Error objects with formatted strings + * instead of structured error objects. The SDK has access to the structured + * JSON-RPC error (code, message, data) but serializes it into the error message: + * + * // a2a-js/src/client/transports/json_rpc_transport.ts + * if ('error' in a2aStreamResponse) { + * const err = a2aStreamResponse.error; + * throw new Error( + * `SSE event contained an error: ${err.message} (Code: ${err.code}) Data: ${JSON.stringify(err.data || {})}` + * ); + * } + * + * Until the SDK exposes structured error data, we parse it back out. + */ +const JSON_RPC_CODE_REGEX = /\(Code:\s*(-?\d+)\)/i; +const JSON_RPC_DATA_REGEX = /Data:\s*(\{[^}]*\})/i; +const JSON_RPC_MESSAGE_REGEX = /error:\s*([^(]+)\s*\(Code:/i; +const ERROR_PREFIX_STREAMING_REGEX = /^Error during streaming[^:]*:\s*/i; +const ERROR_PREFIX_SSE_REGEX = /^SSE event contained an error:\s*/i; +const ERROR_SUFFIX_CODE_REGEX = /\s*\(Code:\s*-?\d+\).*$/i; + +/** + * Parse A2A/JSON-RPC error details from an error message string. + */ +const parseA2AError = (error: unknown): JSONRPCError => { + const errorMessage = error instanceof Error ? error.message : String(error); + + // Try to parse JSON-RPC error from the error message + // Format: "SSE event contained an error: (Code: ) Data: (code: )" + const jsonRpcMatch = errorMessage.match(JSON_RPC_CODE_REGEX); + const dataMatch = errorMessage.match(JSON_RPC_DATA_REGEX); + const messageMatch = errorMessage.match(JSON_RPC_MESSAGE_REGEX); + + // Extract just the core error message without wrapper text + let message = errorMessage; + if (messageMatch?.[1]) { + message = messageMatch[1].trim(); + } else { + // Remove common prefixes + message = message + .replace(ERROR_PREFIX_STREAMING_REGEX, '') + .replace(ERROR_PREFIX_SSE_REGEX, '') + .replace(ERROR_SUFFIX_CODE_REGEX, '') + .trim(); + } + + const code = jsonRpcMatch?.[1] ? Number.parseInt(jsonRpcMatch[1], 10) : -1; + + let data: Record | undefined; + if (dataMatch?.[1]) { + try { + data = JSON.parse(dataMatch[1]); + } catch { + // Invalid JSON in data field + } + } + + return { + code, + message: message || 'Unknown error', + data, + }; +}; + type StreamMessageParams = { prompt: string; agentId: string; @@ -196,10 +260,38 @@ export const streamMessage = async ({ success: true, }; } catch (error) { - const connectError = ConnectError.from(error); - toast.error(formatToastErrorMessageGRPC({ error: connectError, action: 'stream', entity: 'message' })); + // Parse JSON-RPC error details + const a2aError = parseA2AError(error); + + // Create error content block + const errorBlock: ContentBlock = { + type: 'a2a-error', + error: a2aError, + timestamp: new Date(), + }; + + // Build message with error block + const errorMessage = buildMessageWithContentBlocks({ + baseMessage: assistantMessage, + contentBlocks: [errorBlock], + taskId: undefined, + taskState: 'failed', + taskStartIndex: undefined, + }); + + // Update database with error + await updateMessage(assistantMessage.id, { + content: '', + isStreaming: false, + taskState: 'failed', + contentBlocks: [errorBlock], + }); + + // Notify caller about error message + onMessageUpdate(errorMessage); + return { - assistantMessage, + assistantMessage: errorMessage, success: false, }; } diff --git a/frontend/src/components/pages/agents/details/a2a/chat/types.ts b/frontend/src/components/pages/agents/details/a2a/chat/types.ts index 637a5b9ff9..d8f410c405 100644 --- a/frontend/src/components/pages/agents/details/a2a/chat/types.ts +++ b/frontend/src/components/pages/agents/details/a2a/chat/types.ts @@ -9,7 +9,7 @@ * by the Apache License, Version 2.0 */ -import type { TaskState } from '@a2a-js/sdk'; +import type { JSONRPCError, TaskState } from '@a2a-js/sdk'; import type { AIAgent } from 'protogen/redpanda/api/dataplane/v1alpha3/ai_agent_pb'; /** @@ -55,6 +55,11 @@ export type ContentBlock = final: boolean; timestamp: Date; usage?: MessageUsageMetadata; + } + | { + type: 'a2a-error'; + error: JSONRPCError; + timestamp: Date; }; // Message-level usage metadata (stored in database)