codex_cli_sdk/types/
events.rs1use crate::types::items::ThreadItem;
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(tag = "type")]
12pub enum ThreadEvent {
13 #[serde(rename = "thread.started")]
15 ThreadStarted { thread_id: String },
16
17 #[serde(rename = "turn.started")]
19 TurnStarted,
20
21 #[serde(rename = "turn.completed")]
23 TurnCompleted { usage: Usage },
24
25 #[serde(rename = "turn.failed")]
27 TurnFailed { error: ThreadError },
28
29 #[serde(rename = "item.started")]
31 ItemStarted { item: ThreadItem },
32
33 #[serde(rename = "item.updated")]
35 ItemUpdated { item: ThreadItem },
36
37 #[serde(rename = "item.completed")]
39 ItemCompleted { item: ThreadItem },
40
41 #[serde(rename = "exec_approval_request")]
43 ApprovalRequest(ApprovalRequestEvent),
44
45 #[serde(rename = "apply_patch_approval_request")]
47 PatchApprovalRequest(PatchApprovalRequestEvent),
48
49 #[serde(rename = "error")]
51 Error { message: String },
52}
53
54#[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#[derive(Debug, Clone)]
91pub struct Turn {
92 pub events: Vec<ThreadEvent>,
94 pub final_response: String,
96 pub usage: Option<Usage>,
98}
99
100pub 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); }
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}