diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver.rs index 02b48e807..9cc2fb6eb 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver.rs @@ -416,10 +416,6 @@ pub(super) const AGENT_RUNTIME_REAL_E2E_TOOL_PLAN_CHECKPOINT_POLL_MS: u64 = 50; pub(super) const AGENT_RUNTIME_REAL_E2E_TOOL_PLAN_CHECKPOINT_MAX_TTL_MS: u64 = 10 * 60 * 1_000; pub(super) const AGENT_RUNTIME_REAL_E2E_TOOL_PLAN_CHECKPOINT_ERROR: &str = "agent-runtime-real-e2e-tool-plan-handoff-checkpoint-needs-reconciliation"; -pub(super) const AGENT_RUNTIME_PROVIDER_TRANSIENT_RETRY_LIMIT: u32 = 3; -pub(super) const AGENT_RUNTIME_AUTONOMOUS_PROVIDER_TRANSIENT_RETRY_FLOOR: u32 = 12; -pub(super) const AGENT_RUNTIME_AUTONOMOUS_PROVIDER_TRANSIENT_RETRY_LIMIT: u32 = 16; -pub(crate) const AGENT_RUNTIME_AUTONOMOUS_PROVIDER_UPSTREAM_400_RETRY_LIMIT: u32 = 2; pub(super) const AGENT_RUNTIME_AUTONOMOUS_TOOL_PLAN_FORMAT_REPAIR_ATTEMPTS: usize = 4; pub(super) const AGENT_RUNTIME_AUTONOMOUS_FORCED_ACTION_MAX_OUTPUT_TOKENS: u32 = 2_000; pub(crate) const AGENT_RUNTIME_AUTONOMOUS_SCAFFOLD_MAX_OUTPUT_TOKENS: u32 = 2_600; diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol.rs index 65b883879..0bae6dc31 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol.rs @@ -84,7 +84,7 @@ pub(crate) use response_stream::{ pub(crate) use run_configuration::{ agent_runtime_run_profile_identity_at, bind_game_creator_agent_runtime_run_profile_at, game_creator_agent_runtime_project_revision_path, - game_creator_agent_runtime_provider_transient_max_retries_at, + game_creator_agent_runtime_provider_transient_retry_policy_at, game_creator_agent_runtime_run_profile_binding_path, read_game_creator_agent_runtime_run_profile_binding, }; diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/provider_retry.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/provider_retry.rs index 1541acc41..1f2be5915 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/provider_retry.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/provider_retry.rs @@ -717,14 +717,14 @@ where Fut: std::future::Future>, H: FnOnce(&platform_llm::LlmRunResponse) -> platform_llm::LlmRunResponse, { - let max_retries = game_creator_agent_runtime_provider_transient_max_retries_at( + let retry_policy = game_creator_agent_runtime_provider_transient_retry_policy_at( root, &provider_snapshot.agent_id, &provider_snapshot.run_id, llm.max_retries, )?; - let retry_autonomous_upstream_400 = - max_retries >= AGENT_RUNTIME_AUTONOMOUS_PROVIDER_TRANSIENT_RETRY_FLOOR; + let max_retries = retry_policy.max_retries; + let retry_autonomous_upstream_400 = retry_policy.retry_upstream_400; let identity = game_creator_agent_runtime_provider_retry_identity_for_mode( provider_snapshot, llm, @@ -1365,17 +1365,9 @@ where )?; return Err("Provider 瞬态错误编码损坏".to_string()); }; - let error_max_retries = if error_kind == "upstream-400" { - effective_max_retries - .min(AGENT_RUNTIME_AUTONOMOUS_PROVIDER_UPSTREAM_400_RETRY_LIMIT) - } else { - effective_max_retries - }; - if existing - .as_ref() - .is_some_and(|record| record.max_retries != error_max_retries) - || attempt >= error_max_retries - { + // 所有瞬态错误共用设置里的重试预算,上游 400 不再单独收窄上限。 + let error_max_retries = effective_max_retries; + if attempt >= error_max_retries { crate::provider_retry::remove_at( root, &provider_snapshot.agent_id, @@ -1517,14 +1509,14 @@ pub(in crate::agent) async fn request_game_creator_agent_runtime_llm_with_transi operation: &str, request: &LlmRunRequest, ) -> Result, String> { - let max_retries = game_creator_agent_runtime_provider_transient_max_retries_at( + let retry_policy = game_creator_agent_runtime_provider_transient_retry_policy_at( root, &provider_snapshot.agent_id, &provider_snapshot.run_id, llm.max_retries, )?; - let retry_autonomous_upstream_400 = - max_retries >= AGENT_RUNTIME_AUTONOMOUS_PROVIDER_TRANSIENT_RETRY_FLOOR; + let max_retries = retry_policy.max_retries; + let retry_autonomous_upstream_400 = retry_policy.retry_upstream_400; for attempt in 0..=max_retries { let request_slot = if attempt == 0 { provider_snapshot.request_slot.clone() @@ -1585,11 +1577,8 @@ pub(in crate::agent) async fn request_game_creator_agent_runtime_llm_with_transi let Some((error_kind, public_error)) = encoded.split_once('\n') else { return Err("Provider 瞬态错误编码损坏".to_string()); }; - let error_max_retries = if error_kind == "upstream-400" { - max_retries.min(AGENT_RUNTIME_AUTONOMOUS_PROVIDER_UPSTREAM_400_RETRY_LIMIT) - } else { - max_retries - }; + // 所有瞬态错误共用设置里的重试预算,上游 400 不再单独收窄上限。 + let error_max_retries = max_retries; if attempt >= error_max_retries { return Err(game_creator_agent_runtime_provider_retry_exhausted_error( public_error, diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/run_configuration.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/run_configuration.rs index 61396cd9e..7d560ad5f 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/run_configuration.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/run_configuration.rs @@ -395,12 +395,22 @@ pub(crate) fn agent_runtime_run_profile_identity_at( Ok((profile, String::new())) } -pub(crate) fn game_creator_agent_runtime_provider_transient_max_retries_at( +/// 当前持久 run 的 Provider 瞬态重试策略。 +/// +/// 重试次数严格使用设置值:运行档位不再把 `maxRetries` 收进固定区间, +/// 只决定上游 400 是否算瞬态错误。 +#[derive(Debug)] +pub(crate) struct AgentRuntimeProviderTransientRetryPolicy { + pub(crate) max_retries: u32, + pub(crate) retry_upstream_400: bool, +} + +pub(crate) fn game_creator_agent_runtime_provider_transient_retry_policy_at( root: &Path, agent_id: &str, run_id: &str, configured_max_retries: u32, -) -> Result { +) -> Result { let agent_id = normalize_game_creator_runtime_agent_id(agent_id)?; let stored_identity = read_latest_game_creator_agent_runtime_task_by_run_id(root, &agent_id, run_id)? @@ -416,10 +426,8 @@ pub(crate) fn game_creator_agent_runtime_provider_transient_max_retries_at( stored_profile, stored_binding_fingerprint, )?; - if profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD { - return Ok(configured_max_retries - .max(AGENT_RUNTIME_AUTONOMOUS_PROVIDER_TRANSIENT_RETRY_FLOOR) - .min(AGENT_RUNTIME_AUTONOMOUS_PROVIDER_TRANSIENT_RETRY_LIMIT)); - } - Ok(configured_max_retries.min(AGENT_RUNTIME_PROVIDER_TRANSIENT_RETRY_LIMIT)) + Ok(AgentRuntimeProviderTransientRetryPolicy { + max_retries: configured_max_retries, + retry_upstream_400: profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD, + }) } diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/provider.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/provider.rs index 7bc72b7af..279b3e743 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/provider.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/provider.rs @@ -662,7 +662,7 @@ async fn chat_with_game_creator_role_agent_stream_does_not_fallback_on_upstream_ } #[test] -fn autonomous_game_build_profile_uses_durable_provider_retry_floor_and_cap() { +fn provider_transient_retry_uses_configured_max_retries_for_every_run_profile() { let root = unique_project_path(); init_local_game_project_at( &root, @@ -671,16 +671,16 @@ fn autonomous_game_build_profile_uses_durable_provider_retry_floor_and_cap() { ) .expect("project init"); - assert_eq!( - game_creator_agent_runtime_provider_transient_max_retries_at( - &root, - "design-director", - "legacy-standard-run", - 99, - ) - .expect("legacy standard retry policy"), - 3 - ); + // 历史 standard run 仍走同一身份校验,但重试次数不再被收进区间。 + let legacy_standard = game_creator_agent_runtime_provider_transient_retry_policy_at( + &root, + "design-director", + "legacy-standard-run", + 99, + ) + .expect("legacy standard retry policy"); + assert_eq!(legacy_standard.max_retries, 99); + assert!(!legacy_standard.retry_upstream_400); let standard = bind_game_creator_agent_runtime_run_profile_at( &root, "design-director", @@ -691,25 +691,25 @@ fn autonomous_game_build_profile_uses_durable_provider_retry_floor_and_cap() { ) .expect("bind standard profile"); assert_eq!( - game_creator_agent_runtime_provider_transient_max_retries_at( + game_creator_agent_runtime_provider_transient_retry_policy_at( &root, &standard.agent_id, &standard.run_id, 0, ) - .expect("standard zero retry policy"), + .expect("standard zero retry policy") + .max_retries, 0 ); - assert_eq!( - game_creator_agent_runtime_provider_transient_max_retries_at( - &root, - &standard.agent_id, - &standard.run_id, - 99, - ) - .expect("standard capped retry policy"), - 3 - ); + let standard_configured = game_creator_agent_runtime_provider_transient_retry_policy_at( + &root, + &standard.agent_id, + &standard.run_id, + 99, + ) + .expect("standard configured retry policy"); + assert_eq!(standard_configured.max_retries, 99); + assert!(!standard_configured.retry_upstream_400); let parent = bind_game_creator_agent_runtime_run_profile_at( &root, @@ -720,17 +720,16 @@ fn autonomous_game_build_profile_uses_durable_provider_retry_floor_and_cap() { None, ) .expect("bind autonomous parent profile"); - for (configured, expected) in [(0, 12), (14, 14), (99, 16)] { - assert_eq!( - game_creator_agent_runtime_provider_transient_max_retries_at( - &root, - &parent.agent_id, - &parent.run_id, - configured, - ) - .expect("autonomous parent retry policy"), - expected - ); + for (configured, expected) in [(0, 0), (5, 5), (99, 99)] { + let policy = game_creator_agent_runtime_provider_transient_retry_policy_at( + &root, + &parent.agent_id, + &parent.run_id, + configured, + ) + .expect("autonomous parent retry policy"); + assert_eq!(policy.max_retries, expected); + assert!(policy.retry_upstream_400); } let child_link = AgentRuntimeTaskLink { @@ -758,14 +757,25 @@ fn autonomous_game_build_profile_uses_durable_provider_retry_floor_and_cap() { append_game_creator_agent_runtime_task(&root, &child_state) .expect("append autonomous child task projection"); assert_eq!( - game_creator_agent_runtime_provider_transient_max_retries_at( + game_creator_agent_runtime_provider_transient_retry_policy_at( &root, &child.agent_id, &child.run_id, 0, ) - .expect("autonomous child retry policy"), - 12 + .expect("autonomous child retry policy") + .max_retries, + 0 + ); + assert!( + game_creator_agent_runtime_provider_transient_retry_policy_at( + &root, + &child.agent_id, + &child.run_id, + 0, + ) + .expect("autonomous child retry policy") + .retry_upstream_400 ); fs::remove_file(game_creator_agent_runtime_run_profile_binding_path( @@ -775,7 +785,7 @@ fn autonomous_game_build_profile_uses_durable_provider_retry_floor_and_cap() { )) .expect("remove autonomous child binding"); assert!( - game_creator_agent_runtime_provider_transient_max_retries_at( + game_creator_agent_runtime_provider_transient_retry_policy_at( &root, &child.agent_id, &child.run_id, @@ -4558,7 +4568,7 @@ async fn provider_transient_retry_provider_error_is_not_retried() { } #[tokio::test] -async fn provider_transient_retry_autonomous_upstream_400_retries_with_bounded_budget() { +async fn provider_transient_retry_autonomous_upstream_400_uses_configured_budget() { let root = unique_project_path(); init_local_game_project_at(&root, "project-1", "自主构建 Provider 400 重试测试") .expect("project init"); @@ -4631,7 +4641,7 @@ async fn provider_transient_retry_autonomous_upstream_400_retries_with_bounded_b "model": "supervisor-autonomous-upstream-400-model", "apiKind": "openai_chat", "stream": false, - "maxRetries": 0, + "maxRetries": 2, "retryBackoffMs": 1 }} }} @@ -4676,10 +4686,7 @@ async fn provider_transient_retry_autonomous_upstream_400_retries_with_bounded_b .expect("initial autonomous upstream 400 request"); assert_eq!(waiting.error_kind, "upstream-400"); assert_eq!(waiting.next_attempt, 1); - assert_eq!( - waiting.max_retries, - AGENT_RUNTIME_AUTONOMOUS_PROVIDER_UPSTREAM_400_RETRY_LIMIT - ); + assert_eq!(waiting.max_retries, 2); provider_retry::force_provider_retry_due_for_test_at(&root, &waiting.identity) .expect("force autonomous upstream 400 retry due"); @@ -4725,10 +4732,7 @@ async fn provider_transient_retry_autonomous_upstream_400_retries_with_bounded_b .collect::>(); assert_eq!(retry_audits.len(), 1); assert_eq!(retry_audits[0]["errorKind"], "upstream-400"); - assert_eq!( - retry_audits[0]["maxRetries"], - AGENT_RUNTIME_AUTONOMOUS_PROVIDER_UPSTREAM_400_RETRY_LIMIT - ); + assert_eq!(retry_audits[0]["maxRetries"], 2); let lifecycle = records .iter() .filter(|record| { diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 0be34b555..ccd014d82 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -3,12 +3,19 @@ > 用途:记录已经确认、会影响后续开发的长期技术/产品/协作决策。短期讨论不要写在这里。 > 当前口径:历史条目的旧路径、旧版本和已退役对象只用于追溯,不构成现行实现依据;如与当前代码或 `docs/README.md` 冲突,以当前代码和最新专题文档为准。 +## 2026-09-18 Provider 瞬态重试次数严格按设置执行(游戏开发 Agent 与策划 Agent 不再被档位收进区间) + +- 背景:AGC 客户端此前把 `agentLlm..maxRetries` 按运行档位重新收进固定区间——`autonomous-game-build` 档位(自主构建的游戏开发 Agent 及其继承档位的专业子 Agent)被抬到 12~16,`standard` 档位(含立项策划入口的策划 Agent 与普通 Agent 对话)被压到最多 3;瞬态分类里的上游 400 还在同一预算上再收窄到 2 次。现场把 `maxRetries` 设成 5 时,游戏开发与策划两条链路都不按设置执行。 +- 决策:删除这三处区间限制,`maxRetries` 严格等于允许的物理重试次数(显式 `0` 仍表示不重试)。运行档位只保留「上游 400 是否算瞬态错误」的判定(autonomous 档位才重试 400),不再改写次数:`AGENT_RUNTIME_PROVIDER_TRANSIENT_RETRY_LIMIT`、`AGENT_RUNTIME_AUTONOMOUS_PROVIDER_TRANSIENT_RETRY_FLOOR`、`AGENT_RUNTIME_AUTONOMOUS_PROVIDER_TRANSIENT_RETRY_LIMIT`、`AGENT_RUNTIME_AUTONOMOUS_PROVIDER_UPSTREAM_400_RETRY_LIMIT` 四个常量与 `game_creator_agent_runtime_provider_transient_max_retries_at` 一并删除,换成返回 `{max_retries, retry_upstream_400}` 的 `game_creator_agent_runtime_provider_transient_retry_policy_at`。 +- 边界:只改重试次数的来源。瞬态错误分类(`timeout / connectivity / transport / 408 / 429 / 5xx / empty-response / deserialize / stream-unavailable`,以及 autonomous 档位下的 400)、`retryBackoffMs` 指数退避(仍封顶 30s)、durable retry sidecar / lifecycle / `-transient-N` slot 身份、cancel / steer / goal 门禁、耗尽后的 reconciliation 口径与「泥点不足不重试」都不变;前端设置面板与 `agentLlm..maxRetries` 契约不变。 +- 验证方式:`provider_transient_retry_` 7 项中重写后的档位用例与 upstream-400 用例通过(断言 `maxRetries` 直取设置值、400 与其它瞬态共用同一预算),`provider_retry_` 其余 26/28 通过;该组 2 项(`provider_transient_retry_transport_failure_closes_then_stable_retry_succeeds`、`provider_transient_retry_backoff_is_exponential_and_capped_at_thirty_seconds`)与 `provider_retry_waiting_final_reply_*` 2 项在本机改动前后同为失败(`stash` 基线复跑确认,现象是等待自动重试唤醒超时)。本机串行全量套件另有既有环境失败(`tempfile::tempdir()` 归属校验、缺少 npm 构建产物、Windows 启动失败 MessageBox 阻塞 `startup_log_slot_fail_without_path...`);抽查其中 5 项在 `stash` 基线上同样失败,与本次改动无关。仓库 `cargo fmt --check`、`npm run check:encoding`、`git diff --check` 通过。 +- 关联文档:[AI游戏创作智能体App实施计划](../../technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md)、[踩坑记录](pitfalls.md)。 + ## 2026-09-17 AGC 抠图提交使用远端画布项目身份 - 背景:AGC 已通过本地项目 ID 建立并持久化本地项目到主站远端画布项目的绑定,但 `agc_remove_background` 提交请求仍把本地 `manifest.project_id` 放入 `projectId`;`assetFolderId` 已使用远端素材目录 ID。主站因此按项目不存在或不属于当前账号返回 404,主站抠图和 BgFilter 本身均正常。 - 决策:抠图请求及工具回执统一使用 `prepare_external_canvas_generation_context` 返回的远端 `context.project_id`;本地 manifest 项目 ID 只用于绑定键和本地状态,不得作为主站业务请求的 `projectId`。 - 验证:客户端定向 Rust 测试、格式、编码和 diff 检查通过;未修改主站路由或 BgFilter。 - ## 2026-09-17 图集切分模式改为显式声明 - 决策:`sliceMode` 在图标图集生成入口成为必填字段且不保留任何默认值。省略、`null` 或空字符串必须在引用解析、定价、入队和 provider / OSS 副作用之前返回 `400`(`field=sliceMode`);`grid` 必须同时提供 `gridX`/`gridY`,`connected-components` 不得携带网格尺寸,二者矛盾同样在副作用前失败关闭。