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)]
57#[serde(rename_all = "snake_case")]
58pub enum PatchChangeKind {
59 #[serde(alias = "add")]
60 Add,
61 #[serde(alias = "delete")]
62 Delete,
63 #[serde(alias = "update")]
64 Update,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct FileUpdateChange {
70 pub path: String,
71 pub kind: PatchChangeKind,
72}
73
74#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
76#[serde(rename_all = "snake_case")]
77pub enum PatchApplyStatus {
78 #[serde(alias = "completed")]
79 Completed,
80 #[serde(alias = "failed")]
81 Failed,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct FileChangeItem {
87 pub id: String,
88 pub changes: Vec<FileUpdateChange>,
89 pub status: PatchApplyStatus,
90}
91
92#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
94#[serde(rename_all = "snake_case")]
95pub enum McpToolCallStatus {
96 #[serde(alias = "inProgress")]
97 InProgress,
98 #[serde(alias = "completed")]
99 Completed,
100 #[serde(alias = "failed")]
101 Failed,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct McpToolCallResult {
107 pub content: Vec<Value>,
108 pub structured_content: Value,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct McpToolCallError {
114 pub message: String,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct McpToolCallItem {
120 pub id: String,
121 pub server: String,
122 pub tool: String,
123 pub arguments: Value,
124 #[serde(skip_serializing_if = "Option::is_none")]
125 pub result: Option<McpToolCallResult>,
126 #[serde(skip_serializing_if = "Option::is_none")]
127 pub error: Option<McpToolCallError>,
128 pub status: McpToolCallStatus,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct AgentMessageItem {
138 pub id: String,
139 #[serde(default)]
140 pub text: String,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct UserMessageContent {
146 #[serde(rename = "type")]
148 pub kind: String,
149 #[serde(default)]
151 pub text: String,
152 #[serde(default, skip_serializing_if = "Vec::is_empty")]
155 pub text_elements: Vec<Value>,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct UserMessageItem {
164 pub id: String,
165 pub content: Vec<UserMessageContent>,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct ReasoningItem {
174 pub id: String,
175 #[serde(default)]
176 pub text: String,
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize)]
181pub struct WebSearchItem {
182 pub id: String,
183 pub query: String,
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct ErrorItem {
189 pub id: String,
190 pub message: String,
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
195pub struct TodoItem {
196 pub text: String,
197 pub completed: bool,
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct TodoListItem {
203 pub id: String,
204 pub items: Vec<TodoItem>,
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize)]
217#[serde(tag = "type", rename_all = "snake_case")]
218pub enum ThreadItem {
219 #[serde(alias = "userMessage")]
221 UserMessage(UserMessageItem),
222 #[serde(alias = "agentMessage")]
224 AgentMessage(AgentMessageItem),
225 #[serde(alias = "reasoning")]
227 Reasoning(ReasoningItem),
228 #[serde(alias = "commandExecution")]
230 CommandExecution(CommandExecutionItem),
231 #[serde(alias = "fileChange")]
233 FileChange(FileChangeItem),
234 #[serde(alias = "mcpToolCall")]
236 McpToolCall(McpToolCallItem),
237 #[serde(alias = "webSearch")]
239 WebSearch(WebSearchItem),
240 #[serde(alias = "todoList")]
242 TodoList(TodoListItem),
243 #[serde(alias = "error")]
245 Error(ErrorItem),
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 #[test]
253 fn test_deserialize_agent_message() {
254 let json = r#"{"type":"agent_message","id":"msg_1","text":"Hello world"}"#;
255 let item: ThreadItem = serde_json::from_str(json).unwrap();
256 assert!(matches!(item, ThreadItem::AgentMessage(ref m) if m.text == "Hello world"));
257 }
258
259 #[test]
260 fn test_deserialize_command_execution() {
261 let json = r#"{"type":"command_execution","id":"cmd_1","command":"ls -la","aggregated_output":"total 0","exit_code":0,"status":"completed"}"#;
262 let item: ThreadItem = serde_json::from_str(json).unwrap();
263 assert!(matches!(item, ThreadItem::CommandExecution(ref c) if c.exit_code == Some(0)));
264 }
265
266 #[test]
267 fn test_deserialize_file_change() {
268 let json = r#"{"type":"file_change","id":"fc_1","changes":[{"path":"src/main.rs","kind":"update"}],"status":"completed"}"#;
269 let item: ThreadItem = serde_json::from_str(json).unwrap();
270 assert!(
271 matches!(item, ThreadItem::FileChange(ref f) if f.changes[0].kind == PatchChangeKind::Update)
272 );
273 }
274
275 #[test]
276 fn test_deserialize_todo_list() {
277 let json = r#"{"type":"todo_list","id":"td_1","items":[{"text":"Fix bug","completed":false},{"text":"Write tests","completed":true}]}"#;
278 let item: ThreadItem = serde_json::from_str(json).unwrap();
279 assert!(matches!(item, ThreadItem::TodoList(ref t) if t.items.len() == 2));
280 }
281
282 #[test]
283 fn test_deserialize_error() {
284 let json = r#"{"type":"error","id":"err_1","message":"something went wrong"}"#;
285 let item: ThreadItem = serde_json::from_str(json).unwrap();
286 assert!(matches!(item, ThreadItem::Error(ref e) if e.message == "something went wrong"));
287 }
288
289 #[test]
290 fn test_deserialize_reasoning() {
291 let json = r#"{"type":"reasoning","id":"r_1","text":"Let me think about this..."}"#;
292 let item: ThreadItem = serde_json::from_str(json).unwrap();
293 assert!(matches!(item, ThreadItem::Reasoning(ref r) if r.text.contains("think")));
294 }
295
296 #[test]
297 fn test_deserialize_web_search() {
298 let json = r#"{"type":"web_search","id":"ws_1","query":"rust serde tutorial"}"#;
299 let item: ThreadItem = serde_json::from_str(json).unwrap();
300 assert!(matches!(item, ThreadItem::WebSearch(ref w) if w.query == "rust serde tutorial"));
301 }
302
303 #[test]
304 fn test_deserialize_mcp_tool_call() {
305 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}}"#;
306 let item: ThreadItem = serde_json::from_str(json).unwrap();
307 assert!(matches!(item, ThreadItem::McpToolCall(ref m) if m.tool == "my-tool"));
308 }
309
310 #[test]
311 fn test_deserialize_camel_case_agent_message() {
312 let json = r#"{"type":"agentMessage","id":"msg_1","text":"Hello"}"#;
313 let item: ThreadItem = serde_json::from_str(json).unwrap();
314 assert!(matches!(item, ThreadItem::AgentMessage(ref m) if m.text == "Hello"));
315 }
316
317 #[test]
318 fn test_deserialize_camel_case_command_execution() {
319 let json = r#"{"type":"commandExecution","id":"cmd_1","command":"ls","aggregated_output":"","status":"completed"}"#;
320 let item: ThreadItem = serde_json::from_str(json).unwrap();
321 assert!(matches!(item, ThreadItem::CommandExecution(_)));
322 }
323
324 #[test]
325 fn test_deserialize_camel_case_file_change() {
326 let json = r#"{"type":"fileChange","id":"fc_1","changes":[],"status":"completed"}"#;
327 let item: ThreadItem = serde_json::from_str(json).unwrap();
328 assert!(matches!(item, ThreadItem::FileChange(_)));
329 }
330
331 #[test]
332 fn test_command_execution_status_declined() {
333 let json = r#"{"type":"command_execution","id":"cmd_1","command":"rm -rf /","aggregated_output":"","status":"declined"}"#;
334 let item: ThreadItem = serde_json::from_str(json).unwrap();
335 assert!(
336 matches!(item, ThreadItem::CommandExecution(ref c) if c.status == CommandExecutionStatus::Declined)
337 );
338 }
339}