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    /// 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    /// The consumer took the call's contents with `CallModel::into_parts` instead of
97    /// answering it, so the run ends here by the consumer's choice. Distinct from
98    /// [`ResponseDropped`](Self::ResponseDropped), which means the promise was lost.
99    #[error("model call was abandoned by the consumer")]
100    Abandoned,
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn client_call_preserves_target_and_source() {
109        let error = LibsyError::client_call(
110            "strong",
111            LlmClientError::General("upstream down".to_string()),
112        );
113        match &error {
114            LibsyError::ClientCall { target, source } => {
115                assert_eq!(target, "strong");
116                assert_eq!(source.to_string(), "upstream down");
117            }
118            other => panic!("expected ClientCall, got {other:?}"),
119        }
120        assert_eq!(
121            StdError::source(&error).map(ToString::to_string),
122            Some("upstream down".to_string())
123        );
124    }
125
126    #[test]
127    fn external_preserves_operation_and_source() {
128        let error = LibsyError::external(
129            "loading extension",
130            std::io::Error::other("bad configuration"),
131        );
132        assert!(matches!(
133            &error,
134            LibsyError::External { operation, .. } if *operation == "loading extension"
135        ));
136        assert_eq!(
137            StdError::source(&error).map(ToString::to_string),
138            Some("bad configuration".to_string())
139        );
140    }
141}