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)]
26pub enum LcelError {
27    /// Error from an LLM provider (OpenAI, Anthropic, Gemini, Ollama, etc.).
28    Provider(String),
29
30    /// Error from a chain execution.
31    Chain(String),
32
33    /// Error from an agent execution.
34    Agent(String),
35
36    /// Error from a graph execution.
37    Graph(String),
38
39    /// Error from a tool execution.
40    Tool(String),
41
42    /// Error from an output parser.
43    OutputParser(String),
44
45    /// Error during streaming.
46    Stream(String),
47
48    /// Error in pipeline composition or execution.
49    Pipeline(String),
50
51    /// Type-erasure downcast failure.
52    TypeMismatch(String),
53
54    /// Catch-all for errors that don't fit other variants.
55    Other(String),
56}
57
58impl fmt::Display for LcelError {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        match self {
61            LcelError::Provider(msg) => write!(f, "Provider error: {msg}"),
62            LcelError::Chain(msg) => write!(f, "Chain error: {msg}"),
63            LcelError::Agent(msg) => write!(f, "Agent error: {msg}"),
64            LcelError::Graph(msg) => write!(f, "Graph error: {msg}"),
65            LcelError::Tool(msg) => write!(f, "Tool error: {msg}"),
66            LcelError::OutputParser(msg) => write!(f, "Output parser error: {msg}"),
67            LcelError::Stream(msg) => write!(f, "Stream error: {msg}"),
68            LcelError::Pipeline(msg) => write!(f, "Pipeline error: {msg}"),
69            LcelError::TypeMismatch(msg) => write!(f, "Type mismatch: {msg}"),
70            LcelError::Other(msg) => write!(f, "{msg}"),
71        }
72    }
73}
74
75impl std::error::Error for LcelError {}
76
77// Allow `Infallible` to convert into `LcelError` (never actually happens).
78impl From<std::convert::Infallible> for LcelError {
79    fn from(_: std::convert::Infallible) -> Self {
80        unreachable!()
81    }
82}
83
84// Allow output parser errors into `LcelError` so parsers can be the
85// second (or later) step of a `pipe()` chain — `R2::Error: Into<LcelError>`
86// is required by `RunnableExt::pipe`.
87impl From<OutputParserError> for LcelError {
88    fn from(e: OutputParserError) -> Self {
89        LcelError::OutputParser(e.to_string())
90    }
91}
92
93// Allow tool errors into `LcelError` so tools can be a step of a `pipe()` chain
94// (e.g. `tool.pipe(...)`), mapping into the existing `Tool` variant.
95impl From<crate::tools::ToolError> for LcelError {
96    fn from(e: crate::tools::ToolError) -> Self {
97        LcelError::Tool(e.to_string())
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn display_formats_correctly() {
107        assert_eq!(
108            LcelError::Provider("openai timeout".to_string()).to_string(),
109            "Provider error: openai timeout"
110        );
111        assert_eq!(
112            LcelError::Chain("missing input".to_string()).to_string(),
113            "Chain error: missing input"
114        );
115        assert_eq!(
116            LcelError::TypeMismatch("expected String got i32".to_string()).to_string(),
117            "Type mismatch: expected String got i32"
118        );
119    }
120
121    #[test]
122    fn is_send_sync() {
123        fn assert_send_sync<T: Send + Sync + 'static>() {}
124        assert_send_sync::<LcelError>();
125    }
126
127    #[test]
128    fn from_output_parser_error() {
129        // 解析器错误可平滑进入 LcelError(不 panic),pipe 第二段才编译得过
130        let e = OutputParserError::JsonError("bad json".to_string());
131        let lcel: LcelError = e.into();
132        assert!(matches!(
133            lcel,
134            LcelError::OutputParser(ref msg) if msg.contains("bad json")
135        ));
136        assert_eq!(
137            lcel.to_string(),
138            "Output parser error: JSON error: bad json"
139        );
140    }
141
142    #[test]
143    fn from_tool_error() {
144        // 工具错误可平滑进入 LcelError(不 panic),`tool.pipe(...)` 才编译得过
145        use crate::tools::ToolError;
146        let e = ToolError::InvalidInput("bad input".to_string());
147        let lcel: LcelError = e.into();
148        assert!(matches!(
149            lcel,
150            LcelError::Tool(ref msg) if msg.contains("bad input")
151        ));
152        assert_eq!(lcel.to_string(), "Tool error: Invalid input: bad input");
153    }
154}