1use std::fmt;
19
20use serde::{Deserialize, Serialize};
21
22use crate::ids::{IdParseError, LeaseId};
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
30#[non_exhaustive]
31#[allow(clippy::upper_case_acronyms)]
32pub enum ErrorCode {
33 #[serde(rename = "OK")]
35 Ok,
36 #[serde(rename = "CANCELLED")]
38 Cancelled,
39 #[serde(rename = "UNKNOWN")]
41 Unknown,
42 #[serde(rename = "INVALID_ARGUMENT")]
44 InvalidArgument,
45 #[serde(rename = "DEADLINE_EXCEEDED")]
47 DeadlineExceeded,
48 #[serde(rename = "NOT_FOUND")]
50 NotFound,
51 #[serde(rename = "ALREADY_EXISTS")]
53 AlreadyExists,
54 #[serde(rename = "PERMISSION_DENIED")]
56 PermissionDenied,
57 #[serde(rename = "RESOURCE_EXHAUSTED", alias = "RATE_LIMITED")]
59 ResourceExhausted,
60 #[serde(rename = "FAILED_PRECONDITION")]
62 FailedPrecondition,
63 #[serde(rename = "ABORTED")]
65 Aborted,
66 #[serde(rename = "OUT_OF_RANGE")]
68 OutOfRange,
69 #[serde(rename = "UNIMPLEMENTED")]
71 Unimplemented,
72 #[serde(rename = "INTERNAL")]
74 Internal,
75 #[serde(rename = "UNAVAILABLE")]
77 Unavailable,
78 #[serde(rename = "DATA_LOSS")]
80 DataLoss,
81 #[serde(rename = "UNAUTHENTICATED")]
83 Unauthenticated,
84 #[serde(rename = "HEARTBEAT_LOST")]
86 HeartbeatLost,
87 #[serde(rename = "LEASE_EXPIRED")]
89 LeaseExpired,
90 #[serde(rename = "LEASE_REVOKED")]
92 LeaseRevoked,
93 #[serde(rename = "BACKPRESSURE_OVERFLOW")]
95 BackpressureOverflow,
96 #[serde(rename = "BUDGET_EXHAUSTED")]
98 BudgetExhausted,
99 #[serde(rename = "LEASE_SUBSET_VIOLATION")]
101 LeaseSubsetViolation,
102 #[serde(rename = "AGENT_VERSION_NOT_AVAILABLE")]
104 AgentVersionNotAvailable,
105}
106
107impl ErrorCode {
108 #[must_use]
115 pub const fn retryable(self) -> bool {
116 matches!(
117 self,
118 Self::ResourceExhausted
119 | Self::Unavailable
120 | Self::DeadlineExceeded
121 | Self::Internal
122 | Self::Aborted
123 )
124 }
125
126 #[must_use]
128 pub const fn as_str(self) -> &'static str {
129 match self {
130 Self::Ok => "OK",
131 Self::Cancelled => "CANCELLED",
132 Self::Unknown => "UNKNOWN",
133 Self::InvalidArgument => "INVALID_ARGUMENT",
134 Self::DeadlineExceeded => "DEADLINE_EXCEEDED",
135 Self::NotFound => "NOT_FOUND",
136 Self::AlreadyExists => "ALREADY_EXISTS",
137 Self::PermissionDenied => "PERMISSION_DENIED",
138 Self::ResourceExhausted => "RESOURCE_EXHAUSTED",
139 Self::FailedPrecondition => "FAILED_PRECONDITION",
140 Self::Aborted => "ABORTED",
141 Self::OutOfRange => "OUT_OF_RANGE",
142 Self::Unimplemented => "UNIMPLEMENTED",
143 Self::Internal => "INTERNAL",
144 Self::Unavailable => "UNAVAILABLE",
145 Self::DataLoss => "DATA_LOSS",
146 Self::Unauthenticated => "UNAUTHENTICATED",
147 Self::HeartbeatLost => "HEARTBEAT_LOST",
148 Self::LeaseExpired => "LEASE_EXPIRED",
149 Self::LeaseRevoked => "LEASE_REVOKED",
150 Self::BackpressureOverflow => "BACKPRESSURE_OVERFLOW",
151 Self::BudgetExhausted => "BUDGET_EXHAUSTED",
152 Self::LeaseSubsetViolation => "LEASE_SUBSET_VIOLATION",
153 Self::AgentVersionNotAvailable => "AGENT_VERSION_NOT_AVAILABLE",
154 }
155 }
156}
157
158impl fmt::Display for ErrorCode {
159 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160 f.write_str(self.as_str())
161 }
162}
163
164#[derive(Debug, thiserror::Error)]
171#[non_exhaustive]
172#[allow(clippy::upper_case_acronyms)]
173pub enum ARCPError {
174 #[error("operation cancelled: {reason}")]
176 Cancelled {
177 reason: String,
179 },
180
181 #[error("invalid argument: {detail}")]
183 InvalidArgument {
184 detail: String,
186 },
187
188 #[error("operation timed out: {detail}")]
190 DeadlineExceeded {
191 detail: String,
193 },
194
195 #[error("not found: {kind} (id={id})")]
197 NotFound {
198 kind: &'static str,
200 id: String,
202 },
203
204 #[error("already exists: {kind} (id={id})")]
206 AlreadyExists {
207 kind: &'static str,
209 id: String,
211 },
212
213 #[error("permission denied: {detail}")]
215 PermissionDenied {
216 detail: String,
218 },
219
220 #[error("resource exhausted: {detail}")]
222 ResourceExhausted {
223 detail: String,
225 retry_after_seconds: Option<u64>,
227 },
228
229 #[error("failed precondition: {detail}")]
231 FailedPrecondition {
232 detail: String,
234 },
235
236 #[error("operation aborted: {detail}")]
238 Aborted {
239 detail: String,
241 },
242
243 #[error("argument out of range: {detail}")]
245 OutOfRange {
246 detail: String,
248 },
249
250 #[error("not implemented (RFC §{section}): {detail}")]
252 Unimplemented {
253 section: &'static str,
255 detail: String,
257 },
258
259 #[error("internal error: {detail}")]
261 Internal {
262 detail: String,
264 },
265
266 #[error("service unavailable: {detail}")]
268 Unavailable {
269 detail: String,
271 },
272
273 #[error("data loss: {detail}")]
275 DataLoss {
276 detail: String,
278 },
279
280 #[error("unauthenticated: {detail}")]
282 Unauthenticated {
283 detail: String,
285 },
286
287 #[error("heartbeat lost: missed_count={missed_count}")]
289 HeartbeatLost {
290 missed_count: u32,
292 },
293
294 #[error("lease expired: lease_id={lease_id}")]
296 LeaseExpired {
297 lease_id: LeaseId,
299 },
300
301 #[error("lease revoked: lease_id={lease_id} (reason={reason})")]
303 LeaseRevoked {
304 lease_id: LeaseId,
306 reason: String,
308 },
309
310 #[error("backpressure overflow: {detail}")]
312 BackpressureOverflow {
313 detail: String,
315 },
316
317 #[error("budget exhausted: {detail}")]
319 BudgetExhausted {
320 detail: String,
322 },
323
324 #[error("lease subset violation: {detail}")]
326 LeaseSubsetViolation {
327 detail: String,
329 },
330
331 #[error("agent version not available: {agent}@{version}")]
333 AgentVersionNotAvailable {
334 agent: String,
336 version: String,
338 },
339
340 #[error("unknown error: {detail}")]
342 Unknown {
343 detail: String,
345 },
346
347 #[error("serialisation error: {0}")]
349 Serialization(#[from] serde_json::Error),
350
351 #[error("storage error: {detail}")]
355 Storage {
356 detail: String,
358 },
359
360 #[error("id parse error: {0}")]
362 Id(#[from] IdParseError),
363}
364
365impl ARCPError {
366 #[must_use]
368 pub const fn code(&self) -> ErrorCode {
369 match self {
370 Self::Cancelled { .. } => ErrorCode::Cancelled,
371 Self::InvalidArgument { .. } | Self::Id(_) => ErrorCode::InvalidArgument,
372 Self::DeadlineExceeded { .. } => ErrorCode::DeadlineExceeded,
373 Self::NotFound { .. } => ErrorCode::NotFound,
374 Self::AlreadyExists { .. } => ErrorCode::AlreadyExists,
375 Self::PermissionDenied { .. } => ErrorCode::PermissionDenied,
376 Self::ResourceExhausted { .. } => ErrorCode::ResourceExhausted,
377 Self::FailedPrecondition { .. } => ErrorCode::FailedPrecondition,
378 Self::Aborted { .. } => ErrorCode::Aborted,
379 Self::OutOfRange { .. } => ErrorCode::OutOfRange,
380 Self::Unimplemented { .. } => ErrorCode::Unimplemented,
381 Self::Internal { .. } | Self::Storage { .. } => ErrorCode::Internal,
382 Self::Unavailable { .. } => ErrorCode::Unavailable,
383 Self::DataLoss { .. } => ErrorCode::DataLoss,
384 Self::Unauthenticated { .. } => ErrorCode::Unauthenticated,
385 Self::HeartbeatLost { .. } => ErrorCode::HeartbeatLost,
386 Self::LeaseExpired { .. } => ErrorCode::LeaseExpired,
387 Self::LeaseRevoked { .. } => ErrorCode::LeaseRevoked,
388 Self::BackpressureOverflow { .. } => ErrorCode::BackpressureOverflow,
389 Self::BudgetExhausted { .. } => ErrorCode::BudgetExhausted,
390 Self::LeaseSubsetViolation { .. } => ErrorCode::LeaseSubsetViolation,
391 Self::AgentVersionNotAvailable { .. } => ErrorCode::AgentVersionNotAvailable,
392 Self::Unknown { .. } | Self::Serialization(_) => ErrorCode::Unknown,
393 }
394 }
395
396 #[must_use]
398 pub const fn retryable(&self) -> bool {
399 self.code().retryable()
400 }
401}
402
403#[cfg(test)]
404#[allow(
405 clippy::expect_used,
406 clippy::unwrap_used,
407 clippy::panic,
408 clippy::missing_panics_doc
409)]
410mod tests {
411 use super::*;
412
413 #[test]
414 fn error_code_round_trips_through_serde() {
415 for code in [
416 ErrorCode::Ok,
417 ErrorCode::Cancelled,
418 ErrorCode::InvalidArgument,
419 ErrorCode::DeadlineExceeded,
420 ErrorCode::NotFound,
421 ErrorCode::AlreadyExists,
422 ErrorCode::PermissionDenied,
423 ErrorCode::ResourceExhausted,
424 ErrorCode::FailedPrecondition,
425 ErrorCode::Aborted,
426 ErrorCode::OutOfRange,
427 ErrorCode::Unimplemented,
428 ErrorCode::Internal,
429 ErrorCode::Unavailable,
430 ErrorCode::DataLoss,
431 ErrorCode::Unauthenticated,
432 ErrorCode::HeartbeatLost,
433 ErrorCode::LeaseExpired,
434 ErrorCode::LeaseRevoked,
435 ErrorCode::BackpressureOverflow,
436 ErrorCode::BudgetExhausted,
437 ErrorCode::LeaseSubsetViolation,
438 ErrorCode::AgentVersionNotAvailable,
439 ErrorCode::Unknown,
440 ] {
441 let s = serde_json::to_string(&code).expect("serialize");
442 let back: ErrorCode = serde_json::from_str(&s).expect("deserialize");
443 assert_eq!(code, back, "round-trip for {code}");
444 assert_eq!(s.trim_matches('"'), code.as_str());
445 }
446 }
447
448 #[test]
449 fn rate_limited_alias_decodes_to_resource_exhausted() {
450 let code: ErrorCode = serde_json::from_str("\"RATE_LIMITED\"").expect("alias");
451 assert_eq!(code, ErrorCode::ResourceExhausted);
452 }
453
454 #[test]
455 fn retryability_matches_rfc_18_3() {
456 for c in [
458 ErrorCode::ResourceExhausted,
459 ErrorCode::Unavailable,
460 ErrorCode::DeadlineExceeded,
461 ErrorCode::Internal,
462 ErrorCode::Aborted,
463 ] {
464 assert!(c.retryable(), "{c} should be retryable");
465 }
466 for c in [
468 ErrorCode::InvalidArgument,
469 ErrorCode::NotFound,
470 ErrorCode::AlreadyExists,
471 ErrorCode::PermissionDenied,
472 ErrorCode::FailedPrecondition,
473 ErrorCode::Unimplemented,
474 ErrorCode::Unauthenticated,
475 ErrorCode::DataLoss,
476 ErrorCode::LeaseSubsetViolation,
477 ] {
478 assert!(!c.retryable(), "{c} should NOT be retryable");
479 }
480 }
481
482 #[test]
483 fn arcp_error_maps_to_canonical_code() {
484 let err = ARCPError::PermissionDenied {
485 detail: "missing lease".into(),
486 };
487 assert_eq!(err.code(), ErrorCode::PermissionDenied);
488 assert!(!err.retryable());
489 }
490
491 #[test]
492 fn id_parse_error_propagates_via_from() {
493 let parse_err: IdParseError = "junk".parse::<crate::ids::SessionId>().unwrap_err();
494 let err: ARCPError = parse_err.into();
495 assert_eq!(err.code(), ErrorCode::InvalidArgument);
496 }
497
498 #[test]
499 fn v1_1_error_codes_serialize_to_wire_strings() {
500 assert_eq!(ErrorCode::BudgetExhausted.as_str(), "BUDGET_EXHAUSTED");
501 assert_eq!(ErrorCode::LeaseExpired.as_str(), "LEASE_EXPIRED");
502 assert_eq!(
503 ErrorCode::LeaseSubsetViolation.as_str(),
504 "LEASE_SUBSET_VIOLATION"
505 );
506 assert_eq!(
507 ErrorCode::AgentVersionNotAvailable.as_str(),
508 "AGENT_VERSION_NOT_AVAILABLE"
509 );
510 assert_eq!(
511 serde_json::to_string(&ErrorCode::BudgetExhausted).expect("serialize"),
512 "\"BUDGET_EXHAUSTED\""
513 );
514 assert_eq!(
515 serde_json::to_string(&ErrorCode::AgentVersionNotAvailable).expect("serialize"),
516 "\"AGENT_VERSION_NOT_AVAILABLE\""
517 );
518 let budget: ErrorCode =
519 serde_json::from_str("\"BUDGET_EXHAUSTED\"").expect("deserialize budget");
520 assert_eq!(budget, ErrorCode::BudgetExhausted);
521 let subset: ErrorCode =
522 serde_json::from_str("\"LEASE_SUBSET_VIOLATION\"").expect("deserialize subset");
523 assert_eq!(subset, ErrorCode::LeaseSubsetViolation);
524 let agent_ver: ErrorCode = serde_json::from_str("\"AGENT_VERSION_NOT_AVAILABLE\"")
525 .expect("deserialize agent version");
526 assert_eq!(agent_ver, ErrorCode::AgentVersionNotAvailable);
527 }
528
529 #[test]
530 fn v1_1_arcp_errors_map_to_canonical_codes() {
531 let budget = ARCPError::BudgetExhausted {
532 detail: "cost.budget USD counter <= 0".into(),
533 };
534 assert_eq!(budget.code(), ErrorCode::BudgetExhausted);
535 assert!(!budget.retryable());
536
537 let subset = ARCPError::LeaseSubsetViolation {
538 detail: "model.use widened".into(),
539 };
540 assert_eq!(subset.code(), ErrorCode::LeaseSubsetViolation);
541 assert!(!subset.retryable());
542
543 let agent_ver = ARCPError::AgentVersionNotAvailable {
544 agent: "summarizer".into(),
545 version: "2.3.0".into(),
546 };
547 assert_eq!(agent_ver.code(), ErrorCode::AgentVersionNotAvailable);
548 assert!(!agent_ver.retryable());
549 }
550
551 #[test]
552 fn serde_error_propagates_via_from() {
553 let parse: Result<serde_json::Value, _> = serde_json::from_str("not-json");
554 let err: ARCPError = parse.unwrap_err().into();
555 assert_eq!(err.code(), ErrorCode::Unknown);
556 }
557}