Skip to main content

synaps_cli/core/
protocol.rs

1use serde::{Serialize, Deserialize};
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 {
60        tool_id: String,
61        result: String,
62    },
63
64    /// Tool execution incremental delta
65    #[serde(rename = "tool_result_delta")]
66    ToolResultDelta {
67        tool_id: String,
68        delta: String,
69    },
70
71    /// Token usage update
72    #[serde(rename = "usage")]
73    Usage {
74        input_tokens: u64,
75        output_tokens: u64,
76    },
77
78    /// Streaming complete for this turn
79    #[serde(rename = "done")]
80    Done,
81
82    /// Error occurred
83    #[serde(rename = "error")]
84    Error { message: String },
85
86    /// System/info message (command responses, status)
87    #[serde(rename = "system")]
88    System { message: String },
89
90    /// Full conversation history (response to History request)
91    #[serde(rename = "history")]
92    HistoryResponse { messages: Vec<HistoryEntry> },
93
94    /// Server status
95    #[serde(rename = "status")]
96    StatusResponse {
97        model: String,
98        thinking: String,
99        streaming: bool,
100        session_id: String,
101        total_input_tokens: u64,
102        total_output_tokens: u64,
103        session_cost: f64,
104        connected_clients: usize,
105    },
106}
107
108/// A single entry in the conversation history (display-friendly)
109#[derive(Debug, Clone, Serialize, Deserialize)]
110#[serde(tag = "role")]
111pub enum HistoryEntry {
112    #[serde(rename = "user")]
113    User { content: String, time: String },
114    #[serde(rename = "thinking")]
115    Thinking { content: String, time: String },
116    #[serde(rename = "text")]
117    Text { content: String, time: String },
118    #[serde(rename = "tool_use")]
119    ToolUse { tool_name: String, input: String, time: String },
120    #[serde(rename = "tool_result")]
121    ToolResult { result: String, time: String },
122    #[serde(rename = "system")]
123    System { content: String, time: String },
124    #[serde(rename = "error")]
125    Error { content: String, time: String },
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use serde_json::json;
132
133    #[test]
134    fn test_client_message_message_roundtrip() {
135        let msg = ClientMessage::Message {
136            content: "Hello, world!".to_string(),
137        };
138
139        // Serialize to JSON
140        let json_value = serde_json::to_value(&msg).unwrap();
141
142        // Verify JSON structure
143        assert_eq!(json_value["type"], "message");
144        assert_eq!(json_value["content"], "Hello, world!");
145
146        // Deserialize back and verify
147        let deserialized: ClientMessage = serde_json::from_value(json_value).unwrap();
148        match deserialized {
149            ClientMessage::Message { content } => {
150                assert_eq!(content, "Hello, world!");
151            }
152            _ => panic!("Expected Message variant"),
153        }
154    }
155
156    #[test]
157    fn test_client_message_cancel_roundtrip() {
158        let msg = ClientMessage::Cancel;
159
160        // Serialize to JSON
161        let json_value = serde_json::to_value(&msg).unwrap();
162
163        // Verify JSON structure
164        assert_eq!(json_value["type"], "cancel");
165
166        // Deserialize back and verify
167        let deserialized: ClientMessage = serde_json::from_value(json_value).unwrap();
168        matches!(deserialized, ClientMessage::Cancel);
169    }
170
171    #[test]
172    fn test_client_message_command_roundtrip() {
173        let msg = ClientMessage::Command {
174            name: "clear".to_string(),
175            args: "".to_string(),
176        };
177
178        // Serialize to JSON
179        let json_value = serde_json::to_value(&msg).unwrap();
180
181        // Verify JSON structure
182        assert_eq!(json_value["type"], "command");
183        assert_eq!(json_value["name"], "clear");
184        assert_eq!(json_value["args"], "");
185
186        // Deserialize back and verify
187        let deserialized: ClientMessage = serde_json::from_value(json_value).unwrap();
188        match deserialized {
189            ClientMessage::Command { name, args } => {
190                assert_eq!(name, "clear");
191                assert_eq!(args, "");
192            }
193            _ => panic!("Expected Command variant"),
194        }
195    }
196
197    #[test]
198    fn test_server_message_text_roundtrip() {
199        let msg = ServerMessage::Text {
200            content: "Hello from server!".to_string(),
201        };
202
203        // Serialize to JSON
204        let json_value = serde_json::to_value(&msg).unwrap();
205
206        // Verify JSON structure
207        assert_eq!(json_value["type"], "text");
208        assert_eq!(json_value["content"], "Hello from server!");
209
210        // Deserialize back and verify
211        let deserialized: ServerMessage = serde_json::from_value(json_value).unwrap();
212        match deserialized {
213            ServerMessage::Text { content } => {
214                assert_eq!(content, "Hello from server!");
215            }
216            _ => panic!("Expected Text variant"),
217        }
218    }
219
220    #[test]
221    fn test_server_message_done_roundtrip() {
222        let msg = ServerMessage::Done;
223
224        // Serialize to JSON
225        let json_value = serde_json::to_value(&msg).unwrap();
226
227        // Verify JSON structure
228        assert_eq!(json_value["type"], "done");
229
230        // Deserialize back and verify
231        let deserialized: ServerMessage = serde_json::from_value(json_value).unwrap();
232        matches!(deserialized, ServerMessage::Done);
233    }
234
235    #[test]
236    fn test_server_message_error_roundtrip() {
237        let msg = ServerMessage::Error {
238            message: "Something went wrong".to_string(),
239        };
240
241        // Serialize to JSON
242        let json_value = serde_json::to_value(&msg).unwrap();
243
244        // Verify JSON structure
245        assert_eq!(json_value["type"], "error");
246        assert_eq!(json_value["message"], "Something went wrong");
247
248        // Deserialize back and verify
249        let deserialized: ServerMessage = serde_json::from_value(json_value).unwrap();
250        match deserialized {
251            ServerMessage::Error { message } => {
252                assert_eq!(message, "Something went wrong");
253            }
254            _ => panic!("Expected Error variant"),
255        }
256    }
257
258    #[test]
259    fn test_server_message_tool_use_roundtrip() {
260        let msg = ServerMessage::ToolUse {
261            tool_name: "execute_bash".to_string(),
262            tool_id: "tool_123".to_string(),
263            input: json!({"command": "ls -la", "timeout": 30}),
264        };
265
266        // Serialize to JSON
267        let json_value = serde_json::to_value(&msg).unwrap();
268
269        // Verify JSON structure
270        assert_eq!(json_value["type"], "tool_use");
271        assert_eq!(json_value["tool_name"], "execute_bash");
272        assert_eq!(json_value["tool_id"], "tool_123");
273        assert_eq!(json_value["input"]["command"], "ls -la");
274        assert_eq!(json_value["input"]["timeout"], 30);
275
276        // Deserialize back and verify
277        let deserialized: ServerMessage = serde_json::from_value(json_value).unwrap();
278        match deserialized {
279            ServerMessage::ToolUse { tool_name, tool_id, input } => {
280                assert_eq!(tool_name, "execute_bash");
281                assert_eq!(tool_id, "tool_123");
282                assert_eq!(input["command"], "ls -la");
283                assert_eq!(input["timeout"], 30);
284            }
285            _ => panic!("Expected ToolUse variant"),
286        }
287    }
288
289    #[test]
290    fn test_server_message_usage_roundtrip() {
291        let msg = ServerMessage::Usage {
292            input_tokens: 150,
293            output_tokens: 75,
294        };
295
296        // Serialize to JSON
297        let json_value = serde_json::to_value(&msg).unwrap();
298
299        // Verify JSON structure
300        assert_eq!(json_value["type"], "usage");
301        assert_eq!(json_value["input_tokens"], 150);
302        assert_eq!(json_value["output_tokens"], 75);
303
304        // Deserialize back and verify
305        let deserialized: ServerMessage = serde_json::from_value(json_value).unwrap();
306        match deserialized {
307            ServerMessage::Usage { input_tokens, output_tokens } => {
308                assert_eq!(input_tokens, 150);
309                assert_eq!(output_tokens, 75);
310            }
311            _ => panic!("Expected Usage variant"),
312        }
313    }
314
315    #[test]
316    fn test_history_entry_user_roundtrip() {
317        let entry = HistoryEntry::User {
318            content: "User message".to_string(),
319            time: "2023-10-01T12:00:00Z".to_string(),
320        };
321
322        // Serialize to JSON
323        let json_value = serde_json::to_value(&entry).unwrap();
324
325        // Verify JSON structure
326        assert_eq!(json_value["role"], "user");
327        assert_eq!(json_value["content"], "User message");
328        assert_eq!(json_value["time"], "2023-10-01T12:00:00Z");
329
330        // Deserialize back and verify
331        let deserialized: HistoryEntry = serde_json::from_value(json_value).unwrap();
332        match deserialized {
333            HistoryEntry::User { content, time } => {
334                assert_eq!(content, "User message");
335                assert_eq!(time, "2023-10-01T12:00:00Z");
336            }
337            _ => panic!("Expected User variant"),
338        }
339    }
340}