Skip to main content

codex_codes/
client_async.rs

1//! Asynchronous multi-turn client for the Codex app-server.
2//!
3//! Spawns `codex app-server --listen stdio://` and communicates over
4//! newline-delimited JSON-RPC. The connection stays open for multiple
5//! turns until explicitly shut down.
6//!
7//! # Lifecycle
8//!
9//! 1. Create a client with [`AsyncClient::start`] (spawns and initializes the app-server)
10//! 2. Call [`AsyncClient::thread_start`] to create a conversation session
11//! 3. Call [`AsyncClient::turn_start`] to send user input
12//! 4. Consume [`AsyncClient::next_message`] to stream notifications
13//! 5. Handle approval requests via [`AsyncClient::respond`]
14//! 6. Repeat steps 3-5 for follow-up turns
15//! 7. The client kills the app-server on [`Drop`]
16//!
17//! # Example
18//!
19//! ```ignore
20//! use codex_codes::{AsyncClient, ThreadStartParams, TurnStartParams, UserInput, ServerMessage};
21//!
22//! let mut client = AsyncClient::start().await?;
23//! let thread = client.thread_start(&ThreadStartParams::default()).await?;
24//!
25//! client.turn_start(&TurnStartParams {
26//!     thread_id: thread.thread_id().to_string(),
27//!     input: vec![UserInput::Text { text: "Hello!".into() }],
28//!     model: None,
29//!     reasoning_effort: None,
30//!     sandbox_policy: None,
31//! }).await?;
32//!
33//! while let Some(msg) = client.next_message().await? {
34//!     match msg {
35//!         ServerMessage::Notification(n) => {
36//!             if let codex_codes::Notification::TurnCompleted(_) = n { break; }
37//!         }
38//!         ServerMessage::Request { id, .. } => {
39//!             client.respond(id, &serde_json::json!({"decision": "accept"})).await?;
40//!         }
41//!     }
42//! }
43//! ```
44
45use crate::cli::AppServerBuilder;
46use crate::error::{Error, ParseError, Result};
47use crate::jsonrpc::{
48    JsonRpcError, JsonRpcMessage, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, RequestId,
49};
50use crate::messages::{Notification, ServerMessage, ServerRequest};
51use crate::protocol::{
52    ClientInfo, InitializeParams, InitializeResponse, ThreadArchiveParams, ThreadArchiveResponse,
53    ThreadDeleteParams, ThreadDeleteResponse, ThreadForkParams, ThreadForkResponse,
54    ThreadResumeParams, ThreadResumeResponse, ThreadStartParams, ThreadStartResponse,
55    TurnInterruptParams, TurnInterruptResponse, TurnStartParams, TurnStartResponse,
56};
57use crate::protocol_generated::types::{
58    CancelLoginAccountParams, CancelLoginAccountResponse, GetAccountParams,
59    GetAccountRateLimitsResponse, GetAccountResponse, GetAccountTokenUsageResponse,
60    LoginAccountParams, LoginAccountResponse, LogoutAccountResponse,
61};
62use log::{debug, error, warn};
63use serde::de::DeserializeOwned;
64use serde::Serialize;
65use std::collections::VecDeque;
66use std::sync::atomic::{AtomicI64, Ordering};
67use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter};
68use tokio::process::Child;
69
70/// Buffer size for reading stdout (10MB).
71const STDOUT_BUFFER_SIZE: usize = 10 * 1024 * 1024;
72
73/// Asynchronous multi-turn client for the Codex app-server.
74///
75/// Communicates with a long-lived `codex app-server` process via
76/// newline-delimited JSON-RPC over stdio. Manages request/response
77/// correlation and buffers incoming notifications that arrive while
78/// waiting for RPC responses.
79///
80/// The client automatically kills the app-server process when dropped.
81pub struct AsyncClient {
82    child: Child,
83    writer: BufWriter<tokio::process::ChildStdin>,
84    reader: BufReader<tokio::process::ChildStdout>,
85    /// Handle to the background task draining the child's stderr pipe.
86    /// Kept alive for the lifetime of the client; the task exits on EOF
87    /// when the child is killed.
88    _stderr_drain: tokio::task::JoinHandle<()>,
89    next_id: AtomicI64,
90    /// Buffered incoming messages (notifications/server requests) that arrived
91    /// while waiting for a response to a client request.
92    buffered: VecDeque<ServerMessage>,
93}
94
95impl AsyncClient {
96    /// Create a client from an existing Tokio child process.
97    ///
98    /// The child's stdin, stdout, and stderr must all be piped. This does not
99    /// perform the app-server `initialize` handshake or a Codex version check.
100    /// Use [`AppServerBuilder::build_command`] to retain the SDK's command-line
101    /// and stdio configuration while customizing how the process is spawned.
102    pub fn new(mut child: Child) -> Result<Self> {
103        let stdin = child
104            .stdin
105            .take()
106            .ok_or_else(|| Error::Protocol("Failed to get stdin".to_string()))?;
107        let stdout = child
108            .stdout
109            .take()
110            .ok_or_else(|| Error::Protocol("Failed to get stdout".to_string()))?;
111        let stderr = child
112            .stderr
113            .take()
114            .ok_or_else(|| Error::Protocol("Failed to get stderr".to_string()))?;
115
116        // The app-server emits ~200 KB/s of tracing to stderr. Without an
117        // active reader, the ~64 KB kernel pipe fills almost instantly and
118        // the child blocks. Drain in the background and route lines through
119        // the `log` crate (see [`crate::stderr_drain`]).
120        let stderr_drain = crate::stderr_drain::spawn_async(stderr);
121
122        Ok(Self {
123            child,
124            writer: BufWriter::new(stdin),
125            reader: BufReader::with_capacity(STDOUT_BUFFER_SIZE, stdout),
126            _stderr_drain: stderr_drain,
127            next_id: AtomicI64::new(1),
128            buffered: VecDeque::new(),
129        })
130    }
131
132    /// Start an app-server with default settings.
133    ///
134    /// Spawns `codex app-server --listen stdio://`, performs the required
135    /// `initialize` handshake, and returns a connected client ready for
136    /// `thread_start()`.
137    ///
138    /// # Errors
139    ///
140    /// Returns an error if the `codex` CLI is not installed, the version is
141    /// incompatible, the process fails to start, or the initialization
142    /// handshake fails.
143    pub async fn start() -> Result<Self> {
144        Self::start_with(AppServerBuilder::new()).await
145    }
146
147    /// Start an app-server with a custom [`AppServerBuilder`].
148    ///
149    /// Performs the required `initialize` handshake before returning.
150    /// Use this to configure the binary path, working directory, environment,
151    /// or CLI arguments.
152    ///
153    /// # Errors
154    ///
155    /// Returns an error if the process fails to start, stdio pipes
156    /// cannot be established, or the initialization handshake fails.
157    pub async fn start_with(builder: AppServerBuilder) -> Result<Self> {
158        let mut client = Self::spawn(builder).await?;
159        client
160            .initialize(&InitializeParams {
161                client_info: ClientInfo {
162                    name: "codex-codes".to_string(),
163                    version: env!("CARGO_PKG_VERSION").to_string(),
164                    title: None,
165                },
166                capabilities: None,
167            })
168            .await?;
169        Ok(client)
170    }
171
172    /// Spawn an app-server without performing the `initialize` handshake.
173    ///
174    /// Use this if you need to send a custom [`InitializeParams`] (e.g., with
175    /// specific capabilities). You **must** call [`AsyncClient::initialize`]
176    /// before any other requests.
177    pub async fn spawn(builder: AppServerBuilder) -> Result<Self> {
178        crate::version::check_codex_version_async().await?;
179        Self::new(builder.spawn().await?)
180    }
181
182    /// Send a JSON-RPC request and wait for the matching response.
183    ///
184    /// Any notifications or server requests that arrive before the response
185    /// are buffered and can be retrieved via [`AsyncClient::next_message`].
186    ///
187    /// # Errors
188    ///
189    /// - [`Error::JsonRpc`] if the server returns a JSON-RPC error
190    /// - [`Error::ServerClosed`] if the connection drops before a response arrives
191    /// - [`Error::Json`] if response deserialization fails
192    pub async fn request<P: Serialize, R: DeserializeOwned>(
193        &mut self,
194        method: &str,
195        params: &P,
196    ) -> Result<R> {
197        let id = RequestId::Integer(self.next_id.fetch_add(1, Ordering::Relaxed));
198
199        let req = JsonRpcRequest {
200            id: id.clone(),
201            method: method.to_string(),
202            params: Some(serde_json::to_value(params).map_err(Error::Json)?),
203        };
204
205        self.send_raw(&req).await?;
206
207        // Read lines until we get a response matching our id
208        loop {
209            let msg = self.read_message().await?;
210            match msg {
211                JsonRpcMessage::Response(resp) if resp.id == id => {
212                    let result: R = serde_json::from_value(resp.result).map_err(Error::Json)?;
213                    return Ok(result);
214                }
215                JsonRpcMessage::Error(err) if err.id == id => {
216                    return Err(Error::JsonRpc {
217                        code: err.error.code,
218                        message: err.error.message,
219                    });
220                }
221                // Buffer notifications and server requests
222                JsonRpcMessage::Notification(notif) => {
223                    let typed = Notification::from_envelope(&notif.method, notif.params)
224                        .map_err(Error::Json)?;
225                    self.buffered.push_back(ServerMessage::Notification(typed));
226                }
227                JsonRpcMessage::Request(req) => {
228                    let typed = ServerRequest::from_envelope(&req.method, req.params)
229                        .map_err(Error::Json)?;
230                    self.buffered.push_back(ServerMessage::Request {
231                        id: req.id,
232                        request: typed,
233                    });
234                }
235                // Response/error for a different id — unexpected
236                JsonRpcMessage::Response(resp) => {
237                    warn!(
238                        "[CLIENT] Unexpected response for id={}, expected id={}",
239                        resp.id, id
240                    );
241                }
242                JsonRpcMessage::Error(err) => {
243                    warn!(
244                        "[CLIENT] Unexpected error for id={}, expected id={}",
245                        err.id, id
246                    );
247                }
248            }
249        }
250    }
251
252    /// Start a new thread (conversation session).
253    ///
254    /// A thread must be created before any turns can be started. The returned
255    /// [`ThreadStartResponse`] contains the `thread_id` needed for subsequent calls.
256    pub async fn thread_start(
257        &mut self,
258        params: &ThreadStartParams,
259    ) -> Result<ThreadStartResponse> {
260        self.request(crate::protocol::methods::THREAD_START, params)
261            .await
262    }
263
264    /// Resume a previously persisted thread by id.
265    ///
266    /// Replays the thread's history so turns can continue where they left off.
267    pub async fn thread_resume(
268        &mut self,
269        params: &ThreadResumeParams,
270    ) -> Result<ThreadResumeResponse> {
271        self.request(crate::protocol::methods::THREAD_RESUME, params)
272            .await
273    }
274
275    /// Fork an existing thread into a new independent thread.
276    pub async fn thread_fork(&mut self, params: &ThreadForkParams) -> Result<ThreadForkResponse> {
277        self.request(crate::protocol::methods::THREAD_FORK, params)
278            .await
279    }
280
281    /// Start a new turn within a thread.
282    ///
283    /// Sends user input to the agent. After calling this, use [`AsyncClient::next_message`]
284    /// to stream notifications until `turn/completed` arrives.
285    pub async fn turn_start(&mut self, params: &TurnStartParams) -> Result<TurnStartResponse> {
286        self.request(crate::protocol::methods::TURN_START, params)
287            .await
288    }
289
290    /// Interrupt an active turn.
291    pub async fn turn_interrupt(
292        &mut self,
293        params: &TurnInterruptParams,
294    ) -> Result<TurnInterruptResponse> {
295        self.request(crate::protocol::methods::TURN_INTERRUPT, params)
296            .await
297    }
298
299    /// Archive a thread.
300    pub async fn thread_archive(
301        &mut self,
302        params: &ThreadArchiveParams,
303    ) -> Result<ThreadArchiveResponse> {
304        self.request(crate::protocol::methods::THREAD_ARCHIVE, params)
305            .await
306    }
307
308    /// Delete a thread.
309    pub async fn thread_delete(
310        &mut self,
311        params: &ThreadDeleteParams,
312    ) -> Result<ThreadDeleteResponse> {
313        self.request(crate::protocol::methods::THREAD_DELETE, params)
314            .await
315    }
316
317    /// Perform the `initialize` handshake with the app-server.
318    ///
319    /// Sends `initialize` with the given params and then sends the
320    /// `initialized` notification. This must be the first request after
321    /// spawning the process.
322    pub async fn initialize(&mut self, params: &InitializeParams) -> Result<InitializeResponse> {
323        let resp: InitializeResponse = self
324            .request(crate::protocol::methods::INITIALIZE, params)
325            .await?;
326        self.send_notification(crate::protocol::methods::INITIALIZED)
327            .await?;
328        Ok(resp)
329    }
330
331    /// Respond to a server-to-client request (e.g., approval flow).
332    ///
333    /// When the server sends a [`ServerMessage::Request`], it expects a response.
334    /// Use this method with the request's `id` and a result payload. For command
335    /// approval, pass a [`CommandExecutionApprovalResponse`](crate::CommandExecutionApprovalResponse).
336    /// For file change approval, pass a [`FileChangeApprovalResponse`](crate::FileChangeApprovalResponse).
337    pub async fn respond<R: Serialize>(&mut self, id: RequestId, result: &R) -> Result<()> {
338        let resp = JsonRpcResponse {
339            id,
340            result: serde_json::to_value(result).map_err(Error::Json)?,
341        };
342        self.send_raw(&resp).await
343    }
344
345    /// Respond to a server-to-client request with an error.
346    pub async fn respond_error(&mut self, id: RequestId, code: i64, message: &str) -> Result<()> {
347        let err = JsonRpcError {
348            id,
349            error: crate::jsonrpc::JsonRpcErrorData {
350                code,
351                message: message.to_string(),
352                data: None,
353            },
354        };
355        self.send_raw(&err).await
356    }
357
358    /// Read the next incoming server message (notification or server request).
359    ///
360    /// Returns buffered messages first (from notifications that arrived during
361    /// an [`AsyncClient::request`] call), then reads from the wire.
362    ///
363    /// Returns `Ok(None)` when the app-server closes the connection (EOF).
364    ///
365    /// # Typical notification methods
366    ///
367    /// | Method | Meaning |
368    /// |--------|---------|
369    /// | `turn/started` | Agent began processing |
370    /// | `item/agentMessage/delta` | Streaming text chunk |
371    /// | `item/commandExecution/outputDelta` | Command output chunk |
372    /// | `item/started` / `item/completed` | Item lifecycle |
373    /// | `turn/completed` | Agent finished the turn |
374    /// | `error` | Server-side error |
375    pub async fn next_message(&mut self) -> Result<Option<ServerMessage>> {
376        // Drain buffered messages first
377        if let Some(msg) = self.buffered.pop_front() {
378            return Ok(Some(msg));
379        }
380
381        // Read from the wire
382        loop {
383            let msg = match self.read_message_opt().await? {
384                Some(m) => m,
385                None => return Ok(None),
386            };
387
388            match msg {
389                JsonRpcMessage::Notification(notif) => {
390                    let JsonRpcNotification { method, params } = notif;
391                    let typed =
392                        Notification::from_envelope(&method, params.clone()).map_err(|e| {
393                            Error::Deserialization(ParseError::from_envelope(method, params, e))
394                        })?;
395                    return Ok(Some(ServerMessage::Notification(typed)));
396                }
397                JsonRpcMessage::Request(req) => {
398                    let JsonRpcRequest { id, method, params } = req;
399                    let typed =
400                        ServerRequest::from_envelope(&method, params.clone()).map_err(|e| {
401                            Error::Deserialization(ParseError::from_envelope(method, params, e))
402                        })?;
403                    return Ok(Some(ServerMessage::Request { id, request: typed }));
404                }
405                // Unexpected responses without a pending request
406                JsonRpcMessage::Response(resp) => {
407                    warn!(
408                        "[CLIENT] Unexpected response (no pending request): id={}",
409                        resp.id
410                    );
411                }
412                JsonRpcMessage::Error(err) => {
413                    warn!(
414                        "[CLIENT] Unexpected error (no pending request): id={} code={}",
415                        err.id, err.error.code
416                    );
417                }
418            }
419        }
420    }
421
422    /// Return an async event stream over [`ServerMessage`]s.
423    ///
424    /// Wraps [`AsyncClient::next_message`] in a stream-like API. Call
425    /// [`EventStream::next`] in a loop, or [`EventStream::collect`] to
426    /// gather all messages until EOF.
427    pub fn events(&mut self) -> EventStream<'_> {
428        EventStream { client: self }
429    }
430
431    /// Get the process ID.
432    pub fn pid(&self) -> Option<u32> {
433        self.child.id()
434    }
435
436    // ── Account / auth methods ─────────────────────────────────────────
437
438    /// `account/read` — the active account (plan, email, auth mode), or
439    /// `account: null` when logged out.
440    pub async fn account_read(&mut self, params: &GetAccountParams) -> Result<GetAccountResponse> {
441        self.request(crate::protocol::methods::ACCOUNT_READ, params)
442            .await
443    }
444
445    /// `account/login/start` — begin a login. The params select the mode
446    /// (`apiKey` completes immediately; `chatgpt` returns an auth URL to
447    /// open; `chatgptDeviceCode` returns a user code + verification URL).
448    /// Browser/device modes complete asynchronously: watch for the
449    /// `account/login/completed` notification, or cancel with
450    /// [`account_login_cancel`](Self::account_login_cancel).
451    pub async fn account_login_start(
452        &mut self,
453        params: &LoginAccountParams,
454    ) -> Result<LoginAccountResponse> {
455        self.request(crate::protocol::methods::ACCOUNT_LOGIN_START, params)
456            .await
457    }
458
459    /// `account/login/cancel` — abort an in-flight browser/device login.
460    pub async fn account_login_cancel(
461        &mut self,
462        params: &CancelLoginAccountParams,
463    ) -> Result<CancelLoginAccountResponse> {
464        self.request(crate::protocol::methods::ACCOUNT_LOGIN_CANCEL, params)
465            .await
466    }
467
468    /// `account/logout` — remove the stored credential.
469    pub async fn account_logout(&mut self) -> Result<LogoutAccountResponse> {
470        self.request(
471            crate::protocol::methods::ACCOUNT_LOGOUT,
472            &serde_json::json!({}),
473        )
474        .await
475    }
476
477    /// `account/rateLimits/read` — current rate-limit windows.
478    pub async fn account_rate_limits_read(&mut self) -> Result<GetAccountRateLimitsResponse> {
479        self.request(
480            crate::protocol::methods::ACCOUNT_RATELIMITS_READ,
481            &serde_json::json!({}),
482        )
483        .await
484    }
485
486    /// `account/usage/read` — token-usage summary for the account.
487    pub async fn account_usage_read(&mut self) -> Result<GetAccountTokenUsageResponse> {
488        self.request(
489            crate::protocol::methods::ACCOUNT_USAGE_READ,
490            &serde_json::json!({}),
491        )
492        .await
493    }
494
495    /// Check if the child process is still running.
496    pub fn is_alive(&mut self) -> bool {
497        self.child.try_wait().ok().flatten().is_none()
498    }
499
500    /// Shut down the app-server process.
501    ///
502    /// Consumes the client. If you don't call this explicitly, the
503    /// [`Drop`] implementation will kill the process automatically.
504    pub async fn shutdown(mut self) -> Result<()> {
505        debug!("[CLIENT] Shutting down");
506        self.child.kill().await.map_err(Error::Io)?;
507        Ok(())
508    }
509
510    // -- internal --
511
512    async fn send_notification(&mut self, method: &str) -> Result<()> {
513        let notif = JsonRpcNotification {
514            method: method.to_string(),
515            params: None,
516        };
517        self.send_raw(&notif).await
518    }
519
520    async fn send_raw<T: Serialize>(&mut self, msg: &T) -> Result<()> {
521        let json = serde_json::to_string(msg).map_err(Error::Json)?;
522        debug!("[CLIENT] Sending: {}", json);
523        self.writer
524            .write_all(json.as_bytes())
525            .await
526            .map_err(Error::Io)?;
527        self.writer.write_all(b"\n").await.map_err(Error::Io)?;
528        self.writer.flush().await.map_err(Error::Io)?;
529        Ok(())
530    }
531
532    async fn read_message(&mut self) -> Result<JsonRpcMessage> {
533        self.read_message_opt().await?.ok_or(Error::ServerClosed)
534    }
535
536    async fn read_message_opt(&mut self) -> Result<Option<JsonRpcMessage>> {
537        let mut line = String::new();
538
539        loop {
540            line.clear();
541            let bytes_read = self.reader.read_line(&mut line).await.map_err(Error::Io)?;
542
543            if bytes_read == 0 {
544                debug!("[CLIENT] Stream closed (EOF)");
545                return Ok(None);
546            }
547
548            let trimmed = line.trim();
549            if trimmed.is_empty() {
550                continue;
551            }
552
553            debug!("[CLIENT] Received: {}", trimmed);
554
555            match serde_json::from_str::<JsonRpcMessage>(trimmed) {
556                Ok(msg) => return Ok(Some(msg)),
557                Err(e) => {
558                    warn!(
559                        "[CLIENT] Failed to deserialize message. \
560                         Please report this at https://github.com/meawoppl/rust-code-agent-sdks/issues"
561                    );
562                    warn!("[CLIENT] Parse error: {}", e);
563                    warn!("[CLIENT] Raw: {}", trimmed);
564                    return Err(Error::Deserialization(ParseError::from_line(trimmed, e)));
565                }
566            }
567        }
568    }
569}
570
571impl Drop for AsyncClient {
572    fn drop(&mut self) {
573        if self.is_alive() {
574            if let Err(e) = self.child.start_kill() {
575                error!("Failed to kill app-server process on drop: {}", e);
576            }
577        }
578    }
579}
580
581/// Async stream of [`ServerMessage`]s from an [`AsyncClient`].
582pub struct EventStream<'a> {
583    client: &'a mut AsyncClient,
584}
585
586impl EventStream<'_> {
587    /// Get the next server message.
588    pub async fn next(&mut self) -> Option<Result<ServerMessage>> {
589        match self.client.next_message().await {
590            Ok(Some(msg)) => Some(Ok(msg)),
591            Ok(None) => None,
592            Err(e) => Some(Err(e)),
593        }
594    }
595
596    /// Collect all remaining messages.
597    pub async fn collect(mut self) -> Result<Vec<ServerMessage>> {
598        let mut msgs = Vec::new();
599        while let Some(result) = self.next().await {
600            msgs.push(result?);
601        }
602        Ok(msgs)
603    }
604}
605
606#[cfg(test)]
607mod tests {
608    use super::*;
609
610    #[test]
611    fn test_buffer_size() {
612        assert_eq!(STDOUT_BUFFER_SIZE, 10 * 1024 * 1024);
613    }
614}