Skip to content

ReviewTask完整流程

从 Op::Review 追踪 review target 解析、受限子会话、结构化输出过滤和 Review 模式退出。

基于rust-v0.150.0
CodexRustRuntime

ReviewTask完整流程 ​

ReviewTask 不是在父 RegularTask 中增加一个“审查工具调用”。Op::Review 会创建一个新的 review turn, 把目标解析成明确的 prompt,再以受限配置启动一次性 delegate。delegate 的事件不会原样透传:assistant 消息的流式事件被压制,最后一个 agent message 被解析为 ReviewOutputEvent,成功或中断都要退出 Review 模式并把结果写回父 Session。

本文假定读者已经能区分 Session、Turn 和 Task,可先阅读 Task抽象与生命周期。本文不展开普通 Turn 的采样循环,只追踪 Op::Review 的目标解析、子 Turn 隔离、结果回灌和退出清理。读完后应能从 handler 入口定位到 delegate 消费者,并用测试复核结构化输出和取消顺序。

1. Review 入口 ​

submission loop 遇到 Op::Review 后调用 handlers::review。handler 先创建新的 TurnContext,再调用 resolve_review_request。目标解析失败只发送 Error,不会创建 ReviewTask。

源码位置:codex-rs/core/src/session/handlers.rs :: review

rust
pub async fn review(
    sess: &Arc<Session>,
    config: &Arc<Config>,
    sub_id: String,
    review_request: ReviewRequest,
) {
    let turn_context = sess.new_default_turn_with_sub_id(sub_id.clone()).await;
    sess.maybe_emit_model_warnings_for_turn(turn_context.as_ref())
        .await;
    #[allow(deprecated)]
    match resolve_review_request(review_request, &turn_context.cwd) {
        Ok(resolved) => {
            spawn_review_thread(
                Arc::clone(sess),
                Arc::clone(config),
                turn_context.clone(),
                sub_id,
                resolved,
            )
            .await;
        }
        Err(err) => {
            let event = Event {
                id: sub_id,
                msg: EventMsg::Error(ErrorEvent {
                    message: err.to_string(),
                    codex_error_info: Some(CodexErrorInfo::Other),
                }),
            };
            sess.send_event(&turn_context, event.msg).await;
        }
    }
}

这个入口有两个容易误读的边界。第一,ReviewRequest 的 target 不是 prompt 本身;它必须先解析为 包含 prompt 和 UI hint 的 ResolvedReviewRequest。第二,错误分支只发 Error,没有进入 review mode, 所以不能期待随后出现 EnteredReviewMode 或 ExitedReviewMode。

2. Review目标Prompt ​

协议层的 target 有四种:当前未提交变更、相对 base branch 的变更、指定 commit,以及自定义指令。只有 base branch 需要读取 Git merge base;如果找不到 merge base,会使用带 upstream 查找命令的备用 prompt。

源码位置:codex-rs/prompts/src/review_request.rs :: review_prompt

rust
pub fn review_prompt(target: &ReviewTarget, cwd: &AbsolutePathBuf) -> anyhow::Result<String> {
    match target {
        ReviewTarget::UncommittedChanges => Ok(UNCOMMITTED_PROMPT.to_string()),
        ReviewTarget::BaseBranch { branch } => {
            if let Some(commit) = merge_base_with_head(cwd, branch)? {
                Ok(render_review_prompt(
                    &BASE_BRANCH_PROMPT_TEMPLATE,
                    [
                        ("base_branch", branch.as_str()),
                        ("merge_base_sha", commit.as_str()),
                    ],
                ))
            } else {
                Ok(render_review_prompt(
                    &BASE_BRANCH_PROMPT_BACKUP_TEMPLATE,
                    [("branch", branch.as_str())],
                ))
            }
        }
        ReviewTarget::Commit { sha, title } => {
            if let Some(title) = title {
                Ok(render_review_prompt(
                    &COMMIT_PROMPT_WITH_TITLE_TEMPLATE,
                    [("sha", sha.as_str()), ("title", title.as_str())],
                ))
            } else {
                Ok(render_review_prompt(
                    &COMMIT_PROMPT_TEMPLATE,
                    [("sha", sha.as_str())],
                ))
            }
        }
        ReviewTarget::Custom { instructions } => {
            let prompt = instructions.trim();
            if prompt.is_empty() {
                anyhow::bail!("Review prompt cannot be empty");
            }
            Ok(prompt.to_string())
        }
    }
}

