Compare commits

...

1 Commits

Author SHA1 Message Date
kdletters 2e15a70b14 Provider 瞬态重试次数严格按设置执行
Project CI / Frontend tests (push) Waiting to run
Project CI / AI game creator shell Rust smoke (push) Waiting to run
Project CI / AI game creator shell Rust crates (push) Waiting to run
Project CI / Backend tests (push) Waiting to run
Project CI / Native shell tests (push) Waiting to run
Project CI / Repository checks (push) Waiting to run
Project CI / AI game creator shell web tests (push) Waiting to run
Project CI / AI game creator shell Rust shard 1/4 (push) Has started running
Project CI / AI game creator shell Rust shard 2/4 (push) Has started running
Project CI / AI game creator shell Rust shard 3/4 (push) Has started running
Project CI / AI game creator shell Rust shard 4/4 (push) Has started running
删除自主构建档位 12~16 的重试区间与 standard 档位 3 次上限,maxRetries 严格决定物理重试次数
上游 400 不再单独收窄到 2 次,与其它瞬态错误共用设置里的同一份预算
运行档位改为只决定上游 400 是否算瞬态,重试次数不再被档位改写
重写档位重试用例为设置值直取,同步 upstream-400 用例
decision-log 记录本次口径变更与本机既有失败边界
2026-09-18 00:01:40 +08:00
6 changed files with 88 additions and 84 deletions
@@ -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;
@@ -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,
};
@@ -717,14 +717,14 @@ where
Fut: std::future::Future<Output = Result<platform_llm::LlmRunResponse, platform_llm::LlmError>>,
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<Option<platform_llm::LlmRunResponse>, 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,
@@ -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<u32, String> {
) -> Result<AgentRuntimeProviderTransientRetryPolicy, String> {
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,
})
}
@@ -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::<Vec<_>>();
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| {
@@ -3,12 +3,19 @@
> 用途:记录已经确认、会影响后续开发的长期技术/产品/协作决策。短期讨论不要写在这里。
> 当前口径:历史条目的旧路径、旧版本和已退役对象只用于追溯,不构成现行实现依据;如与当前代码或 `docs/README.md` 冲突,以当前代码和最新专题文档为准。
## 2026-09-18 Provider 瞬态重试次数严格按设置执行(游戏开发 Agent 与策划 Agent 不再被档位收进区间)
- 背景:AGC 客户端此前把 `agentLlm.<agent>.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.<agent>.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` 不得携带网格尺寸,二者矛盾同样在副作用前失败关闭。