策划澄清链路按原型拉齐:header 带主题、放开提问面、统一选项形状
对照 local-scripts/deisgn_agent 原型逐条比对后拉回五处分歧。原型 38 条 run 里 22 条走满 3 轮澄清,本仓库 GUI 实测一次都没到过第 3 轮。 1. header 从固定 8 字的轮号计数器「第N轮·关键决定」改回带主题的 「第N轮·当前要决定:<主题>」,决定台账的 topic 改从 header 取。12 字上限是 三条泳道共用的通用 user.input_request 常量,按 header 形状开策划分支——通用 问询今天能过的明天逐字照过,做游戏 / 做素材拿到的仍是 12 字。 2. 提问名额判据恢复原型三分法:「且影响首个可玩闭环」从「空白」的定义挪回提问 优先级,并恢复「空白或存疑」。此前的定义把「无从判断但不影响闭环」的字段整个 排除在空白之外,可问集合被闭合成 pillars + coreLoop 两项。 3. 有默认建议的字段从「一律先用默认建议,不占轮次」改回「优先用默认建议而不是 提问」——有默认不等于不能问。 4. 默认建议清单换回原型那五条(局长偏好、美术、成长、探索、构建)。摘掉 genre.fusion / targetUsers.coreUsers|preferences|referenceGames / outOfScope: 它们进清单等于把第三顺位「制作边界与 MVP」整条轴默认掉,出稿触发器③「剩余 空白都能由默认建议覆盖」随之在第 3 轮恒真。反幻觉那句按原型结构移到「低幻觉 与 GDD 约束」段,outOfScope 的兜底内容与该段已有的 MVP 范围句逐字重复。 5. 选项数 brief 写「2~3 个」(照通用常量生成)、Runtime 硬校验恰好 3、brief 下文 又写「固定三个选项」,三方打架。统一为恰好 3,裸 3 提成 PLAN_CLARIFICATION_OPTION_COUNT 让 brief 与解析器同源。label 分隔符集合按原型 ^A\s*[·•・::..\-] 从 4 个扩到 8 个,两处都只放宽不收紧。 守门两条:策划 header 的放宽只对「第N轮·」形状生效、同长度的通用 header 仍被 12 字 挡下;分隔符集合对着一份逐字来自原型正则的显式清单断言——遍历集合本身是空转的, 删一个就少测一个。 未动分歧 5/6/7(轴级封锁、内容级禁问、oneLiner 禁问)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
|
||||
【转达的规则】
|
||||
|
||||
- 把用户答案回灌给 `project-planning` 时,逐条列出全部已确认决定,每条格式为 `[已确认] 第N轮问的是:{question 原文} | 候选项:{option1.label} / {option2.label} / {option3.label} → 用户答:{原文}`。**问题原文和三个选项标签必须带上**:`{header}` 恒为「第N轮·关键决定」,不含任何信息量;子 Agent 每轮都是全新 run,除了这段正文什么都看不到,只给它 header 和答案,「类似B」「B · 沙盒里程碑成长」这类答案就无从解读,它只能把同一件事再问一遍。用户答案原文一字不改、不归纳、不拆分、不搬轮次;任务长度接近上限时压缩你自己的说明文字和选项描述,绝不压缩用户答案、问题原文和选项标签。
|
||||
- 把用户答案回灌给 `project-planning` 时,逐条列出全部已确认决定,每条格式为 `[已确认] 第N轮问的是:{question 原文} | 候选项:{option1.label} / {option2.label} / {option3.label} → 用户答:{原文}`。**问题原文和三个选项标签必须带上**:`{header}` 只写到「第N轮·当前要决定:{主题}」这一层,答案落在选项上;子 Agent 每轮都是全新 run,除了这段正文什么都看不到,只给它主题和答案,「类似B」「B · 沙盒里程碑成长」这类答案就无从解读,它只能把同一件事再问一遍。用户答案原文一字不改、不归纳、不拆分、不搬轮次;任务长度接近上限时压缩你自己的说明文字和选项描述,绝不压缩用户答案、问题原文和选项标签。
|
||||
- 策划链路的澄清信封**恰好一题**,不是通用静态委派协议里的 1-3 题:`project-planning` 每轮只提一个主要决定,Runtime 也只接受一题,多于一题会在出卡时被拒。委派 task 里不要写“1-3 个结构化问题”。
|
||||
- 上一条格式里的三个选项标签就是决策卡上的 A、B 和“需要原型验证”,必须原样转述、一个都不能省;B 是用户确认的 `confirmed/user_option`,不能转成默认建议。用户后续自由填写推翻了更早的决定时,你只负责把两轮答案的原文都原样带到,并说明后者更晚;怎么记进决定台账由 `project-planning` 判断,不要替它裁定哪条作废。
|
||||
|
||||
|
||||
@@ -4,17 +4,17 @@
|
||||
|
||||
- 当前 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 信封 `{"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 自己呈现。
|
||||
- 你的原生工具目录只应包含 `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` 是决策卡标题,写成 `第N轮·当前要决定:<主题>`,单行且不超过 60 字符;`question` 是决策卡正文,单行且不超过 400 字符;`options` 恰好 3 个 `{"label": ..., "description": ...}`,依次是 A、B、逐字“需要原型验证”(详见下文决策卡一段),label 单行不超过 60 字符、description 单行不超过 240 字符。不要另起一行写答题说明或把选项复述进 `question`,作答方式由 Runtime 自己呈现。
|
||||
- 只有 Runtime 广告并允许 `plan.submit_gdd` 时才可提交 GDD;不要假设未广告的工具存在,也不要把 GDD、审批或下游构建写进普通文本。
|
||||
|
||||
## 目标与轮次
|
||||
|
||||
- 最多进行 3 轮关键澄清;每轮是新 run、同一 session。你看得到自己的历史,但用户答案以 Supervisor 委派任务中的转述为准,缺失信息不能臆造。
|
||||
- **默认先澄清。** 出稿只有四个触发器,除此之外每轮都先做下面的字段差距检测再决定问不问:①任务正文出现“直接出稿”这四个字;②已完成第 3 轮澄清(任务正文写明的已用轮次已达上限);③剩余空白都能由默认建议覆盖,且不影响首个可玩闭环;④收到 Runtime 的活跃预算或超时提示。任务正文能改变流程的只有第 ① 条——它写的其它说明属于内容,不是出稿触发器。既定事实(用户答案、已确认决定)仍以任务正文为准。
|
||||
- 每轮提问前逐项对照 `plan-submit-gdd-input.v1` 的 `game` 字段做差距检测:用户明确提供的 = `confirmed`;有依据可推断的 = 按下面的默认建议填写并标 `default_pending`;无从判断**且影响首个可玩闭环**的 = 空白。提问名额只花在空白项上;有默认建议兜底的字段一律先用默认建议,不占轮次。`title`、`oneLiner`、`mvpSystems`、`creatorTips` 由你生成并标 `default_pending`,不作为提问对象;`platformFacts` 禁问。
|
||||
- **默认建议**(一律 `answerSource=default`、`round=0`;只用于缩短对话,不覆盖用户明确输入):`genre.fusion` 缺 → `null`,MVP 不做融合第二类型;`artStyle` 缺 → `visualType` 风格化、轮廓清楚,`keywords` 取自已确认的核心行为,`mvpArtBoundary` 写明 MVP 用占位资产、资产可复用;`targetUsers.sessionLength` 缺 → 10~20 分钟一局;`targetUsers.coreUsers` / `preferences` 缺 → 按已确认的类型与核心行为写典型玩家,不得编造人群规模、销量或市场数据;`targetUsers.referenceGames` 缺 → 空数组;`outOfScope` 缺 → 多人、商城、服务器、开放世界、赛季、复杂社交、完整剧情、全量内容。**`pillars` 与 `coreLoop` 没有默认建议**:它们就是首个可玩闭环本身,空白时属于该问的空白,不得用默认值填掉。
|
||||
- 每轮提问前逐项对照 `plan-submit-gdd-input.v1` 的 `game` 字段做差距检测:用户明确提供的 = `confirmed`;有依据可推断的 = 按下面的默认建议填写并标 `default_pending`;无从判断的 = 空白。提问名额只花在**空白或存疑、且影响首个可玩闭环**的决定上;有默认建议兜底的字段优先用默认建议而不是提问——「有默认」不等于「不能问」,那条默认明显可能是错的、且选错就做不出首个可玩闭环时,它就是一个该问的存疑项。`title`、`oneLiner`、`mvpSystems`、`creatorTips` 由你生成并标 `default_pending`,不作为提问对象;`platformFacts` 禁问。
|
||||
- **默认建议**(一律 `answerSource=default`、`round=0`;只用于缩短对话,不覆盖用户明确输入):`targetUsers.sessionLength` 缺 → 10~20 分钟一局;`artStyle` 缺 → `visualType` 风格化、轮廓清楚,`keywords` 取自已确认的核心行为,`mvpArtBoundary` 写明 MVP 用占位资产、资产可复用;缺成长时 → 1 条成长线和 2~3 个选择;缺探索时 → 1 条主路线加 1 个有意义的岔路;缺构建时 → 高风险输出和稳健防御两种方向。清单之外的字段没有默认值兜底——`genre.fusion`、`targetUsers.coreUsers` / `preferences` / `referenceGames`、`outOfScope` 缺失时都算空白,该不该花一轮问它们由上面的判据决定,不要自己拍一个值填掉就当它已经定了。**`pillars` 与 `coreLoop` 没有默认建议**:它们就是首个可玩闭环本身,空白时属于该问的空白,不得用默认值填掉。
|
||||
- 优先顺序:核心行为与本局目标 → 重玩动力 → 制作边界与 MVP。每轮最多问一个主要决定。**已确认决定关掉的那条轴不得重问。** 任务正文里每条 `[已确认]` 都带着当轮的问题原文和三个选项标签,先照它判断哪些轴已经关闭,本轮的问题必须落在另一条还没关闭的轴上。把已确认答案换个说法再问一遍——例如用户已经选定“自由经营、靠成就和攒钱升级推进”,你又拿“短周期经营目标 vs 沙盒里程碑成长”去问——是白烧一轮预算。所有轴都已关闭时按出稿触发器③直接出稿。
|
||||
- 决策卡的 header 固定为“第N轮·关键决定”,其中 N 是 Runtime 从委派谱系派生的当前轮号,必须精确相等,写错会被 Runtime 拒收:首轮恒为 1;之后每次续跑的任务正文都会写明已用轮次与上限,本轮该用的 N 就是“已用轮次 + 1”。正文以“当前要决定:”开头,只问尚未由平台事实或 MVP 规则排除的真实产品取舍,并说明为什么现在问;每张卡固定提供三个选项:A 是你的推荐方案(label 以 `A ·`、`A:`、`A:` 或 `A-` 开头并写明推荐、好处和代价),B 是形状不同且真实可行的平行备选(label 以 `B ·`、`B:`、`B:` 或 `B-` 开头并写明后果和代价),第三项逐字为“需要原型验证”,description 必须给出 30~90 分钟微型原型、试玩对象、观察信号和通过标准。自由输入按用户原话处理。
|
||||
- 决策卡的 header 写成“第N轮·当前要决定:<主题>”,最多 60 字符。N 是 Runtime 从委派谱系派生的当前轮号,写错会被 Runtime 拒收:首轮恒为 1;之后每次续跑的任务正文都会写明已用轮次与上限,本轮该用的 N 就是“已用轮次 + 1”。`<主题>` 是这一轮真正要定的那件事本身(例如“塔的构筑方式”“每局变化来源”),一句话说完、不带状态标记——它会原样落进决定台账的 `topic`,也是你下一轮辨认哪些轴已经关掉的唯一线索,写成“关键决定”这类空话等于把它作废。正文只问尚未由平台事实或 MVP 规则排除的真实产品取舍,并说明为什么现在问;每张卡固定提供三个选项:A 是你的推荐方案(label 以 `A ·`、`A:`、`A:` 或 `A-` 开头并写明推荐、好处和代价),B 是形状不同且真实可行的平行备选(label 以 `B ·`、`B:`、`B:` 或 `B-` 开头并写明后果和代价),第三项逐字为“需要原型验证”,description 必须给出 30~90 分钟微型原型、试玩对象、观察信号和通过标准。自由输入按用户原话处理。
|
||||
|
||||
## 低幻觉与 GDD 约束
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
- A、B 或自由填写得到的用户决定标 `confirmed`;用户选择“需要原型验证”标 `prototype_pending`,并保留同 id 的原型验证项——这两项是用户亲手选的,不得改判。只有未提问、由你按默认建议填写的字段才标 `default_pending`,其 `answerSource=default`、`round=0`。不要把用户选择的 B 当成默认项,也不要凭空把没问过的字段标成 `confirmed`——Runtime 会拒收任何没有对应用户作答的 `confirmed`。
|
||||
- 用户的自由填写没有回答你问的那道题时(他谈的是别的取舍,或者推翻了更早的决定),改这条决定的 `topic`,按他**实际说的内容**重新命名——这是你纠正错误绑定的唯一手段,Runtime 不会替你判断一句话答没答上一道题。若他对该题确实没有作出取舍,把该条降级为 `default_pending` + `answerSource=default` 并按默认建议写 `answerSummary`,再另起一条记录他实际确定下来的东西,在新条目的 `topic` 里写明与被推翻决定的关系。降级只能往这个方向;用户已作出的决定不得整条丢弃。
|
||||
- `prototypeValidationItems` 是必填字段(没有就传空数组),与 `prototype_pending` 决定**一一对应**:每条 `prototype_pending` 决定必须有一个同 id 的验证项,每个验证项也必须对应一条 `prototype_pending` 决定,最多 3 项。除了用户亲选“需要原型验证”之外,你自己也可以主动标:手感、节奏、可读性、难度曲线这类你没问过、但选错就做不出首个可玩闭环的判断,标 `prototype_pending`(`answerSource=default`、`round=0`)比标 `default_pending` 诚实——那不是一个默认值,是一个没人验证过的假设。每项写清 30~90 分钟微型原型做什么、让谁试玩、观察什么信号、什么算通过。
|
||||
- 不得编造具体游戏的机制、数值、销量、人群规模、团队规模或来源。写 `targetUsers` 时按已确认的类型与核心行为描述典型玩家即可。
|
||||
- 只定义一个完整可玩闭环。MVP 不含多人、商城、服务器、开放世界、赛季、复杂社交、完整剧情或全量内容,除非用户明确改变范围。
|
||||
- GDD 至少覆盖:游戏名称与类型、一句话描述、2~4 条游戏支柱、核心循环、目标用户、美术方向、3~6 个最小 MVP 系统、先做/暂缓/验证/扩展条件、决定状态和审批请求。不要把 Runtime 注入的身份、时间、指纹、审批 receipt 或平台事实当作 Provider 输入字段。
|
||||
- 平台事实由 Runtime 固定注入为自包含 Web、desktop/mobile 双视口、keyboard/touch 双输入、本地 HTTP 预览;不得修改、删减或向用户询问。
|
||||
|
||||
@@ -872,13 +872,17 @@ mod tests {
|
||||
"brief 要点名这个真实踩过的坑"
|
||||
);
|
||||
for stated in [
|
||||
format!("不超过 {AGENT_RUNTIME_USER_INPUT_MAX_HEADER_CHARS} 字符"),
|
||||
// 策划卡的 header 走 `AGENT_RUNTIME_USER_INPUT_MAX_PLAN_HEADER_CHARS`:它装的是
|
||||
// 这一轮要定的主题本身,不是通用问询那 12 字的标题格。钉住的仍是「brief 与
|
||||
// 解析器同一把尺子」,只是尺子换成了策划链路实际生效的那一把。
|
||||
format!("不超过 {AGENT_RUNTIME_USER_INPUT_MAX_PLAN_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} 个"
|
||||
),
|
||||
// 选项数同理:通用协议是 2-3 个,策划决策卡恒为 A / B /「需要原型验证」
|
||||
// 三项。brief 早先照通用常量写「2~3 个」,和它自己下文的「固定提供三个
|
||||
// 选项」以及 `planning_coordinator` 的硬校验三方打架。
|
||||
format!("恰好 {PLAN_CLARIFICATION_OPTION_COUNT} 个"),
|
||||
] {
|
||||
assert!(
|
||||
planning.contains(&stated),
|
||||
@@ -985,9 +989,17 @@ mod tests {
|
||||
/// 这条会跟着红。
|
||||
///
|
||||
/// 二、`pillars` / `coreLoop` 明确排除在清单外:它们就是首个可玩闭环本身,
|
||||
/// 给它们配默认值等于把最该花提问预算的那两项默认掉。原型那份清单里的
|
||||
/// 成长 / 探索 / 构建三条落到本仓库的 schema 上正好落在这两个字段上,照抄
|
||||
/// 会和「提问顺序:核心行为与本局目标 → 重玩动力」的前两顺位直接打架。
|
||||
/// 给它们配默认值等于把最该花提问预算的那两项默认掉。
|
||||
///
|
||||
/// 清单成员已按原型(`local-scripts/deisgn_agent/prompts.py:136`)那五条拉齐:
|
||||
/// 局长偏好、美术、成长、探索、构建。`genre.fusion` / `targetUsers.coreUsers`
|
||||
/// / `preferences` / `referenceGames` / `outOfScope` 从清单里摘掉了——它们
|
||||
/// 原型就没有默认值,进了清单就等于把第三顺位「制作边界与 MVP」整条轴默认
|
||||
/// 掉,出稿触发器③「剩余空白都能由默认建议覆盖」随之在第 3 轮恒真,3 轮预算
|
||||
/// 实际只花得出 2 轮。成长 / 探索 / 构建三条与上面那句不冲突:它们是维度级
|
||||
/// 缺省内容,不是 `pillars` / `coreLoop` 两个字段的缺省值,而且「优先用默认
|
||||
/// 建议而不是提问」是软优先级,不禁止提问——原型正是带着这三条默认,仍然把
|
||||
/// 第 1 轮花在 coreLoop、第 2 轮花在重玩动力上。
|
||||
///
|
||||
/// 三、出稿触发器是闭集。生产实测过 Supervisor 会把「若缺少会实质改变结果的
|
||||
/// 事实才提问,否则直接提交」写进委派 task,子 Agent 照办后 0 轮出稿;这里
|
||||
@@ -1003,15 +1015,27 @@ mod tests {
|
||||
planning.contains("**默认建议**"),
|
||||
"role brief 三处引用「默认建议」,清单本身必须在场"
|
||||
);
|
||||
for field in [
|
||||
"`genre.fusion`",
|
||||
"`artStyle`",
|
||||
"`targetUsers.sessionLength`",
|
||||
"`targetUsers.referenceGames`",
|
||||
"`outOfScope`",
|
||||
] {
|
||||
for field in ["`artStyle`", "`targetUsers.sessionLength`"] {
|
||||
assert!(planning.contains(field), "默认建议清单缺少字段 {field}");
|
||||
}
|
||||
for dimension in ["缺成长时", "缺探索时", "缺构建时"] {
|
||||
assert!(
|
||||
planning.contains(dimension),
|
||||
"默认建议清单缺少原型的维度级缺省 {dimension}"
|
||||
);
|
||||
}
|
||||
// 反向:这几个字段一旦回到默认清单,轴三就又被默认掉了。它们仍会在 brief 里
|
||||
// 出现(被点名为「没有默认值兜底」),所以只能钉「缺 → 」这个清单条目形状。
|
||||
for defaulted in [
|
||||
"`genre.fusion` 缺 →",
|
||||
"`targetUsers.referenceGames` 缺 →",
|
||||
"`outOfScope` 缺 →",
|
||||
] {
|
||||
assert!(
|
||||
!planning.contains(defaulted),
|
||||
"{defaulted} 不得回到默认建议清单:那会让出稿触发器③在第 3 轮恒真"
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
planning.contains("**`pillars` 与 `coreLoop` 没有默认建议**"),
|
||||
"pillars / coreLoop 不得进默认建议清单"
|
||||
@@ -1529,12 +1553,15 @@ mod tests {
|
||||
|
||||
/// 澄清回灌必须带上问题原文和三个选项标签,两端都要钉住。
|
||||
///
|
||||
/// `header` 按信封契约恒为「第N轮·关键决定」,零信息量;而 `project-planning`
|
||||
/// 每轮都是全新 run(`observations: []`),除了委派任务正文什么都看不到。只回灌
|
||||
/// `{header} → 用户答:{原文}` 时,「类似B」「B · 沙盒里程碑成长」这类答案无从
|
||||
/// 解读——生产实测的农场经营项目里,第 1 轮问「季节订单冲刺 vs 自主农场成长」,
|
||||
/// 用户答了 B,第 2 轮又拿「短周期经营目标 vs 沙盒里程碑成长」问同一条轴,
|
||||
/// 而且 B 选项几乎是用户原话的复述。
|
||||
/// `header` 现在带主题(「第N轮·当前要决定:{主题}」),但只到主题这一层——用户
|
||||
/// 拍的板落在**选项**上。而 `project-planning` 每轮都是全新 run(`observations: []`),
|
||||
/// 除了委派任务正文什么都看不到。只回灌 `{header} → 用户答:{原文}` 时,
|
||||
/// 「类似B」「B · 沙盒里程碑成长」这类答案仍然无从解读——生产实测的农场经营项目里,
|
||||
/// 第 1 轮问「季节订单冲刺 vs 自主农场成长」,用户答了 B,第 2 轮又拿「短周期经营
|
||||
/// 目标 vs 沙盒里程碑成长」问同一条轴,而且 B 选项几乎是用户原话的复述。
|
||||
///
|
||||
/// 这条与 header 带不带主题正交:主题解决「问过哪些轴」,选项标签解决「答案指的是
|
||||
/// 哪一个」。两端都得钉。
|
||||
#[test]
|
||||
fn plan_clarification_relay_carries_the_question_and_option_labels() {
|
||||
let plan = required_runtime_prompt_section("planSupervisorPlaybook");
|
||||
|
||||
@@ -3932,9 +3932,9 @@ mod plan_envelope_repair_tests {
|
||||
use super::*;
|
||||
|
||||
const TRUNCATED: &str = "AGC_NEEDS_USER_INPUT_V1
|
||||
{\"questions\":[{\"id\":\"core_loop\",\"header\":\"第1轮·关键决定\",\"question\":\"当前要决定:?\",\"options\":[{\"label\":\"A\",\"description\":\"甲\"}]}";
|
||||
{\"questions\":[{\"id\":\"core_loop\",\"header\":\"第1轮·当前要决定:核心闭环形状\",\"question\":\"?\",\"options\":[{\"label\":\"A\",\"description\":\"甲\"}]}";
|
||||
const COMPLETE: &str = "AGC_NEEDS_USER_INPUT_V1
|
||||
{\"questions\":[{\"id\":\"core_loop\",\"header\":\"第1轮·关键决定\",\"question\":\"当前要决定:?\",\"options\":[{\"label\":\"A\",\"description\":\"甲\"},{\"label\":\"B\",\"description\":\"乙\"}]}]}";
|
||||
{\"questions\":[{\"id\":\"core_loop\",\"header\":\"第1轮·当前要决定:核心闭环形状\",\"question\":\"?\",\"options\":[{\"label\":\"A\",\"description\":\"甲\"},{\"label\":\"B\",\"description\":\"乙\"}]}]}";
|
||||
|
||||
/// 截断的信封必须在 run 内被认出来,否则它会随 final reply 逃逸成一条
|
||||
/// needs-repair 委派,把返工额度和澄清轮次一起卷进去。
|
||||
@@ -3988,7 +3988,7 @@ mod plan_envelope_repair_tests {
|
||||
fn a_degenerated_tail_no_longer_burns_a_repair_attempt() {
|
||||
let reply = concat!(
|
||||
"AGC_NEEDS_USER_INPUT_V1\n",
|
||||
r#"{"questions":[{"id":"replay_progression","header":"第2轮·关键决定","question":"当前要决定:自由经营农场的长期目标采用哪种组合?","options":[{"label":"A · 推荐:里程碑升级+成就","description":"以累计资金解锁少量新地块或设施。"},{"label":"B · 专注农场扩建","description":"只用经营收益逐步解锁地块与设施。"},{"label":"需要原型验证","description":"制作微型原型让目标玩家试玩两种目标结构。"}]}]}સwerhu рҭ. 北京赛车? тру. [ ]"#,
|
||||
r#"{"questions":[{"id":"replay_progression","header":"第2轮·当前要决定:自由经营农场的长期目标","question":"它决定玩家为何持续规划、赚钱与重玩,也控制 MVP 的范围。","options":[{"label":"A · 推荐:里程碑升级+成就","description":"以累计资金解锁少量新地块或设施。"},{"label":"B · 专注农场扩建","description":"只用经营收益逐步解锁地块与设施。"},{"label":"需要原型验证","description":"制作微型原型让目标玩家试玩两种目标结构。"}]}]}સwerhu рҭ. 北京赛车? тру. [ ]"#,
|
||||
);
|
||||
assert!(game_creator_agent_runtime_plan_envelope_parse_error(
|
||||
GAME_CREATOR_PROJECT_PLANNING_AGENT_ID,
|
||||
|
||||
+87
-16
@@ -5,6 +5,13 @@ use uuid::Uuid;
|
||||
const PLAN_OPTION_A_PREFIX: char = 'A';
|
||||
const PLAN_OPTION_B_PREFIX: char = 'B';
|
||||
const PLAN_OPTION_PROTOTYPE_VALIDATION: &str = "需要原型验证";
|
||||
/// 策划决策卡恒为 A / B /「需要原型验证」三项,不是通用 `user.input_request` 协议的
|
||||
/// 2-3 个。role brief 早先照通用常量写成「2~3 个」,与本文件的硬校验和 brief 自己
|
||||
/// 下文的「固定提供三个选项」三方打架;模型照前者吐两项,整封信封在出卡时被拒、
|
||||
/// 回灌重试,白烧一个未推进回合,丢掉的还恰好是用户产生 `prototype_pending` 的唯一
|
||||
/// 入口。`project_planning_role_brief_states_the_parser_wire_shape_verbatim` 钉住
|
||||
/// brief 与这里同源。
|
||||
pub(crate) const PLAN_CLARIFICATION_OPTION_COUNT: usize = 3;
|
||||
const PLAN_QUESTION_PREFIX: &str = "当前要决定:";
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
@@ -110,14 +117,40 @@ fn exact_plan_child_identity_at(
|
||||
Ok(Some((binding, delivery)))
|
||||
}
|
||||
|
||||
fn plan_question_topic(question: &AgentRuntimeUserInputQuestion) -> Result<String, String> {
|
||||
let remainder = question
|
||||
.question
|
||||
.strip_prefix(PLAN_QUESTION_PREFIX)
|
||||
/// 剥掉 header 的 `第{round}轮·` 前缀,返回其后的正文。
|
||||
///
|
||||
/// 轮号本身由 Runtime 从委派谱系派生,模型只是照着任务正文抄;这里逐字核对它,写错就
|
||||
/// 拒收——否则卡片标题会和 `GddApprovalCard` 那个「第 N 轮 / 共 3 轮」自相矛盾。
|
||||
fn plan_header_body(header: &str, round: u32) -> Option<&str> {
|
||||
let rest = header.trim_start().strip_prefix('第')?.trim_start();
|
||||
let digits = rest
|
||||
.chars()
|
||||
.take_while(char::is_ascii_digit)
|
||||
.collect::<String>();
|
||||
if digits.parse::<u32>().ok()? != round {
|
||||
return None;
|
||||
}
|
||||
let rest = rest[digits.len()..].trim_start().strip_prefix('轮')?.trim();
|
||||
// 原型模板写作 `第 N 轮 · 当前要决定:…`,中文语境下模型高频吐出 `·`/`:`/`:`/`-`
|
||||
// 几种分隔符;不在集合里的后果是整封信封被拒、白吃一个未推进回合。
|
||||
let rest = rest.strip_prefix(&PLAN_OPTION_LABEL_DELIMITERS[..])?;
|
||||
Some(rest.trim_start())
|
||||
}
|
||||
|
||||
/// 决定台账的 `topic` 取自 header。
|
||||
///
|
||||
/// 原型(`design_agent.py:1841`)直接把整条 header 当 topic;这里只是再剥掉 `第N轮·` 和
|
||||
/// 「当前要决定:」两层固定前缀,落进台账的是主题本身。
|
||||
fn plan_question_topic(
|
||||
question: &AgentRuntimeUserInputQuestion,
|
||||
round: u32,
|
||||
) -> Result<String, String> {
|
||||
let remainder = plan_header_body(&question.header, round)
|
||||
.and_then(|body| body.strip_prefix(PLAN_QUESTION_PREFIX))
|
||||
.ok_or_else(|| {
|
||||
plan_coordinator_error(
|
||||
"PLAN_INVALID_CLARIFICATION",
|
||||
"plan question 必须以“当前要决定:”开头",
|
||||
format!("plan header 必须形如“第{round}轮·当前要决定:<主题>”"),
|
||||
)
|
||||
})?;
|
||||
let topic = remainder
|
||||
@@ -129,7 +162,12 @@ fn plan_question_topic(question: &AgentRuntimeUserInputQuestion) -> Result<Strin
|
||||
|
||||
/// 全角冒号必须在集合里:prompt 全中文,模型在中文语境下写 `A:方案名` 是高频输出,
|
||||
/// 而不在集合里的后果是整个信封被拒、回灌重试,白吃一个未推进回合预算。
|
||||
const PLAN_OPTION_LABEL_DELIMITERS: [char; 4] = ['·', ':', ':', '-'];
|
||||
///
|
||||
/// 集合按原型的 `_OPTION_A_PATTERN`(`design_agent.py`,`^A\s*[·•・::..\-]`)拉齐。
|
||||
/// 本仓库先前只收 4 个,是同一条理由下更窄的一份——模型写 `A•路线布防` 或
|
||||
/// `A. 路线布防` 就会整封被拒。`plan_header_body` 解析 `第N轮·` 时复用这同一个集合,
|
||||
/// 两处一起放宽;只放宽、不收紧,既有能过的 label 逐字照过。
|
||||
const PLAN_OPTION_LABEL_DELIMITERS: [char; 8] = ['·', '•', '・', ':', ':', '.', '.', '-'];
|
||||
|
||||
fn plan_option_label_has_prefix(label: &str, prefix: char) -> bool {
|
||||
let Some(remainder) = label.strip_prefix(prefix).map(str::trim_start) else {
|
||||
@@ -154,6 +192,44 @@ fn plan_option_label_has_prefix(label: &str, prefix: char) -> bool {
|
||||
/// `user_freeform`。两边 state 同为 `confirmed`,状态机看不出异常——被污染的恰好是第
|
||||
/// 23.9 节要立起来的那个字段。`planning_clarification_option_pick_survives_untrimmed_label`
|
||||
/// 钉的就是这条不变量。
|
||||
#[cfg(test)]
|
||||
mod option_label_delimiter_tests {
|
||||
use super::*;
|
||||
|
||||
/// 分隔符集合只能放宽、不能收窄,且必须覆盖原型 `_OPTION_A_PATTERN` 的那一份。
|
||||
///
|
||||
/// 锁的是「集合里每一个都被接受」这条不变量,不是某个具体标点:少一个的后果不是
|
||||
/// 「模型换个写法」,而是一封完全合法的信封被判形状错误、回灌重试,白吃一个未推进
|
||||
/// 回合——`planning_clarification_accepts_fullwidth_colon_option_labels` 记的就是
|
||||
/// 全角冒号那一次。
|
||||
/// 逐字来自原型 `design_agent.py` 的 `^A\s*[·•・::..\-]`。这里**不能**改成遍历
|
||||
/// `PLAN_OPTION_LABEL_DELIMITERS` 本身——那样从集合里删掉一个,循环也跟着少测一个,
|
||||
/// 断言恒真。
|
||||
const PROTOTYPE_DELIMITERS: [char; 8] = ['·', '•', '・', ':', ':', '.', '.', '-'];
|
||||
|
||||
#[test]
|
||||
fn every_delimiter_in_the_set_is_accepted_on_both_option_prefixes() {
|
||||
for delimiter in PROTOTYPE_DELIMITERS {
|
||||
for prefix in [PLAN_OPTION_A_PREFIX, PLAN_OPTION_B_PREFIX] {
|
||||
let label = format!("{prefix}{delimiter}方案短语");
|
||||
assert!(
|
||||
plan_option_label_has_prefix(&label, prefix),
|
||||
"分隔符 {delimiter:?} 被拒:{label}"
|
||||
);
|
||||
let spaced = format!("{prefix} {delimiter} 方案短语");
|
||||
assert!(
|
||||
plan_option_label_has_prefix(&spaced, prefix),
|
||||
"带空格写法被拒:{spaced}"
|
||||
);
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
!plan_option_label_has_prefix("A方案短语", PLAN_OPTION_A_PREFIX),
|
||||
"没有分隔符不能算合法 A 选项,否则 A/B 与自由文本会混"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_option_label_matches_answer(label: &str, normalized_answer: &str) -> bool {
|
||||
label == normalized_answer
|
||||
}
|
||||
@@ -178,14 +254,7 @@ pub(crate) fn validate_exact_plan_clarification_question(
|
||||
"plan questionId 必须是最多 32 个 ASCII 字符且不能映射为 initial-request",
|
||||
));
|
||||
}
|
||||
let expected_header = format!("第{round}轮·关键决定");
|
||||
if question.header != expected_header {
|
||||
return Err(plan_coordinator_error(
|
||||
"PLAN_INVALID_CLARIFICATION",
|
||||
format!("plan question header 必须精确等于 {expected_header}"),
|
||||
));
|
||||
}
|
||||
let valid_shape = question.options.len() == 3
|
||||
let valid_shape = question.options.len() == PLAN_CLARIFICATION_OPTION_COUNT
|
||||
&& plan_option_label_has_prefix(&question.options[0].label, PLAN_OPTION_A_PREFIX)
|
||||
&& plan_option_label_has_prefix(&question.options[1].label, PLAN_OPTION_B_PREFIX)
|
||||
&& question.options[2].label == PLAN_OPTION_PROTOTYPE_VALIDATION;
|
||||
@@ -195,7 +264,9 @@ pub(crate) fn validate_exact_plan_clarification_question(
|
||||
"plan question 必须恰好提供 A、B、需要原型验证三个选项",
|
||||
));
|
||||
}
|
||||
plan_question_topic(question)?;
|
||||
// header 的定形连同轮号一起在这里兜底:`plan_question_topic` 要求它形如
|
||||
// `第{round}轮·当前要决定:<主题>`,并把主题本身取出来给决定台账。
|
||||
plan_question_topic(question, round)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -210,7 +281,7 @@ fn build_plan_clarification_decision_projection(
|
||||
let normalized_answer = normalize_plan_text(&answer.answer, "plan answer", 1, 400)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let question = &answer.question;
|
||||
let topic = plan_question_topic(question)?;
|
||||
let topic = plan_question_topic(question, round)?;
|
||||
let decision_id = question.id.replace('_', "-");
|
||||
let (state, answer_source) =
|
||||
if plan_option_label_matches_answer(&question.options[0].label, &normalized_answer)
|
||||
|
||||
@@ -256,7 +256,7 @@ impl StaticDelegateHopNote<'_> {
|
||||
rounds_used,
|
||||
rounds_limit,
|
||||
} => format!(
|
||||
"\n\n这是对已认领委派 {original_delegation_id} 的澄清续跑,不是返工轮,不消耗返工深度。已用澄清轮次 {rounds_used}/{rounds_limit}。仍有会实质改变结果的空白且预算未用尽时,可以继续以 AGC_NEEDS_USER_INPUT_V1 信封退出:questions 恰好一题,header 必须精确等于「第{next_round}轮·关键决定」。预算已用尽,或剩余空白能由默认建议覆盖且不影响首个可玩闭环时,立即提交 GDD。",
|
||||
"\n\n这是对已认领委派 {original_delegation_id} 的澄清续跑,不是返工轮,不消耗返工深度。已用澄清轮次 {rounds_used}/{rounds_limit}。仍有会实质改变结果的空白且预算未用尽时,可以继续以 AGC_NEEDS_USER_INPUT_V1 信封退出:questions 恰好一题,header 写成「第{next_round}轮·当前要决定:<主题>」,轮号必须是 {next_round},主题写这一轮真正要定的那件事。预算已用尽,或剩余空白能由默认建议覆盖且不影响首个可玩闭环时,立即提交 GDD。",
|
||||
next_round = rounds_used.saturating_add(1),
|
||||
),
|
||||
}
|
||||
@@ -1730,7 +1730,7 @@ mod tests {
|
||||
"澄清续跑必须写明已用轮次与上限:{clarification}"
|
||||
);
|
||||
assert!(
|
||||
clarification.contains("第2轮·关键决定"),
|
||||
clarification.contains("第2轮·当前要决定:"),
|
||||
"task 里的轮号必须等于 planning_coordinator 校验 header 时用的 rounds_used + 1:{clarification}"
|
||||
);
|
||||
|
||||
@@ -1753,7 +1753,7 @@ mod tests {
|
||||
"预算用尽时必须要求收稿,出卡侧会直接拒掉第四张卡:{exhausted}"
|
||||
);
|
||||
assert!(
|
||||
!exhausted.contains("第4轮·关键决定"),
|
||||
!exhausted.contains("第4轮·当前要决定:"),
|
||||
"预算用尽时不得再给出下一轮 header,那是一张永远递不上去的卡:{exhausted}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2560,8 +2560,8 @@ mod tests {
|
||||
serde_json::json!({
|
||||
"questions": [{
|
||||
"id": "plan_round_1",
|
||||
"header": "第1轮·关键决定",
|
||||
"question": "当前要决定:影子能力在首个可玩闭环中的核心作用。它会同时决定关卡布局、操作手感与原型优先级,也决定第一批谜题按什么规则组合;现在确认可以避免把三种玩法都做浅,也避免原型做到一半再推翻核心规则。",
|
||||
"header": "第1轮·当前要决定:影子能力在首个可玩闭环中的核心作用",
|
||||
"question": "它会同时决定关卡布局、操作手感与原型优先级,也决定第一批谜题按什么规则组合;现在确认可以避免把三种玩法都做浅,也避免原型做到一半再推翻核心规则。",
|
||||
"options": [
|
||||
{
|
||||
"label": "A · 影子化为可独立移动的暗影分身",
|
||||
@@ -2602,12 +2602,12 @@ mod tests {
|
||||
// abtest-tide2A-2:尾巴是「 马会」。
|
||||
concat!(
|
||||
"AGC_NEEDS_USER_INPUT_V1\n",
|
||||
r#"{"questions":[{"id":"replay_motivation","header":"第1轮·关键决定","question":"当前要决定:固定五岛海图的重复游玩动力采用哪种方案?现在确认它,才能锁定首个可玩闭环之外的得分与重开目标。","options":[{"label":"A · 推荐:固定布局冲榜","description":"每局地图与信件配置固定,玩家通过更优路线、潮汐 timing 和装卸顺序刷新送达数与总分;优点是实现最小、可读性强,代价是内容变化较少。"},{"label":"B · 轮换信件组合","description":"地图固定但每局从预设信件组合中轮换收件岛与期限;优点是重玩变化更明显,代价是需要额外平衡组合并降低可预测性。"},{"label":"需要原型验证","description":"用30~90分钟做可点击五岛地图与两种信件配置原型,让3名偏好轻策略的玩家各玩3局,观察是否主动重开及路线是否有差异;通过标准是多数玩家愿意重开且能说出改进路线。"}]}]} 马会"#,
|
||||
r#"{"questions":[{"id":"replay_motivation","header":"第1轮·当前要决定:固定五岛海图的重复游玩动力","question":"现在确认它,才能锁定首个可玩闭环之外的得分与重开目标。","options":[{"label":"A · 推荐:固定布局冲榜","description":"每局地图与信件配置固定,玩家通过更优路线、潮汐 timing 和装卸顺序刷新送达数与总分;优点是实现最小、可读性强,代价是内容变化较少。"},{"label":"B · 轮换信件组合","description":"地图固定但每局从预设信件组合中轮换收件岛与期限;优点是重玩变化更明显,代价是需要额外平衡组合并降低可预测性。"},{"label":"需要原型验证","description":"用30~90分钟做可点击五岛地图与两种信件配置原型,让3名偏好轻策略的玩家各玩3局,观察是否主动重开及路线是否有差异;通过标准是多数玩家愿意重开且能说出改进路线。"}]}]} 马会"#,
|
||||
),
|
||||
// verify-farm-4:尾巴是古吉拉特语字母、西里尔字母和中文垃圾词的混合物。
|
||||
concat!(
|
||||
"AGC_NEEDS_USER_INPUT_V1\n",
|
||||
r#"{"questions":[{"id":"replay_progression","header":"第2轮·关键决定","question":"当前要决定:自由经营农场的长期目标采用哪种组合?这会决定玩家为何持续规划、赚钱与重玩,并控制 MVP 的范围。","options":[{"label":"A · 推荐:里程碑升级+成就","description":"以累计资金解锁少量新地块或设施,同时完成可选成就;优点是目标清晰又保留自由安排,代价是需要同时做基础升级与成就追踪。"},{"label":"B · 专注农场扩建","description":"只用经营收益逐步解锁地块与设施,成就仅作展示;优点是系统更聚焦、反馈直接,代价是挑战层次和重玩目标较少。"},{"label":"需要原型验证","description":"制作 30–90 分钟微型原型,让 2–3 名目标玩家试玩两种目标结构,观察他们是否主动设定计划、理解进展并愿意继续经营;多数玩家能完成一次扩建且愿意追求第二个目标即通过。"}]}]}સwerhu рҭ. 北京赛车? тру. [ ]"#,
|
||||
r#"{"questions":[{"id":"replay_progression","header":"第2轮·当前要决定:自由经营农场的长期目标","question":"这会决定玩家为何持续规划、赚钱与重玩,并控制 MVP 的范围。","options":[{"label":"A · 推荐:里程碑升级+成就","description":"以累计资金解锁少量新地块或设施,同时完成可选成就;优点是目标清晰又保留自由安排,代价是需要同时做基础升级与成就追踪。"},{"label":"B · 专注农场扩建","description":"只用经营收益逐步解锁地块与设施,成就仅作展示;优点是系统更聚焦、反馈直接,代价是挑战层次和重玩目标较少。"},{"label":"需要原型验证","description":"制作 30–90 分钟微型原型,让 2–3 名目标玩家试玩两种目标结构,观察他们是否主动设定计划、理解进展并愿意继续经营;多数玩家能完成一次扩建且愿意追求第二个目标即通过。"}]}]}સwerhu рҭ. 北京赛车? тру. [ ]"#,
|
||||
),
|
||||
];
|
||||
for response in cases {
|
||||
@@ -2628,7 +2628,7 @@ mod tests {
|
||||
// verify-farm-2 现场原文,结尾是 `}]}` 而非 `}]}]}`。
|
||||
let response = concat!(
|
||||
"AGC_NEEDS_USER_INPUT_V1\n",
|
||||
r#"{"questions":[{"id":"core_loop_goal","header":"第1轮·关键决定","question":"当前要决定:这款农场经营游戏的一局,玩家主要通过什么目标获得满足?现在先定核心闭环,才能控制 MVP 范围。","options":[{"label":"A · 推荐:短周期订单经营","description":"围绕播种、收获、加工并完成限时订单推进;目标清晰、反馈快,代价是自由建造与长期规划较少。"},{"label":"B · 自主农场成长","description":"围绕规划田地、逐步扩建并达成阶段里程碑;沉浸和成长感更强,代价是前期目标反馈较慢、系统边界更难控。"},{"label":"需要原型验证","description":"制作 30~90 分钟微型原型,包含种植、收获和一种目标;让 2~3 名目标玩家试玩,观察是否理解目标、是否愿意继续一轮;通过标准是多数玩家无需讲解即可完成闭环并主动开始第二轮。"}]}"#,
|
||||
r#"{"questions":[{"id":"core_loop_goal","header":"第1轮·当前要决定:一局里玩家靠什么目标获得满足","question":"现在先定核心闭环,才能控制 MVP范围。","options":[{"label":"A · 推荐:短周期订单经营","description":"围绕播种、收获、加工并完成限时订单推进;目标清晰、反馈快,代价是自由建造与长期规划较少。"},{"label":"B · 自主农场成长","description":"围绕规划田地、逐步扩建并达成阶段里程碑;沉浸和成长感更强,代价是前期目标反馈较慢、系统边界更难控。"},{"label":"需要原型验证","description":"制作 30~90 分钟微型原型,包含种植、收获和一种目标;让 2~3 名目标玩家试玩,观察是否理解目标、是否愿意继续一轮;通过标准是多数玩家无需讲解即可完成闭环并主动开始第二轮。"}]}"#,
|
||||
);
|
||||
let error = parse_static_delegate_user_input_request(Some(response))
|
||||
.expect_err("an envelope that stops short of closing must not parse");
|
||||
@@ -2832,8 +2832,8 @@ mod tests {
|
||||
// 实测形态:option 对象里多写了一个 `id` 字段。
|
||||
let response = concat!(
|
||||
"AGC_NEEDS_USER_INPUT_V1\n",
|
||||
"{\"questions\":[{\"id\":\"core_loop\",\"header\":\"第1轮·关键决定\",",
|
||||
"\"question\":\"当前要决定:核心闭环形状。\",\"options\":[",
|
||||
"{\"questions\":[{\"id\":\"core_loop\",\"header\":\"第1轮·当前要决定:核心闭环形状\",",
|
||||
"\"question\":\"它决定首个可玩闭环长什么样。\",\"options\":[",
|
||||
"{\"id\":\"a\",\"label\":\"A · 甲方案\",\"description\":\"甲方案的后果\"},",
|
||||
"{\"id\":\"b\",\"label\":\"B · 乙方案\",\"description\":\"乙方案的后果\"},",
|
||||
"{\"id\":\"c\",\"label\":\"需要原型验证\",\"description\":\"做个微型原型看看\"}]}]}"
|
||||
|
||||
@@ -515,8 +515,8 @@ fn planning_clarification_question_body_with_labels(
|
||||
let body = serde_json::json!({
|
||||
"questions": [{
|
||||
"id": question_id,
|
||||
"header": format!("第{round}轮·关键决定"),
|
||||
"question": format!("当前要决定:第{round}轮核心取舍。现在确认后才能继续收敛 Fast GDD。"),
|
||||
"header": format!("第{round}轮·当前要决定:第{round}轮核心取舍"),
|
||||
"question": format!("第{round}轮核心取舍现在确认后才能继续收敛 Fast GDD。"),
|
||||
"options": [
|
||||
{
|
||||
"label": label_a,
|
||||
|
||||
@@ -41,10 +41,42 @@ 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;
|
||||
pub(crate) const AGENT_RUNTIME_USER_INPUT_MAX_HEADER_CHARS: usize = 12;
|
||||
/// 立项策划澄清卡的 header 是「这一轮要定的是什么」本身,不是一个 12 字的标题格。
|
||||
///
|
||||
/// 策划子 Agent 每轮都是全新 run,跨轮只能靠委派正文里转述的既往问答认路;header 带
|
||||
/// 主题时它一眼能看出哪几条轴已经关掉。原型(`local-scripts/deisgn_agent`)就是这么
|
||||
/// 做的:header 上限 60 字、写成 `第 N 轮 · 当前要决定:…`,决定台账的 `topic` 直接取
|
||||
/// 它。本仓库把 header 压成固定 8 字的轮号计数器后这条通路就断了。
|
||||
///
|
||||
/// 放宽只对 `第{N}轮` 这一种形状生效(`plan_clarification_header_limit`)。通用问询今天
|
||||
/// 能过的 header 明天逐字照过——做游戏 / 做素材两条泳道拿到的仍是 12 字上限,这里没有
|
||||
/// 任何一条既有请求会因此改变结果。
|
||||
pub(crate) const AGENT_RUNTIME_USER_INPUT_MAX_PLAN_HEADER_CHARS: usize = 60;
|
||||
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;
|
||||
|
||||
/// 该 header 能用到的字符上限。
|
||||
///
|
||||
/// 判据是形状而不是身份:这个函数在通用 `user.input_request` 解析路径上,六个调用点里
|
||||
/// 有两个(工具计划校验、动作摘要)拿不到 root,问不出「这封信是不是策划链路的」。形状
|
||||
/// 判据只放宽、从不收紧——非策划 header 一律走 12 字原路,策划 header 的真正定形由
|
||||
/// `planning_coordinator::validate_exact_plan_clarification_question` 逐字兜底。
|
||||
fn plan_clarification_header_limit(header: &str) -> usize {
|
||||
let Some(rest) = header.trim_start().strip_prefix('第') else {
|
||||
return AGENT_RUNTIME_USER_INPUT_MAX_HEADER_CHARS;
|
||||
};
|
||||
let rest = rest.trim_start();
|
||||
let digits = rest
|
||||
.chars()
|
||||
.take_while(char::is_ascii_digit)
|
||||
.collect::<String>();
|
||||
if digits.is_empty() || !rest[digits.len()..].trim_start().starts_with('轮') {
|
||||
return AGENT_RUNTIME_USER_INPUT_MAX_HEADER_CHARS;
|
||||
}
|
||||
AGENT_RUNTIME_USER_INPUT_MAX_PLAN_HEADER_CHARS
|
||||
}
|
||||
|
||||
/// 一份 schema 合法的澄清问询在线上最多可能有多长(字符)。
|
||||
///
|
||||
/// 存在的意义是给中转通道一个由 schema 推导的上限,而不是让它自己拍一个数。
|
||||
@@ -60,8 +92,10 @@ pub(crate) const AGENT_RUNTIME_USER_INPUT_MAX_WIRE_CHARS: usize = {
|
||||
let per_option = AGENT_RUNTIME_USER_INPUT_MAX_OPTION_LABEL_CHARS
|
||||
+ AGENT_RUNTIME_USER_INPUT_MAX_OPTION_DESCRIPTION_CHARS
|
||||
+ OPTION_SYNTAX_CHARS;
|
||||
// 取两种 header 里宽的那个:通道窄于 schema 的后果是一封完全合法的策划信封在父 run
|
||||
// 认领回执时被拒、整条委派链阻断,正是这个常量当初要防的那件事。
|
||||
let per_question = AGENT_RUNTIME_USER_INPUT_MAX_ID_CHARS
|
||||
+ AGENT_RUNTIME_USER_INPUT_MAX_HEADER_CHARS
|
||||
+ AGENT_RUNTIME_USER_INPUT_MAX_PLAN_HEADER_CHARS
|
||||
+ AGENT_RUNTIME_USER_INPUT_MAX_QUESTION_CHARS
|
||||
+ AGENT_RUNTIME_USER_INPUT_MAX_OPTIONS * per_option
|
||||
+ QUESTION_SYNTAX_CHARS;
|
||||
@@ -314,7 +348,7 @@ fn normalize_user_input_questions(
|
||||
}
|
||||
let header = normalize_single_line_user_input_text(
|
||||
&question.header,
|
||||
AGENT_RUNTIME_USER_INPUT_MAX_HEADER_CHARS,
|
||||
plan_clarification_header_limit(&question.header),
|
||||
&format!("user.input_request question {} header", question_index + 1),
|
||||
)?;
|
||||
let question_text = normalize_single_line_user_input_text(
|
||||
@@ -1347,6 +1381,27 @@ mod tests {
|
||||
}]
|
||||
}
|
||||
|
||||
/// 放宽 header 上限只对策划澄清卡那一种形状生效,且只放宽、不收紧。
|
||||
///
|
||||
/// 两个方向都得钉:同一条 31 字的 header,带 `第N轮·` 前缀要过(策划卡装的是决定
|
||||
/// 主题本身),不带就必须照旧被 12 字挡下——否则这次改动就顺手把做游戏 / 做素材
|
||||
/// 的通用问询也放宽了,而那两条泳道本轮不该有任何行为变化。
|
||||
#[test]
|
||||
fn only_the_plan_clarification_header_shape_gets_the_wider_limit() {
|
||||
let long_topic = "当前要决定:一局里玩家靠什么目标获得满足";
|
||||
assert!(long_topic.chars().count() > AGENT_RUNTIME_USER_INPUT_MAX_HEADER_CHARS);
|
||||
|
||||
let mut plan_header = valid_questions();
|
||||
plan_header[0].header = format!("第1轮·{long_topic}");
|
||||
assert!(normalize_user_input_questions(plan_header).is_ok());
|
||||
|
||||
let mut generic_header = valid_questions();
|
||||
generic_header[0].header = long_topic.to_string();
|
||||
assert!(normalize_user_input_questions(generic_header)
|
||||
.expect_err("通用 header 不得因为策划分支被放宽")
|
||||
.contains(&AGENT_RUNTIME_USER_INPUT_MAX_HEADER_CHARS.to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_input_questions_require_unique_snake_case_ids_and_two_options() {
|
||||
assert!(normalize_user_input_questions(valid_questions()).is_ok());
|
||||
@@ -1475,8 +1530,8 @@ mod tests {
|
||||
};
|
||||
let question = AgentRuntimeUserInputQuestion {
|
||||
id: "route_choice".to_string(),
|
||||
header: "第1轮·关键决定".to_string(),
|
||||
question: "当前要决定:首版路线。".to_string(),
|
||||
header: "第1轮·当前要决定:首版路线".to_string(),
|
||||
question: "它决定第一批关卡按什么规则组合。".to_string(),
|
||||
options: vec![
|
||||
AgentRuntimeUserInputOption {
|
||||
label: "接受推荐".to_string(),
|
||||
|
||||
@@ -20,7 +20,7 @@ function clarificationRequest(): AgentRuntimeUserInputRequest {
|
||||
questions: [
|
||||
{
|
||||
id: 'q1',
|
||||
header: '第1轮·关键决定',
|
||||
header: '第1轮·当前要决定:首版路线',
|
||||
question: '这局游戏的重玩动力是什么?',
|
||||
options: [
|
||||
{ label: '分数驱动', description: '刷新纪录后重开' },
|
||||
@@ -46,7 +46,9 @@ describe('AgentRuntimeUserInputCard 澄清输入', () => {
|
||||
</StrictMode>,
|
||||
);
|
||||
|
||||
const textarea = screen.getByLabelText('第1轮·关键决定 其他回答');
|
||||
const textarea = screen.getByLabelText(
|
||||
'第1轮·当前要决定:首版路线 其他回答',
|
||||
);
|
||||
fireEvent.change(textarea, { target: { value: '玩家自己写的答案' } });
|
||||
|
||||
expect((textarea as HTMLTextAreaElement).value).toBe('玩家自己写的答案');
|
||||
@@ -64,7 +66,7 @@ describe('AgentRuntimeUserInputCard 澄清输入', () => {
|
||||
|
||||
fireEvent.click(screen.getByText('分数驱动'));
|
||||
const textarea = screen.getByLabelText(
|
||||
'第1轮·关键决定 其他回答',
|
||||
'第1轮·当前要决定:首版路线 其他回答',
|
||||
) as HTMLTextAreaElement;
|
||||
expect(textarea.value).toBe('分数驱动');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user