Skip to main content

codex_codes/io/
items.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4/// Status of a command execution.
5#[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/// A command execution item.
14#[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/// Kind of patch change applied to a file.
25#[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/// A single file update within a file change item.
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct FileUpdateChange {
36    pub path: String,
37    pub kind: PatchChangeKind,
38}
39
40/// Status of a patch apply operation.
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42#[serde(rename_all = "snake_case")]
43pub enum PatchApplyStatus {
44    Completed,
45    Failed,
46}
47
48/// A file change item representing one or more file modifications.
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct FileChangeItem {
51    pub id: String,
52    pub changes: Vec<FileUpdateChange>,
53    pub status: PatchApplyStatus,
54}
55
56/// Status of an MCP tool call.
57#[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/// Result of an MCP tool call.
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct McpToolCallResult {
68    pub content: Vec<Value>,
69    pub structured_content: Value,
70}
71
72/// Error from an MCP tool call.
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct McpToolCallError {
75    pub message: String,
76}
77
78/// An MCP tool call item.
79#[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/// An agent message item containing text output.
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct AgentMessageItem {
95    pub id: String,
96    pub text: String,
97}
98
99/// A reasoning item containing the model's chain-of-thought.
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct ReasoningItem {
102    pub id: String,
103    pub text: String,
104}
105
106/// A web search item.
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct WebSearchItem {
109    pub id: String,
110    pub query: String,
111}
112
113/// An error item.
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct ErrorItem {
116    pub id: String,
117    pub message: String,
118}
119
120/// A single todo entry within a todo list.
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct TodoItem {
123    pub text: String,
124    pub completed: bool,
125}
126
127/// A todo list item.
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct TodoListItem {
130    pub id: String,
131    pub items: Vec<TodoItem>,
132}
133
134/// All possible thread item types emitted by the Codex CLI.
135#[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}