1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
6#[serde(rename_all = "snake_case")]
7pub enum CommandExecutionStatus {
8 InProgress,
9 Completed,
10 Failed,
11}
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct CommandExecutionItem {
16 pub id: String,
17 pub command: String,
18 pub aggregated_output: String,
19 #[serde(skip_serializing_if = "Option::is_none")]
20 pub exit_code: Option<i32>,
21 pub status: CommandExecutionStatus,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(rename_all = "snake_case")]
27pub enum PatchChangeKind {
28 Add,
29 Delete,
30 Update,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct FileUpdateChange {
36 pub path: String,
37 pub kind: PatchChangeKind,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42#[serde(rename_all = "snake_case")]
43pub enum PatchApplyStatus {
44 Completed,
45 Failed,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct FileChangeItem {
51 pub id: String,
52 pub changes: Vec<FileUpdateChange>,
53 pub status: PatchApplyStatus,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(rename_all = "snake_case")]
59pub enum McpToolCallStatus {
60 InProgress,
61 Completed,
62 Failed,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct McpToolCallResult {
68 pub content: Vec<Value>,
69 pub structured_content: Value,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct McpToolCallError {
75 pub message: String,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct McpToolCallItem {
81 pub id: String,
82 pub server: String,
83 pub tool: String,
84 pub arguments: Value,
85 #[serde(skip_serializing_if = "Option::is_none")]
86 pub result: Option<McpToolCallResult>,
87 #[serde(skip_serializing_if = "Option::is_none")]
88 pub error: Option<McpToolCallError>,
89 pub status: McpToolCallStatus,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct AgentMessageItem {
95 pub id: String,
96 pub text: String,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct ReasoningItem {
102 pub id: String,
103 pub text: String,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct WebSearchItem {
109 pub id: String,
110 pub query: String,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct ErrorItem {
116 pub id: String,
117 pub message: String,
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct TodoItem {
123 pub text: String,
124 pub completed: bool,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct TodoListItem {
130 pub id: String,
131 pub items: Vec<TodoItem>,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
136#[serde(tag = "type", rename_all = "snake_case")]
137pub enum ThreadItem {
138 AgentMessage(AgentMessageItem),
139 Reasoning(ReasoningItem),
140 CommandExecution(CommandExecutionItem),
141 FileChange(FileChangeItem),
142 McpToolCall(McpToolCallItem),
143 WebSearch(WebSearchItem),
144 TodoList(TodoListItem),
145 Error(ErrorItem),
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[test]
153 fn test_deserialize_agent_message() {
154 let json = r#"{"type":"agent_message","id":"msg_1","text":"Hello world"}"#;
155 let item: ThreadItem = serde_json::from_str(json).unwrap();
156 assert!(matches!(item, ThreadItem::AgentMessage(ref m) if m.text == "Hello world"));
157 }
158
159 #[test]
160 fn test_deserialize_command_execution() {
161 let json = r#"{"type":"command_execution","id":"cmd_1","command":"ls -la","aggregated_output":"total 0","exit_code":0,"status":"completed"}"#;
162 let item: ThreadItem = serde_json::from_str(json).unwrap();
163 assert!(matches!(item, ThreadItem::CommandExecution(ref c) if c.exit_code == Some(0)));
164 }
165
166 #[test]
167 fn test_deserialize_file_change() {
168 let json = r#"{"type":"file_change","id":"fc_1","changes":[{"path":"src/main.rs","kind":"update"}],"status":"completed"}"#;
169 let item: ThreadItem = serde_json::from_str(json).unwrap();
170 assert!(
171 matches!(item, ThreadItem::FileChange(ref f) if f.changes[0].kind == PatchChangeKind::Update)
172 );
173 }
174
175 #[test]
176 fn test_deserialize_todo_list() {
177 let json = r#"{"type":"todo_list","id":"td_1","items":[{"text":"Fix bug","completed":false},{"text":"Write tests","completed":true}]}"#;
178 let item: ThreadItem = serde_json::from_str(json).unwrap();
179 assert!(matches!(item, ThreadItem::TodoList(ref t) if t.items.len() == 2));
180 }
181
182 #[test]
183 fn test_deserialize_error() {
184 let json = r#"{"type":"error","id":"err_1","message":"something went wrong"}"#;
185 let item: ThreadItem = serde_json::from_str(json).unwrap();
186 assert!(matches!(item, ThreadItem::Error(ref e) if e.message == "something went wrong"));
187 }
188
189 #[test]
190 fn test_deserialize_reasoning() {
191 let json = r#"{"type":"reasoning","id":"r_1","text":"Let me think about this..."}"#;
192 let item: ThreadItem = serde_json::from_str(json).unwrap();
193 assert!(matches!(item, ThreadItem::Reasoning(ref r) if r.text.contains("think")));
194 }
195
196 #[test]
197 fn test_deserialize_web_search() {
198 let json = r#"{"type":"web_search","id":"ws_1","query":"rust serde tutorial"}"#;
199 let item: ThreadItem = serde_json::from_str(json).unwrap();
200 assert!(matches!(item, ThreadItem::WebSearch(ref w) if w.query == "rust serde tutorial"));
201 }
202
203 #[test]
204 fn test_deserialize_mcp_tool_call() {
205 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}}"#;
206 let item: ThreadItem = serde_json::from_str(json).unwrap();
207 assert!(matches!(item, ThreadItem::McpToolCall(ref m) if m.tool == "my-tool"));
208 }
209}