Centralize resolved multi-agent runtime version

This commit is contained in:
Ahmed Ibrahim
2026-05-29 11:22:02 -07:00
parent 7de051e1ec
commit 9abd55660e
10 changed files with 67 additions and 99 deletions
+6 -19
View File
@@ -218,12 +218,6 @@ impl AgentControl {
options: SpawnAgentOptions,
) -> CodexResult<LiveAgent> {
let state = self.upgrade()?;
let multi_agent_v2 = match session_source.as_ref() {
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { agent_path, .. })) => {
agent_path.is_some()
}
_ => config.features.enabled(Feature::MultiAgentV2),
};
let mut reservation = self.state.reserve_spawn_slot(config.agent_max_threads)?;
let inherited_shell_snapshot = self
.inherited_shell_snapshot_for_source(&state, session_source.as_ref())
@@ -345,7 +339,7 @@ impl AgentControl {
self.send_input(new_thread.thread_id, initial_operation)
.await?;
if !multi_agent_v2 {
if agent_metadata.agent_path.is_none() {
let child_reference = agent_metadata
.agent_path
.as_ref()
@@ -422,10 +416,9 @@ impl AgentControl {
forked_rollout_items =
truncate_rollout_to_last_n_fork_turns(&forked_rollout_items, *last_n_turns);
}
let multi_agent_v2 = session_source.get_agent_path().is_some();
let multi_agent_v2_usage_hint_texts_to_filter: Vec<String> =
if let Some(parent_thread) = parent_thread.as_ref() {
if multi_agent_v2 {
if session_source.get_agent_path().is_some() {
let parent_config = parent_thread.codex.session.get_config().await;
[
parent_config
@@ -443,7 +436,7 @@ impl AgentControl {
} else {
Vec::new()
}
} else if multi_agent_v2 {
} else if session_source.get_agent_path().is_some() {
[
config.multi_agent_v2.root_agent_usage_hint_text.clone(),
config.multi_agent_v2.subagent_usage_hint_text.clone(),
@@ -479,7 +472,7 @@ impl AgentControl {
}
}
if preserve_reference_context_item
&& multi_agent_v2
&& session_source.get_agent_path().is_some()
&& let Some(subagent_usage_hint_text) =
config.multi_agent_v2.subagent_usage_hint_text.clone()
&& let Some(subagent_usage_hint_message) =
@@ -595,15 +588,9 @@ impl AgentControl {
thread_id: ThreadId,
session_source: SessionSource,
) -> CodexResult<ThreadId> {
let multi_agent_v2 = match &session_source {
SessionSource::SubAgent(SubAgentSource::ThreadSpawn { agent_path, .. }) => {
agent_path.is_some()
}
_ => config.features.enabled(Feature::MultiAgentV2),
};
if let SessionSource::SubAgent(SubAgentSource::ThreadSpawn { depth, .. }) = &session_source
&& *depth >= config.agent_max_depth
&& !multi_agent_v2
&& !config.features.enabled(Feature::MultiAgentV2)
{
let _ = config.features.disable(Feature::SpawnCsv);
let _ = config.features.disable(Feature::Collab);
@@ -679,7 +666,7 @@ impl AgentControl {
// Resumed threads are re-registered in-memory and need the same listener
// attachment path as freshly spawned threads.
state.notify_thread_created(resumed_thread.thread_id);
if !multi_agent_v2 {
if agent_metadata.agent_path.is_none() {
let child_reference = agent_metadata
.agent_path
.as_ref()
+13 -12
View File
@@ -226,7 +226,6 @@ use self::turn::collect_explicit_app_ids_from_skill_items;
use self::turn::realtime_text_for_event;
use self::turn_context::TurnContext;
use self::turn_context::TurnSkillsContext;
use self::turn_context::resolve_multi_agent_version;
#[cfg(test)]
mod rollout_reconstruction_tests;
@@ -487,6 +486,14 @@ impl Codex {
let (tx_sub, rx_sub) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY);
let (tx_event, rx_event) = async_channel::unbounded();
if let SessionSource::SubAgent(SubAgentSource::ThreadSpawn { depth, .. }) = session_source
&& depth >= config.agent_max_depth
&& !config.features.enabled(Feature::MultiAgentV2)
{
let _ = config.features.disable(Feature::SpawnCsv);
let _ = config.features.disable(Feature::Collab);
}
let primary_environment = environment_selections.primary_environment();
let mut user_instruction_warnings = Vec::new();
let user_instructions = AgentsMdManager::new(&config)
@@ -512,7 +519,7 @@ impl Codex {
)
};
let mut config = Arc::new(config);
let config = Arc::new(config);
let refresh_strategy = if session_source.is_non_root_agent() {
codex_models_manager::manager::RefreshStrategy::Offline
} else {
@@ -537,16 +544,6 @@ impl Codex {
let model_info = models_manager
.get_model_info(model.as_str(), &config.to_models_manager_config())
.await;
let multi_agent_version =
resolve_multi_agent_version(&model_info, &config, &session_source);
if let SessionSource::SubAgent(SubAgentSource::ThreadSpawn { depth, .. }) = &session_source
&& *depth >= config.agent_max_depth
&& multi_agent_version != Some(MultiAgentVersion::V2)
{
let config = Arc::make_mut(&mut config);
let _ = config.features.disable(Feature::SpawnCsv);
let _ = config.features.disable(Feature::Collab);
}
let base_instructions = config
.base_instructions
.clone()
@@ -1654,6 +1651,10 @@ impl Session {
turn_context: &TurnContext,
msg: &EventMsg,
) {
if turn_context.multi_agent_version != Some(MultiAgentVersion::V2) {
return;
}
if !matches!(msg, EventMsg::TurnComplete(_) | EventMsg::TurnAborted(_)) {
return;
}
+1 -6
View File
@@ -57,11 +57,6 @@ pub(super) async fn spawn_review_thread(
ToolMode::Direct
}
});
let multi_agent_version = resolve_multi_agent_version(
&model_info,
&per_turn_config,
&parent_turn_context.session_source,
);
if let Err(err) = per_turn_config.web_search_mode.set(review_web_search_mode) {
let fallback_value = per_turn_config.web_search_mode.value();
tracing::warn!(
@@ -112,7 +107,7 @@ pub(super) async fn spawn_review_thread(
auth_manager: auth_manager_for_context,
model_info: model_info.clone(),
tool_mode,
multi_agent_version,
multi_agent_version: None,
session_telemetry: session_telemetry_for_context,
provider: provider_for_context,
reasoning_effort,
+15 -1
View File
@@ -2,6 +2,7 @@ use super::*;
use crate::SkillLoadOutcome;
use crate::config::GhostSnapshotConfig;
use crate::environment_selection::ResolvedTurnEnvironments;
use crate::guardian::is_guardian_reviewer_source;
use codex_model_provider::SharedModelProvider;
use codex_model_provider::create_model_provider;
use codex_protocol::SessionId;
@@ -110,14 +111,27 @@ pub(crate) fn resolve_multi_agent_version(
config: &Config,
session_source: &SessionSource,
) -> Option<MultiAgentVersion> {
if is_guardian_reviewer_source(session_source)
|| matches!(
session_source,
SessionSource::SubAgent(SubAgentSource::Review)
)
{
return None;
}
match session_source {
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
agent_path: Some(_),
..
}) => Some(MultiAgentVersion::V2),
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
depth,
agent_path: None,
..
}) if *depth < config.agent_max_depth => Some(MultiAgentVersion::V1),
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
agent_path: None, ..
}) => Some(MultiAgentVersion::V1),
}) => None,
_ => model_info.multi_agent_version.or_else(|| {
if config.features.enabled(Feature::MultiAgentV2) {
Some(MultiAgentVersion::V2)
+11 -16
View File
@@ -71,20 +71,11 @@ pub(crate) enum InterruptedTurnHistoryMarker {
}
impl InterruptedTurnHistoryMarker {
pub(crate) fn from_config(
config: &Config,
multi_agent_version: Option<MultiAgentVersion>,
) -> Self {
pub(crate) fn from_config(config: &Config) -> Self {
if !config.agent_interrupt_message_enabled {
return Self::Disabled;
}
if multi_agent_version.or_else(|| {
config
.features
.enabled(Feature::MultiAgentV2)
.then_some(MultiAgentVersion::V2)
}) == Some(MultiAgentVersion::V2)
{
if config.features.enabled(Feature::MultiAgentV2) {
Self::Developer
} else {
Self::ContextualUser
@@ -850,11 +841,15 @@ impl Session {
.await;
if reason == TurnAbortReason::Interrupted
&& let Some(marker) =
interrupted_turn_history_marker(InterruptedTurnHistoryMarker::from_config(
task.turn_context.config.as_ref(),
task.turn_context.multi_agent_version,
))
&& let Some(marker) = interrupted_turn_history_marker(
if !task.turn_context.config.agent_interrupt_message_enabled {
InterruptedTurnHistoryMarker::Disabled
} else if task.turn_context.multi_agent_version == Some(MultiAgentVersion::V2) {
InterruptedTurnHistoryMarker::Developer
} else {
InterruptedTurnHistoryMarker::ContextualUser
},
)
{
self.record_conversation_items(
task.turn_context.as_ref(),
+2 -6
View File
@@ -640,10 +640,7 @@ impl ThreadManager {
options.initial_history = fork_history_from_snapshot(
ForkSnapshot::Interrupted,
history,
InterruptedTurnHistoryMarker::from_config(
&options.config,
/*multi_agent_version*/ None,
),
InterruptedTurnHistoryMarker::from_config(&options.config),
);
self.start_thread_with_options_and_fork_source(options, Some(forked_from_thread_id))
.await
@@ -898,8 +895,7 @@ impl ThreadManager {
InitialHistory::Forked(_) => history.forked_from_id(),
InitialHistory::New | InitialHistory::Cleared => None,
};
let interrupted_marker =
InterruptedTurnHistoryMarker::from_config(&config, /*multi_agent_version*/ None);
let interrupted_marker = InterruptedTurnHistoryMarker::from_config(&config);
let history = fork_history_from_snapshot(snapshot, history, interrupted_marker);
let environments = default_thread_environment_selections(
self.state.environment_manager.as_ref(),
-18
View File
@@ -14,7 +14,6 @@ use codex_protocol::models::ContentItem;
use codex_protocol::models::ReasoningItemReasoningSummary;
use codex_protocol::models::ResponseItem;
use codex_protocol::openai_models::ModelsResponse;
use codex_protocol::openai_models::MultiAgentVersion;
use codex_protocol::protocol::AgentMessageEvent;
use codex_protocol::protocol::InitialHistory;
use codex_protocol::protocol::InternalSessionSource;
@@ -65,23 +64,6 @@ fn developer_interrupted_marker() -> ResponseItem {
.expect("developer interrupted marker should be enabled")
}
#[tokio::test]
async fn interrupted_turn_history_marker_uses_resolved_multi_agent_version() {
let mut config = test_config().await;
let _ = config.features.enable(Feature::MultiAgentV2);
assert_eq!(
(
InterruptedTurnHistoryMarker::from_config(&config, Some(MultiAgentVersion::V1),),
InterruptedTurnHistoryMarker::from_config(&config, Some(MultiAgentVersion::V2),),
),
(
InterruptedTurnHistoryMarker::ContextualUser,
InterruptedTurnHistoryMarker::Developer,
)
);
}
#[test]
fn truncates_before_requested_user_message() {
let items = [
@@ -119,13 +119,13 @@ async fn build_runner_options(
"agent depth limit reached; this session cannot spawn more subagents".to_string(),
));
}
let max_threads = turn.config.agent_max_threads;
if max_threads == Some(0) {
if turn.config.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, max_threads);
let max_concurrency =
normalize_concurrency(requested_concurrency, turn.config.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 {
-15
View File
@@ -1,4 +1,3 @@
use crate::guardian::is_guardian_reviewer_source;
use crate::session::turn_context::TurnContext;
use crate::tools::code_mode::execute_spec::create_code_mode_tool;
use crate::tools::context::ToolInvocation;
@@ -640,20 +639,6 @@ fn add_core_utility_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut
fn add_collaboration_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut PlannedTools) {
let turn_context = context.turn_context;
if is_guardian_reviewer_source(&turn_context.session_source)
|| matches!(
&turn_context.session_source,
SessionSource::SubAgent(SubAgentSource::Review)
)
|| matches!(
&turn_context.session_source,
SessionSource::SubAgent(SubAgentSource::ThreadSpawn { depth, .. })
if turn_context.multi_agent_version == Some(MultiAgentVersion::V1)
&& *depth >= turn_context.config.agent_max_depth
)
{
return;
}
if turn_context.multi_agent_version.is_some() {
if turn_context.multi_agent_version == Some(MultiAgentVersion::V2) {
let exposure = if turn_context.config.multi_agent_v2.non_code_mode_only {
+16 -3
View File
@@ -35,6 +35,7 @@ use serde_json::json;
use crate::guardian::GUARDIAN_REVIEWER_NAME;
use crate::session::tests::make_session_and_context;
use crate::session::turn_context::TurnContext;
use crate::session::turn_context::resolve_multi_agent_version;
use crate::tools::handlers::multi_agents_spec::MULTI_AGENT_V1_NAMESPACE;
use crate::tools::router::ToolRouter;
use crate::tools::router::ToolRouterParams;
@@ -882,8 +883,12 @@ async fn multi_agent_version_selector_overrides_feature_flags() {
]);
let review = probe(|turn| {
turn.multi_agent_version = Some(MultiAgentVersion::V2);
turn.session_source = SessionSource::SubAgent(SubAgentSource::Review);
turn.multi_agent_version = resolve_multi_agent_version(
&turn.model_info,
turn.config.as_ref(),
&turn.session_source,
);
})
.await;
review.assert_visible_lacks(&[
@@ -897,9 +902,13 @@ async fn multi_agent_version_selector_overrides_feature_flags() {
]);
let guardian_review = probe(|turn| {
turn.multi_agent_version = Some(MultiAgentVersion::V2);
turn.session_source =
SessionSource::SubAgent(SubAgentSource::Other(GUARDIAN_REVIEWER_NAME.to_string()));
turn.multi_agent_version = resolve_multi_agent_version(
&turn.model_info,
turn.config.as_ref(),
&turn.session_source,
);
})
.await;
guardian_review.assert_visible_lacks(&[
@@ -913,7 +922,6 @@ async fn multi_agent_version_selector_overrides_feature_flags() {
]);
let max_depth_v1 = probe(|turn| {
turn.multi_agent_version = Some(MultiAgentVersion::V1);
turn.session_source = SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id: ThreadId::new(),
depth: turn.config.agent_max_depth,
@@ -921,6 +929,11 @@ async fn multi_agent_version_selector_overrides_feature_flags() {
agent_nickname: None,
agent_role: None,
});
turn.multi_agent_version = resolve_multi_agent_version(
&turn.model_info,
turn.config.as_ref(),
&turn.session_source,
);
})
.await;
max_depth_v1.assert_visible_lacks(&[MULTI_AGENT_V1_NAMESPACE]);