Skip to main content

agent_core/core/
protocol.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4/// Messages sent from client → server
5#[derive(Debug, Clone, Serialize, Deserialize)]
6#[serde(tag = "type")]
7pub enum ClientMessage {
8    /// Send a user message to the conversation
9    #[serde(rename = "message")]
10    Message { content: String },
11
12    /// Execute a slash command
13    #[serde(rename = "command")]
14    Command { name: String, args: String },
15
16    /// Cancel the current streaming response
17    #[serde(rename = "cancel")]
18    Cancel,
19
20    /// Request current session state
21    #[serde(rename = "status")]
22    Status,
23
24    /// Request conversation history (for late-joining clients)
25    #[serde(rename = "history")]
26    History,
27}
28
29/// Messages sent from server → client
30#[derive(Debug, Clone, Serialize, Deserialize)]
31#[serde(tag = "type")]
32pub enum ServerMessage {
33    /// Thinking tokens (streamed incrementally)
34    #[serde(rename = "thinking")]
35    Thinking { content: String },
36
37    /// Text tokens (streamed incrementally)
38    #[serde(rename = "text")]
39    Text { content: String },
40
41    /// A tool is ABOUT to be invoked (streaming JSON args)
42    #[serde(rename = "tool_use_start")]
43    ToolUseStart { tool_name: String },
44
45    /// Streaming chunk of the tool's JSON arguments
46    #[serde(rename = "tool_use_delta")]
47    ToolUseDelta(String),
48
49    /// A tool was invoked (JSON finished)
50    #[serde(rename = "tool_use")]
51    ToolUse {
52        tool_name: String,
53        tool_id: String,
54        input: Value,
55    },
56
57    /// Tool execution result
58    #[serde(rename = "tool_result")]
59    ToolResult { tool_id: String, result: String },
60
61    /// Tool execution incremental delta
62    #[serde(rename = "tool_result_delta")]
63    ToolResultDelta { tool_id: String, delta: String },
64
65    /// Token usage update
66    #[serde(rename = "usage")]
67    Usage {
68        input_tokens: u64,
69        output_tokens: u64,
70        /// Cache-write TTL split — optional, omitted when unknown.
71        #[serde(default, skip_serializing_if = "Option::is_none")]
72        cache_creation_5m: Option<u64>,
73        #[serde(default, skip_serializing_if = "Option::is_none")]
74        cache_creation_1h: Option<u64>,
75    },
76
77    /// Streaming complete for this turn
78    #[serde(rename = "done")]
79    Done,
80
81    /// Out-of-band advisory notice (e.g. cache-TTL downgrade warning).
82    /// Additive variant — old clients ignore unknown message types.
83    #[serde(rename = "notice")]
84    Notice { text: String },
85
86    /// Error occurred
87    #[serde(rename = "error")]
88    Error { message: String },
89
90    /// System/info message (command responses, status)
91    #[serde(rename = "system")]
92    System { message: String },
93
94    /// Full conversation history (response to History request)
95    #[serde(rename = "history")]
96    HistoryResponse { messages: Vec<HistoryEntry> },
97
98    /// Server status
99    #[serde(rename = "status")]
100    StatusResponse {
101        model: String,
102        thinking: String,
103        streaming: bool,
104        session_id: String,
105        total_input_tokens: u64,
106        total_output_tokens: u64,
107        session_cost: f64,
108        connected_clients: usize,
109    },
110}
111
112/// A single entry in the conversation history (display-friendly)
113#[derive(Debug, Clone, Serialize, Deserialize)]
114#[serde(tag = "role")]
115pub enum HistoryEntry {
116    #[serde(rename = "user")]
117    User { content: String, time: String },
118    #[serde(rename = "thinking")]
119    Thinking { content: String, time: String },
120    #[serde(rename = "text")]
121    Text { content: String, time: String },
122    #[serde(rename = "tool_use")]
123    ToolUse {
124        tool_name: String,
125        input: String,
126        time: String,
127    },
128    #[serde(rename = "tool_result")]
129    ToolResult { result: String, time: String },
130    #[serde(rename = "system")]
131    System { content: String, time: String },
132    #[serde(rename = "error")]
133    Error { content: String, time: String },
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use serde_json::json;
140
141    #[test]
142    fn test_client_message_message_roundtrip() {
143        let msg = ClientMessage::Message {
144            content: "Hello, world!".to_string(),
145        };
146
147        // Serialize to JSON
148        let json_value = serde_json::to_value(&msg).unwrap();
149
150        // Verify JSON structure
151        assert_eq!(json_value["type"], "message");
152        assert_eq!(json_value["content"], "Hello, world!");
153
154        // Deserialize back and verify
155        let deserialized: ClientMessage = serde_json::from_value(json_value).unwrap();
156        match deserialized {
157            ClientMessage::Message { content } => {
158                assert_eq!(content, "Hello, world!");
159            }
160            _ => panic!("Expected Message variant"),
161        }
162    }
163
164    #[test]
165    fn test_client_message_cancel_roundtrip() {
166        let msg = ClientMessage::Cancel;
167
168        // Serialize to JSON
169        let json_value = serde_json::to_value(&msg).unwrap();
170
171        // Verify JSON structure
172        assert_eq!(json_value["type"], "cancel");
173
174        // Deserialize back and verify
175        let deserialized: ClientMessage = serde_json::from_value(json_value).unwrap();
176        matches!(deserialized, ClientMessage::Cancel);
177    }
178
179    #[test]
180    fn test_client_message_command_roundtrip() {
181        let msg = ClientMessage::Command {
182            name: "clear".to_string(),
183            args: "".to_string(),
184        };
185
186        // Serialize to JSON
187        let json_value = serde_json::to_value(&msg).unwrap();
188
189        // Verify JSON structure
190        assert_eq!(json_value["type"], "command");
191        assert_eq!(json_value["name"], "clear");
192        assert_eq!(json_value["args"], "");
193
194        // Deserialize back and verify
195        let deserialized: ClientMessage = serde_json::from_value(json_value).unwrap();
196        match deserialized {
197            ClientMessage::Command { name, args } => {
198                assert_eq!(name, "clear");
199                assert_eq!(args, "");
200            }
201            _ => panic!("Expected Command variant"),
202        }
203    }
204
205    #[test]
206    fn test_server_message_text_roundtrip() {
207        let msg = ServerMessage::Text {
208            content: "Hello from server!".to_string(),
209        };
210
211        // Serialize to JSON
212        let json_value = serde_json::to_value(&msg).unwrap();
213
214        // Verify JSON structure
215        assert_eq!(json_value["type"], "text");
216        assert_eq!(json_value["content"], "Hello from server!");
217
218        // Deserialize back and verify
219        let deserialized: ServerMessage = serde_json::from_value(json_value).unwrap();
220        match deserialized {
221            ServerMessage::Text { content } => {
222                assert_eq!(content, "Hello from server!");
223            }
224            _ => panic!("Expected Text variant"),
225        }
226    }
227
228    #[test]
229    fn test_server_message_done_roundtrip() {
230        let msg = ServerMessage::Done;
231
232        // Serialize to JSON
233        let json_value = serde_json::to_value(&msg).unwrap();
234
235        // Verify JSON structure
236        assert_eq!(json_value["type"], "done");
237
238        // Deserialize back and verify
239        let deserialized: ServerMessage = serde_json::from_value(json_value).unwrap();
240        matches!(deserialized, ServerMessage::Done);
241    }
242
243    #[test]
244    fn test_server_message_error_roundtrip() {
245        let msg = ServerMessage::Error {
246            message: "Something went wrong".to_string(),
247        };
248
249        // Serialize to JSON
250        let json_value = serde_json::to_value(&msg).unwrap();
251
252        // Verify JSON structure
253        assert_eq!(json_value["type"], "error");
254        assert_eq!(json_value["message"], "Something went wrong");
255
256        // Deserialize back and verify
257        let deserialized: ServerMessage = serde_json::from_value(json_value).unwrap();
258        match deserialized {
259            ServerMessage::Error { message } => {
260                assert_eq!(message, "Something went wrong");
261            }
262            _ => panic!("Expected Error variant"),
263        }
264    }
265
266    #[test]
267    fn test_server_message_tool_use_roundtrip() {
268        let msg = ServerMessage::ToolUse {
269            tool_name: "execute_bash".to_string(),
270            tool_id: "tool_123".to_string(),
271            input: json!({"command": "ls -la", "timeout": 30}),
272        };
273
274        // Serialize to JSON
275        let json_value = serde_json::to_value(&msg).unwrap();
276
277        // Verify JSON structure
278        assert_eq!(json_value["type"], "tool_use");
279        assert_eq!(json_value["tool_name"], "execute_bash");
280        assert_eq!(json_value["tool_id"], "tool_123");
281        assert_eq!(json_value["input"]["command"], "ls -la");
282        assert_eq!(json_value["input"]["timeout"], 30);
283
284        // Deserialize back and verify
285        let deserialized: ServerMessage = serde_json::from_value(json_value).unwrap();
286        match deserialized {
287            ServerMessage::ToolUse {
288                tool_name,
289                tool_id,
290                input,
291            } => {
292                assert_eq!(tool_name, "execute_bash");
293                assert_eq!(tool_id, "tool_123");
294                assert_eq!(input["command"], "ls -la");
295                assert_eq!(input["timeout"], 30);
296            }
297            _ => panic!("Expected ToolUse variant"),
298        }
299    }
300
301    #[test]
302    fn test_server_message_usage_roundtrip() {
303        let msg = ServerMessage::Usage {
304            input_tokens: 150,
305            output_tokens: 75,
306            cache_creation_5m: None,
307            cache_creation_1h: None,
308        };
309
310        // Serialize to JSON
311        let json_value = serde_json::to_value(&msg).unwrap();
312
313        // Verify JSON structure
314        assert_eq!(json_value["type"], "usage");
315        assert_eq!(json_value["input_tokens"], 150);
316        assert_eq!(json_value["output_tokens"], 75);
317        // Split fields are skip-if-none: absent from the wire when unknown,
318        // so pre-existing protocol consumers see an unchanged shape.
319        assert!(json_value.get("cache_creation_5m").is_none());
320        assert!(json_value.get("cache_creation_1h").is_none());
321
322        // Deserialize back and verify
323        let deserialized: ServerMessage = serde_json::from_value(json_value).unwrap();
324        match deserialized {
325            ServerMessage::Usage {
326                input_tokens,
327                output_tokens,
328                cache_creation_5m,
329                cache_creation_1h,
330            } => {
331                assert_eq!(input_tokens, 150);
332                assert_eq!(output_tokens, 75);
333                assert_eq!(cache_creation_5m, None);
334                assert_eq!(cache_creation_1h, None);
335            }
336            _ => panic!("Expected Usage variant"),
337        }
338    }
339
340    #[test]
341    fn test_history_entry_user_roundtrip() {
342        let entry = HistoryEntry::User {
343            content: "User message".to_string(),
344            time: "2023-10-01T12:00:00Z".to_string(),
345        };
346
347        // Serialize to JSON
348        let json_value = serde_json::to_value(&entry).unwrap();
349
350        // Verify JSON structure
351        assert_eq!(json_value["role"], "user");
352        assert_eq!(json_value["content"], "User message");
353        assert_eq!(json_value["time"], "2023-10-01T12:00:00Z");
354
355        // Deserialize back and verify
356        let deserialized: HistoryEntry = serde_json::from_value(json_value).unwrap();
357        match deserialized {
358            HistoryEntry::User { content, time } => {
359                assert_eq!(content, "User message");
360                assert_eq!(time, "2023-10-01T12:00:00Z");
361            }
362            _ => panic!("Expected User variant"),
363        }
364    }
365}