Skip to main content

lc_core/runnables/
error.rs

1// lc-core/src/runnables/error.rs
2//! Unified error type for LCEL (LangChain Expression Language) pipelines.
3//!
4//! `LcelError` bridges all sub-crate error types into a single enum,
5//! enabling `RunnableSequence` and other LCEL combinators to work with
6//! a uniform error type regardless of which components are in the pipeline.
7//!
8//! # Design Decision
9//!
10//! `LcelError` stores error descriptions as `String` rather than wrapping
11//! concrete sub-error types (e.g. `Chain(ChainError)`). This avoids:
12//! 1. Circular dependencies between `lc-core` and downstream crates
13//! 2. Type-erased pipeline steps where the concrete error type is lost anyway
14//! 3. Bloating the enum with every provider-specific error variant
15//!
16//! The `Display` representation preserves enough information for debugging.
17
18use crate::output_parsers::OutputParserError;
19use std::fmt;
20
21/// Unified error type for LCEL pipelines.
22///
23/// All `Runnable` components that participate in LCEL composition
24/// must have an `Error` type that implements `Into<LcelError>`.
25#[derive(Debug, Clone)]
26#[non_exhaustive]
27pub enum LcelError {
28    /// Error from an LLM provider (OpenAI, Anthropic, Gemini, Ollama, etc.).
29    Provider(String),
30
31    /// Error from a chain execution.
32    Chain(String),
33
34    /// Error from an agent execution.
35    Agent(String),
36
37    /// Error from a graph execution.
38    Graph(String),
39
40    /// Error from a tool execution.
41    Tool(String),
42
43    /// Error from an output parser.
44    OutputParser(String),
45
46    /// Error during streaming.
47    Stream(String),
48
49    /// Error in pipeline composition or execution.
50    Pipeline(String),
51
52    /// Type-erasure downcast failure.
53    TypeMismatch(String),
54
55    /// Catch-all for errors that don't fit other variants.
56    Other(String),
57}
58
59impl fmt::Display for LcelError {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        match self {
62            LcelError::Provider(msg) => write!(f, "Provider error: {msg}"),
63            LcelError::Chain(msg) => write!(f, "Chain error: {msg}"),
64            LcelError::Agent(msg) => write!(f, "Agent error: {msg}"),
65            LcelError::Graph(msg) => write!(f, "Graph error: {msg}"),
66            LcelError::Tool(msg) => write!(f, "Tool error: {msg}"),
67            LcelError::OutputParser(msg) => write!(f, "Output parser error: {msg}"),
68            LcelError::Stream(msg) => write!(f, "Stream error: {msg}"),
69            LcelError::Pipeline(msg) => write!(f, "Pipeline error: {msg}"),
70            LcelError::TypeMismatch(msg) => write!(f, "Type mismatch: {msg}"),
71            LcelError::Other(msg) => write!(f, "{msg}"),
72        }
73    }
74}
75
76impl std::error::Error for LcelError {}
77
78// Allow `Infallible` to convert into `LcelError` (never actually happens).
79impl From<std::convert::Infallible> for LcelError {
80    fn from(_: std::convert::Infallible) -> Self {
81        unreachable!()
82    }
83}
84
85// Allow output parser errors into `LcelError` so parsers can be the
86// second (or later) step of a `pipe()` chain — `R2::Error: Into<LcelError>`
87// is required by `RunnableExt::pipe`.
88impl From<OutputParserError> for LcelError {
89    fn from(e: OutputParserError) -> Self {
90        LcelError::OutputParser(e.to_string())
91    }
92}
93
94// Allow tool errors into `LcelError` so tools can be a step of a `pipe()` chain
95// (e.g. `tool.pipe(...)`), mapping into the existing `Tool` variant.
96impl From<crate::tools::ToolError> for LcelError {
97    fn from(e: crate::tools::ToolError) -> Self {
98        LcelError::Tool(e.to_string())
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn display_formats_correctly() {
108        assert_eq!(
109            LcelError::Provider("openai timeout".to_string()).to_string(),
110            "Provider error: openai timeout"
111        );
112        assert_eq!(
113            LcelError::Chain("missing input".to_string()).to_string(),
114            "Chain error: missing input"
115        );
116        assert_eq!(
117            LcelError::TypeMismatch("expected String got i32".to_string()).to_string(),
118            "Type mismatch: expected String got i32"
119        );
120    }
121
122    #[test]
123    fn is_send_sync() {
124        fn assert_send_sync<T: Send + Sync + 'static>() {}
125        assert_send_sync::<LcelError>();
126    }
127
128    #[test]
129    fn from_output_parser_error() {
130        // 解析器错误可平滑进入 LcelError(不 panic),pipe 第二段才编译得过
131        let e = OutputParserError::JsonError("bad json".to_string());
132        let lcel: LcelError = e.into();
133        assert!(matches!(
134            lcel,
135            LcelError::OutputParser(ref msg) if msg.contains("bad json")
136        ));
137        assert_eq!(
138            lcel.to_string(),
139            "Output parser error: JSON error: bad json"
140        );
141    }
142
143    #[test]
144    fn from_tool_error() {
145        // 工具错误可平滑进入 LcelError(不 panic),`tool.pipe(...)` 才编译得过
146        use crate::tools::ToolError;
147        let e = ToolError::InvalidInput("bad input".to_string());
148        let lcel: LcelError = e.into();
149        assert!(matches!(
150            lcel,
151            LcelError::Tool(ref msg) if msg.contains("bad input")
152        ));
153        assert_eq!(lcel.to_string(), "Tool error: Invalid input: bad input");
154    }
155}