daimon_core/error.rs
1//! Error types for the Daimon agent framework.
2
3use thiserror::Error;
4
5/// The central error type for all Daimon operations.
6///
7/// Provider crates should map their transport-specific errors
8/// (HTTP, gRPC, SDK) to [`DaimonError::Model`].
9#[derive(Error, Debug)]
10#[non_exhaustive]
11pub enum DaimonError {
12 /// An error originating from a model provider (API error, bad response, etc.).
13 #[error("model error: {0}")]
14 Model(String),
15
16 /// A tool failed during execution.
17 #[error("tool execution failed for '{tool}': {message}")]
18 ToolExecution {
19 /// Name of the tool that failed.
20 tool: String,
21 /// Description of the failure.
22 message: String,
23 },
24
25 /// The requested tool was not found in the registry.
26 #[error("tool '{0}' not found in registry")]
27 ToolNotFound(String),
28
29 /// Attempted to register a tool with a name that already exists.
30 #[error("duplicate tool '{0}' in registry")]
31 DuplicateTool(String),
32
33 /// The agent builder failed validation (e.g. missing required model).
34 #[error("agent builder validation failed: {0}")]
35 Builder(String),
36
37 /// The agent exceeded the configured maximum number of iterations.
38 #[error("max iterations ({0}) exceeded")]
39 MaxIterations(usize),
40
41 /// A serialization or deserialization error.
42 #[error("serialization error: {0}")]
43 Serialization(#[from] serde_json::Error),
44
45 /// Tool input failed JSON Schema validation.
46 #[error("schema validation failed for tool '{tool}': {errors}")]
47 SchemaValidation {
48 /// Name of the tool whose input failed validation.
49 tool: String,
50 /// Human-readable description of validation errors.
51 errors: String,
52 },
53
54 /// A stream was closed before completing.
55 #[error("stream closed unexpectedly")]
56 StreamClosed,
57
58 /// A request timed out.
59 #[error("request timed out after {0:?}")]
60 Timeout(std::time::Duration),
61
62 /// The operation was cancelled via a cancellation token.
63 #[error("operation cancelled")]
64 Cancelled,
65
66 /// An orchestration error (chain or graph execution failure).
67 #[error("orchestration error: {0}")]
68 Orchestration(String),
69
70 /// An MCP protocol error.
71 #[error("MCP error: {0}")]
72 Mcp(String),
73
74 /// The agent exceeded the configured spending budget.
75 #[error("budget exceeded: ${spent:.6} spent, limit was ${limit:.6}")]
76 BudgetExceeded {
77 /// How much has been spent so far (USD).
78 spent: f64,
79 /// The configured limit (USD).
80 limit: f64,
81 },
82
83 /// An input or output guardrail blocked the request.
84 #[error("guardrail blocked: {0}")]
85 GuardrailBlocked(String),
86
87 /// A storage backend operation failed (checkpoint store, memory backend,
88 /// broker state, or another persistence layer).
89 ///
90 /// `transient` distinguishes failures that may succeed on retry
91 /// (connection refused, timeout, I/O contention) from permanent ones
92 /// (corrupt data, invalid keys, misconfiguration), so callers can decide
93 /// whether retrying is worthwhile without string-matching the message.
94 #[error("storage error: {message}")]
95 Storage {
96 /// Description of the storage failure.
97 message: String,
98 /// `true` when retrying the operation may succeed.
99 transient: bool,
100 },
101
102 /// A catch-all for other errors.
103 #[error("{0}")]
104 Other(String),
105}
106
107impl DaimonError {
108 /// Creates a permanent (non-retryable) [`DaimonError::Storage`] error,
109 /// e.g. corrupt persisted data or an invalid storage key.
110 pub fn storage(message: impl Into<String>) -> Self {
111 Self::Storage {
112 message: message.into(),
113 transient: false,
114 }
115 }
116
117 /// Creates a transient (retryable) [`DaimonError::Storage`] error,
118 /// e.g. a connection failure, timeout, or I/O error.
119 pub fn storage_transient(message: impl Into<String>) -> Self {
120 Self::Storage {
121 message: message.into(),
122 transient: true,
123 }
124 }
125}
126
127/// A type alias for `Result<T, DaimonError>`.
128pub type Result<T> = std::result::Result<T, DaimonError>;
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 #[test]
135 fn test_storage_constructor_is_permanent() {
136 let err = DaimonError::storage("corrupt checkpoint");
137 assert!(matches!(
138 err,
139 DaimonError::Storage {
140 transient: false,
141 ..
142 }
143 ));
144 assert_eq!(err.to_string(), "storage error: corrupt checkpoint");
145 }
146
147 #[test]
148 fn test_storage_transient_constructor_is_retryable() {
149 let err = DaimonError::storage_transient("connection refused");
150 assert!(matches!(
151 err,
152 DaimonError::Storage {
153 transient: true,
154 ..
155 }
156 ));
157 assert_eq!(err.to_string(), "storage error: connection refused");
158 }
159}