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