Skip to main content

brainwires_mdap/
error.rs

1//! MDAP (Massively Decomposed Agentic Processes) Error Types
2//!
3//! Provides domain-specific error types for the MDAP framework implementation,
4//! based on the MAKER paper's error handling requirements.
5
6use std::collections::HashMap;
7use thiserror::Error;
8
9/// Main error type for the MDAP system
10#[derive(Error, Debug)]
11pub enum MdapError {
12    /// Voting error.
13    #[error("Voting error: {0}")]
14    Voting(#[from] VotingError),
15
16    /// Red-flag validation error.
17    #[error("Red-flag validation error: {0}")]
18    RedFlag(#[from] RedFlagError),
19
20    /// Decomposition error.
21    #[error("Decomposition error: {0}")]
22    Decomposition(#[from] DecompositionError),
23
24    /// Microagent execution error.
25    #[error("Microagent error: {0}")]
26    Microagent(#[from] MicroagentError),
27
28    /// Composition error.
29    #[error("Composition error: {0}")]
30    Composition(#[from] CompositionError),
31
32    /// Scaling law calculation error.
33    #[error("Scaling error: {0}")]
34    Scaling(#[from] ScalingError),
35
36    /// AI provider error.
37    #[error("Provider error: {0}")]
38    Provider(String),
39
40    /// Configuration error.
41    #[error("Configuration error: {0}")]
42    Config(#[from] MdapConfigError),
43
44    /// I/O error.
45    #[error("IO error: {0}")]
46    Io(#[from] std::io::Error),
47
48    /// Serialization error.
49    #[error("Serialization error: {0}")]
50    Serialization(#[from] serde_json::Error),
51
52    /// Semaphore acquire error.
53    #[error("Semaphore acquire error: {0}")]
54    Semaphore(String),
55
56    /// Task join error.
57    #[error("Task join error: {0}")]
58    TaskJoin(String),
59
60    /// Catch-all error.
61    #[error("{0}")]
62    Other(String),
63
64    /// Tool recursion limit reached.
65    #[error("Tool recursion limit reached: depth {depth} >= max {max_depth}")]
66    ToolRecursionLimit {
67        /// Current recursion depth.
68        depth: u32,
69        /// Maximum allowed depth.
70        max_depth: u32,
71    },
72
73    /// Tool execution failed.
74    #[error("Tool execution failed: {tool} - {reason}")]
75    ToolExecutionFailed {
76        /// Tool name.
77        tool: String,
78        /// Failure reason.
79        reason: String,
80    },
81
82    /// Tool not allowed for this microagent.
83    #[error("Tool not allowed for microagent: {tool} (category: {category})")]
84    ToolNotAllowed {
85        /// Tool name.
86        tool: String,
87        /// Tool category.
88        category: String,
89    },
90
91    /// Tool intent parsing failed.
92    #[error("Tool intent parsing failed: {0}")]
93    ToolIntentParseFailed(String),
94
95    /// General configuration error.
96    #[error("Configuration error: {0}")]
97    ConfigurationError(String),
98}
99
100/// Errors related to the first-to-ahead-by-k voting system (Algorithm 2)
101#[derive(Error, Debug)]
102pub enum VotingError {
103    /// Maximum samples exceeded without reaching consensus.
104    #[error("Maximum samples exceeded: {samples} samples taken, no consensus reached")]
105    MaxSamplesExceeded {
106        /// Number of samples taken.
107        samples: u32,
108        /// Vote tally per response hash.
109        votes: HashMap<String, u32>,
110    },
111
112    /// All samples were red-flagged as invalid.
113    #[error("All samples were red-flagged: {red_flagged}/{total} samples invalid")]
114    AllSamplesRedFlagged {
115        /// Number of red-flagged samples.
116        red_flagged: u32,
117        /// Total number of samples.
118        total: u32,
119    },
120
121    /// Voting was cancelled.
122    #[error("Voting cancelled")]
123    Cancelled,
124
125    /// No valid responses received.
126    #[error("No valid responses received after {attempts} attempts")]
127    NoValidResponses {
128        /// Number of attempts made.
129        attempts: u32,
130    },
131
132    /// Sampler returned an error.
133    #[error("Sampler returned error: {0}")]
134    SamplerError(String),
135
136    /// Unable to hash a response for comparison.
137    #[error("Vote comparison failed: unable to hash response")]
138    HashError,
139
140    /// Invalid k value (must be >= 1).
141    #[error("Invalid k value: k must be >= 1, got {0}")]
142    InvalidK(u32),
143
144    /// Parallel execution error.
145    #[error("Parallel execution error: {0}")]
146    ParallelError(String),
147}
148
149/// Errors related to red-flag validation (Algorithm 3)
150#[derive(Error, Debug)]
151pub enum RedFlagError {
152    /// Response exceeds token limit.
153    #[error("Response too long: {tokens} tokens exceeds limit of {limit}")]
154    ResponseTooLong {
155        /// Actual token count.
156        tokens: u32,
157        /// Maximum allowed tokens.
158        limit: u32,
159    },
160
161    /// Response format does not match expected format.
162    #[error("Invalid format: expected {expected}, got {got}")]
163    InvalidFormat {
164        /// Expected format.
165        expected: String,
166        /// Actual format received.
167        got: String,
168    },
169
170    /// Self-correction pattern detected in response.
171    #[error("Self-correction detected: '{pattern}' indicates model confusion")]
172    SelfCorrectionDetected {
173        /// Detected self-correction pattern.
174        pattern: String,
175    },
176
177    /// Confused reasoning detected in response.
178    #[error("Confused reasoning detected: '{pattern}'")]
179    ConfusedReasoning {
180        /// Detected confusion pattern.
181        pattern: String,
182    },
183
184    /// Parse error in response.
185    #[error("Parse error: {0}")]
186    ParseError(String),
187
188    /// Response is empty.
189    #[error("Empty response")]
190    EmptyResponse,
191
192    /// Invalid JSON structure in response.
193    #[error("Invalid JSON structure: {0}")]
194    InvalidJson(String),
195
196    /// Missing required field in response.
197    #[error("Missing required field: {0}")]
198    MissingField(String),
199
200    /// Validation pattern error.
201    #[error("Validation pattern error: {0}")]
202    PatternError(String),
203}
204
205/// Errors related to task decomposition
206#[derive(Error, Debug)]
207pub enum DecompositionError {
208    /// Maximum decomposition depth exceeded.
209    #[error("Maximum decomposition depth exceeded: {depth} > {max_depth}")]
210    MaxDepthExceeded {
211        /// Current depth.
212        depth: u32,
213        /// Maximum allowed depth.
214        max_depth: u32,
215    },
216
217    /// Task cannot be decomposed further.
218    #[error("Task cannot be decomposed further: {0}")]
219    CannotDecompose(String),
220
221    /// Circular dependency detected in subtasks.
222    #[error("Circular dependency detected in subtasks: {0}")]
223    CircularDependency(String),
224
225    /// Invalid subtask dependency.
226    #[error("Invalid subtask dependency: '{subtask}' depends on non-existent '{dependency}'")]
227    InvalidDependency {
228        /// Subtask with the invalid dependency.
229        subtask: String,
230        /// Non-existent dependency name.
231        dependency: String,
232    },
233
234    /// Decomposition voting failed.
235    #[error("Decomposition voting failed: {0}")]
236    VotingFailed(String),
237
238    /// Empty decomposition result.
239    #[error("Empty decomposition result for task: {0}")]
240    EmptyResult(String),
241
242    /// Invalid decomposition strategy.
243    #[error("Invalid decomposition strategy: {0}")]
244    InvalidStrategy(String),
245
246    /// Discriminator error.
247    #[error("Discriminator error: {0}")]
248    DiscriminatorError(String),
249}
250
251/// Errors related to microagent execution
252#[derive(Error, Debug)]
253pub enum MicroagentError {
254    /// Subtask execution failed.
255    #[error("Subtask execution failed: {subtask_id} - {reason}")]
256    ExecutionFailed {
257        /// Subtask identifier.
258        subtask_id: String,
259        /// Failure reason.
260        reason: String,
261    },
262
263    /// Subtask timed out.
264    #[error("Subtask timeout after {timeout_ms}ms: {subtask_id}")]
265    Timeout {
266        /// Subtask identifier.
267        subtask_id: String,
268        /// Timeout duration in milliseconds.
269        timeout_ms: u64,
270    },
271
272    /// Invalid input for subtask.
273    #[error("Invalid input state for subtask '{subtask_id}': {reason}")]
274    InvalidInput {
275        /// Subtask identifier.
276        subtask_id: String,
277        /// Reason for invalid input.
278        reason: String,
279    },
280
281    /// Output parsing failed for subtask.
282    #[error("Output parsing failed for subtask '{subtask_id}': {reason}")]
283    OutputParseFailed {
284        /// Subtask identifier.
285        subtask_id: String,
286        /// Parsing failure reason.
287        reason: String,
288    },
289
290    /// Provider communication error.
291    #[error("Provider communication error: {0}")]
292    ProviderError(String),
293
294    /// Context too large for microagent.
295    #[error("Context too large for microagent: {size} tokens > {limit} limit")]
296    ContextTooLarge {
297        /// Actual context size in tokens.
298        size: u32,
299        /// Maximum token limit.
300        limit: u32,
301    },
302
303    /// Missing dependency result.
304    #[error("Missing dependency result: subtask '{subtask_id}' requires '{dependency}'")]
305    MissingDependency {
306        /// Subtask identifier.
307        subtask_id: String,
308        /// Missing dependency name.
309        dependency: String,
310    },
311}
312
313/// Errors related to result composition
314#[derive(Error, Debug)]
315pub enum CompositionError {
316    /// Missing subtask result.
317    #[error("Missing subtask result: {0}")]
318    MissingResult(String),
319
320    /// Incompatible result types for composition.
321    #[error("Incompatible result types: cannot compose {type_a} with {type_b}")]
322    IncompatibleTypes {
323        /// First type.
324        type_a: String,
325        /// Second type.
326        type_b: String,
327    },
328
329    /// Composition function not found.
330    #[error("Composition function '{function}' not found")]
331    FunctionNotFound {
332        /// Function name.
333        function: String,
334    },
335
336    /// Composition execution failed.
337    #[error("Composition execution failed: {0}")]
338    ExecutionFailed(String),
339
340    /// Invalid composition order.
341    #[error("Invalid composition order: {0}")]
342    InvalidOrder(String),
343
344    /// Result validation failed.
345    #[error("Result validation failed: {0}")]
346    ValidationFailed(String),
347}
348
349/// Errors related to scaling law calculations
350#[derive(Error, Debug)]
351pub enum ScalingError {
352    /// Invalid success probability value.
353    #[error("Invalid success probability: {0} must be in range (0.5, 1.0)")]
354    InvalidSuccessProbability(f64),
355
356    /// Invalid target probability value.
357    #[error("Invalid target probability: {0} must be in range (0.0, 1.0)")]
358    InvalidTargetProbability(f64),
359
360    /// Invalid step count.
361    #[error("Invalid step count: must be > 0, got {0}")]
362    InvalidStepCount(u64),
363
364    /// Voting cannot converge at this success rate.
365    #[error("Voting cannot converge: per-step success rate {p} <= 0.5")]
366    VotingCannotConverge {
367        /// Per-step success probability.
368        p: f64,
369    },
370
371    /// Cost estimation failed.
372    #[error("Cost estimation failed: {0}")]
373    CostEstimationFailed(String),
374
375    /// Numerical overflow in calculation.
376    #[error("Numerical overflow in calculation: {0}")]
377    NumericalOverflow(String),
378}
379
380/// Errors related to MDAP configuration
381#[derive(Error, Debug)]
382pub enum MdapConfigError {
383    /// Invalid k value.
384    #[error("Invalid k value: must be >= 1, got {0}")]
385    InvalidK(u32),
386
387    /// Invalid target success rate.
388    #[error("Invalid target success rate: must be in (0.0, 1.0), got {0}")]
389    InvalidTargetSuccessRate(f64),
390
391    /// Invalid parallel samples count.
392    #[error("Invalid parallel samples: must be 1-4, got {0}")]
393    InvalidParallelSamples(u32),
394
395    /// Invalid max samples per subtask.
396    #[error("Invalid max samples per subtask: must be > 0, got {0}")]
397    InvalidMaxSamples(u32),
398
399    /// Invalid max response tokens.
400    #[error("Invalid max response tokens: must be > 0, got {0}")]
401    InvalidMaxTokens(u32),
402
403    /// Invalid max decomposition depth.
404    #[error("Invalid decomposition max depth: must be > 0, got {0}")]
405    InvalidMaxDepth(u32),
406
407    /// Configuration file not found.
408    #[error("Configuration file not found: {0}")]
409    FileNotFound(String),
410
411    /// Configuration parse error.
412    #[error("Configuration parse error: {0}")]
413    ParseError(String),
414}
415
416// Conversion from anyhow::Error to MdapError
417impl From<anyhow::Error> for MdapError {
418    fn from(err: anyhow::Error) -> Self {
419        MdapError::Other(format!("{:#}", err))
420    }
421}
422
423// Conversion from tokio semaphore acquire error
424impl From<tokio::sync::AcquireError> for MdapError {
425    fn from(err: tokio::sync::AcquireError) -> Self {
426        MdapError::Semaphore(err.to_string())
427    }
428}
429
430// Conversion from tokio join error
431impl From<tokio::task::JoinError> for MdapError {
432    fn from(err: tokio::task::JoinError) -> Self {
433        MdapError::TaskJoin(err.to_string())
434    }
435}
436
437// Helper methods for MdapError
438impl MdapError {
439    /// Create a new error from a string message
440    pub fn other(msg: impl Into<String>) -> Self {
441        MdapError::Other(msg.into())
442    }
443
444    /// Create a provider error
445    pub fn provider(msg: impl Into<String>) -> Self {
446        MdapError::Provider(msg.into())
447    }
448
449    /// Convert to a user-facing error string
450    pub fn to_user_string(&self) -> String {
451        format!("{}", self)
452    }
453
454    /// Check if this is a user/configuration error vs system/runtime error
455    pub fn is_user_error(&self) -> bool {
456        matches!(
457            self,
458            MdapError::Config(_)
459                | MdapError::Scaling(ScalingError::InvalidSuccessProbability(_))
460                | MdapError::Scaling(ScalingError::InvalidTargetProbability(_))
461        )
462    }
463
464    /// Check if this error is retryable
465    pub fn is_retryable(&self) -> bool {
466        matches!(
467            self,
468            MdapError::Provider(_)
469                | MdapError::Semaphore(_)
470                | MdapError::Voting(VotingError::SamplerError(_))
471                | MdapError::Microagent(MicroagentError::ProviderError(_))
472                | MdapError::ToolExecutionFailed { .. }
473        )
474    }
475
476    /// Check if this is a tool-related error
477    pub fn is_tool_error(&self) -> bool {
478        matches!(
479            self,
480            MdapError::ToolRecursionLimit { .. }
481                | MdapError::ToolExecutionFailed { .. }
482                | MdapError::ToolNotAllowed { .. }
483                | MdapError::ToolIntentParseFailed(_)
484        )
485    }
486
487    /// Check if this error indicates the voting process should be restarted
488    pub fn should_restart_voting(&self) -> bool {
489        matches!(
490            self,
491            MdapError::RedFlag(RedFlagError::ResponseTooLong { .. })
492                | MdapError::RedFlag(RedFlagError::SelfCorrectionDetected { .. })
493                | MdapError::RedFlag(RedFlagError::ConfusedReasoning { .. })
494        )
495    }
496
497    /// Check if this error is a red-flag (should discard and resample)
498    pub fn is_red_flag(&self) -> bool {
499        matches!(self, MdapError::RedFlag(_))
500    }
501}
502
503/// Result type alias for MDAP operations
504pub type MdapResult<T> = Result<T, MdapError>;
505
506#[cfg(test)]
507mod tests {
508    use super::*;
509
510    #[test]
511    fn test_voting_error_display() {
512        let mut votes = HashMap::new();
513        votes.insert("option_a".to_string(), 3);
514        votes.insert("option_b".to_string(), 2);
515
516        let err = VotingError::MaxSamplesExceeded { samples: 50, votes };
517        assert!(
518            err.to_string()
519                .contains("Maximum samples exceeded: 50 samples taken")
520        );
521    }
522
523    #[test]
524    fn test_red_flag_error_display() {
525        let err = RedFlagError::ResponseTooLong {
526            tokens: 800,
527            limit: 750,
528        };
529        assert_eq!(
530            err.to_string(),
531            "Response too long: 800 tokens exceeds limit of 750"
532        );
533    }
534
535    #[test]
536    fn test_self_correction_error() {
537        let err = RedFlagError::SelfCorrectionDetected {
538            pattern: "Wait,".to_string(),
539        };
540        assert!(err.to_string().contains("Wait,"));
541        assert!(err.to_string().contains("model confusion"));
542    }
543
544    #[test]
545    fn test_decomposition_error() {
546        let err = DecompositionError::MaxDepthExceeded {
547            depth: 15,
548            max_depth: 10,
549        };
550        assert_eq!(
551            err.to_string(),
552            "Maximum decomposition depth exceeded: 15 > 10"
553        );
554    }
555
556    #[test]
557    fn test_microagent_error() {
558        let err = MicroagentError::Timeout {
559            subtask_id: "task_001".to_string(),
560            timeout_ms: 5000,
561        };
562        assert!(err.to_string().contains("task_001"));
563        assert!(err.to_string().contains("5000ms"));
564    }
565
566    #[test]
567    fn test_scaling_error() {
568        let err = ScalingError::VotingCannotConverge { p: 0.45 };
569        assert!(err.to_string().contains("0.45"));
570        assert!(err.to_string().contains("<= 0.5"));
571    }
572
573    #[test]
574    fn test_config_error() {
575        let err = MdapConfigError::InvalidParallelSamples(8);
576        assert_eq!(
577            err.to_string(),
578            "Invalid parallel samples: must be 1-4, got 8"
579        );
580    }
581
582    #[test]
583    fn test_mdap_error_from_voting() {
584        let voting_err = VotingError::Cancelled;
585        let mdap_err: MdapError = voting_err.into();
586        assert!(matches!(mdap_err, MdapError::Voting(_)));
587    }
588
589    #[test]
590    fn test_mdap_error_from_anyhow() {
591        let anyhow_err = anyhow::anyhow!("test error");
592        let mdap_err: MdapError = anyhow_err.into();
593        assert!(matches!(mdap_err, MdapError::Other(_)));
594    }
595
596    #[test]
597    fn test_is_user_error() {
598        let user_err = MdapError::Config(MdapConfigError::InvalidK(0));
599        assert!(user_err.is_user_error());
600
601        let system_err = MdapError::Provider("connection failed".to_string());
602        assert!(!system_err.is_user_error());
603    }
604
605    #[test]
606    fn test_is_retryable() {
607        let retryable = MdapError::Provider("timeout".to_string());
608        assert!(retryable.is_retryable());
609
610        let not_retryable = MdapError::Config(MdapConfigError::InvalidK(0));
611        assert!(!not_retryable.is_retryable());
612    }
613
614    #[test]
615    fn test_is_red_flag() {
616        let red_flag = MdapError::RedFlag(RedFlagError::EmptyResponse);
617        assert!(red_flag.is_red_flag());
618
619        let not_red_flag = MdapError::Voting(VotingError::Cancelled);
620        assert!(!not_red_flag.is_red_flag());
621    }
622
623    #[test]
624    fn test_should_restart_voting() {
625        let should_restart = MdapError::RedFlag(RedFlagError::SelfCorrectionDetected {
626            pattern: "Actually,".to_string(),
627        });
628        assert!(should_restart.should_restart_voting());
629
630        let should_not_restart = MdapError::Voting(VotingError::MaxSamplesExceeded {
631            samples: 50,
632            votes: HashMap::new(),
633        });
634        assert!(!should_not_restart.should_restart_voting());
635    }
636
637    #[test]
638    fn test_error_chain() {
639        let red_flag_err = RedFlagError::InvalidFormat {
640            expected: "JSON".to_string(),
641            got: "plain text".to_string(),
642        };
643        let mdap_err: MdapError = red_flag_err.into();
644        assert!(matches!(mdap_err, MdapError::RedFlag(_)));
645        assert!(mdap_err.to_string().contains("Invalid format"));
646    }
647
648    #[test]
649    fn test_composition_error() {
650        let err = CompositionError::IncompatibleTypes {
651            type_a: "String".to_string(),
652            type_b: "Number".to_string(),
653        };
654        assert!(err.to_string().contains("String"));
655        assert!(err.to_string().contains("Number"));
656    }
657
658    #[test]
659    fn test_circular_dependency() {
660        let err = DecompositionError::CircularDependency("A -> B -> C -> A".to_string());
661        assert!(err.to_string().contains("Circular dependency"));
662    }
663
664    #[test]
665    fn test_invalid_dependency() {
666        let err = DecompositionError::InvalidDependency {
667            subtask: "task_b".to_string(),
668            dependency: "task_unknown".to_string(),
669        };
670        assert!(err.to_string().contains("task_b"));
671        assert!(err.to_string().contains("task_unknown"));
672    }
673}