#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum AgentRuntimeError {
#[error("Memory operation failed: {0}")]
Memory(String),
#[error("Graph operation failed: {0}")]
Graph(String),
#[error("Orchestration failed: {0}")]
Orchestration(String),
#[error("Agent loop error: {0}")]
AgentLoop(String),
#[error("Runtime not configured: missing '{0}'")]
NotConfigured(&'static str),
#[error("Circuit breaker open for '{service}'")]
CircuitOpen {
service: String,
},
#[error("Backpressure threshold exceeded: queue depth {depth}/{capacity}")]
BackpressureShed {
depth: usize,
capacity: usize,
},
#[error("Deduplication key collision: {key}")]
DeduplicationConflict {
key: String,
},
#[error("Provider error: {0}")]
Provider(String),
#[error("Persistence error: {0}")]
Persistence(String),
#[error("Validation failed for field '{field}': [{code}] {message}")]
Validation {
field: String,
code: String,
message: String,
},
}
impl AgentRuntimeError {
pub fn is_circuit_open(&self) -> bool {
matches!(self, Self::CircuitOpen { .. })
}
pub fn is_backpressure(&self) -> bool {
matches!(self, Self::BackpressureShed { .. })
}
pub fn is_provider(&self) -> bool {
matches!(self, Self::Provider(_))
}
pub fn is_validation(&self) -> bool {
matches!(self, Self::Validation { .. })
}
pub fn is_memory(&self) -> bool {
matches!(self, Self::Memory(_))
}
pub fn is_graph(&self) -> bool {
matches!(self, Self::Graph(_))
}
pub fn is_agent_loop(&self) -> bool {
matches!(self, Self::AgentLoop(_))
}
pub fn is_orchestration(&self) -> bool {
matches!(self, Self::Orchestration(_))
}
pub fn is_persistence(&self) -> bool {
matches!(self, Self::Persistence(_))
}
pub fn is_not_configured(&self) -> bool {
matches!(self, Self::NotConfigured(_))
}
pub fn is_deduplication_conflict(&self) -> bool {
matches!(self, Self::DeduplicationConflict { .. })
}
pub fn is_retryable(&self) -> bool {
matches!(self, Self::Provider(_) | Self::Persistence(_))
}
pub fn message(&self) -> String {
match self {
Self::Memory(s)
| Self::Graph(s)
| Self::Orchestration(s)
| Self::AgentLoop(s)
| Self::Provider(s)
| Self::Persistence(s) => s.clone(),
Self::NotConfigured(s) => s.to_string(),
Self::CircuitOpen { service } => format!("circuit open for '{service}'"),
Self::BackpressureShed { depth, capacity } => {
format!("backpressure: queue depth {depth}/{capacity}")
}
Self::DeduplicationConflict { key } => format!("dedup conflict: {key}"),
Self::Validation { field, code, message } => {
format!("[{code}] {field}: {message}")
}
}
}
}
impl From<serde_json::Error> for AgentRuntimeError {
fn from(e: serde_json::Error) -> Self {
AgentRuntimeError::AgentLoop(format!("JSON error: {e}"))
}
}
impl From<std::io::Error> for AgentRuntimeError {
fn from(e: std::io::Error) -> Self {
AgentRuntimeError::Persistence(format!("I/O error: {e}"))
}
}
impl From<Box<dyn std::error::Error + Send + Sync>> for AgentRuntimeError {
fn from(e: Box<dyn std::error::Error + Send + Sync>) -> Self {
AgentRuntimeError::AgentLoop(e.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_memory_error_display() {
let e = AgentRuntimeError::Memory("store full".into());
assert_eq!(e.to_string(), "Memory operation failed: store full");
}
#[test]
fn test_graph_error_display() {
let e = AgentRuntimeError::Graph("entity not found".into());
assert_eq!(e.to_string(), "Graph operation failed: entity not found");
}
#[test]
fn test_orchestration_error_display() {
let e = AgentRuntimeError::Orchestration("pipeline stalled".into());
assert_eq!(e.to_string(), "Orchestration failed: pipeline stalled");
}
#[test]
fn test_agent_loop_error_display() {
let e = AgentRuntimeError::AgentLoop("max iterations".into());
assert_eq!(e.to_string(), "Agent loop error: max iterations");
}
#[test]
fn test_not_configured_error_display() {
let e = AgentRuntimeError::NotConfigured("memory");
assert_eq!(e.to_string(), "Runtime not configured: missing 'memory'");
}
#[test]
fn test_circuit_open_error_display() {
let e = AgentRuntimeError::CircuitOpen {
service: "llm-api".into(),
};
assert_eq!(e.to_string(), "Circuit breaker open for 'llm-api'");
}
#[test]
fn test_backpressure_shed_error_display() {
let e = AgentRuntimeError::BackpressureShed {
depth: 100,
capacity: 100,
};
assert_eq!(
e.to_string(),
"Backpressure threshold exceeded: queue depth 100/100"
);
}
#[test]
fn test_deduplication_conflict_display() {
let e = AgentRuntimeError::DeduplicationConflict {
key: "abc123".into(),
};
assert_eq!(e.to_string(), "Deduplication key collision: abc123");
}
#[test]
fn test_error_is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<AgentRuntimeError>();
}
#[test]
fn test_error_debug_format() {
let e = AgentRuntimeError::Memory("test".into());
let debug = format!("{:?}", e);
assert!(debug.contains("Memory"));
}
#[test]
fn test_validation_error_display() {
let e = AgentRuntimeError::Validation {
field: "n".into(),
code: "out_of_range".into(),
message: "n must be between 1 and 100".into(),
};
assert_eq!(
e.to_string(),
"Validation failed for field 'n': [out_of_range] n must be between 1 and 100"
);
}
#[test]
fn test_is_circuit_open_true() {
let e = AgentRuntimeError::CircuitOpen { service: "svc".into() };
assert!(e.is_circuit_open());
assert!(!e.is_backpressure());
assert!(!e.is_provider());
assert!(!e.is_validation());
assert!(!e.is_memory());
assert!(!e.is_graph());
}
#[test]
fn test_is_backpressure_true() {
let e = AgentRuntimeError::BackpressureShed { depth: 5, capacity: 5 };
assert!(e.is_backpressure());
assert!(!e.is_circuit_open());
}
#[test]
fn test_is_provider_true() {
let e = AgentRuntimeError::Provider("timeout".into());
assert!(e.is_provider());
assert!(!e.is_memory());
}
#[test]
fn test_is_validation_true() {
let e = AgentRuntimeError::Validation {
field: "x".into(),
code: "bad".into(),
message: "msg".into(),
};
assert!(e.is_validation());
assert!(!e.is_graph());
}
#[test]
fn test_is_memory_true() {
let e = AgentRuntimeError::Memory("oom".into());
assert!(e.is_memory());
assert!(!e.is_validation());
}
#[test]
fn test_is_graph_true() {
let e = AgentRuntimeError::Graph("no such entity".into());
assert!(e.is_graph());
assert!(!e.is_memory());
}
#[test]
fn test_is_persistence_true() {
let e = AgentRuntimeError::Persistence("disk full".into());
assert!(e.is_persistence());
assert!(!e.is_memory());
}
#[test]
fn test_is_not_configured_true() {
let e = AgentRuntimeError::NotConfigured("graph");
assert!(e.is_not_configured());
assert!(!e.is_persistence());
}
#[test]
fn test_is_deduplication_conflict_true() {
let e = AgentRuntimeError::DeduplicationConflict { key: "req-1".into() };
assert!(e.is_deduplication_conflict());
assert!(!e.is_circuit_open());
}
#[test]
fn test_is_retryable_true_for_provider() {
let e = AgentRuntimeError::Provider("503".into());
assert!(e.is_retryable());
}
#[test]
fn test_is_retryable_true_for_persistence() {
let e = AgentRuntimeError::Persistence("io error".into());
assert!(e.is_retryable());
}
#[test]
fn test_is_retryable_false_for_logic_errors() {
assert!(!AgentRuntimeError::Memory("x".into()).is_retryable());
assert!(!AgentRuntimeError::Graph("x".into()).is_retryable());
assert!(!AgentRuntimeError::Orchestration("x".into()).is_retryable());
assert!(!AgentRuntimeError::CircuitOpen { service: "s".into() }.is_retryable());
}
#[test]
fn test_from_serde_json_error() {
let json_err = serde_json::from_str::<serde_json::Value>("{invalid}").unwrap_err();
let e = AgentRuntimeError::from(json_err);
assert!(matches!(e, AgentRuntimeError::AgentLoop(_)));
}
#[test]
fn test_provider_error_display() {
let e = AgentRuntimeError::Provider("rate limited".into());
assert!(e.to_string().contains("rate limited"));
}
#[test]
fn test_persistence_error_display() {
let e = AgentRuntimeError::Persistence("file not found".into());
assert!(e.to_string().contains("file not found"));
}
#[test]
fn test_is_agent_loop_true_for_agent_loop_variant() {
let e = AgentRuntimeError::AgentLoop("step failed".into());
assert!(e.is_agent_loop());
}
#[test]
fn test_is_agent_loop_false_for_other_variants() {
let e = AgentRuntimeError::Memory("oom".into());
assert!(!e.is_agent_loop());
}
#[test]
fn test_is_orchestration_true_for_orchestration_variant() {
let e = AgentRuntimeError::Orchestration("pipeline stalled".into());
assert!(e.is_orchestration());
}
#[test]
fn test_is_orchestration_false_for_other_variants() {
let e = AgentRuntimeError::Graph("cycle".into());
assert!(!e.is_orchestration());
}
#[test]
fn test_from_boxed_error_produces_agent_loop_variant() {
let boxed: Box<dyn std::error::Error + Send + Sync> =
Box::new(std::io::Error::new(std::io::ErrorKind::Other, "generic failure"));
let e = AgentRuntimeError::from(boxed);
assert!(matches!(e, AgentRuntimeError::AgentLoop(_)));
assert!(e.to_string().contains("generic failure"));
}
#[test]
fn test_from_boxed_error_preserves_message() {
let boxed: Box<dyn std::error::Error + Send + Sync> =
"custom error message".parse::<i32>().unwrap_err().into();
let e = AgentRuntimeError::from(boxed);
assert!(e.is_agent_loop());
}
#[test]
fn test_message_returns_inner_string_for_memory_variant() {
let e = AgentRuntimeError::Memory("store full".into());
assert_eq!(e.message(), "store full");
}
#[test]
fn test_message_returns_inner_string_for_provider_variant() {
let e = AgentRuntimeError::Provider("timeout".into());
assert_eq!(e.message(), "timeout");
}
#[test]
fn test_message_returns_structured_text_for_circuit_open() {
let e = AgentRuntimeError::CircuitOpen { service: "llm".into() };
assert!(e.message().contains("llm"));
}
#[test]
fn test_message_returns_structured_text_for_validation() {
let e = AgentRuntimeError::Validation {
field: "n".into(),
code: "range".into(),
message: "must be positive".into(),
};
let msg = e.message();
assert!(msg.contains("n") && msg.contains("must be positive"));
}
#[test]
fn test_message_returns_structured_text_for_backpressure() {
let e = AgentRuntimeError::BackpressureShed { depth: 10, capacity: 10 };
let msg = e.message();
assert!(msg.contains("10"));
}
}