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
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
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
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
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
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
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
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
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. 测试与边界
关键测试分别覆盖以下边界:
review_op_emits_lifecycle_and_review_output验证进入/退出 item 生命周期、结构化输出和 rollout。review_filters_agent_message_related_events验证流式 assistant 事件不会泄漏给父消费者。review_does_not_emit_agent_message_on_structured_output验证结构化 review 不产生多余的普通 agent message。review_uses_custom_review_model_from_config与review_uses_session_model_when_review_model_unset验证模型选择。review_input_isolated_from_parent_history验证 delegate 不继承父历史;review_history_surfaces_in_parent_session验证完成后的 review 结果可以被父会话后续 turn 看见。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结果
BaseBranch找到 merge base 与找不到 merge base 时,prompt 分别使用哪两个模板?- 为什么 review 需要同时创建 review
TurnContext和 one-shot delegate 配置?父历史在哪个边界被隔离? - 如果 delegate 只返回普通文本而不是 JSON,最终
ExitedReviewMode的review_output是否为空?请沿解析器 和 fallback 分支回答。 - 取消 ReviewTask 时,为什么
ExitedReviewMode必须先于TurnAborted?哪个函数负责这个顺序?
可以用下面的只读搜索把本文的 review task 主线落回源码:
rg -n "Op::Review|resolve_review_request|ReviewOutputEvent|ExitedReviewMode" codex-rs/core/src codex-rs/prompts/src