vtcode_acp_client/
messages.rs

1//! ACP message types and serialization
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use uuid::Uuid;
6
7/// Core ACP message envelope
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct AcpMessage {
10    /// Unique message ID
11    pub id: String,
12
13    /// Message type (request, response, etc.)
14    #[serde(rename = "type")]
15    pub message_type: MessageType,
16
17    /// Sender agent ID
18    pub sender: String,
19
20    /// Recipient agent ID
21    pub recipient: String,
22
23    /// Message content
24    pub content: MessageContent,
25
26    /// Timestamp (ISO 8601)
27    pub timestamp: String,
28
29    /// Optional correlation ID for request/response pairs
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub correlation_id: Option<String>,
32}
33
34/// Message type enumeration
35#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
36#[serde(rename_all = "lowercase")]
37pub enum MessageType {
38    Request,
39    Response,
40    Error,
41    Notification,
42}
43
44/// Message content payload
45#[derive(Debug, Clone, Serialize, Deserialize)]
46#[serde(untagged)]
47pub enum MessageContent {
48    /// Request to execute a tool or action
49    Request(AcpRequest),
50
51    /// Response with results
52    Response(AcpResponse),
53
54    /// Error response
55    Error(ErrorPayload),
56
57    /// Generic notification
58    Notification(NotificationPayload),
59}
60
61/// ACP request structure
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct AcpRequest {
64    /// Action/tool name to execute
65    pub action: String,
66
67    /// Arguments for the action (any JSON-serializable data)
68    pub args: Value,
69
70    /// Optional timeout in seconds
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub timeout_secs: Option<u64>,
73
74    /// Whether to await response synchronously
75    #[serde(default)]
76    pub sync: bool,
77}
78
79/// ACP response structure
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct AcpResponse {
82    /// Execution status
83    pub status: ResponseStatus,
84
85    /// Result data (on success)
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub result: Option<Value>,
88
89    /// Error details (on failure)
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub error: Option<ErrorDetails>,
92
93    /// Execution time in milliseconds
94    pub execution_time_ms: u64,
95}
96
97/// Response status enum
98#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
99#[serde(rename_all = "lowercase")]
100pub enum ResponseStatus {
101    Success,
102    Failed,
103    Timeout,
104    Partial,
105}
106
107/// Error response payload
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct ErrorPayload {
110    /// Error code (ACP standard or custom)
111    pub code: String,
112
113    /// Human-readable error message
114    pub message: String,
115
116    /// Additional error details
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub details: Option<Value>,
119}
120
121/// Error details in response
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct ErrorDetails {
124    /// Error code
125    pub code: String,
126
127    /// Error message
128    pub message: String,
129
130    /// Additional context
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub context: Option<Value>,
133}
134
135/// Notification payload for one-way messages
136#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct NotificationPayload {
138    /// Notification type
139    pub event: String,
140
141    /// Event-specific data
142    pub data: Value,
143}
144
145impl AcpMessage {
146    /// Create a new ACP request message
147    pub fn request(sender: String, recipient: String, action: String, args: Value) -> Self {
148        Self {
149            id: Uuid::new_v4().to_string(),
150            message_type: MessageType::Request,
151            sender,
152            recipient,
153            content: MessageContent::Request(AcpRequest {
154                action,
155                args,
156                timeout_secs: None,
157                sync: true,
158            }),
159            timestamp: chrono::Utc::now().to_rfc3339(),
160            correlation_id: None,
161        }
162    }
163
164    /// Create a new ACP response message
165    pub fn response(
166        sender: String,
167        recipient: String,
168        result: Value,
169        correlation_id: String,
170    ) -> Self {
171        Self {
172            id: Uuid::new_v4().to_string(),
173            message_type: MessageType::Response,
174            sender,
175            recipient,
176            content: MessageContent::Response(AcpResponse {
177                status: ResponseStatus::Success,
178                result: Some(result),
179                error: None,
180                execution_time_ms: 0,
181            }),
182            timestamp: chrono::Utc::now().to_rfc3339(),
183            correlation_id: Some(correlation_id),
184        }
185    }
186
187    /// Create an error response
188    pub fn error_response(
189        sender: String,
190        recipient: String,
191        code: String,
192        message: String,
193        correlation_id: String,
194    ) -> Self {
195        Self {
196            id: Uuid::new_v4().to_string(),
197            message_type: MessageType::Error,
198            sender,
199            recipient,
200            content: MessageContent::Error(ErrorPayload {
201                code,
202                message,
203                details: None,
204            }),
205            timestamp: chrono::Utc::now().to_rfc3339(),
206            correlation_id: Some(correlation_id),
207        }
208    }
209
210    /// Convert to JSON for transmission
211    pub fn to_json(&self) -> anyhow::Result<String> {
212        Ok(serde_json::to_string(self)?)
213    }
214
215    /// Parse from JSON
216    pub fn from_json(json: &str) -> anyhow::Result<Self> {
217        Ok(serde_json::from_str(json)?)
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224    use serde_json::json;
225
226    #[test]
227    fn test_message_creation() {
228        let msg = AcpMessage::request(
229            "agent-1".to_string(),
230            "agent-2".to_string(),
231            "execute_tool".to_string(),
232            json!({"tool": "bash", "command": "ls"}),
233        );
234
235        assert_eq!(msg.message_type, MessageType::Request);
236        assert_eq!(msg.sender, "agent-1");
237        assert_eq!(msg.recipient, "agent-2");
238    }
239
240    #[test]
241    fn test_message_serialization() {
242        let msg = AcpMessage::request(
243            "agent-1".to_string(),
244            "agent-2".to_string(),
245            "test".to_string(),
246            json!({}),
247        );
248
249        let json = msg.to_json().unwrap();
250        let restored = AcpMessage::from_json(&json).unwrap();
251
252        assert_eq!(msg.id, restored.id);
253        assert_eq!(msg.sender, restored.sender);
254    }
255}