Skip to main content

adk_managed/types/
error.rs

1//! Runtime error types for the managed agent runtime.
2
3use thiserror::Error;
4
5/// Runtime errors aligned with CANON §5 error model.
6///
7/// Each variant represents a distinct failure mode with structured context
8/// for programmatic handling by the platform layer.
9///
10/// # Example
11///
12/// ```
13/// use adk_managed::types::RuntimeError;
14///
15/// let err = RuntimeError::NotFound {
16///     session_id: "ses_abc123".to_string(),
17/// };
18/// assert_eq!(err.to_string(), "session not found: ses_abc123");
19/// ```
20#[derive(Debug, Error)]
21#[non_exhaustive]
22pub enum RuntimeError {
23    /// The request is malformed or contains invalid parameters.
24    #[error("invalid request: {message}")]
25    InvalidRequest {
26        /// Description of what is invalid.
27        message: String,
28        /// The specific parameter that is invalid, if applicable.
29        param: Option<String>,
30    },
31
32    /// The requested session was not found.
33    #[error("session not found: {session_id}")]
34    NotFound {
35        /// The session ID that could not be found.
36        session_id: String,
37    },
38
39    /// A state conflict occurred (e.g., invalid status transition).
40    #[error("conflict: {message}")]
41    Conflict {
42        /// Description of the conflict.
43        message: String,
44    },
45
46    /// The underlying LLM provider returned an error.
47    #[error("provider error ({provider}): {message}")]
48    ProviderError {
49        /// The provider that failed (e.g., "gemini", "openai").
50        provider: String,
51        /// The error message from the provider.
52        message: String,
53    },
54
55    /// A tool call timed out waiting for a response.
56    #[error("tool timeout: {tool_use_id} after {timeout_secs}s")]
57    ToolTimeout {
58        /// The ID of the tool call that timed out.
59        tool_use_id: String,
60        /// The timeout duration in seconds.
61        timeout_secs: u64,
62    },
63
64    /// A checkpoint persistence operation failed.
65    #[error("checkpoint failed: {message}")]
66    CheckpointFailed {
67        /// Description of the checkpoint failure.
68        message: String,
69    },
70
71    /// A sandbox execution error occurred.
72    #[error("sandbox error: {message}")]
73    SandboxError {
74        /// Description of the sandbox error.
75        message: String,
76    },
77
78    /// An internal runtime error that should not normally occur.
79    #[error("internal error: {message}")]
80    Internal {
81        /// Description of the internal error.
82        message: String,
83    },
84}
85
86impl RuntimeError {
87    /// Creates an `InvalidRequest` error with the given message.
88    pub fn invalid_request(message: impl Into<String>) -> Self {
89        Self::InvalidRequest { message: message.into(), param: None }
90    }
91
92    /// Creates an `InvalidRequest` error with a specific parameter name.
93    pub fn invalid_param(message: impl Into<String>, param: impl Into<String>) -> Self {
94        Self::InvalidRequest { message: message.into(), param: Some(param.into()) }
95    }
96
97    /// Creates a `NotFound` error for the given session ID.
98    pub fn not_found(session_id: impl Into<String>) -> Self {
99        Self::NotFound { session_id: session_id.into() }
100    }
101
102    /// Creates a `Conflict` error with the given message.
103    pub fn conflict(message: impl Into<String>) -> Self {
104        Self::Conflict { message: message.into() }
105    }
106
107    /// Creates a `ProviderError` with provider name and message.
108    pub fn provider_error(provider: impl Into<String>, message: impl Into<String>) -> Self {
109        Self::ProviderError { provider: provider.into(), message: message.into() }
110    }
111
112    /// Creates a `ToolTimeout` error.
113    pub fn tool_timeout(tool_use_id: impl Into<String>, timeout_secs: u64) -> Self {
114        Self::ToolTimeout { tool_use_id: tool_use_id.into(), timeout_secs }
115    }
116
117    /// Creates a `CheckpointFailed` error.
118    pub fn checkpoint_failed(message: impl Into<String>) -> Self {
119        Self::CheckpointFailed { message: message.into() }
120    }
121
122    /// Creates a `SandboxError`.
123    pub fn sandbox_error(message: impl Into<String>) -> Self {
124        Self::SandboxError { message: message.into() }
125    }
126
127    /// Creates an `Internal` error.
128    pub fn internal(message: impl Into<String>) -> Self {
129        Self::Internal { message: message.into() }
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn test_invalid_request_display() {
139        let err = RuntimeError::invalid_request("missing field 'model'");
140        assert_eq!(err.to_string(), "invalid request: missing field 'model'");
141    }
142
143    #[test]
144    fn test_invalid_param_display() {
145        let err = RuntimeError::invalid_param("must be positive", "timeout_ms");
146        assert_eq!(err.to_string(), "invalid request: must be positive");
147    }
148
149    #[test]
150    fn test_not_found_display() {
151        let err = RuntimeError::not_found("ses_abc123");
152        assert_eq!(err.to_string(), "session not found: ses_abc123");
153    }
154
155    #[test]
156    fn test_conflict_display() {
157        let err = RuntimeError::conflict("cannot transition from Archived to Running");
158        assert_eq!(err.to_string(), "conflict: cannot transition from Archived to Running");
159    }
160
161    #[test]
162    fn test_provider_error_display() {
163        let err = RuntimeError::provider_error("openai", "rate limit exceeded");
164        assert_eq!(err.to_string(), "provider error (openai): rate limit exceeded");
165    }
166
167    #[test]
168    fn test_tool_timeout_display() {
169        let err = RuntimeError::tool_timeout("tool_use_xyz", 300);
170        assert_eq!(err.to_string(), "tool timeout: tool_use_xyz after 300s");
171    }
172
173    #[test]
174    fn test_checkpoint_failed_display() {
175        let err = RuntimeError::checkpoint_failed("database connection lost");
176        assert_eq!(err.to_string(), "checkpoint failed: database connection lost");
177    }
178
179    #[test]
180    fn test_sandbox_error_display() {
181        let err = RuntimeError::sandbox_error("container crashed");
182        assert_eq!(err.to_string(), "sandbox error: container crashed");
183    }
184
185    #[test]
186    fn test_internal_error_display() {
187        let err = RuntimeError::internal("unexpected state");
188        assert_eq!(err.to_string(), "internal error: unexpected state");
189    }
190
191    #[test]
192    fn test_error_is_send_sync() {
193        fn assert_send_sync<T: Send + Sync>() {}
194        assert_send_sync::<RuntimeError>();
195    }
196
197    #[test]
198    fn test_error_variants_have_structured_fields() {
199        // Verify that structured fields are accessible for programmatic handling
200        let err = RuntimeError::ToolTimeout { tool_use_id: "tu_123".to_string(), timeout_secs: 60 };
201
202        if let RuntimeError::ToolTimeout { tool_use_id, timeout_secs } = &err {
203            assert_eq!(tool_use_id, "tu_123");
204            assert_eq!(*timeout_secs, 60);
205        } else {
206            panic!("expected ToolTimeout variant");
207        }
208    }
209}