diff --git a/codex-rs/core/src/thread_manager.rs b/codex-rs/core/src/thread_manager.rs index 9b633ded378..09c464cd10c 100644 --- a/codex-rs/core/src/thread_manager.rs +++ b/codex-rs/core/src/thread_manager.rs @@ -226,6 +226,15 @@ impl ThreadManager { .await } + pub async fn resume_thread_from_rollout_with_auth( + &self, + config: Config, + rollout_path: PathBuf, + ) -> CodexResult { + self.resume_thread_from_rollout(config, rollout_path, Arc::clone(&self.state.auth_manager)) + .await + } + pub async fn resume_thread_with_history( &self, config: Config, diff --git a/codex-rs/mcp-server/src/codex_tool_config.rs b/codex-rs/mcp-server/src/codex_tool_config.rs index 8131d7da52f..e9ab0966081 100644 --- a/codex-rs/mcp-server/src/codex_tool_config.rs +++ b/codex-rs/mcp-server/src/codex_tool_config.rs @@ -61,6 +61,10 @@ pub struct CodexToolCallParam { /// Prompt used when compacting the conversation. #[serde(default, skip_serializing_if = "Option::is_none")] pub compact_prompt: Option, + + /// Optional path to a rollout file to resume the session from. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub resume_path: Option, } /// Custom enum mirroring [`AskForApproval`], but has an extra dependency on @@ -153,7 +157,7 @@ impl CodexToolCallParam { pub async fn into_config( self, codex_linux_sandbox_exe: Option, - ) -> std::io::Result<(String, Config)> { + ) -> std::io::Result<(String, Config, Option)> { let Self { prompt, model, @@ -165,6 +169,7 @@ impl CodexToolCallParam { base_instructions, developer_instructions, compact_prompt, + resume_path, } = self; // Build the `ConfigOverrides` recognized by codex-core. @@ -190,7 +195,7 @@ impl CodexToolCallParam { let cfg = Config::load_with_cli_overrides_and_harness_overrides(cli_overrides, overrides).await?; - Ok((prompt, cfg)) + Ok((prompt, cfg, resume_path)) } } @@ -325,6 +330,10 @@ mod tests { "description": "The *initial user prompt* to start the Codex conversation.", "type": "string" }, + "resume-path": { + "description": "Optional path to a rollout file to resume the session from.", + "type": "string" + }, "sandbox": { "description": "Sandbox mode: `read-only`, `workspace-write`, or `danger-full-access`.", "enum": [ @@ -407,4 +416,27 @@ mod tests { }); assert_eq!(expected_tool_json, tool_json); } + + #[test] + fn codex_tool_accepts_pre_change_payload() { + let payload = serde_json::json!({ + "prompt": "hello", + "model": "gpt-5.2", + "sandbox": "workspace-write", + "approval-policy": "untrusted", + }); + + let parsed: CodexToolCallParam = serde_json::from_value(payload).expect("payload parses"); + assert_eq!(parsed.prompt, "hello"); + assert_eq!(parsed.model.as_deref(), Some("gpt-5.2")); + assert_eq!( + parsed.sandbox, + Some(CodexToolCallSandboxMode::WorkspaceWrite) + ); + assert_eq!( + parsed.approval_policy, + Some(CodexToolCallApprovalPolicy::Untrusted) + ); + assert!(parsed.resume_path.is_none()); + } } diff --git a/codex-rs/mcp-server/src/codex_tool_runner.rs b/codex-rs/mcp-server/src/codex_tool_runner.rs index dcaf8a89e6c..b409e0136b7 100644 --- a/codex-rs/mcp-server/src/codex_tool_runner.rs +++ b/codex-rs/mcp-server/src/codex_tool_runner.rs @@ -3,6 +3,7 @@ //! and to make future feature-growth easier to manage. use std::collections::HashMap; +use std::path::PathBuf; use std::sync::Arc; use crate::exec_approval::handle_exec_approval_request; @@ -66,6 +67,7 @@ pub async fn run_codex_tool_session( id: RequestId, initial_prompt: String, config: CodexConfig, + resume_path: Option, outgoing: Arc, thread_manager: Arc, running_requests_id_to_codex_uuid: Arc>>, @@ -74,21 +76,42 @@ pub async fn run_codex_tool_session( thread_id, thread, session_configured, - } = match thread_manager.start_thread(config).await { - Ok(res) => res, - Err(e) => { - let result = CallToolResult { - content: vec![ContentBlock::TextContent(TextContent { - r#type: "text".to_string(), - text: format!("Failed to start Codex session: {e}"), - annotations: None, - })], - is_error: Some(true), - structured_content: None, - }; - outgoing.send_response(id.clone(), result).await; - return; - } + } = match resume_path { + Some(path) => match thread_manager + .resume_thread_from_rollout_with_auth(config, path) + .await + { + Ok(res) => res, + Err(e) => { + let result = CallToolResult { + content: vec![ContentBlock::TextContent(TextContent { + r#type: "text".to_string(), + text: format!("Failed to resume Codex session: {e}"), + annotations: None, + })], + is_error: Some(true), + structured_content: None, + }; + outgoing.send_response(id.clone(), result).await; + return; + } + }, + None => match thread_manager.start_thread(config).await { + Ok(res) => res, + Err(e) => { + let result = CallToolResult { + content: vec![ContentBlock::TextContent(TextContent { + r#type: "text".to_string(), + text: format!("Failed to start Codex session: {e}"), + annotations: None, + })], + is_error: Some(true), + structured_content: None, + }; + outgoing.send_response(id.clone(), result).await; + return; + } + }, }; let session_configured_event = Event { diff --git a/codex-rs/mcp-server/src/message_processor.rs b/codex-rs/mcp-server/src/message_processor.rs index 9d947cda3f4..15e08fa1235 100644 --- a/codex-rs/mcp-server/src/message_processor.rs +++ b/codex-rs/mcp-server/src/message_processor.rs @@ -347,20 +347,36 @@ impl MessageProcessor { } } async fn handle_tool_call_codex(&self, id: RequestId, arguments: Option) { - let (initial_prompt, config): (String, Config) = match arguments { - Some(json_val) => match serde_json::from_value::(json_val) { - Ok(tool_cfg) => match tool_cfg - .into_config(self.codex_linux_sandbox_exe.clone()) - .await - { - Ok(cfg) => cfg, + let (initial_prompt, config, resume_path): (String, Config, Option) = + match arguments { + Some(json_val) => match serde_json::from_value::(json_val) { + Ok(tool_cfg) => match tool_cfg + .into_config(self.codex_linux_sandbox_exe.clone()) + .await + { + Ok(cfg) => cfg, + Err(e) => { + let result = CallToolResult { + content: vec![ContentBlock::TextContent(TextContent { + r#type: "text".to_owned(), + text: format!( + "Failed to load Codex configuration from overrides: {e}" + ), + annotations: None, + })], + is_error: Some(true), + structured_content: None, + }; + self.send_response::(id, result) + .await; + return; + } + }, Err(e) => { let result = CallToolResult { content: vec![ContentBlock::TextContent(TextContent { r#type: "text".to_owned(), - text: format!( - "Failed to load Codex configuration from overrides: {e}" - ), + text: format!("Failed to parse configuration for Codex tool: {e}"), annotations: None, })], is_error: Some(true), @@ -371,13 +387,15 @@ impl MessageProcessor { return; } }, - Err(e) => { + None => { let result = CallToolResult { content: vec![ContentBlock::TextContent(TextContent { - r#type: "text".to_owned(), - text: format!("Failed to parse configuration for Codex tool: {e}"), - annotations: None, - })], + r#type: "text".to_string(), + text: + "Missing arguments for codex tool-call; the `prompt` field is required." + .to_string(), + annotations: None, + })], is_error: Some(true), structured_content: None, }; @@ -385,24 +403,7 @@ impl MessageProcessor { .await; return; } - }, - None => { - let result = CallToolResult { - content: vec![ContentBlock::TextContent(TextContent { - r#type: "text".to_string(), - text: - "Missing arguments for codex tool-call; the `prompt` field is required." - .to_string(), - annotations: None, - })], - is_error: Some(true), - structured_content: None, - }; - self.send_response::(id, result) - .await; - return; - } - }; + }; // Clone outgoing and server to move into async task. let outgoing = self.outgoing.clone(); @@ -417,6 +418,7 @@ impl MessageProcessor { id, initial_prompt, config, + resume_path, outgoing, thread_manager, running_requests_id_to_codex_uuid,