use std::sync::Arc;use codex_prompts::render_review_exit_interrupted;use codex_prompts::render_review_exit_success;use codex_protocol::config_types::WebSearchMode;use codex_protocol::items::TurnItem;use codex_protocol::models::ContentItem;use codex_protocol::models::ResponseItem;use codex_protocol::protocol::AgentMessageContentDeltaEvent;use codex_protocol::protocol::AskForApproval;use codex_protocol::protocol::Event;use codex_protocol::protocol::EventMsg;use codex_protocol::protocol::ExitedReviewModeEvent;use codex_protocol::protocol::ItemCompletedEvent;use codex_protocol::protocol::ReviewOutputEvent;use codex_protocol::protocol::SubAgentSource;use tokio_util::sync::CancellationToken;use crate::codex_delegate::run_codex_thread_one_shot;use crate::config::Constrained;use crate::review_format::format_review_findings_block;use crate::review_format::render_review_output_text;use crate::session::TurnInput;use crate::session::session::Session;use crate::session::turn_context::TurnContext;use crate::state::TaskKind;use codex_features::Feature;use codex_protocol::user_input::UserInput;use super::SessionTask;use super::SessionTaskContext;#[derive(Clone, Copy)]pub(crate) struct ReviewTask;impl ReviewTask { pub(crate) fn new() -> Self { Self }}impl SessionTask for ReviewTask { fn kind(&self) -> TaskKind { TaskKind::Review } fn span_name(&self) -> &'static str { "session_task.review" } async fn run( self: Arc<Self>, session: Arc<SessionTaskContext>, ctx: Arc<TurnContext>, input: Vec<TurnInput>, cancellation_token: CancellationToken, ) -> Option<String> { session.session.services.session_telemetry.counter( "codex.task.review", /*inc*/ 1, &[], ); let mut user_input = Vec::new(); for item in input { match item { TurnInput::UserInput { mut content, .. } => user_input.append(&mut content), TurnInput::ResponseItem(_) | TurnInput::InterAgentCommunication(_) => {} } } // Start sub-codex conversation and get the receiver for events. let output = match start_review_conversation( session.clone(), ctx.clone(), user_input, cancellation_token.clone(), ) .await { Some(receiver) => process_review_events(session.clone(), ctx.clone(), receiver).await, None => None, }; if !cancellation_token.is_cancelled() { exit_review_mode(session.clone_session(), output.clone(), ctx.clone()).await; } None } async fn abort(&self, session: Arc<SessionTaskContext>, ctx: Arc<TurnContext>) { exit_review_mode(session.clone_session(), /*review_output*/ None, ctx).await; }}async fn start_review_conversation( session: Arc<SessionTaskContext>, ctx: Arc<TurnContext>, input: Vec<UserInput>, cancellation_token: CancellationToken,) -> Option<async_channel::Receiver<Event>> { let config = ctx.config.clone(); let mut sub_agent_config = config.as_ref().clone(); // Carry over review-only feature restrictions so the delegate cannot // re-enable blocked tools (web search, collab tools, view image). if let Err(err) = sub_agent_config .web_search_mode .set(WebSearchMode::Disabled) { panic!("by construction Constrained<WebSearchMode> must always support Disabled: {err}"); } let _ = sub_agent_config.features.disable(Feature::SpawnCsv); let _ = sub_agent_config.features.disable(Feature::Collab); let _ = sub_agent_config.features.disable(Feature::MultiAgentV2); // Set explicit review rubric for the sub-agent sub_agent_config.base_instructions = Some(crate::REVIEW_PROMPT.to_string()); sub_agent_config.permissions.approval_policy = Constrained::allow_only(AskForApproval::Never); let model = config .review_model .clone() .unwrap_or_else(|| ctx.model_info.slug.clone()); sub_agent_config.model = Some(model); (run_codex_thread_one_shot( sub_agent_config, session.auth_manager(), session.models_manager(), input, session.clone_session(), ctx.clone(), cancellation_token, SubAgentSource::Review, /*final_output_json_schema*/ None, /*initial_history*/ None, ) .await) .ok() .map(|io| io.rx_event)}async fn process_review_events( session: Arc<SessionTaskContext>, ctx: Arc<TurnContext>, receiver: async_channel::Receiver<Event>,) -> Option<ReviewOutputEvent> { let mut prev_agent_message: Option<Event> = None; while let Ok(event) = receiver.recv().await { match event.clone().msg { EventMsg::AgentMessage(_) => { if let Some(prev) = prev_agent_message.take() { session .clone_session() .send_event(ctx.as_ref(), prev.msg) .await; } prev_agent_message = Some(event); } // Suppress ItemCompleted only for assistant messages: forwarding it // would trigger legacy AgentMessage via as_legacy_events(), which this // review flow intentionally hides in favor of structured output. EventMsg::ItemCompleted(ItemCompletedEvent { item: TurnItem::AgentMessage(_), .. }) | EventMsg::AgentMessageContentDelta(AgentMessageContentDeltaEvent { .. }) => {} EventMsg::TurnComplete(task_complete) => { // Parse review output from the last agent message (if present). let out = task_complete .last_agent_message .as_deref() .map(parse_review_output_event); return out; } EventMsg::TurnAborted(_) => { // Cancellation or abort: consumer will finalize with None. return None; } other => { session .clone_session() .send_event(ctx.as_ref(), other) .await; } } } // Channel closed without TurnComplete: treat as interrupted. None}/// Parse a ReviewOutputEvent from a text blob returned by the reviewer model./// If the text is valid JSON matching ReviewOutputEvent, deserialize it./// Otherwise, attempt to extract the first JSON object substring and parse it./// If parsing still fails, return a structured fallback carrying the plain text/// in `overall_explanation`.fn parse_review_output_event(text: &str) -> ReviewOutputEvent { if let Ok(ev) = serde_json::from_str::<ReviewOutputEvent>(text) { return ev; } if let (Some(start), Some(end)) = (text.find('{'), text.rfind('}')) && start < end && let Some(slice) = text.get(start..=end) && let Ok(ev) = serde_json::from_str::<ReviewOutputEvent>(slice) { return ev; } ReviewOutputEvent { overall_explanation: text.to_string(), ..Default::default() }}/// Emits an ExitedReviewMode Event with optional ReviewOutput,/// and records a developer message with the review output.pub(crate) async fn exit_review_mode( session: Arc<Session>, review_output: Option<ReviewOutputEvent>, ctx: Arc<TurnContext>,) { const REVIEW_USER_MESSAGE_ID: &str = "review_rollout_user"; const REVIEW_ASSISTANT_MESSAGE_ID: &str = "review_rollout_assistant"; let (user_message, assistant_message) = if let Some(out) = review_output.clone() { let mut findings_str = String::new(); let text = out.overall_explanation.trim(); if !text.is_empty() { findings_str.push_str(text); } if !out.findings.is_empty() { let block = format_review_findings_block(&out.findings, /*selection*/ None); findings_str.push_str(&format!("\n{block}")); } let rendered = render_review_exit_success(&findings_str); let assistant_message = render_review_output_text(&out); (rendered, assistant_message) } else { let rendered = render_review_exit_interrupted(); let assistant_message = "Review was interrupted. Please re-run /review and wait for it to complete." .to_string(); (rendered, assistant_message) }; session .record_conversation_items( &ctx, &[ResponseItem::Message { id: Some(REVIEW_USER_MESSAGE_ID.to_string()), role: "user".to_string(), content: vec![ContentItem::InputText { text: user_message }], phase: None, metadata: None, }], ) .await; session .send_event( ctx.as_ref(), EventMsg::ExitedReviewMode(ExitedReviewModeEvent { review_output }), ) .await; session .record_response_item_and_emit_turn_item( ctx.as_ref(), ResponseItem::Message { id: Some(REVIEW_ASSISTANT_MESSAGE_ID.to_string()), role: "assistant".to_string(), content: vec![ContentItem::OutputText { text: assistant_message, }], phase: None, metadata: None, }, ) .await; // Review turns can run before any regular user turn, so explicitly // materialize rollout persistence. Do this after emitting review output so // file creation + git metadata collection cannot delay client-facing items. session.ensure_rollout_materialized().await;}