diff --git a/codex-rs/core/src/agent/control.rs b/codex-rs/core/src/agent/control.rs index e44fc8b6ff..3a435ec26a 100644 --- a/codex-rs/core/src/agent/control.rs +++ b/codex-rs/core/src/agent/control.rs @@ -10,7 +10,6 @@ use crate::agent::types::AgentMetadata; use crate::agent::types::LiveAgent; use crate::agent_communication::AgentCommunicationContext; use crate::agent_communication::AgentCommunicationKind; -use crate::codex_thread::ThreadConfigSnapshot; use crate::config::Config; use crate::config::RolloutBudgetConfig; use crate::context::SubagentNotification; @@ -80,6 +79,7 @@ mod residency; mod sender_context; mod service_tier; mod spawn; +mod spawn_request; mod target; mod user_authorization; @@ -425,16 +425,6 @@ impl LocalAgentControl { Ok(thread_ids) } - pub(crate) async fn get_agent_config_snapshot( - &self, - agent_id: ThreadId, - ) -> Option { - match self.inspect_agent(agent_id).await.ok()? { - crate::agent::api::AgentInfo::Loaded { config, .. } => Some(*config), - crate::agent::api::AgentInfo::Unloaded(_) => None, - } - } - /// Subscribe to status updates for `agent_id`, yielding the latest value and changes. pub(crate) async fn subscribe_status( &self, diff --git a/codex-rs/core/src/agent/control/spawn.rs b/codex-rs/core/src/agent/control/spawn.rs index cfd5020634..c495c3d6c6 100644 --- a/codex-rs/core/src/agent/control/spawn.rs +++ b/codex-rs/core/src/agent/control/spawn.rs @@ -8,6 +8,7 @@ use crate::agent::types::SpawnAgentForkMode; use crate::agent::types::SpawnAgentOptions; use crate::agents_md_manager::SessionInstructions; use crate::codex_thread::CodexThread; +use crate::codex_thread::ThreadConfigSnapshot; use crate::config::PermissionProfileSnapshot; use crate::context::ContextualUserFragment; use crate::context::CurrentTimeReminder; @@ -42,7 +43,7 @@ struct SpawnAgentThreadInheritance { /// provide user input directly, making an uncontextualized inter-agent communication /// unrepresentable. #[allow(clippy::large_enum_variant)] -enum SpawnInitialInput { +pub(super) enum SpawnInitialInput { UserInput(Vec), InterAgentCommunication(InterAgentCommunication, AgentCommunicationContext), } @@ -257,7 +258,7 @@ impl LocalAgentControl { initial_input: Vec, session_source: Option, ) -> CodexResult { - let spawned_agent = Box::pin(self.spawn_agent_internal( + let (spawned_agent, _) = Box::pin(self.spawn_agent_internal( config, SpawnInitialInput::UserInput(initial_input), session_source, @@ -267,40 +268,6 @@ impl LocalAgentControl { Ok(spawned_agent.thread_id) } - /// Spawn an agent thread with some metadata. - pub(crate) async fn spawn_agent_with_metadata( - &self, - config: Config, - initial_input: Vec, - session_source: Option, - options: SpawnAgentOptions, // TODO(jif) drop with new fork. - ) -> CodexResult { - Box::pin(self.spawn_agent_internal( - config, - SpawnInitialInput::UserInput(initial_input), - session_source, - options, - )) - .await - } - - pub(crate) async fn spawn_agent_with_communication( - &self, - config: Config, - communication: InterAgentCommunication, - context: AgentCommunicationContext, - session_source: Option, - options: SpawnAgentOptions, - ) -> CodexResult { - Box::pin(self.spawn_agent_internal( - config, - SpawnInitialInput::InterAgentCommunication(communication, context), - session_source, - options, - )) - .await - } - fn validate_loaded_v2_child( &self, thread: &CodexThread, @@ -629,13 +596,13 @@ impl LocalAgentControl { } } - async fn spawn_agent_internal( + pub(super) async fn spawn_agent_internal( &self, config: Config, initial_input: SpawnInitialInput, session_source: Option, options: SpawnAgentOptions, - ) -> CodexResult { + ) -> CodexResult<(LiveAgent, ThreadConfigSnapshot)> { let state = self.upgrade()?; let multi_agent_version = state .effective_multi_agent_version_for_spawn( @@ -829,11 +796,13 @@ impl LocalAgentControl { ); } - Ok(LiveAgent { + let agent = LiveAgent { thread_id: new_thread.thread_id, metadata: agent_metadata, status: self.get_status(new_thread.thread_id).await, - }) + }; + let config = new_thread.thread.config_snapshot().await; + Ok((agent, config)) } async fn spawn_forked_thread( diff --git a/codex-rs/core/src/agent/control/spawn_request.rs b/codex-rs/core/src/agent/control/spawn_request.rs new file mode 100644 index 0000000000..2813f93016 --- /dev/null +++ b/codex-rs/core/src/agent/control/spawn_request.rs @@ -0,0 +1,57 @@ +//! Converts captured spawn input into the existing local startup operation. +//! Agent messages keep their canonical paths and spawn attribution. + +use super::LocalAgentControl; +use super::spawn::SpawnInitialInput; +use crate::agent::api::AgentInput; +use crate::agent::api::SpawnRequest; +use crate::agent::types::LiveAgent; +use crate::agent::types::MessageDeliveryMode; +use crate::agent_communication::AgentCommunicationContext; +use crate::agent_communication::AgentCommunicationKind; +use crate::codex_thread::ThreadConfigSnapshot; +use codex_protocol::AgentPath; +use codex_protocol::error::CodexErr; +use codex_protocol::error::Result as CodexResult; + +impl LocalAgentControl { + pub(crate) async fn spawn( + &self, + request: SpawnRequest, + ) -> CodexResult<(LiveAgent, ThreadConfigSnapshot)> { + let SpawnRequest { + caller, + config, + input, + source, + options, + } = request; + let input = match input { + AgentInput::UserInput(input) => SpawnInitialInput::UserInput(input), + AgentInput::Message { message, mode } => { + if mode != MessageDeliveryMode::TriggerTurn { + return Err(CodexErr::InvalidRequest( + "spawn input must start the child turn".to_string(), + )); + } + let recipient = source.get_agent_path().ok_or_else(|| { + CodexErr::InvalidRequest( + "spawned agent is missing a canonical task name".to_string(), + ) + })?; + let author = recipient + .as_str() + .rsplit_once('/') + .and_then(|(parent, _)| AgentPath::try_from(parent).ok()) + .ok_or_else(|| { + CodexErr::InvalidRequest("spawn input needs a child path".to_string()) + })?; + SpawnInitialInput::InterAgentCommunication( + message.into_communication(author, recipient, mode), + AgentCommunicationContext::new(AgentCommunicationKind::Spawn, caller), + ) + } + }; + Box::pin(self.spawn_agent_internal(config, input, Some(source), options)).await + } +} diff --git a/codex-rs/core/src/agent/control_tests.rs b/codex-rs/core/src/agent/control_tests.rs index 387efab65e..9ea1e71131 100644 --- a/codex-rs/core/src/agent/control_tests.rs +++ b/codex-rs/core/src/agent/control_tests.rs @@ -4,7 +4,9 @@ use crate::StateDbHandle; use crate::ThreadManager; use crate::agent::agent_status_from_event; use crate::agent::api::AgentInfo; +use crate::agent::api::AgentInput; use crate::agent::api::AgentTarget; +use crate::agent::api::SpawnRequest; use crate::agent::next_thread_spawn_depth; use crate::agent::types::AgentMessage; use crate::agent::types::LiveAgent; @@ -101,6 +103,30 @@ use tokio::time::timeout; use tokio_util::sync::CancellationToken; use toml::Value as TomlValue; +impl LocalAgentControl { + /// Create a child fixture through the production spawn entry point. + pub(crate) async fn spawn_agent_with_metadata( + &self, + config: Config, + initial_input: Vec, + source: SessionSource, + options: SpawnAgentOptions, + ) -> CodexResult { + let caller = source + .parent_thread_id() + .expect("child fixture has a parent"); + self.spawn(SpawnRequest { + caller, + config, + input: AgentInput::UserInput(initial_input), + source, + options, + }) + .await + .map(|(agent, _)| agent) + } +} + async fn test_config_with_cli_overrides( mut cli_overrides: Vec<(String, TomlValue)>, ) -> (TempDir, Config) { @@ -261,13 +287,13 @@ impl AgentControlHarness { .spawn_agent_with_metadata( self.config.clone(), text_input("child task"), - Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + SessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id, depth: 1, agent_path: None, agent_nickname: None, agent_role: None, - })), + }), options, ) .await @@ -826,7 +852,7 @@ async fn spawn_v2_reload_test_child( .spawn_agent_with_metadata( config, text_input("hello child"), - Some(source), + source, SpawnAgentOptions { parent_thread_id: Some(parent.session.thread_id), ..Default::default() @@ -2243,13 +2269,13 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() { .spawn_agent_with_metadata( child_config, text_input("child task"), - Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + SessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id, depth: 1, agent_path: None, agent_nickname: None, agent_role: None, - })), + }), SpawnAgentOptions { fork_parent_spawn_call_id: Some(parent_spawn_call_id.clone()), fork_mode: Some(SpawnAgentForkMode::FullHistory), @@ -2368,13 +2394,13 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() { .spawn_agent_with_metadata( no_hint_child_config, text_input("child task without hints"), - Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + SessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id, depth: 1, agent_path: None, agent_nickname: None, agent_role: None, - })), + }), SpawnAgentOptions { fork_parent_spawn_call_id: Some(parent_spawn_call_id.clone()), fork_mode: Some(SpawnAgentForkMode::FullHistory), @@ -2605,13 +2631,13 @@ async fn spawn_agent_fork_strips_parent_usage_hints_from_compacted_history( .spawn_agent_with_metadata( child_config, text_input("child task"), - Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + SessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id, depth: 1, agent_path: None, agent_nickname: None, agent_role: None, - })), + }), SpawnAgentOptions { fork_parent_spawn_call_id: Some(parent_spawn_call_id), fork_mode: Some(SpawnAgentForkMode::FullHistory), @@ -2808,13 +2834,13 @@ async fn spawn_agent_full_fork_restores_instructions_after_compaction_discards_p .spawn_agent_with_metadata( child_config, text_input("child task"), - Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + SessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id, depth: 1, agent_path: None, agent_nickname: None, agent_role: None, - })), + }), SpawnAgentOptions { fork_parent_spawn_call_id: Some(parent_spawn_call_id), fork_mode: Some(SpawnAgentForkMode::FullHistory), @@ -2992,13 +3018,13 @@ async fn spawn_agent_full_fork_legacy_compaction_rebuilds_child_instructions_onc .spawn_agent_with_metadata( child_config, text_input("child task"), - Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + SessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id, depth: 1, agent_path: None, agent_nickname: None, agent_role: None, - })), + }), SpawnAgentOptions { fork_parent_spawn_call_id: Some(parent_spawn_call_id.to_string()), fork_mode: Some(SpawnAgentForkMode::FullHistory), @@ -3178,13 +3204,13 @@ async fn spawn_agent_fork_flushes_parent_rollout_before_loading_history() { .spawn_agent_with_metadata( harness.config.clone(), text_input("child task"), - Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + SessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id, depth: 1, agent_path: None, agent_nickname: None, agent_role: None, - })), + }), SpawnAgentOptions { fork_parent_spawn_call_id: Some(parent_spawn_call_id.clone()), fork_mode: Some(SpawnAgentForkMode::FullHistory), @@ -3318,13 +3344,13 @@ async fn spawn_agent_fork_last_n_turns_keeps_only_recent_turns() { .spawn_agent_with_metadata( harness.config.clone(), text_input("child task"), - Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + SessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id, depth: 1, agent_path: None, agent_nickname: None, agent_role: None, - })), + }), SpawnAgentOptions { fork_parent_spawn_call_id: Some(parent_spawn_call_id.clone()), fork_mode: Some(SpawnAgentForkMode::LastNTurns(2)), @@ -3458,13 +3484,13 @@ async fn spawn_agent_fork_last_n_turns_drops_parent_startup_prefix_when_under_li .spawn_agent_with_metadata( harness.config.clone(), text_input("child task"), - Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + SessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id, depth: 1, agent_path: None, agent_nickname: None, agent_role: None, - })), + }), SpawnAgentOptions { fork_parent_spawn_call_id: Some(parent_spawn_call_id), fork_mode: Some(SpawnAgentForkMode::LastNTurns(2)), @@ -3594,13 +3620,13 @@ async fn spawn_agent_fork_last_n_turns_strips_parent_usage_hints() { .spawn_agent_with_metadata( child_config, text_input("child task"), - Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + SessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id, depth: 1, agent_path: None, agent_nickname: None, agent_role: None, - })), + }), SpawnAgentOptions { fork_parent_spawn_call_id: Some(parent_spawn_call_id), fork_mode: Some(SpawnAgentForkMode::LastNTurns(2)), @@ -4240,13 +4266,13 @@ async fn spawn_thread_subagents_persist_parent_originator_across_new_and_truncat .spawn_agent_with_metadata( harness.config.clone(), text_input("hello forked child"), - Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + SessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id: parent.thread_id, depth: 1, agent_path: None, agent_nickname: None, agent_role: Some("explorer".to_string()), - })), + }), SpawnAgentOptions { fork_parent_spawn_call_id: Some("spawn-call-last-n".to_string()), fork_mode: Some(SpawnAgentForkMode::LastNTurns(1)), diff --git a/codex-rs/core/src/thread_manager_tests.rs b/codex-rs/core/src/thread_manager_tests.rs index 278b69fa46..4e16dd6443 100644 --- a/codex-rs/core/src/thread_manager_tests.rs +++ b/codex-rs/core/src/thread_manager_tests.rs @@ -385,13 +385,13 @@ async fn thread_id_generator_applies_to_roots_children_and_forks() { text: "child task".to_string(), text_elements: Vec::new(), }], - Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + SessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id: root.thread_id, depth: 1, agent_path: None, agent_nickname: None, agent_role: None, - })), + }), SpawnAgentOptions { parent_thread_id: Some(root.thread_id), ..Default::default() diff --git a/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs b/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs index b8cded3455..32a024d8ed 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs @@ -1,4 +1,6 @@ use super::*; +use crate::agent::api::AgentInput; +use crate::agent::api::SpawnRequest; use crate::agent::child_config::SpawnConfigOptions; use crate::agent::child_config::SpawnConfigVersion; use crate::agent::child_config::prepare_agent_spawn_config; @@ -106,17 +108,18 @@ async fn handle_spawn_agent( .await .map_err(FunctionCallError::RespondToModel)?; let config = prepared.config; - let result = Box::pin(session.services.agent_control.spawn_agent_with_metadata( + let result = Box::pin(session.services.agent_control.spawn(SpawnRequest { + caller: session.thread_id, config, - input_items, - Some(thread_spawn_source( + input: AgentInput::UserInput(input_items), + source: thread_spawn_source( session.thread_id, &turn.session_source, child_depth, prepared.role_name.as_deref(), /*task_name*/ None, - )?), - SpawnAgentOptions { + )?, + options: SpawnAgentOptions { fork_parent_spawn_call_id: args.fork_context.then(|| call_id.clone()), fork_mode: args.fork_context.then_some(SpawnAgentForkMode::FullHistory), parent_thread_id: Some(session.thread_id), @@ -127,47 +130,22 @@ async fn handle_spawn_agent( multi_agent_v2_usage_hints: None, cyber_access_program: turn.cyber_access_program, }, - )) + })) .await .map_err(collab_spawn_error); - let (new_thread_id, new_agent_metadata, status) = match &result { - Ok(spawned_agent) => ( - Some(spawned_agent.thread_id), - Some(spawned_agent.metadata.clone()), - spawned_agent.status.clone(), - ), - Err(_) => (None, None, AgentStatus::NotFound), + let (new_thread_id, status) = match &result { + Ok((spawned_agent, _)) => (Some(spawned_agent.thread_id), spawned_agent.status.clone()), + Err(_) => (None, AgentStatus::NotFound), }; - let agent_snapshot = match new_thread_id { - Some(thread_id) => { - session - .services - .agent_control - .get_agent_config_snapshot(thread_id) - .await - } - None => None, - }; - let (_new_agent_path, new_agent_nickname, new_agent_role) = - match (&agent_snapshot, new_agent_metadata) { - (Some(snapshot), _) => ( - snapshot.session_source.get_agent_path().map(String::from), - snapshot.session_source.get_nickname(), - snapshot.session_source.get_agent_role(), - ), - (None, Some(metadata)) => ( - metadata.agent_path.map(String::from), - metadata.agent_nickname, - metadata.agent_role, - ), - (None, None) => (None, None, None), - }; + let agent_snapshot = result.as_ref().ok().map(|(_, config)| config); + let new_agent_nickname = + agent_snapshot.and_then(|snapshot| snapshot.session_source.get_nickname()); + let new_agent_role = + agent_snapshot.and_then(|snapshot| snapshot.session_source.get_agent_role()); let effective_model = agent_snapshot - .as_ref() .map(|snapshot| snapshot.model.clone()) .unwrap_or_else(|| args.model.clone().unwrap_or_default()); let effective_reasoning_effort = agent_snapshot - .as_ref() .and_then(|snapshot| snapshot.reasoning_effort.clone()) .unwrap_or(args.reasoning_effort.unwrap_or_default()); let nickname = new_agent_nickname.clone(); @@ -200,7 +178,8 @@ async fn handle_spawn_agent( }), ) .await; - let new_thread_id = result?.thread_id; + let (spawned_agent, _) = result?; + let new_thread_id = spawned_agent.thread_id; let role_tag = role_name.unwrap_or(DEFAULT_ROLE_NAME); turn.session_telemetry.counter( "codex.multi_agent.spawn", diff --git a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs index e4e027cc8b..8a563f6358 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs @@ -1225,13 +1225,13 @@ async fn multi_agent_v2_send_message_accepts_root_target_from_child() { text: "inspect this repo".to_string(), text_elements: Vec::new(), }], - Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + SessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id: root.thread_id, depth: 1, agent_path: Some(child_path.clone()), agent_nickname: None, agent_role: None, - })), + }), crate::agent::types::SpawnAgentOptions::default(), ) .await @@ -1301,13 +1301,13 @@ async fn multi_agent_v2_followup_task_rejects_root_target_from_child() { text: "inspect this repo".to_string(), text_elements: Vec::new(), }], - Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + SessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id: root.thread_id, depth: 1, agent_path: Some(child_path.clone()), agent_nickname: None, agent_role: None, - })), + }), crate::agent::types::SpawnAgentOptions::default(), ) .await @@ -1466,13 +1466,13 @@ async fn multi_agent_v2_list_agents_filters_by_relative_path_prefix() { text: "research".to_string(), text_elements: Vec::new(), }], - Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + SessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id: root.thread_id, depth: 1, agent_path: Some(researcher_path.clone()), agent_nickname: None, agent_role: None, - })), + }), crate::agent::types::SpawnAgentOptions::default(), ) .await @@ -1486,13 +1486,13 @@ async fn multi_agent_v2_list_agents_filters_by_relative_path_prefix() { text: "build".to_string(), text_elements: Vec::new(), }], - Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + SessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id: root.thread_id, depth: 2, agent_path: Some(worker_path.clone()), agent_nickname: None, agent_role: None, - })), + }), crate::agent::types::SpawnAgentOptions::default(), ) .await @@ -4016,13 +4016,13 @@ async fn multi_agent_v2_interrupt_agent_rejects_self_target_by_id() { text: "inspect this repo".to_string(), text_elements: Vec::new(), }], - Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + SessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id: root.thread_id, depth: 1, agent_path: Some(child_path.clone()), agent_nickname: None, agent_role: None, - })), + }), crate::agent::types::SpawnAgentOptions::default(), ) .await @@ -4083,13 +4083,13 @@ async fn multi_agent_v2_interrupt_agent_rejects_self_target_by_task_name() { text: "inspect this repo".to_string(), text_elements: Vec::new(), }], - Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + SessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id: root.thread_id, depth: 1, agent_path: Some(child_path.clone()), agent_nickname: None, agent_role: None, - })), + }), crate::agent::types::SpawnAgentOptions::default(), ) .await diff --git a/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs b/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs index 3de9822882..bd852d15a6 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs @@ -1,4 +1,6 @@ use super::*; +use crate::agent::api::AgentInput; +use crate::agent::api::SpawnRequest; use crate::agent::child_config::SpawnConfigOptions; use crate::agent::child_config::SpawnConfigVersion; use crate::agent::child_config::prepare_agent_spawn_config; @@ -7,8 +9,6 @@ use crate::agent::role::DEFAULT_ROLE_NAME; use crate::agent::types::MessageDeliveryMode; use crate::agent::types::SpawnAgentForkMode; use crate::agent::types::SpawnAgentOptions; -use crate::agent_communication::AgentCommunicationContext; -use crate::agent_communication::AgentCommunicationKind; use crate::codex_thread::ThreadConfigSnapshot; use crate::session::multi_agents::resolve_usage_hints; use crate::tools::handlers::multi_agents::collab_tool_call_status; @@ -17,7 +17,6 @@ use crate::tools::handlers::multi_agents_spec::create_spawn_agent_tool_v2; use crate::tools::handlers::multi_agents_v2::message_tool::message_content; use crate::turn_timing::now_unix_timestamp_ms; use codex_prompts::ResolvedModelMessages; -use codex_protocol::AgentPath; use codex_protocol::ThreadId; use codex_protocol::protocol::MultiAgentVersion; use codex_tools::ToolSpec; @@ -73,10 +72,7 @@ impl ToolExecutor for Handler { Default::default(), ), }; - let agent_snapshot = result - .as_ref() - .ok() - .and_then(|(_, _, _, snapshot)| snapshot.as_ref()); + let agent_snapshot = result.as_ref().ok().map(|(_, _, _, snapshot)| snapshot); analytics.track_collab_tool_call( turn_id, @@ -109,7 +105,7 @@ async fn handle_spawn_agent( SpawnAgentResult, ThreadId, AgentStatus, - Option, + ThreadConfigSnapshot, ), FunctionCallError, > { @@ -161,16 +157,6 @@ async fn handle_spawn_agent( "spawned agent is missing a canonical task name".to_string(), ) })?; - let author = turn - .session_source - .get_agent_path() - .unwrap_or_else(AgentPath::root); - let communication = agent_message_from_tool(message, &source).into_communication( - author, - new_agent_path.clone(), - MessageDeliveryMode::TriggerTurn, - ); - let context = AgentCommunicationContext::new(AgentCommunicationKind::Spawn, session.thread_id); let multi_agent_v2_usage_hints = if is_full_history_fork && turn.multi_agent_version == MultiAgentVersion::V2 { let child_model_info = match config.model.as_deref() { @@ -195,40 +181,34 @@ async fn handle_spawn_agent( } else { None }; - let spawned_agent = Box::pin( - session - .services - .agent_control - .spawn_agent_with_communication( - config, - communication, - context, - Some(spawn_source), - SpawnAgentOptions { - fork_parent_spawn_call_id: fork_mode.as_ref().map(|_| call_id.clone()), - fork_mode, - parent_thread_id: Some(session.thread_id), - parent_turn_id: Some(turn.sub_id.clone()), - root_turn_id: turn.turn_metadata_state.root_turn_id(), - turn_trigger: turn.turn_metadata_state.current_turn_trigger(), - environments: Some(step_context.environments.to_selections()), - multi_agent_v2_usage_hints, - cyber_access_program: turn.cyber_access_program, - }, - ), - ) - .await - .map_err(collab_spawn_error)?; + let (spawned_agent, agent_snapshot) = + Box::pin(session.services.agent_control.spawn(SpawnRequest { + caller: session.thread_id, + config, + input: AgentInput::Message { + message: agent_message_from_tool(message, &source), + mode: MessageDeliveryMode::TriggerTurn, + }, + source: spawn_source, + options: SpawnAgentOptions { + fork_parent_spawn_call_id: fork_mode.as_ref().map(|_| call_id.clone()), + fork_mode, + parent_thread_id: Some(session.thread_id), + parent_turn_id: Some(turn.sub_id.clone()), + root_turn_id: turn.turn_metadata_state.root_turn_id(), + turn_trigger: turn.turn_metadata_state.current_turn_trigger(), + environments: Some(step_context.environments.to_selections()), + multi_agent_v2_usage_hints, + cyber_access_program: turn.cyber_access_program, + }, + })) + .await + .map_err(collab_spawn_error)?; let new_thread_id = spawned_agent.thread_id; let agent_status = spawned_agent.status; - let agent_snapshot = session - .services - .agent_control - .get_agent_config_snapshot(new_thread_id) - .await; let nickname = agent_snapshot - .as_ref() - .and_then(|snapshot| snapshot.session_source.get_nickname()) + .session_source + .get_nickname() .or(spawned_agent.metadata.agent_nickname); emit_sub_agent_activity( &session,