Skip to main content

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