//! Prepares child configuration from the invoking turn and requested overrides. //! //! Spawn and reload share live runtime policy; role and model precedence, full-history //! inheritance, and validation messages remain the same for each multi-agent version. use crate::agent::role::DEFAULT_ROLE_NAME; use crate::agent::role::apply_role_to_config; use crate::config::Config; use crate::session::session::Session; use crate::session::turn_context::TurnContext; use codex_models_manager::manager::RefreshStrategy; use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE; use codex_protocol::models::BaseInstructions; use codex_protocol::openai_models::ModelPreset; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::openai_models::ReasoningEffortPreset; use codex_protocol::protocol::MultiAgentVersion; pub(crate) const MAX_SPAWN_AGENT_MODEL_OVERRIDES: usize = 5; pub(crate) fn model_supports_multi_agent_backend( model: &ModelPreset, multi_agent_version: MultiAgentVersion, ) -> bool { multi_agent_version != MultiAgentVersion::V2 || model.multi_agent_version != Some(MultiAgentVersion::Disabled) } /// Selects the existing spawn tool's role-inheritance rules. #[derive(Clone, Copy, PartialEq, Eq)] pub(crate) enum SpawnConfigVersion { V1, V2, } pub(crate) struct SpawnConfigOptions<'a> { pub(crate) version: SpawnConfigVersion, pub(crate) full_history_fork: bool, pub(crate) role_name: Option<&'a str>, pub(crate) model: Option<&'a str>, pub(crate) reasoning_effort: Option, } pub(crate) struct PreparedSpawnConfig { pub(crate) config: Config, pub(crate) role_name: Option, } /// Resolves child settings before starting the thread, retaining the invoking tool's precedence. pub(crate) async fn prepare_agent_spawn_config( session: &Session, turn: &TurnContext, options: SpawnConfigOptions<'_>, ) -> Result { let mut config = build_agent_spawn_config(&session.get_base_instructions().await, turn)?; if options.version == SpawnConfigVersion::V1 && options.full_history_fork { reject_full_fork_agent_type_override(options.role_name)?; } apply_requested_spawn_agent_model_overrides( session, turn, &mut config, options.model, options.reasoning_effort, ) .await?; if !options.full_history_fork || (options.version == SpawnConfigVersion::V2 && options.role_name.is_some()) { apply_spawn_agent_role(session, &mut config, options.role_name).await?; if options.version == SpawnConfigVersion::V2 && options.full_history_fork && config.developer_instructions.is_none() { config .developer_instructions .clone_from(&turn.developer_instructions); } } apply_spawn_agent_service_tier(session, &mut config).await?; apply_spawn_agent_runtime_overrides(&mut config, turn)?; // Remember an applied configured default so cold reload reapplies its restrictions. let role_name = options .role_name .or_else(|| { (options.version == SpawnConfigVersion::V2 && !options.full_history_fork && config .agent_roles .get(DEFAULT_ROLE_NAME) .is_some_and(|role| role.config_file.is_some())) .then_some(DEFAULT_ROLE_NAME) }) .map(str::to_owned); Ok(PreparedSpawnConfig { config, role_name }) } /// Builds the base config snapshot for a newly spawned sub-agent. /// /// The returned config starts from the parent's effective config and then refreshes the /// runtime-owned fields carried by the turn, including model selection, reasoning settings, /// approval policy, sandbox, and cwd. Role-specific overrides are layered /// after this step; skipping this helper and cloning stale config state directly can send the child /// agent out with the wrong provider or runtime policy. pub(crate) fn build_agent_spawn_config( base_instructions: &BaseInstructions, turn: &TurnContext, ) -> Result { let mut config = build_agent_shared_config(turn)?; config.base_instructions = Some(base_instructions.text.clone()); config.base_instructions_provenance = base_instructions.provenance.clone(); Ok(config) } pub(crate) fn build_agent_resume_config(turn: &TurnContext) -> Result { let mut config = build_agent_shared_config(turn)?; // For resume, keep base instructions sourced from rollout/session metadata. config.base_instructions = None; config.base_instructions_provenance = None; Ok(config) } fn build_agent_shared_config(turn: &TurnContext) -> Result { let base_config = turn.config.clone(); let mut config = (*base_config).clone(); // Preserve activation for history forks without freezing the parent's model-owned prompts. // Fresh child startup restores configured preferences from the retained snapshot. config.token_budget = turn.configured_token_budget.clone(); config.model = Some(turn.model_info().slug.clone()); config.model_provider = turn.provider.info().clone(); config.model_reasoning_effort = turn .reasoning_effort() .or(turn.model_info().default_reasoning_level.as_ref()) .cloned(); config.model_reasoning_summary = Some(turn.reasoning_summary()); config.developer_instructions = turn.developer_instructions.clone(); if turn.multi_agent_version == MultiAgentVersion::V2 && let Some(developer_instructions) = turn .config .multi_agent_v2 .subagent_developer_instructions .clone() { config.developer_instructions = Some(developer_instructions); } apply_spawn_agent_runtime_overrides(&mut config, turn)?; Ok(config) } fn reject_full_fork_agent_type_override(agent_type: Option<&str>) -> Result<(), String> { if agent_type.is_some() { return Err( "Full-history forked agents inherit the parent agent type; omit agent_type, or spawn without a full-history fork.".to_string()); } Ok(()) } /// Copies runtime-only turn state onto a child config before it is handed to `AgentControl`. /// /// These values are chosen by the live turn rather than persisted config, so leaving them stale can /// make a child agent disagree with its parent about approval policy, cwd, or sandboxing. fn apply_spawn_agent_runtime_overrides( config: &mut Config, turn: &TurnContext, ) -> Result<(), String> { config .permissions .approval_policy .set(turn.approval_policy()) .map_err(|err| format!("approval_policy is invalid: {err}"))?; config.approvals_reviewer = turn.config.approvals_reviewer; #[allow(deprecated)] let turn_cwd = turn.cwd.clone(); config.cwd = turn_cwd; config .permissions .set_permission_profile_from_session_snapshot( turn.config .permissions .permission_profile_state() .snapshot(), ) .map_err(|err| format!("permission_profile is invalid: {err}"))?; Ok(()) } async fn apply_requested_spawn_agent_model_overrides( session: &Session, turn: &TurnContext, config: &mut Config, requested_model: Option<&str>, requested_reasoning_effort: Option, ) -> Result<(), String> { let requested_model = requested_model.or(turn.config.agent_default_subagent_model.as_deref()); let requested_reasoning_effort = requested_reasoning_effort .or_else(|| turn.config.agent_default_subagent_reasoning_effort.clone()); if requested_model.is_none() && requested_reasoning_effort.is_none() { return Ok(()); } if let Some(requested_model) = requested_model { let available_models = session .services .models_manager .list_models(RefreshStrategy::Offline, config.http_client_factory()) .await; let selected_model_name = find_spawn_agent_model_name( &available_models, requested_model, turn.multi_agent_version, )?; let selected_model_info = session .services .models_manager .get_model_info(&selected_model_name, &config.to_models_manager_config()) .await; config.model = Some(selected_model_name.clone()); if let Some(reasoning_effort) = requested_reasoning_effort { validate_spawn_agent_reasoning_effort( &selected_model_name, &selected_model_info.supported_reasoning_levels, &reasoning_effort, )?; config.model_reasoning_effort = Some(reasoning_effort); } else { config.model_reasoning_effort = selected_model_info.default_reasoning_level; } return Ok(()); } if let Some(reasoning_effort) = requested_reasoning_effort { validate_spawn_agent_reasoning_effort( &turn.model_info().slug, &turn.model_info().supported_reasoning_levels, &reasoning_effort, )?; config.model_reasoning_effort = Some(reasoning_effort); } Ok(()) } pub(crate) async fn apply_spawn_agent_service_tier( session: &Session, config: &mut Config, ) -> Result<(), String> { let Some(service_tier) = session.services.agent_control.root_service_tier() else { config.service_tier = None; return Ok(()); }; if service_tier == SERVICE_TIER_DEFAULT_REQUEST_VALUE { config.service_tier = Some(service_tier); return Ok(()); } let model = config.model.clone().ok_or_else(|| { "spawn_agent could not resolve the child model for service tier validation".to_string() })?; let model_info = session .services .models_manager .get_model_info(model.as_str(), &config.to_models_manager_config()) .await; config.service_tier = model_info .supports_service_tier(service_tier.as_str()) .then_some(service_tier); Ok(()) } async fn apply_spawn_agent_role( session: &Session, config: &mut Config, role_name: Option<&str>, ) -> Result<(), String> { let previous_model = config.model.clone(); let previous_reasoning_effort = config.model_reasoning_effort.clone(); apply_role_to_config(config, role_name).await?; if config.model == previous_model && config.model_reasoning_effort == previous_reasoning_effort { return Ok(()); } let Some(reasoning_effort) = config.model_reasoning_effort.clone() else { return Ok(()); }; let model = config.model.clone().ok_or_else(|| { "spawn_agent could not resolve the child model for reasoning effort validation".to_string() })?; let model_info = session .services .models_manager .get_model_info(&model, &config.to_models_manager_config()) .await; if model_info.used_fallback_model_metadata { return Ok(()); } validate_spawn_agent_reasoning_effort( &model, &model_info.supported_reasoning_levels, &reasoning_effort, ) } fn find_spawn_agent_model_name( available_models: &[ModelPreset], requested_model: &str, multi_agent_version: MultiAgentVersion, ) -> Result { available_models .iter() .find(|model| { model.model == requested_model && model_supports_multi_agent_backend(model, multi_agent_version) }) .map(|model| model.model.clone()) .ok_or_else(|| { let available = available_models .iter() .filter(|model| model.show_in_picker) .filter(|model| model_supports_multi_agent_backend(model, multi_agent_version)) .take(MAX_SPAWN_AGENT_MODEL_OVERRIDES) .map(|model| model.model.as_str()) .collect::>() .join(", "); format!( "Unknown model `{requested_model}` for spawn_agent. Available models: {available}" ) }) } fn validate_spawn_agent_reasoning_effort( model: &str, supported_reasoning_levels: &[ReasoningEffortPreset], requested_reasoning_effort: &ReasoningEffort, ) -> Result<(), String> { if supported_reasoning_levels .iter() .any(|preset| &preset.effort == requested_reasoning_effort) { return Ok(()); } let supported = supported_reasoning_levels .iter() .map(|preset| preset.effort.to_string()) .collect::>() .join(", "); Err(format!( "Reasoning effort `{requested_reasoning_effort}` is not supported for model `{model}`. Supported reasoning efforts: {supported}" )) }