use std::collections::VecDeque;
use chrono::{DateTime, TimeDelta, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::error::{A2AError, A2AResult};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum MessageStatus {
Pending,
Delivered,
Acknowledged,
Failed,
Expired,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum MessageType {
QuoteRequest,
QuoteResponse,
CounterOffer,
PurchaseIntent,
DeliveryNotification,
DisputeNotice,
Custom(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct A2AMessage {
pub id: Uuid,
pub conversation_id: Uuid,
pub from_agent_id: Uuid,
pub to_agent_id: Uuid,
pub message_type: MessageType,
pub payload: String,
pub status: MessageStatus,
pub sequence_number: u64,
pub attempts: u32,
pub max_attempts: u32,
pub next_retry_at: Option<DateTime<Utc>>,
pub acknowledged_at: Option<DateTime<Utc>>,
pub error: Option<String>,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Conversation {
pub id: Uuid,
pub agent_a: Uuid,
pub agent_b: Uuid,
pub messages: Vec<A2AMessage>,
pub created_at: DateTime<Utc>,
}
const MAX_BACKOFF_SECS: i64 = 16;
#[derive(Debug)]
pub struct MessageQueue {
queue: VecDeque<A2AMessage>,
}
impl MessageQueue {
#[must_use]
pub const fn new() -> Self {
Self { queue: VecDeque::new() }
}
pub fn enqueue(&mut self, message: A2AMessage) -> Uuid {
let id = message.id;
self.queue.push_back(message);
id
}
pub fn acknowledge(&mut self, message_id: Uuid) -> A2AResult<()> {
let msg = self
.queue
.iter_mut()
.find(|m| m.id == message_id)
.ok_or(A2AError::NotFound { entity: "message" })?;
msg.status = MessageStatus::Acknowledged;
msg.acknowledged_at = Some(Utc::now());
Ok(())
}
pub fn mark_failed(&mut self, message_id: Uuid, error: String) -> A2AResult<()> {
let msg = self
.queue
.iter_mut()
.find(|m| m.id == message_id)
.ok_or(A2AError::NotFound { entity: "message" })?;
msg.attempts += 1;
msg.error = Some(error);
if msg.attempts >= msg.max_attempts {
msg.status = MessageStatus::Expired;
msg.next_retry_at = None;
} else {
msg.status = MessageStatus::Failed;
msg.next_retry_at = Some(Self::compute_next_retry(msg.attempts));
}
Ok(())
}
#[must_use]
pub fn get_pending(&self) -> Vec<A2AMessage> {
self.queue.iter().filter(|m| m.status == MessageStatus::Pending).cloned().collect()
}
#[must_use]
pub fn get_for_retry(&self) -> Vec<A2AMessage> {
let now = Utc::now();
self.queue
.iter()
.filter(|m| {
m.status == MessageStatus::Failed && m.next_retry_at.is_some_and(|t| t <= now)
})
.cloned()
.collect()
}
#[must_use]
pub fn compute_next_retry(attempts: u32) -> DateTime<Utc> {
let secs = 1i64
.checked_shl(attempts.saturating_sub(1))
.unwrap_or(MAX_BACKOFF_SECS)
.min(MAX_BACKOFF_SECS);
Utc::now()
+ TimeDelta::try_seconds(secs)
.unwrap_or_else(|| TimeDelta::try_seconds(MAX_BACKOFF_SECS).expect("16s is valid"))
}
}
impl Default for MessageQueue {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_message() -> A2AMessage {
A2AMessage {
id: Uuid::new_v4(),
conversation_id: Uuid::new_v4(),
from_agent_id: Uuid::new_v4(),
to_agent_id: Uuid::new_v4(),
message_type: MessageType::QuoteRequest,
payload: r#"{"item":"widget","qty":10}"#.into(),
status: MessageStatus::Pending,
sequence_number: 1,
attempts: 0,
max_attempts: 5,
next_retry_at: None,
acknowledged_at: None,
error: None,
created_at: Utc::now(),
}
}
#[test]
fn enqueue_and_get_pending() {
let mut q = MessageQueue::new();
let msg = make_message();
let id = q.enqueue(msg);
let pending = q.get_pending();
assert_eq!(pending.len(), 1);
assert_eq!(pending[0].id, id);
assert_eq!(pending[0].status, MessageStatus::Pending);
}
#[test]
fn acknowledge_marks_delivered() {
let mut q = MessageQueue::new();
let msg = make_message();
let id = q.enqueue(msg);
q.acknowledge(id).expect("acknowledge should succeed");
assert!(q.get_pending().is_empty());
let acked = q.queue.iter().find(|m| m.id == id).expect("message exists");
assert_eq!(acked.status, MessageStatus::Acknowledged);
assert!(acked.acknowledged_at.is_some());
}
#[test]
fn failed_message_schedules_retry() {
let mut q = MessageQueue::new();
let msg = make_message();
let id = q.enqueue(msg);
q.mark_failed(id, "connection refused".into()).expect("mark_failed should succeed");
let entry = q.queue.iter().find(|m| m.id == id).expect("message exists");
assert_eq!(entry.status, MessageStatus::Failed);
assert_eq!(entry.attempts, 1);
assert!(entry.next_retry_at.is_some());
assert_eq!(entry.error.as_deref(), Some("connection refused"));
}
#[test]
fn exponential_backoff_capped_at_16s() {
let t1 = MessageQueue::compute_next_retry(1);
let t2 = MessageQueue::compute_next_retry(2);
let t3 = MessageQueue::compute_next_retry(3);
let t4 = MessageQueue::compute_next_retry(4);
let t5 = MessageQueue::compute_next_retry(5);
let t6 = MessageQueue::compute_next_retry(6);
let now = Utc::now();
let delta = |t: DateTime<Utc>| (t - now).num_milliseconds();
assert!(delta(t1) >= 800, "attempt 1 should be ~1 s");
assert!(delta(t2) >= 1_800, "attempt 2 should be ~2 s");
assert!(delta(t3) >= 3_800, "attempt 3 should be ~4 s");
assert!(delta(t4) >= 7_800, "attempt 4 should be ~8 s");
assert!(delta(t5) >= 15_800, "attempt 5 should be ~16 s");
assert!(delta(t6) <= 18_000, "attempt 6 should be capped at ~16 s");
assert!(delta(t6) >= 15_800, "attempt 6 should still be ~16 s");
}
}