car-multi 0.54.0

Multi-agent coordination patterns for Common Agent Runtime
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
//! Turning coordination runs into topology-selection records.
//!
//! [`car_topology`] fits a selector on `(topology, query, utility, tokens)`
//! tuples. `car-multi` is where those tuples come from: every pattern already
//! returns `AgentOutput`s carrying [`TokenAccounting`](crate::types::TokenAccounting),
//! so the measured cost of a coordination run is already in hand — it just was
//! not being kept.
//!
//! This module is the bridge in both directions:
//!
//! - [`execution_record`] folds a finished run into one
//!   [`car_topology::ExecutionRecord`].
//! - [`team_homogeneity`] answers, for a real `Vec<AgentSpec>`, the question
//!   the paper shows decides whether a profile-node message-passing scorer can
//!   rank anything at all.
//!
//! Nothing here calls a model. Collecting records costs LLM calls because
//! *running the coordination* does; the bookkeeping is free.

use car_topology::{
    CoordinationShape, ExecutionRecord, Homogeneity, JournalError, RecordJournal, ScorerAdvice,
    Selection, Topology, TopologyError, TopologySelector,
};

use car_ir::{AgentOutcome, EvidenceKind, OutcomeStatus};

use crate::types::{AgentOutput, AgentSpec};

/// Total measured tokens across a run's outputs, or `None` when no output
/// reported any accounting.
///
/// `None` rather than `0` on purpose. [`TokenAccounting`](crate::types::TokenAccounting)
/// is opt-in — a runner that does not populate it is not reporting a zero-cost
/// run, it is reporting nothing — and a record claiming zero tokens would drag
/// the proxy's cost head toward a topology that merely went unmetered.
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)
}

/// Fold a finished coordination run into one execution record.
///
/// `team_size` is the number of agents the *topology* is over, which is not
/// always `outputs.len()`: a swarm's optional synthesizer and a supervisor's
/// reviewer rounds add outputs without adding nodes. Pass the length of the
/// `AgentSpec` list the pattern was built from.
///
/// `utility` is the caller's — CAR does not know whether a coordination run
/// answered the question. Use 1.0/0.0 for a pass/fail check, or a graded score
/// where one exists.
///
/// Returns `None` when the run reported no token accounting, so an unmetered
/// run is skipped rather than logged as free.
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> {
    // `Solo` is refused rather than recorded. It folds to the empty adjacency,
    // which is byte-identical to `Swarm`, so a one-agent run logged with
    // `team_size = 4` becomes a *Swarm* record carrying one agent's token cost.
    // Nothing downstream can undo that: `shape_of` cannot recover `Solo` from
    // the matrix, so `select_shape` hands the caller `Swarm`, who then runs
    // four agents against a price learned from one. The paper's model has no
    // room for it either — a solo run is a different `N`, not a different
    // adjacency over the same `N`.
    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,
    )))
}

/// The same fold for a run whose topology is not one of the named shapes —
/// a topology a selector chose, for instance.
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,
    ))
}

/// How much of a success claim has to be demonstrated before it counts.
///
/// CAR already draws this line elsewhere — the flagship assistant cross-checks
/// a final summary's operational claims against same-run tool receipts rather
/// than believing the prose. The same reasoning applies here, and harder: a
/// codebook fitted on runs that merely *said* they succeeded is a codebook of
/// whatever the model is most confident about.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UtilityEvidence {
    /// Take [`OutcomeStatus`] at face value, whatever backs it.
    Reported,
    /// Require at least one evidence item that is not the agent's own
    /// assessment — a tool result, a state change, an external verification, or
    /// a product evaluator. A `Success` backed only by
    /// [`EvidenceKind::SelfAssessment`] yields `None`, not `0.0`: "nobody
    /// checked" is not "it failed", and recording it either way is a fabricated
    /// training label.
    Grounded,
}

/// How to reduce a multi-agent run's outcomes to one utility.
///
/// There is no universal rule, so this is the caller's to state rather than
/// something to infer from the outputs.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UtilityAggregation {
    /// The last output carrying an outcome is the run's answer.
    ///
    /// Right for the shapes where one agent's output *is* the result —
    /// [`CoordinationShape::Pipeline`] (the last stage is what the caller sees)
    /// and [`CoordinationShape::Supervisor`] (the reviewer has the last word).
    Final,
    /// The fraction of outcome-carrying outputs that succeeded.
    ///
    /// Right for the shapes where agents answer independently and the answer is
    /// a vote or a pick — [`CoordinationShape::Swarm`] and
    /// [`CoordinationShape::Debate`]. Yields the graded utility in `[0, 1]` that
    /// [`ExecutionRecord`] already supports, rather than forcing a binary.
    Consensus,
}

/// Map one agent outcome to a utility in `[0, 1]`, or `None` when it carries no
/// success signal.
///
/// The mapping is dictated by the type rather than chosen:
///
/// | [`OutcomeStatus`] | utility |
/// |---|---|
/// | `Success` | `1.0` |
/// | `PartialSuccess` | `0.5` |
/// | `Failure`, `GiveUp`, `Timeout` | `0.0` |
/// | `Done` | `None` |
///
/// `Done` is `None` because `car-ir` documents it as *"neutral — may or may not
/// have succeeded"*. Scoring a deliberately neutral status as either outcome
/// would invent the one number this crate exists to measure honestly.
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,
    }
}

