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}