#[derive(Debug, Clone, thiserror::Error)]
#[non_exhaustive]
pub enum LoopError {
#[error("Tool not found: {tool}. Available: {available}")]
ToolNotFound {
tool: String,
available: String,
},
#[error("Tool execution error: {tool}: {message}")]
ToolExecution {
tool: String,
message: String,
},
#[error("Invalid input: {0}")]
InvalidInput(String),
#[error("API error: {0}")]
Api(
String,
),
#[error("Max turns exceeded: {max}")]
MaxTurnsExceeded {
max: usize,
},
#[error("Context window exceeded: used {used} of {limit} tokens")]
ContextExceeded {
used: u64,
limit: u64,
},
#[error("Phase '{phase}' failed: {message}")]
PhaseFailed {
phase: String,
message: String,
},
#[error("Memory error: {0}")]
Memory(String),
#[error("Reflection error: {0}")]
Reflection(String),
#[error("Agent cancelled")]
Cancelled,
#[error("Loop detected: {message}")]
LoopDetected {
message: String,
},
#[error("Tool limit reached: {message}")]
ToolLimitReached {
message: String,
},
#[error("Stream error: {0}")]
StreamError(
String,
),
#[error("Configuration error: {0}")]
Config(
String,
),
#[error("{0}")]
Internal(String),
}
impl LoopError {
pub fn tool_not_found(tool: impl Into<String>, available: &[&str]) -> Self {
let tool = tool.into();
let available_str = if available.is_empty() {
String::from("none registered")
} else if available.len() <= 10 {
available.join(", ")
} else {
let count = available.len().saturating_sub(10);
format!(
"{}... (and {count} more)",
available
.iter()
.take(10)
.copied()
.collect::<Vec<_>>()
.join(", "),
)
};
Self::ToolNotFound {
tool,
available: available_str,
}
}
#[must_use]
pub const fn is_recoverable(&self) -> bool {
matches!(
self,
Self::ToolExecution { .. }
| Self::Api(_)
| Self::ContextExceeded { .. }
| Self::Reflection(_)
)
}
#[must_use]
pub const fn is_cancelled(&self) -> bool {
matches!(self, Self::Cancelled)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tool_not_found_empty_available_says_none_registered() {
let err = LoopError::tool_not_found("my_tool", &[]);
assert_eq!(
err.to_string(),
"Tool not found: my_tool. Available: none registered"
);
}
#[test]
fn tool_not_found_up_to_10_entries_comma_joined() {
let names: Vec<&str> = (1..=10)
.map(|i| {
static TOOLS: [&str; 10] = [
"tool_1", "tool_2", "tool_3", "tool_4", "tool_5", "tool_6", "tool_7", "tool_8",
"tool_9", "tool_10",
];
TOOLS[i - 1]
})
.collect();
let err = LoopError::tool_not_found("missing", &names);
assert_eq!(
err.to_string(),
"Tool not found: missing. Available: tool_1, tool_2, tool_3, tool_4, tool_5, tool_6, tool_7, tool_8, tool_9, tool_10"
);
}
#[test]
fn tool_not_found_more_than_10_truncates_with_and_n_more() {
let names: Vec<&str> = vec![
"tool_1", "tool_2", "tool_3", "tool_4", "tool_5", "tool_6", "tool_7", "tool_8",
"tool_9", "tool_10", "tool_11", "tool_12", "tool_13",
];
let err = LoopError::tool_not_found("missing", &names);
assert_eq!(
err.to_string(),
"Tool not found: missing. Available: tool_1, tool_2, tool_3, tool_4, tool_5, tool_6, tool_7, tool_8, tool_9, tool_10... (and 3 more)"
);
}
#[test]
fn tool_not_found_exactly_11_shows_one_more() {
let names: Vec<&str> = vec![
"tool_1", "tool_2", "tool_3", "tool_4", "tool_5", "tool_6", "tool_7", "tool_8",
"tool_9", "tool_10", "tool_11",
];
let err = LoopError::tool_not_found("missing", &names);
assert_eq!(
err.to_string(),
"Tool not found: missing. Available: tool_1, tool_2, tool_3, tool_4, tool_5, tool_6, tool_7, tool_8, tool_9, tool_10... (and 1 more)"
);
}
#[test]
fn is_recoverable_true_for_tool_execution() {
let err = LoopError::ToolExecution {
tool: "cat".into(),
message: "something went wrong".into(),
};
assert!(err.is_recoverable());
}
#[test]
fn is_recoverable_true_for_api() {
let err = LoopError::Api("rate limited".into());
assert!(err.is_recoverable());
}
#[test]
fn is_recoverable_true_for_context_exceeded() {
let err = LoopError::ContextExceeded {
used: 200_000,
limit: 128_000,
};
assert!(err.is_recoverable());
}
#[test]
fn is_recoverable_true_for_reflection() {
let err = LoopError::Reflection("need to rethink".into());
assert!(err.is_recoverable());
}
#[test]
fn is_recoverable_false_for_tool_not_found() {
let err = LoopError::tool_not_found("nope", &["a", "b"]);
assert!(!err.is_recoverable());
}
#[test]
fn is_recoverable_false_for_cancelled() {
let err = LoopError::Cancelled;
assert!(!err.is_recoverable());
}
}