1use std::time::Duration;
2
3use crate::protocol::{
4 ClientInfo, InitializeCapabilities, InitializeResponse, ThreadStartParams, ThreadStartResponse,
5 TurnError, TurnStartParams, TurnStartResponse,
6};
7use crate::transport::{AsyncBufRead, AsyncWrite};
8use crate::{
9 ApprovalHandler, CodexAppServerClient, DenyAllApprovalHandler, Error, Event, EventCollector,
10 EventStream, Result,
11};
12
13#[derive(Clone, Debug)]
18pub struct SessionOptions {
19 pub client_info: ClientInfo,
20 pub capabilities: Option<InitializeCapabilities>,
21 pub command: String,
22 pub extra_args: Vec<String>,
23 pub call_timeout: Option<Duration>,
24 pub events_capacity: Option<usize>,
25}
26
27impl SessionOptions {
28 pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
30 Self {
31 client_info: ClientInfo::new(name, version),
32 capabilities: None,
33 command: "codex".to_owned(),
34 extra_args: Vec::new(),
35 call_timeout: None,
36 events_capacity: None,
37 }
38 }
39
40 pub fn with_title(mut self, title: impl Into<String>) -> Self {
42 self.client_info = self.client_info.with_title(title);
43 self
44 }
45
46 pub fn with_command(mut self, command: impl Into<String>) -> Self {
48 self.command = command.into();
49 self
50 }
51
52 pub fn with_extra_arg(mut self, arg: impl Into<String>) -> Self {
54 self.extra_args.push(arg.into());
55 self
56 }
57
58 pub fn with_config(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
60 self.extra_args.push("-c".to_owned());
61 self.extra_args
62 .push(format!("{}={}", key.into(), value.into()));
63 self
64 }
65
66 pub fn with_capabilities(mut self, capabilities: InitializeCapabilities) -> Self {
68 self.capabilities = Some(capabilities);
69 self
70 }
71
72 pub fn with_call_timeout(mut self, timeout: Duration) -> Self {
74 self.call_timeout = Some(timeout);
75 self
76 }
77
78 pub fn with_events_capacity(mut self, events_capacity: usize) -> Self {
85 self.events_capacity = Some(events_capacity);
86 self
87 }
88}
89
90pub struct CodexSession {
96 client: CodexAppServerClient,
97 events: EventStream,
98 initialize_response: InitializeResponse,
99}
100
101#[derive(Clone, Debug)]
107pub struct TextTurnResult {
108 pub thread: ThreadStartResponse,
109 pub turn: TurnStartResponse,
110 pub events: EventCollector,
111}
112
113impl TextTurnResult {
114 pub fn agent_message(&self) -> &str {
116 self.events.agent_message()
117 }
118
119 pub fn latest_diff(&self) -> Option<&str> {
121 self.events.latest_diff()
122 }
123
124 pub fn errors(&self) -> &[TurnError] {
126 self.events.errors()
127 }
128}
129
130impl CodexSession {
131 pub async fn spawn(options: SessionOptions) -> Result<Self> {
133 let (client, events) = match options.events_capacity {
134 Some(capacity) => CodexAppServerClient::spawn_with_events_capacity(
135 &options.command,
136 &options.extra_args,
137 capacity,
138 )?,
139 None => CodexAppServerClient::spawn(&options.command, &options.extra_args)?,
140 };
141 Self::handshake(client, events, options).await
142 }
143
144 pub async fn connect_streams<R, W>(
148 reader: R,
149 writer: W,
150 options: SessionOptions,
151 ) -> Result<Self>
152 where
153 R: AsyncBufRead + Unpin + Send + 'static,
154 W: AsyncWrite + Unpin + Send + 'static,
155 {
156 let (client, events) = match options.events_capacity {
157 Some(capacity) => CodexAppServerClient::connect_streams_with_events_capacity(
158 reader, writer, capacity,
159 )?,
160 None => CodexAppServerClient::connect_streams(reader, writer),
161 };
162 Self::handshake(client, events, options).await
163 }
164
165 #[cfg(unix)]
167 pub async fn connect_unix(
168 path: impl AsRef<std::path::Path>,
169 options: SessionOptions,
170 ) -> Result<Self> {
171 let (client, events) = match options.events_capacity {
172 Some(capacity) => {
173 CodexAppServerClient::connect_unix_with_events_capacity(path, capacity).await?
174 }
175 None => CodexAppServerClient::connect_unix(path).await?,
176 };
177 Self::handshake(client, events, options).await
178 }
179
180 async fn handshake(
181 client: CodexAppServerClient,
182 events: EventStream,
183 options: SessionOptions,
184 ) -> Result<Self> {
185 let client = if let Some(timeout) = options.call_timeout {
186 client.with_call_timeout(timeout)
187 } else {
188 client
189 };
190 let initialize_response = client
191 .initialize(crate::protocol::InitializeParams {
192 capabilities: options.capabilities,
193 client_info: options.client_info,
194 })
195 .await?;
196 client.send_initialized()?;
197 Ok(Self {
198 client,
199 events,
200 initialize_response,
201 })
202 }
203
204 pub fn client(&self) -> &CodexAppServerClient {
205 &self.client
206 }
207
208 pub fn initialize_response(&self) -> &InitializeResponse {
210 &self.initialize_response
211 }
212
213 pub async fn next_event(&mut self) -> Option<Event> {
219 self.events.recv().await
220 }
221
222 pub async fn next_notification<H>(
225 &mut self,
226 handler: &H,
227 ) -> Option<crate::protocol::ServerNotification>
228 where
229 H: ApprovalHandler,
230 {
231 loop {
232 match self.events.recv().await? {
233 Event::Notification(notification) => return Some(notification),
234 Event::Request(request) => {
235 let reply = handler.handle(&request.request).await;
236 let _ = reply.send(request);
237 }
238 Event::Closed => return None,
239 }
240 }
241 }
242
243 pub async fn start_thread(&self, params: ThreadStartParams) -> Result<ThreadStartResponse> {
245 self.client.thread_start(params).await
246 }
247
248 pub async fn start_thread_with_model(
253 &self,
254 model: impl Into<String>,
255 ) -> Result<ThreadStartResponse> {
256 self.start_thread(ThreadStartParams::new().model(model))
257 .await
258 }
259
260 pub async fn send_turn(&self, params: TurnStartParams) -> Result<TurnStartResponse> {
262 self.client.turn_start(params).await
263 }
264
265 pub async fn send_text_turn(
270 &self,
271 thread_id: impl Into<String>,
272 text: impl Into<String>,
273 ) -> Result<TurnStartResponse> {
274 self.send_turn(TurnStartParams::text(thread_id, text)).await
275 }
276
277 pub async fn run_text_turn(&mut self, text: impl Into<String>) -> Result<TextTurnResult> {
283 let handler = DenyAllApprovalHandler::default();
284 self.run_text_turn_with_params_and_handler(ThreadStartParams::new(), text, &handler)
285 .await
286 }
287
288 pub async fn run_text_turn_with_model_and_handler<H>(
294 &mut self,
295 model: impl Into<String>,
296 text: impl Into<String>,
297 handler: &H,
298 ) -> Result<TextTurnResult>
299 where
300 H: ApprovalHandler,
301 {
302 self.run_text_turn_with_params_and_handler(
303 ThreadStartParams::new().model(model),
304 text,
305 handler,
306 )
307 .await
308 }
309
310 pub async fn run_text_turn_with_params_and_handler<H>(
318 &mut self,
319 thread_params: ThreadStartParams,
320 text: impl Into<String>,
321 handler: &H,
322 ) -> Result<TextTurnResult>
323 where
324 H: ApprovalHandler,
325 {
326 let thread = self.start_thread(thread_params).await?;
327 let turn = self.send_text_turn(&thread.thread.id, text).await?;
328 let events = self
329 .wait_for_turn_completed(&thread.thread.id, &turn.turn.id, handler)
330 .await?;
331 Ok(TextTurnResult {
332 thread,
333 turn,
334 events,
335 })
336 }
337
338 pub async fn wait_for_turn_completed<H>(
345 &mut self,
346 thread_id: impl Into<String>,
347 turn_id: impl Into<String>,
348 handler: &H,
349 ) -> Result<EventCollector>
350 where
351 H: ApprovalHandler,
352 {
353 let mut collector = EventCollector::for_turn(thread_id, turn_id);
354 self.collect_until_complete(&mut collector, handler).await?;
355 Ok(collector)
356 }
357
358 pub async fn collect_agent_message<H>(
360 &mut self,
361 thread_id: impl Into<String>,
362 turn_id: impl Into<String>,
363 handler: &H,
364 ) -> Result<String>
365 where
366 H: ApprovalHandler,
367 {
368 let collector = self
369 .wait_for_turn_completed(thread_id, turn_id, handler)
370 .await?;
371 Ok(collector.agent_message().to_owned())
372 }
373
374 pub async fn collect_until_complete<H>(
376 &mut self,
377 collector: &mut EventCollector,
378 handler: &H,
379 ) -> Result<()>
380 where
381 H: ApprovalHandler,
382 {
383 while !collector.is_complete() {
384 let Some(notification) = self.next_notification(handler).await else {
385 return Err(Error::TransportClosed);
386 };
387 collector.observe_notification(¬ification);
388 }
389 Ok(())
390 }
391}