Skip to main content

codex/
events.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::collections::BTreeMap;
4use std::path::PathBuf;
5
6/// Single JSONL event emitted by `codex exec --json`.
7///
8/// Each line on stdout maps to a [`ThreadEvent`] with lifecycle edges:
9/// - `thread.started` is emitted once per invocation.
10/// - `turn.started` begins the turn associated with the provided prompt.
11/// - one or more `item.*` events stream output and tool activity.
12/// - `turn.completed` or `turn.failed` closes the stream; `error` captures transport-level failures.
13///
14/// Item variants mirror the upstream `item_type` field: `agent_message`, `reasoning`,
15/// `command_execution`, `file_change`, `mcp_tool_call`, `web_search`, `todo_list`, and `error`.
16/// Unknown or future fields are preserved in `extra` maps to keep the parser forward-compatible.
17#[derive(Clone, Debug, Deserialize, Serialize)]
18#[serde(tag = "type")]
19pub enum ThreadEvent {
20    #[serde(rename = "thread.started", alias = "thread.resumed")]
21    ThreadStarted(ThreadStarted),
22    #[serde(rename = "turn.started")]
23    TurnStarted(TurnStarted),
24    #[serde(rename = "turn.completed")]
25    TurnCompleted(TurnCompleted),
26    #[serde(rename = "turn.failed")]
27    TurnFailed(TurnFailed),
28    #[serde(rename = "item.started", alias = "item.created")]
29    ItemStarted(ItemEnvelope<ItemSnapshot>),
30    #[serde(rename = "item.delta", alias = "item.updated")]
31    ItemDelta(ItemDelta),
32    #[serde(rename = "item.completed")]
33    ItemCompleted(ItemEnvelope<ItemSnapshot>),
34    #[serde(rename = "item.failed")]
35    ItemFailed(ItemEnvelope<ItemFailure>),
36    #[serde(rename = "error")]
37    Error(EventError),
38}
39
40impl ThreadEvent {
41    pub fn thread_id(&self) -> Option<&str> {
42        match self {
43            ThreadEvent::ThreadStarted(event) => Some(event.thread_id.as_str()),
44            ThreadEvent::TurnStarted(event) => Some(event.thread_id.as_str()),
45            ThreadEvent::TurnCompleted(event) => Some(event.thread_id.as_str()),
46            ThreadEvent::TurnFailed(event) => Some(event.thread_id.as_str()),
47            ThreadEvent::ItemStarted(event) => Some(event.thread_id.as_str()),
48            ThreadEvent::ItemDelta(event) => Some(event.thread_id.as_str()),
49            ThreadEvent::ItemCompleted(event) => Some(event.thread_id.as_str()),
50            ThreadEvent::ItemFailed(event) => Some(event.thread_id.as_str()),
51            ThreadEvent::Error(_) => None,
52        }
53    }
54}
55
56/// Marks the start of a new thread.
57#[derive(Clone, Debug, Deserialize, Serialize)]
58pub struct ThreadStarted {
59    pub thread_id: String,
60    #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
61    pub extra: BTreeMap<String, Value>,
62}
63
64/// Indicates the CLI accepted a new turn within a thread.
65#[derive(Clone, Debug, Deserialize, Serialize)]
66pub struct TurnStarted {
67    pub thread_id: String,
68    pub turn_id: String,
69    /// Original input text when upstream echoes it; may be omitted for security reasons.
70    #[serde(default, skip_serializing_if = "Option::is_none")]
71    pub input_text: Option<String>,
72    #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
73    pub extra: BTreeMap<String, Value>,
74}
75
76/// Reports a completed turn.
77#[derive(Clone, Debug, Deserialize, Serialize)]
78pub struct TurnCompleted {
79    pub thread_id: String,
80    pub turn_id: String,
81    /// Identifier of the last output item when provided by the CLI.
82    #[serde(default, skip_serializing_if = "Option::is_none")]
83    pub last_item_id: Option<String>,
84    #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
85    pub extra: BTreeMap<String, Value>,
86}
87
88/// Indicates a turn-level failure.
89#[derive(Clone, Debug, Deserialize, Serialize)]
90pub struct TurnFailed {
91    pub thread_id: String,
92    pub turn_id: String,
93    pub error: EventError,
94    #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
95    pub extra: BTreeMap<String, Value>,
96}
97
98/// Shared wrapper for item events that always include thread/turn context.
99#[derive(Clone, Debug, Deserialize, Serialize)]
100pub struct ItemEnvelope<T> {
101    pub thread_id: String,
102    pub turn_id: String,
103    #[serde(flatten)]
104    pub item: T,
105}
106
107/// Snapshot of an item at start/completion time.
108#[derive(Clone, Debug, Deserialize, Serialize)]
109pub struct ItemSnapshot {
110    #[serde(rename = "item_id", alias = "id")]
111    pub item_id: String,
112    #[serde(default, skip_serializing_if = "Option::is_none")]
113    pub index: Option<u32>,
114    #[serde(default)]
115    pub status: ItemStatus,
116    #[serde(flatten)]
117    pub payload: ItemPayload,
118    #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
119    pub extra: BTreeMap<String, Value>,
120}
121
122/// Streaming delta describing the next piece of an item.
123#[derive(Clone, Debug, Deserialize, Serialize)]
124pub struct ItemDelta {
125    pub thread_id: String,
126    pub turn_id: String,
127    #[serde(rename = "item_id", alias = "id")]
128    pub item_id: String,
129    #[serde(default, skip_serializing_if = "Option::is_none")]
130    pub index: Option<u32>,
131    #[serde(flatten)]
132    pub delta: ItemDeltaPayload,
133    #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
134    pub extra: BTreeMap<String, Value>,
135}
136
137/// Terminal item failure event.
138#[derive(Clone, Debug, Deserialize, Serialize)]
139pub struct ItemFailure {
140    #[serde(rename = "item_id", alias = "id")]
141    pub item_id: String,
142    #[serde(default, skip_serializing_if = "Option::is_none")]
143    pub index: Option<u32>,
144    pub error: EventError,
145    #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
146    pub extra: BTreeMap<String, Value>,
147}
148
149/// Fully-typed item payload for start/completed events.
150#[derive(Clone, Debug, Deserialize, Serialize)]
151#[serde(tag = "item_type", content = "content", rename_all = "snake_case")]
152pub enum ItemPayload {
153    AgentMessage(TextContent),
154    Reasoning(TextContent),
155    CommandExecution(CommandExecutionState),
156    FileChange(FileChangeState),
157    McpToolCall(McpToolCallState),
158    WebSearch(WebSearchState),
159    TodoList(TodoListState),
160    Error(EventError),
161}
162
163/// Delta form of an item payload. Each delta should be applied in order to reconstruct the item.
164#[derive(Clone, Debug, Deserialize, Serialize)]
165#[serde(tag = "item_type", content = "delta", rename_all = "snake_case")]
166pub enum ItemDeltaPayload {
167    AgentMessage(TextDelta),
168    Reasoning(TextDelta),
169    CommandExecution(CommandExecutionDelta),
170    FileChange(FileChangeDelta),
171    McpToolCall(McpToolCallDelta),
172    WebSearch(WebSearchDelta),
173    TodoList(TodoListDelta),
174    Error(EventError),
175}
176
177/// Item status supplied by the CLI for bookkeeping.
178#[derive(Clone, Debug, Default, Deserialize, Serialize, Eq, PartialEq)]
179#[serde(rename_all = "snake_case")]
180pub enum ItemStatus {
181    #[default]
182    InProgress,
183    Completed,
184    Failed,
185    #[serde(other)]
186    Unknown,
187}
188
189/// Human-readable content emitted by the agent.
190#[derive(Clone, Debug, Deserialize, Serialize)]
191pub struct TextContent {
192    pub text: String,
193    #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
194    pub extra: BTreeMap<String, Value>,
195}
196
197/// Incremental content fragment for streaming items.
198#[derive(Clone, Debug, Deserialize, Serialize)]
199pub struct TextDelta {
200    #[serde(rename = "text_delta", alias = "text")]
201    pub text_delta: String,
202    #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
203    pub extra: BTreeMap<String, Value>,
204}
205
206/// Snapshot of a command execution, including accumulated stdout/stderr.
207#[derive(Clone, Debug, Deserialize, Serialize)]
208pub struct CommandExecutionState {
209    pub command: String,
210    #[serde(default, skip_serializing_if = "Option::is_none")]
211    pub exit_code: Option<i32>,
212    #[serde(
213        default,
214        skip_serializing_if = "String::is_empty",
215        alias = "aggregated_output",
216        alias = "output"
217    )]
218    pub stdout: String,
219    #[serde(
220        default,
221        skip_serializing_if = "String::is_empty",
222        alias = "error_output",
223        alias = "err"
224    )]
225    pub stderr: String,
226    #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
227    pub extra: BTreeMap<String, Value>,
228}
229
230/// Streaming delta for command execution.
231#[derive(Clone, Debug, Deserialize, Serialize)]
232pub struct CommandExecutionDelta {
233    #[serde(
234        default,
235        skip_serializing_if = "String::is_empty",
236        alias = "aggregated_output",
237        alias = "output"
238    )]
239    pub stdout: String,
240    #[serde(
241        default,
242        skip_serializing_if = "String::is_empty",
243        alias = "error_output",
244        alias = "err"
245    )]
246    pub stderr: String,
247    #[serde(default, skip_serializing_if = "Option::is_none")]
248    pub exit_code: Option<i32>,
249    #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
250    pub extra: BTreeMap<String, Value>,
251}
252
253/// File change or diff applied by the agent.
254#[derive(Clone, Debug, Deserialize, Serialize)]
255pub struct FileChangeState {
256    #[serde(alias = "file_path")]
257    pub path: PathBuf,
258    #[serde(default, skip_serializing_if = "Option::is_none")]
259    pub change: Option<FileChangeKind>,
260    #[serde(default, skip_serializing_if = "Option::is_none", alias = "patch")]
261    pub diff: Option<String>,
262    #[serde(default, skip_serializing_if = "Option::is_none")]
263    pub exit_code: Option<i32>,
264    #[serde(
265        default,
266        skip_serializing_if = "String::is_empty",
267        alias = "aggregated_output",
268        alias = "output"
269    )]
270    pub stdout: String,
271    #[serde(
272        default,
273        skip_serializing_if = "String::is_empty",
274        alias = "error_output",
275        alias = "err"
276    )]
277    pub stderr: String,
278    #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
279    pub extra: BTreeMap<String, Value>,
280}
281
282/// Streaming delta describing a file change.
283#[derive(Clone, Debug, Deserialize, Serialize)]
284pub struct FileChangeDelta {
285    #[serde(default, skip_serializing_if = "Option::is_none", alias = "patch")]
286    pub diff: Option<String>,
287    #[serde(
288        default,
289        skip_serializing_if = "String::is_empty",
290        alias = "aggregated_output",
291        alias = "output"
292    )]
293    pub stdout: String,
294    #[serde(
295        default,
296        skip_serializing_if = "String::is_empty",
297        alias = "error_output",
298        alias = "err"
299    )]
300    pub stderr: String,
301    #[serde(default, skip_serializing_if = "Option::is_none")]
302    pub exit_code: Option<i32>,
303    #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
304    pub extra: BTreeMap<String, Value>,
305}
306
307/// Type of file operation being reported.
308#[derive(Clone, Copy, Debug, Deserialize, Serialize, Eq, PartialEq)]
309#[serde(rename_all = "snake_case")]
310pub enum FileChangeKind {
311    Apply,
312    Diff,
313    #[serde(other)]
314    Unknown,
315}
316
317/// State of an MCP tool call.
318#[derive(Clone, Debug, Deserialize, Serialize)]
319pub struct McpToolCallState {
320    #[serde(alias = "server")]
321    pub server_name: String,
322    #[serde(alias = "tool")]
323    pub tool_name: String,
324    #[serde(default, skip_serializing_if = "Option::is_none")]
325    pub arguments: Option<Value>,
326    #[serde(default, skip_serializing_if = "Option::is_none")]
327    pub result: Option<Value>,
328    #[serde(default)]
329    pub status: ToolCallStatus,
330    #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
331    pub extra: BTreeMap<String, Value>,
332}
333
334/// Streaming delta for MCP tool call output.
335#[derive(Clone, Debug, Deserialize, Serialize)]
336pub struct McpToolCallDelta {
337    #[serde(default, skip_serializing_if = "Option::is_none")]
338    pub result: Option<Value>,
339    #[serde(default)]
340    pub status: ToolCallStatus,
341    #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
342    pub extra: BTreeMap<String, Value>,
343}
344
345/// Lifecycle state for a tool call.
346#[derive(Clone, Debug, Default, Deserialize, Serialize, Eq, PartialEq)]
347#[serde(rename_all = "snake_case")]
348pub enum ToolCallStatus {
349    #[default]
350    Pending,
351    Running,
352    Completed,
353    Failed,
354    #[serde(other)]
355    Unknown,
356}
357
358/// Details of a web search step.
359#[derive(Clone, Debug, Deserialize, Serialize)]
360pub struct WebSearchState {
361    pub query: String,
362    #[serde(default, skip_serializing_if = "Option::is_none")]
363    pub results: Option<Value>,
364    #[serde(default)]
365    pub status: WebSearchStatus,
366    #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
367    pub extra: BTreeMap<String, Value>,
368}
369
370/// Streaming delta for search results.
371#[derive(Clone, Debug, Deserialize, Serialize)]
372pub struct WebSearchDelta {
373    #[serde(default, skip_serializing_if = "Option::is_none")]
374    pub results: Option<Value>,
375    #[serde(default)]
376    pub status: WebSearchStatus,
377    #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
378    pub extra: BTreeMap<String, Value>,
379}
380
381/// Search progress indicator.
382#[derive(Clone, Debug, Default, Deserialize, Serialize, Eq, PartialEq)]
383#[serde(rename_all = "snake_case")]
384pub enum WebSearchStatus {
385    #[default]
386    Pending,
387    Running,
388    Completed,
389    Failed,
390    #[serde(other)]
391    Unknown,
392}
393
394/// Checklist maintained by the agent.
395#[derive(Clone, Debug, Deserialize, Serialize)]
396pub struct TodoListState {
397    #[serde(default, skip_serializing_if = "Vec::is_empty")]
398    pub items: Vec<TodoItem>,
399    #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
400    pub extra: BTreeMap<String, Value>,
401}
402
403/// Streaming delta for todo list mutations.
404#[derive(Clone, Debug, Deserialize, Serialize)]
405pub struct TodoListDelta {
406    #[serde(default, skip_serializing_if = "Vec::is_empty")]
407    pub items: Vec<TodoItem>,
408    #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
409    pub extra: BTreeMap<String, Value>,
410}
411
412/// Single todo item.
413#[derive(Clone, Debug, Deserialize, Serialize)]
414pub struct TodoItem {
415    pub title: String,
416    #[serde(default)]
417    pub completed: bool,
418    #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
419    pub extra: BTreeMap<String, Value>,
420}
421
422/// Error payload shared by turn/item failures.
423#[derive(Clone, Debug, Deserialize, Serialize)]
424pub struct EventError {
425    pub message: String,
426    #[serde(default, skip_serializing_if = "Option::is_none")]
427    pub code: Option<String>,
428    #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
429    pub extra: BTreeMap<String, Value>,
430}