Centralize child agent configuration in the agent module (#45669)

## What changed

Move shared spawn and resume configuration helpers into `agent::child_config` and route both multi-agent versions through `prepare_agent_spawn_config`.

Preserve each version's role and model precedence, full-history inheritance, service-tier selection, runtime policy, and default-role metadata for reloads. Return configuration errors as strings and convert them to `FunctionCallError::RespondToModel` at tool-handler boundaries.

GitOrigin-RevId: 9d539050aa44f94043a6487f1f6f1037de1d15ca
This commit is contained in:
jif
2026-09-15 10:07:37 +00:00
committed by copyberry
parent b0af519c39
commit b13164d86f
11 changed files with 404 additions and 347 deletions
+357
View File
@@ -0,0 +1,357 @@
//! 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<ReasoningEffort>,
}
pub(crate) struct PreparedSpawnConfig {
pub(crate) config: Config,
pub(crate) role_name: Option<String>,
}
/// 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<PreparedSpawnConfig, String> {
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<Config, String> {
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<Config, String> {
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<Config, String> {
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<ReasoningEffort>,
) -> 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<String, String> {
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::<Vec<_>>()
.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::<Vec<_>>()
.join(", ");
Err(format!(
"Reasoning effort `{requested_reasoning_effort}` is not supported for model `{model}`. Supported reasoning efforts: {supported}"
))
}
+1 -1
View File
@@ -1,5 +1,6 @@
use super::residency::is_v2_resident_session_source;
use super::*;
use crate::agent::child_config::build_agent_resume_config;
use crate::agent::role::apply_role_to_config;
use crate::codex_thread::CodexThread;
use crate::config::PermissionProfileSnapshot;
@@ -12,7 +13,6 @@ use crate::context::MultiAgentModeInstructions;
use crate::context::MultiAgentRoleInstructions;
use crate::context::world_state::PersistentModeState;
use crate::session::multi_agents::resolve_usage_hints;
use crate::tools::handlers::multi_agents_common::build_agent_resume_config;
use codex_context_fragments::set_annotated_content;
use codex_context_fragments::to_annotated_content;
use codex_extension_api::ExtensionDataInit;
+1
View File
@@ -1,4 +1,5 @@
pub(crate) mod agent_resolver;
pub(crate) mod child_config;
pub(crate) mod control;
mod registry;
pub(crate) mod role;
@@ -1,4 +1,5 @@
use super::*;
use crate::agent::child_config::build_agent_resume_config;
use crate::agent::next_thread_spawn_depth;
use crate::session::session::Session;
use crate::session::turn_context::TurnContext;
@@ -194,7 +195,8 @@ async fn try_resume_closed_agent(
receiver_thread_id: ThreadId,
child_depth: i32,
) -> Result<(), FunctionCallError> {
let config = build_agent_resume_config(turn.as_ref())?;
let config =
build_agent_resume_config(turn.as_ref()).map_err(FunctionCallError::RespondToModel)?;
Box::pin(session.services.agent_control.resume_agent_from_rollout(
config,
receiver_thread_id,
@@ -1,4 +1,5 @@
use super::*;
use crate::agent::child_config::build_agent_resume_config;
use crate::agent::control::render_input_preview;
use crate::tools::handlers::multi_agents_spec::create_send_input_tool_v1;
use codex_tools::ToolSpec;
@@ -51,7 +52,8 @@ impl Handler {
.agent_control
.get_agent_metadata(receiver_thread_id);
if receiver_agent.is_some() {
let resume_config = build_agent_resume_config(turn.as_ref())?;
let resume_config = build_agent_resume_config(turn.as_ref())
.map_err(FunctionCallError::RespondToModel)?;
session
.services
.agent_control
@@ -1,4 +1,7 @@
use super::*;
use crate::agent::child_config::SpawnConfigOptions;
use crate::agent::child_config::SpawnConfigVersion;
use crate::agent::child_config::prepare_agent_spawn_config;
use crate::agent::control::SpawnAgentForkMode;
use crate::agent::control::SpawnAgentOptions;
use crate::agent::control::render_input_preview;
@@ -89,25 +92,20 @@ async fn handle_spawn_agent(
}),
)
.await;
let mut config =
build_agent_spawn_config(&session.get_base_instructions().await, turn.as_ref())?;
if args.fork_context {
reject_full_fork_agent_type_override(role_name)?;
}
apply_requested_spawn_agent_model_overrides(
let prepared = prepare_agent_spawn_config(
&session,
turn.as_ref(),
&mut config,
args.model.as_deref(),
args.reasoning_effort.clone(),
SpawnConfigOptions {
version: SpawnConfigVersion::V1,
full_history_fork: args.fork_context,
role_name,
model: args.model.as_deref(),
reasoning_effort: args.reasoning_effort.clone(),
},
)
.await?;
if !args.fork_context {
apply_spawn_agent_role(&session, &mut config, role_name).await?;
}
apply_spawn_agent_service_tier(&session, &mut config).await?;
apply_spawn_agent_runtime_overrides(&mut config, turn.as_ref())?;
.await
.map_err(FunctionCallError::RespondToModel)?;
let config = prepared.config;
let result = Box::pin(session.services.agent_control.spawn_agent_with_metadata(
config,
input_items,
@@ -115,7 +113,7 @@ async fn handle_spawn_agent(
session.thread_id,
&turn.session_source,
child_depth,
role_name,
prepared.role_name.as_deref(),
/*task_name*/ None,
)?),
SpawnAgentOptions {
@@ -1,25 +1,14 @@
use crate::agent::role::apply_role_to_config;
use crate::config::Config;
use crate::config::DEFAULT_MULTI_AGENT_V2_MIN_WAIT_TIMEOUT_MS;
use crate::config::HARD_MAX_MULTI_AGENT_V2_TIMEOUT_MS;
use crate::function_tool::FunctionCallError;
use crate::session::session::Session;
use crate::session::turn_context::TurnContext;
use crate::tools::context::FunctionToolOutput;
use crate::tools::context::ToolOutput;
use crate::tools::context::ToolPayload;
use codex_models_manager::manager::RefreshStrategy;
use codex_protocol::AgentPath;
use codex_protocol::ThreadId;
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
use codex_protocol::error::CodexErr;
use codex_protocol::error::CodexErrorDetails;
use codex_protocol::models::BaseInstructions;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::openai_models::ModelPreset;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::openai_models::ReasoningEffortPreset;
use codex_protocol::protocol::MultiAgentVersion;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SubAgentSource;
use codex_protocol::user_input::UserInput;
@@ -30,16 +19,6 @@ use serde_json::Value as JsonValue;
pub(crate) const MIN_WAIT_TIMEOUT_MS: i64 = DEFAULT_MULTI_AGENT_V2_MIN_WAIT_TIMEOUT_MS;
pub(crate) const DEFAULT_WAIT_TIMEOUT_MS: i64 = 30_000;
pub(crate) const MAX_WAIT_TIMEOUT_MS: i64 = HARD_MAX_MULTI_AGENT_V2_TIMEOUT_MS;
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)
}
pub(crate) fn function_arguments(payload: ToolPayload) -> Result<String, FunctionCallError> {
match payload {
ToolPayload::Function { arguments } => Ok(arguments),
@@ -166,277 +145,3 @@ pub(crate) fn parse_collab_input(
}
}
}
/// 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<Config, FunctionCallError> {
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<Config, FunctionCallError> {
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<Config, FunctionCallError> {
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)
}
pub(crate) fn reject_full_fork_agent_type_override(
agent_type: Option<&str>,
) -> Result<(), FunctionCallError> {
if agent_type.is_some() {
return Err(FunctionCallError::RespondToModel(
"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.
pub(crate) fn apply_spawn_agent_runtime_overrides(
config: &mut Config,
turn: &TurnContext,
) -> Result<(), FunctionCallError> {
config
.permissions
.approval_policy
.set(turn.approval_policy())
.map_err(|err| {
FunctionCallError::RespondToModel(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| {
FunctionCallError::RespondToModel(format!("permission_profile is invalid: {err}"))
})?;
Ok(())
}
pub(crate) async fn apply_requested_spawn_agent_model_overrides(
session: &Session,
turn: &TurnContext,
config: &mut Config,
requested_model: Option<&str>,
requested_reasoning_effort: Option<ReasoningEffort>,
) -> Result<(), FunctionCallError> {
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<(), FunctionCallError> {
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(|| {
FunctionCallError::RespondToModel(
"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(())
}
pub(crate) async fn apply_spawn_agent_role(
session: &Session,
config: &mut Config,
role_name: Option<&str>,
) -> Result<(), FunctionCallError> {
let previous_model = config.model.clone();
let previous_reasoning_effort = config.model_reasoning_effort.clone();
apply_role_to_config(config, role_name)
.await
.map_err(FunctionCallError::RespondToModel)?;
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(|| {
FunctionCallError::RespondToModel(
"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<String, FunctionCallError> {
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::<Vec<_>>()
.join(", ");
FunctionCallError::RespondToModel(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<(), FunctionCallError> {
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::<Vec<_>>()
.join(", ");
Err(FunctionCallError::RespondToModel(format!(
"Reasoning effort `{requested_reasoning_effort}` is not supported for model `{model}`. Supported reasoning efforts: {supported}"
)))
}
@@ -1,5 +1,5 @@
use super::multi_agents_common::MAX_SPAWN_AGENT_MODEL_OVERRIDES;
use super::multi_agents_common::model_supports_multi_agent_backend;
use crate::agent::child_config::MAX_SPAWN_AGENT_MODEL_OVERRIDES;
use crate::agent::child_config::model_supports_multi_agent_backend;
use codex_protocol::openai_models::ModelPreset;
use codex_protocol::protocol::MultiAgentVersion;
use codex_tools::JsonSchema;
@@ -1,6 +1,9 @@
use super::*;
use crate::StartThreadOptions;
use crate::ThreadManager;
use crate::agent::child_config::apply_spawn_agent_service_tier;
use crate::agent::child_config::build_agent_resume_config;
use crate::agent::child_config::build_agent_spawn_config;
use crate::config::AgentRoleConfig;
use crate::config::DEFAULT_AGENT_MAX_DEPTH;
use crate::config::PermissionProfileSnapshot;
@@ -5,6 +5,7 @@
use super::analytics::ToolCallAnalytics;
use super::*;
use crate::agent::child_config::build_agent_resume_config;
use crate::agent_communication::AgentCommunicationContext;
use crate::agent_communication::AgentCommunicationKind;
use crate::tools::context::FunctionToolOutput;
@@ -85,7 +86,8 @@ pub(super) async fn handle_message_string_tool(
let receiver_agent_path = receiver_agent.agent_path.clone().ok_or_else(|| {
FunctionCallError::RespondToModel("target agent is missing an agent_path".to_string())
})?;
let resume_config = build_agent_resume_config(turn.as_ref())?;
let resume_config =
build_agent_resume_config(turn.as_ref()).map_err(FunctionCallError::RespondToModel)?;
session
.services
.agent_control
@@ -1,4 +1,7 @@
use super::*;
use crate::agent::child_config::SpawnConfigOptions;
use crate::agent::child_config::SpawnConfigVersion;
use crate::agent::child_config::prepare_agent_spawn_config;
use crate::agent::control::SpawnAgentForkMode;
use crate::agent::control::SpawnAgentOptions;
use crate::agent::next_thread_spawn_depth;
@@ -122,42 +125,26 @@ async fn handle_spawn_agent(
let session_source = turn.session_source.clone();
let child_depth = next_thread_spawn_depth(&session_source);
let mut config =
build_agent_spawn_config(&session.get_base_instructions().await, turn.as_ref())?;
let is_full_history_fork = matches!(fork_mode, Some(SpawnAgentForkMode::FullHistory));
apply_requested_spawn_agent_model_overrides(
let prepared = prepare_agent_spawn_config(
&session,
turn.as_ref(),
&mut config,
args.model.as_deref(),
args.reasoning_effort.clone(),
SpawnConfigOptions {
version: SpawnConfigVersion::V2,
full_history_fork: matches!(fork_mode, Some(SpawnAgentForkMode::FullHistory)),
role_name,
model: args.model.as_deref(),
reasoning_effort: args.reasoning_effort.clone(),
},
)
.await?;
if !is_full_history_fork || role_name.is_some() {
apply_spawn_agent_role(&session, &mut config, role_name).await?;
if is_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.as_ref())?;
// Remember an applied configured default so cold reload reapplies its restrictions.
let persisted_role_name = role_name.or_else(|| {
(!is_full_history_fork
&& config
.agent_roles
.get(DEFAULT_ROLE_NAME)
.is_some_and(|role| role.config_file.is_some()))
.then_some(DEFAULT_ROLE_NAME)
});
.await
.map_err(FunctionCallError::RespondToModel)?;
let config = prepared.config;
let is_full_history_fork = matches!(fork_mode, Some(SpawnAgentForkMode::FullHistory));
let spawn_source = thread_spawn_source(
session.thread_id,
&turn.session_source,
child_depth,
persisted_role_name,
prepared.role_name.as_deref(),
Some(args.task_name.clone()),
)?;
let new_agent_path = spawn_source.get_agent_path().ok_or_else(|| {