diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index a0dd9133..4eefc375 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -695,6 +695,7 @@ dependencies = [ "codex-apply-patch", "codex-login", "codex-mcp-client", + "codex-protocol", "core_test_support", "dirs", "env-flags", @@ -889,6 +890,19 @@ dependencies = [ "wiremock", ] +[[package]] +name = "codex-protocol" +version = "0.0.0" +dependencies = [ + "mcp-types", + "serde", + "serde_bytes", + "serde_json", + "strum 0.27.2", + "strum_macros 0.27.2", + "uuid", +] + [[package]] name = "codex-tui" version = "0.0.0" @@ -904,6 +918,7 @@ dependencies = [ "codex-file-search", "codex-login", "codex-ollama", + "codex-protocol", "color-eyre", "crossterm", "diffy", diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 0ed88522..2fb9b927 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -15,6 +15,7 @@ members = [ "mcp-server", "mcp-types", "ollama", + "protocol", "tui", ] resolver = "2" diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index 3fb99d1f..74eaf670 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -19,6 +19,7 @@ chrono = { version = "0.4", features = ["serde"] } codex-apply-patch = { path = "../apply-patch" } codex-login = { path = "../login" } codex-mcp-client = { path = "../mcp-client" } +codex-protocol = { path = "../protocol" } dirs = "6" env-flags = "0.1.1" eventsource-stream = "0.2.3" diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index c683f187..020acd04 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -677,7 +677,10 @@ impl Session { call_id, command: command_for_display.clone(), cwd, - parsed_cmd: parse_command(&command_for_display), + parsed_cmd: parse_command(&command_for_display) + .into_iter() + .map(Into::into) + .collect(), }), }; let event = Event { @@ -1031,8 +1034,8 @@ async fn submission_loop( Arc::new(per_turn_config), None, provider, - effort, - summary, + effort.into(), + summary.into(), sess.session_id, ); @@ -1102,7 +1105,13 @@ async fn submission_loop( crate::protocol::GetHistoryEntryResponseEvent { offset, log_id, - entry: entry_opt, + entry: entry_opt.map(|e| { + codex_protocol::message_history::HistoryEntry { + session_id: e.session_id, + ts: e.ts, + text: e.text, + } + }), }, ), }; @@ -1160,6 +1169,9 @@ async fn submission_loop( } break; } + _ => { + // Ignore unknown ops; enum is non_exhaustive to allow extensions. + } } } debug!("Agent loop exited"); diff --git a/codex-rs/core/src/config_types.rs b/codex-rs/core/src/config_types.rs index cbbc6b49..bb61f820 100644 --- a/codex-rs/core/src/config_types.rs +++ b/codex-rs/core/src/config_types.rs @@ -228,3 +228,27 @@ pub enum ReasoningSummary { /// Option to disable reasoning summaries. None, } + +// Conversions from protocol enums to core config enums used where protocol +// values are supplied by clients and core needs its internal representations. +impl From for ReasoningEffort { + fn from(v: codex_protocol::config_types::ReasoningEffort) -> Self { + match v { + codex_protocol::config_types::ReasoningEffort::Low => ReasoningEffort::Low, + codex_protocol::config_types::ReasoningEffort::Medium => ReasoningEffort::Medium, + codex_protocol::config_types::ReasoningEffort::High => ReasoningEffort::High, + codex_protocol::config_types::ReasoningEffort::None => ReasoningEffort::None, + } + } +} + +impl From for ReasoningSummary { + fn from(v: codex_protocol::config_types::ReasoningSummary) -> Self { + match v { + codex_protocol::config_types::ReasoningSummary::Auto => ReasoningSummary::Auto, + codex_protocol::config_types::ReasoningSummary::Concise => ReasoningSummary::Concise, + codex_protocol::config_types::ReasoningSummary::Detailed => ReasoningSummary::Detailed, + codex_protocol::config_types::ReasoningSummary::None => ReasoningSummary::None, + } + } +} diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index e895377b..28d35f53 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -44,7 +44,6 @@ mod openai_model_info; mod openai_tools; pub mod plan_tool; mod project_doc; -pub mod protocol; mod rollout; pub(crate) mod safety; pub mod seatbelt; @@ -56,3 +55,9 @@ mod user_notification; pub mod util; pub use apply_patch::CODEX_APPLY_PATCH_ARG1; pub use safety::get_platform_sandbox; +// Re-export the protocol types from the standalone `codex-protocol` crate so existing +// `codex_core::protocol::...` references continue to work across the workspace. +pub use codex_protocol::protocol; +// Re-export protocol config enums to ensure call sites can use the same types +// as those in the protocol crate when constructing protocol messages. +pub use codex_protocol::config_types as protocol_config_types; diff --git a/codex-rs/core/src/models.rs b/codex-rs/core/src/models.rs index 5e67fc38..f6323e27 100644 --- a/codex-rs/core/src/models.rs +++ b/codex-rs/core/src/models.rs @@ -183,6 +183,7 @@ impl From> for ResponseInputItem { None } }, + _ => None, }) .collect::>(), } diff --git a/codex-rs/core/src/parse_command.rs b/codex-rs/core/src/parse_command.rs index 6436ce40..6ea06268 100644 --- a/codex-rs/core/src/parse_command.rs +++ b/codex-rs/core/src/parse_command.rs @@ -41,6 +41,24 @@ pub enum ParsedCommand { }, } +// Convert core's parsed command enum into the protocol's simplified type so +// events can carry the canonical representation across process boundaries. +impl From for codex_protocol::parse_command::ParsedCommand { + fn from(v: ParsedCommand) -> Self { + use codex_protocol::parse_command::ParsedCommand as P; + match v { + ParsedCommand::Read { cmd, name } => P::Read { cmd, name }, + ParsedCommand::ListFiles { cmd, path } => P::ListFiles { cmd, path }, + ParsedCommand::Search { cmd, query, path } => P::Search { cmd, query, path }, + ParsedCommand::Format { cmd, tool, targets } => P::Format { cmd, tool, targets }, + ParsedCommand::Test { cmd } => P::Test { cmd }, + ParsedCommand::Lint { cmd, tool, targets } => P::Lint { cmd, tool, targets }, + ParsedCommand::Noop { cmd } => P::Noop { cmd }, + ParsedCommand::Unknown { cmd } => P::Unknown { cmd }, + } + } +} + fn shlex_join(tokens: &[String]) -> String { shlex_try_join(tokens.iter().map(|s| s.as_str())) .unwrap_or_else(|_| "".to_string()) diff --git a/codex-rs/core/src/plan_tool.rs b/codex-rs/core/src/plan_tool.rs index 9e81abcd..bc39e4f6 100644 --- a/codex-rs/core/src/plan_tool.rs +++ b/codex-rs/core/src/plan_tool.rs @@ -1,9 +1,6 @@ use std::collections::BTreeMap; use std::sync::LazyLock; -use serde::Deserialize; -use serde::Serialize; - use crate::codex::Session; use crate::models::FunctionCallOutputPayload; use crate::models::ResponseInputItem; @@ -13,29 +10,13 @@ use crate::openai_tools::ResponsesApiTool; use crate::protocol::Event; use crate::protocol::EventMsg; +// Use the canonical plan tool types from the protocol crate to ensure +// type-identity matches events transported via `codex_protocol`. +pub use codex_protocol::plan_tool::PlanItemArg; +pub use codex_protocol::plan_tool::StepStatus; +pub use codex_protocol::plan_tool::UpdatePlanArgs; + // Types for the TODO tool arguments matching codex-vscode/todo-mcp/src/main.rs -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum StepStatus { - Pending, - InProgress, - Completed, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct PlanItemArg { - pub step: String, - pub status: StepStatus, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct UpdatePlanArgs { - #[serde(default)] - pub explanation: Option, - pub plan: Vec, -} pub(crate) static PLAN_TOOL: LazyLock = LazyLock::new(|| { let mut plan_item_props = BTreeMap::new(); diff --git a/codex-rs/mcp-server/src/wire_format.rs b/codex-rs/mcp-server/src/wire_format.rs index 68d9aeb9..2dca1b79 100644 --- a/codex-rs/mcp-server/src/wire_format.rs +++ b/codex-rs/mcp-server/src/wire_format.rs @@ -2,12 +2,12 @@ use std::collections::HashMap; use std::fmt::Display; use std::path::PathBuf; -use codex_core::config_types::ReasoningEffort; -use codex_core::config_types::ReasoningSummary; use codex_core::protocol::AskForApproval; use codex_core::protocol::FileChange; use codex_core::protocol::ReviewDecision; use codex_core::protocol::SandboxPolicy; +use codex_core::protocol_config_types::ReasoningEffort; +use codex_core::protocol_config_types::ReasoningSummary; use mcp_types::RequestId; use serde::Deserialize; use serde::Serialize; diff --git a/codex-rs/mcp-server/tests/codex_message_processor_flow.rs b/codex-rs/mcp-server/tests/codex_message_processor_flow.rs index e0c7a832..5b89f3fe 100644 --- a/codex-rs/mcp-server/tests/codex_message_processor_flow.rs +++ b/codex-rs/mcp-server/tests/codex_message_processor_flow.rs @@ -1,9 +1,9 @@ use std::path::Path; -use codex_core::config_types::ReasoningEffort; -use codex_core::config_types::ReasoningSummary; use codex_core::protocol::AskForApproval; use codex_core::protocol::SandboxPolicy; +use codex_core::protocol_config_types::ReasoningEffort; +use codex_core::protocol_config_types::ReasoningSummary; use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR; use codex_mcp_server::wire_format::AddConversationListenerParams; use codex_mcp_server::wire_format::AddConversationSubscriptionResponse; diff --git a/codex-rs/protocol/Cargo.toml b/codex-rs/protocol/Cargo.toml new file mode 100644 index 00000000..43c5eac8 --- /dev/null +++ b/codex-rs/protocol/Cargo.toml @@ -0,0 +1,20 @@ +[package] +edition = "2024" +name = "codex-protocol" +version = { workspace = true } + +[lib] +name = "codex_protocol" +path = "src/lib.rs" + +[lints] +workspace = true + +[dependencies] +mcp-types = { path = "../mcp-types" } +serde = { version = "1", features = ["derive"] } +serde_bytes = "0.11" +serde_json = "1" +strum = "0.27.2" +strum_macros = "0.27.2" +uuid = { version = "1", features = ["serde", "v4"] } diff --git a/codex-rs/protocol/README.md b/codex-rs/protocol/README.md new file mode 100644 index 00000000..384d0b48 --- /dev/null +++ b/codex-rs/protocol/README.md @@ -0,0 +1,7 @@ +# codex-protocol + +This crate defines the "types" for the protocol used by Codex CLI, which includes both "internal types" for communication between `codex-core` and `codex-tui`, as well as "external types" used with `codex mcp`. + +This crate should have minimal dependencies. + +Ideally, we should avoid "material business logic" in this crate, as we can always introduce `Ext`-style traits to add functionality to types in other crates. diff --git a/codex-rs/protocol/src/config_types.rs b/codex-rs/protocol/src/config_types.rs new file mode 100644 index 00000000..b4525d79 --- /dev/null +++ b/codex-rs/protocol/src/config_types.rs @@ -0,0 +1,31 @@ +use serde::Deserialize; +use serde::Serialize; +use strum_macros::Display; + +/// 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)] +#[serde(rename_all = "lowercase")] +#[strum(serialize_all = "lowercase")] +pub enum ReasoningEffort { + Low, + #[default] + Medium, + High, + /// Option to disable reasoning. + 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, Deserialize, Default, Clone, Copy, PartialEq, Eq, Display)] +#[serde(rename_all = "lowercase")] +#[strum(serialize_all = "lowercase")] +pub enum ReasoningSummary { + #[default] + Auto, + Concise, + Detailed, + /// Option to disable reasoning summaries. + None, +} diff --git a/codex-rs/protocol/src/lib.rs b/codex-rs/protocol/src/lib.rs new file mode 100644 index 00000000..ec6a4195 --- /dev/null +++ b/codex-rs/protocol/src/lib.rs @@ -0,0 +1,5 @@ +pub mod config_types; +pub mod message_history; +pub mod parse_command; +pub mod plan_tool; +pub mod protocol; diff --git a/codex-rs/protocol/src/message_history.rs b/codex-rs/protocol/src/message_history.rs new file mode 100644 index 00000000..3a561df7 --- /dev/null +++ b/codex-rs/protocol/src/message_history.rs @@ -0,0 +1,9 @@ +use serde::Deserialize; +use serde::Serialize; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct HistoryEntry { + pub session_id: String, + pub ts: u64, + pub text: String, +} diff --git a/codex-rs/protocol/src/parse_command.rs b/codex-rs/protocol/src/parse_command.rs new file mode 100644 index 00000000..495562a7 --- /dev/null +++ b/codex-rs/protocol/src/parse_command.rs @@ -0,0 +1,38 @@ +use serde::Deserialize; +use serde::Serialize; + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +pub enum ParsedCommand { + Read { + cmd: String, + name: String, + }, + ListFiles { + cmd: String, + path: Option, + }, + Search { + cmd: String, + query: Option, + path: Option, + }, + Format { + cmd: String, + tool: Option, + targets: Option>, + }, + Test { + cmd: String, + }, + Lint { + cmd: String, + tool: Option, + targets: Option>, + }, + Noop { + cmd: String, + }, + Unknown { + cmd: String, + }, +} diff --git a/codex-rs/protocol/src/plan_tool.rs b/codex-rs/protocol/src/plan_tool.rs new file mode 100644 index 00000000..78ef9cd4 --- /dev/null +++ b/codex-rs/protocol/src/plan_tool.rs @@ -0,0 +1,26 @@ +use serde::Deserialize; +use serde::Serialize; + +// Types for the TODO tool arguments matching codex-vscode/todo-mcp/src/main.rs +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum StepStatus { + Pending, + InProgress, + Completed, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct PlanItemArg { + pub step: String, + pub status: StepStatus, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct UpdatePlanArgs { + #[serde(default)] + pub explanation: Option, + pub plan: Vec, +} diff --git a/codex-rs/core/src/protocol.rs b/codex-rs/protocol/src/protocol.rs similarity index 99% rename from codex-rs/core/src/protocol.rs rename to codex-rs/protocol/src/protocol.rs index d334c2eb..c4f50b4f 100644 --- a/codex-rs/core/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -193,7 +193,7 @@ pub struct WritableRoot { } impl WritableRoot { - pub(crate) fn is_path_writable(&self, path: &Path) -> bool { + pub fn is_path_writable(&self, path: &Path) -> bool { // Check if the path is under the root. if !path.starts_with(&self.root) { return false; diff --git a/codex-rs/tui/Cargo.toml b/codex-rs/tui/Cargo.toml index 182667a0..75aa6f8e 100644 --- a/codex-rs/tui/Cargo.toml +++ b/codex-rs/tui/Cargo.toml @@ -33,6 +33,7 @@ codex-common = { path = "../common", features = [ "sandbox_summary", ] } codex-core = { path = "../core" } +codex-protocol = { path = "../protocol" } codex-file-search = { path = "../file-search" } codex-login = { path = "../login" } codex-ollama = { path = "../ollama" } diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index c39edb82..637d50bd 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -3,7 +3,6 @@ use std::path::PathBuf; use std::sync::Arc; use codex_core::config::Config; -use codex_core::parse_command::ParsedCommand; use codex_core::protocol::AgentMessageDeltaEvent; use codex_core::protocol::AgentMessageEvent; use codex_core::protocol::AgentReasoningDeltaEvent; @@ -26,6 +25,7 @@ use codex_core::protocol::PatchApplyBeginEvent; use codex_core::protocol::TaskCompleteEvent; use codex_core::protocol::TokenUsage; use codex_core::protocol::TurnDiffEvent; +use codex_protocol::parse_command::ParsedCommand; use crossterm::event::KeyEvent; use crossterm::event::KeyEventKind; use ratatui::buffer::Buffer; diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index bf03a8ed..eb7142ac 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -204,9 +204,12 @@ fn exec_history_cell_shows_working_then_completed() { call_id: "call-1".into(), command: vec!["bash".into(), "-lc".into(), "echo done".into()], cwd: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")), - parsed_cmd: vec![codex_core::parse_command::ParsedCommand::Unknown { - cmd: "echo done".into(), - }], + parsed_cmd: vec![ + codex_core::parse_command::ParsedCommand::Unknown { + cmd: "echo done".into(), + } + .into(), + ], }), }); @@ -246,9 +249,12 @@ fn exec_history_cell_shows_working_then_failed() { call_id: "call-2".into(), command: vec!["bash".into(), "-lc".into(), "false".into()], cwd: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")), - parsed_cmd: vec![codex_core::parse_command::ParsedCommand::Unknown { - cmd: "false".into(), - }], + parsed_cmd: vec![ + codex_core::parse_command::ParsedCommand::Unknown { + cmd: "false".into(), + } + .into(), + ], }), }); diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 36d412ce..37a9d66f 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -9,7 +9,6 @@ use codex_ansi_escape::ansi_escape_line; use codex_common::create_config_summary_entries; use codex_common::elapsed::format_duration; use codex_core::config::Config; -use codex_core::parse_command::ParsedCommand; use codex_core::plan_tool::PlanItemArg; use codex_core::plan_tool::StepStatus; use codex_core::plan_tool::UpdatePlanArgs; @@ -20,6 +19,7 @@ use codex_core::protocol::SessionConfiguredEvent; use codex_core::protocol::TokenUsage; use codex_login::get_auth_file; use codex_login::try_read_auth_json; +use codex_protocol::parse_command::ParsedCommand; use image::DynamicImage; use image::ImageReader; use mcp_types::EmbeddedResourceResource;