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}