宿主:封口返修要求改成 typed 控制流,不再写成失败

- 新增 `HostOutcomeText`:封口复核的"继续返修批次"用独立变体表达,不再伪装成 `LlmError::InvalidRequest`
- 新增 `DirectTurnRunFailure`:app-server 回合结果区分"真失败"与"返修控制流",`RepairRequired` 不写终态
- `direct_turn_terminal_write` 收口终态写出:返修要求跳过,真失败从 typed 错误投影 `kind` / `message`
- 早退取得宿主收尾事实的分支只认真失败,返修控制流不再被中断成一次收束
- `DirectTurnError` 新增 `RepairRequired` 变体并重新生成前端绑定
- 返修循环同时消费 `ReviewRequired` / `RepairRequired`,次数上限仍留在产生侧
- 补单测:返修要求不写终态、真失败投影成 transport-failed、正常收尾不带载荷
This commit is contained in:
2026-09-24 13:40:36 +08:00
parent 0da63721df
commit 64e3eba55a
5 changed files with 285 additions and 95 deletions
@@ -78,12 +78,41 @@ pub(super) enum HostOutcome {
RepairRequired,
}
pub(super) fn outcome_text(outcome: HostOutcome) -> Result<String, platform_llm::LlmError> {
/// [`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<String, super::DirectTurnRunFailure> {
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(),
},
}
}
File diff suppressed because it is too large Load Diff
@@ -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 分类决定诊断摘要与建议,不再降级成文本。
@@ -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)
}
}
}
}
@@ -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 };