Skip to main content

modular_agent_core/
error.rs

1use thiserror::Error;
2
3/// Errors that occur during agent operations.
4///
5/// Errors are categorized into:
6///
7/// - **Configuration errors**: `InvalidConfig`, `UnknownConfig`, `NoConfig`
8/// - **Value errors**: `InvalidValue`, `InvalidArrayValue`
9/// - **Agent management errors**: `AgentNotFound`, `AgentAlreadyExists`
10/// - **Connection errors**: `ConnectionNotFound`, `ConnectionAlreadyExists`
11/// - **I/O errors**: `IoError`, `SerializationError`, `JsonParseError`
12/// - **Retryable / provider errors**: `RateLimited`, `Overloaded`, `Timeout`, `ContextOverflow`, `Cancelled`
13#[derive(Clone, Debug, Error)]
14pub enum AgentError {
15    /// Invalid value in an array element.
16    #[error("Invalid {0} value in array")]
17    InvalidArrayValue(String),
18
19    /// Agent definition is invalid.
20    #[error("{0}: Agent definition \"{1}\" is invalid")]
21    InvalidDefinition(String, String),
22
23    /// Invalid port name.
24    #[error("Invalid port: {0}")]
25    InvalidPin(String),
26
27    /// Invalid preset name.
28    #[error("Invalid preset name: {0}")]
29    InvalidPresetName(String),
30
31    /// Invalid value for the expected type.
32    #[error("Invalid {0} value")]
33    InvalidValue(String),
34
35    /// Agent definition is missing a required field.
36    #[error("{0}: Agent definition \"{1}\" is missing")]
37    MissingDefinition(String, String),
38
39    /// Failed to rename a preset.
40    #[error("Failed to rename preset: {0}")]
41    RenamePresetFailed(String),
42
43    /// Unknown agent definition kind.
44    #[error("Unknown agent def kind: {0}")]
45    UnknownDefKind(String),
46
47    /// Unknown agent definition name.
48    #[error("Unknown agent def name: {0}")]
49    UnknownDefName(String),
50
51    /// Agent definition is not implemented.
52    #[error("Agent definition \"{0}\" is not implemented")]
53    NotImplemented(String),
54
55    /// An agent with this ID already exists.
56    #[error("Agent {0} already exists")]
57    AgentAlreadyExists(String),
58
59    /// Failed to create an agent.
60    #[error("Failed to create agent {0}")]
61    AgentCreationFailed(String),
62
63    /// Agent with the specified ID was not found.
64    #[error("Agent {0} not found")]
65    AgentNotFound(String),
66
67    /// Source agent in a connection was not found.
68    #[error("Source agent {0} not found")]
69    SourceAgentNotFound(String),
70
71    /// Duplicate ID detected.
72    #[error("Duplicate id: {0}")]
73    DuplicateId(String),
74
75    /// Connection source handle is empty.
76    #[error("Source handle is empty")]
77    EmptySourceHandle,
78
79    /// Connection target handle is empty.
80    #[error("Target handle is empty")]
81    EmptyTargetHandle,
82
83    /// A connection between these ports already exists.
84    #[error("Connection already exists")]
85    ConnectionAlreadyExists,
86
87    /// Connection with the specified ID was not found.
88    #[error("Connection {0} not found")]
89    ConnectionNotFound(String),
90
91    /// Preset with the specified name was not found.
92    #[error("Preset {0} not found")]
93    PresetNotFound(String),
94
95    /// Agent definition was not found.
96    #[error("Agent {0} definition not found")]
97    AgentDefinitionNotFound(String),
98
99    /// Agent message sender was not found.
100    #[error("Agent tx for {0} not found")]
101    AgentTxNotFound(String),
102
103    /// Failed to send a message to an agent.
104    #[error("Failed to send message: {0}")]
105    SendMessageFailed(String),
106
107    /// Serialization or deserialization error.
108    #[error("Failed to serialize/deserialize: {0}")]
109    SerializationError(String),
110
111    /// Message sender is not initialized.
112    #[error("Message sender not initialized")]
113    TxNotInitialized,
114
115    /// I/O error.
116    #[error("IO error: {0}")]
117    IoError(String),
118
119    /// JSON parsing error.
120    #[error("JSON parsing error: {0}")]
121    JsonParseError(String),
122
123    /// Invalid file extension (expected JSON).
124    #[error("Invalid file extension: expected JSON")]
125    InvalidFileExtension,
126
127    /// File name is empty.
128    #[error("Empty file name")]
129    EmptyFileName,
130
131    /// Failed to get file stem from path.
132    #[error("Failed to get file stem from path")]
133    FileSystemError,
134
135    /// Invalid configuration value.
136    #[error("Configuration error: {0}")]
137    InvalidConfig(String),
138
139    /// No configuration is available for this agent.
140    #[error("No configuration available")]
141    NoConfig,
142
143    /// Configuration key does not exist.
144    #[error("Unknown configuration: {0}")]
145    UnknownConfig(String),
146
147    /// No global configuration is available.
148    #[error("No global configuration available")]
149    NoGlobalConfig,
150
151    /// Port (pin) was not found.
152    #[error("Pin not found: {0}")]
153    PinNotFound(String),
154
155    /// Request was rejected because the provider rate limit was exceeded.
156    ///
157    /// `retry_after` carries the provider's suggested wait duration when a
158    /// `Retry-After` header is present.
159    #[error("Rate limited: {message}")]
160    RateLimited {
161        message: String,
162        retry_after: Option<std::time::Duration>,
163    },
164
165    /// Provider is temporarily overloaded (e.g. HTTP 529).
166    #[error("Provider overloaded: {0}")]
167    Overloaded(String),
168
169    /// Request did not complete within the allotted time.
170    #[error("Request timed out: {0}")]
171    Timeout(String),
172
173    /// Request exceeded the model's context window.
174    #[error("Context overflow: {0}")]
175    ContextOverflow(String),
176
177    /// Operation was cancelled before completion.
178    #[error("Cancelled")]
179    Cancelled,
180
181    /// Generic agent error.
182    #[error("Agent error: {0}")]
183    Other(String),
184}
185
186impl AgentError {
187    /// Returns `true` for errors that are transient and may succeed on retry.
188    pub fn is_retryable(&self) -> bool {
189        matches!(
190            self,
191            Self::RateLimited { .. } | Self::Overloaded(_) | Self::Timeout(_)
192        )
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    #[test]
201    fn retryable_variants_are_retryable() {
202        assert!(
203            AgentError::RateLimited {
204                message: "slow down".into(),
205                retry_after: None,
206            }
207            .is_retryable()
208        );
209        assert!(AgentError::Overloaded("busy".into()).is_retryable());
210        assert!(AgentError::Timeout("deadline exceeded".into()).is_retryable());
211    }
212
213    #[test]
214    fn non_retryable_variants_are_not_retryable() {
215        assert!(!AgentError::ContextOverflow("too long".into()).is_retryable());
216        assert!(!AgentError::Cancelled.is_retryable());
217        assert!(!AgentError::InvalidValue("bad".into()).is_retryable());
218        assert!(!AgentError::IoError("disk full".into()).is_retryable());
219    }
220}