From 5c2d722b12f3a0b9557c65e60a537bb7ee5e8c2d Mon Sep 17 00:00:00 2001 From: Linghong Date: Thu, 20 Aug 2026 11:08:44 +0000 Subject: [PATCH] =?UTF-8?q?=E7=AB=8B=E9=A1=B9=E7=AD=96=E5=88=92=20role=20b?= =?UTF-8?q?rief=20=E6=8C=89=E8=A7=A3=E6=9E=90=E5=99=A8=E7=9C=9F=E5=AE=9E?= =?UTF-8?q?=E7=BA=BF=E6=A0=BC=E6=8F=8F=E8=BF=B0=E6=BE=84=E6=B8=85=E4=BF=A1?= =?UTF-8?q?=E5=B0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit brief 只写「下一行给出严格 JSON 的单题问题」,模型据此输出裸的单题对象,还自创 了一个 answerFormat 字段: {"header":...,"prompt":...,"options":[...],"answerFormat":"回复 A、B..."} 解析器要的是 {"questions":[{id,header,question,options}]} 且 deny_unknown_fields, 于是父 run 收到 unknown field `answerFormat`, expected `questions`。此时子 Run 已经终止,信封没有修复通道,整条委派直接停在 needs-reconciliation。模型其余部分 (第N轮·关键决定、A/B/需要原型验证 三项合同)都严格照 brief 执行,说明它跟的就是 brief——两份 prompt 描述了两种形状时,模型跟更具体的那份。 brief 改为逐字给出外壳、四个允许字段、id 的 snake_case 规则和各字段字符上限,并 点名 answerFormat 这个真实踩过的坑。上限数字全部来自 schema 常量,新增用例用 format! 把两边钉在一起,任一边先改都会红;为此把相关常量放宽到 pub(crate)。 这一条只改立项策划自己的 role brief,其它 Agent 的 overlay 不受影响。 Co-Authored-By: Claude Opus 5 --- .../prompts/runtime/roles/project-planning.md | 2 +- .../src-tauri/src/agent/prompt.rs | 37 +++++++++++++++++++ .../src-tauri/src/user_input.rs | 12 +++--- 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/prompts/runtime/roles/project-planning.md b/apps/ai-game-creator-shell/src-tauri/prompts/runtime/roles/project-planning.md index 97f680fc7..df797b2d2 100644 --- a/apps/ai-game-creator-shell/src-tauri/prompts/runtime/roles/project-planning.md +++ b/apps/ai-game-creator-shell/src-tauri/prompts/runtime/roles/project-planning.md @@ -4,7 +4,7 @@ - 当前 run 固定为 `source=agent-delegate`、`profile=standard`,父 Agent 是 `project-supervisor`。不得伪造、改写或猜测这些 Runtime 身份。 - 你不能委派或调度其他 Agent,不能创建 isolated child,不能调用 MCP、命令、进程、预览、画布、素材生成、写入/补丁/删除工具,也不能改变项目版本或审批事实。 -- 你的原生工具目录只应包含 `file.read`、`file.list` 以及 Runtime 协议控制函数 `update_agent_plan`、`respond_to_user`;`user.input_request` 不属于你的工具目录。若需要用户决定,必须以终态信封首行 `AGC_NEEDS_USER_INPUT_V1` 退出本轮,下一行给出严格 JSON 的单题问题,交由 Supervisor 转发。 +- 你的原生工具目录只应包含 `file.read`、`file.list` 以及 Runtime 协议控制函数 `update_agent_plan`、`respond_to_user`;`user.input_request` 不属于你的工具目录。若需要用户决定,必须以终态信封首行 `AGC_NEEDS_USER_INPUT_V1` 退出本轮,下一行给出严格 JSON 信封 `{"questions":[{ ... }]}`,交由 Supervisor 转发。`questions` 恰好一个元素;元素字段只能是 `id`、`header`、`question`、`options` 四个,多写任何字段(例如 `answerFormat`)或省掉 `questions` 外壳都会被 Runtime 拒收,整条委派随即作废。`id` 是唯一 snake_case(小写字母开头,只含小写字母、数字、下划线);`header` 是决策卡标题,单行且不超过 12 字符;`question` 是决策卡正文,单行且不超过 400 字符;`options` 是 2~3 个 `{"label": ..., "description": ...}`,label 单行不超过 60 字符、description 单行不超过 240 字符。不要另起一行写答题说明或把选项复述进 `question`,作答方式由 Runtime 自己呈现。 - 只有 Runtime 广告并允许 `plan.submit_gdd` 时才可提交 GDD;不要假设未广告的工具存在,也不要把 GDD、审批或下游构建写进普通文本。 ## 目标与轮次 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 e4c150782..21cbeb0f0 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 @@ -860,6 +860,43 @@ mod tests { ); } + #[test] + fn project_planning_role_brief_states_the_parser_wire_shape_verbatim() { + // 现场事故:brief 只说「严格 JSON 的单题问题」,模型据此输出裸的单题对象 + // 外加自创的 answerFormat 字段,解析器 deny_unknown_fields 直接拒收, + // 子 Run 已终止无从修复,整条委派停在 needs-reconciliation。brief 与 + // schema 必须逐字对齐,任一边改了都得让这条用例先红。 + let planning = game_creator_agent_runtime_role_overlay_prompt( + GAME_CREATOR_PROJECT_PLANNING_AGENT_ID, + None, + ); + assert!( + planning.contains(r#"{"questions":[{ ... }]}"#), + "brief 必须给出 questions 外壳,不能只说「单题问题」" + ); + for field in ["`id`", "`header`", "`question`", "`options`"] { + assert!(planning.contains(field), "brief 缺少字段 {field}"); + } + assert!( + planning.contains("answerFormat"), + "brief 要点名这个真实踩过的坑" + ); + for stated in [ + format!("不超过 {AGENT_RUNTIME_USER_INPUT_MAX_HEADER_CHARS} 字符"), + format!("不超过 {AGENT_RUNTIME_USER_INPUT_MAX_QUESTION_CHARS} 字符"), + format!("不超过 {AGENT_RUNTIME_USER_INPUT_MAX_OPTION_LABEL_CHARS} 字符"), + format!("不超过 {AGENT_RUNTIME_USER_INPUT_MAX_OPTION_DESCRIPTION_CHARS} 字符"), + format!( + "{AGENT_RUNTIME_USER_INPUT_MIN_OPTIONS}~{AGENT_RUNTIME_USER_INPUT_MAX_OPTIONS} 个" + ), + ] { + assert!( + planning.contains(&stated), + "brief 与 schema 不一致:缺少「{stated}」" + ); + } + } + #[test] fn project_planning_role_brief_is_isolated_to_its_manifest_overlay() { let planning = game_creator_agent_runtime_role_overlay_prompt( diff --git a/apps/ai-game-creator-shell/src-tauri/src/user_input.rs b/apps/ai-game-creator-shell/src-tauri/src/user_input.rs index 099642603..bb8845517 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/user_input.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/user_input.rs @@ -11,13 +11,13 @@ pub(crate) const AGENT_RUNTIME_USER_INPUT_STATUS_CANCELLED: &str = "cancelled"; const AGENT_RUNTIME_USER_INPUT_SIDECAR_MAX_BYTES: usize = 128 * 1024; const AGENT_RUNTIME_USER_INPUT_MAX_QUESTIONS: usize = 3; -const AGENT_RUNTIME_USER_INPUT_MIN_OPTIONS: usize = 2; -const AGENT_RUNTIME_USER_INPUT_MAX_OPTIONS: usize = 3; +pub(crate) const AGENT_RUNTIME_USER_INPUT_MIN_OPTIONS: usize = 2; +pub(crate) const AGENT_RUNTIME_USER_INPUT_MAX_OPTIONS: usize = 3; const AGENT_RUNTIME_USER_INPUT_MAX_ID_CHARS: usize = 64; -const AGENT_RUNTIME_USER_INPUT_MAX_HEADER_CHARS: usize = 12; -const AGENT_RUNTIME_USER_INPUT_MAX_QUESTION_CHARS: usize = 400; -const AGENT_RUNTIME_USER_INPUT_MAX_OPTION_LABEL_CHARS: usize = 60; -const AGENT_RUNTIME_USER_INPUT_MAX_OPTION_DESCRIPTION_CHARS: usize = 240; +pub(crate) const AGENT_RUNTIME_USER_INPUT_MAX_HEADER_CHARS: usize = 12; +pub(crate) const AGENT_RUNTIME_USER_INPUT_MAX_QUESTION_CHARS: usize = 400; +pub(crate) const AGENT_RUNTIME_USER_INPUT_MAX_OPTION_LABEL_CHARS: usize = 60; +pub(crate) const AGENT_RUNTIME_USER_INPUT_MAX_OPTION_DESCRIPTION_CHARS: usize = 240; /// 一份 schema 合法的澄清问询在线上最多可能有多长(字符)。 ///