1use std::fmt;
13
14use serde::{Deserialize, Serialize};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
23#[serde(into = "i32", try_from = "i32")]
24#[non_exhaustive]
25pub enum ErrorCode {
26 ParseError = -32700,
29 InvalidRequest = -32600,
31 MethodNotFound = -32601,
33 InvalidParams = -32602,
35 InternalError = -32603,
37
38 TaskNotFound = -32001,
41 TaskNotCancelable = -32002,
43 PushNotificationNotSupported = -32003,
45 UnsupportedOperation = -32004,
47 ContentTypeNotSupported = -32005,
49 InvalidAgentResponse = -32006,
51 ExtendedAgentCardNotConfigured = -32007,
53 ExtensionSupportRequired = -32008,
55 VersionNotSupported = -32009,
57}
58
59impl ErrorCode {
60 #[must_use]
62 pub const fn as_i32(self) -> i32 {
63 self as i32
64 }
65
66 #[must_use]
68 pub const fn default_message(self) -> &'static str {
69 match self {
70 Self::ParseError => "Parse error",
71 Self::InvalidRequest => "Invalid request",
72 Self::MethodNotFound => "Method not found",
73 Self::InvalidParams => "Invalid params",
74 Self::InternalError => "Internal error",
75 Self::TaskNotFound => "Task not found",
76 Self::TaskNotCancelable => "Task not cancelable",
77 Self::PushNotificationNotSupported => "Push notification not supported",
78 Self::UnsupportedOperation => "Unsupported operation",
79 Self::ContentTypeNotSupported => "Content type not supported",
80 Self::InvalidAgentResponse => "Invalid agent response",
81 Self::ExtendedAgentCardNotConfigured => "Extended agent card not configured",
82 Self::ExtensionSupportRequired => "Extension support required",
83 Self::VersionNotSupported => "Version not supported",
84 }
85 }
86
87 #[must_use]
92 pub const fn a2a_reason(self) -> Option<&'static str> {
93 match self {
94 Self::TaskNotFound => Some("TASK_NOT_FOUND"),
95 Self::TaskNotCancelable => Some("TASK_NOT_CANCELABLE"),
96 Self::PushNotificationNotSupported => Some("PUSH_NOTIFICATION_NOT_SUPPORTED"),
97 Self::UnsupportedOperation => Some("UNSUPPORTED_OPERATION"),
98 Self::ContentTypeNotSupported => Some("CONTENT_TYPE_NOT_SUPPORTED"),
99 Self::InvalidAgentResponse => Some("INVALID_AGENT_RESPONSE"),
100 Self::ExtendedAgentCardNotConfigured => Some("EXTENDED_AGENT_CARD_NOT_CONFIGURED"),
101 Self::ExtensionSupportRequired => Some("EXTENSION_SUPPORT_REQUIRED"),
102 Self::VersionNotSupported => Some("VERSION_NOT_SUPPORTED"),
103 _ => None,
104 }
105 }
106
107 #[must_use]
109 pub const fn http_status(self) -> u16 {
110 match self {
111 Self::TaskNotFound | Self::MethodNotFound => 404,
112 Self::TaskNotCancelable => 409,
113 Self::ContentTypeNotSupported => 415,
114 Self::InvalidAgentResponse => 502,
115 Self::PushNotificationNotSupported
116 | Self::UnsupportedOperation
117 | Self::ExtendedAgentCardNotConfigured
118 | Self::ExtensionSupportRequired
119 | Self::VersionNotSupported
120 | Self::ParseError
121 | Self::InvalidRequest
122 | Self::InvalidParams => 400,
123 Self::InternalError => 500,
124 }
125 }
126
127 #[must_use]
129 pub const fn grpc_status(self) -> &'static str {
130 match self {
131 Self::TaskNotFound => "NOT_FOUND",
132 Self::TaskNotCancelable
133 | Self::ExtendedAgentCardNotConfigured
134 | Self::ExtensionSupportRequired => "FAILED_PRECONDITION",
135 Self::PushNotificationNotSupported
136 | Self::UnsupportedOperation
137 | Self::VersionNotSupported
138 | Self::MethodNotFound => "UNIMPLEMENTED",
139 Self::ContentTypeNotSupported
140 | Self::InvalidParams
141 | Self::InvalidRequest
142 | Self::ParseError => "INVALID_ARGUMENT",
143 Self::InvalidAgentResponse | Self::InternalError => "INTERNAL",
144 }
145 }
146}
147
148impl From<ErrorCode> for i32 {
149 fn from(code: ErrorCode) -> Self {
150 code as Self
151 }
152}
153
154impl TryFrom<i32> for ErrorCode {
155 type Error = i32;
156
157 fn try_from(v: i32) -> Result<Self, Self::Error> {
158 match v {
159 -32700 => Ok(Self::ParseError),
160 -32600 => Ok(Self::InvalidRequest),
161 -32601 => Ok(Self::MethodNotFound),
162 -32602 => Ok(Self::InvalidParams),
163 -32603 => Ok(Self::InternalError),
164 -32001 => Ok(Self::TaskNotFound),
165 -32002 => Ok(Self::TaskNotCancelable),
166 -32003 => Ok(Self::PushNotificationNotSupported),
167 -32004 => Ok(Self::UnsupportedOperation),
168 -32005 => Ok(Self::ContentTypeNotSupported),
169 -32006 => Ok(Self::InvalidAgentResponse),
170 -32007 => Ok(Self::ExtendedAgentCardNotConfigured),
171 -32008 => Ok(Self::ExtensionSupportRequired),
172 -32009 => Ok(Self::VersionNotSupported),
173 other => Err(other),
174 }
175 }
176}
177
178impl fmt::Display for ErrorCode {
179 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180 write!(f, "{} ({})", self.default_message(), self.as_i32())
181 }
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize)]
191#[non_exhaustive]
192pub struct A2aError {
193 pub code: ErrorCode,
195 pub message: String,
197 #[serde(skip_serializing_if = "Option::is_none")]
199 pub data: Option<serde_json::Value>,
200}
201
202impl A2aError {
203 #[must_use]
205 pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
206 Self {
207 code,
208 message: message.into(),
209 data: None,
210 }
211 }
212
213 #[must_use]
215 pub fn with_data(code: ErrorCode, message: impl Into<String>, data: serde_json::Value) -> Self {
216 Self {
217 code,
218 message: message.into(),
219 data: Some(data),
220 }
221 }
222
223 #[must_use]
227 pub fn task_not_found(task_id: impl fmt::Display) -> Self {
228 Self::new(
229 ErrorCode::TaskNotFound,
230 format!("Task not found: {task_id}"),
231 )
232 }
233
234 #[must_use]
236 pub fn task_not_cancelable(task_id: impl fmt::Display) -> Self {
237 Self::new(
238 ErrorCode::TaskNotCancelable,
239 format!("Task cannot be canceled: {task_id}"),
240 )
241 }
242
243 #[must_use]
245 pub fn internal(msg: impl Into<String>) -> Self {
246 Self::new(ErrorCode::InternalError, msg)
247 }
248
249 #[must_use]
251 pub fn invalid_params(msg: impl Into<String>) -> Self {
252 Self::new(ErrorCode::InvalidParams, msg)
253 }
254
255 #[must_use]
257 pub fn unsupported_operation(msg: impl Into<String>) -> Self {
258 Self::new(ErrorCode::UnsupportedOperation, msg)
259 }
260
261 #[must_use]
263 pub fn parse_error(msg: impl Into<String>) -> Self {
264 Self::new(ErrorCode::ParseError, msg)
265 }
266
267 #[must_use]
269 pub fn invalid_agent_response(msg: impl Into<String>) -> Self {
270 Self::new(ErrorCode::InvalidAgentResponse, msg)
271 }
272
273 #[must_use]
275 pub fn extended_card_not_configured(msg: impl Into<String>) -> Self {
276 Self::new(ErrorCode::ExtendedAgentCardNotConfigured, msg)
277 }
278
279 #[must_use]
281 pub fn push_not_supported(msg: impl Into<String>) -> Self {
282 Self::new(ErrorCode::PushNotificationNotSupported, msg)
283 }
284
285 #[must_use]
287 pub fn content_type_not_supported(msg: impl Into<String>) -> Self {
288 Self::new(ErrorCode::ContentTypeNotSupported, msg)
289 }
290
291 #[must_use]
293 pub fn extension_support_required(msg: impl Into<String>) -> Self {
294 Self::new(ErrorCode::ExtensionSupportRequired, msg)
295 }
296
297 #[must_use]
299 pub fn version_not_supported(msg: impl Into<String>) -> Self {
300 Self::new(ErrorCode::VersionNotSupported, msg)
301 }
302
303 #[must_use]
308 pub fn error_info_data(&self, metadata: Option<serde_json::Value>) -> serde_json::Value {
309 self.code
310 .a2a_reason()
311 .map_or(serde_json::Value::Null, |reason| {
312 let mut info = serde_json::json!({
313 "@type": "type.googleapis.com/google.rpc.ErrorInfo",
314 "reason": reason,
315 "domain": "a2a-protocol.org"
316 });
317 if let Some(meta) = metadata {
318 info["metadata"] = meta;
319 }
320 serde_json::json!([info])
321 })
322 }
323}
324
325impl fmt::Display for A2aError {
326 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
327 write!(f, "[{}] {}", self.code.as_i32(), self.message)
328 }
329}
330
331impl std::error::Error for A2aError {}
332
333pub type A2aResult<T> = Result<T, A2aError>;
337
338#[cfg(test)]
341mod tests {
342 use super::*;
343
344 #[test]
345 fn error_code_roundtrip() {
346 let code = ErrorCode::TaskNotFound;
347 let n: i32 = code.into();
348 assert_eq!(n, -32001);
349 assert_eq!(ErrorCode::try_from(n), Ok(ErrorCode::TaskNotFound));
350 }
351
352 #[test]
353 fn error_code_unknown_value() {
354 assert!(ErrorCode::try_from(-99999).is_err());
355 }
356
357 #[test]
358 fn a2a_error_display() {
359 let err = A2aError::task_not_found("abc123");
360 let s = err.to_string();
361 assert!(s.contains("-32001"), "expected code in display: {s}");
362 assert!(s.contains("abc123"), "expected task id in display: {s}");
363 }
364
365 #[test]
366 fn a2a_error_serialization() {
367 let err = A2aError::internal("something went wrong");
368 let json = serde_json::to_string(&err).expect("serialize");
369 let back: A2aError = serde_json::from_str(&json).expect("deserialize");
370 assert_eq!(back.code, ErrorCode::InternalError);
371 assert_eq!(back.message, "something went wrong");
372 assert!(back.data.is_none());
373 }
374
375 #[test]
376 fn a2a_error_with_data() {
377 let data = serde_json::json!({"detail": "extra info"});
378 let err = A2aError::with_data(ErrorCode::InvalidParams, "bad input", data.clone());
379 let json = serde_json::to_string(&err).expect("serialize");
380 assert!(json.contains("\"data\""), "data field should be present");
381 let back: A2aError = serde_json::from_str(&json).expect("deserialize");
382 assert_eq!(back.data, Some(data));
383 }
384
385 #[test]
390 #[allow(clippy::too_many_lines)]
391 fn error_code_roundtrip_all_variants() {
392 let cases: &[(ErrorCode, i32, &str)] = &[
393 (ErrorCode::ParseError, -32700, "Parse error"),
394 (ErrorCode::InvalidRequest, -32600, "Invalid request"),
395 (ErrorCode::MethodNotFound, -32601, "Method not found"),
396 (ErrorCode::InvalidParams, -32602, "Invalid params"),
397 (ErrorCode::InternalError, -32603, "Internal error"),
398 (ErrorCode::TaskNotFound, -32001, "Task not found"),
399 (ErrorCode::TaskNotCancelable, -32002, "Task not cancelable"),
400 (
401 ErrorCode::PushNotificationNotSupported,
402 -32003,
403 "Push notification not supported",
404 ),
405 (
406 ErrorCode::UnsupportedOperation,
407 -32004,
408 "Unsupported operation",
409 ),
410 (
411 ErrorCode::ContentTypeNotSupported,
412 -32005,
413 "Content type not supported",
414 ),
415 (
416 ErrorCode::InvalidAgentResponse,
417 -32006,
418 "Invalid agent response",
419 ),
420 (
421 ErrorCode::ExtendedAgentCardNotConfigured,
422 -32007,
423 "Extended agent card not configured",
424 ),
425 (
426 ErrorCode::ExtensionSupportRequired,
427 -32008,
428 "Extension support required",
429 ),
430 (
431 ErrorCode::VersionNotSupported,
432 -32009,
433 "Version not supported",
434 ),
435 ];
436
437 for &(code, expected_i32, expected_msg) in cases {
438 assert_eq!(code.as_i32(), expected_i32, "as_i32 mismatch for {code:?}");
440
441 let n: i32 = code.into();
443 assert_eq!(n, expected_i32, "Into<i32> mismatch for {code:?}");
444
445 let back = ErrorCode::try_from(expected_i32).expect("try_from should succeed");
447 assert_eq!(back, code, "TryFrom roundtrip mismatch for {code:?}");
448
449 assert_eq!(
451 code.default_message(),
452 expected_msg,
453 "default_message mismatch for {code:?}"
454 );
455
456 let display = code.to_string();
458 assert!(
459 display.contains(expected_msg),
460 "Display missing message for {code:?}: {display}"
461 );
462 assert!(
463 display.contains(&expected_i32.to_string()),
464 "Display missing code for {code:?}: {display}"
465 );
466 }
467 }
468
469 #[test]
472 fn error_code_rejects_adjacent_values() {
473 let invalid: &[i32] = &[
474 -32701,
475 -32699, -32599,
477 -32601 + 1, -32000,
479 -32010, 0,
481 1,
482 -1,
483 i32::MIN,
484 i32::MAX,
485 ];
486 for &v in invalid {
487 if ErrorCode::try_from(v).is_ok() {
489 continue;
490 }
491 assert_eq!(
492 ErrorCode::try_from(v),
493 Err(v),
494 "value {v} should not convert to ErrorCode"
495 );
496 }
497 }
498
499 #[test]
502 fn named_constructors_use_correct_codes() {
503 assert_eq!(A2aError::task_not_found("t1").code, ErrorCode::TaskNotFound);
504 assert_eq!(
505 A2aError::task_not_cancelable("t1").code,
506 ErrorCode::TaskNotCancelable
507 );
508 assert_eq!(A2aError::internal("x").code, ErrorCode::InternalError);
509 assert_eq!(A2aError::invalid_params("x").code, ErrorCode::InvalidParams);
510 assert_eq!(
511 A2aError::unsupported_operation("x").code,
512 ErrorCode::UnsupportedOperation
513 );
514 assert_eq!(A2aError::parse_error("x").code, ErrorCode::ParseError);
515 assert_eq!(
516 A2aError::invalid_agent_response("x").code,
517 ErrorCode::InvalidAgentResponse
518 );
519 assert_eq!(
520 A2aError::extended_card_not_configured("x").code,
521 ErrorCode::ExtendedAgentCardNotConfigured
522 );
523 }
524
525 #[test]
526 fn named_constructors_include_argument_in_message() {
527 let err = A2aError::task_not_found("my-task-id");
528 assert!(
529 err.message.contains("my-task-id"),
530 "task_not_found should include task_id: {}",
531 err.message
532 );
533
534 let err = A2aError::task_not_cancelable("cancel-me");
535 assert!(
536 err.message.contains("cancel-me"),
537 "task_not_cancelable should include task_id: {}",
538 err.message
539 );
540 }
541
542 #[test]
543 fn a2a_error_new_has_no_data() {
544 let err = A2aError::new(ErrorCode::InternalError, "msg");
545 assert!(err.data.is_none());
546 }
547
548 #[test]
549 fn a2a_error_with_data_has_some_data() {
550 let err = A2aError::with_data(
551 ErrorCode::InternalError,
552 "msg",
553 serde_json::json!("details"),
554 );
555 assert!(err.data.is_some());
556 assert_eq!(err.data.unwrap(), serde_json::json!("details"));
557 }
558
559 #[test]
560 fn a2a_error_is_std_error() {
561 let err = A2aError::internal("test");
562 let _: &dyn std::error::Error = &err;
563 }
564
565 #[test]
566 fn a2a_error_display_format() {
567 let err = A2aError::new(ErrorCode::ParseError, "bad json");
568 let s = err.to_string();
569 assert_eq!(s, "[-32700] bad json");
570 }
571}