Skip to main content

codex_cli_sdk/types/
items.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4/// A typed item that occurs during a turn.
5///
6/// Items flow through the lifecycle: `ItemStarted` → `ItemUpdated` → `ItemCompleted`.
7#[derive(Debug, Clone, Serialize, Deserialize)]
8#[serde(tag = "type")]
9pub enum ThreadItem {
10    /// Agent text response.
11    #[serde(rename = "agent_message")]
12    AgentMessage {
13        id: String,
14        #[serde(default)]
15        text: String,
16    },
17
18    /// Agent reasoning/thinking.
19    #[serde(rename = "reasoning")]
20    Reasoning {
21        id: String,
22        #[serde(default)]
23        text: String,
24    },
25
26    /// Shell command execution.
27    #[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    /// File change (patch application).
41    #[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    /// MCP tool invocation.
51    #[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    /// Web search invocation.
69    #[serde(rename = "web_search")]
70    WebSearch {
71        id: String,
72        #[serde(default)]
73        query: String,
74    },
75
76    /// Agent-managed todo list.
77    #[serde(rename = "todo_list")]
78    TodoList {
79        id: String,
80        #[serde(default)]
81        items: Vec<TodoItem>,
82    },
83
84    /// Error item.
85    #[serde(rename = "error")]
86    Error {
87        id: String,
88        #[serde(default)]
89        message: String,
90    },
91}
92
93impl ThreadItem {
94    /// Get the item ID regardless of variant.
95    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// ── Supporting types ───────────────────────────────────────────
110
111#[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}