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