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