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