Skip to main content

lellm_graph/
error.rs

1//! Graph 错误类型。
2//!
3//! 错误模型:
4//! - `Terminal` — 终止执行,stream 关闭
5//! - Fallback — 控制流(通过 `StreamNodeResult::Fallback`),非错误
6//! - 可观测性 — 通过 `GraphEvent::ObservedError` 事件发送
7//!
8//! `build()` = 结构正确性校验(纯函数,只产生 BuildError)
9//! `analyze()` = 风险诊断(产生 GraphDiagnostics)
10
11use std::fmt;
12
13// ─── BuildError ──────────────────────────────────────────────
14
15/// 构建时结构校验错误。
16///
17/// 仅验证图的结构性正确性:节点存在、边引用有效、入口/出口存在、Fallback 不指向自身。
18/// **绝不产生 Warning。** Warning 迁移至 `GraphDiagnostics`。
19#[derive(Debug, Clone)]
20pub enum BuildError {
21    /// 节点 ID 重复(后者覆盖前者)
22    DuplicateNode { id: String },
23    /// 边引用了不存在的节点
24    MissingNode { from: String, to: String },
25    /// 未指定入口节点
26    MissingEntryPoint,
27    /// 未指定出口节点
28    MissingExitPoint,
29    /// 边定义无效
30    InvalidEdgeDefinition {
31        from: String,
32        to: String,
33        reason: String,
34    },
35    /// Fallback 边配置无效(如指向自身 = retry,不是 fallback)
36    InvalidFallback { node: String, reason: String },
37}
38
39impl fmt::Display for BuildError {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        match self {
42            Self::DuplicateNode { id } => write!(f, "duplicate node id: '{}'", id),
43            Self::MissingNode { from, to } => {
44                write!(
45                    f,
46                    "edge references non-existent node: '{}' (in {}→{})",
47                    to, from, to
48                )
49            }
50            Self::MissingEntryPoint => write!(f, "entry point not set"),
51            Self::MissingExitPoint => write!(f, "exit point not set"),
52            Self::InvalidEdgeDefinition { from, to, reason } => {
53                write!(f, "invalid edge {}→{}: {}", from, to, reason)
54            }
55            Self::InvalidFallback { node, reason } => {
56                write!(f, "invalid fallback for node '{}': {}", node, reason)
57            }
58        }
59    }
60}
61
62/// 构建错误集合 — 支持多错误收集。
63#[derive(Debug, Clone, Default)]
64pub struct BuildErrors(pub Vec<BuildError>);
65
66impl BuildErrors {
67    pub fn new() -> Self {
68        Self(Vec::new())
69    }
70
71    pub fn push(&mut self, e: BuildError) {
72        self.0.push(e);
73    }
74
75    pub fn is_empty(&self) -> bool {
76        self.0.is_empty()
77    }
78
79    pub fn len(&self) -> usize {
80        self.0.len()
81    }
82
83    pub fn iter(&self) -> impl Iterator<Item = &BuildError> {
84        self.0.iter()
85    }
86}
87
88impl fmt::Display for BuildErrors {
89    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90        if self.0.is_empty() {
91            write!(f, "no errors")
92        } else {
93            writeln!(f, "{} error(s):", self.0.len())?;
94            for e in &self.0 {
95                writeln!(f, "  - {}", e)?;
96            }
97            Ok(())
98        }
99    }
100}
101
102impl std::error::Error for BuildError {}
103impl std::error::Error for BuildErrors {}
104
105// ─── GraphDiagnostics ────────────────────────────────────────
106
107/// 诊断严重级别。
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub enum DiagnosticSeverity {
110    /// 信息性 — 值得注意但不一定有問題
111    Info,
112    /// 警告 — 潜在风险,建议检查
113    Warning,
114}
115
116impl fmt::Display for DiagnosticSeverity {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        match self {
119            Self::Info => write!(f, "info"),
120            Self::Warning => write!(f, "warning"),
121        }
122    }
123}
124
125/// 诊断分类。
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub enum DiagnosticCategory {
128    /// 环检测
129    Cycle,
130    /// Fallback 参与循环
131    FallbackInCycle,
132    /// 不可达路径
133    Unreachable,
134    /// 条件边重叠
135    ConditionOverlap,
136    /// End 节点有出边
137    EndNodeOutgoing,
138    /// 其他
139    Other,
140}
141
142impl std::fmt::Display for DiagnosticCategory {
143    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144        match self {
145            Self::Cycle => write!(f, "cycle"),
146            Self::FallbackInCycle => write!(f, "fallback-in-cycle"),
147            Self::Unreachable => write!(f, "unreachable"),
148            Self::ConditionOverlap => write!(f, "condition-overlap"),
149            Self::EndNodeOutgoing => write!(f, "end-node-outgoing"),
150            Self::Other => write!(f, "other"),
151        }
152    }
153}
154
155/// 单条诊断信息。
156#[derive(Debug, Clone)]
157pub struct Diagnostic {
158    pub severity: DiagnosticSeverity,
159    pub category: DiagnosticCategory,
160    pub message: String,
161}
162
163impl fmt::Display for Diagnostic {
164    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165        write!(
166            f,
167            "[{}] ({}): {}",
168            self.severity, self.category, self.message
169        )
170    }
171}
172
173/// 图诊断结果 — 由 `graph.analyze()` 产生。
174///
175/// 检查风险性问题:环检测、Fallback 参与循环、不可达路径、条件边重叠、End 节点有出边。
176#[derive(Debug, Clone, Default)]
177pub struct GraphDiagnostics {
178    pub warnings: Vec<Diagnostic>,
179    pub infos: Vec<Diagnostic>,
180}
181
182impl GraphDiagnostics {
183    pub fn new() -> Self {
184        Self::default()
185    }
186
187    pub fn add_warning(&mut self, category: DiagnosticCategory, message: impl Into<String>) {
188        self.warnings.push(Diagnostic {
189            severity: DiagnosticSeverity::Warning,
190            category,
191            message: message.into(),
192        });
193    }
194
195    pub fn add_info(&mut self, category: DiagnosticCategory, message: impl Into<String>) {
196        self.infos.push(Diagnostic {
197            severity: DiagnosticSeverity::Info,
198            category,
199            message: message.into(),
200        });
201    }
202
203    pub fn is_empty(&self) -> bool {
204        self.warnings.is_empty() && self.infos.is_empty()
205    }
206
207    pub fn has_warnings(&self) -> bool {
208        !self.warnings.is_empty()
209    }
210}
211
212impl fmt::Display for GraphDiagnostics {
213    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
214        if !self.warnings.is_empty() {
215            writeln!(f, "{} warning(s):", self.warnings.len())?;
216            for w in &self.warnings {
217                writeln!(f, "  - {}", w)?;
218            }
219        }
220        if !self.infos.is_empty() {
221            writeln!(f, "{} info(s):", self.infos.len())?;
222            for i in &self.infos {
223                writeln!(f, "  - {}", i)?;
224            }
225        }
226        if self.is_empty() {
227            write!(f, "no issues found")
228        } else {
229            Ok(())
230        }
231    }
232}
233
234// ─── GraphError ──────────────────────────────────────────────
235
236/// Graph 运行时错误。
237///
238/// 只有 Terminal 变体 — Fallback 改为控制流(`StreamNodeResult::Fallback`)。
239#[derive(Debug)]
240pub enum GraphError {
241    /// 终止执行 — stream 关闭,不可恢复
242    Terminal(TerminalError),
243}
244
245/// 终止错误 — Graph 执行不可恢复地停止。
246#[derive(Debug)]
247pub enum TerminalError {
248    /// 图结构无效(构建时校验遗漏的运行时问题)
249    InvalidGraph(String),
250    /// 节点不存在
251    NodeNotFound(String),
252    /// Goto 目标缺少对应的边
253    MissingEdge { from: String, to: String },
254    /// 节点执行失败(不可恢复)
255    NodeExecutionFailed {
256        node: String,
257        source: Box<dyn std::error::Error + Send + Sync>,
258    },
259    /// 全局步数超限(运行时熔断)
260    StepsExceeded { limit: usize },
261    /// 循环超限
262    LoopLimitExceeded { limit: usize },
263    /// Barrier 超时
264    BarrierTimeout {
265        node: String,
266        timeout: std::time::Duration,
267    },
268    /// Barrier 被取消
269    BarrierCancelled { node: String },
270    /// 无匹配边 — 没有任何 outgoing edge 满足条件,且无 fallback
271    Unrouted {
272        /// 当前节点
273        node: String,
274        /// 尝试的条件及其结果
275        attempted_conditions: Vec<ConditionEval>,
276    },
277    /// State 操作错误
278    StateError(String),
279}
280
281/// 可观测性事件 — 不属于错误体系,通过 GraphEvent 发送。
282#[derive(Debug, Clone)]
283pub enum ObservedError {
284    /// 警告
285    Warning { node: String, message: String },
286    /// 降级执行
287    Degraded { node: String, message: String },
288    /// 部分失败
289    PartialFailure {
290        node: String,
291        succeeded: usize,
292        failed: usize,
293        message: String,
294    },
295}
296
297impl fmt::Display for ObservedError {
298    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
299        match self {
300            Self::Warning { node, message } => write!(f, "node '{}': {}", node, message),
301            Self::Degraded { node, message } => write!(f, "node '{}' degraded: {}", node, message),
302            Self::PartialFailure {
303                node,
304                succeeded,
305                failed,
306                message,
307            } => {
308                write!(
309                    f,
310                    "node '{}' partial: {}/{} ok, {}",
311                    node,
312                    succeeded,
313                    succeeded + failed,
314                    message
315                )
316            }
317        }
318    }
319}
320
321/// 条件评估结果 — 用于 Unrouted 错误报告。
322#[derive(Debug, Clone)]
323pub struct ConditionEval {
324    /// 边描述
325    pub edge: String,
326    /// 条件描述(None = default edge)
327    pub condition: Option<String>,
328    /// 评估结果
329    pub matched: bool,
330}
331
332// ─── Display ─────────────────────────────────────────────────
333
334impl fmt::Display for GraphError {
335    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
336        match self {
337            Self::Terminal(e) => write!(f, "[terminal] {}", e),
338        }
339    }
340}
341
342impl fmt::Display for TerminalError {
343    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
344        match self {
345            Self::InvalidGraph(msg) => write!(f, "invalid graph: {msg}"),
346            Self::NodeNotFound(name) => write!(f, "node not found: {name}"),
347            Self::MissingEdge { from, to } => {
348                write!(
349                    f,
350                    "goto '{}' from '{}' failed: no edge {}→{} exists",
351                    to, from, from, to
352                )
353            }
354            Self::NodeExecutionFailed { node, source } => {
355                write!(f, "node '{node}' execution failed: {source}")
356            }
357            Self::StepsExceeded { limit } => {
358                write!(f, "step limit {limit} exceeded (potential infinite loop)")
359            }
360            Self::LoopLimitExceeded { limit } => write!(f, "loop limit exceeded: {limit}"),
361            Self::BarrierTimeout { node, timeout } => {
362                write!(f, "barrier '{node}' timed out after {timeout:?}")
363            }
364            Self::BarrierCancelled { node } => {
365                write!(
366                    f,
367                    "barrier '{node}' cancelled: consumer dropped the signal channel"
368                )
369            }
370            Self::Unrouted {
371                node,
372                attempted_conditions,
373            } => {
374                write!(f, "node '{}' has no matching outgoing edge", node)?;
375                if !attempted_conditions.is_empty() {
376                    write!(f, ". evaluated: [")?;
377                    for (i, ce) in attempted_conditions.iter().enumerate() {
378                        if i > 0 {
379                            write!(f, ", ")?;
380                        }
381                        write!(f, "{}={}", ce.edge, ce.matched)?;
382                    }
383                    write!(f, "]")?;
384                }
385                Ok(())
386            }
387            Self::StateError(msg) => write!(f, "state error: {msg}"),
388        }
389    }
390}
391
392impl std::error::Error for GraphError {
393    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
394        match self {
395            Self::Terminal(TerminalError::NodeExecutionFailed { source, .. }) => {
396                Some(source.as_ref())
397            }
398            Self::Terminal(_) => None,
399        }
400    }
401}
402
403impl std::error::Error for TerminalError {
404    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
405        match self {
406            Self::NodeExecutionFailed { source, .. } => Some(source.as_ref()),
407            _ => None,
408        }
409    }
410}