Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2e15a70b14 |
@@ -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,
|
||||
|
||||
+16
-8
@@ -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` 不得携带网格尺寸,二者矛盾同样在副作用前失败关闭。
|
||||
|
||||
Reference in New Issue
Block a user