Skip to main content

a2a_rs/domain/
error.rs

1use thiserror::Error;
2
3use crate::domain::error_details::{ErrorDetail, FieldViolation};
4
5/// Convenience alias for results that fail with [`A2AError`].
6///
7/// Mirrors the `std::io::Result` / `serde_json::Result` convention so call
8/// sites can write `Result<Task>` instead of `Result<Task, A2AError>`.
9pub type Result<T, E = A2AError> = std::result::Result<T, E>;
10
11/// Standard JSON-RPC error codes
12pub const PARSE_ERROR: i32 = -32700;
13pub const INVALID_REQUEST: i32 = -32600;
14pub const METHOD_NOT_FOUND: i32 = -32601;
15pub const INVALID_PARAMS: i32 = -32602;
16pub const INTERNAL_ERROR: i32 = -32603;
17
18/// A2A specific error codes
19pub const TASK_NOT_FOUND: i32 = -32001;
20pub const TASK_NOT_CANCELABLE: i32 = -32002;
21pub const PUSH_NOTIFICATION_NOT_SUPPORTED: i32 = -32003;
22pub const UNSUPPORTED_OPERATION: i32 = -32004;
23pub const CONTENT_TYPE_NOT_SUPPORTED: i32 = -32005;
24pub const INVALID_AGENT_RESPONSE: i32 = -32006;
25pub const AUTHENTICATED_EXTENDED_CARD_NOT_CONFIGURED: i32 = -32007;
26
27/// Custom application-specific error codes (outside spec range)
28pub const DATABASE_ERROR: i32 = -32100;
29/// Optimistic-concurrency version mismatch on a task mutation.
30pub const VERSION_CONFLICT: i32 = -32101;
31
32/// Error type for the A2A protocol operations
33#[derive(Error, Debug)]
34pub enum A2AError {
35    #[error("JSON-RPC error: {code} - {message}")]
36    JsonRpc {
37        code: i32,
38        message: String,
39        data: Option<serde_json::Value>,
40    },
41
42    #[error("JSON parse error: {0}")]
43    JsonParse(#[from] serde_json::Error),
44
45    #[error("Invalid request: {0}")]
46    InvalidRequest(String),
47
48    #[error("Invalid parameters: {0}")]
49    InvalidParams(String),
50
51    #[error("Method not found: {0}")]
52    MethodNotFound(String),
53
54    #[error("Task not found: {0}")]
55    TaskNotFound(String),
56
57    #[error("Task not cancelable: {0}")]
58    TaskNotCancelable(String),
59
60    #[error("Push notification not supported")]
61    PushNotificationNotSupported,
62
63    #[error("Unsupported operation: {0}")]
64    UnsupportedOperation(String),
65
66    #[error("Content type not supported: {0}")]
67    ContentTypeNotSupported(String),
68
69    #[error("Invalid agent response: {0}")]
70    InvalidAgentResponse(String),
71
72    #[error("Authenticated extended card not configured")]
73    AuthenticatedExtendedCardNotConfigured,
74
75    #[error("Internal error: {0}")]
76    Internal(String),
77
78    #[error("Validation error in {field}: {message}")]
79    ValidationError { field: String, message: String },
80
81    #[error("Version conflict for task {id}: expected {expected}, found {actual}")]
82    VersionConflict {
83        id: String,
84        expected: u64,
85        actual: u64,
86    },
87
88    #[error("Database error: {0}")]
89    DatabaseError(String),
90
91    #[error("IO error: {0}")]
92    Io(#[from] std::io::Error),
93}
94
95impl A2AError {
96    /// Convert an A2AError to a JSON-RPC error value
97    pub fn to_jsonrpc_error(&self) -> serde_json::Value {
98        let (code, message) = match self {
99            A2AError::JsonParse(_) => (PARSE_ERROR, "Invalid JSON payload"),
100            A2AError::InvalidRequest(_) => (INVALID_REQUEST, "Request payload validation error"),
101            A2AError::MethodNotFound(_) => (METHOD_NOT_FOUND, "Method not found"),
102            A2AError::InvalidParams(_) => (INVALID_PARAMS, "Invalid parameters"),
103            A2AError::TaskNotFound(_) => (TASK_NOT_FOUND, "Task not found"),
104            A2AError::TaskNotCancelable(_) => (TASK_NOT_CANCELABLE, "Task cannot be canceled"),
105            A2AError::PushNotificationNotSupported => (
106                PUSH_NOTIFICATION_NOT_SUPPORTED,
107                "Push Notification is not supported",
108            ),
109            A2AError::UnsupportedOperation(_) => {
110                (UNSUPPORTED_OPERATION, "This operation is not supported")
111            }
112            A2AError::ContentTypeNotSupported(_) => {
113                (CONTENT_TYPE_NOT_SUPPORTED, "Incompatible content types")
114            }
115            A2AError::InvalidAgentResponse(_) => (INVALID_AGENT_RESPONSE, "Invalid agent response"),
116            A2AError::AuthenticatedExtendedCardNotConfigured => (
117                AUTHENTICATED_EXTENDED_CARD_NOT_CONFIGURED,
118                "Authenticated Extended Card is not configured",
119            ),
120            A2AError::ValidationError { .. } => (INVALID_PARAMS, "Validation error"),
121            A2AError::VersionConflict { .. } => (VERSION_CONFLICT, "Task version conflict"),
122            A2AError::DatabaseError(_) => (DATABASE_ERROR, "Database error"),
123            A2AError::Internal(_) => (INTERNAL_ERROR, "Internal error"),
124            _ => (INTERNAL_ERROR, "Internal error"),
125        };
126
127        serde_json::json!({
128            "code": code,
129            "message": message,
130            "data": null,
131        })
132    }
133
134    /// Stable, machine-readable reason code for this error
135    /// (`SCREAMING_SNAKE_CASE`, used as the `ErrorInfo.reason` on the wire).
136    pub fn reason_code(&self) -> &'static str {
137        match self {
138            A2AError::JsonRpc { .. } => "JSON_RPC_ERROR",
139            A2AError::JsonParse(_) => "PARSE_ERROR",
140            A2AError::InvalidRequest(_) => "INVALID_REQUEST",
141            A2AError::InvalidParams(_) => "INVALID_PARAMS",
142            A2AError::MethodNotFound(_) => "METHOD_NOT_FOUND",
143            A2AError::TaskNotFound(_) => "TASK_NOT_FOUND",
144            A2AError::TaskNotCancelable(_) => "TASK_NOT_CANCELABLE",
145            A2AError::PushNotificationNotSupported => "PUSH_NOTIFICATION_NOT_SUPPORTED",
146            A2AError::UnsupportedOperation(_) => "UNSUPPORTED_OPERATION",
147            A2AError::ContentTypeNotSupported(_) => "CONTENT_TYPE_NOT_SUPPORTED",
148            A2AError::InvalidAgentResponse(_) => "INVALID_AGENT_RESPONSE",
149            A2AError::AuthenticatedExtendedCardNotConfigured => {
150                "AUTHENTICATED_EXTENDED_CARD_NOT_CONFIGURED"
151            }
152            A2AError::Internal(_) => "INTERNAL_ERROR",
153            A2AError::ValidationError { .. } => "VALIDATION_ERROR",
154            A2AError::VersionConflict { .. } => "VERSION_CONFLICT",
155            A2AError::DatabaseError(_) => "DATABASE_ERROR",
156            A2AError::Io(_) => "IO_ERROR",
157        }
158    }
159
160    /// Typed details for the JSON-RPC `error.data` array.
161    ///
162    /// Validation failures surface as a Google-RPC `BadRequest` with field
163    /// violations; version conflicts attach the expected/actual versions as
164    /// `ErrorInfo` metadata; every other variant carries at least its stable
165    /// [`reason_code`](Self::reason_code) as an `ErrorInfo`, so a client can
166    /// branch on a machine code instead of parsing the message string.
167    pub fn error_details(&self) -> Vec<ErrorDetail> {
168        match self {
169            A2AError::ValidationError { field, message } => vec![
170                ErrorDetail::BadRequest {
171                    field_violations: vec![FieldViolation::new(field, message)],
172                },
173                ErrorDetail::reason(self.reason_code()),
174            ],
175            A2AError::VersionConflict {
176                id,
177                expected,
178                actual,
179            } => {
180                let mut info = crate::domain::error_details::ErrorInfo::new(self.reason_code());
181                info = info
182                    .with_metadata("task_id", id)
183                    .with_metadata("expected", expected.to_string())
184                    .with_metadata("actual", actual.to_string());
185                vec![ErrorDetail::ErrorInfo(info)]
186            }
187            _ => vec![ErrorDetail::reason(self.reason_code())],
188        }
189    }
190}