Skip to main content

codex_cli_sdk/types/
events.rs

1use crate::types::items::ThreadItem;
2use serde::{Deserialize, Serialize};
3
4// ── High-level SDK event (item lifecycle model) ────────────────
5
6/// Events yielded to SDK consumers during a turn.
7///
8/// Matches the official TypeScript SDK's event model:
9/// lifecycle events + item lifecycle wrappers.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(tag = "type")]
12pub enum ThreadEvent {
13    /// Thread/session initialized.
14    #[serde(rename = "thread.started")]
15    ThreadStarted { thread_id: String },
16
17    /// A new turn has begun processing.
18    #[serde(rename = "turn.started")]
19    TurnStarted,
20
21    /// The turn completed successfully.
22    #[serde(rename = "turn.completed")]
23    TurnCompleted { usage: Usage },
24
25    /// The turn failed or was aborted.
26    #[serde(rename = "turn.failed")]
27    TurnFailed { error: ThreadError },
28
29    /// An item has started (streaming begins).
30    #[serde(rename = "item.started")]
31    ItemStarted { item: ThreadItem },
32
33    /// An item has been updated (streaming delta).
34    #[serde(rename = "item.updated")]
35    ItemUpdated { item: ThreadItem },
36
37    /// An item has completed.
38    #[serde(rename = "item.completed")]
39    ItemCompleted { item: ThreadItem },
40
41    /// The agent is requesting approval for a command execution.
42    #[serde(rename = "exec_approval_request")]
43    ApprovalRequest(ApprovalRequestEvent),
44
45    /// The agent is requesting approval for a file patch.
46    #[serde(rename = "apply_patch_approval_request")]
47    PatchApprovalRequest(PatchApprovalRequestEvent),
48
49    /// An error occurred.
50    #[serde(rename = "error")]
51    Error { message: String },
52}
53
54// ── Approval event structs ─────────────────────────────────────
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct ApprovalRequestEvent {
58    pub id: String,
59    #[serde(default)]
60    pub command: String,
61    #[serde(default)]
62    pub cwd: Option<std::path::PathBuf>,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct PatchApprovalRequestEvent {
67    pub id: String,
68    #[serde(default)]
69    pub changes: std::collections::HashMap<String, serde_json::Value>,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct ThreadError {
74    pub message: String,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct Usage {
79    #[serde(default)]
80    pub input_tokens: u64,
81    #[serde(default)]
82    pub cached_input_tokens: u64,
83    #[serde(default)]
84    pub output_tokens: u64,
85}
86
87// ── Turn result (buffered mode) ────────────────────────────────
88
89/// Result of a buffered `thread.run()` call.
90#[derive(Debug, Clone)]
91pub struct Turn {
92    /// All events received during the turn.
93    pub events: Vec<ThreadEvent>,
94    /// The final agent response text.
95    pub final_response: String,
96    /// Token usage for the turn, if reported.
97    pub usage: Option<Usage>,
98}
99
100/// Handle for streaming `thread.run_streamed()` — yields `ThreadEvent`s.
101pub struct StreamedTurn {
102    inner: std::pin::Pin<Box<dyn futures_core::Stream<Item = crate::Result<ThreadEvent>> + Send>>,
103}
104
105impl StreamedTurn {
106    pub(crate) fn new(
107        stream: impl futures_core::Stream<Item = crate::Result<ThreadEvent>> + Send + 'static,
108    ) -> Self {
109        Self {
110            inner: Box::pin(stream),
111        }
112    }
113}
114
115impl futures_core::Stream for StreamedTurn {
116    type Item = crate::Result<ThreadEvent>;
117
118    fn poll_next(
119        mut self: std::pin::Pin<&mut Self>,
120        cx: &mut std::task::Context<'_>,
121    ) -> std::task::Poll<Option<Self::Item>> {
122        self.inner.as_mut().poll_next(cx)
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn thread_started_round_trip() {
132        let event = ThreadEvent::ThreadStarted {
133            thread_id: "test-123".into(),
134        };
135        let json = serde_json::to_string(&event).unwrap();
136        let parsed: ThreadEvent = serde_json::from_str(&json).unwrap();
137        let ThreadEvent::ThreadStarted { thread_id } = parsed else {
138            panic!("wrong variant");
139        };
140        assert_eq!(thread_id, "test-123");
141    }
142
143    #[test]
144    fn turn_completed_round_trip() {
145        let json = r#"{"type":"turn.completed","usage":{"input_tokens":100,"output_tokens":50}}"#;
146        let event: ThreadEvent = serde_json::from_str(json).unwrap();
147        let ThreadEvent::TurnCompleted { usage } = event else {
148            panic!("wrong variant");
149        };
150        assert_eq!(usage.input_tokens, 100);
151        assert_eq!(usage.output_tokens, 50);
152        assert_eq!(usage.cached_input_tokens, 0); // default
153    }
154
155    #[test]
156    fn item_started_agent_message() {
157        let json =
158            r#"{"type":"item.started","item":{"type":"agent_message","id":"msg-1","text":""}}"#;
159        let event: ThreadEvent = serde_json::from_str(json).unwrap();
160        let ThreadEvent::ItemStarted { item } = event else {
161            panic!("wrong variant");
162        };
163        assert_eq!(item.id(), "msg-1");
164    }
165
166    #[test]
167    fn approval_request_round_trip() {
168        let json =
169            r#"{"type":"exec_approval_request","id":"ap-1","command":"rm -rf /","cwd":null}"#;
170        let event: ThreadEvent = serde_json::from_str(json).unwrap();
171        let ThreadEvent::ApprovalRequest(req) = event else {
172            panic!("wrong variant");
173        };
174        assert_eq!(req.id, "ap-1");
175        assert_eq!(req.command, "rm -rf /");
176    }
177
178    #[test]
179    fn error_event() {
180        let json = r#"{"type":"error","message":"something broke"}"#;
181        let event: ThreadEvent = serde_json::from_str(json).unwrap();
182        let ThreadEvent::Error { message } = event else {
183            panic!("wrong variant");
184        };
185        assert_eq!(message, "something broke");
186    }
187}