Skip to main content

switchyard_libsy/
error.rs

1// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Typed failures surfaced by libsy's orchestration APIs.
5
6use std::{error::Error as StdError, time::Duration};
7
8use switchyard_protocol::LlmClientError;
9use thiserror::Error;
10
11/// Result type returned by libsy APIs.
12pub type Result<T> = std::result::Result<T, LibsyError>;
13
14/// Failures surfaced while selecting a route, driving an algorithm, or serving a model call.
15#[derive(Debug, Error)]
16pub enum LibsyError {
17    /// A named target was not present in the configured target set.
18    #[error("target {target:?} was not found")]
19    TargetNotFound {
20        /// Missing semantic target name.
21        target: String,
22    },
23
24    /// Routing was attempted without any configured targets.
25    #[error("no routing targets are configured")]
26    NoTargets,
27
28    /// An algorithm could not complete for an algorithm-specific reason.
29    #[error("{message}")]
30    AlgorithmError {
31        /// Description of the algorithm failure.
32        message: String,
33    },
34
35    /// The type-erased offload driver could not complete an operation.
36    #[error(transparent)]
37    Driver(#[from] DriverError),
38
39    /// The spawned algorithm task failed before returning normally.
40    #[error("algorithm task failed: {source}")]
41    AlgorithmTask {
42        /// Tokio task failure, including panic and unexpected cancellation details.
43        #[from]
44        source: tokio::task::JoinError,
45    },
46
47    /// An algorithm's step stream ended without a terminal response.
48    #[error("algorithm run ended without a final response")]
49    MissingFinalResponse,
50
51    /// A target's protocol client failed while serving a routed request.
52    #[error("client call to target {target:?} failed: {source}")]
53    ClientCall {
54        /// Target whose client failed.
55        target: String,
56        /// Typed error supplied by the protocol-owned client trait.
57        #[source]
58        source: LlmClientError,
59    },
60
61    /// Every target overflowed its context window.
62    #[error("every target exceeded its context window")]
63    AllTargetsExcluded,
64
65    /// A user extension or other foreign operation failed.
66    #[error("{operation} failed: {source}")]
67    External {
68        /// Short description of the operation that failed.
69        operation: &'static str,
70        /// Original failure.
71        #[source]
72        source: Box<dyn StdError + Send + Sync>,
73    },
74}
75
76impl LibsyError {
77    /// Wrap an error returned by the protocol-owned client trait.
78    pub fn client_call(target: impl Into<String>, source: LlmClientError) -> Self {
79        Self::ClientCall {
80            target: target.into(),
81            source,
82        }
83    }
84
85    /// Preserve a concrete foreign error with a description of the failed operation.
86    pub fn external(
87        operation: &'static str,
88        source: impl StdError + Send + Sync + 'static,
89    ) -> Self {
90        Self::External {
91            operation,
92            source: Box::new(source),
93        }
94    }
95}
96
97/// Failures in the type-erased promise-over-stream driver.
98#[derive(Debug, Error, PartialEq, Eq)]
99pub enum DriverError {
100    /// A producer operation was attempted before taking the consumer stream.
101    #[error("driver stream must be taken before calling producer methods")]
102    NotStarted,
103
104    /// The consumer side of the step channel was dropped.
105    #[error("driver stream is closed")]
106    StreamClosed,
107
108    /// The single-consumer stream had already been taken.
109    #[error("driver stream was already taken")]
110    StreamAlreadyTaken,
111
112    /// One side of a response promise was dropped before delivery.
113    #[error("driver response promise was dropped")]
114    ResponseDropped,
115
116    /// A consumer did not fulfill a request before its deadline.
117    #[error("driver response timed out after {timeout:?}")]
118    ResponseTimedOut {
119        /// Maximum time allowed for request fulfillment.
120        timeout: Duration,
121    },
122
123    /// A type-erased payload did not contain the expected concrete type.
124    #[error("driver payload type mismatch: expected {expected}")]
125    TypeMismatch {
126        /// Human-readable expected payload type or role.
127        expected: &'static str,
128    },
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn client_call_preserves_target_and_source() {
137        let error = LibsyError::client_call(
138            "strong",
139            LlmClientError::General("upstream down".to_string()),
140        );
141        match &error {
142            LibsyError::ClientCall { target, source } => {
143                assert_eq!(target, "strong");
144                assert_eq!(source.to_string(), "upstream down");
145            }
146            other => panic!("expected ClientCall, got {other:?}"),
147        }
148        assert_eq!(
149            StdError::source(&error).map(ToString::to_string),
150            Some("upstream down".to_string())
151        );
152    }
153
154    #[test]
155    fn external_preserves_operation_and_source() {
156        let error = LibsyError::external(
157            "loading extension",
158            std::io::Error::other("bad configuration"),
159        );
160        assert!(matches!(
161            &error,
162            LibsyError::External { operation, .. } if *operation == "loading extension"
163        ));
164        assert_eq!(
165            StdError::source(&error).map(ToString::to_string),
166            Some("bad configuration".to_string())
167        );
168    }
169}