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