From 81d9252b6b49256ef37f20261016d59b83769929 Mon Sep 17 00:00:00 2001 From: AIGameCreator App Date: Tue, 21 Jul 2026 16:18:35 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=87=AA=E4=B8=BB=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=E4=BA=A4=E4=BB=98=E4=B8=8E=E8=AF=95=E7=8E=A9=E6=94=B6?= =?UTF-8?q?=E6=95=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复截断 scaffold 强制小范围补丁并提高修复输出预算 修复验证后并行读取批次快照不一致 收窄已验证交付并支持总控验收专业产物 revision 补充自主构建、Provider 批次和试玩验证门回归测试 --- .../src-tauri/src/agent.rs | 232 +++++- .../src-tauri/src/tests.rs | 700 ++++++++++++++++++ 2 files changed, 897 insertions(+), 35 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent.rs b/apps/ai-game-creator-shell/src-tauri/src/agent.rs index 00d1635fd..7e6382cb6 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent.rs @@ -7984,9 +7984,35 @@ async fn run_game_creator_agent_background_task_pass_with_context( plan = requested_plan.plan; if runtime.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD && !plan.response.trim().is_empty() - && agent_runtime_task_requires_read_only_delivery(&agent_id, &task) { - plan.plan_update = agent_runtime_read_only_delivery_completion_plan_update(&runtime); + let read_only_delivery = + agent_runtime_task_requires_read_only_delivery(&agent_id, &task); + let verification_gate = read_game_creator_agent_runtime_verification_gate( + &root, + &agent_id, + &runtime.run_id, + ); + let verified_delivery = match verification_gate { + Ok(gate) => agent_runtime_autonomous_verified_delivery_allows_plan_completion( + &agent_id, &gate, + ), + Err(error) => { + return fail_game_creator_agent_background_context_at( + &root, + &agent_id, + &session_id, + runtime, + &format!("读取自主构建交付验证门失败:{error}"), + ); + } + }; + plan.plan_update = if read_only_delivery { + agent_runtime_read_only_delivery_completion_plan_update(&runtime) + } else if verified_delivery { + agent_runtime_verified_delivery_completion_plan_update(&runtime) + } else { + plan.plan_update + }; } if plan.actions.iter().any(|action| { action.tool == GAME_CREATOR_MCP_CALL_TOOL @@ -10381,6 +10407,7 @@ pub(crate) const AGENT_RUNTIME_AUTONOMOUS_PROVIDER_UPSTREAM_400_RETRY_LIMIT: u32 const AGENT_RUNTIME_AUTONOMOUS_TOOL_PLAN_FORMAT_REPAIR_ATTEMPTS: usize = 4; 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; +pub(crate) const AGENT_RUNTIME_AUTONOMOUS_TRUNCATED_SCAFFOLD_MAX_OUTPUT_TOKENS: u32 = 8_000; const AGENT_RUNTIME_AUTONOMOUS_SCAFFOLD_SOURCE_MAX_CHARS: usize = 6_000; pub(crate) const AGENT_RUNTIME_AUTONOMOUS_PRE_MUTATION_LOOP_LIMIT: usize = 4; pub(crate) const AGENT_RUNTIME_AUTONOMOUS_LIVENESS_OBSERVATION_LIMIT: usize = 3; @@ -10396,6 +10423,8 @@ const AGENT_RUNTIME_AUTONOMOUS_FAILED_PLAYTEST_LIVENESS_ERROR_PREFIX: &str = "自主构建 Project Supervisor 的最近一次交互试玩仍未通过"; const AGENT_RUNTIME_AUTONOMOUS_RESPONSE_PLAN_LIVENESS_ERROR_PREFIX: &str = "自主构建专业 Agent 已给出结论但结构化计划仍未完成"; +const AGENT_RUNTIME_AUTONOMOUS_TRUNCATED_SCAFFOLD_ERROR_PREFIX: &str = + "自主构建完整写入 game/index.html 时必须提交闭合且可运行的 HTML"; const AGENT_RUNTIME_QUALITY_REVIEW_AGENT_ID: &str = "quality-review"; const AGENT_RUNTIME_SUPERVISOR_INITIAL_COLLABORATION_LIVENESS_ERROR_PREFIX: &str = "Project Supervisor 首批协作在首个 planning 窗口内未建立"; @@ -20486,6 +20515,16 @@ fn prepare_and_execute_game_creator_agent_runtime_parallel_read_batch_blocking( } pending_actions.push(pending); } + if provider_batch.is_some() { + let current_cursor_gate = pending_actions + .first() + .ok_or_else(|| "Provider action 批次缺少只读并行 cursor 成员".to_string())? + .verification_gate_before + .clone(); + for pending in &mut pending_actions { + pending.verification_gate_before = current_cursor_gate.clone(); + } + } let project_id = game_creator_agent_runtime_context_project_id(&root)?; let batch_id = agent_runtime_parallel_read_batch_id( &project_id, @@ -21429,6 +21468,13 @@ pub(crate) fn finish_agent_runtime_project_verification_locked( gate.verified_revision = passed .then_some(current_revision.revision) .filter(|value| *value > 0); + if passed + && gate + .failed_playtest_revision + .is_some_and(|failed_revision| failed_revision < current_revision.revision) + { + gate.failed_playtest_revision = None; + } if passed && verification_tool.as_deref() == Some("preview.validate") { gate.failed_playtest_revision = None; } @@ -21451,7 +21497,7 @@ pub(crate) fn finish_agent_runtime_project_verification_locked( Ok(()) } -fn clear_agent_runtime_failed_playtest_at( +pub(crate) fn clear_agent_runtime_failed_playtest_at( root: &Path, agent_id: &str, run_id: &str, @@ -21467,7 +21513,12 @@ fn clear_agent_runtime_failed_playtest_at( )); } let mut gate = read_game_creator_agent_runtime_verification_gate(root, agent_id, run_id)?; - if gate.mutation_revision != Some(expected_revision) { + let owns_current_mutation = gate.mutation_revision == Some(expected_revision); + let supervisor_verified_current_revision = agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID + && gate.verified_revision == Some(expected_revision) + && gate.last_verification_status.as_deref() + == Some(AGENT_RUNTIME_VERIFICATION_STATUS_PASSED); + if !owns_current_mutation && !supervisor_verified_current_revision { return Err("浏览器试玩通过时没有当前 revision 的项目修改凭证".to_string()); } gate.failed_playtest_revision = None; @@ -21491,10 +21542,21 @@ pub(crate) fn invalidate_agent_runtime_project_verification_after_preview_failur )); } let mut gate = read_game_creator_agent_runtime_verification_gate(root, agent_id, run_id)?; - if gate.mutation_revision != Some(expected_revision) { + let owns_current_mutation = gate.mutation_revision == Some(expected_revision); + let supervisor_verified_current_revision = agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID + && gate.verified_revision == Some(expected_revision) + && gate.last_verification_status.as_deref() + == Some(AGENT_RUNTIME_VERIFICATION_STATUS_PASSED); + if !owns_current_mutation && !supervisor_verified_current_revision { return Err("浏览器试玩失败时没有当前 revision 的项目修改凭证".to_string()); } - gate.requires_verification = true; + if owns_current_mutation { + gate.requires_verification = true; + } else { + gate.requires_verification = false; + gate.mutation_revision = None; + gate.last_mutation_tool = None; + } gate.verified_revision = None; gate.last_verification_tool = Some("preview.validate".to_string()); gate.last_verification_status = Some(AGENT_RUNTIME_VERIFICATION_STATUS_FAILED.to_string()); @@ -24641,6 +24703,17 @@ async fn request_game_creator_agent_background_tool_plan_at( }; let read_only_delivery = run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD && agent_runtime_task_requires_read_only_delivery(agent_id, task); + let verified_delivery = if run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD { + let verification_gate = + read_game_creator_agent_runtime_verification_gate(root, agent_id, run_id)?; + agent_runtime_autonomous_verified_delivery_allows_plan_completion( + agent_id, + &verification_gate, + ) + } else { + false + }; + let allow_runtime_plan_completion = read_only_delivery || verified_delivery; let mut autonomous_scaffold_repair_active = false; for repair_attempt in 0..=format_repair_attempts { if game_creator_agent_runtime_cancel_requested_for(root, agent_id, run_id) { @@ -24765,7 +24838,7 @@ async fn request_game_creator_agent_background_tool_plan_at( root, agent_id, session_id, - read_only_delivery, + allow_runtime_plan_completion, &parsed.plan, ) .map_err(|error| { @@ -24786,9 +24859,12 @@ async fn request_game_creator_agent_background_tool_plan_at( { let verification_gate = read_game_creator_agent_runtime_verification_gate(root, agent_id, run_id)?; + let project_revision = + read_game_creator_agent_runtime_project_revision(root)?.revision; match validate_agent_runtime_autonomous_plan_liveness( agent_id, loop_index, + project_revision, &verification_gate, observations, &parsed.plan, @@ -25029,6 +25105,11 @@ async fn request_game_creator_agent_background_tool_plan_at( AGENT_RUNTIME_AUTONOMOUS_RESPONSE_PLAN_LIVENESS_ERROR_PREFIX, ) && !request.function_tools.is_empty(); + let force_autonomous_truncated_scaffold = run_profile + == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD + && protocol_error + .starts_with(AGENT_RUNTIME_AUTONOMOUS_TRUNCATED_SCAFFOLD_ERROR_PREFIX) + && !request.function_tools.is_empty(); let force_supervisor_initial_collaboration = agent_runtime_protocol_error_requires_supervisor_collaboration_repair( &protocol_error, @@ -25039,6 +25120,7 @@ async fn request_game_creator_agent_background_tool_plan_at( || force_autonomous_pending_verification || force_autonomous_reverify_after_mutation || force_autonomous_verified_delivery + || force_autonomous_truncated_scaffold || force_autonomous_pre_mutation { request.function_tools = @@ -25070,9 +25152,17 @@ async fn request_game_creator_agent_background_tool_plan_at( "上一条输出不符合工具计划协议:{protocol_error}\n本次修复的原生工具目录已收窄。必须直接调用实际项目修改工具修复已知问题,或在项目已经满足要求时立即调用 project.verify / command.run_limited 取得当前 revision 的通过凭证。不得只更新计划、读取、搜索、查询状态或 respond_to_user。源码仍须遵守单字段 {AGENT_RUNTIME_AUTONOMOUS_SOURCE_FIELD_MAX_CHARS} 字符和单轮总计 {AGENT_RUNTIME_AUTONOMOUS_SOURCE_TOTAL_MAX_CHARS} 字符上限;优先使用小范围 file.patch。不要解释,不要 markdown,不要代码围栏。" ))); } else if force_autonomous_verified_delivery { - restrict_agent_runtime_autonomous_liveness_repair_tools(&mut request)?; + restrict_agent_runtime_autonomous_verified_delivery_repair_tools(&mut request)?; request.messages.push(LlmMessage::user(format!( - "上一条输出不符合工具计划协议:{protocol_error}\n当前 revision 已通过验证。本次修复必须直接调用 respond_to_user 交付专业合同结论;只有确有阻塞性交付缺口时才调用一个实际项目修改工具并使验证凭证失效。不得继续只更新计划、读取、搜索或查询状态。不要解释,不要 markdown,不要代码围栏。" + "上一条输出不符合工具计划协议:{protocol_error}\n当前 revision 已通过验证。本次修复的原生工具目录只保留 respond_to_user;必须立即调用它交付专业合同结论,Runtime 会使用当前验证凭证收束结构化计划。不得更新计划、读取、搜索、查询状态、修改项目或解释。不要 markdown,不要代码围栏。" + ))); + } else if force_autonomous_truncated_scaffold { + autonomous_scaffold_repair_active = true; + restrict_agent_runtime_autonomous_truncated_scaffold_repair_tools( + &mut request, + )?; + request.messages.push(LlmMessage::user(format!( + "上一条输出不符合工具计划协议:{protocol_error}\n无效的完整写入已被拒绝,现有 game/index.html 仍保持闭合且未修改。本次修复的原生工具目录只保留 file.patch;必须使用之前 file.read / project.search observation 中的短小精确原文作为 oldText,将一个可独立运行的小功能补丁插入现有文档,保留已有 与 ,不能再次完整重写文件。newText 不得超过 {AGENT_RUNTIME_AUTONOMOUS_SCAFFOLD_SOURCE_MAX_CHARS} 字符,且本轮结束后的 HTML 和 JavaScript 必须完整闭合;其余功能留到后续 planning 继续小步 patch。不要解释,不要 markdown,不要代码围栏。" ))); } else if force_autonomous_read_only_delivery { restrict_agent_runtime_autonomous_read_only_delivery_repair_tools( @@ -28253,6 +28343,7 @@ fn validate_agent_runtime_autonomous_response_plan_completion( fn validate_agent_runtime_autonomous_plan_liveness( agent_id: &str, loop_index: usize, + project_revision: u64, verification_gate: &AgentRuntimeVerificationGate, observations: &[AgentRuntimeToolObservation], plan: &AgentRuntimeToolPlan, @@ -28261,21 +28352,30 @@ fn validate_agent_runtime_autonomous_plan_liveness( .actions .iter() .any(|action| is_supervisor_orchestrator_project_mutation_tool(&action.tool)); + let has_verification = plan.actions.iter().any(|action| { + matches!( + action.tool.trim(), + "project.verify" | "command.run_limited" | "command.exec" + ) + }); if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID { - if verification_gate - .failed_playtest_revision - .zip(verification_gate.mutation_revision) - .is_some_and(|(failed_playtest_revision, mutation_revision)| { - failed_playtest_revision == mutation_revision - }) - && !has_mutation - { - return Err(format!( - "{AGENT_RUNTIME_AUTONOMOUS_FAILED_PLAYTEST_LIVENESS_ERROR_PREFIX};持久验证门仍标记 revision {} 的试玩失败,即使上下文压缩后也必须根据最近一次试玩诊断直接修改 game/index.html;不得继续只更新计划、读取、搜索、重复验证、查询状态、委派或返回最终回复", - verification_gate - .failed_playtest_revision - .unwrap_or_default() - )); + if let Some(failed_playtest_revision) = verification_gate.failed_playtest_revision { + if project_revision < failed_playtest_revision { + return Err("自主构建持久试玩失败 revision 晚于当前项目 revision".to_string()); + } + if project_revision > failed_playtest_revision { + if has_mutation || has_verification { + return Ok(()); + } + return Err(format!( + "{AGENT_RUNTIME_AUTONOMOUS_REVERIFY_AFTER_MUTATION_LIVENESS_ERROR_PREFIX};试玩失败后项目已从 revision {failed_playtest_revision} 推进到 {project_revision},必须先验证当前 revision,才能重新执行 preview.validate;不得继续只更新计划、读取、搜索、查询状态、委派或返回最终回复" + )); + } + if !has_mutation { + return Err(format!( + "{AGENT_RUNTIME_AUTONOMOUS_FAILED_PLAYTEST_LIVENESS_ERROR_PREFIX};持久验证门仍标记 revision {failed_playtest_revision} 的试玩失败,即使上下文压缩后也必须根据最近一次试玩诊断直接修改 game/index.html;不得继续只更新计划、读取、搜索、重复验证、查询状态、委派或返回最终回复" + )); + } } let Some(failed_playtest_index) = observations.iter().rposition(|observation| { observation.tool == "preview.validate" @@ -28299,12 +28399,6 @@ fn validate_agent_runtime_autonomous_plan_liveness( observations_after_failure.len() )); } - let has_verification = plan.actions.iter().any(|action| { - matches!( - action.tool.trim(), - "project.verify" | "command.run_limited" | "command.exec" - ) - }); let latest_mutation_index = observations .iter() .rposition(is_agent_runtime_project_mutation_observation); @@ -28343,11 +28437,10 @@ fn validate_agent_runtime_autonomous_plan_liveness( "{error_prefix};最近一次修改或验证后已有 {trailing_non_progress} 条非推进观察,本响应仍未提交项目修改或验证动作。请根据当前 revision 立即修复或验证;不得继续只更新计划、读取、搜索、查询状态或返回最终回复" )); } - if mutation_revision.is_some() - && verified_revision == mutation_revision - && verification_gate.last_verification_status.as_deref() - == Some(AGENT_RUNTIME_VERIFICATION_STATUS_PASSED) - { + if agent_runtime_autonomous_verified_delivery_allows_plan_completion( + agent_id, + verification_gate, + ) { if has_mutation || !plan.response.trim().is_empty() || trailing_non_progress < AGENT_RUNTIME_AUTONOMOUS_LIVENESS_OBSERVATION_LIMIT @@ -28370,6 +28463,17 @@ fn validate_agent_runtime_autonomous_plan_liveness( )) } +fn agent_runtime_autonomous_verified_delivery_allows_plan_completion( + agent_id: &str, + verification_gate: &AgentRuntimeVerificationGate, +) -> bool { + agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID + && verification_gate.mutation_revision.is_some() + && verification_gate.verified_revision == verification_gate.mutation_revision + && verification_gate.last_verification_status.as_deref() + == Some(AGENT_RUNTIME_VERIFICATION_STATUS_PASSED) +} + fn agent_runtime_task_requires_read_only_delivery(agent_id: &str, task: &str) -> bool { let normalized = task.to_ascii_lowercase(); let explicitly_read_only = [ @@ -28417,6 +28521,22 @@ fn agent_runtime_task_requires_read_only_delivery(agent_id: &str, task: &str) -> pub(crate) fn agent_runtime_read_only_delivery_completion_plan_update( runtime: &AgentRuntimeState, +) -> Option { + agent_runtime_delivery_completion_plan_update(runtime, "只读合同结论和证据已经交付") +} + +pub(crate) fn agent_runtime_verified_delivery_completion_plan_update( + runtime: &AgentRuntimeState, +) -> Option { + agent_runtime_delivery_completion_plan_update( + runtime, + "当前 revision 已通过验证,专业合同结论已经交付", + ) +} + +fn agent_runtime_delivery_completion_plan_update( + runtime: &AgentRuntimeState, + fallback_explanation: &str, ) -> Option { if !agent_runtime_has_structured_plan(runtime) || runtime @@ -28432,7 +28552,7 @@ pub(crate) fn agent_runtime_read_only_delivery_completion_plan_update( } Some(AgentRuntimePlanUpdate { explanation: if runtime.plan_explanation.trim().is_empty() { - "只读合同结论和证据已经交付".to_string() + fallback_explanation.to_string() } else { runtime.plan_explanation.clone() }, @@ -28494,6 +28614,24 @@ fn restrict_agent_runtime_autonomous_read_only_delivery_repair_tools( Ok(()) } +fn restrict_agent_runtime_autonomous_verified_delivery_repair_tools( + request: &mut LlmRunRequest, +) -> Result<(), String> { + request + .function_tools + .retain(|tool| tool.name == AGENT_RUNTIME_RESPOND_FUNCTION_NAME); + if request.function_tools.len() != 1 { + return Err("autonomous 已验证交付修复工具目录缺少 respond_to_user".to_string()); + } + request.max_output_tokens = Some( + request + .max_output_tokens + .unwrap_or(AGENT_RUNTIME_AUTONOMOUS_FORCED_ACTION_MAX_OUTPUT_TOKENS) + .min(AGENT_RUNTIME_AUTONOMOUS_FORCED_ACTION_MAX_OUTPUT_TOKENS), + ); + Ok(()) +} + fn restrict_agent_runtime_autonomous_liveness_repair_tools( request: &mut LlmRunRequest, ) -> Result<(), String> { @@ -28534,6 +28672,30 @@ fn restrict_agent_runtime_autonomous_liveness_repair_tools( Ok(()) } +fn restrict_agent_runtime_autonomous_truncated_scaffold_repair_tools( + request: &mut LlmRunRequest, +) -> Result<(), String> { + let patch_function = native_runtime_function_name("file.patch") + .ok_or_else(|| "无法生成 autonomous 截断 scaffold 修复工具名".to_string())?; + request + .function_tools + .retain(|tool| tool.name == patch_function); + apply_agent_runtime_autonomous_source_schema_limits_with_max( + &mut request.function_tools, + AGENT_RUNTIME_AUTONOMOUS_SCAFFOLD_SOURCE_MAX_CHARS, + )?; + if request.function_tools.len() != 1 { + return Err("autonomous 截断 scaffold 修复工具目录缺少 file.patch".to_string()); + } + request.max_output_tokens = Some( + request + .max_output_tokens + .unwrap_or(AGENT_RUNTIME_AUTONOMOUS_TRUNCATED_SCAFFOLD_MAX_OUTPUT_TOKENS) + .max(AGENT_RUNTIME_AUTONOMOUS_TRUNCATED_SCAFFOLD_MAX_OUTPUT_TOKENS), + ); + Ok(()) +} + fn restrict_agent_runtime_autonomous_failed_playtest_repair_tools( request: &mut LlmRunRequest, ) -> Result<(), String> { diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests.rs b/apps/ai-game-creator-shell/src-tauri/src/tests.rs index 0bc8277bb..8b3d28cd2 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests.rs @@ -6652,6 +6652,162 @@ async fn autonomous_game_build_repairs_oversized_native_source_payload() { fs::remove_dir_all(root).ok(); } +#[tokio::test] +async fn autonomous_game_build_repairs_truncated_scaffold_into_bounded_patch() { + let root = unique_project_path(); + init_local_game_project_at( + &root, + "project-autonomous-truncated-scaffold-repair", + "自主截断 scaffold 修复项目", + ) + .expect("project init"); + write_supervisor_collaboration_policy_at(&root, SupervisorCollaborationPolicy::default()) + .expect("keep truncated scaffold fixture collaboration-neutral"); + let (sender, receiver) = mpsc::channel(); + let write_function = native_runtime_function_name("file.write").expect("write function"); + let patch_function = native_runtime_function_name("file.patch").expect("patch function"); + let truncated_arguments = serde_json::json!({ + "reason": "先写入游戏入口", + "input": { + "path": "game/index.html", + "content": "