//! Topology search / quality-diversity (P5). //! //! Uses the comparison machinery as a fitness function: generate a candidate //! topology for each (kind × team-size) cell, run the task, score it, and keep //! a MAP-Elites-style **archive** of the best elite per cell plus a quality/cost //! Pareto front. This *illuminates* the search space (which structures work for //! a task) rather than returning a single winner — the bridge toward //! Autonomous Organizational Evolution on a safe substrate. //! //! v1 enumerates the grid deterministically; mutation/selection across //! generations slot in by repeatedly evaluating and keeping per-cell bests. use std::cmp::Ordering; use serde::Serialize; use cm_topology::{build, TopologyKind}; use crate::{execute, OrchestratorError, Scorer, TurnExecutor}; /// The best result found for one (kind × size) cell. #[derive(Debug, Clone, Serialize)] pub struct EliteCell { /// Topology kind for this cell. pub kind: TopologyKind, /// Team size (node count) for this cell. pub size: usize, /// Quality in `[0,1]`. pub quality: f64, /// Tokens spent (cost proxy). pub tokens: u64, /// Turns run. pub turns: u32, /// The cell's final output. pub final_output: String, /// Whether this cell is on the quality/cost Pareto front. pub on_pareto: bool, } /// The result of a topology search. #[derive(Debug, Clone, Serialize)] pub struct Evolution { /// The task searched against. pub task: String, /// One elite per evaluated (kind × size) cell. pub archive: Vec, /// Index of the highest-quality elite, if any. pub best: Option, /// Number of candidate topologies evaluated. pub evaluated: u32, } /// Search the (kinds × sizes) grid for the best topology for `task`. Sizes /// larger than `roles.len()` (or zero) are skipped; `roles[..size]` staffs each /// candidate. pub async fn evolve( roles: &[&str], task: &str, executor: &E, scorer: &S, kinds: &[TopologyKind], sizes: &[usize], ) -> Result { let mut archive: Vec = Vec::new(); let mut evaluated = 0u32; for &kind in kinds { for &size in sizes { if size == 0 || size > roles.len() { continue; } let graph = build(kind, &roles[..size]) .map_err(|e| OrchestratorError::Malformed(e.to_string()))?; let rec = execute(&graph, task, executor).await?; let quality = scorer.score(task, &rec).await.clamp(0.0, 1.0); evaluated += 1; archive.push(EliteCell { kind, size, quality, tokens: rec.totals.tokens, turns: rec.totals.turns, final_output: rec.final_output, on_pareto: false, }); } } // Pareto: maximize quality, minimize tokens. for i in 0..archive.len() { let dominated = archive.iter().enumerate().any(|(j, o)| { j != i && o.quality >= archive[i].quality && o.tokens <= archive[i].tokens && (o.quality > archive[i].quality || o.tokens < archive[i].tokens) }); archive[i].on_pareto = !dominated; } let best = archive .iter() .enumerate() .max_by(|a, b| { a.1.quality .partial_cmp(&b.1.quality) .unwrap_or(Ordering::Equal) }) .map(|(i, _)| i); Ok(Evolution { task: task.to_string(), archive, best, evaluated, }) } /// Convenience: search every kind at the full team size. pub async fn evolve_all( roles: &[&str], task: &str, executor: &E, scorer: &S, ) -> Result { evolve( roles, task, executor, scorer, &TopologyKind::ALL, &[roles.len()], ) .await } #[cfg(test)] mod tests { use super::*; use crate::{RunRecord, TurnOutcome, TurnRequest}; struct Echo; impl TurnExecutor for Echo { async fn run_turn(&self, req: TurnRequest) -> Result { Ok(TurnOutcome { output: format!("{}<{}>", req.role, req.context.join("|")), tokens: 10, gated: vec![], }) } } struct LengthScorer; impl Scorer for LengthScorer { async fn score(&self, _t: &str, r: &RunRecord) -> f64 { (r.final_output.len() as f64 / 200.0).min(1.0) } } #[tokio::test] async fn searches_kinds_and_marks_pareto() { let roles = ["coordinator", "a", "b"]; let ev = evolve( &roles, "task", &Echo, &LengthScorer, &[ TopologyKind::Hierarchical, TopologyKind::Pipeline, TopologyKind::Swarm, ], &[3], ) .await .unwrap(); assert_eq!(ev.archive.len(), 3); assert_eq!(ev.evaluated, 3); assert!(ev.best.is_some()); assert!(ev.archive.iter().any(|c| c.on_pareto)); } #[tokio::test] async fn grid_spans_sizes_and_skips_oversized() { let roles = ["a", "b", "c"]; let ev = evolve( &roles, "t", &Echo, &LengthScorer, &[TopologyKind::Pipeline], &[2, 3, 9], ) .await .unwrap(); // sizes 2 and 3 evaluated; 9 skipped (> roles.len()). assert_eq!(ev.evaluated, 2); assert_eq!(ev.archive.len(), 2); } #[tokio::test] async fn evolve_all_covers_every_kind() { let roles = ["a", "b", "c"]; let ev = evolve_all(&roles, "t", &Echo, &LengthScorer).await.unwrap(); assert_eq!(ev.evaluated as usize, TopologyKind::ALL.len()); } }