1use std::collections::HashMap;
7use thiserror::Error;
8
9#[derive(Error, Debug)]
11pub enum MdapError {
12 #[error("Voting error: {0}")]
14 Voting(#[from] VotingError),
15
16 #[error("Red-flag validation error: {0}")]
18 RedFlag(#[from] RedFlagError),
19
20 #[error("Decomposition error: {0}")]
22 Decomposition(#[from] DecompositionError),
23
24 #[error("Microagent error: {0}")]
26 Microagent(#[from] MicroagentError),
27
28 #[error("Composition error: {0}")]
30 Composition(#[from] CompositionError),
31
32 #[error("Scaling error: {0}")]
34 Scaling(#[from] ScalingError),
35
36 #[error("Provider error: {0}")]
38 Provider(String),
39
40 #[error("Configuration error: {0}")]
42 Config(#[from] MdapConfigError),
43
44 #[error("IO error: {0}")]
46 Io(#[from] std::io::Error),
47
48 #[error("Serialization error: {0}")]
50 Serialization(#[from] serde_json::Error),
51
52 #[error("Semaphore acquire error: {0}")]
54 Semaphore(String),
55
56 #[error("Task join error: {0}")]
58 TaskJoin(String),
59
60 #[error("{0}")]
62 Other(String),
63
64 #[error("Tool recursion limit reached: depth {depth} >= max {max_depth}")]
66 ToolRecursionLimit {
67 depth: u32,
69 max_depth: u32,
71 },
72
73 #[error("Tool execution failed: {tool} - {reason}")]
75 ToolExecutionFailed {
76 tool: String,
78 reason: String,
80 },
81
82 #[error("Tool not allowed for microagent: {tool} (category: {category})")]
84 ToolNotAllowed {
85 tool: String,
87 category: String,
89 },
90
91 #[error("Tool intent parsing failed: {0}")]
93 ToolIntentParseFailed(String),
94
95 #[error("Configuration error: {0}")]
97 ConfigurationError(String),
98}
99
100#[derive(Error, Debug)]
102pub enum VotingError {
103 #[error("Maximum samples exceeded: {samples} samples taken, no consensus reached")]
105 MaxSamplesExceeded {
106 samples: u32,
108 votes: HashMap<String, u32>,
110 },
111
112 #[error("All samples were red-flagged: {red_flagged}/{total} samples invalid")]
114 AllSamplesRedFlagged {
115 red_flagged: u32,
117 total: u32,
119 },
120
121 #[error("Voting cancelled")]
123 Cancelled,
124
125 #[error("No valid responses received after {attempts} attempts")]
127 NoValidResponses {
128 attempts: u32,
130 },
131
132 #[error("Sampler returned error: {0}")]
134 SamplerError(String),
135
136 #[error("Vote comparison failed: unable to hash response")]
138 HashError,
139
140 #[error("Invalid k value: k must be >= 1, got {0}")]
142 InvalidK(u32),
143
144 #[error("Parallel execution error: {0}")]
146 ParallelError(String),
147}
148
149#[derive(Error, Debug)]
151pub enum RedFlagError {
152 #[error("Response too long: {tokens} tokens exceeds limit of {limit}")]
154 ResponseTooLong {
155 tokens: u32,
157 limit: u32,
159 },
160
161 #[error("Invalid format: expected {expected}, got {got}")]
163 InvalidFormat {
164 expected: String,
166 got: String,
168 },
169
170 #[error("Self-correction detected: '{pattern}' indicates model confusion")]
172 SelfCorrectionDetected {
173 pattern: String,
175 },
176
177 #[error("Confused reasoning detected: '{pattern}'")]
179 ConfusedReasoning {
180 pattern: String,
182 },
183
184 #[error("Parse error: {0}")]
186 ParseError(String),
187
188 #[error("Empty response")]
190 EmptyResponse,
191
192 #[error("Invalid JSON structure: {0}")]
194 InvalidJson(String),
195
196 #[error("Missing required field: {0}")]
198 MissingField(String),
199
200 #[error("Validation pattern error: {0}")]
202 PatternError(String),
203}
204
205#[derive(Error, Debug)]
207pub enum DecompositionError {
208 #[error("Maximum decomposition depth exceeded: {depth} > {max_depth}")]
210 MaxDepthExceeded {
211 depth: u32,
213 max_depth: u32,
215 },
216
217 #[error("Task cannot be decomposed further: {0}")]
219 CannotDecompose(String),
220
221 #[error("Circular dependency detected in subtasks: {0}")]
223 CircularDependency(String),
224
225 #[error("Invalid subtask dependency: '{subtask}' depends on non-existent '{dependency}'")]
227 InvalidDependency {
228 subtask: String,
230 dependency: String,
232 },
233
234 #[error("Decomposition voting failed: {0}")]
236 VotingFailed(String),
237
238 #[error("Empty decomposition result for task: {0}")]
240 EmptyResult(String),
241
242 #[error("Invalid decomposition strategy: {0}")]
244 InvalidStrategy(String),
245
246 #[error("Discriminator error: {0}")]
248 DiscriminatorError(String),
249}
250
251#[derive(Error, Debug)]
253pub enum MicroagentError {
254 #[error("Subtask execution failed: {subtask_id} - {reason}")]
256 ExecutionFailed {
257 subtask_id: String,
259 reason: String,
261 },
262
263 #[error("Subtask timeout after {timeout_ms}ms: {subtask_id}")]
265 Timeout {
266 subtask_id: String,
268 timeout_ms: u64,
270 },
271
272 #[error("Invalid input state for subtask '{subtask_id}': {reason}")]
274 InvalidInput {
275 subtask_id: String,
277 reason: String,
279 },
280
281 #[error("Output parsing failed for subtask '{subtask_id}': {reason}")]
283 OutputParseFailed {
284 subtask_id: String,
286 reason: String,
288 },
289
290 #[error("Provider communication error: {0}")]
292 ProviderError(String),
293
294 #[error("Context too large for microagent: {size} tokens > {limit} limit")]
296 ContextTooLarge {
297 size: u32,
299 limit: u32,
301 },
302
303 #[error("Missing dependency result: subtask '{subtask_id}' requires '{dependency}'")]
305 MissingDependency {
306 subtask_id: String,
308 dependency: String,
310 },
311}
312
313#[derive(Error, Debug)]
315pub enum CompositionError {
316 #[error("Missing subtask result: {0}")]
318 MissingResult(String),
319
320 #[error("Incompatible result types: cannot compose {type_a} with {type_b}")]
322 IncompatibleTypes {
323 type_a: String,
325 type_b: String,
327 },
328
329 #[error("Composition function '{function}' not found")]
331 FunctionNotFound {
332 function: String,
334 },
335
336 #[error("Composition execution failed: {0}")]
338 ExecutionFailed(String),
339
340 #[error("Invalid composition order: {0}")]
342 InvalidOrder(String),
343
344 #[error("Result validation failed: {0}")]
346 ValidationFailed(String),
347}
348
349#[derive(Error, Debug)]
351pub enum ScalingError {
352 #[error("Invalid success probability: {0} must be in range (0.5, 1.0)")]
354 InvalidSuccessProbability(f64),
355
356 #[error("Invalid target probability: {0} must be in range (0.0, 1.0)")]
358 InvalidTargetProbability(f64),
359
360 #[error("Invalid step count: must be > 0, got {0}")]
362 InvalidStepCount(u64),
363
364 #[error("Voting cannot converge: per-step success rate {p} <= 0.5")]
366 VotingCannotConverge {
367 p: f64,
369 },
370
371 #[error("Cost estimation failed: {0}")]
373 CostEstimationFailed(String),
374
375 #[error("Numerical overflow in calculation: {0}")]
377 NumericalOverflow(String),
378}
379
380#[derive(Error, Debug)]
382pub enum MdapConfigError {
383 #[error("Invalid k value: must be >= 1, got {0}")]
385 InvalidK(u32),
386
387 #[error("Invalid target success rate: must be in (0.0, 1.0), got {0}")]
389 InvalidTargetSuccessRate(f64),
390
391 #[error("Invalid parallel samples: must be 1-4, got {0}")]
393 InvalidParallelSamples(u32),
394
395 #[error("Invalid max samples per subtask: must be > 0, got {0}")]
397 InvalidMaxSamples(u32),
398
399 #[error("Invalid max response tokens: must be > 0, got {0}")]
401 InvalidMaxTokens(u32),
402
403 #[error("Invalid decomposition max depth: must be > 0, got {0}")]
405 InvalidMaxDepth(u32),
406
407 #[error("Configuration file not found: {0}")]
409 FileNotFound(String),
410
411 #[error("Configuration parse error: {0}")]
413 ParseError(String),
414}
415
416impl From<anyhow::Error> for MdapError {
418 fn from(err: anyhow::Error) -> Self {
419 MdapError::Other(format!("{:#}", err))
420 }
421}
422
423impl From<tokio::sync::AcquireError> for MdapError {
425 fn from(err: tokio::sync::AcquireError) -> Self {
426 MdapError::Semaphore(err.to_string())
427 }
428}
429
430impl From<tokio::task::JoinError> for MdapError {
432 fn from(err: tokio::task::JoinError) -> Self {
433 MdapError::TaskJoin(err.to_string())
434 }
435}
436
437impl MdapError {
439 pub fn other(msg: impl Into<String>) -> Self {
441 MdapError::Other(msg.into())
442 }
443
444 pub fn provider(msg: impl Into<String>) -> Self {
446 MdapError::Provider(msg.into())
447 }
448
449 pub fn to_user_string(&self) -> String {
451 format!("{}", self)
452 }
453
454 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 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 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 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 pub fn is_red_flag(&self) -> bool {
499 matches!(self, MdapError::RedFlag(_))
500 }
501}
502
503pub 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}