Skip to main content

car_workflow/
result.rs

1//! Workflow execution result types.
2
3use std::collections::HashMap;
4
5use car_ir::ProposalResult;
6use car_multi::AgentOutput;
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11use crate::types::{ApprovalField, Workflow};
12
13/// Overall workflow execution result.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct WorkflowResult {
16    pub workflow_id: String,
17    pub workflow_name: String,
18    pub status: WorkflowStatus,
19    /// Results for each stage that executed (in execution order).
20    pub stages: Vec<StageResult>,
21    /// Compensation records (in compensation order, reverse of execution).
22    pub compensations: Vec<CompensationResult>,
23    pub duration_ms: f64,
24    pub timestamp: DateTime<Utc>,
25    /// Final workflow state snapshot.
26    #[serde(default)]
27    pub final_state: HashMap<String, Value>,
28    /// Present only when `status == Paused`: the checkpoint needed to resume.
29    #[serde(default, skip_serializing_if = "Option::is_none")]
30    pub paused: Option<PausedWorkflow>,
31}
32
33impl WorkflowResult {
34    pub fn succeeded(&self) -> bool {
35        self.status == WorkflowStatus::Completed
36    }
37
38    /// True when the run is parked at a human-in-the-loop approval gate.
39    pub fn is_paused(&self) -> bool {
40        self.status == WorkflowStatus::Paused
41    }
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(rename_all = "snake_case")]
46pub enum WorkflowStatus {
47    /// All stages completed successfully.
48    Completed,
49    /// A stage failed and no compensation was attempted.
50    Failed,
51    /// A stage failed and all compensations succeeded.
52    Compensated,
53    /// A stage failed and some compensations also failed.
54    PartiallyCompensated,
55    /// Parked at an approval gate, awaiting human input via
56    /// [`crate::WorkflowEngine::resume`]. The `paused` field of
57    /// [`WorkflowResult`] carries the checkpoint.
58    Paused,
59}
60
61/// A serializable checkpoint of a paused run — everything needed to resume,
62/// including across a process restart.
63///
64/// The full [`Workflow`] definition is embedded so resumption is self-contained;
65/// callers persist this (e.g. via [`crate::CheckpointStore`]) keyed by `run_id`.
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct PausedWorkflow {
68    /// Stable identifier for this run; the token used to resume.
69    pub run_id: String,
70    /// The workflow being executed (embedded for self-contained resume).
71    pub workflow: Workflow,
72    /// The approval stage the run is parked at.
73    pub paused_stage_id: String,
74    /// Prompt shown to the human.
75    pub prompt: String,
76    /// Fields the human is asked to fill in.
77    pub fields: Vec<ApprovalField>,
78    /// State key the response will be written to on resume.
79    pub output_key: String,
80    /// Accumulated workflow state at the moment of pausing.
81    pub wf_state: HashMap<String, Value>,
82    /// Stage results recorded before the pause.
83    pub stage_results: Vec<StageResult>,
84    /// IDs of stages completed before the pause (for saga compensation order).
85    pub completed_stage_ids: Vec<String>,
86    /// Loop-guard counter at the moment of pausing.
87    pub iterations: u32,
88    /// Accumulated compute wall time before the pause, in milliseconds (excludes
89    /// the human wait). Carried so the resumed run reports total compute time.
90    #[serde(default)]
91    pub prior_duration_ms: f64,
92    /// When the run paused.
93    pub created_at: DateTime<Utc>,
94}
95
96/// Result of a single stage execution.
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct StageResult {
99    pub stage_id: String,
100    pub stage_name: String,
101    pub status: StageStatus,
102    pub output: StageOutput,
103    pub duration_ms: f64,
104    #[serde(default, skip_serializing_if = "Option::is_none")]
105    pub error: Option<String>,
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
109#[serde(rename_all = "snake_case")]
110pub enum StageStatus {
111    Succeeded,
112    Failed,
113    Skipped,
114    Compensated,
115}
116
117/// The typed output of a stage, depending on its step type.
118#[derive(Debug, Clone, Serialize, Deserialize)]
119#[serde(tag = "type", rename_all = "snake_case")]
120pub enum StageOutput {
121    Pattern {
122        outputs: Vec<AgentOutput>,
123        final_answer: String,
124    },
125    Proposal {
126        result: ProposalResult,
127    },
128    SubWorkflow {
129        result: Box<WorkflowResult>,
130    },
131    /// The human's response recorded when an approval gate was resumed.
132    Approval {
133        response: Value,
134    },
135    /// Result of an `adversarial_review` pattern stage. `passed` is the typed
136    /// verdict — edges should branch on `stage.<id>.review_passed` (a real bool)
137    /// rather than substring-matching the answer.
138    Review {
139        passed: bool,
140        blocker_count: usize,
141        findings: Vec<car_multi::ReviewFinding>,
142        reviewer: AgentOutput,
143    },
144    /// Result of a `LoopUntil` step.
145    Loop {
146        /// How many body iterations actually ran.
147        iterations: u32,
148        /// Whether the `until` predicate was satisfied (vs. hitting the cap).
149        satisfied: bool,
150        /// The per-iteration body outputs, in order.
151        iterations_output: Vec<Box<StageOutput>>,
152    },
153    /// Result of a `ForEach` step.
154    ForEach {
155        /// The items the body ran over (rendered as strings), in order.
156        items: Vec<String>,
157        /// The per-item body outputs, aligned with `items`.
158        outputs: Vec<Box<StageOutput>>,
159    },
160    Empty,
161}
162
163/// Record of a compensation attempt.
164#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct CompensationResult {
166    pub for_stage_id: String,
167    pub status: StageStatus,
168    pub duration_ms: f64,
169    #[serde(default, skip_serializing_if = "Option::is_none")]
170    pub error: Option<String>,
171}