gemini_adk_rs/
error.rs

1//! Error types for the agent runtime.
2
3use gemini_genai_rs::session::SessionError;
4
5/// Convenience alias for fallible agent-runtime operations.
6///
7/// Lets call sites write `AgentResult<T>` instead of the more verbose
8/// `Result<T, AgentError>`.
9pub type AgentResult<T> = std::result::Result<T, AgentError>;
10
11/// Errors that can occur during agent execution.
12///
13/// A model failure arrives as [`AgentError::Llm`] with its kind intact, so a
14/// caller can branch on it:
15///
16/// ```
17/// use gemini_adk_rs::error::AgentError;
18/// use gemini_adk_rs::llm::LlmError;
19///
20/// let err = AgentError::from(LlmError::Api { status: 429, message: "slow down".into() });
21/// assert!(err.as_llm().is_some_and(LlmError::is_rate_limited));
22/// ```
23#[derive(Debug, thiserror::Error)]
24#[non_exhaustive]
25pub enum AgentError {
26    /// A wire-level session error (WebSocket, auth, setup).
27    #[error("Session error: {0}")]
28    Session(#[from] SessionError),
29
30    /// A tool execution error.
31    #[error("Tool error: {0}")]
32    Tool(#[from] ToolError),
33
34    /// The model call failed; the [`LlmError`](crate::llm::LlmError) says how.
35    #[error("model call failed: {0}")]
36    Llm(#[from] crate::llm::LlmError),
37
38    /// Reading or writing session state failed.
39    #[error(transparent)]
40    State(#[from] crate::state::StateError),
41
42    /// The model's reply did not match the requested output type, even after
43    /// it was asked to correct it.
44    #[error("the model's reply is not a valid {expected}: {reason}")]
45    InvalidOutput {
46        /// The Rust type the reply had to deserialize into.
47        expected: &'static str,
48        /// Why it did not.
49        reason: String,
50        /// The reply as received.
51        text: String,
52    },
53
54    /// The requested agent was not found in the registry.
55    #[error("Unknown agent: {0}")]
56    UnknownAgent(String),
57
58    /// The agent requested a transfer to another agent.
59    #[error("Transfer requested to agent: {0}")]
60    TransferRequested(String),
61
62    /// An agent transfer was attempted but failed.
63    #[error("Agent transfer failed: {0}")]
64    TransferFailed(String),
65
66    /// The underlying session has been closed.
67    #[error("Agent session closed")]
68    SessionClosed,
69
70    /// The operation timed out.
71    #[error("Timeout")]
72    Timeout,
73
74    /// A configuration error.
75    #[error("Configuration error: {0}")]
76    Config(String),
77
78    /// A catch-all for other errors.
79    #[error("{0}")]
80    Other(String),
81}
82
83impl AgentError {
84    /// The model error behind this one, if a model call failed.
85    pub fn as_llm(&self) -> Option<&crate::llm::LlmError> {
86        match self {
87            Self::Llm(e) => Some(e),
88            _ => None,
89        }
90    }
91}
92
93/// A build-time configuration error: one or more problems found while
94/// validating user-supplied configuration (a [`Flow`](crate::flow::Flow), a
95/// [`PhaseMachine`](crate::live::PhaseMachine), a
96/// [`ComputedRegistry`](crate::live::ComputedRegistry), …).
97///
98/// Every issue is reported, not just the first; `Display` joins them with
99/// `"; "`. Converts into [`AgentError::Config`] via `?`.
100#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
101#[error("{}", issues.join("; "))]
102pub struct ConfigError {
103    /// Every problem found, in discovery order. Never empty.
104    pub issues: Vec<String>,
105}
106
107impl ConfigError {
108    /// A single-issue error.
109    pub fn new(issue: impl Into<String>) -> Self {
110        Self {
111            issues: vec![issue.into()],
112        }
113    }
114
115    /// Collect a list of issues into an error, or `Ok(())` when there are none.
116    pub fn from_issues(issues: Vec<String>) -> Result<(), Self> {
117        if issues.is_empty() {
118            Ok(())
119        } else {
120            Err(Self { issues })
121        }
122    }
123}
124
125impl From<ConfigError> for AgentError {
126    fn from(err: ConfigError) -> Self {
127        AgentError::Config(err.to_string())
128    }
129}
130
131/// Errors that can occur during tool execution.
132#[derive(Debug, Clone, thiserror::Error)]
133#[non_exhaustive]
134pub enum ToolError {
135    /// The tool's execution logic failed.
136    #[error("Tool execution failed: {0}")]
137    ExecutionFailed(String),
138
139    /// No tool with this name is registered.
140    #[error("Tool not found: {0}")]
141    NotFound(String),
142
143    /// The arguments provided to the tool were invalid.
144    #[error("Invalid arguments: {0}")]
145    InvalidArgs(String),
146
147    /// The tool call was cancelled before completion.
148    #[error("Tool cancelled")]
149    Cancelled,
150
151    /// A confirmation-gated call was declined; carries the reason given, so
152    /// the model can tell the user why and what to do instead.
153    #[error("Tool call declined: {0}")]
154    Declined(String),
155
156    /// The tool call exceeded its timeout.
157    #[error("Tool execution timed out after {0:?}")]
158    Timeout(std::time::Duration),
159
160    /// A catch-all for other tool errors.
161    #[error("{0}")]
162    Other(String),
163}
164
165impl ToolError {
166    /// Turn any error into a `ToolError`, the way `?` would if it could.
167    ///
168    /// A `ToolError` passes through unchanged, so a tool that returns
169    /// `ToolError::InvalidArgs` keeps that meaning. Anything else — an
170    /// `io::Error`, a `reqwest::Error`, a `String`, an `anyhow::Error` —
171    /// becomes [`ToolError::ExecutionFailed`] carrying its message, which is
172    /// what the model is shown.
173    ///
174    /// ```
175    /// use gemini_adk_rs::error::ToolError;
176    ///
177    /// let io = std::io::Error::other("disk full");
178    /// assert!(matches!(ToolError::from_error(io), ToolError::ExecutionFailed(m) if m == "disk full"));
179    /// assert!(matches!(ToolError::from_error(ToolError::Cancelled), ToolError::Cancelled));
180    /// assert!(matches!(ToolError::from_error("no such city"), ToolError::ExecutionFailed(_)));
181    /// ```
182    pub fn from_error(error: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
183        match error.into().downcast::<ToolError>() {
184            Ok(tool_error) => *tool_error,
185            Err(other) => ToolError::ExecutionFailed(other.to_string()),
186        }
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193    use std::time::Duration;
194
195    #[test]
196    fn agent_error_display_messages() {
197        let err = AgentError::UnknownAgent("foo".into());
198        assert_eq!(err.to_string(), "Unknown agent: foo");
199
200        let err = AgentError::TransferRequested("bar".into());
201        assert_eq!(err.to_string(), "Transfer requested to agent: bar");
202
203        let err = AgentError::TransferFailed("baz".into());
204        assert_eq!(err.to_string(), "Agent transfer failed: baz");
205
206        let err = AgentError::SessionClosed;
207        assert_eq!(err.to_string(), "Agent session closed");
208
209        let err = AgentError::Timeout;
210        assert_eq!(err.to_string(), "Timeout");
211
212        let err = AgentError::Config("bad value".into());
213        assert_eq!(err.to_string(), "Configuration error: bad value");
214
215        let err = AgentError::Other("something".into());
216        assert_eq!(err.to_string(), "something");
217    }
218
219    #[test]
220    fn agent_error_from_session_error() {
221        use gemini_genai_rs::session::SessionError;
222        use gemini_genai_rs::session::WebSocketError;
223
224        let ws_err = SessionError::WebSocket(WebSocketError::ConnectionRefused("refused".into()));
225        let agent_err: AgentError = ws_err.into();
226        let msg = agent_err.to_string();
227        assert!(msg.contains("Session error"), "got: {msg}");
228    }
229
230    #[test]
231    fn agent_error_from_tool_error() {
232        let tool_err = ToolError::NotFound("my_tool".into());
233        let agent_err: AgentError = tool_err.into();
234        let msg = agent_err.to_string();
235        assert!(msg.contains("Tool error"), "got: {msg}");
236        assert!(msg.contains("my_tool"), "got: {msg}");
237    }
238
239    #[test]
240    fn config_error_joins_issues_and_converts() {
241        let err = ConfigError {
242            issues: vec!["a".into(), "b".into()],
243        };
244        assert_eq!(err.to_string(), "a; b");
245        assert!(ConfigError::from_issues(vec![]).is_ok());
246        let agent_err: AgentError = ConfigError::new("bad").into();
247        assert_eq!(agent_err.to_string(), "Configuration error: bad");
248    }
249
250    #[test]
251    fn tool_error_display_messages() {
252        assert_eq!(
253            ToolError::ExecutionFailed("boom".into()).to_string(),
254            "Tool execution failed: boom"
255        );
256        assert_eq!(
257            ToolError::NotFound("x".into()).to_string(),
258            "Tool not found: x"
259        );
260        assert_eq!(
261            ToolError::InvalidArgs("bad".into()).to_string(),
262            "Invalid arguments: bad"
263        );
264        assert_eq!(ToolError::Cancelled.to_string(), "Tool cancelled");
265        assert_eq!(ToolError::Other("misc".into()).to_string(), "misc");
266    }
267
268    #[test]
269    fn tool_error_timeout_shows_duration() {
270        let err = ToolError::Timeout(Duration::from_secs(5));
271        let msg = err.to_string();
272        assert!(msg.contains("5s"), "got: {msg}");
273        assert!(msg.contains("timed out"), "got: {msg}");
274    }
275
276    #[test]
277    fn tool_error_is_clone() {
278        let err = ToolError::ExecutionFailed("test".into());
279        let cloned = err.clone();
280        assert_eq!(err.to_string(), cloned.to_string());
281
282        let err2 = ToolError::Timeout(Duration::from_millis(100));
283        let cloned2 = err2.clone();
284        assert_eq!(err2.to_string(), cloned2.to_string());
285    }
286}