From 732c6ee3f4a21542264bd3788af2f231d001d5a4 Mon Sep 17 00:00:00 2001 From: kdletters Date: Mon, 3 Aug 2026 19:03:54 +0800 Subject: [PATCH 01/60] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20game-chat=20?= =?UTF-8?q?=E7=BB=AD=E8=B7=91=E7=9B=AE=E6=A0=87=E6=BC=82=E7=A7=BB=E4=B8=8E?= =?UTF-8?q?=E4=BA=A7=E7=89=A9=E8=A6=86=E7=9B=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 继承同一会话失败根任务的原始目标与完成基线 保护非占位游戏入口并拒绝重复整文件覆盖 纠正规范图运行时语义与状态序列推进规则 补充续跑继承、图集门禁和事故文档回归 --- .../provider_request_builders.rs | 26 +- .../src/agent/runtime_driver/entrypoints.rs | 18 + .../runtime_driver/game_chat_fast_path.rs | 483 +++++++++++++----- .../src/agent/runtime_driver/task_start.rs | 2 +- .../runtime_protocol/autonomous_completion.rs | 218 +++++++- .../autonomous_completion_contract_tests.rs | 366 ++++++++----- .../shared-memory/decision-log.md | 7 + docs/project-memory/shared-memory/pitfalls.md | 8 + ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 11 +- 9 files changed, 842 insertions(+), 297 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_request_builders.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_request_builders.rs index 383a7c415..b34101253 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_request_builders.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_request_builders.rs @@ -2,7 +2,7 @@ use super::*; const AGENT_RUNTIME_COMPLETION_BLOCKER_TOOL_PLAN_PROTOCOL: &str = "通用完成阻断规则:如果最新 observation 的 tool 为 runtime.autonomous_completion 且 status 为 blocked,本轮禁止直接调用 respond_to_user,也禁止在 legacy response 中填写最终回复;必须先读取该 observation.detail 的 nextRequiredAction,并据此调用合适的读取、修复和验证工具。只有完成要求的动作、取得后续可信 observation 且完成门禁不再阻断后,才能给最终回复;不得反复提交 final response,也不得按项目正文硬编码某一种 blocker 的处理方式。"; -const GAME_CHAT_CODE_PROTOTYPE_FAST_PATH_PROMPT: &str = "game-chat 首版使用五分钟快车道。当前任务的第一目标是在一次 Provider planning 内产出首个完整可玩版本:如果最新 observation 尚未显示 game/index.html 已由本 run 写入,本响应必须直接调用一次 file.write,把完整、自包含、可运行的 game/index.html 一次写完;禁止先调用读取、搜索、任务查询、委派、只更新计划或提交半成品。HTML 必须满足下方固定试玩合同,包含真实 Canvas 游戏循环、键盘与触控输入、开始、主要操作、重开、胜负状态和移动端布局;可以采用保守的原创玩法默认值。已登记的平台视觉规范图 ../assets/art-spec.png 是首版必需资源,必须在主要游戏画面中显著可见使用:至少把规范图实际绘制为主要背景,并从规范图中绘制玩家角色和目标实体。禁止仅放置隐藏 img、透明或屏外元素、微小水印、不可见预加载或只在源码中引用;也禁止用纯几何图形冒充平台图片使用。若规范图无法加载,游戏必须明确失败关闭,不能退回纯 Canvas 几何兜底。一次写入后不要继续扩写功能;Runtime 会在下一步自动执行静态自检并在通过后立即试玩。"; +const GAME_CHAT_CODE_PROTOTYPE_FAST_PATH_PROMPT: &str = "game-chat 首版使用五分钟快车道。必须先保护既有游戏语义和已完成产物:如果已有 observation 尚未包含 game/index.html 的当前正文与摘要,本响应第一步必须调用 file.read(path=game/index.html),不得猜测入口仍是初始化占位。只有文件缺失或正文与初始化页面一致、且当前 run 尚未写入项目时,才允许用一次 file.write 生成完整、自包含、可运行的 game/index.html;检测到非占位入口、已有可玩实现、当前 revision 已验证,或当前 run 已发生修改时,禁止整文件 file.write,必须保留原玩法,优先执行静态检查/试玩,确需修复时只能依据已读取的精确原文做最小 file.patch。不得把“继续”、continue 或其它纯续跑词当作游戏主题;缺少可继承的具体目标时必须失败关闭,不得另造收集能量等新玩法。HTML 必须满足下方固定试玩合同,包含真实 Canvas 游戏循环、键盘与触控输入、开始、主要操作、重开、胜负状态和移动端布局。../assets/art-spec.png 是视觉规范图,只能用于理解风格、构图与色板,禁止把整张规范图铺成游戏背景,禁止从中裁切或绘制玩家、目标和场景实体,也不能把它当作运行时必需图片;只有存在已登记的独立派生 spritesheet 时才可加载该派生素材,否则使用与规范一致的自包含原创 Canvas 视觉,并保证规范图加载失败不影响游戏启动。首次实现或最小修复后不要继续扩写功能;Runtime 会自动执行静态自检并在通过后立即试玩。"; fn game_chat_fast_path_prompt_for_root_source( agent_id: &str, @@ -507,7 +507,7 @@ mod tests { } #[test] - fn game_chat_fast_path_prompt_forces_one_shot_playable_write_only_for_code_agent() { + fn game_chat_fast_path_prompt_protects_existing_game_and_treats_art_spec_as_reference() { let prompt = game_chat_fast_path_prompt_for_root_source( "code-prototype", AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE, @@ -515,16 +515,20 @@ mod tests { .expect("game-chat code fast path prompt"); assert!(prompt.contains("五分钟快车道")); - assert!(prompt.contains("一次 Provider planning")); - assert!(prompt.contains("直接调用一次 file.write")); - assert!(prompt.contains("禁止先调用读取、搜索、任务查询、委派")); + assert!(prompt.contains("第一步必须调用 file.read(path=game/index.html)")); + assert!(prompt.contains("正文与初始化页面一致")); + assert!(prompt.contains("当前 run 尚未写入项目")); + assert!(prompt.contains("检测到非占位入口")); + assert!(prompt.contains("禁止整文件 file.write")); + assert!(prompt.contains("最小 file.patch")); + assert!(prompt.contains("不得把“继续”")); assert!(prompt.contains("../assets/art-spec.png")); - assert!(prompt.contains("平台视觉规范图")); - assert!(prompt.contains("主要背景")); - assert!(prompt.contains("玩家角色和目标实体")); - assert!(prompt.contains("禁止仅放置隐藏 img")); - assert!(prompt.contains("不能退回纯 Canvas 几何兜底")); - assert!(!prompt.contains("art-spritesheet.png")); + assert!(prompt.contains("只能用于理解风格、构图与色板")); + assert!(prompt.contains("禁止把整张规范图铺成游戏背景")); + assert!(prompt.contains("禁止从中裁切或绘制玩家、目标和场景实体")); + assert!(prompt.contains("自包含原创 Canvas 视觉")); + assert!(prompt.contains("规范图加载失败不影响游戏启动")); + assert!(prompt.contains("独立派生 spritesheet")); assert!(game_chat_fast_path_prompt_for_root_source( "quality-review", AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE, diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/entrypoints.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/entrypoints.rs index e19b59259..685e48136 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/entrypoints.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/entrypoints.rs @@ -432,6 +432,24 @@ pub(in crate::agent) fn read_game_creator_agent_runtime_with_session_filter_at( } } } + if !state.run_id.trim().is_empty() && !state.current_task.trim().is_empty() { + match autonomous_effective_root_task_at( + root, + &state.agent_id, + &state.run_id, + &state.current_task, + ) { + Ok(effective_task) => state.current_task = effective_task, + Err(error) => { + state.status = "failed".to_string(); + state.phase = "needs-reconciliation".to_string(); + state.current_action = "自主构建续跑语义需要人工核对".to_string(); + state.waiting_on = "开发者核对上一根 Run 与完成合同".to_string(); + state.next_step = "修复续跑身份后恢复当前 run".to_string(); + state.error = Some(sanitize_agent_runtime_text(&error, 500)); + } + } + } if let Err(error) = hydrate_game_creator_agent_goal_state_at(root, &mut state) { state.status = "failed".to_string(); state.phase = "needs-reconciliation".to_string(); diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/game_chat_fast_path.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/game_chat_fast_path.rs index 8a8f51cdc..5275693a4 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/game_chat_fast_path.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/game_chat_fast_path.rs @@ -12,7 +12,6 @@ pub(crate) const GAME_CHAT_FIRST_PLAYABLE_HARD_BUDGET_ERROR_PREFIX: &str = "game-chat-first-playable-hard-budget-exhausted"; const FALLBACK_THEME_MARKER: &str = "__GAME_CHAT_THEME__"; -const FALLBACK_PLATFORM_ART_MARKER: &str = "__GAME_CHAT_PLATFORM_ART__"; #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct GameChatFastPathBudget { @@ -93,11 +92,8 @@ fn game_chat_fast_path_fallback_write_plan_for_root( root: &Path, task: &str, ) -> Result { - if !game_chat_fast_path_has_platform_art_asset(root) { - return Err( - "game-chat 首版缺少已登记且可验证的平台视觉规范图,拒绝退回纯几何 Canvas".to_string(), - ); - } + ensure_game_chat_fallback_theme_is_specific(task)?; + ensure_game_chat_fallback_targets_initial_placeholder(root)?; Ok(game_chat_fast_path_action( "file.write", serde_json::json!({ @@ -107,11 +103,53 @@ fn game_chat_fast_path_fallback_write_plan_for_root( )) } -fn game_chat_fast_path_has_platform_art_asset(root: &Path) -> bool { - let Ok(manifest) = read_manifest_for_project(root) else { - return false; - }; - validate_manifest_required_visual_asset(root, &manifest, "art-director").is_ok() +fn ensure_game_chat_fallback_theme_is_specific(task: &str) -> Result<(), String> { + let normalized = task + .trim() + .trim_matches(|character: char| { + character.is_whitespace() + || matches!(character, '。' | ',' | ',' | '.' | '!' | '!' | '?' | '?') + }) + .to_ascii_lowercase(); + if normalized.is_empty() + || matches!( + normalized.as_str(), + "继续" | "继续吧" | "继续做" | "接着" | "接着做" | "continue" | "go on" + ) + { + return Err( + "game-chat 首版缺少可继承的具体游戏目标,拒绝把纯续跑指令当作新游戏主题".to_string(), + ); + } + Ok(()) +} + +fn ensure_game_chat_fallback_targets_initial_placeholder(root: &Path) -> Result<(), String> { + if game_chat_fallback_targets_initial_placeholder(root)? { + Ok(()) + } else { + Err( + "game-chat 首版检测到既有非占位 game/index.html,拒绝 fallback 整文件覆盖;应保留现有玩法并继续静态检查或试玩" + .to_string(), + ) + } +} + +fn game_chat_fallback_targets_initial_placeholder(root: &Path) -> Result { + let index_path = root.join(AGENT_RUNTIME_GAME_INDEX_PATH); + match fs::symlink_metadata(&index_path) { + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(true), + Err(error) => Err(format!( + "game-chat 首版无法检查现有游戏入口,拒绝 fallback 覆盖:{error}" + )), + Ok(metadata) if !metadata.file_type().is_file() => { + Err("game-chat 首版现有游戏入口不是普通文件,拒绝 fallback 覆盖".to_string()) + } + Ok(_) => { + let current = read_local_project_file_at(root, AGENT_RUNTIME_GAME_INDEX_PATH)?; + Ok(current.content == DEFAULT_GAME_INDEX_HTML) + } + } } fn game_chat_fast_path_has_visual_asset(root: &Path, task_id: &str) -> bool { @@ -137,13 +175,18 @@ fn game_chat_fast_path_root_task( root: &Path, budget: &GameChatFastPathBudget, ) -> Result { - let root_task = read_latest_game_creator_agent_runtime_task_by_run_id( + let root_record = read_latest_game_creator_agent_runtime_task_by_run_id( root, &budget.root_agent_id, &budget.root_run_id, )? - .ok_or_else(|| "game-chat 首版快车道缺少 root 任务记录".to_string())? - .task; + .ok_or_else(|| "game-chat 首版快车道缺少 root 任务记录".to_string())?; + let root_task = autonomous_effective_root_task_at( + root, + &budget.root_agent_id, + &budget.root_run_id, + &root_record.task, + )?; if root_task.trim().is_empty() { return Err("game-chat 首版快车道 root 任务为空".to_string()); } @@ -155,7 +198,7 @@ fn game_chat_fast_path_canvas_asset_plan(task_id: &str, root_task: &str) -> Agen let (prompt, output_path, aspect_ratio, image_size, asset_kind, asset_label) = match task_id { "art-director" => ( format!( - "为原创小游戏“{theme}”生成统一视觉规范图:清晰展示玩家主体、目标物、场景地块、障碍、UI 图标、状态反馈、统一色板和材质规则;同一张图必须可直接作为首版主要背景、玩家和目标的可见绘制来源,不得使用现有知名游戏角色或标识。" + "为原创小游戏“{theme}”生成统一视觉规范图:清晰展示玩家主体、目标物、场景地块、障碍、UI 图标、状态反馈、统一色板和材质规则;该图只用于指导风格、构图、色板和后续独立素材派生,不是游戏截图或可直接铺入运行画面的图集,不得从中裁切玩家、目标或背景,不得使用现有知名游戏角色或标识。" ), AGENT_RUNTIME_ART_SPEC_PATH, "1:1", @@ -185,6 +228,44 @@ pub(crate) fn game_chat_fast_path_fallback_write_plan_for_budget_at( _fallback_task: &str, ) -> Result { let root_task = game_chat_fast_path_root_task(root, budget)?; + let contract = + read_autonomous_completion_contract(root, &budget.root_agent_id, &budget.root_run_id)? + .ok_or_else(|| "game-chat 首版快车道缺少 root 完成合同".to_string())?; + let default_index_sha256 = format!("{:x}", Sha256::digest(DEFAULT_GAME_INDEX_HTML.as_bytes())); + if contract + .baseline_index_sha256 + .as_deref() + .is_some_and(|sha256| sha256 != default_index_sha256) + { + return Ok(game_chat_fast_path_action( + "command.run_limited", + serde_json::json!({ "commandId": "game.static_smoke" }), + )); + } + let code_run_id = autonomous_manifest_ready_task_run_id(&budget.root_run_id, "code-prototype"); + let code_gate = + read_game_creator_agent_runtime_verification_gate(root, "code-prototype", &code_run_id)?; + if !game_chat_fallback_targets_initial_placeholder(root)? { + return Ok(game_chat_fast_path_action( + "command.run_limited", + serde_json::json!({ "commandId": "game.static_smoke" }), + )); + } + if code_gate.mutation_revision.is_some() { + return Err( + "game-chat 首版 code-prototype 已在当前 Run 写入项目,拒绝 fallback 再次整文件覆盖" + .to_string(), + ); + } + let revision = read_game_creator_agent_runtime_project_revision(root)?; + if read_autonomous_playtest_receipt(root, &contract)? + .is_some_and(|receipt| receipt.revision == revision.revision) + { + return Ok(game_chat_fast_path_action( + "command.run_limited", + serde_json::json!({ "commandId": "game.static_smoke" }), + )); + } game_chat_fast_path_fallback_write_plan_for_root(root, &root_task) } @@ -330,8 +411,10 @@ pub(crate) fn game_chat_fast_path_plan_at( serde_json::json!({ "commandId": "game.static_smoke" }), ))); } - if current_revision_failed - || runtime.loop_iteration > 1 + if current_revision_failed { + return Ok(None); + } + if runtime.loop_iteration > 1 || budget.elapsed_seconds >= GAME_CHAT_FIRST_PLAYABLE_SOFT_BUDGET_SECONDS { return Ok(Some(game_chat_fast_path_fallback_write_plan_for_budget_at( @@ -350,11 +433,7 @@ pub(crate) fn game_chat_fast_path_plan_at( /// attribute and text nodes; it is never interpolated into JavaScript source. pub(crate) fn render_game_chat_fast_path_html(prompt: &str) -> String { let theme = html_escape(&safe_theme_summary(prompt)); - let platform_art = - r#"平台生成的统一视觉规范图"#; - FALLBACK_GAME_HTML - .replace(FALLBACK_THEME_MARKER, &theme) - .replace(FALLBACK_PLATFORM_ART_MARKER, platform_art) + FALLBACK_GAME_HTML.replace(FALLBACK_THEME_MARKER, &theme) } fn safe_theme_summary(prompt: &str) -> String { @@ -427,7 +506,6 @@ const FALLBACK_GAME_HTML: &str = r###" button:hover, button:focus-visible { border-color: #9ed0ff; background: #214a76; outline: none; } .state-line { display: flex; justify-content: space-between; gap: 12px; padding: 12px 14px; color: #cce1fa; background: #102541; } .state-line strong { color: #8fe2c4; } - #platform-art { position: absolute; z-index: 2; top: 18px; right: 18px; width: min(24%, 180px); aspect-ratio: 1; object-fit: contain; border: 2px solid #9ed0ff; border-radius: 16px; background: #07101dcc; box-shadow: 0 12px 28px #02081499; opacity: .88; pointer-events: none; } @media (max-width: 560px) { header { display: block; } .hud { justify-content: flex-start; margin-top: 10px; } .controls { grid-template-columns: 1fr; } } @@ -441,7 +519,6 @@ const FALLBACK_GAME_HTML: &str = r###"
得分 0准备就绪
- __GAME_CHAT_PLATFORM_ART__
点击开始,然后操作收集能量准备就绪