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)]
34pub struct CommandExecutionItem {
35 pub id: String,
36 pub command: String,
38 pub aggregated_output: String,
40 #[serde(skip_serializing_if = "Option::is_none")]
42 pub exit_code: Option<i32>,
43 pub status: CommandExecutionStatus,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(rename_all = "snake_case")]
49pub enum PatchChangeKind {
50 #[serde(alias = "add")]
51 Add,
52 #[serde(alias = "delete")]
53 Delete,
54 #[serde(alias = "update")]
55 Update,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct FileUpdateChange {
61 pub path: String,
62 pub kind: PatchChangeKind,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67#[serde(rename_all = "snake_case")]
68pub enum PatchApplyStatus {
69 #[serde(alias = "completed")]
70 Completed,
71 #[serde(alias = "failed")]
72 Failed,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct FileChangeItem {
78 pub id: String,
79 pub changes: Vec<FileUpdateChange>,
80 pub status: PatchApplyStatus,
81}
82
83#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
85#[serde(rename_all = "snake_case")]
86pub enum McpToolCallStatus {
87 #[serde(alias = "inProgress")]
88 InProgress,
89 #[serde(alias = "completed")]
90 Completed,
91 #[serde(alias = "failed")]
92 Failed,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct McpToolCallResult {
98 pub content: Vec<Value>,
99 pub structured_content: Value,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct McpToolCallError {
105 pub message: String,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct McpToolCallItem {
111 pub id: String,
112 pub server: String,
113 pub tool: String,
114 pub arguments: Value,
115 #[serde(skip_serializing_if = "Option::is_none")]
116 pub result: Option<McpToolCallResult>,
117 #[serde(skip_serializing_if = "Option::is_none")]
118 pub error: Option<McpToolCallError>,
119 pub status: McpToolCallStatus,
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct AgentMessageItem {
125 pub id: String,
126 pub text: String,
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct ReasoningItem {
132 pub id: String,
133 pub text: String,
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct WebSearchItem {
139 pub id: String,
140 pub query: String,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct ErrorItem {
146 pub id: String,
147 pub message: String,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct TodoItem {
153 pub text: String,
154 pub completed: bool,
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct TodoListItem {
160 pub id: String,
161 pub items: Vec<TodoItem>,
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
174#[serde(tag = "type", rename_all = "snake_case")]
175pub enum ThreadItem {
176 #[serde(alias = "agentMessage")]
178 AgentMessage(AgentMessageItem),
179 #[serde(alias = "reasoning")]
181 Reasoning(ReasoningItem),
182 #[serde(alias = "commandExecution")]
184 CommandExecution(CommandExecutionItem),
185 #[serde(alias = "fileChange")]
187 FileChange(FileChangeItem),
188 #[serde(alias = "mcpToolCall")]
190 McpToolCall(McpToolCallItem),
191 #[serde(alias = "webSearch")]
193 WebSearch(WebSearchItem),
194 #[serde(alias = "todoList")]
196 TodoList(TodoListItem),
197 #[serde(alias = "error")]
199 Error(ErrorItem),
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn test_deserialize_agent_message() {
208 let json = r#"{"type":"agent_message","id":"msg_1","text":"Hello world"}"#;
209 let item: ThreadItem = serde_json::from_str(json).unwrap();
210 assert!(matches!(item, ThreadItem::AgentMessage(ref m) if m.text == "Hello world"));
211 }
212
213 #[test]
214 fn test_deserialize_command_execution() {
215 let json = r#"{"type":"command_execution","id":"cmd_1","command":"ls -la","aggregated_output":"total 0","exit_code":0,"status":"completed"}"#;
216 let item: ThreadItem = serde_json::from_str(json).unwrap();
217 assert!(matches!(item, ThreadItem::CommandExecution(ref c) if c.exit_code == Some(0)));
218 }
219
220 #[test]
221 fn test_deserialize_file_change() {
222 let json = r#"{"type":"file_change","id":"fc_1","changes":[{"path":"src/main.rs","kind":"update"}],"status":"completed"}"#;
223 let item: ThreadItem = serde_json::from_str(json).unwrap();
224 assert!(
225 matches!(item, ThreadItem::FileChange(ref f) if f.changes[0].kind == PatchChangeKind::Update)
226 );
227 }
228
229 #[test]
230 fn test_deserialize_todo_list() {
231 let json = r#"{"type":"todo_list","id":"td_1","items":[{"text":"Fix bug","completed":false},{"text":"Write tests","completed":true}]}"#;
232 let item: ThreadItem = serde_json::from_str(json).unwrap();
233 assert!(matches!(item, ThreadItem::TodoList(ref t) if t.items.len() == 2));
234 }
235
236 #[test]
237 fn test_deserialize_error() {
238 let json = r#"{"type":"error","id":"err_1","message":"something went wrong"}"#;
239 let item: ThreadItem = serde_json::from_str(json).unwrap();
240 assert!(matches!(item, ThreadItem::Error(ref e) if e.message == "something went wrong"));
241 }
242
243 #[test]
244 fn test_deserialize_reasoning() {
245 let json = r#"{"type":"reasoning","id":"r_1","text":"Let me think about this..."}"#;
246 let item: ThreadItem = serde_json::from_str(json).unwrap();
247 assert!(matches!(item, ThreadItem::Reasoning(ref r) if r.text.contains("think")));
248 }
249
250 #[test]
251 fn test_deserialize_web_search() {
252 let json = r#"{"type":"web_search","id":"ws_1","query":"rust serde tutorial"}"#;
253 let item: ThreadItem = serde_json::from_str(json).unwrap();
254 assert!(matches!(item, ThreadItem::WebSearch(ref w) if w.query == "rust serde tutorial"));
255 }
256
257 #[test]
258 fn test_deserialize_mcp_tool_call() {
259 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}}"#;
260 let item: ThreadItem = serde_json::from_str(json).unwrap();
261 assert!(matches!(item, ThreadItem::McpToolCall(ref m) if m.tool == "my-tool"));
262 }
263
264 #[test]
265 fn test_deserialize_camel_case_agent_message() {
266 let json = r#"{"type":"agentMessage","id":"msg_1","text":"Hello"}"#;
267 let item: ThreadItem = serde_json::from_str(json).unwrap();
268 assert!(matches!(item, ThreadItem::AgentMessage(ref m) if m.text == "Hello"));
269 }
270
271 #[test]
272 fn test_deserialize_camel_case_command_execution() {
273 let json = r#"{"type":"commandExecution","id":"cmd_1","command":"ls","aggregated_output":"","status":"completed"}"#;
274 let item: ThreadItem = serde_json::from_str(json).unwrap();
275 assert!(matches!(item, ThreadItem::CommandExecution(_)));
276 }
277
278 #[test]
279 fn test_deserialize_camel_case_file_change() {
280 let json = r#"{"type":"fileChange","id":"fc_1","changes":[],"status":"completed"}"#;
281 let item: ThreadItem = serde_json::from_str(json).unwrap();
282 assert!(matches!(item, ThreadItem::FileChange(_)));
283 }
284
285 #[test]
286 fn test_command_execution_status_declined() {
287 let json = r#"{"type":"command_execution","id":"cmd_1","command":"rm -rf /","aggregated_output":"","status":"declined"}"#;
288 let item: ThreadItem = serde_json::from_str(json).unwrap();
289 assert!(
290 matches!(item, ThreadItem::CommandExecution(ref c) if c.status == CommandExecutionStatus::Declined)
291 );
292 }
293}