use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum ThreadItem {
#[serde(rename = "agent_message")]
AgentMessage {
id: String,
#[serde(default)]
text: String,
},
#[serde(rename = "reasoning")]
Reasoning {
id: String,
#[serde(default)]
text: String,
},
#[serde(rename = "command_execution")]
CommandExecution {
id: String,
#[serde(default)]
command: String,
#[serde(default)]
aggregated_output: String,
#[serde(default)]
exit_code: Option<i32>,
#[serde(default)]
status: CommandExecutionStatus,
},
#[serde(rename = "file_change")]
FileChange {
id: String,
#[serde(default)]
changes: Vec<FileUpdateChange>,
#[serde(default)]
status: PatchApplyStatus,
},
#[serde(rename = "mcp_tool_call")]
McpToolCall {
id: String,
#[serde(default)]
server: String,
#[serde(default)]
tool: String,
#[serde(default)]
arguments: Value,
#[serde(default)]
result: Option<McpToolCallResult>,
#[serde(default)]
error: Option<McpToolCallError>,
#[serde(default)]
status: McpToolCallStatus,
},
#[serde(rename = "web_search")]
WebSearch {
id: String,
#[serde(default)]
query: String,
},
#[serde(rename = "todo_list")]
TodoList {
id: String,
#[serde(default)]
items: Vec<TodoItem>,
},
#[serde(rename = "error")]
Error {
id: String,
#[serde(default)]
message: String,
},
}
impl ThreadItem {
pub fn id(&self) -> &str {
match self {
Self::AgentMessage { id, .. }
| Self::Reasoning { id, .. }
| Self::CommandExecution { id, .. }
| Self::FileChange { id, .. }
| Self::McpToolCall { id, .. }
| Self::WebSearch { id, .. }
| Self::TodoList { id, .. }
| Self::Error { id, .. } => id,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CommandExecutionStatus {
#[default]
InProgress,
Completed,
Failed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileUpdateChange {
pub path: String,
pub kind: PatchChangeKind,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PatchChangeKind {
Add,
Delete,
Update,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PatchApplyStatus {
#[default]
Completed,
Failed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpToolCallResult {
#[serde(default)]
pub content: Vec<Value>,
#[serde(default)]
pub structured_content: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpToolCallError {
pub message: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum McpToolCallStatus {
#[default]
InProgress,
Completed,
Failed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TodoItem {
pub text: String,
#[serde(default)]
pub completed: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn agent_message_round_trip() {
let item = ThreadItem::AgentMessage {
id: "msg-1".into(),
text: "Hello".into(),
};
let json = serde_json::to_string(&item).unwrap();
let parsed: ThreadItem = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.id(), "msg-1");
}
#[test]
fn command_execution_defaults() {
let json = r#"{"type":"command_execution","id":"cmd-1"}"#;
let item: ThreadItem = serde_json::from_str(json).unwrap();
let ThreadItem::CommandExecution {
command, exit_code, ..
} = item
else {
panic!("wrong variant");
};
assert_eq!(command, "");
assert_eq!(exit_code, None);
}
#[test]
fn mcp_tool_call_round_trip() {
let json = r#"{"type":"mcp_tool_call","id":"mcp-1","server":"test","tool":"search","arguments":{},"status":"completed"}"#;
let item: ThreadItem = serde_json::from_str(json).unwrap();
assert_eq!(item.id(), "mcp-1");
}
#[test]
fn todo_list_round_trip() {
let json =
r#"{"type":"todo_list","id":"todo-1","items":[{"text":"Do thing","completed":false}]}"#;
let item: ThreadItem = serde_json::from_str(json).unwrap();
let ThreadItem::TodoList { items, .. } = item else {
panic!("wrong variant");
};
assert_eq!(items.len(), 1);
assert_eq!(items[0].text, "Do thing");
assert!(!items[0].completed);
}
}