From df6d885d2cad7dfd509adc8988fe0c7a810cef56 Mon Sep 17 00:00:00 2001 From: Linghong Date: Mon, 7 Sep 2026 04:51:12 +0000 Subject: [PATCH] =?UTF-8?q?=E7=AD=96=E5=88=92=E4=BC=9A=E8=AF=9D=E7=9E=AC?= =?UTF-8?q?=E6=80=81Provider=E6=95=85=E9=9A=9C=E8=87=AA=E5=8A=A8=E9=80=80?= =?UTF-8?q?=E9=81=BF=E9=87=8D=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - invoke_provider_v2 错误携带主 Runtime 同款瞬态分类,timeout/connectivity/transport/空响应/反序列化/断流/上游408、429、5xx 不再直接判死 - run_turn_v2 对瞬态故障按 maxRetries 预算指数退避自动重试本回合,耗尽后才投 provider_failed 并附已重试次数,上游 4xx 硬错误仍直接失败 - 同步策划会话 Runtime V2 文档:tool_choice 口径改为 required,补充瞬态故障重试规则 --- .../runtime_protocol/planning_session_v2.rs | 73 +++++++++++++++++-- ...策划会话RuntimeV2接入与旧链路退役-2026-09-03.md | 7 +- 2 files changed, 69 insertions(+), 11 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_session_v2.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_session_v2.rs index 460c69009..499e1bd5f 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_session_v2.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_session_v2.rs @@ -841,6 +841,20 @@ fn planning_turn_result_from_llm(response: &platform_llm::LlmRunResponse) -> Pla } } +struct PlanningProviderFailureV2 { + detail: String, + transient_kind: Option<&'static str>, +} + +impl PlanningProviderFailureV2 { + fn terminal(detail: String) -> Self { + Self { + detail, + transient_kind: None, + } + } +} + async fn invoke_provider_v2( root: &Path, session: &PlanningSessionV2, @@ -850,14 +864,17 @@ async fn invoke_provider_v2( prompt: &str, context_messages: Vec, mut on_delta: F, -) -> Result +) -> Result where F: FnMut(&str, &str, Option<&str>) + Send, { - let config = load_game_creator_app_config()?; + let config = + load_game_creator_app_config().map_err(PlanningProviderFailureV2::terminal)?; let llm = resolve_game_creator_llm_config_for_agent(&config, "planning-agent-v2"); - let client = build_game_creator_llm_client_from_llm_config(&llm, "planning.v2")?; - let request = build_provider_request_v2(session, context_messages, prompt, &llm)?; + let client = build_game_creator_llm_client_from_llm_config(&llm, "planning.v2") + .map_err(PlanningProviderFailureV2::terminal)?; + let request = build_provider_request_v2(session, context_messages, prompt, &llm) + .map_err(PlanningProviderFailureV2::terminal)?; persist_planning_debug_request_v2( root, debug_call_id, @@ -891,6 +908,8 @@ where Ok(planning_turn_result_from_llm(&response)) } Err(error) => { + let transient_kind = + game_creator_agent_runtime_transient_provider_error_kind(&error, false); let detail = format!("Planning V2 Provider 流式调用失败:{error}"); persist_planning_debug_response_v2( root, @@ -901,7 +920,10 @@ where None, Some(detail.as_str()), ); - Err(detail) + Err(PlanningProviderFailureV2 { + detail, + transient_kind, + }) } } } else { @@ -926,6 +948,8 @@ where Ok(planning_turn_result_from_llm(&response)) } Err(error) => { + let transient_kind = + game_creator_agent_runtime_transient_provider_error_kind(&error, false); let detail = format!("Planning V2 Provider 调用失败:{error}"); persist_planning_debug_response_v2( root, @@ -936,7 +960,10 @@ where None, Some(detail.as_str()), ); - Err(detail) + Err(PlanningProviderFailureV2 { + detail, + transient_kind, + }) } } } @@ -1024,10 +1051,23 @@ where let mut accumulated = String::new(); let mut attempt_prompt = prompt.clone(); let mut policy_retry = 0_u8; + let mut provider_retry = 0_u32; + let mut provider_attempt = 0_u8; + let provider_retry_llm = load_game_creator_app_config() + .map(|config| resolve_game_creator_llm_config_for_agent(&config, "planning-agent-v2")) + .ok(); + let provider_max_retries = provider_retry_llm + .as_ref() + .map(|llm| llm.max_retries) + .unwrap_or(0); + let provider_retry_backoff_ms = provider_retry_llm + .as_ref() + .map(|llm| llm.retry_backoff_ms) + .unwrap_or(0); let debug_call_id = format!("call-{}", Uuid::new_v4().simple()); let policy_output = loop { accumulated.clear(); - let provider_attempt = policy_retry.saturating_add(1); + provider_attempt = provider_attempt.saturating_add(1); let provider_result = invoke_provider_v2( root, &start.session, @@ -1043,7 +1083,24 @@ where .await; let result = match provider_result { Ok(result) => result, - Err(detail) => { + Err(failure) => { + if failure.transient_kind.is_some() && provider_retry < provider_max_retries { + provider_retry = provider_retry.saturating_add(1); + let backoff_ms = game_creator_agent_runtime_transient_retry_backoff_ms( + provider_retry_backoff_ms, + provider_retry, + ); + tokio::time::sleep(std::time::Duration::from_millis(backoff_ms)).await; + continue; + } + let detail = if failure.transient_kind.is_some() && provider_max_retries > 0 { + format!( + "{}(瞬态故障已自动重试 {}/{} 次)", + failure.detail, provider_retry, provider_max_retries + ) + } else { + failure.detail + }; let error = safe_error("PROVIDER_FAILED", detail); let session = persist_turn_failure_v2( root, diff --git a/docs/technical/【技术方案】策划会话RuntimeV2接入与旧链路退役-2026-09-03.md b/docs/technical/【技术方案】策划会话RuntimeV2接入与旧链路退役-2026-09-03.md index 7f482683d..5dd7cf9fd 100644 --- a/docs/technical/【技术方案】策划会话RuntimeV2接入与旧链路退役-2026-09-03.md +++ b/docs/technical/【技术方案】策划会话RuntimeV2接入与旧链路退役-2026-09-03.md @@ -25,7 +25,7 @@ V2 复用底层能力,但不复用旧策划编排身份: - 复用 Provider 连接、Provider 流式传输能力、超时/瞬态重试、会话消息持久化、项目路径边界、单项目并发控制和原子文件写入。这里的“Provider 流式传输能力”只描述底层请求实现,不承诺把每个文本 delta 投影给前端或用户。 - 不经过 Project Supervisor,不创建 `project-planning` 子 Run,不使用 `agent.delegate`、delivery、continuation、Acceptance Graph 或 acceptance evidence。 - 当前只启用 `mode=gdd`、最多展示 8 个有效问题、GDD 审批和用户修改。 -- 问询和出稿通过两个协议 function tools(`plan_ask_question` / `plan_submit_gdd`)输出,`tool_choice=auto`;Runtime 解析工具参数后归一为 Question/Artifact,不执行工具、不把 `tool_call` 写入会话消息。 +- 问询和出稿通过两个协议 function tools(`plan_ask_question` / `plan_submit_gdd`)输出,`tool_choice=required`;Runtime 解析工具参数后归一为 Question/Artifact,不执行工具、不把 `tool_call` 写入会话消息。 - 当前不启用 MCP、Skill、第三方工具或无限问询;`capabilities.tools/skills` 仍为空,预留未来能力快照。 - 新旧会话分开持久化,不自动转换旧会话;切换时所有未完成旧会话强制失败;同一项目同一时间只允许一条策划权威会话推进。 @@ -170,7 +170,7 @@ ToolCall(toolCall) # 仅保留消息/事件类型;当前不写入 conver AssistantText(text) # 当前策略视为非法输出;未来可由 conversation 模式使用 ``` -Provider 请求携带 `plan_ask_question` 与 `plan_submit_gdd`,`tool_choice=auto`。`GddPlanningPolicy` 只接受恰好一个已知工具,并将其参数归一为 `Question` 或 `Artifact(kind=gdd)`。正文 JSON、多个工具或未知工具不写成成功产物;按输出重试策略处理,超过重试上限后进入可恢复失败状态。 +Provider 请求携带 `plan_ask_question` 与 `plan_submit_gdd`,`tool_choice=required`。`GddPlanningPolicy` 只接受恰好一个已知工具,并将其参数归一为 `Question` 或 `Artifact(kind=gdd)`。正文 JSON、多个工具或未知工具不写成成功产物;按输出重试策略处理,超过重试上限后进入可恢复失败状态。 ## 4. 会话与状态合同 @@ -385,6 +385,7 @@ questionCount=8,本次返回 question - Provider 请求失败不增加 `questionCount`,也不创建 GDD 版本。 - 用户修改不受 `questionLimit` 限制,但修改回合仍不能再次向用户展示 question;若 Provider 返回 question,按一次内部出稿重试处理。 - 达到内部输出重试上限后,保留当前会话和错误摘要,允许用户再次提交或恢复,不伪造 GDD。 +- 瞬态 Provider 故障不直接判死:timeout、connectivity、transport、空响应、反序列化失败、流式中途断连和上游 408/429/5xx 统一按主 Agent Runtime 同款分类判定为瞬态,会话层沿用该 Agent 的 `maxRetries` / `retryBackoffMs` 预算做指数退避自动重试本回合;重试不增加 `questionCount`、不产生 GDD 版本,debug `attempt-N` 随每次物理尝试递增。耗尽后才投影 `provider_failed`,错误摘要附带已重试次数;上游 4xx 等硬错误不进入该重试,直接 `provider_failed`。 ## 5. Provider、上下文与未来 MCP/Skill 兼容性 @@ -737,7 +738,7 @@ hydrate_planning_session_v2 - 对已存在 V2 Session 的项目,打开项目时先 hydrate V2;没有 V2 authority 的旧项目继续走旧读取路径,避免误把旧项目数据当成 V2。 - P3 已完成;P4 的真实 Provider、前端工作台、失败恢复和安全门禁验收已通过。旧会话 `legacy_retired` 封存与旧入口彻底关闭仍属于 P5。 -P3 之后的协议修正:V2 不再用正文 JSON 输出问询/GDD;Provider 请求挂 `plan_ask_question` / `plan_submit_gdd`,`tool_choice=auto`,形状由工具 schema 承担。system prompt 只保留三项核心闭环等策略和当前问询进度;数量和字数由既有校验器在失败时回灌。`plan_submit_gdd` 的工具参数只包含决定和原型验证内容,不包含任何 Runtime 分配的 ID;Runtime 在落盘前为决定分配首项 `initial-request`、后续 `decision-{序号}`,并按 `prototype_pending` 顺序绑定原型验证项。入参不必回声 `schemaVersion`,落盘 GDD 仍写 `plan-gdd.v2`。失败结果不重复渲染,严格解析和失败不落盘成功产物的规则保持不变。历史 V2 数据不做兼容转换,按现有恢复/失败策略处理。 +P3 之后的协议修正:V2 不再用正文 JSON 输出问询/GDD;Provider 请求挂 `plan_ask_question` / `plan_submit_gdd`,`tool_choice=required`,形状由工具 schema 承担。system prompt 只保留三项核心闭环等策略和当前问询进度;数量和字数由既有校验器在失败时回灌。`plan_submit_gdd` 的工具参数只包含决定和原型验证内容,不包含任何 Runtime 分配的 ID;Runtime 在落盘前为决定分配首项 `initial-request`、后续 `decision-{序号}`,并按 `prototype_pending` 顺序绑定原型验证项。入参不必回声 `schemaVersion`,落盘 GDD 仍写 `plan-gdd.v2`。失败结果不重复渲染,严格解析和失败不落盘成功产物的规则保持不变。历史 V2 数据不做兼容转换,按现有恢复/失败策略处理。 ### P4:灰度、真实 Provider 与回归验收(已完成)