car-workflow 0.24.0

Declarative multi-stage workflow orchestration for Common Agent Runtime
//! Workflow execution result types.

use std::collections::HashMap;

use car_ir::ProposalResult;
use car_multi::AgentOutput;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;

use crate::types::{ApprovalField, Workflow};

/// Overall workflow execution result.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkflowResult {
    pub workflow_id: String,
    pub workflow_name: String,
    pub status: WorkflowStatus,
    /// Results for each stage that executed (in execution order).
    pub stages: Vec<StageResult>,
    /// Compensation records (in compensation order, reverse of execution).
    pub compensations: Vec<CompensationResult>,
    pub duration_ms: f64,
    pub timestamp: DateTime<Utc>,
    /// Final workflow state snapshot.
    #[serde(default)]
    pub final_state: HashMap<String, Value>,
    /// Present only when `status == Paused`: the checkpoint needed to resume.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub paused: Option<PausedWorkflow>,
}

impl WorkflowResult {
    pub fn succeeded(&self) -> bool {
        self.status == WorkflowStatus::Completed
    }

    /// True when the run is parked at a human-in-the-loop approval gate.
    pub fn is_paused(&self) -> bool {
        self.status == WorkflowStatus::Paused
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WorkflowStatus {
    /// All stages completed successfully.
    Completed,
    /// A stage failed and no compensation was attempted.
    Failed,
    /// A stage failed and all compensations succeeded.
    Compensated,
    /// A stage failed and some compensations also failed.
    PartiallyCompensated,
    /// Parked at an approval gate, awaiting human input via
    /// [`crate::WorkflowEngine::resume`]. The `paused` field of
    /// [`WorkflowResult`] carries the checkpoint.
    Paused,
}

/// A serializable checkpoint of a paused run — everything needed to resume,
/// including across a process restart.
///
/// The full [`Workflow`] definition is embedded so resumption is self-contained;
/// callers persist this (e.g. via [`crate::CheckpointStore`]) keyed by `run_id`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PausedWorkflow {
    /// Stable identifier for this run; the token used to resume.
    pub run_id: String,
    /// The workflow being executed (embedded for self-contained resume).
    pub workflow: Workflow,
    /// The approval stage the run is parked at.
    pub paused_stage_id: String,
    /// Prompt shown to the human.
    pub prompt: String,
    /// Fields the human is asked to fill in.
    pub fields: Vec<ApprovalField>,
    /// State key the response will be written to on resume.
    pub output_key: String,
    /// Accumulated workflow state at the moment of pausing.
    pub wf_state: HashMap<String, Value>,
    /// Stage results recorded before the pause.
    pub stage_results: Vec<StageResult>,
    /// IDs of stages completed before the pause (for saga compensation order).
    pub completed_stage_ids: Vec<String>,
    /// Loop-guard counter at the moment of pausing.
    pub iterations: u32,
    /// Accumulated compute wall time before the pause, in milliseconds (excludes
    /// the human wait). Carried so the resumed run reports total compute time.
    #[serde(default)]
    pub prior_duration_ms: f64,
    /// When the run paused.
    pub created_at: DateTime<Utc>,
}

/// Result of a single stage execution.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StageResult {
    pub stage_id: String,
    pub stage_name: String,
    pub status: StageStatus,
    pub output: StageOutput,
    pub duration_ms: f64,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StageStatus {
    Succeeded,
    Failed,
    Skipped,
    Compensated,
}

/// The typed output of a stage, depending on its step type.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum StageOutput {
    Pattern {
        outputs: Vec<AgentOutput>,
        final_answer: String,
    },
    Proposal {
        result: ProposalResult,
    },
    SubWorkflow {
        result: Box<WorkflowResult>,
    },
    /// The human's response recorded when an approval gate was resumed.
    Approval {
        response: Value,
    },
    /// Result of an `adversarial_review` pattern stage. `passed` is the typed
    /// verdict — edges should branch on `stage.<id>.review_passed` (a real bool)
    /// rather than substring-matching the answer.
    Review {
        passed: bool,
        blocker_count: usize,
        findings: Vec<car_multi::ReviewFinding>,
        reviewer: AgentOutput,
    },
    /// Result of a `LoopUntil` step.
    Loop {
        /// How many body iterations actually ran.
        iterations: u32,
        /// Whether the `until` predicate was satisfied (vs. hitting the cap).
        satisfied: bool,
        /// The per-iteration body outputs, in order.
        iterations_output: Vec<Box<StageOutput>>,
    },
    /// Result of a `ForEach` step.
    ForEach {
        /// The items the body ran over (rendered as strings), in order.
        items: Vec<String>,
        /// The per-item body outputs, aligned with `items`.
        outputs: Vec<Box<StageOutput>>,
    },
    Empty,
}

/// Record of a compensation attempt.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompensationResult {
    pub for_stage_id: String,
    pub status: StageStatus,
    pub duration_ms: f64,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
}