mirror of
https://github.com/openai/codex.git
synced 2026-11-06 16:32:29 +00:00
tui: warn on Ultra with high multi-agent concurrency (#31621)
## Why Ultra reasoning may proactively use multiple agents. When `features.multi_agent_v2.max_concurrent_threads_per_session` is configured at 8 or higher, explicitly selecting Ultra can allow up to `N - 1` subagents to work concurrently and increase usage quickly. Showing the configured limits at selection time makes that tradeoff visible and points users to the setting that controls it. ## What changed - Show a warning history cell after the user explicitly selects Ultra reasoning with a concurrent-thread limit of at least 8. - Include the configured concurrent-thread count and maximum subagent count in the warning. - Apply the warning consistently across model and reasoning pickers, Plan-mode scope selection, and reasoning shortcuts. - Keep the trigger limited to the selected reasoning effort and configured thread limit, independent of how multi-agent v2 is activated. - Add focused threshold coverage and an `insta` snapshot for the rendered warning. ## User impact Users with high multi-agent concurrency receive a concrete warning immediately after selecting Ultra. Other reasoning efforts, limits below 8, and startup behavior are unchanged. https://github.com/user-attachments/assets/5795bcb9-432e-42bc-bde3-ee363e9aeb69 ## Test plan - `just test -p codex-tui ultra_reasoning_selection` - Built the full debug CLI with `cargo build -p codex-cli`. - Manually verified the debug TUI with a 10-thread limit: switching from Max to Ultra displayed the warning with 10 concurrent threads and up to 9 subagents.
This commit is contained in:
1 parent
1d14221af2
commit
927004c06d
4 files changed
+141
-1
No files matched your search
@@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
const ULTRA_REASONING_CONCURRENCY_WARNING_THRESHOLD: usize = 8;
|
||||||
|
|
||||||
impl ChatWidget {
|
impl ChatWidget {
|
||||||
/// Open a popup to choose a quick auto model. Selecting "All models"
|
/// Open a popup to choose a quick auto model. Selecting "All models"
|
||||||
/// opens the full picker with every available preset.
|
/// opens the full picker with every available preset.
|
||||||
@@ -102,7 +104,7 @@ impl ChatWidget {
|
|||||||
model.as_str(),
|
model.as_str(),
|
||||||
Some(preset.default_reasoning_effort.clone()),
|
Some(preset.default_reasoning_effort.clone()),
|
||||||
);
|
);
|
||||||
let actions = Self::model_selection_actions(
|
let actions = self.model_selection_actions(
|
||||||
model.clone(),
|
model.clone(),
|
||||||
Some(preset.default_reasoning_effort.clone()),
|
Some(preset.default_reasoning_effort.clone()),
|
||||||
should_prompt_plan_mode_scope,
|
should_prompt_plan_mode_scope,
|
||||||
@@ -214,10 +216,14 @@ impl ChatWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn model_selection_actions(
|
fn model_selection_actions(
|
||||||
|
&self,
|
||||||
model_for_action: String,
|
model_for_action: String,
|
||||||
effort_for_action: Option<ReasoningEffortConfig>,
|
effort_for_action: Option<ReasoningEffortConfig>,
|
||||||
should_prompt_plan_mode_scope: bool,
|
should_prompt_plan_mode_scope: bool,
|
||||||
) -> Vec<SelectionAction> {
|
) -> Vec<SelectionAction> {
|
||||||
|
let warning = effort_for_action
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|effort| self.ultra_reasoning_concurrency_warning(effort));
|
||||||
vec![Box::new(move |tx| {
|
vec![Box::new(move |tx| {
|
||||||
if should_prompt_plan_mode_scope {
|
if should_prompt_plan_mode_scope {
|
||||||
tx.send(AppEvent::OpenPlanReasoningScopePrompt {
|
tx.send(AppEvent::OpenPlanReasoningScopePrompt {
|
||||||
@@ -233,6 +239,11 @@ impl ChatWidget {
|
|||||||
model: model_for_action.clone(),
|
model: model_for_action.clone(),
|
||||||
effort: effort_for_action.clone(),
|
effort: effort_for_action.clone(),
|
||||||
});
|
});
|
||||||
|
if let Some(warning) = warning.clone() {
|
||||||
|
tx.send(AppEvent::InsertHistoryCell(Box::new(
|
||||||
|
history_cell::new_warning_event(warning),
|
||||||
|
)));
|
||||||
|
}
|
||||||
})]
|
})]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,14 +310,23 @@ impl ChatWidget {
|
|||||||
"Set the global default reasoning level and the Plan mode override. This replaces the current {plan_reasoning_source}."
|
"Set the global default reasoning level and the Plan mode override. This replaces the current {plan_reasoning_source}."
|
||||||
);
|
);
|
||||||
let subtitle = format!("Choose where to apply {reasoning_phrase}.");
|
let subtitle = format!("Choose where to apply {reasoning_phrase}.");
|
||||||
|
let warning = effort
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|effort| self.ultra_reasoning_concurrency_warning(effort));
|
||||||
|
|
||||||
let plan_only_actions: Vec<SelectionAction> = vec![Box::new({
|
let plan_only_actions: Vec<SelectionAction> = vec![Box::new({
|
||||||
let model = model.clone();
|
let model = model.clone();
|
||||||
let effort = effort.clone();
|
let effort = effort.clone();
|
||||||
|
let warning = warning.clone();
|
||||||
move |tx| {
|
move |tx| {
|
||||||
tx.send(AppEvent::UpdateModel(model.clone()));
|
tx.send(AppEvent::UpdateModel(model.clone()));
|
||||||
tx.send(AppEvent::UpdatePlanModeReasoningEffort(effort.clone()));
|
tx.send(AppEvent::UpdatePlanModeReasoningEffort(effort.clone()));
|
||||||
tx.send(AppEvent::PersistPlanModeReasoningEffort(effort.clone()));
|
tx.send(AppEvent::PersistPlanModeReasoningEffort(effort.clone()));
|
||||||
|
if let Some(warning) = warning.clone() {
|
||||||
|
tx.send(AppEvent::InsertHistoryCell(Box::new(
|
||||||
|
history_cell::new_warning_event(warning),
|
||||||
|
)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})];
|
})];
|
||||||
let all_modes_actions: Vec<SelectionAction> = vec![Box::new(move |tx| {
|
let all_modes_actions: Vec<SelectionAction> = vec![Box::new(move |tx| {
|
||||||
@@ -318,6 +338,11 @@ impl ChatWidget {
|
|||||||
model: model.clone(),
|
model: model.clone(),
|
||||||
effort: effort.clone(),
|
effort: effort.clone(),
|
||||||
});
|
});
|
||||||
|
if let Some(warning) = warning.clone() {
|
||||||
|
tx.send(AppEvent::InsertHistoryCell(Box::new(
|
||||||
|
history_cell::new_warning_event(warning),
|
||||||
|
)));
|
||||||
|
}
|
||||||
})];
|
})];
|
||||||
|
|
||||||
self.bottom_pane.show_selection_view(SelectionViewParams {
|
self.bottom_pane.show_selection_view(SelectionViewParams {
|
||||||
@@ -427,6 +452,7 @@ impl ChatWidget {
|
|||||||
let mut items: Vec<SelectionItem> = Vec::new();
|
let mut items: Vec<SelectionItem> = Vec::new();
|
||||||
for choice in choices.iter() {
|
for choice in choices.iter() {
|
||||||
let effort = choice.clone();
|
let effort = choice.clone();
|
||||||
|
let warning = self.ultra_reasoning_concurrency_warning(&effort);
|
||||||
let mut effort_label = Self::reasoning_effort_label(&effort);
|
let mut effort_label = Self::reasoning_effort_label(&effort);
|
||||||
if Some(choice) == default_choice.as_ref() {
|
if Some(choice) == default_choice.as_ref() {
|
||||||
effort_label.push_str(" (default)");
|
effort_label.push_str(" (default)");
|
||||||
@@ -469,6 +495,11 @@ impl ChatWidget {
|
|||||||
model: model_for_action.clone(),
|
model: model_for_action.clone(),
|
||||||
effort: choice_effort.clone(),
|
effort: choice_effort.clone(),
|
||||||
});
|
});
|
||||||
|
if let Some(warning) = warning.clone() {
|
||||||
|
tx.send(AppEvent::InsertHistoryCell(Box::new(
|
||||||
|
history_cell::new_warning_event(warning),
|
||||||
|
)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})];
|
})];
|
||||||
|
|
||||||
@@ -518,14 +549,47 @@ impl ChatWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn ultra_reasoning_concurrency_warning(
|
||||||
|
&self,
|
||||||
|
effort: &ReasoningEffortConfig,
|
||||||
|
) -> Option<String> {
|
||||||
|
if effort != &ReasoningEffortConfig::Ultra {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let max_threads = self
|
||||||
|
.config
|
||||||
|
.multi_agent_v2
|
||||||
|
.max_concurrent_threads_per_session;
|
||||||
|
if max_threads < ULTRA_REASONING_CONCURRENCY_WARNING_THRESHOLD {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let max_subagents = max_threads.saturating_sub(1);
|
||||||
|
Some(format!(
|
||||||
|
"Ultra reasoning may proactively use multiple agents. This session is configured for \
|
||||||
|
{max_threads} concurrent threads with up to {max_subagents} subagents which can \
|
||||||
|
increase usage quickly. Consider setting \
|
||||||
|
features.multi_agent_v2.max_concurrent_threads_per_session below 8."
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn apply_model_and_effort_without_persist(
|
pub(super) fn apply_model_and_effort_without_persist(
|
||||||
&self,
|
&self,
|
||||||
model: String,
|
model: String,
|
||||||
effort: Option<ReasoningEffortConfig>,
|
effort: Option<ReasoningEffortConfig>,
|
||||||
) {
|
) {
|
||||||
|
let warning = effort
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|effort| self.ultra_reasoning_concurrency_warning(effort));
|
||||||
self.app_event_tx.send(AppEvent::UpdateModel(model));
|
self.app_event_tx.send(AppEvent::UpdateModel(model));
|
||||||
self.app_event_tx
|
self.app_event_tx
|
||||||
.send(AppEvent::UpdateReasoningEffort(effort));
|
.send(AppEvent::UpdateReasoningEffort(effort));
|
||||||
|
if let Some(warning) = warning {
|
||||||
|
self.app_event_tx.send(AppEvent::InsertHistoryCell(Box::new(
|
||||||
|
history_cell::new_warning_event(warning),
|
||||||
|
)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_model_and_effort(&self, model: String, effort: Option<ReasoningEffortConfig>) {
|
fn apply_model_and_effort(&self, model: String, effort: Option<ReasoningEffortConfig>) {
|
||||||
|
|||||||
@@ -110,8 +110,14 @@ impl ChatWidget {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if self.collaboration_modes_enabled() && self.active_mode_kind() == ModeKind::Plan {
|
if self.collaboration_modes_enabled() && self.active_mode_kind() == ModeKind::Plan {
|
||||||
|
let warning = self.ultra_reasoning_concurrency_warning(&next_effort);
|
||||||
self.app_event_tx
|
self.app_event_tx
|
||||||
.send(AppEvent::UpdatePlanModeReasoningEffort(Some(next_effort)));
|
.send(AppEvent::UpdatePlanModeReasoningEffort(Some(next_effort)));
|
||||||
|
if let Some(warning) = warning {
|
||||||
|
self.app_event_tx.send(AppEvent::InsertHistoryCell(Box::new(
|
||||||
|
crate::history_cell::new_warning_event(warning),
|
||||||
|
)));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
self.apply_model_and_effort_without_persist(current_model, Some(next_effort));
|
self.apply_model_and_effort_without_persist(current_model, Some(next_effort));
|
||||||
}
|
}
|
||||||
|
|||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
source: tui/src/chatwidget/tests/popups_and_settings.rs
|
||||||
|
expression: "&warnings[0]"
|
||||||
|
---
|
||||||
|
⚠ Ultra reasoning may proactively use multiple agents. This session is
|
||||||
|
configured for 8 concurrent threads with up to 7 subagents which can increase
|
||||||
|
usage quickly. Consider setting
|
||||||
|
features.multi_agent_v2.max_concurrent_threads_per_session below 8.
|
||||||
@@ -3236,6 +3236,68 @@ async fn model_reasoning_selection_popup_applies_custom_effort() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn select_ultra_with_multi_agent_thread_limit(max_threads: usize) -> (bool, Vec<String>) {
|
||||||
|
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
|
||||||
|
chat.config
|
||||||
|
.multi_agent_v2
|
||||||
|
.max_concurrent_threads_per_session = max_threads;
|
||||||
|
chat.set_reasoning_effort(Some(ReasoningEffortConfig::High));
|
||||||
|
|
||||||
|
let mut preset = get_available_model(&chat, "gpt-5.4");
|
||||||
|
preset.default_reasoning_effort = ReasoningEffortConfig::High;
|
||||||
|
preset.supported_reasoning_efforts = vec![
|
||||||
|
ReasoningEffortPreset {
|
||||||
|
effort: ReasoningEffortConfig::High,
|
||||||
|
description: "High reasoning".to_string(),
|
||||||
|
},
|
||||||
|
ReasoningEffortPreset {
|
||||||
|
effort: ReasoningEffortConfig::Ultra,
|
||||||
|
description: "Ultra reasoning".to_string(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
chat.open_reasoning_popup(preset);
|
||||||
|
while rx.try_recv().is_ok() {}
|
||||||
|
|
||||||
|
chat.handle_key_event(KeyEvent::from(KeyCode::Down));
|
||||||
|
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||||
|
|
||||||
|
let mut selected_ultra = false;
|
||||||
|
let mut warnings = Vec::new();
|
||||||
|
while let Ok(event) = rx.try_recv() {
|
||||||
|
match event {
|
||||||
|
AppEvent::UpdateReasoningEffort(Some(ReasoningEffortConfig::Ultra)) => {
|
||||||
|
selected_ultra = true;
|
||||||
|
}
|
||||||
|
AppEvent::InsertHistoryCell(cell) => {
|
||||||
|
warnings.push(lines_to_single_string(&cell.display_lines(/*width*/ 80)));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(selected_ultra, warnings)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn ultra_reasoning_selection_warns_for_high_multi_agent_concurrency() {
|
||||||
|
let (selected_ultra, warnings) =
|
||||||
|
select_ultra_with_multi_agent_thread_limit(/*max_threads*/ 8).await;
|
||||||
|
|
||||||
|
assert!(selected_ultra);
|
||||||
|
assert_eq!(warnings.len(), 1);
|
||||||
|
assert_chatwidget_snapshot!(
|
||||||
|
"ultra_reasoning_selection_high_multi_agent_concurrency_warning",
|
||||||
|
&warnings[0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn ultra_reasoning_selection_skips_warning_below_threshold() {
|
||||||
|
let below_threshold = select_ultra_with_multi_agent_thread_limit(/*max_threads*/ 7).await;
|
||||||
|
|
||||||
|
assert_eq!(below_threshold, (true, Vec::new()));
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn model_reasoning_selection_popup_extra_high_warning_snapshot() {
|
async fn model_reasoning_selection_popup_extra_high_warning_snapshot() {
|
||||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.2")).await;
|
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.2")).await;
|
||||||
|
|||||||
Reference in new issue
Block a user