File size: 7,041 Bytes
7a1ad33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
/**
 * @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);
}