1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
8#[serde(tag = "type")]
9pub enum ThreadItem {
10 #[serde(rename = "agent_message")]
12 AgentMessage {
13 id: String,
14 #[serde(default)]
15 text: String,
16 },
17
18 #[serde(rename = "reasoning")]
20 Reasoning {
21 id: String,
22 #[serde(default)]
23 text: String,
24 },
25
26 #[serde(rename = "command_execution")]
28 CommandExecution {
29 id: String,
30 #[serde(default)]
31 command: String,
32 #[serde(default)]
33 aggregated_output: String,
34 #[serde(default)]
35 exit_code: Option<i32>,
36 #[serde(default)]
37 status: CommandExecutionStatus,
38 },
39
40 #[serde(rename = "file_change")]
42 FileChange {
43 id: String,
44 #[serde(default)]
45 changes: Vec<FileUpdateChange>,
46 #[serde(default)]
47 status: PatchApplyStatus,
48 },
49
50 #[serde(rename = "mcp_tool_call")]
52 McpToolCall {
53 id: String,
54 #[serde(default)]
55 server: String,
56 #[serde(default)]
57 tool: String,
58 #[serde(default)]
59 arguments: Value,
60 #[serde(default)]
61 result: Option<McpToolCallResult>,
62 #[serde(default)]
63 error: Option<McpToolCallError>,
64 #[serde(default)]
65 status: McpToolCallStatus,
66 },
67
68 #[serde(rename = "web_search")]
70 WebSearch {
71 id: String,
72 #[serde(default)]
73 query: String,
74 },
75
76 #[serde(rename = "todo_list")]
78 TodoList {
79 id: String,
80 #[serde(default)]
81 items: Vec<TodoItem>,
82 },
83
84 #[serde(rename = "error")]
86 Error {
87 id: String,
88 #[serde(default)]
89 message: String,
90 },
91}
92
93impl ThreadItem {
94 pub fn id(&self) -> &str {
96 match self {
97 Self::AgentMessage { id, .. }
98 | Self::Reasoning { id, .. }
99 | Self::CommandExecution { id, .. }
100 | Self::FileChange { id, .. }
101 | Self::McpToolCall { id, .. }
102 | Self::WebSearch { id, .. }
103 | Self::TodoList { id, .. }
104 | Self::Error { id, .. } => id,
105 }
106 }
107}
108
109#[derive(Debug, Clone, Default, Serialize, Deserialize)]
112#[serde(rename_all = "snake_case")]
113pub enum CommandExecutionStatus {
114 #[default]
115 InProgress,
116 Completed,
117 Failed,
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct FileUpdateChange {
122 pub path: String,
123 pub kind: PatchChangeKind,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
127#[serde(rename_all = "snake_case")]
128pub enum PatchChangeKind {
129 Add,
130 Delete,
131 Update,
132}
133
134#[derive(Debug, Clone, Default, Serialize, Deserialize)]
135#[serde(rename_all = "snake_case")]
136pub enum PatchApplyStatus {
137 #[default]
138 Completed,
139 Failed,
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct McpToolCallResult {
144 #[serde(default)]
145 pub content: Vec<Value>,
146 #[serde(default)]
147 pub structured_content: Value,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct McpToolCallError {
152 pub message: String,
153}
154
155#[derive(Debug, Clone, Default, Serialize, Deserialize)]
156#[serde(rename_all = "snake_case")]
157pub enum McpToolCallStatus {
158 #[default]
159 InProgress,
160 Completed,
161 Failed,
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct TodoItem {
166 pub text: String,
167 #[serde(default)]
168 pub completed: bool,
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[test]
176 fn agent_message_round_trip() {
177 let item = ThreadItem::AgentMessage {
178 id: "msg-1".into(),
179 text: "Hello".into(),
180 };
181 let json = serde_json::to_string(&item).unwrap();
182 let parsed: ThreadItem = serde_json::from_str(&json).unwrap();
183 assert_eq!(parsed.id(), "msg-1");
184 }
185
186 #[test]
187 fn command_execution_defaults() {
188 let json = r#"{"type":"command_execution","id":"cmd-1"}"#;
189 let item: ThreadItem = serde_json::from_str(json).unwrap();
190 let ThreadItem::CommandExecution {
191 command, exit_code, ..
192 } = item
193 else {
194 panic!("wrong variant");
195 };
196 assert_eq!(command, "");
197 assert_eq!(exit_code, None);
198 }
199
200 #[test]
201 fn mcp_tool_call_round_trip() {
202 let json = r#"{"type":"mcp_tool_call","id":"mcp-1","server":"test","tool":"search","arguments":{},"status":"completed"}"#;
203 let item: ThreadItem = serde_json::from_str(json).unwrap();
204 assert_eq!(item.id(), "mcp-1");
205 }
206
207 #[test]
208 fn todo_list_round_trip() {
209 let json =
210 r#"{"type":"todo_list","id":"todo-1","items":[{"text":"Do thing","completed":false}]}"#;
211 let item: ThreadItem = serde_json::from_str(json).unwrap();
212 let ThreadItem::TodoList { items, .. } = item else {
213 panic!("wrong variant");
214 };
215 assert_eq!(items.len(), 1);
216 assert_eq!(items[0].text, "Do thing");
217 assert!(!items[0].completed);
218 }
219}