Files
clawmates/crates/cm-orchestrator/src/plan.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

254 lines
7.2 KiB
Rust

//! Pure, synchronous planners: turn a [`TopologyGraph`] into an ordered list
//! of steps. Separated from execution so the control flow is unit-testable
//! without any async/LLM machinery.
use std::collections::{HashMap, HashSet, VecDeque};
use cm_topology::TopologyGraph;
use crate::{OrchestratorError, StepPhase};
/// One planned step: which node acts, in what phase, and which prior step
/// outputs (plus optionally the top-level task) form its context.
pub(crate) struct PlanStep {
pub node_idx: usize,
pub phase: StepPhase,
pub use_task: bool,
pub ctx_from: Vec<usize>,
}
fn index_map(g: &TopologyGraph) -> HashMap<&str, usize> {
g.nodes
.iter()
.enumerate()
.map(|(i, n)| (n.id.as_str(), i))
.collect()
}
fn in_degrees(g: &TopologyGraph, idx: &HashMap<&str, usize>) -> Vec<usize> {
let mut indeg = vec![0usize; g.nodes.len()];
let mut seen = HashSet::new();
for e in &g.edges {
if let (Some(&a), Some(&b)) = (idx.get(e.from.as_str()), idx.get(e.to.as_str())) {
if a != b && seen.insert((a, b)) {
indeg[b] += 1;
}
}
}
indeg
}
/// Hierarchical: root plans (top-down), direct children work, root synthesizes
/// (bottom-up). Deeper trees are flattened to one level for v1.
pub(crate) fn hierarchical(g: &TopologyGraph) -> Result<Vec<PlanStep>, OrchestratorError> {
let idx = index_map(g);
let n = g.nodes.len();
let indeg = in_degrees(g, &idx);
let root = (0..n).find(|&i| indeg[i] == 0).unwrap_or(0);
let mut children = Vec::new();
let mut seen = HashSet::new();
for e in &g.edges {
if e.from == g.nodes[root].id {
if let Some(&c) = idx.get(e.to.as_str()) {
if c != root && seen.insert(c) {
children.push(c);
}
}
}
}
let mut steps = Vec::new();
if children.is_empty() {
steps.push(PlanStep {
node_idx: root,
phase: StepPhase::Work,
use_task: true,
ctx_from: vec![],
});
return Ok(steps);
}
steps.push(PlanStep {
node_idx: root,
phase: StepPhase::Plan,
use_task: true,
ctx_from: vec![],
});
let mut child_steps = Vec::new();
for c in children {
steps.push(PlanStep {
node_idx: c,
phase: StepPhase::Work,
use_task: true,
ctx_from: vec![0],
});
child_steps.push(steps.len() - 1);
}
steps.push(PlanStep {
node_idx: root,
phase: StepPhase::Synth,
use_task: false,
ctx_from: child_steps,
});
Ok(steps)
}
/// Pipeline: topological order; each stage takes the previous stage's output.
pub(crate) fn pipeline(g: &TopologyGraph) -> Result<Vec<PlanStep>, OrchestratorError> {
let idx = index_map(g);
let n = g.nodes.len();
let mut succ = vec![Vec::new(); n];
let mut indeg = vec![0usize; n];
let mut seen = HashSet::new();
for e in &g.edges {
if let (Some(&a), Some(&b)) = (idx.get(e.from.as_str()), idx.get(e.to.as_str())) {
if a != b && seen.insert((a, b)) {
succ[a].push(b);
indeg[b] += 1;
}
}
}
let mut queue: VecDeque<usize> = (0..n).filter(|&i| indeg[i] == 0).collect();
let mut order = Vec::new();
while let Some(u) = queue.pop_front() {
order.push(u);
for &v in &succ[u] {
indeg[v] -= 1;
if indeg[v] == 0 {
queue.push_back(v);
}
}
}
// Any nodes left in a cycle: append in node order so we still run them.
for i in 0..n {
if !order.contains(&i) {
order.push(i);
}
}
let steps = order
.iter()
.enumerate()
.map(|(i, &node)| PlanStep {
node_idx: node,
phase: StepPhase::Work,
use_task: i == 0,
ctx_from: if i == 0 { vec![] } else { vec![i - 1] },
})
.collect();
Ok(steps)
}
/// Swarm: every node attempts the task independently, then an aggregator
/// (a "coordinator" role, else a source node) synthesizes all outputs.
pub(crate) fn swarm(g: &TopologyGraph) -> Result<Vec<PlanStep>, OrchestratorError> {
let n = g.nodes.len();
if n == 0 {
return Err(OrchestratorError::Malformed("empty topology".into()));
}
let idx = index_map(g);
let indeg = in_degrees(g, &idx);
let mut steps: Vec<PlanStep> = (0..n)
.map(|i| PlanStep {
node_idx: i,
phase: StepPhase::Work,
use_task: true,
ctx_from: vec![],
})
.collect();
let aggregator = (0..n)
.find(|&i| g.nodes[i].role.to_lowercase().contains("coordinator"))
.or_else(|| (0..n).find(|&i| indeg[i] == 0))
.unwrap_or(0);
steps.push(PlanStep {
node_idx: aggregator,
phase: StepPhase::Aggregate,
use_task: false,
ctx_from: (0..n).collect(),
});
Ok(steps)
}
/// Mesh / blackboard: two rounds of peer exchange (round 2 sees every round-1
/// output), then the first peer aggregates — models iterative convergence.
pub(crate) fn mesh(g: &TopologyGraph) -> Result<Vec<PlanStep>, OrchestratorError> {
let n = g.nodes.len();
if n == 0 {
return Err(OrchestratorError::Malformed("empty topology".into()));
}
let mut steps = Vec::new();
for i in 0..n {
steps.push(PlanStep {
node_idx: i,
phase: StepPhase::Work,
use_task: true,
ctx_from: vec![],
});
}
let round1: Vec<usize> = (0..n).collect();
for i in 0..n {
steps.push(PlanStep {
node_idx: i,
phase: StepPhase::Work,
use_task: false,
ctx_from: round1.clone(),
});
}
let round2: Vec<usize> = (n..2 * n).collect();
steps.push(PlanStep {
node_idx: 0,
phase: StepPhase::Aggregate,
use_task: false,
ctx_from: round2,
});
Ok(steps)
}
/// Debate: proposer drafts, critic critiques, proposer revises, judge decides.
pub(crate) fn debate(g: &TopologyGraph) -> Result<Vec<PlanStep>, OrchestratorError> {
let n = g.nodes.len();
if n == 0 {
return Err(OrchestratorError::Malformed("empty topology".into()));
}
if n == 1 {
return Ok(vec![PlanStep {
node_idx: 0,
phase: StepPhase::Work,
use_task: true,
ctx_from: vec![],
}]);
}
let proposer = 0;
let critic = 1;
let judge = if n >= 3 { 2 } else { 0 };
Ok(vec![
PlanStep {
node_idx: proposer,
phase: StepPhase::Work,
use_task: true,
ctx_from: vec![],
},
PlanStep {
node_idx: critic,
phase: StepPhase::Work,
use_task: true,
ctx_from: vec![0],
},
PlanStep {
node_idx: proposer,
phase: StepPhase::Synth,
use_task: false,
ctx_from: vec![0, 1],
},
PlanStep {
node_idx: judge,
phase: StepPhase::Aggregate,
use_task: false,
ctx_from: vec![2, 1],
},
])
}