Files
gemini-cli/packages/core/src/agents/a2a-errors.ts
Shyam Raghuwanshi 7c4570339e fix: robust UX for remote agent errors (#20307)
Co-authored-by: Adam Weidman <adamfweidman@google.com>
2026-03-10 23:50:25 +00:00

207 lines
6.9 KiB
TypeScript

/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview Custom error types for A2A remote agent operations.
* Provides structured, user-friendly error messages for common failure modes
* during agent card fetching, authentication, and communication.
*/
/**
* Base class for all A2A agent errors.
* Provides a `userMessage` field with a human-readable description.
*/
export class A2AAgentError extends Error {
/** A user-friendly message suitable for display in the CLI. */
readonly userMessage: string;
/** The agent name associated with this error. */
readonly agentName: string;
constructor(
agentName: string,
message: string,
userMessage: string,
options?: ErrorOptions,
) {
super(message, options);
this.name = 'A2AAgentError';
this.agentName = agentName;
this.userMessage = userMessage;
}
}
/**
* Thrown when the agent card URL returns a 404 Not Found response.
*/
export class AgentCardNotFoundError extends A2AAgentError {
constructor(agentName: string, agentCardUrl: string) {
const message = `Agent card not found at ${agentCardUrl} (HTTP 404)`;
const userMessage = `Agent card not found (404) at ${agentCardUrl}. Verify the agent_card_url in your agent definition.`;
super(agentName, message, userMessage);
this.name = 'AgentCardNotFoundError';
}
}
/**
* Thrown when the agent card URL returns a 401/403 response,
* indicating an authentication or authorization failure.
*/
export class AgentCardAuthError extends A2AAgentError {
readonly statusCode: number;
constructor(agentName: string, agentCardUrl: string, statusCode: 401 | 403) {
const statusText = statusCode === 401 ? 'Unauthorized' : 'Forbidden';
const message = `Agent card request returned ${statusCode} ${statusText} for ${agentCardUrl}`;
const userMessage = `Authentication failed (${statusCode} ${statusText}) at ${agentCardUrl}. Check the "auth" configuration in your agent definition.`;
super(agentName, message, userMessage);
this.name = 'AgentCardAuthError';
this.statusCode = statusCode;
}
}
/**
* Thrown when the agent card's security schemes require authentication
* but the agent definition does not include the necessary auth configuration.
*/
export class AgentAuthConfigMissingError extends A2AAgentError {
/** Human-readable description of required authentication schemes. */
readonly requiredAuth: string;
/** Specific fields or config entries that are missing. */
readonly missingFields: string[];
constructor(
agentName: string,
requiredAuth: string,
missingFields: string[],
) {
const message = `Agent "${agentName}" requires authentication but none is configured`;
const userMessage = `Agent requires ${requiredAuth} but no auth is configured. Missing: ${missingFields.join(', ')}`;
super(agentName, message, userMessage);
this.name = 'AgentAuthConfigMissingError';
this.requiredAuth = requiredAuth;
this.missingFields = missingFields;
}
}
/**
* Thrown when a generic/unexpected network or server error occurs
* while fetching the agent card or communicating with the remote agent.
*/
export class AgentConnectionError extends A2AAgentError {
constructor(agentName: string, agentCardUrl: string, cause: unknown) {
const causeMessage = cause instanceof Error ? cause.message : String(cause);
const message = `Failed to connect to agent "${agentName}" at ${agentCardUrl}: ${causeMessage}`;
const userMessage = `Connection failed for ${agentCardUrl}: ${causeMessage}`;
super(agentName, message, userMessage, { cause });
this.name = 'AgentConnectionError';
}
}
/** Shape of an error-like object in a cause chain (Error, HTTP response, or plain object). */
interface ErrorLikeObject {
message?: string;
code?: string;
status?: number;
statusCode?: number;
cause?: unknown;
}
/** Type guard for objects that may carry error metadata (message, code, status, cause). */
function isErrorLikeObject(val: unknown): val is ErrorLikeObject {
return typeof val === 'object' && val !== null;
}
/**
* Collects all error messages from an error's cause chain into a single string
* for pattern matching. This is necessary because the A2A SDK and Node's fetch
* often wrap the real error (e.g. HTTP status) deep inside nested causes.
*/
function collectErrorMessages(error: unknown): string {
const parts: string[] = [];
let current: unknown = error;
let depth = 0;
const maxDepth = 10;
while (current && depth < maxDepth) {
if (isErrorLikeObject(current)) {
// Save reference before instanceof narrows the type from ErrorLikeObject to Error.
const obj = current;
if (current instanceof Error) {
parts.push(current.message);
} else if (typeof obj.message === 'string') {
parts.push(obj.message);
}
if (typeof obj.code === 'string') {
parts.push(obj.code);
}
if (typeof obj.status === 'number') {
parts.push(String(obj.status));
} else if (typeof obj.statusCode === 'number') {
parts.push(String(obj.statusCode));
}
current = obj.cause;
} else if (typeof current === 'string') {
parts.push(current);
break;
} else {
parts.push(String(current));
break;
}
depth++;
}
return parts.join(' ');
}
/**
* Attempts to classify a raw error from the A2A SDK into a typed A2AAgentError.
*
* Inspects the error message and full cause chain for HTTP status codes and
* well-known patterns to produce a structured, user-friendly error.
*
* @param agentName The name of the agent being loaded.
* @param agentCardUrl The URL of the agent card.
* @param error The raw error caught during agent loading.
* @returns A classified A2AAgentError subclass.
*/
export function classifyAgentError(
agentName: string,
agentCardUrl: string,
error: unknown,
): A2AAgentError {
// Collect messages from the entire cause chain for thorough matching.
const fullErrorText = collectErrorMessages(error);
// Check for well-known connection error codes in the cause chain.
// NOTE: This is checked before the 404 pattern as a defensive measure
// to prevent DNS errors (ENOTFOUND) from being misclassified as 404s.
if (
/\b(ECONNREFUSED|ENOTFOUND|EHOSTUNREACH|ETIMEDOUT)\b/i.test(fullErrorText)
) {
return new AgentConnectionError(agentName, agentCardUrl, error);
}
// Check for HTTP status code patterns across the full cause chain.
if (/\b404\b|\bnot[\s_-]?found\b/i.test(fullErrorText)) {
return new AgentCardNotFoundError(agentName, agentCardUrl);
}
if (/\b401\b|unauthorized/i.test(fullErrorText)) {
return new AgentCardAuthError(agentName, agentCardUrl, 401);
}
if (/\b403\b|forbidden/i.test(fullErrorText)) {
return new AgentCardAuthError(agentName, agentCardUrl, 403);
}
// Fallback to a generic connection error.
return new AgentConnectionError(agentName, agentCardUrl, error);
}