Custom 的空白指令在这里被拒绝,而不是让模型收到空用户消息。Commit 的 title 只用于提高 prompt 可读性,不改变 commit SHA;BaseBranch 的 merge base 则直接进入 prompt,要求 delegate 用该提交点执行 diff。

3. 子Turn隔离 ​

spawn_review_thread 不复用父 turn 的完整配置。它选择 review_model(未配置时回退父模型),复制父配置 后关闭 review 不应使用的能力,并建立一个新的 TurnContext。当前实现明确关闭 Web Search、View Image、Goals 和 多 agent 运行时;同时把 web search mode 固定为 Disabled,必要时把 reasoning effort 调整到 review 模型支持的等级。

源码位置:codex-rs/core/src/session/review.rs :: spawn_review_thread

rust
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;
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 mut per_turn_config = (*parent_turn_context.config).clone();
per_turn_config.token_budget = config.token_budget.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"
    );
}

这段配置隔离不是安全沙箱的替代品,而是 review 子 turn 的能力收窄。它保留父 turn 的工作目录、网络和 权限快照等上下文,但不把父 turn 的历史直接作为 review 请求输入。测试 review_input_isolated_from_parent_history 专门断言模型请求只含 review prompt,不含父会话旧消息。

4. ReviewTask委托 ​

ReviewTask::run 先从 TurnInput 中只收集 UserInput,忽略已经是 response item 或 inter-agent communication 的输入。随后 start_review_conversation 再复制配置,设置 review rubric、关闭额外能力、 禁止审批,并通过 run_codex_thread_one_shot 建立 SubAgentSource::Review 的一次性子会话。

源码位置:codex-rs/core/src/tasks/review.rs :: ReviewTask::run

rust
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(_) => {}
    }
}

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(Arc::clone(&session), output.clone(), ctx.clone()).await;
}
Ok(None)

ReviewTask 自己返回 Ok(None),因为 review 的用户可见结果由 ExitedReviewMode、最终 assistant message 和 TurnComplete 组成,而不是由 SessionTaskResult 携带一个普通文本结果。取消时 run 不主动退出模式, 由 task 的 abort 方法负责在统一 abort 生命周期中先发退出事件。

源码位置:codex-rs/core/src/tasks/review.rs :: start_review_conversation

rust
let mut sub_agent_config = config.as_ref().clone();
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::Collab);
let _ = sub_agent_config.features.disable(Feature::MultiAgentV2);
sub_agent_config.base_instructions = Some(crate::REVIEW_PROMPT.to_string());
sub_agent_config.permissions.approval_policy = Constrained::allow_only(AskForApproval::Never);

run_codex_thread_one_shot(
    sub_agent_config,
    Arc::clone(&session.services.auth_manager),
    Arc::clone(&session.services.models_manager),
    input,
    Arc::clone(&session),
    ctx.clone(),
    cancellation_token,
    SubAgentSource::Review,
    /*final_output_json_schema*/ None,
    /*initial_history*/ None,
)

这里有两层“隔离”不能混为一谈:session/review.rs 建立 review turn 的 per-turn 配置; tasks/review.rs 再为 one-shot delegate 设置 rubric 和更窄的功能 gate。initial_history: None 是 review 输入不继承父历史的直接证据。

review prompt 被包装成新 turn 的首个 UserInput,随后先调用 spawn_task,再发布 EnteredReviewModeItem:

源码位置:codex-rs/core/src/session/review.rs :: spawn_review_thread

rust
let input = vec![TurnInput::UserInput {
    content: vec![UserInput::Text {
        text: review_prompt,
        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();
}
sess.spawn_task(tc.clone(), input, ReviewTask::new()).await;

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;

说明: 当前源码在这里保留 TODO:review turn 尚未显式发送父级 TurnStarted,但仍由 spawn_task 产生 TurnComplete。因此不能把普通 standalone task 的完整起止事件对称性直接套到 ReviewTask;这是当前实现边界,不是文档省略。

5. 事件过滤输出 ​

delegate 的事件接收器由 process_review_events 消费。相邻的 AgentMessage 会延迟一个事件发送,以便只 转发前一个消息;ItemCompleted(AgentMessage) 和 AgentMessageContentDelta 被丢弃,避免 legacy 事件把 结构化 review 误显示成普通流式回答。TurnComplete 的 last_agent_message 才是解析入口。

源码位置:codex-rs/core/src/tasks/review.rs :: process_review_events

rust
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.send_event(ctx.as_ref(), prev.msg).await;
            }
            prev_agent_message = Some(event);
        }
        EventMsg::ItemCompleted(ItemCompletedEvent {
            item: TurnItem::AgentMessage(_),
            ..
        })
        | EventMsg::AgentMessageContentDelta(AgentMessageContentDeltaEvent { .. }) => {}
        EventMsg::TurnComplete(task_complete) => {
            let out = task_complete
                .last_agent_message
                .as_deref()
                .map(parse_review_output_event);
            return out;
        }
        EventMsg::TurnAborted(_) => return None,
        other => session.send_event(ctx.as_ref(), other).await,
    }
}
None

