Skip to main content

codex_app_server_client/
session.rs

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/// Options used when creating a high-level [`CodexSession`].
14///
15/// This wraps the generated `initialize` params plus process-spawn knobs so a
16/// caller can connect, handshake, and start using the app-server in one call.
17#[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    /// Creates default session options for a client name and version.
29    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    /// Adds a human-readable title to the `initialize` client info.
41    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    /// Uses a different binary than `codex` when spawning the app-server.
47    pub fn with_command(mut self, command: impl Into<String>) -> Self {
48        self.command = command.into();
49        self
50    }
51
52    /// Appends one extra argument after `app-server` when spawning Codex.
53    pub fn with_extra_arg(mut self, arg: impl Into<String>) -> Self {
54        self.extra_args.push(arg.into());
55        self
56    }
57
58    /// Appends a Codex `-c key=value` config override.
59    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    /// Supplies explicit initialize capabilities.
67    pub fn with_capabilities(mut self, capabilities: InitializeCapabilities) -> Self {
68        self.capabilities = Some(capabilities);
69        self
70    }
71
72    /// Overrides the request/response timeout used by generated client calls.
73    pub fn with_call_timeout(mut self, timeout: Duration) -> Self {
74        self.call_timeout = Some(timeout);
75        self
76    }
77
78    /// Overrides the capacity of the session's internal event channel
79    /// instead of [`crate::DEFAULT_EVENTS_CHANNEL_CAPACITY`]. See
80    /// [`crate::CodexAppServerClient::spawn_with_events_capacity`] for what
81    /// this bounds and the drop policy once it fills up. A `0` capacity is
82    /// rejected (not silently clamped) by the underlying client constructor
83    /// when the session is created.
84    pub fn with_events_capacity(mut self, events_capacity: usize) -> Self {
85        self.events_capacity = Some(events_capacity);
86        self
87    }
88}
89
90/// High-level app-server session that owns a client plus event stream.
91///
92/// `CodexSession` performs the `initialize` / `initialized` handshake before it
93/// is returned, then offers convenience methods for common thread and text-turn
94/// workflows while keeping the full low-level [`CodexAppServerClient`] exposed.
95pub struct CodexSession {
96    client: CodexAppServerClient,
97    events: EventStream,
98    initialize_response: InitializeResponse,
99}
100
101/// Result of a one-shot text turn.
102///
103/// The raw `thread` and `turn` responses are kept alongside the collected turn
104/// events so callers can inspect IDs, model details, diffs, and errors without
105/// re-querying the app-server.
106#[derive(Clone, Debug)]
107pub struct TextTurnResult {
108    pub thread: ThreadStartResponse,
109    pub turn: TurnStartResponse,
110    pub events: EventCollector,
111}
112
113impl TextTurnResult {
114    /// Returns the concatenated assistant text deltas observed for the turn.
115    pub fn agent_message(&self) -> &str {
116        self.events.agent_message()
117    }
118
119    /// Returns the latest unified diff observed for the turn, if any.
120    pub fn latest_diff(&self) -> Option<&str> {
121        self.events.latest_diff()
122    }
123
124    /// Returns turn-level errors observed while waiting for completion.
125    pub fn errors(&self) -> &[TurnError] {
126        self.events.errors()
127    }
128}
129
130impl CodexSession {
131    /// Spawns `codex app-server`, performs the handshake, and returns a session.
132    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    /// Connects over caller-provided async streams and performs the handshake.
145    ///
146    /// This is primarily useful for tests and custom transports.
147    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    /// Connects to an app-server Unix socket and performs the handshake.
166    #[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    /// Returns the initialize response captured during session setup.
209    pub fn initialize_response(&self) -> &InitializeResponse {
210        &self.initialize_response
211    }
212
213    /// Receives the next raw event without applying an approval policy.
214    ///
215    /// Most integrations should prefer [`Self::next_notification`],
216    /// [`Self::collect_until_complete`], or the higher-level text helpers so
217    /// server requests are answered promptly.
218    pub async fn next_event(&mut self) -> Option<Event> {
219        self.events.recv().await
220    }
221
222    /// Receives the next server notification, replying to server requests with
223    /// the supplied [`ApprovalHandler`] while waiting.
224    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    /// Starts a thread with fully-typed generated params.
244    pub async fn start_thread(&self, params: ThreadStartParams) -> Result<ThreadStartResponse> {
245        self.client.thread_start(params).await
246    }
247
248    /// Starts a thread using only a model override.
249    ///
250    /// This is a convenience wrapper around
251    /// `start_thread(ThreadStartParams::new().model(model))`.
252    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    /// Sends a turn with fully-typed generated params.
261    pub async fn send_turn(&self, params: TurnStartParams) -> Result<TurnStartResponse> {
262        self.client.turn_start(params).await
263    }
264
265    /// Sends one text input item to an existing thread.
266    ///
267    /// This is a convenience wrapper around
268    /// `send_turn(TurnStartParams::text(thread_id, text))`.
269    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    /// Runs a one-shot text turn using a new default thread.
278    ///
279    /// Server requests are answered with [`DenyAllApprovalHandler`]. Use
280    /// [`Self::run_text_turn_with_model_and_handler`] when the turn needs a
281    /// specific model or a real approval/tool policy.
282    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    /// Runs a one-shot text turn with an explicit model and approval handler.
289    ///
290    /// The helper starts a new thread, sends the text turn, drains
291    /// notifications until that turn completes, and returns the collected
292    /// assistant text/diff/errors.
293    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    /// Runs a one-shot text turn with explicit `thread/start` params and an
311    /// approval handler.
312    ///
313    /// This is the lowest-level text-turn convenience helper: callers can set
314    /// the full typed [`ThreadStartParams`] instead of only the model, while
315    /// still reusing the shared thread start, turn start, server-request
316    /// handling, and event collection flow.
317    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    /// Drains notifications until a specific turn reaches a terminal status.
339    ///
340    /// Any server request encountered while waiting is answered through
341    /// `handler`. The returned [`EventCollector`] contains assistant text,
342    /// latest diff, completion state, and turn errors observed for the named
343    /// thread/turn pair.
344    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    /// Drains notifications for a turn and returns only the assistant text.
359    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    /// Drains notifications into an existing collector until it is complete.
375    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(&notification);
388        }
389        Ok(())
390    }
391}