1use std::collections::HashMap;
4
5use car_ir::{ActionProposal, Precondition};
6use car_multi::AgentSpec;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct Workflow {
13 pub id: String,
15 pub name: String,
17 pub start: String,
19 pub stages: Vec<Stage>,
21 pub edges: Vec<Edge>,
23 #[serde(default = "default_max_iterations")]
25 pub max_iterations: u32,
26 #[serde(default)]
28 pub metadata: HashMap<String, Value>,
29}
30
31fn default_max_iterations() -> u32 {
32 100
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct Stage {
38 pub id: String,
40 pub name: String,
42 pub step: StageStep,
44 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub compensation: Option<CompensationHandler>,
47 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub timeout_ms: Option<u64>,
50 #[serde(default)]
52 pub metadata: HashMap<String, Value>,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
57#[serde(tag = "type", rename_all = "snake_case")]
58pub enum StageStep {
59 Pattern(PatternStep),
61 Proposal(ProposalStep),
63 SubWorkflow(SubWorkflowStep),
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct PatternStep {
70 pub pattern: PatternKind,
72 pub task: String,
74 pub agents: Vec<AgentSpec>,
76 #[serde(default)]
81 pub config: HashMap<String, Value>,
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
86#[serde(rename_all = "snake_case")]
87pub enum PatternKind {
88 SwarmParallel,
89 SwarmSequential,
90 SwarmDebate,
91 Pipeline,
92 Supervisor,
93 Delegator,
94 MapReduce,
95 Vote,
96 Fleet,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct ProposalStep {
102 pub proposal: ActionProposal,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct SubWorkflowStep {
108 pub workflow: Box<Workflow>,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct Edge {
114 pub from: String,
116 pub to: String,
118 #[serde(default)]
122 pub conditions: Vec<Precondition>,
123 #[serde(default)]
125 pub label: String,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
130#[serde(tag = "type", rename_all = "snake_case")]
131pub enum CompensationHandler {
132 Proposal(ProposalStep),
134 StageRef { stage_id: String },
136}
137
138impl Workflow {
139 pub fn stage(&self, id: &str) -> Option<&Stage> {
141 self.stages.iter().find(|s| s.id == id)
142 }
143
144 pub fn outgoing_edges(&self, stage_id: &str) -> Vec<&Edge> {
146 self.edges.iter().filter(|e| e.from == stage_id).collect()
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 fn simple_workflow() -> Workflow {
155 Workflow {
156 id: "test-wf".into(),
157 name: "Test Workflow".into(),
158 start: "stage-a".into(),
159 stages: vec![
160 Stage {
161 id: "stage-a".into(),
162 name: "Stage A".into(),
163 step: StageStep::Proposal(ProposalStep {
164 proposal: ActionProposal {
165 id: "p1".into(),
166 source: "test".into(),
167 actions: vec![],
168 timestamp: chrono::Utc::now(),
169 context: HashMap::new(),
170 },
171 }),
172 compensation: None,
173 timeout_ms: None,
174 metadata: HashMap::new(),
175 },
176 Stage {
177 id: "stage-b".into(),
178 name: "Stage B".into(),
179 step: StageStep::Proposal(ProposalStep {
180 proposal: ActionProposal {
181 id: "p2".into(),
182 source: "test".into(),
183 actions: vec![],
184 timestamp: chrono::Utc::now(),
185 context: HashMap::new(),
186 },
187 }),
188 compensation: None,
189 timeout_ms: None,
190 metadata: HashMap::new(),
191 },
192 ],
193 edges: vec![Edge {
194 from: "stage-a".into(),
195 to: "stage-b".into(),
196 conditions: vec![],
197 label: "always".into(),
198 }],
199 max_iterations: 100,
200 metadata: HashMap::new(),
201 }
202 }
203
204 #[test]
205 fn serde_roundtrip() {
206 let wf = simple_workflow();
207 let json = serde_json::to_string_pretty(&wf).unwrap();
208 let parsed: Workflow = serde_json::from_str(&json).unwrap();
209 assert_eq!(parsed.id, "test-wf");
210 assert_eq!(parsed.stages.len(), 2);
211 assert_eq!(parsed.edges.len(), 1);
212 assert_eq!(parsed.start, "stage-a");
213 }
214
215 #[test]
216 fn stage_lookup() {
217 let wf = simple_workflow();
218 assert!(wf.stage("stage-a").is_some());
219 assert!(wf.stage("nonexistent").is_none());
220 }
221
222 #[test]
223 fn outgoing_edges() {
224 let wf = simple_workflow();
225 assert_eq!(wf.outgoing_edges("stage-a").len(), 1);
226 assert_eq!(wf.outgoing_edges("stage-b").len(), 0);
227 }
228}