Skip to main content

juncture_core/
error.rs

1use std::backtrace::Backtrace;
2
3/// Core error kind for Juncture
4#[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/// Error code categorizing the error type
34///
35/// Mirrors `ErrorKind` but is public, enabling callers to match on
36/// error categories without accessing private implementation details.
37#[derive(Clone, Debug, PartialEq, Eq)]
38pub enum ErrorCode {
39    /// Graph construction error
40    Graph,
41    /// Graph execution error
42    Execution,
43    /// Checkpoint persistence error
44    Checkpoint,
45    /// Human-in-the-loop interrupt signal
46    Interrupt,
47    /// Human-in-the-loop interrupted execution
48    Interrupted,
49    /// Subgraph error
50    Subgraph,
51    /// Invalid state update
52    InvalidUpdate,
53    /// Channel value is empty
54    EmptyChannel,
55    /// Graph input is empty
56    EmptyInput,
57    /// Task not found
58    TaskNotFound,
59    /// Operation timed out
60    Timeout,
61    /// Recursion limit exceeded
62    RecursionLimit,
63    /// Graph recursion limit exceeded
64    GraphRecursionLimit,
65    /// Recursion limit exceeded (alternative name)
66    RecursionLimitExceeded,
67    /// Invalid concurrent update detected
68    InvalidConcurrentUpdate,
69    /// Invalid node return value
70    InvalidNodeReturnValue,
71    /// Multiple subgraphs detected
72    MultipleSubgraphs,
73    /// Invalid chat history
74    InvalidChatHistory,
75    /// Execution was cancelled
76    Cancelled,
77    /// Multiple writers on a replace channel
78    MultipleWriters,
79    /// Task panicked during execution
80    TaskPanicked,
81    /// Node execution failed
82    NodeFailed,
83    /// Budget exceeded
84    BudgetExceeded,
85    /// Serialization error
86    Serialize,
87    /// LLM provider error
88    Llm,
89    /// Node timeout error
90    NodeTimeout,
91    /// Subgraph-to-parent routing command
92    ParentCommand,
93}
94
95/// Invalid update error variants
96///
97/// Describes specific ways a state update can be invalid, such as
98/// multiple writers on a replace channel or invalid values.
99#[derive(Clone, Debug, thiserror::Error)]
100pub enum InvalidUpdateError {
101    /// Multiple writers attempted to write to a replace channel
102    #[error("multiple writers for field '{field}': {conflicting_nodes:?}")]
103    MultipleWriters {
104        /// The field name that had multiple writers
105        field: String,
106        /// Names of the conflicting nodes
107        conflicting_nodes: Vec<String>,
108    },
109    /// Multiple overwrite attempts on the same field
110    #[error("multiple overwrite attempts for field '{field}'")]
111    MultipleOverwrite {
112        /// The field name that was overwritten
113        field: String,
114    },
115    /// An invalid value was provided for a field
116    #[error("invalid value for field '{field}': {reason}")]
117    InvalidValue {
118        /// The field name with the invalid value
119        field: String,
120        /// Why the value is invalid
121        reason: String,
122    },
123}
124
125/// Node timeout error variants
126///
127/// Describes timeout conditions during node execution.
128#[derive(Clone, Debug, thiserror::Error)]
129pub enum NodeTimeoutError {
130    /// Node execution exceeded the specified timeout duration
131    #[error("node '{node}' timed out after {timeout_ms}ms")]
132    Timeout {
133        /// Name of the node that timed out
134        node: String,
135        /// Timeout duration in milliseconds
136        timeout_ms: u64,
137    },
138    /// Node execution exceeded its run timeout
139    #[error("node '{node}' run timeout after {timeout}ms")]
140    RunTimeout {
141        /// Name of the node that timed out
142        node: String,
143        /// Timeout duration in milliseconds
144        timeout: u64,
145    },
146    /// Node execution exceeded its idle timeout
147    #[error("node '{node}' idle timeout after {timeout}ms")]
148    IdleTimeout {
149        /// Name of the node that timed out
150        node: String,
151        /// Timeout duration in milliseconds
152        timeout: u64,
153    },
154    /// Node execution exceeded its deadline
155    #[error("node '{node}' deadline exceeded")]
156    DeadlineExceeded {
157        /// Name of the node that exceeded its deadline
158        node: String,
159    },
160}
161
162/// Juncture error with backtrace
163#[derive(Debug)]
164pub struct JunctureError {
165    kind: ErrorKind,
166    backtrace: Backtrace,
167}
168
169impl JunctureError {
170    /// Graph construction error
171    pub fn graph(msg: impl Into<String>) -> Self {
172        Self {
173            kind: ErrorKind::Graph(msg.into()),
174            backtrace: Backtrace::capture(),
175        }
176    }
177
178    /// Graph execution error
179    pub fn execution(msg: impl Into<String>) -> Self {
180        Self {
181            kind: ErrorKind::Execution(msg.into()),
182            backtrace: Backtrace::capture(),
183        }
184    }
185
186    /// Checkpoint persistence error
187    pub fn checkpoint(msg: impl Into<String>) -> Self {
188        Self {
189            kind: ErrorKind::Checkpoint(msg.into()),
190            backtrace: Backtrace::capture(),
191        }
192    }
193
194    /// Human-in-the-loop interrupt
195    pub fn interrupt(msg: impl Into<String>) -> Self {
196        Self {
197            kind: ErrorKind::Interrupt(msg.into()),
198            backtrace: Backtrace::capture(),
199        }
200    }
201
202    /// Human-in-the-loop interrupted execution
203    #[must_use]
204    pub fn interrupted(index: usize) -> Self {
205        Self {
206            kind: ErrorKind::Interrupted { index },
207            backtrace: Backtrace::capture(),
208        }
209    }
210
211    /// Subgraph error
212    pub fn subgraph(msg: impl Into<String>) -> Self {
213        Self {
214            kind: ErrorKind::Subgraph(msg.into()),
215            backtrace: Backtrace::capture(),
216        }
217    }
218
219    /// Invalid state update
220    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    /// Channel value is empty
228    #[must_use]
229    pub fn empty_channel() -> Self {
230        Self {
231            kind: ErrorKind::EmptyChannel,
232            backtrace: Backtrace::capture(),
233        }
234    }
235
236    /// Graph input is empty
237    #[must_use]
238    pub fn empty_input() -> Self {
239        Self {
240            kind: ErrorKind::EmptyInput,
241            backtrace: Backtrace::capture(),
242        }
243    }
244
245    /// Task not found
246    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    /// Operation timed out
254    pub fn timeout(msg: impl Into<String>) -> Self {
255        Self {
256            kind: ErrorKind::Timeout(msg.into()),
257            backtrace: Backtrace::capture(),
258        }
259    }
260
261    /// Recursion limit exceeded
262    #[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    /// Access the backtrace for this error
271    #[must_use = "backtrace should be used for debugging"]
272    pub const fn backtrace(&self) -> &Backtrace {
273        &self.backtrace
274    }
275
276    /// Get the error code categorizing this error
277    #[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    /// Execution was cancelled
359    #[must_use]
360    pub fn cancelled() -> Self {
361        Self {
362            kind: ErrorKind::Cancelled,
363            backtrace: Backtrace::capture(),
364        }
365    }
366
367    /// Check if this is a cancellation error
368    #[must_use]
369    pub const fn is_cancelled(&self) -> bool {
370        matches!(self.kind, ErrorKind::Cancelled)
371    }
372
373    /// Multiple writers on a replace channel
374    #[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    /// Check if this is a multiple writers error
386    #[must_use]
387    pub const fn is_multiple_writers(&self) -> bool {
388        matches!(self.kind, ErrorKind::MultipleWriters { .. })
389    }
390
391    /// Task panicked during execution
392    #[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    /// Check if this is a task panic error
401    #[must_use]
402    pub const fn is_task_panicked(&self) -> bool {
403        matches!(self.kind, ErrorKind::TaskPanicked(_))
404    }
405
406    /// Node execution exceeded its timeout
407    #[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    /// Check if this is a node timeout error
416    #[must_use]
417    pub const fn is_node_timeout(&self) -> bool {
418        matches!(self.kind, ErrorKind::NodeTimeout(_))
419    }
420
421    /// Subgraph-to-parent routing command
422    ///
423    /// Used by nodes inside a subgraph to request routing to a specific node
424    /// in the parent graph. The subgraph node returns this error as an
425    /// exception mechanism, which the `SubgraphNode` wrapper catches and
426    /// converts to a `Command::goto(target)`.
427    ///
428    /// # Arguments
429    ///
430    /// * `target` - Name of the target node in the parent graph
431    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    /// Check if this is a parent command routing signal
439    ///
440    /// Returns `true` when a subgraph node has requested routing to
441    /// a node in the parent graph.
442    #[must_use]
443    pub const fn is_parent_command(&self) -> bool {
444        matches!(self.kind, ErrorKind::ParentCommand(_))
445    }
446
447    /// Get the target node name for a parent command
448    ///
449    /// Returns `Some(target)` when this is a parent command error,
450    /// containing the name of the target node in the parent graph.
451    /// Returns `None` for all other error types.
452    #[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    /// Get the error code categorizing this error (alias for `code()`)
461    ///
462    /// This method is an alias for [`code()`](Self::code) and exists for
463    /// compatibility with external code that expects this name.
464    #[must_use]
465    pub const fn error_code(&self) -> ErrorCode {
466        self.code()
467    }
468
469    /// Check if this is a graph recursion limit error (alias for `is_recursion_limit()`)
470    ///
471    /// This method is an alias for [`is_recursion_limit()`](Self::is_recursion_limit)
472    /// and exists for compatibility with external code that expects this name.
473    #[must_use]
474    pub const fn is_graph_recursion_limit(&self) -> bool {
475        self.is_recursion_limit()
476    }
477
478    /// Check if this is an invalid concurrent update error (alias for `is_multiple_writers()`)
479    ///
480    /// This method is an alias for [`is_multiple_writers()`](Self::is_multiple_writers)
481    /// and exists for compatibility with external code that expects this name.
482    #[must_use]
483    pub const fn is_invalid_concurrent_update(&self) -> bool {
484        self.is_multiple_writers()
485    }
486
487    /// Check if this is a node execution failed error
488    ///
489    /// Returns true if this error represents a node execution failure.
490    #[must_use]
491    pub const fn is_node_failed(&self) -> bool {
492        self.is_execution()
493    }
494
495    /// Check if this is a budget exceeded error
496    ///
497    /// Returns true if this error represents a budget/tokens limit exceeded.
498    #[must_use]
499    pub const fn is_budget_exceeded(&self) -> bool {
500        self.is_timeout()
501    }
502
503    /// Check if this is a serialization error
504    ///
505    /// Returns true if this error represents a serialization/deserialization failure.
506    #[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// Rust guideline compliant 2026-05-21