use crate::brain::provider::ProviderError;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum AgentError {
#[error("Provider error: {0}")]
Provider(#[from] ProviderError),
#[error("Database error: {0}")]
Database(String),
#[error("Session not found: {0}")]
SessionNotFound(uuid::Uuid),
#[error("Invalid request: {0}")]
InvalidRequest(String),
#[error("Context too large: {current} tokens exceeds limit of {limit}")]
ContextTooLarge { current: usize, limit: usize },
#[error("Tool execution error: {0}")]
ToolError(String),
#[error("Tool not found: {0}")]
ToolNotFound(String),
#[error("Maximum tool iterations exceeded: {0}")]
MaxIterationsExceeded(usize),
#[error("Cancelled")]
Cancelled,
#[error("Internal error: {0}")]
Internal(String),
}
pub type Result<T> = std::result::Result<T, AgentError>;
pub fn format_user_error(err: &AgentError) -> String {
let raw = err.to_string();
if raw.contains("error decoding response body") {
return "Provider stream broke mid-response (connection dropped). \
Self-heal already retried with no luck. Try again or \
switch to a different model via `/models`."
.to_string();
}
if raw.contains("Repetition detected") {
return "Provider got stuck repeating itself. The stream was \
terminated automatically. Try rephrasing your request \
or switching models via `/models`."
.to_string();
}
if let AgentError::ContextTooLarge { current, limit } = err {
return format!(
"Context too large: {current} tokens exceeds the {limit}-token \
limit. Run `/compact` to shrink the conversation, or start \
a fresh session."
);
}
if let Some(status) = extract_http_status(&raw) {
match status {
502..=504 => {
return format!(
"All fallback providers returned {status} within the \
retry window (likely shared upstream gateway outage). \
Self-heal already tried 4 fallbacks. Wait a minute \
and retry, or switch provider via `/models`."
);
}
429 => {
return "Rate limit hit on the active provider. Wait a \
minute or switch provider via `/models`."
.to_string();
}
401 | 403 => {
return format!(
"Authentication failed on the active provider \
({status}). Check your API key in `/onboard:provider` \
or `keys.toml`."
);
}
_ => {
let display_raw = if raw.len() > 200 {
format!("{}...", &raw[..200])
} else {
raw.clone()
};
return format!(
"Provider returned HTTP {status}. \
Try again, or switch provider via `/models`. \
Details: {display_raw}"
);
}
}
}
raw
}
fn extract_http_status(s: &str) -> Option<u16> {
let lower = s.to_lowercase();
for prefix in &["api error (", "http "] {
if let Some(idx) = lower.find(prefix) {
let tail = &s[idx + prefix.len()..];
let num: String = tail.chars().take_while(|c| c.is_ascii_digit()).collect();
if !num.is_empty()
&& let Ok(n) = num.parse::<u16>()
&& (100..=599).contains(&n)
{
return Some(n);
}
}
}
None
}