Skip to main content

artifactize_core/agent/
error.rs

1//! Why an Agent review failed, as a request `errorCode`.
2
3use std::fmt;
4
5/// The `errorCode` of a request whose Agent review failed.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
7#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
8pub enum Code {
9    /// Credentials are missing, expired or rejected (HTTP 401 or 403).
10    Authentication,
11    /// A billing, credit or plan usage limit.
12    Quota,
13    /// Rate limited (HTTP 429) after the retries, or asked to wait past the deadline.
14    RateLimit,
15    /// A connection failure, an overloaded or failing server, or an interrupted stream,
16    /// after the retries or once the turn had produced output.
17    Transient,
18    /// The review's `timeoutMs` deadline passed.
19    Timeout,
20    Cancelled,
21    /// `maxTokens` or `maxToolCalls` was exceeded.
22    ProviderBudgetExceeded,
23    /// No valid verdict after the one format repair.
24    InvalidResult,
25    /// Any other provider failure: a rejected request, an unknown or different model,
26    /// an incomplete response or a malformed tool call.
27    ProviderError,
28    /// Anything else, such as unusable configuration or tools.
29    AgentError,
30}
31
32/// An Agent failure's code as its saved `errorCode`.
33impl From<Code> for crate::types::FailureCode {
34    fn from(code: Code) -> Self {
35        match code {
36            Code::Authentication => Self::Authentication,
37            Code::Quota => Self::Quota,
38            Code::RateLimit => Self::RateLimit,
39            Code::Transient => Self::Transient,
40            Code::Timeout => Self::Timeout,
41            Code::Cancelled => Self::Cancelled,
42            Code::ProviderBudgetExceeded => Self::ProviderBudgetExceeded,
43            Code::InvalidResult => Self::InvalidResult,
44            Code::ProviderError => Self::ProviderError,
45            Code::AgentError => Self::AgentError,
46        }
47    }
48}
49
50impl Code {
51    pub fn as_str(self) -> &'static str {
52        match self {
53            Self::Authentication => "AUTHENTICATION",
54            Self::Quota => "QUOTA",
55            Self::RateLimit => "RATE_LIMIT",
56            Self::Transient => "TRANSIENT",
57            Self::Timeout => "TIMEOUT",
58            Self::Cancelled => "CANCELLED",
59            Self::ProviderBudgetExceeded => "PROVIDER_BUDGET_EXCEEDED",
60            Self::InvalidResult => "INVALID_RESULT",
61            Self::ProviderError => "PROVIDER_ERROR",
62            Self::AgentError => "AGENT_ERROR",
63        }
64    }
65
66    /// Failures that a turn without output may retry.
67    pub fn retryable(self) -> bool {
68        matches!(self, Self::RateLimit | Self::Transient)
69    }
70
71    /// Failures that every later review on the backend would repeat: the Run stops
72    /// admitting reviews on it.
73    pub fn stops_backend(code: &str) -> bool {
74        code == Self::Authentication.as_str() || code == Self::Quota.as_str()
75    }
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct Failure {
80    pub code: Code,
81    pub message: String,
82}
83
84impl Failure {
85    pub fn new(code: Code, message: impl Into<String>) -> Self {
86        Self {
87            code,
88            message: message.into(),
89        }
90    }
91
92    pub fn cancelled() -> Self {
93        Self::new(Code::Cancelled, "Agent review was cancelled.")
94    }
95
96    pub fn timeout() -> Self {
97        Self::new(Code::Timeout, "Agent review timed out.")
98    }
99}
100
101/// An unclassified failure, such as a tool or configuration error.
102impl From<String> for Failure {
103    fn from(message: String) -> Self {
104        Self::new(Code::AgentError, message)
105    }
106}
107
108impl fmt::Display for Failure {
109    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
110        formatter.write_str(&self.message)
111    }
112}