Skip to main content

codex_codes/
protocol.rs

1//! App-server v2 protocol types for the Codex CLI.
2//!
3//! These types represent the JSON-RPC request parameters, response payloads,
4//! and notification bodies used by `codex app-server`. All wire types use
5//! camelCase field names via `#[serde(rename_all = "camelCase")]`.
6//!
7//! # Organization
8//!
9//! - **Request/Response pairs** — [`ThreadStartParams`]/[`ThreadStartResponse`],
10//!   [`TurnStartParams`]/[`TurnStartResponse`], etc.
11//! - **Server notifications** — Structs like [`TurnCompletedNotification`],
12//!   [`AgentMessageDeltaNotification`] that can be deserialized from the `params`
13//!   field of a [`ServerMessage::Notification`]
14//! - **Approval flow types** — [`CommandExecutionApprovalParams`] and
15//!   [`FileChangeApprovalParams`] for server-to-client requests that need a response
16//! - **Method constants** — The [`methods`] module contains all JSON-RPC method
17//!   name strings
18//!
19//! # Parsing notifications
20//!
21//! ```
22//! use codex_codes::protocol::{methods, TurnCompletedNotification};
23//! use serde_json::Value;
24//!
25//! fn handle_notification(method: &str, params: Option<Value>) {
26//!     if method == methods::TURN_COMPLETED {
27//!         if let Some(p) = params {
28//!             let notif: TurnCompletedNotification = serde_json::from_value(p).unwrap();
29//!             println!("Turn {} completed", notif.turn_id);
30//!         }
31//!     }
32//! }
33//! ```
34
35use crate::io::items::ThreadItem;
36use crate::jsonrpc::RequestId;
37use serde::{Deserialize, Serialize};
38use serde_json::Value;
39
40// ---------------------------------------------------------------------------
41// User input
42// ---------------------------------------------------------------------------
43
44/// User input sent as part of a [`TurnStartParams`].
45///
46/// # Example
47///
48/// ```
49/// use codex_codes::UserInput;
50///
51/// let text = UserInput::Text { text: "What is 2+2?".into() };
52/// let json = serde_json::to_string(&text).unwrap();
53/// assert!(json.contains(r#""type":"text""#));
54/// ```
55#[derive(Debug, Clone, Serialize, Deserialize)]
56#[serde(tag = "type", rename_all = "camelCase")]
57pub enum UserInput {
58    /// Text input from the user.
59    Text { text: String },
60    /// Pre-encoded image as a data URI (e.g., `data:image/png;base64,...`).
61    Image { data: String },
62}
63
64// ---------------------------------------------------------------------------
65// Thread lifecycle requests
66// ---------------------------------------------------------------------------
67
68/// Parameters for `thread/start`.
69///
70/// Use `ThreadStartParams::default()` for a basic thread with no custom instructions.
71#[derive(Debug, Clone, Default, Serialize, Deserialize)]
72#[serde(rename_all = "camelCase")]
73pub struct ThreadStartParams {
74    /// Optional system instructions for the agent.
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub instructions: Option<String>,
77    /// Optional tool definitions to make available to the agent.
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub tools: Option<Vec<Value>>,
80}
81
82/// Response from `thread/start`.
83#[derive(Debug, Clone, Serialize, Deserialize)]
84#[serde(rename_all = "camelCase")]
85pub struct ThreadStartResponse {
86    pub thread_id: String,
87}
88
89/// Parameters for `thread/archive`.
90#[derive(Debug, Clone, Serialize, Deserialize)]
91#[serde(rename_all = "camelCase")]
92pub struct ThreadArchiveParams {
93    pub thread_id: String,
94}
95
96/// Response from `thread/archive`.
97#[derive(Debug, Clone, Default, Serialize, Deserialize)]
98#[serde(rename_all = "camelCase")]
99pub struct ThreadArchiveResponse {}
100
101// ---------------------------------------------------------------------------
102// Turn lifecycle requests
103// ---------------------------------------------------------------------------
104
105/// Parameters for `turn/start`.
106///
107/// Starts a new agent turn within an existing thread. The agent processes the
108/// input and streams notifications until the turn completes.
109#[derive(Debug, Clone, Serialize, Deserialize)]
110#[serde(rename_all = "camelCase")]
111pub struct TurnStartParams {
112    /// The thread ID from [`ThreadStartResponse`].
113    pub thread_id: String,
114    /// One or more user inputs (text and/or images).
115    pub input: Vec<UserInput>,
116    /// Override the model for this turn (e.g., `"o4-mini"`).
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub model: Option<String>,
119    /// Override reasoning effort for this turn (e.g., `"low"`, `"medium"`, `"high"`).
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub reasoning_effort: Option<String>,
122    /// Override sandbox policy for this turn.
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub sandbox_policy: Option<Value>,
125}
126
127/// Response from `turn/start`.
128#[derive(Debug, Clone, Default, Serialize, Deserialize)]
129#[serde(rename_all = "camelCase")]
130pub struct TurnStartResponse {}
131
132/// Parameters for `turn/interrupt`.
133#[derive(Debug, Clone, Serialize, Deserialize)]
134#[serde(rename_all = "camelCase")]
135pub struct TurnInterruptParams {
136    pub thread_id: String,
137}
138
139/// Response from `turn/interrupt`.
140#[derive(Debug, Clone, Default, Serialize, Deserialize)]
141#[serde(rename_all = "camelCase")]
142pub struct TurnInterruptResponse {}
143
144// ---------------------------------------------------------------------------
145// Turn status & data types
146// ---------------------------------------------------------------------------
147
148/// Status of a turn within a [`Turn`].
149#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
150#[serde(rename_all = "camelCase")]
151pub enum TurnStatus {
152    /// The agent finished normally.
153    Completed,
154    /// The turn was interrupted by the client via `turn/interrupt`.
155    Interrupted,
156    /// The turn failed with an error (see [`Turn::error`]).
157    Failed,
158    /// The turn is still being processed.
159    InProgress,
160}
161
162/// Error information from a failed turn.
163#[derive(Debug, Clone, Serialize, Deserialize)]
164#[serde(rename_all = "camelCase")]
165pub struct TurnError {
166    pub message: String,
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub codex_error_info: Option<Value>,
169}
170
171/// A completed turn with its items and final status.
172///
173/// Included in [`TurnCompletedNotification`] when a turn finishes.
174#[derive(Debug, Clone, Serialize, Deserialize)]
175#[serde(rename_all = "camelCase")]
176pub struct Turn {
177    /// Unique turn identifier.
178    pub id: String,
179    /// All items produced during this turn (messages, commands, file changes, etc.).
180    #[serde(default)]
181    pub items: Vec<ThreadItem>,
182    /// Final status of the turn.
183    pub status: TurnStatus,
184    /// Error details if `status` is [`TurnStatus::Failed`].
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub error: Option<TurnError>,
187}
188
189// ---------------------------------------------------------------------------
190// Token usage
191// ---------------------------------------------------------------------------
192
193/// Cumulative token usage for a thread.
194///
195/// Sent via [`ThreadTokenUsageUpdatedNotification`] after each turn.
196#[derive(Debug, Clone, Serialize, Deserialize)]
197#[serde(rename_all = "camelCase")]
198pub struct TokenUsage {
199    /// Total input tokens consumed.
200    pub input_tokens: u64,
201    /// Total output tokens generated.
202    pub output_tokens: u64,
203    /// Input tokens served from cache.
204    #[serde(default)]
205    pub cached_input_tokens: u64,
206}
207
208// ---------------------------------------------------------------------------
209// Thread status
210// ---------------------------------------------------------------------------
211
212/// Status of a thread, sent via [`ThreadStatusChangedNotification`].
213#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
214#[serde(rename_all = "camelCase")]
215pub enum ThreadStatus {
216    /// Thread is not yet loaded.
217    NotLoaded,
218    /// Thread is idle (no active turn).
219    Idle,
220    /// Thread has an active turn being processed.
221    Active,
222    /// Thread encountered an unrecoverable error.
223    SystemError,
224}
225
226// ---------------------------------------------------------------------------
227// Server notifications
228// ---------------------------------------------------------------------------
229
230/// `thread/started` notification.
231#[derive(Debug, Clone, Serialize, Deserialize)]
232#[serde(rename_all = "camelCase")]
233pub struct ThreadStartedNotification {
234    pub thread_id: String,
235}
236
237/// `thread/status/changed` notification.
238#[derive(Debug, Clone, Serialize, Deserialize)]
239#[serde(rename_all = "camelCase")]
240pub struct ThreadStatusChangedNotification {
241    pub thread_id: String,
242    pub status: ThreadStatus,
243}
244
245/// `turn/started` notification.
246#[derive(Debug, Clone, Serialize, Deserialize)]
247#[serde(rename_all = "camelCase")]
248pub struct TurnStartedNotification {
249    pub thread_id: String,
250    pub turn_id: String,
251}
252
253/// `turn/completed` notification.
254#[derive(Debug, Clone, Serialize, Deserialize)]
255#[serde(rename_all = "camelCase")]
256pub struct TurnCompletedNotification {
257    pub thread_id: String,
258    pub turn_id: String,
259    pub turn: Turn,
260}
261
262/// `item/started` notification.
263#[derive(Debug, Clone, Serialize, Deserialize)]
264#[serde(rename_all = "camelCase")]
265pub struct ItemStartedNotification {
266    pub thread_id: String,
267    pub turn_id: String,
268    pub item: ThreadItem,
269}
270
271/// `item/completed` notification.
272#[derive(Debug, Clone, Serialize, Deserialize)]
273#[serde(rename_all = "camelCase")]
274pub struct ItemCompletedNotification {
275    pub thread_id: String,
276    pub turn_id: String,
277    pub item: ThreadItem,
278}
279
280/// `item/agentMessage/delta` notification.
281#[derive(Debug, Clone, Serialize, Deserialize)]
282#[serde(rename_all = "camelCase")]
283pub struct AgentMessageDeltaNotification {
284    pub thread_id: String,
285    pub item_id: String,
286    pub delta: String,
287}
288
289/// `item/commandExecution/outputDelta` notification.
290#[derive(Debug, Clone, Serialize, Deserialize)]
291#[serde(rename_all = "camelCase")]
292pub struct CmdOutputDeltaNotification {
293    pub thread_id: String,
294    pub item_id: String,
295    pub delta: String,
296}
297
298/// `item/fileChange/outputDelta` notification.
299#[derive(Debug, Clone, Serialize, Deserialize)]
300#[serde(rename_all = "camelCase")]
301pub struct FileChangeOutputDeltaNotification {
302    pub thread_id: String,
303    pub item_id: String,
304    pub delta: String,
305}
306
307/// `item/reasoning/summaryTextDelta` notification.
308#[derive(Debug, Clone, Serialize, Deserialize)]
309#[serde(rename_all = "camelCase")]
310pub struct ReasoningDeltaNotification {
311    pub thread_id: String,
312    pub item_id: String,
313    pub delta: String,
314}
315
316/// `error` notification.
317#[derive(Debug, Clone, Serialize, Deserialize)]
318#[serde(rename_all = "camelCase")]
319pub struct ErrorNotification {
320    pub error: String,
321    #[serde(skip_serializing_if = "Option::is_none")]
322    pub thread_id: Option<String>,
323    #[serde(skip_serializing_if = "Option::is_none")]
324    pub turn_id: Option<String>,
325    #[serde(default)]
326    pub will_retry: bool,
327}
328
329/// `thread/tokenUsage/updated` notification.
330#[derive(Debug, Clone, Serialize, Deserialize)]
331#[serde(rename_all = "camelCase")]
332pub struct ThreadTokenUsageUpdatedNotification {
333    pub thread_id: String,
334    pub usage: TokenUsage,
335}
336
337// ---------------------------------------------------------------------------
338// Approval flow types (server-to-client requests)
339// ---------------------------------------------------------------------------
340
341/// Decision for command execution approval.
342///
343/// Sent as part of [`CommandExecutionApprovalResponse`] when responding to
344/// a command approval request from the server.
345#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
346#[serde(rename_all = "camelCase")]
347pub enum CommandApprovalDecision {
348    /// Allow this specific command to execute.
349    Accept,
350    /// Allow this command and similar future commands in this session.
351    AcceptForSession,
352    /// Reject this command.
353    Decline,
354    /// Cancel the entire turn.
355    Cancel,
356}
357
358/// Parameters for `item/commandExecution/requestApproval` (server → client).
359///
360/// The server sends this as a [`ServerMessage::Request`] when the agent wants
361/// to execute a command that requires user approval. Respond with
362/// [`CommandExecutionApprovalResponse`].
363#[derive(Debug, Clone, Serialize, Deserialize)]
364#[serde(rename_all = "camelCase")]
365pub struct CommandExecutionApprovalParams {
366    pub thread_id: String,
367    pub turn_id: String,
368    /// Unique identifier for this tool call.
369    pub call_id: String,
370    /// The shell command the agent wants to run.
371    pub command: String,
372    /// Working directory for the command.
373    pub cwd: String,
374    /// Human-readable explanation of why the command is needed.
375    #[serde(skip_serializing_if = "Option::is_none")]
376    pub reason: Option<String>,
377}
378
379/// Response for `item/commandExecution/requestApproval`.
380#[derive(Debug, Clone, Serialize, Deserialize)]
381#[serde(rename_all = "camelCase")]
382pub struct CommandExecutionApprovalResponse {
383    pub decision: CommandApprovalDecision,
384}
385
386/// Decision for file change approval.
387///
388/// Sent as part of [`FileChangeApprovalResponse`] when responding to
389/// a file change approval request from the server.
390#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
391#[serde(rename_all = "camelCase")]
392pub enum FileChangeApprovalDecision {
393    /// Allow this specific file change.
394    Accept,
395    /// Allow this and similar future file changes in this session.
396    AcceptForSession,
397    /// Reject this file change.
398    Decline,
399    /// Cancel the entire turn.
400    Cancel,
401}
402
403/// Parameters for `item/fileChange/requestApproval` (server → client).
404///
405/// The server sends this as a [`ServerMessage::Request`] when the agent wants
406/// to modify files and requires user approval. Respond with
407/// [`FileChangeApprovalResponse`].
408#[derive(Debug, Clone, Serialize, Deserialize)]
409#[serde(rename_all = "camelCase")]
410pub struct FileChangeApprovalParams {
411    pub thread_id: String,
412    pub turn_id: String,
413    /// Unique identifier for this tool call.
414    pub call_id: String,
415    /// The proposed file changes (structure varies by patch format).
416    pub changes: Value,
417    /// Human-readable explanation of why the changes are needed.
418    #[serde(skip_serializing_if = "Option::is_none")]
419    pub reason: Option<String>,
420}
421
422/// Response for `item/fileChange/requestApproval`.
423#[derive(Debug, Clone, Serialize, Deserialize)]
424#[serde(rename_all = "camelCase")]
425pub struct FileChangeApprovalResponse {
426    pub decision: FileChangeApprovalDecision,
427}
428
429// ---------------------------------------------------------------------------
430// Server message (what the client receives)
431// ---------------------------------------------------------------------------
432
433/// An incoming message from the app-server that the client should handle.
434///
435/// This is what [`AsyncClient::next_message`](crate::AsyncClient::next_message) and
436/// [`SyncClient::next_message`](crate::SyncClient::next_message) return.
437///
438/// # Handling
439///
440/// - **Notifications** are informational — no response is needed. Match on the `method`
441///   field and deserialize `params` into the appropriate notification type.
442/// - **Requests** require a response via `client.respond(id, &result)`. Currently
443///   used for approval flows (`item/commandExecution/requestApproval` and
444///   `item/fileChange/requestApproval`).
445#[derive(Debug, Clone)]
446pub enum ServerMessage {
447    /// A notification (no response needed). Deserialize `params` based on `method`.
448    Notification {
449        method: String,
450        params: Option<Value>,
451    },
452    /// A request from the server that needs a response (e.g., approval flow).
453    /// Use the client's `respond()` method with the `id`.
454    Request {
455        id: RequestId,
456        method: String,
457        params: Option<Value>,
458    },
459}
460
461// ---------------------------------------------------------------------------
462// Method name constants
463// ---------------------------------------------------------------------------
464
465/// JSON-RPC method names used by the app-server protocol.
466///
467/// Use these constants when matching on [`ServerMessage::Notification`] or
468/// [`ServerMessage::Request`] method fields to avoid typos.
469pub mod methods {
470    // Client → server requests
471    pub const THREAD_START: &str = "thread/start";
472    pub const THREAD_ARCHIVE: &str = "thread/archive";
473    pub const TURN_START: &str = "turn/start";
474    pub const TURN_INTERRUPT: &str = "turn/interrupt";
475    pub const TURN_STEER: &str = "turn/steer";
476
477    // Server → client notifications
478    pub const THREAD_STARTED: &str = "thread/started";
479    pub const THREAD_STATUS_CHANGED: &str = "thread/status/changed";
480    pub const THREAD_TOKEN_USAGE_UPDATED: &str = "thread/tokenUsage/updated";
481    pub const TURN_STARTED: &str = "turn/started";
482    pub const TURN_COMPLETED: &str = "turn/completed";
483    pub const ITEM_STARTED: &str = "item/started";
484    pub const ITEM_COMPLETED: &str = "item/completed";
485    pub const AGENT_MESSAGE_DELTA: &str = "item/agentMessage/delta";
486    pub const CMD_OUTPUT_DELTA: &str = "item/commandExecution/outputDelta";
487    pub const FILE_CHANGE_OUTPUT_DELTA: &str = "item/fileChange/outputDelta";
488    pub const REASONING_DELTA: &str = "item/reasoning/summaryTextDelta";
489    pub const ERROR: &str = "error";
490
491    // Server → client requests (approval)
492    pub const CMD_EXEC_APPROVAL: &str = "item/commandExecution/requestApproval";
493    pub const FILE_CHANGE_APPROVAL: &str = "item/fileChange/requestApproval";
494}
495
496#[cfg(test)]
497mod tests {
498    use super::*;
499
500    #[test]
501    fn test_user_input_text() {
502        let input = UserInput::Text {
503            text: "Hello".to_string(),
504        };
505        let json = serde_json::to_string(&input).unwrap();
506        assert!(json.contains(r#""type":"text""#));
507        let parsed: UserInput = serde_json::from_str(&json).unwrap();
508        assert!(matches!(parsed, UserInput::Text { text } if text == "Hello"));
509    }
510
511    #[test]
512    fn test_thread_start_params() {
513        let params = ThreadStartParams {
514            instructions: Some("Be helpful".to_string()),
515            tools: None,
516        };
517        let json = serde_json::to_string(&params).unwrap();
518        assert!(json.contains("instructions"));
519        assert!(!json.contains("tools"));
520    }
521
522    #[test]
523    fn test_thread_start_response() {
524        let json = r#"{"threadId":"th_abc123"}"#;
525        let resp: ThreadStartResponse = serde_json::from_str(json).unwrap();
526        assert_eq!(resp.thread_id, "th_abc123");
527    }
528
529    #[test]
530    fn test_turn_start_params() {
531        let params = TurnStartParams {
532            thread_id: "th_1".to_string(),
533            input: vec![UserInput::Text {
534                text: "What is 2+2?".to_string(),
535            }],
536            model: None,
537            reasoning_effort: None,
538            sandbox_policy: None,
539        };
540        let json = serde_json::to_string(&params).unwrap();
541        assert!(json.contains("threadId"));
542        assert!(json.contains("input"));
543    }
544
545    #[test]
546    fn test_turn_status() {
547        let json = r#""completed""#;
548        let status: TurnStatus = serde_json::from_str(json).unwrap();
549        assert_eq!(status, TurnStatus::Completed);
550    }
551
552    #[test]
553    fn test_turn_completed_notification() {
554        let json = r#"{
555            "threadId": "th_1",
556            "turnId": "t_1",
557            "turn": {
558                "id": "t_1",
559                "items": [],
560                "status": "completed"
561            }
562        }"#;
563        let notif: TurnCompletedNotification = serde_json::from_str(json).unwrap();
564        assert_eq!(notif.thread_id, "th_1");
565        assert_eq!(notif.turn.status, TurnStatus::Completed);
566    }
567
568    #[test]
569    fn test_agent_message_delta() {
570        let json = r#"{"threadId":"th_1","itemId":"msg_1","delta":"Hello "}"#;
571        let notif: AgentMessageDeltaNotification = serde_json::from_str(json).unwrap();
572        assert_eq!(notif.delta, "Hello ");
573    }
574
575    #[test]
576    fn test_command_approval_decision() {
577        let json = r#""accept""#;
578        let decision: CommandApprovalDecision = serde_json::from_str(json).unwrap();
579        assert_eq!(decision, CommandApprovalDecision::Accept);
580
581        let json = r#""acceptForSession""#;
582        let decision: CommandApprovalDecision = serde_json::from_str(json).unwrap();
583        assert_eq!(decision, CommandApprovalDecision::AcceptForSession);
584    }
585
586    #[test]
587    fn test_command_approval_params() {
588        let json = r#"{
589            "threadId": "th_1",
590            "turnId": "t_1",
591            "callId": "call_1",
592            "command": "rm -rf /tmp/test",
593            "cwd": "/home/user"
594        }"#;
595        let params: CommandExecutionApprovalParams = serde_json::from_str(json).unwrap();
596        assert_eq!(params.command, "rm -rf /tmp/test");
597    }
598
599    #[test]
600    fn test_error_notification() {
601        let json = r#"{"error":"something failed","willRetry":true}"#;
602        let notif: ErrorNotification = serde_json::from_str(json).unwrap();
603        assert_eq!(notif.error, "something failed");
604        assert!(notif.will_retry);
605    }
606
607    #[test]
608    fn test_thread_status() {
609        let json = r#""idle""#;
610        let status: ThreadStatus = serde_json::from_str(json).unwrap();
611        assert_eq!(status, ThreadStatus::Idle);
612    }
613
614    #[test]
615    fn test_token_usage() {
616        let json = r#"{"inputTokens":100,"outputTokens":200,"cachedInputTokens":50}"#;
617        let usage: TokenUsage = serde_json::from_str(json).unwrap();
618        assert_eq!(usage.input_tokens, 100);
619        assert_eq!(usage.output_tokens, 200);
620        assert_eq!(usage.cached_input_tokens, 50);
621    }
622}