TopologyKind describes twelve distinct intents, but the orchestrator implements five planners and mapped the kinds onto them inside plan_steps. So Market never auctions, StarMoe never routes to experts, Ring never cycles and Holacratic never self-organizes -- each silently runs as whichever pattern it collapses to, while kind::description() and the UI catalog kept promising the distinct behaviour. Rather than delete variants that appear in persisted rows, the collapse is now named: ExecutionPattern + TopologyKind::execution_pattern() in cm-topology, with plan_steps dispatching on the pattern instead of re-listing the mapping. One source of truth, and the two cannot drift. GET /api/topologies now reports `executes_as` and `distinct_at_execution` so a UI can stop offering aliases as if they behaved differently. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
181 lines
6.8 KiB
Rust
181 lines
6.8 KiB
Rust
//! The curated taxonomy of execution-meaningful topologies (v1).
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// An organizational topology pattern. The v1 set covers shapes that differ
|
|
/// *in how a task executes*; governance/novel forms are deferred.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum TopologyKind {
|
|
/// Tree: an orchestrator delegates down; results bubble up.
|
|
Hierarchical,
|
|
/// Autonomous peers with minimal coordination.
|
|
Flat,
|
|
/// Linear stages: output of stage n feeds stage n+1.
|
|
Pipeline,
|
|
/// Parallel attempts with consensus/aggregation.
|
|
Swarm,
|
|
/// Dense peer-to-peer exchange until convergence.
|
|
Mesh,
|
|
/// A central hub routes to spokes and aggregates.
|
|
HubSpoke,
|
|
/// A cycle: sequential round-trips, refining each lap.
|
|
Ring,
|
|
/// Router + experts (mixture-of-experts).
|
|
StarMoe,
|
|
/// Tasks auctioned to agents by fit/cost.
|
|
Market,
|
|
/// Agents read/write a shared workspace.
|
|
Blackboard,
|
|
/// Adversarial proposer vs critic rounds, then a judge.
|
|
Debate,
|
|
/// Self-organizing circles assign within roles.
|
|
Holacratic,
|
|
}
|
|
|
|
/// How a topology kind actually executes.
|
|
///
|
|
/// The twelve kinds above describe twelve distinct *intents*, but the
|
|
/// orchestrator implements five execution patterns and maps the kinds onto
|
|
/// them. So `Market` never auctions, `StarMoe` never routes to experts, `Ring`
|
|
/// never cycles and `Holacratic` never self-organizes — each runs as whichever
|
|
/// pattern it collapses to. Naming that here keeps the gap honest, lets the
|
|
/// catalog API report it, and makes the collapse a single source of truth that
|
|
/// `cm-orchestrator::plan_steps` matches on rather than duplicating.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum ExecutionPattern {
|
|
/// Coordinator plans, members work, coordinator aggregates.
|
|
Hierarchical,
|
|
/// Each node in sequence, output feeding the next.
|
|
Pipeline,
|
|
/// All nodes in parallel, then one aggregates.
|
|
Swarm,
|
|
/// Two exchange rounds, then node 0 aggregates.
|
|
Mesh,
|
|
/// Proposer, critic, judge.
|
|
Debate,
|
|
}
|
|
|
|
impl ExecutionPattern {
|
|
pub fn as_str(&self) -> &'static str {
|
|
match self {
|
|
ExecutionPattern::Hierarchical => "hierarchical",
|
|
ExecutionPattern::Pipeline => "pipeline",
|
|
ExecutionPattern::Swarm => "swarm",
|
|
ExecutionPattern::Mesh => "mesh",
|
|
ExecutionPattern::Debate => "debate",
|
|
}
|
|
}
|
|
}
|
|
|
|
impl TopologyKind {
|
|
/// The execution pattern this kind actually runs as.
|
|
pub fn execution_pattern(&self) -> ExecutionPattern {
|
|
match self {
|
|
TopologyKind::Hierarchical
|
|
| TopologyKind::HubSpoke
|
|
| TopologyKind::StarMoe
|
|
| TopologyKind::Market => ExecutionPattern::Hierarchical,
|
|
TopologyKind::Pipeline | TopologyKind::Ring => ExecutionPattern::Pipeline,
|
|
TopologyKind::Swarm | TopologyKind::Flat | TopologyKind::Holacratic => {
|
|
ExecutionPattern::Swarm
|
|
}
|
|
TopologyKind::Mesh | TopologyKind::Blackboard => ExecutionPattern::Mesh,
|
|
TopologyKind::Debate => ExecutionPattern::Debate,
|
|
}
|
|
}
|
|
|
|
/// Whether this kind's own semantics are realized at execution, or whether
|
|
/// it is an alias for another kind's pattern. `false` means the label is
|
|
/// currently aspirational — useful for a UI that shouldn't promise
|
|
/// behaviour the engine doesn't implement.
|
|
pub fn is_distinct_at_execution(&self) -> bool {
|
|
matches!(
|
|
self,
|
|
TopologyKind::Hierarchical
|
|
| TopologyKind::Pipeline
|
|
| TopologyKind::Swarm
|
|
| TopologyKind::Mesh
|
|
| TopologyKind::Debate
|
|
)
|
|
}
|
|
|
|
/// Every supported kind, for iteration in tests/UIs/benchmarks.
|
|
pub const ALL: [TopologyKind; 12] = [
|
|
TopologyKind::Hierarchical,
|
|
TopologyKind::Flat,
|
|
TopologyKind::Pipeline,
|
|
TopologyKind::Swarm,
|
|
TopologyKind::Mesh,
|
|
TopologyKind::HubSpoke,
|
|
TopologyKind::Ring,
|
|
TopologyKind::StarMoe,
|
|
TopologyKind::Market,
|
|
TopologyKind::Blackboard,
|
|
TopologyKind::Debate,
|
|
TopologyKind::Holacratic,
|
|
];
|
|
|
|
/// The snake_case wire name (matches the serde representation).
|
|
pub fn as_str(&self) -> &'static str {
|
|
match self {
|
|
TopologyKind::Hierarchical => "hierarchical",
|
|
TopologyKind::Flat => "flat",
|
|
TopologyKind::Pipeline => "pipeline",
|
|
TopologyKind::Swarm => "swarm",
|
|
TopologyKind::Mesh => "mesh",
|
|
TopologyKind::HubSpoke => "hub_spoke",
|
|
TopologyKind::Ring => "ring",
|
|
TopologyKind::StarMoe => "star_moe",
|
|
TopologyKind::Market => "market",
|
|
TopologyKind::Blackboard => "blackboard",
|
|
TopologyKind::Debate => "debate",
|
|
TopologyKind::Holacratic => "holacratic",
|
|
}
|
|
}
|
|
|
|
/// One-line human description.
|
|
pub fn description(&self) -> &'static str {
|
|
match self {
|
|
TopologyKind::Hierarchical => "orchestrator delegates down; results bubble up",
|
|
TopologyKind::Flat => "autonomous peers with minimal coordination",
|
|
TopologyKind::Pipeline => "linear stages; each feeds the next",
|
|
TopologyKind::Swarm => "parallel attempts with consensus/aggregation",
|
|
TopologyKind::Mesh => "dense peer-to-peer exchange until convergence",
|
|
TopologyKind::HubSpoke => "a central hub routes to spokes and aggregates",
|
|
TopologyKind::Ring => "a cycle refining the result each lap",
|
|
TopologyKind::StarMoe => "a router dispatches subtasks to experts",
|
|
TopologyKind::Market => "tasks auctioned to agents by fit/cost",
|
|
TopologyKind::Blackboard => "agents collaborate via a shared workspace",
|
|
TopologyKind::Debate => "proposer vs critic rounds, then a judge",
|
|
TopologyKind::Holacratic => "self-organizing circles assign within roles",
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn as_str_matches_serde() {
|
|
for k in TopologyKind::ALL {
|
|
let json = serde_json::to_string(&k).unwrap();
|
|
assert_eq!(json, format!("\"{}\"", k.as_str()));
|
|
let back: TopologyKind = serde_json::from_str(&json).unwrap();
|
|
assert_eq!(back, k);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn all_is_complete_and_unique() {
|
|
let mut seen = std::collections::HashSet::new();
|
|
for k in TopologyKind::ALL {
|
|
assert!(seen.insert(k.as_str()), "duplicate in ALL: {}", k.as_str());
|
|
assert!(!k.description().is_empty());
|
|
}
|
|
assert_eq!(seen.len(), 12);
|
|
}
|
|
}
|