1use std::fmt;
5use std::str::FromStr;
6
7use crate::providers::ProviderName;
8use schemars::JsonSchema;
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
28#[serde(rename_all = "snake_case")]
29#[non_exhaustive]
30pub enum FailureStrategy {
31 #[default]
33 Abort,
34 Retry,
36 Skip,
38 Ask,
40}
41
42impl fmt::Display for FailureStrategy {
43 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44 match self {
45 Self::Abort => write!(f, "abort"),
46 Self::Retry => write!(f, "retry"),
47 Self::Skip => write!(f, "skip"),
48 Self::Ask => write!(f, "ask"),
49 }
50 }
51}
52
53impl FromStr for FailureStrategy {
54 type Err = String;
55
56 fn from_str(s: &str) -> Result<Self, Self::Err> {
57 match s {
58 "abort" => Ok(Self::Abort),
59 "retry" => Ok(Self::Retry),
60 "skip" => Ok(Self::Skip),
61 "ask" => Ok(Self::Ask),
62 other => Err(format!(
63 "unknown failure strategy '{other}': expected one of abort, retry, skip, ask"
64 )),
65 }
66 }
67}
68
69fn default_planner_max_tokens() -> u32 {
70 4096
71}
72
73fn default_aggregator_max_tokens() -> u32 {
74 4096
75}
76
77fn default_deferral_backoff_ms() -> u64 {
78 100
79}
80
81fn default_experiment_max_experiments() -> u32 {
82 20
83}
84
85fn default_experiment_max_wall_time_secs() -> u64 {
86 3600
87}
88
89fn default_experiment_min_improvement() -> f64 {
90 0.5
91}
92
93fn default_experiment_eval_budget_tokens() -> u64 {
94 100_000
95}
96
97fn default_experiment_schedule_cron() -> String {
98 "0 3 * * *".to_string()
99}
100
101fn default_experiment_max_experiments_per_run() -> u32 {
102 20
103}
104
105fn default_experiment_schedule_max_wall_time_secs() -> u64 {
106 1800
107}
108
109fn default_verify_max_tokens() -> u32 {
110 1024
111}
112
113fn default_max_replans() -> u32 {
114 2
115}
116
117fn default_completeness_threshold() -> f32 {
118 0.7
119}
120
121fn default_cascade_failure_threshold() -> f32 {
122 0.5
123}
124
125fn default_cascade_chain_threshold() -> usize {
126 3
127}
128
129fn default_lineage_ttl_secs() -> u64 {
130 300
131}
132
133fn default_max_predicate_replans() -> u32 {
134 2
135}
136
137fn default_predicate_timeout_secs() -> u64 {
138 30
139}
140
141fn default_persistence_enabled() -> bool {
142 true
143}
144
145fn default_aggregator_timeout_secs() -> u64 {
146 60
147}
148
149fn default_planner_timeout_secs() -> u64 {
150 120
151}
152
153fn default_verifier_timeout_secs() -> u64 {
154 30
155}
156
157fn default_plan_cache_similarity_threshold() -> f32 {
158 0.90
159}
160
161fn default_plan_cache_ttl_days() -> u32 {
162 30
163}
164
165fn default_plan_cache_max_templates() -> u32 {
166 100
167}
168
169#[derive(Debug, Clone, Deserialize, Serialize)]
171#[serde(default)]
172pub struct PlanCacheConfig {
173 pub enabled: bool,
175 #[serde(default = "default_plan_cache_similarity_threshold")]
177 pub similarity_threshold: f32,
178 #[serde(default = "default_plan_cache_ttl_days")]
180 pub ttl_days: u32,
181 #[serde(default = "default_plan_cache_max_templates")]
183 pub max_templates: u32,
184}
185
186impl Default for PlanCacheConfig {
187 fn default() -> Self {
188 Self {
189 enabled: false,
190 similarity_threshold: default_plan_cache_similarity_threshold(),
191 ttl_days: default_plan_cache_ttl_days(),
192 max_templates: default_plan_cache_max_templates(),
193 }
194 }
195}
196
197impl PlanCacheConfig {
198 pub fn validate(&self) -> Result<(), String> {
204 if !(0.5..=1.0).contains(&self.similarity_threshold) {
205 return Err(format!(
206 "plan_cache.similarity_threshold must be in [0.5, 1.0], got {}",
207 self.similarity_threshold
208 ));
209 }
210 if self.max_templates == 0 || self.max_templates > 10_000 {
211 return Err(format!(
212 "plan_cache.max_templates must be in [1, 10000], got {}",
213 self.max_templates
214 ));
215 }
216 if self.ttl_days == 0 || self.ttl_days > 365 {
217 return Err(format!(
218 "plan_cache.ttl_days must be in [1, 365], got {}",
219 self.ttl_days
220 ));
221 }
222 Ok(())
223 }
224}
225
226#[derive(Debug, Clone, Deserialize, Serialize)]
228#[serde(default)]
229#[allow(clippy::struct_excessive_bools)] pub struct OrchestrationConfig {
231 pub enabled: bool,
233 pub max_tasks: u32,
235 pub max_parallel: u32,
237 #[serde(default)]
239 pub default_failure_strategy: FailureStrategy,
240 pub default_max_retries: u32,
242 pub task_timeout_secs: u64,
244 #[serde(default)]
247 pub planner_provider: ProviderName,
248 #[serde(default = "default_planner_max_tokens")]
251 pub planner_max_tokens: u32,
252 pub dependency_context_budget: usize,
254 pub confirm_before_execute: bool,
256 #[serde(default = "default_aggregator_max_tokens")]
258 pub aggregator_max_tokens: u32,
259 #[serde(default = "default_deferral_backoff_ms")]
261 pub deferral_backoff_ms: u64,
262 #[serde(default)]
264 pub plan_cache: PlanCacheConfig,
265 #[serde(default)]
268 pub topology_selection: bool,
269 #[serde(default)]
272 pub verify_provider: ProviderName,
273 #[serde(default = "default_verify_max_tokens")]
275 pub verify_max_tokens: u32,
276 #[serde(default = "default_max_replans")]
281 pub max_replans: u32,
282 #[serde(default)]
288 pub verify_completeness: bool,
289 #[serde(default)]
294 pub tool_provider: ProviderName,
295 #[serde(default = "default_completeness_threshold")]
301 pub completeness_threshold: f32,
302 #[serde(default)]
306 pub cascade_routing: bool,
307 #[serde(default = "default_cascade_failure_threshold")]
310 pub cascade_failure_threshold: f32,
311 #[serde(default)]
315 pub tree_optimized_dispatch: bool,
316
317 #[serde(default)]
319 pub adaptorch: AdaptOrchConfig,
320 #[serde(default = "default_cascade_chain_threshold")]
326 pub cascade_chain_threshold: usize,
327 #[serde(default)]
333 pub cascade_failure_rate_abort_threshold: f32,
334 #[serde(default = "default_lineage_ttl_secs")]
339 pub lineage_ttl_secs: u64,
340 #[serde(default)]
345 pub verify_predicate_enabled: bool,
346 #[serde(default)]
350 pub predicate_provider: ProviderName,
351 #[serde(default = "default_max_predicate_replans")]
355 pub max_predicate_replans: u32,
356 #[serde(default = "default_predicate_timeout_secs")]
361 pub predicate_timeout_secs: u64,
362 #[serde(default = "default_persistence_enabled")]
369 pub persistence_enabled: bool,
370 #[serde(default)]
382 pub orchestrator_provider: ProviderName,
383
384 #[serde(default)]
393 pub default_task_budget_cents: f64,
394
395 #[serde(default = "default_aggregator_timeout_secs")]
400 pub aggregator_timeout_secs: u64,
401
402 #[serde(default = "default_planner_timeout_secs")]
408 pub planner_timeout_secs: u64,
409
410 #[serde(default = "default_verifier_timeout_secs")]
416 pub verifier_timeout_secs: u64,
417}
418
419impl Default for OrchestrationConfig {
420 fn default() -> Self {
421 Self {
422 enabled: false,
423 max_tasks: 20,
424 max_parallel: 4,
425 default_failure_strategy: FailureStrategy::default(),
426 default_max_retries: 3,
427 task_timeout_secs: 300,
428 planner_provider: ProviderName::default(),
429 planner_max_tokens: default_planner_max_tokens(),
430 dependency_context_budget: 16384,
431 confirm_before_execute: true,
432 aggregator_max_tokens: default_aggregator_max_tokens(),
433 deferral_backoff_ms: default_deferral_backoff_ms(),
434 plan_cache: PlanCacheConfig::default(),
435 topology_selection: false,
436 verify_provider: ProviderName::default(),
437 verify_max_tokens: default_verify_max_tokens(),
438 max_replans: default_max_replans(),
439 verify_completeness: false,
440 completeness_threshold: default_completeness_threshold(),
441 tool_provider: ProviderName::default(),
442 cascade_routing: false,
443 cascade_failure_threshold: default_cascade_failure_threshold(),
444 tree_optimized_dispatch: false,
445 adaptorch: AdaptOrchConfig::default(),
446 cascade_chain_threshold: default_cascade_chain_threshold(),
447 cascade_failure_rate_abort_threshold: 0.0,
448 lineage_ttl_secs: default_lineage_ttl_secs(),
449 verify_predicate_enabled: false,
450 predicate_provider: ProviderName::default(),
451 max_predicate_replans: default_max_predicate_replans(),
452 predicate_timeout_secs: default_predicate_timeout_secs(),
453 persistence_enabled: default_persistence_enabled(),
454 orchestrator_provider: ProviderName::default(),
455 default_task_budget_cents: 0.0,
456 aggregator_timeout_secs: default_aggregator_timeout_secs(),
457 planner_timeout_secs: default_planner_timeout_secs(),
458 verifier_timeout_secs: default_verifier_timeout_secs(),
459 }
460 }
461}
462
463#[derive(Debug, Clone, Deserialize, Serialize)]
477#[serde(default)]
478pub struct ExperimentConfig {
479 pub enabled: bool,
481 pub eval_model: Option<String>,
483 pub benchmark_file: Option<std::path::PathBuf>,
485 #[serde(default = "default_experiment_max_experiments")]
486 pub max_experiments: u32,
487 #[serde(default = "default_experiment_max_wall_time_secs")]
488 pub max_wall_time_secs: u64,
489 #[serde(default = "default_experiment_min_improvement")]
490 pub min_improvement: f64,
491 #[serde(default = "default_experiment_eval_budget_tokens")]
492 pub eval_budget_tokens: u64,
493 pub auto_apply: bool,
494 #[serde(default)]
495 pub schedule: ExperimentSchedule,
496}
497
498impl Default for ExperimentConfig {
499 fn default() -> Self {
500 Self {
501 enabled: false,
502 eval_model: None,
503 benchmark_file: None,
504 max_experiments: default_experiment_max_experiments(),
505 max_wall_time_secs: default_experiment_max_wall_time_secs(),
506 min_improvement: default_experiment_min_improvement(),
507 eval_budget_tokens: default_experiment_eval_budget_tokens(),
508 auto_apply: false,
509 schedule: ExperimentSchedule::default(),
510 }
511 }
512}
513
514#[derive(Debug, Clone, Deserialize, Serialize)]
526#[serde(default)]
527pub struct AdaptOrchConfig {
528 pub enabled: bool,
530 pub topology_provider: ProviderName,
532 #[serde(default = "default_classify_timeout_secs")]
534 pub classify_timeout_secs: u64,
535 #[serde(default)]
538 pub state_path: String,
539 #[serde(default = "default_max_classify_tokens")]
541 pub max_classify_tokens: u32,
542}
543
544fn default_classify_timeout_secs() -> u64 {
545 4
546}
547
548fn default_max_classify_tokens() -> u32 {
549 80
550}
551
552impl Default for AdaptOrchConfig {
553 fn default() -> Self {
554 Self {
555 enabled: false,
556 topology_provider: ProviderName::default(),
557 classify_timeout_secs: default_classify_timeout_secs(),
558 state_path: String::new(),
559 max_classify_tokens: default_max_classify_tokens(),
560 }
561 }
562}
563
564#[derive(Debug, Clone, Deserialize, Serialize)]
566#[serde(default)]
567pub struct ExperimentSchedule {
568 pub enabled: bool,
569 #[serde(default = "default_experiment_schedule_cron")]
570 pub cron: String,
571 #[serde(default = "default_experiment_max_experiments_per_run")]
572 pub max_experiments_per_run: u32,
573 #[serde(default = "default_experiment_schedule_max_wall_time_secs")]
578 pub max_wall_time_secs: u64,
579}
580
581impl Default for ExperimentSchedule {
582 fn default() -> Self {
583 Self {
584 enabled: false,
585 cron: default_experiment_schedule_cron(),
586 max_experiments_per_run: default_experiment_max_experiments_per_run(),
587 max_wall_time_secs: default_experiment_schedule_max_wall_time_secs(),
588 }
589 }
590}
591
592impl ExperimentConfig {
593 pub fn validate(&self) -> Result<(), String> {
599 if !(1..=1_000).contains(&self.max_experiments) {
600 return Err(format!(
601 "experiments.max_experiments must be in 1..=1000, got {}",
602 self.max_experiments
603 ));
604 }
605 if !(60..=86_400).contains(&self.max_wall_time_secs) {
606 return Err(format!(
607 "experiments.max_wall_time_secs must be in 60..=86400, got {}",
608 self.max_wall_time_secs
609 ));
610 }
611 if !(1_000..=10_000_000).contains(&self.eval_budget_tokens) {
612 return Err(format!(
613 "experiments.eval_budget_tokens must be in 1000..=10000000, got {}",
614 self.eval_budget_tokens
615 ));
616 }
617 if !(0.0..=100.0).contains(&self.min_improvement) {
618 return Err(format!(
619 "experiments.min_improvement must be in 0.0..=100.0, got {}",
620 self.min_improvement
621 ));
622 }
623 if !(1..=100).contains(&self.schedule.max_experiments_per_run) {
624 return Err(format!(
625 "experiments.schedule.max_experiments_per_run must be in 1..=100, got {}",
626 self.schedule.max_experiments_per_run
627 ));
628 }
629 if !(60..=86_400).contains(&self.schedule.max_wall_time_secs) {
630 return Err(format!(
631 "experiments.schedule.max_wall_time_secs must be in 60..=86400, got {}",
632 self.schedule.max_wall_time_secs
633 ));
634 }
635 Ok(())
636 }
637}
638
639#[cfg(test)]
640mod tests {
641 use super::*;
642
643 #[test]
644 fn plan_cache_similarity_threshold_above_one_is_rejected() {
645 let cfg = PlanCacheConfig {
646 similarity_threshold: 1.1,
647 ..PlanCacheConfig::default()
648 };
649 let result = cfg.validate();
650 assert!(
651 result.is_err(),
652 "similarity_threshold = 1.1 must return a validation error"
653 );
654 }
655
656 #[test]
657 fn completeness_threshold_default_is_0_7() {
658 let cfg = OrchestrationConfig::default();
659 assert!(
660 (cfg.completeness_threshold - 0.7).abs() < f32::EPSILON,
661 "completeness_threshold default must be 0.7, got {}",
662 cfg.completeness_threshold
663 );
664 }
665
666 #[test]
667 fn completeness_threshold_serde_round_trip() {
668 let toml_in = r"
669 enabled = true
670 completeness_threshold = 0.85
671 ";
672 let cfg: OrchestrationConfig = toml::from_str(toml_in).expect("deserialize");
673 assert!((cfg.completeness_threshold - 0.85).abs() < f32::EPSILON);
674
675 let serialized = toml::to_string(&cfg).expect("serialize");
676 let cfg2: OrchestrationConfig = toml::from_str(&serialized).expect("re-deserialize");
677 assert!((cfg2.completeness_threshold - 0.85).abs() < f32::EPSILON);
678 }
679
680 #[test]
681 fn completeness_threshold_missing_uses_default() {
682 let toml_in = "enabled = true\n";
683 let cfg: OrchestrationConfig = toml::from_str(toml_in).expect("deserialize");
684 assert!(
685 (cfg.completeness_threshold - 0.7).abs() < f32::EPSILON,
686 "missing field must use default 0.7, got {}",
687 cfg.completeness_threshold
688 );
689 }
690}