diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index e8b0c9f3b48..c269fc73def 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -6694,6 +6694,13 @@ impl CodexMessageProcessor { None => None, }; + if let Some(chatgpt_user_id) = self + .auth_manager + .auth_cached() + .and_then(|auth| auth.get_chatgpt_user_id()) + { + tracing::info!(target: "feedback_tags", chatgpt_user_id); + } let snapshot = self.feedback.snapshot(conversation_id); let thread_id = snapshot.thread_id.clone(); let sqlite_feedback_logs = if include_logs { diff --git a/codex-rs/core/src/auth.rs b/codex-rs/core/src/auth.rs index ddce81b2483..9f13cdf2b57 100644 --- a/codex-rs/core/src/auth.rs +++ b/codex-rs/core/src/auth.rs @@ -266,6 +266,12 @@ impl CodexAuth { self.get_current_token_data().and_then(|t| t.id_token.email) } + /// Returns `None` if `is_chatgpt_auth()` is false. + pub fn get_chatgpt_user_id(&self) -> Option { + self.get_current_token_data() + .and_then(|t| t.id_token.chatgpt_user_id) + } + /// Account-facing plan classification derived from the current token. /// Returns a high-level `AccountPlanType` (e.g., Free/Plus/Pro/Team/…) /// mapped from the ID token's internal plan value. Prefer this when you @@ -1466,6 +1472,7 @@ mod tests { .unwrap(); assert_eq!(None, auth.api_key()); assert_eq!(AuthMode::Chatgpt, auth.auth_mode()); + assert_eq!(auth.get_chatgpt_user_id().as_deref(), Some("user-12345")); let auth_dot_json = auth .get_current_auth_json() diff --git a/codex-rs/core/src/client_common.rs b/codex-rs/core/src/client_common.rs index 13516691952..08613f0eab9 100644 --- a/codex-rs/core/src/client_common.rs +++ b/codex-rs/core/src/client_common.rs @@ -160,6 +160,7 @@ pub(crate) mod tools { use codex_protocol::config_types::WebSearchUserLocationType; use serde::Deserialize; use serde::Serialize; + use serde_json::Value; /// When serialized as JSON, this produces a valid "Tool" in the OpenAI /// Responses API. @@ -268,6 +269,8 @@ pub(crate) mod tools { /// `properties` must be present in `required`. pub(crate) strict: bool, pub(crate) parameters: JsonSchema, + #[serde(skip)] + pub(crate) output_schema: Option, } } diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index 480b96f7c1e..51167dd3fa6 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -58,6 +58,7 @@ use codex_app_server_protocol::AppInfo; use codex_otel::TelemetryAuthMode; use codex_protocol::models::BaseInstructions; use codex_protocol::models::ContentItem; +use codex_protocol::models::McpToolOutput; use codex_protocol::models::ResponseInputItem; use codex_protocol::models::ResponseItem; use codex_protocol::openai_models::ModelsResponse; @@ -1607,7 +1608,7 @@ fn prefers_structured_content_when_present() { meta: None, }; - let got = FunctionCallOutputPayload::from(&ctr); + let got = McpToolOutput::from(&ctr).into_function_call_output_payload(); let expected = FunctionCallOutputPayload { body: FunctionCallOutputBody::Text( serde_json::to_string(&json!({ @@ -1689,7 +1690,7 @@ fn falls_back_to_content_when_structured_is_null() { meta: None, }; - let got = FunctionCallOutputPayload::from(&ctr); + let got = McpToolOutput::from(&ctr).into_function_call_output_payload(); let expected = FunctionCallOutputPayload { body: FunctionCallOutputBody::Text( serde_json::to_string(&vec![text_block("hello"), text_block("world")]).unwrap(), @@ -1709,7 +1710,7 @@ fn success_flag_reflects_is_error_true() { meta: None, }; - let got = FunctionCallOutputPayload::from(&ctr); + let got = McpToolOutput::from(&ctr).into_function_call_output_payload(); let expected = FunctionCallOutputPayload { body: FunctionCallOutputBody::Text( serde_json::to_string(&json!({ "message": "bad" })).unwrap(), @@ -1729,7 +1730,7 @@ fn success_flag_true_with_no_error_and_content_used() { meta: None, }; - let got = FunctionCallOutputPayload::from(&ctr); + let got = McpToolOutput::from(&ctr).into_function_call_output_payload(); let expected = FunctionCallOutputPayload { body: FunctionCallOutputBody::Text( serde_json::to_string(&vec![text_block("alpha")]).unwrap(), diff --git a/codex-rs/core/src/mcp_tool_call.rs b/codex-rs/core/src/mcp_tool_call.rs index 29d1de5b6df..6bce8c09306 100644 --- a/codex-rs/core/src/mcp_tool_call.rs +++ b/codex-rs/core/src/mcp_tool_call.rs @@ -29,9 +29,7 @@ use crate::protocol::McpToolCallBeginEvent; use crate::protocol::McpToolCallEndEvent; use crate::state_db; use codex_protocol::mcp::CallToolResult; -use codex_protocol::models::FunctionCallOutputBody; -use codex_protocol::models::FunctionCallOutputPayload; -use codex_protocol::models::ResponseInputItem; +use codex_protocol::models::McpToolOutput; use codex_protocol::openai_models::InputModality; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::ReviewDecision; @@ -58,7 +56,7 @@ pub(crate) async fn handle_mcp_tool_call( server: String, tool_name: String, arguments: String, -) -> ResponseInputItem { +) -> McpToolOutput { // Parse the `arguments` as JSON. An empty string is OK, but invalid JSON // is not. let arguments_value = if arguments.trim().is_empty() { @@ -68,13 +66,7 @@ pub(crate) async fn handle_mcp_tool_call( Ok(value) => Some(value), Err(e) => { error!("failed to parse tool call arguments: {e}"); - return ResponseInputItem::FunctionCallOutput { - call_id: call_id.clone(), - output: FunctionCallOutputPayload { - body: FunctionCallOutputBody::Text(format!("err: {e}")), - success: Some(false), - }, - }; + return McpToolOutput::from_error_text(format!("err: {e}")); } } }; @@ -118,7 +110,7 @@ pub(crate) async fn handle_mcp_tool_call( turn_context .session_telemetry .counter("codex.mcp.call", 1, &[("status", status)]); - return ResponseInputItem::McpToolCallOutput { call_id, result }; + return McpToolOutput::from_result(result); } if let Some(decision) = maybe_request_mcp_tool_approval( @@ -212,7 +204,7 @@ pub(crate) async fn handle_mcp_tool_call( .session_telemetry .counter("codex.mcp.call", 1, &[("status", status)]); - return ResponseInputItem::McpToolCallOutput { call_id, result }; + return McpToolOutput::from_result(result); } let tool_call_begin_event = EventMsg::McpToolCallBegin(McpToolCallBeginEvent { @@ -258,7 +250,7 @@ pub(crate) async fn handle_mcp_tool_call( .session_telemetry .counter("codex.mcp.call", 1, &[("status", status)]); - ResponseInputItem::McpToolCallOutput { call_id, result } + McpToolOutput::from_result(result) } async fn maybe_mark_thread_memory_mode_polluted(sess: &Session, turn_context: &TurnContext) { diff --git a/codex-rs/core/src/shell.rs b/codex-rs/core/src/shell.rs index cec5d8a93aa..4cd728992e9 100644 --- a/codex-rs/core/src/shell.rs +++ b/codex-rs/core/src/shell.rs @@ -90,22 +90,62 @@ impl Eq for Shell {} #[cfg(unix)] fn get_user_shell_path() -> Option { - use libc::getpwuid; - use libc::getuid; + let uid = unsafe { libc::getuid() }; use std::ffi::CStr; + use std::mem::MaybeUninit; + use std::ptr; + + let mut passwd = MaybeUninit::::uninit(); + + // We cannot use getpwuid here: it returns pointers into libc-managed + // storage, which is not safe to read concurrently on all targets (the musl + // static build used by the CLI can segfault when parallel callers race on + // that buffer). getpwuid_r keeps the passwd data in caller-owned memory. + let suggested_buffer_len = unsafe { libc::sysconf(libc::_SC_GETPW_R_SIZE_MAX) }; + let buffer_len = usize::try_from(suggested_buffer_len) + .ok() + .filter(|len| *len > 0) + .unwrap_or(1024); + let mut buffer = vec![0; buffer_len]; + + loop { + let mut result = ptr::null_mut(); + let status = unsafe { + libc::getpwuid_r( + uid, + passwd.as_mut_ptr(), + buffer.as_mut_ptr().cast(), + buffer.len(), + &mut result, + ) + }; - unsafe { - let uid = getuid(); - let pw = getpwuid(uid); + if status == 0 { + if result.is_null() { + return None; + } - if !pw.is_null() { - let shell_path = CStr::from_ptr((*pw).pw_shell) + let passwd = unsafe { passwd.assume_init_ref() }; + if passwd.pw_shell.is_null() { + return None; + } + + let shell_path = unsafe { CStr::from_ptr(passwd.pw_shell) } .to_string_lossy() .into_owned(); - Some(PathBuf::from(shell_path)) - } else { - None + return Some(PathBuf::from(shell_path)); + } + + if status != libc::ERANGE { + return None; + } + + // Retry with a larger buffer until libc can materialize the passwd entry. + let new_len = buffer.len().checked_mul(2)?; + if new_len > 1024 * 1024 { + return None; } + buffer.resize(new_len, 0); } } @@ -500,7 +540,7 @@ mod tests { } #[test] - fn finds_poweshell() { + fn finds_powershell() { if !cfg!(windows) { return; } diff --git a/codex-rs/core/src/stream_events_utils.rs b/codex-rs/core/src/stream_events_utils.rs index 8f77a80e370..afd600c942d 100644 --- a/codex-rs/core/src/stream_events_utils.rs +++ b/codex-rs/core/src/stream_events_utils.rs @@ -359,14 +359,8 @@ pub(crate) fn response_input_to_response_item(input: &ResponseInputItem) -> Opti output: output.clone(), }) } - ResponseInputItem::McpToolCallOutput { call_id, result } => { - let output = match result { - Ok(call_tool_result) => FunctionCallOutputPayload::from(call_tool_result), - Err(err) => FunctionCallOutputPayload { - body: FunctionCallOutputBody::Text(err.clone()), - success: Some(false), - }, - }; + ResponseInputItem::McpToolCallOutput { call_id, output } => { + let output = output.as_function_call_output_payload(); Some(ResponseItem::FunctionCallOutput { call_id: call_id.clone(), output, diff --git a/codex-rs/core/src/tools/code_mode.rs b/codex-rs/core/src/tools/code_mode.rs index 9c1df684c75..7fef60f6844 100644 --- a/codex-rs/core/src/tools/code_mode.rs +++ b/codex-rs/core/src/tools/code_mode.rs @@ -14,15 +14,10 @@ use crate::tools::context::ToolPayload; use crate::tools::js_repl::resolve_compatible_node; use crate::tools::router::ToolCall; use crate::tools::router::ToolCallSource; -use codex_protocol::models::ContentItem; -use codex_protocol::models::FunctionCallOutputBody; use codex_protocol::models::FunctionCallOutputContentItem; -use codex_protocol::models::FunctionCallOutputPayload; -use codex_protocol::models::ResponseInputItem; use serde::Deserialize; use serde::Serialize; use serde_json::Value as JsonValue; -use serde_json::json; use tokio::io::AsyncBufReadExt; use tokio::io::AsyncReadExt; use tokio::io::AsyncWriteExt; @@ -60,7 +55,7 @@ enum HostToNodeMessage { }, Response { id: String, - content_items: Vec, + code_mode_result: JsonValue, }, } @@ -90,11 +85,11 @@ pub(crate) fn instructions(config: &Config) -> Option { section.push_str("- `code_mode` is a freeform/custom tool. Direct `code_mode` calls must send raw JavaScript tool input. Do not wrap code in JSON, quotes, or markdown code fences.\n"); section.push_str("- Direct tool calls remain available while `code_mode` is enabled.\n"); section.push_str("- `code_mode` uses the same Node runtime resolution as `js_repl`. If needed, point `js_repl_node_path` at the Node binary you want Codex to use.\n"); - section.push_str("- Import nested tools from `tools.js`, for example `import { exec_command } from \"tools.js\"` or `import { tools } from \"tools.js\"`. `tools[name]` and identifier wrappers like `await exec_command(args)` remain available for compatibility. Nested tool calls resolve to arrays of content items.\n"); + section.push_str("- Import nested tools from `tools.js`, for example `import { exec_command } from \"tools.js\"` or `import { tools } from \"tools.js\"`. `tools[name]` and identifier wrappers like `await exec_command(args)` remain available for compatibility. Nested tool calls resolve to their code-mode result values.\n"); section.push_str( "- Function tools require JSON object arguments. Freeform tools require raw strings.\n", ); - section.push_str("- `add_content(value)` is synchronous. It accepts a content item or an array of content items, so `add_content(await exec_command(...))` returns the same content items a direct tool call would expose to the model.\n"); + section.push_str("- `add_content(value)` is synchronous. It accepts a content item, an array of content items, or a string. Structured nested-tool results should be converted to text first, for example with `JSON.stringify(...)`.\n"); section .push_str("- Only content passed to `add_content(value)` is surfaced back to the model."); Some(section) @@ -186,7 +181,7 @@ async fn execute_node( NodeToHostMessage::ToolCall { id, name, input } => { let response = HostToNodeMessage::Response { id, - content_items: call_nested_tool(exec.clone(), name, input).await, + code_mode_result: call_nested_tool(exec.clone(), name, input).await, }; write_message(&mut stdin, &response).await?; } @@ -290,9 +285,9 @@ async fn call_nested_tool( exec: ExecContext, tool_name: String, input: Option, -) -> Vec { +) -> JsonValue { if tool_name == "code_mode" { - return error_content_items_json("code_mode cannot invoke itself".to_string()); + return JsonValue::String("code_mode cannot invoke itself".to_string()); } let nested_config = exec.turn.tools_config.for_code_mode_nested_tools(); @@ -306,7 +301,7 @@ async fn call_nested_tool( let specs = router.specs(); let payload = match build_nested_tool_payload(&specs, &tool_name, input) { Ok(payload) => payload, - Err(error) => return error_content_items_json(error), + Err(error) => return JsonValue::String(error), }; let call = ToolCall { @@ -314,8 +309,8 @@ async fn call_nested_tool( call_id: format!("code_mode-{}", uuid::Uuid::new_v4()), payload, }; - let response = router - .dispatch_tool_call( + let result = router + .dispatch_tool_call_with_code_mode_result( Arc::clone(&exec.session), Arc::clone(&exec.turn), Arc::clone(&exec.tracker), @@ -324,11 +319,9 @@ async fn call_nested_tool( ) .await; - match response { - Ok(response) => { - json_values_from_output_content_items(content_items_from_response_input(response)) - } - Err(error) => error_content_items_json(error.to_string()), + match result { + Ok(result) => result.code_mode_result(), + Err(error) => JsonValue::String(error.to_string()), } } @@ -387,70 +380,6 @@ fn build_freeform_tool_payload( } } -fn content_items_from_response_input( - response: ResponseInputItem, -) -> Vec { - match response { - ResponseInputItem::Message { content, .. } => content - .into_iter() - .map(function_output_content_item_from_content_item) - .collect(), - ResponseInputItem::FunctionCallOutput { output, .. } => { - content_items_from_function_output(output) - } - ResponseInputItem::CustomToolCallOutput { output, .. } => { - content_items_from_function_output(output) - } - ResponseInputItem::McpToolCallOutput { result, .. } => match result { - Ok(result) => { - content_items_from_function_output(FunctionCallOutputPayload::from(&result)) - } - Err(error) => vec![FunctionCallOutputContentItem::InputText { text: error }], - }, - } -} - -fn content_items_from_function_output( - output: FunctionCallOutputPayload, -) -> Vec { - match output.body { - FunctionCallOutputBody::Text(text) => { - vec![FunctionCallOutputContentItem::InputText { text }] - } - FunctionCallOutputBody::ContentItems(items) => items, - } -} - -fn function_output_content_item_from_content_item( - item: ContentItem, -) -> FunctionCallOutputContentItem { - match item { - ContentItem::InputText { text } | ContentItem::OutputText { text } => { - FunctionCallOutputContentItem::InputText { text } - } - ContentItem::InputImage { image_url } => FunctionCallOutputContentItem::InputImage { - image_url, - detail: None, - }, - } -} - -fn json_values_from_output_content_items( - content_items: Vec, -) -> Vec { - content_items - .into_iter() - .map(|item| match item { - FunctionCallOutputContentItem::InputText { text } => { - json!({ "type": "input_text", "text": text }) - } - FunctionCallOutputContentItem::InputImage { image_url, detail } => { - json!({ "type": "input_image", "image_url": image_url, "detail": detail }) - } - }) - .collect() -} - fn output_content_items_from_json_values( content_items: Vec, ) -> Result, String> { @@ -463,7 +392,3 @@ fn output_content_items_from_json_values( }) .collect() } - -fn error_content_items_json(message: String) -> Vec { - vec![json!({ "type": "input_text", "text": message })] -} diff --git a/codex-rs/core/src/tools/code_mode_bridge.js b/codex-rs/core/src/tools/code_mode_bridge.js index eba69c9f3f4..aca85f7354c 100644 --- a/codex-rs/core/src/tools/code_mode_bridge.js +++ b/codex-rs/core/src/tools/code_mode_bridge.js @@ -22,13 +22,20 @@ function __codexCloneContentItem(item) { } } -function __codexNormalizeContentItems(value) { +function __codexNormalizeRawContentItems(value) { if (Array.isArray(value)) { - return value.flatMap((entry) => __codexNormalizeContentItems(entry)); + return value.flatMap((entry) => __codexNormalizeRawContentItems(entry)); } return [__codexCloneContentItem(value)]; } +function __codexNormalizeContentItems(value) { + if (typeof value === 'string') { + return [{ type: 'input_text', text: value }]; + } + return __codexNormalizeRawContentItems(value); +} + Object.defineProperty(globalThis, '__codexContentItems', { value: __codexContentItems, configurable: true, diff --git a/codex-rs/core/src/tools/code_mode_runner.cjs b/codex-rs/core/src/tools/code_mode_runner.cjs index 09fe9e8af08..e2fac0817c7 100644 --- a/codex-rs/core/src/tools/code_mode_runner.cjs +++ b/codex-rs/core/src/tools/code_mode_runner.cjs @@ -44,7 +44,7 @@ function createProtocol() { return; } pending.delete(message.id); - entry.resolve(Array.isArray(message.content_items) ? message.content_items : []); + entry.resolve(message.code_mode_result ?? ''); return; } diff --git a/codex-rs/core/src/tools/context.rs b/codex-rs/core/src/tools/context.rs index a3521c466e6..ce6f8ea53c5 100644 --- a/codex-rs/core/src/tools/context.rs +++ b/codex-rs/core/src/tools/context.rs @@ -7,14 +7,16 @@ use crate::truncate::TruncationPolicy; use crate::truncate::formatted_truncate_text; use crate::turn_diff_tracker::TurnDiffTracker; use crate::unified_exec::resolve_max_tokens; -use codex_protocol::mcp::CallToolResult; use codex_protocol::models::FunctionCallOutputBody; use codex_protocol::models::FunctionCallOutputContentItem; use codex_protocol::models::FunctionCallOutputPayload; +use codex_protocol::models::McpToolOutput; use codex_protocol::models::ResponseInputItem; use codex_protocol::models::ShellToolCallParams; use codex_protocol::models::function_call_output_content_items_to_text; use codex_utils_string::take_bytes_at_char_boundary; +use serde::Serialize; +use serde_json::Value as JsonValue; use std::borrow::Cow; use std::sync::Arc; use std::time::Duration; @@ -73,27 +75,28 @@ pub trait ToolOutput: Send { fn success_for_logging(&self) -> bool; - fn into_response(self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem; -} + fn to_response_item(&self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem; -pub struct McpToolOutput { - pub result: Result, + fn code_mode_result(&self, payload: &ToolPayload) -> JsonValue { + response_input_to_code_mode_result(self.to_response_item("", payload)) + } } impl ToolOutput for McpToolOutput { fn log_preview(&self) -> String { - format!("{:?}", self.result) + let output = self.as_function_call_output_payload(); + let preview = output.body.to_text().unwrap_or_else(|| output.to_string()); + telemetry_preview(&preview) } fn success_for_logging(&self) -> bool { - self.result.is_ok() + self.success } - fn into_response(self, call_id: &str, _payload: &ToolPayload) -> ResponseInputItem { - let Self { result } = self; + fn to_response_item(&self, call_id: &str, _payload: &ToolPayload) -> ResponseInputItem { ResponseInputItem::McpToolCallOutput { call_id: call_id.to_string(), - result, + output: self.clone(), } } } @@ -137,9 +140,8 @@ impl ToolOutput for FunctionToolOutput { self.success.unwrap_or(true) } - fn into_response(self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem { - let Self { body, success } = self; - function_tool_response(call_id, payload, body, success) + fn to_response_item(&self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem { + function_tool_response(call_id, payload, self.body.clone(), self.success) } } @@ -166,7 +168,7 @@ impl ToolOutput for ExecCommandToolOutput { true } - fn into_response(self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem { + fn to_response_item(&self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem { function_tool_response( call_id, payload, @@ -176,6 +178,35 @@ impl ToolOutput for ExecCommandToolOutput { Some(true), ) } + + fn code_mode_result(&self, _payload: &ToolPayload) -> JsonValue { + #[derive(Serialize)] + struct UnifiedExecCodeModeResult { + #[serde(skip_serializing_if = "Option::is_none")] + chunk_id: Option, + wall_time_seconds: f64, + #[serde(skip_serializing_if = "Option::is_none")] + exit_code: Option, + #[serde(skip_serializing_if = "Option::is_none")] + session_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + original_token_count: Option, + output: String, + } + + let result = UnifiedExecCodeModeResult { + chunk_id: (!self.chunk_id.is_empty()).then(|| self.chunk_id.clone()), + wall_time_seconds: self.wall_time.as_secs_f64(), + exit_code: self.exit_code, + session_id: self.process_id.clone(), + original_token_count: self.original_token_count, + output: self.truncated_output(), + }; + + serde_json::to_value(result).unwrap_or_else(|err| { + JsonValue::String(format!("failed to serialize exec result: {err}")) + }) + } } impl ExecCommandToolOutput { @@ -214,6 +245,64 @@ impl ExecCommandToolOutput { } } +fn response_input_to_code_mode_result(response: ResponseInputItem) -> JsonValue { + match response { + ResponseInputItem::Message { content, .. } => content_items_to_code_mode_result( + &content + .into_iter() + .map(|item| match item { + codex_protocol::models::ContentItem::InputText { text } + | codex_protocol::models::ContentItem::OutputText { text } => { + FunctionCallOutputContentItem::InputText { text } + } + codex_protocol::models::ContentItem::InputImage { image_url } => { + FunctionCallOutputContentItem::InputImage { + image_url, + detail: None, + } + } + }) + .collect::>(), + ), + ResponseInputItem::FunctionCallOutput { output, .. } + | ResponseInputItem::CustomToolCallOutput { output, .. } => match output.body { + FunctionCallOutputBody::Text(text) => JsonValue::String(text), + FunctionCallOutputBody::ContentItems(items) => { + content_items_to_code_mode_result(&items) + } + }, + ResponseInputItem::McpToolCallOutput { output, .. } => { + match output.as_function_call_output_payload().body { + FunctionCallOutputBody::Text(text) => JsonValue::String(text), + FunctionCallOutputBody::ContentItems(items) => { + content_items_to_code_mode_result(&items) + } + } + } + } +} + +fn content_items_to_code_mode_result(items: &[FunctionCallOutputContentItem]) -> JsonValue { + JsonValue::String( + items + .iter() + .filter_map(|item| match item { + FunctionCallOutputContentItem::InputText { text } if !text.trim().is_empty() => { + Some(text.clone()) + } + FunctionCallOutputContentItem::InputImage { image_url, .. } + if !image_url.trim().is_empty() => + { + Some(image_url.clone()) + } + FunctionCallOutputContentItem::InputText { .. } + | FunctionCallOutputContentItem::InputImage { .. } => None, + }) + .collect::>() + .join("\n"), + ) +} + fn function_tool_response( call_id: &str, payload: &ToolPayload, @@ -292,7 +381,7 @@ mod tests { input: "patch".to_string(), }; let response = FunctionToolOutput::from_text("patched".to_string(), Some(true)) - .into_response("call-42", &payload); + .to_response_item("call-42", &payload); match response { ResponseInputItem::CustomToolCallOutput { call_id, output } => { @@ -311,7 +400,7 @@ mod tests { arguments: "{}".to_string(), }; let response = FunctionToolOutput::from_text("ok".to_string(), Some(true)) - .into_response("fn-1", &payload); + .to_response_item("fn-1", &payload); match response { ResponseInputItem::FunctionCallOutput { call_id, output } => { @@ -344,7 +433,7 @@ mod tests { ], Some(true), ) - .into_response("call-99", &payload); + .to_response_item("call-99", &payload); match response { ResponseInputItem::CustomToolCallOutput { call_id, output } => { @@ -433,7 +522,7 @@ mod tests { original_token_count: Some(10), session_command: None, } - .into_response("call-42", &payload); + .to_response_item("call-42", &payload); match response { ResponseInputItem::FunctionCallOutput { call_id, output } => { diff --git a/codex-rs/core/src/tools/handlers/agent_jobs.rs b/codex-rs/core/src/tools/handlers/agent_jobs.rs index ff02a3fbd1d..956107ed90b 100644 --- a/codex-rs/core/src/tools/handlers/agent_jobs.rs +++ b/codex-rs/core/src/tools/handlers/agent_jobs.rs @@ -15,9 +15,12 @@ use crate::tools::registry::ToolHandler; use crate::tools::registry::ToolKind; use async_trait::async_trait; use codex_protocol::ThreadId; +use codex_protocol::protocol::AgentStatus; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; use codex_protocol::user_input::UserInput; +use futures::StreamExt; +use futures::stream::FuturesUnordered; use serde::Deserialize; use serde::Serialize; use serde_json::Value; @@ -26,8 +29,10 @@ use std::collections::HashSet; use std::path::Path; use std::path::PathBuf; use std::sync::Arc; +use tokio::sync::watch::Receiver; use tokio::time::Duration; use tokio::time::Instant; +use tokio::time::timeout; use uuid::Uuid; pub struct BatchJobHandler; @@ -103,6 +108,7 @@ struct JobRunnerOptions { struct ActiveJobItem { item_id: String, started_at: Instant, + status_rx: Option>, } struct JobProgressEmitter { @@ -670,6 +676,12 @@ async fn run_agent_job_loop( ActiveJobItem { item_id: item.item_id.clone(), started_at: Instant::now(), + status_rx: session + .services + .agent_control + .subscribe_status(thread_id) + .await + .ok(), }, ); progressed = true; @@ -702,7 +714,7 @@ async fn run_agent_job_loop( break; } if !progressed { - tokio::time::sleep(STATUS_POLL_INTERVAL).await; + wait_for_status_change(&active_items).await; } continue; } @@ -833,6 +845,12 @@ async fn recover_running_items( ActiveJobItem { item_id: item.item_id.clone(), started_at: started_at_from_item(&item), + status_rx: session + .services + .agent_control + .subscribe_status(thread_id) + .await + .ok(), }, ); } @@ -846,13 +864,44 @@ async fn find_finished_threads( ) -> Vec<(ThreadId, String)> { let mut finished = Vec::new(); for (thread_id, item) in active_items { - if is_final(&session.services.agent_control.get_status(*thread_id).await) { + let status = active_item_status(session.as_ref(), *thread_id, item).await; + if is_final(&status) { finished.push((*thread_id, item.item_id.clone())); } } finished } +async fn active_item_status( + session: &Session, + thread_id: ThreadId, + item: &ActiveJobItem, +) -> AgentStatus { + if let Some(status_rx) = item.status_rx.as_ref() + && status_rx.has_changed().is_ok() + { + return status_rx.borrow().clone(); + } + session.services.agent_control.get_status(thread_id).await +} + +async fn wait_for_status_change(active_items: &HashMap) { + let mut waiters = FuturesUnordered::new(); + for item in active_items.values() { + if let Some(status_rx) = item.status_rx.as_ref() { + let mut status_rx = status_rx.clone(); + waiters.push(async move { + let _ = status_rx.changed().await; + }); + } + } + if waiters.is_empty() { + tokio::time::sleep(STATUS_POLL_INTERVAL).await; + return; + } + let _ = timeout(STATUS_POLL_INTERVAL, waiters.next()).await; +} + async fn reap_stale_active_items( session: Arc, db: Arc, @@ -890,37 +939,24 @@ async fn finalize_finished_item( item_id: &str, thread_id: ThreadId, ) -> anyhow::Result<()> { - let mut item = db + let item = db .get_agent_job_item(job_id, item_id) .await? .ok_or_else(|| { anyhow::anyhow!("job item not found for finalization: {job_id}/{item_id}") })?; - if item.result_json.is_none() { - tokio::time::sleep(Duration::from_millis(250)).await; - item = db - .get_agent_job_item(job_id, item_id) - .await? - .ok_or_else(|| { - anyhow::anyhow!("job item not found after grace period: {job_id}/{item_id}") - })?; - } - if item.result_json.is_some() { - if !db.mark_agent_job_item_completed(job_id, item_id).await? { - db.mark_agent_job_item_failed( - job_id, - item_id, - "worker reported result but item could not transition to completed", - ) - .await?; + if matches!(item.status, codex_state::AgentJobItemStatus::Running) { + if item.result_json.is_some() { + let _ = db.mark_agent_job_item_completed(job_id, item_id).await?; + } else { + let _ = db + .mark_agent_job_item_failed( + job_id, + item_id, + "worker finished without calling report_agent_job_result", + ) + .await?; } - } else { - db.mark_agent_job_item_failed( - job_id, - item_id, - "worker finished without calling report_agent_job_result", - ) - .await?; } let _ = session .services diff --git a/codex-rs/core/src/tools/handlers/apply_patch.rs b/codex-rs/core/src/tools/handlers/apply_patch.rs index e31a47ab13c..b98a721e36d 100644 --- a/codex-rs/core/src/tools/handlers/apply_patch.rs +++ b/codex-rs/core/src/tools/handlers/apply_patch.rs @@ -420,13 +420,14 @@ It is important to remember: - You must prefix new lines with `+` even when creating a new file - File references can only be relative, NEVER ABSOLUTE. "# - .to_string(), + .to_string(), strict: false, parameters: JsonSchema::Object { properties, required: Some(vec!["input".to_string()]), additional_properties: Some(false.into()), }, + output_schema: None, }) } diff --git a/codex-rs/core/src/tools/handlers/mcp.rs b/codex-rs/core/src/tools/handlers/mcp.rs index 1564206be7a..14b6926e8a4 100644 --- a/codex-rs/core/src/tools/handlers/mcp.rs +++ b/codex-rs/core/src/tools/handlers/mcp.rs @@ -3,47 +3,16 @@ use std::sync::Arc; use crate::function_tool::FunctionCallError; use crate::mcp_tool_call::handle_mcp_tool_call; -use crate::tools::context::FunctionToolOutput; -use crate::tools::context::McpToolOutput; use crate::tools::context::ToolInvocation; use crate::tools::context::ToolPayload; use crate::tools::registry::ToolHandler; use crate::tools::registry::ToolKind; -use codex_protocol::models::ResponseInputItem; +use codex_protocol::models::McpToolOutput; pub struct McpHandler; - -pub enum McpHandlerOutput { - Mcp(McpToolOutput), - Function(FunctionToolOutput), -} - -impl crate::tools::context::ToolOutput for McpHandlerOutput { - fn log_preview(&self) -> String { - match self { - Self::Mcp(output) => output.log_preview(), - Self::Function(output) => output.log_preview(), - } - } - - fn success_for_logging(&self) -> bool { - match self { - Self::Mcp(output) => output.success_for_logging(), - Self::Function(output) => output.success_for_logging(), - } - } - - fn into_response(self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem { - match self { - Self::Mcp(output) => output.into_response(call_id, payload), - Self::Function(output) => output.into_response(call_id, payload), - } - } -} - #[async_trait] impl ToolHandler for McpHandler { - type Output = McpHandlerOutput; + type Output = McpToolOutput; fn kind(&self) -> ToolKind { ToolKind::Mcp @@ -74,7 +43,7 @@ impl ToolHandler for McpHandler { let (server, tool, raw_arguments) = payload; let arguments_str = raw_arguments; - let response = handle_mcp_tool_call( + let output = handle_mcp_tool_call( Arc::clone(&session), &turn, call_id.clone(), @@ -84,26 +53,6 @@ impl ToolHandler for McpHandler { ) .await; - match response { - ResponseInputItem::McpToolCallOutput { result, .. } => { - Ok(McpHandlerOutput::Mcp(McpToolOutput { result })) - } - ResponseInputItem::FunctionCallOutput { output, .. } => { - let success = output.success; - match output.body { - codex_protocol::models::FunctionCallOutputBody::Text(text) => Ok( - McpHandlerOutput::Function(FunctionToolOutput::from_text(text, success)), - ), - codex_protocol::models::FunctionCallOutputBody::ContentItems(content) => { - Ok(McpHandlerOutput::Function( - FunctionToolOutput::from_content(content, success), - )) - } - } - } - _ => Err(FunctionCallError::RespondToModel( - "mcp handler received unexpected response variant".to_string(), - )), - } + Ok(output) } } diff --git a/codex-rs/core/src/tools/handlers/plan.rs b/codex-rs/core/src/tools/handlers/plan.rs index 6b810e0a6dd..bd70418a65f 100644 --- a/codex-rs/core/src/tools/handlers/plan.rs +++ b/codex-rs/core/src/tools/handlers/plan.rs @@ -57,6 +57,7 @@ At most one step can be in_progress at a time. required: Some(vec!["plan".to_string()]), additional_properties: Some(false.into()), }, + output_schema: None, }) }); diff --git a/codex-rs/core/src/tools/js_repl/mod.rs b/codex-rs/core/src/tools/js_repl/mod.rs index bc2a5342cea..4ffb92518ce 100644 --- a/codex-rs/core/src/tools/js_repl/mod.rs +++ b/codex-rs/core/src/tools/js_repl/mod.rs @@ -620,29 +620,23 @@ impl JsReplManager { output, ) } - ResponseInputItem::McpToolCallOutput { result, .. } => match result { - Ok(result) => { - let output = FunctionCallOutputPayload::from(result); - let mut summary = Self::summarize_function_output_payload( - "mcp_tool_call_output", - JsReplToolCallPayloadKind::McpResult, - &output, - ); - summary.payload_item_count = Some(result.content.len()); - summary.structured_content_present = Some(result.structured_content.is_some()); - summary.result_is_error = Some(result.is_error.unwrap_or(false)); - summary - } - Err(error) => { - let mut summary = Self::summarize_text_payload( - Some("mcp_tool_call_output"), - JsReplToolCallPayloadKind::McpErrorResult, - error, - ); - summary.result_is_error = Some(true); - summary - } - }, + ResponseInputItem::McpToolCallOutput { output, .. } => { + let function_output = output.as_function_call_output_payload(); + let payload_kind = if output.success { + JsReplToolCallPayloadKind::McpResult + } else { + JsReplToolCallPayloadKind::McpErrorResult + }; + let mut summary = Self::summarize_function_output_payload( + "mcp_tool_call_output", + payload_kind, + &function_output, + ); + summary.payload_item_count = Some(output.content.len()); + summary.structured_content_present = Some(output.structured_content.is_some()); + summary.result_is_error = Some(!output.success); + summary + } } } diff --git a/codex-rs/core/src/tools/parallel.rs b/codex-rs/core/src/tools/parallel.rs index a37c93db912..e64597675ce 100644 --- a/codex-rs/core/src/tools/parallel.rs +++ b/codex-rs/core/src/tools/parallel.rs @@ -124,7 +124,9 @@ impl ToolCallRuntime { }, ToolPayload::Mcp { .. } => ResponseInputItem::McpToolCallOutput { call_id: call.call_id.clone(), - result: Err(Self::abort_message(call, secs)), + output: codex_protocol::models::McpToolOutput::from_error_text( + Self::abort_message(call, secs), + ), }, _ => ResponseInputItem::FunctionCallOutput { call_id: call.call_id.clone(), diff --git a/codex-rs/core/src/tools/registry.rs b/codex-rs/core/src/tools/registry.rs index eb74f90dbcf..f78df2f1a74 100644 --- a/codex-rs/core/src/tools/registry.rs +++ b/codex-rs/core/src/tools/registry.rs @@ -57,10 +57,28 @@ pub trait ToolHandler: Send + Sync { async fn handle(&self, invocation: ToolInvocation) -> Result; } -struct AnyToolResult { - preview: String, - success: bool, - response: ResponseInputItem, +pub(crate) struct AnyToolResult { + pub(crate) call_id: String, + pub(crate) payload: ToolPayload, + pub(crate) result: Box, +} + +impl AnyToolResult { + pub(crate) fn into_response(self) -> ResponseInputItem { + let Self { + call_id, + payload, + result, + } = self; + result.to_response_item(&call_id, &payload) + } + + pub(crate) fn code_mode_result(self) -> serde_json::Value { + let Self { + payload, result, .. + } = self; + result.code_mode_result(&payload) + } } #[async_trait] @@ -95,13 +113,10 @@ where let call_id = invocation.call_id.clone(); let payload = invocation.payload.clone(); let output = self.handle(invocation).await?; - let preview = output.log_preview(); - let success = output.success_for_logging(); - let response = output.into_response(&call_id, &payload); Ok(AnyToolResult { - preview, - success, - response, + call_id, + payload, + result: Box::new(output), }) } } @@ -127,10 +142,10 @@ impl ToolRegistry { // } // } - pub async fn dispatch( + pub(crate) async fn dispatch_any( &self, invocation: ToolInvocation, - ) -> Result { + ) -> Result { let tool_name = invocation.tool_name.clone(); let call_id_owned = invocation.call_id.clone(); let otel = invocation.turn.session_telemetry.clone(); @@ -237,13 +252,10 @@ impl ToolRegistry { } match handler.handle_any(invocation_for_tool).await { Ok(result) => { - let AnyToolResult { - preview, - success, - response, - } = result; + let preview = result.result.log_preview(); + let success = result.result.success_for_logging(); let mut guard = response_cell.lock().await; - *guard = Some(response); + *guard = Some(result); Ok((preview, success)) } Err(err) => Err(err), @@ -275,10 +287,10 @@ impl ToolRegistry { match result { Ok(_) => { let mut guard = response_cell.lock().await; - let response = guard.take().ok_or_else(|| { + let result = guard.take().ok_or_else(|| { FunctionCallError::Fatal("tool produced no output".to_string()) })?; - Ok(response) + Ok(result) } Err(err) => Err(err), } diff --git a/codex-rs/core/src/tools/router.rs b/codex-rs/core/src/tools/router.rs index a55fb5fd5a6..7095a38cea6 100644 --- a/codex-rs/core/src/tools/router.rs +++ b/codex-rs/core/src/tools/router.rs @@ -4,15 +4,16 @@ use crate::codex::TurnContext; use crate::function_tool::FunctionCallError; use crate::mcp_connection_manager::ToolInfo; use crate::sandboxing::SandboxPermissions; +use crate::tools::context::FunctionToolOutput; use crate::tools::context::SharedTurnDiffTracker; use crate::tools::context::ToolInvocation; use crate::tools::context::ToolPayload; +use crate::tools::registry::AnyToolResult; use crate::tools::registry::ConfiguredToolSpec; use crate::tools::registry::ToolRegistry; use crate::tools::spec::ToolsConfig; use crate::tools::spec::build_specs; use codex_protocol::dynamic_tools::DynamicToolSpec; -use codex_protocol::models::FunctionCallOutputBody; use codex_protocol::models::LocalShellAction; use codex_protocol::models::ResponseInputItem; use codex_protocol::models::ResponseItem; @@ -145,6 +146,21 @@ impl ToolRouter { call: ToolCall, source: ToolCallSource, ) -> Result { + Ok(self + .dispatch_tool_call_with_code_mode_result(session, turn, tracker, call, source) + .await? + .into_response()) + } + + #[instrument(level = "trace", skip_all, err)] + pub async fn dispatch_tool_call_with_code_mode_result( + &self, + session: Arc, + turn: Arc, + tracker: SharedTurnDiffTracker, + call: ToolCall, + source: ToolCallSource, + ) -> Result { let ToolCall { tool_name, call_id, @@ -161,7 +177,7 @@ impl ToolRouter { "direct tool calls are disabled; use js_repl and codex.tool(...) instead" .to_string(), ); - return Ok(Self::failure_response( + return Ok(Self::failure_result( failure_call_id, payload_outputs_custom, err, @@ -177,10 +193,10 @@ impl ToolRouter { payload, }; - match self.registry.dispatch(invocation).await { + match self.registry.dispatch_any(invocation).await { Ok(response) => Ok(response), Err(FunctionCallError::Fatal(message)) => Err(FunctionCallError::Fatal(message)), - Err(err) => Ok(Self::failure_response( + Err(err) => Ok(Self::failure_result( failure_call_id, payload_outputs_custom, err, @@ -188,27 +204,27 @@ impl ToolRouter { } } - fn failure_response( + fn failure_result( call_id: String, payload_outputs_custom: bool, err: FunctionCallError, - ) -> ResponseInputItem { + ) -> AnyToolResult { let message = err.to_string(); if payload_outputs_custom { - ResponseInputItem::CustomToolCallOutput { + AnyToolResult { call_id, - output: codex_protocol::models::FunctionCallOutputPayload { - body: FunctionCallOutputBody::Text(message), - success: Some(false), + payload: ToolPayload::Custom { + input: String::new(), }, + result: Box::new(FunctionToolOutput::from_text(message, Some(false))), } } else { - ResponseInputItem::FunctionCallOutput { + AnyToolResult { call_id, - output: codex_protocol::models::FunctionCallOutputPayload { - body: FunctionCallOutputBody::Text(message), - success: Some(false), + payload: ToolPayload::Function { + arguments: "{}".to_string(), }, + result: Box::new(FunctionToolOutput::from_text(message, Some(false))), } } } diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index 2b823e0a07a..51bc84b23f8 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -40,6 +40,40 @@ use std::collections::HashMap; const SEARCH_TOOL_BM25_DESCRIPTION_TEMPLATE: &str = include_str!("../../templates/search_tool/tool_description.md"); const WEB_SEARCH_CONTENT_TYPES: [&str; 2] = ["text", "image"]; + +fn unified_exec_output_schema() -> JsonValue { + json!({ + "type": "object", + "properties": { + "chunk_id": { + "type": "string", + "description": "Chunk identifier included when the response reports one." + }, + "wall_time_seconds": { + "type": "number", + "description": "Elapsed wall time spent waiting for output in seconds." + }, + "exit_code": { + "type": "number", + "description": "Process exit code when the command finished during this call." + }, + "session_id": { + "type": "string", + "description": "Session identifier to pass to write_stdin when the process is still running." + }, + "original_token_count": { + "type": "number", + "description": "Approximate token count before output truncation." + }, + "output": { + "type": "string", + "description": "Command output text, possibly truncated." + } + }, + "required": ["wall_time_seconds", "output"], + "additionalProperties": false + }) +} #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum ShellCommandBackendConfig { Classic, @@ -479,6 +513,7 @@ fn create_exec_command_tool(allow_login_shell: bool, request_permission_enabled: required: Some(vec!["cmd".to_string()]), additional_properties: Some(false.into()), }, + output_schema: Some(unified_exec_output_schema()), }) } @@ -526,6 +561,7 @@ fn create_write_stdin_tool() -> ToolSpec { required: Some(vec!["session_id".to_string()]), additional_properties: Some(false.into()), }, + output_schema: Some(unified_exec_output_schema()), }) } @@ -579,6 +615,7 @@ Examples of valid command strings: required: Some(vec!["command".to_string()]), additional_properties: Some(false.into()), }, + output_schema: None, }) } @@ -646,6 +683,7 @@ Examples of valid command strings: required: Some(vec!["command".to_string()]), additional_properties: Some(false.into()), }, + output_schema: None, }) } @@ -668,6 +706,7 @@ fn create_view_image_tool() -> ToolSpec { required: Some(vec!["path".to_string()]), additional_properties: Some(false.into()), }, + output_schema: None, }) } @@ -793,6 +832,7 @@ fn create_spawn_agent_tool(config: &ToolsConfig) -> ToolSpec { required: None, additional_properties: Some(false.into()), }, + output_schema: None, }) } @@ -869,6 +909,7 @@ fn create_spawn_agents_on_csv_tool() -> ToolSpec { required: Some(vec!["csv_path".to_string(), "instruction".to_string()]), additional_properties: Some(false.into()), }, + output_schema: None, }) } @@ -918,6 +959,7 @@ fn create_report_agent_job_result_tool() -> ToolSpec { ]), additional_properties: Some(false.into()), }, + output_schema: None, }) } @@ -960,6 +1002,7 @@ fn create_send_input_tool() -> ToolSpec { required: Some(vec!["id".to_string()]), additional_properties: Some(false.into()), }, + output_schema: None, }) } @@ -983,6 +1026,7 @@ fn create_resume_agent_tool() -> ToolSpec { required: Some(vec!["id".to_string()]), additional_properties: Some(false.into()), }, + output_schema: None, }) } @@ -1017,6 +1061,7 @@ fn create_wait_tool() -> ToolSpec { required: Some(vec!["ids".to_string()]), additional_properties: Some(false.into()), }, + output_schema: None, }) } @@ -1102,6 +1147,7 @@ fn create_request_user_input_tool( required: Some(vec!["questions".to_string()]), additional_properties: Some(false.into()), }, + output_schema: None, }) } @@ -1126,6 +1172,7 @@ fn create_request_permissions_tool() -> ToolSpec { required: Some(vec!["permissions".to_string()]), additional_properties: Some(false.into()), }, + output_schema: None, }) } @@ -1148,6 +1195,7 @@ fn create_close_agent_tool() -> ToolSpec { required: Some(vec!["id".to_string()]), additional_properties: Some(false.into()), }, + output_schema: None, }) } @@ -1215,6 +1263,7 @@ fn create_test_sync_tool() -> ToolSpec { required: None, additional_properties: Some(false.into()), }, + output_schema: None, }) } @@ -1266,6 +1315,7 @@ fn create_grep_files_tool() -> ToolSpec { required: Some(vec!["pattern".to_string()]), additional_properties: Some(false.into()), }, + output_schema: None, }) } @@ -1311,6 +1361,7 @@ fn create_search_tool_bm25_tool(app_tools: &HashMap) -> ToolSp required: Some(vec!["query".to_string()]), additional_properties: Some(false.into()), }, + output_schema: None, }) } @@ -1414,6 +1465,7 @@ fn create_read_file_tool() -> ToolSpec { required: Some(vec!["file_path".to_string()]), additional_properties: Some(false.into()), }, + output_schema: None, }) } @@ -1460,6 +1512,7 @@ fn create_list_dir_tool() -> ToolSpec { required: Some(vec!["dir_path".to_string()]), additional_properties: Some(false.into()), }, + output_schema: None, }) } @@ -1534,6 +1587,7 @@ fn create_js_repl_reset_tool() -> ToolSpec { required: None, additional_properties: Some(false.into()), }, + output_schema: None, }) } @@ -1549,7 +1603,7 @@ source: /[\s\S]+/ enabled_tool_names.join(", ") }; let description = format!( - "Runs JavaScript in a Node-backed `node:vm` context. This is a freeform tool: send raw JavaScript source text (no JSON/quotes/markdown fences). Direct tool calls remain available while `code_mode` is enabled. Inside JavaScript, import nested tools from `tools.js`, for example `import {{ exec_command }} from \"tools.js\"` or `import {{ tools }} from \"tools.js\"`. `tools[name]` and identifier wrappers like `await shell(args)` remain available for compatibility when the tool name is a valid JS identifier. Nested tool calls resolve to arrays of content items. Function tools require JSON object arguments. Freeform tools require raw strings. Use synchronous `add_content(value)` with a content item or content-item array, including `add_content(await exec_command(...))`, to return the same content items a direct tool call would expose to the model. Only content passed to `add_content(value)` is surfaced back to the model. Enabled nested tools: {enabled_list}." + "Runs JavaScript in a Node-backed `node:vm` context. This is a freeform tool: send raw JavaScript source text (no JSON/quotes/markdown fences). Direct tool calls remain available while `code_mode` is enabled. Inside JavaScript, import nested tools from `tools.js`, for example `import {{ exec_command }} from \"tools.js\"` or `import {{ tools }} from \"tools.js\"`. `tools[name]` and identifier wrappers like `await shell(args)` remain available for compatibility when the tool name is a valid JS identifier. Nested tool calls resolve to their code-mode result values. Function tools require JSON object arguments. Freeform tools require raw strings. Use synchronous `add_content(value)` with a content item, content-item array, or string. Structured nested-tool results should be converted to text first, for example with `JSON.stringify(...)`. Only content passed to `add_content(value)` is surfaced back to the model. Enabled nested tools: {enabled_list}." ); ToolSpec::Freeform(FreeformTool { @@ -1594,6 +1648,7 @@ fn create_list_mcp_resources_tool() -> ToolSpec { required: None, additional_properties: Some(false.into()), }, + output_schema: None, }) } @@ -1628,6 +1683,7 @@ fn create_list_mcp_resource_templates_tool() -> ToolSpec { required: None, additional_properties: Some(false.into()), }, + output_schema: None, }) } @@ -1664,6 +1720,7 @@ fn create_read_mcp_resource_tool() -> ToolSpec { required: Some(vec!["server".to_string(), "uri".to_string()]), additional_properties: Some(false.into()), }, + output_schema: None, }) } @@ -1726,6 +1783,7 @@ pub(crate) fn mcp_tool_to_openai_tool( description: description.map(Into::into).unwrap_or_default(), strict: false, parameters: input_schema, + output_schema: None, }) } @@ -1739,6 +1797,7 @@ fn dynamic_tool_to_openai_tool( description: tool.description.clone(), strict: false, parameters: input_schema, + output_schema: None, }) } @@ -3278,6 +3337,7 @@ mod tests { }, description: "Do something cool".to_string(), strict: false, + output_schema: None, }) ); } @@ -3516,6 +3576,7 @@ mod tests { }, description: "Search docs".to_string(), strict: false, + output_schema: None, }) ); } @@ -3567,6 +3628,7 @@ mod tests { }, description: "Pagination".to_string(), strict: false, + output_schema: None, }) ); } @@ -3622,6 +3684,7 @@ mod tests { }, description: "Tags".to_string(), strict: false, + output_schema: None, }) ); } @@ -3675,6 +3738,7 @@ mod tests { }, description: "AnyOf Value".to_string(), strict: false, + output_schema: None, }) ); } @@ -3933,6 +3997,7 @@ Examples of valid command strings: }, description: "Do something cool".to_string(), strict: false, + output_schema: None, }) ); } @@ -3950,6 +4015,7 @@ Examples of valid command strings: required: None, additional_properties: None, }, + output_schema: None, })]; let responses_json = create_tools_json_for_responses_api(&tools).unwrap(); diff --git a/codex-rs/core/tests/suite/code_mode.rs b/codex-rs/core/tests/suite/code_mode.rs index 55e23ce1c75..a77ccf5a14d 100644 --- a/codex-rs/core/tests/suite/code_mode.rs +++ b/codex-rs/core/tests/suite/code_mode.rs @@ -14,7 +14,7 @@ use core_test_support::skip_if_no_network; use core_test_support::test_codex::TestCodex; use core_test_support::test_codex::test_codex; use pretty_assertions::assert_eq; -use regex_lite::Regex; +use serde_json::Value; use std::fs; use wiremock::MockServer; @@ -75,7 +75,7 @@ async fn code_mode_can_return_exec_command_output() -> Result<()> { r#" import { exec_command } from "tools.js"; -add_content(await exec_command({ cmd: "printf code_mode_exec_marker" })); +add_content(JSON.stringify(await exec_command({ cmd: "printf code_mode_exec_marker" }))); "#, false, ) @@ -88,19 +88,20 @@ add_content(await exec_command({ cmd: "printf code_mode_exec_marker" })); Some(false), "code_mode call failed unexpectedly: {output}" ); - let regex = Regex::new( - r#"(?ms)^Chunk ID: [[:xdigit:]]+ -Wall time: [0-9]+(?:\.[0-9]+)? seconds -Process exited with code 0 -Original token count: [0-9]+ -Output: -code_mode_exec_marker -?$"#, - )?; + let parsed: Value = serde_json::from_str(&output)?; assert!( - regex.is_match(&output), - "expected exec_command output envelope to match regex, got: {output}" + parsed + .get("chunk_id") + .and_then(Value::as_str) + .is_some_and(|chunk_id| !chunk_id.is_empty()) ); + assert_eq!( + parsed.get("output").and_then(Value::as_str), + Some("code_mode_exec_marker"), + ); + assert_eq!(parsed.get("exit_code").and_then(Value::as_i64), Some(0)); + assert!(parsed.get("wall_time_seconds").is_some()); + assert!(parsed.get("session_id").is_none()); Ok(()) } diff --git a/codex-rs/core/tests/suite/view_image.rs b/codex-rs/core/tests/suite/view_image.rs index cf06b872edc..7a585137317 100644 --- a/codex-rs/core/tests/suite/view_image.rs +++ b/codex-rs/core/tests/suite/view_image.rs @@ -41,6 +41,10 @@ use serde_json::Value; use tokio::time::Duration; use wiremock::BodyPrintLimit; use wiremock::MockServer; +#[cfg(not(debug_assertions))] +use wiremock::ResponseTemplate; +#[cfg(not(debug_assertions))] +use wiremock::matchers::body_string_contains; fn image_messages(body: &Value) -> Vec<&Value> { body.get("input") diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index f8948a772e0..ca15ea951c9 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -206,7 +206,7 @@ pub enum ResponseInputItem { }, McpToolCallOutput { call_id: String, - result: Result, + output: McpToolOutput, }, CustomToolCallOutput { call_id: String, @@ -843,14 +843,8 @@ impl From for ResponseItem { ResponseInputItem::FunctionCallOutput { call_id, output } => { Self::FunctionCallOutput { call_id, output } } - ResponseInputItem::McpToolCallOutput { call_id, result } => { - let output = match result { - Ok(result) => FunctionCallOutputPayload::from(&result), - Err(tool_call_err) => FunctionCallOutputPayload { - body: FunctionCallOutputBody::Text(format!("err: {tool_call_err:?}")), - success: Some(false), - }, - }; + ResponseInputItem::McpToolCallOutput { call_id, output } => { + let output = output.into_function_call_output_payload(); Self::FunctionCallOutput { call_id, output } } ResponseInputItem::CustomToolCallOutput { call_id, output } => { @@ -1190,25 +1184,59 @@ impl<'de> Deserialize<'de> for FunctionCallOutputPayload { } } -impl From<&CallToolResult> for FunctionCallOutputPayload { - fn from(call_tool_result: &CallToolResult) -> Self { - let CallToolResult { +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct McpToolOutput { + pub content: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub structured_content: Option, + pub success: bool, +} + +impl McpToolOutput { + pub fn from_result(result: Result) -> Self { + match result { + Ok(result) => Self::from(&result), + Err(error) => Self::from_error_text(error), + } + } + + pub fn from_error_text(text: String) -> Self { + Self { + content: vec![serde_json::json!({ + "type": "text", + "text": text, + })], + structured_content: None, + success: false, + } + } + + pub fn into_call_tool_result(self) -> CallToolResult { + let Self { content, structured_content, - is_error, - meta: _, - } = call_tool_result; + success, + } = self; - let is_success = is_error != &Some(true); + CallToolResult { + content, + structured_content, + is_error: Some(!success), + meta: None, + } + } - if let Some(structured_content) = structured_content + pub fn as_function_call_output_payload(&self) -> FunctionCallOutputPayload { + if let Some(structured_content) = &self.structured_content && !structured_content.is_null() { match serde_json::to_string(structured_content) { Ok(serialized_structured_content) => { return FunctionCallOutputPayload { body: FunctionCallOutputBody::Text(serialized_structured_content), - success: Some(is_success), + success: Some(self.success), }; } Err(err) => { @@ -1220,7 +1248,7 @@ impl From<&CallToolResult> for FunctionCallOutputPayload { } } - let serialized_content = match serde_json::to_string(content) { + let serialized_content = match serde_json::to_string(&self.content) { Ok(serialized_content) => serialized_content, Err(err) => { return FunctionCallOutputPayload { @@ -1230,7 +1258,7 @@ impl From<&CallToolResult> for FunctionCallOutputPayload { } }; - let content_items = convert_mcp_content_to_items(content); + let content_items = convert_mcp_content_to_items(&self.content); let body = match content_items { Some(content_items) => FunctionCallOutputBody::ContentItems(content_items), @@ -1239,7 +1267,28 @@ impl From<&CallToolResult> for FunctionCallOutputPayload { FunctionCallOutputPayload { body, - success: Some(is_success), + success: Some(self.success), + } + } + + pub fn into_function_call_output_payload(self) -> FunctionCallOutputPayload { + self.as_function_call_output_payload() + } +} + +impl From<&CallToolResult> for McpToolOutput { + fn from(call_tool_result: &CallToolResult) -> Self { + let CallToolResult { + content, + structured_content, + is_error, + meta: _, + } = call_tool_result; + + Self { + content: content.clone(), + structured_content: structured_content.clone(), + success: is_error != &Some(true), } } } @@ -1833,7 +1882,7 @@ mod tests { meta: None, }; - let payload = FunctionCallOutputPayload::from(&call_tool_result); + let payload = McpToolOutput::from(&call_tool_result).into_function_call_output_payload(); assert_eq!(payload.success, Some(true)); let Some(items) = payload.content_items() else { panic!("expected content items"); @@ -1900,7 +1949,7 @@ mod tests { meta: None, }; - let payload = FunctionCallOutputPayload::from(&call_tool_result); + let payload = McpToolOutput::from(&call_tool_result).into_function_call_output_payload(); let Some(items) = payload.content_items() else { panic!("expected content items"); }; diff --git a/codex-rs/state/src/runtime/agent_jobs.rs b/codex-rs/state/src/runtime/agent_jobs.rs index c6856059457..3f5526c58dd 100644 --- a/codex-rs/state/src/runtime/agent_jobs.rs +++ b/codex-rs/state/src/runtime/agent_jobs.rs @@ -435,10 +435,13 @@ WHERE job_id = ? AND item_id = ? AND status = ? r#" UPDATE agent_job_items SET + status = ?, result_json = ?, reported_at = ?, + completed_at = ?, updated_at = ?, - last_error = NULL + last_error = NULL, + assigned_thread_id = NULL WHERE job_id = ? AND item_id = ? @@ -446,9 +449,11 @@ WHERE AND assigned_thread_id = ? "#, ) + .bind(AgentJobItemStatus::Completed.as_str()) .bind(serialized) .bind(now) .bind(now) + .bind(now) .bind(job_id) .bind(item_id) .bind(AgentJobItemStatus::Running.as_str()) @@ -560,3 +565,120 @@ WHERE job_id = ? }) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::runtime::test_support::unique_temp_dir; + use pretty_assertions::assert_eq; + use serde_json::json; + + async fn create_running_single_item_job( + runtime: &StateRuntime, + ) -> anyhow::Result<(String, String, String)> { + let job_id = "job-1".to_string(); + let item_id = "item-1".to_string(); + let thread_id = "thread-1".to_string(); + runtime + .create_agent_job( + &AgentJobCreateParams { + id: job_id.clone(), + name: "test-job".to_string(), + instruction: "Return a result".to_string(), + auto_export: true, + max_runtime_seconds: None, + output_schema_json: None, + input_headers: vec!["path".to_string()], + input_csv_path: "/tmp/in.csv".to_string(), + output_csv_path: "/tmp/out.csv".to_string(), + }, + &[AgentJobItemCreateParams { + item_id: item_id.clone(), + row_index: 0, + source_id: None, + row_json: json!({"path":"file-1"}), + }], + ) + .await?; + runtime.mark_agent_job_running(job_id.as_str()).await?; + let marked_running = runtime + .mark_agent_job_item_running_with_thread( + job_id.as_str(), + item_id.as_str(), + thread_id.as_str(), + ) + .await?; + assert!(marked_running); + Ok((job_id, item_id, thread_id)) + } + + #[tokio::test] + async fn report_agent_job_item_result_completes_item_atomically() -> anyhow::Result<()> { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home, "test-provider".to_string()).await?; + let (job_id, item_id, thread_id) = create_running_single_item_job(runtime.as_ref()).await?; + + let accepted = runtime + .report_agent_job_item_result( + job_id.as_str(), + item_id.as_str(), + thread_id.as_str(), + &json!({"ok": true}), + ) + .await?; + assert!(accepted); + + let item = runtime + .get_agent_job_item(job_id.as_str(), item_id.as_str()) + .await? + .expect("job item should exist"); + assert_eq!(item.status, AgentJobItemStatus::Completed); + assert_eq!(item.result_json, Some(json!({"ok": true}))); + assert_eq!(item.assigned_thread_id, None); + assert_eq!(item.last_error, None); + assert!(item.reported_at.is_some()); + assert!(item.completed_at.is_some()); + let progress = runtime.get_agent_job_progress(job_id.as_str()).await?; + assert_eq!( + progress, + AgentJobProgress { + total_items: 1, + pending_items: 0, + running_items: 0, + completed_items: 1, + failed_items: 0, + } + ); + Ok(()) + } + + #[tokio::test] + async fn report_agent_job_item_result_rejects_late_reports() -> anyhow::Result<()> { + let codex_home = unique_temp_dir(); + let runtime = StateRuntime::init(codex_home, "test-provider".to_string()).await?; + let (job_id, item_id, thread_id) = create_running_single_item_job(runtime.as_ref()).await?; + + let marked_failed = runtime + .mark_agent_job_item_failed(job_id.as_str(), item_id.as_str(), "missing report") + .await?; + assert!(marked_failed); + let accepted = runtime + .report_agent_job_item_result( + job_id.as_str(), + item_id.as_str(), + thread_id.as_str(), + &json!({"late": true}), + ) + .await?; + assert!(!accepted); + + let item = runtime + .get_agent_job_item(job_id.as_str(), item_id.as_str()) + .await? + .expect("job item should exist"); + assert_eq!(item.status, AgentJobItemStatus::Failed); + assert_eq!(item.result_json, None); + assert_eq!(item.last_error, Some("missing report".to_string())); + Ok(()) + } +} diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 676ce4a95b5..7fdc294dac2 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -1369,6 +1369,13 @@ impl ChatWidget { category: crate::app_event::FeedbackCategory, include_logs: bool, ) { + if let Some(chatgpt_user_id) = self + .auth_manager + .auth_cached() + .and_then(|auth| auth.get_chatgpt_user_id()) + { + tracing::info!(target: "feedback_tags", chatgpt_user_id); + } let snapshot = self.feedback.snapshot(self.thread_id); self.show_feedback_note(category, include_logs, snapshot); } @@ -1403,6 +1410,13 @@ impl ChatWidget { } pub(crate) fn open_feedback_consent(&mut self, category: crate::app_event::FeedbackCategory) { + if let Some(chatgpt_user_id) = self + .auth_manager + .auth_cached() + .and_then(|auth| auth.get_chatgpt_user_id()) + { + tracing::info!(target: "feedback_tags", chatgpt_user_id); + } let snapshot = self.feedback.snapshot(self.thread_id); let params = crate::bottom_pane::feedback_upload_consent_params( self.app_event_tx.clone(), diff --git a/codex-rs/utils/pty/src/tests.rs b/codex-rs/utils/pty/src/tests.rs index 2efb0f1ae6f..c2856a95c68 100644 --- a/codex-rs/utils/pty/src/tests.rs +++ b/codex-rs/utils/pty/src/tests.rs @@ -56,7 +56,13 @@ fn echo_sleep_command(marker: &str) -> String { } fn split_stdout_stderr_command() -> String { - "printf 'split-out\\n'; printf 'split-err\\n' >&2".to_string() + if cfg!(windows) { + // Keep this in cmd.exe syntax so the test does not depend on a runner-local + // PowerShell/Python setup just to produce deterministic split output. + "(echo split-out)&(>&2 echo split-err)".to_string() + } else { + "printf 'split-out\\n'; printf 'split-err\\n' >&2".to_string() + } } async fn collect_split_output(mut output_rx: tokio::sync::mpsc::Receiver>) -> Vec { @@ -418,21 +424,7 @@ async fn pipe_drains_stderr_without_stdout_activity() -> anyhow::Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn pipe_process_can_expose_split_stdout_and_stderr() -> anyhow::Result<()> { let env_map: HashMap = std::env::vars().collect(); - let (program, args) = if cfg!(windows) { - let Some(python) = find_python() else { - eprintln!("python not found; skipping pipe_process_can_expose_split_stdout_and_stderr"); - return Ok(()); - }; - ( - python, - vec![ - "-c".to_string(), - "import sys; sys.stdout.buffer.write(b'split-out\\n'); sys.stdout.buffer.flush(); sys.stderr.buffer.write(b'split-err\\n'); sys.stderr.buffer.flush()".to_string(), - ], - ) - } else { - shell_command(&split_stdout_stderr_command()) - }; + let (program, args) = shell_command(&split_stdout_stderr_command()); let spawned = spawn_pipe_process_no_stdin(&program, &args, Path::new("."), &env_map, &None).await?; let SpawnedProcess { @@ -457,8 +449,19 @@ async fn pipe_process_can_expose_split_stdout_and_stderr() -> anyhow::Result<()> .await .map_err(|_| anyhow::anyhow!("timed out waiting to drain split stderr"))??; - assert_eq!(stdout, b"split-out\n".to_vec()); - assert_eq!(stderr, b"split-err\n".to_vec()); + let expected_stdout = if cfg!(windows) { + b"split-out\r\n".to_vec() + } else { + b"split-out\n".to_vec() + }; + let expected_stderr = if cfg!(windows) { + b"split-err\r\n".to_vec() + } else { + b"split-err\n".to_vec() + }; + + assert_eq!(stdout, expected_stdout); + assert_eq!(stderr, expected_stderr); assert_eq!(code, 0); Ok(()) diff --git a/sdk/python/README.md b/sdk/python/README.md new file mode 100644 index 00000000000..1fa2e5a882b --- /dev/null +++ b/sdk/python/README.md @@ -0,0 +1,79 @@ +# Codex App Server Python SDK (Experimental) + +Experimental Python SDK for `codex app-server` JSON-RPC v2 over stdio, with a small default surface optimized for real scripts and apps. + +The generated wire-model layer is currently sourced from the bundled v2 schema and exposed as Pydantic models with snake_case Python fields that serialize back to the app-server’s camelCase wire format. + +## Install + +```bash +cd sdk/python +python -m pip install -e . +``` + +## Quickstart + +```python +from codex_app_server import Codex, TextInput + +with Codex() as codex: + thread = codex.thread_start(model="gpt-5") + result = thread.turn(TextInput("Say hello in one sentence.")).run() + print(result.text) +``` + +## Docs map + +- Golden path tutorial: `docs/getting-started.md` +- API reference (signatures + behavior): `docs/api-reference.md` +- Common decisions and pitfalls: `docs/faq.md` +- Runnable examples index: `examples/README.md` +- Jupyter walkthrough notebook: `notebooks/sdk_walkthrough.ipynb` + +## Examples + +Start here: + +```bash +cd sdk/python +python examples/01_quickstart_constructor/sync.py +python examples/01_quickstart_constructor/async.py +``` + +## Bundled runtime binaries (out of the box) + +The SDK ships with platform-specific bundled binaries, so end users do not need updater scripts. + +Runtime binary source (single source, no fallback): + +- `src/codex_app_server/bin/darwin-arm64/codex` +- `src/codex_app_server/bin/darwin-x64/codex` +- `src/codex_app_server/bin/linux-arm64/codex` +- `src/codex_app_server/bin/linux-x64/codex` +- `src/codex_app_server/bin/windows-arm64/codex.exe` +- `src/codex_app_server/bin/windows-x64/codex.exe` + +## Maintainer workflow (refresh binaries/types) + +```bash +cd sdk/python +python scripts/update_sdk_artifacts.py --channel stable --bundle-all-platforms +# or +python scripts/update_sdk_artifacts.py --channel alpha --bundle-all-platforms +``` + +This refreshes all bundled OS/arch binaries and regenerates protocol-derived Python types. + +## Compatibility and versioning + +- Package: `codex-app-server-sdk` +- Current SDK version in this repo: `0.2.0` +- Python: `>=3.10` +- Target protocol: Codex `app-server` JSON-RPC v2 +- Recommendation: keep SDK and `codex` CLI reasonably up to date together + +## Notes + +- `Codex()` is eager and performs startup + `initialize` in the constructor. +- Use context managers (`with Codex() as codex:`) to ensure shutdown. +- For transient overload, use `codex_app_server.retry.retry_on_overload`. diff --git a/sdk/python/docs/faq.md b/sdk/python/docs/faq.md new file mode 100644 index 00000000000..410c210b277 --- /dev/null +++ b/sdk/python/docs/faq.md @@ -0,0 +1,65 @@ +# FAQ + +## Thread vs turn + +- A `Thread` is conversation state. +- A `Turn` is one model execution inside that thread. +- Multi-turn chat means multiple turns on the same `Thread`. + +## `run()` vs `stream()` + +- `Turn.run()` is the easiest path. It consumes events until completion and returns `TurnResult`. +- `Turn.stream()` yields raw notifications (`Notification`) so you can react event-by-event. + +Choose `run()` for most apps. Choose `stream()` for progress UIs, custom timeout logic, or custom parsing. + +## Sync vs async clients + +- `Codex` is the minimal sync SDK and best default. +- `AsyncAppServerClient` wraps the sync transport with `asyncio.to_thread(...)` for async-friendly call sites. + +If your app is not already async, stay with `Codex`. + +## `thread(...)` vs `thread_resume(...)` + +- `codex.thread(thread_id)` only binds a local helper to an existing thread ID. +- `codex.thread_resume(thread_id, ...)` performs a `thread/resume` RPC and can apply overrides (model, instructions, sandbox, etc.). + +Use `thread(...)` for simple continuation. Use `thread_resume(...)` when you need explicit resume semantics or override fields. + +## Why does constructor fail? + +`Codex()` is eager: it starts transport and calls `initialize` in `__init__`. + +Common causes: + +- bundled runtime binary missing for your OS/arch under `src/codex_app_server/bin/*` +- local auth/session is missing +- incompatible/old app-server + +Maintainers can refresh bundled binaries with: + +```bash +cd sdk/python +python scripts/update_sdk_artifacts.py --channel stable --bundle-all-platforms +``` + +## Why does a turn "hang"? + +A turn is complete only when `turn/completed` arrives for that turn ID. + +- `run()` waits for this automatically. +- With `stream()`, make sure you keep consuming notifications until completion. + +## How do I retry safely? + +Use `retry_on_overload(...)` for transient overload failures (`ServerBusyError`). + +Do not blindly retry all errors. For `InvalidParamsError` or `MethodNotFoundError`, fix inputs/version compatibility instead. + +## Common pitfalls + +- Starting a new thread for every prompt when you wanted continuity. +- Forgetting to `close()` (or not using `with Codex() as codex:`). +- Ignoring `TurnResult.status` and `TurnResult.error`. +- Mixing SDK input classes with raw dicts incorrectly in minimal API paths. diff --git a/sdk/python/docs/getting-started.md b/sdk/python/docs/getting-started.md new file mode 100644 index 00000000000..a3d52afdf88 --- /dev/null +++ b/sdk/python/docs/getting-started.md @@ -0,0 +1,75 @@ +# Getting Started + +This is the fastest path from install to a multi-turn thread using the minimal SDK surface. + +## 1) Install + +From repo root: + +```bash +cd sdk/python +python -m pip install -e . +``` + +Requirements: + +- Python `>=3.10` +- bundled runtime binary for your platform (shipped in package) +- Local Codex auth/session configured + +## 2) Run your first turn + +```python +from codex_app_server import Codex, TextInput + +with Codex() as codex: + print("Server:", codex.metadata.server_name, codex.metadata.server_version) + + thread = codex.thread_start(model="gpt-5") + result = thread.turn(TextInput("Say hello in one sentence.")).run() + + print("Thread:", result.thread_id) + print("Turn:", result.turn_id) + print("Status:", result.status) + print("Text:", result.text) +``` + +What happened: + +- `Codex()` started and initialized `codex app-server`. +- `thread_start(...)` created a thread. +- `turn(...).run()` consumed events until `turn/completed` and returned a `TurnResult`. + +## 3) Continue the same thread (multi-turn) + +```python +from codex_app_server import Codex, TextInput + +with Codex() as codex: + thread = codex.thread_start(model="gpt-5") + + first = thread.turn(TextInput("Summarize Rust ownership in 2 bullets.")).run() + second = thread.turn(TextInput("Now explain it to a Python developer.")).run() + + print("first:", first.text) + print("second:", second.text) +``` + +## 4) Resume an existing thread + +```python +from codex_app_server import Codex, TextInput + +THREAD_ID = "thr_123" # replace with a real id + +with Codex() as codex: + thread = codex.thread(THREAD_ID) + result = thread.turn(TextInput("Continue where we left off.")).run() + print(result.text) +``` + +## 5) Next stops + +- API surface and signatures: `docs/api-reference.md` +- Common decisions/pitfalls: `docs/faq.md` +- End-to-end runnable examples: `examples/README.md` diff --git a/sdk/python/pyproject.toml b/sdk/python/pyproject.toml new file mode 100644 index 00000000000..2bfc7de2aee --- /dev/null +++ b/sdk/python/pyproject.toml @@ -0,0 +1,63 @@ +[build-system] +requires = ["hatchling>=1.24.0"] +build-backend = "hatchling.build" + +[project] +name = "codex-app-server-sdk" +version = "0.2.0" +description = "Python SDK for Codex app-server v2" +readme = "README.md" +requires-python = ">=3.10" +license = { text = "Apache-2.0" } +authors = [{ name = "OpenClaw Assistant" }] +keywords = ["codex", "json-rpc", "sdk", "llm", "app-server"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Software Development :: Libraries :: Python Modules", +] +dependencies = ["pydantic>=2.12"] + +[project.urls] +Homepage = "https://github.com/openai/codex" +Repository = "https://github.com/openai/codex" +Issues = "https://github.com/openai/codex/issues" + +[project.optional-dependencies] +dev = ["pytest>=8.0", "datamodel-code-generator==0.31.2"] + +[tool.hatch.build] +exclude = [ + ".venv/**", + ".venv2/**", + ".pytest_cache/**", + "dist/**", + "build/**", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/codex_app_server"] +include = [ + "src/codex_app_server/bin/**", + "src/codex_app_server/py.typed", +] + +[tool.hatch.build.targets.sdist] +include = [ + "src/codex_app_server/**", + "README.md", + "CHANGELOG.md", + "CONTRIBUTING.md", + "RELEASE_CHECKLIST.md", + "pyproject.toml", +] + +[tool.pytest.ini_options] +addopts = "-q" +testpaths = ["tests"] diff --git a/sdk/python/scripts/update_sdk_artifacts.py b/sdk/python/scripts/update_sdk_artifacts.py new file mode 100755 index 00000000000..11521caee3d --- /dev/null +++ b/sdk/python/scripts/update_sdk_artifacts.py @@ -0,0 +1,741 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import importlib +import json +import platform +import re +import shutil +import stat +import subprocess +import sys +import tarfile +import tempfile +import types +import typing +import urllib.request +import zipfile +from dataclasses import dataclass +from pathlib import Path +from typing import Any, get_args, get_origin + + +def repo_root() -> Path: + return Path(__file__).resolve().parents[3] + + +def sdk_root() -> Path: + return repo_root() / "sdk" / "python" + + +def schema_bundle_path() -> Path: + return ( + repo_root() + / "codex-rs" + / "app-server-protocol" + / "schema" + / "json" + / "codex_app_server_protocol.v2.schemas.json" + ) + + +def schema_root_dir() -> Path: + return repo_root() / "codex-rs" / "app-server-protocol" / "schema" / "json" + + +def _is_windows() -> bool: + return platform.system().lower().startswith("win") + + +def pinned_bin_path() -> Path: + name = "codex.exe" if _is_windows() else "codex" + return sdk_root() / "bin" / name + + +def bundled_platform_bin_path(platform_key: str) -> Path: + exe = "codex.exe" if platform_key.startswith("windows") else "codex" + return sdk_root() / "src" / "codex_app_server" / "bin" / platform_key / exe + + +PLATFORMS: dict[str, tuple[list[str], list[str]]] = { + "darwin-arm64": (["darwin", "apple-darwin", "macos"], ["aarch64", "arm64"]), + "darwin-x64": (["darwin", "apple-darwin", "macos"], ["x86_64", "amd64", "x64"]), + "linux-arm64": (["linux", "unknown-linux", "musl", "gnu"], ["aarch64", "arm64"]), + "linux-x64": (["linux", "unknown-linux", "musl", "gnu"], ["x86_64", "amd64", "x64"]), + "windows-arm64": (["windows", "pc-windows", "win", "msvc", "gnu"], ["aarch64", "arm64"]), + "windows-x64": (["windows", "pc-windows", "win", "msvc", "gnu"], ["x86_64", "amd64", "x64"]), +} + + +def run(cmd: list[str], cwd: Path) -> None: + subprocess.run(cmd, cwd=str(cwd), check=True) + + +def run_python_module(module: str, args: list[str], cwd: Path) -> None: + run([sys.executable, "-m", module, *args], cwd) + + +def platform_tokens() -> tuple[list[str], list[str]]: + sys_name = platform.system().lower() + machine = platform.machine().lower() + + if sys_name == "darwin": + os_tokens = ["darwin", "apple-darwin", "macos"] + elif sys_name == "linux": + os_tokens = ["linux", "unknown-linux", "musl", "gnu"] + elif sys_name.startswith("win"): + os_tokens = ["windows", "pc-windows", "win", "msvc", "gnu"] + else: + raise RuntimeError(f"Unsupported OS: {sys_name}") + + if machine in {"arm64", "aarch64"}: + arch_tokens = ["aarch64", "arm64"] + elif machine in {"x86_64", "amd64"}: + arch_tokens = ["x86_64", "amd64", "x64"] + else: + raise RuntimeError(f"Unsupported architecture: {machine}") + + return os_tokens, arch_tokens + + +def pick_release(channel: str) -> dict[str, Any]: + releases = json.loads( + subprocess.check_output(["gh", "api", "repos/openai/codex/releases?per_page=50"], text=True) + ) + if channel == "stable": + candidates = [r for r in releases if not r.get("prerelease") and not r.get("draft")] + else: + candidates = [r for r in releases if r.get("prerelease") and not r.get("draft")] + if not candidates: + raise RuntimeError(f"No {channel} release found") + return candidates[0] + + +def pick_asset(release: dict[str, Any], os_tokens: list[str], arch_tokens: list[str]) -> dict[str, Any]: + scored: list[tuple[int, dict[str, Any]]] = [] + for asset in release.get("assets", []): + name = (asset.get("name") or "").lower() + + # Accept only primary codex cli artifacts. + if not (name.startswith("codex-") or name == "codex"): + continue + if name.startswith("codex-responses") or name.startswith("codex-command-runner") or name.startswith("codex-windows-sandbox") or name.startswith("codex-npm"): + continue + if not (name.endswith(".tar.gz") or name.endswith(".zip")): + continue + + os_score = sum(1 for t in os_tokens if t in name) + arch_score = sum(1 for t in arch_tokens if t in name) + if os_score == 0 or arch_score == 0: + continue + + score = os_score * 10 + arch_score + scored.append((score, asset)) + + if not scored: + raise RuntimeError("Could not find matching codex CLI asset for this platform") + + scored.sort(key=lambda x: x[0], reverse=True) + return scored[0][1] + + +def download(url: str, out: Path) -> None: + req = urllib.request.Request(url, headers={"User-Agent": "codex-python-sdk-updater"}) + with urllib.request.urlopen(req) as resp, out.open("wb") as f: + shutil.copyfileobj(resp, f) + + +def extract_codex_binary(archive: Path, out_bin: Path) -> None: + with tempfile.TemporaryDirectory() as td: + tmp = Path(td) + if archive.name.endswith(".tar.gz"): + with tarfile.open(archive, "r:gz") as tar: + tar.extractall(tmp) + elif archive.name.endswith(".zip"): + with zipfile.ZipFile(archive) as zf: + zf.extractall(tmp) + else: + raise RuntimeError(f"Unsupported archive format: {archive}") + + preferred_names = {"codex.exe", "codex"} + candidates = [ + p for p in tmp.rglob("*") if p.is_file() and (p.name.lower() in preferred_names or p.name.lower().startswith("codex-")) + ] + if not candidates: + raise RuntimeError("No codex binary found in release archive") + + candidates.sort(key=lambda p: (p.name.lower() not in preferred_names, p.name.lower())) + + out_bin.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(candidates[0], out_bin) + if not _is_windows(): + out_bin.chmod(out_bin.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) + + +def _download_asset_to_binary(release: dict[str, Any], os_tokens: list[str], arch_tokens: list[str], out_bin: Path) -> None: + asset = pick_asset(release, os_tokens, arch_tokens) + print(f"Asset: {asset.get('name')} -> {out_bin}") + with tempfile.TemporaryDirectory() as td: + archive = Path(td) / (asset.get("name") or "codex-release.tar.gz") + download(asset["browser_download_url"], archive) + extract_codex_binary(archive, out_bin) + + +def update_binary(channel: str) -> None: + if shutil.which("gh") is None: + raise RuntimeError("GitHub CLI (`gh`) is required to download release binaries") + + release = pick_release(channel) + os_tokens, arch_tokens = platform_tokens() + print(f"Release: {release.get('tag_name')} ({channel})") + + # refresh current platform in bundled runtime location + current_key = next((k for k, v in PLATFORMS.items() if v == (os_tokens, arch_tokens)), None) + out = bundled_platform_bin_path(current_key) if current_key else pinned_bin_path() + _download_asset_to_binary(release, os_tokens, arch_tokens, out) + print(f"Pinned binary updated: {out}") + + +def bundle_all_platform_binaries(channel: str) -> None: + if shutil.which("gh") is None: + raise RuntimeError("GitHub CLI (`gh`) is required to download release binaries") + + release = pick_release(channel) + print(f"Release: {release.get('tag_name')} ({channel})") + for platform_key, (os_tokens, arch_tokens) in PLATFORMS.items(): + _download_asset_to_binary(release, os_tokens, arch_tokens, bundled_platform_bin_path(platform_key)) + print("Bundled all platform binaries.") + + +def _flatten_string_enum_one_of(definition: dict[str, Any]) -> bool: + branches = definition.get("oneOf") + if not isinstance(branches, list) or not branches: + return False + + enum_values: list[str] = [] + for branch in branches: + if not isinstance(branch, dict): + return False + if branch.get("type") != "string": + return False + + enum = branch.get("enum") + if not isinstance(enum, list) or len(enum) != 1 or not isinstance(enum[0], str): + return False + + extra_keys = set(branch) - {"type", "enum", "description", "title"} + if extra_keys: + return False + + enum_values.append(enum[0]) + + description = definition.get("description") + title = definition.get("title") + definition.clear() + definition["type"] = "string" + definition["enum"] = enum_values + if isinstance(description, str): + definition["description"] = description + if isinstance(title, str): + definition["title"] = title + return True + + +def _normalized_schema_bundle_text() -> str: + schema = json.loads(schema_bundle_path().read_text()) + definitions = schema.get("definitions", {}) + if isinstance(definitions, dict): + for definition in definitions.values(): + if isinstance(definition, dict): + _flatten_string_enum_one_of(definition) + return json.dumps(schema, indent=2, sort_keys=True) + "\n" + + +def generate_v2_all() -> None: + out_path = sdk_root() / "src" / "codex_app_server" / "generated" / "v2_all.py" + out_dir = out_path.parent + old_package_dir = out_dir / "v2_all" + if old_package_dir.exists(): + shutil.rmtree(old_package_dir) + out_dir.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory() as td: + normalized_bundle = Path(td) / schema_bundle_path().name + normalized_bundle.write_text(_normalized_schema_bundle_text()) + run_python_module( + "datamodel_code_generator", + [ + "--input", + str(normalized_bundle), + "--input-file-type", + "jsonschema", + "--output", + str(out_path), + "--output-model-type", + "pydantic_v2.BaseModel", + "--target-python-version", + "3.10", + "--snake-case-field", + "--allow-population-by-field-name", + "--use-union-operator", + "--reuse-model", + "--disable-timestamp", + "--use-double-quotes", + ], + cwd=sdk_root(), + ) + _normalize_generated_timestamps(out_path) + +def _notification_specs() -> list[tuple[str, str]]: + server_notifications = json.loads((schema_root_dir() / "ServerNotification.json").read_text()) + one_of = server_notifications.get("oneOf", []) + generated_source = ( + sdk_root() / "src" / "codex_app_server" / "generated" / "v2_all.py" + ).read_text() + + specs: list[tuple[str, str]] = [] + + for variant in one_of: + props = variant.get("properties", {}) + method_meta = props.get("method", {}) + params_meta = props.get("params", {}) + + methods = method_meta.get("enum", []) + if len(methods) != 1: + continue + method = methods[0] + if not isinstance(method, str): + continue + + ref = params_meta.get("$ref") + if not isinstance(ref, str) or not ref.startswith("#/definitions/"): + continue + class_name = ref.split("/")[-1] + if f"class {class_name}(" not in generated_source and f"{class_name} =" not in generated_source: + # Skip schema variants that are not emitted into the generated v2 surface. + continue + specs.append((method, class_name)) + + specs.sort() + return specs + + +def generate_notification_registry() -> None: + out = sdk_root() / "src" / "codex_app_server" / "generated" / "notification_registry.py" + specs = _notification_specs() + class_names = sorted({class_name for _, class_name in specs}) + + lines = [ + "# Auto-generated by scripts/update_sdk_artifacts.py", + "# DO NOT EDIT MANUALLY.", + "", + "from __future__ import annotations", + "", + "from pydantic import BaseModel", + "", + ] + + for class_name in class_names: + lines.append(f"from .v2_all import {class_name}") + lines.extend( + [ + "", + "NOTIFICATION_MODELS: dict[str, type[BaseModel]] = {", + ] + ) + for method, class_name in specs: + lines.append(f' "{method}": {class_name},') + lines.extend(["}", ""]) + + out.write_text("\n".join(lines)) + + +def _normalize_generated_timestamps(root: Path) -> None: + timestamp_re = re.compile(r"^#\s+timestamp:\s+.+$", flags=re.MULTILINE) + py_files = [root] if root.is_file() else sorted(root.rglob("*.py")) + for py_file in py_files: + content = py_file.read_text() + normalized = timestamp_re.sub("# timestamp: ", content) + if normalized != content: + py_file.write_text(normalized) + +FIELD_ANNOTATION_OVERRIDES: dict[str, str] = { + # Keep public API typed without falling back to `Any`. + "config": "JsonObject", + "output_schema": "JsonObject", +} + + +@dataclass(slots=True) +class PublicFieldSpec: + wire_name: str + py_name: str + annotation: str + required: bool + + +def _annotation_to_source(annotation: Any) -> str: + origin = get_origin(annotation) + if origin is typing.Annotated: + return _annotation_to_source(get_args(annotation)[0]) + if origin in (typing.Union, types.UnionType): + parts: list[str] = [] + for arg in get_args(annotation): + rendered = _annotation_to_source(arg) + if rendered not in parts: + parts.append(rendered) + return " | ".join(parts) + if origin is list: + args = get_args(annotation) + item = _annotation_to_source(args[0]) if args else "Any" + return f"list[{item}]" + if origin is dict: + args = get_args(annotation) + key = _annotation_to_source(args[0]) if args else "str" + val = _annotation_to_source(args[1]) if len(args) > 1 else "Any" + return f"dict[{key}, {val}]" + if annotation is Any or annotation is typing.Any: + return "Any" + if annotation is None or annotation is type(None): + return "None" + if isinstance(annotation, type): + if annotation.__module__ == "builtins": + return annotation.__name__ + return annotation.__name__ + return repr(annotation) + + +def _camel_to_snake(name: str) -> str: + head = re.sub(r"(.)([A-Z][a-z]+)", r"\1_\2", name) + return re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", head).lower() + + +def _load_public_fields(module_name: str, class_name: str, *, exclude: set[str] | None = None) -> list[PublicFieldSpec]: + exclude = exclude or set() + module = importlib.import_module(module_name) + model = getattr(module, class_name) + fields: list[PublicFieldSpec] = [] + for name, field in model.model_fields.items(): + if name in exclude: + continue + required = field.is_required() + annotation = _annotation_to_source(field.annotation) + override = FIELD_ANNOTATION_OVERRIDES.get(name) + if override is not None: + annotation = override if required else f"{override} | None" + fields.append( + PublicFieldSpec( + wire_name=name, + py_name=name, + annotation=annotation, + required=required, + ) + ) + return fields + + +def _kw_signature_lines(fields: list[PublicFieldSpec]) -> list[str]: + lines: list[str] = [] + for field in fields: + default = "" if field.required else " = None" + lines.append(f" {field.py_name}: {field.annotation}{default},") + return lines + + +def _model_arg_lines(fields: list[PublicFieldSpec], *, indent: str = " ") -> list[str]: + return [f"{indent}{field.wire_name}={field.py_name}," for field in fields] + + +def _replace_generated_block(source: str, block_name: str, body: str) -> str: + start_tag = f" # BEGIN GENERATED: {block_name}" + end_tag = f" # END GENERATED: {block_name}" + pattern = re.compile( + rf"(?s){re.escape(start_tag)}\n.*?\n{re.escape(end_tag)}" + ) + replacement = f"{start_tag}\n{body.rstrip()}\n{end_tag}" + updated, count = pattern.subn(replacement, source, count=1) + if count != 1: + raise RuntimeError(f"Could not update generated block: {block_name}") + return updated + + +def _render_codex_block( + thread_start_fields: list[PublicFieldSpec], + thread_list_fields: list[PublicFieldSpec], + resume_fields: list[PublicFieldSpec], + fork_fields: list[PublicFieldSpec], +) -> str: + lines = [ + " def thread_start(", + " self,", + " *,", + *_kw_signature_lines(thread_start_fields), + " ) -> Thread:", + " params = ThreadStartParams(", + *_model_arg_lines(thread_start_fields), + " )", + " started = self._client.thread_start(params)", + " return Thread(self._client, started.thread.id)", + "", + " def thread_list(", + " self,", + " *,", + *_kw_signature_lines(thread_list_fields), + " ) -> ThreadListResponse:", + " params = ThreadListParams(", + *_model_arg_lines(thread_list_fields), + " )", + " return self._client.thread_list(params)", + "", + " def thread_resume(", + " self,", + " thread_id: str,", + " *,", + *_kw_signature_lines(resume_fields), + " ) -> Thread:", + " params = ThreadResumeParams(", + " thread_id=thread_id,", + *_model_arg_lines(resume_fields), + " )", + " resumed = self._client.thread_resume(thread_id, params)", + " return Thread(self._client, resumed.thread.id)", + "", + " def thread_fork(", + " self,", + " thread_id: str,", + " *,", + *_kw_signature_lines(fork_fields), + " ) -> Thread:", + " params = ThreadForkParams(", + " thread_id=thread_id,", + *_model_arg_lines(fork_fields), + " )", + " forked = self._client.thread_fork(thread_id, params)", + " return Thread(self._client, forked.thread.id)", + "", + " def thread_archive(self, thread_id: str) -> ThreadArchiveResponse:", + " return self._client.thread_archive(thread_id)", + "", + " def thread_unarchive(self, thread_id: str) -> Thread:", + " unarchived = self._client.thread_unarchive(thread_id)", + " return Thread(self._client, unarchived.thread.id)", + ] + return "\n".join(lines) + + +def _render_async_codex_block( + thread_start_fields: list[PublicFieldSpec], + thread_list_fields: list[PublicFieldSpec], + resume_fields: list[PublicFieldSpec], + fork_fields: list[PublicFieldSpec], +) -> str: + lines = [ + " async def thread_start(", + " self,", + " *,", + *_kw_signature_lines(thread_start_fields), + " ) -> AsyncThread:", + " await self._ensure_initialized()", + " params = ThreadStartParams(", + *_model_arg_lines(thread_start_fields), + " )", + " started = await self._client.thread_start(params)", + " return AsyncThread(self, started.thread.id)", + "", + " async def thread_list(", + " self,", + " *,", + *_kw_signature_lines(thread_list_fields), + " ) -> ThreadListResponse:", + " await self._ensure_initialized()", + " params = ThreadListParams(", + *_model_arg_lines(thread_list_fields), + " )", + " return await self._client.thread_list(params)", + "", + " async def thread_resume(", + " self,", + " thread_id: str,", + " *,", + *_kw_signature_lines(resume_fields), + " ) -> AsyncThread:", + " await self._ensure_initialized()", + " params = ThreadResumeParams(", + " thread_id=thread_id,", + *_model_arg_lines(resume_fields), + " )", + " resumed = await self._client.thread_resume(thread_id, params)", + " return AsyncThread(self, resumed.thread.id)", + "", + " async def thread_fork(", + " self,", + " thread_id: str,", + " *,", + *_kw_signature_lines(fork_fields), + " ) -> AsyncThread:", + " await self._ensure_initialized()", + " params = ThreadForkParams(", + " thread_id=thread_id,", + *_model_arg_lines(fork_fields), + " )", + " forked = await self._client.thread_fork(thread_id, params)", + " return AsyncThread(self, forked.thread.id)", + "", + " async def thread_archive(self, thread_id: str) -> ThreadArchiveResponse:", + " await self._ensure_initialized()", + " return await self._client.thread_archive(thread_id)", + "", + " async def thread_unarchive(self, thread_id: str) -> AsyncThread:", + " await self._ensure_initialized()", + " unarchived = await self._client.thread_unarchive(thread_id)", + " return AsyncThread(self, unarchived.thread.id)", + ] + return "\n".join(lines) + + +def _render_thread_block( + turn_fields: list[PublicFieldSpec], +) -> str: + lines = [ + " def turn(", + " self,", + " input: Input,", + " *,", + *_kw_signature_lines(turn_fields), + " ) -> Turn:", + " wire_input = _to_wire_input(input)", + " params = TurnStartParams(", + " thread_id=self.id,", + " input=wire_input,", + *_model_arg_lines(turn_fields), + " )", + " turn = self._client.turn_start(self.id, wire_input, params=params)", + " return Turn(self._client, self.id, turn.turn.id)", + ] + return "\n".join(lines) + + +def _render_async_thread_block( + turn_fields: list[PublicFieldSpec], +) -> str: + lines = [ + " async def turn(", + " self,", + " input: Input,", + " *,", + *_kw_signature_lines(turn_fields), + " ) -> AsyncTurn:", + " await self._codex._ensure_initialized()", + " wire_input = _to_wire_input(input)", + " params = TurnStartParams(", + " thread_id=self.id,", + " input=wire_input,", + *_model_arg_lines(turn_fields), + " )", + " turn = await self._codex._client.turn_start(", + " self.id,", + " wire_input,", + " params=params,", + " )", + " return AsyncTurn(self._codex, self.id, turn.turn.id)", + ] + return "\n".join(lines) + + +def generate_public_api_flat_methods() -> None: + src_dir = sdk_root() / "src" + public_api_path = src_dir / "codex_app_server" / "public_api.py" + if not public_api_path.exists(): + # PR2 can run codegen before the ergonomic public API layer is added. + return + src_dir_str = str(src_dir) + if src_dir_str not in sys.path: + sys.path.insert(0, src_dir_str) + + thread_start_fields = _load_public_fields( + "codex_app_server.generated.v2_all", + "ThreadStartParams", + ) + thread_list_fields = _load_public_fields( + "codex_app_server.generated.v2_all", + "ThreadListParams", + ) + thread_resume_fields = _load_public_fields( + "codex_app_server.generated.v2_all", + "ThreadResumeParams", + exclude={"thread_id"}, + ) + thread_fork_fields = _load_public_fields( + "codex_app_server.generated.v2_all", + "ThreadForkParams", + exclude={"thread_id"}, + ) + turn_start_fields = _load_public_fields( + "codex_app_server.generated.v2_all", + "TurnStartParams", + exclude={"thread_id", "input"}, + ) + + source = public_api_path.read_text() + source = _replace_generated_block( + source, + "Codex.flat_methods", + _render_codex_block( + thread_start_fields, + thread_list_fields, + thread_resume_fields, + thread_fork_fields, + ), + ) + source = _replace_generated_block( + source, + "AsyncCodex.flat_methods", + _render_async_codex_block( + thread_start_fields, + thread_list_fields, + thread_resume_fields, + thread_fork_fields, + ), + ) + source = _replace_generated_block( + source, + "Thread.flat_methods", + _render_thread_block(turn_start_fields), + ) + source = _replace_generated_block( + source, + "AsyncThread.flat_methods", + _render_async_thread_block(turn_start_fields), + ) + public_api_path.write_text(source) + + +def generate_types() -> None: + # v2_all is the authoritative generated surface. + generate_v2_all() + generate_notification_registry() + generate_public_api_flat_methods() + + +def main() -> None: + parser = argparse.ArgumentParser(description="Single SDK maintenance entrypoint") + parser.add_argument("--channel", choices=["stable", "alpha"], default="stable") + parser.add_argument("--types-only", action="store_true", help="Regenerate types only (skip binary update)") + parser.add_argument( + "--bundle-all-platforms", + action="store_true", + help="Download and bundle codex binaries for all supported OS/arch targets", + ) + args = parser.parse_args() + + if not args.types_only: + if args.bundle_all_platforms: + bundle_all_platform_binaries(args.channel) + else: + update_binary(args.channel) + generate_types() + print("Done.") + + +if __name__ == "__main__": + main() diff --git a/sdk/python/src/codex_app_server/__init__.py b/sdk/python/src/codex_app_server/__init__.py new file mode 100644 index 00000000000..aff63176b9f --- /dev/null +++ b/sdk/python/src/codex_app_server/__init__.py @@ -0,0 +1,10 @@ +from .client import AppServerClient, AppServerConfig +from .errors import AppServerError, JsonRpcError, TransportClosedError + +__all__ = [ + "AppServerClient", + "AppServerConfig", + "AppServerError", + "JsonRpcError", + "TransportClosedError", +] diff --git a/sdk/python/src/codex_app_server/bin/darwin-arm64/codex b/sdk/python/src/codex_app_server/bin/darwin-arm64/codex new file mode 100755 index 00000000000..70c50814c64 Binary files /dev/null and b/sdk/python/src/codex_app_server/bin/darwin-arm64/codex differ diff --git a/sdk/python/src/codex_app_server/bin/darwin-x64/codex b/sdk/python/src/codex_app_server/bin/darwin-x64/codex new file mode 100755 index 00000000000..b7d9086c7af Binary files /dev/null and b/sdk/python/src/codex_app_server/bin/darwin-x64/codex differ diff --git a/sdk/python/src/codex_app_server/bin/linux-arm64/codex b/sdk/python/src/codex_app_server/bin/linux-arm64/codex new file mode 100755 index 00000000000..6eb65043efc Binary files /dev/null and b/sdk/python/src/codex_app_server/bin/linux-arm64/codex differ diff --git a/sdk/python/src/codex_app_server/bin/linux-x64/codex b/sdk/python/src/codex_app_server/bin/linux-x64/codex new file mode 100755 index 00000000000..0b2541a0ce0 Binary files /dev/null and b/sdk/python/src/codex_app_server/bin/linux-x64/codex differ diff --git a/sdk/python/src/codex_app_server/bin/windows-arm64/codex.exe b/sdk/python/src/codex_app_server/bin/windows-arm64/codex.exe new file mode 100755 index 00000000000..457c5c96536 Binary files /dev/null and b/sdk/python/src/codex_app_server/bin/windows-arm64/codex.exe differ diff --git a/sdk/python/src/codex_app_server/bin/windows-x64/codex.exe b/sdk/python/src/codex_app_server/bin/windows-x64/codex.exe new file mode 100755 index 00000000000..31450cb3fdd Binary files /dev/null and b/sdk/python/src/codex_app_server/bin/windows-x64/codex.exe differ diff --git a/sdk/python/src/codex_app_server/client.py b/sdk/python/src/codex_app_server/client.py new file mode 100644 index 00000000000..fa20304cf29 --- /dev/null +++ b/sdk/python/src/codex_app_server/client.py @@ -0,0 +1,521 @@ +from __future__ import annotations + +import json +import os +import subprocess +import threading +import uuid +from collections import deque +from dataclasses import dataclass +from pathlib import Path +from typing import Callable, Iterable, Iterator, TypeVar + +from pydantic import BaseModel + +from .errors import AppServerError, TransportClosedError, map_jsonrpc_error +from .generated.notification_registry import NOTIFICATION_MODELS +from .generated.v2_all import ( + AgentMessageDeltaNotification, + ModelListResponse, + ThreadArchiveResponse, + ThreadCompactStartResponse, + ThreadForkParams as V2ThreadForkParams, + ThreadForkResponse, + ThreadListParams as V2ThreadListParams, + ThreadListResponse, + ThreadReadResponse, + ThreadResumeParams as V2ThreadResumeParams, + ThreadResumeResponse, + ThreadSetNameResponse, + ThreadStartParams as V2ThreadStartParams, + ThreadStartResponse, + ThreadUnarchiveResponse, + TurnCompletedNotification, + TurnInterruptResponse, + TurnStartParams as V2TurnStartParams, + TurnStartResponse, + TurnSteerResponse, +) +from .models import ( + InitializeResponse, + JsonObject, + JsonValue, + Notification, + UnknownNotification, +) +from .retry import retry_on_overload + +ModelT = TypeVar("ModelT", bound=BaseModel) +ApprovalHandler = Callable[[str, JsonObject | None], JsonObject] + + +def _params_dict( + params: ( + V2ThreadStartParams + | V2ThreadResumeParams + | V2ThreadListParams + | V2ThreadForkParams + | V2TurnStartParams + | JsonObject + | None + ), +) -> JsonObject: + if params is None: + return {} + if hasattr(params, "model_dump"): + dumped = params.model_dump( + by_alias=True, + exclude_none=True, + mode="json", + ) + if not isinstance(dumped, dict): + raise TypeError("Expected model_dump() to return dict") + return dumped + if isinstance(params, dict): + return params + raise TypeError(f"Expected generated params model or dict, got {type(params).__name__}") + + +def _bundled_codex_path() -> Path: + import platform + + sys_name = platform.system().lower() + machine = platform.machine().lower() + + if sys_name.startswith("darwin"): + platform_dir = "darwin-arm64" if machine in {"arm64", "aarch64"} else "darwin-x64" + exe = "codex" + elif sys_name.startswith("linux"): + platform_dir = "linux-arm64" if machine in {"arm64", "aarch64"} else "linux-x64" + exe = "codex" + elif sys_name.startswith("windows") or os.name == "nt": + platform_dir = "windows-arm64" if machine in {"arm64", "aarch64"} else "windows-x64" + exe = "codex.exe" + else: + raise RuntimeError(f"Unsupported OS for bundled codex binary: {sys_name}/{machine}") + + return Path(__file__).resolve().parent / "bin" / platform_dir / exe + + +@dataclass(slots=True) +class AppServerConfig: + codex_bin: str = str(_bundled_codex_path()) + launch_args_override: tuple[str, ...] | None = None + config_overrides: tuple[str, ...] = () + cwd: str | None = None + env: dict[str, str] | None = None + client_name: str = "codex_python_sdk" + client_title: str = "Codex Python SDK" + client_version: str = "0.2.0" + experimental_api: bool = True + + +class AppServerClient: + """Synchronous typed JSON-RPC client for `codex app-server` over stdio.""" + + def __init__( + self, + config: AppServerConfig | None = None, + approval_handler: ApprovalHandler | None = None, + ) -> None: + self.config = config or AppServerConfig() + self._approval_handler = approval_handler or self._default_approval_handler + self._proc: subprocess.Popen[str] | None = None + self._lock = threading.Lock() + self._turn_consumer_lock = threading.Lock() + self._active_turn_consumer: str | None = None + self._pending_notifications: deque[Notification] = deque() + self._stderr_lines: deque[str] = deque(maxlen=400) + self._stderr_thread: threading.Thread | None = None + + def __enter__(self) -> "AppServerClient": + self.start() + return self + + def __exit__(self, _exc_type, _exc, _tb) -> None: + self.close() + + def start(self) -> None: + if self._proc is not None: + return + + if self.config.launch_args_override is not None: + args = list(self.config.launch_args_override) + else: + codex_bin = Path(self.config.codex_bin) + if not codex_bin.exists(): + raise FileNotFoundError( + f"Pinned codex binary not found at {codex_bin}. Run `python scripts/update_sdk_artifacts.py --channel stable` from sdk/python." + ) + args = [str(codex_bin)] + for kv in self.config.config_overrides: + args.extend(["--config", kv]) + args.extend(["app-server", "--listen", "stdio://"]) + + env = os.environ.copy() + if self.config.env: + env.update(self.config.env) + + self._proc = subprocess.Popen( + args, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + cwd=self.config.cwd, + env=env, + bufsize=1, + ) + + self._start_stderr_drain_thread() + + def close(self) -> None: + if self._proc is None: + return + proc = self._proc + self._proc = None + self._active_turn_consumer = None + + if proc.stdin: + proc.stdin.close() + try: + proc.terminate() + proc.wait(timeout=2) + except Exception: + proc.kill() + + if self._stderr_thread and self._stderr_thread.is_alive(): + self._stderr_thread.join(timeout=0.5) + + def initialize(self) -> InitializeResponse: + result = self.request( + "initialize", + { + "clientInfo": { + "name": self.config.client_name, + "title": self.config.client_title, + "version": self.config.client_version, + }, + "capabilities": { + "experimentalApi": self.config.experimental_api, + }, + }, + response_model=InitializeResponse, + ) + self.notify("initialized", None) + return result + + def request( + self, + method: str, + params: JsonObject | None, + *, + response_model: type[ModelT], + ) -> ModelT: + result = self._request_raw(method, params) + if not isinstance(result, dict): + raise AppServerError(f"{method} response must be a JSON object") + return response_model.model_validate(result) + + def _request_raw(self, method: str, params: JsonObject | None = None) -> JsonValue: + request_id = str(uuid.uuid4()) + self._write_message({"id": request_id, "method": method, "params": params or {}}) + + while True: + msg = self._read_message() + + if "method" in msg and "id" in msg: + response = self._handle_server_request(msg) + self._write_message({"id": msg["id"], "result": response}) + continue + + if "method" in msg and "id" not in msg: + self._pending_notifications.append( + self._coerce_notification(msg["method"], msg.get("params")) + ) + continue + + if msg.get("id") != request_id: + continue + + if "error" in msg: + err = msg["error"] + if isinstance(err, dict): + raise map_jsonrpc_error( + int(err.get("code", -32000)), + str(err.get("message", "unknown")), + err.get("data"), + ) + raise AppServerError("Malformed JSON-RPC error response") + + return msg.get("result") + + def notify(self, method: str, params: JsonObject | None = None) -> None: + self._write_message({"method": method, "params": params or {}}) + + def next_notification(self) -> Notification: + if self._pending_notifications: + return self._pending_notifications.popleft() + + while True: + msg = self._read_message() + if "method" in msg and "id" in msg: + response = self._handle_server_request(msg) + self._write_message({"id": msg["id"], "result": response}) + continue + if "method" in msg and "id" not in msg: + return self._coerce_notification(msg["method"], msg.get("params")) + + def acquire_turn_consumer(self, turn_id: str) -> None: + with self._turn_consumer_lock: + if self._active_turn_consumer is not None: + raise RuntimeError( + "Concurrent turn consumers are not yet supported in the experimental SDK. " + f"Client is already streaming turn {self._active_turn_consumer!r}; " + f"cannot start turn {turn_id!r} until the active consumer finishes." + ) + self._active_turn_consumer = turn_id + + def release_turn_consumer(self, turn_id: str) -> None: + with self._turn_consumer_lock: + if self._active_turn_consumer == turn_id: + self._active_turn_consumer = None + + def thread_start(self, params: V2ThreadStartParams | JsonObject | None = None) -> ThreadStartResponse: + return self.request("thread/start", _params_dict(params), response_model=ThreadStartResponse) + + def thread_resume( + self, + thread_id: str, + params: V2ThreadResumeParams | JsonObject | None = None, + ) -> ThreadResumeResponse: + payload = {"threadId": thread_id, **_params_dict(params)} + return self.request("thread/resume", payload, response_model=ThreadResumeResponse) + + def thread_list(self, params: V2ThreadListParams | JsonObject | None = None) -> ThreadListResponse: + return self.request("thread/list", _params_dict(params), response_model=ThreadListResponse) + + def thread_read(self, thread_id: str, include_turns: bool = False) -> ThreadReadResponse: + return self.request( + "thread/read", + {"threadId": thread_id, "includeTurns": include_turns}, + response_model=ThreadReadResponse, + ) + + def thread_fork( + self, + thread_id: str, + params: V2ThreadForkParams | JsonObject | None = None, + ) -> ThreadForkResponse: + payload = {"threadId": thread_id, **_params_dict(params)} + return self.request("thread/fork", payload, response_model=ThreadForkResponse) + + def thread_archive(self, thread_id: str) -> ThreadArchiveResponse: + return self.request("thread/archive", {"threadId": thread_id}, response_model=ThreadArchiveResponse) + + def thread_unarchive(self, thread_id: str) -> ThreadUnarchiveResponse: + return self.request("thread/unarchive", {"threadId": thread_id}, response_model=ThreadUnarchiveResponse) + + def thread_set_name(self, thread_id: str, name: str) -> ThreadSetNameResponse: + return self.request( + "thread/name/set", + {"threadId": thread_id, "name": name}, + response_model=ThreadSetNameResponse, + ) + + def thread_compact(self, thread_id: str) -> ThreadCompactStartResponse: + return self.request( + "thread/compact/start", + {"threadId": thread_id}, + response_model=ThreadCompactStartResponse, + ) + + def turn_start( + self, + thread_id: str, + input_items: list[JsonObject] | JsonObject | str, + params: V2TurnStartParams | JsonObject | None = None, + ) -> TurnStartResponse: + payload = { + **_params_dict(params), + "threadId": thread_id, + "input": self._normalize_input_items(input_items), + } + return self.request("turn/start", payload, response_model=TurnStartResponse) + + def turn_interrupt(self, thread_id: str, turn_id: str) -> TurnInterruptResponse: + return self.request( + "turn/interrupt", + {"threadId": thread_id, "turnId": turn_id}, + response_model=TurnInterruptResponse, + ) + + def turn_steer( + self, + thread_id: str, + expected_turn_id: str, + input_items: list[JsonObject] | JsonObject | str, + ) -> TurnSteerResponse: + return self.request( + "turn/steer", + { + "threadId": thread_id, + "expectedTurnId": expected_turn_id, + "input": self._normalize_input_items(input_items), + }, + response_model=TurnSteerResponse, + ) + + def model_list(self, include_hidden: bool = False) -> ModelListResponse: + return self.request( + "model/list", + {"includeHidden": include_hidden}, + response_model=ModelListResponse, + ) + + def request_with_retry_on_overload( + self, + method: str, + params: JsonObject | None, + *, + response_model: type[ModelT], + max_attempts: int = 3, + initial_delay_s: float = 0.25, + max_delay_s: float = 2.0, + ) -> ModelT: + return retry_on_overload( + lambda: self.request(method, params, response_model=response_model), + max_attempts=max_attempts, + initial_delay_s=initial_delay_s, + max_delay_s=max_delay_s, + ) + + def wait_for_turn_completed(self, turn_id: str) -> TurnCompletedNotification: + while True: + notification = self.next_notification() + if ( + notification.method == "turn/completed" + and isinstance(notification.payload, TurnCompletedNotification) + and notification.payload.turn.id == turn_id + ): + return notification.payload + + def stream_until_methods(self, methods: Iterable[str] | str) -> list[Notification]: + target_methods = {methods} if isinstance(methods, str) else set(methods) + out: list[Notification] = [] + while True: + notification = self.next_notification() + out.append(notification) + if notification.method in target_methods: + return out + + def stream_text( + self, + thread_id: str, + text: str, + params: V2TurnStartParams | JsonObject | None = None, + ) -> Iterator[AgentMessageDeltaNotification]: + started = self.turn_start(thread_id, text, params=params) + turn_id = started.turn.id + while True: + notification = self.next_notification() + if ( + notification.method == "item/agentMessage/delta" + and isinstance(notification.payload, AgentMessageDeltaNotification) + and notification.payload.turn_id == turn_id + ): + yield notification.payload + continue + if ( + notification.method == "turn/completed" + and isinstance(notification.payload, TurnCompletedNotification) + and notification.payload.turn.id == turn_id + ): + break + + def _coerce_notification(self, method: str, params: object) -> Notification: + params_dict = params if isinstance(params, dict) else {} + + model = NOTIFICATION_MODELS.get(method) + if model is None: + return Notification(method=method, payload=UnknownNotification(params=params_dict)) + + try: + payload = model.model_validate(params_dict) + except Exception: # noqa: BLE001 + return Notification(method=method, payload=UnknownNotification(params=params_dict)) + return Notification(method=method, payload=payload) + + def _normalize_input_items( + self, + input_items: list[JsonObject] | JsonObject | str, + ) -> list[JsonObject]: + if isinstance(input_items, str): + return [{"type": "text", "text": input_items}] + if isinstance(input_items, dict): + return [input_items] + return input_items + + def _default_approval_handler(self, method: str, params: JsonObject | None) -> JsonObject: + if method == "item/commandExecution/requestApproval": + return {"decision": "accept"} + if method == "item/fileChange/requestApproval": + return {"decision": "accept"} + return {} + + def _start_stderr_drain_thread(self) -> None: + if self._proc is None or self._proc.stderr is None: + return + + def _drain() -> None: + stderr = self._proc.stderr + if stderr is None: + return + for line in stderr: + self._stderr_lines.append(line.rstrip("\n")) + + self._stderr_thread = threading.Thread(target=_drain, daemon=True) + self._stderr_thread.start() + + def _stderr_tail(self, limit: int = 40) -> str: + return "\n".join(list(self._stderr_lines)[-limit:]) + + def _handle_server_request(self, msg: dict[str, JsonValue]) -> JsonObject: + method = msg["method"] + params = msg.get("params") + if not isinstance(method, str): + return {} + return self._approval_handler( + method, + params if isinstance(params, dict) else None, + ) + + def _write_message(self, payload: JsonObject) -> None: + if self._proc is None or self._proc.stdin is None: + raise TransportClosedError("app-server is not running") + with self._lock: + self._proc.stdin.write(json.dumps(payload) + "\n") + self._proc.stdin.flush() + + def _read_message(self) -> dict[str, JsonValue]: + if self._proc is None or self._proc.stdout is None: + raise TransportClosedError("app-server is not running") + + line = self._proc.stdout.readline() + if not line: + raise TransportClosedError( + f"app-server closed stdout. stderr_tail={self._stderr_tail()[:2000]}" + ) + + try: + message = json.loads(line) + except json.JSONDecodeError as exc: + raise AppServerError(f"Invalid JSON-RPC line: {line!r}") from exc + + if not isinstance(message, dict): + raise AppServerError(f"Invalid JSON-RPC payload: {message!r}") + return message + + +def default_codex_home() -> str: + return str(Path.home() / ".codex") diff --git a/sdk/python/src/codex_app_server/errors.py b/sdk/python/src/codex_app_server/errors.py new file mode 100644 index 00000000000..24972e4fd77 --- /dev/null +++ b/sdk/python/src/codex_app_server/errors.py @@ -0,0 +1,125 @@ +from __future__ import annotations + +from typing import Any + + +class AppServerError(Exception): + """Base exception for SDK errors.""" + + +class JsonRpcError(AppServerError): + """Raw JSON-RPC error wrapper from the server.""" + + def __init__(self, code: int, message: str, data: Any = None): + super().__init__(f"JSON-RPC error {code}: {message}") + self.code = code + self.message = message + self.data = data + + +class TransportClosedError(AppServerError): + """Raised when the app-server transport closes unexpectedly.""" + + +class AppServerRpcError(JsonRpcError): + """Base typed error for JSON-RPC failures.""" + + +class ParseError(AppServerRpcError): + pass + + +class InvalidRequestError(AppServerRpcError): + pass + + +class MethodNotFoundError(AppServerRpcError): + pass + + +class InvalidParamsError(AppServerRpcError): + pass + + +class InternalRpcError(AppServerRpcError): + pass + + +class ServerBusyError(AppServerRpcError): + """Server is overloaded / unavailable and caller should retry.""" + + +class RetryLimitExceededError(ServerBusyError): + """Server exhausted internal retry budget for a retryable operation.""" + + +def _contains_retry_limit_text(message: str) -> bool: + lowered = message.lower() + return "retry limit" in lowered or "too many failed attempts" in lowered + + +def _is_server_overloaded(data: Any) -> bool: + if data is None: + return False + + if isinstance(data, str): + return data.lower() == "server_overloaded" + + if isinstance(data, dict): + direct = ( + data.get("codex_error_info") + or data.get("codexErrorInfo") + or data.get("errorInfo") + ) + if isinstance(direct, str) and direct.lower() == "server_overloaded": + return True + if isinstance(direct, dict): + for value in direct.values(): + if isinstance(value, str) and value.lower() == "server_overloaded": + return True + for value in data.values(): + if _is_server_overloaded(value): + return True + + if isinstance(data, list): + return any(_is_server_overloaded(value) for value in data) + + return False + + +def map_jsonrpc_error(code: int, message: str, data: Any = None) -> JsonRpcError: + """Map a raw JSON-RPC error into a richer SDK exception class.""" + + if code == -32700: + return ParseError(code, message, data) + if code == -32600: + return InvalidRequestError(code, message, data) + if code == -32601: + return MethodNotFoundError(code, message, data) + if code == -32602: + return InvalidParamsError(code, message, data) + if code == -32603: + return InternalRpcError(code, message, data) + + if -32099 <= code <= -32000: + if _is_server_overloaded(data): + if _contains_retry_limit_text(message): + return RetryLimitExceededError(code, message, data) + return ServerBusyError(code, message, data) + if _contains_retry_limit_text(message): + return RetryLimitExceededError(code, message, data) + return AppServerRpcError(code, message, data) + + return JsonRpcError(code, message, data) + + +def is_retryable_error(exc: BaseException) -> bool: + """True if the exception is a transient overload-style error.""" + + if isinstance(exc, ServerBusyError): + return True + + if isinstance(exc, JsonRpcError): + return _is_server_overloaded(exc.data) + + return False diff --git a/sdk/python/src/codex_app_server/generated/__init__.py b/sdk/python/src/codex_app_server/generated/__init__.py new file mode 100644 index 00000000000..d7b3f674b27 --- /dev/null +++ b/sdk/python/src/codex_app_server/generated/__init__.py @@ -0,0 +1 @@ +"""Auto-generated Python types derived from the app-server schemas.""" diff --git a/sdk/python/src/codex_app_server/generated/notification_registry.py b/sdk/python/src/codex_app_server/generated/notification_registry.py new file mode 100644 index 00000000000..ef5b182d78b --- /dev/null +++ b/sdk/python/src/codex_app_server/generated/notification_registry.py @@ -0,0 +1,102 @@ +# Auto-generated by scripts/update_sdk_artifacts.py +# DO NOT EDIT MANUALLY. + +from __future__ import annotations + +from pydantic import BaseModel + +from .v2_all import AccountLoginCompletedNotification +from .v2_all import AccountRateLimitsUpdatedNotification +from .v2_all import AccountUpdatedNotification +from .v2_all import AgentMessageDeltaNotification +from .v2_all import AppListUpdatedNotification +from .v2_all import CommandExecOutputDeltaNotification +from .v2_all import CommandExecutionOutputDeltaNotification +from .v2_all import ConfigWarningNotification +from .v2_all import ContextCompactedNotification +from .v2_all import DeprecationNoticeNotification +from .v2_all import ErrorNotification +from .v2_all import FileChangeOutputDeltaNotification +from .v2_all import FuzzyFileSearchSessionCompletedNotification +from .v2_all import FuzzyFileSearchSessionUpdatedNotification +from .v2_all import HookCompletedNotification +from .v2_all import HookStartedNotification +from .v2_all import ItemCompletedNotification +from .v2_all import ItemStartedNotification +from .v2_all import McpServerOauthLoginCompletedNotification +from .v2_all import McpToolCallProgressNotification +from .v2_all import ModelReroutedNotification +from .v2_all import PlanDeltaNotification +from .v2_all import ReasoningSummaryPartAddedNotification +from .v2_all import ReasoningSummaryTextDeltaNotification +from .v2_all import ReasoningTextDeltaNotification +from .v2_all import ServerRequestResolvedNotification +from .v2_all import SkillsChangedNotification +from .v2_all import TerminalInteractionNotification +from .v2_all import ThreadArchivedNotification +from .v2_all import ThreadClosedNotification +from .v2_all import ThreadNameUpdatedNotification +from .v2_all import ThreadRealtimeClosedNotification +from .v2_all import ThreadRealtimeErrorNotification +from .v2_all import ThreadRealtimeItemAddedNotification +from .v2_all import ThreadRealtimeOutputAudioDeltaNotification +from .v2_all import ThreadRealtimeStartedNotification +from .v2_all import ThreadStartedNotification +from .v2_all import ThreadStatusChangedNotification +from .v2_all import ThreadTokenUsageUpdatedNotification +from .v2_all import ThreadUnarchivedNotification +from .v2_all import TurnCompletedNotification +from .v2_all import TurnDiffUpdatedNotification +from .v2_all import TurnPlanUpdatedNotification +from .v2_all import TurnStartedNotification +from .v2_all import WindowsSandboxSetupCompletedNotification +from .v2_all import WindowsWorldWritableWarningNotification + +NOTIFICATION_MODELS: dict[str, type[BaseModel]] = { + "account/login/completed": AccountLoginCompletedNotification, + "account/rateLimits/updated": AccountRateLimitsUpdatedNotification, + "account/updated": AccountUpdatedNotification, + "app/list/updated": AppListUpdatedNotification, + "command/exec/outputDelta": CommandExecOutputDeltaNotification, + "configWarning": ConfigWarningNotification, + "deprecationNotice": DeprecationNoticeNotification, + "error": ErrorNotification, + "fuzzyFileSearch/sessionCompleted": FuzzyFileSearchSessionCompletedNotification, + "fuzzyFileSearch/sessionUpdated": FuzzyFileSearchSessionUpdatedNotification, + "hook/completed": HookCompletedNotification, + "hook/started": HookStartedNotification, + "item/agentMessage/delta": AgentMessageDeltaNotification, + "item/commandExecution/outputDelta": CommandExecutionOutputDeltaNotification, + "item/commandExecution/terminalInteraction": TerminalInteractionNotification, + "item/completed": ItemCompletedNotification, + "item/fileChange/outputDelta": FileChangeOutputDeltaNotification, + "item/mcpToolCall/progress": McpToolCallProgressNotification, + "item/plan/delta": PlanDeltaNotification, + "item/reasoning/summaryPartAdded": ReasoningSummaryPartAddedNotification, + "item/reasoning/summaryTextDelta": ReasoningSummaryTextDeltaNotification, + "item/reasoning/textDelta": ReasoningTextDeltaNotification, + "item/started": ItemStartedNotification, + "mcpServer/oauthLogin/completed": McpServerOauthLoginCompletedNotification, + "model/rerouted": ModelReroutedNotification, + "serverRequest/resolved": ServerRequestResolvedNotification, + "skills/changed": SkillsChangedNotification, + "thread/archived": ThreadArchivedNotification, + "thread/closed": ThreadClosedNotification, + "thread/compacted": ContextCompactedNotification, + "thread/name/updated": ThreadNameUpdatedNotification, + "thread/realtime/closed": ThreadRealtimeClosedNotification, + "thread/realtime/error": ThreadRealtimeErrorNotification, + "thread/realtime/itemAdded": ThreadRealtimeItemAddedNotification, + "thread/realtime/outputAudio/delta": ThreadRealtimeOutputAudioDeltaNotification, + "thread/realtime/started": ThreadRealtimeStartedNotification, + "thread/started": ThreadStartedNotification, + "thread/status/changed": ThreadStatusChangedNotification, + "thread/tokenUsage/updated": ThreadTokenUsageUpdatedNotification, + "thread/unarchived": ThreadUnarchivedNotification, + "turn/completed": TurnCompletedNotification, + "turn/diff/updated": TurnDiffUpdatedNotification, + "turn/plan/updated": TurnPlanUpdatedNotification, + "turn/started": TurnStartedNotification, + "windows/worldWritableWarning": WindowsWorldWritableWarningNotification, + "windowsSandbox/setupCompleted": WindowsSandboxSetupCompletedNotification, +} diff --git a/sdk/python/src/codex_app_server/generated/v2_all.py b/sdk/python/src/codex_app_server/generated/v2_all.py new file mode 100644 index 00000000000..824c86c44b5 --- /dev/null +++ b/sdk/python/src/codex_app_server/generated/v2_all.py @@ -0,0 +1,8167 @@ +# generated by datamodel-codegen: +# filename: codex_app_server_protocol.v2.schemas.json + +from __future__ import annotations + +from enum import Enum +from typing import Any, Dict, List + +from pydantic import BaseModel, ConfigDict, Field, RootModel, conint + + +class CodexAppServerProtocolV2(BaseModel): + pass + model_config = ConfigDict( + populate_by_name=True, + ) + + +class AbsolutePathBuf(RootModel[str]): + model_config = ConfigDict( + populate_by_name=True, + ) + root: str = Field( + ..., + description="A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", + ) + + +class Type(Enum): + api_key = "apiKey" + + +class Account1(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + type: Type = Field(..., title="ApiKeyAccountType") + + +class Type1(Enum): + chatgpt = "chatgpt" + + +class AccountLoginCompletedNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + error: str | None = None + login_id: str | None = Field(None, alias="loginId") + success: bool + + +class Type2(Enum): + text = "Text" + + +class AgentMessageContent1(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + text: str + type: Type2 = Field(..., title="TextAgentMessageContentType") + + +class AgentMessageContent(RootModel[AgentMessageContent1]): + model_config = ConfigDict( + populate_by_name=True, + ) + root: AgentMessageContent1 + + +class AgentMessageDeltaNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + delta: str + item_id: str = Field(..., alias="itemId") + thread_id: str = Field(..., alias="threadId") + turn_id: str = Field(..., alias="turnId") + + +class AgentStatus1(Enum): + pending_init = "pending_init" + + +class AgentStatus2(Enum): + running = "running" + + +class AgentStatus3(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + completed: str | None = None + + +class AgentStatus4(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + errored: str + + +class AgentStatus5(Enum): + shutdown = "shutdown" + + +class AgentStatus6(Enum): + not_found = "not_found" + + +class AgentStatus( + RootModel[ + AgentStatus1 + | AgentStatus2 + | AgentStatus3 + | AgentStatus4 + | AgentStatus5 + | AgentStatus6 + ] +): + model_config = ConfigDict( + populate_by_name=True, + ) + root: ( + AgentStatus1 + | AgentStatus2 + | AgentStatus3 + | AgentStatus4 + | AgentStatus5 + | AgentStatus6 + ) = Field(..., description="Agent lifecycle status, derived from emitted events.") + + +class AnalyticsConfig(BaseModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + enabled: bool | None = None + + +class AppBranding(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + category: str | None = None + developer: str | None = None + is_discoverable_app: bool = Field(..., alias="isDiscoverableApp") + privacy_policy: str | None = Field(None, alias="privacyPolicy") + terms_of_service: str | None = Field(None, alias="termsOfService") + website: str | None = None + + +class AppReview(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + status: str + + +class AppScreenshot(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + file_id: str | None = Field(None, alias="fileId") + url: str | None = None + user_prompt: str = Field(..., alias="userPrompt") + + +class AppSummary(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + description: str | None = None + id: str + install_url: str | None = Field(None, alias="installUrl") + name: str + + +class AppToolApproval(Enum): + auto = "auto" + prompt = "prompt" + approve = "approve" + + +class AppToolConfig(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + approval_mode: AppToolApproval | None = None + enabled: bool | None = None + + +AppToolsConfig = CodexAppServerProtocolV2 + + +class AppsDefaultConfig(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + destructive_enabled: bool | None = True + enabled: bool | None = True + open_world_enabled: bool | None = True + + +class AppsListParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + cursor: str | None = Field( + None, description="Opaque pagination cursor returned by a previous call." + ) + force_refetch: bool | None = Field( + None, + alias="forceRefetch", + description="When true, bypass app caches and fetch the latest data from sources.", + ) + limit: conint(ge=0) | None = Field( + None, + description="Optional page size; defaults to a reasonable server-side value.", + ) + thread_id: str | None = Field( + None, + alias="threadId", + description="Optional thread id used to evaluate app feature gating from that thread's config.", + ) + + +class AskForApproval1(Enum): + untrusted = "untrusted" + on_failure = "on-failure" + on_request = "on-request" + never = "never" + + +class Reject(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + mcp_elicitations: bool + request_permissions: bool | None = False + rules: bool + sandbox_approval: bool + + +class AskForApproval2(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + reject: Reject + + +class AskForApproval(RootModel[AskForApproval1 | AskForApproval2]): + model_config = ConfigDict( + populate_by_name=True, + ) + root: AskForApproval1 | AskForApproval2 + + +class AuthMode(Enum): + apikey = "apikey" + chatgpt = "chatgpt" + chatgpt_auth_tokens = "chatgptAuthTokens" + + +class ByteRange(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + end: conint(ge=0) + start: conint(ge=0) + + +class CallToolResult(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + field_meta: Any | None = Field(None, alias="_meta") + content: List + is_error: bool | None = Field(None, alias="isError") + structured_content: Any | None = Field(None, alias="structuredContent") + + +class CancelLoginAccountParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + login_id: str = Field(..., alias="loginId") + + +class CancelLoginAccountStatus(Enum): + canceled = "canceled" + not_found = "notFound" + + +class ClientInfo(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + name: str + title: str | None = None + version: str + + +class Method(Enum): + initialize = "initialize" + + +class Method1(Enum): + thread_start = "thread/start" + + +class Method2(Enum): + thread_resume = "thread/resume" + + +class Method3(Enum): + thread_fork = "thread/fork" + + +class Method4(Enum): + thread_archive = "thread/archive" + + +class Method5(Enum): + thread_unsubscribe = "thread/unsubscribe" + + +class Method6(Enum): + thread_name_set = "thread/name/set" + + +class Method7(Enum): + thread_metadata_update = "thread/metadata/update" + + +class Method8(Enum): + thread_unarchive = "thread/unarchive" + + +class Method9(Enum): + thread_compact_start = "thread/compact/start" + + +class Method10(Enum): + thread_rollback = "thread/rollback" + + +class Method11(Enum): + thread_list = "thread/list" + + +class Method12(Enum): + thread_loaded_list = "thread/loaded/list" + + +class Method13(Enum): + thread_read = "thread/read" + + +class Method14(Enum): + skills_list = "skills/list" + + +class Method15(Enum): + plugin_list = "plugin/list" + + +class Method16(Enum): + skills_remote_list = "skills/remote/list" + + +class Method17(Enum): + skills_remote_export = "skills/remote/export" + + +class Method18(Enum): + app_list = "app/list" + + +class Method19(Enum): + skills_config_write = "skills/config/write" + + +class Method20(Enum): + plugin_install = "plugin/install" + + +class Method21(Enum): + plugin_uninstall = "plugin/uninstall" + + +class Method22(Enum): + turn_start = "turn/start" + + +class Method23(Enum): + turn_steer = "turn/steer" + + +class Method24(Enum): + turn_interrupt = "turn/interrupt" + + +class Method25(Enum): + review_start = "review/start" + + +class Method26(Enum): + model_list = "model/list" + + +class Method27(Enum): + experimental_feature_list = "experimentalFeature/list" + + +class Method28(Enum): + mcp_server_oauth_login = "mcpServer/oauth/login" + + +class Method29(Enum): + config_mcp_server_reload = "config/mcpServer/reload" + + +class Method30(Enum): + mcp_server_status_list = "mcpServerStatus/list" + + +class Method31(Enum): + windows_sandbox_setup_start = "windowsSandbox/setupStart" + + +class Method32(Enum): + account_login_start = "account/login/start" + + +class Method33(Enum): + account_login_cancel = "account/login/cancel" + + +class Method34(Enum): + account_logout = "account/logout" + + +class Method35(Enum): + account_rate_limits_read = "account/rateLimits/read" + + +class Method36(Enum): + feedback_upload = "feedback/upload" + + +class Method37(Enum): + command_exec = "command/exec" + + +class Method38(Enum): + command_exec_write = "command/exec/write" + + +class Method39(Enum): + command_exec_terminate = "command/exec/terminate" + + +class Method40(Enum): + command_exec_resize = "command/exec/resize" + + +class Method41(Enum): + config_read = "config/read" + + +class Method42(Enum): + external_agent_config_detect = "externalAgentConfig/detect" + + +class Method43(Enum): + external_agent_config_import = "externalAgentConfig/import" + + +class Method44(Enum): + config_value_write = "config/value/write" + + +class Method45(Enum): + config_batch_write = "config/batchWrite" + + +class Method46(Enum): + config_requirements_read = "configRequirements/read" + + +class Method47(Enum): + account_read = "account/read" + + +class Method48(Enum): + fuzzy_file_search = "fuzzyFileSearch" + + +class CodexErrorInfo1(Enum): + context_window_exceeded = "contextWindowExceeded" + usage_limit_exceeded = "usageLimitExceeded" + server_overloaded = "serverOverloaded" + internal_server_error = "internalServerError" + unauthorized = "unauthorized" + bad_request = "badRequest" + thread_rollback_failed = "threadRollbackFailed" + sandbox_error = "sandboxError" + other = "other" + + +class HttpConnectionFailed(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + http_status_code: conint(ge=0) | None = Field(None, alias="httpStatusCode") + + +class CodexErrorInfo2(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + http_connection_failed: HttpConnectionFailed = Field( + ..., alias="httpConnectionFailed" + ) + + +ResponseStreamConnectionFailed = HttpConnectionFailed + + +class CodexErrorInfo3(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + response_stream_connection_failed: ResponseStreamConnectionFailed = Field( + ..., alias="responseStreamConnectionFailed" + ) + + +ResponseStreamDisconnected = HttpConnectionFailed + + +class CodexErrorInfo4(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + response_stream_disconnected: ResponseStreamDisconnected = Field( + ..., alias="responseStreamDisconnected" + ) + + +ResponseTooManyFailedAttempts = HttpConnectionFailed + + +class CodexErrorInfo5(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + response_too_many_failed_attempts: ResponseTooManyFailedAttempts = Field( + ..., alias="responseTooManyFailedAttempts" + ) + + +class CodexErrorInfo( + RootModel[ + CodexErrorInfo1 + | CodexErrorInfo2 + | CodexErrorInfo3 + | CodexErrorInfo4 + | CodexErrorInfo5 + ] +): + model_config = ConfigDict( + populate_by_name=True, + ) + root: ( + CodexErrorInfo1 + | CodexErrorInfo2 + | CodexErrorInfo3 + | CodexErrorInfo4 + | CodexErrorInfo5 + ) = Field( + ..., + description="This translation layer make sure that we expose codex error code in camel case.\n\nWhen an upstream HTTP status is available (for example, from the Responses API or a provider), it is forwarded in `httpStatusCode` on the relevant `codexErrorInfo` variant.", + ) + + +class CollabAgentStatus(Enum): + pending_init = "pendingInit" + running = "running" + completed = "completed" + errored = "errored" + shutdown = "shutdown" + not_found = "notFound" + + +class CollabAgentTool(Enum): + spawn_agent = "spawnAgent" + send_input = "sendInput" + resume_agent = "resumeAgent" + wait = "wait" + close_agent = "closeAgent" + + +class CollabAgentToolCallStatus(Enum): + in_progress = "inProgress" + completed = "completed" + failed = "failed" + + +class Type3(Enum): + read = "read" + + +class CommandAction1(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + command: str + name: str + path: str + type: Type3 = Field(..., title="ReadCommandActionType") + + +class Type4(Enum): + list_files = "listFiles" + + +class CommandAction2(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + command: str + path: str | None = None + type: Type4 = Field(..., title="ListFilesCommandActionType") + + +class Type5(Enum): + search = "search" + + +class CommandAction3(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + command: str + path: str | None = None + query: str | None = None + type: Type5 = Field(..., title="SearchCommandActionType") + + +class Type6(Enum): + unknown = "unknown" + + +class CommandAction4(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + command: str + type: Type6 = Field(..., title="UnknownCommandActionType") + + +class CommandAction( + RootModel[CommandAction1 | CommandAction2 | CommandAction3 | CommandAction4] +): + model_config = ConfigDict( + populate_by_name=True, + ) + root: CommandAction1 | CommandAction2 | CommandAction3 | CommandAction4 + + +class CommandExecOutputStream(Enum): + stdout = "stdout" + stderr = "stderr" + + +CommandExecResizeResponse = CodexAppServerProtocolV2 + + +class CommandExecResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + exit_code: int = Field(..., alias="exitCode", description="Process exit code.") + stderr: str = Field( + ..., + description="Buffered stderr capture.\n\nEmpty when stderr was streamed via `command/exec/outputDelta`.", + ) + stdout: str = Field( + ..., + description="Buffered stdout capture.\n\nEmpty when stdout was streamed via `command/exec/outputDelta`.", + ) + + +class CommandExecTerminalSize(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + cols: conint(ge=0) = Field(..., description="Terminal width in character cells.") + rows: conint(ge=0) = Field(..., description="Terminal height in character cells.") + + +class CommandExecTerminateParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + process_id: str = Field( + ..., + alias="processId", + description="Client-supplied, connection-scoped `processId` from the original `command/exec` request.", + ) + + +CommandExecTerminateResponse = CodexAppServerProtocolV2 + + +class CommandExecWriteParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + close_stdin: bool | None = Field( + None, + alias="closeStdin", + description="Close stdin after writing `deltaBase64`, if present.", + ) + delta_base64: str | None = Field( + None, + alias="deltaBase64", + description="Optional base64-encoded stdin bytes to write.", + ) + process_id: str = Field( + ..., + alias="processId", + description="Client-supplied, connection-scoped `processId` from the original `command/exec` request.", + ) + + +CommandExecWriteResponse = CodexAppServerProtocolV2 + + +CommandExecutionOutputDeltaNotification = AgentMessageDeltaNotification + + +class CommandExecutionStatus(Enum): + in_progress = "inProgress" + completed = "completed" + failed = "failed" + declined = "declined" + + +class Type7(Enum): + mdm = "mdm" + + +class ConfigLayerSource1(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + domain: str + key: str + type: Type7 = Field(..., title="MdmConfigLayerSourceType") + + +class Type8(Enum): + system = "system" + + +class ConfigLayerSource2(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + file: AbsolutePathBuf = Field( + ..., + description="This is the path to the system config.toml file, though it is not guaranteed to exist.", + ) + type: Type8 = Field(..., title="SystemConfigLayerSourceType") + + +class Type9(Enum): + user = "user" + + +class ConfigLayerSource3(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + file: AbsolutePathBuf = Field( + ..., + description="This is the path to the user's config.toml file, though it is not guaranteed to exist.", + ) + type: Type9 = Field(..., title="UserConfigLayerSourceType") + + +class Type10(Enum): + project = "project" + + +class ConfigLayerSource4(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + dot_codex_folder: AbsolutePathBuf = Field(..., alias="dotCodexFolder") + type: Type10 = Field(..., title="ProjectConfigLayerSourceType") + + +class Type11(Enum): + session_flags = "sessionFlags" + + +class ConfigLayerSource5(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + type: Type11 = Field(..., title="SessionFlagsConfigLayerSourceType") + + +class Type12(Enum): + legacy_managed_config_toml_from_file = "legacyManagedConfigTomlFromFile" + + +class ConfigLayerSource6(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + file: AbsolutePathBuf + type: Type12 = Field( + ..., title="LegacyManagedConfigTomlFromFileConfigLayerSourceType" + ) + + +class Type13(Enum): + legacy_managed_config_toml_from_mdm = "legacyManagedConfigTomlFromMdm" + + +class ConfigLayerSource7(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + type: Type13 = Field( + ..., title="LegacyManagedConfigTomlFromMdmConfigLayerSourceType" + ) + + +class ConfigLayerSource( + RootModel[ + ConfigLayerSource1 + | ConfigLayerSource2 + | ConfigLayerSource3 + | ConfigLayerSource4 + | ConfigLayerSource5 + | ConfigLayerSource6 + | ConfigLayerSource7 + ] +): + model_config = ConfigDict( + populate_by_name=True, + ) + root: ( + ConfigLayerSource1 + | ConfigLayerSource2 + | ConfigLayerSource3 + | ConfigLayerSource4 + | ConfigLayerSource5 + | ConfigLayerSource6 + | ConfigLayerSource7 + ) + + +class ConfigReadParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + cwd: str | None = Field( + None, + description="Optional working directory to resolve project config layers. If specified, return the effective config as seen from that directory (i.e., including any project layers between `cwd` and the project/repo root).", + ) + include_layers: bool | None = Field(False, alias="includeLayers") + + +class Type14(Enum): + input_text = "input_text" + + +class ContentItem1(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + text: str + type: Type14 = Field(..., title="InputTextContentItemType") + + +class Type15(Enum): + input_image = "input_image" + + +class ContentItem2(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + image_url: str + type: Type15 = Field(..., title="InputImageContentItemType") + + +class Type16(Enum): + output_text = "output_text" + + +class ContentItem3(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + text: str + type: Type16 = Field(..., title="OutputTextContentItemType") + + +class ContentItem(RootModel[ContentItem1 | ContentItem2 | ContentItem3]): + model_config = ConfigDict( + populate_by_name=True, + ) + root: ContentItem1 | ContentItem2 | ContentItem3 + + +class ContextCompactedNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + thread_id: str = Field(..., alias="threadId") + turn_id: str = Field(..., alias="turnId") + + +class CreditsSnapshot(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + balance: str | None = None + has_credits: bool = Field(..., alias="hasCredits") + unlimited: bool + + +class CustomPrompt(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + argument_hint: str | None = None + content: str + description: str | None = None + name: str + path: str + + +class DeprecationNoticeNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + details: str | None = Field( + None, + description="Optional extra guidance, such as migration steps or rationale.", + ) + summary: str = Field(..., description="Concise summary of what is deprecated.") + + +class Duration(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + nanos: conint(ge=0) + secs: conint(ge=0) + + +class Type17(Enum): + input_text = "inputText" + + +class DynamicToolCallOutputContentItem1(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + text: str + type: Type17 = Field(..., title="InputTextDynamicToolCallOutputContentItemType") + + +class Type18(Enum): + input_image = "inputImage" + + +class DynamicToolCallOutputContentItem2(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + image_url: str = Field(..., alias="imageUrl") + type: Type18 = Field(..., title="InputImageDynamicToolCallOutputContentItemType") + + +class DynamicToolCallOutputContentItem( + RootModel[DynamicToolCallOutputContentItem1 | DynamicToolCallOutputContentItem2] +): + model_config = ConfigDict( + populate_by_name=True, + ) + root: DynamicToolCallOutputContentItem1 | DynamicToolCallOutputContentItem2 + + +class DynamicToolSpec(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + description: str + input_schema: Any = Field(..., alias="inputSchema") + name: str + + +class Mode(Enum): + form = "form" + + +class ElicitationRequest1(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + field_meta: Any | None = Field(None, alias="_meta") + message: str + mode: Mode + requested_schema: Any + + +class Mode1(Enum): + url = "url" + + +class ElicitationRequest2(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + field_meta: Any | None = Field(None, alias="_meta") + elicitation_id: str + message: str + mode: Mode1 + url: str + + +class ElicitationRequest(RootModel[ElicitationRequest1 | ElicitationRequest2]): + model_config = ConfigDict( + populate_by_name=True, + ) + root: ElicitationRequest1 | ElicitationRequest2 + + +class Type19(Enum): + error = "error" + + +class EventMsg1(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + codex_error_info: CodexErrorInfo | None = None + message: str + type: Type19 = Field(..., title="ErrorEventMsgType") + + +class Type20(Enum): + warning = "warning" + + +class EventMsg2(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + message: str + type: Type20 = Field(..., title="WarningEventMsgType") + + +class Type21(Enum): + realtime_conversation_started = "realtime_conversation_started" + + +class EventMsg3(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + session_id: str | None = None + type: Type21 = Field(..., title="RealtimeConversationStartedEventMsgType") + + +class Type22(Enum): + realtime_conversation_realtime = "realtime_conversation_realtime" + + +class Type23(Enum): + realtime_conversation_closed = "realtime_conversation_closed" + + +class EventMsg5(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + reason: str | None = None + type: Type23 = Field(..., title="RealtimeConversationClosedEventMsgType") + + +class Type24(Enum): + model_reroute = "model_reroute" + + +class Type25(Enum): + context_compacted = "context_compacted" + + +class EventMsg7(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + type: Type25 = Field(..., title="ContextCompactedEventMsgType") + + +class Type26(Enum): + thread_rolled_back = "thread_rolled_back" + + +class EventMsg8(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + num_turns: conint(ge=0) = Field( + ..., description="Number of user turns that were removed from context." + ) + type: Type26 = Field(..., title="ThreadRolledBackEventMsgType") + + +class Type27(Enum): + task_started = "task_started" + + +class Type28(Enum): + task_complete = "task_complete" + + +class EventMsg10(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + last_agent_message: str | None = None + turn_id: str + type: Type28 = Field(..., title="TaskCompleteEventMsgType") + + +class Type29(Enum): + token_count = "token_count" + + +class Type30(Enum): + agent_message = "agent_message" + + +class Type31(Enum): + user_message = "user_message" + + +class Type32(Enum): + agent_message_delta = "agent_message_delta" + + +class EventMsg14(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + delta: str + type: Type32 = Field(..., title="AgentMessageDeltaEventMsgType") + + +class Type33(Enum): + agent_reasoning = "agent_reasoning" + + +class EventMsg15(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + text: str + type: Type33 = Field(..., title="AgentReasoningEventMsgType") + + +class Type34(Enum): + agent_reasoning_delta = "agent_reasoning_delta" + + +class EventMsg16(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + delta: str + type: Type34 = Field(..., title="AgentReasoningDeltaEventMsgType") + + +class Type35(Enum): + agent_reasoning_raw_content = "agent_reasoning_raw_content" + + +class EventMsg17(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + text: str + type: Type35 = Field(..., title="AgentReasoningRawContentEventMsgType") + + +class Type36(Enum): + agent_reasoning_raw_content_delta = "agent_reasoning_raw_content_delta" + + +class EventMsg18(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + delta: str + type: Type36 = Field(..., title="AgentReasoningRawContentDeltaEventMsgType") + + +class Type37(Enum): + agent_reasoning_section_break = "agent_reasoning_section_break" + + +class EventMsg19(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + item_id: str | None = "" + summary_index: int | None = 0 + type: Type37 = Field(..., title="AgentReasoningSectionBreakEventMsgType") + + +class Type38(Enum): + session_configured = "session_configured" + + +class Type39(Enum): + thread_name_updated = "thread_name_updated" + + +class Type40(Enum): + mcp_startup_update = "mcp_startup_update" + + +class Type41(Enum): + mcp_startup_complete = "mcp_startup_complete" + + +class Type42(Enum): + mcp_tool_call_begin = "mcp_tool_call_begin" + + +class Type43(Enum): + mcp_tool_call_end = "mcp_tool_call_end" + + +class Type44(Enum): + web_search_begin = "web_search_begin" + + +class EventMsg26(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + call_id: str + type: Type44 = Field(..., title="WebSearchBeginEventMsgType") + + +class Type45(Enum): + web_search_end = "web_search_end" + + +class Type46(Enum): + image_generation_begin = "image_generation_begin" + + +class EventMsg28(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + call_id: str + type: Type46 = Field(..., title="ImageGenerationBeginEventMsgType") + + +class Type47(Enum): + image_generation_end = "image_generation_end" + + +class EventMsg29(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + call_id: str + result: str + revised_prompt: str | None = None + saved_path: str | None = None + status: str + type: Type47 = Field(..., title="ImageGenerationEndEventMsgType") + + +class Type48(Enum): + exec_command_begin = "exec_command_begin" + + +class Type49(Enum): + exec_command_output_delta = "exec_command_output_delta" + + +class Type50(Enum): + terminal_interaction = "terminal_interaction" + + +class EventMsg32(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + call_id: str = Field( + ..., description="Identifier for the ExecCommandBegin that produced this chunk." + ) + process_id: str = Field( + ..., description="Process id associated with the running command." + ) + stdin: str = Field(..., description="Stdin sent to the running session.") + type: Type50 = Field(..., title="TerminalInteractionEventMsgType") + + +class Type51(Enum): + exec_command_end = "exec_command_end" + + +class Type52(Enum): + view_image_tool_call = "view_image_tool_call" + + +class EventMsg34(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + call_id: str = Field(..., description="Identifier for the originating tool call.") + path: str = Field(..., description="Local filesystem path provided to the tool.") + type: Type52 = Field(..., title="ViewImageToolCallEventMsgType") + + +class Type53(Enum): + exec_approval_request = "exec_approval_request" + + +class Type54(Enum): + request_permissions = "request_permissions" + + +class Type55(Enum): + request_user_input = "request_user_input" + + +class Type56(Enum): + dynamic_tool_call_request = "dynamic_tool_call_request" + + +class EventMsg38(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + arguments: Any + call_id: str = Field(..., alias="callId") + tool: str + turn_id: str = Field(..., alias="turnId") + type: Type56 = Field(..., title="DynamicToolCallRequestEventMsgType") + + +class Type57(Enum): + dynamic_tool_call_response = "dynamic_tool_call_response" + + +class EventMsg39(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + arguments: Any = Field(..., description="Dynamic tool call arguments.") + call_id: str = Field( + ..., description="Identifier for the corresponding DynamicToolCallRequest." + ) + content_items: List[DynamicToolCallOutputContentItem] = Field( + ..., description="Dynamic tool response content items." + ) + duration: Duration = Field( + ..., description="The duration of the dynamic tool call." + ) + error: str | None = Field( + None, + description="Optional error text when the tool call failed before producing a response.", + ) + success: bool = Field(..., description="Whether the tool call succeeded.") + tool: str = Field(..., description="Dynamic tool name.") + turn_id: str = Field( + ..., description="Turn ID that this dynamic tool call belongs to." + ) + type: Type57 = Field(..., title="DynamicToolCallResponseEventMsgType") + + +class Type58(Enum): + elicitation_request = "elicitation_request" + + +class Type59(Enum): + apply_patch_approval_request = "apply_patch_approval_request" + + +class Type60(Enum): + deprecation_notice = "deprecation_notice" + + +class EventMsg42(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + details: str | None = Field( + None, + description="Optional extra guidance, such as migration steps or rationale.", + ) + summary: str = Field(..., description="Concise summary of what is deprecated.") + type: Type60 = Field(..., title="DeprecationNoticeEventMsgType") + + +class Type61(Enum): + background_event = "background_event" + + +class EventMsg43(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + message: str + type: Type61 = Field(..., title="BackgroundEventEventMsgType") + + +class Type62(Enum): + undo_started = "undo_started" + + +class EventMsg44(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + message: str | None = None + type: Type62 = Field(..., title="UndoStartedEventMsgType") + + +class Type63(Enum): + undo_completed = "undo_completed" + + +class EventMsg45(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + message: str | None = None + success: bool + type: Type63 = Field(..., title="UndoCompletedEventMsgType") + + +class Type64(Enum): + stream_error = "stream_error" + + +class EventMsg46(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + additional_details: str | None = Field( + None, + description="Optional details about the underlying stream failure (often the same human-readable message that is surfaced as the terminal error if retries are exhausted).", + ) + codex_error_info: CodexErrorInfo | None = None + message: str + type: Type64 = Field(..., title="StreamErrorEventMsgType") + + +class Type65(Enum): + patch_apply_begin = "patch_apply_begin" + + +class Type66(Enum): + patch_apply_end = "patch_apply_end" + + +class Type67(Enum): + turn_diff = "turn_diff" + + +class EventMsg49(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + type: Type67 = Field(..., title="TurnDiffEventMsgType") + unified_diff: str + + +class Type68(Enum): + get_history_entry_response = "get_history_entry_response" + + +class Type69(Enum): + mcp_list_tools_response = "mcp_list_tools_response" + + +class Type70(Enum): + list_custom_prompts_response = "list_custom_prompts_response" + + +class EventMsg52(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + custom_prompts: List[CustomPrompt] + type: Type70 = Field(..., title="ListCustomPromptsResponseEventMsgType") + + +class Type71(Enum): + list_skills_response = "list_skills_response" + + +class Type72(Enum): + list_remote_skills_response = "list_remote_skills_response" + + +class Type73(Enum): + remote_skill_downloaded = "remote_skill_downloaded" + + +class EventMsg55(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: str + name: str + path: str + type: Type73 = Field(..., title="RemoteSkillDownloadedEventMsgType") + + +class Type74(Enum): + skills_update_available = "skills_update_available" + + +class EventMsg56(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + type: Type74 = Field(..., title="SkillsUpdateAvailableEventMsgType") + + +class Type75(Enum): + plan_update = "plan_update" + + +class Type76(Enum): + turn_aborted = "turn_aborted" + + +class Type77(Enum): + shutdown_complete = "shutdown_complete" + + +class EventMsg59(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + type: Type77 = Field(..., title="ShutdownCompleteEventMsgType") + + +class Type78(Enum): + entered_review_mode = "entered_review_mode" + + +class Type79(Enum): + exited_review_mode = "exited_review_mode" + + +class Type80(Enum): + raw_response_item = "raw_response_item" + + +class Type81(Enum): + item_started = "item_started" + + +class Type82(Enum): + item_completed = "item_completed" + + +class Type83(Enum): + hook_started = "hook_started" + + +class Type84(Enum): + hook_completed = "hook_completed" + + +class Type85(Enum): + agent_message_content_delta = "agent_message_content_delta" + + +class EventMsg67(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + delta: str + item_id: str + thread_id: str + turn_id: str + type: Type85 = Field(..., title="AgentMessageContentDeltaEventMsgType") + + +class Type86(Enum): + plan_delta = "plan_delta" + + +class EventMsg68(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + delta: str + item_id: str + thread_id: str + turn_id: str + type: Type86 = Field(..., title="PlanDeltaEventMsgType") + + +class Type87(Enum): + reasoning_content_delta = "reasoning_content_delta" + + +class EventMsg69(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + delta: str + item_id: str + summary_index: int | None = 0 + thread_id: str + turn_id: str + type: Type87 = Field(..., title="ReasoningContentDeltaEventMsgType") + + +class Type88(Enum): + reasoning_raw_content_delta = "reasoning_raw_content_delta" + + +class EventMsg70(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + content_index: int | None = 0 + delta: str + item_id: str + thread_id: str + turn_id: str + type: Type88 = Field(..., title="ReasoningRawContentDeltaEventMsgType") + + +class Type89(Enum): + collab_agent_spawn_begin = "collab_agent_spawn_begin" + + +class Type90(Enum): + collab_agent_spawn_end = "collab_agent_spawn_end" + + +class Type91(Enum): + collab_agent_interaction_begin = "collab_agent_interaction_begin" + + +class Type92(Enum): + collab_agent_interaction_end = "collab_agent_interaction_end" + + +class Type93(Enum): + collab_waiting_begin = "collab_waiting_begin" + + +class Type94(Enum): + collab_waiting_end = "collab_waiting_end" + + +class Type95(Enum): + collab_close_begin = "collab_close_begin" + + +class Type96(Enum): + collab_close_end = "collab_close_end" + + +class Type97(Enum): + collab_resume_begin = "collab_resume_begin" + + +class Type98(Enum): + collab_resume_end = "collab_resume_end" + + +class ExecApprovalRequestSkillMetadata(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + path_to_skills_md: str + + +class ExecCommandSource(Enum): + agent = "agent" + user_shell = "user_shell" + unified_exec_startup = "unified_exec_startup" + unified_exec_interaction = "unified_exec_interaction" + + +class ExecCommandStatus(Enum): + completed = "completed" + failed = "failed" + declined = "declined" + + +class ExperimentalFeatureListParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + cursor: str | None = Field( + None, description="Opaque pagination cursor returned by a previous call." + ) + limit: conint(ge=0) | None = Field( + None, + description="Optional page size; defaults to a reasonable server-side value.", + ) + + +class ExperimentalFeatureStage(Enum): + beta = "beta" + under_development = "underDevelopment" + stable = "stable" + deprecated = "deprecated" + removed = "removed" + + +class ExternalAgentConfigDetectParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + cwds: List[str] | None = Field( + None, + description="Zero or more working directories to include for repo-scoped detection.", + ) + include_home: bool | None = Field( + None, + alias="includeHome", + description="If true, include detection under the user's home (~/.claude, ~/.codex, etc.).", + ) + + +ExternalAgentConfigImportResponse = CodexAppServerProtocolV2 + + +class ExternalAgentConfigMigrationItemType(Enum): + agents_md = "AGENTS_MD" + config = "CONFIG" + skills = "SKILLS" + mcp_server_config = "MCP_SERVER_CONFIG" + + +class FeedbackUploadParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + classification: str + extra_log_files: List[str] | None = Field(None, alias="extraLogFiles") + include_logs: bool = Field(..., alias="includeLogs") + reason: str | None = None + thread_id: str | None = Field(None, alias="threadId") + + +class FeedbackUploadResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + thread_id: str = Field(..., alias="threadId") + + +class Type99(Enum): + add = "add" + + +class FileChange1(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + content: str + type: Type99 = Field(..., title="AddFileChangeType") + + +class Type100(Enum): + delete = "delete" + + +class FileChange2(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + content: str + type: Type100 = Field(..., title="DeleteFileChangeType") + + +class Type101(Enum): + update = "update" + + +class FileChange3(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + move_path: str | None = None + type: Type101 = Field(..., title="UpdateFileChangeType") + unified_diff: str + + +class FileChange(RootModel[FileChange1 | FileChange2 | FileChange3]): + model_config = ConfigDict( + populate_by_name=True, + ) + root: FileChange1 | FileChange2 | FileChange3 + + +FileChangeOutputDeltaNotification = AgentMessageDeltaNotification + + +class FileSystemPermissions(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + read: List[AbsolutePathBuf] | None = None + write: List[AbsolutePathBuf] | None = None + + +class ForcedLoginMethod(Enum): + chatgpt = "chatgpt" + api = "api" + + +class FunctionCallOutputContentItem1(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + text: str + type: Type14 = Field(..., title="InputTextFunctionCallOutputContentItemType") + + +class FuzzyFileSearchParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + cancellation_token: str | None = Field(None, alias="cancellationToken") + query: str + roots: List[str] + + +class FuzzyFileSearchResult(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + file_name: str + indices: List[conint(ge=0)] | None = None + path: str + root: str + score: conint(ge=0) + + +class FuzzyFileSearchSessionCompletedNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + session_id: str = Field(..., alias="sessionId") + + +class FuzzyFileSearchSessionUpdatedNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + files: List[FuzzyFileSearchResult] + query: str + session_id: str = Field(..., alias="sessionId") + + +class GetAccountParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + refresh_token: bool | None = Field( + False, + alias="refreshToken", + description="When `true`, requests a proactive token refresh before returning.\n\nIn managed auth mode this triggers the normal refresh-token flow. In external auth mode this flag is ignored. Clients should refresh tokens themselves and call `account/login/start` with `chatgptAuthTokens`.", + ) + + +class GhostCommit(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: str + parent: str | None = None + preexisting_untracked_dirs: List[str] + preexisting_untracked_files: List[str] + + +class GitInfo(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + branch: str | None = None + origin_url: str | None = Field(None, alias="originUrl") + sha: str | None = None + + +class HazelnutScope(Enum): + example = "example" + workspace_shared = "workspace-shared" + all_shared = "all-shared" + personal = "personal" + + +class HistoryEntry(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + conversation_id: str + text: str + ts: conint(ge=0) + + +class HookEventName(Enum): + session_start = "sessionStart" + stop = "stop" + + +class HookExecutionMode(Enum): + sync = "sync" + async_ = "async" + + +class HookHandlerType(Enum): + command = "command" + prompt = "prompt" + agent = "agent" + + +class HookOutputEntryKind(Enum): + warning = "warning" + stop = "stop" + feedback = "feedback" + context = "context" + error = "error" + + +class HookRunStatus(Enum): + running = "running" + completed = "completed" + failed = "failed" + blocked = "blocked" + stopped = "stopped" + + +class HookScope(Enum): + thread = "thread" + turn = "turn" + + +class ImageDetail(Enum): + auto = "auto" + low = "low" + high = "high" + original = "original" + + +class InitializeCapabilities(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + experimental_api: bool | None = Field( + False, + alias="experimentalApi", + description="Opt into receiving experimental API methods and fields.", + ) + opt_out_notification_methods: List[str] | None = Field( + None, + alias="optOutNotificationMethods", + description="Exact notification method names that should be suppressed for this connection (for example `codex/event/session_configured`).", + ) + + +class InitializeParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + capabilities: InitializeCapabilities | None = None + client_info: ClientInfo = Field(..., alias="clientInfo") + + +class InputModality(Enum): + text = "text" + image = "image" + + +class ListMcpServerStatusParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + cursor: str | None = Field( + None, description="Opaque pagination cursor returned by a previous call." + ) + limit: conint(ge=0) | None = Field( + None, description="Optional page size; defaults to a server-defined value." + ) + + +class Type104(Enum): + exec = "exec" + + +class LocalShellAction1(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + command: List[str] + env: Dict[str, Any] | None = None + timeout_ms: conint(ge=0) | None = None + type: Type104 = Field(..., title="ExecLocalShellActionType") + user: str | None = None + working_directory: str | None = None + + +class LocalShellAction(RootModel[LocalShellAction1]): + model_config = ConfigDict( + populate_by_name=True, + ) + root: LocalShellAction1 + + +class LocalShellStatus(Enum): + completed = "completed" + in_progress = "in_progress" + incomplete = "incomplete" + + +class LoginAccountParams1(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + api_key: str = Field(..., alias="apiKey") + type: Type = Field(..., title="ApiKeyv2::LoginAccountParamsType") + + +class LoginAccountParams2(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + type: Type1 = Field(..., title="Chatgptv2::LoginAccountParamsType") + + +class Type107(Enum): + chatgpt_auth_tokens = "chatgptAuthTokens" + + +class LoginAccountParams3(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + access_token: str = Field( + ..., + alias="accessToken", + description="Access token (JWT) supplied by the client. This token is used for backend API requests and email extraction.", + ) + chatgpt_account_id: str = Field( + ..., + alias="chatgptAccountId", + description="Workspace/account identifier supplied by the client.", + ) + chatgpt_plan_type: str | None = Field( + None, + alias="chatgptPlanType", + description="Optional plan type supplied by the client.\n\nWhen `null`, Codex attempts to derive the plan type from access-token claims. If unavailable, the plan defaults to `unknown`.", + ) + type: Type107 = Field(..., title="ChatgptAuthTokensv2::LoginAccountParamsType") + + +class LoginAccountParams( + RootModel[LoginAccountParams1 | LoginAccountParams2 | LoginAccountParams3] +): + model_config = ConfigDict( + populate_by_name=True, + ) + root: LoginAccountParams1 | LoginAccountParams2 | LoginAccountParams3 = Field( + ..., title="LoginAccountParams" + ) + + +class LoginAccountResponse1(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + type: Type = Field(..., title="ApiKeyv2::LoginAccountResponseType") + + +class LoginAccountResponse2(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + auth_url: str = Field( + ..., + alias="authUrl", + description="URL the client should open in a browser to initiate the OAuth flow.", + ) + login_id: str = Field(..., alias="loginId") + type: Type1 = Field(..., title="Chatgptv2::LoginAccountResponseType") + + +class LoginAccountResponse3(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + type: Type107 = Field(..., title="ChatgptAuthTokensv2::LoginAccountResponseType") + + +class LoginAccountResponse( + RootModel[LoginAccountResponse1 | LoginAccountResponse2 | LoginAccountResponse3] +): + model_config = ConfigDict( + populate_by_name=True, + ) + root: LoginAccountResponse1 | LoginAccountResponse2 | LoginAccountResponse3 = Field( + ..., title="LoginAccountResponse" + ) + + +LogoutAccountResponse = CodexAppServerProtocolV2 + + +class MacOsAutomationPermission1(Enum): + none = "none" + all = "all" + + +class MacOsAutomationPermission2(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + bundle_ids: List[str] + + +class MacOsAutomationPermission( + RootModel[MacOsAutomationPermission1 | MacOsAutomationPermission2] +): + model_config = ConfigDict( + populate_by_name=True, + ) + root: MacOsAutomationPermission1 | MacOsAutomationPermission2 + + +class MacOsPreferencesPermission(Enum): + none = "none" + read_only = "read_only" + read_write = "read_write" + + +class MacOsSeatbeltProfileExtensions(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + macos_accessibility: bool | None = False + macos_automation: MacOsAutomationPermission | None = Field( + default_factory=lambda: MacOsAutomationPermission.model_validate("none") + ) + macos_calendar: bool | None = False + macos_preferences: MacOsPreferencesPermission | None = "read_only" + + +class McpAuthStatus(Enum): + unsupported = "unsupported" + not_logged_in = "notLoggedIn" + bearer_token = "bearerToken" + o_auth = "oAuth" + + +class McpInvocation(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + arguments: Any | None = Field(None, description="Arguments to the tool call.") + server: str = Field( + ..., description="Name of the MCP server as defined in the config." + ) + tool: str = Field(..., description="Name of the tool as given by the MCP server.") + + +class McpServerOauthLoginCompletedNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + error: str | None = None + name: str + success: bool + + +class McpServerOauthLoginParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + name: str + scopes: List[str] | None = None + timeout_secs: int | None = Field(None, alias="timeoutSecs") + + +class McpServerOauthLoginResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + authorization_url: str = Field(..., alias="authorizationUrl") + + +McpServerRefreshResponse = CodexAppServerProtocolV2 + + +class McpStartupFailure(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + error: str + server: str + + +class State(Enum): + starting = "starting" + + +class McpStartupStatus1(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + state: State + + +class State1(Enum): + ready = "ready" + + +class McpStartupStatus2(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + state: State1 + + +class State2(Enum): + failed = "failed" + + +class McpStartupStatus3(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + error: str + state: State2 + + +class State3(Enum): + cancelled = "cancelled" + + +class McpStartupStatus4(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + state: State3 + + +class McpStartupStatus( + RootModel[ + McpStartupStatus1 | McpStartupStatus2 | McpStartupStatus3 | McpStartupStatus4 + ] +): + model_config = ConfigDict( + populate_by_name=True, + ) + root: McpStartupStatus1 | McpStartupStatus2 | McpStartupStatus3 | McpStartupStatus4 + + +class McpToolCallError(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + message: str + + +class McpToolCallProgressNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + item_id: str = Field(..., alias="itemId") + message: str + thread_id: str = Field(..., alias="threadId") + turn_id: str = Field(..., alias="turnId") + + +class McpToolCallResult(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + content: List + structured_content: Any | None = Field(None, alias="structuredContent") + + +class MergeStrategy(Enum): + replace = "replace" + upsert = "upsert" + + +class MessagePhase(Enum): + commentary = "commentary" + final_answer = "final_answer" + + +class ModeKind(Enum): + plan = "plan" + default = "default" + + +ModelAvailabilityNux = McpToolCallError + + +class ModelListParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + cursor: str | None = Field( + None, description="Opaque pagination cursor returned by a previous call." + ) + include_hidden: bool | None = Field( + None, + alias="includeHidden", + description="When true, include models that are hidden from the default picker list.", + ) + limit: conint(ge=0) | None = Field( + None, + description="Optional page size; defaults to a reasonable server-side value.", + ) + + +class ModelRerouteReason(Enum): + high_risk_cyber_activity = "highRiskCyberActivity" + + +class ModelReroutedNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + from_model: str = Field(..., alias="fromModel") + reason: ModelRerouteReason + thread_id: str = Field(..., alias="threadId") + to_model: str = Field(..., alias="toModel") + turn_id: str = Field(..., alias="turnId") + + +class ModelUpgradeInfo(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + migration_markdown: str | None = Field(None, alias="migrationMarkdown") + model: str + model_link: str | None = Field(None, alias="modelLink") + upgrade_copy: str | None = Field(None, alias="upgradeCopy") + + +class NetworkAccess(Enum): + restricted = "restricted" + enabled = "enabled" + + +class NetworkApprovalProtocol(Enum): + http = "http" + https = "https" + socks5_tcp = "socks5Tcp" + socks5_udp = "socks5Udp" + + +class NetworkPermissions(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + enabled: bool | None = None + + +class NetworkPolicyRuleAction(Enum): + allow = "allow" + deny = "deny" + + +class NetworkRequirements(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + allow_local_binding: bool | None = Field(None, alias="allowLocalBinding") + allow_unix_sockets: List[str] | None = Field(None, alias="allowUnixSockets") + allow_upstream_proxy: bool | None = Field(None, alias="allowUpstreamProxy") + allowed_domains: List[str] | None = Field(None, alias="allowedDomains") + dangerously_allow_all_unix_sockets: bool | None = Field( + None, alias="dangerouslyAllowAllUnixSockets" + ) + dangerously_allow_non_loopback_proxy: bool | None = Field( + None, alias="dangerouslyAllowNonLoopbackProxy" + ) + denied_domains: List[str] | None = Field(None, alias="deniedDomains") + enabled: bool | None = None + http_port: conint(ge=0) | None = Field(None, alias="httpPort") + socks_port: conint(ge=0) | None = Field(None, alias="socksPort") + + +class ParsedCommand1(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + cmd: str + name: str + path: str = Field( + ..., + description="(Best effort) Path to the file being read by the command. When possible, this is an absolute path, though when relative, it should be resolved against the `cwd`` that will be used to run the command to derive the absolute path.", + ) + type: Type3 = Field(..., title="ReadParsedCommandType") + + +class Type112(Enum): + list_files = "list_files" + + +class ParsedCommand2(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + cmd: str + path: str | None = None + type: Type112 = Field(..., title="ListFilesParsedCommandType") + + +class ParsedCommand3(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + cmd: str + path: str | None = None + query: str | None = None + type: Type5 = Field(..., title="SearchParsedCommandType") + + +class ParsedCommand4(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + cmd: str + type: Type6 = Field(..., title="UnknownParsedCommandType") + + +class ParsedCommand( + RootModel[ParsedCommand1 | ParsedCommand2 | ParsedCommand3 | ParsedCommand4] +): + model_config = ConfigDict( + populate_by_name=True, + ) + root: ParsedCommand1 | ParsedCommand2 | ParsedCommand3 | ParsedCommand4 + + +class PatchChangeKind1(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + type: Type99 = Field(..., title="AddPatchChangeKindType") + + +class PatchChangeKind2(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + type: Type100 = Field(..., title="DeletePatchChangeKindType") + + +class PatchChangeKind3(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + move_path: str | None = None + type: Type101 = Field(..., title="UpdatePatchChangeKindType") + + +class PatchChangeKind( + RootModel[PatchChangeKind1 | PatchChangeKind2 | PatchChangeKind3] +): + model_config = ConfigDict( + populate_by_name=True, + ) + root: PatchChangeKind1 | PatchChangeKind2 | PatchChangeKind3 + + +class PermissionProfile(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + file_system: FileSystemPermissions | None = None + macos: MacOsSeatbeltProfileExtensions | None = None + network: NetworkPermissions | None = None + + +class Personality(Enum): + none = "none" + friendly = "friendly" + pragmatic = "pragmatic" + + +PlanDeltaNotification = AgentMessageDeltaNotification + + +class PlanType(Enum): + free = "free" + go = "go" + plus = "plus" + pro = "pro" + team = "team" + business = "business" + enterprise = "enterprise" + edu = "edu" + unknown = "unknown" + + +class PluginInstallParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + marketplace_path: AbsolutePathBuf = Field(..., alias="marketplacePath") + plugin_name: str = Field(..., alias="pluginName") + + +class PluginInstallResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + apps_needing_auth: List[AppSummary] = Field(..., alias="appsNeedingAuth") + + +class PluginInterface(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + brand_color: str | None = Field(None, alias="brandColor") + capabilities: List[str] + category: str | None = None + composer_icon: AbsolutePathBuf | None = Field(None, alias="composerIcon") + default_prompt: str | None = Field(None, alias="defaultPrompt") + developer_name: str | None = Field(None, alias="developerName") + display_name: str | None = Field(None, alias="displayName") + logo: AbsolutePathBuf | None = None + long_description: str | None = Field(None, alias="longDescription") + privacy_policy_url: str | None = Field(None, alias="privacyPolicyUrl") + screenshots: List[AbsolutePathBuf] + short_description: str | None = Field(None, alias="shortDescription") + terms_of_service_url: str | None = Field(None, alias="termsOfServiceUrl") + website_url: str | None = Field(None, alias="websiteUrl") + + +class PluginListParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + cwds: List[AbsolutePathBuf] | None = Field( + None, + description="Optional working directories used to discover repo marketplaces. When omitted, only home-scoped marketplaces and the official curated marketplace are considered.", + ) + + +class Type118(Enum): + local = "local" + + +class PluginSource1(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + path: AbsolutePathBuf + type: Type118 = Field(..., title="LocalPluginSourceType") + + +class PluginSource(RootModel[PluginSource1]): + model_config = ConfigDict( + populate_by_name=True, + ) + root: PluginSource1 + + +class PluginSummary(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + enabled: bool + id: str + installed: bool + interface: PluginInterface | None = None + name: str + source: PluginSource + + +class PluginUninstallParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + plugin_id: str = Field(..., alias="pluginId") + + +PluginUninstallResponse = CodexAppServerProtocolV2 + + +class ProductSurface(Enum): + chatgpt = "chatgpt" + codex = "codex" + api = "api" + atlas = "atlas" + + +class RateLimitWindow(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + resets_at: int | None = Field(None, alias="resetsAt") + used_percent: int = Field(..., alias="usedPercent") + window_duration_mins: int | None = Field(None, alias="windowDurationMins") + + +class Type119(Enum): + restricted = "restricted" + + +class ReadOnlyAccess1(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + include_platform_defaults: bool | None = Field( + True, alias="includePlatformDefaults" + ) + readable_roots: List[AbsolutePathBuf] | None = Field([], alias="readableRoots") + type: Type119 = Field(..., title="RestrictedReadOnlyAccessType") + + +class Type120(Enum): + full_access = "fullAccess" + + +class ReadOnlyAccess2(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + type: Type120 = Field(..., title="FullAccessReadOnlyAccessType") + + +class ReadOnlyAccess(RootModel[ReadOnlyAccess1 | ReadOnlyAccess2]): + model_config = ConfigDict( + populate_by_name=True, + ) + root: ReadOnlyAccess1 | ReadOnlyAccess2 + + +class RealtimeAudioFrame(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + data: str + num_channels: conint(ge=0) + sample_rate: conint(ge=0) + samples_per_channel: conint(ge=0) | None = None + + +class SessionUpdated(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + instructions: str | None = None + session_id: str + + +class RealtimeEvent1(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + session_updated: SessionUpdated = Field(..., alias="SessionUpdated") + + +class RealtimeEvent4(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + audio_out: RealtimeAudioFrame = Field(..., alias="AudioOut") + + +class RealtimeEvent5(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + conversation_item_added: Any = Field(..., alias="ConversationItemAdded") + + +class ConversationItemDone(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + item_id: str + + +class RealtimeEvent6(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + conversation_item_done: ConversationItemDone = Field( + ..., alias="ConversationItemDone" + ) + + +class RealtimeEvent8(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + error: str = Field(..., alias="Error") + + +class RealtimeTranscriptDelta(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + delta: str + + +class RealtimeTranscriptEntry(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + role: str + text: str + + +class ReasoningEffort(Enum): + none = "none" + minimal = "minimal" + low = "low" + medium = "medium" + high = "high" + xhigh = "xhigh" + + +class ReasoningEffortOption(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + description: str + reasoning_effort: ReasoningEffort = Field(..., alias="reasoningEffort") + + +class Type121(Enum): + reasoning_text = "reasoning_text" + + +class ReasoningItemContent1(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + text: str + type: Type121 = Field(..., title="ReasoningTextReasoningItemContentType") + + +class Type122(Enum): + text = "text" + + +class ReasoningItemContent2(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + text: str + type: Type122 = Field(..., title="TextReasoningItemContentType") + + +class ReasoningItemContent(RootModel[ReasoningItemContent1 | ReasoningItemContent2]): + model_config = ConfigDict( + populate_by_name=True, + ) + root: ReasoningItemContent1 | ReasoningItemContent2 + + +class Type123(Enum): + summary_text = "summary_text" + + +class ReasoningItemReasoningSummary1(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + text: str + type: Type123 = Field(..., title="SummaryTextReasoningItemReasoningSummaryType") + + +class ReasoningItemReasoningSummary(RootModel[ReasoningItemReasoningSummary1]): + model_config = ConfigDict( + populate_by_name=True, + ) + root: ReasoningItemReasoningSummary1 + + +class ReasoningSummary1(Enum): + auto = "auto" + concise = "concise" + detailed = "detailed" + + +class ReasoningSummary2(Enum): + none = "none" + + +class ReasoningSummary(RootModel[ReasoningSummary1 | ReasoningSummary2]): + model_config = ConfigDict( + populate_by_name=True, + ) + root: ReasoningSummary1 | ReasoningSummary2 = Field( + ..., + description="A summary of the reasoning performed by the model. This can be useful for debugging and understanding the model's reasoning process. See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#reasoning-summaries", + ) + + +class ReasoningSummaryPartAddedNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + item_id: str = Field(..., alias="itemId") + summary_index: int = Field(..., alias="summaryIndex") + thread_id: str = Field(..., alias="threadId") + turn_id: str = Field(..., alias="turnId") + + +class ReasoningSummaryTextDeltaNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + delta: str + item_id: str = Field(..., alias="itemId") + summary_index: int = Field(..., alias="summaryIndex") + thread_id: str = Field(..., alias="threadId") + turn_id: str = Field(..., alias="turnId") + + +class ReasoningTextDeltaNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + content_index: int = Field(..., alias="contentIndex") + delta: str + item_id: str = Field(..., alias="itemId") + thread_id: str = Field(..., alias="threadId") + turn_id: str = Field(..., alias="turnId") + + +class RemoteSkillSummary(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + description: str + id: str + name: str + + +class RequestId(RootModel[str | int]): + model_config = ConfigDict( + populate_by_name=True, + ) + root: str | int + + +class RequestUserInputQuestionOption(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + description: str + label: str + + +class ResidencyRequirement(Enum): + us = "us" + + +class Resource(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + field_meta: Any | None = Field(None, alias="_meta") + annotations: Any | None = None + description: str | None = None + icons: List | None = None + mime_type: str | None = Field(None, alias="mimeType") + name: str + size: int | None = None + title: str | None = None + uri: str + + +class ResourceTemplate(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + annotations: Any | None = None + description: str | None = None + mime_type: str | None = Field(None, alias="mimeType") + name: str + title: str | None = None + uri_template: str = Field(..., alias="uriTemplate") + + +class Type124(Enum): + message = "message" + + +class ResponseItem1(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + content: List[ContentItem] + end_turn: bool | None = None + id: str | None = None + phase: MessagePhase | None = None + role: str + type: Type124 = Field(..., title="MessageResponseItemType") + + +class Type125(Enum): + reasoning = "reasoning" + + +class ResponseItem2(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + content: List[ReasoningItemContent] | None = None + encrypted_content: str | None = None + id: str + summary: List[ReasoningItemReasoningSummary] + type: Type125 = Field(..., title="ReasoningResponseItemType") + + +class Type126(Enum): + local_shell_call = "local_shell_call" + + +class ResponseItem3(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + action: LocalShellAction + call_id: str | None = Field(None, description="Set when using the Responses API.") + id: str | None = Field( + None, + description="Legacy id field retained for compatibility with older payloads.", + ) + status: LocalShellStatus + type: Type126 = Field(..., title="LocalShellCallResponseItemType") + + +class Type127(Enum): + function_call = "function_call" + + +class ResponseItem4(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + arguments: str + call_id: str + id: str | None = None + name: str + type: Type127 = Field(..., title="FunctionCallResponseItemType") + + +class Type128(Enum): + function_call_output = "function_call_output" + + +class Type129(Enum): + custom_tool_call = "custom_tool_call" + + +class ResponseItem6(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + call_id: str + id: str | None = None + input: str + name: str + status: str | None = None + type: Type129 = Field(..., title="CustomToolCallResponseItemType") + + +class Type130(Enum): + custom_tool_call_output = "custom_tool_call_output" + + +class Type131(Enum): + web_search_call = "web_search_call" + + +class Type132(Enum): + image_generation_call = "image_generation_call" + + +class ResponseItem9(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: str + result: str + revised_prompt: str | None = None + status: str + type: Type132 = Field(..., title="ImageGenerationCallResponseItemType") + + +class Type133(Enum): + ghost_snapshot = "ghost_snapshot" + + +class ResponseItem10(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + ghost_commit: GhostCommit + type: Type133 = Field(..., title="GhostSnapshotResponseItemType") + + +class Type134(Enum): + compaction = "compaction" + + +class ResponseItem11(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + encrypted_content: str + type: Type134 = Field(..., title="CompactionResponseItemType") + + +class Type135(Enum): + other = "other" + + +class ResponseItem12(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + type: Type135 = Field(..., title="OtherResponseItemType") + + +class ResponsesApiWebSearchAction1(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + queries: List[str] | None = None + query: str | None = None + type: Type5 = Field(..., title="SearchResponsesApiWebSearchActionType") + + +class Type137(Enum): + open_page = "open_page" + + +class ResponsesApiWebSearchAction2(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + type: Type137 = Field(..., title="OpenPageResponsesApiWebSearchActionType") + url: str | None = None + + +class Type138(Enum): + find_in_page = "find_in_page" + + +class ResponsesApiWebSearchAction3(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + pattern: str | None = None + type: Type138 = Field(..., title="FindInPageResponsesApiWebSearchActionType") + url: str | None = None + + +class ResponsesApiWebSearchAction4(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + type: Type135 = Field(..., title="OtherResponsesApiWebSearchActionType") + + +class ResponsesApiWebSearchAction( + RootModel[ + ResponsesApiWebSearchAction1 + | ResponsesApiWebSearchAction2 + | ResponsesApiWebSearchAction3 + | ResponsesApiWebSearchAction4 + ] +): + model_config = ConfigDict( + populate_by_name=True, + ) + root: ( + ResponsesApiWebSearchAction1 + | ResponsesApiWebSearchAction2 + | ResponsesApiWebSearchAction3 + | ResponsesApiWebSearchAction4 + ) + + +class ResultOfCallToolResultOrString1(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + ok: CallToolResult = Field(..., alias="Ok") + + +class ResultOfCallToolResultOrString2(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + err: str = Field(..., alias="Err") + + +class ResultOfCallToolResultOrString( + RootModel[ResultOfCallToolResultOrString1 | ResultOfCallToolResultOrString2] +): + model_config = ConfigDict( + populate_by_name=True, + ) + root: ResultOfCallToolResultOrString1 | ResultOfCallToolResultOrString2 + + +class ReviewDecision1(Enum): + approved = "approved" + + +class ApprovedExecpolicyAmendment(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + proposed_execpolicy_amendment: List[str] + + +class ReviewDecision2(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + approved_execpolicy_amendment: ApprovedExecpolicyAmendment + + +class ReviewDecision3(Enum): + approved_for_session = "approved_for_session" + + +class ReviewDecision5(Enum): + denied = "denied" + + +class ReviewDecision6(Enum): + abort = "abort" + + +class ReviewDelivery(Enum): + inline = "inline" + detached = "detached" + + +ReviewLineRange = ByteRange + + +class Type140(Enum): + uncommitted_changes = "uncommittedChanges" + + +class ReviewTarget1(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + type: Type140 = Field(..., title="UncommittedChangesReviewTargetType") + + +class Type141(Enum): + base_branch = "baseBranch" + + +class ReviewTarget2(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + branch: str + type: Type141 = Field(..., title="BaseBranchReviewTargetType") + + +class Type142(Enum): + commit = "commit" + + +class ReviewTarget3(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + sha: str + title: str | None = Field( + None, + description="Optional human-readable label (e.g., commit subject) for UIs.", + ) + type: Type142 = Field(..., title="CommitReviewTargetType") + + +class Type143(Enum): + custom = "custom" + + +class ReviewTarget4(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + instructions: str + type: Type143 = Field(..., title="CustomReviewTargetType") + + +class ReviewTarget( + RootModel[ReviewTarget1 | ReviewTarget2 | ReviewTarget3 | ReviewTarget4] +): + model_config = ConfigDict( + populate_by_name=True, + ) + root: ReviewTarget1 | ReviewTarget2 | ReviewTarget3 | ReviewTarget4 + + +class SandboxMode(Enum): + read_only = "read-only" + workspace_write = "workspace-write" + danger_full_access = "danger-full-access" + + +class Type144(Enum): + danger_full_access = "dangerFullAccess" + + +class SandboxPolicy1(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + type: Type144 = Field(..., title="DangerFullAccessSandboxPolicyType") + + +class Type145(Enum): + read_only = "readOnly" + + +class SandboxPolicy2(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + access: ReadOnlyAccess | None = Field( + default_factory=lambda: ReadOnlyAccess.model_validate({"type": "fullAccess"}) + ) + network_access: bool | None = Field(False, alias="networkAccess") + type: Type145 = Field(..., title="ReadOnlySandboxPolicyType") + + +class Type146(Enum): + external_sandbox = "externalSandbox" + + +class SandboxPolicy3(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + network_access: NetworkAccess | None = Field("restricted", alias="networkAccess") + type: Type146 = Field(..., title="ExternalSandboxSandboxPolicyType") + + +class Type147(Enum): + workspace_write = "workspaceWrite" + + +class SandboxPolicy4(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + exclude_slash_tmp: bool | None = Field(False, alias="excludeSlashTmp") + exclude_tmpdir_env_var: bool | None = Field(False, alias="excludeTmpdirEnvVar") + network_access: bool | None = Field(False, alias="networkAccess") + read_only_access: ReadOnlyAccess | None = Field( + default_factory=lambda: ReadOnlyAccess.model_validate({"type": "fullAccess"}), + alias="readOnlyAccess", + ) + type: Type147 = Field(..., title="WorkspaceWriteSandboxPolicyType") + writable_roots: List[AbsolutePathBuf] | None = Field([], alias="writableRoots") + + +class SandboxPolicy( + RootModel[SandboxPolicy1 | SandboxPolicy2 | SandboxPolicy3 | SandboxPolicy4] +): + model_config = ConfigDict( + populate_by_name=True, + ) + root: SandboxPolicy1 | SandboxPolicy2 | SandboxPolicy3 | SandboxPolicy4 + + +class SandboxWorkspaceWrite(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + exclude_slash_tmp: bool | None = False + exclude_tmpdir_env_var: bool | None = False + network_access: bool | None = False + writable_roots: List[str] | None = [] + + +class Method50(Enum): + thread_started = "thread/started" + + +class Method51(Enum): + thread_status_changed = "thread/status/changed" + + +class Method52(Enum): + thread_archived = "thread/archived" + + +class Method53(Enum): + thread_unarchived = "thread/unarchived" + + +class Method54(Enum): + thread_closed = "thread/closed" + + +class Method55(Enum): + skills_changed = "skills/changed" + + +class Method56(Enum): + thread_name_updated = "thread/name/updated" + + +class Method57(Enum): + thread_token_usage_updated = "thread/tokenUsage/updated" + + +class Method58(Enum): + turn_started = "turn/started" + + +class Method59(Enum): + hook_started = "hook/started" + + +class Method60(Enum): + turn_completed = "turn/completed" + + +class Method61(Enum): + hook_completed = "hook/completed" + + +class Method62(Enum): + turn_diff_updated = "turn/diff/updated" + + +class Method63(Enum): + turn_plan_updated = "turn/plan/updated" + + +class Method64(Enum): + item_started = "item/started" + + +class Method65(Enum): + item_completed = "item/completed" + + +class Method66(Enum): + item_agent_message_delta = "item/agentMessage/delta" + + +class ServerNotification18(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method66 = Field(..., title="Item/agentMessage/deltaNotificationMethod") + params: AgentMessageDeltaNotification + + +class Method67(Enum): + item_plan_delta = "item/plan/delta" + + +class ServerNotification19(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method67 = Field(..., title="Item/plan/deltaNotificationMethod") + params: PlanDeltaNotification + + +class Method68(Enum): + command_exec_output_delta = "command/exec/outputDelta" + + +class Method69(Enum): + item_command_execution_output_delta = "item/commandExecution/outputDelta" + + +class ServerNotification21(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method69 = Field( + ..., title="Item/commandExecution/outputDeltaNotificationMethod" + ) + params: CommandExecutionOutputDeltaNotification + + +class Method70(Enum): + item_command_execution_terminal_interaction = ( + "item/commandExecution/terminalInteraction" + ) + + +class Method71(Enum): + item_file_change_output_delta = "item/fileChange/outputDelta" + + +class ServerNotification23(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method71 = Field(..., title="Item/fileChange/outputDeltaNotificationMethod") + params: FileChangeOutputDeltaNotification + + +class Method72(Enum): + server_request_resolved = "serverRequest/resolved" + + +class Method73(Enum): + item_mcp_tool_call_progress = "item/mcpToolCall/progress" + + +class ServerNotification25(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method73 = Field(..., title="Item/mcpToolCall/progressNotificationMethod") + params: McpToolCallProgressNotification + + +class Method74(Enum): + mcp_server_oauth_login_completed = "mcpServer/oauthLogin/completed" + + +class ServerNotification26(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method74 = Field( + ..., title="McpServer/oauthLogin/completedNotificationMethod" + ) + params: McpServerOauthLoginCompletedNotification + + +class Method75(Enum): + account_updated = "account/updated" + + +class Method76(Enum): + account_rate_limits_updated = "account/rateLimits/updated" + + +class Method77(Enum): + app_list_updated = "app/list/updated" + + +class Method78(Enum): + item_reasoning_summary_text_delta = "item/reasoning/summaryTextDelta" + + +class ServerNotification30(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method78 = Field( + ..., title="Item/reasoning/summaryTextDeltaNotificationMethod" + ) + params: ReasoningSummaryTextDeltaNotification + + +class Method79(Enum): + item_reasoning_summary_part_added = "item/reasoning/summaryPartAdded" + + +class ServerNotification31(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method79 = Field( + ..., title="Item/reasoning/summaryPartAddedNotificationMethod" + ) + params: ReasoningSummaryPartAddedNotification + + +class Method80(Enum): + item_reasoning_text_delta = "item/reasoning/textDelta" + + +class ServerNotification32(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method80 = Field(..., title="Item/reasoning/textDeltaNotificationMethod") + params: ReasoningTextDeltaNotification + + +class Method81(Enum): + thread_compacted = "thread/compacted" + + +class ServerNotification33(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method81 = Field(..., title="Thread/compactedNotificationMethod") + params: ContextCompactedNotification + + +class Method82(Enum): + model_rerouted = "model/rerouted" + + +class ServerNotification34(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method82 = Field(..., title="Model/reroutedNotificationMethod") + params: ModelReroutedNotification + + +class Method83(Enum): + deprecation_notice = "deprecationNotice" + + +class ServerNotification35(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method83 = Field(..., title="DeprecationNoticeNotificationMethod") + params: DeprecationNoticeNotification + + +class Method84(Enum): + config_warning = "configWarning" + + +class Method85(Enum): + fuzzy_file_search_session_updated = "fuzzyFileSearch/sessionUpdated" + + +class ServerNotification37(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method85 = Field( + ..., title="FuzzyFileSearch/sessionUpdatedNotificationMethod" + ) + params: FuzzyFileSearchSessionUpdatedNotification + + +class Method86(Enum): + fuzzy_file_search_session_completed = "fuzzyFileSearch/sessionCompleted" + + +class ServerNotification38(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method86 = Field( + ..., title="FuzzyFileSearch/sessionCompletedNotificationMethod" + ) + params: FuzzyFileSearchSessionCompletedNotification + + +class Method87(Enum): + thread_realtime_started = "thread/realtime/started" + + +class Method88(Enum): + thread_realtime_item_added = "thread/realtime/itemAdded" + + +class Method89(Enum): + thread_realtime_output_audio_delta = "thread/realtime/outputAudio/delta" + + +class Method90(Enum): + thread_realtime_error = "thread/realtime/error" + + +class Method91(Enum): + thread_realtime_closed = "thread/realtime/closed" + + +class Method92(Enum): + windows_world_writable_warning = "windows/worldWritableWarning" + + +class Method93(Enum): + windows_sandbox_setup_completed = "windowsSandbox/setupCompleted" + + +class Method94(Enum): + account_login_completed = "account/login/completed" + + +class ServerNotification46(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method94 = Field(..., title="Account/login/completedNotificationMethod") + params: AccountLoginCompletedNotification + + +class ServerRequestResolvedNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + request_id: RequestId = Field(..., alias="requestId") + thread_id: str = Field(..., alias="threadId") + + +class ServiceTier(Enum): + fast = "fast" + flex = "flex" + + +class SessionNetworkProxyRuntime(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + http_addr: str + socks_addr: str + + +class SessionSource1(Enum): + cli = "cli" + vscode = "vscode" + exec = "exec" + app_server = "appServer" + unknown = "unknown" + + +class Settings(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + developer_instructions: str | None = None + model: str + reasoning_effort: ReasoningEffort | None = None + + +class SkillErrorInfo(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + message: str + path: str + + +class SkillInterface(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + brand_color: str | None = Field(None, alias="brandColor") + default_prompt: str | None = Field(None, alias="defaultPrompt") + display_name: str | None = Field(None, alias="displayName") + icon_large: str | None = Field(None, alias="iconLarge") + icon_small: str | None = Field(None, alias="iconSmall") + short_description: str | None = Field(None, alias="shortDescription") + + +class SkillScope(Enum): + user = "user" + repo = "repo" + system = "system" + admin = "admin" + + +class SkillToolDependency(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + command: str | None = None + description: str | None = None + transport: str | None = None + type: str + url: str | None = None + value: str + + +SkillsChangedNotification = CodexAppServerProtocolV2 + + +class SkillsConfigWriteParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + enabled: bool + path: str + + +class SkillsConfigWriteResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + effective_enabled: bool = Field(..., alias="effectiveEnabled") + + +class SkillsListExtraRootsForCwd(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + cwd: str + extra_user_roots: List[str] = Field(..., alias="extraUserRoots") + + +class SkillsListParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + cwds: List[str] | None = Field( + None, + description="When empty, defaults to the current session working directory.", + ) + force_reload: bool | None = Field( + None, + alias="forceReload", + description="When true, bypass the skills cache and re-scan skills from disk.", + ) + per_cwd_extra_user_roots: List[SkillsListExtraRootsForCwd] | None = Field( + None, + alias="perCwdExtraUserRoots", + description="Optional per-cwd extra roots to scan as user-scoped skills.", + ) + + +class SkillsRemoteReadParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + enabled: bool | None = False + hazelnut_scope: HazelnutScope | None = Field("example", alias="hazelnutScope") + product_surface: ProductSurface | None = Field("codex", alias="productSurface") + + +class SkillsRemoteReadResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + data: List[RemoteSkillSummary] + + +class SkillsRemoteWriteParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + hazelnut_id: str = Field(..., alias="hazelnutId") + + +class SkillsRemoteWriteResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: str + path: str + + +class StepStatus(Enum): + pending = "pending" + in_progress = "in_progress" + completed = "completed" + + +class SubAgentSource1(Enum): + review = "review" + compact = "compact" + memory_consolidation = "memory_consolidation" + + +class SubAgentSource3(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + other: str + + +class TerminalInteractionNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + item_id: str = Field(..., alias="itemId") + process_id: str = Field(..., alias="processId") + stdin: str + thread_id: str = Field(..., alias="threadId") + turn_id: str = Field(..., alias="turnId") + + +class TextElement(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + byte_range: ByteRange = Field( + ..., + alias="byteRange", + description="Byte range in the parent `text` buffer that this element occupies.", + ) + placeholder: str | None = Field( + None, + description="Optional human-readable placeholder for the element, displayed in the UI.", + ) + + +class TextPosition(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + column: conint(ge=0) = Field( + ..., description="1-based column number (in Unicode scalar values)." + ) + line: conint(ge=0) = Field(..., description="1-based line number.") + + +class TextRange(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + end: TextPosition + start: TextPosition + + +class ThreadActiveFlag(Enum): + waiting_on_approval = "waitingOnApproval" + waiting_on_user_input = "waitingOnUserInput" + + +ThreadArchiveParams = FeedbackUploadResponse + + +ThreadArchiveResponse = CodexAppServerProtocolV2 + + +ThreadArchivedNotification = FeedbackUploadResponse + + +ThreadClosedNotification = FeedbackUploadResponse + + +ThreadCompactStartParams = FeedbackUploadResponse + + +ThreadCompactStartResponse = CodexAppServerProtocolV2 + + +class ThreadForkParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + approval_policy: AskForApproval | None = Field(None, alias="approvalPolicy") + base_instructions: str | None = Field(None, alias="baseInstructions") + config: Dict[str, Any] | None = None + cwd: str | None = None + developer_instructions: str | None = Field(None, alias="developerInstructions") + model: str | None = Field( + None, description="Configuration overrides for the forked thread, if any." + ) + model_provider: str | None = Field(None, alias="modelProvider") + sandbox: SandboxMode | None = None + service_tier: ServiceTier | None = Field(None, alias="serviceTier") + thread_id: str = Field(..., alias="threadId") + + +class ThreadId(RootModel[str]): + model_config = ConfigDict( + populate_by_name=True, + ) + root: str + + +class Type148(Enum): + user_message = "userMessage" + + +class Type149(Enum): + agent_message = "agentMessage" + + +class ThreadItem2(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: str + phase: MessagePhase | None = None + text: str + type: Type149 = Field(..., title="AgentMessageThreadItemType") + + +class Type150(Enum): + plan = "plan" + + +class ThreadItem3(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: str + text: str + type: Type150 = Field(..., title="PlanThreadItemType") + + +class ThreadItem4(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + content: List[str] | None = [] + id: str + summary: List[str] | None = [] + type: Type125 = Field(..., title="ReasoningThreadItemType") + + +class Type152(Enum): + command_execution = "commandExecution" + + +class ThreadItem5(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + aggregated_output: str | None = Field( + None, + alias="aggregatedOutput", + description="The command's output, aggregated from stdout and stderr.", + ) + command: str = Field(..., description="The command to be executed.") + command_actions: List[CommandAction] = Field( + ..., + alias="commandActions", + description="A best-effort parsing of the command to understand the action(s) it will perform. This returns a list of CommandAction objects because a single shell command may be composed of many commands piped together.", + ) + cwd: str = Field(..., description="The command's working directory.") + duration_ms: int | None = Field( + None, + alias="durationMs", + description="The duration of the command execution in milliseconds.", + ) + exit_code: int | None = Field( + None, alias="exitCode", description="The command's exit code." + ) + id: str + process_id: str | None = Field( + None, + alias="processId", + description="Identifier for the underlying PTY process (when available).", + ) + status: CommandExecutionStatus + type: Type152 = Field(..., title="CommandExecutionThreadItemType") + + +class Type153(Enum): + file_change = "fileChange" + + +class Type154(Enum): + mcp_tool_call = "mcpToolCall" + + +class ThreadItem7(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + arguments: Any + duration_ms: int | None = Field( + None, + alias="durationMs", + description="The duration of the MCP tool call in milliseconds.", + ) + error: McpToolCallError | None = None + id: str + result: McpToolCallResult | None = None + server: str + status: CollabAgentToolCallStatus + tool: str + type: Type154 = Field(..., title="McpToolCallThreadItemType") + + +class Type155(Enum): + dynamic_tool_call = "dynamicToolCall" + + +class ThreadItem8(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + arguments: Any + content_items: List[DynamicToolCallOutputContentItem] | None = Field( + None, alias="contentItems" + ) + duration_ms: int | None = Field( + None, + alias="durationMs", + description="The duration of the dynamic tool call in milliseconds.", + ) + id: str + status: CollabAgentToolCallStatus + success: bool | None = None + tool: str + type: Type155 = Field(..., title="DynamicToolCallThreadItemType") + + +class Type156(Enum): + collab_agent_tool_call = "collabAgentToolCall" + + +class Type157(Enum): + web_search = "webSearch" + + +class Type158(Enum): + image_view = "imageView" + + +class ThreadItem11(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: str + path: str + type: Type158 = Field(..., title="ImageViewThreadItemType") + + +class Type159(Enum): + image_generation = "imageGeneration" + + +class ThreadItem12(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: str + result: str + revised_prompt: str | None = Field(None, alias="revisedPrompt") + status: str + type: Type159 = Field(..., title="ImageGenerationThreadItemType") + + +class Type160(Enum): + entered_review_mode = "enteredReviewMode" + + +class ThreadItem13(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: str + review: str + type: Type160 = Field(..., title="EnteredReviewModeThreadItemType") + + +class Type161(Enum): + exited_review_mode = "exitedReviewMode" + + +class ThreadItem14(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: str + review: str + type: Type161 = Field(..., title="ExitedReviewModeThreadItemType") + + +class Type162(Enum): + context_compaction = "contextCompaction" + + +class ThreadItem15(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: str + type: Type162 = Field(..., title="ContextCompactionThreadItemType") + + +class ThreadLoadedListParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + cursor: str | None = Field( + None, description="Opaque pagination cursor returned by a previous call." + ) + limit: conint(ge=0) | None = Field( + None, description="Optional page size; defaults to no limit." + ) + + +class ThreadLoadedListResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + data: List[str] = Field( + ..., description="Thread ids for sessions currently loaded in memory." + ) + next_cursor: str | None = Field( + None, + alias="nextCursor", + description="Opaque cursor to pass to the next call to continue after the last item. if None, there are no more items to return.", + ) + + +class ThreadMetadataGitInfoUpdateParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + branch: str | None = Field( + None, + description="Omit to leave the stored branch unchanged, set to `null` to clear it, or provide a non-empty string to replace it.", + ) + origin_url: str | None = Field( + None, + alias="originUrl", + description="Omit to leave the stored origin URL unchanged, set to `null` to clear it, or provide a non-empty string to replace it.", + ) + sha: str | None = Field( + None, + description="Omit to leave the stored commit unchanged, set to `null` to clear it, or provide a non-empty string to replace it.", + ) + + +class ThreadMetadataUpdateParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + git_info: ThreadMetadataGitInfoUpdateParams | None = Field( + None, + alias="gitInfo", + description="Patch the stored Git metadata for this thread. Omit a field to leave it unchanged, set it to `null` to clear it, or provide a string to replace the stored value.", + ) + thread_id: str = Field(..., alias="threadId") + + +class ThreadNameUpdatedNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + thread_id: str = Field(..., alias="threadId") + thread_name: str | None = Field(None, alias="threadName") + + +class ThreadReadParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + include_turns: bool | None = Field( + False, + alias="includeTurns", + description="When true, include turns and their items from rollout history.", + ) + thread_id: str = Field(..., alias="threadId") + + +class ThreadRealtimeAudioChunk(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + data: str + num_channels: conint(ge=0) = Field(..., alias="numChannels") + sample_rate: conint(ge=0) = Field(..., alias="sampleRate") + samples_per_channel: conint(ge=0) | None = Field(None, alias="samplesPerChannel") + + +class ThreadRealtimeClosedNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + reason: str | None = None + thread_id: str = Field(..., alias="threadId") + + +class ThreadRealtimeErrorNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + message: str + thread_id: str = Field(..., alias="threadId") + + +class ThreadRealtimeItemAddedNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + item: Any + thread_id: str = Field(..., alias="threadId") + + +class ThreadRealtimeOutputAudioDeltaNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + audio: ThreadRealtimeAudioChunk + thread_id: str = Field(..., alias="threadId") + + +class ThreadRealtimeStartedNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + session_id: str | None = Field(None, alias="sessionId") + thread_id: str = Field(..., alias="threadId") + + +class ThreadResumeParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + approval_policy: AskForApproval | None = Field(None, alias="approvalPolicy") + base_instructions: str | None = Field(None, alias="baseInstructions") + config: Dict[str, Any] | None = None + cwd: str | None = None + developer_instructions: str | None = Field(None, alias="developerInstructions") + model: str | None = Field( + None, description="Configuration overrides for the resumed thread, if any." + ) + model_provider: str | None = Field(None, alias="modelProvider") + personality: Personality | None = None + sandbox: SandboxMode | None = None + service_tier: ServiceTier | None = Field(None, alias="serviceTier") + thread_id: str = Field(..., alias="threadId") + + +class ThreadRollbackParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + num_turns: conint(ge=0) = Field( + ..., + alias="numTurns", + description="The number of turns to drop from the end of the thread. Must be >= 1.\n\nThis only modifies the thread's history and does not revert local file changes that have been made by the agent. Clients are responsible for reverting these changes.", + ) + thread_id: str = Field(..., alias="threadId") + + +class ThreadSetNameParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + name: str + thread_id: str = Field(..., alias="threadId") + + +ThreadSetNameResponse = CodexAppServerProtocolV2 + + +class ThreadSortKey(Enum): + created_at = "created_at" + updated_at = "updated_at" + + +class ThreadSourceKind(Enum): + cli = "cli" + vscode = "vscode" + exec = "exec" + app_server = "appServer" + sub_agent = "subAgent" + sub_agent_review = "subAgentReview" + sub_agent_compact = "subAgentCompact" + sub_agent_thread_spawn = "subAgentThreadSpawn" + sub_agent_other = "subAgentOther" + unknown = "unknown" + + +class ThreadStartParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + approval_policy: AskForApproval | None = Field(None, alias="approvalPolicy") + base_instructions: str | None = Field(None, alias="baseInstructions") + config: Dict[str, Any] | None = None + cwd: str | None = None + developer_instructions: str | None = Field(None, alias="developerInstructions") + ephemeral: bool | None = None + model: str | None = None + model_provider: str | None = Field(None, alias="modelProvider") + personality: Personality | None = None + sandbox: SandboxMode | None = None + service_name: str | None = Field(None, alias="serviceName") + service_tier: ServiceTier | None = Field(None, alias="serviceTier") + + +class Type163(Enum): + not_loaded = "notLoaded" + + +class ThreadStatus1(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + type: Type163 = Field(..., title="NotLoadedThreadStatusType") + + +class Type164(Enum): + idle = "idle" + + +class ThreadStatus2(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + type: Type164 = Field(..., title="IdleThreadStatusType") + + +class Type165(Enum): + system_error = "systemError" + + +class ThreadStatus3(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + type: Type165 = Field(..., title="SystemErrorThreadStatusType") + + +class Type166(Enum): + active = "active" + + +class ThreadStatus4(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + active_flags: List[ThreadActiveFlag] = Field(..., alias="activeFlags") + type: Type166 = Field(..., title="ActiveThreadStatusType") + + +class ThreadStatus( + RootModel[ThreadStatus1 | ThreadStatus2 | ThreadStatus3 | ThreadStatus4] +): + model_config = ConfigDict( + populate_by_name=True, + ) + root: ThreadStatus1 | ThreadStatus2 | ThreadStatus3 | ThreadStatus4 + + +class ThreadStatusChangedNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + status: ThreadStatus + thread_id: str = Field(..., alias="threadId") + + +ThreadUnarchiveParams = FeedbackUploadResponse + + +ThreadUnarchivedNotification = FeedbackUploadResponse + + +ThreadUnsubscribeParams = FeedbackUploadResponse + + +class ThreadUnsubscribeStatus(Enum): + not_loaded = "notLoaded" + not_subscribed = "notSubscribed" + unsubscribed = "unsubscribed" + + +class TokenUsage(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + cached_input_tokens: int + input_tokens: int + output_tokens: int + reasoning_output_tokens: int + total_tokens: int + + +class TokenUsageBreakdown(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + cached_input_tokens: int = Field(..., alias="cachedInputTokens") + input_tokens: int = Field(..., alias="inputTokens") + output_tokens: int = Field(..., alias="outputTokens") + reasoning_output_tokens: int = Field(..., alias="reasoningOutputTokens") + total_tokens: int = Field(..., alias="totalTokens") + + +class TokenUsageInfo(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + last_token_usage: TokenUsage + model_context_window: int | None = None + total_token_usage: TokenUsage + + +class Tool(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + field_meta: Any | None = Field(None, alias="_meta") + annotations: Any | None = None + description: str | None = None + icons: List | None = None + input_schema: Any = Field(..., alias="inputSchema") + name: str + output_schema: Any | None = Field(None, alias="outputSchema") + title: str | None = None + + +class TurnAbortReason(Enum): + interrupted = "interrupted" + replaced = "replaced" + review_ended = "review_ended" + + +class TurnDiffUpdatedNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + diff: str + thread_id: str = Field(..., alias="threadId") + turn_id: str = Field(..., alias="turnId") + + +class TurnError(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + additional_details: str | None = Field(None, alias="additionalDetails") + codex_error_info: CodexErrorInfo | None = Field(None, alias="codexErrorInfo") + message: str + + +TurnInterruptParams = ContextCompactedNotification + + +TurnInterruptResponse = CodexAppServerProtocolV2 + + +class Type167(Enum): + user_message = "UserMessage" + + +class Type168(Enum): + agent_message = "AgentMessage" + + +class TurnItem2(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + content: List[AgentMessageContent] + id: str + phase: MessagePhase | None = Field( + None, + description="Optional phase metadata carried through from `ResponseItem::Message`.\n\nThis is currently used by TUI rendering to distinguish mid-turn commentary from a final answer and avoid status-indicator jitter.", + ) + type: Type168 = Field(..., title="AgentMessageTurnItemType") + + +class Type169(Enum): + plan = "Plan" + + +class TurnItem3(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: str + text: str + type: Type169 = Field(..., title="PlanTurnItemType") + + +class Type170(Enum): + reasoning = "Reasoning" + + +class TurnItem4(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: str + raw_content: List[str] | None = [] + summary_text: List[str] + type: Type170 = Field(..., title="ReasoningTurnItemType") + + +class Type171(Enum): + web_search = "WebSearch" + + +class TurnItem5(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + action: ResponsesApiWebSearchAction + id: str + query: str + type: Type171 = Field(..., title="WebSearchTurnItemType") + + +class Type172(Enum): + image_generation = "ImageGeneration" + + +class TurnItem6(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: str + result: str + revised_prompt: str | None = None + saved_path: str | None = None + status: str + type: Type172 = Field(..., title="ImageGenerationTurnItemType") + + +class Type173(Enum): + context_compaction = "ContextCompaction" + + +class TurnItem7(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: str + type: Type173 = Field(..., title="ContextCompactionTurnItemType") + + +class TurnPlanStepStatus(Enum): + pending = "pending" + in_progress = "inProgress" + completed = "completed" + + +class TurnStatus(Enum): + completed = "completed" + interrupted = "interrupted" + failed = "failed" + in_progress = "inProgress" + + +class TurnSteerResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + turn_id: str = Field(..., alias="turnId") + + +class UserInput1(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + text: str + text_elements: List[TextElement] | None = Field( + [], + description="UI-defined spans within `text` used to render or persist special elements.", + ) + type: Type122 = Field(..., title="TextUserInputType") + + +class Type175(Enum): + image = "image" + + +class UserInput2(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + type: Type175 = Field(..., title="ImageUserInputType") + url: str + + +class Type176(Enum): + local_image = "localImage" + + +class UserInput3(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + path: str + type: Type176 = Field(..., title="LocalImageUserInputType") + + +class Type177(Enum): + skill = "skill" + + +class UserInput4(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + name: str + path: str + type: Type177 = Field(..., title="SkillUserInputType") + + +class Type178(Enum): + mention = "mention" + + +class UserInput5(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + name: str + path: str + type: Type178 = Field(..., title="MentionUserInputType") + + +class UserInput( + RootModel[UserInput1 | UserInput2 | UserInput3 | UserInput4 | UserInput5] +): + model_config = ConfigDict( + populate_by_name=True, + ) + root: UserInput1 | UserInput2 | UserInput3 | UserInput4 | UserInput5 + + +class Verbosity(Enum): + low = "low" + medium = "medium" + high = "high" + + +class WebSearchAction1(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + queries: List[str] | None = None + query: str | None = None + type: Type5 = Field(..., title="SearchWebSearchActionType") + + +class Type180(Enum): + open_page = "openPage" + + +class WebSearchAction2(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + type: Type180 = Field(..., title="OpenPageWebSearchActionType") + url: str | None = None + + +class Type181(Enum): + find_in_page = "findInPage" + + +class WebSearchAction3(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + pattern: str | None = None + type: Type181 = Field(..., title="FindInPageWebSearchActionType") + url: str | None = None + + +class WebSearchAction4(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + type: Type135 = Field(..., title="OtherWebSearchActionType") + + +class WebSearchAction( + RootModel[WebSearchAction1 | WebSearchAction2 | WebSearchAction3 | WebSearchAction4] +): + model_config = ConfigDict( + populate_by_name=True, + ) + root: WebSearchAction1 | WebSearchAction2 | WebSearchAction3 | WebSearchAction4 + + +class WebSearchLocation(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + city: str | None = None + country: str | None = None + region: str | None = None + timezone: str | None = None + + +class WebSearchMode(Enum): + disabled = "disabled" + cached = "cached" + live = "live" + + +class WebSearchToolConfig(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + allowed_domains: List[str] | None = None + context_size: Verbosity | None = None + location: WebSearchLocation | None = None + + +class WindowsSandboxSetupMode(Enum): + elevated = "elevated" + unelevated = "unelevated" + + +class WindowsSandboxSetupStartParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + cwd: AbsolutePathBuf | None = None + mode: WindowsSandboxSetupMode + + +class WindowsSandboxSetupStartResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + started: bool + + +class WindowsWorldWritableWarningNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + extra_count: conint(ge=0) = Field(..., alias="extraCount") + failed_scan: bool = Field(..., alias="failedScan") + sample_paths: List[str] = Field(..., alias="samplePaths") + + +class WriteStatus(Enum): + ok = "ok" + ok_overridden = "okOverridden" + + +class Account2(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + email: str + plan_type: PlanType = Field(..., alias="planType") + type: Type1 = Field(..., title="ChatgptAccountType") + + +class Account(RootModel[Account1 | Account2]): + model_config = ConfigDict( + populate_by_name=True, + ) + root: Account1 | Account2 + + +class AccountUpdatedNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + auth_mode: AuthMode | None = Field(None, alias="authMode") + plan_type: PlanType | None = Field(None, alias="planType") + + +class AppConfig(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + default_tools_approval_mode: AppToolApproval | None = None + default_tools_enabled: bool | None = None + destructive_enabled: bool | None = None + enabled: bool | None = True + open_world_enabled: bool | None = None + tools: AppToolsConfig | None = None + + +class AppMetadata(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + categories: List[str] | None = None + developer: str | None = None + first_party_requires_install: bool | None = Field( + None, alias="firstPartyRequiresInstall" + ) + first_party_type: str | None = Field(None, alias="firstPartyType") + review: AppReview | None = None + screenshots: List[AppScreenshot] | None = None + seo_description: str | None = Field(None, alias="seoDescription") + show_in_composer_when_unlinked: bool | None = Field( + None, alias="showInComposerWhenUnlinked" + ) + sub_categories: List[str] | None = Field(None, alias="subCategories") + version: str | None = None + version_id: str | None = Field(None, alias="versionId") + version_notes: str | None = Field(None, alias="versionNotes") + + +class AppsConfig(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + field_default: AppsDefaultConfig | None = Field(None, alias="_default") + + +class CancelLoginAccountResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + status: CancelLoginAccountStatus + + +class ClientRequest1(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method = Field(..., title="InitializeRequestMethod") + params: InitializeParams + + +class ClientRequest2(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method1 = Field(..., title="Thread/startRequestMethod") + params: ThreadStartParams + + +class ClientRequest3(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method2 = Field(..., title="Thread/resumeRequestMethod") + params: ThreadResumeParams + + +class ClientRequest4(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method3 = Field(..., title="Thread/forkRequestMethod") + params: ThreadForkParams + + +class ClientRequest5(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method4 = Field(..., title="Thread/archiveRequestMethod") + params: ThreadArchiveParams + + +class ClientRequest6(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method5 = Field(..., title="Thread/unsubscribeRequestMethod") + params: ThreadUnsubscribeParams + + +class ClientRequest7(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method6 = Field(..., title="Thread/name/setRequestMethod") + params: ThreadSetNameParams + + +class ClientRequest8(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method7 = Field(..., title="Thread/metadata/updateRequestMethod") + params: ThreadMetadataUpdateParams + + +class ClientRequest9(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method8 = Field(..., title="Thread/unarchiveRequestMethod") + params: ThreadUnarchiveParams + + +class ClientRequest10(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method9 = Field(..., title="Thread/compact/startRequestMethod") + params: ThreadCompactStartParams + + +class ClientRequest11(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method10 = Field(..., title="Thread/rollbackRequestMethod") + params: ThreadRollbackParams + + +class ClientRequest13(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method12 = Field(..., title="Thread/loaded/listRequestMethod") + params: ThreadLoadedListParams + + +class ClientRequest14(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method13 = Field(..., title="Thread/readRequestMethod") + params: ThreadReadParams + + +class ClientRequest15(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method14 = Field(..., title="Skills/listRequestMethod") + params: SkillsListParams + + +class ClientRequest16(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method15 = Field(..., title="Plugin/listRequestMethod") + params: PluginListParams + + +class ClientRequest17(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method16 = Field(..., title="Skills/remote/listRequestMethod") + params: SkillsRemoteReadParams + + +class ClientRequest18(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method17 = Field(..., title="Skills/remote/exportRequestMethod") + params: SkillsRemoteWriteParams + + +class ClientRequest19(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method18 = Field(..., title="App/listRequestMethod") + params: AppsListParams + + +class ClientRequest20(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method19 = Field(..., title="Skills/config/writeRequestMethod") + params: SkillsConfigWriteParams + + +class ClientRequest21(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method20 = Field(..., title="Plugin/installRequestMethod") + params: PluginInstallParams + + +class ClientRequest22(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method21 = Field(..., title="Plugin/uninstallRequestMethod") + params: PluginUninstallParams + + +class ClientRequest25(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method24 = Field(..., title="Turn/interruptRequestMethod") + params: TurnInterruptParams + + +class ClientRequest27(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method26 = Field(..., title="Model/listRequestMethod") + params: ModelListParams + + +class ClientRequest28(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method27 = Field(..., title="ExperimentalFeature/listRequestMethod") + params: ExperimentalFeatureListParams + + +class ClientRequest29(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method28 = Field(..., title="McpServer/oauth/loginRequestMethod") + params: McpServerOauthLoginParams + + +class ClientRequest30(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method29 = Field(..., title="Config/mcpServer/reloadRequestMethod") + params: None = None + + +class ClientRequest31(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method30 = Field(..., title="McpServerStatus/listRequestMethod") + params: ListMcpServerStatusParams + + +class ClientRequest32(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method31 = Field(..., title="WindowsSandbox/setupStartRequestMethod") + params: WindowsSandboxSetupStartParams + + +class ClientRequest33(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method32 = Field(..., title="Account/login/startRequestMethod") + params: LoginAccountParams + + +class ClientRequest34(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method33 = Field(..., title="Account/login/cancelRequestMethod") + params: CancelLoginAccountParams + + +class ClientRequest35(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method34 = Field(..., title="Account/logoutRequestMethod") + params: None = None + + +class ClientRequest36(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method35 = Field(..., title="Account/rateLimits/readRequestMethod") + params: None = None + + +class ClientRequest37(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method36 = Field(..., title="Feedback/uploadRequestMethod") + params: FeedbackUploadParams + + +class ClientRequest39(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method38 = Field(..., title="Command/exec/writeRequestMethod") + params: CommandExecWriteParams + + +class ClientRequest40(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method39 = Field(..., title="Command/exec/terminateRequestMethod") + params: CommandExecTerminateParams + + +class ClientRequest42(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method41 = Field(..., title="Config/readRequestMethod") + params: ConfigReadParams + + +class ClientRequest43(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method42 = Field(..., title="ExternalAgentConfig/detectRequestMethod") + params: ExternalAgentConfigDetectParams + + +class ClientRequest47(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method46 = Field(..., title="ConfigRequirements/readRequestMethod") + params: None = None + + +class ClientRequest48(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method47 = Field(..., title="Account/readRequestMethod") + params: GetAccountParams + + +class ClientRequest49(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method48 = Field(..., title="FuzzyFileSearchRequestMethod") + params: FuzzyFileSearchParams + + +class CollabAgentRef(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + agent_nickname: str | None = Field( + None, + description="Optional nickname assigned to an AgentControl-spawned sub-agent.", + ) + agent_role: str | None = Field( + None, + description="Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.", + ) + thread_id: ThreadId = Field(..., description="Thread ID of the receiver/new agent.") + + +class CollabAgentState(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + message: str | None = None + status: CollabAgentStatus + + +class CollabAgentStatusEntry(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + agent_nickname: str | None = Field( + None, + description="Optional nickname assigned to an AgentControl-spawned sub-agent.", + ) + agent_role: str | None = Field( + None, + description="Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.", + ) + status: AgentStatus = Field(..., description="Last known status of the agent.") + thread_id: ThreadId = Field(..., description="Thread ID of the receiver/new agent.") + + +class CollaborationMode(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + mode: ModeKind + settings: Settings + + +class CollaborationModeMask(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + mode: ModeKind | None = None + model: str | None = None + name: str + reasoning_effort: ReasoningEffort | None = None + + +class CommandExecOutputDeltaNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + cap_reached: bool = Field( + ..., + alias="capReached", + description="`true` on the final streamed chunk for a stream when `outputBytesCap` truncated later output on that stream.", + ) + delta_base64: str = Field( + ..., alias="deltaBase64", description="Base64-encoded output bytes." + ) + process_id: str = Field( + ..., + alias="processId", + description="Client-supplied, connection-scoped `processId` from the original `command/exec` request.", + ) + stream: CommandExecOutputStream = Field( + ..., description="Output stream for this chunk." + ) + + +class CommandExecParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + command: List[str] = Field( + ..., description="Command argv vector. Empty arrays are rejected." + ) + cwd: str | None = Field( + None, description="Optional working directory. Defaults to the server cwd." + ) + disable_output_cap: bool | None = Field( + None, + alias="disableOutputCap", + description="Disable stdout/stderr capture truncation for this request.\n\nCannot be combined with `outputBytesCap`.", + ) + disable_timeout: bool | None = Field( + None, + alias="disableTimeout", + description="Disable the timeout entirely for this request.\n\nCannot be combined with `timeoutMs`.", + ) + env: Dict[str, Any] | None = Field( + None, + description="Optional environment overrides merged into the server-computed environment.\n\nMatching names override inherited values. Set a key to `null` to unset an inherited variable.", + ) + output_bytes_cap: conint(ge=0) | None = Field( + None, + alias="outputBytesCap", + description="Optional per-stream stdout/stderr capture cap in bytes.\n\nWhen omitted, the server default applies. Cannot be combined with `disableOutputCap`.", + ) + process_id: str | None = Field( + None, + alias="processId", + description="Optional client-supplied, connection-scoped process id.\n\nRequired for `tty`, `streamStdin`, `streamStdoutStderr`, and follow-up `command/exec/write`, `command/exec/resize`, and `command/exec/terminate` calls. When omitted, buffered execution gets an internal id that is not exposed to the client.", + ) + sandbox_policy: SandboxPolicy | None = Field( + None, + alias="sandboxPolicy", + description="Optional sandbox policy for this command.\n\nUses the same shape as thread/turn execution sandbox configuration and defaults to the user's configured policy when omitted.", + ) + size: CommandExecTerminalSize | None = Field( + None, + description="Optional initial PTY size in character cells. Only valid when `tty` is true.", + ) + stream_stdin: bool | None = Field( + None, + alias="streamStdin", + description="Allow follow-up `command/exec/write` requests to write stdin bytes.\n\nRequires a client-supplied `processId`.", + ) + stream_stdout_stderr: bool | None = Field( + None, + alias="streamStdoutStderr", + description="Stream stdout/stderr via `command/exec/outputDelta` notifications.\n\nStreamed bytes are not duplicated into the final response and require a client-supplied `processId`.", + ) + timeout_ms: int | None = Field( + None, + alias="timeoutMs", + description="Optional timeout in milliseconds.\n\nWhen omitted, the server default applies. Cannot be combined with `disableTimeout`.", + ) + tty: bool | None = Field( + None, + description="Enable PTY mode.\n\nThis implies `streamStdin` and `streamStdoutStderr`.", + ) + + +class CommandExecResizeParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + process_id: str = Field( + ..., + alias="processId", + description="Client-supplied, connection-scoped `processId` from the original `command/exec` request.", + ) + size: CommandExecTerminalSize = Field( + ..., description="New PTY size in character cells." + ) + + +class ConfigEdit(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + key_path: str = Field(..., alias="keyPath") + merge_strategy: MergeStrategy = Field(..., alias="mergeStrategy") + value: Any + + +class ConfigLayer(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + config: Any + disabled_reason: str | None = Field(None, alias="disabledReason") + name: ConfigLayerSource + version: str + + +class ConfigLayerMetadata(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + name: ConfigLayerSource + version: str + + +class ConfigRequirements(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + allowed_approval_policies: List[AskForApproval] | None = Field( + None, alias="allowedApprovalPolicies" + ) + allowed_sandbox_modes: List[SandboxMode] | None = Field( + None, alias="allowedSandboxModes" + ) + allowed_web_search_modes: List[WebSearchMode] | None = Field( + None, alias="allowedWebSearchModes" + ) + enforce_residency: ResidencyRequirement | None = Field( + None, alias="enforceResidency" + ) + feature_requirements: Dict[str, Any] | None = Field( + None, alias="featureRequirements" + ) + + +class ConfigRequirementsReadResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + requirements: ConfigRequirements | None = Field( + None, + description="Null if no requirements are configured (e.g. no requirements.toml/MDM entries).", + ) + + +class ConfigValueWriteParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + expected_version: str | None = Field(None, alias="expectedVersion") + file_path: str | None = Field( + None, + alias="filePath", + description="Path to the config file to write; defaults to the user's `config.toml` when omitted.", + ) + key_path: str = Field(..., alias="keyPath") + merge_strategy: MergeStrategy = Field(..., alias="mergeStrategy") + value: Any + + +class ConfigWarningNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + details: str | None = Field( + None, description="Optional extra guidance or error details." + ) + path: str | None = Field( + None, description="Optional path to the config file that triggered the warning." + ) + range: TextRange | None = Field( + None, + description="Optional range for the error location inside the config file.", + ) + summary: str = Field(..., description="Concise summary of the warning.") + + +class ErrorNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + error: TurnError + thread_id: str = Field(..., alias="threadId") + turn_id: str = Field(..., alias="turnId") + will_retry: bool = Field(..., alias="willRetry") + + +class EventMsg6(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + from_model: str + reason: ModelRerouteReason + to_model: str + type: Type24 = Field(..., title="ModelRerouteEventMsgType") + + +class EventMsg9(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + collaboration_mode_kind: ModeKind | None = "default" + model_context_window: int | None = None + turn_id: str + type: Type27 = Field(..., title="TaskStartedEventMsgType") + + +class EventMsg12(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + message: str + phase: MessagePhase | None = None + type: Type30 = Field(..., title="AgentMessageEventMsgType") + + +class EventMsg13(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + images: List[str] | None = Field( + None, + description="Image URLs sourced from `UserInput::Image`. These are safe to replay in legacy UI history events and correspond to images sent to the model.", + ) + local_images: List[str] | None = Field( + [], + description="Local file paths sourced from `UserInput::LocalImage`. These are kept so the UI can reattach images when editing history, and should not be sent to the model or treated as API-ready URLs.", + ) + message: str + text_elements: List[TextElement] | None = Field( + [], + description="UI-defined spans within `message` used to render or persist special elements.", + ) + type: Type31 = Field(..., title="UserMessageEventMsgType") + + +class EventMsg21(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + thread_id: ThreadId + thread_name: str | None = None + type: Type39 = Field(..., title="ThreadNameUpdatedEventMsgType") + + +class EventMsg22(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + server: str = Field(..., description="Server name being started.") + status: McpStartupStatus = Field(..., description="Current startup status.") + type: Type40 = Field(..., title="McpStartupUpdateEventMsgType") + + +class EventMsg23(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + cancelled: List[str] + failed: List[McpStartupFailure] + ready: List[str] + type: Type41 = Field(..., title="McpStartupCompleteEventMsgType") + + +class EventMsg24(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + call_id: str = Field( + ..., + description="Identifier so this can be paired with the McpToolCallEnd event.", + ) + invocation: McpInvocation + type: Type42 = Field(..., title="McpToolCallBeginEventMsgType") + + +class EventMsg25(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + call_id: str = Field( + ..., + description="Identifier for the corresponding McpToolCallBegin that finished.", + ) + duration: Duration + invocation: McpInvocation + result: ResultOfCallToolResultOrString = Field( + ..., description="Result of the tool call. Note this could be an error." + ) + type: Type43 = Field(..., title="McpToolCallEndEventMsgType") + + +class EventMsg27(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + action: ResponsesApiWebSearchAction + call_id: str + query: str + type: Type45 = Field(..., title="WebSearchEndEventMsgType") + + +class EventMsg30(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + call_id: str = Field( + ..., + description="Identifier so this can be paired with the ExecCommandEnd event.", + ) + command: List[str] = Field(..., description="The command to be executed.") + cwd: str = Field( + ..., + description="The command's working directory if not the default cwd for the agent.", + ) + interaction_input: str | None = Field( + None, + description="Raw input sent to a unified exec session (if this is an interaction event).", + ) + parsed_cmd: List[ParsedCommand] + process_id: str | None = Field( + None, description="Identifier for the underlying PTY process (when available)." + ) + source: ExecCommandSource | None = Field( + "agent", + description="Where the command originated. Defaults to Agent for backward compatibility.", + ) + turn_id: str = Field(..., description="Turn ID that this command belongs to.") + type: Type48 = Field(..., title="ExecCommandBeginEventMsgType") + + +class EventMsg31(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + call_id: str = Field( + ..., description="Identifier for the ExecCommandBegin that produced this chunk." + ) + chunk: str = Field( + ..., description="Raw bytes from the stream (may not be valid UTF-8)." + ) + stream: CommandExecOutputStream = Field( + ..., description="Which stream produced this chunk." + ) + type: Type49 = Field(..., title="ExecCommandOutputDeltaEventMsgType") + + +class EventMsg33(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + aggregated_output: str | None = Field("", description="Captured aggregated output") + call_id: str = Field( + ..., description="Identifier for the ExecCommandBegin that finished." + ) + command: List[str] = Field(..., description="The command that was executed.") + cwd: str = Field( + ..., + description="The command's working directory if not the default cwd for the agent.", + ) + duration: Duration = Field( + ..., description="The duration of the command execution." + ) + exit_code: int = Field(..., description="The command's exit code.") + formatted_output: str = Field( + ..., description="Formatted output from the command, as seen by the model." + ) + interaction_input: str | None = Field( + None, + description="Raw input sent to a unified exec session (if this is an interaction event).", + ) + parsed_cmd: List[ParsedCommand] + process_id: str | None = Field( + None, description="Identifier for the underlying PTY process (when available)." + ) + source: ExecCommandSource | None = Field( + "agent", + description="Where the command originated. Defaults to Agent for backward compatibility.", + ) + status: ExecCommandStatus = Field( + ..., description="Completion status for this command execution." + ) + stderr: str = Field(..., description="Captured stderr") + stdout: str = Field(..., description="Captured stdout") + turn_id: str = Field(..., description="Turn ID that this command belongs to.") + type: Type51 = Field(..., title="ExecCommandEndEventMsgType") + + +class EventMsg36(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + call_id: str = Field( + ..., + description="Responses API call id for the associated tool call, if available.", + ) + permissions: PermissionProfile + reason: str | None = None + turn_id: str | None = Field( + "", + description="Turn ID that this request belongs to. Uses `#[serde(default)]` for backwards compatibility.", + ) + type: Type54 = Field(..., title="RequestPermissionsEventMsgType") + + +class EventMsg40(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + request: ElicitationRequest + server_name: str + turn_id: str | None = Field( + None, description="Turn ID that this elicitation belongs to, when known." + ) + type: Type58 = Field(..., title="ElicitationRequestEventMsgType") + + +class EventMsg41(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + call_id: str = Field( + ..., + description="Responses API call id for the associated patch apply call, if available.", + ) + changes: Dict[str, FileChange] + grant_root: str | None = Field( + None, + description="When set, the agent is asking the user to allow writes under this root for the remainder of the session.", + ) + reason: str | None = Field( + None, + description="Optional explanatory reason (e.g. request for extra write access).", + ) + turn_id: str | None = Field( + "", + description="Turn ID that this patch belongs to. Uses `#[serde(default)]` for backwards compatibility with older senders.", + ) + type: Type59 = Field(..., title="ApplyPatchApprovalRequestEventMsgType") + + +class EventMsg47(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + auto_approved: bool = Field( + ..., + description="If true, there was no ApplyPatchApprovalRequest for this patch.", + ) + call_id: str = Field( + ..., + description="Identifier so this can be paired with the PatchApplyEnd event.", + ) + changes: Dict[str, FileChange] = Field( + ..., description="The changes to be applied." + ) + turn_id: str | None = Field( + "", + description="Turn ID that this patch belongs to. Uses `#[serde(default)]` for backwards compatibility.", + ) + type: Type65 = Field(..., title="PatchApplyBeginEventMsgType") + + +class EventMsg48(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + call_id: str = Field( + ..., description="Identifier for the PatchApplyBegin that finished." + ) + changes: Dict[str, FileChange] | None = Field( + {}, + description="The changes that were applied (mirrors PatchApplyBeginEvent::changes).", + ) + status: CommandExecutionStatus = Field( + ..., description="Completion status for this patch application." + ) + stderr: str = Field( + ..., description="Captured stderr (parser errors, IO failures, etc.)." + ) + stdout: str = Field( + ..., description="Captured stdout (summary printed by apply_patch)." + ) + success: bool = Field( + ..., description="Whether the patch was applied successfully." + ) + turn_id: str | None = Field( + "", + description="Turn ID that this patch belongs to. Uses `#[serde(default)]` for backwards compatibility.", + ) + type: Type66 = Field(..., title="PatchApplyEndEventMsgType") + + +class EventMsg50(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + entry: HistoryEntry | None = Field( + None, + description="The entry at the requested offset, if available and parseable.", + ) + log_id: conint(ge=0) + offset: conint(ge=0) + type: Type68 = Field(..., title="GetHistoryEntryResponseEventMsgType") + + +class EventMsg51(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + auth_statuses: Dict[str, McpAuthStatus] = Field( + ..., description="Authentication status for each configured MCP server." + ) + resource_templates: Dict[str, List[ResourceTemplate]] = Field( + ..., description="Known resource templates grouped by server name." + ) + resources: Dict[str, List[Resource]] = Field( + ..., description="Known resources grouped by server name." + ) + tools: Dict[str, Tool] = Field( + ..., description="Fully qualified tool name -> tool definition." + ) + type: Type69 = Field(..., title="McpListToolsResponseEventMsgType") + + +class EventMsg54(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + skills: List[RemoteSkillSummary] + type: Type72 = Field(..., title="ListRemoteSkillsResponseEventMsgType") + + +class EventMsg58(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + reason: TurnAbortReason + turn_id: str | None = None + type: Type76 = Field(..., title="TurnAbortedEventMsgType") + + +class EventMsg60(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + target: ReviewTarget + type: Type78 = Field(..., title="EnteredReviewModeEventMsgType") + user_facing_hint: str | None = None + + +class EventMsg71(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + call_id: str = Field(..., description="Identifier for the collab tool call.") + prompt: str = Field( + ..., + description="Initial prompt sent to the agent. Can be empty to prevent CoT leaking at the beginning.", + ) + sender_thread_id: ThreadId = Field(..., description="Thread ID of the sender.") + type: Type89 = Field(..., title="CollabAgentSpawnBeginEventMsgType") + + +class EventMsg72(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + call_id: str = Field(..., description="Identifier for the collab tool call.") + new_agent_nickname: str | None = Field( + None, description="Optional nickname assigned to the new agent." + ) + new_agent_role: str | None = Field( + None, description="Optional role assigned to the new agent." + ) + new_thread_id: ThreadId | None = Field( + None, description="Thread ID of the newly spawned agent, if it was created." + ) + prompt: str = Field( + ..., + description="Initial prompt sent to the agent. Can be empty to prevent CoT leaking at the beginning.", + ) + sender_thread_id: ThreadId = Field(..., description="Thread ID of the sender.") + status: AgentStatus = Field( + ..., + description="Last known status of the new agent reported to the sender agent.", + ) + type: Type90 = Field(..., title="CollabAgentSpawnEndEventMsgType") + + +class EventMsg73(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + call_id: str = Field(..., description="Identifier for the collab tool call.") + prompt: str = Field( + ..., + description="Prompt sent from the sender to the receiver. Can be empty to prevent CoT leaking at the beginning.", + ) + receiver_thread_id: ThreadId = Field(..., description="Thread ID of the receiver.") + sender_thread_id: ThreadId = Field(..., description="Thread ID of the sender.") + type: Type91 = Field(..., title="CollabAgentInteractionBeginEventMsgType") + + +class EventMsg74(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + call_id: str = Field(..., description="Identifier for the collab tool call.") + prompt: str = Field( + ..., + description="Prompt sent from the sender to the receiver. Can be empty to prevent CoT leaking at the beginning.", + ) + receiver_agent_nickname: str | None = Field( + None, description="Optional nickname assigned to the receiver agent." + ) + receiver_agent_role: str | None = Field( + None, description="Optional role assigned to the receiver agent." + ) + receiver_thread_id: ThreadId = Field(..., description="Thread ID of the receiver.") + sender_thread_id: ThreadId = Field(..., description="Thread ID of the sender.") + status: AgentStatus = Field( + ..., + description="Last known status of the receiver agent reported to the sender agent.", + ) + type: Type92 = Field(..., title="CollabAgentInteractionEndEventMsgType") + + +class EventMsg75(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + call_id: str = Field(..., description="ID of the waiting call.") + receiver_agents: List[CollabAgentRef] | None = Field( + None, description="Optional nicknames/roles for receivers." + ) + receiver_thread_ids: List[ThreadId] = Field( + ..., description="Thread ID of the receivers." + ) + sender_thread_id: ThreadId = Field(..., description="Thread ID of the sender.") + type: Type93 = Field(..., title="CollabWaitingBeginEventMsgType") + + +class EventMsg76(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + agent_statuses: List[CollabAgentStatusEntry] | None = Field( + None, description="Optional receiver metadata paired with final statuses." + ) + call_id: str = Field(..., description="ID of the waiting call.") + sender_thread_id: ThreadId = Field(..., description="Thread ID of the sender.") + statuses: Dict[str, AgentStatus] = Field( + ..., + description="Last known status of the receiver agents reported to the sender agent.", + ) + type: Type94 = Field(..., title="CollabWaitingEndEventMsgType") + + +class EventMsg77(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + call_id: str = Field(..., description="Identifier for the collab tool call.") + receiver_thread_id: ThreadId = Field(..., description="Thread ID of the receiver.") + sender_thread_id: ThreadId = Field(..., description="Thread ID of the sender.") + type: Type95 = Field(..., title="CollabCloseBeginEventMsgType") + + +class EventMsg78(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + call_id: str = Field(..., description="Identifier for the collab tool call.") + receiver_agent_nickname: str | None = Field( + None, description="Optional nickname assigned to the receiver agent." + ) + receiver_agent_role: str | None = Field( + None, description="Optional role assigned to the receiver agent." + ) + receiver_thread_id: ThreadId = Field(..., description="Thread ID of the receiver.") + sender_thread_id: ThreadId = Field(..., description="Thread ID of the sender.") + status: AgentStatus = Field( + ..., + description="Last known status of the receiver agent reported to the sender agent before the close.", + ) + type: Type96 = Field(..., title="CollabCloseEndEventMsgType") + + +class EventMsg79(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + call_id: str = Field(..., description="Identifier for the collab tool call.") + receiver_agent_nickname: str | None = Field( + None, description="Optional nickname assigned to the receiver agent." + ) + receiver_agent_role: str | None = Field( + None, description="Optional role assigned to the receiver agent." + ) + receiver_thread_id: ThreadId = Field(..., description="Thread ID of the receiver.") + sender_thread_id: ThreadId = Field(..., description="Thread ID of the sender.") + type: Type97 = Field(..., title="CollabResumeBeginEventMsgType") + + +class EventMsg80(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + call_id: str = Field(..., description="Identifier for the collab tool call.") + receiver_agent_nickname: str | None = Field( + None, description="Optional nickname assigned to the receiver agent." + ) + receiver_agent_role: str | None = Field( + None, description="Optional role assigned to the receiver agent." + ) + receiver_thread_id: ThreadId = Field(..., description="Thread ID of the receiver.") + sender_thread_id: ThreadId = Field(..., description="Thread ID of the sender.") + status: AgentStatus = Field( + ..., + description="Last known status of the receiver agent reported to the sender agent after resume.", + ) + type: Type98 = Field(..., title="CollabResumeEndEventMsgType") + + +class ExperimentalFeature(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + announcement: str | None = Field( + None, + description="Announcement copy shown to users when the feature is introduced. Null when this feature is not in beta.", + ) + default_enabled: bool = Field( + ..., + alias="defaultEnabled", + description="Whether this feature is enabled by default.", + ) + description: str | None = Field( + None, + description="Short summary describing what the feature does. Null when this feature is not in beta.", + ) + display_name: str | None = Field( + None, + alias="displayName", + description="User-facing display name shown in the experimental features UI. Null when this feature is not in beta.", + ) + enabled: bool = Field( + ..., + description="Whether this feature is currently enabled in the loaded config.", + ) + name: str = Field( + ..., description="Stable key used in config.toml and CLI flag toggles." + ) + stage: ExperimentalFeatureStage = Field( + ..., description="Lifecycle stage of this feature flag." + ) + + +class ExperimentalFeatureListResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + data: List[ExperimentalFeature] + next_cursor: str | None = Field( + None, + alias="nextCursor", + description="Opaque cursor to pass to the next call to continue after the last item. If None, there are no more items to return.", + ) + + +class ExternalAgentConfigMigrationItem(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + cwd: str | None = Field( + None, + description="Null or empty means home-scoped migration; non-empty means repo-scoped migration.", + ) + description: str + item_type: ExternalAgentConfigMigrationItemType = Field(..., alias="itemType") + + +class FileUpdateChange(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + diff: str + kind: PatchChangeKind + path: str + + +class FunctionCallOutputContentItem2(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + detail: ImageDetail | None = None + image_url: str + type: Type15 = Field(..., title="InputImageFunctionCallOutputContentItemType") + + +class FunctionCallOutputContentItem( + RootModel[FunctionCallOutputContentItem1 | FunctionCallOutputContentItem2] +): + model_config = ConfigDict( + populate_by_name=True, + ) + root: FunctionCallOutputContentItem1 | FunctionCallOutputContentItem2 = Field( + ..., + description="Responses API compatible content items that can be returned by a tool call. This is a subset of ContentItem with the types we support as function call outputs.", + ) + + +class GetAccountResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + account: Account | None = None + requires_openai_auth: bool = Field(..., alias="requiresOpenaiAuth") + + +class HookOutputEntry(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + kind: HookOutputEntryKind + text: str + + +class HookRunSummary(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + completed_at: int | None = Field(None, alias="completedAt") + display_order: int = Field(..., alias="displayOrder") + duration_ms: int | None = Field(None, alias="durationMs") + entries: List[HookOutputEntry] + event_name: HookEventName = Field(..., alias="eventName") + execution_mode: HookExecutionMode = Field(..., alias="executionMode") + handler_type: HookHandlerType = Field(..., alias="handlerType") + id: str + scope: HookScope + source_path: str = Field(..., alias="sourcePath") + started_at: int = Field(..., alias="startedAt") + status: HookRunStatus + status_message: str | None = Field(None, alias="statusMessage") + + +class HookStartedNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + run: HookRunSummary + thread_id: str = Field(..., alias="threadId") + turn_id: str | None = Field(None, alias="turnId") + + +class McpServerStatus(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + auth_status: McpAuthStatus = Field(..., alias="authStatus") + name: str + resource_templates: List[ResourceTemplate] = Field(..., alias="resourceTemplates") + resources: List[Resource] + tools: Dict[str, Tool] + + +class Model(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + availability_nux: ModelAvailabilityNux | None = Field(None, alias="availabilityNux") + default_reasoning_effort: ReasoningEffort = Field( + ..., alias="defaultReasoningEffort" + ) + description: str + display_name: str = Field(..., alias="displayName") + hidden: bool + id: str + input_modalities: List[InputModality] | None = Field( + ["text", "image"], alias="inputModalities" + ) + is_default: bool = Field(..., alias="isDefault") + model: str + supported_reasoning_efforts: List[ReasoningEffortOption] = Field( + ..., alias="supportedReasoningEfforts" + ) + supports_personality: bool | None = Field(False, alias="supportsPersonality") + upgrade: str | None = None + upgrade_info: ModelUpgradeInfo | None = Field(None, alias="upgradeInfo") + + +class ModelListResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + data: List[Model] + next_cursor: str | None = Field( + None, + alias="nextCursor", + description="Opaque cursor to pass to the next call to continue after the last item. If None, there are no more items to return.", + ) + + +class NetworkApprovalContext(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + host: str + protocol: NetworkApprovalProtocol + + +class NetworkPolicyAmendment(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + action: NetworkPolicyRuleAction + host: str + + +class OverriddenMetadata(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + effective_value: Any = Field(..., alias="effectiveValue") + message: str + overriding_layer: ConfigLayerMetadata = Field(..., alias="overridingLayer") + + +class PlanItemArg(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + status: StepStatus + step: str + + +class PluginMarketplaceEntry(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + name: str + path: AbsolutePathBuf + plugins: List[PluginSummary] + + +class RateLimitSnapshot(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + credits: CreditsSnapshot | None = None + limit_id: str | None = Field(None, alias="limitId") + limit_name: str | None = Field(None, alias="limitName") + plan_type: PlanType | None = Field(None, alias="planType") + primary: RateLimitWindow | None = None + secondary: RateLimitWindow | None = None + + +class RealtimeEvent2(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + input_transcript_delta: RealtimeTranscriptDelta = Field( + ..., alias="InputTranscriptDelta" + ) + + +class RealtimeEvent3(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + output_transcript_delta: RealtimeTranscriptDelta = Field( + ..., alias="OutputTranscriptDelta" + ) + + +class RealtimeHandoffRequested(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + active_transcript: List[RealtimeTranscriptEntry] + handoff_id: str + input_transcript: str + item_id: str + + +class RequestUserInputQuestion(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + header: str + id: str + is_other: bool | None = Field(False, alias="isOther") + is_secret: bool | None = Field(False, alias="isSecret") + options: List[RequestUserInputQuestionOption] | None = None + question: str + + +class ResponseItem8(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + action: ResponsesApiWebSearchAction | None = None + id: str | None = None + status: str | None = None + type: Type131 = Field(..., title="WebSearchCallResponseItemType") + + +class ReviewCodeLocation(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + absolute_file_path: str + line_range: ReviewLineRange + + +class NetworkPolicyAmendment1(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + network_policy_amendment: NetworkPolicyAmendment + + +class ReviewDecision4(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + network_policy_amendment: NetworkPolicyAmendment1 + + +class ReviewDecision( + RootModel[ + ReviewDecision1 + | ReviewDecision2 + | ReviewDecision3 + | ReviewDecision4 + | ReviewDecision5 + | ReviewDecision6 + ] +): + model_config = ConfigDict( + populate_by_name=True, + ) + root: ( + ReviewDecision1 + | ReviewDecision2 + | ReviewDecision3 + | ReviewDecision4 + | ReviewDecision5 + | ReviewDecision6 + ) = Field(..., description="User's decision in response to an ExecApprovalRequest.") + + +class ReviewFinding(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + body: str + code_location: ReviewCodeLocation + confidence_score: float + priority: int + title: str + + +class ReviewOutputEvent(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + findings: List[ReviewFinding] + overall_confidence_score: float + overall_correctness: str + overall_explanation: str + + +class ReviewStartParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + delivery: ReviewDelivery | None = Field( + None, + description="Where to run the review: inline (default) on the current thread or detached on a new thread (returned in `reviewThreadId`).", + ) + target: ReviewTarget + thread_id: str = Field(..., alias="threadId") + + +class ServerNotification1(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Type19 = Field(..., title="ErrorNotificationMethod") + params: ErrorNotification + + +class ServerNotification3(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method51 = Field(..., title="Thread/status/changedNotificationMethod") + params: ThreadStatusChangedNotification + + +class ServerNotification4(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method52 = Field(..., title="Thread/archivedNotificationMethod") + params: ThreadArchivedNotification + + +class ServerNotification5(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method53 = Field(..., title="Thread/unarchivedNotificationMethod") + params: ThreadUnarchivedNotification + + +class ServerNotification6(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method54 = Field(..., title="Thread/closedNotificationMethod") + params: ThreadClosedNotification + + +class ServerNotification7(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method55 = Field(..., title="Skills/changedNotificationMethod") + params: SkillsChangedNotification + + +class ServerNotification8(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method56 = Field(..., title="Thread/name/updatedNotificationMethod") + params: ThreadNameUpdatedNotification + + +class ServerNotification11(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method59 = Field(..., title="Hook/startedNotificationMethod") + params: HookStartedNotification + + +class ServerNotification14(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method62 = Field(..., title="Turn/diff/updatedNotificationMethod") + params: TurnDiffUpdatedNotification + + +class ServerNotification20(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method68 = Field(..., title="Command/exec/outputDeltaNotificationMethod") + params: CommandExecOutputDeltaNotification + + +class ServerNotification22(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method70 = Field( + ..., title="Item/commandExecution/terminalInteractionNotificationMethod" + ) + params: TerminalInteractionNotification + + +class ServerNotification24(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method72 = Field(..., title="ServerRequest/resolvedNotificationMethod") + params: ServerRequestResolvedNotification + + +class ServerNotification27(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method75 = Field(..., title="Account/updatedNotificationMethod") + params: AccountUpdatedNotification + + +class ServerNotification36(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method84 = Field(..., title="ConfigWarningNotificationMethod") + params: ConfigWarningNotification + + +class ServerNotification39(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method87 = Field(..., title="Thread/realtime/startedNotificationMethod") + params: ThreadRealtimeStartedNotification + + +class ServerNotification40(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method88 = Field(..., title="Thread/realtime/itemAddedNotificationMethod") + params: ThreadRealtimeItemAddedNotification + + +class ServerNotification41(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method89 = Field( + ..., title="Thread/realtime/outputAudio/deltaNotificationMethod" + ) + params: ThreadRealtimeOutputAudioDeltaNotification + + +class ServerNotification42(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method90 = Field(..., title="Thread/realtime/errorNotificationMethod") + params: ThreadRealtimeErrorNotification + + +class ServerNotification43(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method91 = Field(..., title="Thread/realtime/closedNotificationMethod") + params: ThreadRealtimeClosedNotification + + +class ServerNotification44(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method92 = Field( + ..., title="Windows/worldWritableWarningNotificationMethod" + ) + params: WindowsWorldWritableWarningNotification + + +class SkillDependencies(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + tools: List[SkillToolDependency] + + +class SkillMetadata(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + dependencies: SkillDependencies | None = None + description: str + enabled: bool + interface: SkillInterface | None = None + name: str + path: str + scope: SkillScope + short_description: str | None = Field( + None, + alias="shortDescription", + description="Legacy short_description from SKILL.md. Prefer SKILL.json interface.short_description.", + ) + + +class SkillsListEntry(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + cwd: str + errors: List[SkillErrorInfo] + skills: List[SkillMetadata] + + +class SkillsListResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + data: List[SkillsListEntry] + + +class ThreadSpawn(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + agent_nickname: str | None = None + agent_role: str | None = None + depth: int + parent_thread_id: ThreadId + + +class SubAgentSource2(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + thread_spawn: ThreadSpawn + + +class SubAgentSource(RootModel[SubAgentSource1 | SubAgentSource2 | SubAgentSource3]): + model_config = ConfigDict( + populate_by_name=True, + ) + root: SubAgentSource1 | SubAgentSource2 | SubAgentSource3 + + +class ThreadItem1(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + content: List[UserInput] + id: str + type: Type148 = Field(..., title="UserMessageThreadItemType") + + +class ThreadItem6(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + changes: List[FileUpdateChange] + id: str + status: CommandExecutionStatus + type: Type153 = Field(..., title="FileChangeThreadItemType") + + +class ThreadItem9(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + agents_states: Dict[str, CollabAgentState] = Field( + ..., + alias="agentsStates", + description="Last known status of the target agents, when available.", + ) + id: str = Field(..., description="Unique identifier for this collab tool call.") + prompt: str | None = Field( + None, + description="Prompt text sent as part of the collab tool call, when available.", + ) + receiver_thread_ids: List[str] = Field( + ..., + alias="receiverThreadIds", + description="Thread ID of the receiving agent, when applicable. In case of spawn operation, this corresponds to the newly spawned agent.", + ) + sender_thread_id: str = Field( + ..., + alias="senderThreadId", + description="Thread ID of the agent issuing the collab request.", + ) + status: CollabAgentToolCallStatus = Field( + ..., description="Current status of the collab tool call." + ) + tool: CollabAgentTool = Field( + ..., description="Name of the collab tool that was invoked." + ) + type: Type156 = Field(..., title="CollabAgentToolCallThreadItemType") + + +class ThreadItem10(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + action: WebSearchAction | None = None + id: str + query: str + type: Type157 = Field(..., title="WebSearchThreadItemType") + + +class ThreadItem( + RootModel[ + ThreadItem1 + | ThreadItem2 + | ThreadItem3 + | ThreadItem4 + | ThreadItem5 + | ThreadItem6 + | ThreadItem7 + | ThreadItem8 + | ThreadItem9 + | ThreadItem10 + | ThreadItem11 + | ThreadItem12 + | ThreadItem13 + | ThreadItem14 + | ThreadItem15 + ] +): + model_config = ConfigDict( + populate_by_name=True, + ) + root: ( + ThreadItem1 + | ThreadItem2 + | ThreadItem3 + | ThreadItem4 + | ThreadItem5 + | ThreadItem6 + | ThreadItem7 + | ThreadItem8 + | ThreadItem9 + | ThreadItem10 + | ThreadItem11 + | ThreadItem12 + | ThreadItem13 + | ThreadItem14 + | ThreadItem15 + ) + + +class ThreadListParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + archived: bool | None = Field( + None, + description="Optional archived filter; when set to true, only archived threads are returned. If false or null, only non-archived threads are returned.", + ) + cursor: str | None = Field( + None, description="Opaque pagination cursor returned by a previous call." + ) + cwd: str | None = Field( + None, + description="Optional cwd filter; when set, only threads whose session cwd exactly matches this path are returned.", + ) + limit: conint(ge=0) | None = Field( + None, + description="Optional page size; defaults to a reasonable server-side value.", + ) + model_providers: List[str] | None = Field( + None, + alias="modelProviders", + description="Optional provider filter; when set, only sessions recorded under these providers are returned. When present but empty, includes all providers.", + ) + search_term: str | None = Field( + None, + alias="searchTerm", + description="Optional substring filter for the extracted thread title.", + ) + sort_key: ThreadSortKey | None = Field( + None, alias="sortKey", description="Optional sort key; defaults to created_at." + ) + source_kinds: List[ThreadSourceKind] | None = Field( + None, + alias="sourceKinds", + description="Optional source filter; when set, only sessions from these source kinds are returned. When omitted or empty, defaults to interactive sources.", + ) + + +class ThreadTokenUsage(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + last: TokenUsageBreakdown + model_context_window: int | None = Field(None, alias="modelContextWindow") + total: TokenUsageBreakdown + + +class ThreadTokenUsageUpdatedNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + thread_id: str = Field(..., alias="threadId") + token_usage: ThreadTokenUsage = Field(..., alias="tokenUsage") + turn_id: str = Field(..., alias="turnId") + + +class ThreadUnsubscribeResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + status: ThreadUnsubscribeStatus + + +class ToolsV2(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + view_image: bool | None = None + web_search: WebSearchToolConfig | None = None + + +class Turn(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + error: TurnError | None = Field( + None, description="Only populated when the Turn's status is failed." + ) + id: str + items: List[ThreadItem] = Field( + ..., + description="Only populated on a `thread/resume` or `thread/fork` response. For all other responses and notifications returning a Turn, the items field will be an empty list.", + ) + status: TurnStatus + + +class TurnCompletedNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + thread_id: str = Field(..., alias="threadId") + turn: Turn + + +class TurnItem1(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + content: List[UserInput] + id: str + type: Type167 = Field(..., title="UserMessageTurnItemType") + + +class TurnItem( + RootModel[ + TurnItem1 + | TurnItem2 + | TurnItem3 + | TurnItem4 + | TurnItem5 + | TurnItem6 + | TurnItem7 + ] +): + model_config = ConfigDict( + populate_by_name=True, + ) + root: ( + TurnItem1 + | TurnItem2 + | TurnItem3 + | TurnItem4 + | TurnItem5 + | TurnItem6 + | TurnItem7 + ) + + +class TurnPlanStep(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + status: TurnPlanStepStatus + step: str + + +class TurnPlanUpdatedNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + explanation: str | None = None + plan: List[TurnPlanStep] + thread_id: str = Field(..., alias="threadId") + turn_id: str = Field(..., alias="turnId") + + +class TurnStartParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + approval_policy: AskForApproval | None = Field( + None, + alias="approvalPolicy", + description="Override the approval policy for this turn and subsequent turns.", + ) + cwd: str | None = Field( + None, + description="Override the working directory for this turn and subsequent turns.", + ) + effort: ReasoningEffort | None = Field( + None, + description="Override the reasoning effort for this turn and subsequent turns.", + ) + input: List[UserInput] + model: str | None = Field( + None, description="Override the model for this turn and subsequent turns." + ) + output_schema: Any | None = Field( + None, + alias="outputSchema", + description="Optional JSON Schema used to constrain the final assistant message for this turn.", + ) + personality: Personality | None = Field( + None, description="Override the personality for this turn and subsequent turns." + ) + sandbox_policy: SandboxPolicy | None = Field( + None, + alias="sandboxPolicy", + description="Override the sandbox policy for this turn and subsequent turns.", + ) + service_tier: ServiceTier | None = Field( + None, + alias="serviceTier", + description="Override the service tier for this turn and subsequent turns.", + ) + summary: ReasoningSummary | None = Field( + None, + description="Override the reasoning summary for this turn and subsequent turns.", + ) + thread_id: str = Field(..., alias="threadId") + + +class TurnStartResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + turn: Turn + + +TurnStartedNotification = TurnCompletedNotification + + +class TurnSteerParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + expected_turn_id: str = Field( + ..., + alias="expectedTurnId", + description="Required active turn id precondition. The request fails when it does not match the currently active turn.", + ) + input: List[UserInput] + thread_id: str = Field(..., alias="threadId") + + +class WindowsSandboxSetupCompletedNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + error: str | None = None + mode: WindowsSandboxSetupMode + success: bool + + +class AccountRateLimitsUpdatedNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + rate_limits: RateLimitSnapshot = Field(..., alias="rateLimits") + + +class AppInfo(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + app_metadata: AppMetadata | None = Field(None, alias="appMetadata") + branding: AppBranding | None = None + description: str | None = None + distribution_channel: str | None = Field(None, alias="distributionChannel") + id: str + install_url: str | None = Field(None, alias="installUrl") + is_accessible: bool | None = Field(False, alias="isAccessible") + is_enabled: bool | None = Field( + True, + alias="isEnabled", + description="Whether this app is enabled in config.toml. Example: ```toml [apps.bad_app] enabled = false ```", + ) + labels: Dict[str, Any] | None = None + logo_url: str | None = Field(None, alias="logoUrl") + logo_url_dark: str | None = Field(None, alias="logoUrlDark") + name: str + plugin_display_names: List[str] | None = Field([], alias="pluginDisplayNames") + + +class AppListUpdatedNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + data: List[AppInfo] + + +class AppsListResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + data: List[AppInfo] + next_cursor: str | None = Field( + None, + alias="nextCursor", + description="Opaque cursor to pass to the next call to continue after the last item. If None, there are no more items to return.", + ) + + +class ClientRequest12(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method11 = Field(..., title="Thread/listRequestMethod") + params: ThreadListParams + + +class ClientRequest23(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method22 = Field(..., title="Turn/startRequestMethod") + params: TurnStartParams + + +class ClientRequest24(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method23 = Field(..., title="Turn/steerRequestMethod") + params: TurnSteerParams + + +class ClientRequest26(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method25 = Field(..., title="Review/startRequestMethod") + params: ReviewStartParams + + +class ClientRequest38(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method37 = Field(..., title="Command/execRequestMethod") + params: CommandExecParams + + +class ClientRequest41(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method40 = Field(..., title="Command/exec/resizeRequestMethod") + params: CommandExecResizeParams + + +class ClientRequest45(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method44 = Field(..., title="Config/value/writeRequestMethod") + params: ConfigValueWriteParams + + +class ConfigBatchWriteParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + edits: List[ConfigEdit] + expected_version: str | None = Field(None, alias="expectedVersion") + file_path: str | None = Field( + None, + alias="filePath", + description="Path to the config file to write; defaults to the user's `config.toml` when omitted.", + ) + reload_user_config: bool | None = Field( + None, + alias="reloadUserConfig", + description="When true, hot-reload the updated user config into all loaded threads after writing.", + ) + + +class ConfigWriteResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + file_path: AbsolutePathBuf = Field( + ..., + alias="filePath", + description="Canonical path to the config file that was written.", + ) + overridden_metadata: OverriddenMetadata | None = Field( + None, alias="overriddenMetadata" + ) + status: WriteStatus + version: str + + +class EventMsg11(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + info: TokenUsageInfo | None = None + rate_limits: RateLimitSnapshot | None = None + type: Type29 = Field(..., title="TokenCountEventMsgType") + + +class EventMsg35(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + additional_permissions: PermissionProfile | None = Field( + None, + description="Optional additional filesystem permissions requested for this command.", + ) + approval_id: str | None = Field( + None, + description="Identifier for this specific approval callback.\n\nWhen absent, the approval is for the command item itself (`call_id`). This is present for subcommand approvals (via execve intercept).", + ) + available_decisions: List[ReviewDecision] | None = Field( + None, + description="Ordered list of decisions the client may present for this prompt.\n\nWhen absent, clients should derive the legacy default set from the other fields on this request.", + ) + call_id: str = Field( + ..., description="Identifier for the associated command execution item." + ) + command: List[str] = Field(..., description="The command to be executed.") + cwd: str = Field(..., description="The command's working directory.") + network_approval_context: NetworkApprovalContext | None = Field( + None, + description="Optional network context for a blocked request that can be approved.", + ) + parsed_cmd: List[ParsedCommand] + proposed_execpolicy_amendment: List[str] | None = Field( + None, + description="Proposed execpolicy amendment that can be applied to allow future runs.", + ) + proposed_network_policy_amendments: List[NetworkPolicyAmendment] | None = Field( + None, + description="Proposed network policy amendments (for example allow/deny this host in future).", + ) + reason: str | None = Field( + None, + description="Optional human-readable reason for the approval (e.g. retry without sandbox).", + ) + skill_metadata: ExecApprovalRequestSkillMetadata | None = Field( + None, + description="Optional skill metadata when the approval was triggered by a skill script.", + ) + turn_id: str | None = Field( + "", + description="Turn ID that this command belongs to. Uses `#[serde(default)]` for backwards compatibility.", + ) + type: Type53 = Field(..., title="ExecApprovalRequestEventMsgType") + + +class EventMsg37(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + call_id: str = Field( + ..., + description="Responses API call id for the associated tool call, if available.", + ) + questions: List[RequestUserInputQuestion] + turn_id: str | None = Field( + "", + description="Turn ID that this request belongs to. Uses `#[serde(default)]` for backwards compatibility.", + ) + type: Type55 = Field(..., title="RequestUserInputEventMsgType") + + +class EventMsg53(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + skills: List[SkillsListEntry] + type: Type71 = Field(..., title="ListSkillsResponseEventMsgType") + + +class EventMsg57(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + explanation: str | None = Field( + None, + description="Arguments for the `update_plan` todo/checklist tool (not plan mode).", + ) + plan: List[PlanItemArg] + type: Type75 = Field(..., title="PlanUpdateEventMsgType") + + +class EventMsg61(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + review_output: ReviewOutputEvent | None = None + type: Type79 = Field(..., title="ExitedReviewModeEventMsgType") + + +class EventMsg63(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + item: TurnItem + thread_id: ThreadId + turn_id: str + type: Type81 = Field(..., title="ItemStartedEventMsgType") + + +class EventMsg64(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + item: TurnItem + thread_id: ThreadId + turn_id: str + type: Type82 = Field(..., title="ItemCompletedEventMsgType") + + +class EventMsg65(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + run: HookRunSummary + turn_id: str | None = None + type: Type83 = Field(..., title="HookStartedEventMsgType") + + +class EventMsg66(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + run: HookRunSummary + turn_id: str | None = None + type: Type84 = Field(..., title="HookCompletedEventMsgType") + + +class ExternalAgentConfigDetectResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + items: List[ExternalAgentConfigMigrationItem] + + +class ExternalAgentConfigImportParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + migration_items: List[ExternalAgentConfigMigrationItem] = Field( + ..., alias="migrationItems" + ) + + +class FunctionCallOutputBody(RootModel[str | List[FunctionCallOutputContentItem]]): + model_config = ConfigDict( + populate_by_name=True, + ) + root: str | List[FunctionCallOutputContentItem] + + +class FunctionCallOutputPayload(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + body: FunctionCallOutputBody + success: bool | None = None + + +class GetAccountRateLimitsResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + rate_limits: RateLimitSnapshot = Field( + ..., + alias="rateLimits", + description="Backward-compatible single-bucket view; mirrors the historical payload.", + ) + rate_limits_by_limit_id: Dict[str, Any] | None = Field( + None, + alias="rateLimitsByLimitId", + description="Multi-bucket view keyed by metered `limit_id` (for example, `codex`).", + ) + + +HookCompletedNotification = HookStartedNotification + + +class ItemCompletedNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + item: ThreadItem + thread_id: str = Field(..., alias="threadId") + turn_id: str = Field(..., alias="turnId") + + +ItemStartedNotification = ItemCompletedNotification + + +class ListMcpServerStatusResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + data: List[McpServerStatus] + next_cursor: str | None = Field( + None, + alias="nextCursor", + description="Opaque cursor to pass to the next call to continue after the last item. If None, there are no more items to return.", + ) + + +class PluginListResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + marketplaces: List[PluginMarketplaceEntry] + + +class ProfileV2(BaseModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + approval_policy: AskForApproval | None = None + chatgpt_base_url: str | None = None + model: str | None = None + model_provider: str | None = None + model_reasoning_effort: ReasoningEffort | None = None + model_reasoning_summary: ReasoningSummary | None = None + model_verbosity: Verbosity | None = None + service_tier: ServiceTier | None = None + tools: ToolsV2 | None = None + web_search: WebSearchMode | None = None + + +class RealtimeEvent7(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + handoff_requested: RealtimeHandoffRequested = Field(..., alias="HandoffRequested") + + +class RealtimeEvent( + RootModel[ + RealtimeEvent1 + | RealtimeEvent2 + | RealtimeEvent3 + | RealtimeEvent4 + | RealtimeEvent5 + | RealtimeEvent6 + | RealtimeEvent7 + | RealtimeEvent8 + ] +): + model_config = ConfigDict( + populate_by_name=True, + ) + root: ( + RealtimeEvent1 + | RealtimeEvent2 + | RealtimeEvent3 + | RealtimeEvent4 + | RealtimeEvent5 + | RealtimeEvent6 + | RealtimeEvent7 + | RealtimeEvent8 + ) + + +class ResponseItem5(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + call_id: str + output: FunctionCallOutputPayload + type: Type128 = Field(..., title="FunctionCallOutputResponseItemType") + + +class ResponseItem7(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + call_id: str + output: FunctionCallOutputPayload + type: Type130 = Field(..., title="CustomToolCallOutputResponseItemType") + + +class ResponseItem( + RootModel[ + ResponseItem1 + | ResponseItem2 + | ResponseItem3 + | ResponseItem4 + | ResponseItem5 + | ResponseItem6 + | ResponseItem7 + | ResponseItem8 + | ResponseItem9 + | ResponseItem10 + | ResponseItem11 + | ResponseItem12 + ] +): + model_config = ConfigDict( + populate_by_name=True, + ) + root: ( + ResponseItem1 + | ResponseItem2 + | ResponseItem3 + | ResponseItem4 + | ResponseItem5 + | ResponseItem6 + | ResponseItem7 + | ResponseItem8 + | ResponseItem9 + | ResponseItem10 + | ResponseItem11 + | ResponseItem12 + ) + + +class ReviewStartResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + review_thread_id: str = Field( + ..., + alias="reviewThreadId", + description="Identifies the thread where the review runs.\n\nFor inline reviews, this is the original thread id. For detached reviews, this is the id of the new review thread.", + ) + turn: Turn + + +class ServerNotification9(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method57 = Field(..., title="Thread/tokenUsage/updatedNotificationMethod") + params: ThreadTokenUsageUpdatedNotification + + +class ServerNotification10(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method58 = Field(..., title="Turn/startedNotificationMethod") + params: TurnStartedNotification + + +class ServerNotification12(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method60 = Field(..., title="Turn/completedNotificationMethod") + params: TurnCompletedNotification + + +class ServerNotification13(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method61 = Field(..., title="Hook/completedNotificationMethod") + params: HookCompletedNotification + + +class ServerNotification15(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method63 = Field(..., title="Turn/plan/updatedNotificationMethod") + params: TurnPlanUpdatedNotification + + +class ServerNotification16(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method64 = Field(..., title="Item/startedNotificationMethod") + params: ItemStartedNotification + + +class ServerNotification17(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method65 = Field(..., title="Item/completedNotificationMethod") + params: ItemCompletedNotification + + +class ServerNotification28(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method76 = Field(..., title="Account/rateLimits/updatedNotificationMethod") + params: AccountRateLimitsUpdatedNotification + + +class ServerNotification29(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method77 = Field(..., title="App/list/updatedNotificationMethod") + params: AppListUpdatedNotification + + +class ServerNotification45(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method93 = Field( + ..., title="WindowsSandbox/setupCompletedNotificationMethod" + ) + params: WindowsSandboxSetupCompletedNotification + + +class SessionSource2(BaseModel): + model_config = ConfigDict( + extra="forbid", + populate_by_name=True, + ) + sub_agent: SubAgentSource = Field(..., alias="subAgent") + + +class SessionSource(RootModel[SessionSource1 | SessionSource2]): + model_config = ConfigDict( + populate_by_name=True, + ) + root: SessionSource1 | SessionSource2 + + +class Thread(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + agent_nickname: str | None = Field( + None, + alias="agentNickname", + description="Optional random unique nickname assigned to an AgentControl-spawned sub-agent.", + ) + agent_role: str | None = Field( + None, + alias="agentRole", + description="Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.", + ) + cli_version: str = Field( + ..., + alias="cliVersion", + description="Version of the CLI that created the thread.", + ) + created_at: int = Field( + ..., + alias="createdAt", + description="Unix timestamp (in seconds) when the thread was created.", + ) + cwd: str = Field(..., description="Working directory captured for the thread.") + ephemeral: bool = Field( + ..., + description="Whether the thread is ephemeral and should not be materialized on disk.", + ) + git_info: GitInfo | None = Field( + None, + alias="gitInfo", + description="Optional Git metadata captured when the thread was created.", + ) + id: str + model_provider: str = Field( + ..., + alias="modelProvider", + description="Model provider used for this thread (for example, 'openai').", + ) + name: str | None = Field(None, description="Optional user-facing thread title.") + path: str | None = Field(None, description="[UNSTABLE] Path to the thread on disk.") + preview: str = Field( + ..., description="Usually the first user message in the thread, if available." + ) + source: SessionSource = Field( + ..., + description="Origin of the thread (CLI, VSCode, codex exec, codex app-server, etc.).", + ) + status: ThreadStatus = Field( + ..., description="Current runtime status for the thread." + ) + turns: List[Turn] = Field( + ..., + description="Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` (when `includeTurns` is true) responses. For all other responses and notifications returning a Thread, the turns field will be an empty list.", + ) + updated_at: int = Field( + ..., + alias="updatedAt", + description="Unix timestamp (in seconds) when the thread was last updated.", + ) + + +class ThreadForkResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + approval_policy: AskForApproval = Field(..., alias="approvalPolicy") + cwd: str + model: str + model_provider: str = Field(..., alias="modelProvider") + reasoning_effort: ReasoningEffort | None = Field(None, alias="reasoningEffort") + sandbox: SandboxPolicy + service_tier: ServiceTier | None = Field(None, alias="serviceTier") + thread: Thread + + +class ThreadListResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + data: List[Thread] + next_cursor: str | None = Field( + None, + alias="nextCursor", + description="Opaque cursor to pass to the next call to continue after the last item. if None, there are no more items to return.", + ) + + +class ThreadMetadataUpdateResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + thread: Thread + + +ThreadReadResponse = ThreadMetadataUpdateResponse + + +ThreadResumeResponse = ThreadForkResponse + + +class ThreadRollbackResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + thread: Thread = Field( + ..., + description="The updated thread after applying the rollback, with `turns` populated.\n\nThe ThreadItems stored in each Turn are lossy since we explicitly do not persist all agent interactions, such as command executions. This is the same behavior as `thread/resume`.", + ) + + +ThreadStartResponse = ThreadForkResponse + + +ThreadStartedNotification = ThreadMetadataUpdateResponse + + +ThreadUnarchiveResponse = ThreadMetadataUpdateResponse + + +class ClientRequest44(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method43 = Field(..., title="ExternalAgentConfig/importRequestMethod") + params: ExternalAgentConfigImportParams + + +class ClientRequest46(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Method45 = Field(..., title="Config/batchWriteRequestMethod") + params: ConfigBatchWriteParams + + +class ClientRequest( + RootModel[ + ClientRequest1 + | ClientRequest2 + | ClientRequest3 + | ClientRequest4 + | ClientRequest5 + | ClientRequest6 + | ClientRequest7 + | ClientRequest8 + | ClientRequest9 + | ClientRequest10 + | ClientRequest11 + | ClientRequest12 + | ClientRequest13 + | ClientRequest14 + | ClientRequest15 + | ClientRequest16 + | ClientRequest17 + | ClientRequest18 + | ClientRequest19 + | ClientRequest20 + | ClientRequest21 + | ClientRequest22 + | ClientRequest23 + | ClientRequest24 + | ClientRequest25 + | ClientRequest26 + | ClientRequest27 + | ClientRequest28 + | ClientRequest29 + | ClientRequest30 + | ClientRequest31 + | ClientRequest32 + | ClientRequest33 + | ClientRequest34 + | ClientRequest35 + | ClientRequest36 + | ClientRequest37 + | ClientRequest38 + | ClientRequest39 + | ClientRequest40 + | ClientRequest41 + | ClientRequest42 + | ClientRequest43 + | ClientRequest44 + | ClientRequest45 + | ClientRequest46 + | ClientRequest47 + | ClientRequest48 + | ClientRequest49 + ] +): + model_config = ConfigDict( + populate_by_name=True, + ) + root: ( + ClientRequest1 + | ClientRequest2 + | ClientRequest3 + | ClientRequest4 + | ClientRequest5 + | ClientRequest6 + | ClientRequest7 + | ClientRequest8 + | ClientRequest9 + | ClientRequest10 + | ClientRequest11 + | ClientRequest12 + | ClientRequest13 + | ClientRequest14 + | ClientRequest15 + | ClientRequest16 + | ClientRequest17 + | ClientRequest18 + | ClientRequest19 + | ClientRequest20 + | ClientRequest21 + | ClientRequest22 + | ClientRequest23 + | ClientRequest24 + | ClientRequest25 + | ClientRequest26 + | ClientRequest27 + | ClientRequest28 + | ClientRequest29 + | ClientRequest30 + | ClientRequest31 + | ClientRequest32 + | ClientRequest33 + | ClientRequest34 + | ClientRequest35 + | ClientRequest36 + | ClientRequest37 + | ClientRequest38 + | ClientRequest39 + | ClientRequest40 + | ClientRequest41 + | ClientRequest42 + | ClientRequest43 + | ClientRequest44 + | ClientRequest45 + | ClientRequest46 + | ClientRequest47 + | ClientRequest48 + | ClientRequest49 + ) = Field( + ..., description="Request from the client to the server.", title="ClientRequest" + ) + + +class Config(BaseModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + analytics: AnalyticsConfig | None = None + approval_policy: AskForApproval | None = None + compact_prompt: str | None = None + developer_instructions: str | None = None + forced_chatgpt_workspace_id: str | None = None + forced_login_method: ForcedLoginMethod | None = None + instructions: str | None = None + model: str | None = None + model_auto_compact_token_limit: int | None = None + model_context_window: int | None = None + model_provider: str | None = None + model_reasoning_effort: ReasoningEffort | None = None + model_reasoning_summary: ReasoningSummary | None = None + model_verbosity: Verbosity | None = None + profile: str | None = None + profiles: Dict[str, ProfileV2] | None = {} + review_model: str | None = None + sandbox_mode: SandboxMode | None = None + sandbox_workspace_write: SandboxWorkspaceWrite | None = None + service_tier: ServiceTier | None = None + tools: ToolsV2 | None = None + web_search: WebSearchMode | None = None + + +class ConfigReadResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + config: Config + layers: List[ConfigLayer] | None = None + origins: Dict[str, ConfigLayerMetadata] + + +class EventMsg4(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + payload: RealtimeEvent + type: Type22 = Field(..., title="RealtimeConversationRealtimeEventMsgType") + + +class EventMsg62(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + item: ResponseItem + type: Type80 = Field(..., title="RawResponseItemEventMsgType") + + +class RawResponseItemCompletedNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + item: ResponseItem + thread_id: str = Field(..., alias="threadId") + turn_id: str = Field(..., alias="turnId") + + +class ServerNotification2(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Method50 = Field(..., title="Thread/startedNotificationMethod") + params: ThreadStartedNotification + + +class ServerNotification( + RootModel[ + ServerNotification1 + | ServerNotification2 + | ServerNotification3 + | ServerNotification4 + | ServerNotification5 + | ServerNotification6 + | ServerNotification7 + | ServerNotification8 + | ServerNotification9 + | ServerNotification10 + | ServerNotification11 + | ServerNotification12 + | ServerNotification13 + | ServerNotification14 + | ServerNotification15 + | ServerNotification16 + | ServerNotification17 + | ServerNotification18 + | ServerNotification19 + | ServerNotification20 + | ServerNotification21 + | ServerNotification22 + | ServerNotification23 + | ServerNotification24 + | ServerNotification25 + | ServerNotification26 + | ServerNotification27 + | ServerNotification28 + | ServerNotification29 + | ServerNotification30 + | ServerNotification31 + | ServerNotification32 + | ServerNotification33 + | ServerNotification34 + | ServerNotification35 + | ServerNotification36 + | ServerNotification37 + | ServerNotification38 + | ServerNotification39 + | ServerNotification40 + | ServerNotification41 + | ServerNotification42 + | ServerNotification43 + | ServerNotification44 + | ServerNotification45 + | ServerNotification46 + ] +): + model_config = ConfigDict( + populate_by_name=True, + ) + root: ( + ServerNotification1 + | ServerNotification2 + | ServerNotification3 + | ServerNotification4 + | ServerNotification5 + | ServerNotification6 + | ServerNotification7 + | ServerNotification8 + | ServerNotification9 + | ServerNotification10 + | ServerNotification11 + | ServerNotification12 + | ServerNotification13 + | ServerNotification14 + | ServerNotification15 + | ServerNotification16 + | ServerNotification17 + | ServerNotification18 + | ServerNotification19 + | ServerNotification20 + | ServerNotification21 + | ServerNotification22 + | ServerNotification23 + | ServerNotification24 + | ServerNotification25 + | ServerNotification26 + | ServerNotification27 + | ServerNotification28 + | ServerNotification29 + | ServerNotification30 + | ServerNotification31 + | ServerNotification32 + | ServerNotification33 + | ServerNotification34 + | ServerNotification35 + | ServerNotification36 + | ServerNotification37 + | ServerNotification38 + | ServerNotification39 + | ServerNotification40 + | ServerNotification41 + | ServerNotification42 + | ServerNotification43 + | ServerNotification44 + | ServerNotification45 + | ServerNotification46 + ) = Field( + ..., + description="Notification sent from the server to the client.", + title="ServerNotification", + ) + + +class EventMsg20(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + approval_policy: AskForApproval = Field( + ..., description="When to escalate for approval for execution" + ) + cwd: str = Field( + ..., + description="Working directory that should be treated as the *root* of the session.", + ) + forked_from_id: ThreadId | None = None + history_entry_count: conint(ge=0) = Field( + ..., description="Current number of entries in the history log." + ) + history_log_id: conint(ge=0) = Field( + ..., + description="Identifier of the history log file (inode on Unix, 0 otherwise).", + ) + initial_messages: List[EventMsg] | None = Field( + None, + description="Optional initial messages (as events) for resumed sessions. When present, UIs can use these to seed the history.", + ) + model: str = Field(..., description="Tell the client what model is being queried.") + model_provider_id: str + network_proxy: SessionNetworkProxyRuntime | None = Field( + None, + description="Runtime proxy bind addresses, when the managed proxy was started for this session.", + ) + reasoning_effort: ReasoningEffort | None = Field( + None, + description="The effort the model is putting into reasoning about the user's request.", + ) + rollout_path: str | None = Field( + None, + description="Path in which the rollout is stored. Can be `None` for ephemeral threads", + ) + sandbox_policy: SandboxPolicy = Field( + ..., description="How to sandbox commands executed in the system" + ) + service_tier: ServiceTier | None = None + session_id: ThreadId + thread_name: str | None = Field( + None, description="Optional user-facing thread name (may be unset)." + ) + type: Type38 = Field(..., title="SessionConfiguredEventMsgType") + + +class EventMsg( + RootModel[ + EventMsg1 + | EventMsg2 + | EventMsg3 + | EventMsg4 + | EventMsg5 + | EventMsg6 + | EventMsg7 + | EventMsg8 + | EventMsg9 + | EventMsg10 + | EventMsg11 + | EventMsg12 + | EventMsg13 + | EventMsg14 + | EventMsg15 + | EventMsg16 + | EventMsg17 + | EventMsg18 + | EventMsg19 + | EventMsg20 + | EventMsg21 + | EventMsg22 + | EventMsg23 + | EventMsg24 + | EventMsg25 + | EventMsg26 + | EventMsg27 + | EventMsg28 + | EventMsg29 + | EventMsg30 + | EventMsg31 + | EventMsg32 + | EventMsg33 + | EventMsg34 + | EventMsg35 + | EventMsg36 + | EventMsg37 + | EventMsg38 + | EventMsg39 + | EventMsg40 + | EventMsg41 + | EventMsg42 + | EventMsg43 + | EventMsg44 + | EventMsg45 + | EventMsg46 + | EventMsg47 + | EventMsg48 + | EventMsg49 + | EventMsg50 + | EventMsg51 + | EventMsg52 + | EventMsg53 + | EventMsg54 + | EventMsg55 + | EventMsg56 + | EventMsg57 + | EventMsg58 + | EventMsg59 + | EventMsg60 + | EventMsg61 + | EventMsg62 + | EventMsg63 + | EventMsg64 + | EventMsg65 + | EventMsg66 + | EventMsg67 + | EventMsg68 + | EventMsg69 + | EventMsg70 + | EventMsg71 + | EventMsg72 + | EventMsg73 + | EventMsg74 + | EventMsg75 + | EventMsg76 + | EventMsg77 + | EventMsg78 + | EventMsg79 + | EventMsg80 + ] +): + model_config = ConfigDict( + populate_by_name=True, + ) + root: ( + EventMsg1 + | EventMsg2 + | EventMsg3 + | EventMsg4 + | EventMsg5 + | EventMsg6 + | EventMsg7 + | EventMsg8 + | EventMsg9 + | EventMsg10 + | EventMsg11 + | EventMsg12 + | EventMsg13 + | EventMsg14 + | EventMsg15 + | EventMsg16 + | EventMsg17 + | EventMsg18 + | EventMsg19 + | EventMsg20 + | EventMsg21 + | EventMsg22 + | EventMsg23 + | EventMsg24 + | EventMsg25 + | EventMsg26 + | EventMsg27 + | EventMsg28 + | EventMsg29 + | EventMsg30 + | EventMsg31 + | EventMsg32 + | EventMsg33 + | EventMsg34 + | EventMsg35 + | EventMsg36 + | EventMsg37 + | EventMsg38 + | EventMsg39 + | EventMsg40 + | EventMsg41 + | EventMsg42 + | EventMsg43 + | EventMsg44 + | EventMsg45 + | EventMsg46 + | EventMsg47 + | EventMsg48 + | EventMsg49 + | EventMsg50 + | EventMsg51 + | EventMsg52 + | EventMsg53 + | EventMsg54 + | EventMsg55 + | EventMsg56 + | EventMsg57 + | EventMsg58 + | EventMsg59 + | EventMsg60 + | EventMsg61 + | EventMsg62 + | EventMsg63 + | EventMsg64 + | EventMsg65 + | EventMsg66 + | EventMsg67 + | EventMsg68 + | EventMsg69 + | EventMsg70 + | EventMsg71 + | EventMsg72 + | EventMsg73 + | EventMsg74 + | EventMsg75 + | EventMsg76 + | EventMsg77 + | EventMsg78 + | EventMsg79 + | EventMsg80 + ) = Field( + ..., + description="Response event from the agent NOTE: Make sure none of these values have optional types, as it will mess up the extension code-gen.", + title="EventMsg", + ) + + +EventMsg20.model_rebuild() diff --git a/sdk/python/src/codex_app_server/generated/v2_types.py b/sdk/python/src/codex_app_server/generated/v2_types.py new file mode 100644 index 00000000000..932ab438dba --- /dev/null +++ b/sdk/python/src/codex_app_server/generated/v2_types.py @@ -0,0 +1,25 @@ +"""Stable aliases over full v2 autogenerated models (datamodel-code-generator).""" + +from .v2_all.ModelListResponse import ModelListResponse +from .v2_all.ThreadCompactStartResponse import ThreadCompactStartResponse +from .v2_all.ThreadListResponse import ThreadListResponse +from .v2_all.ThreadReadResponse import ThreadReadResponse +from .v2_all.ThreadTokenUsageUpdatedNotification import ( + ThreadTokenUsageUpdatedNotification, +) +from .v2_all.TurnCompletedNotification import ThreadItem153 as ThreadItem +from .v2_all.TurnCompletedNotification import ( + TurnCompletedNotification as TurnCompletedNotificationPayload, +) +from .v2_all.TurnSteerResponse import TurnSteerResponse + +__all__ = [ + "ModelListResponse", + "ThreadCompactStartResponse", + "ThreadListResponse", + "ThreadReadResponse", + "ThreadTokenUsageUpdatedNotification", + "TurnCompletedNotificationPayload", + "TurnSteerResponse", + "ThreadItem", +] diff --git a/sdk/python/src/codex_app_server/models.py b/sdk/python/src/codex_app_server/models.py new file mode 100644 index 00000000000..7c5bb34de8d --- /dev/null +++ b/sdk/python/src/codex_app_server/models.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import TypeAlias + +from pydantic import BaseModel + +from .generated.v2_all import ( + AccountLoginCompletedNotification, + AccountRateLimitsUpdatedNotification, + AccountUpdatedNotification, + AgentMessageDeltaNotification, + AppListUpdatedNotification, + CommandExecutionOutputDeltaNotification, + ConfigWarningNotification, + ContextCompactedNotification, + DeprecationNoticeNotification, + ErrorNotification, + FileChangeOutputDeltaNotification, + ItemCompletedNotification, + ItemStartedNotification, + McpServerOauthLoginCompletedNotification, + McpToolCallProgressNotification, + PlanDeltaNotification, + RawResponseItemCompletedNotification, + ReasoningSummaryPartAddedNotification, + ReasoningSummaryTextDeltaNotification, + ReasoningTextDeltaNotification, + TerminalInteractionNotification, + ThreadNameUpdatedNotification, + ThreadStartedNotification, + ThreadTokenUsageUpdatedNotification, + TurnCompletedNotification, + TurnDiffUpdatedNotification, + TurnPlanUpdatedNotification, + TurnStartedNotification, + WindowsWorldWritableWarningNotification, +) + +JsonScalar: TypeAlias = str | int | float | bool | None +JsonValue: TypeAlias = JsonScalar | dict[str, "JsonValue"] | list["JsonValue"] +JsonObject: TypeAlias = dict[str, JsonValue] + + +@dataclass(slots=True) +class UnknownNotification: + params: JsonObject + + +NotificationPayload: TypeAlias = ( + AccountLoginCompletedNotification + | AccountRateLimitsUpdatedNotification + | AccountUpdatedNotification + | AgentMessageDeltaNotification + | AppListUpdatedNotification + | CommandExecutionOutputDeltaNotification + | ConfigWarningNotification + | ContextCompactedNotification + | DeprecationNoticeNotification + | ErrorNotification + | FileChangeOutputDeltaNotification + | ItemCompletedNotification + | ItemStartedNotification + | McpServerOauthLoginCompletedNotification + | McpToolCallProgressNotification + | PlanDeltaNotification + | RawResponseItemCompletedNotification + | ReasoningSummaryPartAddedNotification + | ReasoningSummaryTextDeltaNotification + | ReasoningTextDeltaNotification + | TerminalInteractionNotification + | ThreadNameUpdatedNotification + | ThreadStartedNotification + | ThreadTokenUsageUpdatedNotification + | TurnCompletedNotification + | TurnDiffUpdatedNotification + | TurnPlanUpdatedNotification + | TurnStartedNotification + | WindowsWorldWritableWarningNotification + | UnknownNotification +) + + +@dataclass(slots=True) +class Notification: + method: str + payload: NotificationPayload + + +class ServerInfo(BaseModel): + name: str | None = None + version: str | None = None + + +class InitializeResponse(BaseModel): + serverInfo: ServerInfo | None = None + userAgent: str | None = None diff --git a/sdk/python/src/codex_app_server/py.typed b/sdk/python/src/codex_app_server/py.typed new file mode 100644 index 00000000000..e69de29bb2d diff --git a/sdk/python/src/codex_app_server/retry.py b/sdk/python/src/codex_app_server/retry.py new file mode 100644 index 00000000000..b7e4f774034 --- /dev/null +++ b/sdk/python/src/codex_app_server/retry.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +import random +import time +from typing import Callable, TypeVar + +from .errors import is_retryable_error + +T = TypeVar("T") + + +def retry_on_overload( + op: Callable[[], T], + *, + max_attempts: int = 3, + initial_delay_s: float = 0.25, + max_delay_s: float = 2.0, + jitter_ratio: float = 0.2, +) -> T: + """Retry helper for transient server-overload errors.""" + + if max_attempts < 1: + raise ValueError("max_attempts must be >= 1") + + delay = initial_delay_s + attempt = 0 + while True: + attempt += 1 + try: + return op() + except Exception as exc: + if attempt >= max_attempts: + raise + if not is_retryable_error(exc): + raise + + jitter = delay * jitter_ratio + sleep_for = min(max_delay_s, delay) + random.uniform(-jitter, jitter) + if sleep_for > 0: + time.sleep(sleep_for) + delay = min(max_delay_s, delay * 2) diff --git a/sdk/python/tests/conftest.py b/sdk/python/tests/conftest.py new file mode 100644 index 00000000000..f23f55e4ddf --- /dev/null +++ b/sdk/python/tests/conftest.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SRC = ROOT / "src" + +src_str = str(SRC) +if src_str in sys.path: + sys.path.remove(src_str) +sys.path.insert(0, src_str) + +for module_name in list(sys.modules): + if module_name == "codex_app_server" or module_name.startswith("codex_app_server."): + sys.modules.pop(module_name) diff --git a/sdk/python/tests/test_artifact_workflow_and_binaries.py b/sdk/python/tests/test_artifact_workflow_and_binaries.py new file mode 100644 index 00000000000..2fd8a34f140 --- /dev/null +++ b/sdk/python/tests/test_artifact_workflow_and_binaries.py @@ -0,0 +1,146 @@ +from __future__ import annotations + +import ast +import importlib.util +import json +import platform +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +def _load_update_script_module(): + script_path = ROOT / "scripts" / "update_sdk_artifacts.py" + spec = importlib.util.spec_from_file_location("update_sdk_artifacts", script_path) + if spec is None or spec.loader is None: + raise AssertionError(f"Failed to load script module: {script_path}") + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +def test_generation_has_single_maintenance_entrypoint_script() -> None: + scripts = sorted(p.name for p in (ROOT / "scripts").glob("*.py")) + assert scripts == ["update_sdk_artifacts.py"] + + +def test_generate_types_wires_all_generation_steps() -> None: + source = (ROOT / "scripts" / "update_sdk_artifacts.py").read_text() + tree = ast.parse(source) + + generate_types_fn = next( + (node for node in tree.body if isinstance(node, ast.FunctionDef) and node.name == "generate_types"), + None, + ) + assert generate_types_fn is not None + + calls: list[str] = [] + for node in generate_types_fn.body: + if isinstance(node, ast.Expr) and isinstance(node.value, ast.Call): + fn = node.value.func + if isinstance(fn, ast.Name): + calls.append(fn.id) + + assert calls == [ + "generate_v2_all", + "generate_notification_registry", + "generate_public_api_flat_methods", + ] + + +def test_schema_normalization_only_flattens_string_literal_oneofs() -> None: + script = _load_update_script_module() + schema = json.loads( + ( + ROOT.parent.parent + / "codex-rs" + / "app-server-protocol" + / "schema" + / "json" + / "codex_app_server_protocol.v2.schemas.json" + ).read_text() + ) + + definitions = schema["definitions"] + flattened = [ + name + for name, definition in definitions.items() + if isinstance(definition, dict) + and script._flatten_string_enum_one_of(definition.copy()) + ] + + assert flattened == [ + "AuthMode", + "CommandExecOutputStream", + "ExperimentalFeatureStage", + "InputModality", + "MessagePhase", + ] + + +def test_bundled_binaries_exist_for_all_supported_platforms() -> None: + script = _load_update_script_module() + for platform_key in script.PLATFORMS: + bin_path = script.bundled_platform_bin_path(platform_key) + assert bin_path.is_file(), f"Missing bundled binary: {bin_path}" + + +def test_default_runtime_uses_current_platform_bundled_binary() -> None: + client_source = (ROOT / "src" / "codex_app_server" / "client.py").read_text() + client_tree = ast.parse(client_source) + + # Keep this assertion source-level so it works in both PR2 (types foundation) + # and PR3 (full SDK), regardless of runtime module wiring. + app_server_config = next( + ( + node + for node in client_tree.body + if isinstance(node, ast.ClassDef) and node.name == "AppServerConfig" + ), + None, + ) + assert app_server_config is not None + + codex_bin_field = next( + ( + node + for node in app_server_config.body + if isinstance(node, ast.AnnAssign) + and isinstance(node.target, ast.Name) + and node.target.id == "codex_bin" + ), + None, + ) + assert codex_bin_field is not None + assert isinstance(codex_bin_field.value, ast.Call) + assert isinstance(codex_bin_field.value.func, ast.Name) + assert codex_bin_field.value.func.id == "str" + assert len(codex_bin_field.value.args) == 1 + bundled_call = codex_bin_field.value.args[0] + assert isinstance(bundled_call, ast.Call) + assert isinstance(bundled_call.func, ast.Name) + assert bundled_call.func.id == "_bundled_codex_path" + + bin_root = (ROOT / "src" / "codex_app_server" / "bin").resolve() + + sys_name = platform.system().lower() + machine = platform.machine().lower() + is_arm = machine in {"arm64", "aarch64"} + + if sys_name.startswith("darwin"): + platform_dir = "darwin-arm64" if is_arm else "darwin-x64" + exe = "codex" + elif sys_name.startswith("linux"): + platform_dir = "linux-arm64" if is_arm else "linux-x64" + exe = "codex" + elif sys_name.startswith("windows"): + platform_dir = "windows-arm64" if is_arm else "windows-x64" + exe = "codex.exe" + else: + raise AssertionError(f"Unsupported platform in test: {sys_name}/{machine}") + + expected = (bin_root / platform_dir / exe).resolve() + assert expected.is_file() diff --git a/sdk/python/tests/test_client_rpc_methods.py b/sdk/python/tests/test_client_rpc_methods.py new file mode 100644 index 00000000000..27421805669 --- /dev/null +++ b/sdk/python/tests/test_client_rpc_methods.py @@ -0,0 +1,95 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Any + +from codex_app_server.client import AppServerClient, _params_dict +from codex_app_server.generated.v2_all import ThreadListParams, ThreadTokenUsageUpdatedNotification +from codex_app_server.models import UnknownNotification + +ROOT = Path(__file__).resolve().parents[1] + + +def test_thread_set_name_and_compact_use_current_rpc_methods() -> None: + client = AppServerClient() + calls: list[tuple[str, dict[str, Any] | None]] = [] + + def fake_request(method: str, params, *, response_model): # type: ignore[no-untyped-def] + calls.append((method, params)) + return response_model.model_validate({}) + + client.request = fake_request # type: ignore[method-assign] + + client.thread_set_name("thread-1", "sdk-name") + client.thread_compact("thread-1") + + assert calls[0][0] == "thread/name/set" + assert calls[1][0] == "thread/compact/start" + + +def test_generated_params_models_are_snake_case_and_dump_by_alias() -> None: + params = ThreadListParams(search_term="needle", limit=5) + + assert "search_term" in ThreadListParams.model_fields + dumped = _params_dict(params) + assert dumped == {"searchTerm": "needle", "limit": 5} + + +def test_generated_v2_bundle_has_single_shared_plan_type_definition() -> None: + source = (ROOT / "src" / "codex_app_server" / "generated" / "v2_all.py").read_text() + assert source.count("class PlanType(") == 1 + + +def test_notifications_are_typed_with_canonical_v2_methods() -> None: + client = AppServerClient() + event = client._coerce_notification( + "thread/tokenUsage/updated", + { + "threadId": "thread-1", + "turnId": "turn-1", + "tokenUsage": { + "last": { + "cachedInputTokens": 0, + "inputTokens": 1, + "outputTokens": 2, + "reasoningOutputTokens": 0, + "totalTokens": 3, + }, + "total": { + "cachedInputTokens": 0, + "inputTokens": 1, + "outputTokens": 2, + "reasoningOutputTokens": 0, + "totalTokens": 3, + }, + }, + }, + ) + + assert event.method == "thread/tokenUsage/updated" + assert isinstance(event.payload, ThreadTokenUsageUpdatedNotification) + assert event.payload.turn_id == "turn-1" + + +def test_unknown_notifications_fall_back_to_unknown_payloads() -> None: + client = AppServerClient() + event = client._coerce_notification( + "unknown/notification", + { + "id": "evt-1", + "conversationId": "thread-1", + "msg": {"type": "turn_aborted"}, + }, + ) + + assert event.method == "unknown/notification" + assert isinstance(event.payload, UnknownNotification) + assert event.payload.params["msg"] == {"type": "turn_aborted"} + + +def test_invalid_notification_payload_falls_back_to_unknown() -> None: + client = AppServerClient() + event = client._coerce_notification("thread/tokenUsage/updated", {"threadId": "missing"}) + + assert event.method == "thread/tokenUsage/updated" + assert isinstance(event.payload, UnknownNotification) diff --git a/sdk/python/tests/test_contract_generation.py b/sdk/python/tests/test_contract_generation.py new file mode 100644 index 00000000000..4865494e23c --- /dev/null +++ b/sdk/python/tests/test_contract_generation.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +import os +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +GENERATED_TARGETS = [ + Path("src/codex_app_server/generated/notification_registry.py"), + Path("src/codex_app_server/generated/v2_all.py"), + Path("src/codex_app_server/public_api.py"), +] + + +def _snapshot_target(root: Path, rel_path: Path) -> dict[str, bytes] | bytes | None: + target = root / rel_path + if not target.exists(): + return None + if target.is_file(): + return target.read_bytes() + + snapshot: dict[str, bytes] = {} + for path in sorted(target.rglob("*")): + if path.is_file() and "__pycache__" not in path.parts: + snapshot[str(path.relative_to(target))] = path.read_bytes() + return snapshot + + +def _snapshot_targets(root: Path) -> dict[str, dict[str, bytes] | bytes | None]: + return { + str(rel_path): _snapshot_target(root, rel_path) for rel_path in GENERATED_TARGETS + } + + +def test_generated_files_are_up_to_date(): + before = _snapshot_targets(ROOT) + + # Regenerate contract artifacts via single maintenance entrypoint. + env = os.environ.copy() + python_bin = str(Path(sys.executable).parent) + env["PATH"] = f"{python_bin}{os.pathsep}{env.get('PATH', '')}" + + subprocess.run( + [sys.executable, "scripts/update_sdk_artifacts.py", "--types-only"], + cwd=ROOT, + check=True, + env=env, + ) + + after = _snapshot_targets(ROOT) + assert before == after, "Generated files drifted after regeneration"