use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HookStage {
PreCompletion,
PostCompletion,
PreToolCall,
PostToolCall,
FilterTools,
OnAssistantMessage,
}
impl fmt::Display for HookStage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
HookStage::PreCompletion => write!(f, "pre_completion"),
HookStage::PostCompletion => write!(f, "post_completion"),
HookStage::PreToolCall => write!(f, "pre_tool_call"),
HookStage::PostToolCall => write!(f, "post_tool_call"),
HookStage::FilterTools => write!(f, "filter_tools"),
HookStage::OnAssistantMessage => write!(f, "on_assistant_message"),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum HookError {
#[error("Hook '{hook_name}' failed at {stage}: {message}")]
HookFailed {
hook_name: String,
stage: HookStage,
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("Hook chain cancelled")]
Cancelled,
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
}
impl HookError {
pub fn hook_failed(
hook_name: impl Into<String>,
stage: HookStage,
message: impl Into<String>,
) -> Self {
Self::HookFailed {
hook_name: hook_name.into(),
stage,
message: message.into(),
source: None,
}
}
pub fn hook_failed_with_source(
hook_name: impl Into<String>,
stage: HookStage,
message: impl Into<String>,
source: impl std::error::Error + Send + Sync + 'static,
) -> Self {
Self::HookFailed {
hook_name: hook_name.into(),
stage,
message: message.into(),
source: Some(Box::new(source)),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum CompletionError {
#[error("HTTP error: {0}")]
HttpError(String),
#[error("JSON error: {0}")]
JsonError(#[from] serde_json::Error),
#[error("Provider error: {0}")]
ProviderError(String),
#[error("Rate limited: retry after {retry_after_secs:?} seconds")]
RateLimited {
retry_after_secs: Option<u64>,
},
#[error("Invalid request: {0}")]
InvalidRequest(String),
#[error("Authentication failed")]
AuthenticationFailed,
#[error("Request cancelled")]
Cancelled,
#[error("Hook error: {0}")]
HookError(#[from] HookError),
#[error("Tool error: {0}")]
ToolError(#[from] ToolError),
}
impl From<reqwest::Error> for CompletionError {
fn from(err: reqwest::Error) -> Self {
CompletionError::HttpError(err.to_string())
}
}
impl CompletionError {
pub fn is_retryable(&self) -> bool {
match self {
CompletionError::RateLimited { .. } => true,
CompletionError::HttpError(msg) => {
let lower = msg.to_lowercase();
lower.contains("timeout")
|| lower.contains("connection")
|| lower.contains("network")
|| lower.contains("500")
|| lower.contains("502")
|| lower.contains("503")
|| lower.contains("504")
|| lower.contains("529")
|| lower.contains("overloaded")
|| lower.contains("temporarily unavailable")
|| lower.contains("error sending request")
|| lower.contains("error receiving")
|| lower.contains("broken pipe")
|| lower.contains("reset by peer")
|| lower.contains("timed out")
|| lower.contains("error decoding")
}
CompletionError::ProviderError(msg) => {
let lower = msg.to_lowercase();
lower.contains("timeout")
|| lower.contains("overload")
|| lower.contains("capacity")
|| lower.contains("unavailable")
}
CompletionError::JsonError(_) => false,
CompletionError::InvalidRequest(_) => false,
CompletionError::AuthenticationFailed => false,
CompletionError::Cancelled => false,
CompletionError::HookError(_) => false,
CompletionError::ToolError(_) => false,
}
}
pub fn retry_after_secs(&self) -> Option<u64> {
match self {
CompletionError::RateLimited { retry_after_secs } => *retry_after_secs,
_ => None,
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum ToolError {
#[error("Tool execution failed: {0}")]
ExecutionFailed(String),
#[error("Tool not found: {0}")]
NotFound(String),
#[error("Invalid arguments: {0}")]
InvalidArguments(#[from] serde_json::Error),
#[error("Tool call interrupted")]
Interrupted,
#[error("MCP error: {0}")]
McpError(String),
#[error("Tool call blocked: {0}")]
Blocked(String),
}
#[derive(Debug, thiserror::Error)]
pub enum RegistryError {
#[error("Agent '{0}' already registered")]
AlreadyRegistered(String),
#[error("Agent '{0}' not found")]
NotFound(String),
#[error("Maximum invocation depth ({0}) exceeded - possible cycle detected")]
MaxDepthExceeded(usize),
#[error("Invocation failed: {0}")]
InvocationFailed(String),
}
pub type HookResult<T> = Result<T, HookError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hook_stage_display() {
assert_eq!(HookStage::PreCompletion.to_string(), "pre_completion");
assert_eq!(HookStage::PostCompletion.to_string(), "post_completion");
assert_eq!(HookStage::PreToolCall.to_string(), "pre_tool_call");
assert_eq!(HookStage::PostToolCall.to_string(), "post_tool_call");
assert_eq!(HookStage::FilterTools.to_string(), "filter_tools");
assert_eq!(
HookStage::OnAssistantMessage.to_string(),
"on_assistant_message"
);
}
#[test]
fn test_hook_error_display() {
let err = HookError::hook_failed("TestHook", HookStage::PreCompletion, "test error");
assert!(err.to_string().contains("TestHook"));
assert!(err.to_string().contains("pre_completion"));
assert!(err.to_string().contains("test error"));
}
#[test]
fn test_completion_error_from_reqwest() {
let err = CompletionError::HttpError("connection refused".to_string());
assert!(err.to_string().contains("connection refused"));
}
#[test]
fn test_http_error_retryable_patterns() {
assert!(CompletionError::HttpError("connection refused".to_string()).is_retryable());
assert!(CompletionError::HttpError("timeout occurred".to_string()).is_retryable());
assert!(CompletionError::HttpError("network unreachable".to_string()).is_retryable());
assert!(CompletionError::HttpError("502 bad gateway".to_string()).is_retryable());
assert!(CompletionError::HttpError("503 service unavailable".to_string()).is_retryable());
assert!(
CompletionError::HttpError("HTTP 529: overloaded_error".to_string()).is_retryable()
);
assert!(
CompletionError::HttpError("anthropic overloaded_error (529)".to_string())
.is_retryable()
);
assert!(CompletionError::HttpError(
"error sending request for url (https://api.example.com/v1/completions)".to_string()
)
.is_retryable());
assert!(
CompletionError::HttpError("error receiving response body".to_string()).is_retryable()
);
assert!(CompletionError::HttpError("broken pipe".to_string()).is_retryable());
assert!(CompletionError::HttpError("reset by peer".to_string()).is_retryable());
assert!(CompletionError::HttpError("operation timed out".to_string()).is_retryable());
assert!(
CompletionError::HttpError("error decoding response body".to_string()).is_retryable()
);
assert!(!CompletionError::HttpError("400 bad request".to_string()).is_retryable());
assert!(!CompletionError::HttpError("401 unauthorized".to_string()).is_retryable());
assert!(!CompletionError::HttpError("invalid json response".to_string()).is_retryable());
}
}