[codex] Add model multi-agent version selector

This commit is contained in:
Ahmed Ibrahim
2026-05-29 01:30:57 -07:00
parent e558c37be5
commit 1f68c3dbdd
25 changed files with 308 additions and 52 deletions
@@ -53,6 +53,7 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo {
used_fallback_model_metadata: false,
supports_search_tool: false,
tool_mode: None,
multi_agent_version: None,
}
}
@@ -99,6 +99,7 @@ async fn models_client_hits_models_endpoint() {
used_fallback_model_metadata: false,
supports_search_tool: false,
tool_mode: None,
multi_agent_version: None,
}],
};
+49 -17
View File
@@ -5,6 +5,7 @@ use crate::agent::role::DEFAULT_ROLE_NAME;
use crate::agent::role::resolve_role_config;
use crate::agent::status::is_final;
use crate::codex_thread::ThreadConfigSnapshot;
use crate::multi_agent_runtime::MultiAgentRuntime;
use crate::session::emit_subagent_session_started;
use crate::session_prefix::format_subagent_context_line;
use crate::session_prefix::format_subagent_notification_message;
@@ -212,13 +213,26 @@ impl AgentControl {
async fn spawn_agent_internal(
&self,
config: crate::config::Config,
mut config: crate::config::Config,
initial_operation: Op,
session_source: Option<SessionSource>,
options: SpawnAgentOptions,
) -> CodexResult<LiveAgent> {
let state = self.upgrade()?;
let mut reservation = self.state.reserve_spawn_slot(config.agent_max_threads)?;
let mut multi_agent_runtime = state.resolve_multi_agent_runtime(&config).await;
if let Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { depth, .. })) =
session_source.as_ref()
&& *depth >= config.agent_max_depth
&& !multi_agent_runtime.multi_agent_v2_enabled()
{
let _ = config.features.disable(Feature::SpawnCsv);
let _ = config.features.disable(Feature::Collab);
multi_agent_runtime.disable_collab_tools();
}
let mut reservation = self
.state
.reserve_spawn_slot(multi_agent_runtime.agent_max_threads(&config))?;
let multi_agent_v2_enabled = multi_agent_runtime.multi_agent_v2_enabled();
let inherited_shell_snapshot = self
.inherited_shell_snapshot_for_source(&state, session_source.as_ref())
.await;
@@ -254,6 +268,7 @@ impl AgentControl {
Box::pin(self.spawn_forked_thread(
&state,
config,
multi_agent_runtime,
session_source,
&options,
inherited_shell_snapshot,
@@ -339,7 +354,7 @@ impl AgentControl {
self.send_input(new_thread.thread_id, initial_operation)
.await?;
if !new_thread.thread.enabled(Feature::MultiAgentV2) {
if !multi_agent_v2_enabled {
let child_reference = agent_metadata
.agent_path
.as_ref()
@@ -364,6 +379,7 @@ impl AgentControl {
&self,
state: &Arc<ThreadManagerState>,
config: crate::config::Config,
multi_agent_runtime: MultiAgentRuntime,
session_source: SessionSource,
options: &SpawnAgentOptions,
inherited_shell_snapshot: Option<Arc<ShellSnapshot>>,
@@ -418,8 +434,14 @@ impl AgentControl {
}
let multi_agent_v2_usage_hint_texts_to_filter: Vec<String> =
if let Some(parent_thread) = parent_thread.as_ref() {
if parent_thread.enabled(Feature::MultiAgentV2) {
let parent_config = parent_thread.codex.session.get_config().await;
let parent_config = parent_thread.codex.session.get_config().await;
if parent_thread
.codex
.session
.multi_agent_runtime()
.await
.multi_agent_v2_enabled()
{
[
parent_config
.multi_agent_v2
@@ -436,7 +458,7 @@ impl AgentControl {
} else {
Vec::new()
}
} else if config.features.enabled(Feature::MultiAgentV2) {
} else if multi_agent_runtime.multi_agent_v2_enabled() {
[
config.multi_agent_v2.root_agent_usage_hint_text.clone(),
config.multi_agent_v2.subagent_usage_hint_text.clone(),
@@ -472,7 +494,7 @@ impl AgentControl {
}
}
if preserve_reference_context_item
&& config.features.enabled(Feature::MultiAgentV2)
&& multi_agent_runtime.multi_agent_v2_enabled()
&& let Some(subagent_usage_hint_text) =
config.multi_agent_v2.subagent_usage_hint_text.clone()
&& let Some(subagent_usage_hint_message) =
@@ -581,16 +603,21 @@ impl AgentControl {
thread_id: ThreadId,
session_source: SessionSource,
) -> CodexResult<ThreadId> {
let state = self.upgrade()?;
let mut multi_agent_runtime = state.resolve_multi_agent_runtime(&config).await;
if let SessionSource::SubAgent(SubAgentSource::ThreadSpawn { depth, .. }) = &session_source
&& *depth >= config.agent_max_depth
&& !config.features.enabled(Feature::MultiAgentV2)
&& !multi_agent_runtime.multi_agent_v2_enabled()
{
let _ = config.features.disable(Feature::SpawnCsv);
let _ = config.features.disable(Feature::Collab);
multi_agent_runtime.disable_collab_tools();
}
let state = self.upgrade()?;
let state_db_ctx = state.state_db();
let mut reservation = self.state.reserve_spawn_slot(config.agent_max_threads)?;
let mut reservation = self
.state
.reserve_spawn_slot(multi_agent_runtime.agent_max_threads(&config))?;
let multi_agent_v2_enabled = multi_agent_runtime.multi_agent_v2_enabled();
let (session_source, agent_metadata) = match session_source {
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id,
@@ -659,7 +686,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 !resumed_thread.thread.enabled(Feature::MultiAgentV2) {
if !multi_agent_v2_enabled {
let child_reference = agent_metadata
.agent_path
.as_ref()
@@ -1044,12 +1071,17 @@ impl AgentControl {
};
let child_thread = state.get_thread(child_thread_id).await.ok();
let message = format_subagent_notification_message(child_reference.as_str(), &status);
if child_agent_path.is_some()
&& child_thread
.as_ref()
.map(|thread| thread.enabled(Feature::MultiAgentV2))
.unwrap_or(true)
{
let child_multi_agent_v2_enabled = if let Some(child_thread) = child_thread.as_ref() {
child_thread
.codex
.session
.multi_agent_runtime()
.await
.multi_agent_v2_enabled()
} else {
true
};
if child_agent_path.is_some() && child_multi_agent_v2_enabled {
let Some(child_agent_path) = child_agent_path.clone() else {
return;
};
+142
View File
@@ -0,0 +1,142 @@
use crate::config::Config;
use crate::config::DEFAULT_AGENT_MAX_THREADS;
use crate::config::ManagedFeatures;
use codex_features::Feature;
use codex_models_manager::manager::RefreshStrategy;
use codex_models_manager::manager::SharedModelsManager;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::MultiAgentVersion;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct MultiAgentRuntime {
version: Option<MultiAgentVersion>,
collab_tools_disabled: bool,
}
impl MultiAgentRuntime {
#[cfg(test)]
pub(crate) fn from_features(features: &ManagedFeatures) -> Self {
Self {
version: multi_agent_version_from_features(features),
collab_tools_disabled: false,
}
}
pub(crate) fn resolve(model_info: &ModelInfo, features: &ManagedFeatures) -> Self {
Self {
version: model_info
.multi_agent_version
.or_else(|| multi_agent_version_from_features(features)),
collab_tools_disabled: false,
}
}
pub(crate) async fn resolve_for_config(
models_manager: &SharedModelsManager,
config: &Config,
) -> Self {
let model = models_manager
.get_default_model(&config.model, RefreshStrategy::Offline)
.await;
let model_info = models_manager
.get_model_info(&model, &config.to_models_manager_config())
.await;
Self::resolve(&model_info, &config.features)
}
pub(crate) fn with_model(self, model_info: &ModelInfo, features: &ManagedFeatures) -> Self {
Self {
collab_tools_disabled: self.collab_tools_disabled,
..Self::resolve(model_info, features)
}
}
pub(crate) fn collab_tools_enabled(self) -> bool {
!self.collab_tools_disabled && self.version.is_some()
}
pub(crate) fn multi_agent_v2_enabled(self) -> bool {
self.version == Some(MultiAgentVersion::V2)
}
pub(crate) fn agent_max_threads(self, config: &Config) -> Option<usize> {
if self.multi_agent_v2_enabled() {
Some(
config
.multi_agent_v2
.max_concurrent_threads_per_session
.saturating_sub(1),
)
} else if config.features.enabled(Feature::MultiAgentV2) {
DEFAULT_AGENT_MAX_THREADS
} else {
config.agent_max_threads
}
}
pub(crate) fn disable_collab_tools(&mut self) {
self.collab_tools_disabled = true;
}
}
fn multi_agent_version_from_features(features: &ManagedFeatures) -> Option<MultiAgentVersion> {
if features.enabled(Feature::MultiAgentV2) {
Some(MultiAgentVersion::V2)
} else if features.enabled(Feature::Collab) {
Some(MultiAgentVersion::V1)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use codex_features::Features;
fn features(enabled: &[Feature]) -> ManagedFeatures {
let mut features = Features::default();
for feature in enabled {
features.enable(*feature);
}
features.into()
}
fn model_info(multi_agent_version: Option<MultiAgentVersion>) -> ModelInfo {
let mut model_info = codex_models_manager::model_info::model_info_from_slug("test-model");
model_info.multi_agent_version = multi_agent_version;
model_info
}
#[test]
fn omitted_selector_follows_feature_flags() {
let runtime = MultiAgentRuntime::resolve(
&model_info(/*multi_agent_version*/ None),
&features(&[Feature::MultiAgentV2]),
);
assert_eq!(
runtime,
MultiAgentRuntime {
version: Some(MultiAgentVersion::V2),
collab_tools_disabled: false,
}
);
}
#[test]
fn explicit_selector_overrides_feature_flags() {
let runtime = MultiAgentRuntime::resolve(
&model_info(Some(MultiAgentVersion::V1)),
&features(&[Feature::MultiAgentV2]),
);
assert_eq!(
runtime,
MultiAgentRuntime {
version: Some(MultiAgentVersion::V1),
collab_tools_disabled: false,
}
);
}
}
+22 -10
View File
@@ -31,6 +31,7 @@ use crate::context::PersonalitySpecInstructions;
use crate::default_skill_metadata_budget;
use crate::environment_selection::ResolvedTurnEnvironments;
use crate::exec_policy::ExecPolicyManager;
use crate::multi_agent_runtime::MultiAgentRuntime;
use crate::parse_turn_item;
use crate::path_utils::normalize_for_native_workdir;
use crate::realtime_conversation::RealtimeConversationManager;
@@ -485,14 +486,6 @@ 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)
@@ -518,7 +511,7 @@ impl Codex {
)
};
let config = Arc::new(config);
let mut config = Arc::new(config);
let refresh_strategy = if session_source.is_non_root_agent() {
codex_models_manager::manager::RefreshStrategy::Offline
} else {
@@ -543,6 +536,19 @@ impl Codex {
let model_info = models_manager
.get_model_info(model.as_str(), &config.to_models_manager_config())
.await;
let mut multi_agent_runtime = MultiAgentRuntime::resolve(&model_info, &config.features);
{
let config = Arc::make_mut(&mut config);
if let SessionSource::SubAgent(SubAgentSource::ThreadSpawn { depth, .. }) =
session_source
&& depth >= config.agent_max_depth
&& !multi_agent_runtime.multi_agent_v2_enabled()
{
let _ = config.features.disable(Feature::SpawnCsv);
let _ = config.features.disable(Feature::Collab);
multi_agent_runtime.disable_collab_tools();
}
}
let base_instructions = config
.base_instructions
.clone()
@@ -573,6 +579,7 @@ impl Codex {
let session_configuration = SessionConfiguration {
provider: config.model_provider.clone(),
collaboration_mode,
multi_agent_runtime,
model_reasoning_summary: config.model_reasoning_summary,
service_tier,
developer_instructions: config.developer_instructions.clone(),
@@ -1450,6 +1457,11 @@ impl Session {
.clone()
}
pub(crate) async fn multi_agent_runtime(&self) -> MultiAgentRuntime {
let state = self.state.lock().await;
state.session_configuration.multi_agent_runtime
}
pub(crate) async fn provider(&self) -> ModelProviderInfo {
let state = self.state.lock().await;
state.session_configuration.provider.clone()
@@ -1650,7 +1662,7 @@ impl Session {
turn_context: &TurnContext,
msg: &EventMsg,
) {
if !self.enabled(Feature::MultiAgentV2) {
if !turn_context.multi_agent_v2_enabled() {
return;
}
+1 -2
View File
@@ -1,5 +1,4 @@
use crate::session::turn_context::TurnContext;
use codex_features::Feature;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SubAgentSource;
@@ -7,7 +6,7 @@ pub(super) fn usage_hint_text<'a>(
turn_context: &'a TurnContext,
session_source: &SessionSource,
) -> Option<&'a str> {
if !turn_context.features.enabled(Feature::MultiAgentV2) {
if !turn_context.multi_agent_v2_enabled() {
return None;
}
+2
View File
@@ -2,6 +2,7 @@ use super::input_queue::InputQueue;
use super::*;
use crate::config::ConstraintError;
use crate::goals::GoalRuntimeState;
use crate::multi_agent_runtime::MultiAgentRuntime;
use crate::skills::SkillError;
use crate::state::ActiveTurn;
use codex_protocol::SessionId;
@@ -45,6 +46,7 @@ pub(crate) struct SessionConfiguration {
pub(super) provider: ModelProviderInfo,
pub(super) collaboration_mode: CollaborationMode,
pub(super) multi_agent_runtime: MultiAgentRuntime,
pub(super) model_reasoning_summary: Option<ReasoningSummaryConfig>,
pub(super) service_tier: Option<String>,
+9
View File
@@ -61,6 +61,7 @@ use crate::goals::ExternalGoalPreviousStatus;
use crate::goals::ExternalGoalSet;
use crate::goals::GoalRuntimeEvent;
use crate::goals::SetGoalRequest;
use crate::multi_agent_runtime::MultiAgentRuntime;
use crate::rollout::recorder::RolloutRecorder;
use crate::state::ActiveTurn;
use crate::state::TaskKind;
@@ -3071,6 +3072,7 @@ async fn set_rate_limits_retains_previous_credits() {
let session_configuration = SessionConfiguration {
provider: config.model_provider.clone(),
collaboration_mode,
multi_agent_runtime: MultiAgentRuntime::from_features(&config.features),
model_reasoning_summary: config.model_reasoning_summary,
developer_instructions: config.developer_instructions.clone(),
user_instructions: config.user_instructions.clone(),
@@ -3176,6 +3178,7 @@ async fn set_rate_limits_updates_plan_type_when_present() {
let session_configuration = SessionConfiguration {
provider: config.model_provider.clone(),
collaboration_mode,
multi_agent_runtime: MultiAgentRuntime::from_features(&config.features),
model_reasoning_summary: config.model_reasoning_summary,
developer_instructions: config.developer_instructions.clone(),
user_instructions: config.user_instructions.clone(),
@@ -3704,6 +3707,7 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati
SessionConfiguration {
provider: config.model_provider.clone(),
collaboration_mode,
multi_agent_runtime: MultiAgentRuntime::from_features(&config.features),
model_reasoning_summary: config.model_reasoning_summary,
developer_instructions: config.developer_instructions.clone(),
user_instructions: config.user_instructions.clone(),
@@ -4448,6 +4452,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_packaged_zsh() {
let session_configuration = SessionConfiguration {
provider: config.model_provider.clone(),
collaboration_mode,
multi_agent_runtime: MultiAgentRuntime::from_features(&config.features),
model_reasoning_summary: config.model_reasoning_summary,
developer_instructions: config.developer_instructions.clone(),
user_instructions: config.user_instructions.clone(),
@@ -4558,6 +4563,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
let session_configuration = SessionConfiguration {
provider: config.model_provider.clone(),
collaboration_mode,
multi_agent_runtime: MultiAgentRuntime::from_features(&config.features),
model_reasoning_summary: config.model_reasoning_summary,
developer_instructions: config.developer_instructions.clone(),
user_instructions: config.user_instructions.clone(),
@@ -4793,6 +4799,7 @@ async fn make_session_with_config_and_rx(
let session_configuration = SessionConfiguration {
provider: config.model_provider.clone(),
collaboration_mode,
multi_agent_runtime: MultiAgentRuntime::from_features(&config.features),
model_reasoning_summary: config.model_reasoning_summary,
developer_instructions: config.developer_instructions.clone(),
user_instructions: config.user_instructions.clone(),
@@ -4897,6 +4904,7 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
let session_configuration = SessionConfiguration {
provider: config.model_provider.clone(),
collaboration_mode,
multi_agent_runtime: MultiAgentRuntime::from_features(&config.features),
model_reasoning_summary: config.model_reasoning_summary,
developer_instructions: config.developer_instructions.clone(),
user_instructions: config.user_instructions.clone(),
@@ -6401,6 +6409,7 @@ where
let session_configuration = SessionConfiguration {
provider: config.model_provider.clone(),
collaboration_mode,
multi_agent_runtime: MultiAgentRuntime::from_features(&config.features),
model_reasoning_summary: config.model_reasoning_summary,
developer_instructions: config.developer_instructions.clone(),
user_instructions: config.user_instructions.clone(),
+8 -5
View File
@@ -27,6 +27,7 @@ use crate::goals::GoalRuntimeEvent;
use crate::hook_runtime::inspect_pending_input;
use crate::hook_runtime::record_additional_contexts;
use crate::hook_runtime::record_pending_input;
use crate::multi_agent_runtime::MultiAgentRuntime;
use crate::session::TurnInput;
use crate::session::session::Session;
use crate::session::turn_context::TurnContext;
@@ -70,11 +71,11 @@ pub(crate) enum InterruptedTurnHistoryMarker {
}
impl InterruptedTurnHistoryMarker {
pub(crate) fn from_config(config: &Config) -> Self {
pub(crate) fn from_config(config: &Config, multi_agent_runtime: MultiAgentRuntime) -> Self {
if !config.agent_interrupt_message_enabled {
return Self::Disabled;
}
if config.features.enabled(Feature::MultiAgentV2) {
if multi_agent_runtime.multi_agent_v2_enabled() {
Self::Developer
} else {
Self::ContextualUser
@@ -840,9 +841,11 @@ impl Session {
.await;
if reason == TurnAbortReason::Interrupted
&& let Some(marker) = interrupted_turn_history_marker(
InterruptedTurnHistoryMarker::from_config(task.turn_context.config.as_ref()),
)
&& let Some(marker) =
interrupted_turn_history_marker(InterruptedTurnHistoryMarker::from_config(
task.turn_context.config.as_ref(),
task.turn_context.multi_agent_runtime,
))
{
self.record_conversation_items(
task.turn_context.as_ref(),
+14 -2
View File
@@ -7,6 +7,7 @@ use crate::config::ThreadStoreConfig;
use crate::environment_selection::default_thread_environment_selections;
use crate::environment_selection::resolve_environment_selections;
use crate::mcp::McpManager;
use crate::multi_agent_runtime::MultiAgentRuntime;
use crate::rollout::truncation;
use crate::session::Codex;
use crate::session::CodexSpawnArgs;
@@ -637,10 +638,11 @@ impl ThreadManager {
))
})?;
let history = stored_thread_to_initial_history(stored_thread, fork_source.rollout_path())?;
let multi_agent_runtime = self.resolve_multi_agent_runtime(&options.config).await;
options.initial_history = fork_history_from_snapshot(
ForkSnapshot::Interrupted,
history,
InterruptedTurnHistoryMarker::from_config(&options.config),
InterruptedTurnHistoryMarker::from_config(&options.config, multi_agent_runtime),
);
self.start_thread_with_options_and_fork_source(options, Some(forked_from_thread_id))
.await
@@ -895,7 +897,9 @@ impl ThreadManager {
InitialHistory::Forked(_) => history.forked_from_id(),
InitialHistory::New | InitialHistory::Cleared => None,
};
let interrupted_marker = InterruptedTurnHistoryMarker::from_config(&config);
let multi_agent_runtime = self.resolve_multi_agent_runtime(&config).await;
let interrupted_marker =
InterruptedTurnHistoryMarker::from_config(&config, multi_agent_runtime);
let history = fork_history_from_snapshot(snapshot, history, interrupted_marker);
let environments = default_thread_environment_selections(
self.state.environment_manager.as_ref(),
@@ -922,6 +926,10 @@ impl ThreadManager {
AgentControl::new(Arc::downgrade(&self.state))
}
async fn resolve_multi_agent_runtime(&self, config: &Config) -> MultiAgentRuntime {
self.state.resolve_multi_agent_runtime(config).await
}
#[cfg(test)]
pub(crate) fn captured_ops(&self) -> Vec<(ThreadId, Op)> {
self.state
@@ -933,6 +941,10 @@ impl ThreadManager {
}
impl ThreadManagerState {
pub(crate) async fn resolve_multi_agent_runtime(&self, config: &Config) -> MultiAgentRuntime {
MultiAgentRuntime::resolve_for_config(&self.models_manager, config).await
}
pub(crate) fn state_db(&self) -> Option<StateDbHandle> {
self.state_db.clone()
}
@@ -119,13 +119,13 @@ async fn build_runner_options(
"agent depth limit reached; this session cannot spawn more subagents".to_string(),
));
}
if turn.config.agent_max_threads == Some(0) {
let agent_max_threads = turn.multi_agent_runtime.agent_max_threads(&turn.config);
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 {
@@ -109,7 +109,7 @@ async fn handle_spawn_agent(
)
.await?;
apply_spawn_agent_runtime_overrides(&mut config, turn.as_ref())?;
apply_spawn_agent_overrides(&mut config, child_depth);
apply_spawn_agent_overrides(&mut config, turn.multi_agent_runtime, child_depth);
let result = Box::pin(session.services.agent_control.spawn_agent_with_metadata(
config,
@@ -3,6 +3,7 @@ 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::multi_agent_runtime::MultiAgentRuntime;
use crate::session::session::Session;
use crate::session::turn_context::TurnContext;
use crate::tools::context::FunctionToolOutput;
@@ -216,7 +217,7 @@ pub(crate) fn build_agent_resume_config(
child_depth: i32,
) -> Result<Config, FunctionCallError> {
let mut config = build_agent_shared_config(turn)?;
apply_spawn_agent_overrides(&mut config, child_depth);
apply_spawn_agent_overrides(&mut config, turn.multi_agent_runtime, child_depth);
// For resume, keep base instructions sourced from rollout/session metadata.
config.base_instructions = None;
Ok(config)
@@ -280,8 +281,12 @@ 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) {
pub(crate) fn apply_spawn_agent_overrides(
config: &mut Config,
multi_agent_runtime: MultiAgentRuntime,
child_depth: i32,
) {
if child_depth >= config.agent_max_depth && !multi_agent_runtime.multi_agent_v2_enabled() {
let _ = config.features.disable(Feature::SpawnCsv);
let _ = config.features.disable(Feature::Collab);
}
@@ -310,7 +315,6 @@ pub(crate) async fn apply_requested_spawn_agent_model_overrides(
.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(
@@ -364,7 +368,6 @@ pub(crate) async fn apply_spawn_agent_service_tier(
.models_manager
.get_model_info(model.as_str(), &config.to_models_manager_config())
.await;
if let Some(requested_service_tier) = requested_service_tier
&& !model_info.supports_service_tier(requested_service_tier)
{
@@ -108,7 +108,7 @@ async fn handle_spawn_agent(
)
.await?;
apply_spawn_agent_runtime_overrides(&mut config, turn.as_ref())?;
apply_spawn_agent_overrides(&mut config, child_depth);
apply_spawn_agent_overrides(&mut config, turn.multi_agent_runtime, child_depth);
let spawn_source = thread_spawn_source(
session.conversation_id,
+2 -2
View File
@@ -286,11 +286,11 @@ 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_v2_enabled()
}
fn collab_tools_enabled(turn_context: &TurnContext) -> bool {
multi_agent_v2_enabled(turn_context) || turn_context.features.get().enabled(Feature::Collab)
turn_context.collab_tools_enabled()
}
fn goal_tools_enabled(turn_context: &TurnContext) -> bool {
@@ -113,6 +113,7 @@ fn test_model_info(
used_fallback_model_metadata: false,
supports_search_tool: false,
tool_mode: None,
multi_agent_version: None,
priority: 1,
additional_speed_tiers: Vec::new(),
service_tiers: Vec::new(),
@@ -931,6 +932,7 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result<
used_fallback_model_metadata: false,
supports_search_tool: false,
tool_mode: None,
multi_agent_version: None,
priority: 1,
additional_speed_tiers: Vec::new(),
service_tiers: Vec::new(),
@@ -371,5 +371,6 @@ fn test_remote_model(slug: &str, priority: i32) -> ModelInfo {
used_fallback_model_metadata: false,
supports_search_tool: false,
tool_mode: None,
multi_agent_version: None,
}
}
+2
View File
@@ -593,6 +593,7 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow
used_fallback_model_metadata: false,
supports_search_tool: false,
tool_mode: None,
multi_agent_version: None,
};
let _models_mock = mount_models_once(
@@ -704,6 +705,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() -
used_fallback_model_metadata: false,
supports_search_tool: false,
tool_mode: None,
multi_agent_version: None,
};
let _models_mock = mount_models_once(
@@ -478,6 +478,7 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> {
used_fallback_model_metadata: false,
supports_search_tool: false,
tool_mode: None,
multi_agent_version: None,
priority: 1,
additional_speed_tiers: Vec::new(),
service_tiers: Vec::new(),
@@ -728,6 +729,7 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> {
used_fallback_model_metadata: false,
supports_search_tool: false,
tool_mode: None,
multi_agent_version: None,
priority: 1,
additional_speed_tiers: Vec::new(),
service_tiers: Vec::new(),
@@ -1212,6 +1214,7 @@ fn test_remote_model_with_policy(
used_fallback_model_metadata: false,
supports_search_tool: false,
tool_mode: None,
multi_agent_version: None,
priority,
additional_speed_tiers: Vec::new(),
service_tiers: Vec::new(),
+1
View File
@@ -1378,6 +1378,7 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re
used_fallback_model_metadata: false,
supports_search_tool: false,
tool_mode: None,
multi_agent_version: None,
}],
},
)
@@ -61,6 +61,7 @@ fn test_model_info(
used_fallback_model_metadata: false,
supports_search_tool: false,
tool_mode: None,
multi_agent_version: None,
priority: 1,
additional_speed_tiers: Vec::new(),
service_tiers,
+1
View File
@@ -1356,6 +1356,7 @@ async fn view_image_tool_returns_unsupported_message_for_text_only_model() -> an
used_fallback_model_metadata: false,
supports_search_tool: false,
tool_mode: None,
multi_agent_version: None,
priority: 1,
additional_speed_tiers: Vec::new(),
service_tiers: Vec::new(),
@@ -100,6 +100,7 @@ pub fn model_info_from_slug(slug: &str) -> ModelInfo {
used_fallback_model_metadata: true, // this is the fallback model metadata
supports_search_tool: false,
tool_mode: None,
multi_agent_version: None,
}
}
+31 -4
View File
@@ -237,6 +237,13 @@ pub enum ToolMode {
CodeModeOnly,
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, TS, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum MultiAgentVersion {
V1,
V2,
}
fn deserialize_optional_model_selector<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
where
D: Deserializer<'de>,
@@ -345,6 +352,12 @@ pub struct ModelInfo {
deserialize_with = "deserialize_optional_model_selector"
)]
pub tool_mode: Option<ToolMode>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_optional_model_selector"
)]
pub multi_agent_version: Option<MultiAgentVersion>,
}
impl ModelInfo {
@@ -640,6 +653,7 @@ mod tests {
used_fallback_model_metadata: false,
supports_search_tool: false,
tool_mode: None,
multi_agent_version: None,
}
}
@@ -858,10 +872,11 @@ mod tests {
assert_eq!(model.web_search_tool_type, WebSearchToolType::Text);
assert!(!model.supports_search_tool);
assert_eq!(model.tool_mode, None);
assert_eq!(model.multi_agent_version, None);
}
#[test]
fn model_info_deserializes_known_tool_mode() {
fn model_info_deserializes_known_runtime_selectors() {
let mut value =
serde_json::to_value(test_model(/*spec*/ None)).expect("serialize test model");
let object = value
@@ -871,13 +886,20 @@ mod tests {
"tool_mode".to_string(),
serde_json::Value::String("code_mode_only".to_string()),
);
object.insert(
"multi_agent_version".to_string(),
serde_json::Value::String("v2".to_string()),
);
let model = serde_json::from_value::<ModelInfo>(value).expect("deserialize model info");
assert_eq!(model.tool_mode, Some(ToolMode::CodeModeOnly));
assert_eq!(
(model.tool_mode, model.multi_agent_version),
(Some(ToolMode::CodeModeOnly), Some(MultiAgentVersion::V2))
);
}
#[test]
fn model_info_treats_unknown_tool_mode_as_omitted() {
fn model_info_treats_unknown_runtime_selectors_as_omitted() {
let mut value =
serde_json::to_value(test_model(/*spec*/ None)).expect("serialize test model");
let object = value
@@ -887,14 +909,19 @@ mod tests {
"tool_mode".to_string(),
serde_json::Value::String("future_tool_mode".to_string()),
);
object.insert(
"multi_agent_version".to_string(),
serde_json::Value::String("v3".to_string()),
);
let model = serde_json::from_value::<ModelInfo>(value).expect("deserialize model info");
assert_eq!(model.tool_mode, None);
assert_eq!((model.tool_mode, model.multi_agent_version), (None, None));
let serialized = serde_json::to_value(model).expect("serialize model info");
let object = serialized
.as_object()
.expect("model info should be an object");
assert!(!object.contains_key("tool_mode"));
assert!(!object.contains_key("multi_agent_version"));
}
#[test]
+1
View File
@@ -45,6 +45,7 @@ fn model_with_shell_type(shell_type: ConfigShellToolType) -> ModelInfo {
used_fallback_model_metadata: false,
supports_search_tool: false,
tool_mode: None,
multi_agent_version: None,
}
}