merge: pull latest upstream (prefer ours)

This commit is contained in:
James Peter
2025-08-19 08:09:24 +10:00
18 changed files with 528 additions and 92 deletions
+13
View File
@@ -786,6 +786,7 @@ dependencies = [
"codex-login", "codex-login",
"codex-mcp-server", "codex-mcp-server",
"codex-protocol", "codex-protocol",
"codex-protocol-ts",
"codex-tui", "codex-tui",
"serde_json", "serde_json",
"tokio", "tokio",
@@ -1030,9 +1031,20 @@ dependencies = [
"serde_json", "serde_json",
"strum 0.27.2", "strum 0.27.2",
"strum_macros 0.27.2", "strum_macros 0.27.2",
"ts-rs",
"uuid", "uuid",
] ]
[[package]]
name = "codex-protocol-ts"
version = "0.0.0"
dependencies = [
"anyhow",
"clap",
"codex-protocol",
"ts-rs",
]
[[package]] [[package]]
name = "codex-tui" name = "codex-tui"
version = "0.0.0" version = "0.0.0"
@@ -5574,6 +5586,7 @@ dependencies = [
"serde_json", "serde_json",
"thiserror 2.0.12", "thiserror 2.0.12",
"ts-rs-macros", "ts-rs-macros",
"uuid",
] ]
[[package]] [[package]]
+1
View File
@@ -18,6 +18,7 @@ members = [
"protocol", "protocol",
"ollama", "ollama",
"protocol", "protocol",
"protocol-ts",
"tui", "tui",
] ]
resolver = "2" resolver = "2"
+1
View File
@@ -37,3 +37,4 @@ tokio = { version = "1", features = [
] } ] }
tracing = "0.1.41" tracing = "0.1.41"
tracing-subscriber = "0.3.19" tracing-subscriber = "0.3.19"
codex-protocol-ts = { path = "../protocol-ts" }
+18
View File
@@ -73,6 +73,10 @@ enum Subcommand {
/// Apply the latest diff produced by Codex agent as a `git apply` to your local working tree. /// Apply the latest diff produced by Codex agent as a `git apply` to your local working tree.
#[clap(visible_alias = "a")] #[clap(visible_alias = "a")]
Apply(ApplyCommand), Apply(ApplyCommand),
/// Internal: generate TypeScript protocol bindings.
#[clap(hide = true)]
GenerateTs(GenerateTsCommand),
} }
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
@@ -121,6 +125,17 @@ struct LogoutCommand {
config_overrides: CliConfigOverrides, config_overrides: CliConfigOverrides,
} }
#[derive(Debug, Parser)]
struct GenerateTsCommand {
/// Output directory where .ts files will be written
#[arg(short = 'o', long = "out", value_name = "DIR")]
out_dir: PathBuf,
/// Optional path to the Prettier executable to format generated files
#[arg(short = 'p', long = "prettier", value_name = "PRETTIER_BIN")]
prettier: Option<PathBuf>,
}
fn main() -> anyhow::Result<()> { fn main() -> anyhow::Result<()> {
arg0_dispatch_or_else(|codex_linux_sandbox_exe| async move { arg0_dispatch_or_else(|codex_linux_sandbox_exe| async move {
cli_main(codex_linux_sandbox_exe).await?; cli_main(codex_linux_sandbox_exe).await?;
@@ -195,6 +210,9 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
prepend_config_flags(&mut apply_cli.config_overrides, cli.config_overrides); prepend_config_flags(&mut apply_cli.config_overrides, cli.config_overrides);
run_apply_command(apply_cli, None).await?; run_apply_command(apply_cli, None).await?;
} }
Some(Subcommand::GenerateTs(gen_cli)) => {
codex_protocol_ts::generate_ts(&gen_cli.out_dir, gen_cli.prettier.as_deref())?;
}
} }
Ok(()) Ok(())
+27
View File
@@ -37,6 +37,7 @@ use crate::error::CodexErr;
use crate::error::Result; use crate::error::Result;
use crate::error::UsageLimitReachedError; use crate::error::UsageLimitReachedError;
use crate::flags::CODEX_RS_SSE_FIXTURE; use crate::flags::CODEX_RS_SSE_FIXTURE;
use crate::model_family::ModelFamily;
use crate::model_provider_info::ModelProviderInfo; use crate::model_provider_info::ModelProviderInfo;
use crate::model_provider_info::WireApi; use crate::model_provider_info::WireApi;
use crate::models::ResponseItem; use crate::models::ResponseItem;
@@ -44,6 +45,8 @@ use crate::openai_tools::create_tools_json_for_responses_api;
use crate::protocol::TokenUsage; use crate::protocol::TokenUsage;
use crate::user_agent::get_codex_user_agent; use crate::user_agent::get_codex_user_agent;
use crate::util::backoff; use crate::util::backoff;
use codex_protocol::config_types::ReasoningEffort as ReasoningEffortConfig;
use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
use std::sync::Arc; use std::sync::Arc;
use std::sync::Mutex; use std::sync::Mutex;
@@ -385,6 +388,30 @@ impl ModelClient {
pub fn get_provider(&self) -> ModelProviderInfo { pub fn get_provider(&self) -> ModelProviderInfo {
self.provider.clone() self.provider.clone()
} }
/// Returns the currently configured model slug.
pub fn get_model(&self) -> String {
self.config.model.clone()
}
/// Returns the currently configured model family.
pub fn get_model_family(&self) -> ModelFamily {
self.config.model_family.clone()
}
/// Returns the current reasoning effort setting.
pub fn get_reasoning_effort(&self) -> ReasoningEffortConfig {
self.effort
}
/// Returns the current reasoning summary setting.
pub fn get_reasoning_summary(&self) -> ReasoningSummaryConfig {
self.summary
}
pub fn get_auth(&self) -> Option<CodexAuth> {
self.auth.clone()
}
} }
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
+5 -55
View File
@@ -9,6 +9,8 @@ use crate::models::ResponseItem;
use crate::openai_tools::OpenAiTool; use crate::openai_tools::OpenAiTool;
use crate::protocol::TokenUsage; use crate::protocol::TokenUsage;
use codex_apply_patch::APPLY_PATCH_TOOL_INSTRUCTIONS; use codex_apply_patch::APPLY_PATCH_TOOL_INSTRUCTIONS;
use codex_protocol::config_types::ReasoningEffort as ReasoningEffortConfig;
use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
use futures::Stream; use futures::Stream;
use serde::Serialize; use serde::Serialize;
use std::borrow::Cow; use std::borrow::Cow;
@@ -178,55 +180,8 @@ pub enum ResponseEvent {
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
pub(crate) struct Reasoning { pub(crate) struct Reasoning {
pub(crate) effort: OpenAiReasoningEffort, pub(crate) effort: ReasoningEffortConfig,
#[serde(skip_serializing_if = "Option::is_none")] pub(crate) summary: ReasoningSummaryConfig,
pub(crate) summary: Option<OpenAiReasoningSummary>,
}
/// See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning
#[derive(Debug, Serialize, Default, Clone, Copy)]
#[serde(rename_all = "lowercase")]
pub(crate) enum OpenAiReasoningEffort {
Minimal,
Low,
#[default]
Medium,
High,
}
impl From<ReasoningEffortConfig> for Option<OpenAiReasoningEffort> {
fn from(effort: ReasoningEffortConfig) -> Self {
match effort {
ReasoningEffortConfig::Minimal => Some(OpenAiReasoningEffort::Minimal),
ReasoningEffortConfig::Low => Some(OpenAiReasoningEffort::Low),
ReasoningEffortConfig::Medium => Some(OpenAiReasoningEffort::Medium),
ReasoningEffortConfig::High => Some(OpenAiReasoningEffort::High),
ReasoningEffortConfig::None => None,
}
}
}
/// 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
#[derive(Debug, Serialize, Default, Clone, Copy)]
#[serde(rename_all = "lowercase")]
pub(crate) enum OpenAiReasoningSummary {
#[default]
Auto,
Concise,
Detailed,
}
impl From<ReasoningSummaryConfig> for Option<OpenAiReasoningSummary> {
fn from(summary: ReasoningSummaryConfig) -> Self {
match summary {
ReasoningSummaryConfig::Auto => Some(OpenAiReasoningSummary::Auto),
ReasoningSummaryConfig::Concise => Some(OpenAiReasoningSummary::Concise),
ReasoningSummaryConfig::Detailed => Some(OpenAiReasoningSummary::Detailed),
ReasoningSummaryConfig::None => None,
}
}
} }
/// Text configuration for verbosity level in OpenAI API responses. /// Text configuration for verbosity level in OpenAI API responses.
@@ -350,12 +305,7 @@ pub(crate) fn create_reasoning_param_for_request(
summary: ReasoningSummaryConfig, summary: ReasoningSummaryConfig,
) -> Option<Reasoning> { ) -> Option<Reasoning> {
if model_family.supports_reasoning_summaries { if model_family.supports_reasoning_summaries {
let effort: Option<OpenAiReasoningEffort> = effort.into(); Some(Reasoning { effort, summary })
let effort = effort?;
Some(Reasoning {
effort,
summary: summary.into(),
})
} else { } else {
None None
} }
+2 -2
View File
@@ -4,8 +4,6 @@ use crate::config_types::BrowserConfig;
use crate::config_types::History; use crate::config_types::History;
use crate::config_types::ThemeName; use crate::config_types::ThemeName;
use crate::config_types::McpServerConfig; use crate::config_types::McpServerConfig;
use crate::config_types::ReasoningEffort;
use crate::config_types::ReasoningSummary;
use crate::config_types::SandboxWorkspaceWrite; use crate::config_types::SandboxWorkspaceWrite;
use crate::config_types::ShellEnvironmentPolicy; use crate::config_types::ShellEnvironmentPolicy;
use crate::config_types::ShellEnvironmentPolicyToml; use crate::config_types::ShellEnvironmentPolicyToml;
@@ -19,6 +17,8 @@ use crate::model_provider_info::built_in_model_providers;
use crate::openai_model_info::get_model_info; use crate::openai_model_info::get_model_info;
use crate::protocol::AskForApproval; use crate::protocol::AskForApproval;
use crate::protocol::SandboxPolicy; use crate::protocol::SandboxPolicy;
use codex_protocol::config_types::ReasoningEffort;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::SandboxMode; use codex_protocol::config_types::SandboxMode;
use dirs::home_dir; use dirs::home_dir;
use serde::Deserialize; use serde::Deserialize;
+2
View File
@@ -5,6 +5,8 @@ use crate::config_types::ReasoningEffort;
use crate::config_types::ReasoningSummary; use crate::config_types::ReasoningSummary;
use crate::config_types::TextVerbosity; use crate::config_types::TextVerbosity;
use crate::protocol::AskForApproval; use crate::protocol::AskForApproval;
use codex_protocol::config_types::ReasoningEffort;
use codex_protocol::config_types::ReasoningSummary;
/// Collection of common configuration options that a user can define as a unit /// Collection of common configuration options that a user can define as a unit
/// in `config.toml`. /// in `config.toml`.
-2
View File
@@ -5,11 +5,9 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::path::PathBuf; use std::path::PathBuf;
use strum_macros::Display;
use wildmatch::WildMatchPattern; use wildmatch::WildMatchPattern;
use serde::Deserialize; use serde::Deserialize;
use serde::Serialize;
/// Configuration for external agent models /// Configuration for external agent models
#[derive(Deserialize, Debug, Clone, PartialEq)] #[derive(Deserialize, Debug, Clone, PartialEq)]
+231
View File
@@ -1,9 +1,13 @@
use codex_core::ConversationManager; use codex_core::ConversationManager;
use codex_core::ModelProviderInfo; use codex_core::ModelProviderInfo;
use codex_core::built_in_model_providers; use codex_core::built_in_model_providers;
use codex_core::protocol::AskForApproval;
use codex_core::protocol::EventMsg; use codex_core::protocol::EventMsg;
use codex_core::protocol::InputItem; use codex_core::protocol::InputItem;
use codex_core::protocol::Op; use codex_core::protocol::Op;
use codex_core::protocol::SandboxPolicy;
use codex_core::protocol_config_types::ReasoningEffort;
use codex_core::protocol_config_types::ReasoningSummary;
use codex_login::CodexAuth; use codex_login::CodexAuth;
use core_test_support::load_default_config_for_test; use core_test_support::load_default_config_for_test;
use core_test_support::load_sse_fixture_with_id; use core_test_support::load_sse_fixture_with_id;
@@ -129,3 +133,230 @@ async fn prefixes_context_and_instructions_once_and_consistently_across_requests
); );
assert_eq!(body2["input"], expected_body2); assert_eq!(body2["input"], expected_body2);
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() {
use pretty_assertions::assert_eq;
let server = MockServer::start().await;
let sse = sse_completed("resp");
let template = ResponseTemplate::new(200)
.insert_header("content-type", "text/event-stream")
.set_body_raw(sse, "text/event-stream");
// Expect two POSTs to /v1/responses
Mock::given(method("POST"))
.and(path("/v1/responses"))
.respond_with(template)
.expect(2)
.mount(&server)
.await;
let model_provider = ModelProviderInfo {
base_url: Some(format!("{}/v1", server.uri())),
..built_in_model_providers()["openai"].clone()
};
let cwd = TempDir::new().unwrap();
let codex_home = TempDir::new().unwrap();
let mut config = load_default_config_for_test(&codex_home);
config.cwd = cwd.path().to_path_buf();
config.model_provider = model_provider;
config.user_instructions = Some("be consistent and helpful".to_string());
let conversation_manager = ConversationManager::default();
let codex = conversation_manager
.new_conversation_with_auth(config, Some(CodexAuth::from_api_key("Test API Key")))
.await
.expect("create new conversation")
.conversation;
// First turn
codex
.submit(Op::UserInput {
items: vec![InputItem::Text {
text: "hello 1".into(),
}],
})
.await
.unwrap();
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await;
// Change everything about the turn context.
let new_cwd = TempDir::new().unwrap();
let writable = TempDir::new().unwrap();
codex
.submit(Op::OverrideTurnContext {
cwd: Some(new_cwd.path().to_path_buf()),
approval_policy: Some(AskForApproval::Never),
sandbox_policy: Some(SandboxPolicy::WorkspaceWrite {
writable_roots: vec![writable.path().to_path_buf()],
network_access: true,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
}),
model: Some("o3".to_string()),
effort: Some(ReasoningEffort::High),
summary: Some(ReasoningSummary::Detailed),
})
.await
.unwrap();
// Second turn after overrides
codex
.submit(Op::UserInput {
items: vec![InputItem::Text {
text: "hello 2".into(),
}],
})
.await
.unwrap();
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await;
// Verify we issued exactly two requests, and the cached prefix stayed identical.
let requests = server.received_requests().await.unwrap();
assert_eq!(requests.len(), 2, "expected two POST requests");
let body1 = requests[0].body_json::<serde_json::Value>().unwrap();
let body2 = requests[1].body_json::<serde_json::Value>().unwrap();
// prompt_cache_key should remain constant across overrides
assert_eq!(
body1["prompt_cache_key"], body2["prompt_cache_key"],
"prompt_cache_key should not change across overrides"
);
// The entire prefix from the first request should be identical and reused
// as the prefix of the second request, ensuring cache hit potential.
let expected_user_message_2 = serde_json::json!({
"type": "message",
"id": serde_json::Value::Null,
"role": "user",
"content": [ { "type": "input_text", "text": "hello 2" } ]
});
// After overriding the turn context, the environment context should be emitted again
// reflecting the new cwd, approval policy and sandbox settings.
let expected_env_text_2 = format!(
"<environment_context>\nCurrent working directory: {}\nApproval policy: never\nSandbox mode: workspace-write\nNetwork access: enabled\n</environment_context>",
new_cwd.path().to_string_lossy()
);
let expected_env_msg_2 = serde_json::json!({
"type": "message",
"id": serde_json::Value::Null,
"role": "user",
"content": [ { "type": "input_text", "text": expected_env_text_2 } ]
});
let expected_body2 = serde_json::json!(
[
body1["input"].as_array().unwrap().as_slice(),
[expected_env_msg_2, expected_user_message_2].as_slice(),
]
.concat()
);
assert_eq!(body2["input"], expected_body2);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn per_turn_overrides_keep_cached_prefix_and_key_constant() {
use pretty_assertions::assert_eq;
let server = MockServer::start().await;
let sse = sse_completed("resp");
let template = ResponseTemplate::new(200)
.insert_header("content-type", "text/event-stream")
.set_body_raw(sse, "text/event-stream");
// Expect two POSTs to /v1/responses
Mock::given(method("POST"))
.and(path("/v1/responses"))
.respond_with(template)
.expect(2)
.mount(&server)
.await;
let model_provider = ModelProviderInfo {
base_url: Some(format!("{}/v1", server.uri())),
..built_in_model_providers()["openai"].clone()
};
let cwd = TempDir::new().unwrap();
let codex_home = TempDir::new().unwrap();
let mut config = load_default_config_for_test(&codex_home);
config.cwd = cwd.path().to_path_buf();
config.model_provider = model_provider;
config.user_instructions = Some("be consistent and helpful".to_string());
let conversation_manager = ConversationManager::default();
let codex = conversation_manager
.new_conversation_with_auth(config, Some(CodexAuth::from_api_key("Test API Key")))
.await
.expect("create new conversation")
.conversation;
// First turn
codex
.submit(Op::UserInput {
items: vec![InputItem::Text {
text: "hello 1".into(),
}],
})
.await
.unwrap();
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await;
// Second turn using per-turn overrides via UserTurn
let new_cwd = TempDir::new().unwrap();
let writable = TempDir::new().unwrap();
codex
.submit(Op::UserTurn {
items: vec![InputItem::Text {
text: "hello 2".into(),
}],
cwd: new_cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
sandbox_policy: SandboxPolicy::WorkspaceWrite {
writable_roots: vec![writable.path().to_path_buf()],
network_access: true,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
},
model: "o3".to_string(),
effort: ReasoningEffort::High,
summary: ReasoningSummary::Detailed,
})
.await
.unwrap();
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await;
// Verify we issued exactly two requests, and the cached prefix stayed identical.
let requests = server.received_requests().await.unwrap();
assert_eq!(requests.len(), 2, "expected two POST requests");
let body1 = requests[0].body_json::<serde_json::Value>().unwrap();
let body2 = requests[1].body_json::<serde_json::Value>().unwrap();
// prompt_cache_key should remain constant across per-turn overrides
assert_eq!(
body1["prompt_cache_key"], body2["prompt_cache_key"],
"prompt_cache_key should not change across per-turn overrides"
);
// The entire prefix from the first request should be identical and reused
// as the prefix of the second request.
let expected_user_message_2 = serde_json::json!({
"type": "message",
"id": serde_json::Value::Null,
"role": "user",
"content": [ { "type": "input_text", "text": "hello 2" } ]
});
let expected_body2 = serde_json::json!(
[
body1["input"].as_array().unwrap().as_slice(),
[expected_user_message_2].as_slice(),
]
.concat()
);
assert_eq!(body2["input"], expected_body2);
}
+21
View File
@@ -0,0 +1,21 @@
[package]
edition = "2024"
name = "codex-protocol-ts"
version = { workspace = true }
[lints]
workspace = true
[lib]
name = "codex_protocol_ts"
path = "src/lib.rs"
[[bin]]
name = "codex-protocol-ts"
path = "src/main.rs"
[dependencies]
anyhow = "1"
codex-protocol = { path = "../protocol" }
ts-rs = "11"
clap = { version = "4", features = ["derive"] }
+10
View File
@@ -0,0 +1,10 @@
#!/bin/bash
set -euo pipefail
cd "$(dirname "$0")"/..
tmpdir=$(mktemp -d)
just codex generate-ts --prettier ../node_modules/.bin/prettier --out "$tmpdir"
echo "wrote output to $tmpdir"
+108
View File
@@ -0,0 +1,108 @@
use anyhow::Context;
use anyhow::Result;
use anyhow::anyhow;
use std::ffi::OsStr;
use std::fs;
use std::io::Read;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use ts_rs::TS;
const HEADER: &str = "// GENERATED CODE! DO NOT MODIFY BY HAND!\n\n";
pub fn generate_ts(out_dir: &Path, prettier: Option<&Path>) -> Result<()> {
ensure_dir(out_dir)?;
// Generate TS bindings
codex_protocol::mcp_protocol::ConversationId::export_all_to(out_dir)?;
codex_protocol::mcp_protocol::InputItem::export_all_to(out_dir)?;
codex_protocol::mcp_protocol::ClientRequest::export_all_to(out_dir)?;
codex_protocol::mcp_protocol::ServerRequest::export_all_to(out_dir)?;
codex_protocol::mcp_protocol::NewConversationParams::export_all_to(out_dir)?;
codex_protocol::mcp_protocol::NewConversationResponse::export_all_to(out_dir)?;
codex_protocol::mcp_protocol::AddConversationListenerParams::export_all_to(out_dir)?;
codex_protocol::mcp_protocol::AddConversationSubscriptionResponse::export_all_to(out_dir)?;
codex_protocol::mcp_protocol::RemoveConversationListenerParams::export_all_to(out_dir)?;
codex_protocol::mcp_protocol::RemoveConversationSubscriptionResponse::export_all_to(out_dir)?;
codex_protocol::mcp_protocol::SendUserMessageParams::export_all_to(out_dir)?;
codex_protocol::mcp_protocol::SendUserMessageResponse::export_all_to(out_dir)?;
codex_protocol::mcp_protocol::SendUserTurnParams::export_all_to(out_dir)?;
codex_protocol::mcp_protocol::SendUserTurnResponse::export_all_to(out_dir)?;
codex_protocol::mcp_protocol::InterruptConversationParams::export_all_to(out_dir)?;
codex_protocol::mcp_protocol::InterruptConversationResponse::export_all_to(out_dir)?;
codex_protocol::mcp_protocol::LoginChatGptResponse::export_all_to(out_dir)?;
codex_protocol::mcp_protocol::LoginChatGptCompleteNotification::export_all_to(out_dir)?;
codex_protocol::mcp_protocol::CancelLoginChatGptParams::export_all_to(out_dir)?;
codex_protocol::mcp_protocol::CancelLoginChatGptResponse::export_all_to(out_dir)?;
codex_protocol::mcp_protocol::ApplyPatchApprovalParams::export_all_to(out_dir)?;
codex_protocol::mcp_protocol::ApplyPatchApprovalResponse::export_all_to(out_dir)?;
codex_protocol::mcp_protocol::ExecCommandApprovalParams::export_all_to(out_dir)?;
codex_protocol::mcp_protocol::ExecCommandApprovalResponse::export_all_to(out_dir)?;
// Prepend header to each generated .ts file
let ts_files = ts_files_in(out_dir)?;
for file in &ts_files {
prepend_header_if_missing(file)?;
}
// Format with Prettier by passing individual files (no shell globbing)
if let Some(prettier_bin) = prettier {
if !ts_files.is_empty() {
let status = Command::new(prettier_bin)
.arg("--write")
.args(ts_files.iter().map(|p| p.as_os_str()))
.status()
.with_context(|| {
format!("Failed to invoke Prettier at {}", prettier_bin.display())
})?;
if !status.success() {
return Err(anyhow!("Prettier failed with status {}", status));
}
}
}
Ok(())
}
fn ensure_dir(dir: &Path) -> Result<()> {
fs::create_dir_all(dir)
.with_context(|| format!("Failed to create output directory {}", dir.display()))
}
fn prepend_header_if_missing(path: &Path) -> Result<()> {
let mut content = String::new();
{
let mut f = fs::File::open(path)
.with_context(|| format!("Failed to open {} for reading", path.display()))?;
f.read_to_string(&mut content)
.with_context(|| format!("Failed to read {}", path.display()))?;
}
if content.starts_with(HEADER) {
return Ok(());
}
let mut f = fs::File::create(path)
.with_context(|| format!("Failed to open {} for writing", path.display()))?;
f.write_all(HEADER.as_bytes())
.with_context(|| format!("Failed to write header to {}", path.display()))?;
f.write_all(content.as_bytes())
.with_context(|| format!("Failed to write content to {}", path.display()))?;
Ok(())
}
fn ts_files_in(dir: &Path) -> Result<Vec<PathBuf>> {
let mut files = Vec::new();
for entry in
fs::read_dir(dir).with_context(|| format!("Failed to read dir {}", dir.display()))?
{
let entry = entry?;
let path = entry.path();
if path.is_file() && path.extension() == Some(OsStr::new("ts")) {
files.push(path);
}
}
Ok(files)
}
+20
View File
@@ -0,0 +1,20 @@
use anyhow::Result;
use clap::Parser;
use std::path::PathBuf;
#[derive(Parser, Debug)]
#[command(about = "Generate TypeScript bindings for the Codex protocol")]
struct Args {
/// Output directory where .ts files will be written
#[arg(short = 'o', long = "out", value_name = "DIR")]
out_dir: PathBuf,
/// Optional path to the Prettier executable to format generated files
#[arg(short = 'p', long = "prettier", value_name = "PRETTIER_BIN")]
prettier: Option<PathBuf>,
}
fn main() -> Result<()> {
let args = Args::parse();
codex_protocol_ts::generate_ts(&args.out_dir, args.prettier.as_deref())
}
+1
View File
@@ -17,6 +17,7 @@ serde_bytes = "0.11"
serde_json = "1" serde_json = "1"
strum = "0.27.2" strum = "0.27.2"
strum_macros = "0.27.2" strum_macros = "0.27.2"
ts-rs = { version = "11", features = ["uuid-impl", "serde-json-impl"] }
uuid = { version = "1", features = ["serde", "v4"] } uuid = { version = "1", features = ["serde", "v4"] }
[dev-dependencies] [dev-dependencies]
+4 -3
View File
@@ -1,9 +1,10 @@
use serde::Deserialize; use serde::Deserialize;
use serde::Serialize; use serde::Serialize;
use strum_macros::Display; use strum_macros::Display;
use ts_rs::TS;
/// See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning /// See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning
#[derive(Debug, Serialize, Deserialize, Default, Clone, Copy, PartialEq, Eq, Display)] #[derive(Debug, Serialize, Deserialize, Default, Clone, Copy, PartialEq, Eq, Display, TS)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")] #[strum(serialize_all = "lowercase")]
pub enum ReasoningEffort { pub enum ReasoningEffort {
@@ -20,7 +21,7 @@ pub enum ReasoningEffort {
/// A summary of the reasoning performed by the model. This can be useful for /// A summary of the reasoning performed by the model. This can be useful for
/// debugging and understanding the model's reasoning process. /// debugging and understanding the model's reasoning process.
/// See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#reasoning-summaries /// See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#reasoning-summaries
#[derive(Debug, Serialize, Deserialize, Default, Clone, Copy, PartialEq, Eq, Display)] #[derive(Debug, Serialize, Deserialize, Default, Clone, Copy, PartialEq, Eq, Display, TS)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")] #[strum(serialize_all = "lowercase")]
pub enum ReasoningSummary { pub enum ReasoningSummary {
@@ -32,7 +33,7 @@ pub enum ReasoningSummary {
None, None,
} }
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Default, Serialize, Display)] #[derive(Deserialize, Debug, Clone, Copy, PartialEq, Default, Serialize, Display, TS)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
#[strum(serialize_all = "kebab-case")] #[strum(serialize_all = "kebab-case")]
pub enum SandboxMode { pub enum SandboxMode {
+26 -25
View File
@@ -13,10 +13,11 @@ use crate::protocol::TurnAbortReason;
use mcp_types::RequestId; use mcp_types::RequestId;
use serde::Deserialize; use serde::Deserialize;
use serde::Serialize; use serde::Serialize;
use ts_rs::TS;
use uuid::Uuid; use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
#[serde(transparent)] #[ts(type = "string")]
pub struct ConversationId(pub Uuid); pub struct ConversationId(pub Uuid);
impl Display for ConversationId { impl Display for ConversationId {
@@ -26,7 +27,7 @@ impl Display for ConversationId {
} }
/// Request from the client to the server. /// Request from the client to the server.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
#[serde(tag = "method", rename_all = "camelCase")] #[serde(tag = "method", rename_all = "camelCase")]
pub enum ClientRequest { pub enum ClientRequest {
NewConversation { NewConversation {
@@ -70,7 +71,7 @@ pub enum ClientRequest {
}, },
} }
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct NewConversationParams { pub struct NewConversationParams {
/// Optional override for the model name (e.g. "o3", "o4-mini"). /// Optional override for the model name (e.g. "o3", "o4-mini").
@@ -113,24 +114,24 @@ pub struct NewConversationParams {
pub include_apply_patch_tool: Option<bool>, pub include_apply_patch_tool: Option<bool>,
} }
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct NewConversationResponse { pub struct NewConversationResponse {
pub conversation_id: ConversationId, pub conversation_id: ConversationId,
pub model: String, pub model: String,
} }
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct AddConversationSubscriptionResponse { pub struct AddConversationSubscriptionResponse {
pub subscription_id: Uuid, pub subscription_id: Uuid,
} }
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct RemoveConversationSubscriptionResponse {} pub struct RemoveConversationSubscriptionResponse {}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct LoginChatGptResponse { pub struct LoginChatGptResponse {
pub login_id: Uuid, pub login_id: Uuid,
@@ -141,7 +142,7 @@ pub struct LoginChatGptResponse {
// Event name for notifying client of login completion or failure. // Event name for notifying client of login completion or failure.
pub const LOGIN_CHATGPT_COMPLETE_EVENT: &str = "codex/event/login_chatgpt_complete"; pub const LOGIN_CHATGPT_COMPLETE_EVENT: &str = "codex/event/login_chatgpt_complete";
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct LoginChatGptCompleteNotification { pub struct LoginChatGptCompleteNotification {
pub login_id: Uuid, pub login_id: Uuid,
@@ -150,24 +151,24 @@ pub struct LoginChatGptCompleteNotification {
pub error: Option<String>, pub error: Option<String>,
} }
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct CancelLoginChatGptParams { pub struct CancelLoginChatGptParams {
pub login_id: Uuid, pub login_id: Uuid,
} }
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct CancelLoginChatGptResponse {} pub struct CancelLoginChatGptResponse {}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct SendUserMessageParams { pub struct SendUserMessageParams {
pub conversation_id: ConversationId, pub conversation_id: ConversationId,
pub items: Vec<InputItem>, pub items: Vec<InputItem>,
} }
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct SendUserTurnParams { pub struct SendUserTurnParams {
pub conversation_id: ConversationId, pub conversation_id: ConversationId,
@@ -180,39 +181,39 @@ pub struct SendUserTurnParams {
pub summary: ReasoningSummary, pub summary: ReasoningSummary,
} }
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct SendUserTurnResponse {} pub struct SendUserTurnResponse {}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct InterruptConversationParams { pub struct InterruptConversationParams {
pub conversation_id: ConversationId, pub conversation_id: ConversationId,
} }
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct InterruptConversationResponse { pub struct InterruptConversationResponse {
pub abort_reason: TurnAbortReason, pub abort_reason: TurnAbortReason,
} }
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct SendUserMessageResponse {} pub struct SendUserMessageResponse {}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct AddConversationListenerParams { pub struct AddConversationListenerParams {
pub conversation_id: ConversationId, pub conversation_id: ConversationId,
} }
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct RemoveConversationListenerParams { pub struct RemoveConversationListenerParams {
pub subscription_id: Uuid, pub subscription_id: Uuid,
} }
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[serde(tag = "type", content = "data")] #[serde(tag = "type", content = "data")]
pub enum InputItem { pub enum InputItem {
@@ -237,7 +238,7 @@ pub const APPLY_PATCH_APPROVAL_METHOD: &str = "applyPatchApproval";
pub const EXEC_COMMAND_APPROVAL_METHOD: &str = "execCommandApproval"; pub const EXEC_COMMAND_APPROVAL_METHOD: &str = "execCommandApproval";
/// Request initiated from the server and sent to the client. /// Request initiated from the server and sent to the client.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
#[serde(tag = "method", rename_all = "camelCase")] #[serde(tag = "method", rename_all = "camelCase")]
pub enum ServerRequest { pub enum ServerRequest {
/// Request to approve a patch. /// Request to approve a patch.
@@ -254,7 +255,7 @@ pub enum ServerRequest {
}, },
} }
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
pub struct ApplyPatchApprovalParams { pub struct ApplyPatchApprovalParams {
pub conversation_id: ConversationId, pub conversation_id: ConversationId,
/// Use to correlate this with [codex_core::protocol::PatchApplyBeginEvent] /// Use to correlate this with [codex_core::protocol::PatchApplyBeginEvent]
@@ -270,7 +271,7 @@ pub struct ApplyPatchApprovalParams {
pub grant_root: Option<PathBuf>, pub grant_root: Option<PathBuf>,
} }
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
pub struct ExecCommandApprovalParams { pub struct ExecCommandApprovalParams {
pub conversation_id: ConversationId, pub conversation_id: ConversationId,
/// Use to correlate this with [codex_core::protocol::ExecCommandBeginEvent] /// Use to correlate this with [codex_core::protocol::ExecCommandBeginEvent]
@@ -282,12 +283,12 @@ pub struct ExecCommandApprovalParams {
pub reason: Option<String>, pub reason: Option<String>,
} }
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
pub struct ExecCommandApprovalResponse { pub struct ExecCommandApprovalResponse {
pub decision: ReviewDecision, pub decision: ReviewDecision,
} }
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]
pub struct ApplyPatchApprovalResponse { pub struct ApplyPatchApprovalResponse {
pub decision: ReviewDecision, pub decision: ReviewDecision,
} }
+38 -5
View File
@@ -15,6 +15,7 @@ use serde::Deserialize;
use serde::Serialize; use serde::Serialize;
use serde_bytes::ByteBuf; use serde_bytes::ByteBuf;
use strum_macros::Display; use strum_macros::Display;
use ts_rs::TS;
use uuid::Uuid; use uuid::Uuid;
use crate::config_types::ReasoningEffort as ReasoningEffortConfig; use crate::config_types::ReasoningEffort as ReasoningEffortConfig;
@@ -75,6 +76,38 @@ pub enum Op {
summary: ReasoningSummaryConfig, summary: ReasoningSummaryConfig,
}, },
/// Override parts of the persistent turn context for subsequent turns.
///
/// All fields are optional; when omitted, the existing value is preserved.
/// This does not enqueue any input – it only updates defaults used for
/// future `UserInput` turns.
OverrideTurnContext {
/// Updated `cwd` for sandbox/tool calls.
#[serde(skip_serializing_if = "Option::is_none")]
cwd: Option<PathBuf>,
/// Updated command approval policy.
#[serde(skip_serializing_if = "Option::is_none")]
approval_policy: Option<AskForApproval>,
/// Updated sandbox policy for tool calls.
#[serde(skip_serializing_if = "Option::is_none")]
sandbox_policy: Option<SandboxPolicy>,
/// Updated model slug. When set, the model family is derived
/// automatically.
#[serde(skip_serializing_if = "Option::is_none")]
model: Option<String>,
/// Updated reasoning effort (honored only for reasoning-capable models).
#[serde(skip_serializing_if = "Option::is_none")]
effort: Option<ReasoningEffortConfig>,
/// Updated reasoning summary preference (honored only for reasoning-capable models).
#[serde(skip_serializing_if = "Option::is_none")]
summary: Option<ReasoningSummaryConfig>,
},
/// Approve a command execution /// Approve a command execution
ExecApproval { ExecApproval {
/// The id of the submission we are approving /// The id of the submission we are approving
@@ -113,7 +146,7 @@ pub enum Op {
/// Determines the conditions under which the user is consulted to approve /// Determines the conditions under which the user is consulted to approve
/// running the command proposed by Codex. /// running the command proposed by Codex.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize, Display)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize, Display, TS)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
#[strum(serialize_all = "kebab-case")] #[strum(serialize_all = "kebab-case")]
pub enum AskForApproval { pub enum AskForApproval {
@@ -140,7 +173,7 @@ pub enum AskForApproval {
} }
/// Determines execution restrictions for model shell commands. /// Determines execution restrictions for model shell commands.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Display)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Display, TS)]
#[strum(serialize_all = "kebab-case")] #[strum(serialize_all = "kebab-case")]
#[serde(tag = "mode", rename_all = "kebab-case")] #[serde(tag = "mode", rename_all = "kebab-case")]
pub enum SandboxPolicy { pub enum SandboxPolicy {
@@ -706,7 +739,7 @@ pub struct SessionConfiguredEvent {
} }
/// User's decision in response to an ExecApprovalRequest. /// User's decision in response to an ExecApprovalRequest.
#[derive(Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)] #[derive(Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, TS)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum ReviewDecision { pub enum ReviewDecision {
/// User has approved this command and the agent should execute it. /// User has approved this command and the agent should execute it.
@@ -727,7 +760,7 @@ pub enum ReviewDecision {
Abort, Abort,
} }
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, TS)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum FileChange { pub enum FileChange {
Add { Add {
@@ -753,7 +786,7 @@ pub struct TurnAbortedEvent {
pub reason: TurnAbortReason, pub reason: TurnAbortReason,
} }
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, TS)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum TurnAbortReason { pub enum TurnAbortReason {
Interrupted, Interrupted,