Resolve per-thread multi-agent runtime (#25722)

Stack split from #25708. Original PR intentionally left open. This third
PR resolves the effective per-thread multi-agent runtime from persisted
metadata, inherited runtime, and current model selection.
This commit is contained in:
jif-oai
2026-06-02 14:31:00 +02:00
committed by GitHub
Unverified
parent 0c5ccd18ab
commit bf9fd885b2
24 changed files with 599 additions and 206 deletions
+10 -10
View File
@@ -1,6 +1,4 @@
use crate::agent::control::SpawnAgentOptions;
use crate::agent::exceeds_thread_spawn_depth_limit;
use crate::agent::next_thread_spawn_depth;
use crate::agent::status::is_final;
use crate::config::Config;
use crate::function_tool::FunctionCallError;
@@ -11,6 +9,7 @@ use crate::tools::handlers::parse_arguments;
use codex_protocol::ThreadId;
use codex_protocol::error::CodexErr;
use codex_protocol::protocol::AgentStatus;
use codex_protocol::protocol::MultiAgentVersion;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SubAgentSource;
use codex_protocol::user_input::UserInput;
@@ -111,21 +110,22 @@ async fn build_runner_options(
turn: &Arc<TurnContext>,
requested_concurrency: Option<usize>,
) -> Result<JobRunnerOptions, FunctionCallError> {
let session_source = turn.session_source.clone();
let child_depth = next_thread_spawn_depth(&session_source);
let max_depth = turn.config.agent_max_depth;
if exceeds_thread_spawn_depth_limit(child_depth, max_depth) {
let multi_agent_version = turn.multi_agent_version;
if multi_agent_version == MultiAgentVersion::Disabled {
return Err(FunctionCallError::RespondToModel(
"agent depth limit reached; this session cannot spawn more subagents".to_string(),
"multi-agent runtime is disabled; this session cannot spawn workers".to_string(),
));
}
if turn.config.agent_max_threads == Some(0) {
let agent_max_threads = turn
.config
.effective_agent_max_threads(multi_agent_version)
.map_err(|err| FunctionCallError::Fatal(err.to_string()))?;
if agent_max_threads == Some(0) {
return Err(FunctionCallError::RespondToModel(
"agent thread limit reached; this session cannot spawn more subagents".to_string(),
));
}
let max_concurrency =
normalize_concurrency(requested_concurrency, turn.config.agent_max_threads);
let max_concurrency = normalize_concurrency(requested_concurrency, agent_max_threads);
let base_instructions = session.get_base_instructions().await;
let spawn_config = build_agent_spawn_config(&base_instructions, turn.as_ref())?;
Ok(JobRunnerOptions {
@@ -181,7 +181,7 @@ async fn try_resume_closed_agent(
receiver_thread_id: ThreadId,
child_depth: i32,
) -> Result<(), FunctionCallError> {
let config = build_agent_resume_config(turn.as_ref(), child_depth)?;
let config = build_agent_resume_config(turn.as_ref())?;
Box::pin(session.services.agent_control.resume_agent_from_rollout(
config,
receiver_thread_id,
@@ -116,7 +116,6 @@ async fn handle_spawn_agent(
)
.await?;
apply_spawn_agent_runtime_overrides(&mut config, turn.as_ref())?;
apply_spawn_agent_overrides(&mut config, child_depth);
let result = Box::pin(session.services.agent_control.spawn_agent_with_metadata(
config,
@@ -8,7 +8,6 @@ use crate::session::turn_context::TurnContext;
use crate::tools::context::FunctionToolOutput;
use crate::tools::context::ToolOutput;
use crate::tools::context::ToolPayload;
use codex_features::Feature;
use codex_models_manager::manager::RefreshStrategy;
use codex_protocol::AgentPath;
use codex_protocol::ThreadId;
@@ -211,12 +210,8 @@ pub(crate) fn build_agent_spawn_config(
Ok(config)
}
pub(crate) fn build_agent_resume_config(
turn: &TurnContext,
child_depth: i32,
) -> Result<Config, FunctionCallError> {
pub(crate) fn build_agent_resume_config(turn: &TurnContext) -> Result<Config, FunctionCallError> {
let mut config = build_agent_shared_config(turn)?;
apply_spawn_agent_overrides(&mut config, child_depth);
// For resume, keep base instructions sourced from rollout/session metadata.
config.base_instructions = None;
Ok(config)
@@ -280,13 +275,6 @@ pub(crate) fn apply_spawn_agent_runtime_overrides(
Ok(())
}
pub(crate) fn apply_spawn_agent_overrides(config: &mut Config, child_depth: i32) {
if child_depth >= config.agent_max_depth && !config.features.enabled(Feature::MultiAgentV2) {
let _ = config.features.disable(Feature::SpawnCsv);
let _ = config.features.disable(Feature::Collab);
}
}
pub(crate) async fn apply_requested_spawn_agent_model_overrides(
session: &Session,
turn: &TurnContext,
@@ -134,6 +134,11 @@ model_reasoning_effort = "minimal"
role_name
}
fn set_turn_config(turn: &mut TurnContext, config: crate::config::Config) {
turn.multi_agent_version = config.multi_agent_version_from_features();
turn.config = Arc::new(config);
}
fn expect_text_output<T>(output: T) -> (String, Option<bool>)
where
T: ToolOutput,
@@ -382,6 +387,7 @@ async fn multi_agent_v2_spawn_fork_turns_all_rejects_agent_type_override() {
.expect("test config should allow feature update");
let turn = TurnContext {
config: Arc::new(config),
multi_agent_version: codex_protocol::protocol::MultiAgentVersion::V2,
..turn
};
@@ -424,7 +430,7 @@ async fn multi_agent_v2_spawn_defaults_to_full_fork_and_rejects_child_model_over
.features
.enable(Feature::MultiAgentV2)
.expect("test config should allow feature update");
turn.config = Arc::new(config);
set_turn_config(&mut turn, config);
let err = SpawnAgentHandlerV2::default()
.handle(invocation(
@@ -891,7 +897,7 @@ async fn multi_agent_v2_full_history_fork_accepts_explicit_service_tier() {
.features
.enable(Feature::MultiAgentV2)
.expect("test config should allow feature update");
turn.config = Arc::new(config);
set_turn_config(&mut turn, config);
let manager = thread_manager();
let root = manager
.start_thread((*turn.config).clone())
@@ -959,6 +965,7 @@ async fn multi_agent_v2_spawn_partial_fork_turns_allows_agent_type_override() {
.expect("test config should allow feature update");
let turn = TurnContext {
config: Arc::new(config),
multi_agent_version: codex_protocol::protocol::MultiAgentVersion::V2,
..turn
};
@@ -1040,7 +1047,7 @@ async fn multi_agent_v2_spawn_requires_task_name() {
.features
.enable(Feature::MultiAgentV2)
.expect("test config should allow feature update");
turn.config = Arc::new(config);
set_turn_config(&mut turn, config);
let invocation = invocation(
Arc::new(session),
@@ -1074,7 +1081,7 @@ async fn multi_agent_v2_spawn_rejects_legacy_items_field() {
.features
.enable(Feature::MultiAgentV2)
.expect("test config should allow feature update");
turn.config = Arc::new(config);
set_turn_config(&mut turn, config);
let invocation = invocation(
Arc::new(session),
@@ -1134,7 +1141,7 @@ async fn multi_agent_v2_spawn_returns_path_and_send_message_accepts_relative_pat
.features
.enable(Feature::MultiAgentV2)
.expect("test config should allow feature update");
turn.config = Arc::new(config);
set_turn_config(&mut turn, config);
let session = Arc::new(session);
let turn = Arc::new(turn);
@@ -1231,7 +1238,7 @@ async fn multi_agent_v2_spawn_rejects_legacy_fork_context() {
.features
.enable(Feature::MultiAgentV2)
.expect("test config should allow feature update");
turn.config = Arc::new(config);
set_turn_config(&mut turn, config);
let err = SpawnAgentHandlerV2::default()
.handle(invocation(
@@ -1271,7 +1278,7 @@ async fn multi_agent_v2_spawn_rejects_invalid_fork_turns_string() {
.features
.enable(Feature::MultiAgentV2)
.expect("test config should allow feature update");
turn.config = Arc::new(config);
set_turn_config(&mut turn, config);
let err = SpawnAgentHandlerV2::default()
.handle(invocation(
@@ -1311,7 +1318,7 @@ async fn multi_agent_v2_spawn_rejects_zero_fork_turns() {
.features
.enable(Feature::MultiAgentV2)
.expect("test config should allow feature update");
turn.config = Arc::new(config);
set_turn_config(&mut turn, config);
let err = SpawnAgentHandlerV2::default()
.handle(invocation(
@@ -1340,18 +1347,18 @@ async fn multi_agent_v2_spawn_rejects_zero_fork_turns() {
async fn multi_agent_v2_send_message_accepts_root_target_from_child() {
let (mut session, mut turn) = make_session_and_context().await;
let manager = thread_manager();
let mut config = (*turn.config).clone();
config
.features
.enable(Feature::MultiAgentV2)
.expect("test config should allow feature update");
set_turn_config(&mut turn, config);
let root = manager
.start_thread((*turn.config).clone())
.await
.expect("root thread should start");
session.services.agent_control = manager.agent_control();
session.conversation_id = root.thread_id;
let mut config = (*turn.config).clone();
config
.features
.enable(Feature::MultiAgentV2)
.expect("test config should allow feature update");
turn.config = Arc::new(config);
let child_path = AgentPath::try_from("/root/worker").expect("agent path");
let child_thread_id = session
@@ -1416,18 +1423,18 @@ async fn multi_agent_v2_send_message_accepts_root_target_from_child() {
async fn multi_agent_v2_followup_task_rejects_root_target_from_child() {
let (mut session, mut turn) = make_session_and_context().await;
let manager = thread_manager();
let mut config = (*turn.config).clone();
config
.features
.enable(Feature::MultiAgentV2)
.expect("test config should allow feature update");
set_turn_config(&mut turn, config);
let root = manager
.start_thread((*turn.config).clone())
.await
.expect("root thread should start");
session.services.agent_control = manager.agent_control();
session.conversation_id = root.thread_id;
let mut config = (*turn.config).clone();
config
.features
.enable(Feature::MultiAgentV2)
.expect("test config should allow feature update");
turn.config = Arc::new(config);
let child_path = AgentPath::try_from("/root/worker").expect("agent path");
let child_thread_id = session
@@ -1507,7 +1514,7 @@ async fn multi_agent_v2_list_agents_returns_completed_status_and_last_task_messa
session.conversation_id = root.thread_id;
let mut config = (*turn.config).clone();
let _ = config.features.enable(Feature::MultiAgentV2);
turn.config = Arc::new(config);
set_turn_config(&mut turn, config);
let session = Arc::new(session);
let turn = Arc::new(turn);
@@ -1593,15 +1600,15 @@ async fn multi_agent_v2_list_agents_returns_completed_status_and_last_task_messa
async fn multi_agent_v2_list_agents_filters_by_relative_path_prefix() {
let (mut session, mut turn) = make_session_and_context().await;
let manager = thread_manager();
let mut config = (*turn.config).clone();
let _ = config.features.enable(Feature::MultiAgentV2);
set_turn_config(&mut turn, config.clone());
let root = manager
.start_thread((*turn.config).clone())
.await
.expect("root thread should start");
session.services.agent_control = manager.agent_control();
session.conversation_id = root.thread_id;
let mut config = (*turn.config).clone();
let _ = config.features.enable(Feature::MultiAgentV2);
turn.config = Arc::new(config.clone());
let researcher_path = AgentPath::from_string("/root/researcher".to_string()).expect("path");
let worker_path = AgentPath::from_string("/root/researcher/worker".to_string()).expect("path");
@@ -1688,7 +1695,7 @@ async fn multi_agent_v2_list_agents_omits_closed_agents() {
session.conversation_id = root.thread_id;
let mut config = (*turn.config).clone();
let _ = config.features.enable(Feature::MultiAgentV2);
turn.config = Arc::new(config);
set_turn_config(&mut turn, config);
let session = Arc::new(session);
let turn = Arc::new(turn);
@@ -1752,7 +1759,7 @@ async fn multi_agent_v2_send_message_rejects_legacy_items_field() {
session.conversation_id = root.thread_id;
let mut config = turn.config.as_ref().clone();
let _ = config.features.enable(Feature::MultiAgentV2);
turn.config = Arc::new(config);
set_turn_config(&mut turn, config);
let session = Arc::new(session);
let turn = Arc::new(turn);
@@ -1808,7 +1815,7 @@ async fn multi_agent_v2_send_message_rejects_interrupt_parameter() {
session.conversation_id = root.thread_id;
let mut config = turn.config.as_ref().clone();
let _ = config.features.enable(Feature::MultiAgentV2);
turn.config = Arc::new(config);
set_turn_config(&mut turn, config);
let session = Arc::new(session);
let turn = Arc::new(turn);
@@ -1873,15 +1880,15 @@ async fn multi_agent_v2_send_message_rejects_interrupt_parameter() {
async fn multi_agent_v2_followup_task_completion_notifies_parent_on_every_turn() {
let (mut session, mut turn) = make_session_and_context().await;
let manager = thread_manager();
let mut config = turn.config.as_ref().clone();
let _ = config.features.enable(Feature::MultiAgentV2);
set_turn_config(&mut turn, config);
let root = manager
.start_thread((*turn.config).clone())
.await
.expect("root thread should start");
session.services.agent_control = manager.agent_control();
session.conversation_id = root.thread_id;
let mut config = turn.config.as_ref().clone();
let _ = config.features.enable(Feature::MultiAgentV2);
turn.config = Arc::new(config);
let session = Arc::new(session);
let turn = Arc::new(turn);
@@ -2016,7 +2023,7 @@ async fn multi_agent_v2_followup_task_rejects_legacy_items_field() {
session.conversation_id = root.thread_id;
let mut config = turn.config.as_ref().clone();
let _ = config.features.enable(Feature::MultiAgentV2);
turn.config = Arc::new(config);
set_turn_config(&mut turn, config);
let session = Arc::new(session);
let turn = Arc::new(turn);
@@ -2069,7 +2076,7 @@ async fn multi_agent_v2_interrupted_turn_does_not_notify_parent() {
session.conversation_id = root.thread_id;
let mut config = turn.config.as_ref().clone();
let _ = config.features.enable(Feature::MultiAgentV2);
turn.config = Arc::new(config);
set_turn_config(&mut turn, config);
let session = Arc::new(session);
let turn = Arc::new(turn);
@@ -2149,7 +2156,7 @@ async fn multi_agent_v2_spawn_omits_agent_id_when_named() {
.features
.enable(Feature::MultiAgentV2)
.expect("test config should allow feature update");
turn.config = Arc::new(config);
set_turn_config(&mut turn, config);
let output = SpawnAgentHandlerV2::default()
.handle(invocation(
@@ -2188,7 +2195,7 @@ async fn multi_agent_v2_spawn_surfaces_task_name_validation_errors() {
.features
.enable(Feature::MultiAgentV2)
.expect("test config should allow feature update");
turn.config = Arc::new(config);
set_turn_config(&mut turn, config);
let invocation = invocation(
Arc::new(session),
@@ -2398,7 +2405,7 @@ async fn multi_agent_v2_spawn_agent_ignores_configured_max_depth() {
.expect("root thread should start");
session.services.agent_control = manager.agent_control();
session.conversation_id = root.thread_id;
turn.config = Arc::new(config);
set_turn_config(&mut turn, config);
let parent_path = AgentPath::try_from("/root/parent").expect("agent path");
turn.session_source = SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id: root.thread_id,
@@ -2871,7 +2878,7 @@ async fn multi_agent_v2_wait_agent_accepts_timeout_only_argument() {
.features
.enable(Feature::MultiAgentV2)
.expect("test config should allow feature update");
turn.config = Arc::new(config);
set_turn_config(&mut turn, config);
let session = Arc::new(session);
let turn = Arc::new(turn);
@@ -2956,7 +2963,7 @@ async fn multi_agent_v2_wait_agent_rejects_timeout_below_configured_min() {
config.multi_agent_v2.min_wait_timeout_ms = 50;
config.multi_agent_v2.max_wait_timeout_ms = 1_000;
config.multi_agent_v2.default_wait_timeout_ms = 50;
turn.config = Arc::new(config);
set_turn_config(&mut turn, config);
let Err(err) = WaitAgentHandlerV2::default()
.handle(invocation(
@@ -2986,7 +2993,7 @@ async fn multi_agent_v2_wait_agent_accepts_explicit_timeout_at_configured_min()
config.multi_agent_v2.min_wait_timeout_ms = 1;
config.multi_agent_v2.max_wait_timeout_ms = 1_000;
config.multi_agent_v2.default_wait_timeout_ms = 50;
turn.config = Arc::new(config);
set_turn_config(&mut turn, config);
let output = WaitAgentHandlerV2::default()
.handle(invocation(
@@ -3021,7 +3028,7 @@ async fn multi_agent_v2_wait_agent_uses_configured_default_timeout() {
config.multi_agent_v2.min_wait_timeout_ms = 1;
config.multi_agent_v2.max_wait_timeout_ms = 1_000;
config.multi_agent_v2.default_wait_timeout_ms = 50;
turn.config = Arc::new(config);
set_turn_config(&mut turn, config);
let session = Arc::new(session);
let turn = Arc::new(turn);
@@ -3076,7 +3083,7 @@ async fn multi_agent_v2_wait_agent_allows_zero_configured_timeout() {
config.multi_agent_v2.min_wait_timeout_ms = 0;
config.multi_agent_v2.max_wait_timeout_ms = 0;
config.multi_agent_v2.default_wait_timeout_ms = 0;
turn.config = Arc::new(config);
set_turn_config(&mut turn, config);
let session = Arc::new(session);
let turn = Arc::new(turn);
@@ -3116,7 +3123,7 @@ async fn multi_agent_v2_wait_agent_rejects_timeout_above_configured_max() {
config.multi_agent_v2.min_wait_timeout_ms = 1;
config.multi_agent_v2.max_wait_timeout_ms = 50;
config.multi_agent_v2.default_wait_timeout_ms = 1;
turn.config = Arc::new(config);
set_turn_config(&mut turn, config);
let Err(err) = WaitAgentHandlerV2::default()
.handle(invocation(
@@ -3146,7 +3153,7 @@ async fn multi_agent_v2_wait_agent_accepts_explicit_timeout_at_configured_max()
config.multi_agent_v2.min_wait_timeout_ms = 1;
config.multi_agent_v2.max_wait_timeout_ms = 1;
config.multi_agent_v2.default_wait_timeout_ms = 1;
turn.config = Arc::new(config);
set_turn_config(&mut turn, config);
let output = WaitAgentHandlerV2::default()
.handle(invocation(
@@ -3354,7 +3361,7 @@ async fn multi_agent_v2_wait_agent_returns_summary_for_mailbox_activity() {
.features
.enable(Feature::MultiAgentV2)
.expect("test config should allow feature update");
turn.config = Arc::new(config);
set_turn_config(&mut turn, config);
let session = Arc::new(session);
let turn = Arc::new(turn);
@@ -3448,7 +3455,7 @@ async fn multi_agent_v2_wait_agent_returns_for_already_queued_mail() {
.features
.enable(Feature::MultiAgentV2)
.expect("test config should allow feature update");
turn.config = Arc::new(config);
set_turn_config(&mut turn, config);
let session = Arc::new(session);
let turn = Arc::new(turn);
@@ -3529,7 +3536,7 @@ async fn multi_agent_v2_wait_agent_wakes_on_any_mailbox_notification() {
.features
.enable(Feature::MultiAgentV2)
.expect("test config should allow feature update");
turn.config = Arc::new(config);
set_turn_config(&mut turn, config);
let session = Arc::new(session);
let turn = Arc::new(turn);
@@ -3620,7 +3627,7 @@ async fn multi_agent_v2_wait_agent_does_not_return_completed_content() {
.features
.enable(Feature::MultiAgentV2)
.expect("test config should allow feature update");
turn.config = Arc::new(config);
set_turn_config(&mut turn, config);
let session = Arc::new(session);
let turn = Arc::new(turn);
@@ -3709,7 +3716,7 @@ async fn multi_agent_v2_close_agent_accepts_task_name_target() {
.features
.enable(Feature::MultiAgentV2)
.expect("test config should allow feature update");
turn.config = Arc::new(config);
set_turn_config(&mut turn, config);
let session = Arc::new(session);
let turn = Arc::new(turn);
@@ -3757,7 +3764,7 @@ async fn multi_agent_v2_close_agent_accepts_task_name_target() {
async fn multi_agent_v2_close_agent_reaps_stale_task_name_target() {
let (mut session, mut turn) = make_session_and_context().await;
let mut config = (*turn.config).clone();
config.agent_max_threads = Some(1);
config.multi_agent_v2.max_concurrent_threads_per_session = 2;
config
.features
.enable(Feature::MultiAgentV2)
@@ -3782,7 +3789,7 @@ async fn multi_agent_v2_close_agent_reaps_stale_task_name_target() {
.expect("root thread should start");
session.services.agent_control = manager.agent_control();
session.conversation_id = root.thread_id;
turn.config = Arc::new(config.clone());
set_turn_config(&mut turn, config.clone());
let session = Arc::new(session);
let turn = Arc::new(turn);
@@ -3888,7 +3895,7 @@ async fn multi_agent_v2_close_agent_rejects_root_target_and_id() {
.features
.enable(Feature::MultiAgentV2)
.expect("test config should allow feature update");
turn.config = Arc::new(config);
set_turn_config(&mut turn, config);
let session = Arc::new(session);
let turn = Arc::new(turn);
@@ -4283,7 +4290,7 @@ async fn build_agent_resume_config_clears_base_instructions() {
.set(AskForApproval::OnRequest)
.expect("approval policy set");
let config = build_agent_resume_config(&turn, /*child_depth*/ 0).expect("resume config");
let config = build_agent_resume_config(&turn).expect("resume config");
let mut expected = (*turn.config).clone();
expected.base_instructions = None;
@@ -108,7 +108,6 @@ async fn handle_spawn_agent(
)
.await?;
apply_spawn_agent_runtime_overrides(&mut config, turn.as_ref())?;
apply_spawn_agent_overrides(&mut config, child_depth);
let spawn_source = thread_spawn_source(
session.conversation_id,
+13 -3
View File
@@ -1,3 +1,5 @@
use crate::agent::exceeds_thread_spawn_depth_limit;
use crate::agent::next_thread_spawn_depth;
use crate::session::turn_context::TurnContext;
use crate::tools::code_mode::execute_spec::create_code_mode_tool;
use crate::tools::context::ToolInvocation;
@@ -61,6 +63,7 @@ use codex_protocol::dynamic_tools::DynamicToolSpec;
use codex_protocol::openai_models::ConfigShellToolType;
use codex_protocol::openai_models::InputModality;
use codex_protocol::openai_models::ToolMode;
use codex_protocol::protocol::MultiAgentVersion;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SubAgentSource;
use codex_tools::DiscoverableTool;
@@ -288,11 +291,18 @@ fn namespace_tools_enabled(turn_context: &TurnContext) -> bool {
}
fn multi_agent_v2_enabled(turn_context: &TurnContext) -> bool {
turn_context.features.get().enabled(Feature::MultiAgentV2)
turn_context.multi_agent_version == MultiAgentVersion::V2
}
fn collab_tools_enabled(turn_context: &TurnContext) -> bool {
multi_agent_v2_enabled(turn_context) || turn_context.features.get().enabled(Feature::Collab)
match turn_context.multi_agent_version {
MultiAgentVersion::Disabled => false,
MultiAgentVersion::V1 => !exceeds_thread_spawn_depth_limit(
next_thread_spawn_depth(&turn_context.session_source),
turn_context.config.agent_max_depth,
),
MultiAgentVersion::V2 => true,
}
}
fn goal_tools_enabled(turn_context: &TurnContext) -> bool {
@@ -304,7 +314,7 @@ fn goal_tools_enabled(turn_context: &TurnContext) -> bool {
}
fn agent_jobs_tools_enabled(turn_context: &TurnContext) -> bool {
turn_context.features.get().enabled(Feature::SpawnCsv)
turn_context.features.get().enabled(Feature::SpawnCsv) && collab_tools_enabled(turn_context)
}
fn agent_jobs_worker_tools_enabled(turn_context: &TurnContext) -> bool {
@@ -215,6 +215,7 @@ fn set_feature(turn: &mut TurnContext, feature: Feature, enabled: bool) {
.disable(feature)
.expect("test feature should be disableable in config");
}
turn.multi_agent_version = config.multi_agent_version_from_features();
turn.config = Arc::new(config);
turn.tool_mode = turn.model_info.tool_mode.unwrap_or_else(|| {
if turn.config.features.enabled(Feature::CodeModeOnly) {