策划会话瞬态Provider故障自动退避重试
- invoke_provider_v2 错误携带主 Runtime 同款瞬态分类,timeout/connectivity/transport/空响应/反序列化/断流/上游408、429、5xx 不再直接判死 - run_turn_v2 对瞬态故障按 maxRetries 预算指数退避自动重试本回合,耗尽后才投 provider_failed 并附已重试次数,上游 4xx 硬错误仍直接失败 - 同步策划会话 Runtime V2 文档:tool_choice 口径改为 required,补充瞬态故障重试规则
This commit is contained in:
+65
-8
@@ -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<F>(
|
||||
root: &Path,
|
||||
session: &PlanningSessionV2,
|
||||
@@ -850,14 +864,17 @@ async fn invoke_provider_v2<F>(
|
||||
prompt: &str,
|
||||
context_messages: Vec<platform_llm::LlmMessage>,
|
||||
mut on_delta: F,
|
||||
) -> Result<PlanningTurnResultV2, String>
|
||||
) -> Result<PlanningTurnResultV2, PlanningProviderFailureV2>
|
||||
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,
|
||||
|
||||
@@ -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 与回归验收(已完成)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user