use super::turn_context::image_generation_tool_auth_allowed; use super::*; use std::sync::atomic::AtomicBool; /// Spawn a review thread using the given prompt. pub(super) async fn spawn_review_thread( sess: Arc, config: Arc, parent_turn_context: Arc, 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 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_web_search_mode = WebSearchMode::Disabled; let goal_tools_supported = !config.ephemeral && parent_turn_context.tools_config.goal_tools; let provider_capabilities = parent_turn_context.provider.capabilities(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &review_model_info, available_models: &sess .services .models_manager .list_models(RefreshStrategy::OnlineIfUncached) .await, features: &review_features, image_generation_tool_auth_allowed: image_generation_tool_auth_allowed(Some( sess.services.auth_manager.as_ref(), )), web_search_mode: Some(review_web_search_mode), session_source: parent_turn_context.session_source.clone(), permission_profile: &parent_turn_context.permission_profile, windows_sandbox_level: parent_turn_context.windows_sandbox_level, }) .with_namespace_tools_capability(provider_capabilities.namespace_tools) .with_image_generation_capability(provider_capabilities.image_generation) .with_web_search_capability(provider_capabilities.web_search) .with_unified_exec_shell_mode_for_session( crate::tools::spec::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(), ) .with_web_search_config(/*web_search_config*/ None) .with_allow_login_shell(config.permissions.allow_login_shell) .with_environment_mode(parent_turn_context.tools_config.environment_mode) .with_spawn_agent_usage_hint(config.multi_agent_v2.usage_hint_enabled) .with_spawn_agent_usage_hint_text(config.multi_agent_v2.usage_hint_text.clone()) .with_hide_spawn_agent_metadata(config.multi_agent_v2.hide_spawn_agent_metadata) .with_goal_tools_allowed(goal_tools_supported) .with_max_concurrent_threads_per_session(config.agent_max_threads) .with_wait_agent_min_timeout_ms( review_features .enabled(Feature::MultiAgentV2) .then_some(config.multi_agent_v2.min_wait_timeout_ms), ) .with_agent_type_description(crate::agent::role::spawn_tool_spec::build( &config.agent_roles, )); 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(); // Build per‑turn client with the requested model/family. let mut per_turn_config = (*config).clone(); per_turn_config.model = Some(model.clone()); per_turn_config.features = review_features.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 session_telemetry = parent_turn_context .session_telemetry .clone() .with_model(model.as_str(), review_model_info.slug.as_str()); let auth_manager_for_context = auth_manager.clone(); let provider_for_context = provider.clone(); let session_telemetry_for_context = session_telemetry.clone(); let reasoning_effort = per_turn_config.model_reasoning_effort; let reasoning_summary = per_turn_config .model_reasoning_summary .unwrap_or(model_info.default_reasoning_summary); let session_source = parent_turn_context.session_source.clone(); 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.conversation_id.to_string(), parent_turn_context.thread_source, review_turn_id.clone(), parent_turn_context.cwd.clone(), &parent_turn_context.permission_profile, parent_turn_context.windows_sandbox_level, parent_turn_context.network.is_some(), )); let review_turn_context = TurnContext { sub_id: review_turn_id, trace_id: current_span_trace_id(), realtime_active: parent_turn_context.realtime_active, config: per_turn_config, auth_manager: auth_manager_for_context, model_info: model_info.clone(), session_telemetry: session_telemetry_for_context, provider: provider_for_context, reasoning_effort, reasoning_summary, session_source, thread_source: parent_turn_context.thread_source, environments: parent_turn_context.environments.clone(), tools_config, features: parent_turn_context.features.clone(), ghost_snapshot: parent_turn_context.ghost_snapshot.clone(), 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, user_instructions: None, compact_prompt: parent_turn_context.compact_prompt.clone(), collaboration_mode: parent_turn_context.collaboration_mode.clone(), personality: parent_turn_context.personality, approval_policy: parent_turn_context.approval_policy.clone(), permission_profile: parent_turn_context.permission_profile(), network: parent_turn_context.network.clone(), windows_sandbox_level: parent_turn_context.windows_sandbox_level, shell_environment_policy: parent_turn_context.shell_environment_policy.clone(), cwd: parent_turn_context.cwd.clone(), final_output_json_schema: None, codex_self_exe: parent_turn_context.codex_self_exe.clone(), codex_linux_sandbox_exe: parent_turn_context.codex_linux_sandbox_exe.clone(), tool_call_gate: Arc::new(ReadinessFlag::new()), dynamic_tools: parent_turn_context.dynamic_tools.clone(), truncation_policy: model_info.truncation_policy.into(), turn_metadata_state, turn_skills: TurnSkillsContext::new(parent_turn_context.turn_skills.outcome.clone()), turn_timing_state: Arc::new(TurnTimingState::default()), server_model_warning_emitted: AtomicBool::new(false), model_verification_emitted: AtomicBool::new(false), }; // Seed the child task with the review prompt as the initial user message. let input: Vec = vec![UserInput::Text { text: review_prompt, // Review prompt is synthesized; no UI element ranges to preserve. text_elements: Vec::new(), }]; let tc = Arc::new(review_turn_context); 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(tc.clone(), input, ReviewTask::new()).await; // Announce entering review mode so UIs can switch modes. let review_request = ReviewRequest { target: resolved.target, user_facing_hint: Some(resolved.user_facing_hint), }; sess.send_event(&tc, EventMsg::EnteredReviewMode(review_request)) .await; }