diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/prompt.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/prompt.rs index 21cbeb0f0..84546ac3e 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/prompt.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/prompt.rs @@ -530,7 +530,12 @@ pub(crate) fn game_creator_agent_runtime_tool_plan_system_prompt_for_agent( if agent_id == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID { return game_creator_project_planning_tool_plan_system_prompt(); } - let prompt = game_creator_agent_runtime_tool_plan_system_prompt(); + // 判据里带上 agent_id:`source` 只有 Supervisor 自己的 run binding 会填成 + // plan(见 provider_request_builders.rs),其余角色恒为空串,这里再要求一次 + // 是为了让「谁能收窄」这件事在本函数内自证,不依赖调用方的取值纪律。 + let plan_root = agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID + && agent_runtime_supervisor_source_is_plan(source); + let prompt = game_creator_agent_runtime_tool_plan_system_prompt_for_plan_root(plan_root); if agent_id == "design-foundation" { return game_creator_design_foundation_tool_plan_prompt( &prompt, @@ -673,14 +678,31 @@ pub(crate) fn game_creator_agent_runtime_role_overlay_prompt( } pub(crate) fn game_creator_agent_runtime_tool_plan_system_prompt() -> String { + game_creator_agent_runtime_tool_plan_system_prompt_for_plan_root(false) +} + +/// plan 根 run 的 `agent.spawn_isolated` 已被 M1A-4 无条件拒绝,因此共享 +/// runtime 合同里那份「合法 templateAgentId」静态模板目录对它没有任何可执行 +/// 语义,只剩下把 design-director 等全部专业角色名摆在 Provider 眼前的副作用。 +/// 该目录第一个名字就是 `design-director`,实测中 Supervisor 会照着它发起 +/// `agent.delegate`,被执行层拒绝后再也没能自行改回 `project-planning`。 +/// 因此这里在上下文层直接删掉目录本身——这正是第 19 节第 2 条要求的第二层, +/// 不替代执行层硬拒。 +fn game_creator_agent_runtime_tool_plan_system_prompt_for_plan_root(plan_root: bool) -> String { #[cfg(target_os = "linux")] const CURRENT_PLATFORM_IS_LINUX: bool = true; #[cfg(not(target_os = "linux"))] const CURRENT_PLATFORM_IS_LINUX: bool = false; - game_creator_agent_runtime_tool_plan_system_prompt_for_platform(CURRENT_PLATFORM_IS_LINUX) + game_creator_agent_runtime_tool_plan_system_prompt_for_platform( + CURRENT_PLATFORM_IS_LINUX, + plan_root, + ) } -fn game_creator_agent_runtime_tool_plan_system_prompt_for_platform(linux: bool) -> String { +fn game_creator_agent_runtime_tool_plan_system_prompt_for_platform( + linux: bool, + plan_root: bool, +) -> String { let tool_catalog = agent_runtime_native_executable_tools().join("、"); let prompt_header = format!( "你正在使用 Genarrative AI 游戏创作多智能体 Runtime。你必须直接调用当前请求广告的原生函数:复杂任务首次拆解、实际进度变化、steer 调整顺序或最终收束时调用 update_agent_plan,并提交 explanation 与完整 steps;无需更新时不要调用 update_agent_plan。steps 只允许 pending、in_progress、completed 且同时最多一个 in_progress;已完成步骤必须保留且不得回退,所有必要步骤 completed 前不得调用 respond_to_user,Runtime 不会按工具动作下标代替你更新进度。只能请求以下 Runtime 当前注册的原生可执行工具:{tool_catalog}。MCP 工具仅以当前请求提供的动态目录为准。" @@ -694,7 +716,15 @@ fn game_creator_agent_runtime_tool_plan_system_prompt_for_platform(linux: bool) let platform_section = runtime_prompt_platform_section_id(linux); render_runtime_prompt_composition(RUNTIME_PROMPT_RUNTIME_COMPOSITION, |marker| match marker { "$header" => Some(&prompt_header), - "$isolatedAgentTemplates" => Some(&isolated_agent_templates), + // 只删 spawn_isolated 专属的两段。`isolatedAgentContract` 讲的是 + // expectedArtifacts 与 writeScopes,`agent.delegate` 同样要用,plan 根 + // 委派 project-planning 时就带 expectedArtifacts,必须逐字保留。 + "isolatedTemplateCatalogIntro" if plan_root => Some(""), + "$isolatedAgentTemplates" => Some(if plan_root { + "" + } else { + &isolated_agent_templates + }), "$platform" => Some(required_runtime_prompt_section(platform_section)), _ => None, }) @@ -947,8 +977,10 @@ mod tests { #[test] fn runtime_prompt_selects_exactly_one_manifest_platform_variant() { - let default_prompt = game_creator_agent_runtime_tool_plan_system_prompt_for_platform(false); - let linux_prompt = game_creator_agent_runtime_tool_plan_system_prompt_for_platform(true); + let default_prompt = + game_creator_agent_runtime_tool_plan_system_prompt_for_platform(false, false); + let linux_prompt = + game_creator_agent_runtime_tool_plan_system_prompt_for_platform(true, false); let default_section = required_runtime_prompt_section(RUNTIME_PROMPT_PLATFORM_DEFAULT_SECTION).trim(); let linux_section = @@ -1268,12 +1300,16 @@ mod tests { /// system prompt 不拼 supervisorIntro 与 $visualContract 这两个 section /// ——两段都在暗示存在可并行委派的专业组,而策划链路的合同是只能委派 /// project-planning 一个子 Agent。这里用两段各自最短的、只出现在该 section - /// 里的独有句子做断言,不用裸的 agent-id 字面量:`$base`(共享 runtime - /// 合同)里的 isolated agent 模板目录本就会列出 art-director / - /// design-foundation / art-asset-plan / code-prototype 等全部模板 taskId - /// 供 agent.spawn_isolated 使用,这与本次改动无关,也不构成越权——plan 根 - /// 已经在执行层被 A2 无条件拒绝 agent.spawn_isolated(见 delegation.rs), - /// 所以这份共享目录残留只是死文本,不是可利用的委派入口。 + /// 里的独有句子做断言,不用裸的 agent-id 字面量。 + /// + /// **订正(2026-08-20)**:本注释原先记着 `$base` 里的 isolated agent 模板 + /// 目录「只是死文本,不是可利用的委派入口」,理由是执行层已无条件拒绝 + /// `agent.spawn_isolated`。该推理只覆盖了 spawn 这一条路径,漏掉了 Provider + /// 会把目录里的名字挪去当 `agent.delegate` 的目标:实测 Supervisor 首轮就 + /// 委派了目录里排第一的 `design-director`,被执行层拒绝后未能自行改回 + /// `project-planning`,整个 run 空转到 loop 预算耗尽、零产物。目录现已在 + /// plan 根的上下文层删除,覆盖断言见 + /// `plan_root_supervisor_prompt_drops_the_specialist_role_catalog`。 #[test] fn plan_root_supervisor_prompt_drops_intro_and_visual_contract_sections() { for editor_api_key_is_configured in [false, true] { @@ -1316,6 +1352,89 @@ mod tests { } } + /// plan 根 Supervisor 的完整 system prompt 里不得再出现任何专业组角色名。 + /// 断言遍历 `GAME_CREATOR_AGENT_GROUP_DEFINITIONS` 而不是列举几个字面量, + /// 这样将来新增角色自动进入覆盖范围,不依赖有人记得回来补这条。 + #[test] + fn plan_root_supervisor_prompt_drops_the_specialist_role_catalog() { + let plan_prompt = game_creator_agent_runtime_tool_plan_system_prompt_for_agent( + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE, + ); + for group in GAME_CREATOR_AGENT_GROUP_DEFINITIONS { + for role in group.roles { + assert!( + !plan_prompt.contains(role.task_id), + "plan 根 prompt 不应出现专业角色名 {}:{plan_prompt}", + role.task_id + ); + } + } + assert!( + !plan_prompt + .contains(required_runtime_prompt_section("isolatedTemplateCatalogIntro").trim()), + "plan 根 prompt 不应保留 spawn_isolated 静态模板目录抬头" + ); + // agent.delegate 同样按这段填 expectedArtifacts / writeScopes,plan 根 + // 委派 project-planning 时要用,删目录不能把它一起删掉。 + assert!( + plan_prompt.contains(required_runtime_prompt_section("isolatedAgentContract").trim()), + "plan 根 prompt 必须保留 expectedArtifacts / writeScopes 合同" + ); + } + + /// 收窄只对 plan 根 Supervisor 生效。其余 source 与其余 agent 的共享 + /// runtime 合同必须逐字等于「基础 prompt 未被收窄」时的合成结果,且模板 + /// 目录仍然完整——`agent.spawn_isolated` 在那些链路上是真能用的。 + #[test] + fn only_the_plan_root_supervisor_loses_the_specialist_role_catalog() { + let intact_base = game_creator_agent_runtime_tool_plan_system_prompt(); + for (agent_id, source) in [ + (GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, ""), + ( + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE, + ), + ( + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE, + ), + ( + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE, + ), + ( + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + "project-supervisor-plan-forged", + ), + // 专业角色自己即便被伪造成 plan source 也不该被收窄。 + ("code-prototype", AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE), + ("quality-review", ""), + ] { + let actual = + game_creator_agent_runtime_tool_plan_system_prompt_for_agent(agent_id, source); + let expected = if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID { + game_creator_project_supervisor_tool_plan_prompt( + &intact_base, + editor_api_key_is_configured(), + source, + ) + } else { + intact_base.clone() + }; + assert_eq!( + actual, expected, + "agent={agent_id} source={source} 不应被 plan 根收窄逻辑改变" + ); + assert!( + actual.contains( + required_runtime_prompt_section("isolatedTemplateCatalogIntro").trim() + ), + "agent={agent_id} source={source} 必须保留静态模板目录" + ); + } + } + /// gui/cli/game-chat 等非 plan source 的 Supervisor system prompt 必须与 /// M1A-4 之前逐字相同:直接用既有 section 常量手工拼出改动前的合成结果, /// 逐字比对,防止上面的 plan 根分支误伤这条现役路径。