/// Derive a run's utility from its agents' structured outcomes.
///
/// Returns `None` when no output carries a usable outcome — a runner that
/// reports nothing structured is not reporting success, and the caller should
/// skip the record rather than label it. That is the same discipline
/// [`measured_tokens`] applies to cost, for the same reason: the paper's whole
/// argument is that these two numbers must be *measured*, and a default is not
/// a measurement.
///
/// **This is only as good as the runner.** `AgentOutput::outcome` is populated
/// by the caller's [`AgentRunner`](crate::AgentRunner), not by this crate: every
/// aggregate output the coordination patterns build themselves (a swarm's
/// synthesizer, a map-reduce reducer, a vote's tally) sets `outcome: None` by
/// construction, because the runtime does not own the model and cannot classify
/// what it did not run. A team whose runner never fills the field will record
/// nothing through this path, which is the intended failure — fail closed, not
/// fabricate.
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),
    }
}

/// Record a finished coordination run, deriving BOTH measured numbers from the
/// run itself.
///
/// The ergonomic path deliberately cannot fabricate either one: it writes
/// nothing and returns `false` when the runner metered no tokens *or* when no
/// agent reported a usable outcome. Defaulting utility to 1.0 — the obvious
/// shortcut — would make the codebook the set of everything ever run, which is
/// exactly the short list the paper says does not exist.
///
/// Use [`record_run`] instead when the caller has a better utility signal than
/// the agents' self-reported outcomes: a `car-verify` goal verdict, a product
/// evaluator, or a graded score.
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,
    )
}

/// Record a finished coordination run to a [`RecordJournal`].
///
/// The one-call form of [`execution_record`] + [`RecordJournal::append`], which
/// is what a daemon call site wants: run the coordination, decide the utility,
/// hand the run here.
///
/// `embedder` names the encoder that produced `query`. It is not optional
/// because the journal's whole guard is that a record can never be folded into
/// a set from a different embedding space — see [`car_topology::journal`].
///
/// Returns `false` when the run reported no token accounting and so was not
/// recorded. That is the honest outcome, not a failure: an unmetered run has no
/// measured cost, and this crate's whole premise is scoring on measured cost.
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)
}

/// Whether a team's agents are distinguishable, judged on the specs themselves.
///
/// The paper measures homogeneity over profile *embeddings*; for CAR the
/// question is answerable without an encoder, because a team is usually built
/// by cloning one `AgentSpec` N times. Two agents count as distinguishable when
/// their system prompts differ — the prompt is the profile.
///
/// Names are deliberately ignored: `solver_1` … `solver_4` around one identical
/// prompt is the homogeneous case, and letting the name split them would give
/// exactly the false negative this check exists to prevent.
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
    }
}

/// What a profile-node message-passing scorer could do on this team, phrased
/// for a log line next to a selection.
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(),
        },
    }
}

/// Select a coordination shape for a query, falling back to `default_shape`
/// when the selected topology matches no named pattern.
///
/// A learned codebook can hold topologies with no `car-multi` pattern behind
/// them — that is a real outcome of fitting on records, not a bug — and this
/// function is for the caller who can only execute the named patterns. Callers
/// that can execute an arbitrary adjacency should use
/// [`TopologySelector::select`] directly and read [`Selection::topology`].
pub fn select_shape(
    selector: &TopologySelector,
    query: &[f32],
    default_shape: CoordinationShape,
) -> Result<(CoordinationShape, Selection), TopologyError> {
    let selection = selector.select(query)?;
    // Prefer the winner's own shape; otherwise the best-scoring candidate that
    // has one; otherwise the caller's default.
    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() {
        // Solo and Swarm share the empty adjacency, so recording a 1-agent run
        // at team_size 4 would mint a Swarm record priced for one agent — and
        // `shape_of` could never recover the Solo, so a later `select_shape`
        // would hand back Swarm and run four.
        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", .. })
        ));
        // Swarm itself still records fine.
        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() {
        // car-ir documents Done as "neutral -- may or may not have succeeded".
        // Scoring it either way would invent the number this crate measures.
        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),
            // A neutral Done and an outcome-less aggregate must not displace it.
            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()
        };

        // Metered and grounded -> recorded.
        assert!(write(
            &mut journal,
            "ok",
            &[output_with(
                "a",
                Some((400, 100)),
                OutcomeStatus::Success,
                ev
            )]
        ));
        // Grounded but unmetered -> no measured cost, so no record.
        assert!(!write(
            &mut journal,
            "unmetered",
            &[output_with("a", None, OutcomeStatus::Success, ev)]
        ));
        // Metered but only self-assessed -> no measured utility, so no record.
        assert!(!write(
            &mut journal,
            "ungrounded",
            &[output_with(
                "a",
                Some((400, 100)),
                OutcomeStatus::Success,
                &[EvidenceKind::SelfAssessment]
            )]
        ));
        // Metered but neutral -> likewise.
        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");
    }

    /// End to end through the public surface: record a batch of runs, fit a
    /// selector on them, and get a shape back that a `car-multi` caller can
    /// execute.
    #[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;
            // Debate is cheap on "math"-flavoured queries, dear on "code" ones.
            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);
    }
}