use std::fmt;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum A2AError {
#[error("validation error: {0}")]
Validation(String),
#[error(
"invalid state transition from `{from}` to `{to}`: allowed transitions are [{allowed}]"
)]
InvalidTransition {
from: String,
to: String,
allowed: String,
},
#[error("{entity} not found")]
NotFound {
entity: &'static str,
},
#[error(
"rounding drift: total distributed {distributed} does not equal input total {expected}"
)]
RoundingDrift {
distributed: rust_decimal::Decimal,
expected: rust_decimal::Decimal,
},
#[error("SSRF blocked: {0}")]
SsrfBlocked(String),
#[error("HMAC verification failed")]
HmacVerificationFailed,
#[error(
"invalid billing interval: `{0}`. Must be one of: weekly, biweekly, monthly, quarterly, annual"
)]
InvalidBillingInterval(String),
#[error("release conditions not met: {unmet_descriptions:?}")]
ConditionsNotMet {
unmet_descriptions: Vec<String>,
},
#[error("recipient percentages must sum to 100, got {actual}")]
PercentageSumMismatch {
actual: rust_decimal::Decimal,
},
#[error("fixed recipient amounts must sum to {expected}, got {actual}")]
FixedSumMismatch {
expected: rust_decimal::Decimal,
actual: rust_decimal::Decimal,
},
#[error("dispute error: {0}")]
DisputeError(String),
#[error("score out of range: {value} (must be 1–5)")]
ScoreOutOfRange {
value: rust_decimal::Decimal,
},
#[error("circuit breaker blocked: {reason}")]
CircuitBreakerBlocked {
reason: String,
},
#[error("spending limit exceeded: {limit_type} limit is {limit}, attempted {attempted}")]
SpendingLimitExceeded {
limit_type: String,
limit: rust_decimal::Decimal,
attempted: rust_decimal::Decimal,
},
#[error("marketplace error: {0}")]
MarketplaceError(String),
#[error("SLA violation: {metric} — actual {actual}, required {required}")]
SlaViolation {
metric: String,
actual: rust_decimal::Decimal,
required: rust_decimal::Decimal,
},
#[error("agent card error: {0}")]
AgentCardError(String),
#[error("failure rate exceeded: {rate}% (threshold: {threshold}%)")]
FailureRateExceeded {
rate: rust_decimal::Decimal,
threshold: rust_decimal::Decimal,
},
#[error("RFQ expired: {rfq_id}")]
RfqExpired {
rfq_id: String,
},
#[error("dispute deadline missed: {deadline_type}")]
DeadlineMissed {
deadline_type: String,
},
#[error("trust tier requirement not met: requires {required}, agent has {actual}")]
TrustTierInsufficient {
required: String,
actual: String,
},
#[error("negotiation round limit exceeded: {max_rounds} rounds maximum")]
NegotiationLimitExceeded {
max_rounds: u32,
},
#[error("{0}")]
Internal(String),
}
pub type A2AResult<T> = Result<T, A2AError>;
impl A2AError {
pub fn validation(msg: impl fmt::Display) -> Self {
Self::Validation(msg.to_string())
}
pub fn invalid_transition(
from: impl fmt::Display,
to: impl fmt::Display,
allowed: &[&str],
) -> Self {
Self::InvalidTransition {
from: from.to_string(),
to: to.to_string(),
allowed: allowed.join(", "),
}
}
pub fn dispute(msg: impl fmt::Display) -> Self {
Self::DisputeError(msg.to_string())
}
pub fn marketplace(msg: impl fmt::Display) -> Self {
Self::MarketplaceError(msg.to_string())
}
pub fn agent_card(msg: impl fmt::Display) -> Self {
Self::AgentCardError(msg.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validation_error_display() {
let err = A2AError::validation("senderAddress is required");
assert_eq!(err.to_string(), "validation error: senderAddress is required");
}
#[test]
fn invalid_transition_display() {
let err = A2AError::invalid_transition("pending", "released", &["processing"]);
assert!(err.to_string().contains("pending"));
assert!(err.to_string().contains("released"));
assert!(err.to_string().contains("processing"));
}
#[test]
fn not_found_display() {
let err = A2AError::NotFound { entity: "subscription" };
assert_eq!(err.to_string(), "subscription not found");
}
#[test]
fn ssrf_blocked_display() {
let err = A2AError::SsrfBlocked("localhost".into());
assert!(err.to_string().contains("localhost"));
}
#[test]
fn percentage_sum_mismatch_display() {
use rust_decimal_macros::dec;
let err = A2AError::PercentageSumMismatch { actual: dec!(95) };
assert!(err.to_string().contains("95"));
}
#[test]
fn dispute_error_display() {
let err = A2AError::dispute("evidence period expired");
assert!(err.to_string().contains("evidence period expired"));
}
#[test]
fn score_out_of_range_display() {
use rust_decimal_macros::dec;
let err = A2AError::ScoreOutOfRange { value: dec!(6) };
assert!(err.to_string().contains("6"));
}
#[test]
fn circuit_breaker_blocked_display() {
let err = A2AError::CircuitBreakerBlocked { reason: "open state".into() };
assert!(err.to_string().contains("open state"));
}
#[test]
fn spending_limit_exceeded_display() {
use rust_decimal_macros::dec;
let err = A2AError::SpendingLimitExceeded {
limit_type: "daily".into(),
limit: dec!(10000),
attempted: dec!(15000),
};
let msg = err.to_string();
assert!(msg.contains("daily"));
assert!(msg.contains("10000"));
assert!(msg.contains("15000"));
}
#[test]
fn marketplace_error_display() {
let err = A2AError::marketplace("RFQ already awarded");
assert!(err.to_string().contains("RFQ already awarded"));
}
#[test]
fn sla_violation_display() {
use rust_decimal_macros::dec;
let err = A2AError::SlaViolation {
metric: "uptime_percent".into(),
actual: dec!(95.5),
required: dec!(99.9),
};
let msg = err.to_string();
assert!(msg.contains("uptime_percent"));
assert!(msg.contains("95.5"));
assert!(msg.contains("99.9"));
}
#[test]
fn agent_card_error_display() {
let err = A2AError::agent_card("wallet address required");
assert!(err.to_string().contains("wallet address required"));
}
#[test]
fn failure_rate_exceeded_display() {
use rust_decimal_macros::dec;
let err = A2AError::FailureRateExceeded { rate: dec!(45), threshold: dec!(30) };
let msg = err.to_string();
assert!(msg.contains("45"));
assert!(msg.contains("30"));
}
#[test]
fn rfq_expired_display() {
let err = A2AError::RfqExpired { rfq_id: "rfq_123".into() };
assert!(err.to_string().contains("rfq_123"));
}
#[test]
fn deadline_missed_display() {
let err = A2AError::DeadlineMissed { deadline_type: "evidence_period".into() };
assert!(err.to_string().contains("evidence_period"));
}
#[test]
fn trust_tier_insufficient_display() {
let err = A2AError::TrustTierInsufficient {
required: "verified".into(),
actual: "sandbox".into(),
};
let msg = err.to_string();
assert!(msg.contains("verified"));
assert!(msg.contains("sandbox"));
}
#[test]
fn negotiation_limit_exceeded_display() {
let err = A2AError::NegotiationLimitExceeded { max_rounds: 5 };
assert!(err.to_string().contains("5"));
}
}