1use crate::hooks::{HookPoint, HookReasonCode};
4use crate::types::SessionId;
5
6#[derive(Debug, Clone, PartialEq)]
7pub enum LlmFailureReason {
8 RateLimited {
9 retry_after: Option<std::time::Duration>,
10 },
11 ContextExceeded {
12 max: u32,
13 requested: u32,
14 },
15 AuthError,
16 InvalidModel(String),
17 ProviderError(serde_json::Value),
18}
19
20#[derive(Debug, Clone, thiserror::Error, PartialEq)]
22pub enum ToolValidationError {
23 #[error("Tool not found: {name}")]
25 NotFound { name: String },
26 #[error("Invalid arguments for tool '{name}': {reason}")]
28 InvalidArguments { name: String, reason: String },
29}
30
31impl ToolValidationError {
32 pub fn not_found(name: impl Into<String>) -> Self {
33 Self::NotFound { name: name.into() }
34 }
35 pub fn invalid_arguments(name: impl Into<String>, reason: impl Into<String>) -> Self {
36 Self::InvalidArguments {
37 name: name.into(),
38 reason: reason.into(),
39 }
40 }
41}
42
43#[derive(Debug, Clone, thiserror::Error)]
45pub enum ToolError {
46 #[error("Tool not found: {name}")]
48 NotFound { name: String },
49
50 #[error("Tool '{name}' is currently unavailable: {reason}")]
52 Unavailable { name: String, reason: String },
53
54 #[error("Invalid arguments for tool '{name}': {reason}")]
56 InvalidArguments { name: String, reason: String },
57
58 #[error("Tool execution failed: {message}")]
60 ExecutionFailed { message: String },
61
62 #[error("Tool '{name}' timed out after {timeout_ms}ms")]
64 Timeout { name: String, timeout_ms: u64 },
65
66 #[error("Tool '{name}' is not allowed by policy")]
68 AccessDenied { name: String },
69
70 #[error("{0}")]
72 Other(String),
73
74 #[error("Callback pending for tool '{tool_name}'")]
80 CallbackPending {
81 tool_name: String,
82 args: serde_json::Value,
83 },
84}
85
86impl ToolError {
87 pub fn error_code(&self) -> &'static str {
88 match self {
89 Self::NotFound { .. } => "tool_not_found",
90 Self::Unavailable { .. } => "tool_unavailable",
91 Self::InvalidArguments { .. } => "invalid_arguments",
92 Self::ExecutionFailed { .. } => "execution_failed",
93 Self::Timeout { .. } => "timeout",
94 Self::AccessDenied { .. } => "access_denied",
95 Self::Other(_) => "tool_error",
96 Self::CallbackPending { .. } => "callback_pending",
97 }
98 }
99
100 pub fn to_error_payload(&self) -> serde_json::Value {
101 serde_json::json!({
102 "error": self.error_code(),
103 "message": self.to_string(),
104 })
105 }
106
107 pub fn not_found(name: impl Into<String>) -> Self {
108 Self::NotFound { name: name.into() }
109 }
110 pub fn unavailable(name: impl Into<String>, reason: impl Into<String>) -> Self {
111 Self::Unavailable {
112 name: name.into(),
113 reason: reason.into(),
114 }
115 }
116 pub fn invalid_arguments(name: impl Into<String>, reason: impl Into<String>) -> Self {
117 Self::InvalidArguments {
118 name: name.into(),
119 reason: reason.into(),
120 }
121 }
122 pub fn execution_failed(message: impl Into<String>) -> Self {
123 Self::ExecutionFailed {
124 message: message.into(),
125 }
126 }
127 pub fn timeout(name: impl Into<String>, timeout_ms: u64) -> Self {
128 Self::Timeout {
129 name: name.into(),
130 timeout_ms,
131 }
132 }
133 pub fn access_denied(name: impl Into<String>) -> Self {
134 Self::AccessDenied { name: name.into() }
135 }
136 pub fn other(message: impl Into<String>) -> Self {
137 Self::Other(message.into())
138 }
139
140 pub fn callback_pending(tool_name: impl Into<String>, args: serde_json::Value) -> Self {
142 Self::CallbackPending {
143 tool_name: tool_name.into(),
144 args,
145 }
146 }
147
148 pub fn is_callback_pending(&self) -> bool {
150 matches!(self, Self::CallbackPending { .. })
151 }
152
153 pub fn as_callback_pending(&self) -> Option<(&str, &serde_json::Value)> {
155 match self {
156 Self::CallbackPending { tool_name, args } => Some((tool_name, args)),
157 _ => None,
158 }
159 }
160}
161
162impl From<String> for ToolError {
163 fn from(s: String) -> Self {
164 Self::Other(s)
165 }
166}
167impl From<&str> for ToolError {
168 fn from(s: &str) -> Self {
169 Self::Other(s.to_string())
170 }
171}
172
173#[derive(Debug, thiserror::Error)]
175#[non_exhaustive]
176pub enum AgentError {
177 #[error("LLM error ({provider}): {message}")]
178 Llm {
179 provider: &'static str,
180 reason: LlmFailureReason,
181 message: String,
182 },
183 #[error("Storage error: {0}")]
184 StoreError(String),
185 #[error("Tool error: {0}")]
186 ToolError(String),
187 #[error("MCP error: {0}")]
188 McpError(String),
189 #[error("Session not found: {0}")]
190 SessionNotFound(SessionId),
191 #[error("Token budget exceeded: used {used}, limit {limit}")]
192 TokenBudgetExceeded { used: u64, limit: u64 },
193 #[error("Time budget exceeded: {elapsed_secs}s > {limit_secs}s")]
194 TimeBudgetExceeded { elapsed_secs: u64, limit_secs: u64 },
195 #[error("Tool call budget exceeded: {count} calls > {limit} limit")]
196 ToolCallBudgetExceeded { count: usize, limit: usize },
197 #[error("Max tokens reached on turn {turn}, partial output: {partial}")]
198 MaxTokensReached { turn: u32, partial: String },
199 #[error("Content filtered on turn {turn}")]
200 ContentFiltered { turn: u32 },
201 #[error("Max turns reached: {turns}")]
202 MaxTurnsReached { turns: u32 },
203 #[error("Run was cancelled")]
204 Cancelled,
205 #[error("Invalid state transition: {from} -> {to}")]
206 InvalidStateTransition { from: String, to: String },
207 #[error("Operation not found: {0}")]
208 OperationNotFound(String),
209 #[error("Depth limit exceeded: {depth} > {max}")]
210 DepthLimitExceeded { depth: u32, max: u32 },
211 #[error("Concurrency limit exceeded")]
212 ConcurrencyLimitExceeded,
213 #[error("Configuration error: {0}")]
214 ConfigError(String),
215 #[error("Sub-agent limit exceeded: max {limit} concurrent sub-agents")]
216 SubAgentLimitExceeded { limit: usize },
217 #[error("Sub-agent not found: {id}")]
218 SubAgentNotFound { id: String },
219 #[error("Sub-agent {id} not running (state: {state})")]
220 SubAgentNotRunning { id: String, state: String },
221 #[error("Invalid tool in access policy: {tool}")]
222 InvalidToolAccess { tool: String },
223 #[error("Sub-agent spawn failed: {reason}")]
224 SubAgentSpawnFailed { reason: String },
225 #[error("Internal error: {0}")]
226 InternalError(String),
227
228 #[error("Callback pending for tool '{tool_name}'")]
230 CallbackPending {
231 tool_name: String,
232 args: serde_json::Value,
233 },
234
235 #[error("Structured output validation failed after {attempts} attempts: {reason}")]
237 StructuredOutputValidationFailed {
238 attempts: u32,
239 reason: String,
240 last_output: String,
241 },
242
243 #[error("Invalid output schema: {0}")]
245 InvalidOutputSchema(String),
246
247 #[error("Hook denied at {point:?}: {reason_code:?} - {message}")]
248 HookDenied {
249 point: HookPoint,
250 reason_code: HookReasonCode,
251 message: String,
252 payload: Option<serde_json::Value>,
253 },
254
255 #[error("Hook '{hook_id}' timed out after {timeout_ms}ms")]
256 HookTimeout { hook_id: String, timeout_ms: u64 },
257
258 #[error("Hook execution failed for '{hook_id}': {reason}")]
259 HookExecutionFailed { hook_id: String, reason: String },
260
261 #[error("Hook configuration invalid: {reason}")]
262 HookConfigInvalid { reason: String },
263}
264
265impl AgentError {
266 pub fn llm(
267 provider: &'static str,
268 reason: LlmFailureReason,
269 message: impl Into<String>,
270 ) -> Self {
271 Self::Llm {
272 provider,
273 reason,
274 message: message.into(),
275 }
276 }
277 pub fn is_graceful(&self) -> bool {
278 matches!(
279 self,
280 Self::TokenBudgetExceeded { .. }
281 | Self::TimeBudgetExceeded { .. }
282 | Self::ToolCallBudgetExceeded { .. }
283 | Self::MaxTurnsReached { .. }
284 )
285 }
286 pub fn is_recoverable(&self) -> bool {
287 match self {
288 Self::Llm { reason, .. } => match reason {
289 LlmFailureReason::RateLimited { .. } => true,
290 LlmFailureReason::ProviderError(value) => {
291 value.get("retryable").and_then(serde_json::Value::as_bool) == Some(true)
292 }
293 _ => false,
294 },
295 _ => false,
296 }
297 }
298}
299
300pub fn store_error(err: impl std::fmt::Display) -> AgentError {
301 AgentError::StoreError(store_error_message(err))
302}
303pub fn invalid_session_id(err: impl std::fmt::Display) -> AgentError {
304 AgentError::StoreError(invalid_session_id_message(err))
305}
306pub fn store_error_message(err: impl std::fmt::Display) -> String {
307 err.to_string()
308}
309pub fn invalid_session_id_message(err: impl std::fmt::Display) -> String {
310 format!("Invalid session ID: {err}")
311}