Skip to main content

awp_types/
a2a.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5/// Agent-to-agent message following the AWP communication format.
6#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
7#[serde(rename_all = "camelCase")]
8pub struct A2aMessage {
9    pub id: Uuid,
10    pub sender: String,
11    pub recipient: String,
12    pub message_type: A2aMessageType,
13    pub timestamp: DateTime<Utc>,
14    pub payload: serde_json::Value,
15}
16
17/// Type of an agent-to-agent message.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
19#[serde(rename_all = "lowercase")]
20pub enum A2aMessageType {
21    Request,
22    Response,
23    Notification,
24    Error,
25}
26
27/// AWP-specific typed message categories for agent routing.
28///
29/// These extend the generic [`A2aMessageType`] with domain-specific semantics
30/// that enable typed dispatch in AWP gateways and agent meshes.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
32#[serde(rename_all = "snake_case")]
33pub enum AwpMessageType {
34    /// Visitor expressed purchase or service intent.
35    VisitorIntentSignal,
36    /// Content gap detected — missing or outdated information.
37    ContentGapSignal,
38    /// Payment intent lifecycle message.
39    PaymentIntent,
40    /// Escalation to human support.
41    SupportEscalation,
42    /// Review or feedback signal from a platform.
43    ReviewSignal,
44    /// Operational proposal (inventory, scheduling, etc.).
45    OperationsProposal,
46    /// Invoke a declared capability on a remote agent.
47    InvokeCapability,
48    /// Request UI rendering (dual-surface: HTML for humans, JSON-LD for agents).
49    RenderUi,
50    /// Proactive outbound message (follow-up, notification).
51    OutboundTrigger,
52}
53
54impl std::fmt::Display for AwpMessageType {
55    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56        match self {
57            Self::VisitorIntentSignal => write!(f, "visitor_intent_signal"),
58            Self::ContentGapSignal => write!(f, "content_gap_signal"),
59            Self::PaymentIntent => write!(f, "payment_intent"),
60            Self::SupportEscalation => write!(f, "support_escalation"),
61            Self::ReviewSignal => write!(f, "review_signal"),
62            Self::OperationsProposal => write!(f, "operations_proposal"),
63            Self::InvokeCapability => write!(f, "invoke_capability"),
64            Self::RenderUi => write!(f, "render_ui"),
65            Self::OutboundTrigger => write!(f, "outbound_trigger"),
66        }
67    }
68}
69
70/// AWP-typed agent-to-agent message with domain-specific routing.
71///
72/// Unlike [`A2aMessage`] which uses generic request/response types, this
73/// carries an [`AwpMessageType`] for typed dispatch in AWP gateways.
74#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
75#[serde(rename_all = "camelCase")]
76pub struct AwpTypedMessage {
77    pub id: Uuid,
78    pub sender: String,
79    pub recipient: String,
80    pub awp_type: AwpMessageType,
81    pub timestamp: DateTime<Utc>,
82    pub payload: serde_json::Value,
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn test_a2a_message_serde_round_trip() {
91        let msg = A2aMessage {
92            id: Uuid::now_v7(),
93            sender: "agent-a".to_string(),
94            recipient: "agent-b".to_string(),
95            message_type: A2aMessageType::Request,
96            timestamp: Utc::now(),
97            payload: serde_json::json!({"action": "greet"}),
98        };
99        let json = serde_json::to_string(&msg).unwrap();
100        let deserialized: A2aMessage = serde_json::from_str(&json).unwrap();
101        assert_eq!(msg, deserialized);
102    }
103
104    #[test]
105    fn test_message_type_serde() {
106        for mt in [
107            A2aMessageType::Request,
108            A2aMessageType::Response,
109            A2aMessageType::Notification,
110            A2aMessageType::Error,
111        ] {
112            let json = serde_json::to_string(&mt).unwrap();
113            let deserialized: A2aMessageType = serde_json::from_str(&json).unwrap();
114            assert_eq!(mt, deserialized);
115        }
116    }
117
118    #[test]
119    fn test_message_type_lowercase() {
120        assert_eq!(serde_json::to_string(&A2aMessageType::Request).unwrap(), "\"request\"");
121        assert_eq!(serde_json::to_string(&A2aMessageType::Response).unwrap(), "\"response\"");
122        assert_eq!(
123            serde_json::to_string(&A2aMessageType::Notification).unwrap(),
124            "\"notification\""
125        );
126        assert_eq!(serde_json::to_string(&A2aMessageType::Error).unwrap(), "\"error\"");
127    }
128
129    #[test]
130    fn test_awp_message_type_serde_round_trip() {
131        let types = [
132            AwpMessageType::VisitorIntentSignal,
133            AwpMessageType::ContentGapSignal,
134            AwpMessageType::PaymentIntent,
135            AwpMessageType::SupportEscalation,
136            AwpMessageType::ReviewSignal,
137            AwpMessageType::OperationsProposal,
138            AwpMessageType::InvokeCapability,
139            AwpMessageType::RenderUi,
140            AwpMessageType::OutboundTrigger,
141        ];
142        for mt in types {
143            let json = serde_json::to_string(&mt).unwrap();
144            let deserialized: AwpMessageType = serde_json::from_str(&json).unwrap();
145            assert_eq!(mt, deserialized);
146        }
147    }
148
149    #[test]
150    fn test_awp_message_type_snake_case() {
151        assert_eq!(
152            serde_json::to_string(&AwpMessageType::VisitorIntentSignal).unwrap(),
153            "\"visitor_intent_signal\""
154        );
155        assert_eq!(
156            serde_json::to_string(&AwpMessageType::PaymentIntent).unwrap(),
157            "\"payment_intent\""
158        );
159        assert_eq!(serde_json::to_string(&AwpMessageType::RenderUi).unwrap(), "\"render_ui\"");
160    }
161
162    #[test]
163    fn test_awp_message_type_display() {
164        assert_eq!(AwpMessageType::VisitorIntentSignal.to_string(), "visitor_intent_signal");
165        assert_eq!(AwpMessageType::SupportEscalation.to_string(), "support_escalation");
166        assert_eq!(AwpMessageType::OutboundTrigger.to_string(), "outbound_trigger");
167    }
168
169    #[test]
170    fn test_awp_typed_message_serde_round_trip() {
171        let msg = AwpTypedMessage {
172            id: Uuid::now_v7(),
173            sender: "visitor-agent".to_string(),
174            recipient: "payment-agent".to_string(),
175            awp_type: AwpMessageType::PaymentIntent,
176            timestamp: Utc::now(),
177            payload: serde_json::json!({"sku": "WIDGET-001", "amount": 2500}),
178        };
179        let json = serde_json::to_string(&msg).unwrap();
180        let deserialized: AwpTypedMessage = serde_json::from_str(&json).unwrap();
181        assert_eq!(msg, deserialized);
182    }
183}