use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CoordinatorBudget {
pub max_cost_usd: Option<f64>,
pub max_latency_ms: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TiebreakerConfig {
pub model: String,
pub threshold: f64,
#[serde(default = "default_max_candidates")]
pub max_candidates: usize,
}
fn default_max_candidates() -> usize {
3
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DimensionScores {
pub capability_fit: f64,
pub cost_fit: f64,
pub latency_fit: f64,
pub trust_compatibility: f64,
pub historical_performance: f64,
}
impl DimensionScores {
pub fn composite(&self, weights: &DimensionWeights) -> f64 {
let total_weight = weights.capability_fit
+ weights.cost_fit
+ weights.latency_fit
+ weights.trust_compatibility
+ weights.historical_performance;
if total_weight == 0.0 {
return 0.0;
}
let weighted_sum = self.capability_fit * weights.capability_fit
+ self.cost_fit * weights.cost_fit
+ self.latency_fit * weights.latency_fit
+ self.trust_compatibility * weights.trust_compatibility
+ self.historical_performance * weights.historical_performance;
weighted_sum / total_weight
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DimensionWeights {
pub capability_fit: f64,
pub cost_fit: f64,
pub latency_fit: f64,
pub trust_compatibility: f64,
pub historical_performance: f64,
}
impl Default for DimensionWeights {
fn default() -> Self {
Self {
capability_fit: 1.0,
cost_fit: 1.0,
latency_fit: 1.0,
trust_compatibility: 1.0,
historical_performance: 1.0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScoringResult {
pub agent_uri: String,
pub scores: DimensionScores,
pub composite: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DecisionMethod {
SingleCandidate,
Structured,
LlmTiebreaker,
NoCandidates,
BelowThreshold,
TiebreakerFailed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RejectedAgent {
pub agent_uri: String,
pub reason: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenUsage {
pub input: u32,
pub output: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CoordinatorDecision {
pub selected: Option<String>,
pub method: DecisionMethod,
pub reasoning: Option<String>,
pub confidence: f64,
pub rejected: Vec<RejectedAgent>,
pub tiebreaker_tokens: Option<TokenUsage>,
pub tiebreaker_cost: Option<f64>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn composite_equal_weights() {
let scores = DimensionScores {
capability_fit: 0.8,
cost_fit: 0.6,
latency_fit: 0.7,
trust_compatibility: 1.0,
historical_performance: 0.9,
};
let weights = DimensionWeights::default();
let c = scores.composite(&weights);
assert!((c - 0.8).abs() < 1e-9, "composite={c}");
}
#[test]
fn composite_zero_weight_dimension_ignored() {
let scores = DimensionScores {
capability_fit: 1.0,
cost_fit: 0.0, latency_fit: 1.0,
trust_compatibility: 1.0,
historical_performance: 1.0,
};
let weights = DimensionWeights {
capability_fit: 1.0,
cost_fit: 0.0,
latency_fit: 1.0,
trust_compatibility: 1.0,
historical_performance: 1.0,
};
let c = scores.composite(&weights);
assert!((c - 1.0).abs() < 1e-9, "composite={c}");
}
#[test]
fn composite_all_zero_weights_returns_zero() {
let scores = DimensionScores {
capability_fit: 0.9,
cost_fit: 0.9,
latency_fit: 0.9,
trust_compatibility: 0.9,
historical_performance: 0.9,
};
let weights = DimensionWeights {
capability_fit: 0.0,
cost_fit: 0.0,
latency_fit: 0.0,
trust_compatibility: 0.0,
historical_performance: 0.0,
};
assert_eq!(scores.composite(&weights), 0.0);
}
#[test]
fn tiebreaker_config_default_max_candidates() {
let json = r#"{"model":"claude-3-5-haiku-20241022","threshold":0.05}"#;
let cfg: TiebreakerConfig = serde_json::from_str(json).unwrap();
assert_eq!(cfg.max_candidates, 3);
}
#[test]
fn decision_method_snake_case_roundtrip() {
let method = DecisionMethod::LlmTiebreaker;
let json = serde_json::to_string(&method).unwrap();
assert_eq!(json, r#""llm_tiebreaker""#);
let back: DecisionMethod = serde_json::from_str(&json).unwrap();
assert!(matches!(back, DecisionMethod::LlmTiebreaker));
}
}