use thiserror::Error;
#[derive(Error, Debug)]
pub enum GraphError {
#[error("Graph has no entry point defined")]
NoEntryPoint,
#[error("Node not found: {0}")]
NodeNotFound(String),
#[error("Invalid edge target from '{from}' to '{to}'")]
InvalidEdgeTarget {
from: String,
to: String,
},
#[error("Duplicate node ID: {0}")]
DuplicateNode(String),
#[error("Graph contains a cycle: {0}")]
CycleDetected(String),
#[error("Invalid graph configuration: {0}")]
InvalidConfiguration(String),
}
#[derive(Error, Debug)]
pub enum NodeError {
#[error("Node execution failed: {0}")]
ExecutionFailed(String),
#[error("Node timed out after {0:?}")]
Timeout(std::time::Duration),
#[error("LLM error: {0}")]
LLMError(String),
#[error("Tool error: {0}")]
ToolError(String),
#[error("Serialization error: {0}")]
SerializationError(String),
#[error("HTTP error: {0}")]
HttpError(#[from] reqwest::Error),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("{0}")]
Other(String),
}
impl NodeError {
pub fn execution_failed(msg: impl Into<String>) -> Self {
Self::ExecutionFailed(msg.into())
}
pub fn llm_error(msg: impl Into<String>) -> Self {
Self::LLMError(msg.into())
}
pub fn tool_error(msg: impl Into<String>) -> Self {
Self::ToolError(msg.into())
}
pub fn other(msg: impl Into<String>) -> Self {
Self::Other(msg.into())
}
}
#[derive(Error, Debug)]
pub enum RuntimeError {
#[error("Recursion limit exceeded: {0} iterations")]
RecursionLimit(u32),
#[error("Node not found: {0}")]
NodeNotFound(String),
#[error("Node '{node_id}' failed: {error}")]
NodeFailed {
node_id: String,
error: NodeError,
},
#[error("Interrupted: {reason}")]
Interrupted {
reason: String,
},
#[error("No checkpointer configured")]
NoCheckpointer,
#[error("Checkpoint not found for thread: {0}")]
CheckpointNotFound(String),
#[error("Graph error: {0}")]
GraphError(#[from] GraphError),
#[error("Invalid state: {0}")]
InvalidState(String),
}
impl RuntimeError {
pub fn node_failed(node_id: impl Into<String>, error: NodeError) -> Self {
Self::NodeFailed {
node_id: node_id.into(),
error,
}
}
pub fn interrupted(reason: impl Into<String>) -> Self {
Self::Interrupted {
reason: reason.into(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_graph_error_display() {
let err = GraphError::NoEntryPoint;
assert_eq!(err.to_string(), "Graph has no entry point defined");
let err = GraphError::NodeNotFound("my_node".to_string());
assert_eq!(err.to_string(), "Node not found: my_node");
}
#[test]
fn test_node_error_display() {
let err = NodeError::execution_failed("Something went wrong");
assert_eq!(err.to_string(), "Node execution failed: Something went wrong");
let err = NodeError::Timeout(std::time::Duration::from_secs(30));
assert!(err.to_string().contains("30"));
}
#[test]
fn test_runtime_error_display() {
let err = RuntimeError::RecursionLimit(100);
assert_eq!(err.to_string(), "Recursion limit exceeded: 100 iterations");
let err = RuntimeError::interrupted("Need user input");
assert_eq!(err.to_string(), "Interrupted: Need user input");
}
}