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

119 lines
3.8 KiB
Rust

//! Workflow of topologies: run several topologies in sequence, threading each
//! stage's final output into the next stage's task.
//!
//! This is the "take my project through a workflow of topologies" capability —
//! e.g. brainstorm in a `swarm`, execute in a `hierarchy`, review via `debate`.
//! Each stage is itself a full safe topology run, so §15 still holds at every
//! step.
use serde::Serialize;
use cm_topology::TopologyGraph;
use crate::{execute, OrchestratorError, RunMetrics, RunRecord, TurnExecutor};
/// The record of a multi-stage workflow run.
#[derive(Debug, Clone, Serialize)]
pub struct WorkflowRecord {
/// The original task.
pub task: String,
/// One full topology run per stage, in order.
pub stages: Vec<RunRecord>,
/// The last stage's final output.
pub final_output: String,
/// Totals across all stages.
pub totals: RunMetrics,
}
/// Run `stages` in order; each stage after the first receives the previous
/// stage's final output as additional task context.
pub async fn run_workflow<E: TurnExecutor>(
stages: &[TopologyGraph],
task: &str,
executor: &E,
) -> Result<WorkflowRecord, OrchestratorError> {
let mut records: Vec<RunRecord> = Vec::with_capacity(stages.len());
let mut totals = RunMetrics::default();
let mut prior: Option<String> = None;
for stage in stages {
let stage_task = match &prior {
None => task.to_string(),
Some(p) => format!("{task}\n\nBuilding on the previous stage's result:\n{p}"),
};
let rec = execute(stage, &stage_task, executor).await?;
totals.turns += rec.totals.turns;
totals.tokens += rec.totals.tokens;
totals.gated_actions += rec.totals.gated_actions;
totals.approvals_granted += rec.totals.approvals_granted;
totals.approvals_blocked += rec.totals.approvals_blocked;
prior = Some(rec.final_output.clone());
records.push(rec);
}
Ok(WorkflowRecord {
task: task.to_string(),
final_output: prior.unwrap_or_default(),
stages: records,
totals,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{TurnOutcome, TurnRequest};
use cm_topology::{Node, TopologyKind};
struct Echo;
impl TurnExecutor for Echo {
async fn run_turn(&self, req: TurnRequest) -> Result<TurnOutcome, OrchestratorError> {
Ok(TurnOutcome {
output: format!("{}<{}>", req.node_id, req.context.join("|")),
tokens: 5,
gated: vec![],
})
}
}
fn stage(kind: TopologyKind, ids: &[&str]) -> TopologyGraph {
TopologyGraph::new(
kind,
ids.iter().map(|i| Node::new(*i, "worker")).collect(),
vec![],
)
.unwrap()
}
#[tokio::test]
async fn workflow_threads_stages_and_aggregates_totals() {
let stages = vec![
stage(TopologyKind::Swarm, &["s1", "s2"]),
stage(TopologyKind::Pipeline, &["p1", "p2"]),
];
let rec = run_workflow(&stages, "build it", &Echo).await.unwrap();
assert_eq!(rec.stages.len(), 2);
// Stage 2's output reflects stage 1's final output (threaded forward).
let s1_final = &rec.stages[0].final_output;
assert!(
rec.final_output
.contains(&s1_final[..s1_final.len().min(4)]),
"stage 2 should build on stage 1"
);
// Totals are the sum of both stages.
let summed: u32 = rec.stages.iter().map(|s| s.totals.turns).sum();
assert_eq!(rec.totals.turns, summed);
assert!(rec.totals.tokens > 0);
}
#[tokio::test]
async fn empty_workflow_is_empty() {
let rec = run_workflow(&[], "x", &Echo).await.unwrap();
assert!(rec.stages.is_empty());
assert!(rec.final_output.is_empty());
}
}