Skip to main content

codex_app_server_protocol/protocol/
v2.rs

1use std::collections::HashMap;
2use std::path::PathBuf;
3
4use crate::protocol::common::AuthMode;
5use codex_protocol::ConversationId;
6use codex_protocol::account::PlanType;
7use codex_protocol::approvals::SandboxCommandAssessment as CoreSandboxCommandAssessment;
8use codex_protocol::config_types::ReasoningEffort;
9use codex_protocol::config_types::ReasoningSummary;
10use codex_protocol::items::AgentMessageContent as CoreAgentMessageContent;
11use codex_protocol::items::TurnItem as CoreTurnItem;
12use codex_protocol::models::ResponseItem;
13use codex_protocol::parse_command::ParsedCommand as CoreParsedCommand;
14use codex_protocol::protocol::CodexErrorInfo as CoreCodexErrorInfo;
15use codex_protocol::protocol::CreditsSnapshot as CoreCreditsSnapshot;
16use codex_protocol::protocol::RateLimitSnapshot as CoreRateLimitSnapshot;
17use codex_protocol::protocol::RateLimitWindow as CoreRateLimitWindow;
18use codex_protocol::user_input::UserInput as CoreUserInput;
19use mcp_types::ContentBlock as McpContentBlock;
20use schemars::JsonSchema;
21use serde::Deserialize;
22use serde::Serialize;
23use serde_json::Value as JsonValue;
24use thiserror::Error;
25use ts_rs::TS;
26
27// Macro to declare a camelCased API v2 enum mirroring a core enum which
28// tends to use either snake_case or kebab-case.
29macro_rules! v2_enum_from_core {
30    (
31        pub enum $Name:ident from $Src:path { $( $Variant:ident ),+ $(,)? }
32    ) => {
33        #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
34        #[serde(rename_all = "camelCase")]
35        #[ts(export_to = "v2/")]
36        pub enum $Name { $( $Variant ),+ }
37
38        impl $Name {
39            pub fn to_core(self) -> $Src {
40                match self { $( $Name::$Variant => <$Src>::$Variant ),+ }
41            }
42        }
43
44        impl From<$Src> for $Name {
45            fn from(value: $Src) -> Self {
46                match value { $( <$Src>::$Variant => $Name::$Variant ),+ }
47            }
48        }
49    };
50}
51
52/// This translation layer make sure that we expose codex error code in camel case.
53///
54/// When an upstream HTTP status is available (for example, from the Responses API or a provider),
55/// it is forwarded in `httpStatusCode` on the relevant `codexErrorInfo` variant.
56#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
57#[serde(rename_all = "camelCase")]
58#[ts(export_to = "v2/")]
59pub enum CodexErrorInfo {
60    ContextWindowExceeded,
61    UsageLimitExceeded,
62    HttpConnectionFailed {
63        #[serde(rename = "httpStatusCode")]
64        #[ts(rename = "httpStatusCode")]
65        http_status_code: Option<u16>,
66    },
67    /// Failed to connect to the response SSE stream.
68    ResponseStreamConnectionFailed {
69        #[serde(rename = "httpStatusCode")]
70        #[ts(rename = "httpStatusCode")]
71        http_status_code: Option<u16>,
72    },
73    InternalServerError,
74    Unauthorized,
75    BadRequest,
76    SandboxError,
77    /// The response SSE stream disconnected in the middle of a turn before completion.
78    ResponseStreamDisconnected {
79        #[serde(rename = "httpStatusCode")]
80        #[ts(rename = "httpStatusCode")]
81        http_status_code: Option<u16>,
82    },
83    /// Reached the retry limit for responses.
84    ResponseTooManyFailedAttempts {
85        #[serde(rename = "httpStatusCode")]
86        #[ts(rename = "httpStatusCode")]
87        http_status_code: Option<u16>,
88    },
89    Other,
90}
91
92impl From<CoreCodexErrorInfo> for CodexErrorInfo {
93    fn from(value: CoreCodexErrorInfo) -> Self {
94        match value {
95            CoreCodexErrorInfo::ContextWindowExceeded => CodexErrorInfo::ContextWindowExceeded,
96            CoreCodexErrorInfo::UsageLimitExceeded => CodexErrorInfo::UsageLimitExceeded,
97            CoreCodexErrorInfo::HttpConnectionFailed { http_status_code } => {
98                CodexErrorInfo::HttpConnectionFailed { http_status_code }
99            }
100            CoreCodexErrorInfo::ResponseStreamConnectionFailed { http_status_code } => {
101                CodexErrorInfo::ResponseStreamConnectionFailed { http_status_code }
102            }
103            CoreCodexErrorInfo::InternalServerError => CodexErrorInfo::InternalServerError,
104            CoreCodexErrorInfo::Unauthorized => CodexErrorInfo::Unauthorized,
105            CoreCodexErrorInfo::BadRequest => CodexErrorInfo::BadRequest,
106            CoreCodexErrorInfo::SandboxError => CodexErrorInfo::SandboxError,
107            CoreCodexErrorInfo::ResponseStreamDisconnected { http_status_code } => {
108                CodexErrorInfo::ResponseStreamDisconnected { http_status_code }
109            }
110            CoreCodexErrorInfo::ResponseTooManyFailedAttempts { http_status_code } => {
111                CodexErrorInfo::ResponseTooManyFailedAttempts { http_status_code }
112            }
113            CoreCodexErrorInfo::Other => CodexErrorInfo::Other,
114        }
115    }
116}
117
118v2_enum_from_core!(
119    pub enum AskForApproval from codex_protocol::protocol::AskForApproval {
120        UnlessTrusted, OnFailure, OnRequest, Never
121    }
122);
123
124v2_enum_from_core!(
125    pub enum SandboxMode from codex_protocol::config_types::SandboxMode {
126        ReadOnly, WorkspaceWrite, DangerFullAccess
127    }
128);
129
130v2_enum_from_core!(
131    pub enum CommandRiskLevel from codex_protocol::approvals::SandboxRiskLevel {
132        Low,
133        Medium,
134        High
135    }
136);
137
138#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
139#[serde(rename_all = "camelCase")]
140#[ts(export_to = "v2/")]
141pub enum ApprovalDecision {
142    Accept,
143    Decline,
144    Cancel,
145}
146
147#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
148#[serde(tag = "type", rename_all = "camelCase")]
149#[ts(tag = "type")]
150#[ts(export_to = "v2/")]
151pub enum SandboxPolicy {
152    DangerFullAccess,
153    ReadOnly,
154    #[serde(rename_all = "camelCase")]
155    #[ts(rename_all = "camelCase")]
156    WorkspaceWrite {
157        #[serde(default)]
158        writable_roots: Vec<PathBuf>,
159        #[serde(default)]
160        network_access: bool,
161        #[serde(default)]
162        exclude_tmpdir_env_var: bool,
163        #[serde(default)]
164        exclude_slash_tmp: bool,
165    },
166}
167
168impl SandboxPolicy {
169    pub fn to_core(&self) -> codex_protocol::protocol::SandboxPolicy {
170        match self {
171            SandboxPolicy::DangerFullAccess => {
172                codex_protocol::protocol::SandboxPolicy::DangerFullAccess
173            }
174            SandboxPolicy::ReadOnly => codex_protocol::protocol::SandboxPolicy::ReadOnly,
175            SandboxPolicy::WorkspaceWrite {
176                writable_roots,
177                network_access,
178                exclude_tmpdir_env_var,
179                exclude_slash_tmp,
180            } => codex_protocol::protocol::SandboxPolicy::WorkspaceWrite {
181                writable_roots: writable_roots.clone(),
182                network_access: *network_access,
183                exclude_tmpdir_env_var: *exclude_tmpdir_env_var,
184                exclude_slash_tmp: *exclude_slash_tmp,
185            },
186        }
187    }
188}
189
190impl From<codex_protocol::protocol::SandboxPolicy> for SandboxPolicy {
191    fn from(value: codex_protocol::protocol::SandboxPolicy) -> Self {
192        match value {
193            codex_protocol::protocol::SandboxPolicy::DangerFullAccess => {
194                SandboxPolicy::DangerFullAccess
195            }
196            codex_protocol::protocol::SandboxPolicy::ReadOnly => SandboxPolicy::ReadOnly,
197            codex_protocol::protocol::SandboxPolicy::WorkspaceWrite {
198                writable_roots,
199                network_access,
200                exclude_tmpdir_env_var,
201                exclude_slash_tmp,
202            } => SandboxPolicy::WorkspaceWrite {
203                writable_roots,
204                network_access,
205                exclude_tmpdir_env_var,
206                exclude_slash_tmp,
207            },
208        }
209    }
210}
211
212#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
213#[serde(rename_all = "camelCase")]
214#[ts(export_to = "v2/")]
215pub struct SandboxCommandAssessment {
216    pub description: String,
217    pub risk_level: CommandRiskLevel,
218}
219
220impl SandboxCommandAssessment {
221    pub fn into_core(self) -> CoreSandboxCommandAssessment {
222        CoreSandboxCommandAssessment {
223            description: self.description,
224            risk_level: self.risk_level.to_core(),
225        }
226    }
227}
228
229impl From<CoreSandboxCommandAssessment> for SandboxCommandAssessment {
230    fn from(value: CoreSandboxCommandAssessment) -> Self {
231        Self {
232            description: value.description,
233            risk_level: CommandRiskLevel::from(value.risk_level),
234        }
235    }
236}
237
238#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
239#[serde(tag = "type", rename_all = "camelCase")]
240#[ts(tag = "type")]
241#[ts(export_to = "v2/")]
242pub enum CommandAction {
243    Read {
244        command: String,
245        name: String,
246        path: PathBuf,
247    },
248    ListFiles {
249        command: String,
250        path: Option<String>,
251    },
252    Search {
253        command: String,
254        query: Option<String>,
255        path: Option<String>,
256    },
257    Unknown {
258        command: String,
259    },
260}
261
262impl CommandAction {
263    pub fn into_core(self) -> CoreParsedCommand {
264        match self {
265            CommandAction::Read {
266                command: cmd,
267                name,
268                path,
269            } => CoreParsedCommand::Read { cmd, name, path },
270            CommandAction::ListFiles { command: cmd, path } => {
271                CoreParsedCommand::ListFiles { cmd, path }
272            }
273            CommandAction::Search {
274                command: cmd,
275                query,
276                path,
277            } => CoreParsedCommand::Search { cmd, query, path },
278            CommandAction::Unknown { command: cmd } => CoreParsedCommand::Unknown { cmd },
279        }
280    }
281}
282
283impl From<CoreParsedCommand> for CommandAction {
284    fn from(value: CoreParsedCommand) -> Self {
285        match value {
286            CoreParsedCommand::Read { cmd, name, path } => CommandAction::Read {
287                command: cmd,
288                name,
289                path,
290            },
291            CoreParsedCommand::ListFiles { cmd, path } => {
292                CommandAction::ListFiles { command: cmd, path }
293            }
294            CoreParsedCommand::Search { cmd, query, path } => CommandAction::Search {
295                command: cmd,
296                query,
297                path,
298            },
299            CoreParsedCommand::Unknown { cmd } => CommandAction::Unknown { command: cmd },
300        }
301    }
302}
303
304#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
305#[serde(tag = "type", rename_all = "camelCase")]
306#[ts(tag = "type")]
307#[ts(export_to = "v2/")]
308pub enum Account {
309    #[serde(rename = "apiKey", rename_all = "camelCase")]
310    #[ts(rename = "apiKey", rename_all = "camelCase")]
311    ApiKey {},
312
313    #[serde(rename = "chatgpt", rename_all = "camelCase")]
314    #[ts(rename = "chatgpt", rename_all = "camelCase")]
315    Chatgpt { email: String, plan_type: PlanType },
316}
317
318#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
319#[serde(tag = "type")]
320#[ts(tag = "type")]
321#[ts(export_to = "v2/")]
322pub enum LoginAccountParams {
323    #[serde(rename = "apiKey", rename_all = "camelCase")]
324    #[ts(rename = "apiKey", rename_all = "camelCase")]
325    ApiKey {
326        #[serde(rename = "apiKey")]
327        #[ts(rename = "apiKey")]
328        api_key: String,
329    },
330    #[serde(rename = "chatgpt")]
331    #[ts(rename = "chatgpt")]
332    Chatgpt,
333}
334
335#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
336#[serde(tag = "type", rename_all = "camelCase")]
337#[ts(tag = "type")]
338#[ts(export_to = "v2/")]
339pub enum LoginAccountResponse {
340    #[serde(rename = "apiKey", rename_all = "camelCase")]
341    #[ts(rename = "apiKey", rename_all = "camelCase")]
342    ApiKey {},
343    #[serde(rename = "chatgpt", rename_all = "camelCase")]
344    #[ts(rename = "chatgpt", rename_all = "camelCase")]
345    Chatgpt {
346        // Use plain String for identifiers to avoid TS/JSON Schema quirks around uuid-specific types.
347        // Convert to/from UUIDs at the application layer as needed.
348        login_id: String,
349        /// URL the client should open in a browser to initiate the OAuth flow.
350        auth_url: String,
351    },
352}
353
354#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
355#[serde(rename_all = "camelCase")]
356#[ts(export_to = "v2/")]
357pub struct CancelLoginAccountParams {
358    pub login_id: String,
359}
360
361#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
362#[serde(rename_all = "camelCase")]
363#[ts(export_to = "v2/")]
364pub struct CancelLoginAccountResponse {}
365
366#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
367#[serde(rename_all = "camelCase")]
368#[ts(export_to = "v2/")]
369pub struct LogoutAccountResponse {}
370
371#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
372#[serde(rename_all = "camelCase")]
373#[ts(export_to = "v2/")]
374pub struct GetAccountRateLimitsResponse {
375    pub rate_limits: RateLimitSnapshot,
376}
377
378#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
379#[serde(rename_all = "camelCase")]
380#[ts(export_to = "v2/")]
381pub struct GetAccountParams {
382    #[serde(default)]
383    pub refresh_token: bool,
384}
385
386#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
387#[serde(rename_all = "camelCase")]
388#[ts(export_to = "v2/")]
389pub struct GetAccountResponse {
390    pub account: Option<Account>,
391    pub requires_openai_auth: bool,
392}
393
394#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)]
395#[serde(rename_all = "camelCase")]
396#[ts(export_to = "v2/")]
397pub struct ModelListParams {
398    /// Opaque pagination cursor returned by a previous call.
399    pub cursor: Option<String>,
400    /// Optional page size; defaults to a reasonable server-side value.
401    pub limit: Option<u32>,
402}
403
404#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
405#[serde(rename_all = "camelCase")]
406#[ts(export_to = "v2/")]
407pub struct Model {
408    pub id: String,
409    pub model: String,
410    pub display_name: String,
411    pub description: String,
412    pub supported_reasoning_efforts: Vec<ReasoningEffortOption>,
413    pub default_reasoning_effort: ReasoningEffort,
414    // Only one model should be marked as default.
415    pub is_default: bool,
416}
417
418#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
419#[serde(rename_all = "camelCase")]
420#[ts(export_to = "v2/")]
421pub struct ReasoningEffortOption {
422    pub reasoning_effort: ReasoningEffort,
423    pub description: String,
424}
425
426#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
427#[serde(rename_all = "camelCase")]
428#[ts(export_to = "v2/")]
429pub struct ModelListResponse {
430    pub data: Vec<Model>,
431    /// Opaque cursor to pass to the next call to continue after the last item.
432    /// If None, there are no more items to return.
433    pub next_cursor: Option<String>,
434}
435
436#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
437#[serde(rename_all = "camelCase")]
438#[ts(export_to = "v2/")]
439pub struct FeedbackUploadParams {
440    pub classification: String,
441    pub reason: Option<String>,
442    pub conversation_id: Option<ConversationId>,
443    pub include_logs: bool,
444}
445
446#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
447#[serde(rename_all = "camelCase")]
448#[ts(export_to = "v2/")]
449pub struct FeedbackUploadResponse {
450    pub thread_id: String,
451}
452
453// === Threads, Turns, and Items ===
454// Thread APIs
455#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)]
456#[serde(rename_all = "camelCase")]
457#[ts(export_to = "v2/")]
458pub struct ThreadStartParams {
459    pub model: Option<String>,
460    pub model_provider: Option<String>,
461    pub cwd: Option<String>,
462    pub approval_policy: Option<AskForApproval>,
463    pub sandbox: Option<SandboxMode>,
464    pub config: Option<HashMap<String, JsonValue>>,
465    pub base_instructions: Option<String>,
466    pub developer_instructions: Option<String>,
467}
468
469#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
470#[serde(rename_all = "camelCase")]
471#[ts(export_to = "v2/")]
472pub struct ThreadStartResponse {
473    pub thread: Thread,
474    pub model: String,
475    pub model_provider: String,
476    pub cwd: PathBuf,
477    pub approval_policy: AskForApproval,
478    pub sandbox: SandboxPolicy,
479    pub reasoning_effort: Option<ReasoningEffort>,
480}
481
482#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)]
483#[serde(rename_all = "camelCase")]
484#[ts(export_to = "v2/")]
485/// There are three ways to resume a thread:
486/// 1. By thread_id: load the thread from disk by thread_id and resume it.
487/// 2. By history: instantiate the thread from memory and resume it.
488/// 3. By path: load the thread from disk by path and resume it.
489///
490/// The precedence is: history > path > thread_id.
491/// If using history or path, the thread_id param will be ignored.
492///
493/// Prefer using thread_id whenever possible.
494pub struct ThreadResumeParams {
495    pub thread_id: String,
496
497    /// [UNSTABLE] FOR CODEX CLOUD - DO NOT USE.
498    /// If specified, the thread will be resumed with the provided history
499    /// instead of loaded from disk.
500    pub history: Option<Vec<ResponseItem>>,
501
502    /// [UNSTABLE] Specify the rollout path to resume from.
503    /// If specified, the thread_id param will be ignored.
504    pub path: Option<PathBuf>,
505
506    /// Configuration overrides for the resumed thread, if any.
507    pub model: Option<String>,
508    pub model_provider: Option<String>,
509    pub cwd: Option<String>,
510    pub approval_policy: Option<AskForApproval>,
511    pub sandbox: Option<SandboxMode>,
512    pub config: Option<HashMap<String, serde_json::Value>>,
513    pub base_instructions: Option<String>,
514    pub developer_instructions: Option<String>,
515}
516
517#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
518#[serde(rename_all = "camelCase")]
519#[ts(export_to = "v2/")]
520pub struct ThreadResumeResponse {
521    pub thread: Thread,
522    pub model: String,
523    pub model_provider: String,
524    pub cwd: PathBuf,
525    pub approval_policy: AskForApproval,
526    pub sandbox: SandboxPolicy,
527    pub reasoning_effort: Option<ReasoningEffort>,
528}
529
530#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
531#[serde(rename_all = "camelCase")]
532#[ts(export_to = "v2/")]
533pub struct ThreadArchiveParams {
534    pub thread_id: String,
535}
536
537#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
538#[serde(rename_all = "camelCase")]
539#[ts(export_to = "v2/")]
540pub struct ThreadArchiveResponse {}
541
542#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
543#[serde(rename_all = "camelCase")]
544#[ts(export_to = "v2/")]
545pub struct ThreadListParams {
546    /// Opaque pagination cursor returned by a previous call.
547    pub cursor: Option<String>,
548    /// Optional page size; defaults to a reasonable server-side value.
549    pub limit: Option<u32>,
550    /// Optional provider filter; when set, only sessions recorded under these
551    /// providers are returned. When present but empty, includes all providers.
552    pub model_providers: Option<Vec<String>>,
553}
554
555#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
556#[serde(rename_all = "camelCase")]
557#[ts(export_to = "v2/")]
558pub struct ThreadListResponse {
559    pub data: Vec<Thread>,
560    /// Opaque cursor to pass to the next call to continue after the last item.
561    /// if None, there are no more items to return.
562    pub next_cursor: Option<String>,
563}
564
565#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
566#[serde(rename_all = "camelCase")]
567#[ts(export_to = "v2/")]
568pub struct ThreadCompactParams {
569    pub thread_id: String,
570}
571
572#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
573#[serde(rename_all = "camelCase")]
574#[ts(export_to = "v2/")]
575pub struct ThreadCompactResponse {}
576
577#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
578#[serde(rename_all = "camelCase")]
579#[ts(export_to = "v2/")]
580pub struct Thread {
581    pub id: String,
582    /// Usually the first user message in the thread, if available.
583    pub preview: String,
584    pub model_provider: String,
585    /// Unix timestamp (in seconds) when the thread was created.
586    pub created_at: i64,
587    /// [UNSTABLE] Path to the thread on disk.
588    pub path: PathBuf,
589    /// Only populated on a `thread/resume` response.
590    /// For all other responses and notifications returning a Thread,
591    /// the turns field will be an empty list.
592    pub turns: Vec<Turn>,
593}
594
595#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
596#[serde(rename_all = "camelCase")]
597#[ts(export_to = "v2/")]
598pub struct AccountUpdatedNotification {
599    pub auth_mode: Option<AuthMode>,
600}
601
602#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
603#[serde(rename_all = "camelCase")]
604#[ts(export_to = "v2/")]
605pub struct Turn {
606    pub id: String,
607    /// Only populated on a `thread/resume` response.
608    /// For all other responses and notifications returning a Turn,
609    /// the items field will be an empty list.
610    pub items: Vec<ThreadItem>,
611    #[serde(flatten)]
612    pub status: TurnStatus,
613}
614
615#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, Error)]
616#[serde(rename_all = "camelCase")]
617#[ts(export_to = "v2/")]
618#[error("{message}")]
619pub struct TurnError {
620    pub message: String,
621    pub codex_error_info: Option<CodexErrorInfo>,
622}
623
624#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
625#[serde(rename_all = "camelCase")]
626#[ts(export_to = "v2/")]
627pub struct ErrorNotification {
628    pub error: TurnError,
629}
630
631#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
632#[serde(tag = "status", rename_all = "camelCase")]
633#[ts(tag = "status", export_to = "v2/")]
634pub enum TurnStatus {
635    Completed,
636    Interrupted,
637    Failed { error: TurnError },
638    InProgress,
639}
640
641// Turn APIs
642#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)]
643#[serde(rename_all = "camelCase")]
644#[ts(export_to = "v2/")]
645pub struct TurnStartParams {
646    pub thread_id: String,
647    pub input: Vec<UserInput>,
648    /// Override the working directory for this turn and subsequent turns.
649    pub cwd: Option<PathBuf>,
650    /// Override the approval policy for this turn and subsequent turns.
651    pub approval_policy: Option<AskForApproval>,
652    /// Override the sandbox policy for this turn and subsequent turns.
653    pub sandbox_policy: Option<SandboxPolicy>,
654    /// Override the model for this turn and subsequent turns.
655    pub model: Option<String>,
656    /// Override the reasoning effort for this turn and subsequent turns.
657    pub effort: Option<ReasoningEffort>,
658    /// Override the reasoning summary for this turn and subsequent turns.
659    pub summary: Option<ReasoningSummary>,
660}
661
662#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
663#[serde(rename_all = "camelCase")]
664#[ts(export_to = "v2/")]
665pub struct ReviewStartParams {
666    pub thread_id: String,
667    pub target: ReviewTarget,
668
669    /// When true, also append the final review message to the original thread.
670    #[serde(default)]
671    pub append_to_original_thread: bool,
672}
673
674#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
675#[serde(tag = "type", rename_all = "camelCase")]
676#[ts(tag = "type", export_to = "v2/")]
677pub enum ReviewTarget {
678    /// Review the working tree: staged, unstaged, and untracked files.
679    UncommittedChanges,
680
681    /// Review changes between the current branch and the given base branch.
682    #[serde(rename_all = "camelCase")]
683    #[ts(rename_all = "camelCase")]
684    BaseBranch { branch: String },
685
686    /// Review the changes introduced by a specific commit.
687    #[serde(rename_all = "camelCase")]
688    #[ts(rename_all = "camelCase")]
689    Commit {
690        sha: String,
691        /// Optional human-readable label (e.g., commit subject) for UIs.
692        title: Option<String>,
693    },
694
695    /// Arbitrary instructions, equivalent to the old free-form prompt.
696    #[serde(rename_all = "camelCase")]
697    #[ts(rename_all = "camelCase")]
698    Custom { instructions: String },
699}
700
701#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
702#[serde(rename_all = "camelCase")]
703#[ts(export_to = "v2/")]
704pub struct TurnStartResponse {
705    pub turn: Turn,
706}
707
708#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
709#[serde(rename_all = "camelCase")]
710#[ts(export_to = "v2/")]
711pub struct TurnInterruptParams {
712    pub thread_id: String,
713    pub turn_id: String,
714}
715
716#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
717#[serde(rename_all = "camelCase")]
718#[ts(export_to = "v2/")]
719pub struct TurnInterruptResponse {}
720
721// User input types
722#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
723#[serde(tag = "type", rename_all = "camelCase")]
724#[ts(tag = "type")]
725#[ts(export_to = "v2/")]
726pub enum UserInput {
727    Text { text: String },
728    Image { url: String },
729    LocalImage { path: PathBuf },
730}
731
732impl UserInput {
733    pub fn into_core(self) -> CoreUserInput {
734        match self {
735            UserInput::Text { text } => CoreUserInput::Text { text },
736            UserInput::Image { url } => CoreUserInput::Image { image_url: url },
737            UserInput::LocalImage { path } => CoreUserInput::LocalImage { path },
738        }
739    }
740}
741
742impl From<CoreUserInput> for UserInput {
743    fn from(value: CoreUserInput) -> Self {
744        match value {
745            CoreUserInput::Text { text } => UserInput::Text { text },
746            CoreUserInput::Image { image_url } => UserInput::Image { url: image_url },
747            CoreUserInput::LocalImage { path } => UserInput::LocalImage { path },
748            _ => unreachable!("unsupported user input variant"),
749        }
750    }
751}
752
753#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
754#[serde(tag = "type", rename_all = "camelCase")]
755#[ts(tag = "type")]
756#[ts(export_to = "v2/")]
757pub enum ThreadItem {
758    #[serde(rename_all = "camelCase")]
759    #[ts(rename_all = "camelCase")]
760    UserMessage { id: String, content: Vec<UserInput> },
761    #[serde(rename_all = "camelCase")]
762    #[ts(rename_all = "camelCase")]
763    AgentMessage { id: String, text: String },
764    #[serde(rename_all = "camelCase")]
765    #[ts(rename_all = "camelCase")]
766    Reasoning {
767        id: String,
768        #[serde(default)]
769        summary: Vec<String>,
770        #[serde(default)]
771        content: Vec<String>,
772    },
773    #[serde(rename_all = "camelCase")]
774    #[ts(rename_all = "camelCase")]
775    CommandExecution {
776        id: String,
777        /// The command to be executed.
778        command: String,
779        /// The command's working directory.
780        cwd: PathBuf,
781        status: CommandExecutionStatus,
782        /// A best-effort parsing of the command to understand the action(s) it will perform.
783        /// This returns a list of CommandAction objects because a single shell command may
784        /// be composed of many commands piped together.
785        command_actions: Vec<CommandAction>,
786        /// The command's output, aggregated from stdout and stderr.
787        aggregated_output: Option<String>,
788        /// The command's exit code.
789        exit_code: Option<i32>,
790        /// The duration of the command execution in milliseconds.
791        duration_ms: Option<i64>,
792    },
793    #[serde(rename_all = "camelCase")]
794    #[ts(rename_all = "camelCase")]
795    FileChange {
796        id: String,
797        changes: Vec<FileUpdateChange>,
798        status: PatchApplyStatus,
799    },
800    #[serde(rename_all = "camelCase")]
801    #[ts(rename_all = "camelCase")]
802    McpToolCall {
803        id: String,
804        server: String,
805        tool: String,
806        status: McpToolCallStatus,
807        arguments: JsonValue,
808        result: Option<McpToolCallResult>,
809        error: Option<McpToolCallError>,
810    },
811    #[serde(rename_all = "camelCase")]
812    #[ts(rename_all = "camelCase")]
813    WebSearch { id: String, query: String },
814    #[serde(rename_all = "camelCase")]
815    #[ts(rename_all = "camelCase")]
816    TodoList { id: String, items: Vec<TodoItem> },
817    #[serde(rename_all = "camelCase")]
818    #[ts(rename_all = "camelCase")]
819    ImageView { id: String, path: String },
820    #[serde(rename_all = "camelCase")]
821    #[ts(rename_all = "camelCase")]
822    CodeReview { id: String, review: String },
823}
824
825impl From<CoreTurnItem> for ThreadItem {
826    fn from(value: CoreTurnItem) -> Self {
827        match value {
828            CoreTurnItem::UserMessage(user) => ThreadItem::UserMessage {
829                id: user.id,
830                content: user.content.into_iter().map(UserInput::from).collect(),
831            },
832            CoreTurnItem::AgentMessage(agent) => {
833                let text = agent
834                    .content
835                    .into_iter()
836                    .map(|entry| match entry {
837                        CoreAgentMessageContent::Text { text } => text,
838                    })
839                    .collect::<String>();
840                ThreadItem::AgentMessage { id: agent.id, text }
841            }
842            CoreTurnItem::Reasoning(reasoning) => ThreadItem::Reasoning {
843                id: reasoning.id,
844                summary: reasoning.summary_text,
845                content: reasoning.raw_content,
846            },
847            CoreTurnItem::WebSearch(search) => ThreadItem::WebSearch {
848                id: search.id,
849                query: search.query,
850            },
851        }
852    }
853}
854
855#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
856#[serde(rename_all = "camelCase")]
857#[ts(export_to = "v2/")]
858pub enum CommandExecutionStatus {
859    InProgress,
860    Completed,
861    Failed,
862    Declined,
863}
864
865#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
866#[serde(rename_all = "camelCase")]
867#[ts(export_to = "v2/")]
868pub struct FileUpdateChange {
869    pub path: String,
870    pub kind: PatchChangeKind,
871    pub diff: String,
872}
873
874#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
875#[serde(tag = "type", rename_all = "camelCase")]
876#[ts(tag = "type")]
877#[ts(export_to = "v2/")]
878pub enum PatchChangeKind {
879    Add,
880    Delete,
881    Update { move_path: Option<PathBuf> },
882}
883
884#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
885#[serde(rename_all = "camelCase")]
886#[ts(export_to = "v2/")]
887pub enum PatchApplyStatus {
888    InProgress,
889    Completed,
890    Failed,
891    Declined,
892}
893
894#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
895#[serde(rename_all = "camelCase")]
896#[ts(export_to = "v2/")]
897pub enum McpToolCallStatus {
898    InProgress,
899    Completed,
900    Failed,
901}
902
903#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
904#[serde(rename_all = "camelCase")]
905#[ts(export_to = "v2/")]
906pub struct McpToolCallResult {
907    pub content: Vec<McpContentBlock>,
908    pub structured_content: Option<JsonValue>,
909}
910
911#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
912#[serde(rename_all = "camelCase")]
913#[ts(export_to = "v2/")]
914pub struct McpToolCallError {
915    pub message: String,
916}
917
918#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
919#[serde(rename_all = "camelCase")]
920#[ts(export_to = "v2/")]
921pub struct TodoItem {
922    pub id: String,
923    pub text: String,
924    pub completed: bool,
925}
926
927// === Server Notifications ===
928// Thread/Turn lifecycle notifications and item progress events
929#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
930#[serde(rename_all = "camelCase")]
931#[ts(export_to = "v2/")]
932pub struct ThreadStartedNotification {
933    pub thread: Thread,
934}
935
936#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
937#[serde(rename_all = "camelCase")]
938#[ts(export_to = "v2/")]
939pub struct TurnStartedNotification {
940    pub turn: Turn,
941}
942
943#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
944#[serde(rename_all = "camelCase")]
945#[ts(export_to = "v2/")]
946pub struct Usage {
947    pub input_tokens: i32,
948    pub cached_input_tokens: i32,
949    pub output_tokens: i32,
950}
951
952#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
953#[serde(rename_all = "camelCase")]
954#[ts(export_to = "v2/")]
955pub struct TurnCompletedNotification {
956    pub turn: Turn,
957}
958
959#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
960#[serde(rename_all = "camelCase")]
961#[ts(export_to = "v2/")]
962pub struct ItemStartedNotification {
963    pub item: ThreadItem,
964}
965
966#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
967#[serde(rename_all = "camelCase")]
968#[ts(export_to = "v2/")]
969pub struct ItemCompletedNotification {
970    pub item: ThreadItem,
971}
972
973// Item-specific progress notifications
974#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
975#[serde(rename_all = "camelCase")]
976#[ts(export_to = "v2/")]
977pub struct AgentMessageDeltaNotification {
978    pub item_id: String,
979    pub delta: String,
980}
981
982#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
983#[serde(rename_all = "camelCase")]
984#[ts(export_to = "v2/")]
985pub struct ReasoningSummaryTextDeltaNotification {
986    pub item_id: String,
987    pub delta: String,
988    pub summary_index: i64,
989}
990
991#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
992#[serde(rename_all = "camelCase")]
993#[ts(export_to = "v2/")]
994pub struct ReasoningSummaryPartAddedNotification {
995    pub item_id: String,
996    pub summary_index: i64,
997}
998
999#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
1000#[serde(rename_all = "camelCase")]
1001#[ts(export_to = "v2/")]
1002pub struct ReasoningTextDeltaNotification {
1003    pub item_id: String,
1004    pub delta: String,
1005    pub content_index: i64,
1006}
1007
1008#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
1009#[serde(rename_all = "camelCase")]
1010#[ts(export_to = "v2/")]
1011pub struct CommandExecutionOutputDeltaNotification {
1012    pub item_id: String,
1013    pub delta: String,
1014}
1015
1016#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
1017#[serde(rename_all = "camelCase")]
1018#[ts(export_to = "v2/")]
1019pub struct McpToolCallProgressNotification {
1020    pub item_id: String,
1021    pub message: String,
1022}
1023
1024#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
1025#[serde(rename_all = "camelCase")]
1026#[ts(export_to = "v2/")]
1027pub struct WindowsWorldWritableWarningNotification {
1028    pub sample_paths: Vec<String>,
1029    pub extra_count: usize,
1030    pub failed_scan: bool,
1031}
1032
1033#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
1034#[serde(rename_all = "camelCase")]
1035#[ts(export_to = "v2/")]
1036pub struct CommandExecutionRequestApprovalParams {
1037    pub thread_id: String,
1038    pub turn_id: String,
1039    pub item_id: String,
1040    /// Optional explanatory reason (e.g. request for network access).
1041    pub reason: Option<String>,
1042    /// Optional model-provided risk assessment describing the blocked command.
1043    pub risk: Option<SandboxCommandAssessment>,
1044}
1045
1046#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
1047#[serde(rename_all = "camelCase")]
1048#[ts(export_to = "v2/")]
1049pub struct CommandExecutionRequestAcceptSettings {
1050    /// If true, automatically approve this command for the duration of the session.
1051    #[serde(default)]
1052    pub for_session: bool,
1053}
1054
1055#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
1056#[serde(rename_all = "camelCase")]
1057#[ts(export_to = "v2/")]
1058pub struct CommandExecutionRequestApprovalResponse {
1059    pub decision: ApprovalDecision,
1060    /// Optional approval settings for when the decision is `accept`.
1061    /// Ignored if the decision is `decline` or `cancel`.
1062    #[serde(default)]
1063    pub accept_settings: Option<CommandExecutionRequestAcceptSettings>,
1064}
1065
1066#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
1067#[serde(rename_all = "camelCase")]
1068#[ts(export_to = "v2/")]
1069pub struct FileChangeRequestApprovalParams {
1070    pub thread_id: String,
1071    pub turn_id: String,
1072    pub item_id: String,
1073    /// Optional explanatory reason (e.g. request for extra write access).
1074    pub reason: Option<String>,
1075    /// [UNSTABLE] When set, the agent is asking the user to allow writes under this root
1076    /// for the remainder of the session (unclear if this is honored today).
1077    pub grant_root: Option<PathBuf>,
1078}
1079
1080#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
1081#[ts(export_to = "v2/")]
1082pub struct FileChangeRequestApprovalResponse {
1083    pub decision: ApprovalDecision,
1084}
1085
1086#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
1087#[serde(rename_all = "camelCase")]
1088#[ts(export_to = "v2/")]
1089pub struct AccountRateLimitsUpdatedNotification {
1090    pub rate_limits: RateLimitSnapshot,
1091}
1092
1093#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
1094#[serde(rename_all = "camelCase")]
1095#[ts(export_to = "v2/")]
1096pub struct RateLimitSnapshot {
1097    pub primary: Option<RateLimitWindow>,
1098    pub secondary: Option<RateLimitWindow>,
1099    pub credits: Option<CreditsSnapshot>,
1100}
1101
1102impl From<CoreRateLimitSnapshot> for RateLimitSnapshot {
1103    fn from(value: CoreRateLimitSnapshot) -> Self {
1104        Self {
1105            primary: value.primary.map(RateLimitWindow::from),
1106            secondary: value.secondary.map(RateLimitWindow::from),
1107            credits: value.credits.map(CreditsSnapshot::from),
1108        }
1109    }
1110}
1111
1112#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
1113#[serde(rename_all = "camelCase")]
1114#[ts(export_to = "v2/")]
1115pub struct RateLimitWindow {
1116    pub used_percent: i32,
1117    pub window_duration_mins: Option<i64>,
1118    pub resets_at: Option<i64>,
1119}
1120
1121impl From<CoreRateLimitWindow> for RateLimitWindow {
1122    fn from(value: CoreRateLimitWindow) -> Self {
1123        Self {
1124            used_percent: value.used_percent.round() as i32,
1125            window_duration_mins: value.window_minutes,
1126            resets_at: value.resets_at,
1127        }
1128    }
1129}
1130
1131#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
1132#[serde(rename_all = "camelCase")]
1133#[ts(export_to = "v2/")]
1134pub struct CreditsSnapshot {
1135    pub has_credits: bool,
1136    pub unlimited: bool,
1137    pub balance: Option<String>,
1138}
1139
1140impl From<CoreCreditsSnapshot> for CreditsSnapshot {
1141    fn from(value: CoreCreditsSnapshot) -> Self {
1142        Self {
1143            has_credits: value.has_credits,
1144            unlimited: value.unlimited,
1145            balance: value.balance,
1146        }
1147    }
1148}
1149
1150#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
1151#[serde(rename_all = "camelCase")]
1152#[ts(export_to = "v2/")]
1153pub struct AccountLoginCompletedNotification {
1154    // Use plain String for identifiers to avoid TS/JSON Schema quirks around uuid-specific types.
1155    // Convert to/from UUIDs at the application layer as needed.
1156    pub login_id: Option<String>,
1157    pub success: bool,
1158    pub error: Option<String>,
1159}
1160
1161#[cfg(test)]
1162mod tests {
1163    use super::*;
1164    use codex_protocol::items::AgentMessageContent;
1165    use codex_protocol::items::AgentMessageItem;
1166    use codex_protocol::items::ReasoningItem;
1167    use codex_protocol::items::TurnItem;
1168    use codex_protocol::items::UserMessageItem;
1169    use codex_protocol::items::WebSearchItem;
1170    use codex_protocol::user_input::UserInput as CoreUserInput;
1171    use pretty_assertions::assert_eq;
1172    use serde_json::json;
1173    use std::path::PathBuf;
1174
1175    #[test]
1176    fn core_turn_item_into_thread_item_converts_supported_variants() {
1177        let user_item = TurnItem::UserMessage(UserMessageItem {
1178            id: "user-1".to_string(),
1179            content: vec![
1180                CoreUserInput::Text {
1181                    text: "hello".to_string(),
1182                },
1183                CoreUserInput::Image {
1184                    image_url: "https://example.com/image.png".to_string(),
1185                },
1186                CoreUserInput::LocalImage {
1187                    path: PathBuf::from("local/image.png"),
1188                },
1189            ],
1190        });
1191
1192        assert_eq!(
1193            ThreadItem::from(user_item),
1194            ThreadItem::UserMessage {
1195                id: "user-1".to_string(),
1196                content: vec![
1197                    UserInput::Text {
1198                        text: "hello".to_string(),
1199                    },
1200                    UserInput::Image {
1201                        url: "https://example.com/image.png".to_string(),
1202                    },
1203                    UserInput::LocalImage {
1204                        path: PathBuf::from("local/image.png"),
1205                    },
1206                ],
1207            }
1208        );
1209
1210        let agent_item = TurnItem::AgentMessage(AgentMessageItem {
1211            id: "agent-1".to_string(),
1212            content: vec![
1213                AgentMessageContent::Text {
1214                    text: "Hello ".to_string(),
1215                },
1216                AgentMessageContent::Text {
1217                    text: "world".to_string(),
1218                },
1219            ],
1220        });
1221
1222        assert_eq!(
1223            ThreadItem::from(agent_item),
1224            ThreadItem::AgentMessage {
1225                id: "agent-1".to_string(),
1226                text: "Hello world".to_string(),
1227            }
1228        );
1229
1230        let reasoning_item = TurnItem::Reasoning(ReasoningItem {
1231            id: "reasoning-1".to_string(),
1232            summary_text: vec!["line one".to_string(), "line two".to_string()],
1233            raw_content: vec![],
1234        });
1235
1236        assert_eq!(
1237            ThreadItem::from(reasoning_item),
1238            ThreadItem::Reasoning {
1239                id: "reasoning-1".to_string(),
1240                summary: vec!["line one".to_string(), "line two".to_string()],
1241                content: vec![],
1242            }
1243        );
1244
1245        let search_item = TurnItem::WebSearch(WebSearchItem {
1246            id: "search-1".to_string(),
1247            query: "docs".to_string(),
1248        });
1249
1250        assert_eq!(
1251            ThreadItem::from(search_item),
1252            ThreadItem::WebSearch {
1253                id: "search-1".to_string(),
1254                query: "docs".to_string(),
1255            }
1256        );
1257    }
1258
1259    #[test]
1260    fn codex_error_info_serializes_http_status_code_in_camel_case() {
1261        let value = CodexErrorInfo::ResponseTooManyFailedAttempts {
1262            http_status_code: Some(401),
1263        };
1264
1265        assert_eq!(
1266            serde_json::to_value(value).unwrap(),
1267            json!({
1268                "responseTooManyFailedAttempts": {
1269                    "httpStatusCode": 401
1270                }
1271            })
1272        );
1273    }
1274}