use roboticus_db::Database;
#[derive(Debug, Clone)]
pub struct CapabilityFitScore {
pub agent_id: String,
pub keyword_overlap: f64,
pub historical_success: f64,
pub avg_latency_ms: f64,
pub composite_score: f64,
}
pub fn score_agent_fit(
db: &Database,
task_description: &str,
candidate_agents: &[String],
agent_skill_tokens: &std::collections::HashMap<String, Vec<String>>,
) -> Vec<CapabilityFitScore> {
let task_tokens: std::collections::HashSet<String> = task_description
.to_ascii_lowercase()
.split(|c: char| !c.is_ascii_alphanumeric())
.filter(|t| t.len() >= 4)
.map(|s| s.to_string())
.collect();
let stats = roboticus_db::delegation::delegation_stats_by_agent(db, 168).unwrap_or_default();
let stats_map: std::collections::HashMap<
String,
&roboticus_db::delegation::AgentDelegationStats,
> = stats.iter().map(|s| (s.agent_id.clone(), s)).collect();
let max_latency = stats
.iter()
.filter_map(|s| s.avg_duration_ms)
.fold(1.0_f64, f64::max);
let mut scores: Vec<CapabilityFitScore> = candidate_agents
.iter()
.map(|agent_id| {
let skill_tokens = agent_skill_tokens
.get(agent_id)
.map(|v| v.as_slice())
.unwrap_or(&[]);
let overlap = if task_tokens.is_empty() {
0.0
} else {
let hits = skill_tokens
.iter()
.filter(|t| task_tokens.contains(t.as_str()))
.count();
(hits as f64 / task_tokens.len() as f64).min(1.0)
};
let (success_rate, latency_norm) = if let Some(s) = stats_map.get(agent_id) {
let sr = s.success_rate;
let ln = s.avg_duration_ms.map(|d| d / max_latency).unwrap_or(0.5);
(sr, ln)
} else {
(0.5, 0.5) };
let composite = 0.4 * overlap + 0.4 * success_rate + 0.2 * (1.0 - latency_norm);
CapabilityFitScore {
agent_id: agent_id.clone(),
keyword_overlap: overlap,
historical_success: success_rate,
avg_latency_ms: stats_map
.get(agent_id)
.and_then(|s| s.avg_duration_ms)
.unwrap_or(0.0),
composite_score: composite,
}
})
.collect();
scores.sort_by(|a, b| b.composite_score.partial_cmp(&a.composite_score).unwrap());
scores
}
pub fn composite_fit_ratio(scores: &[CapabilityFitScore]) -> f64 {
if scores.is_empty() {
return 0.0;
}
scores.iter().map(|s| s.composite_score).sum::<f64>() / scores.len() as f64
}
pub fn sandbox_allows_delegation(
sandbox: Option<&crate::sandbox_inheritance::SandboxInheritance>,
) -> bool {
match sandbox {
Some(sb) => sb.can_delegate(),
None => true,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_db() -> Database {
let db = Database::new(":memory:").unwrap();
roboticus_db::schema::initialize_db(&db).unwrap();
db
}
#[test]
fn score_with_no_history_gives_neutral() {
let db = test_db();
let mut skill_map = std::collections::HashMap::new();
skill_map.insert(
"agent-a".to_string(),
vec!["code".to_string(), "review".to_string()],
);
let scores = score_agent_fit(
&db,
"code review task",
&["agent-a".to_string()],
&skill_map,
);
assert_eq!(scores.len(), 1);
assert!(scores[0].keyword_overlap > 0.0);
assert!((scores[0].historical_success - 0.5).abs() < f64::EPSILON);
}
#[test]
fn agent_with_100pct_success_scores_higher() {
let db = test_db();
for i in 0..3 {
roboticus_db::delegation::insert_delegation_outcome(
&db,
&roboticus_db::delegation::DelegationOutcomeRow {
id: format!("d-a-{i}"),
turn_id: format!("t-{i}"),
session_id: "s-1".into(),
task_description: "task".into(),
subtask_count: 1,
pattern: "fan-out".into(),
assigned_agents_json: r#"["agent-a"]"#.into(),
total_duration_ms: Some(100),
success: true,
quality_score: None,
created_at: chrono::Utc::now().format("%Y-%m-%dT%H:%M:%S").to_string(),
},
)
.unwrap();
roboticus_db::delegation::insert_delegation_outcome(
&db,
&roboticus_db::delegation::DelegationOutcomeRow {
id: format!("d-b-{i}"),
turn_id: format!("t-b-{i}"),
session_id: "s-1".into(),
task_description: "task".into(),
subtask_count: 1,
pattern: "fan-out".into(),
assigned_agents_json: r#"["agent-b"]"#.into(),
total_duration_ms: Some(500),
success: false,
quality_score: None,
created_at: chrono::Utc::now().format("%Y-%m-%dT%H:%M:%S").to_string(),
},
)
.unwrap();
}
let skill_map = std::collections::HashMap::new();
let scores = score_agent_fit(
&db,
"some task",
&["agent-a".to_string(), "agent-b".to_string()],
&skill_map,
);
let a = scores.iter().find(|s| s.agent_id == "agent-a").unwrap();
let b = scores.iter().find(|s| s.agent_id == "agent-b").unwrap();
assert!(
a.composite_score > b.composite_score,
"agent-a ({:.3}) should score higher than agent-b ({:.3})",
a.composite_score,
b.composite_score
);
}
#[test]
fn composite_fit_ratio_empty() {
assert!((composite_fit_ratio(&[]) - 0.0).abs() < f64::EPSILON);
}
#[test]
fn score_empty_candidates_returns_empty() {
let db = test_db();
let scores = score_agent_fit(&db, "some task", &[], &std::collections::HashMap::new());
assert!(scores.is_empty());
}
#[test]
fn score_empty_task_description() {
let db = test_db();
let mut skill_map = std::collections::HashMap::new();
skill_map.insert(
"agent-x".to_string(),
vec!["deploy".to_string(), "release".to_string()],
);
let scores = score_agent_fit(&db, "", &["agent-x".to_string()], &skill_map);
assert_eq!(scores.len(), 1);
assert!((scores[0].keyword_overlap - 0.0).abs() < f64::EPSILON);
}
#[test]
fn composite_weights_sum_correctly() {
let db = test_db();
for i in 0..3 {
roboticus_db::delegation::insert_delegation_outcome(
&db,
&roboticus_db::delegation::DelegationOutcomeRow {
id: format!("perf-{i}"),
turn_id: format!("t-perf-{i}"),
session_id: "s-1".into(),
task_description: "task".into(),
subtask_count: 1,
pattern: "fan-out".into(),
assigned_agents_json: r#"["agent-perfect"]"#.into(),
total_duration_ms: Some(0),
success: true,
quality_score: None,
created_at: chrono::Utc::now().format("%Y-%m-%dT%H:%M:%S").to_string(),
},
)
.unwrap();
}
let mut skill_map = std::collections::HashMap::new();
skill_map.insert(
"agent-perfect".to_string(),
vec!["code".to_string(), "review".to_string()],
);
let scores = score_agent_fit(
&db,
"code review",
&["agent-perfect".to_string()],
&skill_map,
);
assert_eq!(scores.len(), 1);
assert!(
(scores[0].composite_score - 1.0).abs() < 0.01,
"expected ~1.0, got {}",
scores[0].composite_score
);
}
#[test]
fn scores_sorted_descending() {
let db = test_db();
let mut skill_map = std::collections::HashMap::new();
skill_map.insert(
"agent-a".to_string(),
vec!["deploy".to_string(), "release".to_string()],
);
skill_map.insert("agent-b".to_string(), vec!["deploy".to_string()]);
skill_map.insert("agent-c".to_string(), vec!["unrelated".to_string()]);
let scores = score_agent_fit(
&db,
"deploy release",
&[
"agent-a".to_string(),
"agent-b".to_string(),
"agent-c".to_string(),
],
&skill_map,
);
assert_eq!(scores.len(), 3);
for w in scores.windows(2) {
assert!(
w[0].composite_score >= w[1].composite_score,
"{} should >= {}",
w[0].composite_score,
w[1].composite_score
);
}
assert_eq!(scores[0].agent_id, "agent-a");
}
#[test]
fn latency_normalization_single_agent() {
let db = test_db();
roboticus_db::delegation::insert_delegation_outcome(
&db,
&roboticus_db::delegation::DelegationOutcomeRow {
id: "single-1".into(),
turn_id: "t-s1".into(),
session_id: "s-1".into(),
task_description: "task".into(),
subtask_count: 1,
pattern: "fan-out".into(),
assigned_agents_json: r#"["agent-solo"]"#.into(),
total_duration_ms: Some(500),
success: true,
quality_score: None,
created_at: chrono::Utc::now().format("%Y-%m-%dT%H:%M:%S").to_string(),
},
)
.unwrap();
let scores = score_agent_fit(
&db,
"some task",
&["agent-solo".to_string()],
&std::collections::HashMap::new(),
);
assert_eq!(scores.len(), 1);
assert!(
(scores[0].composite_score - 0.4).abs() < 0.01,
"expected ~0.4, got {}",
scores[0].composite_score
);
}
#[test]
fn sandbox_allows_delegation_none() {
assert!(super::sandbox_allows_delegation(None));
}
#[test]
fn sandbox_allows_delegation_ok() {
let sb = crate::sandbox_inheritance::SandboxInheritance {
depth: 0,
max_depth: 4,
..Default::default()
};
assert!(super::sandbox_allows_delegation(Some(&sb)));
}
#[test]
fn sandbox_blocks_at_max_depth() {
let sb = crate::sandbox_inheritance::SandboxInheritance {
depth: 4,
max_depth: 4,
..Default::default()
};
assert!(!super::sandbox_allows_delegation(Some(&sb)));
}
#[test]
fn composite_fit_ratio_averages() {
let scores = vec![
CapabilityFitScore {
agent_id: "a".into(),
keyword_overlap: 0.5,
historical_success: 0.5,
avg_latency_ms: 100.0,
composite_score: 0.8,
},
CapabilityFitScore {
agent_id: "b".into(),
keyword_overlap: 0.3,
historical_success: 0.5,
avg_latency_ms: 200.0,
composite_score: 0.6,
},
CapabilityFitScore {
agent_id: "c".into(),
keyword_overlap: 0.1,
historical_success: 0.5,
avg_latency_ms: 300.0,
composite_score: 0.4,
},
];
let ratio = composite_fit_ratio(&scores);
let expected = (0.8 + 0.6 + 0.4) / 3.0;
assert!(
(ratio - expected).abs() < f64::EPSILON,
"expected {expected}, got {ratio}"
);
}
}