use car_topology::{
CoordinationShape, ExecutionRecord, Homogeneity, JournalError, RecordJournal, ScorerAdvice,
Selection, Topology, TopologyError, TopologySelector,
};
use car_ir::{AgentOutcome, EvidenceKind, OutcomeStatus};
use crate::types::{AgentOutput, AgentSpec};
pub fn measured_tokens(outputs: &[AgentOutput]) -> Option<u64> {
let mut total = 0u64;
let mut any = false;
for output in outputs {
if let Some(tokens) = &output.tokens {
any = true;
total = total
.saturating_add(tokens.input_tokens)
.saturating_add(tokens.output_tokens);
}
}
any.then_some(total)
}
pub fn execution_record(
task_id: impl Into<String>,
query: Vec<f32>,
shape: CoordinationShape,
team_size: usize,
outputs: &[AgentOutput],
utility: f32,
) -> Result<Option<ExecutionRecord>, TopologyError> {
if shape == CoordinationShape::Solo {
return Err(TopologyError::BadConfig {
field: "shape",
expected: "a shape with a distinguishable adjacency",
found: "solo — indistinguishable from swarm; record the run under its real team size, or not at all"
.into(),
});
}
let Some(tokens) = measured_tokens(outputs) else {
return Ok(None);
};
let topology = shape.topology(team_size)?;
Ok(Some(ExecutionRecord::new(
task_id, query, topology, utility, tokens,
)))
}
pub fn execution_record_for(
task_id: impl Into<String>,
query: Vec<f32>,
topology: Topology,
outputs: &[AgentOutput],
utility: f32,
) -> Option<ExecutionRecord> {
let tokens = measured_tokens(outputs)?;
Some(ExecutionRecord::new(
task_id, query, topology, utility, tokens,
))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UtilityEvidence {
Reported,
Grounded,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UtilityAggregation {
Final,
Consensus,
}
pub fn outcome_utility(outcome: &AgentOutcome, evidence: UtilityEvidence) -> Option<f32> {
if evidence == UtilityEvidence::Grounded
&& !outcome
.evidence
.iter()
.any(|e| e.kind != EvidenceKind::SelfAssessment)
{
return None;
}
match outcome.status {
OutcomeStatus::Success => Some(1.0),
OutcomeStatus::PartialSuccess => Some(0.5),
OutcomeStatus::Failure | OutcomeStatus::GiveUp | OutcomeStatus::Timeout => Some(0.0),
OutcomeStatus::Done => None,
}
}
pub fn run_utility(
outputs: &[AgentOutput],
aggregation: UtilityAggregation,
evidence: UtilityEvidence,
) -> Option<f32> {
let scored: Vec<f32> = outputs
.iter()
.filter_map(|o| o.outcome.as_ref())
.filter_map(|o| outcome_utility(o, evidence))
.collect();
if scored.is_empty() {
return None;
}
match aggregation {
UtilityAggregation::Final => scored.last().copied(),
UtilityAggregation::Consensus => Some(scored.iter().sum::<f32>() / scored.len() as f32),
}
}
pub fn record_run_from_outcomes(
journal: &mut RecordJournal,
embedder: &str,
task_id: impl Into<String>,
query: Vec<f32>,
shape: CoordinationShape,
team_size: usize,
outputs: &[AgentOutput],
aggregation: UtilityAggregation,
evidence: UtilityEvidence,
) -> Result<bool, JournalError> {
let Some(utility) = run_utility(outputs, aggregation, evidence) else {
return Ok(false);
};
record_run(
journal, embedder, task_id, query, shape, team_size, outputs, utility,
)
}
pub fn record_run(
journal: &mut RecordJournal,
embedder: &str,
task_id: impl Into<String>,
query: Vec<f32>,
shape: CoordinationShape,
team_size: usize,
outputs: &[AgentOutput],
utility: f32,
) -> Result<bool, JournalError> {
let Some(record) = execution_record(task_id, query, shape, team_size, outputs, utility)? else {
return Ok(false);
};
journal.append(embedder, &record)?;
Ok(true)
}
pub fn team_homogeneity(agents: &[AgentSpec]) -> Homogeneity {
let Some(first) = agents.first() else {
return Homogeneity::Homogeneous;
};
if agents
.iter()
.skip(1)
.any(|a| a.system_prompt != first.system_prompt)
{
Homogeneity::Heterogeneous
} else {
Homogeneity::Homogeneous
}
}
pub fn scorer_advice(agents: &[AgentSpec]) -> ScorerAdvice {
match team_homogeneity(agents) {
Homogeneity::Homogeneous => ScorerAdvice {
homogeneity: Homogeneity::Homogeneous,
message_passing_is_adjacency_blind: true,
reason: format!(
"all {} agents share one system prompt, so a message-passing scorer over \
profile nodes pools identical features and scores every candidate topology \
the same; rank on the adjacency itself",
agents.len()
),
},
Homogeneity::Heterogeneous => ScorerAdvice {
homogeneity: Homogeneity::Heterogeneous,
message_passing_is_adjacency_blind: false,
reason: "agents carry different system prompts, so profile-node message passing \
can in principle distinguish adjacencies"
.into(),
},
}
}
pub fn select_shape(
selector: &TopologySelector,
query: &[f32],
default_shape: CoordinationShape,
) -> Result<(CoordinationShape, Selection), TopologyError> {
let selection = selector.select(query)?;
let shape = selection.shape.or_else(|| {
let mut named: Vec<_> = selection
.considered
.iter()
.filter(|c| c.shape.is_some())
.collect();
named.sort_by(|a, b| {
b.objective
.total_cmp(&a.objective)
.then(a.code.cmp(&b.code))
});
named.first().and_then(|c| c.shape)
});
Ok((shape.unwrap_or(default_shape), selection))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::TokenAccounting;
use car_topology::{RecordSet, SelectorConfig};
fn output(name: &str, tokens: Option<(u64, u64)>) -> AgentOutput {
AgentOutput {
name: name.into(),
answer: "done".into(),
turns: 1,
tool_calls: 0,
duration_ms: 1.0,
error: None,
outcome: None,
tokens: tokens.map(|(i, o)| TokenAccounting::new(i, o, 0.0)),
tools_used: Vec::new(),
}
}
fn spec(name: &str, prompt: &str) -> AgentSpec {
AgentSpec::new(name, prompt)
}
#[test]
fn tokens_sum_across_outputs() {
let outputs = vec![output("a", Some((100, 20))), output("b", Some((50, 30)))];
assert_eq!(measured_tokens(&outputs), Some(200));
}
#[test]
fn an_unmetered_run_reports_none_not_zero() {
let outputs = vec![output("a", None), output("b", None)];
assert_eq!(measured_tokens(&outputs), None);
assert_eq!(
execution_record(
"t",
vec![0.5],
CoordinationShape::Pipeline,
4,
&outputs,
1.0
)
.unwrap(),
None
);
}
#[test]
fn a_partially_metered_run_still_counts() {
let outputs = vec![output("a", Some((100, 20))), output("b", None)];
assert_eq!(measured_tokens(&outputs), Some(120));
}
#[test]
fn a_solo_run_is_refused_rather_than_recorded_as_a_swarm() {
let outputs = vec![output("a", Some((100, 20)))];
assert!(matches!(
execution_record("t", vec![0.5], CoordinationShape::Solo, 4, &outputs, 1.0),
Err(TopologyError::BadConfig { field: "shape", .. })
));
assert!(
execution_record("t", vec![0.5], CoordinationShape::Swarm, 4, &outputs, 1.0)
.unwrap()
.is_some()
);
}
#[test]
fn a_record_carries_the_shapes_topology_over_the_team_size() {
let outputs = vec![output("a", Some((100, 20)))];
let record = execution_record("t", vec![0.5], CoordinationShape::Debate, 4, &outputs, 1.0)
.unwrap()
.unwrap();
assert_eq!(record.topology, Topology::complete(4).unwrap());
assert_eq!(record.tokens, 120);
assert_eq!(record.task_id, "t");
}
#[test]
fn a_cloned_team_is_homogeneous_whatever_the_names() {
let team = vec![
spec("solver_1", "You solve math problems."),
spec("solver_2", "You solve math problems."),
spec("solver_3", "You solve math problems."),
];
let advice = scorer_advice(&team);
assert_eq!(advice.homogeneity, Homogeneity::Homogeneous);
assert!(advice.message_passing_is_adjacency_blind);
assert!(advice.reason.contains("share one system prompt"));
}
#[test]
fn distinct_roles_are_heterogeneous() {
let team = vec![
spec("researcher", "You gather evidence."),
spec("verifier", "You check the answer."),
];
assert_eq!(team_homogeneity(&team), Homogeneity::Heterogeneous);
assert!(!scorer_advice(&team).message_passing_is_adjacency_blind);
}
#[test]
fn an_empty_team_is_homogeneous() {
assert_eq!(team_homogeneity(&[]), Homogeneity::Homogeneous);
}
fn outcome(status: OutcomeStatus, kinds: &[EvidenceKind]) -> AgentOutcome {
AgentOutcome {
status,
summary: String::new(),
evidence: kinds
.iter()
.map(|&kind| car_ir::Evidence {
kind,
description: String::new(),
data: None,
})
.collect(),
metrics: Default::default(),
timestamp: chrono::Utc::now(),
}
}
fn output_with(
name: &str,
tokens: Option<(u64, u64)>,
status: OutcomeStatus,
kinds: &[EvidenceKind],
) -> AgentOutput {
let mut o = output(name, tokens);
o.outcome = Some(outcome(status, kinds));
o
}
#[test]
fn the_status_to_utility_mapping_is_the_one_the_type_dictates() {
let ev = &[EvidenceKind::ToolResult];
for (status, expected) in [
(OutcomeStatus::Success, Some(1.0)),
(OutcomeStatus::PartialSuccess, Some(0.5)),
(OutcomeStatus::Failure, Some(0.0)),
(OutcomeStatus::GiveUp, Some(0.0)),
(OutcomeStatus::Timeout, Some(0.0)),
] {
assert_eq!(
outcome_utility(&outcome(status, ev), UtilityEvidence::Reported),
expected,
"{status:?}"
);
}
}
#[test]
fn a_neutral_done_carries_no_success_signal_and_is_not_scored() {
assert_eq!(
outcome_utility(
&outcome(OutcomeStatus::Done, &[EvidenceKind::ToolResult]),
UtilityEvidence::Reported
),
None
);
}
#[test]
fn grounded_evidence_rejects_a_success_backed_only_by_self_assessment() {
let self_only = outcome(OutcomeStatus::Success, &[EvidenceKind::SelfAssessment]);
assert_eq!(
outcome_utility(&self_only, UtilityEvidence::Reported),
Some(1.0)
);
assert_eq!(
outcome_utility(&self_only, UtilityEvidence::Grounded),
None,
"unchecked is not failed — it must not be scored 0.0 either"
);
let backed = outcome(
OutcomeStatus::Success,
&[
EvidenceKind::SelfAssessment,
EvidenceKind::ExternalVerification,
],
);
assert_eq!(
outcome_utility(&backed, UtilityEvidence::Grounded),
Some(1.0)
);
}
#[test]
fn an_outcome_with_no_evidence_at_all_is_ungrounded() {
let bare = outcome(OutcomeStatus::Success, &[]);
assert_eq!(outcome_utility(&bare, UtilityEvidence::Reported), Some(1.0));
assert_eq!(outcome_utility(&bare, UtilityEvidence::Grounded), None);
}
#[test]
fn final_aggregation_takes_the_last_scored_stage() {
let ev = &[EvidenceKind::ToolResult];
let outputs = vec![
output_with("a", Some((10, 10)), OutcomeStatus::Failure, ev),
output_with("b", Some((10, 10)), OutcomeStatus::Success, ev),
];
assert_eq!(
run_utility(
&outputs,
UtilityAggregation::Final,
UtilityEvidence::Reported
),
Some(1.0)
);
}
#[test]
fn final_aggregation_skips_trailing_outputs_that_carry_no_signal() {
let ev = &[EvidenceKind::ToolResult];
let outputs = vec![
output_with("a", Some((10, 10)), OutcomeStatus::Success, ev),
output_with("done", Some((10, 10)), OutcomeStatus::Done, ev),
output("aggregate", Some((10, 10))),
];
assert_eq!(
run_utility(
&outputs,
UtilityAggregation::Final,
UtilityEvidence::Reported
),
Some(1.0)
);
}
#[test]
fn consensus_aggregation_grades_independent_answers() {
let ev = &[EvidenceKind::ToolResult];
let outputs = vec![
output_with("a", Some((10, 10)), OutcomeStatus::Success, ev),
output_with("b", Some((10, 10)), OutcomeStatus::Success, ev),
output_with("c", Some((10, 10)), OutcomeStatus::Failure, ev),
output_with("d", Some((10, 10)), OutcomeStatus::Failure, ev),
];
assert_eq!(
run_utility(
&outputs,
UtilityAggregation::Consensus,
UtilityEvidence::Reported
),
Some(0.5)
);
}
#[test]
fn a_run_with_no_structured_outcomes_yields_no_utility() {
let outputs = vec![output("a", Some((10, 10))), output("b", Some((10, 10)))];
for aggregation in [UtilityAggregation::Final, UtilityAggregation::Consensus] {
assert_eq!(
run_utility(&outputs, aggregation, UtilityEvidence::Reported),
None
);
}
}
#[test]
fn the_derived_path_refuses_to_fabricate_either_measured_number() {
let dir = tempfile::tempdir().unwrap();
let path = car_topology::journal_path(dir.path());
let mut journal = RecordJournal::open(&path).unwrap();
let ev = &[EvidenceKind::ToolResult];
let write = |journal: &mut RecordJournal, task: &str, outputs: &[AgentOutput]| {
record_run_from_outcomes(
journal,
"mini-lm",
task,
vec![0.5, 0.5],
CoordinationShape::Debate,
4,
outputs,
UtilityAggregation::Final,
UtilityEvidence::Grounded,
)
.unwrap()
};
assert!(write(
&mut journal,
"ok",
&[output_with(
"a",
Some((400, 100)),
OutcomeStatus::Success,
ev
)]
));
assert!(!write(
&mut journal,
"unmetered",
&[output_with("a", None, OutcomeStatus::Success, ev)]
));
assert!(!write(
&mut journal,
"ungrounded",
&[output_with(
"a",
Some((400, 100)),
OutcomeStatus::Success,
&[EvidenceKind::SelfAssessment]
)]
));
assert!(!write(
&mut journal,
"neutral",
&[output_with("a", Some((400, 100)), OutcomeStatus::Done, ev)]
));
drop(journal);
let entries = RecordJournal::load(&path).unwrap();
assert_eq!(entries.len(), 1, "only the fully measured run is recorded");
assert_eq!(entries[0].record.task_id, "ok");
assert_eq!(entries[0].record.utility, 1.0);
}
#[test]
fn record_run_writes_a_metered_run_and_skips_an_unmetered_one() {
let dir = tempfile::tempdir().unwrap();
let path = car_topology::journal_path(dir.path());
let mut journal = RecordJournal::open(&path).unwrap();
assert!(record_run(
&mut journal,
"mini-lm",
"t1",
vec![0.5, 0.5],
CoordinationShape::Debate,
4,
&[output("a", Some((400, 100)))],
1.0,
)
.unwrap());
assert!(!record_run(
&mut journal,
"mini-lm",
"t2",
vec![0.5, 0.5],
CoordinationShape::Pipeline,
4,
&[output("a", None)],
1.0,
)
.unwrap());
drop(journal);
let entries = RecordJournal::load(&path).unwrap();
assert_eq!(entries.len(), 1, "only the metered run is recorded");
assert_eq!(entries[0].record.tokens, 500);
assert_eq!(entries[0].embedder, "mini-lm");
}
#[test]
fn recorded_runs_fit_a_selector_that_returns_an_executable_shape() {
let team_size = 4;
let mut records = Vec::new();
for i in 0..6 {
let drift = i as f32 * 0.01;
for (family, query, cheap, dear) in [
(
"math",
vec![1.0, 0.0, drift],
CoordinationShape::Debate,
CoordinationShape::Pipeline,
),
(
"code",
vec![0.0, 1.0, drift],
CoordinationShape::Pipeline,
CoordinationShape::Debate,
),
] {
let task = format!("{family}{i}");
records.push(
execution_record(
&task,
query.clone(),
cheap,
team_size,
&[output("a", Some((400, 200)))],
1.0,
)
.unwrap()
.unwrap(),
);
records.push(
execution_record(
&task,
query,
dear,
team_size,
&[output("a", Some((1600, 800)))],
1.0,
)
.unwrap()
.unwrap(),
);
}
}
let selector = TopologySelector::fit(
&RecordSet::new(records).unwrap(),
&SelectorConfig::default(),
)
.unwrap();
let (math, _) =
select_shape(&selector, &[1.0, 0.0, 0.0], CoordinationShape::Swarm).unwrap();
let (code, _) =
select_shape(&selector, &[0.0, 1.0, 0.0], CoordinationShape::Swarm).unwrap();
assert_eq!(math, CoordinationShape::Debate);
assert_eq!(code, CoordinationShape::Pipeline);
}
}