use serde::{Deserialize, Serialize};
fn default_planner_max_tokens() -> u32 {
4096
}
fn default_aggregator_max_tokens() -> u32 {
4096
}
fn default_deferral_backoff_ms() -> u64 {
100
}
fn default_experiment_max_experiments() -> u32 {
20
}
fn default_experiment_max_wall_time_secs() -> u64 {
3600
}
fn default_experiment_min_improvement() -> f64 {
0.5
}
fn default_experiment_eval_budget_tokens() -> u64 {
100_000
}
fn default_experiment_schedule_cron() -> String {
"0 3 * * *".to_string()
}
fn default_experiment_max_experiments_per_run() -> u32 {
20
}
fn default_experiment_schedule_max_wall_time_secs() -> u64 {
1800
}
fn default_verify_max_tokens() -> u32 {
1024
}
fn default_max_replans() -> u32 {
2
}
fn default_completeness_threshold() -> f32 {
0.7
}
fn default_cascade_failure_threshold() -> f32 {
0.5
}
fn default_plan_cache_similarity_threshold() -> f32 {
0.90
}
fn default_plan_cache_ttl_days() -> u32 {
30
}
fn default_plan_cache_max_templates() -> u32 {
100
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct PlanCacheConfig {
pub enabled: bool,
#[serde(default = "default_plan_cache_similarity_threshold")]
pub similarity_threshold: f32,
#[serde(default = "default_plan_cache_ttl_days")]
pub ttl_days: u32,
#[serde(default = "default_plan_cache_max_templates")]
pub max_templates: u32,
}
impl Default for PlanCacheConfig {
fn default() -> Self {
Self {
enabled: false,
similarity_threshold: default_plan_cache_similarity_threshold(),
ttl_days: default_plan_cache_ttl_days(),
max_templates: default_plan_cache_max_templates(),
}
}
}
impl PlanCacheConfig {
pub fn validate(&self) -> Result<(), String> {
if !(0.5..=1.0).contains(&self.similarity_threshold) {
return Err(format!(
"plan_cache.similarity_threshold must be in [0.5, 1.0], got {}",
self.similarity_threshold
));
}
if self.max_templates == 0 || self.max_templates > 10_000 {
return Err(format!(
"plan_cache.max_templates must be in [1, 10000], got {}",
self.max_templates
));
}
if self.ttl_days == 0 || self.ttl_days > 365 {
return Err(format!(
"plan_cache.ttl_days must be in [1, 365], got {}",
self.ttl_days
));
}
Ok(())
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
#[allow(clippy::struct_excessive_bools)]
pub struct OrchestrationConfig {
pub enabled: bool,
pub max_tasks: u32,
pub max_parallel: u32,
pub default_failure_strategy: String,
pub default_max_retries: u32,
pub task_timeout_secs: u64,
#[serde(default)]
pub planner_provider: String,
#[serde(default = "default_planner_max_tokens")]
pub planner_max_tokens: u32,
pub dependency_context_budget: usize,
pub confirm_before_execute: bool,
#[serde(default = "default_aggregator_max_tokens")]
pub aggregator_max_tokens: u32,
#[serde(default = "default_deferral_backoff_ms")]
pub deferral_backoff_ms: u64,
#[serde(default)]
pub plan_cache: PlanCacheConfig,
#[serde(default)]
pub topology_selection: bool,
#[serde(default)]
pub verify_provider: String,
#[serde(default = "default_verify_max_tokens")]
pub verify_max_tokens: u32,
#[serde(default = "default_max_replans")]
pub max_replans: u32,
#[serde(default)]
pub verify_completeness: bool,
#[serde(default)]
pub tool_provider: String,
#[serde(default = "default_completeness_threshold")]
pub completeness_threshold: f32,
#[serde(default)]
pub cascade_routing: bool,
#[serde(default = "default_cascade_failure_threshold")]
pub cascade_failure_threshold: f32,
#[serde(default)]
pub tree_optimized_dispatch: bool,
}
impl Default for OrchestrationConfig {
fn default() -> Self {
Self {
enabled: false,
max_tasks: 20,
max_parallel: 4,
default_failure_strategy: "abort".to_string(),
default_max_retries: 3,
task_timeout_secs: 300,
planner_provider: String::new(),
planner_max_tokens: default_planner_max_tokens(),
dependency_context_budget: 16384,
confirm_before_execute: true,
aggregator_max_tokens: default_aggregator_max_tokens(),
deferral_backoff_ms: default_deferral_backoff_ms(),
plan_cache: PlanCacheConfig::default(),
topology_selection: false,
verify_provider: String::new(),
verify_max_tokens: default_verify_max_tokens(),
max_replans: default_max_replans(),
verify_completeness: false,
completeness_threshold: default_completeness_threshold(),
tool_provider: String::new(),
cascade_routing: false,
cascade_failure_threshold: default_cascade_failure_threshold(),
tree_optimized_dispatch: false,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct ExperimentConfig {
pub enabled: bool,
pub eval_model: Option<String>,
pub benchmark_file: Option<std::path::PathBuf>,
#[serde(default = "default_experiment_max_experiments")]
pub max_experiments: u32,
#[serde(default = "default_experiment_max_wall_time_secs")]
pub max_wall_time_secs: u64,
#[serde(default = "default_experiment_min_improvement")]
pub min_improvement: f64,
#[serde(default = "default_experiment_eval_budget_tokens")]
pub eval_budget_tokens: u64,
pub auto_apply: bool,
#[serde(default)]
pub schedule: ExperimentSchedule,
}
impl Default for ExperimentConfig {
fn default() -> Self {
Self {
enabled: false,
eval_model: None,
benchmark_file: None,
max_experiments: default_experiment_max_experiments(),
max_wall_time_secs: default_experiment_max_wall_time_secs(),
min_improvement: default_experiment_min_improvement(),
eval_budget_tokens: default_experiment_eval_budget_tokens(),
auto_apply: false,
schedule: ExperimentSchedule::default(),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct ExperimentSchedule {
pub enabled: bool,
#[serde(default = "default_experiment_schedule_cron")]
pub cron: String,
#[serde(default = "default_experiment_max_experiments_per_run")]
pub max_experiments_per_run: u32,
#[serde(default = "default_experiment_schedule_max_wall_time_secs")]
pub max_wall_time_secs: u64,
}
impl Default for ExperimentSchedule {
fn default() -> Self {
Self {
enabled: false,
cron: default_experiment_schedule_cron(),
max_experiments_per_run: default_experiment_max_experiments_per_run(),
max_wall_time_secs: default_experiment_schedule_max_wall_time_secs(),
}
}
}
impl ExperimentConfig {
pub fn validate(&self) -> Result<(), String> {
if !(1..=1_000).contains(&self.max_experiments) {
return Err(format!(
"experiments.max_experiments must be in 1..=1000, got {}",
self.max_experiments
));
}
if !(60..=86_400).contains(&self.max_wall_time_secs) {
return Err(format!(
"experiments.max_wall_time_secs must be in 60..=86400, got {}",
self.max_wall_time_secs
));
}
if !(1_000..=10_000_000).contains(&self.eval_budget_tokens) {
return Err(format!(
"experiments.eval_budget_tokens must be in 1000..=10000000, got {}",
self.eval_budget_tokens
));
}
if !(0.0..=100.0).contains(&self.min_improvement) {
return Err(format!(
"experiments.min_improvement must be in 0.0..=100.0, got {}",
self.min_improvement
));
}
if !(1..=100).contains(&self.schedule.max_experiments_per_run) {
return Err(format!(
"experiments.schedule.max_experiments_per_run must be in 1..=100, got {}",
self.schedule.max_experiments_per_run
));
}
if !(60..=86_400).contains(&self.schedule.max_wall_time_secs) {
return Err(format!(
"experiments.schedule.max_wall_time_secs must be in 60..=86400, got {}",
self.schedule.max_wall_time_secs
));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn plan_cache_similarity_threshold_above_one_is_rejected() {
let cfg = PlanCacheConfig {
similarity_threshold: 1.1,
..PlanCacheConfig::default()
};
let result = cfg.validate();
assert!(
result.is_err(),
"similarity_threshold = 1.1 must return a validation error"
);
}
#[test]
fn completeness_threshold_default_is_0_7() {
let cfg = OrchestrationConfig::default();
assert!(
(cfg.completeness_threshold - 0.7).abs() < f32::EPSILON,
"completeness_threshold default must be 0.7, got {}",
cfg.completeness_threshold
);
}
#[test]
fn completeness_threshold_serde_round_trip() {
let toml_in = r"
enabled = true
completeness_threshold = 0.85
";
let cfg: OrchestrationConfig = toml::from_str(toml_in).expect("deserialize");
assert!((cfg.completeness_threshold - 0.85).abs() < f32::EPSILON);
let serialized = toml::to_string(&cfg).expect("serialize");
let cfg2: OrchestrationConfig = toml::from_str(&serialized).expect("re-deserialize");
assert!((cfg2.completeness_threshold - 0.85).abs() < f32::EPSILON);
}
#[test]
fn completeness_threshold_missing_uses_default() {
let toml_in = "enabled = true\n";
let cfg: OrchestrationConfig = toml::from_str(toml_in).expect("deserialize");
assert!(
(cfg.completeness_threshold - 0.7).abs() < f32::EPSILON,
"missing field must use default 0.7, got {}",
cfg.completeness_threshold
);
}
}