Skip to main content

sayiir_core/
error.rs

1//! Error types for sayiir-core.
2
3/// Generic boxed error type used throughout the crate.
4pub type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
5
6/// Unified error type for workflow operations.
7#[derive(Debug, Clone, thiserror::Error)]
8pub enum WorkflowError {
9    /// A duplicate task ID was found during workflow building.
10    #[error("Duplicate task id: '{0}'")]
11    DuplicateTaskId(String),
12
13    /// A referenced task ID was not found in the registry.
14    #[error("Task '{0}' not found in registry")]
15    TaskNotFound(String),
16
17    /// The task has no implementation (function body).
18    #[error("Task '{0}' has no implementation")]
19    TaskNotImplemented(String),
20
21    /// The workflow definition hash doesn't match.
22    /// This indicates the serialized state was created with a different workflow definition.
23    #[error("Workflow definition mismatch: expected hash '{expected}', found '{found}'")]
24    DefinitionMismatch {
25        /// The expected hash (from current workflow).
26        expected: String,
27        /// The hash found in the serialized state.
28        found: String,
29    },
30
31    /// The workflow was cancelled.
32    #[error("Workflow cancelled{}", reason.as_ref().map(|r| format!(": {r}")).unwrap_or_default())]
33    Cancelled {
34        /// Optional reason for the cancellation.
35        reason: Option<String>,
36        /// Optional identifier of who cancelled the workflow.
37        cancelled_by: Option<String>,
38    },
39
40    /// The workflow was paused.
41    #[error("Workflow paused{}", reason.as_ref().map(|r| format!(": {r}")).unwrap_or_default())]
42    Paused {
43        /// Optional reason for the pause.
44        reason: Option<String>,
45        /// Optional identifier of who paused the workflow.
46        paused_by: Option<String>,
47    },
48
49    /// A fork has no branches and no join task.
50    #[error("Fork has no branches and no join task")]
51    EmptyFork,
52
53    /// A task panicked during execution.
54    #[error("Task panicked: {0}")]
55    TaskPanicked(String),
56
57    /// Cannot resume workflow from saved state.
58    #[error("Cannot resume workflow: {0}")]
59    ResumeError(String),
60
61    /// Deserialization of binary data failed.
62    #[error("Deserialization error: {0}")]
63    Deserialization(String),
64
65    /// A named branch was not found in the outputs.
66    #[error("Branch '{0}' not found")]
67    BranchNotFound(String),
68
69    /// The workflow is waiting for a delay to expire.
70    #[error("Workflow waiting until {wake_at}")]
71    Waiting {
72        /// When the delay expires.
73        wake_at: chrono::DateTime<chrono::Utc>,
74    },
75
76    /// Task exceeded its configured timeout duration.
77    ///
78    /// This marks the entire workflow as `Failed`. The task future is actively
79    /// dropped (cancelled mid-flight) via `tokio::select!` in all runners.
80    #[error("Task '{task_id}' timed out after {timeout:?}")]
81    TaskTimedOut {
82        /// The task that timed out.
83        task_id: String,
84        /// The configured timeout duration.
85        timeout: std::time::Duration,
86    },
87
88    /// The workflow is waiting for an external signal.
89    #[error("Workflow awaiting signal '{signal_name}' at node '{signal_id}'")]
90    AwaitingSignal {
91        /// The signal node ID.
92        signal_id: String,
93        /// The named signal being waited on.
94        signal_name: String,
95        /// Optional timeout deadline.
96        wake_at: Option<chrono::DateTime<chrono::Utc>>,
97    },
98
99    /// A buffered signal was consumed during park — execution should continue.
100    ///
101    /// This is an internal sentinel used by `park_at_signal` when a signal is
102    /// already buffered. The executor should re-enter the loop.
103    #[error("Signal consumed (internal)")]
104    SignalConsumed,
105}
106
107impl WorkflowError {
108    /// Create a new `Cancelled` error with no reason or source.
109    #[must_use]
110    pub fn cancelled() -> Self {
111        Self::Cancelled {
112            reason: None,
113            cancelled_by: None,
114        }
115    }
116
117    /// Create a new `Paused` error with no reason or source.
118    #[must_use]
119    pub fn paused() -> Self {
120        Self::Paused {
121            reason: None,
122            paused_by: None,
123        }
124    }
125}