Files
codex/codex-rs/core/src/session/review.rs
T
Abhinav 8228e9b867 Resolve token budgets from each step's active model (#41162)
## Why

Model settings can change between steps in the same turn. Token-budget defaults,
context-window limits, and model-visible guidance need to stay aligned with the
model captured for each step.

## What changed

- Preserve the turn's original token-budget preferences and resolve model-owned
  defaults when each `StepContext` is created.
- Use the step's model to determine context-window availability and token-budget
  guidance, while keeping explicit user settings unchanged.
- Emit one-time replacement or removal notices when context-window guidance
  changes, including compatibility with previously persisted world-state
  snapshots.
- Add `ModelInfo::usable_context_window()` to distinguish reserved-headroom
  capacity from the resolved context window and auto-compaction limit.

## Testing

- Cover mid-turn model switches with model defaults, explicit settings, config
  reloads, missing context windows, and missing destination guidance.
- Cover unchanged, replaced, removed, blank, and legacy guidance transitions.

GitOrigin-RevId: 91100a9f3625bef8a79ecd158eb028051c309eee
2026-08-27 15:01:22 +00:00

211 lines
9.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use super::step_settings::ResolvedStepSettings;
use super::*;
use arc_swap::ArcSwap;
use std::sync::atomic::AtomicBool;
/// Spawn a review thread using the given prompt.
pub(super) async fn spawn_review_thread(
sess: Arc<Session>,
config: Arc<Config>,
parent_turn_context: Arc<TurnContext>,
sub_id: String,
resolved: crate::review_prompts::ResolvedReviewRequest,
) {
let model = config
.review_model
.clone()
.unwrap_or_else(|| parent_turn_context.model_info().slug.clone());
let available_models = sess
.services
.models_manager
.list_models(
RefreshStrategy::OnlineIfUncached,
config.http_client_factory(),
)
.await;
let review_model_info = sess
.services
.models_manager
.get_model_info(&model, &config.to_models_manager_config())
.await;
// For reviews, disable web_search and view_image regardless of global settings.
let mut review_features = sess.features.clone();
let _ = review_features.disable(Feature::WebSearchRequest);
let _ = review_features.disable(Feature::WebSearchCached);
let _ = review_features.disable(Feature::Goals);
let review_web_search_mode = WebSearchMode::Disabled;
let unified_exec_shell_mode = UnifiedExecShellMode::for_session(
review_features.get(),
crate::tools::tool_user_shell_type(sess.services.user_shell.as_ref()),
sess.services.shell_zsh_path.as_ref(),
sess.services.main_execve_wrapper_exe.as_ref(),
);
let review_prompt = resolved.prompt.clone();
let provider = parent_turn_context.provider.clone();
let auth_manager = parent_turn_context.auth_manager.clone();
let model_info = review_model_info.clone();
let mut selected = parent_turn_context.initial_settings.selected().clone();
let mut reasoning_effort = selected.collaboration_mode.reasoning_effort();
// Build per‑turn client with the requested model/family.
let mut per_turn_config = (*parent_turn_context.config).clone();
// Preserve configured overrides without carrying over the parent model's defaults.
per_turn_config.token_budget = config.token_budget.clone();
per_turn_config.features = review_features.clone();
if let Some(current_effort) = reasoning_effort.as_ref()
&& review_model_info.slug != parent_turn_context.model_info().slug
&& !review_model_info.used_fallback_model_metadata
&& !review_model_info
.supported_reasoning_levels
.iter()
.any(|preset| &preset.effort == current_effort)
{
let supported_reasoning_levels = &review_model_info.supported_reasoning_levels;
reasoning_effort = supported_reasoning_levels
.get(supported_reasoning_levels.len().saturating_sub(1) / 2)
.map(|preset| preset.effort.clone())
.or_else(|| review_model_info.default_reasoning_level.clone());
}
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!(
error = %err,
?review_web_search_mode,
?fallback_value,
"review web_search_mode is disallowed by requirements; keeping constrained value"
);
}
let auth_manager_for_context = auth_manager.clone();
let provider_for_context = provider.clone();
let session_source = parent_turn_context.session_source.clone();
let (forked_from_thread_id, thread_source, service_tier) = {
let state = sess.state.lock().await;
(
state.session_configuration.forked_from_thread_id,
state.session_configuration.thread_source.clone(),
state
.session_configuration
.step_settings
.service_tier
.clone()
.or_else(|| config.service_tier.clone()),
)
};
let auto_review_enabled = crate::guardian::routes_approval_policy_to_guardian(
per_turn_config.permissions.approval_policy.value(),
per_turn_config.approvals_reviewer,
);
// The original config may retain a requested tier unsupported by the parent
// model. Resolve it against the review model and the shared feature gate.
selected.collaboration_mode.settings.model = model.clone();
selected.collaboration_mode.settings.reasoning_effort = reasoning_effort.clone();
selected.service_tier = service_tier;
let step_settings = Arc::new(ResolvedStepSettings::new(
Arc::new(selected),
Arc::new(model_info.clone()),
review_features.enabled(Feature::FastMode),
));
per_turn_config.model = Some(model);
per_turn_config.model_reasoning_effort = reasoning_effort;
per_turn_config.service_tier = step_settings.service_tier.clone();
let session_telemetry_for_context =
step_settings.telemetry(&parent_turn_context.session_telemetry);
let per_turn_config = Arc::new(per_turn_config);
let review_turn_id = sub_id.to_string();
let turn_metadata_state = Arc::new(TurnMetadataState::new(
sess.session_id().to_string(),
sess.thread_id().to_string(),
forked_from_thread_id,
parent_turn_context.parent_thread_id,
&session_source,
thread_source,
review_turn_id.clone(),
#[allow(deprecated)]
parent_turn_context.cwd.clone(),
&parent_turn_context.permission_profile(),
parent_turn_context.windows_sandbox_level,
parent_turn_context.network.is_some(),
auto_review_enabled,
&model_info,
));
if turn_metadata_state.can_start_root_turn(&session_source) {
turn_metadata_state.set_root_turn_id(review_turn_id.clone());
}
let extension_data = Arc::new(codex_extension_api::ExtensionData::new(
review_turn_id.clone(),
));
extension_data.insert(parent_turn_context.skills_snapshot().as_ref().clone());
let review_turn_context = TurnContext {
sub_id: review_turn_id.clone(),
trace_id: current_span_trace_id(),
realtime_active: parent_turn_context.realtime_active,
code_mode_available: parent_turn_context.code_mode_available,
configured_token_budget: per_turn_config.token_budget.clone(),
use_model_token_budget_defaults: per_turn_config.features.enabled(Feature::TokenBudget)
&& !super::token_budget::has_explicit_settings(&per_turn_config),
config: per_turn_config,
auth_manager: auth_manager_for_context,
initial_settings: Arc::clone(&step_settings),
current_settings: ArcSwap::from(step_settings),
session_telemetry: session_telemetry_for_context,
provider: provider_for_context,
session_source,
history_mode: parent_turn_context.history_mode,
parent_thread_id: parent_turn_context.parent_thread_id,
originator: parent_turn_context.originator.clone(),
environments: parent_turn_context.environments.clone(),
available_models,
unified_exec_shell_mode,
current_date: parent_turn_context.current_date.clone(),
timezone: parent_turn_context.timezone.clone(),
app_server_client_name: parent_turn_context.app_server_client_name.clone(),
developer_instructions: None,
multi_agent_version: MultiAgentVersion::Disabled,
network: parent_turn_context.network.clone(),
windows_sandbox_level: parent_turn_context.windows_sandbox_level,
#[allow(deprecated)]
cwd: parent_turn_context.cwd.clone(),
final_output_json_schema: None,
dynamic_tools: parent_turn_context.dynamic_tools.clone(),
turn_metadata_state,
extension_data,
turn_timing_state: Arc::new(TurnTimingState::default()),
terminal_error: Arc::new(Mutex::new(None)),
server_model_warning_emitted: AtomicBool::new(false),
model_verification_emitted: AtomicBool::new(false),
cyber_access_program: None,
};
// Seed the child task with the review prompt as the initial user message.
let input = vec![TurnInput::UserInput {
content: vec![UserInput::Text {
text: review_prompt,
// Review prompt is synthesized; no UI element ranges to preserve.
text_elements: Vec::new(),
}],
client_id: None,
}];
let tc = Arc::new(review_turn_context);
if tc.environments.single_local_environment_cwd().is_some() {
tc.turn_metadata_state.spawn_git_enrichment_task();
}
// TODO(ccunningham): Review turns currently rely on `spawn_task` for TurnComplete but do not
// emit a parent TurnStarted. Consider giving review a full parent turn lifecycle
// (TurnStarted + TurnComplete) for consistency with other standalone tasks.
sess.spawn_task(Arc::clone(&tc), input, ReviewTask::new())
.await;
// Announce entering review mode so UIs can switch modes.
let item = TurnItem::EnteredReviewMode(EnteredReviewModeItem {
id: uuid::Uuid::now_v7().to_string(),
target: resolved.target,
user_facing_hint: resolved.user_facing_hint,
});
sess.emit_turn_item_started(&tc, &item).await;
sess.emit_turn_item_completed(&tc, item).await;
}