use core::fmt::Debug;
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use crate::errors::GenerationError;
use crate::types::{Messages, ModelId};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LoopDetection {
pub kind: String,
pub occurrences: u32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AuthError {
pub reason: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct UserId(pub String);
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum AgenticError {
Generation(GenerationFailure),
ToolCall { tool_name: String, reason: String },
ToolNotAuthorized { tool_name: String, user: UserId },
MessageStore(String),
Memory(String),
VectorStore(String),
Embedding(String),
Session(String),
Queue(String),
LoopDetected(LoopDetection),
Auth(AuthError),
Budget { limit: u64 },
BudgetExhausted {
snapshot: crate::agentic::token_ledger::TokenSnapshot,
},
Routing(String),
Unexpected(String),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GenerationFailure {
pub kind: GenKind,
pub message: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GenKind {
ContextOverflow,
RateLimit,
Provider,
Network,
Other,
}
impl AgenticError {
#[must_use]
pub fn from_generation(
error: &GenerationError,
last: Option<&Messages>,
context_window: u64,
) -> Self {
let kind = if last.is_some_and(|m| m.is_context_overflow(context_window)) {
GenKind::ContextOverflow
} else if detect_rate_limit(error) {
GenKind::RateLimit
} else if detect_network(error) {
GenKind::Network
} else {
GenKind::Provider
};
AgenticError::Generation(GenerationFailure {
kind,
message: error.to_string(),
})
}
}
impl std::fmt::Display for AgenticError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AgenticError::Generation(g) => write!(f, "generation ({:?}): {}", g.kind, g.message),
AgenticError::ToolCall { tool_name, reason } => {
write!(f, "tool '{tool_name}' failed: {reason}")
}
AgenticError::ToolNotAuthorized { tool_name, user } => {
write!(f, "tool '{tool_name}' not authorized for user '{}'", user.0)
}
AgenticError::MessageStore(m) => write!(f, "message store: {m}"),
AgenticError::Memory(m) => write!(f, "memory: {m}"),
AgenticError::VectorStore(m) => write!(f, "vector store: {m}"),
AgenticError::Embedding(m) => write!(f, "embedding: {m}"),
AgenticError::Session(m) => write!(f, "session: {m}"),
AgenticError::Queue(m) => write!(f, "queue: {m}"),
AgenticError::LoopDetected(l) => {
write!(f, "loop detected: {} x{}", l.kind, l.occurrences)
}
AgenticError::Auth(a) => write!(f, "auth: {}", a.reason),
AgenticError::Budget { limit } => write!(f, "budget exceeded (limit {limit})"),
AgenticError::BudgetExhausted { snapshot } => write!(
f,
"token budget exhausted ({} / {:?} tokens)",
snapshot.total, snapshot.budget
),
AgenticError::Routing(m) => write!(f, "routing: {m}"),
AgenticError::Unexpected(m) => write!(f, "unexpected: {m}"),
}
}
}
impl std::error::Error for AgenticError {}
impl From<crate::types::RouterError> for AgenticError {
fn from(e: crate::types::RouterError) -> Self {
AgenticError::Routing(e.to_string())
}
}
impl AgenticError {
#[must_use]
pub fn into_failed_action(self) -> crate::types::SessionRecord {
let trace = foundation_errstacks::ErrorTrace::new(self.clone()).to_structured();
crate::types::SessionRecord::FailedAction { error: self, trace }
}
}
fn detect_rate_limit(error: &GenerationError) -> bool {
let msg = error.to_string().to_lowercase();
msg.contains("rate limit") || msg.contains("429") || msg.contains("too many requests")
}
fn detect_network(error: &GenerationError) -> bool {
let msg = error.to_string().to_lowercase();
msg.contains("connection")
|| msg.contains("timeout")
|| msg.contains("timed out")
|| msg.contains("dns")
|| msg.contains("network")
|| msg.contains("connect")
}
#[derive(Debug, Clone, PartialEq)]
pub enum AgentAction {
Continue,
RetryWithReducedContext,
SwitchModel,
Terminate(AgenticError),
}
pub trait ErrorPolicyDeriver: Sync + Send {
fn derive_action(&self, error: AgenticError) -> AgentAction;
}
pub struct FnErrorPolicyDeriver<F> {
f: F,
}
impl<F> FnErrorPolicyDeriver<F>
where
F: Fn(AgenticError) -> AgentAction + Sync + Send + 'static,
{
pub fn new(f: F) -> Self {
Self { f }
}
}
impl<F> ErrorPolicyDeriver for FnErrorPolicyDeriver<F>
where
F: Fn(AgenticError) -> AgentAction + Sync + Send + 'static,
{
fn derive_action(&self, error: AgenticError) -> AgentAction {
(self.f)(error)
}
}
#[derive(Clone, Default)]
pub struct ErrorPolicy {
_private: (),
custom_classifier: Option<Arc<Box<dyn ErrorPolicyDeriver>>>,
}
impl Debug for ErrorPolicy {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("ErrorPolicy")
.field("custom_classifier", &self.custom_classifier.is_some())
.finish_non_exhaustive()
}
}
impl ErrorPolicy {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn customize<F>(f: F) -> Self
where
F: Fn(AgenticError) -> AgentAction + Sync + Send + 'static,
{
Self {
_private: (),
custom_classifier: Some(Arc::new(Box::new(FnErrorPolicyDeriver::new(f)))),
}
}
#[must_use]
pub fn classify(&self, error: AgenticError) -> AgentAction {
if let Some(classifier) = &self.custom_classifier {
return classifier.derive_action(error);
}
match error {
AgenticError::Generation(GenerationFailure {
kind: GenKind::ContextOverflow,
..
}) => AgentAction::RetryWithReducedContext,
AgenticError::Generation(GenerationFailure {
kind: GenKind::RateLimit,
..
}) => AgentAction::SwitchModel,
AgenticError::ToolCall { .. } | AgenticError::LoopDetected(_) => AgentAction::Continue,
_ => AgentAction::Terminate(error),
}
}
}
#[derive(Debug, Clone)]
pub struct CircuitBreaker {
failures: u32,
threshold: u32,
fallbacks: Vec<ModelId>,
idx: usize,
}
impl CircuitBreaker {
#[must_use]
pub fn new(threshold: u32, fallbacks: Vec<ModelId>) -> Self {
Self {
failures: 0,
threshold: threshold.max(1),
fallbacks,
idx: 0,
}
}
pub fn on_failure(&mut self) -> Option<ModelId> {
self.failures += 1;
if self.failures < self.threshold {
return None;
}
let next = self.fallbacks.get(self.idx).cloned();
if next.is_some() {
self.idx += 1;
self.failures = 0;
}
next
}
pub fn on_success(&mut self) {
self.failures = 0;
}
#[must_use]
pub fn is_exhausted(&self) -> bool {
self.idx >= self.fallbacks.len()
}
}