Skip to main content

jamjet_core/
workflow.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5/// Unique identifier for a workflow definition.
6pub type WorkflowId = String;
7
8/// Unique identifier for a workflow execution instance.
9#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub struct ExecutionId(pub Uuid);
11
12impl ExecutionId {
13    pub fn new() -> Self {
14        Self(Uuid::new_v4())
15    }
16}
17
18impl Default for ExecutionId {
19    fn default() -> Self {
20        Self::new()
21    }
22}
23
24impl std::fmt::Display for ExecutionId {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        write!(f, "exec_{}", self.0.simple())
27    }
28}
29
30/// The lifecycle status of a workflow execution.
31#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32#[serde(rename_all = "snake_case")]
33pub enum WorkflowStatus {
34    /// Created, not yet started.
35    Pending,
36    /// One or more nodes are active or queued.
37    Running,
38    /// Paused waiting for: human approval, external event, or timer.
39    Paused,
40    /// All terminal nodes reached successfully.
41    Completed,
42    /// A node failed beyond its retry policy.
43    Failed,
44    /// Explicitly cancelled.
45    Cancelled,
46    /// A strategy limit (max_iterations, max_cost_usd, timeout_seconds) was
47    /// exceeded. Terminal — does not transition to any other state.
48    LimitExceeded,
49}
50
51impl WorkflowStatus {
52    /// Returns true if this is a terminal status (no further transitions possible).
53    pub fn is_terminal(&self) -> bool {
54        matches!(
55            self,
56            Self::Completed | Self::Failed | Self::Cancelled | Self::LimitExceeded
57        )
58    }
59
60    /// Returns true if this execution can still accept work.
61    pub fn is_active(&self) -> bool {
62        matches!(self, Self::Pending | Self::Running | Self::Paused)
63    }
64
65    /// Validate a state transition. Returns Ok(()) if the transition is valid.
66    pub fn validate_transition(&self, next: &WorkflowStatus) -> crate::error::Result<()> {
67        let valid = matches!(
68            (self, next),
69            (Self::Pending, Self::Running)
70                | (Self::Running, Self::Paused)
71                | (Self::Running, Self::Completed)
72                | (Self::Running, Self::Failed)
73                | (Self::Running, Self::Cancelled)
74                | (Self::Running, Self::LimitExceeded)
75                | (Self::Paused, Self::Running)
76                | (Self::Paused, Self::Cancelled)
77        );
78        if valid {
79            Ok(())
80        } else {
81            Err(crate::Error::InvalidTransition {
82                current: self.clone(),
83                requested: next.clone(),
84            })
85        }
86    }
87}
88
89/// Session type label for execution metadata.
90/// Describes the governance and durability model of an execution session.
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
92#[serde(rename_all = "snake_case")]
93pub enum SessionType {
94    /// Fire-and-forget, no state persisted beyond completion.
95    Stateless,
96    /// Checkpointed execution that can resume after interruption.
97    Resumable,
98    /// Long-lived session with audit trail and governance policies.
99    PersistentGoverned,
100    /// Short-lived session discarded after use (e.g. preview, dry-run).
101    Ephemeral,
102    /// Session that requires human approval at one or more gates.
103    ApprovalGated,
104}
105
106/// Metadata for a workflow definition.
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct WorkflowMetadata {
109    pub id: WorkflowId,
110    pub version: String,
111    pub name: Option<String>,
112    pub description: Option<String>,
113    pub state_schema: String,
114    pub created_at: DateTime<Utc>,
115}
116
117/// A running execution of a workflow.
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct WorkflowExecution {
120    pub execution_id: ExecutionId,
121    pub workflow_id: WorkflowId,
122    pub workflow_version: String,
123    pub status: WorkflowStatus,
124    pub initial_input: serde_json::Value,
125    pub current_state: serde_json::Value,
126    pub started_at: DateTime<Utc>,
127    pub updated_at: DateTime<Utc>,
128    pub completed_at: Option<DateTime<Utc>>,
129    /// Session type label for governance and durability classification.
130    #[serde(default, skip_serializing_if = "Option::is_none")]
131    pub session_type: Option<SessionType>,
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn valid_transitions() {
140        let s = WorkflowStatus::Pending;
141        assert!(s.validate_transition(&WorkflowStatus::Running).is_ok());
142    }
143
144    #[test]
145    fn invalid_transitions() {
146        let s = WorkflowStatus::Completed;
147        assert!(s.validate_transition(&WorkflowStatus::Running).is_err());
148    }
149
150    #[test]
151    fn terminal_states() {
152        assert!(WorkflowStatus::Completed.is_terminal());
153        assert!(WorkflowStatus::Failed.is_terminal());
154        assert!(WorkflowStatus::Cancelled.is_terminal());
155        assert!(WorkflowStatus::LimitExceeded.is_terminal());
156        assert!(!WorkflowStatus::Running.is_terminal());
157        assert!(!WorkflowStatus::Paused.is_terminal());
158    }
159
160    #[test]
161    fn limit_exceeded_transition() {
162        let s = WorkflowStatus::Running;
163        assert!(s
164            .validate_transition(&WorkflowStatus::LimitExceeded)
165            .is_ok());
166        let s = WorkflowStatus::LimitExceeded;
167        assert!(s.validate_transition(&WorkflowStatus::Running).is_err());
168    }
169
170    #[test]
171    fn execution_id_display() {
172        let id = ExecutionId::new();
173        assert!(id.to_string().starts_with("exec_"));
174    }
175}