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:
co-authored by
Claude Opus 5
parent
b1bdfbbf87
commit
d94487d3ba
@@ -33,6 +33,13 @@ pub struct CatalogEntry {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub role_distribution: Vec<RoleWeight>,
|
pub role_distribution: Vec<RoleWeight>,
|
||||||
|
/// The execution pattern this kind actually runs as. Twelve kinds map onto
|
||||||
|
/// five patterns, so this differs from `name` for the aliased ones.
|
||||||
|
pub executes_as: String,
|
||||||
|
/// False when the kind is an alias — its description promises semantics the
|
||||||
|
/// engine does not implement (Market never auctions, Ring never cycles).
|
||||||
|
/// A UI should not offer these as if they behaved differently.
|
||||||
|
pub distinct_at_execution: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `GET /api/topologies` — the catalog of supported topology kinds.
|
/// `GET /api/topologies` — the catalog of supported topology kinds.
|
||||||
@@ -53,6 +60,8 @@ pub async fn catalog(_auth: Authed) -> Json<Vec<CatalogEntry>> {
|
|||||||
weight: *weight,
|
weight: *weight,
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
|
executes_as: kind.execution_pattern().as_str().to_string(),
|
||||||
|
distinct_at_execution: kind.is_distinct_at_execution(),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ pub use provider_executor::ProviderExecutor;
|
|||||||
pub use workflow::{run_workflow, WorkflowRecord};
|
pub use workflow::{run_workflow, WorkflowRecord};
|
||||||
|
|
||||||
use cm_domain::GatedCategory;
|
use cm_domain::GatedCategory;
|
||||||
use cm_topology::{TopologyGraph, TopologyKind};
|
use cm_topology::{ExecutionPattern, TopologyGraph, TopologyKind};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// Errors from planning or running a topology.
|
/// Errors from planning or running a topology.
|
||||||
@@ -175,18 +175,19 @@ pub struct RunProgress {
|
|||||||
pub totals: RunMetrics,
|
pub totals: RunMetrics,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Map every topology kind onto one of five execution patterns. The match is
|
/// Dispatch to the planner for this graph's execution pattern.
|
||||||
/// exhaustive, so adding a `TopologyKind` upstream forces a decision here.
|
///
|
||||||
|
/// The kind→pattern collapse lives on `TopologyKind::execution_pattern` so the
|
||||||
|
/// catalog API and this dispatch cannot disagree about what a kind actually
|
||||||
|
/// does. The match is exhaustive, so adding an `ExecutionPattern` upstream
|
||||||
|
/// forces a decision here.
|
||||||
fn plan_steps(graph: &TopologyGraph) -> Result<Vec<plan::PlanStep>, OrchestratorError> {
|
fn plan_steps(graph: &TopologyGraph) -> Result<Vec<plan::PlanStep>, OrchestratorError> {
|
||||||
Ok(match graph.kind {
|
Ok(match graph.kind.execution_pattern() {
|
||||||
TopologyKind::Hierarchical
|
ExecutionPattern::Hierarchical => plan::hierarchical(graph)?,
|
||||||
| TopologyKind::HubSpoke
|
ExecutionPattern::Pipeline => plan::pipeline(graph)?,
|
||||||
| TopologyKind::StarMoe
|
ExecutionPattern::Swarm => plan::swarm(graph)?,
|
||||||
| TopologyKind::Market => plan::hierarchical(graph)?,
|
ExecutionPattern::Mesh => plan::mesh(graph)?,
|
||||||
TopologyKind::Pipeline | TopologyKind::Ring => plan::pipeline(graph)?,
|
ExecutionPattern::Debate => plan::debate(graph)?,
|
||||||
TopologyKind::Swarm | TopologyKind::Flat | TopologyKind::Holacratic => plan::swarm(graph)?,
|
|
||||||
TopologyKind::Mesh | TopologyKind::Blackboard => plan::mesh(graph)?,
|
|
||||||
TopologyKind::Debate => plan::debate(graph)?,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,74 @@ pub enum TopologyKind {
|
|||||||
Holacratic,
|
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 {
|
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.
|
/// Every supported kind, for iteration in tests/UIs/benchmarks.
|
||||||
pub const ALL: [TopologyKind; 12] = [
|
pub const ALL: [TopologyKind; 12] = [
|
||||||
TopologyKind::Hierarchical,
|
TopologyKind::Hierarchical,
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ pub use builders::build;
|
|||||||
pub use classifier::{classify, Classification, GraphMetrics};
|
pub use classifier::{classify, Classification, GraphMetrics};
|
||||||
pub use graph::{Edge, EdgeKind, Node, TopologyGraph};
|
pub use graph::{Edge, EdgeKind, Node, TopologyGraph};
|
||||||
pub use heuristics::{heuristics, Heuristics};
|
pub use heuristics::{heuristics, Heuristics};
|
||||||
pub use kind::TopologyKind;
|
pub use kind::{ExecutionPattern, TopologyKind};
|
||||||
|
|
||||||
/// Errors produced while building or validating a topology.
|
/// Errors produced while building or validating a topology.
|
||||||
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
|
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
|
||||||
|
|||||||
Reference in New Issue
Block a user