use std::fmt;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum LellmError {
#[error("LLM error: {0}")]
Llm(#[from] LlmError),
#[error("Tool error: {0}")]
Tool(#[from] ToolError),
#[error("Memory error: {0}")]
Memory(#[from] MemoryError),
#[error("Parse error: {0}")]
Parse(#[from] ParseError),
}
#[derive(Debug, Error, Clone)]
pub enum LlmError {
#[error("invalid request: {message}")]
InvalidRequest { message: String },
#[error("unsupported feature: {feature}")]
UnsupportedFeature { feature: String },
#[error("duplicate system prompt: both config and conversation contain system message")]
DuplicateSystemPrompt,
#[error("network error: {detail}")]
Network { detail: String },
#[error("request timeout: {detail}")]
Timeout { detail: String },
#[error("provider error [{provider}]: {message}")]
Provider {
provider: String,
status: Option<u16>,
code: Option<String>,
message: String,
},
#[error("response parse error: {detail}")]
Parse { detail: String },
#[error("unexpected EOF: stream ended without ResponseComplete")]
UnexpectedEof,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToolErrorKind {
NotFound,
ToolUnavailable,
Timeout,
Network,
PermissionDenied,
InvalidInput,
RateLimited,
LoopDetected,
Internal,
External { source: &'static str },
}
impl ToolErrorKind {
pub fn is_retryable(self) -> bool {
matches!(
self,
Self::Timeout | Self::Network | Self::RateLimited | Self::ToolUnavailable
)
}
}
impl fmt::Display for ToolErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NotFound => write!(f, "NotFound"),
Self::ToolUnavailable => write!(f, "ToolUnavailable"),
Self::Timeout => write!(f, "Timeout"),
Self::Network => write!(f, "Network"),
Self::PermissionDenied => write!(f, "PermissionDenied"),
Self::InvalidInput => write!(f, "InvalidInput"),
Self::RateLimited => write!(f, "RateLimited"),
Self::LoopDetected => write!(f, "LoopDetected"),
Self::Internal => write!(f, "Internal"),
Self::External { source } => write!(f, "External({})", source),
}
}
}
#[derive(Clone)]
pub struct ToolError {
pub kind: ToolErrorKind,
pub message: String,
}
impl ToolError {
pub fn invalid_input(msg: impl Into<String>) -> Self {
Self {
kind: ToolErrorKind::InvalidInput,
message: msg.into(),
}
}
pub fn not_found(msg: impl Into<String>) -> Self {
Self {
kind: ToolErrorKind::NotFound,
message: msg.into(),
}
}
pub fn external<E: std::fmt::Display>(source: E) -> Self {
Self {
kind: ToolErrorKind::External {
source: std::any::type_name::<E>(),
},
message: source.to_string(),
}
}
}
impl fmt::Display for ToolError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[{}] {}", self.kind, self.message)
}
}
impl fmt::Debug for ToolError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "ToolError({}: {})", self.kind, self.message)
}
}
impl std::error::Error for ToolError {}
pub type ToolResult = Result<serde_json::Value, ToolError>;
pub trait IntoToolError {
fn into_tool_error(self) -> ToolError;
}
impl IntoToolError for ToolError {
fn into_tool_error(self) -> ToolError {
self
}
}
impl IntoToolError for std::io::Error {
fn into_tool_error(self) -> ToolError {
ToolError::external(self)
}
}
impl IntoToolError for serde_json::Error {
fn into_tool_error(self) -> ToolError {
ToolError {
kind: ToolErrorKind::Internal,
message: self.to_string(),
}
}
}
#[cfg(feature = "anyhow")]
impl IntoToolError for anyhow::Error {
fn into_tool_error(self) -> ToolError {
ToolError::external(self)
}
}
pub trait IntoToolResult: Sized {
fn into_tool(self) -> ToolResult;
}
impl IntoToolResult for String {
fn into_tool(self) -> ToolResult {
Ok(serde_json::Value::String(self))
}
}
impl IntoToolResult for serde_json::Value {
fn into_tool(self) -> ToolResult {
Ok(self)
}
}
impl<T> IntoToolResult for Option<T>
where
T: serde::Serialize,
{
fn into_tool(self) -> ToolResult {
match self {
Some(v) => serde_json::to_value(v).map_err(|e| ToolError {
kind: ToolErrorKind::Internal,
message: format!("failed to serialize tool result: {}", e),
}),
None => Ok(serde_json::Value::Null),
}
}
}
impl<T, E> IntoToolResult for Result<T, E>
where
T: serde::Serialize,
E: IntoToolError,
{
fn into_tool(self) -> ToolResult {
match self {
Ok(v) => serde_json::to_value(v).map_err(|e| ToolError {
kind: ToolErrorKind::Internal,
message: format!("failed to serialize tool result: {}", e),
}),
Err(e) => Err(e.into_tool_error()),
}
}
}
#[derive(Debug, Error)]
pub enum MemoryError {
#[error("memory IO error: {0}")]
IoError(String),
#[error("memory database error: {0}")]
DatabaseError(String),
}
#[derive(Debug, Error)]
#[error("parse error: {detail}")]
pub struct ParseError {
pub detail: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_llm_error_display() {
let err = LlmError::Timeout {
detail: "timed out after 60s".into(),
};
assert!(format!("{}", err).contains("timeout"));
assert!(format!("{}", err).contains("60s"));
}
#[test]
fn test_llm_error_provider_display() {
let err = LlmError::Provider {
provider: "openai".into(),
status: Some(429),
code: Some("rate_limit".into()),
message: "Too many requests".into(),
};
assert!(format!("{}", err).contains("openai"));
assert!(format!("{}", err).contains("Too many requests"));
}
#[test]
fn test_llm_error_invalid_request_display() {
let err = LlmError::InvalidRequest {
message: "Anthropic requires max_tokens".into(),
};
assert!(format!("{}", err).contains("invalid request"));
assert!(format!("{}", err).contains("max_tokens"));
}
#[test]
fn test_tool_error_display() {
let err = ToolError {
kind: ToolErrorKind::NotFound,
message: "read_file".into(),
};
assert!(format!("{}", err).contains("read_file"));
}
#[test]
fn test_lellm_error_from_tool_error() {
let tool_err = ToolError {
kind: ToolErrorKind::Timeout,
message: "timeout".into(),
};
let top_err: LellmError = tool_err.into();
assert!(format!("{}", top_err).contains("Tool error"));
}
#[test]
fn test_tool_error_is_retryable() {
assert!(ToolErrorKind::Timeout.is_retryable());
assert!(ToolErrorKind::Network.is_retryable());
assert!(ToolErrorKind::RateLimited.is_retryable());
assert!(ToolErrorKind::ToolUnavailable.is_retryable());
assert!(!ToolErrorKind::NotFound.is_retryable());
assert!(!ToolErrorKind::InvalidInput.is_retryable());
assert!(!ToolErrorKind::PermissionDenied.is_retryable());
assert!(!ToolErrorKind::Internal.is_retryable());
assert!(!ToolErrorKind::LoopDetected.is_retryable());
assert!(!ToolErrorKind::External { source: "test" }.is_retryable());
}
#[test]
fn test_into_tool_result_string() {
let result: ToolResult = "hello".to_string().into_tool();
assert_eq!(result.unwrap(), serde_json::json!("hello"));
}
#[test]
fn test_into_tool_result_option() {
let some: Option<String> = Some("hello".to_string());
assert_eq!(some.into_tool().unwrap(), serde_json::json!("hello"));
let none: Option<String> = None;
assert_eq!(none.into_tool().unwrap(), serde_json::json!(null));
}
#[test]
fn test_into_tool_result_external_error() {
#[derive(Debug)]
struct MyError;
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "my error")
}
}
impl IntoToolError for MyError {
fn into_tool_error(self) -> ToolError {
ToolError::external(self)
}
}
let result: ToolResult = Err::<(), MyError>(MyError).into_tool();
let err = result.unwrap_err();
assert_eq!(
err.kind,
ToolErrorKind::External {
source: std::any::type_name::<MyError>()
}
);
assert_eq!(err.message, "my error");
}
#[test]
fn test_into_tool_result_tool_error_passthrough() {
let err = ToolError::invalid_input("bad param");
let result: ToolResult = Err::<serde_json::Value, ToolError>(err).into_tool();
let out_err = result.unwrap_err();
assert_eq!(out_err.kind, ToolErrorKind::InvalidInput);
assert_eq!(out_err.message, "bad param");
}
#[test]
fn test_tool_error_factories() {
let err = ToolError::invalid_input("bad input");
assert_eq!(err.kind, ToolErrorKind::InvalidInput);
assert_eq!(err.message, "bad input");
let err = ToolError::not_found("search");
assert_eq!(err.kind, ToolErrorKind::NotFound);
assert_eq!(err.message, "search");
}
}