mirror of
https://github.com/openai/codex.git
synced 2026-11-05 20:10:33 +00:00
[codex] Add model multi-agent version selector
This commit is contained in:
@@ -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,
|
||||
}],
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,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,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>,
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user