1use std::fmt;
2
3#[derive(Debug, thiserror::Error)]
8pub enum ForgeError {
9 #[error(transparent)]
11 UnsupportedModel(#[from] UnsupportedModelError),
12 #[error(transparent)]
14 ToolCall(#[from] ToolCallError),
15 #[error(transparent)]
17 ToolExecution(#[from] ToolExecutionError),
18 #[error(transparent)]
20 WorkflowCancelled(#[from] WorkflowCancelledError),
21 #[error(transparent)]
23 MaxIterations(#[from] MaxIterationsError),
24 #[error(transparent)]
26 StepEnforcement(#[from] StepEnforcementError),
27 #[error(transparent)]
29 Prerequisite(#[from] PrerequisiteError),
30 #[error(transparent)]
32 ContextBudgetExceeded(#[from] ContextBudgetExceeded),
33 #[error(transparent)]
35 HardwareDetection(#[from] HardwareDetectionError),
36 #[error(transparent)]
38 ContextDiscovery(#[from] ContextDiscoveryError),
39 #[error(transparent)]
41 BudgetResolution(#[from] BudgetResolutionError),
42 #[error(transparent)]
44 Backend(#[from] BackendError),
45 #[error(transparent)]
47 Stream(#[from] StreamError),
48}
49
50#[derive(Debug, thiserror::Error)]
52pub struct UnsupportedModelError {
53 pub model: String,
55}
56
57impl UnsupportedModelError {
58 pub fn new(model: impl Into<String>) -> Self {
60 Self {
61 model: model.into(),
62 }
63 }
64}
65
66impl fmt::Display for UnsupportedModelError {
67 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68 write!(
69 f,
70 "Unsupported model '{}'. Add sampling defaults or use non-strict mode.",
71 self.model
72 )
73 }
74}
75
76#[derive(Debug, thiserror::Error)]
78pub struct ToolCallError {
79 pub message: String,
81 pub raw_response: Option<String>,
83 pub cause: Option<String>,
85}
86
87impl ToolCallError {
88 pub fn new(message: impl Into<String>) -> Self {
90 Self {
91 message: message.into(),
92 raw_response: None,
93 cause: None,
94 }
95 }
96
97 pub fn with_raw_response(mut self, raw: impl Into<String>) -> Self {
99 self.raw_response = Some(raw.into());
100 self
101 }
102
103 pub fn with_cause(mut self, cause: impl Into<String>) -> Self {
105 self.cause = Some(cause.into());
106 self
107 }
108}
109
110impl fmt::Display for ToolCallError {
111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112 write!(f, "{}", self.message)
113 }
114}
115
116#[derive(Debug, thiserror::Error)]
118pub struct ToolExecutionError {
119 pub tool_name: String,
121 pub cause: String,
123}
124
125impl ToolExecutionError {
126 pub fn new(tool_name: impl Into<String>, cause: impl Into<String>) -> Self {
128 Self {
129 tool_name: tool_name.into(),
130 cause: cause.into(),
131 }
132 }
133}
134
135impl fmt::Display for ToolExecutionError {
136 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137 write!(
138 f,
139 "Tool '{}' execution failed: {}",
140 self.tool_name, self.cause
141 )
142 }
143}
144
145#[derive(Debug, Clone, thiserror::Error, PartialEq)]
148pub struct ToolResolutionError {
149 pub message: String,
151 pub tool_name: Option<String>,
153}
154
155impl ToolResolutionError {
156 pub fn new(message: impl Into<String>) -> Self {
158 Self {
159 message: message.into(),
160 tool_name: None,
161 }
162 }
163
164 pub fn with_tool_name(mut self, tool_name: impl Into<String>) -> Self {
166 self.tool_name = Some(tool_name.into());
167 self
168 }
169}
170
171impl fmt::Display for ToolResolutionError {
172 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173 write!(f, "{}", self.message)
174 }
175}
176
177#[derive(Debug, Clone, thiserror::Error, PartialEq)]
179pub enum ToolError {
180 #[error(transparent)]
182 Resolution(#[from] ToolResolutionError),
183 #[error("Tool execution failed: {0}")]
185 Execution(String),
186}
187
188#[derive(Debug, thiserror::Error)]
190pub struct WorkflowCancelledError {
191 pub messages: Vec<String>,
193 pub completed_steps: indexmap::IndexMap<String, ()>,
195 pub iteration: i64,
197}
198
199impl WorkflowCancelledError {
200 pub fn new(
202 messages: Vec<String>,
203 completed_steps: indexmap::IndexMap<String, ()>,
204 iteration: i64,
205 ) -> Self {
206 Self {
207 messages,
208 completed_steps,
209 iteration,
210 }
211 }
212}
213
214impl fmt::Display for WorkflowCancelledError {
215 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216 let step_names: Vec<&str> = self.completed_steps.keys().map(|s| s.as_str()).collect();
217 write!(
218 f,
219 "Workflow cancelled at iteration {} with completed steps: [{}]",
220 self.iteration,
221 step_names.join(", ")
222 )
223 }
224}
225
226#[derive(Debug, thiserror::Error)]
228pub struct MaxIterationsError {
229 pub iterations: i64,
231 pub completed_steps: indexmap::IndexMap<String, ()>,
233 pub pending_steps: Vec<String>,
235}
236
237impl MaxIterationsError {
238 pub fn new(
240 iterations: i64,
241 completed_steps: indexmap::IndexMap<String, ()>,
242 pending_steps: Vec<String>,
243 ) -> Self {
244 Self {
245 iterations,
246 completed_steps,
247 pending_steps,
248 }
249 }
250}
251
252impl fmt::Display for MaxIterationsError {
253 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
254 let completed: Vec<&str> = self.completed_steps.keys().map(|s| s.as_str()).collect();
255 write!(
256 f,
257 "Max iterations ({}) reached. Completed: [{}]. Pending: [{}]",
258 self.iterations,
259 completed.join(", "),
260 self.pending_steps.join(", ")
261 )
262 }
263}
264
265#[derive(Debug, thiserror::Error)]
267pub struct StepEnforcementError {
268 pub terminal_tool: String,
270 pub attempts: i64,
272 pub pending_steps: Vec<String>,
274}
275
276impl StepEnforcementError {
277 pub fn new(
279 terminal_tool: impl Into<String>,
280 attempts: i64,
281 pending_steps: Vec<String>,
282 ) -> Self {
283 Self {
284 terminal_tool: terminal_tool.into(),
285 attempts,
286 pending_steps,
287 }
288 }
289}
290
291impl fmt::Display for StepEnforcementError {
292 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
293 write!(
294 f,
295 "Terminal tool '{}' called prematurely (attempt {}), pending steps: [{}]",
296 self.terminal_tool,
297 self.attempts,
298 self.pending_steps.join(", ")
299 )
300 }
301}
302
303#[derive(Debug, thiserror::Error)]
305pub struct PrerequisiteError {
306 pub tool_name: String,
308 pub violations: i64,
310 pub missing_prereqs: Vec<String>,
312}
313
314impl PrerequisiteError {
315 pub fn new(
317 tool_name: impl Into<String>,
318 violations: i64,
319 missing_prereqs: Vec<String>,
320 ) -> Self {
321 Self {
322 tool_name: tool_name.into(),
323 violations,
324 missing_prereqs,
325 }
326 }
327}
328
329impl fmt::Display for PrerequisiteError {
330 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
331 write!(
332 f,
333 "Prerequisite violation for '{}' ({} violations), missing: [{}]",
334 self.tool_name,
335 self.violations,
336 self.missing_prereqs.join(", ")
337 )
338 }
339}
340
341#[derive(Debug, thiserror::Error)]
343pub struct ContextBudgetExceeded {
344 pub estimated_tokens: i64,
346 pub budget_tokens: i64,
348}
349
350impl ContextBudgetExceeded {
351 pub fn new(estimated_tokens: i64, budget_tokens: i64) -> Self {
353 Self {
354 estimated_tokens,
355 budget_tokens,
356 }
357 }
358}
359
360impl fmt::Display for ContextBudgetExceeded {
361 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
362 write!(
363 f,
364 "Context budget exceeded: estimated {} tokens, budget {} tokens",
365 self.estimated_tokens, self.budget_tokens
366 )
367 }
368}
369
370#[derive(Debug, thiserror::Error)]
372pub struct HardwareDetectionError {
373 pub cause: String,
375}
376
377impl HardwareDetectionError {
378 pub fn new(cause: impl Into<String>) -> Self {
380 Self {
381 cause: cause.into(),
382 }
383 }
384}
385
386impl fmt::Display for HardwareDetectionError {
387 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
388 write!(f, "Hardware detection failed: {}", self.cause)
389 }
390}
391
392#[derive(Debug, thiserror::Error)]
394pub struct ContextDiscoveryError {
395 pub cause: String,
397}
398
399impl ContextDiscoveryError {
400 pub fn new(cause: impl Into<String>) -> Self {
402 Self {
403 cause: cause.into(),
404 }
405 }
406}
407
408impl fmt::Display for ContextDiscoveryError {
409 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
410 write!(f, "Context length discovery failed: {}", self.cause)
411 }
412}
413
414#[derive(Debug, thiserror::Error)]
416pub struct BudgetResolutionError {
417 pub cause: Option<String>,
419}
420
421impl BudgetResolutionError {
422 pub fn new() -> Self {
424 Self { cause: None }
425 }
426
427 pub fn with_cause(mut self, cause: impl Into<String>) -> Self {
429 self.cause = Some(cause.into());
430 self
431 }
432}
433
434impl Default for BudgetResolutionError {
435 fn default() -> Self {
436 Self::new()
437 }
438}
439
440impl fmt::Display for BudgetResolutionError {
441 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
442 match &self.cause {
443 Some(c) => write!(f, "Could not determine context budget: {}", c),
444 None => write!(f, "No GPU detected and no explicit budget provided"),
445 }
446 }
447}
448
449#[derive(Debug, thiserror::Error)]
451pub enum BackendError {
452 #[error("Backend error (status {status_code}): {body}")]
454 Generic {
455 status_code: i64,
457 body: String,
459 },
460 #[error("Thinking mode not supported for model '{model}'")]
462 ThinkingNotSupported {
463 model: String,
465 status_code: i64,
467 body: String,
469 },
470}
471
472impl BackendError {
473 pub fn new(status_code: i64, body: impl Into<String>) -> Self {
475 Self::Generic {
476 status_code,
477 body: body.into(),
478 }
479 }
480
481 pub fn status_code(&self) -> i64 {
484 match self {
485 Self::Generic { status_code, .. } | Self::ThinkingNotSupported { status_code, .. } => {
486 *status_code
487 }
488 }
489 }
490
491 pub fn status_from_display(message: &str) -> Option<i64> {
499 let marker = "Backend error (status ";
500 let start = message.find(marker)? + marker.len();
501 let rest = &message[start..];
502 let end = rest.find(')')?;
503 rest[..end].trim().parse::<i64>().ok()
504 }
505
506 pub fn thinking_not_supported(model: impl Into<String>) -> Self {
508 Self::ThinkingNotSupported {
509 model: model.into(),
510 status_code: 400,
511 body: String::new(),
512 }
513 }
514}
515
516pub type ThinkingNotSupportedError = BackendError;
519
520#[derive(Debug, thiserror::Error)]
522pub struct StreamError {
523 pub message: String,
525}
526
527impl StreamError {
528 pub fn new(message: impl Into<String>) -> Self {
530 Self {
531 message: message.into(),
532 }
533 }
534}
535
536impl Default for StreamError {
537 fn default() -> Self {
538 Self::new("Stream ended without a final chunk")
539 }
540}
541
542impl fmt::Display for StreamError {
543 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
544 write!(f, "{}", self.message)
545 }
546}
547
548#[cfg(test)]
549mod tests {
550 use super::*;
551
552 #[test]
553 fn status_code_reads_both_variants() {
554 assert_eq!(BackendError::new(429, "x").status_code(), 429);
555 assert_eq!(BackendError::new(0, "x").status_code(), 0);
556 assert_eq!(BackendError::thinking_not_supported("m").status_code(), 400);
557 }
558
559 #[test]
560 fn status_from_display_recovers_marker() {
561 assert_eq!(
562 BackendError::status_from_display(
563 "Backend error (status 429): {\"error\":\"rate limited\"}"
564 ),
565 Some(429)
566 );
567 assert_eq!(
568 BackendError::status_from_display("Backend error (status 503): boom"),
569 Some(503)
570 );
571 let display = BackendError::new(504, "gateway timeout").to_string();
573 assert_eq!(BackendError::status_from_display(&display), Some(504));
574 }
575
576 #[test]
577 fn status_from_display_ignores_unmarked_messages() {
578 assert_eq!(
579 BackendError::status_from_display("model failed guarded tool-call validation"),
580 None
581 );
582 assert_eq!(
583 BackendError::status_from_display("some other failure"),
584 None
585 );
586 }
587}