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/// A caller asked for a conversation belonging to a different principal.
32pub const CONTEXT_ACCESS_DENIED: i32 = -32102;
33
34/// Error type for the A2A protocol operations
35#[derive(Error, Debug)]
36pub enum A2AError {
37    #[error("JSON-RPC error: {code} - {message}")]
38    JsonRpc {
39        code: i32,
40        message: String,
41        data: Option<serde_json::Value>,
42    },
43
44    #[error("JSON parse error: {0}")]
45    JsonParse(#[from] serde_json::Error),
46
47    #[error("Invalid request: {0}")]
48    InvalidRequest(String),
49
50    #[error("Invalid parameters: {0}")]
51    InvalidParams(String),
52
53    #[error("Method not found: {0}")]
54    MethodNotFound(String),
55
56    #[error("Task not found: {0}")]
57    TaskNotFound(String),
58
59    #[error("Task not cancelable: {0}")]
60    TaskNotCancelable(String),
61
62    #[error("Push notification not supported")]
63    PushNotificationNotSupported,
64
65    #[error("Unsupported operation: {0}")]
66    UnsupportedOperation(String),
67
68    #[error("Content type not supported: {0}")]
69    ContentTypeNotSupported(String),
70
71    #[error("Invalid agent response: {0}")]
72    InvalidAgentResponse(String),
73
74    #[error("Authenticated extended card not configured")]
75    AuthenticatedExtendedCardNotConfigured,
76
77    #[error("Internal error: {0}")]
78    Internal(String),
79
80    #[error("Validation error in {field}: {message}")]
81    ValidationError { field: String, message: String },
82
83    #[error("Version conflict for task {id}: expected {expected}, found {actual}")]
84    VersionConflict {
85        id: String,
86        expected: u64,
87        actual: u64,
88    },
89
90    #[error("Database error: {0}")]
91    DatabaseError(String),
92
93    /// The caller does not own this conversation.
94    ///
95    /// A `context_id` groups the tasks of one conversation, and reading it back
96    /// as prompt history means anyone holding the id can read what was said in
97    /// it. This is the refusal for a principal that is not the one that started
98    /// the context.
99    #[error("context {context_id} belongs to another principal")]
100    ContextAccessDenied { context_id: String },
101
102    #[error("IO error: {0}")]
103    Io(#[from] std::io::Error),
104}
105
106impl A2AError {
107    /// Convert an A2AError to a JSON-RPC error value
108    pub fn to_jsonrpc_error(&self) -> serde_json::Value {
109        let (code, message) = match self {
110            A2AError::JsonParse(_) => (PARSE_ERROR, "Invalid JSON payload"),
111            A2AError::InvalidRequest(_) => (INVALID_REQUEST, "Request payload validation error"),
112            A2AError::MethodNotFound(_) => (METHOD_NOT_FOUND, "Method not found"),
113            A2AError::InvalidParams(_) => (INVALID_PARAMS, "Invalid parameters"),
114            A2AError::TaskNotFound(_) => (TASK_NOT_FOUND, "Task not found"),
115            A2AError::TaskNotCancelable(_) => (TASK_NOT_CANCELABLE, "Task cannot be canceled"),
116            A2AError::PushNotificationNotSupported => (
117                PUSH_NOTIFICATION_NOT_SUPPORTED,
118                "Push Notification is not supported",
119            ),
120            A2AError::UnsupportedOperation(_) => {
121                (UNSUPPORTED_OPERATION, "This operation is not supported")
122            }
123            A2AError::ContentTypeNotSupported(_) => {
124                (CONTENT_TYPE_NOT_SUPPORTED, "Incompatible content types")
125            }
126            A2AError::InvalidAgentResponse(_) => (INVALID_AGENT_RESPONSE, "Invalid agent response"),
127            A2AError::AuthenticatedExtendedCardNotConfigured => (
128                AUTHENTICATED_EXTENDED_CARD_NOT_CONFIGURED,
129                "Authenticated Extended Card is not configured",
130            ),
131            A2AError::ValidationError { .. } => (INVALID_PARAMS, "Validation error"),
132            A2AError::VersionConflict { .. } => (VERSION_CONFLICT, "Task version conflict"),
133            A2AError::DatabaseError(_) => (DATABASE_ERROR, "Database error"),
134            A2AError::ContextAccessDenied { .. } => (
135                CONTEXT_ACCESS_DENIED,
136                "Context belongs to another principal",
137            ),
138            A2AError::Internal(_) => (INTERNAL_ERROR, "Internal error"),
139            _ => (INTERNAL_ERROR, "Internal error"),
140        };
141
142        serde_json::json!({
143            "code": code,
144            "message": message,
145            "data": null,
146        })
147    }
148
149    /// Stable, machine-readable reason code for this error
150    /// (`SCREAMING_SNAKE_CASE`, used as the `ErrorInfo.reason` on the wire).
151    pub fn reason_code(&self) -> &'static str {
152        match self {
153            A2AError::JsonRpc { .. } => "JSON_RPC_ERROR",
154            A2AError::JsonParse(_) => "PARSE_ERROR",
155            A2AError::InvalidRequest(_) => "INVALID_REQUEST",
156            A2AError::InvalidParams(_) => "INVALID_PARAMS",
157            A2AError::MethodNotFound(_) => "METHOD_NOT_FOUND",
158            A2AError::TaskNotFound(_) => "TASK_NOT_FOUND",
159            A2AError::TaskNotCancelable(_) => "TASK_NOT_CANCELABLE",
160            A2AError::PushNotificationNotSupported => "PUSH_NOTIFICATION_NOT_SUPPORTED",
161            A2AError::UnsupportedOperation(_) => "UNSUPPORTED_OPERATION",
162            A2AError::ContentTypeNotSupported(_) => "CONTENT_TYPE_NOT_SUPPORTED",
163            A2AError::InvalidAgentResponse(_) => "INVALID_AGENT_RESPONSE",
164            A2AError::AuthenticatedExtendedCardNotConfigured => {
165                "AUTHENTICATED_EXTENDED_CARD_NOT_CONFIGURED"
166            }
167            A2AError::Internal(_) => "INTERNAL_ERROR",
168            A2AError::ValidationError { .. } => "VALIDATION_ERROR",
169            A2AError::VersionConflict { .. } => "VERSION_CONFLICT",
170            A2AError::DatabaseError(_) => "DATABASE_ERROR",
171            A2AError::ContextAccessDenied { .. } => "CONTEXT_ACCESS_DENIED",
172            A2AError::Io(_) => "IO_ERROR",
173        }
174    }
175
176    /// Typed details for the JSON-RPC `error.data` array.
177    ///
178    /// Validation failures surface as a Google-RPC `BadRequest` with field
179    /// violations; version conflicts attach the expected/actual versions as
180    /// `ErrorInfo` metadata; every other variant carries at least its stable
181    /// [`reason_code`](Self::reason_code) as an `ErrorInfo`, so a client can
182    /// branch on a machine code instead of parsing the message string.
183    pub fn error_details(&self) -> Vec<ErrorDetail> {
184        match self {
185            A2AError::ValidationError { field, message } => vec![
186                ErrorDetail::BadRequest {
187                    field_violations: vec![FieldViolation::new(field, message)],
188                },
189                ErrorDetail::reason(self.reason_code()),
190            ],
191            A2AError::VersionConflict {
192                id,
193                expected,
194                actual,
195            } => {
196                let mut info = crate::domain::error_details::ErrorInfo::new(self.reason_code());
197                info = info
198                    .with_metadata("task_id", id)
199                    .with_metadata("expected", expected.to_string())
200                    .with_metadata("actual", actual.to_string());
201                vec![ErrorDetail::ErrorInfo(info)]
202            }
203            _ => vec![ErrorDetail::reason(self.reason_code())],
204        }
205    }
206}