Files
clawmates/crates/cm-orchestrator/src/evolve.rs
T
Omar SobhandClaude Opus 4.8 3eca4ed70c Recursive deploy ladder: Company + Org tiers, mesh mark, two-tier rail
Completes the scale ladder (single → team → company → org). Every tier is a
topology whose nodes are the tier below; running a parent recursively runs each
child's sub-topology down to the leaf claws.

Backend:
- migration 0011: companies/company_teams, orgs/org_companies, topology_runs.tier
- cm-db repos for companies + orgs (mirror teams)
- TurnRequest.attrs (forwarded from node.attrs) for child-id binding
- SubTopologyExecutor (recursive_exec.rs): a parent "turn" runs the child's
  sub-topology; durability via parent updated_at keepalive + cancel propagation
  + depth cap; boxed future breaks the org→company recursion
- topology_worker selects executor by job.tier
- routes: /api/companies, /api/orgs (create/list/get/run) + unified
  /api/structure/{level}/{id} for the zoom canvas

Frontend:
- MeshMark: node-mesh brand glyph (replaces the claw PNG), tier variants
- TopologyGraphView: optional onNodeClick/nodeMeta + dark-token theming
- StructureCanvas + Breadcrumb: one recursive zoom view for every tier
  (drill down on node click, breadcrumb up); TeamRunPanel extracted + shared
- two-tier Discord-style rail: StructureRail (mesh mark + org/company/team
  glyphs + tools popover + deploy + user) | RosterColumn (selected group's
  children, or your claws); SecondaryNav for cross-cutting tools
- ComposeWizard (company/org) wired into DeployWizard; /companies + /orgs pages

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-18 14:25:06 -07:00

207 lines
5.9 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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<EliteCell>,
/// Index of the highest-quality elite, if any.
pub best: Option<usize>,
/// 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<E: TurnExecutor, S: Scorer>(
roles: &[&str],
task: &str,
executor: &E,
scorer: &S,
kinds: &[TopologyKind],
sizes: &[usize],
) -> Result<Evolution, OrchestratorError> {
let mut archive: Vec<EliteCell> = 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<E: TurnExecutor, S: Scorer>(
roles: &[&str],
task: &str,
executor: &E,
scorer: &S,
) -> Result<Evolution, OrchestratorError> {
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<TurnOutcome, OrchestratorError> {
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());
}
}