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 response.
40    #[error("algorithm run ended without a final response")]
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    /// Every target overflowed its context window.
54    #[error("every target exceeded its context window")]
55    AllTargetsExcluded,
56
57    /// A user extension or other foreign operation failed.
58    #[error("{operation} failed: {source}")]
59    External {
60        /// Short description of the operation that failed.
61        operation: &'static str,
62        /// Original failure.
63        #[source]
64        source: Box<dyn StdError + Send + Sync>,
65    },
66}
67
68impl LibsyError {
69    /// Wrap an error returned by the protocol-owned client trait.
70    pub fn client_call(target: impl Into<ModelId>, source: LlmClientError) -> Self {
71        Self::ClientCall {
72            target: target.into(),
73            source,
74        }
75    }
76
77    /// Preserve a concrete foreign error with a description of the failed operation.
78    pub fn external(
79        operation: &'static str,
80        source: impl StdError + Send + Sync + 'static,
81    ) -> Self {
82        Self::External {
83            operation,
84            source: Box::new(source),
85        }
86    }
87}
88
89/// Failures in the step-stream driver.
90#[derive(Debug, Error)]
91pub enum DriverError {
92    /// The consumer side of the step channel was dropped.
93    #[error("driver stream is closed")]
94    StreamClosed,
95
96    /// One side of a response promise was dropped before delivery.
97    #[error("driver response promise was dropped")]
98    ResponseDropped,
99
100    /// The consumer took the call's contents with `CallModel::into_parts` instead of
101    /// answering it, so the run ends here by the consumer's choice. Distinct from
102    /// [`ResponseDropped`](Self::ResponseDropped), which means the promise was lost.
103    #[error("model call was abandoned by the consumer")]
104    Abandoned,
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn client_call_preserves_target_and_source() {
113        let error = LibsyError::client_call(
114            "strong",
115            LlmClientError::General("upstream down".to_string()),
116        );
117        match &error {
118            LibsyError::ClientCall { target, source } => {
119                assert_eq!(target, "strong");
120                assert_eq!(source.to_string(), "upstream down");
121            }
122            other => panic!("expected ClientCall, got {other:?}"),
123        }
124        assert_eq!(
125            StdError::source(&error).map(ToString::to_string),
126            Some("upstream down".to_string())
127        );
128    }
129
130    #[test]
131    fn external_preserves_operation_and_source() {
132        let error = LibsyError::external(
133            "loading extension",
134            std::io::Error::other("bad configuration"),
135        );
136        assert!(matches!(
137            &error,
138            LibsyError::External { operation, .. } if *operation == "loading extension"
139        ));
140        assert_eq!(
141            StdError::source(&error).map(ToString::to_string),
142            Some("bad configuration".to_string())
143        );
144    }
145}