use crate::typed_id::{AgentId, HarnessId, SessionId};
use crate::user_facing_error::{
UserFacingError, UserFacingErrorContext, classify_runtime_error_message,
codes as user_facing_error_codes,
};
use serde::{Serialize, de::DeserializeOwned};
use thiserror::Error;
pub type Result<T> = std::result::Result<T, AgentLoopError>;
#[derive(Debug, Error)]
pub enum AgentLoopError {
#[error("LLM error: {0}")]
Llm(String),
#[error("Request too large: {0}")]
RequestTooLarge(String),
#[error("Model not available: {0}")]
ModelNotAvailable(String),
#[error("Tool execution error: {0}")]
ToolExecution(String),
#[error("Message store error: {0}")]
MessageStore(String),
#[error("Event emission error: {0}")]
EventEmission(String),
#[error("Configuration error: {0}")]
Configuration(String),
#[error("Max iterations ({0}) reached")]
MaxIterationsReached(usize),
#[error("Loop cancelled")]
Cancelled,
#[error("No messages to process")]
NoMessages,
#[error("Agent not found: {0}")]
AgentNotFound(AgentId),
#[error("Harness not found: {0}")]
HarnessNotFound(HarnessId),
#[error("Session not found: {0}")]
SessionNotFound(SessionId),
#[error("Internal error: {0}")]
Internal(#[from] anyhow::Error),
#[error(
"No driver registered for provider type '{0}'. Make sure the driver is registered at startup."
)]
DriverNotRegistered(String),
}
impl AgentLoopError {
pub fn llm(msg: impl Into<String>) -> Self {
AgentLoopError::Llm(msg.into())
}
pub fn tool(msg: impl Into<String>) -> Self {
AgentLoopError::ToolExecution(msg.into())
}
pub fn store(msg: impl Into<String>) -> Self {
AgentLoopError::MessageStore(msg.into())
}
pub fn event(msg: impl Into<String>) -> Self {
AgentLoopError::EventEmission(msg.into())
}
pub fn config(msg: impl Into<String>) -> Self {
AgentLoopError::Configuration(msg.into())
}
pub fn agent_not_found(agent_id: AgentId) -> Self {
AgentLoopError::AgentNotFound(agent_id)
}
pub fn harness_not_found(harness_id: HarnessId) -> Self {
AgentLoopError::HarnessNotFound(harness_id)
}
pub fn session_not_found(session_id: SessionId) -> Self {
AgentLoopError::SessionNotFound(session_id)
}
pub fn driver_not_registered(provider_type: impl Into<String>) -> Self {
AgentLoopError::DriverNotRegistered(provider_type.into())
}
pub fn request_too_large(msg: impl Into<String>) -> Self {
AgentLoopError::RequestTooLarge(msg.into())
}
pub fn model_not_available(model_id: impl Into<String>) -> Self {
AgentLoopError::ModelNotAvailable(model_id.into())
}
pub fn is_request_too_large(&self) -> bool {
matches!(self, AgentLoopError::RequestTooLarge(_))
}
pub fn is_model_not_available(&self) -> bool {
matches!(self, AgentLoopError::ModelNotAvailable(_))
}
pub fn model_not_available_id(&self) -> Option<&str> {
match self {
AgentLoopError::ModelNotAvailable(id) => Some(id),
_ => None,
}
}
pub fn is_rate_limited(&self) -> bool {
match self {
AgentLoopError::Llm(msg) => {
let msg_lower = msg.to_ascii_lowercase();
msg_lower.contains("(429)")
|| msg_lower.contains("rate limit")
|| msg_lower.contains("too many requests")
}
_ => false,
}
}
pub fn is_auth_error(&self) -> bool {
match self {
AgentLoopError::Llm(msg) => msg.contains("(401)") || msg.contains("(403)"),
_ => false,
}
}
pub fn is_server_error(&self) -> bool {
match self {
AgentLoopError::Llm(msg) => {
msg.contains("(500)")
|| msg.contains("(502)")
|| msg.contains("(503)")
|| msg.contains("(504)")
|| msg.contains("(529)")
}
_ => false,
}
}
pub fn is_non_retryable(&self) -> bool {
match self {
AgentLoopError::AgentNotFound(_)
| AgentLoopError::HarnessNotFound(_)
| AgentLoopError::SessionNotFound(_)
| AgentLoopError::NoMessages => true,
AgentLoopError::Configuration(_) | AgentLoopError::DriverNotRegistered(_) => true,
AgentLoopError::MessageStore(msg) => msg.to_ascii_lowercase().contains("not found"),
_ => false,
}
}
pub fn user_facing_message(&self) -> String {
self.user_facing_error(UserFacingErrorContext::default())
.fallback_message()
}
pub fn user_facing_error(&self, context: UserFacingErrorContext) -> UserFacingError {
match self {
AgentLoopError::ModelNotAvailable(model_id) => {
UserFacingError::new(user_facing_error_codes::MODEL_UNAVAILABLE)
.with_field("model_id", model_id)
.with_optional_field("provider", context.provider)
}
AgentLoopError::RequestTooLarge(_) => {
UserFacingError::new(user_facing_error_codes::REQUEST_TOO_LARGE)
.with_optional_field("provider", context.provider)
.with_optional_field("model_id", context.model_id)
}
AgentLoopError::MaxIterationsReached(max_iterations) => {
UserFacingError::new(user_facing_error_codes::MAX_ITERATIONS)
.with_field("max_iterations", max_iterations)
}
AgentLoopError::Llm(message) => classify_runtime_error_message(message, &context),
_ => UserFacingError::new(user_facing_error_codes::PROCESSING_ERROR)
.with_optional_field("provider", context.provider)
.with_optional_field("model_id", context.model_id),
}
}
}
pub trait StoreResultExt<T> {
fn store_err(self) -> Result<T>;
}
impl<T, E: std::fmt::Display> StoreResultExt<T> for std::result::Result<T, E> {
fn store_err(self) -> Result<T> {
self.map_err(|e| AgentLoopError::store(e.to_string()))
}
}
pub fn json_val<T: Serialize>(value: &T) -> serde_json::Value {
serde_json::to_value(value).unwrap_or_default()
}
pub fn from_json<T: DeserializeOwned + Default>(value: serde_json::Value) -> T {
serde_json::from_value(value).unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_request_too_large_returns_true_for_typed_error() {
let err = AgentLoopError::request_too_large("context length exceeded");
assert!(err.is_request_too_large());
}
#[test]
fn test_is_request_too_large_returns_false_for_llm_error() {
let err = AgentLoopError::llm("OpenAI API error (500): Internal server error");
assert!(!err.is_request_too_large());
}
#[test]
fn test_is_request_too_large_returns_false_for_other_errors() {
let err = AgentLoopError::ToolExecution("some error".to_string());
assert!(!err.is_request_too_large());
let err = AgentLoopError::Cancelled;
assert!(!err.is_request_too_large());
}
#[test]
fn test_request_too_large_error_preserves_message() {
let original_msg = "OpenAI API error (429): Request too large for gpt-4";
let err = AgentLoopError::request_too_large(original_msg);
assert_eq!(
err.to_string(),
format!("Request too large: {}", original_msg)
);
}
#[test]
fn test_is_model_not_available_returns_true_for_typed_error() {
let err = AgentLoopError::model_not_available("claude-sonnet-4-6-20260217");
assert!(err.is_model_not_available());
assert_eq!(
err.model_not_available_id(),
Some("claude-sonnet-4-6-20260217")
);
}
#[test]
fn test_is_model_not_available_returns_false_for_llm_error() {
let err = AgentLoopError::llm("some error");
assert!(!err.is_model_not_available());
assert_eq!(err.model_not_available_id(), None);
}
#[test]
fn test_model_not_available_error_display() {
let err = AgentLoopError::model_not_available("gpt-99");
assert_eq!(err.to_string(), "Model not available: gpt-99");
}
#[test]
fn test_is_rate_limited_detects_429() {
let err = AgentLoopError::llm("Anthropic API error (429): rate limit exceeded");
assert!(err.is_rate_limited());
}
#[test]
fn test_is_rate_limited_detects_rate_limit_keyword() {
let err =
AgentLoopError::llm("Rate limit exceeded (after 2 retries, last error: too many)");
assert!(err.is_rate_limited());
}
#[test]
fn test_is_rate_limited_false_for_server_error() {
let err = AgentLoopError::llm("Anthropic API error (500): internal server error");
assert!(!err.is_rate_limited());
}
#[test]
fn test_is_auth_error_detects_401() {
let err = AgentLoopError::llm("Anthropic API error (401): invalid api key");
assert!(err.is_auth_error());
}
#[test]
fn test_is_auth_error_detects_403() {
let err = AgentLoopError::llm("OpenAI API error (403): forbidden");
assert!(err.is_auth_error());
}
#[test]
fn test_is_server_error_detects_500() {
let err = AgentLoopError::llm("Anthropic API error (500): internal server error");
assert!(err.is_server_error());
}
#[test]
fn test_is_server_error_detects_503() {
let err = AgentLoopError::llm("OpenAI API error (503): service unavailable");
assert!(err.is_server_error());
}
#[test]
fn test_user_facing_message_rate_limited() {
let err = AgentLoopError::llm("Anthropic API error (429): rate limit exceeded");
assert_eq!(
err.user_facing_message(),
"Rate limited by the AI provider. Please wait a moment."
);
}
#[test]
fn test_user_facing_message_auth_error() {
let err = AgentLoopError::llm("Anthropic API error (401): invalid api key");
assert_eq!(
err.user_facing_message(),
"There is a misconfiguration with the AI provider. Please contact support."
);
}
#[test]
fn test_user_facing_message_server_error() {
let err = AgentLoopError::llm("Anthropic API error (500): internal server error");
assert_eq!(
err.user_facing_message(),
"The AI provider is experiencing issues. Please try again shortly."
);
}
#[test]
fn test_user_facing_message_generic_fallback() {
let err = AgentLoopError::llm("Failed to send request: connection refused");
assert_eq!(
err.user_facing_message(),
"I encountered an error while processing your request. Please try again later."
);
}
#[test]
fn test_user_facing_message_model_not_available() {
let err = AgentLoopError::model_not_available("gpt-99");
assert!(err.user_facing_message().contains("gpt-99"));
assert!(err.user_facing_message().contains("not available"));
}
#[test]
fn test_user_facing_message_request_too_large() {
let err = AgentLoopError::request_too_large("context length exceeded");
assert!(err.user_facing_message().contains("too long"));
}
#[test]
fn test_user_facing_error_model_not_available_includes_model_id() {
let err = AgentLoopError::model_not_available("gpt-99");
let user_error = err.user_facing_error(UserFacingErrorContext::default());
assert_eq!(user_error.code, user_facing_error_codes::MODEL_UNAVAILABLE);
assert_eq!(
user_error.fields.get("model_id"),
Some(&serde_json::Value::String("gpt-99".to_string()))
);
}
#[test]
fn test_user_facing_error_rate_limited_includes_provider_context() {
let err = AgentLoopError::llm("Anthropic API error (429): rate limit exceeded");
let user_error = err.user_facing_error(
UserFacingErrorContext::default()
.with_provider("anthropic")
.with_model_id("claude-sonnet-4-5")
.with_retry_after(12),
);
assert_eq!(
user_error.code,
user_facing_error_codes::PROVIDER_RATE_LIMITED
);
assert_eq!(
user_error.fields.get("provider"),
Some(&serde_json::Value::String("anthropic".to_string()))
);
assert_eq!(
user_error.fields.get("model_id"),
Some(&serde_json::Value::String("claude-sonnet-4-5".to_string()))
);
assert_eq!(
user_error.fields.get("retry_after"),
Some(&serde_json::json!(12))
);
}
#[test]
fn test_store_result_ext_ok() {
let result: std::result::Result<i32, String> = Ok(42);
assert_eq!(result.store_err().unwrap(), 42);
}
#[test]
fn test_store_result_ext_err() {
let result: std::result::Result<i32, String> = Err("db error".to_string());
let err = result.store_err().unwrap_err();
assert!(matches!(err, AgentLoopError::MessageStore(_)));
assert!(err.to_string().contains("db error"));
}
#[test]
fn test_json_val() {
let v = json_val(&vec![1, 2, 3]);
assert_eq!(v, serde_json::json!([1, 2, 3]));
}
#[test]
fn test_from_json() {
let v = serde_json::json!(["a", "b"]);
let result: Vec<String> = from_json(v);
assert_eq!(result, vec!["a", "b"]);
}
#[test]
fn test_from_json_default_on_mismatch() {
let v = serde_json::json!("not a number");
let result: i32 = from_json(v);
assert_eq!(result, 0);
}
}