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_plan_cache_similarity_threshold() -> f32 {
188 0.90
189}
190
191fn default_plan_cache_ttl_days() -> u32 {
192 30
193}
194
195fn default_plan_cache_max_templates() -> u32 {
196 100
197}
198
199#[derive(Debug, Clone, Deserialize, Serialize)]
201#[serde(default)]
202pub struct PlanCacheConfig {
203 pub enabled: bool,
205 #[serde(default = "default_plan_cache_similarity_threshold")]
207 pub similarity_threshold: f32,
208 #[serde(default = "default_plan_cache_ttl_days")]
210 pub ttl_days: u32,
211 #[serde(default = "default_plan_cache_max_templates")]
213 pub max_templates: u32,
214}
215
216impl Default for PlanCacheConfig {
217 fn default() -> Self {
218 Self {
219 enabled: false,
220 similarity_threshold: default_plan_cache_similarity_threshold(),
221 ttl_days: default_plan_cache_ttl_days(),
222 max_templates: default_plan_cache_max_templates(),
223 }
224 }
225}
226
227impl PlanCacheConfig {
228 #[must_use = "validation result must be checked"]
234 pub fn validate(&self) -> Result<(), String> {
235 if !(0.5..=1.0).contains(&self.similarity_threshold) {
236 return Err(format!(
237 "plan_cache.similarity_threshold must be in [0.5, 1.0], got {}",
238 self.similarity_threshold
239 ));
240 }
241 if self.max_templates == 0 || self.max_templates > 10_000 {
242 return Err(format!(
243 "plan_cache.max_templates must be in [1, 10000], got {}",
244 self.max_templates
245 ));
246 }
247 if self.ttl_days == 0 || self.ttl_days > 365 {
248 return Err(format!(
249 "plan_cache.ttl_days must be in [1, 365], got {}",
250 self.ttl_days
251 ));
252 }
253 Ok(())
254 }
255}
256
257#[derive(Debug, Clone, Deserialize, Serialize)]
259#[serde(default)]
260#[allow(clippy::struct_excessive_bools)] pub struct OrchestrationConfig {
262 pub enabled: bool,
264 pub max_tasks: u32,
266 pub max_parallel: u32,
268 #[serde(default)]
270 pub default_failure_strategy: FailureStrategy,
271 pub default_max_retries: u32,
273 pub task_timeout_secs: u64,
275 #[serde(default)]
278 pub planner_provider: ProviderName,
279 #[serde(default = "default_planner_max_tokens")]
282 pub planner_max_tokens: u32,
283 pub dependency_context_budget: usize,
285 pub confirm_before_execute: bool,
287 #[serde(default = "default_aggregator_max_tokens")]
289 pub aggregator_max_tokens: u32,
290 #[serde(default = "default_deferral_backoff_ms")]
292 pub deferral_backoff_ms: u64,
293 #[serde(default)]
295 pub plan_cache: PlanCacheConfig,
296 #[serde(default)]
299 pub topology_selection: bool,
300 #[serde(default)]
303 pub verify_provider: ProviderName,
304 #[serde(default = "default_verify_max_tokens")]
306 pub verify_max_tokens: u32,
307 #[serde(default = "default_max_replans")]
312 pub max_replans: u32,
313 #[serde(default)]
319 pub verify_completeness: bool,
320 #[serde(default)]
325 pub tool_provider: ProviderName,
326 #[serde(default = "default_completeness_threshold")]
332 pub completeness_threshold: f32,
333 #[serde(default)]
337 pub cascade_routing: bool,
338 #[serde(default = "default_cascade_failure_threshold")]
341 pub cascade_failure_threshold: f32,
342 #[serde(default)]
346 pub tree_optimized_dispatch: bool,
347
348 #[serde(default)]
350 pub adaptorch: AdaptOrchConfig,
351 #[serde(default = "default_cascade_chain_threshold")]
357 pub cascade_chain_threshold: usize,
358 #[serde(default)]
364 pub cascade_failure_rate_abort_threshold: f32,
365 #[serde(default = "default_lineage_ttl_secs")]
370 pub lineage_ttl_secs: u64,
371 #[serde(default)]
376 pub verify_predicate_enabled: bool,
377 #[serde(default)]
381 pub predicate_provider: ProviderName,
382 #[serde(default = "default_max_predicate_replans")]
386 pub max_predicate_replans: u32,
387 #[serde(default = "default_predicate_timeout_secs")]
392 pub predicate_timeout_secs: u64,
393 #[serde(default = "default_persistence_enabled")]
400 pub persistence_enabled: bool,
401 #[serde(default)]
413 pub orchestrator_provider: ProviderName,
414
415 #[serde(default)]
424 pub default_task_budget_cents: f64,
425
426 #[serde(default)]
434 pub default_asset_sensitivity: AssetSensitivity,
435
436 #[serde(default = "default_aggregator_timeout_secs")]
441 pub aggregator_timeout_secs: u64,
442
443 #[serde(default = "default_planner_timeout_secs")]
449 pub planner_timeout_secs: u64,
450
451 #[serde(default = "default_verifier_timeout_secs")]
457 pub verifier_timeout_secs: u64,
458}
459
460impl Default for OrchestrationConfig {
461 fn default() -> Self {
462 Self {
463 enabled: false,
464 max_tasks: 20,
465 max_parallel: 4,
466 default_failure_strategy: FailureStrategy::default(),
467 default_max_retries: 3,
468 task_timeout_secs: 300,
469 planner_provider: ProviderName::default(),
470 planner_max_tokens: default_planner_max_tokens(),
471 dependency_context_budget: 16384,
472 confirm_before_execute: true,
473 aggregator_max_tokens: default_aggregator_max_tokens(),
474 deferral_backoff_ms: default_deferral_backoff_ms(),
475 plan_cache: PlanCacheConfig::default(),
476 topology_selection: false,
477 verify_provider: ProviderName::default(),
478 verify_max_tokens: default_verify_max_tokens(),
479 max_replans: default_max_replans(),
480 verify_completeness: false,
481 completeness_threshold: default_completeness_threshold(),
482 tool_provider: ProviderName::default(),
483 cascade_routing: false,
484 cascade_failure_threshold: default_cascade_failure_threshold(),
485 tree_optimized_dispatch: false,
486 adaptorch: AdaptOrchConfig::default(),
487 cascade_chain_threshold: default_cascade_chain_threshold(),
488 cascade_failure_rate_abort_threshold: 0.0,
489 lineage_ttl_secs: default_lineage_ttl_secs(),
490 verify_predicate_enabled: false,
491 predicate_provider: ProviderName::default(),
492 max_predicate_replans: default_max_predicate_replans(),
493 predicate_timeout_secs: default_predicate_timeout_secs(),
494 persistence_enabled: default_persistence_enabled(),
495 orchestrator_provider: ProviderName::default(),
496 default_task_budget_cents: 0.0,
497 default_asset_sensitivity: AssetSensitivity::default(),
498 aggregator_timeout_secs: default_aggregator_timeout_secs(),
499 planner_timeout_secs: default_planner_timeout_secs(),
500 verifier_timeout_secs: default_verifier_timeout_secs(),
501 }
502 }
503}
504
505#[derive(Debug, Clone, Deserialize, Serialize)]
519#[serde(default)]
520pub struct ExperimentConfig {
521 pub enabled: bool,
523 #[serde(default)]
527 pub eval_provider: ProviderName,
528 pub benchmark_file: Option<std::path::PathBuf>,
530 #[serde(default = "default_experiment_max_experiments")]
531 pub max_experiments: u32,
532 #[serde(default = "default_experiment_max_wall_time_secs")]
533 pub max_wall_time_secs: u64,
534 #[serde(default = "default_experiment_min_improvement")]
535 pub min_improvement: f64,
536 #[serde(default = "default_experiment_eval_budget_tokens")]
537 pub eval_budget_tokens: u64,
538 pub auto_apply: bool,
539 #[serde(default)]
540 pub schedule: ExperimentSchedule,
541 #[serde(default)]
549 pub tolerate_subject_errors: bool,
550}
551
552impl Default for ExperimentConfig {
553 fn default() -> Self {
554 Self {
555 enabled: false,
556 eval_provider: ProviderName::default(),
557 benchmark_file: None,
558 max_experiments: default_experiment_max_experiments(),
559 max_wall_time_secs: default_experiment_max_wall_time_secs(),
560 min_improvement: default_experiment_min_improvement(),
561 eval_budget_tokens: default_experiment_eval_budget_tokens(),
562 auto_apply: false,
563 schedule: ExperimentSchedule::default(),
564 tolerate_subject_errors: false,
565 }
566 }
567}
568
569#[derive(Debug, Clone, Deserialize, Serialize)]
581#[serde(default)]
582pub struct AdaptOrchConfig {
583 pub enabled: bool,
585 pub topology_provider: ProviderName,
587 #[serde(default = "default_classify_timeout_secs")]
589 pub classify_timeout_secs: u64,
590 #[serde(default)]
593 pub state_path: String,
594 #[serde(default = "default_max_classify_tokens")]
596 pub max_classify_tokens: u32,
597}
598
599fn default_classify_timeout_secs() -> u64 {
600 4
601}
602
603fn default_max_classify_tokens() -> u32 {
604 80
605}
606
607impl Default for AdaptOrchConfig {
608 fn default() -> Self {
609 Self {
610 enabled: false,
611 topology_provider: ProviderName::default(),
612 classify_timeout_secs: default_classify_timeout_secs(),
613 state_path: String::new(),
614 max_classify_tokens: default_max_classify_tokens(),
615 }
616 }
617}
618
619#[derive(Debug, Clone, Deserialize, Serialize)]
621#[serde(default)]
622pub struct ExperimentSchedule {
623 pub enabled: bool,
624 #[serde(default = "default_experiment_schedule_cron")]
625 pub cron: String,
626 #[serde(default = "default_experiment_max_experiments_per_run")]
627 pub max_experiments_per_run: u32,
628 #[serde(default = "default_experiment_schedule_max_wall_time_secs")]
633 pub max_wall_time_secs: u64,
634}
635
636impl Default for ExperimentSchedule {
637 fn default() -> Self {
638 Self {
639 enabled: false,
640 cron: default_experiment_schedule_cron(),
641 max_experiments_per_run: default_experiment_max_experiments_per_run(),
642 max_wall_time_secs: default_experiment_schedule_max_wall_time_secs(),
643 }
644 }
645}
646
647impl ExperimentConfig {
648 #[must_use = "validation result must be checked"]
654 pub fn validate(&self) -> Result<(), String> {
655 if !(1..=1_000).contains(&self.max_experiments) {
656 return Err(format!(
657 "experiments.max_experiments must be in 1..=1000, got {}",
658 self.max_experiments
659 ));
660 }
661 if !(60..=86_400).contains(&self.max_wall_time_secs) {
662 return Err(format!(
663 "experiments.max_wall_time_secs must be in 60..=86400, got {}",
664 self.max_wall_time_secs
665 ));
666 }
667 if !(1_000..=10_000_000).contains(&self.eval_budget_tokens) {
668 return Err(format!(
669 "experiments.eval_budget_tokens must be in 1000..=10000000, got {}",
670 self.eval_budget_tokens
671 ));
672 }
673 if !(0.0..=100.0).contains(&self.min_improvement) {
674 return Err(format!(
675 "experiments.min_improvement must be in 0.0..=100.0, got {}",
676 self.min_improvement
677 ));
678 }
679 if !(1..=100).contains(&self.schedule.max_experiments_per_run) {
680 return Err(format!(
681 "experiments.schedule.max_experiments_per_run must be in 1..=100, got {}",
682 self.schedule.max_experiments_per_run
683 ));
684 }
685 if !(60..=86_400).contains(&self.schedule.max_wall_time_secs) {
686 return Err(format!(
687 "experiments.schedule.max_wall_time_secs must be in 60..=86400, got {}",
688 self.schedule.max_wall_time_secs
689 ));
690 }
691 Ok(())
692 }
693}
694
695#[cfg(test)]
696mod tests {
697 use super::*;
698
699 #[test]
700 fn plan_cache_similarity_threshold_above_one_is_rejected() {
701 let cfg = PlanCacheConfig {
702 similarity_threshold: 1.1,
703 ..PlanCacheConfig::default()
704 };
705 let result = cfg.validate();
706 assert!(
707 result.is_err(),
708 "similarity_threshold = 1.1 must return a validation error"
709 );
710 }
711
712 #[test]
713 fn completeness_threshold_default_is_0_7() {
714 let cfg = OrchestrationConfig::default();
715 assert!(
716 (cfg.completeness_threshold - 0.7).abs() < f32::EPSILON,
717 "completeness_threshold default must be 0.7, got {}",
718 cfg.completeness_threshold
719 );
720 }
721
722 #[test]
723 fn completeness_threshold_serde_round_trip() {
724 let toml_in = r"
725 enabled = true
726 completeness_threshold = 0.85
727 ";
728 let cfg: OrchestrationConfig = toml::from_str(toml_in).expect("deserialize");
729 assert!((cfg.completeness_threshold - 0.85).abs() < f32::EPSILON);
730
731 let serialized = toml::to_string(&cfg).expect("serialize");
732 let cfg2: OrchestrationConfig = toml::from_str(&serialized).expect("re-deserialize");
733 assert!((cfg2.completeness_threshold - 0.85).abs() < f32::EPSILON);
734 }
735
736 #[test]
737 fn completeness_threshold_missing_uses_default() {
738 let toml_in = "enabled = true\n";
739 let cfg: OrchestrationConfig = toml::from_str(toml_in).expect("deserialize");
740 assert!(
741 (cfg.completeness_threshold - 0.7).abs() < f32::EPSILON,
742 "missing field must use default 0.7, got {}",
743 cfg.completeness_threshold
744 );
745 }
746
747 #[test]
748 fn asset_sensitivity_default_is_public() {
749 assert_eq!(AssetSensitivity::default(), AssetSensitivity::Public);
750 }
751
752 #[test]
753 fn asset_sensitivity_serde_snake_case() {
754 assert_eq!(
755 serde_json::to_string(&AssetSensitivity::Public).unwrap(),
756 "\"public\""
757 );
758 assert_eq!(
759 serde_json::to_string(&AssetSensitivity::Confidential).unwrap(),
760 "\"confidential\""
761 );
762 let v: AssetSensitivity = serde_json::from_str("\"internal\"").unwrap();
763 assert_eq!(v, AssetSensitivity::Internal);
764 }
765
766 #[test]
767 fn orchestration_config_default_asset_sensitivity_is_public() {
768 let cfg = OrchestrationConfig::default();
769 assert_eq!(cfg.default_asset_sensitivity, AssetSensitivity::Public);
770 }
771
772 #[test]
773 fn orchestration_config_asset_sensitivity_toml_roundtrip() {
774 let toml_in = "enabled = true\ndefault_asset_sensitivity = \"confidential\"\n";
775 let cfg: OrchestrationConfig = toml::from_str(toml_in).expect("deserialize");
776 assert_eq!(
777 cfg.default_asset_sensitivity,
778 AssetSensitivity::Confidential
779 );
780 let serialized = toml::to_string(&cfg).expect("serialize");
781 let cfg2: OrchestrationConfig = toml::from_str(&serialized).expect("re-deserialize");
782 assert_eq!(
783 cfg2.default_asset_sensitivity,
784 AssetSensitivity::Confidential
785 );
786 }
787
788 #[test]
789 fn orchestration_config_missing_asset_sensitivity_uses_default() {
790 let toml_in = "enabled = true\n";
791 let cfg: OrchestrationConfig = toml::from_str(toml_in).expect("deserialize");
792 assert_eq!(cfg.default_asset_sensitivity, AssetSensitivity::Public);
793 }
794}