Skip to main content

car_workflow/
types.rs

1//! Workflow definition types — fully serializable for use through all bindings.
2
3use std::collections::HashMap;
4
5use car_ir::{ActionProposal, Precondition};
6use car_multi::AgentSpec;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9
10/// Top-level workflow definition: a named graph of stages with conditional edges.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct Workflow {
13    /// Unique identifier.
14    pub id: String,
15    /// Human-readable name.
16    pub name: String,
17    /// Stage ID of the entry point.
18    pub start: String,
19    /// All stages in this workflow.
20    pub stages: Vec<Stage>,
21    /// Directed edges between stages (may have conditions).
22    pub edges: Vec<Edge>,
23    /// Maximum total stage executions before aborting (loop guard).
24    #[serde(default = "default_max_iterations")]
25    pub max_iterations: u32,
26    /// Opaque metadata (owner, version, tags, etc.).
27    #[serde(default)]
28    pub metadata: HashMap<String, Value>,
29}
30
31fn default_max_iterations() -> u32 {
32    100
33}
34
35/// A named step in the workflow.
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct Stage {
38    /// Unique stage identifier (referenced by edges).
39    pub id: String,
40    /// Human-readable name.
41    pub name: String,
42    /// What this stage does.
43    pub step: StageStep,
44    /// Optional compensation to run if a later stage fails (saga pattern).
45    #[serde(default, skip_serializing_if = "Option::is_none")]
46    pub compensation: Option<CompensationHandler>,
47    /// Optional timeout for this stage in milliseconds.
48    #[serde(default, skip_serializing_if = "Option::is_none")]
49    pub timeout_ms: Option<u64>,
50    /// Opaque metadata.
51    #[serde(default)]
52    pub metadata: HashMap<String, Value>,
53}
54
55/// What a stage actually does.
56#[derive(Debug, Clone, Serialize, Deserialize)]
57#[serde(tag = "type", rename_all = "snake_case")]
58pub enum StageStep {
59    /// Run a car-multi agent coordination pattern.
60    Pattern(PatternStep),
61    /// Execute a car-engine action proposal directly.
62    Proposal(ProposalStep),
63    /// Run a nested sub-workflow.
64    SubWorkflow(SubWorkflowStep),
65}
66
67/// Run one of the car-multi coordination patterns.
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct PatternStep {
70    /// Which pattern to run.
71    pub pattern: PatternKind,
72    /// Task description passed to the pattern.
73    pub task: String,
74    /// Agents involved. Interpretation depends on pattern kind.
75    pub agents: Vec<AgentSpec>,
76    /// Pattern-specific configuration.
77    /// - supervisor: `{"max_rounds": 3, "supervisor_index": 0}`
78    /// - map_reduce: `{"max_concurrent": 5, "items": ["a", "b", "c"]}`
79    /// - fleet: `{"timeout_secs": 60}`
80    #[serde(default)]
81    pub config: HashMap<String, Value>,
82}
83
84/// All supported car-multi coordination patterns.
85#[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/// Execute a car-engine action proposal.
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct ProposalStep {
102    pub proposal: ActionProposal,
103}
104
105/// Run a nested workflow.
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct SubWorkflowStep {
108    pub workflow: Box<Workflow>,
109}
110
111/// Directed edge between two stages, optionally conditional.
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct Edge {
114    /// Source stage ID.
115    pub from: String,
116    /// Target stage ID.
117    pub to: String,
118    /// Conditions that must ALL be satisfied (AND) for this edge to be taken.
119    /// Evaluated against the workflow state after `from` completes.
120    /// Empty = unconditional.
121    #[serde(default)]
122    pub conditions: Vec<Precondition>,
123    /// Human-readable label (e.g., "on success", "if approved").
124    #[serde(default)]
125    pub label: String,
126}
127
128/// What to run when compensating a stage on workflow failure (saga pattern).
129#[derive(Debug, Clone, Serialize, Deserialize)]
130#[serde(tag = "type", rename_all = "snake_case")]
131pub enum CompensationHandler {
132    /// Run a proposal to undo side effects.
133    Proposal(ProposalStep),
134    /// Execute a named stage from this workflow.
135    StageRef { stage_id: String },
136}
137
138impl Workflow {
139    /// Look up a stage by ID.
140    pub fn stage(&self, id: &str) -> Option<&Stage> {
141        self.stages.iter().find(|s| s.id == id)
142    }
143
144    /// Get all outgoing edges from a stage.
145    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}