Skip to main content

agent_runtime/
workflow.rs

1use crate::step::{Step, StepType};
2use crate::types::JsonValue;
3use serde::{Deserialize, Serialize};
4
5#[cfg(test)]
6#[path = "workflow_test.rs"]
7mod workflow_test;
8
9/// Workflow execution state
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
11#[serde(rename_all = "lowercase")]
12pub enum WorkflowState {
13    Pending,
14    Running,
15    Completed,
16    Failed,
17}
18
19/// Workflow definition
20pub struct Workflow {
21    pub id: String,
22    pub steps: Vec<Box<dyn Step>>,
23    pub initial_input: JsonValue,
24    pub state: WorkflowState,
25}
26
27impl Workflow {
28    pub fn builder() -> WorkflowBuilder {
29        WorkflowBuilder::new()
30    }
31
32    /// Generate a Mermaid flowchart diagram of this workflow with full expansion
33    pub fn to_mermaid(&self) -> String {
34        let mut diagram = String::from("flowchart TD\n");
35        let mut node_counter = 0;
36
37        // Start node
38        diagram.push_str("    Start([Start])\n");
39
40        if self.steps.is_empty() {
41            diagram.push_str("    Start --> End\n");
42        } else {
43            // Connect start to first step
44            let first_node = format!("N{}", node_counter);
45            diagram.push_str(&format!("    Start --> {}\n", first_node));
46
47            // Generate recursive structure
48            let last_node =
49                self.generate_mermaid_steps(&mut diagram, &mut node_counter, &first_node, 0);
50
51            // Connect last step to end
52            diagram.push_str(&format!("    {} --> End\n", last_node));
53        }
54
55        diagram.push_str("    End([End])\n");
56
57        // Add styling
58        diagram.push('\n');
59        diagram.push_str("    classDef agentStyle fill:#e1f5ff,stroke:#01579b,stroke-width:2px\n");
60        diagram
61            .push_str("    classDef transformStyle fill:#f3e5f5,stroke:#4a148c,stroke-width:2px\n");
62        diagram.push_str(
63            "    classDef conditionalStyle fill:#fff3e0,stroke:#e65100,stroke-width:2px\n",
64        );
65        diagram.push_str(
66            "    classDef subworkflowStyle fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px\n",
67        );
68        diagram
69            .push_str("    classDef convergeStyle fill:#f5f5f5,stroke:#757575,stroke-width:1px\n");
70
71        diagram
72    }
73
74    /// Recursive helper to generate mermaid steps
75    fn generate_mermaid_steps(
76        &self,
77        diagram: &mut String,
78        node_counter: &mut usize,
79        entry_node: &str,
80        step_index: usize,
81    ) -> String {
82        if step_index >= self.steps.len() {
83            return entry_node.to_string();
84        }
85
86        let step = &self.steps[step_index];
87        let step_type = step.step_type();
88
89        match step_type {
90            StepType::Conditional => {
91                // Get branches
92                if let Some((then_step, else_step)) = step.get_branches() {
93                    let conditional_node = entry_node;
94
95                    // Create conditional diamond
96                    let step_name = step.name();
97                    diagram.push_str(&format!(
98                        "    {}{{{{\"{}\"}}}}:::conditionalStyle\n",
99                        conditional_node, step_name
100                    ));
101
102                    // Then branch
103                    *node_counter += 1;
104                    let then_node = format!("N{}", node_counter);
105                    let then_exit_node;
106
107                    // Check if then branch is a SubWorkflow
108                    if then_step.step_type() == StepType::SubWorkflow {
109                        if let Some(sub_wf) = then_step.get_sub_workflow() {
110                            // Generate subworkflow and get (entry, exit)
111                            let (_entry, exit) = self.generate_subworkflow_inline(
112                                diagram,
113                                node_counter,
114                                &then_node,
115                                sub_wf,
116                                then_step.name(),
117                            );
118                            then_exit_node = exit;
119                        } else {
120                            self.generate_step_node(diagram, &then_node, then_step);
121                            then_exit_node = then_node.clone();
122                        }
123                    } else {
124                        self.generate_step_node(diagram, &then_node, then_step);
125                        then_exit_node = then_node.clone();
126                    }
127
128                    diagram.push_str(&format!(
129                        "    {} -->|\"✓ TRUE\"| {}\n",
130                        conditional_node, then_node
131                    ));
132
133                    // Else branch
134                    *node_counter += 1;
135                    let else_node = format!("N{}", node_counter);
136                    let else_exit_node;
137
138                    // Check if else branch is a SubWorkflow
139                    if else_step.step_type() == StepType::SubWorkflow {
140                        if let Some(sub_wf) = else_step.get_sub_workflow() {
141                            let (_entry, exit) = self.generate_subworkflow_inline(
142                                diagram,
143                                node_counter,
144                                &else_node,
145                                sub_wf,
146                                else_step.name(),
147                            );
148                            else_exit_node = exit;
149                        } else {
150                            self.generate_step_node(diagram, &else_node, else_step);
151                            else_exit_node = else_node.clone();
152                        }
153                    } else {
154                        self.generate_step_node(diagram, &else_node, else_step);
155                        else_exit_node = else_node.clone();
156                    }
157
158                    diagram.push_str(&format!(
159                        "    {} -->|\"✗ FALSE\"| {}\n",
160                        conditional_node, else_node
161                    ));
162
163                    // Convergence point - use EXIT nodes from branches
164                    *node_counter += 1;
165                    let converge_node = format!("N{}", node_counter);
166                    diagram.push_str(&format!("    {}(( )):::convergeStyle\n", converge_node));
167                    diagram.push_str(&format!("    {} --> {}\n", then_exit_node, converge_node));
168                    diagram.push_str(&format!("    {} --> {}\n", else_exit_node, converge_node));
169
170                    // Continue with next step
171                    if step_index + 1 < self.steps.len() {
172                        // Check if next step is a subworkflow - if so, DON'T create intermediate node
173                        let next_step = &self.steps[step_index + 1];
174                        if next_step.step_type() == StepType::SubWorkflow {
175                            // Pass converge_node directly as entry for subworkflow
176                            return self.generate_mermaid_steps(
177                                diagram,
178                                node_counter,
179                                &converge_node,
180                                step_index + 1,
181                            );
182                        } else {
183                            *node_counter += 1;
184                            let next_node = format!("N{}", node_counter);
185                            diagram.push_str(&format!("    {} --> {}\n", converge_node, next_node));
186                            return self.generate_mermaid_steps(
187                                diagram,
188                                node_counter,
189                                &next_node,
190                                step_index + 1,
191                            );
192                        }
193                    } else {
194                        return converge_node;
195                    }
196                }
197            }
198            StepType::SubWorkflow => {
199                // Get sub-workflow and expand it
200                if let Some(sub_wf) = step.get_sub_workflow() {
201                    let sub_start_node = entry_node;
202
203                    // Generate subworkflow and get its exit node
204                    let (_entry, sub_exit_node) = self.generate_subworkflow_inline(
205                        diagram,
206                        node_counter,
207                        sub_start_node,
208                        sub_wf,
209                        step.name(),
210                    );
211
212                    // Continue with next step from subworkflow exit
213                    return self.generate_mermaid_steps(
214                        diagram,
215                        node_counter,
216                        &sub_exit_node,
217                        step_index + 1,
218                    );
219                }
220            }
221            _ => {
222                // Regular step (Agent, Transform, etc.)
223                let current_node = entry_node;
224                self.generate_step_node(diagram, current_node, step.as_ref());
225
226                // Continue with next step if there is one
227                if step_index + 1 < self.steps.len() {
228                    // Check if next step is subworkflow
229                    let next_step = &self.steps[step_index + 1];
230                    if next_step.step_type() == StepType::SubWorkflow {
231                        // Pass current_node directly as entry for subworkflow
232                        return self.generate_mermaid_steps(
233                            diagram,
234                            node_counter,
235                            current_node,
236                            step_index + 1,
237                        );
238                    } else {
239                        *node_counter += 1;
240                        let next_node = format!("N{}", node_counter);
241                        diagram.push_str(&format!("    {} --> {}\n", current_node, next_node));
242                        return self.generate_mermaid_steps(
243                            diagram,
244                            node_counter,
245                            &next_node,
246                            step_index + 1,
247                        );
248                    }
249                } else {
250                    // This is the last step
251                    return current_node.to_string();
252                }
253            }
254        }
255
256        entry_node.to_string()
257    }
258
259    /// Generate a subworkflow inline as a subgraph
260    /// Returns (entry_node, exit_node) tuple
261    fn generate_subworkflow_inline(
262        &self,
263        diagram: &mut String,
264        node_counter: &mut usize,
265        entry_point: &str,
266        sub_wf: Workflow,
267        step_name: &str,
268    ) -> (String, String) {
269        let subgraph_id = *node_counter;
270
271        // Create sub-workflow container
272        diagram.push_str(&format!(
273            "    subgraph SUB{}[\"📦 {}\"]\n",
274            subgraph_id, step_name
275        ));
276
277        *node_counter += 1;
278        let sub_entry = format!("N{}", node_counter);
279        diagram.push_str(&format!("        {}([Start])\n", sub_entry));
280
281        // Generate sub-workflow steps recursively
282        let sub_exit =
283            sub_wf.generate_mermaid_steps_in_subgraph(diagram, node_counter, &sub_entry, 0);
284
285        *node_counter += 1;
286        let sub_end = format!("N{}", node_counter);
287        diagram.push_str(&format!("        {}([End])\n", sub_end));
288        diagram.push_str(&format!("        {} --> {}\n", sub_exit, sub_end));
289
290        diagram.push_str("    end\n");
291
292        // Connect entry point to subworkflow start
293        diagram.push_str(&format!("    {} --> {}\n", entry_point, sub_entry));
294
295        // Return both entry point (for conditional connection) and exit node (for continuation)
296        (entry_point.to_string(), sub_end)
297    }
298
299    /// Generate steps within a subgraph (different indentation)
300    fn generate_mermaid_steps_in_subgraph(
301        &self,
302        diagram: &mut String,
303        node_counter: &mut usize,
304        entry_node: &str,
305        step_index: usize,
306    ) -> String {
307        if step_index >= self.steps.len() {
308            return entry_node.to_string();
309        }
310
311        let step = &self.steps[step_index];
312        let step_type = step.step_type();
313
314        match step_type {
315            StepType::Conditional => {
316                if let Some((then_step, else_step)) = step.get_branches() {
317                    let conditional_node = entry_node;
318
319                    diagram.push_str(&format!(
320                        "        {}{{{{\"{}\"}}}}:::conditionalStyle\n",
321                        conditional_node,
322                        step.name()
323                    ));
324
325                    *node_counter += 1;
326                    let then_node = format!("N{}", node_counter);
327                    self.generate_step_node_indented(diagram, &then_node, then_step);
328                    diagram.push_str(&format!(
329                        "        {} -->|\"✓\"| {}\n",
330                        conditional_node, then_node
331                    ));
332
333                    *node_counter += 1;
334                    let else_node = format!("N{}", node_counter);
335                    self.generate_step_node_indented(diagram, &else_node, else_step);
336                    diagram.push_str(&format!(
337                        "        {} -->|\"✗\"| {}\n",
338                        conditional_node, else_node
339                    ));
340
341                    *node_counter += 1;
342                    let converge_node = format!("N{}", node_counter);
343                    diagram.push_str(&format!("        {}(( )):::convergeStyle\n", converge_node));
344                    diagram.push_str(&format!("        {} --> {}\n", then_node, converge_node));
345                    diagram.push_str(&format!("        {} --> {}\n", else_node, converge_node));
346
347                    *node_counter += 1;
348                    let next_node = format!("N{}", node_counter);
349                    diagram.push_str(&format!("        {} --> {}\n", converge_node, next_node));
350
351                    return self.generate_mermaid_steps_in_subgraph(
352                        diagram,
353                        node_counter,
354                        &next_node,
355                        step_index + 1,
356                    );
357                }
358            }
359            StepType::SubWorkflow => {
360                // Nested subworkflow within a subworkflow
361                if let Some(nested_wf) = step.get_sub_workflow() {
362                    let nested_entry = entry_node;
363
364                    let nested_id = *node_counter;
365                    diagram.push_str(&format!(
366                        "        subgraph NESTED{}[\"📦 {}\"]\n",
367                        nested_id,
368                        step.name()
369                    ));
370
371                    *node_counter += 1;
372                    let nested_start = format!("N{}", node_counter);
373                    diagram.push_str(&format!("            {}([Start])\n", nested_start));
374
375                    let nested_exit = nested_wf.generate_mermaid_steps_in_nested_subgraph(
376                        diagram,
377                        node_counter,
378                        &nested_start,
379                        0,
380                    );
381
382                    *node_counter += 1;
383                    let nested_end = format!("N{}", node_counter);
384                    diagram.push_str(&format!("            {}([End])\n", nested_end));
385                    diagram.push_str(&format!("            {} --> {}\n", nested_exit, nested_end));
386                    diagram.push_str("        end\n");
387
388                    diagram.push_str(&format!("        {} --> {}\n", nested_entry, nested_start));
389
390                    *node_counter += 1;
391                    let next_node = format!("N{}", node_counter);
392                    diagram.push_str(&format!("        {} --> {}\n", nested_end, next_node));
393
394                    return self.generate_mermaid_steps_in_subgraph(
395                        diagram,
396                        node_counter,
397                        &next_node,
398                        step_index + 1,
399                    );
400                }
401            }
402            _ => {
403                let current_node = entry_node;
404                self.generate_step_node_indented(diagram, current_node, step.as_ref());
405
406                *node_counter += 1;
407                let next_node = format!("N{}", node_counter);
408                diagram.push_str(&format!("        {} --> {}\n", current_node, next_node));
409
410                return self.generate_mermaid_steps_in_subgraph(
411                    diagram,
412                    node_counter,
413                    &next_node,
414                    step_index + 1,
415                );
416            }
417        }
418
419        entry_node.to_string()
420    }
421
422    /// Generate steps within a nested subgraph (triple indentation)
423    fn generate_mermaid_steps_in_nested_subgraph(
424        &self,
425        diagram: &mut String,
426        node_counter: &mut usize,
427        entry_node: &str,
428        step_index: usize,
429    ) -> String {
430        if step_index >= self.steps.len() {
431            return entry_node.to_string();
432        }
433
434        let step = &self.steps[step_index];
435        let current_node = entry_node;
436
437        // For simplicity at 3rd level, just show step names without further nesting
438        let step_name = step.name();
439        let step_type = step.step_type();
440
441        let (node_def, style) = match step_type {
442            StepType::Agent => (
443                format!("            {}[\"{}\"]", current_node, step_name),
444                ":::agentStyle",
445            ),
446            StepType::Transform => (
447                format!("            {}[/\"{}\"/]", current_node, step_name),
448                ":::transformStyle",
449            ),
450            StepType::Conditional => (
451                format!("            {}{{\"{}\"}}", current_node, step_name),
452                ":::conditionalStyle",
453            ),
454            _ => (
455                format!("            {}[\"{}\"]", current_node, step_name),
456                "",
457            ),
458        };
459
460        diagram.push_str(&format!("{}{}\n", node_def, style));
461
462        *node_counter += 1;
463        let next_node = format!("N{}", node_counter);
464        diagram.push_str(&format!("            {} --> {}\n", current_node, next_node));
465
466        self.generate_mermaid_steps_in_nested_subgraph(
467            diagram,
468            node_counter,
469            &next_node,
470            step_index + 1,
471        )
472    }
473
474    /// Generate a single step node (normal indentation)
475    fn generate_step_node(&self, diagram: &mut String, node_id: &str, step: &dyn Step) {
476        let step_name = step.name();
477        let step_type = step.step_type();
478
479        let (node_def, style) = match step_type {
480            StepType::Agent => (
481                format!("    {}[\"{}\"]", node_id, step_name),
482                ":::agentStyle",
483            ),
484            StepType::Transform => (
485                format!("    {}[/\"{}\"/]", node_id, step_name),
486                ":::transformStyle",
487            ),
488            StepType::SubWorkflow => (
489                format!("    {}[[\"{}\"]", node_id, step_name),
490                ":::subworkflowStyle",
491            ),
492            _ => (format!("    {}[\"{}\"]", node_id, step_name), ""),
493        };
494
495        diagram.push_str(&format!("{}{}\n", node_def, style));
496    }
497
498    /// Generate a single step node (indented for subgraph)
499    fn generate_step_node_indented(&self, diagram: &mut String, node_id: &str, step: &dyn Step) {
500        let step_name = step.name();
501        let step_type = step.step_type();
502
503        let (node_def, style) = match step_type {
504            StepType::Agent => (
505                format!("        {}[\"{}\"]", node_id, step_name),
506                ":::agentStyle",
507            ),
508            StepType::Transform => (
509                format!("        {}[/\"{}\"/]", node_id, step_name),
510                ":::transformStyle",
511            ),
512            _ => (format!("        {}[\"{}\"]", node_id, step_name), ""),
513        };
514
515        diagram.push_str(&format!("{}{}\n", node_def, style));
516    }
517}
518
519/// Builder for Workflow
520pub struct WorkflowBuilder {
521    steps: Vec<Box<dyn Step>>,
522    initial_input: Option<JsonValue>,
523}
524
525impl WorkflowBuilder {
526    pub fn new() -> Self {
527        Self {
528            steps: Vec::new(),
529            initial_input: None,
530        }
531    }
532
533    /// Add a step to the workflow
534    pub fn step(mut self, step: Box<dyn Step>) -> Self {
535        self.steps.push(step);
536        self
537    }
538
539    /// Set the initial input
540    pub fn initial_input(mut self, input: JsonValue) -> Self {
541        self.initial_input = Some(input);
542        self
543    }
544
545    pub fn build(self) -> Workflow {
546        Workflow {
547            id: format!("wf_{}", uuid::Uuid::new_v4()),
548            steps: self.steps,
549            initial_input: self.initial_input.unwrap_or(serde_json::json!({})),
550            state: WorkflowState::Pending,
551        }
552    }
553}
554
555impl Default for WorkflowBuilder {
556    fn default() -> Self {
557        Self::new()
558    }
559}
560
561/// A workflow execution run with complete history
562#[derive(Debug, Clone, Serialize, Deserialize)]
563pub struct WorkflowRun {
564    pub workflow_id: String,
565    pub state: WorkflowState,
566    pub steps: Vec<WorkflowStepRecord>,
567    pub final_output: Option<JsonValue>,
568
569    /// Parent workflow ID if this is a sub-workflow
570    #[serde(skip_serializing_if = "Option::is_none")]
571    pub parent_workflow_id: Option<String>,
572}
573
574impl WorkflowRun {
575    /// Generate a Mermaid flowchart with execution results
576    pub fn to_mermaid_with_results(&self) -> String {
577        let mut diagram = String::from("flowchart TD\n");
578
579        // Start node
580        let start_style = match self.state {
581            WorkflowState::Completed => ":::successStyle",
582            WorkflowState::Failed => ":::failureStyle",
583            _ => "",
584        };
585        diagram.push_str(&format!("    Start([Start]){}  \n", start_style));
586
587        // Connect start to first step
588        if !self.steps.is_empty() {
589            diagram.push_str("    Start --> Step0\n");
590        }
591
592        // Generate nodes for each step
593        for (i, step) in self.steps.iter().enumerate() {
594            let node_id = format!("Step{}", i);
595            let step_name = &step.step_name;
596            let step_type = &step.step_type;
597            let exec_time = step.execution_time_ms.unwrap_or(0);
598
599            // Determine style based on output
600            let has_output = step.output.is_some();
601            let style_class = if has_output {
602                ":::successStyle"
603            } else {
604                ":::failureStyle"
605            };
606
607            // Choose node shape based on step type
608            let node_def = if step_type.contains("Agent") {
609                format!(
610                    "    {}[\"{}<br/><i>{}</i><br/>{}ms\"]{}",
611                    node_id, step_name, step_type, exec_time, style_class
612                )
613            } else if step_type.contains("Transform") {
614                format!(
615                    "    {}[/\"{}<br/><i>{}</i><br/>{}ms\"/]{}",
616                    node_id, step_name, step_type, exec_time, style_class
617                )
618            } else if step_type.contains("Conditional") {
619                format!(
620                    "    {}{{\"{}<br/><i>{}</i><br/>{}ms\"}}{}",
621                    node_id, step_name, step_type, exec_time, style_class
622                )
623            } else if step_type.contains("SubWorkflow") {
624                format!(
625                    "    {}[[\"{}<br/><i>{}</i><br/>{}ms\"]]{}",
626                    node_id, step_name, step_type, exec_time, style_class
627                )
628            } else {
629                format!(
630                    "    {}[\"{}<br/><i>{}</i><br/>{}ms\"]{}",
631                    node_id, step_name, step_type, exec_time, style_class
632                )
633            };
634
635            diagram.push_str(&node_def);
636            diagram.push('\n');
637
638            // Connect to next step
639            if i < self.steps.len() - 1 {
640                diagram.push_str(&format!("    {} --> Step{}\n", node_id, i + 1));
641            }
642        }
643
644        // End node
645        if !self.steps.is_empty() {
646            let last_step = self.steps.len() - 1;
647            diagram.push_str(&format!("    Step{} --> End\n", last_step));
648        } else {
649            diagram.push_str("    Start --> End\n");
650        }
651
652        let end_style = match self.state {
653            WorkflowState::Completed => ":::successStyle",
654            WorkflowState::Failed => ":::failureStyle",
655            _ => "",
656        };
657        diagram.push_str(&format!("    End([End]){}\n", end_style));
658
659        // Add styling
660        diagram.push('\n');
661        diagram
662            .push_str("    classDef successStyle fill:#c8e6c9,stroke:#2e7d32,stroke-width:3px\n");
663        diagram
664            .push_str("    classDef failureStyle fill:#ffcdd2,stroke:#c62828,stroke-width:3px\n");
665
666        diagram
667    }
668}
669
670/// A single step record in workflow execution
671#[derive(Debug, Clone, Serialize, Deserialize)]
672pub struct WorkflowStepRecord {
673    pub step_index: usize,
674    pub step_name: String,
675    pub step_type: String,
676    pub input: JsonValue,
677    pub output: Option<JsonValue>,
678    pub execution_time_ms: Option<u64>,
679}