use std::collections::HashMap;
use car_ir::{ActionProposal, Precondition};
use car_multi::AgentSpec;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Workflow {
pub id: String,
pub name: String,
pub start: String,
pub stages: Vec<Stage>,
pub edges: Vec<Edge>,
#[serde(default = "default_max_iterations")]
pub max_iterations: u32,
#[serde(default)]
pub metadata: HashMap<String, Value>,
}
fn default_max_iterations() -> u32 {
100
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Stage {
pub id: String,
pub name: String,
pub step: StageStep,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub compensation: Option<CompensationHandler>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timeout_ms: Option<u64>,
#[serde(default)]
pub metadata: HashMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum StageStep {
Pattern(PatternStep),
Proposal(ProposalStep),
SubWorkflow(SubWorkflowStep),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PatternStep {
pub pattern: PatternKind,
pub task: String,
pub agents: Vec<AgentSpec>,
#[serde(default)]
pub config: HashMap<String, Value>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PatternKind {
SwarmParallel,
SwarmSequential,
SwarmDebate,
Pipeline,
Supervisor,
Delegator,
MapReduce,
Vote,
Fleet,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProposalStep {
pub proposal: ActionProposal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubWorkflowStep {
pub workflow: Box<Workflow>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Edge {
pub from: String,
pub to: String,
#[serde(default)]
pub conditions: Vec<Precondition>,
#[serde(default)]
pub label: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum CompensationHandler {
Proposal(ProposalStep),
StageRef { stage_id: String },
}
impl Workflow {
pub fn stage(&self, id: &str) -> Option<&Stage> {
self.stages.iter().find(|s| s.id == id)
}
pub fn outgoing_edges(&self, stage_id: &str) -> Vec<&Edge> {
self.edges.iter().filter(|e| e.from == stage_id).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn simple_workflow() -> Workflow {
Workflow {
id: "test-wf".into(),
name: "Test Workflow".into(),
start: "stage-a".into(),
stages: vec![
Stage {
id: "stage-a".into(),
name: "Stage A".into(),
step: StageStep::Proposal(ProposalStep {
proposal: ActionProposal {
id: "p1".into(),
source: "test".into(),
actions: vec![],
timestamp: chrono::Utc::now(),
context: HashMap::new(),
},
}),
compensation: None,
timeout_ms: None,
metadata: HashMap::new(),
},
Stage {
id: "stage-b".into(),
name: "Stage B".into(),
step: StageStep::Proposal(ProposalStep {
proposal: ActionProposal {
id: "p2".into(),
source: "test".into(),
actions: vec![],
timestamp: chrono::Utc::now(),
context: HashMap::new(),
},
}),
compensation: None,
timeout_ms: None,
metadata: HashMap::new(),
},
],
edges: vec![Edge {
from: "stage-a".into(),
to: "stage-b".into(),
conditions: vec![],
label: "always".into(),
}],
max_iterations: 100,
metadata: HashMap::new(),
}
}
#[test]
fn serde_roundtrip() {
let wf = simple_workflow();
let json = serde_json::to_string_pretty(&wf).unwrap();
let parsed: Workflow = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.id, "test-wf");
assert_eq!(parsed.stages.len(), 2);
assert_eq!(parsed.edges.len(), 1);
assert_eq!(parsed.start, "stage-a");
}
#[test]
fn stage_lookup() {
let wf = simple_workflow();
assert!(wf.stage("stage-a").is_some());
assert!(wf.stage("nonexistent").is_none());
}
#[test]
fn outgoing_edges() {
let wf = simple_workflow();
assert_eq!(wf.outgoing_edges("stage-a").len(), 1);
assert_eq!(wf.outgoing_edges("stage-b").len(), 0);
}
}