refactor(topology): make the 12-kinds-to-5-patterns collapse explicit

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]>
This commit is contained in:
Omar Sobh
2026-07-30 10:54:43 -07:00
co-authored by Claude Opus 5
parent b1bdfbbf87
commit d94487d3ba
4 changed files with 90 additions and 13 deletions
+67
View File
@@ -33,7 +33,74 @@ pub enum TopologyKind {
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,
+1 -1
View File
@@ -26,7 +26,7 @@ pub use builders::build;
pub use classifier::{classify, Classification, GraphMetrics};
pub use graph::{Edge, EdgeKind, Node, TopologyGraph};
pub use heuristics::{heuristics, Heuristics};
pub use kind::TopologyKind;
pub use kind::{ExecutionPattern, TopologyKind};
/// Errors produced while building or validating a topology.
#[derive(Debug, thiserror::Error, PartialEq, Eq)]