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;
7
8use switchyard_protocol::{LlmClientError, ModelId};
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    /// Request content violates the algorithm's input constraints.
18    #[error("{message}")]
19    InvalidRequest {
20        /// Description of the invalid input.
21        message: String,
22    },
23
24    /// A named target was not present in the configured target set.
25    #[error("target {target:?} was not found")]
26    TargetNotFound {
27        /// Missing target model id.
28        target: ModelId,
29    },
30
31    /// Routing was attempted without any configured targets.
32    #[error("no routing targets are configured")]
33    NoTargets,
34
35    /// An algorithm could not complete for an algorithm-specific reason.
36    #[error("{message}")]
37    AlgorithmError {
38        /// Description of the algorithm failure.
39        message: String,
40    },
41
42    /// The step-stream driver could not complete an operation.
43    #[error(transparent)]
44    Driver(#[from] DriverError),
45
46    /// An algorithm's step stream ended without a terminal routing outcome.
47    #[error("algorithm run ended without a routing outcome")]
48    MissingFinalResponse,
49
50    /// A target's protocol client failed while serving a routed request.
51    #[error("client call to target {target:?} failed: {source}")]
52    ClientCall {
53        /// Target whose client failed.
54        target: ModelId,
55        /// Typed error supplied by the protocol-owned client trait.
56        #[source]
57        source: LlmClientError,
58    },
59
60    /// A user extension or other foreign operation failed.
61    #[error("{operation} failed: {source}")]
62    External {
63        /// Short description of the operation that failed.
64        operation: &'static str,
65        /// Original failure.
66        #[source]
67        source: Box<dyn StdError + Send + Sync>,
68    },
69}
70
71impl LibsyError {
72    /// Wrap an error returned by the protocol-owned client trait.
73    pub fn client_call(target: impl Into<ModelId>, source: LlmClientError) -> Self {
74        Self::ClientCall {
75            target: target.into(),
76            source,
77        }
78    }
79
80    /// Preserve a concrete foreign error with a description of the failed operation.
81    pub fn external(
82        operation: &'static str,
83        source: impl StdError + Send + Sync + 'static,
84    ) -> Self {
85        Self::External {
86            operation,
87            source: Box::new(source),
88        }
89    }
90}
91
92/// Failures in the step-stream driver.
93#[derive(Debug, Error)]
94pub enum DriverError {
95    /// The consumer side of the step channel was dropped.
96    #[error("driver stream is closed")]
97    StreamClosed,
98
99    /// One side of a response promise was dropped before delivery.
100    #[error("driver response promise was dropped")]
101    ResponseDropped,
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn client_call_preserves_target_and_source() {
110        let error = LibsyError::client_call(
111            "strong",
112            LlmClientError::General("upstream down".to_string()),
113        );
114        match &error {
115            LibsyError::ClientCall { target, source } => {
116                assert_eq!(target, "strong");
117                assert_eq!(source.to_string(), "upstream down");
118            }
119            other => panic!("expected ClientCall, got {other:?}"),
120        }
121        assert_eq!(
122            StdError::source(&error).map(ToString::to_string),
123            Some("upstream down".to_string())
124        );
125    }
126
127    #[test]
128    fn external_preserves_operation_and_source() {
129        let error = LibsyError::external(
130            "loading extension",
131            std::io::Error::other("bad configuration"),
132        );
133        assert!(matches!(
134            &error,
135            LibsyError::External { operation, .. } if *operation == "loading extension"
136        ));
137        assert_eq!(
138            StdError::source(&error).map(ToString::to_string),
139            Some("bad configuration".to_string())
140        );
141    }
142}