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, PartialEq, 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, PartialEq, 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 = "camelCase")]
86pub enum PatchApplyStatus {
87 #[serde(alias = "in_progress")]
89 InProgress,
90 #[serde(alias = "completed")]
91 Completed,
92 #[serde(alias = "failed")]
93 Failed,
94 #[serde(alias = "declined")]
97 Declined,
98}
99
100#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
102pub struct FileChangeItem {
103 pub id: String,
104 pub changes: Vec<FileUpdateChange>,
105 pub status: PatchApplyStatus,
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
110#[serde(rename_all = "snake_case")]
111pub enum McpToolCallStatus {
112 #[serde(alias = "inProgress")]
113 InProgress,
114 #[serde(alias = "completed")]
115 Completed,
116 #[serde(alias = "failed")]
117 Failed,
118}
119
120#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
122pub struct McpToolCallResult {
123 pub content: Vec<Value>,
124 pub structured_content: Value,
125}
126
127#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
129pub struct McpToolCallError {
130 pub message: String,
131}
132
133#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
135pub struct McpToolCallItem {
136 pub id: String,
137 pub server: String,
138 pub tool: String,
139 pub arguments: Value,
140 #[serde(skip_serializing_if = "Option::is_none")]
141 pub result: Option<McpToolCallResult>,
142 #[serde(skip_serializing_if = "Option::is_none")]
143 pub error: Option<McpToolCallError>,
144 pub status: McpToolCallStatus,
145}
146
147#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
153pub struct AgentMessageItem {
154 pub id: String,
155 #[serde(default)]
156 pub text: String,
157}
158
159#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
161pub struct UserMessageContent {
162 #[serde(rename = "type")]
164 pub kind: String,
165 #[serde(default)]
167 pub text: String,
168 #[serde(default, skip_serializing_if = "Vec::is_empty")]
171 pub text_elements: Vec<Value>,
172}
173
174#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
179pub struct UserMessageItem {
180 pub id: String,
181 pub content: Vec<UserMessageContent>,
182}
183
184#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
189pub struct ReasoningItem {
190 pub id: String,
191 #[serde(default)]
192 pub text: String,
193}
194
195#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
197pub struct WebSearchItem {
198 pub id: String,
199 pub query: String,
200}
201
202#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
204pub struct ErrorItem {
205 pub id: String,
206 pub message: String,
207}
208
209#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
211pub struct TodoItem {
212 pub text: String,
213 pub completed: bool,
214}
215
216#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
218pub struct TodoListItem {
219 pub id: String,
220 pub items: Vec<TodoItem>,
221}
222
223#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
233#[serde(tag = "type", rename_all = "snake_case")]
234pub enum ThreadItem {
235 #[serde(alias = "userMessage")]
237 UserMessage(UserMessageItem),
238 #[serde(alias = "agentMessage")]
240 AgentMessage(AgentMessageItem),
241 #[serde(alias = "reasoning")]
243 Reasoning(ReasoningItem),
244 #[serde(alias = "commandExecution")]
246 CommandExecution(CommandExecutionItem),
247 #[serde(alias = "fileChange")]
249 FileChange(FileChangeItem),
250 #[serde(alias = "mcpToolCall")]
252 McpToolCall(McpToolCallItem),
253 #[serde(alias = "webSearch")]
255 WebSearch(WebSearchItem),
256 #[serde(alias = "todoList")]
258 TodoList(TodoListItem),
259 #[serde(alias = "error")]
261 Error(ErrorItem),
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267
268 #[test]
269 fn test_deserialize_agent_message() {
270 let json = r#"{"type":"agent_message","id":"msg_1","text":"Hello world"}"#;
271 let item: ThreadItem = serde_json::from_str(json).unwrap();
272 assert!(matches!(item, ThreadItem::AgentMessage(ref m) if m.text == "Hello world"));
273 }
274
275 #[test]
276 fn test_deserialize_command_execution() {
277 let json = r#"{"type":"command_execution","id":"cmd_1","command":"ls -la","aggregated_output":"total 0","exit_code":0,"status":"completed"}"#;
278 let item: ThreadItem = serde_json::from_str(json).unwrap();
279 assert!(matches!(item, ThreadItem::CommandExecution(ref c) if c.exit_code == Some(0)));
280 }
281
282 #[test]
283 fn test_deserialize_file_change() {
284 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"}"#;
285 let item: ThreadItem = serde_json::from_str(json).unwrap();
286 assert!(matches!(
287 item,
288 ThreadItem::FileChange(ref f) if matches!(f.changes[0].kind, PatchChangeKind::Update { .. })
289 ));
290 }
291
292 #[test]
293 fn test_deserialize_todo_list() {
294 let json = r#"{"type":"todo_list","id":"td_1","items":[{"text":"Fix bug","completed":false},{"text":"Write tests","completed":true}]}"#;
295 let item: ThreadItem = serde_json::from_str(json).unwrap();
296 assert!(matches!(item, ThreadItem::TodoList(ref t) if t.items.len() == 2));
297 }
298
299 #[test]
300 fn test_deserialize_error() {
301 let json = r#"{"type":"error","id":"err_1","message":"something went wrong"}"#;
302 let item: ThreadItem = serde_json::from_str(json).unwrap();
303 assert!(matches!(item, ThreadItem::Error(ref e) if e.message == "something went wrong"));
304 }
305
306 #[test]
307 fn test_deserialize_reasoning() {
308 let json = r#"{"type":"reasoning","id":"r_1","text":"Let me think about this..."}"#;
309 let item: ThreadItem = serde_json::from_str(json).unwrap();
310 assert!(matches!(item, ThreadItem::Reasoning(ref r) if r.text.contains("think")));
311 }
312
313 #[test]
314 fn test_deserialize_web_search() {
315 let json = r#"{"type":"web_search","id":"ws_1","query":"rust serde tutorial"}"#;
316 let item: ThreadItem = serde_json::from_str(json).unwrap();
317 assert!(matches!(item, ThreadItem::WebSearch(ref w) if w.query == "rust serde tutorial"));
318 }
319
320 #[test]
321 fn test_deserialize_mcp_tool_call() {
322 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}}"#;
323 let item: ThreadItem = serde_json::from_str(json).unwrap();
324 assert!(matches!(item, ThreadItem::McpToolCall(ref m) if m.tool == "my-tool"));
325 }
326
327 #[test]
328 fn test_deserialize_camel_case_agent_message() {
329 let json = r#"{"type":"agentMessage","id":"msg_1","text":"Hello"}"#;
330 let item: ThreadItem = serde_json::from_str(json).unwrap();
331 assert!(matches!(item, ThreadItem::AgentMessage(ref m) if m.text == "Hello"));
332 }
333
334 #[test]
335 fn test_deserialize_camel_case_command_execution() {
336 let json = r#"{"type":"commandExecution","id":"cmd_1","command":"ls","aggregated_output":"","status":"completed"}"#;
337 let item: ThreadItem = serde_json::from_str(json).unwrap();
338 assert!(matches!(item, ThreadItem::CommandExecution(_)));
339 }
340
341 #[test]
342 fn test_deserialize_camel_case_file_change() {
343 let json = r#"{"type":"fileChange","id":"fc_1","changes":[],"status":"completed"}"#;
344 let item: ThreadItem = serde_json::from_str(json).unwrap();
345 assert!(matches!(item, ThreadItem::FileChange(_)));
346 }
347
348 #[test]
349 fn test_command_execution_status_declined() {
350 let json = r#"{"type":"command_execution","id":"cmd_1","command":"rm -rf /","aggregated_output":"","status":"declined"}"#;
351 let item: ThreadItem = serde_json::from_str(json).unwrap();
352 assert!(
353 matches!(item, ThreadItem::CommandExecution(ref c) if c.status == CommandExecutionStatus::Declined)
354 );
355 }
356}