From effdaefc63ef5d34b7d03d113fc7678b35985e5d Mon Sep 17 00:00:00 2001 From: kdletters Date: Wed, 12 Aug 2026 17:00:50 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=B8=B8=E6=88=8F=E6=99=BA?= =?UTF-8?q?=E8=83=BD=E4=BD=93=E8=AE=A1=E5=88=92=E6=8B=92=E7=BB=9D=E5=90=8E?= =?UTF-8?q?=E7=9A=84=E9=87=8D=E5=A4=8D=E8=A7=84=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在 runtime.plan_update 被拒绝后按角色收窄下一轮工具目录 恢复 Supervisor 首次项目修改的活性门禁 补充请求目录与活性门禁回归测试 同步更新游戏创作智能体实施规则 --- .../runtime_actions/autonomous_policy.rs | 50 ++++++++ .../provider_request_builders.rs | 107 +++++++++++++++++- ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 2 + 3 files changed, 158 insertions(+), 1 deletion(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/autonomous_policy.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/autonomous_policy.rs index cc6d337e5..7727f2188 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/autonomous_policy.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/autonomous_policy.rs @@ -702,6 +702,21 @@ pub(crate) fn validate_agent_runtime_autonomous_plan_liveness( .actions .iter() .any(|action| action.tool.trim() == "agent.route_manifest"); + // A Supervisor without a mutation must still hit the same first-mutation + // liveness gate as every other autonomous Agent. Previously the presence + // of the Supervisor role alone could bypass this check. + if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID + && loop_index > AGENT_RUNTIME_AUTONOMOUS_PRE_MUTATION_LOOP_LIMIT + && verification_gate.mutation_revision.is_none() + && !has_mutation + && !has_code_asset_route + && plan.response.trim().is_empty() + && !has_specialist_delegation + { + return Err(format!( + "{AGENT_RUNTIME_AUTONOMOUS_LIVENESS_ERROR_PREFIX};Supervisor 尚未提交首次项目 mutation 或有效协作动作,禁止继续只规划、读取、验证或空转" + )); + } 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 { @@ -1911,4 +1926,39 @@ mod tests { .is_err()); } } + + #[test] + fn autonomous_supervisor_without_playtest_still_hits_pre_mutation_liveness_gate() { + let verification_gate = AgentRuntimeVerificationGate { + schema_version: "test".to_string(), + project_id: "test".to_string(), + agent_id: GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string(), + run_id: "supervisor-pre-mutation".to_string(), + requires_verification: false, + mutation_revision: None, + verified_revision: None, + last_mutation_tool: None, + last_verification_tool: None, + last_verification_status: None, + static_smoke_verified_revision: None, + failed_playtest_revision: None, + updated_at: 0, + }; + let plan = AgentRuntimeToolPlan { + thinking_summary: "继续只读规划".to_string(), + ..AgentRuntimeToolPlan::default() + }; + let error = validate_agent_runtime_autonomous_plan_liveness( + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + AGENT_RUNTIME_AUTONOMOUS_PRE_MUTATION_LOOP_LIMIT + 1, + 0, + &verification_gate, + &[], + &plan, + false, + false, + ) + .expect_err("Supervisor without a playtest must not bypass pre-mutation liveness"); + assert!(error.starts_with(AGENT_RUNTIME_AUTONOMOUS_LIVENESS_ERROR_PREFIX)); + } } 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 3f2a0951d..f3a72dfd6 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 @@ -316,6 +316,43 @@ pub(in crate::agent) fn build_game_creator_agent_background_tool_plan_request( tool.name != goal_contract_function && tool.name != acceptance_update_function }); } + // A rejected structured-plan update is a request-scoped liveness signal. + // The next Provider turn must perform the concrete mutation (or deliver a + // read-only result) instead of entering another planning loop. + let latest_plan_rejection = observations.iter().rposition(|observation| { + observation.tool == "runtime.plan_update" && observation.status == "rejected" + }); + let plan_rejection_needs_repair = latest_plan_rejection.is_some_and(|index| { + !observations[index + 1..] + .iter() + .any(is_agent_runtime_project_mutation_observation) + }); + if autonomous_game_build && plan_rejection_needs_repair { + let supervisor_orchestrator_repair = if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID { + let policy = resolve_supervisor_collaboration_policy_for_run_at(root, agent_id, run_id)?.policy; + let state = read_supervisor_collaboration_state_at(root, agent_id, run_id)?; + policy.orchestrator_only_after_delegation && state.has_collaboration() + } else { + false + }; + let repair_tools: &[&str] = if supervisor_orchestrator_repair { + &["agent.delegate", "agent.run_status"] + } else { + &["file.write", "file.patch", "file.delete", "project.patchset", "project.restore", "canvas.asset_generate"] + }; + let mut allowed_function_names = BTreeSet::from([AGENT_RUNTIME_RESPOND_FUNCTION_NAME.to_string()]); + for tool in repair_tools { + if let Some(name) = native_runtime_function_name(tool) { + allowed_function_names.insert(name); + } + } + request.function_tools.retain(|tool| allowed_function_names.contains(&tool.name)); + request.messages.push(LlmMessage::user(if supervisor_orchestrator_repair { + "上一轮 runtime.plan_update 被拒绝。本轮 Supervisor 已进入协作编排模式,只能调用 agent.run_status 或 agent.delegate 继续收束,或在证据足够时 respond_to_user;禁止再次规划、读取、搜索、验证或直接修改项目。" + } else { + "上一轮 runtime.plan_update 被拒绝。本轮必须立即提交当前 in_progress 步骤对应的实际项目 mutation,或在只读合同已满足时 respond_to_user;禁止再次规划、读取、搜索、验证、委派或普通文本解释。" + })); + } if autonomous_game_build && !editor_api_key_is_configured() { let canvas_function = native_runtime_function_name("canvas.asset_generate") .ok_or_else(|| "无法生成画布素材工具函数名".to_string())?; @@ -530,11 +567,14 @@ mod tests { game_creator_project_supervisor_chat_system_prompt, init_local_game_project_at, provider_command_exec_contract, provider_command_start_contract, required_runtime_prompt_section, resolve_agent_conversation_session_id_at, - start_game_creator_agent_runtime_task_at, AgentRuntimeTaskLink, AgentRuntimeToolPlan, + start_game_creator_agent_runtime_task_at, AgentRuntimeTaskLink, AgentRuntimeToolObservation, + AgentRuntimeToolPlan, GameCreatorMcpCatalog, GameCreatorMcpCatalogTool, AGENT_RUNTIME_COMPLETION_BLOCKER_TOOL_PLAN_PROTOCOL, + AGENT_RUNTIME_RESPOND_FUNCTION_NAME, AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD, AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE, AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE, AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE, + AGENT_RUNTIME_UPDATE_PLAN_FUNCTION_NAME, GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, RUNTIME_PROMPT_SUPERVISOR_CHAT_COMPOSITION, }; @@ -556,6 +596,71 @@ mod tests { .collect() } + #[test] + fn rejected_plan_update_forces_request_scoped_mutation_catalog() { + let directory = crate::tests::canonical_test_tempdir("provider-plan-rejection-repair-"); + let root = directory.path().join("project"); + init_local_game_project_at(&root, "plan-rejection-repair", "修复现有游戏") + .expect("project init"); + let binding = bind_game_creator_agent_runtime_run_profile_at( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + "plan-rejection-repair-root", + AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE, + Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD), + None, + ) + .expect("bind root"); + let state = start_game_creator_agent_runtime_task_at( + &root, + &binding.agent_id, + "修复现有游戏", + &binding.run_id, + &binding.source, + "执行当前计划中的项目修改", + vec!["立即修改 game/index.html".to_string()], + ) + .expect("start task"); + let catalog = GameCreatorMcpCatalog { + fingerprint: String::new(), + servers: Vec::new(), + tools: Vec::new(), + }; + let rejected = AgentRuntimeToolObservation { + tool: "runtime.plan_update".to_string(), + status: "rejected".to_string(), + summary: "结构化计划更新被 Runtime 拒绝".to_string(), + detail: Some("计划状态回退".to_string()), + }; + let (_, _, request, _) = build_game_creator_agent_background_tool_plan_request( + &root, + &state.agent_id, + &state.session_id, + &state.run_id, + &state.current_task, + &[rejected], + 1, + &catalog, + ) + .expect("build request"); + let names = request + .function_tools + .iter() + .map(|tool| tool.name.as_str()) + .collect::>(); + assert!(names.contains( + crate::agent_native_tools::native_runtime_function_name("file.patch") + .expect("file.patch function") + .as_str() + )); + assert!(names.contains(AGENT_RUNTIME_RESPOND_FUNCTION_NAME)); + assert!(!names.contains(AGENT_RUNTIME_UPDATE_PLAN_FUNCTION_NAME)); + assert!(request + .messages + .iter() + .any(|message| message.content.contains("runtime.plan_update 被拒绝"))); + } + fn build_request_system_prompt_for_root_source( agent_id: &str, root_source: &str, diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index e5f75e4b4..1c3113d76 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -1051,6 +1051,8 @@ game-project/ ## 2026-08-11 通用 Goal Contract 与动态 Acceptance Graph +- 2026-08-12 计划拒绝恢复:结构化 `runtime.plan_update` 被 Runtime 拒绝后,下一轮 Provider 请求按请求级目录收窄到实际项目 mutation 与 `respond_to_user`(已进入协作编排的 Supervisor 保留 `agent.delegate / agent.run_status`),并明确禁止再次规划、读取、搜索或验证;后续已有真实 mutation observation 后解除临时目录,不改变持久 executable policy。 + - Goal Contract 绑定 project、可信根 Run Profile、source task SHA-256 和不可变 fingerprint;同一根 Run 只允许幂等重放完全相同的合同,语义变化必须进入新根 Run。已有合同的根 Supervisor 收到 steer 时,Runtime 必须按旧 rootRunId 串行化转换并在持锁后重验 Session 当前权威 Run,再取消并确认旧 rootRunId 的静态、ready、isolated 整棵树已进入终态或 `needs-reconciliation`;旧树未停稳时拒绝启动 replacement,停稳后才在同一 Session、source 和 Run Profile 创建唯一的新根 Run,不能把新增要求塞进旧合同继续完成。合同摘要作为 `decision` 投影到共享黑板,JSON sidecar 才是权威源;黑板冲突条目和专家事实仍追加保留。 - 所有静态 delegate、ready child 和 isolated child 都只读继承根合同与当前 Acceptance Graph;isolated child 还必须实际收到自己的 `acceptanceCriteria / expectedArtifacts / writeScopes`。继承上下文不扩大工具、目录、写入权限或 expectedArtifacts。专业 Agent 只能报告局部结果、证据和剩余风险,不能修改根合同、根验收图或宣布用户总目标完成。 - Acceptance Graph 节点由 Supervisor 针对当前任务动态生成,不来自玩法模板。节点记录 required/optional、依赖、状态、证据引用与摘要;只有同一可信根 Supervisor 能调用 `agent.acceptance_update`,且该动作必须独占一轮。failed 与 not-observed 节点形成下一轮定向返工集合,未提交的 passed 节点保持不变。