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