解析器先尝试把完整文本反序列化为 ReviewOutputEvent;失败时截取第一个 { 到最后一个 } 再试一次; 仍失败则把原始文本放入 overall_explanation,因此普通文本不是空结果,而是结构化 fallback。成功结果的 字段包括 findings、整体正确性、解释和置信度,每个 finding 还有标题、正文、优先级和代码位置。

6. Review退出 ​

成功退出时,exit_review_mode 把整体解释和 findings 渲染为用户消息,把 ReviewOutputEvent 放入 ExitedReviewModeItem,随后记录最终 assistant message,并调用 ensure_rollout_materialized。中断退出时 review output 为 None,assistant message 明确提示重新运行 /review。

源码位置:codex-rs/core/src/tasks/review.rs :: exit_review_mode

rust
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(ResponseItemId::new("msg")),
            role: "user".to_string(),
            content: vec![ContentItem::InputText { text: user_message }],
            phase: None,
            internal_chat_message_metadata_passthrough: None,
        }],
    )
    .await;

let item = TurnItem::ExitedReviewMode(ExitedReviewModeItem {
    id: uuid::Uuid::now_v7().to_string(),
    review_output,
});
session.emit_turn_item_started(ctx.as_ref(), &item).await;
session.emit_turn_item_completed(ctx.as_ref(), item).await;
session
    .record_response_item_and_emit_turn_item(
        ctx.as_ref(),
        ResponseItem::Message {
            id: Some(ResponseItemId::new("msg")),
            role: "assistant".to_string(),
            content: vec![ContentItem::OutputText {
                text: assistant_message,
            }],
            phase: None,
            internal_chat_message_metadata_passthrough: None,
        },
    )
    .await;
session.ensure_rollout_materialized().await;

上面省略了 ResponseItem::Message 的字段构造,但保留了源码中的顺序:先记录退出用的 user marker,再发 ExitedReviewMode item 生命周期,最后记录 assistant response 并确保 rollout 存在。abort 路径调用同一 个函数但传入 None;测试 abort_review_task_emits_exited_then_aborted_and_records_history 断言 ExitedReviewMode 早于 TurnAborted,并且 <turn_aborted> marker 仍被写入 history。

7. 测试与边界 ​

关键测试分别覆盖以下边界:

  1. review_op_emits_lifecycle_and_review_output 验证进入/退出 item 生命周期、结构化输出和 rollout。
  2. review_filters_agent_message_related_events 验证流式 assistant 事件不会泄漏给父消费者。
  3. review_does_not_emit_agent_message_on_structured_output 验证结构化 review 不产生多余的普通 agent message。
  4. review_uses_custom_review_model_from_config 与 review_uses_session_model_when_review_model_unset 验证模型选择。
  5. review_input_isolated_from_parent_history 验证 delegate 不继承父历史;review_history_surfaces_in_parent_session 验证完成后的 review 结果可以被父会话后续 turn 看见。
  6. abort_review_task_emits_exited_then_aborted_and_records_history 验证取消时的退出顺序和历史 marker。

这些测试不意味着 review 可以访问任意工具:能力 gate 仍由 per-turn 配置和 one-shot delegate 配置共同决定。 也不要把 ReviewTask 与 Guardian 的自动审批 review 混为一条实现;本文讨论的是 Op::Review 的用户发起 review task。

8. Review结果 ​

  1. BaseBranch 找到 merge base 与找不到 merge base 时,prompt 分别使用哪两个模板?
  2. 为什么 review 需要同时创建 review TurnContext 和 one-shot delegate 配置?父历史在哪个边界被隔离?
  3. 如果 delegate 只返回普通文本而不是 JSON,最终 ExitedReviewMode 的 review_output 是否为空?请沿解析器 和 fallback 分支回答。
  4. 取消 ReviewTask 时,为什么 ExitedReviewMode 必须先于 TurnAborted?哪个函数负责这个顺序?

可以用下面的只读搜索把本文的 review task 主线落回源码:

bash
rg -n "Op::Review|resolve_review_request|ReviewOutputEvent|ExitedReviewMode" codex-rs/core/src codex-rs/prompts/src