1use std::backtrace::Backtrace;
2
3#[derive(Debug)]
5pub(crate) enum ErrorKind {
6 Graph(String),
7 Execution(String),
8 Checkpoint(String),
9 Interrupt(String),
10 Interrupted {
11 index: usize,
12 },
13 Subgraph(String),
14 InvalidUpdate(String),
15 EmptyChannel,
16 EmptyInput,
17 TaskNotFound(String),
18 Timeout(String),
19 RecursionLimit {
20 step: usize,
21 limit: usize,
22 },
23 Cancelled,
24 MultipleWriters {
25 field_index: usize,
26 writers: Vec<String>,
27 },
28 TaskPanicked(String),
29 NodeTimeout(NodeTimeoutError),
30 ParentCommand(String),
31}
32
33#[derive(Clone, Debug, PartialEq, Eq)]
38pub enum ErrorCode {
39 Graph,
41 Execution,
43 Checkpoint,
45 Interrupt,
47 Interrupted,
49 Subgraph,
51 InvalidUpdate,
53 EmptyChannel,
55 EmptyInput,
57 TaskNotFound,
59 Timeout,
61 RecursionLimit,
63 GraphRecursionLimit,
65 RecursionLimitExceeded,
67 InvalidConcurrentUpdate,
69 InvalidNodeReturnValue,
71 MultipleSubgraphs,
73 InvalidChatHistory,
75 Cancelled,
77 MultipleWriters,
79 TaskPanicked,
81 NodeFailed,
83 BudgetExceeded,
85 Serialize,
87 Llm,
89 NodeTimeout,
91 ParentCommand,
93}
94
95#[derive(Clone, Debug, thiserror::Error)]
100pub enum InvalidUpdateError {
101 #[error("multiple writers for field '{field}': {conflicting_nodes:?}")]
103 MultipleWriters {
104 field: String,
106 conflicting_nodes: Vec<String>,
108 },
109 #[error("multiple overwrite attempts for field '{field}'")]
111 MultipleOverwrite {
112 field: String,
114 },
115 #[error("invalid value for field '{field}': {reason}")]
117 InvalidValue {
118 field: String,
120 reason: String,
122 },
123}
124
125#[derive(Clone, Debug, thiserror::Error)]
129pub enum NodeTimeoutError {
130 #[error("node '{node}' timed out after {timeout_ms}ms")]
132 Timeout {
133 node: String,
135 timeout_ms: u64,
137 },
138 #[error("node '{node}' run timeout after {timeout}ms")]
140 RunTimeout {
141 node: String,
143 timeout: u64,
145 },
146 #[error("node '{node}' idle timeout after {timeout}ms")]
148 IdleTimeout {
149 node: String,
151 timeout: u64,
153 },
154 #[error("node '{node}' deadline exceeded")]
156 DeadlineExceeded {
157 node: String,
159 },
160}
161
162#[derive(Debug)]
164pub struct JunctureError {
165 kind: ErrorKind,
166 backtrace: Backtrace,
167}
168
169impl JunctureError {
170 pub fn graph(msg: impl Into<String>) -> Self {
172 Self {
173 kind: ErrorKind::Graph(msg.into()),
174 backtrace: Backtrace::capture(),
175 }
176 }
177
178 pub fn execution(msg: impl Into<String>) -> Self {
180 Self {
181 kind: ErrorKind::Execution(msg.into()),
182 backtrace: Backtrace::capture(),
183 }
184 }
185
186 pub fn checkpoint(msg: impl Into<String>) -> Self {
188 Self {
189 kind: ErrorKind::Checkpoint(msg.into()),
190 backtrace: Backtrace::capture(),
191 }
192 }
193
194 pub fn interrupt(msg: impl Into<String>) -> Self {
196 Self {
197 kind: ErrorKind::Interrupt(msg.into()),
198 backtrace: Backtrace::capture(),
199 }
200 }
201
202 #[must_use]
204 pub fn interrupted(index: usize) -> Self {
205 Self {
206 kind: ErrorKind::Interrupted { index },
207 backtrace: Backtrace::capture(),
208 }
209 }
210
211 pub fn subgraph(msg: impl Into<String>) -> Self {
213 Self {
214 kind: ErrorKind::Subgraph(msg.into()),
215 backtrace: Backtrace::capture(),
216 }
217 }
218
219 pub fn invalid_update(msg: impl Into<String>) -> Self {
221 Self {
222 kind: ErrorKind::InvalidUpdate(msg.into()),
223 backtrace: Backtrace::capture(),
224 }
225 }
226
227 #[must_use]
229 pub fn empty_channel() -> Self {
230 Self {
231 kind: ErrorKind::EmptyChannel,
232 backtrace: Backtrace::capture(),
233 }
234 }
235
236 #[must_use]
238 pub fn empty_input() -> Self {
239 Self {
240 kind: ErrorKind::EmptyInput,
241 backtrace: Backtrace::capture(),
242 }
243 }
244
245 pub fn task_not_found(id: impl Into<String>) -> Self {
247 Self {
248 kind: ErrorKind::TaskNotFound(id.into()),
249 backtrace: Backtrace::capture(),
250 }
251 }
252
253 pub fn timeout(msg: impl Into<String>) -> Self {
255 Self {
256 kind: ErrorKind::Timeout(msg.into()),
257 backtrace: Backtrace::capture(),
258 }
259 }
260
261 #[must_use]
263 pub fn recursion_limit(step: usize, limit: usize) -> Self {
264 Self {
265 kind: ErrorKind::RecursionLimit { step, limit },
266 backtrace: Backtrace::capture(),
267 }
268 }
269
270 #[must_use = "backtrace should be used for debugging"]
272 pub const fn backtrace(&self) -> &Backtrace {
273 &self.backtrace
274 }
275
276 #[must_use]
278 pub const fn code(&self) -> ErrorCode {
279 match &self.kind {
280 ErrorKind::Graph(_) => ErrorCode::Graph,
281 ErrorKind::Execution(_) => ErrorCode::Execution,
282 ErrorKind::Checkpoint(_) => ErrorCode::Checkpoint,
283 ErrorKind::Interrupt(_) => ErrorCode::Interrupt,
284 ErrorKind::Interrupted { .. } => ErrorCode::Interrupted,
285 ErrorKind::Subgraph(_) => ErrorCode::Subgraph,
286 ErrorKind::InvalidUpdate(_) => ErrorCode::InvalidUpdate,
287 ErrorKind::EmptyChannel => ErrorCode::EmptyChannel,
288 ErrorKind::EmptyInput => ErrorCode::EmptyInput,
289 ErrorKind::TaskNotFound(_) => ErrorCode::TaskNotFound,
290 ErrorKind::Timeout(_) => ErrorCode::Timeout,
291 ErrorKind::RecursionLimit { .. } => ErrorCode::RecursionLimit,
292 ErrorKind::Cancelled => ErrorCode::Cancelled,
293 ErrorKind::MultipleWriters { .. } => ErrorCode::MultipleWriters,
294 ErrorKind::TaskPanicked(_) => ErrorCode::TaskPanicked,
295 ErrorKind::NodeTimeout(_) => ErrorCode::NodeTimeout,
296 ErrorKind::ParentCommand(_) => ErrorCode::ParentCommand,
297 }
298 }
299
300 #[must_use]
301 pub const fn is_graph(&self) -> bool {
302 matches!(self.kind, ErrorKind::Graph(_))
303 }
304
305 #[must_use]
306 pub const fn is_execution(&self) -> bool {
307 matches!(self.kind, ErrorKind::Execution(_))
308 }
309
310 #[must_use]
311 pub const fn is_checkpoint(&self) -> bool {
312 matches!(self.kind, ErrorKind::Checkpoint(_))
313 }
314
315 #[must_use]
316 pub const fn is_interrupt(&self) -> bool {
317 matches!(
318 self.kind,
319 ErrorKind::Interrupt(_) | ErrorKind::Interrupted { .. }
320 )
321 }
322
323 #[must_use]
324 pub const fn is_subgraph(&self) -> bool {
325 matches!(self.kind, ErrorKind::Subgraph(_))
326 }
327
328 #[must_use]
329 pub const fn is_invalid_update(&self) -> bool {
330 matches!(self.kind, ErrorKind::InvalidUpdate(_))
331 }
332
333 #[must_use]
334 pub const fn is_empty_channel(&self) -> bool {
335 matches!(self.kind, ErrorKind::EmptyChannel)
336 }
337
338 #[must_use]
339 pub const fn is_empty_input(&self) -> bool {
340 matches!(self.kind, ErrorKind::EmptyInput)
341 }
342
343 #[must_use]
344 pub const fn is_task_not_found(&self) -> bool {
345 matches!(self.kind, ErrorKind::TaskNotFound(_))
346 }
347
348 #[must_use]
349 pub const fn is_timeout(&self) -> bool {
350 matches!(self.kind, ErrorKind::Timeout(_))
351 }
352
353 #[must_use]
354 pub const fn is_recursion_limit(&self) -> bool {
355 matches!(self.kind, ErrorKind::RecursionLimit { .. })
356 }
357
358 #[must_use]
360 pub fn cancelled() -> Self {
361 Self {
362 kind: ErrorKind::Cancelled,
363 backtrace: Backtrace::capture(),
364 }
365 }
366
367 #[must_use]
369 pub const fn is_cancelled(&self) -> bool {
370 matches!(self.kind, ErrorKind::Cancelled)
371 }
372
373 #[must_use]
375 pub fn multiple_writers(field_index: usize, writers: Vec<String>) -> Self {
376 Self {
377 kind: ErrorKind::MultipleWriters {
378 field_index,
379 writers,
380 },
381 backtrace: Backtrace::capture(),
382 }
383 }
384
385 #[must_use]
387 pub const fn is_multiple_writers(&self) -> bool {
388 matches!(self.kind, ErrorKind::MultipleWriters { .. })
389 }
390
391 #[must_use]
393 pub fn task_panicked(msg: impl Into<String>) -> Self {
394 Self {
395 kind: ErrorKind::TaskPanicked(msg.into()),
396 backtrace: Backtrace::capture(),
397 }
398 }
399
400 #[must_use]
402 pub const fn is_task_panicked(&self) -> bool {
403 matches!(self.kind, ErrorKind::TaskPanicked(_))
404 }
405
406 #[must_use]
408 pub fn node_timeout(err: NodeTimeoutError) -> Self {
409 Self {
410 kind: ErrorKind::NodeTimeout(err),
411 backtrace: Backtrace::capture(),
412 }
413 }
414
415 #[must_use]
417 pub const fn is_node_timeout(&self) -> bool {
418 matches!(self.kind, ErrorKind::NodeTimeout(_))
419 }
420
421 pub fn parent_command(target: impl Into<String>) -> Self {
432 Self {
433 kind: ErrorKind::ParentCommand(target.into()),
434 backtrace: Backtrace::capture(),
435 }
436 }
437
438 #[must_use]
443 pub const fn is_parent_command(&self) -> bool {
444 matches!(self.kind, ErrorKind::ParentCommand(_))
445 }
446
447 #[must_use]
453 pub fn parent_command_target(&self) -> Option<&str> {
454 match &self.kind {
455 ErrorKind::ParentCommand(target) => Some(target),
456 _ => None,
457 }
458 }
459
460 #[must_use]
465 pub const fn error_code(&self) -> ErrorCode {
466 self.code()
467 }
468
469 #[must_use]
474 pub const fn is_graph_recursion_limit(&self) -> bool {
475 self.is_recursion_limit()
476 }
477
478 #[must_use]
483 pub const fn is_invalid_concurrent_update(&self) -> bool {
484 self.is_multiple_writers()
485 }
486
487 #[must_use]
491 pub const fn is_node_failed(&self) -> bool {
492 self.is_execution()
493 }
494
495 #[must_use]
499 pub const fn is_budget_exceeded(&self) -> bool {
500 self.is_timeout()
501 }
502
503 #[must_use]
507 pub const fn is_serialize(&self) -> bool {
508 self.is_checkpoint()
509 }
510}
511
512impl std::fmt::Display for JunctureError {
513 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
514 match &self.kind {
515 ErrorKind::Graph(msg) => write!(f, "Graph error: {msg}"),
516 ErrorKind::Execution(msg) => write!(f, "Execution error: {msg}"),
517 ErrorKind::Checkpoint(msg) => write!(f, "Checkpoint error: {msg}"),
518 ErrorKind::Interrupt(msg) => write!(f, "Interrupt: {msg}"),
519 ErrorKind::Interrupted { index } => write!(f, "Interrupted at index {index}"),
520 ErrorKind::Subgraph(msg) => write!(f, "Subgraph error: {msg}"),
521 ErrorKind::InvalidUpdate(msg) => write!(f, "Invalid update: {msg}"),
522 ErrorKind::EmptyChannel => write!(f, "Empty channel"),
523 ErrorKind::EmptyInput => write!(f, "Empty input"),
524 ErrorKind::TaskNotFound(id) => write!(f, "Task not found: {id}"),
525 ErrorKind::Timeout(msg) => write!(f, "Timeout: {msg}"),
526 ErrorKind::RecursionLimit { step, limit } => {
527 write!(f, "Recursion limit exceeded: step {step} > limit {limit}")
528 }
529 ErrorKind::Cancelled => write!(f, "Execution cancelled"),
530 ErrorKind::MultipleWriters {
531 field_index,
532 writers,
533 } => {
534 write!(
535 f,
536 "Multiple writers for replace channel: field {field_index} written by {writers:?}"
537 )
538 }
539 ErrorKind::TaskPanicked(msg) => write!(f, "Task panicked: {msg}"),
540 ErrorKind::NodeTimeout(err) => write!(f, "Node timeout: {err}"),
541 ErrorKind::ParentCommand(target) => {
542 write!(f, "Parent command: route to '{target}'")
543 }
544 }
545 }
546}
547
548impl std::error::Error for JunctureError {}
549
550#[cfg(test)]
551mod tests {
552 use super::*;
553
554 #[test]
555 fn error_code_matches_error_kind() {
556 assert_eq!(JunctureError::graph("x").code(), ErrorCode::Graph);
557 assert_eq!(JunctureError::execution("x").code(), ErrorCode::Execution);
558 assert_eq!(JunctureError::checkpoint("x").code(), ErrorCode::Checkpoint);
559 assert_eq!(JunctureError::interrupt("x").code(), ErrorCode::Interrupt);
560 assert_eq!(JunctureError::interrupted(0).code(), ErrorCode::Interrupted);
561 assert_eq!(JunctureError::subgraph("x").code(), ErrorCode::Subgraph);
562 assert_eq!(
563 JunctureError::invalid_update("x").code(),
564 ErrorCode::InvalidUpdate
565 );
566 assert_eq!(
567 JunctureError::empty_channel().code(),
568 ErrorCode::EmptyChannel
569 );
570 assert_eq!(JunctureError::empty_input().code(), ErrorCode::EmptyInput);
571 assert_eq!(
572 JunctureError::task_not_found("x").code(),
573 ErrorCode::TaskNotFound
574 );
575 assert_eq!(JunctureError::timeout("x").code(), ErrorCode::Timeout);
576 assert_eq!(
577 JunctureError::recursion_limit(1, 10).code(),
578 ErrorCode::RecursionLimit
579 );
580 assert_eq!(JunctureError::cancelled().code(), ErrorCode::Cancelled);
581 assert_eq!(
582 JunctureError::multiple_writers(0, vec!["a".to_string()]).code(),
583 ErrorCode::MultipleWriters
584 );
585 assert_eq!(
586 JunctureError::task_panicked("boom").code(),
587 ErrorCode::TaskPanicked
588 );
589 assert_eq!(
590 JunctureError::node_timeout(NodeTimeoutError::RunTimeout {
591 node: "n".to_string(),
592 timeout: 1000,
593 })
594 .code(),
595 ErrorCode::NodeTimeout
596 );
597 assert_eq!(
598 JunctureError::parent_command("publish").code(),
599 ErrorCode::ParentCommand
600 );
601 }
602
603 #[test]
604 fn node_timeout_error_construct_and_check() {
605 let err = JunctureError::node_timeout(NodeTimeoutError::RunTimeout {
606 node: "my_node".to_string(),
607 timeout: 5000,
608 });
609 assert!(err.is_node_timeout());
610 assert!(!err.is_execution());
611 assert_eq!(err.code(), ErrorCode::NodeTimeout);
612 }
613
614 #[test]
615 fn node_timeout_juncture_error_display() {
616 let err = JunctureError::node_timeout(NodeTimeoutError::RunTimeout {
617 node: "my_node".to_string(),
618 timeout: 5000,
619 });
620 let msg = err.to_string();
621 assert!(
622 msg.contains("my_node"),
623 "display should contain node name: {msg}"
624 );
625 }
626
627 #[test]
628 fn invalid_update_error_display() {
629 assert_eq!(
630 InvalidUpdateError::MultipleWriters {
631 field: "my_field".to_string(),
632 conflicting_nodes: vec!["node_a".to_string(), "node_b".to_string()],
633 }
634 .to_string(),
635 "multiple writers for field 'my_field': [\"node_a\", \"node_b\"]"
636 );
637 assert_eq!(
638 InvalidUpdateError::MultipleOverwrite {
639 field: "my_field".to_string(),
640 }
641 .to_string(),
642 "multiple overwrite attempts for field 'my_field'"
643 );
644 assert_eq!(
645 InvalidUpdateError::InvalidValue {
646 field: "my_field".to_string(),
647 reason: "bad".to_string(),
648 }
649 .to_string(),
650 "invalid value for field 'my_field': bad"
651 );
652 }
653
654 #[test]
655 fn node_timeout_error_display() {
656 assert_eq!(
657 NodeTimeoutError::Timeout {
658 node: "my_node".to_string(),
659 timeout_ms: 5000
660 }
661 .to_string(),
662 "node 'my_node' timed out after 5000ms"
663 );
664 assert_eq!(
665 NodeTimeoutError::DeadlineExceeded {
666 node: "my_node".to_string()
667 }
668 .to_string(),
669 "node 'my_node' deadline exceeded"
670 );
671 }
672
673 #[test]
674 fn error_code_equality() {
675 assert_eq!(ErrorCode::Graph, ErrorCode::Graph);
676 assert_ne!(ErrorCode::Graph, ErrorCode::Execution);
677 }
678
679 #[test]
680 fn new_error_variants_display() {
681 assert_eq!(
682 JunctureError::cancelled().to_string(),
683 "Execution cancelled"
684 );
685 assert!(
686 JunctureError::multiple_writers(2, vec!["a".to_string(), "b".to_string()])
687 .to_string()
688 .contains("field 2")
689 );
690 assert_eq!(
691 JunctureError::task_panicked("overflow").to_string(),
692 "Task panicked: overflow"
693 );
694 }
695
696 #[test]
697 fn new_error_is_methods() {
698 assert!(JunctureError::cancelled().is_cancelled());
699 assert!(!JunctureError::cancelled().is_execution());
700 assert!(JunctureError::multiple_writers(0, vec![]).is_multiple_writers());
701 assert!(JunctureError::task_panicked("x").is_task_panicked());
702 }
703
704 #[test]
705 fn parent_command_construct_and_check() {
706 let err = JunctureError::parent_command("publish");
707 assert!(err.is_parent_command());
708 assert!(!err.is_execution());
709 assert!(!err.is_interrupt());
710 assert_eq!(err.code(), ErrorCode::ParentCommand);
711 assert_eq!(
712 err.parent_command_target(),
713 Some("publish"),
714 "target should be the provided node name"
715 );
716 }
717
718 #[test]
719 fn parent_command_target_returns_none_for_other_errors() {
720 let err = JunctureError::execution("something");
721 assert_eq!(err.parent_command_target(), None);
722 }
723
724 #[test]
725 fn parent_command_display() {
726 let err = JunctureError::parent_command("review");
727 let msg = err.to_string();
728 assert!(
729 msg.contains("review"),
730 "display should contain target node name: {msg}"
731 );
732 assert!(
733 msg.contains("Parent command"),
734 "display should identify as parent command: {msg}"
735 );
736 }
737
738 #[test]
739 fn parent_command_error_code_equality() {
740 assert_eq!(ErrorCode::ParentCommand, ErrorCode::ParentCommand);
741 assert_ne!(ErrorCode::ParentCommand, ErrorCode::Execution);
742 }
743}
744
745