1use std::fmt;
5use std::str::FromStr;
6
7use crate::providers::ProviderName;
8use schemars::JsonSchema;
9use serde::{Deserialize, Serialize};
10
11#[non_exhaustive]
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
30#[serde(rename_all = "snake_case")]
31pub enum AssetSensitivity {
32 #[default]
34 Public,
35 Internal,
37 Confidential,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
58#[serde(rename_all = "snake_case")]
59#[non_exhaustive]
60pub enum FailureStrategy {
61 #[default]
63 Abort,
64 Retry,
66 Skip,
68 Ask,
70}
71
72impl fmt::Display for FailureStrategy {
73 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74 match self {
75 Self::Abort => write!(f, "abort"),
76 Self::Retry => write!(f, "retry"),
77 Self::Skip => write!(f, "skip"),
78 Self::Ask => write!(f, "ask"),
79 }
80 }
81}
82
83impl FromStr for FailureStrategy {
84 type Err = String;
85
86 fn from_str(s: &str) -> Result<Self, Self::Err> {
87 match s {
88 "abort" => Ok(Self::Abort),
89 "retry" => Ok(Self::Retry),
90 "skip" => Ok(Self::Skip),
91 "ask" => Ok(Self::Ask),
92 other => Err(format!(
93 "unknown failure strategy '{other}': expected one of abort, retry, skip, ask"
94 )),
95 }
96 }
97}
98
99fn default_planner_max_tokens() -> u32 {
100 4096
101}
102
103fn default_aggregator_max_tokens() -> u32 {
104 4096
105}
106
107fn default_deferral_backoff_ms() -> u64 {
108 100
109}
110
111fn default_experiment_max_experiments() -> u32 {
112 20
113}
114
115fn default_experiment_max_wall_time_secs() -> u64 {
116 3600
117}
118
119fn default_experiment_min_improvement() -> f64 {
120 0.5
121}
122
123fn default_experiment_eval_budget_tokens() -> u64 {
124 100_000
125}
126
127fn default_experiment_schedule_cron() -> String {
128 "0 3 * * *".to_string()
129}
130
131fn default_experiment_max_experiments_per_run() -> u32 {
132 20
133}
134
135fn default_experiment_schedule_max_wall_time_secs() -> u64 {
136 1800
137}
138
139fn default_verify_max_tokens() -> u32 {
140 1024
141}
142
143fn default_max_replans() -> u32 {
144 2
145}
146
147fn default_completeness_threshold() -> f32 {
148 0.7
149}
150
151fn default_cascade_failure_threshold() -> f32 {
152 0.5
153}
154
155fn default_cascade_chain_threshold() -> usize {
156 3
157}
158
159fn default_lineage_ttl_secs() -> u64 {
160 300
161}
162
163fn default_max_predicate_replans() -> u32 {
164 2
165}
166
167fn default_predicate_timeout_secs() -> u64 {
168 30
169}
170
171fn default_persistence_enabled() -> bool {
172 true
173}
174
175fn default_aggregator_timeout_secs() -> u64 {
176 60
177}
178
179fn default_planner_timeout_secs() -> u64 {
180 120
181}
182
183fn default_verifier_timeout_secs() -> u64 {
184 30
185}
186
187fn default_ensemble_ema_alpha() -> f64 {
188 0.3
189}
190
191fn default_ensemble_ema_decay() -> f64 {
192 0.95
193}
194
195fn default_ensemble_min_observations() -> u32 {
196 5
197}
198
199fn default_plan_cache_similarity_threshold() -> f32 {
200 0.90
201}
202
203fn default_plan_cache_ttl_days() -> u32 {
204 30
205}
206
207fn default_plan_cache_max_templates() -> u32 {
208 100
209}
210
211#[derive(Debug, Clone, Deserialize, Serialize)]
213#[serde(default)]
214pub struct PlanCacheConfig {
215 pub enabled: bool,
217 #[serde(default = "default_plan_cache_similarity_threshold")]
219 pub similarity_threshold: f32,
220 #[serde(default = "default_plan_cache_ttl_days")]
222 pub ttl_days: u32,
223 #[serde(default = "default_plan_cache_max_templates")]
225 pub max_templates: u32,
226}
227
228impl Default for PlanCacheConfig {
229 fn default() -> Self {
230 Self {
231 enabled: false,
232 similarity_threshold: default_plan_cache_similarity_threshold(),
233 ttl_days: default_plan_cache_ttl_days(),
234 max_templates: default_plan_cache_max_templates(),
235 }
236 }
237}
238
239impl PlanCacheConfig {
240 #[must_use = "validation result must be checked"]
246 pub fn validate(&self) -> Result<(), String> {
247 if !(0.5..=1.0).contains(&self.similarity_threshold) {
248 return Err(format!(
249 "plan_cache.similarity_threshold must be in [0.5, 1.0], got {}",
250 self.similarity_threshold
251 ));
252 }
253 if self.max_templates == 0 || self.max_templates > 10_000 {
254 return Err(format!(
255 "plan_cache.max_templates must be in [1, 10000], got {}",
256 self.max_templates
257 ));
258 }
259 if self.ttl_days == 0 || self.ttl_days > 365 {
260 return Err(format!(
261 "plan_cache.ttl_days must be in [1, 365], got {}",
262 self.ttl_days
263 ));
264 }
265 Ok(())
266 }
267}
268
269#[derive(Debug, Clone, Deserialize, Serialize)]
279#[serde(default)]
280pub struct EnsembleConfig {
281 pub enabled: bool,
286 pub verify: bool,
292 pub members: Vec<String>,
298 #[serde(default = "default_ensemble_ema_alpha")]
301 pub ema_alpha: f64,
302 #[serde(default = "default_ensemble_ema_decay")]
304 pub ema_decay: f64,
305 #[serde(default = "default_ensemble_min_observations")]
308 pub min_observations: u32,
309 #[serde(default)]
312 pub member_timeout_secs: u64,
313}
314
315impl Default for EnsembleConfig {
316 fn default() -> Self {
317 Self {
318 enabled: false,
319 verify: false,
320 members: Vec::new(),
321 ema_alpha: default_ensemble_ema_alpha(),
322 ema_decay: default_ensemble_ema_decay(),
323 min_observations: default_ensemble_min_observations(),
324 member_timeout_secs: 0,
325 }
326 }
327}
328
329#[derive(Debug, Clone, Deserialize, Serialize)]
331#[serde(default)]
332#[allow(clippy::struct_excessive_bools)] pub struct OrchestrationConfig {
334 pub enabled: bool,
336 pub max_tasks: u32,
338 pub max_parallel: u32,
340 #[serde(default)]
342 pub default_failure_strategy: FailureStrategy,
343 pub default_max_retries: u32,
345 pub task_timeout_secs: u64,
347 #[serde(default)]
350 pub planner_provider: ProviderName,
351 #[serde(default = "default_planner_max_tokens")]
354 pub planner_max_tokens: u32,
355 pub dependency_context_budget: usize,
357 pub confirm_before_execute: bool,
359 #[serde(default = "default_aggregator_max_tokens")]
361 pub aggregator_max_tokens: u32,
362 #[serde(default = "default_deferral_backoff_ms")]
364 pub deferral_backoff_ms: u64,
365 #[serde(default)]
367 pub plan_cache: PlanCacheConfig,
368 #[serde(default)]
371 pub topology_selection: bool,
372 #[serde(default)]
375 pub verify_provider: ProviderName,
376 #[serde(default = "default_verify_max_tokens")]
378 pub verify_max_tokens: u32,
379 #[serde(default = "default_max_replans")]
384 pub max_replans: u32,
385 #[serde(default)]
391 pub verify_completeness: bool,
392 #[serde(default)]
397 pub tool_provider: ProviderName,
398 #[serde(default = "default_completeness_threshold")]
404 pub completeness_threshold: f32,
405 #[serde(default)]
409 pub cascade_routing: bool,
410 #[serde(default = "default_cascade_failure_threshold")]
413 pub cascade_failure_threshold: f32,
414 #[serde(default)]
418 pub tree_optimized_dispatch: bool,
419
420 #[serde(default)]
422 pub adaptorch: AdaptOrchConfig,
423 #[serde(default = "default_cascade_chain_threshold")]
429 pub cascade_chain_threshold: usize,
430 #[serde(default)]
436 pub cascade_failure_rate_abort_threshold: f32,
437 #[serde(default = "default_lineage_ttl_secs")]
442 pub lineage_ttl_secs: u64,
443 #[serde(default)]
448 pub verify_predicate_enabled: bool,
449 #[serde(default)]
453 pub predicate_provider: ProviderName,
454 #[serde(default = "default_max_predicate_replans")]
458 pub max_predicate_replans: u32,
459 #[serde(default = "default_predicate_timeout_secs")]
464 pub predicate_timeout_secs: u64,
465 #[serde(default = "default_persistence_enabled")]
472 pub persistence_enabled: bool,
473 #[serde(default)]
485 pub orchestrator_provider: ProviderName,
486
487 #[serde(default)]
496 pub default_task_budget_cents: f64,
497
498 #[serde(default)]
506 pub default_asset_sensitivity: AssetSensitivity,
507
508 #[serde(default = "default_aggregator_timeout_secs")]
513 pub aggregator_timeout_secs: u64,
514
515 #[serde(default = "default_planner_timeout_secs")]
521 pub planner_timeout_secs: u64,
522
523 #[serde(default = "default_verifier_timeout_secs")]
529 pub verifier_timeout_secs: u64,
530
531 #[serde(default)]
534 pub ensemble: EnsembleConfig,
535
536 #[serde(default)]
544 pub default_idle_timeout_secs: Option<u64>,
545}
546
547impl Default for OrchestrationConfig {
548 fn default() -> Self {
549 Self {
550 enabled: false,
551 max_tasks: 20,
552 max_parallel: 4,
553 default_failure_strategy: FailureStrategy::default(),
554 default_max_retries: 3,
555 task_timeout_secs: 300,
556 planner_provider: ProviderName::default(),
557 planner_max_tokens: default_planner_max_tokens(),
558 dependency_context_budget: 16384,
559 confirm_before_execute: true,
560 aggregator_max_tokens: default_aggregator_max_tokens(),
561 deferral_backoff_ms: default_deferral_backoff_ms(),
562 plan_cache: PlanCacheConfig::default(),
563 topology_selection: false,
564 verify_provider: ProviderName::default(),
565 verify_max_tokens: default_verify_max_tokens(),
566 max_replans: default_max_replans(),
567 verify_completeness: false,
568 completeness_threshold: default_completeness_threshold(),
569 tool_provider: ProviderName::default(),
570 cascade_routing: false,
571 cascade_failure_threshold: default_cascade_failure_threshold(),
572 tree_optimized_dispatch: false,
573 adaptorch: AdaptOrchConfig::default(),
574 cascade_chain_threshold: default_cascade_chain_threshold(),
575 cascade_failure_rate_abort_threshold: 0.0,
576 lineage_ttl_secs: default_lineage_ttl_secs(),
577 verify_predicate_enabled: false,
578 predicate_provider: ProviderName::default(),
579 max_predicate_replans: default_max_predicate_replans(),
580 predicate_timeout_secs: default_predicate_timeout_secs(),
581 persistence_enabled: default_persistence_enabled(),
582 orchestrator_provider: ProviderName::default(),
583 default_task_budget_cents: 0.0,
584 default_asset_sensitivity: AssetSensitivity::default(),
585 aggregator_timeout_secs: default_aggregator_timeout_secs(),
586 planner_timeout_secs: default_planner_timeout_secs(),
587 verifier_timeout_secs: default_verifier_timeout_secs(),
588 ensemble: EnsembleConfig::default(),
589 default_idle_timeout_secs: None,
590 }
591 }
592}
593
594#[derive(Debug, Clone, Deserialize, Serialize)]
608#[serde(default)]
609pub struct ExperimentConfig {
610 pub enabled: bool,
612 #[serde(default)]
616 pub eval_provider: ProviderName,
617 pub benchmark_file: Option<std::path::PathBuf>,
619 #[serde(default = "default_experiment_max_experiments")]
620 pub max_experiments: u32,
621 #[serde(default = "default_experiment_max_wall_time_secs")]
622 pub max_wall_time_secs: u64,
623 #[serde(default = "default_experiment_min_improvement")]
624 pub min_improvement: f64,
625 #[serde(default = "default_experiment_eval_budget_tokens")]
626 pub eval_budget_tokens: u64,
627 pub auto_apply: bool,
628 #[serde(default)]
629 pub schedule: ExperimentSchedule,
630 #[serde(default)]
638 pub tolerate_subject_errors: bool,
639}
640
641impl Default for ExperimentConfig {
642 fn default() -> Self {
643 Self {
644 enabled: false,
645 eval_provider: ProviderName::default(),
646 benchmark_file: None,
647 max_experiments: default_experiment_max_experiments(),
648 max_wall_time_secs: default_experiment_max_wall_time_secs(),
649 min_improvement: default_experiment_min_improvement(),
650 eval_budget_tokens: default_experiment_eval_budget_tokens(),
651 auto_apply: false,
652 schedule: ExperimentSchedule::default(),
653 tolerate_subject_errors: false,
654 }
655 }
656}
657
658#[derive(Debug, Clone, Deserialize, Serialize)]
670#[serde(default)]
671pub struct AdaptOrchConfig {
672 pub enabled: bool,
674 pub topology_provider: ProviderName,
676 #[serde(default = "default_classify_timeout_secs")]
678 pub classify_timeout_secs: u64,
679 #[serde(default)]
682 pub state_path: String,
683 #[serde(default = "default_max_classify_tokens")]
685 pub max_classify_tokens: u32,
686}
687
688fn default_classify_timeout_secs() -> u64 {
689 4
690}
691
692fn default_max_classify_tokens() -> u32 {
693 80
694}
695
696impl Default for AdaptOrchConfig {
697 fn default() -> Self {
698 Self {
699 enabled: false,
700 topology_provider: ProviderName::default(),
701 classify_timeout_secs: default_classify_timeout_secs(),
702 state_path: String::new(),
703 max_classify_tokens: default_max_classify_tokens(),
704 }
705 }
706}
707
708#[derive(Debug, Clone, Deserialize, Serialize)]
710#[serde(default)]
711pub struct ExperimentSchedule {
712 pub enabled: bool,
713 #[serde(default = "default_experiment_schedule_cron")]
714 pub cron: String,
715 #[serde(default = "default_experiment_max_experiments_per_run")]
716 pub max_experiments_per_run: u32,
717 #[serde(default = "default_experiment_schedule_max_wall_time_secs")]
722 pub max_wall_time_secs: u64,
723}
724
725impl Default for ExperimentSchedule {
726 fn default() -> Self {
727 Self {
728 enabled: false,
729 cron: default_experiment_schedule_cron(),
730 max_experiments_per_run: default_experiment_max_experiments_per_run(),
731 max_wall_time_secs: default_experiment_schedule_max_wall_time_secs(),
732 }
733 }
734}
735
736impl ExperimentConfig {
737 #[must_use = "validation result must be checked"]
743 pub fn validate(&self) -> Result<(), String> {
744 if !(1..=1_000).contains(&self.max_experiments) {
745 return Err(format!(
746 "experiments.max_experiments must be in 1..=1000, got {}",
747 self.max_experiments
748 ));
749 }
750 if !(60..=86_400).contains(&self.max_wall_time_secs) {
751 return Err(format!(
752 "experiments.max_wall_time_secs must be in 60..=86400, got {}",
753 self.max_wall_time_secs
754 ));
755 }
756 if !(1_000..=10_000_000).contains(&self.eval_budget_tokens) {
757 return Err(format!(
758 "experiments.eval_budget_tokens must be in 1000..=10000000, got {}",
759 self.eval_budget_tokens
760 ));
761 }
762 if !(0.0..=100.0).contains(&self.min_improvement) {
763 return Err(format!(
764 "experiments.min_improvement must be in 0.0..=100.0, got {}",
765 self.min_improvement
766 ));
767 }
768 if !(1..=100).contains(&self.schedule.max_experiments_per_run) {
769 return Err(format!(
770 "experiments.schedule.max_experiments_per_run must be in 1..=100, got {}",
771 self.schedule.max_experiments_per_run
772 ));
773 }
774 if !(60..=86_400).contains(&self.schedule.max_wall_time_secs) {
775 return Err(format!(
776 "experiments.schedule.max_wall_time_secs must be in 60..=86400, got {}",
777 self.schedule.max_wall_time_secs
778 ));
779 }
780 Ok(())
781 }
782}
783
784#[cfg(test)]
785mod tests {
786 use super::*;
787
788 #[test]
789 fn plan_cache_similarity_threshold_above_one_is_rejected() {
790 let cfg = PlanCacheConfig {
791 similarity_threshold: 1.1,
792 ..PlanCacheConfig::default()
793 };
794 let result = cfg.validate();
795 assert!(
796 result.is_err(),
797 "similarity_threshold = 1.1 must return a validation error"
798 );
799 }
800
801 #[test]
802 fn completeness_threshold_default_is_0_7() {
803 let cfg = OrchestrationConfig::default();
804 assert!(
805 (cfg.completeness_threshold - 0.7).abs() < f32::EPSILON,
806 "completeness_threshold default must be 0.7, got {}",
807 cfg.completeness_threshold
808 );
809 }
810
811 #[test]
812 fn completeness_threshold_serde_round_trip() {
813 let toml_in = r"
814 enabled = true
815 completeness_threshold = 0.85
816 ";
817 let cfg: OrchestrationConfig = toml::from_str(toml_in).expect("deserialize");
818 assert!((cfg.completeness_threshold - 0.85).abs() < f32::EPSILON);
819
820 let serialized = toml::to_string(&cfg).expect("serialize");
821 let cfg2: OrchestrationConfig = toml::from_str(&serialized).expect("re-deserialize");
822 assert!((cfg2.completeness_threshold - 0.85).abs() < f32::EPSILON);
823 }
824
825 #[test]
826 fn completeness_threshold_missing_uses_default() {
827 let toml_in = "enabled = true\n";
828 let cfg: OrchestrationConfig = toml::from_str(toml_in).expect("deserialize");
829 assert!(
830 (cfg.completeness_threshold - 0.7).abs() < f32::EPSILON,
831 "missing field must use default 0.7, got {}",
832 cfg.completeness_threshold
833 );
834 }
835
836 #[test]
837 fn ensemble_config_default_is_disabled() {
838 let cfg = EnsembleConfig::default();
839 assert!(!cfg.enabled);
840 assert!(!cfg.verify);
841 assert!(cfg.members.is_empty());
842 assert!((cfg.ema_alpha - 0.3).abs() < f64::EPSILON);
843 assert!((cfg.ema_decay - 0.95).abs() < f64::EPSILON);
844 assert_eq!(cfg.min_observations, 5);
845 assert_eq!(cfg.member_timeout_secs, 0);
846 }
847
848 #[test]
849 fn orchestration_config_ensemble_is_disabled_by_default() {
850 assert!(!OrchestrationConfig::default().ensemble.enabled);
851 }
852
853 #[test]
854 fn ensemble_config_serde_round_trip() {
855 let toml_in = r#"
856 enabled = true
857 verify = true
858 members = ["fast", "quality", "cheap"]
859 ema_alpha = 0.4
860 ema_decay = 0.9
861 min_observations = 10
862 member_timeout_secs = 15
863 "#;
864 let cfg: EnsembleConfig = toml::from_str(toml_in).expect("deserialize");
865 assert!(cfg.enabled);
866 assert!(cfg.verify);
867 assert_eq!(cfg.members, vec!["fast", "quality", "cheap"]);
868 assert!((cfg.ema_alpha - 0.4).abs() < f64::EPSILON);
869
870 let serialized = toml::to_string(&cfg).expect("serialize");
871 let cfg2: EnsembleConfig = toml::from_str(&serialized).expect("re-deserialize");
872 assert_eq!(cfg2.members, cfg.members);
873 assert_eq!(cfg2.member_timeout_secs, 15);
874 }
875
876 #[test]
877 fn ensemble_config_missing_section_uses_defaults() {
878 let toml_in = "enabled = true\n";
882 let cfg: OrchestrationConfig = toml::from_str(toml_in).expect("deserialize");
883 assert!(!cfg.ensemble.enabled);
884 assert!(cfg.ensemble.members.is_empty());
885 }
886
887 #[test]
888 fn asset_sensitivity_default_is_public() {
889 assert_eq!(AssetSensitivity::default(), AssetSensitivity::Public);
890 }
891
892 #[test]
893 fn asset_sensitivity_serde_snake_case() {
894 assert_eq!(
895 serde_json::to_string(&AssetSensitivity::Public).unwrap(),
896 "\"public\""
897 );
898 assert_eq!(
899 serde_json::to_string(&AssetSensitivity::Confidential).unwrap(),
900 "\"confidential\""
901 );
902 let v: AssetSensitivity = serde_json::from_str("\"internal\"").unwrap();
903 assert_eq!(v, AssetSensitivity::Internal);
904 }
905
906 #[test]
907 fn orchestration_config_default_asset_sensitivity_is_public() {
908 let cfg = OrchestrationConfig::default();
909 assert_eq!(cfg.default_asset_sensitivity, AssetSensitivity::Public);
910 }
911
912 #[test]
913 fn orchestration_config_asset_sensitivity_toml_roundtrip() {
914 let toml_in = "enabled = true\ndefault_asset_sensitivity = \"confidential\"\n";
915 let cfg: OrchestrationConfig = toml::from_str(toml_in).expect("deserialize");
916 assert_eq!(
917 cfg.default_asset_sensitivity,
918 AssetSensitivity::Confidential
919 );
920 let serialized = toml::to_string(&cfg).expect("serialize");
921 let cfg2: OrchestrationConfig = toml::from_str(&serialized).expect("re-deserialize");
922 assert_eq!(
923 cfg2.default_asset_sensitivity,
924 AssetSensitivity::Confidential
925 );
926 }
927
928 #[test]
929 fn orchestration_config_missing_asset_sensitivity_uses_default() {
930 let toml_in = "enabled = true\n";
931 let cfg: OrchestrationConfig = toml::from_str(toml_in).expect("deserialize");
932 assert_eq!(cfg.default_asset_sensitivity, AssetSensitivity::Public);
933 }
934
935 #[test]
938 fn orchestration_config_default_idle_timeout_secs_is_none() {
939 let cfg = OrchestrationConfig::default();
940 assert_eq!(cfg.default_idle_timeout_secs, None);
941 }
942
943 #[test]
944 fn orchestration_config_idle_timeout_secs_toml_roundtrip() {
945 let toml_in = "enabled = true\ndefault_idle_timeout_secs = 60\n";
946 let cfg: OrchestrationConfig = toml::from_str(toml_in).expect("deserialize");
947 assert_eq!(cfg.default_idle_timeout_secs, Some(60));
948 let serialized = toml::to_string(&cfg).expect("serialize");
949 let cfg2: OrchestrationConfig = toml::from_str(&serialized).expect("re-deserialize");
950 assert_eq!(cfg2.default_idle_timeout_secs, Some(60));
951 }
952
953 #[test]
954 fn orchestration_config_missing_idle_timeout_secs_migrates_to_none() {
955 let toml_in = "enabled = true\n";
958 let cfg: OrchestrationConfig = toml::from_str(toml_in).expect("deserialize");
959 assert_eq!(cfg.default_idle_timeout_secs, None);
960 }
961}