Skip to main content

agnt5_sdk_core/
error.rs

1//! Error types for the AGNT5 SDK Core
2
3use thiserror::Error;
4
5/// Indicates whether an error is retryable
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum RetryHint {
8    /// Error is transient and can be retried immediately
9    Retryable,
10    /// Error is transient but should be retried with backoff
11    RetryableWithBackoff,
12    /// Error is permanent and should not be retried
13    NotRetryable,
14}
15
16/// Error codes for programmatic error handling
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum ErrorCode {
19    // Connection errors (retryable)
20    ConnectionFailed,
21    ConnectionTimeout,
22    ServiceUnavailable,
23
24    // Configuration errors (not retryable)
25    InvalidConfiguration,
26    MissingConfiguration,
27
28    // Execution errors
29    ExecutionFailed,
30    ExecutionTimeout,
31    ExecutionSuspended,
32    DurabilityUnavailable,
33    NondeterministicReplay,
34    StaleAuthority,
35    ActivationCancelled,
36    ActivationContended,
37    UnknownOutcome,
38    PayloadConflict,
39    IllegalTransition,
40    StateVersionConflict,
41    RequiredChildUnresolved,
42
43    // Validation errors (not retryable)
44    InvalidInput,
45    InvalidMessage,
46    InvalidState,
47
48    // Resource errors
49    ResourceExhausted,
50    QuotaExceeded,
51
52    // Internal errors
53    InternalError,
54    NotImplemented,
55
56    // Sandbox errors
57    SandboxExecutionFailed,
58    SandboxUnavailable,
59    UnsupportedLanguage,
60    UnsupportedOperation,
61}
62
63#[derive(Error, Debug)]
64pub enum SdkError {
65    // External crate errors with automatic conversion
66    #[error("gRPC transport error: {0}")]
67    Transport(#[from] tonic::transport::Error),
68
69    #[error("gRPC status error: {0}")]
70    Status(#[from] tonic::Status),
71
72    #[error("Serialization error: {0}")]
73    Serialization(#[from] serde_json::Error),
74
75    #[error("Other error: {0}")]
76    Other(#[from] anyhow::Error),
77
78    // AGNT5-specific errors with codes and context
79    #[error("Connection error: {message}")]
80    Connection {
81        message: String,
82        code: ErrorCode,
83        #[source]
84        source: Option<Box<dyn std::error::Error + Send + Sync>>,
85    },
86
87    #[error("Worker registration failed: {message}")]
88    Registration { message: String, code: ErrorCode },
89
90    #[error("Invalid configuration: {message}")]
91    Configuration {
92        message: String,
93        field: Option<String>,
94    },
95
96    #[error("Function invocation error: {message}")]
97    Invocation {
98        message: String,
99        function_name: Option<String>,
100    },
101
102    #[error("State management error: {message}")]
103    State { message: String, code: ErrorCode },
104
105    #[error("Timeout error: {message}")]
106    Timeout {
107        message: String,
108        operation: String,
109        duration_ms: Option<u64>,
110    },
111
112    #[error("Invalid message: {message}")]
113    InvalidMessage {
114        message: String,
115        field: Option<String>,
116    },
117
118    #[error("Execution suspended: {message}")]
119    SuspendedExecution { message: String, reason: String },
120
121    #[error("Durable timer suspended: {}", suspension.timer_key)]
122    DurableSuspension {
123        suspension: Box<crate::pb::WorkerSuspension>,
124    },
125
126    #[error("Replay error: {message}")]
127    ReplayError {
128        message: String,
129        step_id: Option<String>,
130    },
131
132    #[error("Durable activation error ({code:?}): {message}")]
133    Activation {
134        message: String,
135        code: ErrorCode,
136        activation_id: Option<String>,
137        attempt: Option<u32>,
138    },
139
140    #[error("Service call error: {message}")]
141    ServiceCallError { message: String, service: String },
142
143    #[error("Telemetry error: {0}")]
144    TelemetryError(String),
145
146    #[error("Internal error: {0}")]
147    Internal(String),
148
149    #[error("Service unavailable: {message}")]
150    Unavailable {
151        message: String,
152        service: Option<String>,
153    },
154
155    #[error("Invalid argument: {message}")]
156    InvalidArgument {
157        message: String,
158        argument: Option<String>,
159    },
160
161    #[error("LM API error ({status}): {message}")]
162    LmApiError {
163        status: u16,
164        provider: String,
165        message: String,
166        request_id: Option<String>,
167    },
168
169    #[error("Sandbox error ({operation}): {message}")]
170    Sandbox {
171        message: String,
172        operation: String,
173        code: ErrorCode,
174    },
175}
176
177impl SdkError {
178    /// Returns the error code for programmatic error handling
179    pub fn code(&self) -> ErrorCode {
180        match self {
181            Self::Transport(_) | Self::Status(_) => ErrorCode::ConnectionFailed,
182            Self::Connection { code, .. } => *code,
183            Self::Registration { code, .. } => *code,
184            Self::Configuration { .. } => ErrorCode::InvalidConfiguration,
185            Self::Invocation { .. } => ErrorCode::ExecutionFailed,
186            Self::State { code, .. } => *code,
187            Self::Timeout { .. } => ErrorCode::ExecutionTimeout,
188            Self::InvalidMessage { .. } => ErrorCode::InvalidMessage,
189            Self::SuspendedExecution { .. } => ErrorCode::ExecutionSuspended,
190            Self::DurableSuspension { .. } => ErrorCode::ExecutionSuspended,
191            Self::ReplayError { .. } => ErrorCode::ExecutionFailed,
192            Self::Activation { code, .. } => *code,
193            Self::ServiceCallError { .. } => ErrorCode::ServiceUnavailable,
194            Self::TelemetryError(_) => ErrorCode::InternalError,
195            Self::Internal(_) => ErrorCode::InternalError,
196            Self::Unavailable { .. } => ErrorCode::ServiceUnavailable,
197            Self::InvalidArgument { .. } => ErrorCode::InvalidInput,
198            Self::Serialization(_) => ErrorCode::InvalidInput,
199            Self::Other(_) => ErrorCode::InternalError,
200            Self::LmApiError { status, .. } => match *status {
201                401 | 403 => ErrorCode::InvalidConfiguration,
202                429 => ErrorCode::ResourceExhausted,
203                408 => ErrorCode::ExecutionTimeout,
204                500 | 502 | 503 | 504 | 529 => ErrorCode::ServiceUnavailable,
205                _ => ErrorCode::InvalidInput,
206            },
207            Self::Sandbox { code, .. } => *code,
208        }
209    }
210
211    /// Returns a hint about whether this error is retryable
212    pub fn retry_hint(&self) -> RetryHint {
213        match self {
214            // Network errors are retryable with backoff
215            Self::Transport(_) | Self::Status(_) => RetryHint::RetryableWithBackoff,
216
217            Self::Connection { code, .. } => match code {
218                ErrorCode::ConnectionTimeout | ErrorCode::ServiceUnavailable => {
219                    RetryHint::RetryableWithBackoff
220                }
221                _ => RetryHint::NotRetryable,
222            },
223
224            // Service unavailable is retryable
225            Self::Unavailable { .. } => RetryHint::RetryableWithBackoff,
226            Self::ServiceCallError { .. } => RetryHint::RetryableWithBackoff,
227
228            // Timeouts are retryable
229            Self::Timeout { .. } => RetryHint::Retryable,
230
231            // Resource exhaustion might be retryable after backoff
232            Self::State {
233                code: ErrorCode::ResourceExhausted,
234                ..
235            } => RetryHint::RetryableWithBackoff,
236
237            // Configuration, validation, and internal errors are not retryable
238            Self::Configuration { .. }
239            | Self::InvalidMessage { .. }
240            | Self::InvalidArgument { .. }
241            | Self::Serialization(_)
242            | Self::Internal(_)
243            | Self::TelemetryError(_) => RetryHint::NotRetryable,
244
245            // Execution errors depend on the specific error
246            Self::Invocation { .. } | Self::ReplayError { .. } => RetryHint::NotRetryable,
247
248            // Activation decisions are journal-authoritative. Callers must
249            // begin again or inspect the typed receipt instead of retrying a
250            // failed correctness write generically.
251            Self::Activation { .. } => RetryHint::NotRetryable,
252
253            // Suspended execution is not an error to retry
254            Self::SuspendedExecution { .. } => RetryHint::NotRetryable,
255            Self::DurableSuspension { .. } => RetryHint::NotRetryable,
256
257            // Registration errors are not retryable
258            Self::Registration { .. } => RetryHint::NotRetryable,
259
260            // Default for state errors
261            Self::State { .. } => RetryHint::NotRetryable,
262
263            // Unknown errors are not retryable by default
264            Self::Other(_) => RetryHint::NotRetryable,
265
266            // LM API errors: retry on transient HTTP status codes
267            Self::LmApiError { status, .. } => match *status {
268                408 | 429 | 500 | 502 | 503 | 504 | 529 => RetryHint::RetryableWithBackoff,
269                _ => RetryHint::NotRetryable,
270            },
271
272            // Sandbox errors
273            Self::Sandbox { code, .. } => match code {
274                ErrorCode::SandboxUnavailable => RetryHint::RetryableWithBackoff,
275                _ => RetryHint::NotRetryable,
276            },
277        }
278    }
279
280    /// Returns true if this error suggests the operation should be retried
281    pub fn is_retryable(&self) -> bool {
282        matches!(
283            self.retry_hint(),
284            RetryHint::Retryable | RetryHint::RetryableWithBackoff
285        )
286    }
287}
288
289pub type Result<T> = std::result::Result<T, SdkError>;