1use std::fmt;
9
10#[derive(Debug, Clone)]
16pub enum BuildError {
17 DuplicateNode { id: String },
19 MissingNode { from: String, to: String },
21 MissingEntryPoint,
23 MissingExitPoint,
25 InvalidEdgeDefinition { from: String, to: String, reason: String },
27}
28
29impl fmt::Display for BuildError {
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31 match self {
32 Self::DuplicateNode { id } => write!(f, "duplicate node id: '{}'", id),
33 Self::MissingNode { from, to } => {
34 write!(f, "edge references non-existent node: '{}' (in {}→{})", to, from, to)
35 }
36 Self::MissingEntryPoint => write!(f, "entry point not set"),
37 Self::MissingExitPoint => write!(f, "exit point not set"),
38 Self::InvalidEdgeDefinition { from, to, reason } => {
39 write!(f, "invalid edge {}→{}: {}", from, to, reason)
40 }
41 }
42 }
43}
44
45impl std::error::Error for BuildError {}
46
47#[derive(Debug)]
51pub enum GraphError {
52 Terminal(TerminalError),
54 Recoverable(RecoverableError),
56 Observed(ObservedError),
58}
59
60#[derive(Debug)]
62pub enum TerminalError {
63 InvalidGraph(String),
65 NodeNotFound(String),
67 MissingEdge { from: String, to: String },
69 NodeExecutionFailed { node: String, source: Box<dyn std::error::Error + Send + Sync> },
71 StepsExceeded { limit: usize },
73 LoopLimitExceeded { limit: usize },
75 EdgePolicyExceeded { edge: String, limit: usize },
77 BarrierTimeout { node: String, timeout: std::time::Duration },
79 BarrierCancelled { node: String },
81 Unrouted {
83 node: String,
85 attempted_conditions: Vec<ConditionEval>,
87 },
88 StateError(String),
90}
91
92#[derive(Debug)]
94pub enum RecoverableError {
95 Retryable {
97 node: String,
98 attempt: usize,
99 max_attempts: usize,
100 reason: String,
101 },
102 FallbackTriggered {
104 from: String,
105 to: String,
106 reason: String,
107 },
108}
109
110#[derive(Debug, Clone)]
112pub enum ObservedError {
113 Warning { node: String, message: String },
115 Degraded { node: String, message: String },
117 PartialFailure { node: String, succeeded: usize, failed: usize, message: String },
119}
120
121#[derive(Debug, Clone)]
123pub struct ConditionEval {
124 pub edge: String,
126 pub condition: Option<String>,
128 pub matched: bool,
130}
131
132impl fmt::Display for GraphError {
135 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136 match self {
137 Self::Terminal(e) => write!(f, "[terminal] {}", e),
138 Self::Recoverable(e) => write!(f, "[recoverable] {}", e),
139 Self::Observed(e) => write!(f, "[observed] {}", e),
140 }
141 }
142}
143
144impl fmt::Display for TerminalError {
145 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146 match self {
147 Self::InvalidGraph(msg) => write!(f, "invalid graph: {msg}"),
148 Self::NodeNotFound(name) => write!(f, "node not found: {name}"),
149 Self::MissingEdge { from, to } => {
150 write!(f, "goto '{}' from '{}' failed: no edge {}→{} exists", to, from, from, to)
151 }
152 Self::NodeExecutionFailed { node, source } => {
153 write!(f, "node '{node}' execution failed: {source}")
154 }
155 Self::StepsExceeded { limit } => {
156 write!(f, "step limit {limit} exceeded (potential infinite loop)")
157 }
158 Self::LoopLimitExceeded { limit } => write!(f, "loop limit exceeded: {limit}"),
159 Self::EdgePolicyExceeded { edge, limit } => {
160 write!(f, "edge '{edge}' policy limit {limit} exceeded (cycle protection)")
161 }
162 Self::BarrierTimeout { node, timeout } => {
163 write!(f, "barrier '{node}' timed out after {timeout:?}")
164 }
165 Self::BarrierCancelled { node } => {
166 write!(f, "barrier '{node}' cancelled: consumer dropped the signal channel")
167 }
168 Self::Unrouted { node, attempted_conditions } => {
169 write!(f, "node '{}' has no matching outgoing edge", node)?;
170 if !attempted_conditions.is_empty() {
171 write!(f, ". evaluated: [")?;
172 for (i, ce) in attempted_conditions.iter().enumerate() {
173 if i > 0 { write!(f, ", ")?; }
174 write!(f, "{}={}", ce.edge, ce.matched)?;
175 }
176 write!(f, "]")?;
177 }
178 Ok(())
179 }
180 Self::StateError(msg) => write!(f, "state error: {msg}"),
181 }
182 }
183}
184
185impl fmt::Display for RecoverableError {
186 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187 match self {
188 Self::Retryable { node, attempt, max_attempts, reason } => {
189 write!(f, "node '{node}' retry {}/{}, reason: {}", attempt, max_attempts, reason)
190 }
191 Self::FallbackTriggered { from, to, reason } => {
192 write!(f, "fallback edge {}→{} triggered: {}", from, to, reason)
193 }
194 }
195 }
196}
197
198impl fmt::Display for ObservedError {
199 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200 match self {
201 Self::Warning { node, message } => write!(f, "node '{}': {}", node, message),
202 Self::Degraded { node, message } => write!(f, "node '{}' degraded: {}", node, message),
203 Self::PartialFailure { node, succeeded, failed, message } => {
204 write!(f, "node '{}' partial: {}/{} ok, {}", node, succeeded, succeeded + failed, message)
205 }
206 }
207 }
208}
209
210impl std::error::Error for GraphError {
211 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
212 match self {
213 Self::Terminal(TerminalError::NodeExecutionFailed { source, .. }) => {
214 Some(source.as_ref())
215 }
216 _ => None,
217 }
218 }
219}
220
221impl std::error::Error for TerminalError {
222 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
223 match self {
224 Self::NodeExecutionFailed { source, .. } => Some(source.as_ref()),
225 _ => None,
226 }
227 }
228}
229
230impl std::error::Error for RecoverableError {}
231impl std::error::Error for ObservedError {}