diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/lifecycle_control.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/lifecycle_control.rs index 1a99dd10c..4a8869bbc 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/lifecycle_control.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/lifecycle_control.rs @@ -807,7 +807,7 @@ pub(crate) fn retry_game_creator_agent_runtime_task_at( if game_chat_dynamic_art_child_structural_identity_at(root, &task)? { return Err( AgentRuntimeRetryErrorKind::GameChatDynamicArtRetryUnsupported.wire_error( - "game-chat 动态美术 child 不支持通用 Agent Runtime retry;请回到当前 code-prototype,先完成 asset.list 审计,再按真实缺口建立新的 durable 美术委派", + "game-chat 动态美术 child 不支持通用 Agent Runtime retry;请继续 game-chat 对话,由下一轮 main code-prototype 重新完成 asset.list 审计,再按仍存在的缺口建立新的 durable 美术委派", ), ); } diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop_tests.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop_tests.rs index f26e6fa4d..5175b1eca 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop_tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop_tests.rs @@ -500,7 +500,8 @@ fn game_chat_dynamic_art_terminal_children_reject_generic_retry_without_durable_ error.contains("kind=game-chat-dynamic-art-retry-unsupported"), "unexpected typed retry error: {error}" ); - assert!(error.contains("code-prototype"), "{error}"); + assert!(error.contains("继续 game-chat 对话"), "{error}"); + assert!(error.contains("下一轮 main code-prototype"), "{error}"); assert!(error.contains("asset.list"), "{error}"); assert_eq!( snapshot_agent_durable_files(&root), @@ -674,6 +675,13 @@ async fn legacy_game_chat_dynamic_art_retry_runs_are_read_only_and_fail_closed() forged_observation.summary.contains("不支持通用 retry"), "{forged_observation:?}" ); + assert!( + forged_observation + .detail + .as_deref() + .is_some_and(|detail| detail.contains("下一轮 main code-prototype")), + "{forged_observation:?}" + ); assert!(!root.join("assets/forged-retry-must-not-write.txt").exists()); } @@ -880,6 +888,260 @@ fn game_chat_dynamic_art_retry_guard_preserves_dag_art_and_non_art_delegated_ret drop(non_art_lane); } +#[test] +fn game_chat_failed_art_child_can_only_redelegate_in_the_next_main_round() { + for (case_index, terminal_status) in ["failed", "cancelled"].into_iter().enumerate() { + let temporary = tempfile::tempdir().expect("create cross-round art recovery root"); + let root = temporary.path().join("project"); + init_local_game_project_at(&root, "game-chat-art-child", "单主美术委派") + .expect("pre-initialize cross-round art recovery project"); + let _main_runtime_lane = + try_acquire_game_creator_agent_runtime_task_lock(&root, "code-prototype") + .expect("acquire cross-round main runtime lane") + .expect("cross-round main runtime lane is free"); + let _art_runtime_lane = + try_acquire_game_creator_agent_runtime_task_lock(&root, "art-asset-plan") + .expect("acquire cross-round art runtime lane") + .expect("cross-round art runtime lane is free"); + let (mut first_main, mut terminal_child, first_delegation_id) = + game_chat_main_art_child_fixture(&root, "art-asset-plan", &["core-spritesheet"]); + first_main.status = "running".to_string(); + first_main.phase = "waiting-for-delegate-receipts".to_string(); + first_main.current_action = "等待失败美术 child 回执".to_string(); + append_game_creator_agent_runtime_task(&root, &first_main) + .expect("persist first waiting main task"); + write_game_creator_agent_runtime_state(&root, &first_main) + .expect("persist first waiting main state"); + + terminal_child.status = terminal_status.to_string(); + terminal_child.phase = terminal_status.to_string(); + terminal_child.current_action = "美术 child 未能交付图集".to_string(); + terminal_child.error = + (terminal_status == "failed").then(|| "kind=cross-round-art-child-failure".to_string()); + append_game_creator_agent_runtime_task(&root, &terminal_child) + .expect("persist terminal first-round art child"); + write_game_creator_agent_runtime_state(&root, &terminal_child) + .expect("persist terminal first-round art child state"); + let terminal_task = read_latest_game_creator_agent_runtime_task_by_run_id( + &root, + &terminal_child.agent_id, + &terminal_child.run_id, + ) + .expect("read terminal first-round art child") + .expect("terminal first-round art child exists"); + publish_game_creator_agent_delegate_result( + &root, + &terminal_task, + Some("美术 child 未完成,等待主 Agent 认领失败回执"), + ); + let ready_delivery = read_static_delegate_delivery_at(&root, &first_delegation_id) + .expect("read ready failed art delivery") + .expect("failed art delivery exists"); + assert_eq!(ready_delivery.status, StaticDelegateDeliveryStatus::Ready); + assert_eq!( + ready_delivery.terminal_status.as_deref(), + Some(terminal_status) + ); + let claimed = claim_ready_static_delegate_receipts_at( + &root, + "code-prototype", + &first_main.run_id, + &format!("cross-round-art-claim-{case_index}"), + ) + .expect("claim failed art delivery"); + assert_eq!(claimed.len(), 1); + let claimed_delivery = read_static_delegate_delivery_at(&root, &first_delegation_id) + .expect("read claimed failed art delivery") + .expect("claimed failed art delivery exists"); + assert_eq!( + claimed_delivery.status, + StaticDelegateDeliveryStatus::ClaimedByParent + ); + + let same_run_action_id = format!("same-run-art-redelegate-{case_index}"); + let same_run = observe_agent_runtime_agent_delegate( + &root, + "code-prototype", + &first_main.run_id, + Some(&same_run_action_id), + &serde_json::json!({ + "agentId": "art-asset-plan", + "task": "同一 main run 不得再次尝试同一缺口", + "acceptanceCriteria": ["不应创建第二个 child"], + "expectedArtifacts": [AGENT_RUNTIME_ART_SPRITESHEET_PATH], + "runId": null + }), + ); + assert_eq!(same_run.status, "failed", "{same_run:?}"); + assert!(same_run.summary.contains("最多委派一次"), "{same_run:?}"); + let rejected_delegation_id = agent_runtime_delegation_id( + "code-prototype", + &first_main.run_id, + "art-asset-plan", + &same_run_action_id, + ); + assert!( + read_static_delegate_delivery_at(&root, &rejected_delegation_id) + .expect("inspect rejected same-run delivery") + .is_none(), + "same-run rejection must not create a second delivery" + ); + + first_main.status = "failed".to_string(); + first_main.phase = "failed".to_string(); + first_main.current_action = "本轮在失败美术回执后结束".to_string(); + append_game_creator_agent_runtime_task(&root, &first_main) + .expect("persist terminal first main task"); + write_game_creator_agent_runtime_state(&root, &first_main) + .expect("persist terminal first main state"); + let first_root_task = read_latest_game_creator_agent_runtime_task_by_run_id( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + "game-chat-main-art-root", + ) + .expect("read first game-chat root") + .expect("first game-chat root exists"); + let mut first_root_state = agent_runtime_state_from_task_record(&first_root_task); + first_root_state.status = "failed".to_string(); + first_root_state.phase = "failed".to_string(); + first_root_state.current_action = "等待用户继续 game-chat 对话".to_string(); + append_game_creator_agent_runtime_task(&root, &first_root_state) + .expect("persist terminal first root task"); + write_game_creator_agent_runtime_state(&root, &first_root_state) + .expect("persist terminal first root state"); + + let next_root_run_id = format!("game-chat-cross-round-recovery-root-{case_index}"); + let (next_root, mut next_main) = queue_game_chat_fast_path_child( + &root, + &next_root_run_id, + "继续 game-chat 对话并重新审计仍存在的美术缺口", + "code-prototype", + ); + next_main.status = "running".to_string(); + next_main.phase = "planning".to_string(); + next_main.current_task = "下一轮重新审计并补齐仍存在的美术缺口".to_string(); + next_main.current_action = "重新执行 asset.list 审计".to_string(); + next_main + .recent_tool_calls + .push(AgentRuntimeToolCallRecord { + action_id: Some(format!("next-round-asset-list-{case_index}")), + tool: "asset.list".to_string(), + status: "ok".to_string(), + action_fingerprint: None, + input_summary: None, + reason: Some("下一轮重新审计当前项目资产".to_string()), + summary: "已确认核心图集仍缺失".to_string(), + detail: None, + updated_at: unix_timestamp(), + }); + append_game_creator_agent_runtime_task(&root, &next_main) + .expect("persist next-round main asset audit task"); + write_game_creator_agent_runtime_state(&root, &next_main) + .expect("persist next-round main asset audit state"); + let audited_runtime = read_game_creator_agent_runtime_for_session_at( + &root, + "code-prototype", + Some(&next_main.session_id), + ) + .expect("read next-round audited main runtime") + .state; + assert_eq!(audited_runtime.run_id, next_main.run_id); + persist_game_chat_supervisor_workflow_decision_at( + &root, + &next_root.agent_id, + &next_root.run_id, + GAME_CHAT_WORKFLOW_STRATEGY_AUDIT_EXISTING_FIRST, + "下一轮继续补齐仍存在的核心图集缺口", + ) + .expect("persist next-round Supervisor decision"); + let next_coverage = + game_chat_current_asset_coverage_at(&root, &next_root.run_id, &next_main.run_id) + .expect("calculate next-round asset coverage"); + assert_eq!(next_coverage.missing_slots, vec!["core-spritesheet"]); + persist_game_chat_code_asset_route_at( + &root, + &next_main.agent_id, + &next_main.run_id, + GAME_CHAT_ASSET_ROUTE_GENERATE_MISSING, + &next_coverage.missing_slots, + ) + .expect("persist next-round asset route"); + + let next_action_id = format!("next-round-art-delegate-{case_index}"); + let next_round = observe_agent_runtime_agent_delegate( + &root, + "code-prototype", + &next_main.run_id, + Some(&next_action_id), + &serde_json::json!({ + "agentId": "art-asset-plan", + "task": "补齐下一轮审计确认仍缺失的核心图集", + "acceptanceCriteria": ["只写入受委派的 assets 产物并回执"], + "expectedArtifacts": [AGENT_RUNTIME_ART_SPRITESHEET_PATH], + "runId": null + }), + ); + assert_eq!(next_round.status, "ok", "{next_round:?}"); + let next_delegation_id = agent_runtime_delegation_id( + "code-prototype", + &next_main.run_id, + "art-asset-plan", + &next_action_id, + ); + assert_ne!(next_delegation_id, first_delegation_id); + let next_delivery = read_static_delegate_delivery_at(&root, &next_delegation_id) + .expect("read next-round art delivery") + .expect("next-round art delivery exists"); + let next_child = read_latest_game_creator_agent_runtime_task_by_delegation_id( + &root, + "art-asset-plan", + &next_delegation_id, + ) + .expect("read next-round art child") + .expect("next-round art child exists"); + assert_eq!( + next_delivery.status, + StaticDelegateDeliveryStatus::Dispatched + ); + assert_eq!(next_delivery.parent_run_id, next_main.run_id); + assert_eq!(next_delivery.target_run_id, next_child.run_id); + assert_eq!(next_delivery.target_agent_id, "art-asset-plan"); + assert_eq!( + next_delivery.expected_artifacts, + [AGENT_RUNTIME_ART_SPRITESHEET_PATH.to_string()] + ); + assert!(next_delivery.terminal_status.is_none()); + + let mut next_child_state = agent_runtime_state_from_task_record(&next_child); + next_child_state.status = "running".to_string(); + next_child_state.phase = "planning".to_string(); + append_game_creator_agent_runtime_task(&root, &next_child_state) + .expect("persist running next-round art child"); + write_game_creator_agent_runtime_state(&root, &next_child_state) + .expect("persist running next-round art child state"); + assert!( + game_chat_fast_path_scheduled_art_contract_repair_is_authorized_at( + &root, + "art-asset-plan", + &next_child_state.run_id, + AGENT_RUNTIME_ART_SPRITESHEET_PATH, + ) + .expect("verify next-round art lineage") + ); + assert!( + game_chat_delegated_art_agent_project_path_mutation_block( + &root, + "art-asset-plan", + &next_child_state.run_id, + "file.write", + AGENT_RUNTIME_ART_SPRITESHEET_PATH, + ) + .is_none(), + "new parent run and new delivery must restore assets-only authorization" + ); + } +} + fn game_chat_main_without_asset_audit_fixture(root: &Path) -> String { init_local_game_project_at(root, "game-chat-no-audit", "缺失审计的单主任务") .expect("init no-audit project"); diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/file_ops.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/file_ops.rs index a3f45eff8..e1469b246 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/file_ops.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/file_ops.rs @@ -171,7 +171,7 @@ fn game_chat_dynamic_art_retry_mutation_block(tool: &str) -> AgentRuntimeToolObs status: "blocked".to_string(), summary: "game-chat 动态美术 child 不支持通用 retry 写入".to_string(), detail: Some( - "必须回到当前 code-prototype 先完成 asset.list 审计,再按真实缺口建立新的 durable 美术委派" + "请继续 game-chat 对话,由下一轮 main code-prototype 重新完成 asset.list 审计,再按仍存在的缺口建立新的 durable 美术委派" .to_string(), ), } diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 35167ff8e..88702efe2 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -2,13 +2,14 @@ ## 2026-08-11 game-chat 动态美术 child 通用 retry 失败关闭 -- 裁决:game-chat 动态美术 child(`art-director` / `art-asset-plan`,parent 为单主 `code-prototype`、root 绑定 `project-supervisor-game-chat` source)不允许走通用 Agent Runtime retry。retry 入口识别该类 child 后,在创建 successor run、delegationId、task journal 或其它 durable 副作用前直接返回类型化错误,引导回到 game-chat,由当前 `code-prototype` 先完成 `asset.list` 审计,再按真实缺口建立新的 durable 美术委派;新委派自然产生新的 delegationId 与 durable delivery,沿用现行严格 lineage 判定,不新增 delivery 换绑、续发或可变机制。 +- 裁决:game-chat 动态美术 child(`art-director` / `art-asset-plan`,parent 为单主 `code-prototype`、root 绑定 `project-supervisor-game-chat` source)不允许走通用 Agent Runtime retry。retry 入口识别该类 child 后,在创建 successor run、delegationId、task journal 或其它 durable 副作用前直接返回类型化错误,引导用户继续 game-chat 对话:下一轮 main `code-prototype` 重新完成 `asset.list` 审计后,按仍存在的缺口建立新的 durable 美术委派。委派去重以 parent run 为键,跨轮自然放行;新委派产生新的 delegationId 与 durable delivery,沿用现行严格 lineage 判定,不新增 delivery 换绑、续发或可变机制。 - 结构性依据:delivery 的 delegationId 由父动作 ID 派生,且 targetRunId 绑定原 child run;通用 retry 铸造的 `retry:{旧run}:{新run}:{纳秒}` 身份在结构上不可能匹配任何现有 delivery。即使只圈住 retry 的写边界,其产出也没有合法消费者。让 retry 继承 lineage 必须引入可变 delivery、换绑或放宽 exact binding,与不可变事实和失败关闭方向冲突,明确不采纳。 +- 既有语义确认:同一 main run 内“每个审计缺口最多委派一次”(不含 `Suppressed`、包含终态失败或取消 delivery)是 2026-08-08 单主编排重构的 master 既有防抖语义,本裁决有意保留,不为失败 child 开豁免。同 run 重来与通用 retry 一样被拒绝,恢复只走下一轮 main 重新审计;这与禁止 retry 继承 lineage 是同一设计哲学。 - 纵深防御:执行策略层把 `source=agent-delegate-retry` 的 game-chat 美术 run 视为“声称委派 lineage 但无法证明”,只允许现有诊断性只读白名单,全部可变工具 fail closed;不得因严格授权返回 false 而回落为普通美术 Agent 权限。首次 `agent-delegate` 与遗留/伪造 `agent-delegate-retry` 共用同一动态美术 lineage 分类入口,但严格 Canvas、route、delivery 授权 predicate 继续只接受现行可证明的首次委派,不为 retry 放宽。 -- 错误 wire:现有 Tauri retry 命令继续保持 `Result`,M0B-1 不修改命令 DTO。Rust 内部使用稳定的类型化 retry error kind,并在 String wire 中固定保留 `kind=game-chat-dynamic-art-retry-unsupported` 与公开恢复建议;测试同时断言 error kind、可见文本和零 successor durable 副作用。 +- 错误 wire:现有 Tauri retry 命令继续保持 `Result`,M0B-1 不修改命令 DTO。Rust 内部使用稳定的类型化 retry error kind,并在 String wire 中固定保留 `kind=game-chat-dynamic-art-retry-unsupported` 与“继续 game-chat 对话、由下一轮 main 重新审计”的公开恢复建议;测试同时断言 error kind、可见文本和零 successor durable 副作用。 - 范围限定:仅限上述 game-chat 动态美术 child。完整 16 任务 DAG 的 children 走文件交接、不依赖 delegationId delivery,其通用 retry 语义不变;非美术委派与顶层任务 retry 亦不受影响。前端隐藏或置灰该类 child 的 retry 控件归 M0B-2 source-aware 投影收敛,在此之前由后端类型化错误兜底。 - 实现边界:入口守卫落在 `runtime_driver/lifecycle_control.rs` 的 `retry_game_creator_agent_runtime_task_at`,必须使用不要求 child 仍为 running 的结构身份分类;`resolve_game_creator_agent_runtime_retry_configuration_at` 保持不变。纵深防御落在 `runtime_tools/file_ops.rs` 的动态美术分类入口;严格 lineage/Canvas 授权 predicate 本身不放宽。除这两处与对应测试外,不扩展 M0B-1 生产改动面。 -- 验证方式:终态 failed/cancelled 动态美术 child 的 retry 返回类型化错误且不产生新 run;遗留或伪造的 `agent-delegate-retry` 美术 run 对 `file.write`、`project.patchset`、`canvas.asset_generate` 及其它全部可变工具阻断;完整 DAG child、非美术委派和顶层 retry 非回归;只有当前 `code-prototype` 重新审计并创建新委派、且新 delivery 全链一致时,新的首次 `agent-delegate` 才恢复 `assets/**` 授权。 +- 验证方式:终态 failed/cancelled 动态美术 child 的 retry 返回类型化错误且不产生新 run;遗留或伪造的 `agent-delegate-retry` 美术 run 对 `file.write`、`project.patchset`、`canvas.asset_generate` 及其它全部可变工具阻断;完整 DAG child、非美术委派和顶层 retry 非回归;失败或取消 child 的回执被认领后,同一 main run 对同一 target 的新 action 仍拒绝且零新 delivery,下一轮 main 重新 `asset.list` 并路由同一缺口后允许新委派,且新 delegationId、targetRunId、parent run、delivery 与 assets-only 授权全链一致。 - 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`、`docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md`。 ## 2026-08-11 固定 owner 产物验证与可玩验收分离 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 331ed264e..cc8d1fcfe 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -18,8 +18,8 @@ - 现象:game-chat 动态美术 child 以通用 retry 创建 `source=agent-delegate-retry` 的新 run 后,严格 Canvas/delivery 授权只接受首次 `agent-delegate`,返回 false;调用方却把 false 解释为“不是受限动态美术”,使 retry 回落到普通 autonomous 美术权限并可能写入 `game/**`、`.agent/**`,调用 preview 或继续委派。 - 原因:代码混用了“是否声称动态美术 lineage”和“是否已证明当前首次委派授权”两个事实。retry 使用新 run 和新 delegationId,但没有与之绑定的 durable delivery,结构上无法满足现行 exact lineage;授权失败应表示不可信候选,而不是普通 Agent。 -- 处理:game-chat 动态美术 child 在通用 retry 入队前以终态可用的结构身份分类并返回 `kind=game-chat-dynamic-art-retry-unsupported`,引导当前 `code-prototype` 重新 `asset.list` 后建立新委派。对遗留/伪造的 retry source,美术分类入口只允许诊断性只读工具,全部 mutation fail closed;严格首次委派 predicate、delivery 和 Canvas 凭证不换绑、不续发、不放宽。完整 DAG、非美术委派和顶层 retry 不受影响。 -- 验证:覆盖 failed/cancelled child 重试零 successor run、遗留 retry 的 file/patchset/Canvas/command/preview/再委派拒绝、新首次委派 delivery 全链一致后恢复 `assets/**`,并对完整 DAG 与非美术 retry 做非回归。 +- 处理:game-chat 动态美术 child 在通用 retry 入队前以终态可用的结构身份分类并返回 `kind=game-chat-dynamic-art-retry-unsupported`,引导用户继续 game-chat 对话,由下一轮 main `code-prototype` 重新 `asset.list` 后建立新委派。同一 main run 的同 target 重委派继续遵守“每个缺口最多一次”,不为失败 child 开豁免。对遗留/伪造的 retry source,美术分类入口只允许诊断性只读工具,全部 mutation fail closed;严格首次委派 predicate、delivery 和 Canvas 凭证不换绑、不续发、不放宽。完整 DAG、非美术委派和顶层 retry 不受影响。 +- 验证:覆盖 failed/cancelled child 重试零 successor run、遗留 retry 的 file/patchset/Canvas/command/preview/再委派拒绝、失败回执认领后同 run 同 target 重委派拒绝、下一轮 main 重新审计后同 target 新委派放行且 delivery 全链一致并恢复 `assets/**`,同时对完整 DAG 与非美术 retry 做非回归。 - 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/lifecycle_control.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/file_ops.rs`、`docs/project-memory/shared-memory/decision-log.md`。 ## 不能用可玩游戏 smoke 验证 code-prototype 上游的固定文档产物 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 33e90f3ec..3707b31a7 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -38,7 +38,7 @@ - ready-task 启动活性:`background_task.queued`、`autonomous_ready_task.scheduled`、Runner heartbeat 或执行锁已移交都不等于 child 已启动。实际持有执行权的 Runner 必须在释放项目写锁后同步写入 child 的 running task、`turn.started` 与 started journal,再把已启动 state 和 per-Agent 执行锁交给已确认开始轮询的独立 execution worker;同步启动或 worker 接管失败时,要在仍持有执行锁期间依次把 child 和 manifest Graph 节点明确落为 failed,再释放锁并让 parent 收到调度错误。`autonomous_ready_task.scheduled` 只作诊断审计,其写入失败不能阻断 durable child 启动;external client 只 wake Runner,不在客户端抢占执行。Supervisor 进度卡通过 durable `startedAt`(旧 Run 从完整 task journal 恢复,最新 task-record fallback 保持 0)显示真实持续时间,并以父 Run 与当前关联专业 Agent 的最大事件时间计算运行态活跃度:运行超过 5 分钟无新事件时显示“运行中 · 疑似停滞”和静默时长;等待用户、等待确认、Provider retry、视觉资产、进程会话、pausing 与 paused 不误报。父 Run terminal 后,持续时间冻结在父 Run 自身最后活动,不随 child 晚到收口事件增长。消息时间统一校验为 JavaScript 可表示的 Date;越界值显示“时间未知”且不写无效 `datetime`。实时回复只显示 response stream 自己的 `updatedAt`,缺失时同样显示“时间未知”,不能借用其它 Runtime 活动时间或随前端时钟漂移。该提示只提供可观测性,不改变 Runtime/manifest 正式状态。 - ready-task manifest 漂移:父 Supervisor 必须分别判断“能否调度新节点”和“是否存在必须等待的工作”。派生视觉需要父规划修复时不再调度新 child,但当前最新且活跃的根 Run 下,只要存在确定性 runId、scheduler source、正确父绑定且 durable journal 为 queued/running 的 ready child,父 Run 就保持 `waiting-for-manifest-tasks`,不能因旧 hydration 快照把 manifest running 覆盖成 pending 而提前 fixed-graph-stalled。game-chat child 可在相同严格身份下容忍 pending 漂移;正式产物、Canvas、revision、`game.static_smoke` 与 `preview.validate` 门禁不放宽。GUI/CLI、旧父 Run、终态、确认/用户输入/reconciliation、伪造绑定或非确定性 runId 全部失败关闭;更新根 Run 后旧 child 不得继续维持新 DAG 或投影完成。 - Supervisor 持久决策与单主条件美术:game-chat 的关键词、用户是否报告“美术未接入”、占位状态和当前资产探测只形成 `advisoryOnly=true` 的补充上下文,不得直接重置 Graph、预完成美术节点、选择复用/生成分支或继承历史试玩类型。当前根 Run 没有持久化 Supervisor 决策时,scheduler 不启动任何 child;Supervisor Provider 只通过 auto-safe 的 `agent.route_manifest` 提交 `game-chat-workflow-decision.v2`:`intentSummary` 是 Supervisor 自行理解并持久化的用户意图,`strategy=audit-existing-first` 只是固定安全执行策略,两者不得混用。此动作不能审计、生成、委派或替代后续判断,也不能把整体视觉重做解释成整套美术的强制重生成;成功后 Runtime 只启动唯一 `code-prototype` 主 Agent。升级恢复时严格校验 v1 sidecar 的旧 fingerprint,并从完成合同绑定的有效任务恢复 `intentSummary`;旧 `code-director` coverage/route 只作为迁移输入,不作为当前完成证据,必须由同一根 Run 的 `code-prototype` 重新 `asset.list` 后原位替换为单主合同。确定性 `code-prototype` Run 仅兼容已知 canonical task 文本版本,其余 task/binding/root 身份继续失败关闭;升级前已运行的 fixed-graph 美术 child 不再具备任何 mutation 或生图权限。主 Agent 必须以当前正式资产、Canvas 登记、私有图集合同、四张语义切片和 art manifest 判断真实缺口;完整覆盖时直接接入,不得生成或扣费。只有可证实缺失 `art-spec` 或核心 spritesheet 时,主 Agent 才可对相应 `art-director` 或 `art-asset-plan` 建立一条 durable 委派;每次最多一个活跃美术 child,child 仅可写 `assets/**`,不得修改 `game/**` 或接入/验收游戏。若两个槽位都缺失,必须先完成 `art-director`,由同一主 Run 认领其 `EvidenceReady` delivery 后,才能委派依赖规范图的 `art-asset-plan`;失败或未就绪 delivery 不得消耗不可重试的图集委派槽位。主 Agent 认领必要回执后继续同一 Run 完成素材接入、原玩法语义校验、`game.static_smoke` 与桌面/移动 `preview.validate`。绝对硬截止对嵌套美术 child 继续核验 `root -> code-prototype -> agent-delegate` 完整身份并保留未知外部生成的 reconciliation 证据。Runtime 只负责校验根/父子身份、当前 revision、路径、Canvas 登记、缺口/路由 fingerprint、写入范围及完成证据;纯“继续”仍走既有正式 continuation 合同,普通美术措辞不得借用更老项目的具体试玩场景。不得以增加 loop 预算、伪造 revision、机械改写 manifest 或重放历史图片 action 代替 Supervisor 决策和程序侧审计。 -- 动态美术 child 通用 retry 失败关闭:game-chat 的 `art-director` / `art-asset-plan` delivery 精确绑定原父动作派生 delegationId 与 targetRunId,不允许通用 Agent Runtime retry 换绑或继承。retry 入口以不要求 child 仍为 running 的结构身份识别当前 `root -> code-prototype -> art child` 后,在创建新 run 前返回 `kind=game-chat-dynamic-art-retry-unsupported`,引导当前主 Agent 重新 `asset.list` 并按真实缺口创建新的 durable 委派。遗留或伪造的 `source=agent-delegate-retry` 美术 run 只允许诊断性只读工具,`file.write`、patchset、Canvas、command、preview、memory/manifest 和再次委派等 mutation 全部失败关闭,不能因严格授权不成立而回落普通美术权限。完整 16 任务 DAG、非美术委派、顶层 retry 与现有 Tauri String error wire 不变;前端隐藏/置灰控件留给 source-aware 单主投影阶段。 +- 动态美术 child 通用 retry 失败关闭:game-chat 的 `art-director` / `art-asset-plan` delivery 精确绑定原父动作派生 delegationId 与 targetRunId,不允许通用 Agent Runtime retry 换绑或继承。retry 入口以不要求 child 仍为 running 的结构身份识别当前 `root -> code-prototype -> art child` 后,在创建新 run 前返回 `kind=game-chat-dynamic-art-retry-unsupported`,引导用户继续 game-chat 对话,由下一轮 main `code-prototype` 重新 `asset.list` 并按仍存在的真实缺口创建新的 durable 委派。委派去重以 parent run 为键;同一 main run 内每个缺口最多委派一次,终态失败或取消 delivery 也不为同 run 开重试豁免,跨轮则以新 parent run、新 delegationId、targetRunId 和 delivery 自然放行。遗留或伪造的 `source=agent-delegate-retry` 美术 run 只允许诊断性只读工具,`file.write`、patchset、Canvas、command、preview、memory/manifest 和再次委派等 mutation 全部失败关闭,不能因严格授权不成立而回落普通美术权限。完整 16 任务 DAG、非美术委派、顶层 retry 与现有 Tauri String error wire 不变;前端隐藏/置灰控件留给 source-aware 单主投影阶段。 - ready-task 对账取消续跑:未知工具结果仍停在 `needs-reconciliation` 且禁止自动重放;人工核对后显式取消原 child,保留 cancel tombstone,旧 child 和旧父 Run 按真实终态收口。若随后创建同 Session、同 Supervisor source、同有效任务语义的 continuation,新完成合同只对同时具有历史 `failed / needs-reconciliation`、最终 `cancelled` 和 durable tombstone 的 ready-task,把当前 manifest 对应 failed 节点恢复为 pending,并由 scheduler 创建全新 child Run。manifest 的读取、failed 筛选、每任务一次的 child journal 索引、证据重验和写回必须位于同一项目写锁域;较新的无 child 根 Run 只有在 durable journal 精确表明为旧 failed Graph 在进入调度前即失败时才能跨过,scheduler 自身失败必须阻断借用更老 tombstone。普通失败、无 tombstone、不同 source/Session/任务语义或证据冲突均保持失败关闭;不得复活旧 pending action、补造 observation 或把取消任务标成 completed。 - 完成门静态分析预算:Canvas 视觉门必须先做只会提前拒绝的词法预检。经典或模块脚本同时不含大小写精确的 `import` 与 `export` 字节序列时,不运行模块依赖语义分析;纯 `export ... from` / `export * from` 仍须进入正式模块图分析。当前脚本不含目标文件名或任一已绑定 DOM 图片元素 ID 时,先低成本解码 `\\xNN`、`\\uNNNN`、`\\u{...}`、简单转义和续行;解码后仍无候选才不运行完整 Canvas alias / 函数可达性分析,解码不确定则保守进入 Oxc。存在任一候选时仍执行原 parser、semantic binding、解码后的 computed 属性/StringLiteral 路径、可达 `drawImage`、可见 Canvas、路径大小写和动态 namespace 写入门禁;HTML 中存在某个绑定元素不得使所有无关 JavaScript 单元进入重分析,禁止把词法命中当作通过条件。 - Provider 故障展示:Provider retry 的“是否可重试”继续使用 `upstream-5xx` 等稳定类别判断,但 durable retry record 保留安全的精确 `upstream-` 身份。等待态必须从真实 record 显示 HTTP 状态、`nextAttempt/maxRetries` 与当前持久退避剩余秒数,例如“Provider 上游返回 HTTP 503,准备自动重试 1/3;预计 8 秒后重试”;不得以动画或前端自增计时伪造 attempt。重试耗尽的 Runtime 私有错误只保存 `kind/httpStatus/fingerprint/chars/retryAttempt/maxRetries/retryState`,前端和持久 conversation 仅在字段顺序、范围、状态一致且无尾随正文时派生“上游服务返回 HTTP 503;自动重试已耗尽(3/3)”;其它错误使用固定安全摘要。Provider 响应正文、URL/query、凭据、本地绝对路径、fingerprint、字符数和 `[redacted ...]` 占位符均不得进入用户可见消息。 diff --git a/docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md b/docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md index 94a9a9a95..f919a943b 100644 --- a/docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md +++ b/docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md @@ -1348,7 +1348,7 @@ Canvas 是条件外部能力,不是 M0-3 四个固定 owner 的通用前置: 正式任务 M0-4 的 PR 工作包 `M0B-1` 还必须让 `agent-delegate` 与 `agent-delegate-retry` 共用严格动态美术 lineage predicate;当前 retry source 不能绕过 `assets/**`。该修复不属于本文档 PR 的功能实现,但在 `M0B-2` 收敛 game-chat 前端投影前必须完成。 -2026-08-11 裁决进一步冻结:game-chat 动态美术 child 不允许使用通用 Agent Runtime retry。原 child 的 delivery 同时精确绑定父动作派生 delegationId 与 targetRunId;通用 retry 产生的新 run/new delegationId 没有合法 delivery,禁止通过换绑、续发、复制凭证或放宽 strict predicate 赋权。`retry_game_creator_agent_runtime_task_at` 必须在任何 successor durable 副作用前,以不要求 child 仍为 running 的结构身份识别该类终态 child,并返回稳定类型 `kind=game-chat-dynamic-art-retry-unsupported`,引导当前 `code-prototype` 重新 `asset.list` 后创建新的首次委派。遗留/伪造的 `agent-delegate-retry` 美术 run 只保留诊断性只读白名单,全部 mutation fail closed;现有 Tauri String error wire、其它 delegated retry、完整 DAG 和顶层 retry 不变。 +2026-08-11 裁决进一步冻结:game-chat 动态美术 child 不允许使用通用 Agent Runtime retry。原 child 的 delivery 同时精确绑定父动作派生 delegationId 与 targetRunId;通用 retry 产生的新 run/new delegationId 没有合法 delivery,禁止通过换绑、续发、复制凭证或放宽 strict predicate 赋权。`retry_game_creator_agent_runtime_task_at` 必须在任何 successor durable 副作用前,以不要求 child 仍为 running 的结构身份识别该类终态 child,并返回稳定类型 `kind=game-chat-dynamic-art-retry-unsupported`,引导用户继续 game-chat 对话,由下一轮 main `code-prototype` 重新 `asset.list` 后按仍存在的缺口创建新的首次委派。委派去重以 parent run 为键;同一 main run 内每个缺口最多委派一次,终态失败或取消 delivery 不为同 run 开豁免,跨轮以新 parent run、新 delegationId、targetRunId 与 delivery 自然放行。遗留/伪造的 `agent-delegate-retry` 美术 run 只保留诊断性只读白名单,全部 mutation fail closed;现有 Tauri String error wire、其它 delegated retry、完整 DAG 和顶层 retry 不变。 ## 20. rollout、停用与回滚 @@ -1448,7 +1448,7 @@ M0A-1 是 M1 详细设计与技术 spike 的输入,不是功能交付。M1 可 ### 23.3 M0-4(PR 工作包 `M0B-1` / `M0B-2`) -`M0B-1` 先封闭 game-chat 动态美术 delegate/retry 的安全边界:首次 `agent-delegate` 继续按严格 delivery/route/Canvas lineage 只写 `assets/**`;该类 child 的通用 retry 在入队前返回 `kind=game-chat-dynamic-art-retry-unsupported`,遗留或伪造的 `agent-delegate-retry` 只读且全部 mutation 失败关闭,恢复只能由当前 `code-prototype` 重新审计并创建新的首次委派。`M0B-2` 再以 source-aware lineage 修复单主进度、最终回复、可玩 revision 与归档投影。M0-4 不阻塞 M1 策划闭环开工,但阻塞 M3 game-chat 接入和“M0 全部完成”。M0-1~M0-4 全部合入并通过各自门禁后,才能标记“M0 全部完成”。 +`M0B-1` 先封闭 game-chat 动态美术 delegate/retry 的安全边界:首次 `agent-delegate` 继续按严格 delivery/route/Canvas lineage 只写 `assets/**`;该类 child 的通用 retry 在入队前返回 `kind=game-chat-dynamic-art-retry-unsupported`,遗留或伪造的 `agent-delegate-retry` 只读且全部 mutation 失败关闭。恢复只走跨轮:用户继续 game-chat 对话,由下一轮 main `code-prototype` 重新审计并按仍存在的缺口创建新的首次委派;同一 main run 对同一 target 的第二次委派继续拒绝,新一轮以新 parent run、新 delegationId、targetRunId 与 delivery 全链恢复。`M0B-2` 再以 source-aware lineage 修复单主进度、最终回复、可玩 revision 与归档投影。M0-4 不阻塞 M1 策划闭环开工,但阻塞 M3 game-chat 接入和“M0 全部完成”。M0-1~M0-4 全部合入并通过各自门禁后,才能标记“M0 全部完成”。 ## 24. 最终不变量摘要