Skip to main content

vtcode_acp/
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(crate) id: String,
12
13    /// Message type (request, response, etc.)
14    #[serde(rename = "type")]
15    message_type: MessageType,
16
17    /// Sender agent ID
18    sender: String,
19
20    /// Recipient agent ID
21    recipient: String,
22
23    /// Message content
24    pub(crate) content: MessageContent,
25
26    /// Timestamp (ISO 8601)
27    timestamp: String,
28
29    /// Optional correlation ID for request/response pairs
30    #[serde(skip_serializing_if = "Option::is_none")]
31    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    action: String,
66
67    /// Arguments for the action (any JSON-serializable data)
68    args: Value,
69
70    /// Optional timeout in seconds
71    #[serde(skip_serializing_if = "Option::is_none")]
72    timeout_secs: Option<u64>,
73
74    /// Whether to await response synchronously
75    #[serde(default)]
76    pub(crate) sync: bool,
77}
78
79/// ACP response structure
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct AcpResponse {
82    /// Execution status
83    status: ResponseStatus,
84
85    /// Result data (on success)
86    #[serde(skip_serializing_if = "Option::is_none")]
87    result: Option<Value>,
88
89    /// Error details (on failure)
90    #[serde(skip_serializing_if = "Option::is_none")]
91    error: Option<ErrorDetails>,
92
93    /// Execution time in milliseconds
94    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    code: String,
112
113    /// Human-readable error message
114    message: String,
115
116    /// Additional error details
117    #[serde(skip_serializing_if = "Option::is_none")]
118    details: Option<Value>,
119}
120
121/// Error details in response
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct ErrorDetails {
124    /// Error code
125    code: String,
126
127    /// Error message
128    message: String,
129
130    /// Additional context
131    #[serde(skip_serializing_if = "Option::is_none")]
132    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    event: String,
140
141    /// Event-specific data
142    data: Value,
143}
144
145impl AcpMessage {
146    /// Create a new ACP request message
147    pub(crate) 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 { action, args, timeout_secs: None, sync: true }),
154            timestamp: chrono::Utc::now().to_rfc3339(),
155            correlation_id: None,
156        }
157    }
158
159    /// Create a new ACP response message
160    pub fn response(sender: String, recipient: String, result: Value, correlation_id: String) -> Self {
161        Self {
162            id: Uuid::new_v4().to_string(),
163            message_type: MessageType::Response,
164            sender,
165            recipient,
166            content: MessageContent::Response(AcpResponse {
167                status: ResponseStatus::Success,
168                result: Some(result),
169                error: None,
170                execution_time_ms: 0,
171            }),
172            timestamp: chrono::Utc::now().to_rfc3339(),
173            correlation_id: Some(correlation_id),
174        }
175    }
176
177    /// Create an error response
178    pub fn error_response(
179        sender: String,
180        recipient: String,
181        code: String,
182        message: String,
183        correlation_id: String,
184    ) -> Self {
185        Self {
186            id: Uuid::new_v4().to_string(),
187            message_type: MessageType::Error,
188            sender,
189            recipient,
190            content: MessageContent::Error(ErrorPayload { code, message, details: None }),
191            timestamp: chrono::Utc::now().to_rfc3339(),
192            correlation_id: Some(correlation_id),
193        }
194    }
195
196    /// Convert to JSON for transmission
197    fn to_json(&self) -> anyhow::Result<String> {
198        Ok(serde_json::to_string(self)?)
199    }
200
201    /// Parse from JSON
202    fn from_json(json: &str) -> anyhow::Result<Self> {
203        Ok(serde_json::from_str(json)?)
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210    use serde_json::json;
211
212    #[test]
213    fn test_message_creation() {
214        let msg = AcpMessage::request(
215            "agent-1".to_string(),
216            "agent-2".to_string(),
217            "execute_tool".to_string(),
218            json!({"tool": "bash", "command": "ls"}),
219        );
220
221        assert_eq!(msg.message_type, MessageType::Request);
222        assert_eq!(msg.sender, "agent-1");
223        assert_eq!(msg.recipient, "agent-2");
224    }
225
226    #[test]
227    fn test_message_serialization() {
228        let msg = AcpMessage::request("agent-1".to_string(), "agent-2".to_string(), "test".to_string(), json!({}));
229
230        let json = msg.to_json().unwrap();
231        let restored = AcpMessage::from_json(&json).unwrap();
232
233        assert_eq!(msg.id, restored.id);
234        assert_eq!(msg.sender, restored.sender);
235    }
236}