1use std::collections::HashMap;
4
5use chrono::Utc;
6use serde_json::Value;
7
8use crate::errordetails::{self, TypedDetail};
9
10pub mod error_code {
12 pub const TASK_NOT_FOUND: i32 = -32001;
14 pub const TASK_NOT_CANCELABLE: i32 = -32002;
15 pub const PUSH_NOTIFICATION_NOT_SUPPORTED: i32 = -32003;
16 pub const UNSUPPORTED_OPERATION: i32 = -32004;
17 pub const CONTENT_TYPE_NOT_SUPPORTED: i32 = -32005;
18 pub const INVALID_AGENT_RESPONSE: i32 = -32006;
19 pub const EXTENDED_CARD_NOT_CONFIGURED: i32 = -32007;
20 pub const EXTENSION_SUPPORT_REQUIRED: i32 = -32008;
21 pub const VERSION_NOT_SUPPORTED: i32 = -32009;
22
23 pub const PARSE_ERROR: i32 = -32700;
25 pub const INVALID_REQUEST: i32 = -32600;
26 pub const METHOD_NOT_FOUND: i32 = -32601;
27 pub const INVALID_PARAMS: i32 = -32602;
28 pub const INTERNAL_ERROR: i32 = -32603;
29}
30
31pub fn error_reason(code: i32) -> &'static str {
33 match code {
34 error_code::TASK_NOT_FOUND => "TASK_NOT_FOUND",
35 error_code::TASK_NOT_CANCELABLE => "TASK_NOT_CANCELABLE",
36 error_code::PUSH_NOTIFICATION_NOT_SUPPORTED => "PUSH_NOTIFICATION_NOT_SUPPORTED",
37 error_code::UNSUPPORTED_OPERATION => "UNSUPPORTED_OPERATION",
38 error_code::CONTENT_TYPE_NOT_SUPPORTED => "CONTENT_TYPE_NOT_SUPPORTED",
39 error_code::INVALID_AGENT_RESPONSE => "INVALID_AGENT_RESPONSE",
40 error_code::EXTENDED_CARD_NOT_CONFIGURED => "EXTENDED_AGENT_CARD_NOT_CONFIGURED",
41 error_code::EXTENSION_SUPPORT_REQUIRED => "EXTENSION_SUPPORT_REQUIRED",
42 error_code::VERSION_NOT_SUPPORTED => "VERSION_NOT_SUPPORTED",
43 error_code::PARSE_ERROR => "PARSE_ERROR",
44 error_code::INVALID_REQUEST => "INVALID_REQUEST",
45 error_code::METHOD_NOT_FOUND => "METHOD_NOT_FOUND",
46 error_code::INVALID_PARAMS => "INVALID_PARAMS",
47 _ => "INTERNAL_ERROR",
48 }
49}
50
51pub fn reason_to_error_code(reason: &str) -> Option<i32> {
53 match reason {
54 "TASK_NOT_FOUND" => Some(error_code::TASK_NOT_FOUND),
55 "TASK_NOT_CANCELABLE" => Some(error_code::TASK_NOT_CANCELABLE),
56 "PUSH_NOTIFICATION_NOT_SUPPORTED" => Some(error_code::PUSH_NOTIFICATION_NOT_SUPPORTED),
57 "UNSUPPORTED_OPERATION" => Some(error_code::UNSUPPORTED_OPERATION),
58 "UNSUPPORTED_CONTENT_TYPE" | "CONTENT_TYPE_NOT_SUPPORTED" => {
59 Some(error_code::CONTENT_TYPE_NOT_SUPPORTED)
60 }
61 "INVALID_AGENT_RESPONSE" => Some(error_code::INVALID_AGENT_RESPONSE),
62 "EXTENDED_AGENT_CARD_NOT_CONFIGURED" | "EXTENDED_CARD_NOT_CONFIGURED" => {
63 Some(error_code::EXTENDED_CARD_NOT_CONFIGURED)
64 }
65 "EXTENSION_SUPPORT_REQUIRED" => Some(error_code::EXTENSION_SUPPORT_REQUIRED),
66 "VERSION_NOT_SUPPORTED" => Some(error_code::VERSION_NOT_SUPPORTED),
67 "PARSE_ERROR" => Some(error_code::PARSE_ERROR),
68 "INVALID_REQUEST" => Some(error_code::INVALID_REQUEST),
69 "METHOD_NOT_FOUND" => Some(error_code::METHOD_NOT_FOUND),
70 "INVALID_PARAMS" => Some(error_code::INVALID_PARAMS),
71 "INTERNAL_ERROR" => Some(error_code::INTERNAL_ERROR),
72 _ => None,
73 }
74}
75
76#[derive(Debug, Clone, thiserror::Error)]
78#[error("{message}")]
79pub struct A2AError {
80 pub code: i32,
81 pub message: String,
82 pub details: Option<Vec<TypedDetail>>,
83}
84
85impl A2AError {
86 pub fn new(code: i32, message: impl Into<String>) -> Self {
87 A2AError {
88 code,
89 message: message.into(),
90 details: None,
91 }
92 }
93
94 pub fn with_details(mut self, details: Vec<TypedDetail>) -> Self {
95 self.details = Some(details);
96 self
97 }
98
99 pub fn task_not_found(task_id: &str) -> Self {
102 A2AError::new(
103 error_code::TASK_NOT_FOUND,
104 format!("task not found: {task_id}"),
105 )
106 }
107
108 pub fn task_not_cancelable(task_id: &str) -> Self {
109 A2AError::new(
110 error_code::TASK_NOT_CANCELABLE,
111 format!("task cannot be canceled: {task_id}"),
112 )
113 }
114
115 pub fn push_notification_not_supported() -> Self {
116 A2AError::new(
117 error_code::PUSH_NOTIFICATION_NOT_SUPPORTED,
118 "push notification not supported",
119 )
120 }
121
122 pub fn unsupported_operation(msg: impl Into<String>) -> Self {
123 A2AError::new(error_code::UNSUPPORTED_OPERATION, msg)
124 }
125
126 pub fn extended_card_not_configured() -> Self {
131 A2AError::new(
132 error_code::EXTENDED_CARD_NOT_CONFIGURED,
133 "extended agent card not configured",
134 )
135 }
136
137 pub fn content_type_not_supported() -> Self {
138 A2AError::new(
139 error_code::CONTENT_TYPE_NOT_SUPPORTED,
140 "incompatible content types",
141 )
142 }
143
144 pub fn invalid_agent_response() -> Self {
145 A2AError::new(error_code::INVALID_AGENT_RESPONSE, "invalid agent response")
146 }
147
148 pub fn version_not_supported(version: &str) -> Self {
149 A2AError::new(
150 error_code::VERSION_NOT_SUPPORTED,
151 format!("version not supported: {version}"),
152 )
153 }
154
155 pub fn internal(msg: impl Into<String>) -> Self {
156 A2AError::new(error_code::INTERNAL_ERROR, msg)
157 }
158
159 pub fn invalid_params(msg: impl Into<String>) -> Self {
160 A2AError::new(error_code::INVALID_PARAMS, msg)
161 }
162
163 pub fn parse_error(msg: impl Into<String>) -> Self {
164 A2AError::new(error_code::PARSE_ERROR, msg)
165 }
166
167 pub fn invalid_request(msg: impl Into<String>) -> Self {
168 A2AError::new(error_code::INVALID_REQUEST, msg)
169 }
170
171 pub fn method_not_found(method: &str) -> Self {
172 A2AError::new(
173 error_code::METHOD_NOT_FOUND,
174 format!("method not found: {method}"),
175 )
176 }
177
178 pub fn http_status_code(&self) -> u16 {
180 match self.code {
181 error_code::TASK_NOT_FOUND => 404,
182 error_code::TASK_NOT_CANCELABLE => 400,
183 error_code::PUSH_NOTIFICATION_NOT_SUPPORTED => 400,
184 error_code::UNSUPPORTED_OPERATION => 400,
185 error_code::CONTENT_TYPE_NOT_SUPPORTED => 400,
186 error_code::VERSION_NOT_SUPPORTED => 400,
187 error_code::EXTENDED_CARD_NOT_CONFIGURED => 400,
192 error_code::EXTENSION_SUPPORT_REQUIRED => 400,
193 error_code::PARSE_ERROR => 400,
194 error_code::INVALID_REQUEST => 400,
195 error_code::METHOD_NOT_FOUND => 501,
196 error_code::INVALID_PARAMS => 400,
197 error_code::INTERNAL_ERROR => 500,
198 _ => 500,
199 }
200 }
201
202 pub fn to_jsonrpc_error(&self) -> crate::JsonRpcError {
208 let reason = error_reason(self.code);
209 let metadata = HashMap::from([("timestamp".to_string(), Utc::now().to_rfc3339())]);
210
211 let mut data: Vec<Value> = self
212 .details
213 .as_ref()
214 .map(|d| {
215 d.iter()
216 .filter(|detail| detail.type_url != errordetails::ERROR_INFO_TYPE)
217 .map(|detail| serde_json::to_value(detail).unwrap_or_default())
218 .collect()
219 })
220 .unwrap_or_default();
221
222 let mut error_info =
223 TypedDetail::error_info(reason, errordetails::PROTOCOL_DOMAIN, Some(metadata));
224
225 if let Some(details) = &self.details {
226 for existing in details
227 .iter()
228 .filter(|d| d.type_url == errordetails::ERROR_INFO_TYPE)
229 {
230 if let Some(Value::Object(meta)) = existing.value.get("metadata") {
231 if let Some(Value::Object(info_meta)) = error_info.value.get_mut("metadata") {
232 for (k, v) in meta {
233 info_meta.entry(k.clone()).or_insert_with(|| v.clone());
234 }
235 }
236 }
237 }
238 }
239
240 data.push(serde_json::to_value(&error_info).unwrap_or_default());
241
242 crate::JsonRpcError {
243 code: self.code,
244 message: self.message.clone(),
245 data: Some(Value::Array(data)),
246 }
247 }
248}
249
250impl From<A2AError> for crate::JsonRpcError {
251 fn from(e: A2AError) -> Self {
252 e.to_jsonrpc_error()
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 #[test]
261 fn test_error_constructors() {
262 let e = A2AError::task_not_found("t1");
263 assert_eq!(e.code, error_code::TASK_NOT_FOUND);
264 assert!(e.message.contains("t1"));
265
266 let e = A2AError::task_not_cancelable("t2");
267 assert_eq!(e.code, error_code::TASK_NOT_CANCELABLE);
268
269 let e = A2AError::push_notification_not_supported();
270 assert_eq!(e.code, error_code::PUSH_NOTIFICATION_NOT_SUPPORTED);
271
272 let e = A2AError::unsupported_operation("nope");
273 assert_eq!(e.code, error_code::UNSUPPORTED_OPERATION);
274
275 let e = A2AError::content_type_not_supported();
276 assert_eq!(e.code, error_code::CONTENT_TYPE_NOT_SUPPORTED);
277
278 let e = A2AError::invalid_agent_response();
279 assert_eq!(e.code, error_code::INVALID_AGENT_RESPONSE);
280
281 let e = A2AError::version_not_supported("2.0");
282 assert_eq!(e.code, error_code::VERSION_NOT_SUPPORTED);
283
284 let e = A2AError::internal("boom");
285 assert_eq!(e.code, error_code::INTERNAL_ERROR);
286
287 let e = A2AError::invalid_params("bad param");
288 assert_eq!(e.code, error_code::INVALID_PARAMS);
289
290 let e = A2AError::parse_error("bad json");
291 assert_eq!(e.code, error_code::PARSE_ERROR);
292
293 let e = A2AError::invalid_request("bad req");
294 assert_eq!(e.code, error_code::INVALID_REQUEST);
295
296 let e = A2AError::method_not_found("foo");
297 assert_eq!(e.code, error_code::METHOD_NOT_FOUND);
298 }
299
300 #[test]
301 fn test_http_status_codes() {
302 assert_eq!(A2AError::task_not_found("x").http_status_code(), 404);
303 assert_eq!(A2AError::task_not_cancelable("x").http_status_code(), 400);
304 assert_eq!(A2AError::internal("x").http_status_code(), 500);
305 assert_eq!(A2AError::invalid_params("x").http_status_code(), 400);
306 assert_eq!(
307 A2AError::content_type_not_supported().http_status_code(),
308 400
309 );
310 assert_eq!(A2AError::new(9999, "unknown").http_status_code(), 500);
311 }
312
313 #[test]
314 fn test_http_status_codes_for_remaining_a2a_mappings() {
315 assert_eq!(
316 A2AError::push_notification_not_supported().http_status_code(),
317 400
318 );
319 assert_eq!(
320 A2AError::unsupported_operation("nope").http_status_code(),
321 400
322 );
323 assert_eq!(
324 A2AError::version_not_supported("9.9").http_status_code(),
325 400
326 );
327 assert_eq!(A2AError::parse_error("bad").http_status_code(), 400);
328 assert_eq!(A2AError::invalid_request("bad").http_status_code(), 400);
329 assert_eq!(
330 A2AError::method_not_found("missing").http_status_code(),
331 501
332 );
333 }
334
335 #[test]
340 fn test_precondition_codes_are_client_errors_not_server_faults() {
341 assert_eq!(
342 A2AError::extended_card_not_configured().http_status_code(),
343 400
344 );
345 assert_eq!(
346 A2AError::new(error_code::EXTENSION_SUPPORT_REQUIRED, "needs ext").http_status_code(),
347 400
348 );
349 }
350
351 #[test]
352 fn test_extended_card_not_configured_carries_its_own_code() {
353 let error = A2AError::extended_card_not_configured();
354 assert_eq!(error.code, error_code::EXTENDED_CARD_NOT_CONFIGURED);
355 assert_ne!(error.code, error_code::UNSUPPORTED_OPERATION);
356 assert_eq!(
357 error_reason(error.code),
358 "EXTENDED_AGENT_CARD_NOT_CONFIGURED"
359 );
360 }
361
362 #[test]
363 fn test_to_jsonrpc_error() {
364 let e = A2AError::task_not_found("t1");
365 let rpc = e.to_jsonrpc_error();
366 assert_eq!(rpc.code, error_code::TASK_NOT_FOUND);
367 assert!(rpc.message.contains("t1"));
368 let data = rpc.data.expect("data should always be present");
369 let arr = data.as_array().expect("data should be an array");
370 assert_eq!(arr.len(), 1);
371 assert_eq!(arr[0]["@type"], errordetails::ERROR_INFO_TYPE);
372 assert_eq!(arr[0]["reason"], "TASK_NOT_FOUND");
373 assert_eq!(arr[0]["domain"], errordetails::PROTOCOL_DOMAIN);
374 assert!(arr[0]["metadata"]["timestamp"].is_string());
375 }
376
377 #[test]
378 fn test_with_details() {
379 use std::collections::HashMap;
380 let struct_detail = TypedDetail::from_struct(HashMap::from([(
381 "key".to_string(),
382 Value::String("val".to_string()),
383 )]));
384 let e = A2AError::internal("err").with_details(vec![struct_detail]);
385
386 let rpc = e.to_jsonrpc_error();
387 let data = rpc.data.expect("data should always be present");
388 let arr = data.as_array().unwrap();
389 assert_eq!(arr.len(), 2);
390 assert_eq!(arr[0]["key"], "val");
391 assert_eq!(arr[1]["@type"], errordetails::ERROR_INFO_TYPE);
392 assert_eq!(arr[1]["reason"], "INTERNAL_ERROR");
393 }
394
395 #[test]
396 fn test_to_jsonrpc_error_merges_existing_error_info_metadata() {
397 let existing_info = TypedDetail::error_info(
398 "TASK_NOT_FOUND",
399 errordetails::PROTOCOL_DOMAIN,
400 Some(HashMap::from([("taskId".to_string(), "t1".to_string())])),
401 );
402 let e = A2AError::task_not_found("t1").with_details(vec![existing_info]);
403 let rpc = e.to_jsonrpc_error();
404 let data = rpc.data.unwrap();
405 let arr = data.as_array().unwrap();
406 assert_eq!(arr.len(), 1);
408 assert_eq!(arr[0]["@type"], errordetails::ERROR_INFO_TYPE);
409 assert_eq!(arr[0]["metadata"]["taskId"], "t1");
410 assert!(arr[0]["metadata"]["timestamp"].is_string());
411 }
412
413 #[test]
414 fn test_reason_to_error_code() {
415 assert_eq!(
416 reason_to_error_code("TASK_NOT_FOUND"),
417 Some(error_code::TASK_NOT_FOUND)
418 );
419 assert_eq!(
420 reason_to_error_code("TASK_NOT_CANCELABLE"),
421 Some(error_code::TASK_NOT_CANCELABLE)
422 );
423 assert_eq!(
424 reason_to_error_code("PUSH_NOTIFICATION_NOT_SUPPORTED"),
425 Some(error_code::PUSH_NOTIFICATION_NOT_SUPPORTED)
426 );
427 assert_eq!(
428 reason_to_error_code("UNSUPPORTED_OPERATION"),
429 Some(error_code::UNSUPPORTED_OPERATION)
430 );
431 assert_eq!(
432 reason_to_error_code("CONTENT_TYPE_NOT_SUPPORTED"),
433 Some(error_code::CONTENT_TYPE_NOT_SUPPORTED)
434 );
435 assert_eq!(
436 reason_to_error_code("UNSUPPORTED_CONTENT_TYPE"),
437 Some(error_code::CONTENT_TYPE_NOT_SUPPORTED)
438 );
439 assert_eq!(
440 reason_to_error_code("INVALID_AGENT_RESPONSE"),
441 Some(error_code::INVALID_AGENT_RESPONSE)
442 );
443 assert_eq!(
444 reason_to_error_code("EXTENDED_AGENT_CARD_NOT_CONFIGURED"),
445 Some(error_code::EXTENDED_CARD_NOT_CONFIGURED)
446 );
447 assert_eq!(
448 reason_to_error_code("EXTENDED_CARD_NOT_CONFIGURED"),
449 Some(error_code::EXTENDED_CARD_NOT_CONFIGURED)
450 );
451 assert_eq!(
452 reason_to_error_code("EXTENSION_SUPPORT_REQUIRED"),
453 Some(error_code::EXTENSION_SUPPORT_REQUIRED)
454 );
455 assert_eq!(
456 reason_to_error_code("VERSION_NOT_SUPPORTED"),
457 Some(error_code::VERSION_NOT_SUPPORTED)
458 );
459 assert_eq!(
460 reason_to_error_code("PARSE_ERROR"),
461 Some(error_code::PARSE_ERROR)
462 );
463 assert_eq!(
464 reason_to_error_code("INVALID_REQUEST"),
465 Some(error_code::INVALID_REQUEST)
466 );
467 assert_eq!(
468 reason_to_error_code("METHOD_NOT_FOUND"),
469 Some(error_code::METHOD_NOT_FOUND)
470 );
471 assert_eq!(
472 reason_to_error_code("INVALID_PARAMS"),
473 Some(error_code::INVALID_PARAMS)
474 );
475 assert_eq!(
476 reason_to_error_code("INTERNAL_ERROR"),
477 Some(error_code::INTERNAL_ERROR)
478 );
479 assert_eq!(reason_to_error_code("UNKNOWN_REASON"), None);
480 }
481
482 #[test]
483 fn test_error_display() {
484 let e = A2AError::internal("test message");
485 assert_eq!(format!("{e}"), "test message");
486 }
487
488 #[test]
489 fn test_jsonrpc_error_from() {
490 let e = A2AError::internal("test");
491 let rpc: crate::JsonRpcError = e.into();
492 assert_eq!(rpc.code, error_code::INTERNAL_ERROR);
493 }
494}