1use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16pub enum AgentRole {
17 Developer,
19 Reviewer,
21 Commit,
23 Analysis,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
32pub enum AgentDrain {
33 Planning,
34 Development,
35 Review,
36 Fix,
37 Commit,
38 Analysis,
39}
40
41impl AgentDrain {
42 #[must_use]
44 pub const fn role(self) -> AgentRole {
45 match self {
46 Self::Planning | Self::Development => AgentRole::Developer,
47 Self::Review | Self::Fix => AgentRole::Reviewer,
48 Self::Commit => AgentRole::Commit,
49 Self::Analysis => AgentRole::Analysis,
50 }
51 }
52
53 #[must_use]
55 pub const fn as_str(self) -> &'static str {
56 match self {
57 Self::Planning => "planning",
58 Self::Development => "development",
59 Self::Review => "review",
60 Self::Fix => "fix",
61 Self::Commit => "commit",
62 Self::Analysis => "analysis",
63 }
64 }
65
66 #[must_use]
68 pub fn from_name(name: &str) -> Option<Self> {
69 match name {
70 "planning" => Some(Self::Planning),
71 "development" => Some(Self::Development),
72 "review" => Some(Self::Review),
73 "fix" => Some(Self::Fix),
74 "commit" => Some(Self::Commit),
75 "analysis" => Some(Self::Analysis),
76 _ => None,
77 }
78 }
79
80 #[must_use]
82 pub const fn all() -> [Self; 6] {
83 [
84 Self::Planning,
85 Self::Development,
86 Self::Review,
87 Self::Fix,
88 Self::Commit,
89 Self::Analysis,
90 ]
91 }
92}
93
94impl From<AgentRole> for AgentDrain {
95 fn from(value: AgentRole) -> Self {
96 match value {
97 AgentRole::Developer => Self::Development,
98 AgentRole::Reviewer => Self::Review,
99 AgentRole::Commit => Self::Commit,
100 AgentRole::Analysis => Self::Analysis,
101 }
102 }
103}
104
105impl std::fmt::Display for AgentDrain {
106 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107 write!(f, "{}", self.as_str())
108 }
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
113pub enum DrainMode {
114 #[default]
115 Normal,
116 Continuation,
117 SameAgentRetry,
118 XsdRetry,
119}
120
121#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
123pub struct ResolvedDrainBinding {
124 pub chain_name: String,
125 pub agents: Vec<String>,
126}
127
128#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
130pub struct ResolvedDrainConfig {
131 pub bindings: HashMap<AgentDrain, ResolvedDrainBinding>,
132 pub provider_fallback: HashMap<String, Vec<String>>,
133 pub max_retries: u32,
134 pub retry_delay_ms: u64,
135 pub backoff_multiplier: f64,
136 pub max_backoff_ms: u64,
137 pub max_cycles: u32,
138}
139
140impl ResolvedDrainConfig {
141 #[must_use]
143 pub fn binding(&self, drain: AgentDrain) -> Option<&ResolvedDrainBinding> {
144 self.bindings.get(&drain)
145 }
146
147 #[must_use]
149 pub fn from_legacy(fallback: &FallbackConfig) -> Self {
150 let mut bindings = HashMap::new();
151 for drain in AgentDrain::all() {
152 let role = drain.role();
153 let chain_name = fallback.effective_chain_name_for_role(role).to_string();
154 bindings.insert(
155 drain,
156 ResolvedDrainBinding {
157 chain_name,
158 agents: fallback.get_fallbacks(role).to_vec(),
159 },
160 );
161 }
162
163 Self {
164 bindings,
165 provider_fallback: fallback.provider_fallback.clone(),
166 max_retries: fallback.max_retries,
167 retry_delay_ms: fallback.retry_delay_ms,
168 backoff_multiplier: fallback.backoff_multiplier,
169 max_backoff_ms: fallback.max_backoff_ms,
170 max_cycles: fallback.max_cycles,
171 }
172 }
173
174 #[must_use]
179 pub fn to_legacy_fallback(&self) -> FallbackConfig {
180 FallbackConfig {
181 developer: self
182 .binding(AgentDrain::Development)
183 .map_or_else(Vec::new, |binding| binding.agents.clone()),
184 reviewer: self
185 .binding(AgentDrain::Review)
186 .map_or_else(Vec::new, |binding| binding.agents.clone()),
187 commit: self
188 .binding(AgentDrain::Commit)
189 .map_or_else(Vec::new, |binding| binding.agents.clone()),
190 analysis: self
191 .binding(AgentDrain::Analysis)
192 .map_or_else(Vec::new, |binding| binding.agents.clone()),
193 provider_fallback: self.provider_fallback.clone(),
194 max_retries: self.max_retries,
195 retry_delay_ms: self.retry_delay_ms,
196 backoff_multiplier: self.backoff_multiplier,
197 max_backoff_ms: self.max_backoff_ms,
198 max_cycles: self.max_cycles,
199 legacy_role_keys_present: false,
200 }
201 }
202}
203
204impl std::fmt::Display for AgentRole {
205 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
206 match self {
207 Self::Developer => write!(f, "developer"),
208 Self::Reviewer => write!(f, "reviewer"),
209 Self::Commit => write!(f, "commit"),
210 Self::Analysis => write!(f, "analysis"),
211 }
212 }
213}
214
215#[derive(Debug, Clone, Serialize)]
246pub struct FallbackConfig {
247 #[serde(default)]
249 pub developer: Vec<String>,
250 #[serde(default)]
252 pub reviewer: Vec<String>,
253 #[serde(default)]
255 pub commit: Vec<String>,
256 #[serde(default)]
260 pub analysis: Vec<String>,
261 #[serde(default)]
264 pub provider_fallback: HashMap<String, Vec<String>>,
265 #[serde(default = "default_max_retries")]
267 pub max_retries: u32,
268 #[serde(default = "default_retry_delay_ms")]
270 pub retry_delay_ms: u64,
271 #[serde(default = "default_backoff_multiplier")]
273 pub backoff_multiplier: f64,
274 #[serde(default = "default_max_backoff_ms")]
276 pub max_backoff_ms: u64,
277 #[serde(default = "default_max_cycles")]
279 pub max_cycles: u32,
280 #[serde(skip)]
281 pub(crate) legacy_role_keys_present: bool,
282}
283
284impl<'de> Deserialize<'de> for FallbackConfig {
285 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
286 where
287 D: serde::Deserializer<'de>,
288 {
289 #[derive(Deserialize)]
290 struct FallbackConfigSerde {
291 #[serde(default)]
292 developer: Option<Vec<String>>,
293 #[serde(default)]
294 reviewer: Option<Vec<String>>,
295 #[serde(default)]
296 commit: Option<Vec<String>>,
297 #[serde(default)]
298 analysis: Option<Vec<String>>,
299 #[serde(default)]
300 provider_fallback: HashMap<String, Vec<String>>,
301 #[serde(default = "default_max_retries")]
302 max_retries: u32,
303 #[serde(default = "default_retry_delay_ms")]
304 retry_delay_ms: u64,
305 #[serde(default = "default_backoff_multiplier")]
306 backoff_multiplier: f64,
307 #[serde(default = "default_max_backoff_ms")]
308 max_backoff_ms: u64,
309 #[serde(default = "default_max_cycles")]
310 max_cycles: u32,
311 }
312
313 let raw = FallbackConfigSerde::deserialize(deserializer)?;
314 let legacy_role_keys_present = raw.developer.is_some()
315 || raw.reviewer.is_some()
316 || raw.commit.is_some()
317 || raw.analysis.is_some();
318
319 Ok(Self {
320 developer: raw.developer.unwrap_or_default(),
321 reviewer: raw.reviewer.unwrap_or_default(),
322 commit: raw.commit.unwrap_or_default(),
323 analysis: raw.analysis.unwrap_or_default(),
324 provider_fallback: raw.provider_fallback,
325 max_retries: raw.max_retries,
326 retry_delay_ms: raw.retry_delay_ms,
327 backoff_multiplier: raw.backoff_multiplier,
328 max_backoff_ms: raw.max_backoff_ms,
329 max_cycles: raw.max_cycles,
330 legacy_role_keys_present,
331 })
332 }
333}
334
335const fn default_max_retries() -> u32 {
336 3
337}
338
339const fn default_retry_delay_ms() -> u64 {
340 1000
341}
342
343const fn default_backoff_multiplier() -> f64 {
344 2.0
345}
346
347const fn default_max_backoff_ms() -> u64 {
348 60000 }
350
351const fn default_max_cycles() -> u32 {
352 3
353}
354
355const IEEE_754_EXP_BIAS: i32 = 1023;
357const IEEE_754_EXP_MASK: u64 = 0x7FF;
358const IEEE_754_MANTISSA_MASK: u64 = 0x000F_FFFF_FFFF_FFFF;
359const IEEE_754_IMPLICIT_ONE: u64 = 1u64 << 52;
360
361fn f64_to_u64_via_bits(value: f64) -> u64 {
367 if !value.is_finite() || value < 0.0 {
369 return 0;
370 }
371
372 let bits = value.to_bits();
374
375 let exp_biased = ((bits >> 52) & IEEE_754_EXP_MASK) as i32;
380 let mantissa = bits & IEEE_754_MANTISSA_MASK;
381
382 if exp_biased == 0 {
384 return 0;
387 }
388
389 let exp = exp_biased - IEEE_754_EXP_BIAS;
391
392 if exp < 0 {
395 return 0;
396 }
397
398 let full_mantissa = mantissa | IEEE_754_IMPLICIT_ONE;
401
402 let shift = 52i32 - exp;
405
406 if shift <= 0 {
407 u64::MAX
410 } else if shift < 64 {
411 full_mantissa >> shift
412 } else {
413 0
414 }
415}
416
417impl Default for FallbackConfig {
418 fn default() -> Self {
419 Self {
420 developer: Vec::new(),
421 reviewer: Vec::new(),
422 commit: Vec::new(),
423 analysis: Vec::new(),
424 provider_fallback: HashMap::new(),
425 max_retries: default_max_retries(),
426 retry_delay_ms: default_retry_delay_ms(),
427 backoff_multiplier: default_backoff_multiplier(),
428 max_backoff_ms: default_max_backoff_ms(),
429 max_cycles: default_max_cycles(),
430 legacy_role_keys_present: false,
431 }
432 }
433}
434
435impl FallbackConfig {
436 #[must_use]
438 pub fn has_role_bindings(&self) -> bool {
439 [
440 self.developer.as_slice(),
441 self.reviewer.as_slice(),
442 self.commit.as_slice(),
443 self.analysis.as_slice(),
444 ]
445 .into_iter()
446 .any(|chain| !chain.is_empty())
447 }
448
449 #[must_use]
451 pub const fn has_legacy_role_key_presence(&self) -> bool {
452 self.legacy_role_keys_present
453 }
454
455 #[must_use]
457 pub fn uses_legacy_role_schema(&self) -> bool {
458 self.legacy_role_keys_present || self.has_role_bindings()
459 }
460
461 const fn effective_chain_name_for_role(&self, role: AgentRole) -> &'static str {
462 match role {
463 AgentRole::Developer => "developer",
464 AgentRole::Reviewer => "reviewer",
465 AgentRole::Commit => {
466 if self.commit.is_empty() {
467 "reviewer"
468 } else {
469 "commit"
470 }
471 }
472 AgentRole::Analysis => {
473 if self.analysis.is_empty() {
474 "developer"
475 } else {
476 "analysis"
477 }
478 }
479 }
480 }
481
482 #[must_use]
488 pub fn calculate_backoff(&self, cycle: u32) -> u64 {
489 let multiplier_hundredths = self.get_multiplier_hundredths();
492 let base_hundredths = self.retry_delay_ms.saturating_mul(100);
493
494 let mut delay_hundredths = base_hundredths;
497 for _ in 0..cycle {
498 delay_hundredths = delay_hundredths.saturating_mul(multiplier_hundredths);
499 delay_hundredths = delay_hundredths.saturating_div(100);
500 }
501
502 delay_hundredths.div_euclid(100).min(self.max_backoff_ms)
504 }
505
506 fn get_multiplier_hundredths(&self) -> u64 {
511 const EPSILON: f64 = 0.0001;
512
513 let m = self.backoff_multiplier;
516 if (m - 1.0).abs() < EPSILON {
517 return 100;
518 } else if (m - 1.5).abs() < EPSILON {
519 return 150;
520 } else if (m - 2.0).abs() < EPSILON {
521 return 200;
522 } else if (m - 2.5).abs() < EPSILON {
523 return 250;
524 } else if (m - 3.0).abs() < EPSILON {
525 return 300;
526 } else if (m - 4.0).abs() < EPSILON {
527 return 400;
528 } else if (m - 5.0).abs() < EPSILON {
529 return 500;
530 } else if (m - 10.0).abs() < EPSILON {
531 return 1000;
532 }
533
534 let clamped = m.clamp(0.0, 1000.0);
538 let multiplied = clamped * 100.0;
539 let rounded = multiplied.round();
540
541 f64_to_u64_via_bits(rounded)
543 }
544
545 #[must_use]
547 pub fn get_fallbacks(&self, role: AgentRole) -> &[String] {
548 match role {
549 AgentRole::Developer => &self.developer,
550 AgentRole::Reviewer => &self.reviewer,
551 AgentRole::Commit => self.get_effective_commit_fallbacks(),
552 AgentRole::Analysis => self.get_effective_analysis_fallbacks(),
553 }
554 }
555
556 fn get_effective_analysis_fallbacks(&self) -> &[String] {
560 if self.analysis.is_empty() {
561 &self.developer
562 } else {
563 &self.analysis
564 }
565 }
566
567 fn get_effective_commit_fallbacks(&self) -> &[String] {
573 if self.commit.is_empty() {
574 &self.reviewer
575 } else {
576 &self.commit
577 }
578 }
579
580 #[must_use]
582 pub fn has_fallbacks(&self, role: AgentRole) -> bool {
583 !self.get_fallbacks(role).is_empty()
584 }
585
586 pub fn get_provider_fallbacks(&self, agent_name: &str) -> &[String] {
591 self.provider_fallback
592 .get(agent_name)
593 .map_or(&[], std::vec::Vec::as_slice)
594 }
595
596 #[must_use]
598 pub fn has_provider_fallbacks(&self, agent_name: &str) -> bool {
599 self.provider_fallback
600 .get(agent_name)
601 .is_some_and(|v| !v.is_empty())
602 }
603
604 #[must_use]
606 pub fn resolve_drains(&self) -> ResolvedDrainConfig {
607 ResolvedDrainConfig::from_legacy(self)
608 }
609}
610
611#[cfg(test)]
612mod tests {
613 use super::*;
614
615 #[test]
616 fn test_agent_role_display() {
617 assert_eq!(format!("{}", AgentRole::Developer), "developer");
618 assert_eq!(format!("{}", AgentRole::Reviewer), "reviewer");
619 assert_eq!(format!("{}", AgentRole::Commit), "commit");
620 assert_eq!(format!("{}", AgentRole::Analysis), "analysis");
621 }
622
623 #[test]
624 fn test_agent_drain_role_mapping() {
625 assert_eq!(AgentDrain::Planning.role(), AgentRole::Developer);
626 assert_eq!(AgentDrain::Development.role(), AgentRole::Developer);
627 assert_eq!(AgentDrain::Review.role(), AgentRole::Reviewer);
628 assert_eq!(AgentDrain::Fix.role(), AgentRole::Reviewer);
629 assert_eq!(AgentDrain::Commit.role(), AgentRole::Commit);
630 assert_eq!(AgentDrain::Analysis.role(), AgentRole::Analysis);
631 }
632
633 #[test]
634 fn test_fallback_config_defaults() {
635 let config = FallbackConfig::default();
636 assert!(config.developer.is_empty());
637 assert!(config.reviewer.is_empty());
638 assert!(config.commit.is_empty());
639 assert!(config.analysis.is_empty());
640 assert_eq!(config.max_retries, 3);
641 assert_eq!(config.retry_delay_ms, 1000);
642 assert!((config.backoff_multiplier - 2.0).abs() < f64::EPSILON);
644 assert_eq!(config.max_backoff_ms, 60000);
645 assert_eq!(config.max_cycles, 3);
646 }
647
648 #[test]
649 fn test_fallback_config_calculate_backoff() {
650 let config = FallbackConfig {
651 retry_delay_ms: 1000,
652 backoff_multiplier: 2.0,
653 max_backoff_ms: 60000,
654 ..Default::default()
655 };
656
657 assert_eq!(config.calculate_backoff(0), 1000);
658 assert_eq!(config.calculate_backoff(1), 2000);
659 assert_eq!(config.calculate_backoff(2), 4000);
660 assert_eq!(config.calculate_backoff(3), 8000);
661
662 assert_eq!(config.calculate_backoff(10), 60000);
664 }
665
666 #[test]
667 fn test_fallback_config_get_fallbacks() {
668 let config = FallbackConfig {
669 developer: vec!["claude".to_string(), "codex".to_string()],
670 reviewer: vec!["codex".to_string()],
671 ..Default::default()
672 };
673
674 assert_eq!(
675 config.get_fallbacks(AgentRole::Developer),
676 &["claude", "codex"]
677 );
678 assert_eq!(config.get_fallbacks(AgentRole::Reviewer), &["codex"]);
679
680 assert_eq!(
682 config.get_fallbacks(AgentRole::Analysis),
683 &["claude", "codex"]
684 );
685 }
686
687 #[test]
688 fn test_fallback_config_has_fallbacks() {
689 let config = FallbackConfig {
690 developer: vec!["claude".to_string()],
691 reviewer: vec![],
692 ..Default::default()
693 };
694
695 assert!(config.has_fallbacks(AgentRole::Developer));
696 assert!(config.has_fallbacks(AgentRole::Analysis));
697 assert!(!config.has_fallbacks(AgentRole::Reviewer));
698 }
699
700 #[test]
701 fn test_fallback_config_defaults_provider_fallback() {
702 let config = FallbackConfig::default();
703 assert!(config.get_provider_fallbacks("opencode").is_empty());
704 assert!(!config.has_provider_fallbacks("opencode"));
705 }
706
707 #[test]
708 fn test_provider_fallback_config() {
709 let mut provider_fallback = HashMap::new();
710 provider_fallback.insert(
711 "opencode".to_string(),
712 vec![
713 "-m opencode/glm-4.7-free".to_string(),
714 "-m opencode/claude-sonnet-4".to_string(),
715 ],
716 );
717
718 let config = FallbackConfig {
719 provider_fallback,
720 ..Default::default()
721 };
722
723 let fallbacks = config.get_provider_fallbacks("opencode");
724 assert_eq!(fallbacks.len(), 2);
725 assert_eq!(fallbacks[0], "-m opencode/glm-4.7-free");
726 assert_eq!(fallbacks[1], "-m opencode/claude-sonnet-4");
727
728 assert!(config.has_provider_fallbacks("opencode"));
729 assert!(!config.has_provider_fallbacks("claude"));
730 }
731
732 #[test]
733 fn test_fallback_config_from_toml() {
734 let toml_str = r#"
735 developer = ["claude", "codex"]
736 reviewer = ["codex", "claude"]
737 max_retries = 5
738 retry_delay_ms = 2000
739
740 [provider_fallback]
741 opencode = ["-m opencode/glm-4.7-free", "-m zai/glm-4.7"]
742 "#;
743
744 let config: FallbackConfig = toml::from_str(toml_str).unwrap();
745 assert_eq!(config.developer, vec!["claude", "codex"]);
746 assert_eq!(config.reviewer, vec!["codex", "claude"]);
747 assert_eq!(config.max_retries, 5);
748 assert_eq!(config.retry_delay_ms, 2000);
749 assert_eq!(config.get_provider_fallbacks("opencode").len(), 2);
750 }
751
752 #[test]
753 fn test_commit_uses_reviewer_chain_when_empty() {
754 let config = FallbackConfig {
756 commit: vec![],
757 reviewer: vec!["agent1".to_string(), "agent2".to_string()],
758 ..Default::default()
759 };
760
761 assert_eq!(
763 config.get_fallbacks(AgentRole::Commit),
764 &["agent1", "agent2"]
765 );
766 assert!(config.has_fallbacks(AgentRole::Commit));
767 }
768
769 #[test]
770 fn test_commit_uses_own_chain_when_configured() {
771 let config = FallbackConfig {
773 commit: vec!["commit-agent".to_string()],
774 reviewer: vec!["reviewer-agent".to_string()],
775 ..Default::default()
776 };
777
778 assert_eq!(config.get_fallbacks(AgentRole::Commit), &["commit-agent"]);
780 }
781}