1use serde::{Deserialize, Serialize};
12use serde_json::Value;
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "snake_case")]
17pub enum CommandExecutionStatus {
18 #[serde(alias = "inProgress")]
20 InProgress,
21 #[serde(alias = "completed")]
23 Completed,
24 #[serde(alias = "failed")]
26 Failed,
27 #[serde(alias = "declined")]
29 Declined,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
39#[serde(rename_all = "camelCase")]
40pub struct CommandExecutionItem {
41 pub id: String,
42 pub command: String,
44 #[serde(alias = "aggregated_output", default)]
48 pub aggregated_output: Option<String>,
49 #[serde(alias = "exit_code", default, skip_serializing_if = "Option::is_none")]
51 pub exit_code: Option<i32>,
52 pub status: CommandExecutionStatus,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
62#[serde(tag = "type", rename_all = "snake_case")]
63pub enum PatchChangeKind {
64 Add,
65 Delete,
66 Update {
67 #[serde(default, skip_serializing_if = "Option::is_none")]
69 move_path: Option<String>,
70 },
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct FileUpdateChange {
76 pub path: String,
77 pub kind: PatchChangeKind,
78 #[serde(default)]
80 pub diff: String,
81}
82
83#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
85#[serde(rename_all = "snake_case")]
86pub enum PatchApplyStatus {
87 #[serde(alias = "completed")]
88 Completed,
89 #[serde(alias = "failed")]
90 Failed,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct FileChangeItem {
96 pub id: String,
97 pub changes: Vec<FileUpdateChange>,
98 pub status: PatchApplyStatus,
99}
100
101#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
103#[serde(rename_all = "snake_case")]
104pub enum McpToolCallStatus {
105 #[serde(alias = "inProgress")]
106 InProgress,
107 #[serde(alias = "completed")]
108 Completed,
109 #[serde(alias = "failed")]
110 Failed,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct McpToolCallResult {
116 pub content: Vec<Value>,
117 pub structured_content: Value,
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct McpToolCallError {
123 pub message: String,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct McpToolCallItem {
129 pub id: String,
130 pub server: String,
131 pub tool: String,
132 pub arguments: Value,
133 #[serde(skip_serializing_if = "Option::is_none")]
134 pub result: Option<McpToolCallResult>,
135 #[serde(skip_serializing_if = "Option::is_none")]
136 pub error: Option<McpToolCallError>,
137 pub status: McpToolCallStatus,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct AgentMessageItem {
147 pub id: String,
148 #[serde(default)]
149 pub text: String,
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct UserMessageContent {
155 #[serde(rename = "type")]
157 pub kind: String,
158 #[serde(default)]
160 pub text: String,
161 #[serde(default, skip_serializing_if = "Vec::is_empty")]
164 pub text_elements: Vec<Value>,
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct UserMessageItem {
173 pub id: String,
174 pub content: Vec<UserMessageContent>,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct ReasoningItem {
183 pub id: String,
184 #[serde(default)]
185 pub text: String,
186}
187
188#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct WebSearchItem {
191 pub id: String,
192 pub query: String,
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct ErrorItem {
198 pub id: String,
199 pub message: String,
200}
201
202#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct TodoItem {
205 pub text: String,
206 pub completed: bool,
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize)]
211pub struct TodoListItem {
212 pub id: String,
213 pub items: Vec<TodoItem>,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize)]
226#[serde(tag = "type", rename_all = "snake_case")]
227pub enum ThreadItem {
228 #[serde(alias = "userMessage")]
230 UserMessage(UserMessageItem),
231 #[serde(alias = "agentMessage")]
233 AgentMessage(AgentMessageItem),
234 #[serde(alias = "reasoning")]
236 Reasoning(ReasoningItem),
237 #[serde(alias = "commandExecution")]
239 CommandExecution(CommandExecutionItem),
240 #[serde(alias = "fileChange")]
242 FileChange(FileChangeItem),
243 #[serde(alias = "mcpToolCall")]
245 McpToolCall(McpToolCallItem),
246 #[serde(alias = "webSearch")]
248 WebSearch(WebSearchItem),
249 #[serde(alias = "todoList")]
251 TodoList(TodoListItem),
252 #[serde(alias = "error")]
254 Error(ErrorItem),
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260
261 #[test]
262 fn test_deserialize_agent_message() {
263 let json = r#"{"type":"agent_message","id":"msg_1","text":"Hello world"}"#;
264 let item: ThreadItem = serde_json::from_str(json).unwrap();
265 assert!(matches!(item, ThreadItem::AgentMessage(ref m) if m.text == "Hello world"));
266 }
267
268 #[test]
269 fn test_deserialize_command_execution() {
270 let json = r#"{"type":"command_execution","id":"cmd_1","command":"ls -la","aggregated_output":"total 0","exit_code":0,"status":"completed"}"#;
271 let item: ThreadItem = serde_json::from_str(json).unwrap();
272 assert!(matches!(item, ThreadItem::CommandExecution(ref c) if c.exit_code == Some(0)));
273 }
274
275 #[test]
276 fn test_deserialize_file_change() {
277 let json = r#"{"type":"file_change","id":"fc_1","changes":[{"path":"src/main.rs","kind":{"type":"update"},"diff":"@@ -1 +1 @@\n-a\n+b\n"}],"status":"completed"}"#;
278 let item: ThreadItem = serde_json::from_str(json).unwrap();
279 assert!(matches!(
280 item,
281 ThreadItem::FileChange(ref f) if matches!(f.changes[0].kind, PatchChangeKind::Update { .. })
282 ));
283 }
284
285 #[test]
286 fn test_deserialize_todo_list() {
287 let json = r#"{"type":"todo_list","id":"td_1","items":[{"text":"Fix bug","completed":false},{"text":"Write tests","completed":true}]}"#;
288 let item: ThreadItem = serde_json::from_str(json).unwrap();
289 assert!(matches!(item, ThreadItem::TodoList(ref t) if t.items.len() == 2));
290 }
291
292 #[test]
293 fn test_deserialize_error() {
294 let json = r#"{"type":"error","id":"err_1","message":"something went wrong"}"#;
295 let item: ThreadItem = serde_json::from_str(json).unwrap();
296 assert!(matches!(item, ThreadItem::Error(ref e) if e.message == "something went wrong"));
297 }
298
299 #[test]
300 fn test_deserialize_reasoning() {
301 let json = r#"{"type":"reasoning","id":"r_1","text":"Let me think about this..."}"#;
302 let item: ThreadItem = serde_json::from_str(json).unwrap();
303 assert!(matches!(item, ThreadItem::Reasoning(ref r) if r.text.contains("think")));
304 }
305
306 #[test]
307 fn test_deserialize_web_search() {
308 let json = r#"{"type":"web_search","id":"ws_1","query":"rust serde tutorial"}"#;
309 let item: ThreadItem = serde_json::from_str(json).unwrap();
310 assert!(matches!(item, ThreadItem::WebSearch(ref w) if w.query == "rust serde tutorial"));
311 }
312
313 #[test]
314 fn test_deserialize_mcp_tool_call() {
315 let json = r#"{"type":"mcp_tool_call","id":"mcp_1","server":"my-server","tool":"my-tool","arguments":{"key":"value"},"status":"completed","result":{"content":[],"structured_content":null}}"#;
316 let item: ThreadItem = serde_json::from_str(json).unwrap();
317 assert!(matches!(item, ThreadItem::McpToolCall(ref m) if m.tool == "my-tool"));
318 }
319
320 #[test]
321 fn test_deserialize_camel_case_agent_message() {
322 let json = r#"{"type":"agentMessage","id":"msg_1","text":"Hello"}"#;
323 let item: ThreadItem = serde_json::from_str(json).unwrap();
324 assert!(matches!(item, ThreadItem::AgentMessage(ref m) if m.text == "Hello"));
325 }
326
327 #[test]
328 fn test_deserialize_camel_case_command_execution() {
329 let json = r#"{"type":"commandExecution","id":"cmd_1","command":"ls","aggregated_output":"","status":"completed"}"#;
330 let item: ThreadItem = serde_json::from_str(json).unwrap();
331 assert!(matches!(item, ThreadItem::CommandExecution(_)));
332 }
333
334 #[test]
335 fn test_deserialize_camel_case_file_change() {
336 let json = r#"{"type":"fileChange","id":"fc_1","changes":[],"status":"completed"}"#;
337 let item: ThreadItem = serde_json::from_str(json).unwrap();
338 assert!(matches!(item, ThreadItem::FileChange(_)));
339 }
340
341 #[test]
342 fn test_command_execution_status_declined() {
343 let json = r#"{"type":"command_execution","id":"cmd_1","command":"rm -rf /","aggregated_output":"","status":"declined"}"#;
344 let item: ThreadItem = serde_json::from_str(json).unwrap();
345 assert!(
346 matches!(item, ThreadItem::CommandExecution(ref c) if c.status == CommandExecutionStatus::Declined)
347 );
348 }
349}