diff --git a/codex-rs/core/src/agent/control.rs b/codex-rs/core/src/agent/control.rs index 7287d25283..25de042117 100644 --- a/codex-rs/core/src/agent/control.rs +++ b/codex-rs/core/src/agent/control.rs @@ -4,6 +4,7 @@ use crate::agent::status::is_final; use crate::error::CodexErr; use crate::error::Result as CodexResult; use crate::find_thread_path_by_id_str; +use crate::rollout::RolloutRecorder; use crate::session_prefix::format_subagent_notification_message; use crate::state_db; use crate::thread_manager::ThreadManagerState; @@ -19,6 +20,11 @@ use tokio::sync::watch; const AGENT_NAMES: &str = include_str!("agent_names.txt"); +#[derive(Clone, Copy, Debug, Default)] +pub(crate) struct SpawnAgentOptions { + pub(crate) fork_parent_thread: bool, +} + fn agent_nickname_list() -> Vec<&'static str> { AGENT_NAMES .lines() @@ -57,6 +63,17 @@ impl AgentControl { config: crate::config::Config, items: Vec, session_source: Option, + ) -> CodexResult { + self.spawn_agent_with_options(config, items, session_source, SpawnAgentOptions::default()) + .await + } + + pub(crate) async fn spawn_agent_with_options( + &self, + config: crate::config::Config, + items: Vec, + session_source: Option, + options: SpawnAgentOptions, ) -> CodexResult { let state = self.upgrade()?; let mut reservation = self.state.reserve_spawn_slot(config.agent_max_threads)?; @@ -82,9 +99,47 @@ impl AgentControl { // The same `AgentControl` is sent to spawn the thread. let new_thread = match session_source { Some(session_source) => { - state - .spawn_new_thread_with_source(config, self.clone(), session_source, false) - .await? + if options.fork_parent_thread { + let SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + parent_thread_id, + .. + }) = session_source.clone() + else { + return Err(CodexErr::Fatal( + "spawn_agent fork requires a thread-spawn session source".to_string(), + )); + }; + let rollout_path = match state.get_thread(parent_thread_id).await { + Ok(parent_thread) => parent_thread.rollout_path(), + Err(_) => None, + } + .or( + find_thread_path_by_id_str( + config.codex_home.as_path(), + &parent_thread_id.to_string(), + ) + .await?, + ) + .ok_or_else(|| { + CodexErr::Fatal(format!( + "parent thread rollout unavailable for fork: {parent_thread_id}" + )) + })?; + let initial_history = RolloutRecorder::get_rollout_history(&rollout_path).await?; + state + .fork_thread_with_source( + config, + initial_history, + self.clone(), + session_source, + false, + ) + .await? + } else { + state + .spawn_new_thread_with_source(config, self.clone(), session_source, false) + .await? + } } None => state.spawn_new_thread(config, self.clone()).await?, }; diff --git a/codex-rs/core/src/thread_manager.rs b/codex-rs/core/src/thread_manager.rs index cb7c464362..42eabe3873 100644 --- a/codex-rs/core/src/thread_manager.rs +++ b/codex-rs/core/src/thread_manager.rs @@ -464,6 +464,26 @@ impl ThreadManagerState { .await } + pub(crate) async fn fork_thread_with_source( + &self, + config: Config, + initial_history: InitialHistory, + agent_control: AgentControl, + session_source: SessionSource, + persist_extended_history: bool, + ) -> CodexResult { + self.spawn_thread_with_source( + config, + initial_history, + Arc::clone(&self.auth_manager), + agent_control, + session_source, + Vec::new(), + persist_extended_history, + ) + .await + } + /// Spawn a new thread with optional history and register it with the manager. pub(crate) async fn spawn_thread( &self, diff --git a/codex-rs/core/src/tools/handlers/multi_agents.rs b/codex-rs/core/src/tools/handlers/multi_agents.rs index 455a209526..7c02e210dd 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents.rs @@ -94,6 +94,7 @@ impl ToolHandler for MultiAgentHandler { mod spawn { use super::*; use crate::agent::role::apply_role_to_config; + use crate::agent::control::SpawnAgentOptions; use crate::agent::exceeds_thread_spawn_depth_limit; use crate::agent::next_thread_spawn_depth; @@ -104,6 +105,8 @@ mod spawn { message: Option, items: Option>, agent_type: Option, + #[serde(default)] + fork_current_thread: bool, } #[derive(Debug, Serialize)] @@ -156,7 +159,7 @@ mod spawn { let result = session .services .agent_control - .spawn_agent( + .spawn_agent_with_options( config, input_items, Some(thread_spawn_source( @@ -164,6 +167,9 @@ mod spawn { child_depth, role_name, )), + SpawnAgentOptions { + fork_parent_thread: args.fork_current_thread, + }, ) .await .map_err(collab_spawn_error); diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index 7bd1fc9788..86513f7baf 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -552,6 +552,15 @@ fn create_spawn_agent_tool(config: &ToolsConfig) -> ToolSpec { )), }, ), + ( + "fork_current_thread".to_string(), + JsonSchema::Boolean { + description: Some( + "When true, fork the current thread history into the new agent before sending the initial prompt." + .to_string(), + ), + }, + ), ]); ToolSpec::Function(ResponsesApiTool {