diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/execution.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/execution.rs index 4bf1fa2e2..2bf6360b3 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/execution.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/execution.rs @@ -78,12 +78,41 @@ pub(super) enum HostOutcome { RepairRequired, } -pub(super) fn outcome_text(outcome: HostOutcome) -> Result { +/// [`HostOutcome`] 的文本投影。 +/// +/// 返修要求([`HostOutcome::RepairRequired`])用**自己的变体**表达:它是控制流("继续当前返修 +/// 批次"),不是失败。以前它伪装成 `LlmError::InvalidRequest("validation-source-changed: …")`, +/// 于是和真失败走同一条投影——终态被判成 `failed`、界面收到一条用户可见的失败说明。 +#[derive(Clone, Debug, PartialEq, Eq)] +pub(super) enum HostOutcomeText { + /// 正常收尾:可展示的回复 / 交付报告文本。 + Report(String), + /// 封口复核要求继续当前返修批次(控制流,不是失败)。 + RepairRequired { detail: String }, +} + +/// 返修要求写回提示词时用的说明。 +pub(super) const HOST_OUTCOME_REPAIR_REQUIRED_DETAIL: &str = + "宿主收尾复核发现输入或证据变化,请读取交付状态后继续当前返修批次"; + +impl HostOutcomeText { + /// 投影成这一轮的收尾结果:正常报告是文本,返修要求是控制流(走 `Err` 侧自己的变体)。 + pub(super) fn into_run_result(self) -> Result { + match self { + Self::Report(text) => Ok(text), + Self::RepairRequired { detail } => { + Err(super::DirectTurnRunFailure::RepairRequired { detail }) + } + } + } +} + +pub(super) fn outcome_text(outcome: HostOutcome) -> HostOutcomeText { match outcome { - HostOutcome::Report(report) => Ok(report), - HostOutcome::RepairRequired => Err(platform_llm::LlmError::InvalidRequest( - "validation-source-changed: 宿主收尾复核发现输入或证据变化,请读取交付状态后继续当前返修批次".into(), - )), + HostOutcome::Report(report) => HostOutcomeText::Report(report), + HostOutcome::RepairRequired => HostOutcomeText::RepairRequired { + detail: HOST_OUTCOME_REPAIR_REQUIRED_DETAIL.to_string(), + }, } } diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs index b029ee4c7..6810c4d4f 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs @@ -3199,6 +3199,9 @@ impl CodexAppServerConnection { None, ) .await + // 非 Direct 入口没有"继续返修批次"这条控制流(那是封口复核才有的),遇上只当一次普通的 + // 请求被拒;Direct 回合走下面的 `_and_history` 版本,控制流在那里有自己的变体。 + .map_err(DirectTurnRunFailure::into_llm_error) } async fn run_turn_with_direct_observer( @@ -3209,7 +3212,7 @@ impl CodexAppServerConnection { mut on_agent_message_delta: Option<&mut (dyn FnMut(&platform_llm::LlmStreamDelta) + Send)>, mut direct_observer: Option<&mut (dyn FnMut(DirectCodexTurnObservation) + Send)>, mut audit: Option<&mut DirectCodexTurnAudit>, - ) -> Result { + ) -> Result { self.run_turn_with_direct_observer_and_history( snapshot, llm, @@ -3237,7 +3240,7 @@ impl CodexAppServerConnection { mut direct_observer: Option<&mut (dyn FnMut(DirectCodexTurnObservation) + Send)>, mut audit: Option<&mut DirectCodexTurnAudit>, metrics_attempt: Option, - ) -> Result { + ) -> Result { let mut gate_timing = metrics_attempt .as_ref() .map(|attempt| attempt.span("local-turn-gate")); @@ -3273,8 +3276,10 @@ impl CodexAppServerConnection { if self.inner.workspace_mode == CodexAppServerWorkspaceMode::DirectProject { let current_prompt = direct_codex_current_user_prompt(&request).trim(); if current_prompt.is_empty() { - return Err(platform_llm::LlmError::InvalidRequest( - "DirectProject 用户消息不能为空".to_string(), + return Err(DirectTurnRunFailure::from( + platform_llm::LlmError::InvalidRequest( + "DirectProject 用户消息不能为空".to_string(), + ), )); } if let Some(client_turn_id) = direct_client_turn_id { @@ -3332,8 +3337,9 @@ impl CodexAppServerConnection { .filter(|adapter| adapter.is_host_ending()) { if let Some(outcome) = adapter.finish_model_attempt(&self.inner, false).await { - let text = execution::outcome_text(outcome)?; - return parse_game_creator_codex_app_server_text(&text, &thread_id, &request); + let text = execution::outcome_text(outcome).into_run_result()?; + return parse_game_creator_codex_app_server_text(&text, &thread_id, &request) + .map_err(DirectTurnRunFailure::from); } } if self.inner.workspace_mode == CodexAppServerWorkspaceMode::DirectProject { @@ -3343,12 +3349,14 @@ impl CodexAppServerConnection { Ok(params) => params, Err(error) => { self.release_thread(snapshot, &thread_id).await; - return Err(error); + return Err(error.into()); } }; if let Err(error) = self.request("thread/inject_items", params).await { self.release_thread(snapshot, &thread_id).await; - return Err(platform_llm::LlmError::Transport(error)); + return Err(DirectTurnRunFailure::from( + platform_llm::LlmError::Transport(error), + )); } } } @@ -3504,14 +3512,18 @@ impl CodexAppServerConnection { .as_ref() .filter(|adapter| adapter.is_host_ending()) { - let text = execution::outcome_text(adapter.wait_outcome().await)?; - return parse_game_creator_codex_app_server_text(&text, &thread_id, &request); + let text = + execution::outcome_text(adapter.wait_outcome().await).into_run_result()?; + return parse_game_creator_codex_app_server_text(&text, &thread_id, &request) + .map_err(DirectTurnRunFailure::from); } - return Err(isolate_game_creator_codex_app_server_terminal_unknown( - &self.inner, - format!("turn/start 终态未知:{error}"), - ) - .await); + return Err(DirectTurnRunFailure::from( + isolate_game_creator_codex_app_server_terminal_unknown( + &self.inner, + format!("turn/start 终态未知:{error}"), + ) + .await, + )); } }; let turn_id = match result @@ -3521,11 +3533,13 @@ impl CodexAppServerConnection { { Some(turn_id) => turn_id.to_string(), None => { - return Err(isolate_game_creator_codex_app_server_terminal_unknown( - &self.inner, - "turn/start 响应缺少 turn.id", - ) - .await) + return Err(DirectTurnRunFailure::from( + isolate_game_creator_codex_app_server_terminal_unknown( + &self.inner, + "turn/start 响应缺少 turn.id", + ) + .await, + )); } }; if let Some(adapter) = approval_adapter.as_ref() { @@ -3533,8 +3547,10 @@ impl CodexAppServerConnection { adapter .interrupt("执行许可收到不一致的 app-server 回合身份,已停止本轮。") .await; - let text = execution::outcome_text(adapter.wait_outcome().await)?; - return parse_game_creator_codex_app_server_text(&text, &thread_id, &request); + let text = + execution::outcome_text(adapter.wait_outcome().await).into_run_result()?; + return parse_game_creator_codex_app_server_text(&text, &thread_id, &request) + .map_err(DirectTurnRunFailure::from); } } turn_start_guard.armed = false; @@ -3600,13 +3616,15 @@ impl CodexAppServerConnection { deadline: DirectTurnDeadline::TurnHardLimit, }) .await; - return execution::outcome_text(adapter.wait_outcome().await); + return execution::outcome_text(adapter.wait_outcome().await) + .into_run_result(); } return Err(isolate_game_creator_codex_app_server_terminal_unknown( &self.inner, "等待 turn/completed 超时(达到 DirectProject 硬上限)", ) - .await); + .await + .into()); } let idle_timeout_ms = game_creator_codex_app_server_idle_timeout_ms( self.inner.workspace_mode, @@ -3617,7 +3635,7 @@ impl CodexAppServerConnection { std::cmp::min(remaining, std::time::Duration::from_millis(idle_timeout_ms)); let received = tokio::select! { outcome = execution::wait_outcome(&mut host_outcome) => { - return execution::outcome_text(outcome); + return execution::outcome_text(outcome).into_run_result(); } event = tokio::time::timeout(wait_timeout, receiver.recv()) => event, }; @@ -3630,13 +3648,16 @@ impl CodexAppServerConnection { deadline: DirectTurnDeadline::ResponseIdle, }) .await; - return execution::outcome_text(adapter.wait_outcome().await); + return execution::outcome_text(adapter.wait_outcome().await) + .into_run_result(); } - return Err(isolate_game_creator_codex_app_server_terminal_unknown( - &self.inner, - "等待 turn/completed 超时", - ) - .await); + return Err(DirectTurnRunFailure::from( + isolate_game_creator_codex_app_server_terminal_unknown( + &self.inner, + "等待 turn/completed 超时", + ) + .await, + )); } }; if event.is_some() { @@ -3722,8 +3743,10 @@ impl CodexAppServerConnection { } if self.inner.workspace_mode == CodexAppServerWorkspaceMode::DirectProject { if item.is_null() { - return Err(platform_llm::LlmError::Deserialize( - "rawResponseItem/completed 缺少 item".to_string(), + return Err(DirectTurnRunFailure::from( + platform_llm::LlmError::Deserialize( + "rawResponseItem/completed 缺少 item".to_string(), + ), )); } let entry_item = direct_thread_visible_item(history_root, &item); @@ -3868,10 +3891,12 @@ impl CodexAppServerConnection { "userMessage" | "plan" | "reasoning" | "contextCompaction" ) { - return Err(platform_llm::LlmError::InvalidRequest(format!( - "Codex app-server 违反 {}边界,产生非被动 item {item_type}", - self.inner.workspace_mode.passive_item_boundary_name(), - ))); + return Err(DirectTurnRunFailure::from( + platform_llm::LlmError::InvalidRequest(format!( + "Codex app-server 违反 {}边界,产生非被动 item {item_type}", + self.inner.workspace_mode.passive_item_boundary_name(), + )), + )); } if !completed && self.inner.workspace_mode @@ -3949,12 +3974,16 @@ impl CodexAppServerConnection { if let Some(outcome) = adapter.finish_model_attempt(&self.inner, true).await { - return execution::outcome_text(outcome); + return execution::outcome_text(outcome).into_run_result(); } } return final_text .filter(|text| !text.trim().is_empty()) - .ok_or(platform_llm::LlmError::EmptyResponse); + .ok_or_else(|| { + DirectTurnRunFailure::from( + platform_llm::LlmError::EmptyResponse, + ) + }); } "interrupted" => { if let Some(adapter) = approval_adapter.as_ref() { @@ -3971,13 +4000,16 @@ impl CodexAppServerConnection { }) .await; } - return execution::outcome_text(adapter.wait_outcome().await); + return execution::outcome_text(adapter.wait_outcome().await) + .into_run_result(); } if let Some(attempt) = metrics_attempt.as_ref() { attempt.finish("interrupted"); } - return Err(platform_llm::LlmError::InvalidRequest( - "Codex app-server turn 已中断".to_string(), + return Err(DirectTurnRunFailure::from( + platform_llm::LlmError::InvalidRequest( + "Codex app-server turn 已中断".to_string(), + ), )); } "failed" => { @@ -3992,17 +4024,21 @@ impl CodexAppServerConnection { if let Some(outcome) = adapter.finish_model_attempt(&self.inner, false).await { - if let Err(repair) = execution::outcome_text(outcome) { + if let Err(repair) = + execution::outcome_text(outcome).into_run_result() + { return Err(repair); } } } - return Err(native); + return Err(DirectTurnRunFailure::from(native)); } status => { - return Err(platform_llm::LlmError::Deserialize(format!( - "Codex app-server turn/completed 状态无效:{status}" - ))) + return Err(DirectTurnRunFailure::from( + platform_llm::LlmError::Deserialize(format!( + "Codex app-server turn/completed 状态无效:{status}" + )), + )) } } } @@ -4018,13 +4054,16 @@ impl CodexAppServerConnection { }) .await; } - return execution::outcome_text(adapter.wait_outcome().await); + return execution::outcome_text(adapter.wait_outcome().await) + .into_run_result(); } - return Err(isolate_game_creator_codex_app_server_terminal_unknown( - &self.inner, - error, - ) - .await); + return Err(DirectTurnRunFailure::from( + isolate_game_creator_codex_app_server_terminal_unknown( + &self.inner, + error, + ) + .await, + )); } None => { if let Some(adapter) = approval_adapter.as_ref() { @@ -4037,27 +4076,36 @@ impl CodexAppServerConnection { }) .await; } - return execution::outcome_text(adapter.wait_outcome().await); + return execution::outcome_text(adapter.wait_outcome().await) + .into_run_result(); } - return Err(isolate_game_creator_codex_app_server_terminal_unknown( - &self.inner, - "Codex app-server turn 事件通道已关闭", - ) - .await); + return Err(DirectTurnRunFailure::from( + isolate_game_creator_codex_app_server_terminal_unknown( + &self.inner, + "Codex app-server turn 事件通道已关闭", + ) + .await, + )); } } } }; - let mut collect_result = collect.await; - if let (Some(adapter), Err(error)) = (approval_adapter.as_ref(), &collect_result) { + let mut collect_result: Result = collect.await; + // 早退要先取得宿主收尾事实,但**只对真失败**:`RepairRequired`(封口复核要求继续当前返修 + // 批次)是控制流——这一轮还没结束,不能被这里中断成一次收束失败。它的产生点(封口复核) + // 一定先收束适配器,所以它也落不进下面的 `is_settled` 判据。 + if let (Some(adapter), Err(DirectTurnRunFailure::Failed(error))) = + (approval_adapter.as_ref(), &collect_result) + { if !adapter.is_settled() { - // 协议/条目持久化等早退也要先取得宿主收尾事实;已收束的返修请求保持原语义。 + // 协议/条目持久化等早退也要先取得宿主收尾事实。 let reason = format!( "执行回执处理失败,已停止本轮并核对后台操作:{}", redact_agent_runtime_error(history_root, &error.to_string(), 600) ); adapter.interrupt(&reason).await; - collect_result = execution::outcome_text(adapter.wait_outcome().await); + collect_result = + execution::outcome_text(adapter.wait_outcome().await).into_run_result(); } } if self.inner.workspace_mode == CodexAppServerWorkspaceMode::DirectProject { @@ -4067,8 +4115,10 @@ impl CodexAppServerConnection { }) .await .unwrap_or_else(|_| { - Err(platform_llm::LlmError::Transport( - "DirectProject 收尾历史任务退出,未确认历史完整落盘".into(), + Err(DirectTurnRunFailure::from( + platform_llm::LlmError::Transport( + "DirectProject 收尾历史任务退出,未确认历史完整落盘".into(), + ), )) }); let fallback_status = model_terminal @@ -4101,22 +4151,22 @@ impl CodexAppServerConnection { let turn_failure = approval_adapter .as_ref() .and_then(|adapter| adapter.turn_failure()); - // 收尾结果在这里投影成 typed 错误:载荷的 `kind` / `message` 都从这一份值出来。 - let collect_outcome = match collect_result.as_ref() { - Ok(report) => Ok(report.as_str()), - Err(error) => Err(DirectTurnError::from_model_call(error)), - }; - let terminal = direct_turn_terminal( - &status, - collect_outcome, - turn_failure.as_ref(), - history_root, - ); - // 终态走 Thread Manager 的深出口:解除这一轮的占用并写下 `turn.completed`。 - complete_direct_thread_turn( - &direct_thread_id, - terminal.event(completed_at, direct_turn_user_item_id.as_deref()), - ); + // 收尾结果在这里投影成 typed 错误:载荷的 `kind` / `message` 都从这一份值出来; + // `None` 表示这一轮不该写终态(返修要求是控制流,见 `direct_turn_terminal_write`)。 + let collect_outcome = direct_turn_terminal_write(&collect_result); + if let Some(collect_outcome) = collect_outcome { + let terminal = direct_turn_terminal( + &status, + collect_outcome, + turn_failure.as_ref(), + history_root, + ); + // 终态走 Thread Manager 的深出口:解除这一轮的占用并写下 `turn.completed`。 + complete_direct_thread_turn( + &direct_thread_id, + terminal.event(completed_at, direct_turn_user_item_id.as_deref()), + ); + } } let text = collect_result?; guard.armed = false; @@ -4132,11 +4182,68 @@ impl CodexAppServerConnection { } } +/// 一次 app-server 回合的失败。 +/// +/// 为什么不是一个 `LlmError`:宿主封口复核要求"继续当前返修批次"是**控制流**,不是这一轮的失败 +/// (见 [`execution::HostOutcomeText`])。以前它伪装成 +/// `LlmError::InvalidRequest("validation-source-changed: …")`,于是和真失败共用一条投影——终态被 +/// 判成 `failed`、界面收到一条用户可见的失败说明,外层还会把它当"可修复错误"再喂给模型。 +#[derive(Debug)] +enum DirectTurnRunFailure { + /// 这一轮真的失败了(模型 / 传输 / 交付 / 解析)。 + Failed(platform_llm::LlmError), + /// 封口复核要求继续当前返修批次:**不写终态**,由调用方把 `detail` 写回提示词继续跑。 + RepairRequired { detail: String }, +} + +impl From for DirectTurnRunFailure { + fn from(error: platform_llm::LlmError) -> Self { + Self::Failed(error) + } +} + +impl DirectTurnRunFailure { + /// 压回 `LlmError`:只给拿不到"继续返修批次"这条控制流的入口用(非 Direct 的 `run_turn`)。 + fn into_llm_error(self) -> platform_llm::LlmError { + match self { + Self::Failed(error) => error, + Self::RepairRequired { detail } => platform_llm::LlmError::InvalidRequest(detail), + } + } +} + +/// 这一轮要不要写终态、写什么内容。 +/// +/// - `Ok(text)`:正常终态(文本交给 `direct_turn_terminal` 判定); +/// - `Err(Failed)`:失败终态,载荷从这条 typed 错误投影; +/// - `Err(RepairRequired)`:**不写**——"继续当前返修批次"是控制流,这一轮还没结束。写成 `failed` +/// 会让界面收到一条假失败,而且同一个逻辑回合稍后还会再写一条终态。 +fn direct_turn_terminal_write( + collect_result: &Result, +) -> Option> { + match collect_result { + Ok(report) => Some(Ok(report.as_str())), + Err(DirectTurnRunFailure::RepairRequired { .. }) => None, + Err(DirectTurnRunFailure::Failed(error)) => { + Some(Err(DirectTurnError::from_model_call(error))) + } + } +} + +impl std::fmt::Display for DirectTurnRunFailure { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Failed(error) => write!(formatter, "{error}"), + Self::RepairRequired { detail } => formatter.write_str(detail), + } + } +} + fn finish_direct_project_collect_history( root: &Path, mut history: DirectProjectHistoryAccumulator, - result: Result, -) -> Result { + result: Result, +) -> Result { // 宿主预算/交付终态也会返回 Ok(report),同样需要保留被中断的流式正文。 if let Err(persist_error) = persist_direct_project_partial_items_at(root, &mut history) { let prior = result @@ -4144,9 +4251,11 @@ fn finish_direct_project_collect_history( .err() .map(|error| format!(";原始回合错误:{error}")) .unwrap_or_default(); - return Err(platform_llm::LlmError::Transport(format!( - "DirectProject 收尾历史失败:{persist_error}{prior}", - ))); + return Err(DirectTurnRunFailure::Failed( + platform_llm::LlmError::Transport(format!( + "DirectProject 收尾历史失败:{persist_error}{prior}" + )), + )); } result } @@ -5273,7 +5382,13 @@ pub(crate) async fn direct_game_creator_codex_chat_at_with_optional_observer( ) .await .map(|value| value.text) - .map_err(|error| DirectTurnError::from_model_call(&error)); + // 真失败才投影成回合失败;"继续返修批次"这条控制流有自己的变体,直接交给调用方。 + .map_err(|failure| match failure { + DirectTurnRunFailure::Failed(error) => DirectTurnError::from_model_call(&error), + DirectTurnRunFailure::RepairRequired { detail } => { + DirectTurnError::RepairRequired { detail } + } + }); if let Some(attempt) = metrics_attempt.as_ref() { attempt.finish(if result.is_ok() { "completed" @@ -6019,6 +6134,33 @@ mod tests { assert_eq!(direct_codex_user_item_id_for_client_turn_id(""), None); } + /// 封口复核要求(`RepairRequired`)是控制流:这一轮还没结束,不能写出失败终态。 + /// + /// 反过来,真失败必须写成载荷,`kind` / `message` 从 typed 错误投影——两者以前共用 + /// `validation-source-changed:` 那条 `InvalidRequest`,于是"继续返修"会被讲成一次用户可见的失败。 + #[test] + fn terminal_write_skips_the_repair_request_and_projects_real_failures() { + let repair: Result = + Err(DirectTurnRunFailure::RepairRequired { + detail: "继续当前返修批次".into(), + }); + assert!( + direct_turn_terminal_write(&repair).is_none(), + "返修要求是控制流,不允许写终态" + ); + + let failed: Result = Err(DirectTurnRunFailure::Failed( + platform_llm::LlmError::Transport("执行通道已断开".into()), + )); + let payload = direct_turn_terminal_write(&failed).expect("真失败必须写终态"); + let error = payload.expect_err("失败终态必须带载荷"); + assert_eq!(error.wire_kind(), Some("transport-failed")); + + let report: Result = Ok("本轮交付已完成".into()); + let payload = direct_turn_terminal_write(&report).expect("正常收尾要写终态"); + assert_eq!(payload.expect("正常收尾不带失败载荷"), "本轮交付已完成"); + } + #[test] fn host_report_persists_unfinished_stream_history_before_terminal() { let temp = tempfile::tempdir().expect("temp dir"); diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs index 48ed8236f..f8c7ce6c0 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/mod.rs @@ -5021,7 +5021,13 @@ async fn run_direct_game_creator_turn_inner( Ok(Some(report)) => break Ok(report), Ok(None) => break Ok(value), // 返修要求是控制流,不是失败:把要求写回 prompt 再跑一轮。 - Err(DirectTurnError::ReviewRequired { detail }) => { + // `RepairRequired` 是同一族的第二条来源(app-server 封口复核),处理完全一样; + // 两条路的次数上限都在产生侧(交付复核 `ledger.max_runs`、执行账本的批次上限), + // 这里不另设计数,否则会把本来能收敛的长返修提前掐断。 + Err( + DirectTurnError::ReviewRequired { detail } + | DirectTurnError::RepairRequired { detail }, + ) => { emitter.emit("running",Some("host-review"),Some("宿主复核发现必需证据尚未齐备,正在按冻结范围继续处理".into()),None); feedback_prompt = format!(prompt_text!("direct.deliveryFeedback"),detail=detail); } @@ -5084,7 +5090,12 @@ async fn run_direct_game_creator_turn_inner( Ok(Some(report)) => { response = Some(report); break; } Ok(None) => { response = Some(value); break; } // 返修要求是控制流,不是失败:把要求写回 prompt 再跑一轮。 - Err(DirectTurnError::ReviewRequired { detail }) => { + // `RepairRequired` 是同一族的第二条来源(app-server 封口复核),处理完全一样; + // 次数上限在产生侧(见流式分支同一处注释),这里不另设计数。 + Err( + DirectTurnError::ReviewRequired { detail } + | DirectTurnError::RepairRequired { detail }, + ) => { feedback_prompt = format!(prompt_text!("direct.deliveryFeedback"),detail=detail); } // 回合级失败原样带出:typed 分类决定诊断摘要与建议,不再降级成文本。 diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_turn_error.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_turn_error.rs index f24b1da7d..913a99644 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_turn_error.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_turn_error.rs @@ -403,6 +403,10 @@ pub(crate) enum DirectTurnError { /// 宿主复核要求继续本轮的返修批次 —— **控制流,不是失败**: /// 交付模块用它把"还缺证据"交给下一步,界面不应该看到失败。 ReviewRequired { detail: String }, + /// 宿主**封口**复核要求继续当前返修批次(app-server 收尾的 `HostOutcome::RepairRequired`) + /// —— **控制流,不是失败**:与 [`DirectTurnError::ReviewRequired`] 同一族,只是产生点在收尾 + /// 阶段而不是交付复核。调用方把它写回提示词继续跑:不写终态、不进失败载荷、不上报。 + RepairRequired { detail: String }, /// 已经写成诊断记录的回合失败:`detail` 是诊断正文(阶段 / 分类 / 建议 / 详情引用)。 TurnFailed { stage: DirectCodexFailureStage, @@ -464,6 +468,7 @@ impl DirectTurnError { | Self::TimedOut { .. } | Self::TurnInterrupted { .. } | Self::ReviewRequired { .. } + | Self::RepairRequired { .. } | Self::TurnFailed { .. } | Self::TurnFailedUnclassified { .. } => false, } @@ -647,7 +652,9 @@ impl fmt::Display for DirectTurnError { "执行通道已断开,不能自动重放未确认操作:{diagnostic}" ), Self::TimedOut { deadline } => formatter.write_str(deadline.message()), - Self::ReviewRequired { detail } => formatter.write_str(detail), + Self::ReviewRequired { detail } | Self::RepairRequired { detail } => { + formatter.write_str(detail) + } } } } diff --git a/apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectTurnError.ts b/apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectTurnError.ts index c14aec237..7e14e32c0 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectTurnError.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectTurnError.ts @@ -26,5 +26,6 @@ export type DirectTurnError = | { type: 'timedOut'; deadline: DirectTurnDeadline } | { type: 'turnInterrupted'; detail: string } | { type: 'reviewRequired'; detail: string } + | { type: 'repairRequired'; detail: string } | { type: 'turnFailed'; stage: DirectCodexFailureStage; detail: string } | { type: 'turnFailedUnclassified'; detail: string };