diff --git a/apps/ai-game-creator-shell/src-tauri/build_support/runtime_prompt_bundle.rs b/apps/ai-game-creator-shell/src-tauri/build_support/runtime_prompt_bundle.rs index 4225d51f4..d04cd43b9 100644 --- a/apps/ai-game-creator-shell/src-tauri/build_support/runtime_prompt_bundle.rs +++ b/apps/ai-game-creator-shell/src-tauri/build_support/runtime_prompt_bundle.rs @@ -39,6 +39,11 @@ struct PromptBundleManifest { struct PromptCompositions { runtime: Vec, supervisor: Vec, + /// 立项策划根 run 的 Supervisor system prompt。它不是 `supervisor` 的差集, + /// 而是一份独立的完整清单:plan 根的工具面只有 7 个原生工具,专业组、 + /// isolated child、任务图与视觉产物合同在这条链路上全部不可执行,逐段 + /// 减法会把「plan 根到底看到什么」摊在两个函数的四个否定分支里。 + supervisor_plan: Vec, supervisor_chat: SupervisorChatComposition, } @@ -241,6 +246,12 @@ pub fn compile_manifest(manifest_path: &Path) -> Result Result>(); @@ -431,6 +443,14 @@ fn validate_section_ownership(manifest: &PromptBundleManifest) -> Result<(), Str { register("composition supervisor", section); } + for section in manifest + .compositions + .supervisor_plan + .iter() + .filter(|section| !section.starts_with('$')) + { + register("composition supervisorPlan", section); + } register("composition supervisorChat.identity", identity); register( "composition supervisorChat.finalReply", @@ -459,9 +479,19 @@ fn validate_section_ownership(manifest: &PromptBundleManifest) -> Result<(), Str "composition supervisor", "composition supervisorChat.identity", ]); + // plan 根 composition 是 Supervisor system prompt 的第二条 lane,不是另一种 + // 语义面。它按设计复用 runtime lane 的 `isolatedAgentContract`(`agent.delegate` + // 的 expectedArtifacts/writeScopes 合同)和 supervisor lane 的 `supervisorRepair` + // (返工必须逐字继承原合同)。除这两个方向外,跨所有者复用仍然是错误。 + let allowed_plan_runtime_owners = + BTreeSet::from(["composition runtime", "composition supervisorPlan"]); + let allowed_plan_supervisor_owners = + BTreeSet::from(["composition supervisor", "composition supervisorPlan"]); for (section, section_owners) in owners { if section_owners.len() > 1 && !(section == identity && section_owners == allowed_identity_owners) + && section_owners != allowed_plan_runtime_owners + && section_owners != allowed_plan_supervisor_owners { return Err(format!( "Prompt section 跨语义所有者复用:{section} -> {section_owners:?}" @@ -912,6 +942,10 @@ fn render_rust(manifest: &PromptBundleManifest, sections: &BTreeMap String { - let plan_root = agent_runtime_supervisor_source_is_plan(source); + // 只看 `source` 就够:本函数唯一的调用方在上面用 + // `agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID` 提前返回过,而 + // `source` 只有 Supervisor 自己的 run binding 会填成 plan(见 + // provider_request_builders.rs),其余角色恒为空串。 + if agent_runtime_supervisor_source_is_plan(source) { + return game_creator_project_supervisor_plan_tool_plan_system_prompt(); + } let visual_section = if editor_api_key_is_configured { RUNTIME_PROMPT_VISUAL_EDITOR_SECTION } else { @@ -597,17 +598,41 @@ fn game_creator_project_supervisor_tool_plan_prompt( RUNTIME_PROMPT_SUPERVISOR_COMPOSITION, |marker| match marker { "$base" => Some(prompt), - "$visualContract" => Some(if plan_root { - "" - } else { - required_runtime_prompt_section(visual_section) - }), - "supervisorIntro" if plan_root => Some(""), + "$visualContract" => Some(required_runtime_prompt_section(visual_section)), _ => None, }, ) } +/// 立项策划根 run(`source == project-supervisor-plan`)的 Supervisor system +/// prompt 是一份独立清单,不是通用 Supervisor 合同的差集。 +/// +/// 通用合同预设 Supervisor 手里有 43 个原生工具、六个专业组、isolated child、 +/// 正式任务图和视觉产物合同;plan 根一个都没有——它只有 7 个工具,只能委派 +/// `project-planning` 一个子 Agent。继续用逐段否定去改写通用合同,等于让 +/// 「plan 根到底看到什么」散落在几个 `if plan_root` 分支里,而每漏一段就是一次 +/// 已经实测到的偏航(专业角色目录残留曾让 Supervisor 照着委派 +/// `design-director`)。因此这里整份换成 `supervisorPlan` composition,plan 根 +/// 的全部段落在 manifest 里一眼可读。 +fn game_creator_project_supervisor_plan_tool_plan_system_prompt() -> String { + // 与 Provider 请求的函数目录共用 `agent_runtime_plan_root_supervisor_tools`: + // 提示词里列的工具和实际广告的工具必须是同一份事实,否则又会出现 + // 「合同说有、请求里没有」的自相矛盾。 + let tool_catalog = agent_runtime_plan_root_supervisor_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 不会按工具动作下标代替你更新进度。本 run 的原生可执行工具目录只有:{tool_catalog}。写入、补丁、删除、命令、预览、素材生成、任务图、记忆、黑板、isolated child 与 MCP 工具在本 run 都不存在,调用它们只会失败。" + ); + // `$platform` 整段是 command.start/exec/poll/stdin/terminate 的用法合同, + // plan 根一个 command 工具都没有;GDD 里的平台事实由 Runtime 另行注入,与 + // 这段无关。 + render_runtime_prompt_composition(RUNTIME_PROMPT_SUPERVISOR_PLAN_COMPOSITION, |marker| { + match marker { + "$header" => Some(&prompt_header), + _ => None, + } + }) +} + fn render_runtime_prompt_sections(sections: &[&str]) -> String { sections .iter() @@ -678,31 +703,14 @@ 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, - plan_root, - ) + game_creator_agent_runtime_tool_plan_system_prompt_for_platform(CURRENT_PLATFORM_IS_LINUX) } -fn game_creator_agent_runtime_tool_plan_system_prompt_for_platform( - linux: bool, - plan_root: bool, -) -> String { +fn game_creator_agent_runtime_tool_plan_system_prompt_for_platform(linux: 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 工具仅以当前请求提供的动态目录为准。" @@ -716,15 +724,7 @@ fn game_creator_agent_runtime_tool_plan_system_prompt_for_platform( let platform_section = runtime_prompt_platform_section_id(linux); render_runtime_prompt_composition(RUNTIME_PROMPT_RUNTIME_COMPOSITION, |marker| match marker { "$header" => Some(&prompt_header), - // 只删 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 - }), + "$isolatedAgentTemplates" => Some(&isolated_agent_templates), "$platform" => Some(required_runtime_prompt_section(platform_section)), _ => None, }) @@ -928,6 +928,38 @@ mod tests { } #[test] + /// Supervisor 的核心行为准则是「用验收标准和预期产物把边界清晰的任务委派 + /// 出去」(`identity-contract.md`),这在自主构建链路上正确,搬到策划链路 + /// 上却恰好碾过决策卡协议:把边界定清楚等于把用户没说的都替他决定掉。 + /// 实测中 Supervisor 写出的 acceptanceCriteria 含「未由用户指定的具体规则 + /// 标注为立项假设」和「目标平台、输入设备…须标为待定或建议」,前者作废了 + /// 3 轮问询预算(三次真实 run 里信封一次都没触发),后者还与 Runtime 强制 + /// 注入的平台事实直接矛盾。playbook 因此必须显式反向约束,否则整条策划链路 + /// 上只有「问完之后怎么转述」有指导、没有「你不该替用户回答」。 + #[test] + fn supervisor_playbook_forbids_pre_deciding_the_planning_tradeoffs() { + let playbook = required_runtime_prompt_section("supervisorPlaybook"); + for required in [ + "不得替用户预先裁定产品取舍", + "3 轮问询预算", + "不得要求标为待定", + ] { + assert!( + playbook.contains(required), + "supervisorPlaybook 缺少策划委派约束:{required}" + ); + } + // 该约束必须真的到达 plan 根 Supervisor,而不只是躺在 section 里。 + let plan_prompt = game_creator_agent_runtime_tool_plan_system_prompt_for_agent( + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE, + ); + assert!( + plan_prompt.contains("不得替用户预先裁定产品取舍"), + "plan 根 Supervisor 的 system prompt 必须带上该约束" + ); + } + /// `common.md` 的原创性红线随 `$base` 分发给做游戏链路的每个专业 Agent,但 /// 策划子 Agent 走 exact allowlist 的专用小 prompt、不拼 `$base`(理由见 /// `game_creator_project_planning_tool_plan_system_prompt` 的注释),该条款 @@ -1014,10 +1046,8 @@ 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, false); - let linux_prompt = - game_creator_agent_runtime_tool_plan_system_prompt_for_platform(true, false); + 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_section = required_runtime_prompt_section(RUNTIME_PROMPT_PLATFORM_DEFAULT_SECTION).trim(); let linux_section = @@ -1334,21 +1364,30 @@ mod tests { } /// M1A-4:plan 根 run(source == project-supervisor-plan)的 Supervisor - /// system prompt 不拼 supervisorIntro 与 $visualContract 这两个 section - /// ——两段都在暗示存在可并行委派的专业组,而策划链路的合同是只能委派 - /// project-planning 一个子 Agent。这里用两段各自最短的、只出现在该 section - /// 里的独有句子做断言,不用裸的 agent-id 字面量。 + /// system prompt 整份换成 `supervisorPlan` composition,不再是通用 Supervisor + /// 合同的差集。 /// - /// **订正(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`。 + /// **沿革**:这条最早只断言「不拼 supervisorIntro 与 $visualContract」——两段 + /// 都在暗示存在可并行委派的专业组,而策划链路只能委派 project-planning。 + /// 2026-08-20 又发现 `$base` 里的 isolated agent 模板目录同样会被挪去当 + /// `agent.delegate` 的目标:实测 Supervisor 首轮委派了目录里排第一的 + /// `design-director`,被执行层拒绝后未能自行改回 `project-planning`,整个 run + /// 空转到 loop 预算耗尽、零产物。逐段做减法每漏一段就是一次这样的偏航, + /// 因此现在改为整份替换:plan 根的段落清单由 manifest 的 `supervisorPlan` + /// 单点定义,本测试钉住它。 #[test] - fn plan_root_supervisor_prompt_drops_intro_and_visual_contract_sections() { + fn plan_root_supervisor_prompt_is_the_dedicated_plan_composition() { + assert_eq!( + RUNTIME_PROMPT_SUPERVISOR_PLAN_COMPOSITION, + &[ + "$header", + "planCommon", + "isolatedAgentContract", + "planSupervisorIdentity", + "planSupervisorPlaybook", + "supervisorRepair" + ] + ); for editor_api_key_is_configured in [false, true] { let plan_prompt = game_creator_project_supervisor_tool_plan_prompt( "shared runtime contract", @@ -1373,19 +1412,146 @@ mod tests { ), "plan 根 prompt 不应再包含 without-editor 视觉合同" ); + // 通用 $base 不再进入 plan 根:它带着改码流程、revision 验证门禁、 + // git 提交与「作为被委派的专业 Agent 时」等整段不可执行的合同。 assert!( - plan_prompt.contains("shared runtime contract"), - "plan 根 prompt 必须保留 $base" + !plan_prompt.contains("shared runtime contract"), + "plan 根 prompt 不应再拼通用 $base" ); + for dropped in [ + "supervisorIdentityContract", + "supervisorIntro", + "supervisorPlaybook", + "supervisorClaimGate", + "common", + "isolatedTemplateCatalogIntro", + "platformDefault", + "platformLinux", + ] { + assert!( + !plan_prompt.contains(required_runtime_prompt_section(dropped).trim()), + "plan 根 prompt 不应包含 section {dropped}" + ); + } + for kept in [ + "planCommon", + "isolatedAgentContract", + "planSupervisorIdentity", + "planSupervisorPlaybook", + // 返工必须逐字继承原 acceptanceCriteria / expectedArtifacts, + // 这条规则只在 supervisorRepair 里,plan 根实测撞过两次。 + "supervisorRepair", + ] { + assert!( + plan_prompt.contains(required_runtime_prompt_section(kept).trim()), + "plan 根 prompt 必须包含 section {kept}" + ); + } + } + } + + /// 提示词头部列出的工具清单和 Provider 请求实际广告的函数目录必须是同一份 + /// 事实。二者各自维护一份,就会退回「合同说有 43 个、请求里只有 9 个」的 + /// 自相矛盾——这正是收窄工具面本身要消灭的东西。 + #[test] + fn plan_root_prompt_tool_catalog_matches_the_advertised_allowlist() { + let plan_prompt = game_creator_agent_runtime_tool_plan_system_prompt_for_agent( + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE, + ); + let allowlist = agent_runtime_plan_root_supervisor_tools(); + assert!( + plan_prompt.contains(&format!( + "本 run 的原生可执行工具目录只有:{}。", + allowlist.join("、") + )), + "plan 根 prompt 头部必须逐字列出 allowlist:{plan_prompt}" + ); + for tool in agent_runtime_native_executable_tools() { + if allowlist.contains(&tool) { + continue; + } assert!( - plan_prompt.contains(required_runtime_prompt_section("supervisorPlaybook").trim()) + !plan_prompt.contains(&format!("、{tool}")) + && !plan_prompt.contains(&format!("{tool}、")), + "plan 根 prompt 不应在工具目录里出现被裁掉的 {tool}" ); + } + } + + /// `planCommon` 是 `common.md` 的子集副本,不是重写。 + /// + /// 通用 `common` 段有 43% 是 plan 根执行不了的内容(改码流程、revision 验证 + /// 门禁、git 提交、联网检索、以及整段「作为被委派的专业 Agent 时」的身份错位), + /// 其中「最后一次修改后必须成功执行 project.verify 才能 respond_to_user」还是 + /// 一道 plan 根永远满足不了的假门禁。但同一段里也压着这条链路唯一的原创性 + /// 红线、`user.input_request` 协议和静态委派协议——D11 拓扑的协议正文就在 + /// 这里,不在 playbook。所以 plan lane 只挑走这三段,逐字复制。 + /// + /// 复制就会漂移,因此这条断言反过来钉:`planCommon` 里的每一段都必须能在 + /// `common.md` 里逐字找到。谁单改一边,这条就红。 + #[test] + fn plan_common_paragraphs_are_verbatim_slices_of_the_shared_common_section() { + let common = required_runtime_prompt_section("common"); + let plan_common = required_runtime_prompt_section("planCommon"); + let inherited = [ + "用户只描述玩法类型、机制或相似体验时,不代表授权复刻现有游戏。", + "用户输入请求协议:", + "静态委派协议:", + ]; + let mut matched = 0; + for paragraph in plan_common + .split( + " + +", + ) + .map(str::trim) + { + if !inherited.iter().any(|head| paragraph.starts_with(head)) { + continue; + } assert!( - plan_prompt.contains(required_runtime_prompt_section("supervisorClaimGate").trim()) - ); - assert!( - plan_prompt.contains(required_runtime_prompt_section("supervisorRepair").trim()) + common.contains(paragraph), + "planCommon 段落已与 common.md 漂移:{paragraph}" ); + matched += 1; + } + assert_eq!(matched, inherited.len(), "planCommon 缺少继承段落"); + // 反向:plan 根不得继承那道它永远满足不了的验证门禁。 + assert!(common.contains("才能调用 respond_to_user 收束")); + assert!(!plan_common.contains("才能调用 respond_to_user 收束")); + } + + /// 「不得替用户预先裁定产品取舍」这段同时写进两条 lane 的 playbook。 + /// + /// 执行层只单向拦住 plan 根(`立项策划根 Run 只能委派 project-planning`), + /// 反方向没有对称限制:`agent.delegate` 的 `agentId` 是自由字符串,gui/cli + /// Supervisor 委派 `project-planning` 并未被禁。所以通用 playbook 里这段不是 + /// 死文本,两边都要有;这条断言钉住它们逐字相同。 + #[test] + fn both_playbooks_carry_the_same_anti_pre_deciding_contract() { + const SHARED: &str = "不得替用户预先裁定产品取舍"; + let generic = required_runtime_prompt_section("supervisorPlaybook"); + let plan = required_runtime_prompt_section("planSupervisorPlaybook"); + let paragraph = generic + .split( + " + +", + ) + .map(str::trim) + .find(|paragraph| paragraph.contains(SHARED)) + .expect("supervisorPlaybook 必须包含反预先裁定段落"); + assert!( + plan.contains(paragraph), + "两条 lane 的反预先裁定段落已漂移: +{paragraph} +--- +{plan}" + ); + for required in ["3 轮问询预算", "不得要求标为待定"] { + assert!(paragraph.contains(required), "缺少 {required}"); } } diff --git a/apps/ai-game-creator-shell/src-tauri/tests/runtime_prompt_bundle_build.rs b/apps/ai-game-creator-shell/src-tauri/tests/runtime_prompt_bundle_build.rs index 62941d560..540193af2 100644 --- a/apps/ai-game-creator-shell/src-tauri/tests/runtime_prompt_bundle_build.rs +++ b/apps/ai-game-creator-shell/src-tauri/tests/runtime_prompt_bundle_build.rs @@ -28,6 +28,9 @@ const SECTION_FILES: &[(&str, &str)] = &[ ("supervisorPlaybook", "supervisor/playbook.md"), ("supervisorClaimGate", "supervisor/claim-gate.md"), ("supervisorRepair", "supervisor/repair.md"), + ("planCommon", "plan/common.md"), + ("planSupervisorIdentity", "plan/supervisor-identity.md"), + ("planSupervisorPlaybook", "plan/supervisor-playbook.md"), ("codePrototypeGameChat", "roles/code-prototype-game-chat.md"), ( "providerIsolatedToolContract", @@ -141,6 +144,14 @@ fn valid_manifest() -> Value { "supervisorClaimGate", "supervisorRepair" ], + "supervisorPlan": [ + "$header", + "planCommon", + "isolatedAgentContract", + "planSupervisorIdentity", + "planSupervisorPlaybook", + "supervisorRepair" + ], "supervisorChat": { "identity": "supervisorIdentityContract", "finalReply": "supervisorFinalReplyContract"