From 4d0c8e40b375c4e8fbf4b313c345ad977d41f3b1 Mon Sep 17 00:00:00 2001 From: Linghong Date: Tue, 11 Aug 2026 10:40:12 +0000 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E5=B0=81=E9=97=AD?= =?UTF-8?q?=20game-chat=20=E7=BE=8E=E6=9C=AF=E9=87=8D=E8=AF=95=E5=86=99?= =?UTF-8?q?=E5=85=A5=E8=BE=B9=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在 successor durable 写入前类型化拒绝动态美术 child 通用 retry 将遗留 retry-source 美术 run 收敛为只读并阻断全部 mutation 补齐终态零副作用、工具矩阵及 DAG/非美术 retry 非回归测试 同步技术方案、决策记录与踩坑说明 --- .../agent/runtime_driver/lifecycle_control.rs | 24 + .../agent/runtime_driver/main_loop_tests.rs | 485 +++++++++++++++++- .../src/agent/runtime_tools/file_ops.rs | 229 +++++++-- .../shared-memory/decision-log.md | 11 + docs/project-memory/shared-memory/pitfalls.md | 8 + ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 1 + ...方案】立项策划Agent(Fast GDD)-2026-08-10.md | 4 +- 7 files changed, 722 insertions(+), 40 deletions(-) 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 512271191..1a99dd10c 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 @@ -772,6 +772,23 @@ pub(crate) fn resolve_game_creator_agent_runtime_retry_configuration_at( Ok((run_profile, source)) } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(in crate::agent) enum AgentRuntimeRetryErrorKind { + GameChatDynamicArtRetryUnsupported, +} + +impl AgentRuntimeRetryErrorKind { + pub(in crate::agent) const fn as_str(self) -> &'static str { + match self { + Self::GameChatDynamicArtRetryUnsupported => "game-chat-dynamic-art-retry-unsupported", + } + } + + fn wire_error(self, message: &str) -> String { + format!("kind={} {message}", self.as_str()) + } +} + pub(crate) fn retry_game_creator_agent_runtime_task_at( root: &Path, agent_id: &str, @@ -787,6 +804,13 @@ pub(crate) fn retry_game_creator_agent_runtime_task_at( let task = read_latest_game_creator_agent_runtime_task_by_run_id(root, &agent_id, &target_run_id)? .ok_or_else(|| format!("未找到 Agent Runtime 任务:{target_run_id}"))?; + 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 美术委派", + ), + ); + } if task.goal_id.is_some() { return Err("持久 Goal 任务不能使用普通 retry;请清理后创建新 Goal".to_string()); } 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 1764bb157..126c202cb 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 @@ -342,6 +342,455 @@ fn game_chat_main_art_child_fixture( ) } +fn snapshot_agent_durable_files(root: &Path) -> std::collections::BTreeMap> { + fn visit( + base: &Path, + directory: &Path, + snapshot: &mut std::collections::BTreeMap>, + ) { + let Ok(entries) = fs::read_dir(directory) else { + return; + }; + for entry in entries { + let entry = entry.expect("read durable Agent entry"); + let path = entry.path(); + let file_type = entry.file_type().expect("read durable Agent file type"); + if file_type.is_dir() { + visit(base, &path, snapshot); + } else if file_type.is_file() { + if path + .file_name() + .and_then(|value| value.to_str()) + .is_some_and(|value| value.ends_with(".lock")) + { + continue; + } + let relative = path + .strip_prefix(base) + .expect("durable Agent file remains below project root") + .to_string_lossy() + .replace('\\', "/"); + snapshot.insert(relative, fs::read(&path).expect("read durable Agent file")); + } + } + } + + let mut snapshot = std::collections::BTreeMap::new(); + visit(root, &root.join(".agent"), &mut snapshot); + snapshot +} + +fn persist_legacy_game_chat_art_retry_run( + root: &Path, + original_child: &AgentRuntimeState, + retry_run_id: &str, +) -> AgentRuntimeState { + let parent_agent_id = original_child + .parent_agent_id + .as_deref() + .expect("game-chat art child has parent agent"); + let parent_run_id = original_child + .parent_run_id + .as_deref() + .expect("game-chat art child has parent run"); + let retry_link = AgentRuntimeTaskLink { + parent_agent_id: Some(parent_agent_id.to_string()), + parent_run_id: Some(parent_run_id.to_string()), + delegation_id: Some(format!("legacy-retry-{retry_run_id}")), + }; + let binding = bind_game_creator_agent_runtime_run_profile_at( + root, + &original_child.agent_id, + retry_run_id, + "agent-delegate-retry", + Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD), + Some(&retry_link), + ) + .expect("bind legacy game-chat art retry run"); + let mut retry = read_latest_game_creator_agent_runtime_task_by_run_id( + root, + &original_child.agent_id, + &original_child.run_id, + ) + .expect("read original game-chat art child") + .expect("original game-chat art child exists"); + retry.run_id = retry_run_id.to_string(); + retry.source = "agent-delegate-retry".to_string(); + retry.run_profile = binding.profile; + retry.run_profile_binding_fingerprint = binding.binding_fingerprint; + retry.delegation_id = retry_link.delegation_id; + retry.status = "running".to_string(); + retry.phase = "planning".to_string(); + retry.current_action = "遗留 retry 等待执行".to_string(); + retry.terminal_detail = None; + retry.error = None; + retry.updated_at = unix_timestamp(); + let state = agent_runtime_state_from_task_record(&retry); + append_game_creator_agent_runtime_task(root, &state) + .expect("persist legacy game-chat art retry task"); + write_game_creator_agent_runtime_state(root, &state) + .expect("persist legacy game-chat art retry state"); + state +} + +#[test] +fn game_chat_dynamic_art_terminal_children_reject_generic_retry_without_durable_side_effects() { + for (case_index, (agent_id, missing_slots, terminal_status)) in [ + ( + "art-director", + vec!["art-spec", "core-spritesheet"], + "failed", + ), + ( + "art-director", + vec!["art-spec", "core-spritesheet"], + "cancelled", + ), + ("art-asset-plan", vec!["core-spritesheet"], "failed"), + ("art-asset-plan", vec!["core-spritesheet"], "cancelled"), + ] + .into_iter() + .enumerate() + { + let temporary = tempfile::tempdir().expect("create terminal art retry root"); + let root = temporary.path().join("project"); + let (_main, mut child, _) = + game_chat_main_art_child_fixture(&root, agent_id, &missing_slots); + child.status = terminal_status.to_string(); + child.phase = terminal_status.to_string(); + child.current_action = "等待用户恢复".to_string(); + child.error = (terminal_status == "failed").then(|| "kind=test-failure".to_string()); + append_game_creator_agent_runtime_task(&root, &child) + .expect("persist terminal game-chat art child task"); + write_game_creator_agent_runtime_state(&root, &child) + .expect("persist terminal game-chat art child state"); + let durable_before = snapshot_agent_durable_files(&root); + let retry_run_id = format!("blocked-game-chat-art-retry-{case_index}"); + + let error = retry_game_creator_agent_runtime_task_at( + &root, + &child.agent_id, + &child.run_id, + &retry_run_id, + ) + .expect_err("game-chat dynamic art child retry must be rejected"); + + assert!( + error.contains(&format!( + "kind={}", + AgentRuntimeRetryErrorKind::GameChatDynamicArtRetryUnsupported.as_str() + )), + "unexpected typed retry error: {error}" + ); + assert!(error.contains("code-prototype"), "{error}"); + assert!(error.contains("asset.list"), "{error}"); + assert_eq!( + snapshot_agent_durable_files(&root), + durable_before, + "blocked retry must not persist any successor state: {agent_id}/{terminal_status}" + ); + assert!(read_latest_game_creator_agent_runtime_task_by_run_id( + &root, + &child.agent_id, + &retry_run_id, + ) + .expect("inspect blocked retry successor") + .is_none()); + } +} + +#[tokio::test] +async fn legacy_game_chat_dynamic_art_retry_runs_are_read_only_and_fail_closed() { + let temporary = tempfile::tempdir().expect("create legacy art retry root"); + let root = temporary.path().join("project"); + let (_main, child, _) = + game_chat_main_art_child_fixture(&root, "art-asset-plan", &["core-spritesheet"]); + let retry = + persist_legacy_game_chat_art_retry_run(&root, &child, "legacy-game-chat-art-retry-run"); + let retry_task = read_latest_game_creator_agent_runtime_task_by_run_id( + &root, + &retry.agent_id, + &retry.run_id, + ) + .expect("read legacy art retry") + .expect("legacy art retry exists"); + assert!( + game_chat_dynamic_art_child_structural_identity_at(&root, &retry_task) + .expect("classify legacy art retry lineage") + ); + + for read_only_tool in ["asset.list", "file.read", "project.diff", "image.inspect"] { + assert!( + game_chat_delegated_art_agent_input_mutation_block( + &root, + &retry.agent_id, + &retry.run_id, + read_only_tool, + &serde_json::json!({}), + ) + .is_none(), + "diagnostic read-only tool must remain available: {read_only_tool}" + ); + } + + let actions = [ + AgentRuntimeToolAction { + tool: "file.write".to_string(), + reason: None, + input: serde_json::json!({"path":"assets/retry-must-not-write.txt","content":"blocked"}), + }, + AgentRuntimeToolAction { + tool: "project.patchset".to_string(), + reason: None, + input: serde_json::json!({"changes":[{"path":"assets/retry-patchset.txt","operation":"write","content":"blocked"}]}), + }, + AgentRuntimeToolAction { + tool: "canvas.asset_generate".to_string(), + reason: None, + input: serde_json::json!({"outputPath":"assets/art-spritesheet.png","assetKind":"art-spritesheet"}), + }, + AgentRuntimeToolAction { + tool: "memory.write".to_string(), + reason: None, + input: serde_json::json!({"scope":"project","title":"blocked","content":"blocked","mode":"append"}), + }, + AgentRuntimeToolAction { + tool: "task.create".to_string(), + reason: None, + input: serde_json::json!({"taskId":"blocked-retry-task"}), + }, + AgentRuntimeToolAction { + tool: "command.run_limited".to_string(), + reason: None, + input: serde_json::json!({"command":"npm test"}), + }, + AgentRuntimeToolAction { + tool: "preview.validate".to_string(), + reason: None, + input: serde_json::json!({}), + }, + AgentRuntimeToolAction { + tool: "agent.delegate".to_string(), + reason: None, + input: serde_json::json!({"agentId":"art-director","task":"blocked"}), + }, + AgentRuntimeToolAction { + tool: "agent.spawn_isolated".to_string(), + reason: None, + input: serde_json::json!({"children":[]}), + }, + ]; + for action in actions { + let observation = execute_game_creator_agent_runtime_tool_action( + &root, + &retry.agent_id, + &retry.run_id, + &retry.current_task, + &action, + ) + .await; + assert_eq!(observation.status, "blocked", "{action:?}: {observation:?}"); + assert!( + observation.summary.contains("不支持通用 retry"), + "{action:?}: {observation:?}" + ); + } + assert!(game_chat_delegated_art_agent_project_path_mutation_block( + &root, + &retry.agent_id, + &retry.run_id, + "file.write", + "assets/direct-path-bypass.txt", + ) + .is_some()); + assert!(!root.join("assets/retry-must-not-write.txt").exists()); + assert!(!root.join("assets/retry-patchset.txt").exists()); +} + +#[test] +fn game_chat_dynamic_art_retry_guard_preserves_dag_art_and_non_art_delegated_retry() { + let dag_temporary = tempfile::tempdir().expect("create full DAG art retry root"); + let dag_root = dag_temporary.path().join("project"); + init_local_game_project_at(&dag_root, "full-dag-art-retry", "完整 DAG 美术重试") + .expect("init full DAG art retry project"); + let dag_root_session = resolve_agent_conversation_session_id_at( + &dag_root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + None, + true, + ) + .expect("resolve full DAG root session"); + let dag_root_record = append_unique_game_creator_agent_runtime_pending_task( + &dag_root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + &dag_root_session, + "执行完整任务图", + "full-dag-art-retry-root", + AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE, + Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD), + None, + ) + .expect("queue full DAG root"); + let mut dag_root_state = agent_runtime_state_from_task_record(&dag_root_record); + dag_root_state.status = "running".to_string(); + dag_root_state.phase = "planning".to_string(); + append_game_creator_agent_runtime_task(&dag_root, &dag_root_state) + .expect("persist running full DAG root"); + write_game_creator_agent_runtime_state(&dag_root, &dag_root_state) + .expect("persist full DAG root state"); + let dag_manifest = read_manifest_for_project(&dag_root).expect("read full DAG manifest"); + let dag_art_task = dag_manifest + .tasks + .iter() + .find(|task| task.id == "art-director") + .expect("full DAG art-director task"); + let dag_art_session = + resolve_agent_conversation_session_id_at(&dag_root, "art-director", None, true) + .expect("resolve full DAG art session"); + let dag_art_record = append_unique_game_creator_agent_runtime_pending_task( + &dag_root, + "art-director", + &dag_art_session, + &render_autonomous_manifest_ready_task_background_prompt(dag_art_task), + "full-dag-art-failed-run", + "agent-ready-task-scheduler", + None, + Some(&AgentRuntimeTaskLink { + parent_agent_id: Some(dag_root_state.agent_id.clone()), + parent_run_id: Some(dag_root_state.run_id.clone()), + delegation_id: None, + }), + ) + .expect("queue full DAG art task"); + let mut dag_art_state = agent_runtime_state_from_task_record(&dag_art_record); + dag_art_state.status = "failed".to_string(); + dag_art_state.phase = "failed".to_string(); + dag_art_state.error = Some("kind=test-failure".to_string()); + append_game_creator_agent_runtime_task(&dag_root, &dag_art_state) + .expect("persist failed full DAG art task"); + write_game_creator_agent_runtime_state(&dag_root, &dag_art_state) + .expect("persist failed full DAG art state"); + let dag_art_lane = try_acquire_game_creator_agent_runtime_task_lock(&dag_root, "art-director") + .expect("acquire full DAG art lane") + .expect("full DAG art lane is free"); + let dag_retry = retry_game_creator_agent_runtime_task_at( + &dag_root, + "art-director", + &dag_art_state.run_id, + "full-dag-art-retry-run", + ) + .expect("full DAG art retry semantics remain available"); + assert_eq!( + dag_retry.accepted_run_id.as_deref(), + Some("full-dag-art-retry-run") + ); + let dag_retry_task = read_latest_game_creator_agent_runtime_task_by_run_id( + &dag_root, + "art-director", + "full-dag-art-retry-run", + ) + .expect("read full DAG art retry") + .expect("full DAG art retry exists"); + assert_eq!(dag_retry_task.source, "agent-delegate-retry"); + assert_eq!( + dag_retry_task.parent_agent_id.as_deref(), + Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID) + ); + assert!( + !game_chat_dynamic_art_child_structural_identity_at(&dag_root, &dag_retry_task) + .expect("classify full DAG art retry") + ); + assert!(game_chat_delegated_art_agent_input_mutation_block( + &dag_root, + "art-director", + "full-dag-art-retry-run", + "file.write", + &serde_json::json!({"path":AGENT_RUNTIME_ART_SPEC_PATH,"content":"unchanged semantics"}), + ) + .is_none()); + drop(dag_art_lane); + + let delegated_temporary = tempfile::tempdir().expect("create non-art delegated retry root"); + let delegated_root = delegated_temporary.path().join("project"); + init_local_game_project_at(&delegated_root, "non-art-delegated-retry", "非美术委派重试") + .expect("init non-art delegated retry project"); + let parent_session = resolve_agent_conversation_session_id_at( + &delegated_root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + None, + true, + ) + .expect("resolve non-art parent session"); + let parent_record = append_unique_game_creator_agent_runtime_pending_task( + &delegated_root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + &parent_session, + "委派非美术任务", + "non-art-delegated-parent", + "agent-background-task", + Some(AGENT_RUNTIME_RUN_PROFILE_STANDARD), + None, + ) + .expect("queue non-art parent"); + let mut parent_state = agent_runtime_state_from_task_record(&parent_record); + parent_state.status = "running".to_string(); + parent_state.phase = "planning".to_string(); + append_game_creator_agent_runtime_task(&delegated_root, &parent_state) + .expect("persist running non-art parent"); + write_game_creator_agent_runtime_state(&delegated_root, &parent_state) + .expect("persist non-art parent state"); + let child_session = + resolve_agent_conversation_session_id_at(&delegated_root, "design-director", None, true) + .expect("resolve non-art child session"); + let child_record = append_unique_game_creator_agent_runtime_pending_task( + &delegated_root, + "design-director", + &child_session, + "完成非美术设计任务", + "non-art-delegated-failed-run", + "agent-delegate", + None, + Some(&AgentRuntimeTaskLink { + parent_agent_id: Some(parent_state.agent_id.clone()), + parent_run_id: Some(parent_state.run_id.clone()), + delegation_id: Some("non-art-original-delegation".to_string()), + }), + ) + .expect("queue non-art delegated child"); + let mut child_state = agent_runtime_state_from_task_record(&child_record); + child_state.status = "failed".to_string(); + child_state.phase = "failed".to_string(); + child_state.error = Some("kind=test-failure".to_string()); + append_game_creator_agent_runtime_task(&delegated_root, &child_state) + .expect("persist failed non-art child"); + write_game_creator_agent_runtime_state(&delegated_root, &child_state) + .expect("persist failed non-art child state"); + let non_art_lane = + try_acquire_game_creator_agent_runtime_task_lock(&delegated_root, "design-director") + .expect("acquire non-art child lane") + .expect("non-art child lane is free"); + let delegated_retry = retry_game_creator_agent_runtime_task_at( + &delegated_root, + "design-director", + &child_state.run_id, + "non-art-delegated-retry-run", + ) + .expect("non-art delegated retry semantics remain available"); + assert_eq!( + delegated_retry.accepted_run_id.as_deref(), + Some("non-art-delegated-retry-run") + ); + let delegated_retry_task = read_latest_game_creator_agent_runtime_task_by_run_id( + &delegated_root, + "design-director", + "non-art-delegated-retry-run", + ) + .expect("read non-art delegated retry") + .expect("non-art delegated retry exists"); + assert_eq!(delegated_retry_task.source, "agent-delegate-retry"); + drop(non_art_lane); +} + 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"); @@ -360,8 +809,20 @@ fn game_chat_main_without_asset_audit_fixture(root: &Path) -> String { async fn game_chat_main_agent_delegates_only_real_missing_art_and_limits_children_to_assets() { let temporary = tempfile::tempdir().expect("create game-chat art child root"); let root = temporary.path().join("project"); - let (_main, child, _delegation_id) = + init_local_game_project_at(&root, "game-chat-art-child", "单主美术委派") + .expect("pre-initialize game-chat art child project"); + let _child_runtime_lane = + try_acquire_game_creator_agent_runtime_task_lock(&root, "art-asset-plan") + .expect("acquire game-chat art child runtime lane") + .expect("game-chat art child runtime lane is free"); + let (_main, mut child, _delegation_id) = game_chat_main_art_child_fixture(&root, "art-asset-plan", &["core-spritesheet"]); + child.status = "running".to_string(); + child.phase = "planning".to_string(); + append_game_creator_agent_runtime_task(&root, &child) + .expect("persist running game-chat art child task"); + write_game_creator_agent_runtime_state(&root, &child) + .expect("persist running game-chat art child state"); assert_eq!(child.agent_id, "art-asset-plan"); assert_eq!(child.source, "agent-delegate"); assert_eq!(child.parent_agent_id.as_deref(), Some("code-prototype")); @@ -420,7 +881,10 @@ async fn game_chat_main_agent_delegates_only_real_missing_art_and_limits_childre _ => unreachable!(), }; assert_eq!(observation.status, "blocked", "{tool}: {observation:?}"); - assert!(observation.summary.contains("只能修改 assets/**")); + assert!( + observation.summary.contains("只能修改 assets/**"), + "{action:?}: {observation:?}" + ); } let patchset = observe_agent_runtime_project_patchset( @@ -478,6 +942,18 @@ async fn game_chat_main_agent_delegates_only_real_missing_art_and_limits_childre }), }, ] { + let preflight = game_chat_delegated_art_agent_input_mutation_block( + &root, + &child.agent_id, + &child.run_id, + &action.tool, + &action.input, + ) + .unwrap_or_else(|| panic!("delegated art preflight must block {action:?}")); + assert!( + preflight.summary.contains("只能修改 assets/**"), + "{action:?}: {preflight:?}" + ); let observation = execute_game_creator_agent_runtime_tool_action( &root, &child.agent_id, @@ -487,7 +963,10 @@ async fn game_chat_main_agent_delegates_only_real_missing_art_and_limits_childre ) .await; assert_eq!(observation.status, "blocked", "{action:?}: {observation:?}"); - assert!(observation.summary.contains("只能修改 assets/**")); + assert!( + observation.summary.contains("只能修改 assets/**"), + "{action:?}: {observation:?}" + ); } assert_eq!(fs::read(root.join("memory/project.md")).ok(), memory_before); assert_eq!( 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 231e095e4..e8f676dbb 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 @@ -1,27 +1,161 @@ use super::*; -fn game_chat_delegated_art_agent_is_assets_only_at( +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum GameChatDelegatedArtAgentMutationScope { + Unrestricted, + AssetsOnly, + RetryLineageBlocked, +} + +pub(in crate::agent) fn game_chat_dynamic_art_child_structural_identity_at( + root: &Path, + task: &AgentRuntimeTaskRecord, +) -> Result { + if !matches!(task.agent_id.as_str(), "art-director" | "art-asset-plan") + || task.parent_agent_id.as_deref() != Some("code-prototype") + { + return Ok(false); + } + let Some(parent_run_id) = task + .parent_run_id + .as_deref() + .filter(|value| !value.is_empty()) + else { + return Ok(false); + }; + let Some(child_binding) = + read_game_creator_agent_runtime_run_profile_binding(root, &task.agent_id, &task.run_id)? + else { + return Ok(false); + }; + let Some(parent) = read_latest_game_creator_agent_runtime_task_by_run_id( + root, + "code-prototype", + parent_run_id, + )? + else { + return Ok(false); + }; + let Some(parent_binding) = + read_game_creator_agent_runtime_run_profile_binding(root, "code-prototype", parent_run_id)? + else { + return Ok(false); + }; + let Some(root_binding) = read_game_creator_agent_runtime_run_profile_binding( + root, + &child_binding.root_agent_id, + &child_binding.root_run_id, + )? + else { + return Ok(false); + }; + let Some(root_task) = read_latest_game_creator_agent_runtime_task_by_run_id( + root, + &root_binding.agent_id, + &root_binding.run_id, + )? + else { + return Ok(false); + }; + + Ok(child_binding.agent_id == task.agent_id + && child_binding.run_id == task.run_id + && child_binding.source == task.source + && child_binding.profile == task.run_profile + && child_binding.binding_fingerprint == task.run_profile_binding_fingerprint + && child_binding.parent_agent_id == task.parent_agent_id + && child_binding.parent_run_id == task.parent_run_id + && child_binding.profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD + && child_binding.root_agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID + && parent.agent_id == "code-prototype" + && parent.run_id == parent_run_id + && parent.source == "agent-ready-task-scheduler" + && parent.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD + && parent.parent_agent_id.as_deref() == Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID) + && parent.parent_run_id.as_deref() == Some(child_binding.root_run_id.as_str()) + && parent.delegation_id.is_none() + && parent_binding.agent_id == parent.agent_id + && parent_binding.run_id == parent.run_id + && parent_binding.source == parent.source + && parent_binding.profile == parent.run_profile + && parent_binding.binding_fingerprint == parent.run_profile_binding_fingerprint + && parent_binding.parent_agent_id == parent.parent_agent_id + && parent_binding.parent_run_id == parent.parent_run_id + && parent_binding.project_id == child_binding.project_id + && parent_binding.root_agent_id == child_binding.root_agent_id + && parent_binding.root_run_id == child_binding.root_run_id + && child_binding.parent_binding_fingerprint.as_deref() + == Some(parent_binding.binding_fingerprint.as_str()) + && root_binding.project_id == child_binding.project_id + && root_binding.agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID + && root_binding.run_id == child_binding.root_run_id + && root_binding.source == AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE + && root_binding.profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD + && root_binding.root_agent_id == root_binding.agent_id + && root_binding.root_run_id == root_binding.run_id + && root_binding.parent_agent_id.is_none() + && root_binding.parent_run_id.is_none() + && parent_binding.parent_binding_fingerprint.as_deref() + == Some(root_binding.binding_fingerprint.as_str()) + && root_task.agent_id == root_binding.agent_id + && root_task.run_id == root_binding.run_id + && root_task.source == root_binding.source + && root_task.run_profile == root_binding.profile + && root_task.run_profile_binding_fingerprint == root_binding.binding_fingerprint + && root_task.parent_agent_id.is_none() + && root_task.parent_run_id.is_none() + && root_task.delegation_id.is_none()) +} + +fn game_chat_delegated_art_agent_mutation_scope_at( root: &Path, agent_id: &str, run_id: &str, -) -> Result { +) -> Result { let canonical_output = match agent_id { "art-director" => AGENT_RUNTIME_ART_SPEC_PATH, "art-asset-plan" => AGENT_RUNTIME_ART_SPRITESHEET_PATH, - _ => return Ok(false), + _ => return Ok(GameChatDelegatedArtAgentMutationScope::Unrestricted), }; + if let Some(task) = + read_latest_game_creator_agent_runtime_task_by_run_id(root, agent_id, run_id)? + { + if task.source == "agent-delegate-retry" + && game_chat_dynamic_art_child_structural_identity_at(root, &task)? + { + return Ok(GameChatDelegatedArtAgentMutationScope::RetryLineageBlocked); + } + } // `assets/**` is a broad write scope. Grant it only after the same // canonical-output authorization used by Canvas replacement has verified // the live root, main parent, child binding, durable delivery and audited // generate-missing route. - game_chat_fast_path_scheduled_art_contract_repair_is_authorized_at( - root, - agent_id, - run_id, - canonical_output, + Ok( + if game_chat_fast_path_scheduled_art_contract_repair_is_authorized_at( + root, + agent_id, + run_id, + canonical_output, + )? { + GameChatDelegatedArtAgentMutationScope::AssetsOnly + } else { + GameChatDelegatedArtAgentMutationScope::Unrestricted + }, ) } +fn game_chat_dynamic_art_retry_mutation_block(tool: &str) -> AgentRuntimeToolObservation { + AgentRuntimeToolObservation { + tool: tool.to_string(), + status: "blocked".to_string(), + summary: "game-chat 动态美术 child 不支持通用 retry 写入".to_string(), + detail: Some( + "必须回到当前 code-prototype 先完成 asset.list 审计,再按真实缺口建立新的 durable 美术委派" + .to_string(), + ), + } +} + fn game_chat_art_child_tool_is_read_only(tool: &str) -> bool { matches!( tool, @@ -48,17 +182,26 @@ pub(in crate::agent) fn game_chat_delegated_art_agent_project_path_mutation_bloc tool: &str, path: &str, ) -> Option { - match game_chat_delegated_art_agent_is_assets_only_at(root, agent_id, run_id) { - Ok(false) => None, - Ok(true) if path == "assets" || path.starts_with("assets/") => None, - Ok(true) => Some(AgentRuntimeToolObservation { - tool: tool.to_string(), - status: "blocked".to_string(), - summary: "game-chat 临时美术 Agent 只能修改 assets/**".to_string(), - detail: Some(format!( - "path={path} · allowed=assets/** · parentAgentId=code-prototype" - )), - }), + match game_chat_delegated_art_agent_mutation_scope_at(root, agent_id, run_id) { + Ok(GameChatDelegatedArtAgentMutationScope::Unrestricted) => None, + Ok(GameChatDelegatedArtAgentMutationScope::RetryLineageBlocked) => { + Some(game_chat_dynamic_art_retry_mutation_block(tool)) + } + Ok(GameChatDelegatedArtAgentMutationScope::AssetsOnly) + if path == "assets" || path.starts_with("assets/") => + { + None + } + Ok(GameChatDelegatedArtAgentMutationScope::AssetsOnly) => { + Some(AgentRuntimeToolObservation { + tool: tool.to_string(), + status: "blocked".to_string(), + summary: "game-chat 临时美术 Agent 只能修改 assets/**".to_string(), + detail: Some(format!( + "path={path} · allowed=assets/** · parentAgentId=code-prototype" + )), + }) + } Err(error) => Some(AgentRuntimeToolObservation { tool: tool.to_string(), status: "blocked".to_string(), @@ -100,21 +243,28 @@ pub(in crate::agent) fn game_chat_delegated_art_agent_input_mutation_block( }); } } - let assets_only = match game_chat_delegated_art_agent_is_assets_only_at(root, agent_id, run_id) - { - Ok(value) => value, - Err(error) => { - return Some(AgentRuntimeToolObservation { - tool: tool.to_string(), - status: "blocked".to_string(), - summary: "无法校验 game-chat 临时美术 Agent 的写入边界".to_string(), - detail: Some(sanitize_agent_runtime_text(&error, 240)), - }); - } - }; - if !assets_only || game_chat_art_child_tool_is_read_only(tool) { + let mutation_scope = + match game_chat_delegated_art_agent_mutation_scope_at(root, agent_id, run_id) { + Ok(value) => value, + Err(error) => { + return Some(AgentRuntimeToolObservation { + tool: tool.to_string(), + status: "blocked".to_string(), + summary: "无法校验 game-chat 临时美术 Agent 的写入边界".to_string(), + detail: Some(sanitize_agent_runtime_text(&error, 240)), + }); + } + }; + if game_chat_art_child_tool_is_read_only(tool) { return None; } + match mutation_scope { + GameChatDelegatedArtAgentMutationScope::Unrestricted => return None, + GameChatDelegatedArtAgentMutationScope::RetryLineageBlocked => { + return Some(game_chat_dynamic_art_retry_mutation_block(tool)); + } + GameChatDelegatedArtAgentMutationScope::AssetsOnly => {} + } let mut block_path = |path: &str| { game_chat_delegated_art_agent_project_path_mutation_block( root, agent_id, run_id, tool, path, @@ -169,9 +319,16 @@ pub(in crate::agent) fn agent_role_project_path_mutation_block( } } Ok(false) => { - match game_chat_delegated_art_agent_is_assets_only_at(root, agent_id, run_id) { - Ok(true) if path == "assets" || path.starts_with("assets/") => return None, - Ok(true) => { + match game_chat_delegated_art_agent_mutation_scope_at(root, agent_id, run_id) { + Ok(GameChatDelegatedArtAgentMutationScope::RetryLineageBlocked) => { + return Some(game_chat_dynamic_art_retry_mutation_block(tool)); + } + Ok(GameChatDelegatedArtAgentMutationScope::AssetsOnly) + if path == "assets" || path.starts_with("assets/") => + { + return None; + } + Ok(GameChatDelegatedArtAgentMutationScope::AssetsOnly) => { return Some(AgentRuntimeToolObservation { tool: tool.to_string(), status: "blocked".to_string(), @@ -181,7 +338,7 @@ pub(in crate::agent) fn agent_role_project_path_mutation_block( )), }); } - Ok(false) => {} + Ok(GameChatDelegatedArtAgentMutationScope::Unrestricted) => {} Err(error) => { return Some(AgentRuntimeToolObservation { tool: tool.to_string(), diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 7aa0ee9e5..35167ff8e 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -1,5 +1,16 @@ # 决策记录 +## 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 换绑、续发或可变机制。 +- 结构性依据:delivery 的 delegationId 由父动作 ID 派生,且 targetRunId 绑定原 child run;通用 retry 铸造的 `retry:{旧run}:{新run}:{纳秒}` 身份在结构上不可能匹配任何现有 delivery。即使只圈住 retry 的写边界,其产出也没有合法消费者。让 retry 继承 lineage 必须引入可变 delivery、换绑或放宽 exact binding,与不可变事实和失败关闭方向冲突,明确不采纳。 +- 纵深防御:执行策略层把 `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 副作用。 +- 范围限定:仅限上述 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/**` 授权。 +- 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`、`docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md`。 + ## 2026-08-11 固定 owner 产物验证与可玩验收分离 - 背景:真实新项目初始化后没有 `package.json`,默认 `game/index.html` 只是无 `` 的占位页。`design-foundation`、`balance-seed`、`art-asset-plan`、`audio-asset-plan` 位于 `code-prototype` 上游,只负责策划、数值、美术清单和音频清单;若要求它们执行 `project.verify` 或 `game.static_smoke`,前者没有可执行合同,后者只能检查尚未生成的占位游戏并必然失败。曾在测试中预先写入 `fake_llm_game_draft()` 会把占位入口替换成可玩页面,从而掩盖这条真实新项目死锁。 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 1147b581c..331ed264e 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -14,6 +14,14 @@ - 关联:相关文件、文档、提交或 Issue ``` +## 严格 delegated 授权失败不能把 retry 回落为普通美术权限 + +- 现象: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 做非回归。 +- 关联:`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 上游的固定文档产物 - 现象:真实新项目的 `design-foundation`、`balance-seed`、`art-asset-plan` 或 `audio-asset-plan` 已写完自己的固定文件,却始终无法收束;`project.verify` 因项目没有 `package.json` 不可用,`game.static_smoke` 又报告缺少活动 ``。测试若先调用 `fake_llm_game_draft()`,同一路径却会“通过”。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index f0f3cb3a5..33e90f3ec 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -38,6 +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 单主投影阶段。 - 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 87ad95ae3..94a9a9a95 100644 --- a/docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md +++ b/docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md @@ -1348,6 +1348,8 @@ 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 不变。 + ## 20. rollout、停用与回滚 - M1 以 AppData/Runtime capability 开关启用,不把 feature flag 写进用户项目。 @@ -1446,7 +1448,7 @@ M0A-1 是 M1 详细设计与技术 spike 的输入,不是功能交付。M1 可 ### 23.3 M0-4(PR 工作包 `M0B-1` / `M0B-2`) -`M0B-1` 先封闭 game-chat 动态美术 delegate/retry 的 `assets/**` 安全边界,`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 失败关闭,恢复只能由当前 `code-prototype` 重新审计并创建新的首次委派。`M0B-2` 再以 source-aware lineage 修复单主进度、最终回复、可玩 revision 与归档投影。M0-4 不阻塞 M1 策划闭环开工,但阻塞 M3 game-chat 接入和“M0 全部完成”。M0-1~M0-4 全部合入并通过各自门禁后,才能标记“M0 全部完成”。 ## 24. 最终不变量摘要