From 1fce5460f9064648f41f3e7c26265c0c696b4179 Mon Sep 17 00:00:00 2001 From: AIGameCreator App Date: Tue, 21 Jul 2026 17:26:16 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=80=BB=E6=8E=A7=E8=AF=95?= =?UTF-8?q?=E7=8E=A9=E5=A4=B1=E8=B4=A5=E5=90=8E=E7=9A=84=E4=B8=93=E4=B8=9A?= =?UTF-8?q?=E8=BF=94=E5=B7=A5=E6=94=B6=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 总控进入协作后将试玩失败收窄为 code-prototype 后续委派,避免越权直接修改项目 保留无协作兼容路径和新 revision 先复验再试玩的完成门禁 补齐协作分支回归测试并修正自主完成合同测试夹具 同步 Runtime 技术方案、踩坑记录和 Rust 格式 --- .../src-tauri/src/agent.rs | 153 +++++++--- .../src-tauri/src/project.rs | 6 +- .../src-tauri/src/tests.rs | 283 +++++++++++++++--- docs/project-memory/shared-memory/pitfalls.md | 8 + ...¹案】AI游戏创作智能体App实施计划-2026-06-24.md | 2 + 5 files changed, 373 insertions(+), 79 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 7e6382cb6..caf06458d 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent.rs @@ -10421,6 +10421,8 @@ const AGENT_RUNTIME_AUTONOMOUS_VERIFIED_DELIVERY_LIVENESS_ERROR_PREFIX: &str = "自主构建专业 Agent 已取得当前 revision 的通过凭证"; const AGENT_RUNTIME_AUTONOMOUS_FAILED_PLAYTEST_LIVENESS_ERROR_PREFIX: &str = "自主构建 Project Supervisor 的最近一次交互试玩仍未通过"; +const AGENT_RUNTIME_AUTONOMOUS_DELEGATED_PLAYTEST_REPAIR_LIVENESS_ERROR_PREFIX: &str = + "自主构建 Project Supervisor 的最近一次交互试玩需要专业 Agent 修复"; const AGENT_RUNTIME_AUTONOMOUS_RESPONSE_PLAN_LIVENESS_ERROR_PREFIX: &str = "自主构建专业 Agent 已给出结论但结构化计划仍未完成"; const AGENT_RUNTIME_AUTONOMOUS_TRUNCATED_SCAFFOLD_ERROR_PREFIX: &str = @@ -17196,7 +17198,10 @@ mod autonomous_completion_contract_tests { "Next Level", "Restart", ] { - assert!(prompt.contains(label), "missing lane-defense label: {label}"); + assert!( + prompt.contains(label), + "missing lane-defense label: {label}" + ); } for selector in [ "start", @@ -17206,7 +17211,10 @@ mod autonomous_completion_contract_tests { "next-level", "restart", ] { - assert!(prompt.contains(selector), "missing playtest selector: {selector}"); + assert!( + prompt.contains(selector), + "missing playtest selector: {selector}" + ); } } @@ -17243,10 +17251,11 @@ mod autonomous_completion_contract_tests { #[test] fn autonomous_completion_requires_changed_index_static_smoke_and_bound_playtest() { - let (_temporary, root, state, contract) = autonomous_fixture( + let (_temporary, root, mut state, contract) = autonomous_fixture( "做一个塔防游戏,选择植物阻挡敌人并正常闯关", "autonomous-completion-evidence-run", ); + complete_agent_runtime_remaining_plan_steps(&mut state, "测试已完成实现与静态验证"); let blocker = autonomous_game_build_completion_blocker_at_locked(&root, &state) .expect("baseline project must remain blocked"); assert!(blocker.summary.contains("revision")); @@ -17265,6 +17274,14 @@ mod autonomous_completion_contract_tests { let blocker = autonomous_game_build_completion_blocker_at_locked(&root, &state) .expect("missing playtest receipt must block completion"); assert!(blocker.summary.contains("交互试玩回执")); + bind_supervisor_collaboration_policy_snapshot_at( + &root, + &state.agent_id, + &state.run_id, + &SupervisorCollaborationPolicy::default(), + "legacy-current-project-policy", + ) + .expect("isolate autonomous completion gate from collaboration policy"); let outcome = finish_game_creator_agent_background_runtime_turn_at( &root, state.clone(), @@ -17273,11 +17290,14 @@ mod autonomous_completion_contract_tests { &[], ) .expect("finalization should return a stale blocker"); - assert!(matches!( - outcome, - AgentBackgroundFinalizationOutcome::Stale(ref blocker) - if blocker.tool == "runtime.autonomous_completion" - )); + assert!( + matches!( + &outcome, + AgentBackgroundFinalizationOutcome::Stale(ref blocker) + if blocker.tool == "runtime.autonomous_completion" + ), + "unexpected finalization outcome: {outcome:?}" + ); assert!(!game_creator_agent_runtime_finalization_path( &root, &state.agent_id, @@ -17492,8 +17512,13 @@ fn validate_agent_runtime_verification_gate( { return Err("Agent Runtime verification gate 的 verifiedRevision 早于修改".to_string()); } - if gate.failed_playtest_revision.is_some_and(|revision| revision == 0) { - return Err("Agent Runtime verification gate 的 failedPlaytestRevision 必须大于 0".to_string()); + if gate + .failed_playtest_revision + .is_some_and(|revision| revision == 0) + { + return Err( + "Agent Runtime verification gate 的 failedPlaytestRevision 必须大于 0".to_string(), + ); } if gate .failed_playtest_revision @@ -24861,6 +24886,19 @@ async fn request_game_creator_agent_background_tool_plan_at( read_game_creator_agent_runtime_verification_gate(root, agent_id, run_id)?; let project_revision = read_game_creator_agent_runtime_project_revision(root)?.revision; + let supervisor_requires_delegated_repair = if agent_id + == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID + { + let collaboration_policy = + resolve_supervisor_collaboration_policy_for_run_at(root, agent_id, run_id)? + .policy; + let collaboration_state = + read_supervisor_collaboration_state_at(root, agent_id, run_id)?; + collaboration_policy.orchestrator_only_after_delegation + && collaboration_state.has_collaboration() + } else { + false + }; match validate_agent_runtime_autonomous_plan_liveness( agent_id, loop_index, @@ -24868,6 +24906,7 @@ async fn request_game_creator_agent_background_tool_plan_at( &verification_gate, observations, &parsed.plan, + supervisor_requires_delegated_repair, ) { Ok(()) => Ok((parsed, source_payload)), Err(error) => Err(AgentRuntimeToolPlanProtocolError::new( @@ -25099,12 +25138,17 @@ async fn request_game_creator_agent_background_tool_plan_at( AGENT_RUNTIME_AUTONOMOUS_FAILED_PLAYTEST_LIVENESS_ERROR_PREFIX, ) && !request.function_tools.is_empty(); - let force_autonomous_response_plan_completion = run_profile + let force_autonomous_delegated_playtest_repair = run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD && protocol_error.starts_with( - AGENT_RUNTIME_AUTONOMOUS_RESPONSE_PLAN_LIVENESS_ERROR_PREFIX, + AGENT_RUNTIME_AUTONOMOUS_DELEGATED_PLAYTEST_REPAIR_LIVENESS_ERROR_PREFIX, ) && !request.function_tools.is_empty(); + let force_autonomous_response_plan_completion = run_profile + == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD + && protocol_error + .starts_with(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 @@ -25116,6 +25160,7 @@ async fn request_game_creator_agent_background_tool_plan_at( ) && !request.function_tools.is_empty(); if force_supervisor_initial_collaboration || force_autonomous_response_plan_completion + || force_autonomous_delegated_playtest_repair || force_autonomous_failed_playtest || force_autonomous_pending_verification || force_autonomous_reverify_after_mutation @@ -25136,6 +25181,13 @@ async fn request_game_creator_agent_background_tool_plan_at( request.messages.push(LlmMessage::user(format!( "上一条输出不符合工具计划协议:{protocol_error}\n本次修复的原生工具目录只保留 update_agent_plan 与 respond_to_user。必须在同一响应先调用 update_agent_plan,保留原步骤标题并把已经真实完成的全部步骤标为 completed,再调用 respond_to_user 交付刚才已经形成的结论。不得只调用其中一个函数,不得新增步骤、继续读取、修改项目或解释。" ))); + } else if force_autonomous_delegated_playtest_repair { + restrict_agent_runtime_autonomous_delegated_playtest_repair_tools( + &mut request, + )?; + request.messages.push(LlmMessage::user(format!( + "上一条输出不符合工具计划协议:{protocol_error}\n当前父 run 已进入只编排模式,本次修复的原生工具目录只保留 agent.delegate。必须立即向 code-prototype 创建一个新的后续修复委派,把最近一次 preview.validate 的全部失败诊断写入 task 和 acceptanceCriteria,expectedArtifacts 必须包含 game/index.html;repairOfDelegationId 与 runId 都设为 null,由专业 Agent 产生新的 revision。该任务是对新发现试玩缺口的后续修复,不得对已返工 delivery 再返工。不得直接修改项目、更新计划、读取、搜索、重复验证、查询状态或 respond_to_user。不要解释,不要 markdown,不要代码围栏。" + ))); } else if force_autonomous_failed_playtest { restrict_agent_runtime_autonomous_failed_playtest_repair_tools(&mut request)?; request.messages.push(LlmMessage::user(format!( @@ -28313,12 +28365,8 @@ fn validate_agent_runtime_autonomous_response_plan_completion( if allow_runtime_plan_completion { return Ok(()); } - let mut runtime = read_game_creator_agent_runtime_for_session_at( - root, - agent_id, - Some(session_id), - )? - .state; + let mut runtime = + read_game_creator_agent_runtime_for_session_at(root, agent_id, Some(session_id))?.state; if let Some(update) = plan.plan_update.as_ref() { apply_agent_runtime_plan_update(&mut runtime, update).map_err(|error| { format!( @@ -28347,6 +28395,7 @@ fn validate_agent_runtime_autonomous_plan_liveness( verification_gate: &AgentRuntimeVerificationGate, observations: &[AgentRuntimeToolObservation], plan: &AgentRuntimeToolPlan, + supervisor_requires_delegated_repair: bool, ) -> Result<(), String> { let has_mutation = plan .actions @@ -28358,19 +28407,31 @@ fn validate_agent_runtime_autonomous_plan_liveness( "project.verify" | "command.run_limited" | "command.exec" ) }); + let has_specialist_delegation = plan + .actions + .iter() + .any(|action| action.tool.trim() == "agent.delegate"); if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID { 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 { + if has_verification || (!supervisor_requires_delegated_repair && has_mutation) { return Ok(()); } return Err(format!( "{AGENT_RUNTIME_AUTONOMOUS_REVERIFY_AFTER_MUTATION_LIVENESS_ERROR_PREFIX};试玩失败后项目已从 revision {failed_playtest_revision} 推进到 {project_revision},必须先验证当前 revision,才能重新执行 preview.validate;不得继续只更新计划、读取、搜索、查询状态、委派或返回最终回复" )); } + if supervisor_requires_delegated_repair { + if has_specialist_delegation { + return Ok(()); + } + return Err(format!( + "{AGENT_RUNTIME_AUTONOMOUS_DELEGATED_PLAYTEST_REPAIR_LIVENESS_ERROR_PREFIX};持久验证门仍标记 revision {failed_playtest_revision} 的试玩失败。当前父 run 已进入只编排模式,必须立即把最近一次 preview.validate 诊断委派给 code-prototype 创建新的后续修复 revision;不得直接修改项目、只更新计划、读取、搜索、重复验证、查询状态或返回最终回复" + )); + } if !has_mutation { return Err(format!( "{AGENT_RUNTIME_AUTONOMOUS_FAILED_PLAYTEST_LIVENESS_ERROR_PREFIX};持久验证门仍标记 revision {failed_playtest_revision} 的试玩失败,即使上下文压缩后也必须根据最近一次试玩诊断直接修改 game/index.html;不得继续只更新计划、读取、搜索、重复验证、查询状态、委派或返回最终回复" @@ -28389,11 +28450,18 @@ fn validate_agent_runtime_autonomous_plan_liveness( .iter() .any(is_agent_runtime_project_mutation_observation) || has_mutation + || (supervisor_requires_delegated_repair && has_specialist_delegation) || observations_after_failure.len() < AGENT_RUNTIME_AUTONOMOUS_LIVENESS_OBSERVATION_LIMIT { return Ok(()); } + if supervisor_requires_delegated_repair { + return Err(format!( + "{AGENT_RUNTIME_AUTONOMOUS_DELEGATED_PLAYTEST_REPAIR_LIVENESS_ERROR_PREFIX};失败后已有 {} 条非推进观察,本响应仍未委派专业修复。当前父 run 已进入只编排模式,必须立即把最近一次 preview.validate 诊断委派给 code-prototype 创建新的后续修复 revision;不得直接修改项目、只更新计划、读取、搜索、重复验证、查询状态或返回最终回复", + observations_after_failure.len() + )); + } return Err(format!( "{AGENT_RUNTIME_AUTONOMOUS_FAILED_PLAYTEST_LIVENESS_ERROR_PREFIX};失败后已有 {} 条非修改观察,本响应仍未提交项目修改。必须根据 preview.validate 的持久诊断直接修复 game/index.html;不得继续只更新计划、读取、搜索、重复验证、查询状态、委派或返回最终回复", observations_after_failure.len() @@ -28426,7 +28494,8 @@ fn validate_agent_runtime_autonomous_plan_liveness( return Ok(()); } let latest_progress_is_mutation = latest_mutation_index.is_some_and(|mutation_index| { - latest_verification_index.is_none_or(|verification_index| mutation_index > verification_index) + latest_verification_index + .is_none_or(|verification_index| mutation_index > verification_index) }); let error_prefix = if latest_progress_is_mutation { AGENT_RUNTIME_AUTONOMOUS_REVERIFY_AFTER_MUTATION_LIVENESS_ERROR_PREFIX @@ -28581,10 +28650,12 @@ fn restrict_agent_runtime_autonomous_response_plan_repair_tools( AGENT_RUNTIME_UPDATE_PLAN_FUNCTION_NAME, AGENT_RUNTIME_RESPOND_FUNCTION_NAME, ] { - if !request.function_tools.iter().any(|tool| tool.name == required) { - return Err(format!( - "autonomous 计划收束修复工具目录缺少 {required}" - )); + if !request + .function_tools + .iter() + .any(|tool| tool.name == required) + { + return Err(format!("autonomous 计划收束修复工具目录缺少 {required}")); } } request.max_output_tokens = Some( @@ -28727,6 +28798,26 @@ fn restrict_agent_runtime_autonomous_failed_playtest_repair_tools( Ok(()) } +fn restrict_agent_runtime_autonomous_delegated_playtest_repair_tools( + request: &mut LlmRunRequest, +) -> Result<(), String> { + let delegate_function = native_runtime_function_name("agent.delegate") + .ok_or_else(|| "无法生成自主试玩专业修复委派工具名".to_string())?; + request + .function_tools + .retain(|tool| tool.name == delegate_function); + if request.function_tools.len() != 1 { + return Err("自主试玩专业修复工具目录缺少 agent.delegate".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_reverification_repair_tools( request: &mut LlmRunRequest, ) -> Result<(), String> { @@ -28808,10 +28899,7 @@ fn apply_agent_runtime_autonomous_source_schema_limits_with_max( max_chars: usize, ) -> Result<(), String> { let field_pointers = [ - ( - "file.write", - vec!["/properties/input/properties/content"], - ), + ("file.write", vec!["/properties/input/properties/content"]), ( "file.patch", vec![ @@ -28848,10 +28936,7 @@ fn apply_agent_runtime_autonomous_source_schema_limits_with_max( "autonomous 源码 schema 字段不是 object:{runtime_tool}{pointer}" )); }; - schema.insert( - "maxLength".to_string(), - serde_json::json!(max_chars), - ); + schema.insert("maxLength".to_string(), serde_json::json!(max_chars)); } } Ok(()) @@ -28876,9 +28961,7 @@ fn restrict_agent_runtime_supervisor_collaboration_repair_tools( .iter() .any(|tool| tool.name == required) { - return Err(format!( - "Supervisor 首批协作修复工具目录缺少 {required}" - )); + return Err(format!("Supervisor 首批协作修复工具目录缺少 {required}")); } } Ok(()) diff --git a/apps/ai-game-creator-shell/src-tauri/src/project.rs b/apps/ai-game-creator-shell/src-tauri/src/project.rs index a702b9116..298d8fa30 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project.rs @@ -1834,10 +1834,8 @@ pub(crate) fn append_agent_db_tool_plan_audit_idempotent( return Err("Agent DB tool-plan 自主源码总长度无效".to_string()); }; if mutation_count > 1 - || max_field_chars - > AGENT_RUNTIME_AUTONOMOUS_SOURCE_FIELD_MAX_CHARS as u64 - || total_chars - > AGENT_RUNTIME_AUTONOMOUS_SOURCE_TOTAL_MAX_CHARS as u64 + || max_field_chars > AGENT_RUNTIME_AUTONOMOUS_SOURCE_FIELD_MAX_CHARS as u64 + || total_chars > AGENT_RUNTIME_AUTONOMOUS_SOURCE_TOTAL_MAX_CHARS as u64 || total_chars < max_field_chars { return Err("Agent DB tool-plan 自主源码载荷审计越界".to_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 8b3d28cd2..145dabad9 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests.rs @@ -6787,8 +6787,7 @@ async fn autonomous_game_build_repairs_truncated_scaffold_into_bounded_patch() { let repairs = records .iter() .filter(|record| { - record["recordType"] == "agent.runtime.tool_plan.repair" - && record["runId"] == run_id + record["recordType"] == "agent.runtime.tool_plan.repair" && record["runId"] == run_id }) .collect::>(); assert_eq!(repairs.len(), 1); @@ -7052,8 +7051,7 @@ async fn autonomous_game_build_repairs_final_response_with_incomplete_plan() { Some(&child_link), ) .expect("bind autonomous child profile"); - let read_arguments = - serde_json::json!({"reason": "继续重复读取项目", "input": {}}).to_string(); + let read_arguments = serde_json::json!({"reason": "继续重复读取项目", "input": {}}).to_string(); let incomplete_response = serde_json::json!({ "response": "只读审查已经完成,当前入口缺少可玩状态合同。" }) @@ -7154,7 +7152,10 @@ async fn autonomous_game_build_repairs_final_response_with_incomplete_plan() { .await .expect("repair autonomous response plan") .expect("repaired response plan"); - assert_eq!(plan.response, "只读审查已经完成,当前入口缺少可玩状态合同。"); + assert_eq!( + plan.response, + "只读审查已经完成,当前入口缺少可玩状态合同。" + ); assert!(plan.actions.is_empty()); assert!(plan.plan_update.as_ref().is_some_and(|update| update .steps @@ -7251,8 +7252,7 @@ async fn autonomous_game_build_repairs_explicit_read_only_loop_into_completed_de Some(&child_link), ) .expect("bind autonomous child profile"); - let read_arguments = - serde_json::json!({"reason": "核对当前入口", "input": {}}).to_string(); + let read_arguments = serde_json::json!({"reason": "核对当前入口", "input": {}}).to_string(); let response = serde_json::json!({ "response": "当前入口缺少可玩状态合同,需要程序 Agent 完成实现。" }) @@ -7313,8 +7313,7 @@ async fn autonomous_game_build_repairs_explicit_read_only_loop_into_completed_de }, ) .expect("persist read-only plan"); - write_game_creator_agent_runtime_state(&root, &runtime) - .expect("write read-only runtime"); + write_game_creator_agent_runtime_state(&root, &runtime).expect("write read-only runtime"); let plan = request_game_creator_agent_background_tool_plan_for_test( &root, @@ -7604,8 +7603,7 @@ async fn autonomous_game_build_repairs_post_mutation_read_loop_into_verification .expect("bind autonomous child profile"); let (sender, receiver) = mpsc::channel(); - let read_arguments = serde_json::json!({"reason": "继续重复读取项目", "input": {}}) - .to_string(); + let read_arguments = serde_json::json!({"reason": "继续重复读取项目", "input": {}}).to_string(); let verify_arguments = serde_json::json!({ "reason": "停止空转并立即重跑正式验证", "input": { @@ -7703,14 +7701,16 @@ async fn autonomous_game_build_repairs_post_mutation_read_loop_into_verification detail: None, }, ]; - observations.extend((0..AGENT_RUNTIME_AUTONOMOUS_LIVENESS_OBSERVATION_LIMIT).map(|_| { - AgentRuntimeToolObservation { - tool: "runtime.plan_update".to_string(), - status: "blocked".to_string(), - summary: "结构化计划尚未完成".to_string(), - detail: None, - } - })); + observations.extend( + (0..AGENT_RUNTIME_AUTONOMOUS_LIVENESS_OBSERVATION_LIMIT).map(|_| { + AgentRuntimeToolObservation { + tool: "runtime.plan_update".to_string(), + status: "blocked".to_string(), + summary: "结构化计划尚未完成".to_string(), + detail: None, + } + }), + ); let plan = request_game_creator_agent_background_tool_plan_for_test( &root, @@ -7851,14 +7851,16 @@ async fn autonomous_game_build_repairs_supervisor_failed_playtest_stall_into_mut summary: "浏览器验证未通过,请根据诊断修复后重试".to_string(), detail: Some(r#"{"passed":false,"diagnostics":["缺少可玩状态"]}"#.to_string()), }]; - observations.extend((0..AGENT_RUNTIME_AUTONOMOUS_LIVENESS_OBSERVATION_LIMIT).map(|_| { - AgentRuntimeToolObservation { - tool: "runtime.plan_update".to_string(), - status: "blocked".to_string(), - summary: "结构化计划尚未完成".to_string(), - detail: None, - } - })); + observations.extend( + (0..AGENT_RUNTIME_AUTONOMOUS_LIVENESS_OBSERVATION_LIMIT).map(|_| { + AgentRuntimeToolObservation { + tool: "runtime.plan_update".to_string(), + status: "blocked".to_string(), + summary: "结构化计划尚未完成".to_string(), + detail: None, + } + }), + ); let plan = request_game_creator_agent_background_tool_plan_for_test( &root, @@ -8115,6 +8117,192 @@ async fn autonomous_game_build_repairs_persisted_failed_playtest_after_context_c fs::remove_dir_all(root).ok(); } +#[tokio::test] +async fn autonomous_supervisor_delegates_failed_playtest_repair_after_collaboration() { + let root = unique_project_path(); + init_local_game_project_at( + &root, + "project-autonomous-supervisor-delegated-playtest-repair", + "自主构建总控委派试玩修复测试", + ) + .expect("project init"); + let run_id = "autonomous-supervisor-delegated-playtest-repair-run"; + bind_game_creator_agent_runtime_run_profile_at( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + run_id, + AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE, + Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD), + None, + ) + .expect("bind autonomous supervisor profile"); + let runtime = start_game_creator_agent_runtime_task_at( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + "委派专业 Agent 修复试玩失败", + run_id, + AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE, + "根据试玩诊断继续专业协作", + vec!["委派试玩修复".to_string(), "重新验证并交付".to_string()], + ) + .expect("start autonomous supervisor runtime"); + bind_supervisor_collaboration_policy_snapshot_at( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + run_id, + &SupervisorCollaborationPolicy::default(), + "legacy-current-project-policy", + ) + .expect("bind collaboration policy snapshot"); + let initial_delivery = new_static_delegate_delivery( + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + &runtime.session_id, + run_id, + "autonomous-supervisor-initial-delegate-action", + "autonomous-supervisor-initial-delivery", + "quality-review", + "autonomous-supervisor-initial-child-session", + "autonomous-supervisor-initial-child-run", + ); + create_or_read_static_delegate_delivery_at(&root, &initial_delivery) + .expect("create existing collaboration fact"); + + let failed_revision = prepare_agent_runtime_project_mutation_locked( + &root, + "code-prototype", + "autonomous-supervisor-specialist-mutation-run", + "file.patch", + ) + .expect("record specialist mutation"); + let (expected_revision, gate) = begin_agent_runtime_project_verification_locked( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + run_id, + "game.static_smoke", + ) + .expect("begin supervisor static smoke"); + finish_agent_runtime_project_verification_locked(&root, &expected_revision, gate, true) + .expect("finish supervisor static smoke"); + invalidate_agent_runtime_project_verification_after_preview_failure_at( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + run_id, + failed_revision, + ) + .expect("persist supervisor failed playtest"); + + let patch_arguments = serde_json::json!({ + "reason": "总控误尝试直接修复试玩失败", + "input": { + "path": "game/index.html", + "oldText": "canvas{display:none}", + "newText": "canvas{display:block}" + } + }) + .to_string(); + let delegate_arguments = serde_json::json!({ + "reason": "把最新试玩诊断交给程序专业 Agent", + "input": { + "agentId": "code-prototype", + "task": "修复最近一次 preview.validate 报告的桌面和移动端 canvas 不可见问题,并保持 lane-defense-v1 全部交互断言通过。", + "acceptanceCriteria": [ + "桌面和移动端均存在可见且非空的 canvas", + "lane-defense-v1 全部固定试玩断言继续通过", + "修复后通过 game.static_smoke" + ], + "expectedArtifacts": ["game/index.html"], + "repairOfDelegationId": null, + "runId": null + } + }) + .to_string(); + let (sender, receiver) = mpsc::channel(); + let base_url = spawn_mock_llm_raw_responses_with_capture( + vec![ + native_agent_tool_plan_chat_response( + "call-autonomous-supervisor-blocked-direct-playtest-patch", + &native_runtime_function_name("file.patch").expect("patch function"), + patch_arguments, + ), + native_agent_tool_plan_chat_response( + "call-autonomous-supervisor-delegated-playtest-repair", + &native_runtime_function_name("agent.delegate").expect("delegate function"), + delegate_arguments, + ), + ], + Some(sender), + ); + let _config_guard = write_test_local_config(format!( + r#"{{ + "agentLlm": {{ + "project-supervisor": {{ + "apiKey": "autonomous-supervisor-delegated-playtest-repair-key", + "baseUrl": {base_url:?}, + "model": "autonomous-supervisor-delegated-playtest-repair-model", + "apiKind": "openai_chat", + "maxRetries": 0 + }} + }} +}}"# + )); + + let plan = request_game_creator_agent_background_tool_plan_for_test( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + &runtime.session_id, + run_id, + &runtime.current_task, + &[], + 30, + 0, + ) + .await + .expect("repair collaborated supervisor playtest stall") + .expect("delegated supervisor playtest repair plan"); + assert_eq!(plan.actions.len(), 1); + assert_eq!(plan.actions[0].tool, "agent.delegate"); + assert_eq!( + plan.actions[0] + .input + .get("agentId") + .and_then(serde_json::Value::as_str), + Some("code-prototype") + ); + + receiver + .recv_timeout(Duration::from_secs(2)) + .expect("initial direct mutation request"); + let repair_request = receiver + .recv_timeout(Duration::from_secs(2)) + .expect("delegated playtest repair request"); + assert!(repair_request.contains("最近一次交互试玩需要专业 Agent 修复")); + assert!(repair_request.contains("只编排模式")); + let repair_request_json = mock_http_request_json(&repair_request); + let repair_function_names = repair_request_json["tools"] + .as_array() + .expect("restricted delegated playtest repair tools") + .iter() + .filter_map(|tool| { + tool.get("name") + .and_then(serde_json::Value::as_str) + .or_else(|| { + tool.get("function") + .and_then(|function| function.get("name")) + .and_then(serde_json::Value::as_str) + }) + }) + .collect::>(); + assert_eq!( + repair_function_names, + BTreeSet::from([native_runtime_function_name("agent.delegate") + .expect("delegate function") + .as_str(),]) + ); + assert!(receiver.recv_timeout(Duration::from_millis(200)).is_err()); + + fs::remove_dir_all(root).ok(); +} + #[tokio::test] async fn autonomous_game_build_repairs_new_revision_after_failed_playtest_with_verification() { let root = unique_project_path(); @@ -8147,9 +8335,13 @@ async fn autonomous_game_build_repairs_new_revision_after_failed_playtest_with_v ) .expect("start autonomous supervisor runtime"); - let failed_revision = - prepare_agent_runtime_project_mutation_locked(&root, "code-prototype", "child-run-1", "file.write") - .expect("record specialist mutation"); + let failed_revision = prepare_agent_runtime_project_mutation_locked( + &root, + "code-prototype", + "child-run-1", + "file.write", + ) + .expect("record specialist mutation"); let (expected_revision, gate) = begin_agent_runtime_project_verification_locked( &root, GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, @@ -8166,13 +8358,16 @@ async fn autonomous_game_build_repairs_new_revision_after_failed_playtest_with_v failed_revision, ) .expect("persist supervisor failed playtest"); - let repaired_revision = - prepare_agent_runtime_project_mutation_locked(&root, "code-prototype", "child-run-2", "file.patch") - .expect("record specialist repair mutation"); + let repaired_revision = prepare_agent_runtime_project_mutation_locked( + &root, + "code-prototype", + "child-run-2", + "file.patch", + ) + .expect("record specialist repair mutation"); assert_eq!(repaired_revision, failed_revision + 1); - let read_arguments = - serde_json::json!({"reason": "继续读取而不验证", "input": {}}).to_string(); + let read_arguments = serde_json::json!({"reason": "继续读取而不验证", "input": {}}).to_string(); let verify_arguments = serde_json::json!({ "reason": "专业 Agent 已产生新 revision,先重新执行静态验证", "input": {"commandId": "game.static_smoke"} @@ -10728,8 +10923,7 @@ async fn supervisor_collaboration_read_only_first_window_repairs_with_collaborat ] }) .to_string(); - let read_arguments = - serde_json::json!({"reason": "继续读取项目索引", "input": {}}).to_string(); + let read_arguments = serde_json::json!({"reason": "继续读取项目索引", "input": {}}).to_string(); let delegate_function = native_runtime_function_name("agent.delegate").expect("delegate function"); let isolated_function = @@ -10819,7 +11013,10 @@ async fn supervisor_collaboration_read_only_first_window_repairs_with_collaborat run_id, AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE, "读取后建立首批协作", - vec!["读取必要上下文".to_string(), "一次性委派两个专业 Agent".to_string()], + vec![ + "读取必要上下文".to_string(), + "一次性委派两个专业 Agent".to_string(), + ], ) .expect("start supervisor runtime"); @@ -10837,7 +11034,10 @@ async fn supervisor_collaboration_read_only_first_window_repairs_with_collaborat .expect("repair read-only initial collaboration plan") .expect("repaired collaboration plan"); assert_eq!(plan.actions.len(), 2); - assert!(plan.actions.iter().all(|action| action.tool == "agent.delegate")); + assert!(plan + .actions + .iter() + .all(|action| action.tool == "agent.delegate")); assert_eq!(plan.actions[0].input["agentId"], "code-prototype"); assert_eq!(plan.actions[1].input["agentId"], "quality-review"); @@ -37745,7 +37945,10 @@ async fn provider_transient_retry_autonomous_upstream_400_retries_with_bounded_b .recv_timeout(Duration::from_millis(100)) .is_err()); assert_eq!(plan.actions.len(), 2); - assert!(plan.actions.iter().all(|action| action.tool == "agent.delegate")); + assert!(plan + .actions + .iter() + .all(|action| action.tool == "agent.delegate")); assert!(provider_retry::read_for_run_at( &root, GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index d7d4b121a..715baf5e8 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -3467,3 +3467,11 @@ - 处理:平台白底工具弹窗优先复用 `PlatformToolModalShell`,由共享壳读取当前 `AuthUiContext.platformTheme`,并把 `platform-theme platform-theme--` 挂到 portal overlay;不要用硬编码白底掩盖主题变量缺失。必须直接使用 `UnifiedModal` 的特殊场景,也要在 `overlayClassName` 显式传递当前平台主题。 - 验证:在 light / dark 主题下打开 portal 弹窗,断言 dialog 的 overlay 携带对应主题类,并在真实浏览器核对 panel 与遮罩的 computed background 均非透明。 - 关联:`src/components/project/ProjectGalleryView.tsx`、`src/components/common/PlatformToolModalShell.tsx`、`src/components/common/UnifiedModal.tsx`。 + +## 自主试玩失败后的修复责任不能同时落给总控和专业 Agent + +- 现象:专业 Agent 已交付新 revision,Project Supervisor 的固定试玩已通过全部业务交互断言,但双视口可见性等外围门禁仍失败;下一轮 Provider 被要求直接 `file.patch`,随后又被 `orchestratorOnlyAfterDelegation` 正确拦截,格式修复耗尽后父 run 失败且没有最终回复。 +- 原因:试玩失败活性门只检查“必须出现项目 mutation”,没有区分父 run 是否已经存在 durable 协作事实;它与“进入协作后 Supervisor 只委派、读取、认领和验证”的策略形成互斥合同。 +- 处理:同一失败 revision 上,尚无协作事实的兼容 run 可以保留总控直接修复;已有协作事实且总控只编排时,只允许创建新的 `code-prototype` 后续修复委派,明确继承最新 `preview.validate` 诊断和 `game/index.html` 产物要求,不把它伪装成已有 repair delivery 的二次返工。专业 Agent 推进 revision 后,总控先验证新 revision,再重新试玩。 +- 验证:回归测试同时覆盖“无协作时仍可直接修复”“有协作时工具目录只剩 `agent.delegate`”“专业 Agent 推进新 revision 后总控只能先复验”,并用 `supervisor-autonomous-playable-lane-defense` 真实 E2E 检查静态烟雾、桌面/移动浏览器、全部固定试玩断言、唯一 Supervisor 回复和零残留。 +- 关联:`apps/ai-game-creator-shell/src-tauri/src/agent.rs`、`apps/ai-game-creator-shell/src-tauri/src/tests.rs`、`apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e.mjs`。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 56dd1d721..d9e67f323 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -63,6 +63,8 @@ V1.11 的受保护仓库控制目录同时包含 `.git / .agent / .agents / .cod 2026-07-17 起,Runtime V1.30 已把真实 `agc:chat / --swarm-chat` 与自主专业编排纳入同一门禁。用户只描述业务交付,不提供 Agent ID、数量、并行、工具、repair 或 Runner 配方;`project-supervisor` 必须在单个 native planning 批次自主选择至少两个不同专业 Agent 并形成真实 Provider 重叠,语义判定不满足时在同一父 Session/run 发起唯一 repair,最终以单条 `[turn.report] game-creator-swarm-turn-report.v1` 和唯一 Supervisor assistant 收束。正式 `openai_chat / gpt-5.5` 最终诊断轮已完成 `51/51` Provider lifecycle、双专业并行、2+1 delivery、2 个 Observed claim、pidfd Runner 强杀/boot 恢复、唯一正式回复和零重复/残留/泄漏,当前门禁状态为 PASS;static delivery + isolated all-join 的真实组合和 Tauri/WebView 宿主级 E2E 仍是独立后续项。 +2026-07-21 补充:自主构建的最终 `preview.validate` 在当前 revision 失败后,修复责任必须继续服从 Project Supervisor 的只编排边界。父 run 尚无协作事实时可沿用总控直接修复兼容路径;一旦 durable 协作事实已建立且 `orchestratorOnlyAfterDelegation=true`,活性门不得再强迫 Supervisor 调用 `file.write / file.patch / project.patchset`,而应把原生工具目录收窄到 `agent.delegate`,向 `code-prototype` 创建 `repairOfDelegationId=null / runId=null` 的新后续修复任务,并把最新浏览器诊断和 `game/index.html` 验收产物写入合同。该后续任务用于处理最终试玩新发现的缺口,不是对已有 repair delivery 的二次返工;专业 Agent 推进到更高 revision 后,Supervisor 仍必须先取得该 revision 的静态通过凭证,再重跑固定试玩。失败 revision、专业修改、总控复验与最终试玩之间不得用伪造 mutation 衔接。 + 2026-07-15 起,Runtime V1.1 文档的“V1.17 单 Agent 持久计划”作为后台工具规划进度的新事实源。`submit_agent_tool_plan` 新增 nullable `planUpdate={explanation,steps[{step,status}]}`;步骤只接受 `pending / in_progress / completed`,最多 8 步且至多一个 `in_progress`。结构化计划一旦建立,legacy `plan` 只作旧协议 fallback;终态步骤必须保留,`planRevision` 只在真实变化时单调递增,工具 action 下标不得自动完成结构化步骤,存在未完成步骤时不得写最终回复或 completed。 V1.17 计划快照随 `game-creator-runtime-context-bundle.v3` 持久化,v2 在通过原身份、revision 和 verification gate 校验后从当前 Runtime state 补齐计划字段继续恢复;计划元数据本身不推进项目 revision、不改变 verification gate,也不触发项目权限确认。开发 UI 和 CLI 有界展示 revision、说明与完整 8 步;正式用户的 Supervisor 只展示完成数、当前步骤、等待对象、下一步和协作数量的紧凑摘要。恢复、same-run steer 和真实 Provider 的完整验收矩阵以 Runtime V1.17 章节为准;2026-07-16 已在当前 v5 context 上完成正式 `openai_chat / gpt-5.5` 的同 run steer + Runner 强杀恢复专项,门禁状态为 PASS。