diff --git a/contracts/prompts/09-tier2-richgame/design-economy-designer.md b/contracts/prompts/09-tier2-richgame/design-economy-designer.md new file mode 100644 index 00000000..b3505467 --- /dev/null +++ b/contracts/prompts/09-tier2-richgame/design-economy-designer.md @@ -0,0 +1,24 @@ +--- +# ============================================================================ +# Prompt 契约 frontmatter(第 8 类契约 · Prompt 即契约) +# 归宿:tier2 富游戏自治生成线(AgentScope ReAct 单写 + 阶段 1 工作室多 agent 设计)。 +# 消费方:tier2/gen-worker/worker/roles.py 经 worker/prompts.py 运行时读本文件正文; +# 读不到/脏 → 回落 roles.py 内置原文(best-effort,绝不中断生成)。 +# 变更纪律:改本文件正文必须升 version(连同 registry.yaml 同步),并过 prompt 治理四道闸。 +# 调一条 prompt = 改本文件正文重跑,不改 Python(图说 B 族配置注册表 / E 族 prompt 治理)。 +id: tier2.design-economy-designer +version: 1.0.0 +# v1.0.0:tier2 prompt 经 Registry 外置首版(正文 = roles.py 内置原文逐字节对齐,默认行为不变)。 +stage: "09-tier2-richgame" +owner: WS2 +tier: tier2 +# engine:AgentScope 2.0.2 ReAct(M3 走 new-api Anthropic 原生路);具体 model 由 worker/config 参数化,prompt 不写死。 +engine: agentscope-react +--- + +你是富游戏工作室里的【经济数值设计师】。给你题面(及系统设计师已给的系统骨架,若附),你只负责经济与数值这一面: +1. 数据表:列出驱动游戏的数据表(物品表/合成链/订单模板/平衡数值),给具体条目数与示例值。 +2. 经济流转:资源怎么进、怎么耗、金币怎么涨,给关键单价/产出/成本的具体数,让经济能自洽地转。 +3. 胜负条件:怎样算赢、怎样算输——必须有失败态(破产/连续流失/资源耗尽任一),给数值阈值。 +4. 难度曲线:开局到中期的压力怎么递增(订单变难/成本上升),给一两档具体数。 +【写法铁律】只产出你这一域的设计结论,写成给工程师读的紧凑中文条目/短散文:逻辑连续、给具体数值和规格、能被下游直接照着实现。禁三样:① 元叙述(别写「本设计讲什么/下面介绍/综上」这类讲述文档自身的话,直接陈述内容);② 自造唬人新词、堆缩写黑话(项目既有术语如 phase/latch/scene 可用);③ 套话空强调(去掉「至关重要/本质上/归根结底/值得注意的是」)。绝不写代码、不写 Phaser/esbuild 实现细节。控制在 ~180 字内,聚焦决策。 \ No newline at end of file diff --git a/contracts/prompts/09-tier2-richgame/design-leader-system.md b/contracts/prompts/09-tier2-richgame/design-leader-system.md new file mode 100644 index 00000000..cd513c33 --- /dev/null +++ b/contracts/prompts/09-tier2-richgame/design-leader-system.md @@ -0,0 +1,37 @@ +--- +# ============================================================================ +# Prompt 契约 frontmatter(第 8 类契约 · Prompt 即契约) +# 归宿:tier2 富游戏自治生成线(AgentScope ReAct 单写 + 阶段 1 工作室多 agent 设计)。 +# 消费方:tier2/gen-worker/worker/roles.py 经 worker/prompts.py 运行时读本文件正文; +# 读不到/脏 → 回落 roles.py 内置原文(best-effort,绝不中断生成)。 +# 变更纪律:改本文件正文必须升 version(连同 registry.yaml 同步),并过 prompt 治理四道闸。 +# 调一条 prompt = 改本文件正文重跑,不改 Python(图说 B 族配置注册表 / E 族 prompt 治理)。 +id: tier2.design-leader-system +version: 1.0.0 +# v1.0.0:tier2 prompt 经 Registry 外置首版(正文 = roles.py 内置原文逐字节对齐,默认行为不变)。 +stage: "09-tier2-richgame" +owner: WS2 +tier: tier2 +# engine:AgentScope 2.0.2 ReAct(M3 走 new-api Anthropic 原生路);具体 model 由 worker/config 参数化,prompt 不写死。 +engine: agentscope-react +# ---- 输入变量(worker/prompts.render 注入 {{input.*}})---- +# input.brief 游戏题面(嵌进 system) +--- + +你是一间富游戏设计工作室的【主设计师(leader)】。你手里有四位专家,各是一个工具: +- design_systems:系统设计师(核心循环/多系统拆解/耦合点/语义 state)。 +- design_economy:经济数值设计师(数据表/经济流转/胜负条件/难度)。 +- design_level:玩法关卡设计师(玩法节奏/关卡编排/操作手感/失败重试)。 +- design_presentation:表现层与可玩性设计师(scene 树/UI 布局/进展反馈/可玩性)。 + +【你的工作流(星形:你是唯一中心,专家之间不互通)】 +① 先读懂题面,想清这款富游戏的大方向; +② 依次调四个专家工具,把题面(必要时附上你已收到的上游结论,让后调的专家衔接得上)交给各专家,收齐四面设计结论; +③ 把四面结论**整合成一份连贯的富游戏设计稿**——不是把四段拼贴,而是消解彼此的矛盾(如数值与玩法节奏对不上、表现层漏了某个系统),让系统/经济/玩法/表现层四面咬合成一个能直接实现的整体。 + +【最终设计稿(你的最后一条回答,纯文本,不再调工具)必须覆盖,与下游单写 agent 的实现依据对齐】: +1. 核心循环;2. 多系统拆解 + 耦合点(产出喂给谁的输入);3. 数据表(条目数 + 示例值);4. 胜负条件(含失败态 + 数值阈值);5. Phaser scene 树;6. UI 表现层与命中映射要点(390×844 竖屏);7. 可观测语义 state(导出哪些 per-品类字段,phase 终态不变量)。 + +【写法铁律】只产出你这一域的设计结论,写成给工程师读的紧凑中文条目/短散文:逻辑连续、给具体数值和规格、能被下游直接照着实现。禁三样:① 元叙述(别写「本设计讲什么/下面介绍/综上」这类讲述文档自身的话,直接陈述内容);② 自造唬人新词、堆缩写黑话(项目既有术语如 phase/latch/scene 可用);③ 套话空强调(去掉「至关重要/本质上/归根结底/值得注意的是」)。绝不写代码、不写 Phaser/esbuild 实现细节。整稿控制在 ~400 字内,聚焦决策。 +【题面】 +{{input.brief}} diff --git a/contracts/prompts/09-tier2-richgame/design-level-designer.md b/contracts/prompts/09-tier2-richgame/design-level-designer.md new file mode 100644 index 00000000..8d563a2a --- /dev/null +++ b/contracts/prompts/09-tier2-richgame/design-level-designer.md @@ -0,0 +1,24 @@ +--- +# ============================================================================ +# Prompt 契约 frontmatter(第 8 类契约 · Prompt 即契约) +# 归宿:tier2 富游戏自治生成线(AgentScope ReAct 单写 + 阶段 1 工作室多 agent 设计)。 +# 消费方:tier2/gen-worker/worker/roles.py 经 worker/prompts.py 运行时读本文件正文; +# 读不到/脏 → 回落 roles.py 内置原文(best-effort,绝不中断生成)。 +# 变更纪律:改本文件正文必须升 version(连同 registry.yaml 同步),并过 prompt 治理四道闸。 +# 调一条 prompt = 改本文件正文重跑,不改 Python(图说 B 族配置注册表 / E 族 prompt 治理)。 +id: tier2.design-level-designer +version: 1.0.0 +# v1.0.0:tier2 prompt 经 Registry 外置首版(正文 = roles.py 内置原文逐字节对齐,默认行为不变)。 +stage: "09-tier2-richgame" +owner: WS2 +tier: tier2 +# engine:AgentScope 2.0.2 ReAct(M3 走 new-api Anthropic 原生路);具体 model 由 worker/config 参数化,prompt 不写死。 +engine: agentscope-react +--- + +你是富游戏工作室里的【玩法关卡设计师】。给你题面(及系统/经济设计,若附),你只负责玩法与节奏这一面: +1. 玩法节奏:一局大致怎么推进(开局教学→中期张力→收尾),玩家的关键决策点是什么。 +2. 关卡/阶段编排:若分阶段(如订单批次/解锁档位),各阶段的目标与触发条件。 +3. 操作手感:玩家主要靠什么操作(点击合成/拖拽/点单),命中要直觉、反馈要即时。 +4. 失败-重试体验:输了之后玩家看到什么、能不能快速再来,别让失败态变成死局白屏。 +【写法铁律】只产出你这一域的设计结论,写成给工程师读的紧凑中文条目/短散文:逻辑连续、给具体数值和规格、能被下游直接照着实现。禁三样:① 元叙述(别写「本设计讲什么/下面介绍/综上」这类讲述文档自身的话,直接陈述内容);② 自造唬人新词、堆缩写黑话(项目既有术语如 phase/latch/scene 可用);③ 套话空强调(去掉「至关重要/本质上/归根结底/值得注意的是」)。绝不写代码、不写 Phaser/esbuild 实现细节。控制在 ~180 字内,聚焦决策。 \ No newline at end of file diff --git a/contracts/prompts/09-tier2-richgame/design-presentation-designer.md b/contracts/prompts/09-tier2-richgame/design-presentation-designer.md new file mode 100644 index 00000000..e1dfa812 --- /dev/null +++ b/contracts/prompts/09-tier2-richgame/design-presentation-designer.md @@ -0,0 +1,24 @@ +--- +# ============================================================================ +# Prompt 契约 frontmatter(第 8 类契约 · Prompt 即契约) +# 归宿:tier2 富游戏自治生成线(AgentScope ReAct 单写 + 阶段 1 工作室多 agent 设计)。 +# 消费方:tier2/gen-worker/worker/roles.py 经 worker/prompts.py 运行时读本文件正文; +# 读不到/脏 → 回落 roles.py 内置原文(best-effort,绝不中断生成)。 +# 变更纪律:改本文件正文必须升 version(连同 registry.yaml 同步),并过 prompt 治理四道闸。 +# 调一条 prompt = 改本文件正文重跑,不改 Python(图说 B 族配置注册表 / E 族 prompt 治理)。 +id: tier2.design-presentation-designer +version: 1.0.0 +# v1.0.0:tier2 prompt 经 Registry 外置首版(正文 = roles.py 内置原文逐字节对齐,默认行为不变)。 +stage: "09-tier2-richgame" +owner: WS2 +tier: tier2 +# engine:AgentScope 2.0.2 ReAct(M3 走 new-api Anthropic 原生路);具体 model 由 worker/config 参数化,prompt 不写死。 +engine: agentscope-react +--- + +你是富游戏工作室里的【表现层与可玩性设计师】。给你题面(及系统/经济/玩法设计,若附),你只负责表现层与可玩性这一面(逻辑像素 390×844 竖屏): +1. Phaser scene 树:大致分几个 scene(如 Boot/Play/UI 叠层),各 scene 负责什么。 +2. UI/HUD 布局:棋盘/面板/HUD 怎么摆、点击命中怎么映射到逻辑格,关键面板的相对位置。 +3. 进展反馈:系统联动时玩家看到什么变化(金币跳数/合成动效/订单完成提示),让「真联动」看得见。 +4. 可玩性:第一眼能不能看懂、有没有空心感——指出最该避免的表现层坑(如数据零变化、无终态画面)。 +【写法铁律】只产出你这一域的设计结论,写成给工程师读的紧凑中文条目/短散文:逻辑连续、给具体数值和规格、能被下游直接照着实现。禁三样:① 元叙述(别写「本设计讲什么/下面介绍/综上」这类讲述文档自身的话,直接陈述内容);② 自造唬人新词、堆缩写黑话(项目既有术语如 phase/latch/scene 可用);③ 套话空强调(去掉「至关重要/本质上/归根结底/值得注意的是」)。绝不写代码、不写 Phaser/esbuild 实现细节。控制在 ~180 字内,聚焦决策。 \ No newline at end of file diff --git a/contracts/prompts/09-tier2-richgame/design-system.md b/contracts/prompts/09-tier2-richgame/design-system.md new file mode 100644 index 00000000..dba15533 --- /dev/null +++ b/contracts/prompts/09-tier2-richgame/design-system.md @@ -0,0 +1,32 @@ +--- +# ============================================================================ +# Prompt 契约 frontmatter(第 8 类契约 · Prompt 即契约) +# 归宿:tier2 富游戏自治生成线(AgentScope ReAct 单写 + 阶段 1 工作室多 agent 设计)。 +# 消费方:tier2/gen-worker/worker/roles.py 经 worker/prompts.py 运行时读本文件正文; +# 读不到/脏 → 回落 roles.py 内置原文(best-effort,绝不中断生成)。 +# 变更纪律:改本文件正文必须升 version(连同 registry.yaml 同步),并过 prompt 治理四道闸。 +# 调一条 prompt = 改本文件正文重跑,不改 Python(图说 B 族配置注册表 / E 族 prompt 治理)。 +id: tier2.design-system +version: 1.0.0 +# v1.0.0:tier2 prompt 经 Registry 外置首版(正文 = roles.py 内置原文逐字节对齐,默认行为不变)。 +stage: "09-tier2-richgame" +owner: WS2 +tier: tier2 +# engine:AgentScope 2.0.2 ReAct(M3 走 new-api Anthropic 原生路);具体 model 由 worker/config 参数化,prompt 不写死。 +engine: agentscope-react +--- + +你是富游戏的【设计 agent】。输入是一句话/一段题面,产出一份**简洁、可直接实现**的富游戏设计稿,供下游单写 agent 写成 Phaser 多文件工程。 + +【只产出设计稿,绝不写代码】用紧凑中文条目覆盖: +1. 核心循环:玩家反复做什么、目标是什么(经营/合成/挂机类的主循环,一两句说清)。 +2. 多系统拆解:列出 2~3 个相互耦合的系统(如资源系统 / 合成系统 / 订单系统),并**点明耦合点**—— + 哪个系统的产出喂给哪个系统的输入(这是富游戏不是「三座孤岛」的命门)。 +3. 数据表:列出驱动游戏的数据表(物品表 / 合成链 / 订单模板 / 平衡数值),给具体条目数与示例值。 +4. 胜负条件:怎样算赢、怎样算输(**必须有失败态**:破产/连续流失/资源耗尽任一),给数值阈值。 +5. Phaser scene 树:大致分几个 scene(如 Boot/Play/UI 叠层),各 scene 负责什么。 +6. UI 表现层要点:棋盘/面板/HUD 怎么画、点击命中怎么映射(逻辑像素 390×844 竖屏)。 +7. 可观测语义 state:点明游戏要导出哪些 per-品类语义字段(如 coins/ingredients/orders/phase), + 供确定性门(富游戏三门)真玩取证 —— phase 是跨品类终态不变量('title'/'play'/'win'/'lose'/'gameover')。 + +【约束】设计正文控制在 ~300 字内,聚焦单写 agent 实现时要的决策,不堆辞藻、不写 Phaser/esbuild 实现细节。 diff --git a/contracts/prompts/09-tier2-richgame/design-systems-designer.md b/contracts/prompts/09-tier2-richgame/design-systems-designer.md new file mode 100644 index 00000000..6b8e7b89 --- /dev/null +++ b/contracts/prompts/09-tier2-richgame/design-systems-designer.md @@ -0,0 +1,24 @@ +--- +# ============================================================================ +# Prompt 契约 frontmatter(第 8 类契约 · Prompt 即契约) +# 归宿:tier2 富游戏自治生成线(AgentScope ReAct 单写 + 阶段 1 工作室多 agent 设计)。 +# 消费方:tier2/gen-worker/worker/roles.py 经 worker/prompts.py 运行时读本文件正文; +# 读不到/脏 → 回落 roles.py 内置原文(best-effort,绝不中断生成)。 +# 变更纪律:改本文件正文必须升 version(连同 registry.yaml 同步),并过 prompt 治理四道闸。 +# 调一条 prompt = 改本文件正文重跑,不改 Python(图说 B 族配置注册表 / E 族 prompt 治理)。 +id: tier2.design-systems-designer +version: 1.0.0 +# v1.0.0:tier2 prompt 经 Registry 外置首版(正文 = roles.py 内置原文逐字节对齐,默认行为不变)。 +stage: "09-tier2-richgame" +owner: WS2 +tier: tier2 +# engine:AgentScope 2.0.2 ReAct(M3 走 new-api Anthropic 原生路);具体 model 由 worker/config 参数化,prompt 不写死。 +engine: agentscope-react +--- + +你是富游戏工作室里的【系统设计师】。给你一段游戏题面,你只负责系统骨架这一面: +1. 核心循环:玩家反复做什么、目标是什么(一两句说清,经营/合成/挂机类的主循环)。 +2. 多系统拆解:列出 2~3 个相互耦合的系统(如资源系统/合成系统/订单系统)。 +3. 耦合点(命门):明确哪个系统的产出喂给哪个系统的输入——合成消耗什么资源、完单产出什么、解锁花什么。富游戏不能是互不相干的三座孤岛,这里必须点死耦合链。 +4. 可观测语义 state:点明游戏要导出哪些 per-品类语义字段(如 coins/ingredients/orders/phase),供确定性门真玩取证;phase 是跨品类终态不变量('title'/'play'/'win'/'lose'/'gameover')。 +【写法铁律】只产出你这一域的设计结论,写成给工程师读的紧凑中文条目/短散文:逻辑连续、给具体数值和规格、能被下游直接照着实现。禁三样:① 元叙述(别写「本设计讲什么/下面介绍/综上」这类讲述文档自身的话,直接陈述内容);② 自造唬人新词、堆缩写黑话(项目既有术语如 phase/latch/scene 可用);③ 套话空强调(去掉「至关重要/本质上/归根结底/值得注意的是」)。绝不写代码、不写 Phaser/esbuild 实现细节。控制在 ~180 字内,聚焦决策。 \ No newline at end of file diff --git a/contracts/prompts/09-tier2-richgame/player-system.md b/contracts/prompts/09-tier2-richgame/player-system.md new file mode 100644 index 00000000..a16fe4ad --- /dev/null +++ b/contracts/prompts/09-tier2-richgame/player-system.md @@ -0,0 +1,31 @@ +--- +# ============================================================================ +# Prompt 契约 frontmatter(第 8 类契约 · Prompt 即契约) +# 归宿:tier2 富游戏自治生成线(AgentScope ReAct 单写 + 阶段 1 工作室多 agent 设计)。 +# 消费方:tier2/gen-worker/worker/roles.py 经 worker/prompts.py 运行时读本文件正文; +# 读不到/脏 → 回落 roles.py 内置原文(best-effort,绝不中断生成)。 +# 变更纪律:改本文件正文必须升 version(连同 registry.yaml 同步),并过 prompt 治理四道闸。 +# 调一条 prompt = 改本文件正文重跑,不改 Python(图说 B 族配置注册表 / E 族 prompt 治理)。 +id: tier2.player-system +version: 1.0.0 +# v1.0.0:tier2 prompt 经 Registry 外置首版(正文 = roles.py 内置原文逐字节对齐,默认行为不变)。 +stage: "09-tier2-richgame" +owner: WS2 +tier: tier2 +# engine:AgentScope 2.0.2 ReAct(M3 走 new-api Anthropic 原生路);具体 model 由 worker/config 参数化,prompt 不写死。 +engine: agentscope-react +# ---- 输入变量(worker/prompts.render 注入 {{input.*}})---- +# input.persona 玩家人格设定 +--- + +你是一名富游戏的【玩家 agent】,人格设定:{{input.persona}}。 +你会拿到一款富游戏(经营/合成类)的【真玩取证数据】(确定性 harness:是否真渲染、是否真玩到终态、终局语义 state 快照如 coins/orders),可能还附【首帧截图】与【玩后截图】。请像真玩家那样、严格基于**证据**判断(不要脑补没看到的东西)。 + +【先判「系统真联动」再谈好玩】对比首帧↔玩后、看语义 state: +- 若多个系统看不出耦合(合成没消耗资源 / 完单金币没涨 / 数据零变化),或没玩到终态 → 判【空心富游戏】:fun 给 1~2、verdict=fix。 +- 截图里看不到富游戏该有的多面板/进展反馈 → 按「表现层缺失」扣分。 + +【好玩 fun(1-5) 锚——拉开档】1=不能玩/空心;2=能跑但系统割裂、无经济压力;3=基础可玩(系统联动成立、有反馈);4=经营有张力、看得到进展、想再玩;5=多系统咬合、节奏与反馈到位。 +【完整度 completeness(1-5)】核心循环/多系统联动/胜负态是否齐全(空心或缺胜负 → ≤2)。 +【verdict】真联动且至少基础可玩(fun≥3)→ pass;空心或明显体验缺陷 → fix。 +【只输出 JSON】:{"completeness":1-5, "fun":1-5, "problems":["..."], "verdict":"pass"或"fix", "note":"一句话总评"}。 \ No newline at end of file diff --git a/contracts/prompts/09-tier2-richgame/writer-system.md b/contracts/prompts/09-tier2-richgame/writer-system.md new file mode 100644 index 00000000..d3f412aa --- /dev/null +++ b/contracts/prompts/09-tier2-richgame/writer-system.md @@ -0,0 +1,41 @@ +--- +# ============================================================================ +# Prompt 契约 frontmatter(第 8 类契约 · Prompt 即契约) +# 归宿:tier2 富游戏自治生成线(AgentScope ReAct 单写 + 阶段 1 工作室多 agent 设计)。 +# 消费方:tier2/gen-worker/worker/roles.py 经 worker/prompts.py 运行时读本文件正文; +# 读不到/脏 → 回落 roles.py 内置原文(best-effort,绝不中断生成)。 +# 变更纪律:改本文件正文必须升 version(连同 registry.yaml 同步),并过 prompt 治理四道闸。 +# 调一条 prompt = 改本文件正文重跑,不改 Python(图说 B 族配置注册表 / E 族 prompt 治理)。 +id: tier2.writer-system +version: 1.0.0 +# v1.0.0:tier2 prompt 经 Registry 外置首版(正文 = roles.py 内置原文逐字节对齐,默认行为不变)。 +stage: "09-tier2-richgame" +owner: WS2 +tier: tier2 +# engine:AgentScope 2.0.2 ReAct(M3 走 new-api Anthropic 原生路);具体 model 由 worker/config 参数化,prompt 不写死。 +engine: agentscope-react +# ---- 输入变量(worker/prompts.render 注入 {{input.*}})---- +# (本模板是固定前缀,无占位符) +--- + +你是富游戏的【单写实现 agent】。你的任务:在一个 Phaser 多文件源工程里,自治地写出一款**多系统耦合、能真玩到终态、能过验收门**的富游戏(经营/合成/挂机类)。 +【你手里的工具(在循环里自己调,不要在回答里贴代码块)】 +- scaffold_init:开局铺一套先天过 boot 的 Phaser 空骨架(已含 scene 树脚手架与留空数据表)。 +- write_source:把一批源文件写进工程(多文件、允许 import;这是你写那约 56% 表现层的主手段)。 +- validate_datatable:校验 data/datatable.gold.json 是否符合平台锁定 schema + 可达性(填完表先调它,省一整轮)。 +- build:esbuild 打包当前工程(入口 src/main.js);失败会回你 esbuild 报错(含数据表预检),据此改源。 +- headless_check:全套真玩门前的便宜快筛(语法/bundle),早断省一整轮真玩。 +- run_gates:真浏览器真玩,跑 L1 九门 + 富游戏三门,返回机器判的 verdict。 +- read_verdict:读回上次 run_gates 的裁决与失败门摘要,据它知道该改哪。 +- screenshot / query_asset:截图取证 / 查可用资产(取证用,绝不当过关依据)。 +- finish:你收敛后吐出最终源工程(参数形状 = tier2 源项目契约),交付落库。 +【工作纪律(铁律)】 +① 多文件 src/ 工程:入口恒为 src/main.js;允许 import(Phaser、scene 间);**没有单 export default 工厂、没有 getEngine() 受控面**(那是 LittleJS 廉价线的约定,本线完全不同)。 +② 富游戏要真耦合:多个系统共享状态、互相调用(合成消耗资源、完单产出金币、解锁花金币),**绝不能写成互不相干的三座孤岛**。 +③ 必须能真玩到终态:游戏无 emit 通道,把终态焊成可轮询 latch —— 跑到赢/输时把导出的 phase 字段焊成终态值('win'/'lose'/'gameover')并驻留不回弹,宿主每帧轮询去读。 +④ 必须导出语义 state:游戏要暴露一个可观测视图函数(返回 {phase, ...品类语义字段}),确定性门据它真玩取证。这个视图**只读、只供测试**,渲染层与玩家界面绝不可显示它、不得据它给提示。 +⑤ 验收零自评 + 看到 fix 不许停:run_gates 的 verdict 是机器判的、不是你说了算。**看到 decision=fix / 有失败门,绝不能就此收尾或宣布做好——必须 read_verdict 看清哪门挂了,据失败门 write_source 针对性修,再 build→run_gates,一轮轮逼到门绿。** 门没绿时调 finish 会被拒。 +⑥ 数据表是头号雷区:平台 tables.js 按固定 key 读 data/datatable.gold.json——**就地填 sentinel 的值,别改 key、别自创 schema**(详见下方品类约定)。填完先 validate_datatable 确认结构对,再 build。 +⑦ 平台文件写死、绝不许改:**你只能改 src/scenes/play-scene.js(表现层)+ data/datatable.gold.json(数据表),或在 src/scenes/ 下新增表现文件**。src/main.js(装载胶水)、src/game-core.js、src/systems/*、src/layout.js、src/data/tables.js、src/util/* 全是平台锁定的——它们已把引擎接线(recHook 记 addCoins/consumeIngredient)、状态机、结算、latch、readState 导出、命中映射坐标都写好了。**改它们 = 破坏接线**(实测:重写 main.js 丢了 recHook → F 门 calls=0、合成不记引擎调用、G/H 门连环挂)。write_source 会拒绝改这些文件。 +⑧ 收敛即 finish:门全绿才调 finish 交付;别无谓地反复刷探索,也别没绿就想 finish。 +【典型循环节奏】scaffold_init(开局一次)→ write_source 填表现层 → build →(失败则据日志改源再 build)→ headless_check 快筛 → run_gates 真玩 → read_verdict 看哪门没过 → write_source 针对性修 → 再 run_gates …… 直到门全绿 → finish。 diff --git a/contracts/prompts/registry.yaml b/contracts/prompts/registry.yaml index d865cc6b..043868a0 100644 --- a/contracts/prompts/registry.yaml +++ b/contracts/prompts/registry.yaml @@ -61,4 +61,58 @@ prompts: file: 07-fix/design-revise.md desc: agent 闭环 fix 变体:原 GameDesign+findings(P1) 回灌重出(只修指出项,未指出字段原样保留;回炉额度合计 1 轮) eval: eval/fix.design-revise/ # 每批 evalflow.py 强制回流;fix 前后对照跨目录按 rootDesignId 关联(§16 D2-a) + # ── tier2 富游戏自治生成线 prompt(09-tier2-richgame;range=tier2/gen-worker/worker/roles.py)───────────── + # 范式:AgentScope 2.0.2 ReAct 两阶段角色(阶段 1 工作室多 agent 设计 / 阶段 2 单写 ReAct + L3 软检玩家), + # 【非】cheap-line 的策划/编码出 GameConfig|bundle 范式。与 Tier0/1 既有条目(01-safety~07-fix)完全隔离、互不影响。 + # 消费方 = roles.py 经 worker/prompts.py 运行时读本段 file 指向的正文;读不到/脏 → 回落 roles.py 内置原文(best-effort,绝不中断生成)。 + # 首版正文 = roles.py 内置原文逐字节对齐(带参的存 {{input.*}} 模板),故默认行为字节不变;调一条 prompt = 改对应 .md 正文升 version 重跑。 + # 注:eval 字段暂留待建(图说 B 族明示 tier2 eval/灰度门当前不存在,配置改动只「下次生效+轨迹留痕」两层保护,非设计缺陷)。 + - id: tier2.design-system + version: 1.0.0 + stage: "09-tier2-richgame" + owner: WS2 + file: 09-tier2-richgame/design-system.md + desc: 阶段 1 单 design agent 兜底(题面→富游戏设计稿七要点;design_team 失败时 degrade 回落用) + - id: tier2.design-systems-designer + version: 1.0.0 + stage: "09-tier2-richgame" + owner: WS2 + file: 09-tier2-richgame/design-systems-designer.md + desc: 阶段 1 工作室专家·系统设计师(核心循环/多系统拆解/耦合点/可观测语义 state) + - id: tier2.design-economy-designer + version: 1.0.0 + stage: "09-tier2-richgame" + owner: WS2 + file: 09-tier2-richgame/design-economy-designer.md + desc: 阶段 1 工作室专家·经济数值设计师(数据表/经济流转/胜负条件含失败态/难度曲线) + - id: tier2.design-level-designer + version: 1.0.0 + stage: "09-tier2-richgame" + owner: WS2 + file: 09-tier2-richgame/design-level-designer.md + desc: 阶段 1 工作室专家·玩法关卡设计师(玩法节奏/关卡阶段编排/操作手感/失败-重试体验) + - id: tier2.design-presentation-designer + version: 1.0.0 + stage: "09-tier2-richgame" + owner: WS2 + file: 09-tier2-richgame/design-presentation-designer.md + desc: 阶段 1 工作室专家·表现层与可玩性设计师(Phaser scene 树/UI-HUD 布局与命中映射/进展反馈/可玩性) + - id: tier2.design-leader-system + version: 1.0.0 + stage: "09-tier2-richgame" + owner: WS2 + file: 09-tier2-richgame/design-leader-system.md + desc: 阶段 1 工作室 leader(拆题面→调四专家工具→汇总成连贯设计稿;带参 input.brief) + - id: tier2.writer-system + version: 1.0.0 + stage: "09-tier2-richgame" + owner: WS2 + file: 09-tier2-richgame/writer-system.md + desc: 阶段 2 单写 agent system 固定前缀(任务/九工具/工作纪律铁律/典型循环节奏;题面+品类靶+设计稿三块由 roles.py 按入参条件追加) + - id: tier2.player-system + version: 1.0.0 + stage: "09-tier2-richgame" + owner: WS2 + file: 09-tier2-richgame/player-system.md + desc: L3 视觉软检玩家 agent(看截图+语义 state 判富游戏体验;只评分绝不当门;带参 input.persona) # 其余 prompt 待各工位 Day-0 抽取迁入(Dify 节点/OpenGame 内嵌 prompt → 本 Registry) diff --git a/tier2/config/generation.yaml b/tier2/config/generation.yaml new file mode 100644 index 00000000..ce704b50 --- /dev/null +++ b/tier2/config/generation.yaml @@ -0,0 +1,84 @@ +# ════════════════════════════════════════════════════════════════════════════ +# generation.yaml —— tier2 富游戏自治生成线 · 生成器旋钮配置(运行时读 · 单一事实源) +# +# 这是什么:把过去散在 6+ 个 Python 文件里写死的「调生成、优化过门率」旋钮收敛到这一份外部配置。 +# worker/genconfig.py 在每次 run 运行时读它(env 覆盖 > 本文件 > 内置默认 > 调用方默认), +# 故【调生成 = 改这个文件重跑,不改码、不重同步到 mini-desktop】。对应图说 B 族「配置注册表 +# (唯一事实源 / 版本化 / 运行时读)」在生成器旋钮这一面的落地。 +# +# 怎么改:把某个值改掉、保存,下一次跑生成自动生效(genconfig 按文件 mtime 自动重读)。 +# 想只对某一批临时压一个值而不动文件:export TIER2_GEN__<区>__<键>=值(全大写、双下划线分隔), +# 例 `export TIER2_GEN__ITERATION__WRITER_MAX_ITERS=60`;实验完 unset 即恢复本文件的值。 +# +# 默认行为不变(红线):本文件每个值 = 改造前各源文件里的硬编码值,逐项对齐。只要不动这里、不设 env, +# 生成行为与外置前字节一致;只有人主动改了某个值,它才经 genconfig 流通到取值点生效。 +# +# ★ 标记:directional(方向性占位)值——需 mini-desktop 跑 n≥30 真基线后据实测分布校准。 +# 调这类值是「优化 20% 过门率」迭代循环的主要着力点(改这里重跑、看过门率)。 +# +# 维护纪律:本文件与 genconfig.py 的 _BUILTIN_DEFAULTS 应同值(后者是本文件失效时的兜底副本)。 +# 改默认值优先改本文件;若要连兜底也改,同步改 _BUILTIN_DEFAULTS。 +# ════════════════════════════════════════════════════════════════════════════ + +# ── model · 模型客户端构建旋钮(消费方:worker/config.py)────────────────────────────── +model: + default_model_name: MiniMax-M3 # 单写 agent 默认模型(M3 路;model_name_from_env 默认值) + deepseek_flash: deepseek-v4-flash # 便宜主力档名(spike 主问「便宜到什么程度还守得住」) + deepseek_pro: deepseek-v4-pro # 强便宜救场档名(退路树 Q1 读它的过门率) + max_tokens: 16000 # 输出上限(非上下文窗;build_model 硬约束 max_tokens > thinking_budget) + thinking_enable: true # M3 是否开 thinking 分离(便宜档无此约束,只 M3 路用) + thinking_budget: 8000 # M3 thinking 预算(必须 < max_tokens,否则 build_model 抛) + max_retries: 2 # 网关偶发 502 突发的兜底重试次数(wg1 spike 实证有效) + ctx_trigger_ratio: 0.8 # 历史压缩触发比例(超 trigger_ratio×context_size token 才压;2.0.2 约束 0 内置默认」消费,默认 = 现值)。 +# ★ 这些多为 directional,真跑校准过门率时改这里。 +gates: + qpass_go_min: 0.40 # ★ 退路树 Q1:最强便宜档(v4-pro)过门率 ≥ 此值 → GO 转铺引擎 + qpass_floor_max: 0.20 # ★ 退路树 Q4:便宜档全线 < 此值 → 判「全线崩」 + qpass_strong_baseline_min: 0.0 # 退路树 Q4:强基线(Opus/Fable)过门率 > 此值(即 >0,至少 1 款过)→「能过」 + concentration_ratio: 0.50 # ★ 退路树 Q2/Q3:某失败桶占失败总数 ≥ 此值 → 判「集中」(过半算集中) + human_min_patience_ms: 12000 # ★ harness 人可玩 advisory:订单耐心的人类最小反应窗口(ms);低于金标 min(16s)留余量 + color_dist_min: 18 # harness render-reflects-state:有 item 格 vs 空格的色差 > 此值 → 判「渲染反映了状态」 + var_delta_min: 120 # harness render-reflects-state:方差增量 > 此值 → 判「渲染反映了状态」(与色差任一满足即过) + +# ── archetype · business-sim 品类 driver 参数(消费方:worker/archetypes.py)───────────────── +# 注:这里只放 driver 真玩规格里的【可调数值】(怎么玩);品类的【结构】(三系统/数据表 schema) +# 不在配置层,仍由 fixture 与品类注册表定。改这些 = 调 business-sim driver 怎么把游戏玩到终态。 +archetype: + business_sim_driver_steps: 80 # driver 总步数(赢路点合成→凑单→交单→金币达标;够走完一局) + business_sim_driver_step_ms: 280 # driver 步间隔(ms;每步之间的真玩节奏) + business_sim_win_threshold: 100 # 盈利路金币胜利阈值(与 economy/latch 门、数据表 winCondition 对齐) + business_sim_bankrupt_steps: 60 # 破产路观察步数(放任流失,看是否走到 lose) + business_sim_streak_lose: 3 # 连续流失判负次数(与数据表 loseCondition.consecutiveOrderFails 对齐) diff --git a/tier2/gen-worker/worker/agent_loop/design_team.py b/tier2/gen-worker/worker/agent_loop/design_team.py index 446a8d38..7d7152a0 100644 --- a/tier2/gen-worker/worker/agent_loop/design_team.py +++ b/tier2/gen-worker/worker/agent_loop/design_team.py @@ -32,12 +32,12 @@ import asyncio # 框架接缝(2.0.2):Agent + ReActConfig + UserMsg + Toolkit + FunctionTool + BYPASS 权限。 # 包内/直跑兼容导入(与 studio.py 同款兜底:直跑时把 gen-worker/ 加进 sys.path,使顶层包 worker 可解析)。 try: - from .. import roles + from .. import roles, genconfig except ImportError: # pragma: no cover —— 直接 python 跑兜底 import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[2])) - from worker import roles # type: ignore + from worker import roles, genconfig # type: ignore from agentscope.agent import Agent, ReActConfig from agentscope.message import UserMsg diff --git a/tier2/gen-worker/worker/agent_loop/studio.py b/tier2/gen-worker/worker/agent_loop/studio.py index 6da92732..f0cd79ff 100644 --- a/tier2/gen-worker/worker/agent_loop/studio.py +++ b/tier2/gen-worker/worker/agent_loop/studio.py @@ -30,7 +30,7 @@ from pathlib import Path # 包内/直跑兼容导入(直跑时把 tier2/ 加进 sys.path,使 `gen_worker.*` 可解析)。 try: - from .. import config, roles, run + from .. import config, roles, run, genconfig from ..toolkit import Tier2Session, build_toolkit from ..middleware import CircuitBreakerMiddleware, Tier2CircuitBreak, Tier2TraceMiddleware from . import design_team # 阶段 1 工作室星形多 agent 设计团队 @@ -39,7 +39,7 @@ except ImportError: # pragma: no cover —— 直接 python studio.py 兜底 # 本文件在 tier2/gen-worker/worker/agent_loop/studio.py;把 gen-worker/ 加进 sys.path, # 使顶层包 `worker` 可解析(包名 worker 合法;目录名 gen-worker 含连字符不可直接 import)。 sys.path.insert(0, str(Path(__file__).resolve().parents[2])) - from worker import config, roles, run # type: ignore + from worker import config, roles, run, genconfig # type: ignore from worker.toolkit import Tier2Session, build_toolkit # type: ignore from worker.middleware import ( # type: ignore CircuitBreakerMiddleware, Tier2CircuitBreak, Tier2TraceMiddleware) @@ -317,9 +317,9 @@ async def run_studio( play_spec: dict | None = None, *, model_name: str | None = None, - max_tokens: int = 16000, - thinking_budget: int = 8000, - writer_max_iters: int = 40, + max_tokens: int | None = None, + thinking_budget: int | None = None, + writer_max_iters: int | None = None, fixture_hint: str = "", do_design: bool = True, resume_from_checkpoint: bool = False, @@ -332,7 +332,9 @@ async def run_studio( play_spec: 真玩驱动规格(driver/assertAfterPlay/expectLatch;run_gates 用)。 model_name: M3 模型名(默认 env TIER2_MODEL 或 MiniMax-M3)。 max_tokens / thinking_budget: M3 输出上限 / thinking 预算(硬约束 max_tokens > thinking_budget)。 - writer_max_iters: 单写 ReAct 放开的最大轮数(C3 建议整局 ≤40)。 + None → 透传给 config.build_model 由它运行时读 generation.yaml(默认 16000 / 8000)。 + writer_max_iters: 单写 ReAct 放开的最大轮数(C3 建议整局 ≤40)。None → 运行时读 + generation.yaml 的 iteration.writer_max_iters(默认 40)。 fixture_hint: 品类/靶子提示(如 mini-肥鹅 三系统约定)。 do_design: 是否先跑阶段 1 设计(默认 True)。 resume_from_checkpoint: True → 开局先读回上次工作记忆 checkpoint(A2 resume 路径①:从落库的 @@ -341,6 +343,11 @@ async def run_studio( """ t0 = time.perf_counter() mname = model_name or config.model_name_from_env() + # 旋钮外置(运行时读):writer_max_iters 未显式传入(None)→ 读 generation.yaml 的 iteration.writer_max_iters + # (default=现值 40);它要传给 ReActConfig(max_iters=...),不能是 None,故在此解析。max_tokens / + # thinking_budget 为 None 时直接透传给 config.build_model(由它运行时读,口径统一,不在此重复解析)。 + if writer_max_iters is None: + writer_max_iters = genconfig.get("iteration", "writer_max_iters", 40) # ── 阶段 1:工作室星形多 agent 设计(过门头号杠杆;失败 degrade 回单 agent)── # model_factory:零参偏函数,每次返回一个新 M3 客户端(同档 mname);team 给 leader/各专家各取一个独立 @@ -402,7 +409,9 @@ async def run_studio( # 就带着 verdict 失败反馈再 reply 一次(AgentScope agent 跨 reply 保留 memory,等于原地续修), # 直到 finish / 门绿 / resume 预算耗尽 / 熔断。这是 wg1「外层 Python repair」范式的回归—— # 实证表明纯自治 ReAct 会过早放弃,有界外层踹更稳,且不放松任何验收门(门仍是 judge 纯代码判)。 - max_resumes = 6 # 外层 resume 上限(每次 resume 内层 ReAct 仍可多轮;总轮数受 middleware 硬熔断兜底) + # 外层 resume 上限(每次 resume 内层 ReAct 仍可多轮;总轮数受 middleware 硬熔断兜底)。 + # 旋钮外置(运行时读 generation.yaml iteration.max_resumes;default=现值 6)。 + max_resumes = genconfig.get("iteration", "max_resumes", 6) kick_text = ( "开始实现这款富游戏。先 scaffold_init 起手,然后在循环里 write_source→(validate_datatable)→" "build→run_gates→read_verdict→针对失败门 write_source 修→再 run_gates……门全绿后调 finish 交付。" diff --git a/tier2/gen-worker/worker/config.py b/tier2/gen-worker/worker/config.py index 53c6792e..19239fd8 100644 --- a/tier2/gen-worker/worker/config.py +++ b/tier2/gen-worker/worker/config.py @@ -27,6 +27,10 @@ import os # ① 先装代理旁路:resolve_base_url 把网关 host 并入 NO_PROXY,必须在 import agentscope(内部 import # anthropic→httpx)之前完成。返回值是本进程默认 base_url(host 根)。 from . import client # 包内导入 +# 生成配置层(运行时读外部 generation.yaml + env 覆盖 + 缺省回落内置默认;读不到/脏 → 内置默认,绝不抛)。 +# 本模块的模型旋钮(模型名 / max_tokens / thinking / 历史压缩 / 取价估算)默认值改为从这里读、default=现值, +# 故「调生成 = 改 YAML 重跑」。genconfig 不 import agentscope,先于下方 agentscope 导入无副作用。 +from . import genconfig _DEFAULT_BASE_URL = client.resolve_base_url() @@ -42,8 +46,10 @@ from agentscope.agent import ContextConfig # noqa: E402 —— 历史压缩配 # ── 便宜档默认模型名(G 族图 G3 模型矩阵:deepseek 两档为现行 Tier0/1 在产模型)── # 只是默认常量、不硬编死:build_model_openai 的 model_name 形参显式传入优先, # model_name_from_env 亦可经 env 覆盖(下文)。spike 跑矩阵时由调用方点名到具体档。 -DEEPSEEK_FLASH = "deepseek-v4-flash" # 主力便宜档(spike 主问「便宜到什么程度还守得住」) -DEEPSEEK_PRO = "deepseek-v4-pro" # 强便宜档(救场档;退路树第一条触发线 v4-pro<40% 读它) +# 值改为从生成配置层读(default=现值);要换便宜档名改 generation.yaml 的 model.deepseek_* 即可。 +# 模块级常量在 import 时取一次(它俩是「默认档名」常量、非每 run 重算的热路径,import 时定即可)。 +DEEPSEEK_FLASH = genconfig.get("model", "deepseek_flash", "deepseek-v4-flash") # 主力便宜档 +DEEPSEEK_PRO = genconfig.get("model", "deepseek_pro", "deepseek-v4-pro") # 强便宜档(退路树 Q1 读它的过门率) class RecordingChatModel(AnthropicChatModel): @@ -180,11 +186,11 @@ def build_model( *, base_url: str | None = None, api_key: str | None = None, - max_tokens: int = 16000, - thinking_enable: bool = True, - thinking_budget: int = 8000, + max_tokens: int | None = None, + thinking_enable: bool | None = None, + thinking_budget: int | None = None, stream: bool = False, - max_retries: int = 2, + max_retries: int | None = None, record: bool = True, ) -> AnthropicChatModel: """构建 M3 的 AgentScope AnthropicChatModel 客户端(全部参数显式传入,tier2 红线禁读 wg1 全局)。 @@ -193,16 +199,31 @@ def build_model( model_name: 模型名(如 'MiniMax-M3')。 base_url: new-api 网关 host 根;None → 用本进程默认(client.resolve_base_url,已装代理旁路)。 api_key: new-api token;None → 从 env/.env 读(client.get_api_key)。 - max_tokens: 输出上限(非上下文窗;必须 > thinking_budget,见下硬约束)。 - thinking_enable / thinking_budget: M3 thinking 分离开关与预算。 + max_tokens: 输出上限(非上下文窗;必须 > thinking_budget,见下硬约束)。None → 运行时读 + generation.yaml 的 model.max_tokens(默认 16000;显式传入优先,便于单测/调用方点名)。 + thinking_enable / thinking_budget: M3 thinking 分离开关与预算。None → 运行时读 + model.thinking_enable / model.thinking_budget(默认 True / 8000)。 stream: 是否流式(默认 False,便于一次性取完整 response + usage)。 - max_retries: 网关偶发 502 突发兜底(wg1 spike 实证有效)。 + max_retries: 网关偶发 502 突发兜底(wg1 spike 实证有效)。None → 读 model.max_retries(默认 2)。 record: True → 用 RecordingChatModel 记 usage 供成本取证;False → 裸 AnthropicChatModel。 硬约束:max_tokens > thinking_budget(Anthropic 要求 max_tokens 严格大于 budget_tokens)。 源码 _model.py:176-179 会在 budget>=max_tokens 时自动抬 max_tokens=budget+1024 兜底, 但契约要求显式满足、别依赖兜底 —— 这里直接断言,不满足即抛(尽早暴露配置错)。 + + 旋钮外置(运行时读):未显式传入的形参从生成配置层取(default=现值),故「调生成 = 改 YAML 重跑」。 + sentinel=None → 体内取值,保证每次调用都读当前 YAML(而非函数定义时定死)。 """ + # 未显式传入(None)→ 运行时读 generation.yaml(default=现值;读不到/脏由 genconfig 兜底为现值)。 + if max_tokens is None: + max_tokens = genconfig.get("model", "max_tokens", 16000) + if thinking_enable is None: + thinking_enable = genconfig.get("model", "thinking_enable", True) + if thinking_budget is None: + thinking_budget = genconfig.get("model", "thinking_budget", 8000) + if max_retries is None: + max_retries = genconfig.get("model", "max_retries", 2) + if max_tokens <= thinking_budget: raise ValueError( f"max_tokens({max_tokens}) 必须 > thinking_budget({thinking_budget}):" @@ -235,9 +256,9 @@ def build_model_openai( *, base_url: str | None = None, api_key: str | None = None, - max_tokens: int = 16000, + max_tokens: int | None = None, stream: bool = False, - max_retries: int = 2, + max_retries: int | None = None, record: bool = True, ) -> OpenAIChatModel: """构建便宜档(deepseek 两档)的 AgentScope OpenAIChatModel 客户端,走 new-api OpenAI 兼容路。 @@ -259,7 +280,16 @@ def build_model_openai( stream: 是否流式(默认 False,便于一次性取完整 response + usage)。 max_retries: 网关偶发 502 突发兜底(wg1 spike 实证有效)。 record: True → 用 RecordingOpenAIChatModel 记 usage 供成本取证;False → 裸 OpenAIChatModel。 + + 旋钮外置(运行时读):max_tokens / max_retries 未显式传入(None)→ 读 generation.yaml 的 + model.max_tokens / model.max_retries(default=现值 16000 / 2),与 build_model 同口径。 """ + # 未显式传入(None)→ 运行时读 generation.yaml(default=现值)。便宜档无 thinking 约束,直接作 max_tokens。 + if max_tokens is None: + max_tokens = genconfig.get("model", "max_tokens", 16000) + if max_retries is None: + max_retries = genconfig.get("model", "max_retries", 2) + resolved_base = base_url or _DEFAULT_BASE_URL # base_url 若是调用方临时换的端点,也补一次代理旁路(幂等,已在 NO_PROXY 里则不动)。 client.install_proxy_bypass(resolved_base) @@ -285,8 +315,14 @@ def build_model_openai( ) -def model_name_from_env(default: str = "MiniMax-M3") -> str: - """单写 agent 的模型名:env TIER2_MODEL 覆盖,默认 MiniMax-M3(M3 路)。""" +def model_name_from_env(default: str | None = None) -> str: + """单写 agent 的模型名:env TIER2_MODEL 覆盖 > 显式 default > generation.yaml 的 model.default_model_name(现值 MiniMax-M3)。 + + 保持原有最高优先级 = 专用 env TIER2_MODEL(向后兼容,既有真跑脚本不受影响);它缺失时的默认值从 + 生成配置层读(default=现值 MiniMax-M3),故换默认模型改 generation.yaml 即可、不必改码。 + """ + if default is None: + default = genconfig.get("model", "default_model_name", "MiniMax-M3") return os.environ.get("TIER2_MODEL", default) @@ -333,8 +369,8 @@ TIER2_SUMMARY_TEMPLATE = ( def build_context_config( *, - trigger_ratio: float = 0.8, - reserve_ratio: float = 0.1, + trigger_ratio: float | None = None, + reserve_ratio: float | None = None, ) -> ContextConfig: """构建 tier2 单写 agent 的历史压缩配置(2.0.2 ContextConfig;加性,不改任何模型/循环行为)。 @@ -350,6 +386,12 @@ def build_context_config( Returns: ContextConfig:直接传给 `Agent(context_config=...)`。 """ + # 旋钮外置(运行时读):未显式传入(None)→ 读 generation.yaml(default=现值 0.8 / 0.1)。 + # sentinel=None 而非签名直接写死,保证每次调用都读当前 YAML(而非函数定义时定死)。 + if trigger_ratio is None: + trigger_ratio = genconfig.get("model", "ctx_trigger_ratio", 0.8) + if reserve_ratio is None: + reserve_ratio = genconfig.get("model", "ctx_reserve_ratio", 0.1) return ContextConfig( trigger_ratio=trigger_ratio, reserve_ratio=reserve_ratio, diff --git a/tier2/gen-worker/worker/genconfig.py b/tier2/gen-worker/worker/genconfig.py new file mode 100644 index 00000000..4f3d879f --- /dev/null +++ b/tier2/gen-worker/worker/genconfig.py @@ -0,0 +1,397 @@ +"""genconfig.py —— tier2 生成配置层加载器(把散在 Python 里的生成器旋钮收敛成一份外部 YAML,运行时读)。 + +【这份解决什么问题】 +"调生成、优化过门率"是 tier2 当前最高频的迭代循环:改一个 max_iters、抬一档 thinking_budget、调一条 +退路树阈值,跑一批看过门率有没有涨 20%。在此之前这些旋钮散在 6+ 个 Python 文件里写死,改一个值要 +改码、再同步到 mini-desktop 重跑。本模块把它们集中成 tier2/config/generation.yaml,**运行时读**: +调生成 = 改 YAML 重跑,不改码、不重同步。这正是图说 B 族「配置注册表(唯一事实源 / 版本化 / 运行时读)」 +在生成器旋钮这一面的最小落地——配置是真相,代码只是按 id 取值的消费方。 + +【取值三级回落(运行时读,失败绝不中断主链)】 +get(area, key, default) 每次调用按以下顺序取值,任一级取到即用: + ① env 单点覆盖:环境变量 TIER2_GEN____(全大写、双下划线分隔)——临时压一个值不必改文件, + 适合「这一批我想试 max_iters=60」这种一次性实验(改完 unset 即恢复)。 + ② 外部 YAML:tier2/config/generation.yaml(路径可被 env TIER2_GENCONFIG 指向别处)的 area.key。 + ③ 内置默认 _BUILTIN_DEFAULTS[area][key]:本模块自带的一份与现行硬编码值字节一致的副本——YAML 缺失 / + 读不出 / 脏值时的兜底,保证「配置文件没了也能按现行行为跑」。 + ④ 调用方传入的 default:连内置默认都没登记这个 key 时的最后防线(调用方按惯例传现行硬编码值)。 + +【默认行为字节不变(本相红线)】 +外置后,只要没人去改 YAML / 没设 env,每个旋钮取到的值必须 == 改造前的硬编码值。为此: + - generation.yaml 里每个值 = 当前硬编码值(逐项对齐); + - _BUILTIN_DEFAULTS 里每个值 = 当前硬编码值(逐项对齐,作 YAML 失效兜底); + - 各调用点 get(..., default=现值):default 也填现值。 +三处同值 ⇒ 无论哪一级取到,结果都是现值。改 YAML 一个值即可让它流通到取值点(运行时读)。 + +【best-effort 铁律(对齐项目 client.py / newapi_pricing.py 同款纪律)】 +读文件 / 解析 YAML / env 转型的任何异常都不抛、不中断生成主链:读不到或脏 → 落内置默认 + 一次性告警。 +YAML 只在「首次访问 + 文件 mtime 变化」时重读(运行时读但不必每次 get 都打开文件;改了文件下一次 get 自动生效)。 +""" + +from __future__ import annotations + +import os +import threading +from pathlib import Path +from typing import Any + +# YAML 解析:pyyaml 是声明依赖(requirements.txt),但仍 best-effort 兜底——import 失败则整层降级为 +# 「只用内置默认 + env 覆盖」,绝不让一个缺失的解析库阻断生成(读不到 = 用默认,与脏文件同处置)。 +try: + import yaml as _yaml # type: ignore +except Exception: # pragma: no cover —— 极端环境缺 pyyaml:降级为内置默认 + env(不抛) + _yaml = None + + +# ── 配置文件默认路径(可被 env TIER2_GENCONFIG 指向别处)───────────────────────────── +# 本文件在 tier2/gen-worker/worker/genconfig.py → 上溯 2 级到 gen-worker/,再上 1 级到 tier2/, +# 配置在 tier2/config/generation.yaml。与 archetypes.py / run.py 同源的「从 __file__ 推 tier2/ 根」口径。 +_TIER2_DIR = Path(__file__).resolve().parents[2] +_DEFAULT_CONFIG_PATH = _TIER2_DIR / "config" / "generation.yaml" + +# env 覆盖前缀:TIER2_GEN____(双下划线分隔 area / key;全大写)。 +# 例:压一档单写轮数 → export TIER2_GEN__ITERATION__WRITER_MAX_ITERS=60。 +_ENV_PREFIX = "TIER2_GEN__" + + +# ════════════════════════════════════════════════════════════════════════════ +# 内置默认(_BUILTIN_DEFAULTS)—— 与现行硬编码值【字节一致】的兜底副本 +# 作用:generation.yaml 读不到 / 脏 / 缺某 key 时,按这里的值兜底,保证默认行为不漂移。 +# ⚠️ 维护纪律:这里每个值必须与对应源文件里改造前的硬编码值严格相等;改默认值优先改 YAML, +# 这份兜底只在 YAML 失效时生效,不应与 YAML 的「现值」语义分叉。 +# 分区(area)与各源文件一一对应,便于查改: +# model ← worker/config.py(模型名 / max_tokens / thinking / 历史压缩 / 取价估算) +# budget ← worker/middleware.py(¥ 硬闸 / 四道熔断 / 软刹 / 折价倍率 / 首调估算 token) +# iteration ← worker/agent_loop/studio.py(单写轮数 / 外层 resume 上限) +# design_team ← worker/agent_loop/design_team.py(leader 轮数 / 单专家上限 / 团队超时) +# gates ← worker/fallback_tree.py(退路树四阈值)+ harness 门阈值(人可玩 / 渲染反映) +# archetype ← worker/archetypes.py(business-sim driver 参数) +# ════════════════════════════════════════════════════════════════════════════ +_BUILTIN_DEFAULTS: dict[str, dict[str, Any]] = { + # ── model:模型客户端构建旋钮(worker/config.py)── + "model": { + "default_model_name": "MiniMax-M3", # model_name_from_env 默认(M3 路) + "deepseek_flash": "deepseek-v4-flash", # DEEPSEEK_FLASH 便宜主力档 + "deepseek_pro": "deepseek-v4-pro", # DEEPSEEK_PRO 强便宜救场档 + "max_tokens": 16000, # build_model / build_model_openai 默认输出上限 + "thinking_enable": True, # build_model 默认开 thinking 分离 + "thinking_budget": 8000, # build_model 默认 thinking 预算(须 < max_tokens) + "max_retries": 2, # 网关偶发 502 突发兜底重试次数 + "ctx_trigger_ratio": 0.8, # build_context_config 触发压缩比例 + "ctx_reserve_ratio": 0.1, # build_context_config 保留最近上下文比例 + }, + # ── budget:¥ 累进硬闸 + 四道熔断 + 软刹(worker/middleware.py)── + "budget": { + "rmb_hard_limit": 3.0, # DEFAULT_RMB_HARD_LIMIT 单局 ¥ 硬上限(directional,待标定) + "max_tool_calls": 60, # step_cap 步数硬顶(工具调用总次数) + "max_model_calls": 80, # budget 预算闸(模型推理总次数,¥ 取价失败时兜底) + "wall_timeout_s": 1800.0, # timeout 整 reply 墙钟超时(秒) + "step_timeout_s": 420.0, # timeout 单步静默超时(秒) + "stuck_repeat_threshold": 4, # stuck 连续同一失败签名达此次数判死圈 + "soft_ratio": 0.8, # 软刹触发比例(达 soft_ratio×硬顶注入收敛提醒) + "group_ratio": 1.0, # new-api 分组倍率(成本折算用) + "est_prompt_tokens": 12000, # _estimate_call_rmb 首调保守估:prompt token 量级 + "est_completion_tokens": 2000, # _estimate_call_rmb 首调保守估:补全 token 量级 + }, + # ── iteration:单写 ReAct 轮数 + 外层有界 resume(worker/agent_loop/studio.py)── + "iteration": { + "writer_max_iters": 40, # run_studio 单写 ReAct 放开的最大轮数(C3 建议整局 ≤40) + "max_resumes": 6, # 外层有界自纠 resume 上限(agent 过早停下再踹回去续修) + }, + # ── design_team:阶段 1 工作室星形多 agent 设计预算(worker/agent_loop/design_team.py)── + "design_team": { + "leader_max_iters": 12, # leader ReAct 总轮数上限(调专家 + 汇总天花板) + "per_expert_cap": 2, # 单专家被 leader 调用次数上限(防反复刷同一专家) + "timeout_s": 240.0, # 整团队墙钟硬超时(秒) + }, + # ── gates:退路树四阈值(fallback_tree.py)+ harness 门阈值(play-phaser.cdp.cjs)── + # 注:harness 是 Node,这些值经导出 JSON / env 注入到 .cjs(见下方 export_for_harness 与 .cjs 注释); + # Python 侧 fallback_tree 直接 import genconfig 读。 + "gates": { + "qpass_go_min": 0.40, # 退路树 Q1:v4-pro 过门率 ≥ 此值 → GO(QPASS_GO_MIN) + "qpass_floor_max": 0.20, # 退路树 Q4:便宜档全线 < 此值 → 全线崩(QPASS_FLOOR_MAX) + "qpass_strong_baseline_min": 0.0, # 退路树 Q4:强基线过门率 > 此值 → 能过(QPASS_STRONG_BASELINE_MIN) + "concentration_ratio": 0.50, # 退路树 Q2/Q3:某桶失败占比 ≥ 此值 → 集中(CONCENTRATION_RATIO) + "human_min_patience_ms": 12000, # harness 人可玩 advisory:订单耐心人类最小窗口(HUMAN_MIN_PATIENCE_MS) + "color_dist_min": 18, # harness render-reflects-state:色差判反映状态阈值(COLOR_DIST_MIN) + "var_delta_min": 120, # harness render-reflects-state:方差增量判反映状态阈值(VAR_DELTA_MIN) + }, + # ── archetype:business-sim 品类 driver 参数(worker/archetypes.py 的 _business_sim_gate_spec)── + # 注:这些是 driver 真玩规格里的可调数(步数 / 步间隔 / 胜负阈值),改它们影响 driver 怎么玩、 + # 富游戏门怎么判;品类的【结构】(三系统 / 数据表 schema)不在配置层,仍由 fixture / 注册表定。 + "archetype": { + "business_sim_driver_steps": 80, # driver 总步数 + "business_sim_driver_step_ms": 280, # driver 步间隔(ms) + "business_sim_win_threshold": 100, # 盈利路金币胜利阈值(与 economy/latch 门对齐) + "business_sim_bankrupt_steps": 60, # 破产路观察步数 + "business_sim_streak_lose": 3, # 连续流失判负次数 + }, +} + + +# ── 运行时读状态(缓存 + mtime 守门;线程安全)────────────────────────────────────── +# _cache 存解析后的 YAML dict;_cache_mtime / _cache_path 记上次读的文件特征,用于判定是否需重读 +# (运行时读 = 改了文件下次 get 自动重读,但不必每次 get 都开文件)。_warned 防重复告警刷屏。 +_lock = threading.Lock() +_cache: dict[str, dict[str, Any]] | None = None +_cache_mtime: float | None = None +_cache_path: str | None = None +_warned: set[str] = set() + + +def _warn_once(tag: str, msg: str) -> None: + """同一 tag 只告警一次(防 best-effort 路径在循环里刷屏);可追溯日志,带 [tier2-genconfig] 前缀。""" + if tag in _warned: + return + _warned.add(tag) + print(f"[tier2-genconfig] {msg}", flush=True) + + +def config_path() -> Path: + """当前生效的配置文件路径:env TIER2_GENCONFIG 指定 > 默认 tier2/config/generation.yaml。""" + p = os.environ.get("TIER2_GENCONFIG") + return Path(p) if p else _DEFAULT_CONFIG_PATH + + +def _load_yaml() -> dict[str, dict[str, Any]]: + """运行时读 generation.yaml(带 mtime 缓存 + best-effort)。返回解析后的 dict(读不到/脏 → 空 dict)。 + + 线程安全:首次读 / 文件 mtime 变化时重读并刷新缓存;否则返回缓存(避免每次 get 都开文件)。 + 任何异常(文件缺失 / YAML 语法错 / 顶层非 dict / 缺 pyyaml)都不抛——返回空 dict(由上层落内置默认)。 + """ + global _cache, _cache_mtime, _cache_path + + path = config_path() + path_str = str(path) + + # pyyaml 缺失:整层降级(只用内置默认 + env)。一次性告警后返回空 dict。 + if _yaml is None: + _warn_once("no-yaml", "pyyaml 不可用 → 配置层降级为「内置默认 + env 覆盖」(行为仍 = 现值)。") + return {} + + with _lock: + # 取文件 mtime;文件不存在 → mtime=None。 + try: + mtime = path.stat().st_mtime if path.exists() else None + except OSError: + mtime = None + + # 命中缓存:路径与 mtime 都没变 → 直接返回上次解析结果(运行时读但不重复开文件)。 + if _cache is not None and _cache_path == path_str and _cache_mtime == mtime: + return _cache + + # 文件不存在:缓存空 dict(按内置默认跑);一次性告警「用内置默认」(非错误,可能就是没建文件)。 + if mtime is None: + _warn_once( + f"missing:{path_str}", + f"配置文件不存在({path_str})→ 全部用内置默认(行为 = 现值);" + "如需调生成,建该文件或 export TIER2_GENCONFIG 指向它。", + ) + _cache, _cache_mtime, _cache_path = {}, None, path_str + return _cache + + # 读 + 解析(best-effort:任何异常都落空 dict + 告警,绝不抛)。 + try: + raw = path.read_text(encoding="utf-8") + data = _yaml.safe_load(raw) + if data is None: + data = {} # 空文件 → 空配置(按内置默认跑) + if not isinstance(data, dict): + # 顶层不是映射(写错了)→ 视为脏文件,落空 dict 用内置默认。 + _warn_once( + f"nonmap:{path_str}", + f"配置文件顶层不是 YAML 映射({path_str})→ 视为脏,全部用内置默认(行为 = 现值)。", + ) + data = {} + except Exception as exc: # noqa: BLE001 —— 脏 YAML / IO 异常都降级,绝不中断生成 + _warn_once( + f"parse:{path_str}", + f"配置文件解析失败({path_str}: {type(exc).__name__}: {exc})→ 全部用内置默认(行为 = 现值)。", + ) + data = {} + + _cache, _cache_mtime, _cache_path = data, mtime, path_str + return _cache + + +def _coerce(value: Any, like: Any) -> Any: + """把从 env / YAML 取到的原始值按「内置默认 like 的类型」做最小转型(best-effort,失败回 None)。 + + 为什么要转型:env 取出来全是字符串,YAML 也可能把 0.8 读成字符串;转型按现值的类型对齐,避免 + 把 "60" 当字符串塞进期望 int 的旋钮。bool 特殊处理(避免 bool('False') == True 的经典坑)。 + 转型失败(如把 'abc' 转 int)→ 返回 None,由上层视为「这一级没取到」继续往下回落。 + """ + if value is None: + return None + try: + if isinstance(like, bool): + # bool 优先:像 like 是 True/False 时,把常见真假字面量与数值都正确映射。 + if isinstance(value, bool): + return value + s = str(value).strip().lower() + if s in ("1", "true", "yes", "on"): + return True + if s in ("0", "false", "no", "off"): + return False + return None # 不认识的 bool 字面量 → 视为没取到 + if isinstance(like, int) and not isinstance(like, bool): + return int(value) + if isinstance(like, float): + return float(value) + if isinstance(like, str): + return str(value) + except (TypeError, ValueError): + return None # 转型失败 → 当作没取到,回落下一级 + # like 是 None / 其它类型:原样返回(不强转)。 + return value + + +def get(area: str, key: str, default: Any = None) -> Any: + """取一个生成器旋钮的当前值:env 覆盖 > 外部 YAML > 内置默认 > 调用方 default。 + + Args: + area: 分区名(model / budget / iteration / design_team / gates / archetype)。 + key: 分区内的旋钮名(见 _BUILTIN_DEFAULTS 与 generation.yaml)。 + default: 调用方传入的兜底值——**按惯例传该旋钮当前硬编码值**;它是连内置默认都没登记该 key + 时的最后防线,保证取值结果在三处同值时 == 现值(默认行为字节不变)。 + + Returns: + 旋钮值(类型按内置默认对齐转型;全级都取不到 → 返回 default)。绝不抛(best-effort)。 + + 取值口径(任一级取到即用): + ① env TIER2_GEN____(全大写、双下划线分隔); + ② YAML 的 area.key; + ③ _BUILTIN_DEFAULTS[area][key]; + ④ 调用方 default。 + 转型基准 `like`:优先用内置默认的类型,缺则用 default 的类型(让 env/YAML 的字符串值按现值类型落位)。 + """ + builtin = _BUILTIN_DEFAULTS.get(area, {}) + # 转型基准:内置默认有该 key 用它的类型,否则用 default 的类型(都为 None 则不强转)。 + like = builtin.get(key, default) + + # ① env 单点覆盖(最高优先级,便于一次性实验,不必改文件)。 + env_name = f"{_ENV_PREFIX}{area.upper()}__{key.upper()}" + env_raw = os.environ.get(env_name) + if env_raw is not None: + coerced = _coerce(env_raw, like) + if coerced is not None: + return coerced + # env 设了但转型失败(写了非法值)→ 告警并继续回落(不让一个手滑的 env 把旋钮变 None)。 + _warn_once( + f"env-bad:{env_name}", + f"env {env_name}={env_raw!r} 无法转成 {type(like).__name__} → 忽略,回落 YAML/默认。", + ) + + # ② 外部 YAML 的 area.key。 + data = _load_yaml() + section = data.get(area) if isinstance(data, dict) else None + if isinstance(section, dict) and key in section: + coerced = _coerce(section.get(key), like) + if coerced is not None: + return coerced + # YAML 写了该 key 但值脏(类型转不动)→ 告警并回落内置默认(不让脏值穿透)。 + _warn_once( + f"yaml-bad:{area}.{key}", + f"配置 {area}.{key}={section.get(key)!r} 无法转成 {type(like).__name__} → 忽略,回落内置默认。", + ) + + # ③ 内置默认。 + if key in builtin: + return builtin[key] + + # ④ 调用方 default(连内置默认都没登记该 key 的最后防线)。 + return default + + +def export_for_harness() -> dict[str, Any]: + """导出 harness(Node)需要的门阈值为一个扁平 dict(供 .cjs 读一份 JSON,或编排器写临时 JSON)。 + + harness 是 .cjs,不便直接读本 Python 加载器;最小改动法是让它读一份导出的 JSON 或经 env 注入。 + 本函数把 gates 区里 harness 关心的几个阈值按 .cjs 端约定的 env 名导出成 dict,值仍走 get() 三级回落 + (故默认 = 现值)。.cjs 端按「process.env.<同名> > 内置默认」消费(见 play-phaser.cdp.cjs 顶部注释)。 + + 返回的键 = .cjs 端读取的 env 变量名,值 = 当前生效值(已转型)。 + """ + return { + # 键名 = play-phaser.cdp.cjs 读的 env 名(TIER2_GEN__GATES__* 同 get() 口径,.cjs 也认这个前缀)。 + "TIER2_GEN__GATES__HUMAN_MIN_PATIENCE_MS": get("gates", "human_min_patience_ms", 12000), + "TIER2_GEN__GATES__COLOR_DIST_MIN": get("gates", "color_dist_min", 18), + "TIER2_GEN__GATES__VAR_DELTA_MIN": get("gates", "var_delta_min", 120), + } + + +def reload() -> None: + """强制丢弃缓存,下次 get 重新读文件(测试 / 手动热更用;运行时 mtime 已自动重读,一般不必显式调)。""" + global _cache, _cache_mtime, _cache_path + with _lock: + _cache, _cache_mtime, _cache_path = None, None, None + + +# ── __main__ 自测块:不依赖 agentscope / 网络,只校验三级回落与转型(6c6g 可直接跑)──────────── +# 校验:python3 -c 'from worker import genconfig'(import 干净)+ python3 worker/genconfig.py(自测)。 +if __name__ == "__main__": # pragma: no cover —— 本地自测 + import tempfile + + def _check(label: str, got: Any, expect: Any) -> None: + ok = "OK" if got == expect and type(got) is type(expect) else "FAIL" + print(f"[{ok}] {label}: got={got!r}(期望 {expect!r})") + assert got == expect and type(got) is type(expect), f"{label}: 期望 {expect!r},实得 {got!r}" + + # ① 无 YAML、无 env:取内置默认(= 现值),类型正确。 + reload() + os.environ.pop("TIER2_GENCONFIG", None) + _check("内置默认 model.max_tokens", get("model", "max_tokens", 16000), 16000) + _check("内置默认 budget.rmb_hard_limit", get("budget", "rmb_hard_limit", 3.0), 3.0) + _check("内置默认 model.thinking_enable(bool)", get("model", "thinking_enable", True), True) + _check("内置默认 iteration.writer_max_iters", get("iteration", "writer_max_iters", 40), 40) + + # ② 调用方 default 兜底:内置默认未登记的 key → 返回 default。 + _check("未登记 key 回落 default", get("model", "不存在的key", 999), 999) + + # ③ env 覆盖 + 转型(字符串 → int / float / bool)。 + os.environ["TIER2_GEN__ITERATION__WRITER_MAX_ITERS"] = "60" + os.environ["TIER2_GEN__BUDGET__RMB_HARD_LIMIT"] = "5.5" + os.environ["TIER2_GEN__MODEL__THINKING_ENABLE"] = "false" + _check("env 覆盖 int", get("iteration", "writer_max_iters", 40), 60) + _check("env 覆盖 float", get("budget", "rmb_hard_limit", 3.0), 5.5) + _check("env 覆盖 bool=false", get("model", "thinking_enable", True), False) + # env 非法值 → 忽略、回落内置默认(不变 None)。 + os.environ["TIER2_GEN__ITERATION__WRITER_MAX_ITERS"] = "abc" + _check("env 非法值回落默认", get("iteration", "writer_max_iters", 40), 40) + for k in ("TIER2_GEN__ITERATION__WRITER_MAX_ITERS", "TIER2_GEN__BUDGET__RMB_HARD_LIMIT", + "TIER2_GEN__MODEL__THINKING_ENABLE"): + os.environ.pop(k, None) + + # ④ 外部 YAML 流通:写一个临时 YAML 改一个值 → get 取到改后值(运行时读)。 + if _yaml is not None: + with tempfile.NamedTemporaryFile("w", suffix=".yaml", delete=False, encoding="utf-8") as f: + f.write("model:\n max_tokens: 24000\nbudget:\n rmb_hard_limit: 2.0\n") + tmp_path = f.name + os.environ["TIER2_GENCONFIG"] = tmp_path + reload() + _check("YAML 改值流通 model.max_tokens", get("model", "max_tokens", 16000), 24000) + _check("YAML 改值流通 budget.rmb_hard_limit", get("budget", "rmb_hard_limit", 3.0), 2.0) + # YAML 没写的 key 仍回落内置默认(= 现值)。 + _check("YAML 未写 key 回落内置默认", get("iteration", "writer_max_iters", 40), 40) + os.environ.pop("TIER2_GENCONFIG", None) + os.unlink(tmp_path) + reload() + + # ⑤ 脏 YAML(顶层非映射 / 语法错):降级内置默认,不抛。 + if _yaml is not None: + with tempfile.NamedTemporaryFile("w", suffix=".yaml", delete=False, encoding="utf-8") as f: + f.write("- 这是个列表不是映射\n- 顶层写错了\n") + bad_path = f.name + os.environ["TIER2_GENCONFIG"] = bad_path + reload() + _check("脏 YAML 降级内置默认", get("model", "max_tokens", 16000), 16000) + os.environ.pop("TIER2_GENCONFIG", None) + os.unlink(bad_path) + reload() + + # ⑥ export_for_harness:默认导出 = 现值。 + h = export_for_harness() + _check("harness 导出 HUMAN_MIN_PATIENCE_MS", h["TIER2_GEN__GATES__HUMAN_MIN_PATIENCE_MS"], 12000) + _check("harness 导出 COLOR_DIST_MIN", h["TIER2_GEN__GATES__COLOR_DIST_MIN"], 18) + + print("\n[genconfig] 三级回落 + 转型 + YAML 流通 + 脏值降级 + harness 导出自测全部通过。") diff --git a/tier2/gen-worker/worker/middleware.py b/tier2/gen-worker/worker/middleware.py index e0467e04..5a297c3e 100644 --- a/tier2/gen-worker/worker/middleware.py +++ b/tier2/gen-worker/worker/middleware.py @@ -48,6 +48,18 @@ from typing import Optional from agentscope.middleware import MiddlewareBase +# 生成配置层(运行时读外部 generation.yaml + env 覆盖 + 缺省回落内置默认;读不到/脏 → 内置默认,绝不抛)。 +# 本模块的熔断/软刹/¥ 闸旋钮默认值改为从这里读、default=现值,故「调成本上限/失控保护 = 改 YAML 重跑」。 +# 包内/直跑兼容导入(与下方 observability 同款兜底:直跑时把 gen-worker/ 加进 sys.path)。 +try: + from . import genconfig # type: ignore +except Exception: # pragma: no cover —— 直跑兜底:worker 包未就位时把 gen-worker/ 加进 sys.path + import sys + from pathlib import Path + + sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + from worker import genconfig # type: ignore + # trace adapter + 相位标记构造器在 observability 模块(H1/H2 产出件);包内/直跑兼容导入。 # make_phase_marker / PHASE_* 是 U3 给三道新钩子产「相位段」typed event 用的(走 adapter 同一条 ingest 路)。 try: @@ -150,8 +162,11 @@ class Tier2CircuitBreak(Exception): # 单次 tier2 富游戏生成(自治多轮)的 ¥ 硬上限。当「已花 ¥ + 本次预估 ¥」越过它即 fail-closed 抛熔断。 # 口径依据:图说 A4「token × new-api 单价换算成 ¥ 累进台账,越硬上限即 fail-closed 抛错终止」+ C3 # 「强制硬闸规模化前必须落地」。当前值是方向性占位(¥3.0/局):0号 spike 富游戏单局观测约 ¥0.x~¥1 量级, -# 留 ~3×头寸防失控发散;待 batch n≥30 的 cost_rmb 分布出来后,按 P95 + 安全裕度回填真值,届时改这一个常量即可。 -DEFAULT_RMB_HARD_LIMIT = 3.0 # directional —— 待 mini-desktop 成本分布标定后回填 +# 留 ~3×头寸防失控发散;待 batch n≥30 的 cost_rmb 分布出来后,按 P95 + 安全裕度回填真值。 +# 值改为从生成配置层读(default=现值 3.0):标定后改 generation.yaml 的 budget.rmb_hard_limit 即可、不必改码。 +# 模块级常量在 import 时取一次(作对外可见的「当前默认上限」快照 + genconfig 兜底 default); +# __init__ 每次实例化会再运行时读一遍(sentinel=None),保证改 YAML 后新建的 middleware 立即用新值。 +DEFAULT_RMB_HARD_LIMIT = genconfig.get("budget", "rmb_hard_limit", 3.0) # directional —— 待标定后改 YAML class CircuitBreakerMiddleware(MiddlewareBase): @@ -169,16 +184,16 @@ class CircuitBreakerMiddleware(MiddlewareBase): def __init__( self, *, - max_tool_calls: int = 60, - max_model_calls: int = 80, - wall_timeout_s: float = 1800.0, - step_timeout_s: float = 420.0, - stuck_repeat_threshold: int = 4, - soft_ratio: float = 0.8, - rmb_hard_limit: float = DEFAULT_RMB_HARD_LIMIT, + max_tool_calls: int | None = None, + max_model_calls: int | None = None, + wall_timeout_s: float | None = None, + step_timeout_s: float | None = None, + stuck_repeat_threshold: int | None = None, + soft_ratio: float | None = None, + rmb_hard_limit: float | None = None, pricing_params: Optional[dict] = None, enable_rmb_gate: bool = True, - group_ratio: float = 1.0, + group_ratio: float | None = None, ) -> None: """ Args: @@ -188,12 +203,35 @@ class CircuitBreakerMiddleware(MiddlewareBase): step_timeout_s: 单步静默超时(相邻事件间隔超此判单步卡死;秒)。 stuck_repeat_threshold: 连续同一失败签名达此次数 → 判死圈。 soft_ratio: 软刹触发比例(达 soft_ratio×硬顶时注入收敛提醒)。 - rmb_hard_limit: ¥ 累进硬上限(单次生成;越过即 fail-closed)。默认取 DEFAULT_RMB_HARD_LIMIT(directional)。 + rmb_hard_limit: ¥ 累进硬上限(单次生成;越过即 fail-closed)。 pricing_params: new-api 计费三件套 {pricing, qpu, usd_rate};None → 首次模型调用时经 fetch_pricing_params 惰性活读取(best-effort,取不到则 ¥ 闸降级为次数闸)。显式传入便于测试/复用。 enable_rmb_gate: 是否启用 ¥ 累进硬闸(False → 只走原四道;留个总开关便于排障/对照)。 - group_ratio: new-api 分组倍率(成本折算用;默认 1.0)。 + group_ratio: new-api 分组倍率(成本折算用)。 + + 旋钮外置(运行时读):上述各熔断/软刹/¥ 闸/倍率旋钮未显式传入(None)→ 实例化时从生成配置层 + (generation.yaml 的 budget 区)读、default=现值;故「调成本上限/失控保护强度 = 改 YAML 重跑」。 + sentinel=None 而非签名写死,保证每次新建 middleware 都读当前 YAML(改 YAML 后下一次 run 生效)。 + 显式传入的参数(如测试/编排器点名)优先,不被配置覆盖。 """ + # 未显式传入(None)→ 运行时读 generation.yaml 的 budget 区(default=现值;读不到/脏由 genconfig 兜底为现值)。 + if max_tool_calls is None: + max_tool_calls = genconfig.get("budget", "max_tool_calls", 60) + if max_model_calls is None: + max_model_calls = genconfig.get("budget", "max_model_calls", 80) + if wall_timeout_s is None: + wall_timeout_s = genconfig.get("budget", "wall_timeout_s", 1800.0) + if step_timeout_s is None: + step_timeout_s = genconfig.get("budget", "step_timeout_s", 420.0) + if stuck_repeat_threshold is None: + stuck_repeat_threshold = genconfig.get("budget", "stuck_repeat_threshold", 4) + if soft_ratio is None: + soft_ratio = genconfig.get("budget", "soft_ratio", 0.8) + if rmb_hard_limit is None: + rmb_hard_limit = genconfig.get("budget", "rmb_hard_limit", 3.0) + if group_ratio is None: + group_ratio = genconfig.get("budget", "group_ratio", 1.0) + self.max_tool_calls = max_tool_calls self.max_model_calls = max_model_calls self.wall_timeout_s = wall_timeout_s @@ -400,11 +438,12 @@ class CircuitBreakerMiddleware(MiddlewareBase): # 自适应均价:本局已花 ¥ 摊到已调用次数(model_calls 在 on_reply 处按 ModelCallStart 累加)。 return self.spent_rmb / max(1, self.model_calls) # 首次调用:保守典型估(富游戏单写轮 prompt 偏大)。用真计费参数折,取不到价则后面不会走到这(降级了)。 + # 估算 token 量级外置(运行时读 generation.yaml budget.est_*;default=现值 12000/2000)。 pp = self._pricing_params or {} return _cost_compute( model_name, - prompt_tokens=12000, # 典型富游戏单写轮 prompt 量级(系统提示 + 历史 + 设计稿) - completion_tokens=2000, # 一轮补全 + tool_use 量级 + prompt_tokens=genconfig.get("budget", "est_prompt_tokens", 12000), # 典型富游戏单写轮 prompt 量级(系统提示 + 历史 + 设计稿) + completion_tokens=genconfig.get("budget", "est_completion_tokens", 2000), # 一轮补全 + tool_use 量级 pricing=pp.get("pricing", {}), qpu=pp.get("qpu", 0) or 1, usd_rate=pp.get("usd_rate", 0) or 0, diff --git a/tier2/gen-worker/worker/prompts.py b/tier2/gen-worker/worker/prompts.py new file mode 100644 index 00000000..63d5b6b2 --- /dev/null +++ b/tier2/gen-worker/worker/prompts.py @@ -0,0 +1,373 @@ +"""prompts.py —— tier2 prompt 加载器(把 roles.py 里写死的 system prompt 改成从 Prompt Registry 读)。 + +【这份解决什么问题】 +prompt 是生成质量的最大杠杆(图说 E 族:阶段 1 设计 + 阶段 2 单写两阶段角色,改 prompt 直接抬过门率)。 +在此之前 tier2 的 prompt 全是 roles.py 里硬编码的 Python 字符串,调一条 prompt(改一句纪律、加一条约束) +要改码、再同步到 mini-desktop 重跑。本模块把它们接到契约 #8 Prompt Registry(contracts/prompts/registry.yaml ++ 09-tier2-richgame/*.md):**运行时按 id 读 registry 引用的 prompt 原文**,故【调一条 prompt = 改 Registry +重跑,不改 Python】。这正是图说 B 族「配置注册表(唯一事实源 / 版本化 / 运行时读)」在 prompt 这一面的落地, +也呼应 prompt-governance「prompt 即第 8 类契约,运行时按 id 加载注入、不在引擎/编排内核里内嵌」。 + +【与 cheap-line(agent-loop v1)PromptStore 的关键区别 —— 降级铁律】 +agent-loop v1 的 orchestrator/prompts.py 是 **hard-fail** 加载器(registry 缺 / 版本不一致即抛 PromptError, +批跑 fail-fast)。tier2 的生成主链不能因为一次 prompt 读失败就中断,故本加载器是 **best-effort + 回落内置原文** +(prompt-governance §9 降级铁律第 1 条:「prompt 加载失败(缺 id/version)→ 回退内置默认 prompt + 告警, +不中断生成」)。读不到 / 脏 / 缺文件 → 用调用方传入的内置原文 + 一次性告警,绝不抛、绝不中断主链。 + +【取值三级回落(运行时读,失败绝不中断主链;与 genconfig 同款纪律)】 +load(prompt_id, default) 每次调用按以下顺序取 prompt 原文,任一级取到即用: + ① 外置目录覆盖:env TIER2_PROMPTS_DIR 指向别处的 contracts/prompts/(临时换一套 prompt 不必动仓内文件, + 适合「这一批我想试另一版 writer 提示词」这种实验);缺省 = 仓内 contracts/prompts/。 + ② Registry 引用的 prompt 文件:registry.yaml 里 id 对应条目的 file 字段所指 .md(剥掉 frontmatter 取正文)。 + ③ 调用方传入的 default:**roles.py 里那条 prompt 的内置原文**——registry 缺该 id / 文件不存在 / 读不出 / + 正文为空时的兜底,保证「registry 没了也能按现行行为跑」。 + +【默认行为字节不变(本相红线)】 +外置后,只要没人去改 registry 的 .md、没设 env,load() 取到的 prompt 文本必须 == 改造前 roles.py 的内置原文。 +为此:registry 09-tier2-richgame/*.md 的正文 = roles.py 对应内置原文逐字节对齐(带参的存模板,占位符 +{{input.*}} 由 render() 注入后与原文一致)。registry 命中 ⇒ 文本 == 内置原文;registry 缺失 ⇒ 回落内置原文 +⇒ 仍 == 内置原文。两条路都得到现行文本,只有人主动改了 .md 才让新文本流通到取值点(运行时读)。 + +【best-effort 铁律(对齐 genconfig.py / client.py / newapi_pricing.py 同款纪律)】 +读 registry / 读 prompt 文件 / 剥 frontmatter 的任何异常都不抛、不中断生成主链:读不到或脏 → 落内置原文 + +一次性告警。registry 与各 prompt 文件只在「首次访问 + 文件 mtime 变化」时重读(运行时读但不必每次都开文件)。 + +【参数注入:render() 复用 cheap-line 的 {{input.xxx}} 约定】 +带参 prompt(writer_system(brief) / design_leader_system(brief) / player_system(persona))在 registry 存 +模板,占位符用 {{input.}}(与 contracts/prompts 既有范本、orchestrator/prompts.py 同款),render() 做 +替换。注意:tier2 的若干 prompt 正文含 JSON 花括号(如 player_system 的 {"fun":...}),故 render 用 +**字符串精确替换**(只替换 {{input.}} 字面),绝不用 str.format(会被裸花括号噎住)。 +""" + +from __future__ import annotations + +import os +import re +import threading +from pathlib import Path +from typing import Any + + +# ── Registry 与 prompt 目录定位(可被 env TIER2_PROMPTS_DIR 指向别处)───────────────────── +# 本文件在 tier2/gen-worker/worker/prompts.py;contracts/ 是仓库根下的目录。 +# 从 __file__ 上溯:worker → gen-worker → tier2 → ,再进 contracts/prompts/。 +# 与 genconfig 的「从 __file__ 推 tier2/ 根」同源口径(这里多上溯一级到 repo 根)。 +_REPO_ROOT = Path(__file__).resolve().parents[3] +_DEFAULT_PROMPTS_DIR = _REPO_ROOT / "contracts" / "prompts" + +# {{input.}} 占位符(兼容花括号内有空白的写法 {{ input.key }};与 orchestrator/prompts.py 同款)。 +_PLACEHOLDER_RE = re.compile(r"\{\{\s*input\.([A-Za-z0-9_]+)\s*\}\}") + + +# ── 运行时读状态(缓存 + mtime 守门;线程安全;与 genconfig 同款)────────────────────────── +# _registry_cache 存解析后的 registry 索引(id → {file, version, ...}); +# _file_cache 存各 prompt 文件剥 frontmatter 后的正文(path → body); +# 各自记 mtime,改了文件下次 load 自动重读(运行时读但不每次开文件)。_warned 防重复告警刷屏。 +_lock = threading.Lock() +_registry_cache: dict[str, dict[str, str]] | None = None +_registry_mtime: float | None = None +_registry_path_cache: str | None = None +_file_cache: dict[str, tuple[float | None, str]] = {} # path_str → (mtime, body) +_warned: set[str] = set() + + +def _warn_once(tag: str, msg: str) -> None: + """同一 tag 只告警一次(防 best-effort 路径在循环里刷屏);可追溯日志,带 [tier2-prompts] 前缀。""" + if tag in _warned: + return + _warned.add(tag) + print(f"[tier2-prompts] {msg}", flush=True) + + +def prompts_dir() -> Path: + """当前生效的 contracts/prompts/ 目录:env TIER2_PROMPTS_DIR 指定 > 默认仓内 contracts/prompts/。""" + p = os.environ.get("TIER2_PROMPTS_DIR") + return Path(p) if p else _DEFAULT_PROMPTS_DIR + + +def _registry_path() -> Path: + """registry.yaml 的当前路径(= prompts_dir()/registry.yaml)。""" + return prompts_dir() / "registry.yaml" + + +def _load_registry() -> dict[str, dict[str, str]]: + """运行时读 registry.yaml,解析出 {id: {file, version}}(带 mtime 缓存 + best-effort)。 + + 解析口径与 orchestrator/prompts.py 一致(最小 YAML 子集:顶层 prompts: 列表,每项 `- id:` 起头的键值块), + 但本函数 **绝不抛** —— registry 缺失 / 语法异常都返回空 dict(由上层 load 回落内置原文)。 + 只取本加载器需要的 id / file / version 三个字段;其余字段(stage/owner/desc/eval)忽略不影响。 + """ + global _registry_cache, _registry_mtime, _registry_path_cache + + path = _registry_path() + path_str = str(path) + + with _lock: + # 取 registry 文件 mtime;不存在 → None。 + try: + mtime = path.stat().st_mtime if path.exists() else None + except OSError: + mtime = None + + # 命中缓存:路径与 mtime 都没变 → 直接返回上次解析结果(运行时读但不重复开文件)。 + if _registry_cache is not None and _registry_path_cache == path_str and _registry_mtime == mtime: + return _registry_cache + + # registry 不存在:缓存空索引(全部回落内置原文);一次性告警。 + if mtime is None: + _warn_once( + f"reg-missing:{path_str}", + f"registry 不存在({path_str})→ tier2 prompt 全部回落内置原文(行为 = 现值)。", + ) + _registry_cache, _registry_mtime, _registry_path_cache = {}, None, path_str + return _registry_cache + + # 解析最小 YAML 子集(best-effort:任何异常都落空索引 + 告警,绝不抛)。 + entries: dict[str, dict[str, str]] = {} + try: + current_id: str | None = None + in_prompts = False + for raw in path.read_text(encoding="utf-8").splitlines(): + line = raw.rstrip("\n") + stripped = line.strip() + if not stripped or stripped.startswith("#"): + continue + # 顶层 `prompts:` 起列表段。 + if re.match(r"^prompts\s*:\s*(#.*)?$", line): + in_prompts = True + continue + if not in_prompts: + continue + # 新列表项 ` - id: xxx`。 + m = re.match(r"^\s*-\s+id\s*:\s*(.+)$", line) + if m: + pid = _strip_inline_comment(m.group(1)) + current_id = pid or None + if current_id: + entries[current_id] = {"id": current_id} + continue + # 列表项内键值 ` key: value`。 + m = re.match(r"^\s+([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(.*)$", line) + if m and current_id is not None: + entries[current_id][m.group(1)] = _strip_inline_comment(m.group(2)) + continue + # prompts 段内出现顶层新键(无缩进的字母起头)→ 列表段结束。 + if re.match(r"^[A-Za-z_]", line): + in_prompts = False + current_id = None + except Exception as exc: # noqa: BLE001 —— registry 读/解析异常都降级,绝不中断生成 + _warn_once( + f"reg-parse:{path_str}", + f"registry 解析失败({path_str}: {type(exc).__name__}: {exc})→ tier2 prompt 回落内置原文。", + ) + entries = {} + + _registry_cache, _registry_mtime, _registry_path_cache = entries, mtime, path_str + return _registry_cache + + +def _strip_inline_comment(value: str) -> str: + """剥 registry 值后的行内注释(` # ...`);引号包裹的值先去引号(与 orchestrator/prompts.py 同款)。 + + 本注册表的 id / file / version 均为简单标量,故只需处理「不在引号内的 # 起注释」。 + """ + out: list[str] = [] + in_quote: str | None = None + for ch in value: + if in_quote: + if ch == in_quote: + in_quote = None + out.append(ch) + elif ch in ("'", '"'): + in_quote = ch + out.append(ch) + elif ch == "#": + break # 注释起点(不在引号内) + else: + out.append(ch) + s = "".join(out).strip() + if len(s) >= 2 and s[0] == s[-1] and s[0] in ("'", '"'): + s = s[1:-1] + return s + + +def _strip_frontmatter(text: str) -> str: + """剥 prompt 文件的 YAML frontmatter(首行 --- 到下一个 ---),**逐字节返回正文本体**。 + + contracts/prompts 的 .md 都带 frontmatter 契约头(id/version/owner/...),正文才是 prompt 本体。 + 无 frontmatter(首行非 ---)→ 整篇都当正文(best-effort:不因缺 frontmatter 而判失败)。 + + ⚠️ 字节保真(本相红线):正文 **不做 .strip()** —— prompt 文本的首尾空白(尤其尾部换行)是 prompt + 本体的一部分,roles.py 的内置原文里有的常量带尾部 \\n、有的不带(系统提示拼接差异),必须逐字节还原 + 才能保证「registry 命中 == 内置原文」。本函数只剥掉「frontmatter 块 + 其后恰好一处分隔的空白」: + 去掉闭合 --- 那行,并吃掉紧随其后的换行与生成时写的一个空行(写文件时正文前固定垫 "\\n\\n"), + 使提取出的正文 == 写入时的 body 原文。生成器(本单元写 09-tier2-richgame/*.md)按此约定逐字节落盘。 + """ + lines = text.split("\n") + if not lines or lines[0].strip() != "---": + # 无 frontmatter:整篇即正文,逐字节返回(不 strip)。 + return text + # 找闭合 ---(从第 2 行起第一处单独的 ---)。 + close_idx = None + for idx in range(1, len(lines)): + if lines[idx].strip() == "---": + close_idx = idx + break + if close_idx is None: + # frontmatter 未闭合(只有一个 ---)→ 退化为整篇当正文(不抛、不 strip)。 + return text + # 正文 = 闭合 --- 之后的所有行。写文件时正文前固定垫了一个空行(见生成约定),故跳过紧随的一个空行, + # 其余逐字节保留(含尾部换行)。这样提取出的正文与写入的 body 原文逐字节一致。 + rest = lines[close_idx + 1:] + if rest and rest[0] == "": + rest = rest[1:] + return "\n".join(rest) + + +def _read_prompt_body(rel_file: str) -> str | None: + """读 registry file 字段所指 prompt 文件,剥 frontmatter 返回正文(带 mtime 缓存 + best-effort)。 + + rel_file 相对 prompts_dir() 解析(registry 的 file 字段就是相对 contracts/prompts/ 的路径)。 + 读不到 / 空文件 / 任何 IO 异常 → 返回 None(由上层回落内置原文 + 告警)。 + """ + path = (prompts_dir() / rel_file).resolve() + path_str = str(path) + with _lock: + try: + mtime = path.stat().st_mtime if path.exists() else None + except OSError: + mtime = None + # 文件不存在 → None(上层回落内置原文)。 + if mtime is None: + return None + # 命中缓存(mtime 未变)→ 返回缓存正文。 + cached = _file_cache.get(path_str) + if cached is not None and cached[0] == mtime: + return cached[1] + # 读 + 剥 frontmatter(best-effort:异常返回 None,不抛)。 + try: + body = _strip_frontmatter(path.read_text(encoding="utf-8")) + except Exception as exc: # noqa: BLE001 —— prompt 文件读异常降级,绝不中断生成 + _warn_once( + f"file-read:{path_str}", + f"prompt 文件读失败({path_str}: {type(exc).__name__}: {exc})→ 回落内置原文。", + ) + return None + _file_cache[path_str] = (mtime, body) + return body + + +def load(prompt_id: str, default: str) -> str: + """取一条 tier2 prompt 的当前原文:registry 引用的 .md 正文 > 调用方内置原文(default)。 + + Args: + prompt_id: registry.yaml 里的 prompt id(如 'tier2.writer-system')。 + default: **roles.py 里该 prompt 的内置原文**——registry 缺该 id / 文件不存在 / 读不出 / 正文为空时 + 的兜底,保证默认行为 == 现值(本相红线)。 + + Returns: + prompt 原文(registry 命中且非空 → 文件正文;否则 → default)。绝不抛(best-effort)。 + + 取值口径(任一级取到即用): + ① env TIER2_PROMPTS_DIR 指向的目录里 registry 引用的文件(缺省 = 仓内 contracts/prompts/); + ② 调用方 default(内置原文)。 + 任何一步失败(registry 缺 / id 未注册 / file 字段缺 / 文件不存在 / 读不出 / 正文为空)→ 回落 default + 告警。 + """ + registry = _load_registry() + entry = registry.get(prompt_id) + if not entry: + # registry 里没登记这条 id(未迁入 / registry 读失败)→ 回落内置原文。 + _warn_once( + f"id-missing:{prompt_id}", + f"prompt id 未在 registry 注册({prompt_id})→ 回落内置原文(行为 = 现值)。", + ) + return default + rel_file = entry.get("file") + if not rel_file: + _warn_once( + f"file-field-missing:{prompt_id}", + f"prompt {prompt_id} 的 registry 条目缺 file 字段 → 回落内置原文。", + ) + return default + body = _read_prompt_body(rel_file) + if not body: + # 文件不存在 / 读不出 / 正文为空 → 回落内置原文(_read_prompt_body 内已就 IO 异常告警; + # 这里再补一条「正文空/缺文件」的告警,便于排查)。 + _warn_once( + f"body-empty:{prompt_id}", + f"prompt {prompt_id} 的文件({rel_file})缺失或正文为空 → 回落内置原文(行为 = 现值)。", + ) + return default + return body + + +def render(text: str, variables: dict[str, Any] | None = None) -> str: + """把 {{input.}} 占位符替换成 variables 里的值(字符串精确替换,绝不用 str.format)。 + + 与 contracts/prompts 既有范本、orchestrator/prompts.py 的 {{input.xxx}} 约定一致;但本函数: + - 用字符串精确替换 + 容忍空白的正则替换两道(花括号内可有空白);**绝不用 str.format** —— tier2 多条 + prompt 正文含裸 JSON 花括号(如 player_system 的 {"fun":1-5}),str.format 会被它噎住报错; + - 非字符串值原样 str() 化(本线注入的全是字符串 brief/design/persona,简单 str 即可,不引 JSON 依赖); + - **不做残留占位符报错**(cheap-line orchestrator 那样 fail-fast 是批跑联调用;tier2 生成主链 best-effort, + 即便调用方漏供某变量也只是留下 {{input.x}} 字面、不中断 —— 但 roles.py 的调用点都齐供变量,不触此情形)。 + + Args: + text: 含 {{input.}} 占位符的模板(load() 取回的 registry 正文,或内置原文)。 + variables: {key: value};key 不带 input. 前缀。None / 空 → 原样返回 text。 + + Returns: + 替换后的文本。 + """ + if not variables: + return text + out = text + for key, value in variables.items(): + sval = value if isinstance(value, str) else str(value) + # 先替无空白的标准写法,再用正则替容忍空白的写法(两道都做,确保两种写法都命中)。 + out = out.replace("{{input.%s}}" % key, sval) + out = re.sub(r"\{\{\s*input\.%s\s*\}\}" % re.escape(key), lambda _m, v=sval: v, out) + return out + + +def reload() -> None: + """强制丢弃所有缓存,下次 load 重新读 registry 与文件(测试 / 手动热更用;运行时 mtime 已自动重读)。""" + global _registry_cache, _registry_mtime, _registry_path_cache, _file_cache + with _lock: + _registry_cache, _registry_mtime, _registry_path_cache = None, None, None + _file_cache = {} + + +# ── __main__ 自测块:不依赖 agentscope / 网络,只校验三级回落 + render(6c6g 可直接跑)──────────── +# 校验:python3 -c 'from worker import prompts'(import 干净)+ python3 worker/prompts.py(自测)。 +if __name__ == "__main__": # pragma: no cover —— 本地自测 + import tempfile + + def _check(label: str, cond: bool) -> None: + print(f"[{'OK' if cond else 'FAIL'}] {label}") + assert cond, label + + # ① 仓内 registry 命中:tier2.writer-system 应读到 09-tier2-richgame 下的真文件正文(非回落)。 + reload() + os.environ.pop("TIER2_PROMPTS_DIR", None) + body = load("tier2.writer-system", "INTERNAL_DEFAULT") + _check("仓内 registry 命中 writer-system(非回落)", body != "INTERNAL_DEFAULT" and len(body) > 100) + + # ② 未注册 id → 回落内置 default。 + _check("未注册 id 回落 default", load("tier2.不存在", "DEFAULT_X") == "DEFAULT_X") + + # ③ render:{{input.brief}} 精确替换;裸 JSON 花括号不受影响。 + tmpl = '题面:{{input.brief}};JSON 样例 {"fun":1-5} 必须原样保留;再来 {{ input.persona }}。' + got = render(tmpl, {"brief": "造面包店", "persona": "急性子"}) + _check("render 替换占位符", "造面包店" in got and "急性子" in got) + _check("render 保住裸 JSON 花括号", '{"fun":1-5}' in got) + _check("render 不残留占位符", "{{input." not in got and "{{ input." not in got) + + # ④ env TIER2_PROMPTS_DIR 指向空目录(无 registry)→ 全部回落内置原文,不抛。 + with tempfile.TemporaryDirectory() as d: + os.environ["TIER2_PROMPTS_DIR"] = d + reload() + _check("空目录(无 registry)回落内置原文", load("tier2.writer-system", "FALLBACK") == "FALLBACK") + os.environ.pop("TIER2_PROMPTS_DIR", None) + reload() + + print("\n[prompts] registry 命中 + 回落 + render + 空目录降级自测全部通过。") diff --git a/tier2/gen-worker/worker/roles.py b/tier2/gen-worker/worker/roles.py index a44a30f7..3cd10c8e 100644 --- a/tier2/gen-worker/worker/roles.py +++ b/tier2/gen-worker/worker/roles.py @@ -11,11 +11,40 @@ ④ 验收零自评:run_gates 的 verdict 由 judge 纯代码产出,agent 不给自己打分,只据门反馈改源。 阶段:本提示是「阶段 2 单写」的 system prompt(阶段 1 工作室多 agent 发散设计另走 design_system,见下)。 + +【prompt 经 Registry 外置(本单元 U2;prompt 即第 8 契约)】 +本模块的所有 system prompt 不再写死在这里,而是经 worker/prompts.py 从契约 #8 Prompt Registry +(contracts/prompts/registry.yaml + 09-tier2-richgame/*.md)**运行时读**:故【调一条 prompt = 改 Registry +对应 .md 正文重跑,不改本 Python】。读取走三级回落(env TIER2_PROMPTS_DIR 指向的目录 > 仓内 registry +引用的 .md > 本模块内置原文),**读不到 / 脏 → 回落内置原文 + 告警,绝不中断生成主链**(prompt-governance +§9 降级铁律)。本模块的 **对外接口(名字 / 调用形状 / 返回类型)逐字节不变** —— 下面每个内置 _BUILTIN_* +原文 = 外置前那条 prompt 的字面量;registry 命中且正文与内置原文逐字节一致时,行为与外置前完全相同 +(本相红线:默认行为字节不变,只有人去改 .md 才让新文本流通)。带参的 prompt(writer_system / +design_leader_system / player_system)在 registry 存模板(占位符 {{input.*}}),由 prompts.render 注入。 """ +# prompt 加载器:运行时按 id 从 Prompt Registry 读正文,缺失/脏 → 回落本模块内置原文(best-effort 绝不抛)。 +# 包内/直跑兼容导入(与 studio.py / design_team.py 同款兜底:直跑时把 gen-worker/ 加进 sys.path)。 +try: + from . import prompts +except ImportError: # pragma: no cover —— 直接 python 跑兜底 + import sys + from pathlib import Path + sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + from worker import prompts # type: ignore + + +# ════════════════════════════════════════════════════════════════════════════ +# 内置原文(_BUILTIN_*)—— 与外置前各 prompt 字面量【逐字节一致】的回落副本 +# 作用:registry 缺该 id / 文件不存在 / 读不出 / 正文为空时,load() 回落到这里,保证默认行为不漂移。 +# ⚠️ 维护纪律:改 prompt 优先改 registry 的 09-tier2-richgame/*.md(那是唯一事实源);这份内置原文只在 +# registry 失效时兜底,应与对应 .md 正文保持同值(语义不分叉)。带参的存「模板」(含 {{input.*}}), +# 与 registry .md 正文逐字节对齐。 +# ════════════════════════════════════════════════════════════════════════════ + # ── 阶段 1:设计 agent(把题面 → 富游戏设计稿;承袭 wg1 design 范式但换富游戏语义)── # 富游戏设计稿要点 = 多系统 + 数据表 + scene 树,而非 LittleJS 的单局 gatespec/driver 二分。 -DESIGN_SYSTEM = r"""你是富游戏的【设计 agent】。输入是一句话/一段题面,产出一份**简洁、可直接实现**的富游戏设计稿,供下游单写 agent 写成 Phaser 多文件工程。 +_BUILTIN_DESIGN_SYSTEM = r"""你是富游戏的【设计 agent】。输入是一句话/一段题面,产出一份**简洁、可直接实现**的富游戏设计稿,供下游单写 agent 写成 Phaser 多文件工程。 【只产出设计稿,绝不写代码】用紧凑中文条目覆盖: 1. 核心循环:玩家反复做什么、目标是什么(经营/合成/挂机类的主循环,一两句说清)。 @@ -45,6 +74,8 @@ DESIGN_SYSTEM = r"""你是富游戏的【设计 agent】。输入是一句话/ # (「至关重要 / 本质上 / 归根结底」)。直接陈述内容,别堆辞藻。 # 四面专家 worker 的写作风格公共前缀(每个 worker system 都带,免得退回 AI 味)。 +# 注:这是构造内置原文的内部拼接片段(不是对外接口),外置后各专家 prompt 整条由 registry .md 提供, +# 本片段只参与「内置原文」的字面量构造(作 registry 失效时的回落兜底)。 _DESIGN_PROSE_RULE = ( "【写法铁律】只产出你这一域的设计结论,写成给工程师读的紧凑中文条目/短散文:逻辑连续、给具体" "数值和规格、能被下游直接照着实现。禁三样:① 元叙述(别写「本设计讲什么/下面介绍/综上」这类讲" @@ -54,7 +85,7 @@ _DESIGN_PROSE_RULE = ( ) # 专家 1:系统设计师 —— 核心循环 + 多系统拆解 + 耦合点(富游戏「不是三座孤岛」的命门)。 -DESIGN_SYSTEMS_DESIGNER = ( +_BUILTIN_DESIGN_SYSTEMS_DESIGNER = ( "你是富游戏工作室里的【系统设计师】。给你一段游戏题面,你只负责系统骨架这一面:\n" "1. 核心循环:玩家反复做什么、目标是什么(一两句说清,经营/合成/挂机类的主循环)。\n" "2. 多系统拆解:列出 2~3 个相互耦合的系统(如资源系统/合成系统/订单系统)。\n" @@ -66,7 +97,7 @@ DESIGN_SYSTEMS_DESIGNER = ( ) # 专家 2:经济数值设计师 —— 数据表 + 平衡数值 + 胜负条件(必须有失败态)。 -DESIGN_ECONOMY_DESIGNER = ( +_BUILTIN_DESIGN_ECONOMY_DESIGNER = ( "你是富游戏工作室里的【经济数值设计师】。给你题面(及系统设计师已给的系统骨架,若附),你只负责" "经济与数值这一面:\n" "1. 数据表:列出驱动游戏的数据表(物品表/合成链/订单模板/平衡数值),给具体条目数与示例值。\n" @@ -77,7 +108,7 @@ DESIGN_ECONOMY_DESIGNER = ( ) # 专家 3:玩法关卡设计师 —— 玩法节奏 + 关卡/阶段编排 + 操作手感。 -DESIGN_LEVEL_DESIGNER = ( +_BUILTIN_DESIGN_LEVEL_DESIGNER = ( "你是富游戏工作室里的【玩法关卡设计师】。给你题面(及系统/经济设计,若附),你只负责玩法与节奏" "这一面:\n" "1. 玩法节奏:一局大致怎么推进(开局教学→中期张力→收尾),玩家的关键决策点是什么。\n" @@ -88,7 +119,7 @@ DESIGN_LEVEL_DESIGNER = ( ) # 专家 4:表现层与可玩性设计师 —— Phaser scene 树 + UI/HUD 布局 + 进展反馈。 -DESIGN_PRESENTATION_DESIGNER = ( +_BUILTIN_DESIGN_PRESENTATION_DESIGNER = ( "你是富游戏工作室里的【表现层与可玩性设计师】。给你题面(及系统/经济/玩法设计,若附),你只负责" "表现层与可玩性这一面(逻辑像素 390×844 竖屏):\n" "1. Phaser scene 树:大致分几个 scene(如 Boot/Play/UI 叠层),各 scene 负责什么。\n" @@ -99,6 +130,112 @@ DESIGN_PRESENTATION_DESIGNER = ( ) +# ── 阶段 1 工作室 leader 的 system prompt 模板(带参 brief;registry 存模板,占位符 {{input.brief}})── +# 外置前是 design_leader_system(brief) 函数体内拼接的字面量(含 _DESIGN_PROSE_RULE 的 ~400 字变体 + +# brief 注入处)。此处把 brief 注入处写成 {{input.brief}} 占位符,由 prompts.render 注入 (brief or "").strip()。 +# 注:_DESIGN_PROSE_RULE 的「~180 字内」在 leader 里被替换为「整稿控制在 ~400 字内」——内置原文直接固化该变体, +# 不再运行时 .replace(保证模板自洽、registry .md 正文即最终文本)。 +_BUILTIN_LEADER_TEMPLATE = ( + "你是一间富游戏设计工作室的【主设计师(leader)】。你手里有四位专家,各是一个工具:\n" + "- design_systems:系统设计师(核心循环/多系统拆解/耦合点/语义 state)。\n" + "- design_economy:经济数值设计师(数据表/经济流转/胜负条件/难度)。\n" + "- design_level:玩法关卡设计师(玩法节奏/关卡编排/操作手感/失败重试)。\n" + "- design_presentation:表现层与可玩性设计师(scene 树/UI 布局/进展反馈/可玩性)。\n\n" + "【你的工作流(星形:你是唯一中心,专家之间不互通)】\n" + "① 先读懂题面,想清这款富游戏的大方向;\n" + "② 依次调四个专家工具,把题面(必要时附上你已收到的上游结论,让后调的专家衔接得上)交给各专家," + "收齐四面设计结论;\n" + "③ 把四面结论**整合成一份连贯的富游戏设计稿**——不是把四段拼贴,而是消解彼此的矛盾(如数值与" + "玩法节奏对不上、表现层漏了某个系统),让系统/经济/玩法/表现层四面咬合成一个能直接实现的整体。\n\n" + "【最终设计稿(你的最后一条回答,纯文本,不再调工具)必须覆盖,与下游单写 agent 的实现依据对齐】:\n" + "1. 核心循环;2. 多系统拆解 + 耦合点(产出喂给谁的输入);3. 数据表(条目数 + 示例值);" + "4. 胜负条件(含失败态 + 数值阈值);5. Phaser scene 树;6. UI 表现层与命中映射要点(390×844 竖屏);" + "7. 可观测语义 state(导出哪些 per-品类字段,phase 终态不变量)。\n\n" + + _DESIGN_PROSE_RULE.replace("控制在 ~180 字内", "整稿控制在 ~400 字内") + + "\n【题面】\n" + "{{input.brief}}" + "\n" +) + + +# ── 阶段 2 单写 agent 的 system prompt 固定前缀(registry 存这段固定大头,无占位符)── +# 外置前是 writer_system(...) 函数体内 parts[0]+parts[1]+parts[2] 的拼接(任务说明 + 九工具清单 + +# 工作纪律铁律 + 典型循环节奏)。题面 / 品类靶 / 设计稿三块是按入参条件追加的,**不进本前缀**,仍由 +# writer_system 在前缀之后用 Python 条件拼接(保证带参装配逐字节不变)。 +_BUILTIN_WRITER_PREFIX = "".join([ + "你是富游戏的【单写实现 agent】。你的任务:在一个 Phaser 多文件源工程里,自治地写出一款" + "**多系统耦合、能真玩到终态、能过验收门**的富游戏(经营/合成/挂机类)。\n", + "【你手里的工具(在循环里自己调,不要在回答里贴代码块)】\n" + "- scaffold_init:开局铺一套先天过 boot 的 Phaser 空骨架(已含 scene 树脚手架与留空数据表)。\n" + "- write_source:把一批源文件写进工程(多文件、允许 import;这是你写那约 56% 表现层的主手段)。\n" + "- validate_datatable:校验 data/datatable.gold.json 是否符合平台锁定 schema + 可达性(填完表先调它,省一整轮)。\n" + "- build:esbuild 打包当前工程(入口 src/main.js);失败会回你 esbuild 报错(含数据表预检),据此改源。\n" + "- headless_check:全套真玩门前的便宜快筛(语法/bundle),早断省一整轮真玩。\n" + "- run_gates:真浏览器真玩,跑 L1 九门 + 富游戏三门,返回机器判的 verdict。\n" + "- read_verdict:读回上次 run_gates 的裁决与失败门摘要,据它知道该改哪。\n" + "- screenshot / query_asset:截图取证 / 查可用资产(取证用,绝不当过关依据)。\n" + "- finish:你收敛后吐出最终源工程(参数形状 = tier2 源项目契约),交付落库。\n", + "【工作纪律(铁律)】\n" + "① 多文件 src/ 工程:入口恒为 src/main.js;允许 import(Phaser、scene 间);**没有单 export default 工厂、" + "没有 getEngine() 受控面**(那是 LittleJS 廉价线的约定,本线完全不同)。\n" + "② 富游戏要真耦合:多个系统共享状态、互相调用(合成消耗资源、完单产出金币、解锁花金币)," + "**绝不能写成互不相干的三座孤岛**。\n" + "③ 必须能真玩到终态:游戏无 emit 通道,把终态焊成可轮询 latch —— 跑到赢/输时把导出的 phase 字段" + "焊成终态值('win'/'lose'/'gameover')并驻留不回弹,宿主每帧轮询去读。\n" + "④ 必须导出语义 state:游戏要暴露一个可观测视图函数(返回 {phase, ...品类语义字段})," + "确定性门据它真玩取证。这个视图**只读、只供测试**,渲染层与玩家界面绝不可显示它、不得据它给提示。\n" + "⑤ 验收零自评 + 看到 fix 不许停:run_gates 的 verdict 是机器判的、不是你说了算。" + "**看到 decision=fix / 有失败门,绝不能就此收尾或宣布做好——必须 read_verdict 看清哪门挂了," + "据失败门 write_source 针对性修,再 build→run_gates,一轮轮逼到门绿。** 门没绿时调 finish 会被拒。\n" + "⑥ 数据表是头号雷区:平台 tables.js 按固定 key 读 data/datatable.gold.json——**就地填 sentinel 的值," + "别改 key、别自创 schema**(详见下方品类约定)。填完先 validate_datatable 确认结构对,再 build。\n" + "⑦ 平台文件写死、绝不许改:**你只能改 src/scenes/play-scene.js(表现层)+ data/datatable.gold.json(数据表)," + "或在 src/scenes/ 下新增表现文件**。src/main.js(装载胶水)、src/game-core.js、src/systems/*、src/layout.js、" + "src/data/tables.js、src/util/* 全是平台锁定的——它们已把引擎接线(recHook 记 addCoins/consumeIngredient)、" + "状态机、结算、latch、readState 导出、命中映射坐标都写好了。**改它们 = 破坏接线**(实测:重写 main.js 丢了 " + "recHook → F 门 calls=0、合成不记引擎调用、G/H 门连环挂)。write_source 会拒绝改这些文件。\n" + "⑧ 收敛即 finish:门全绿才调 finish 交付;别无谓地反复刷探索,也别没绿就想 finish。\n", + "【典型循环节奏】scaffold_init(开局一次)→ write_source 填表现层 → build →(失败则据日志改源再 build)" + "→ headless_check 快筛 → run_gates 真玩 → read_verdict 看哪门没过 → write_source 针对性修 →" + " 再 run_gates …… 直到门全绿 → finish。\n", +]) + + +# ── L3 player panel(软检,只评分绝不当门;承袭 wg1 player 范式但换富游戏维度)模板(带参 persona)── +# 外置前是 player_system(persona) 的 f-string;此处把 {persona} 注入处写成 {{input.persona}} 占位符, +# 由 prompts.render 注入(注意外置前 persona 未做 .strip(),故注入时也原样传入,保证字节不变)。 +_BUILTIN_PLAYER_TEMPLATE = ( + "你是一名富游戏的【玩家 agent】,人格设定:{{input.persona}}。\n" + "你会拿到一款富游戏(经营/合成类)的【真玩取证数据】(确定性 harness:是否真渲染、是否真玩到终态、" + "终局语义 state 快照如 coins/orders),可能还附【首帧截图】与【玩后截图】。" + "请像真玩家那样、严格基于**证据**判断(不要脑补没看到的东西)。\n\n" + "【先判「系统真联动」再谈好玩】对比首帧↔玩后、看语义 state:\n" + "- 若多个系统看不出耦合(合成没消耗资源 / 完单金币没涨 / 数据零变化),或没玩到终态 → 判【空心富游戏】:" + "fun 给 1~2、verdict=fix。\n" + "- 截图里看不到富游戏该有的多面板/进展反馈 → 按「表现层缺失」扣分。\n\n" + "【好玩 fun(1-5) 锚——拉开档】1=不能玩/空心;2=能跑但系统割裂、无经济压力;3=基础可玩(系统联动成立、有反馈);" + "4=经营有张力、看得到进展、想再玩;5=多系统咬合、节奏与反馈到位。\n" + "【完整度 completeness(1-5)】核心循环/多系统联动/胜负态是否齐全(空心或缺胜负 → ≤2)。\n" + "【verdict】真联动且至少基础可玩(fun≥3)→ pass;空心或明显体验缺陷 → fix。\n" + '【只输出 JSON】:{"completeness":1-5, "fun":1-5, "problems":["..."], "verdict":"pass"或"fix", "note":"一句话总评"}。' +) + + +# ════════════════════════════════════════════════════════════════════════════ +# 对外接口(名字 / 调用形状 / 返回类型逐字节不变)—— 经 prompts.load 取 registry 正文,缺失/脏回落内置原文 +# 常量类:模块导入时取一次(system prompt 是相对稳定的资产,非每 run 重算的热路径;改 registry .md 后 +# 新进程即生效。若需同进程热更,调 prompts.reload() 后重新 import 或显式取——本线 worker 是单次进程跑, +# import 时取已满足「调 prompt=改文件重跑」)。带参类:每次调用时取(保证带参装配读当前 registry)。 +# ════════════════════════════════════════════════════════════════════════════ + +# 阶段 1 单 design agent 兜底(design_team 失败时 degrade 回落用)。 +DESIGN_SYSTEM = prompts.load("tier2.design-system", _BUILTIN_DESIGN_SYSTEM) + +# 阶段 1 四面专家 worker 的 system prompt(design_team.py 按这四个名字装进 leader 的工具)。 +DESIGN_SYSTEMS_DESIGNER = prompts.load("tier2.design-systems-designer", _BUILTIN_DESIGN_SYSTEMS_DESIGNER) +DESIGN_ECONOMY_DESIGNER = prompts.load("tier2.design-economy-designer", _BUILTIN_DESIGN_ECONOMY_DESIGNER) +DESIGN_LEVEL_DESIGNER = prompts.load("tier2.design-level-designer", _BUILTIN_DESIGN_LEVEL_DESIGNER) +DESIGN_PRESENTATION_DESIGNER = prompts.load("tier2.design-presentation-designer", _BUILTIN_DESIGN_PRESENTATION_DESIGNER) + + def design_leader_system(brief: str) -> str: """阶段 1 工作室 leader 的 system prompt:拆解题面 → 调四专家 worker 工具 → 汇总成连贯设计稿。 @@ -106,75 +243,30 @@ def design_leader_system(brief: str) -> str: 收齐各域结论后,亲笔整合成**一份连贯的富游戏设计稿**(产物形状 = 阶段 2 单写消费的同款 str, 内容更丰富而已)。汇总稿要素与单 agent 兜底的 DESIGN_SYSTEM 对齐(七要点),保证下游接口不变。 + prompt 外置:模板经 prompts.load 从 registry(id=tier2.design-leader-system)读,缺失/脏回落内置模板; + brief 经 prompts.render 注入 {{input.brief}}(注入值 = (brief or "").strip(),与外置前一致)。 + Args: brief: 游戏题面(嵌进 system,让 leader 一开始就握住要拆什么)。 """ - return ( - "你是一间富游戏设计工作室的【主设计师(leader)】。你手里有四位专家,各是一个工具:\n" - "- design_systems:系统设计师(核心循环/多系统拆解/耦合点/语义 state)。\n" - "- design_economy:经济数值设计师(数据表/经济流转/胜负条件/难度)。\n" - "- design_level:玩法关卡设计师(玩法节奏/关卡编排/操作手感/失败重试)。\n" - "- design_presentation:表现层与可玩性设计师(scene 树/UI 布局/进展反馈/可玩性)。\n\n" - "【你的工作流(星形:你是唯一中心,专家之间不互通)】\n" - "① 先读懂题面,想清这款富游戏的大方向;\n" - "② 依次调四个专家工具,把题面(必要时附上你已收到的上游结论,让后调的专家衔接得上)交给各专家," - "收齐四面设计结论;\n" - "③ 把四面结论**整合成一份连贯的富游戏设计稿**——不是把四段拼贴,而是消解彼此的矛盾(如数值与" - "玩法节奏对不上、表现层漏了某个系统),让系统/经济/玩法/表现层四面咬合成一个能直接实现的整体。\n\n" - "【最终设计稿(你的最后一条回答,纯文本,不再调工具)必须覆盖,与下游单写 agent 的实现依据对齐】:\n" - "1. 核心循环;2. 多系统拆解 + 耦合点(产出喂给谁的输入);3. 数据表(条目数 + 示例值);" - "4. 胜负条件(含失败态 + 数值阈值);5. Phaser scene 树;6. UI 表现层与命中映射要点(390×844 竖屏);" - "7. 可观测语义 state(导出哪些 per-品类字段,phase 终态不变量)。\n\n" - + _DESIGN_PROSE_RULE.replace("控制在 ~180 字内", "整稿控制在 ~400 字内") - + "\n【题面】\n" + (brief or "").strip() + "\n" - ) + template = prompts.load("tier2.design-leader-system", _BUILTIN_LEADER_TEMPLATE) + return prompts.render(template, {"brief": (brief or "").strip()}) def writer_system(brief: str, design_text: str = "", *, fixture_hint: str = "") -> str: """阶段 2 单写 agent 的 system prompt:握九工具、ReAct 多轮自治写 Phaser 富游戏工程。 + prompt 外置:固定前缀(任务/九工具/工作纪律/循环节奏)经 prompts.load 从 registry + (id=tier2.writer-system)读,缺失/脏回落内置前缀;题面 / 品类靶 / 设计稿三块仍按入参条件在前缀 + 之后用 Python 拼接(保证带参装配逐字节不变 —— 这是金标 fixture 无 token 装配冒烟必须字节不漂的关键)。 + Args: brief: 一句话/题面。 design_text: 阶段 1 设计稿(若有;合进上下文供单写 agent 实现)。 fixture_hint: 可选的品类/靶子提示(如 mini-肥鹅 三系统约定),引导语义 state 导出与门对齐。 """ - parts = [ - "你是富游戏的【单写实现 agent】。你的任务:在一个 Phaser 多文件源工程里,自治地写出一款" - "**多系统耦合、能真玩到终态、能过验收门**的富游戏(经营/合成/挂机类)。\n", - "【你手里的工具(在循环里自己调,不要在回答里贴代码块)】\n" - "- scaffold_init:开局铺一套先天过 boot 的 Phaser 空骨架(已含 scene 树脚手架与留空数据表)。\n" - "- write_source:把一批源文件写进工程(多文件、允许 import;这是你写那约 56% 表现层的主手段)。\n" - "- validate_datatable:校验 data/datatable.gold.json 是否符合平台锁定 schema + 可达性(填完表先调它,省一整轮)。\n" - "- build:esbuild 打包当前工程(入口 src/main.js);失败会回你 esbuild 报错(含数据表预检),据此改源。\n" - "- headless_check:全套真玩门前的便宜快筛(语法/bundle),早断省一整轮真玩。\n" - "- run_gates:真浏览器真玩,跑 L1 九门 + 富游戏三门,返回机器判的 verdict。\n" - "- read_verdict:读回上次 run_gates 的裁决与失败门摘要,据它知道该改哪。\n" - "- screenshot / query_asset:截图取证 / 查可用资产(取证用,绝不当过关依据)。\n" - "- finish:你收敛后吐出最终源工程(参数形状 = tier2 源项目契约),交付落库。\n", - "【工作纪律(铁律)】\n" - "① 多文件 src/ 工程:入口恒为 src/main.js;允许 import(Phaser、scene 间);**没有单 export default 工厂、" - "没有 getEngine() 受控面**(那是 LittleJS 廉价线的约定,本线完全不同)。\n" - "② 富游戏要真耦合:多个系统共享状态、互相调用(合成消耗资源、完单产出金币、解锁花金币)," - "**绝不能写成互不相干的三座孤岛**。\n" - "③ 必须能真玩到终态:游戏无 emit 通道,把终态焊成可轮询 latch —— 跑到赢/输时把导出的 phase 字段" - "焊成终态值('win'/'lose'/'gameover')并驻留不回弹,宿主每帧轮询去读。\n" - "④ 必须导出语义 state:游戏要暴露一个可观测视图函数(返回 {phase, ...品类语义字段})," - "确定性门据它真玩取证。这个视图**只读、只供测试**,渲染层与玩家界面绝不可显示它、不得据它给提示。\n" - "⑤ 验收零自评 + 看到 fix 不许停:run_gates 的 verdict 是机器判的、不是你说了算。" - "**看到 decision=fix / 有失败门,绝不能就此收尾或宣布做好——必须 read_verdict 看清哪门挂了," - "据失败门 write_source 针对性修,再 build→run_gates,一轮轮逼到门绿。** 门没绿时调 finish 会被拒。\n" - "⑥ 数据表是头号雷区:平台 tables.js 按固定 key 读 data/datatable.gold.json——**就地填 sentinel 的值," - "别改 key、别自创 schema**(详见下方品类约定)。填完先 validate_datatable 确认结构对,再 build。\n" - "⑦ 平台文件写死、绝不许改:**你只能改 src/scenes/play-scene.js(表现层)+ data/datatable.gold.json(数据表)," - "或在 src/scenes/ 下新增表现文件**。src/main.js(装载胶水)、src/game-core.js、src/systems/*、src/layout.js、" - "src/data/tables.js、src/util/* 全是平台锁定的——它们已把引擎接线(recHook 记 addCoins/consumeIngredient)、" - "状态机、结算、latch、readState 导出、命中映射坐标都写好了。**改它们 = 破坏接线**(实测:重写 main.js 丢了 " - "recHook → F 门 calls=0、合成不记引擎调用、G/H 门连环挂)。write_source 会拒绝改这些文件。\n" - "⑧ 收敛即 finish:门全绿才调 finish 交付;别无谓地反复刷探索,也别没绿就想 finish。\n", - "【典型循环节奏】scaffold_init(开局一次)→ write_source 填表现层 → build →(失败则据日志改源再 build)" - "→ headless_check 快筛 → run_gates 真玩 → read_verdict 看哪门没过 → write_source 针对性修 →" - " 再 run_gates …… 直到门全绿 → finish。\n", - ] + prefix = prompts.load("tier2.writer-system", _BUILTIN_WRITER_PREFIX) + parts = [prefix] if fixture_hint: parts.append("【品类/靶子约定】\n" + fixture_hint.strip() + "\n") parts.append("【题面】\n" + (brief or "").strip() + "\n") @@ -185,19 +277,10 @@ def writer_system(brief: str, design_text: str = "", *, fixture_hint: str = "") # ── L3 player panel(软检,只评分绝不当门;承袭 wg1 player 范式但换富游戏维度)── def player_system(persona: str) -> str: - """L3 软检玩家 agent system prompt(看截图 + 语义 state 判富游戏体验;只评分,不参与 decision)。""" - return ( - f"你是一名富游戏的【玩家 agent】,人格设定:{persona}。\n" - "你会拿到一款富游戏(经营/合成类)的【真玩取证数据】(确定性 harness:是否真渲染、是否真玩到终态、" - "终局语义 state 快照如 coins/orders),可能还附【首帧截图】与【玩后截图】。" - "请像真玩家那样、严格基于**证据**判断(不要脑补没看到的东西)。\n\n" - "【先判「系统真联动」再谈好玩】对比首帧↔玩后、看语义 state:\n" - "- 若多个系统看不出耦合(合成没消耗资源 / 完单金币没涨 / 数据零变化),或没玩到终态 → 判【空心富游戏】:" - "fun 给 1~2、verdict=fix。\n" - "- 截图里看不到富游戏该有的多面板/进展反馈 → 按「表现层缺失」扣分。\n\n" - "【好玩 fun(1-5) 锚——拉开档】1=不能玩/空心;2=能跑但系统割裂、无经济压力;3=基础可玩(系统联动成立、有反馈);" - "4=经营有张力、看得到进展、想再玩;5=多系统咬合、节奏与反馈到位。\n" - "【完整度 completeness(1-5)】核心循环/多系统联动/胜负态是否齐全(空心或缺胜负 → ≤2)。\n" - "【verdict】真联动且至少基础可玩(fun≥3)→ pass;空心或明显体验缺陷 → fix。\n" - '【只输出 JSON】:{"completeness":1-5, "fun":1-5, "problems":["..."], "verdict":"pass"或"fix", "note":"一句话总评"}。' - ) + """L3 软检玩家 agent system prompt(看截图 + 语义 state 判富游戏体验;只评分,不参与 decision)。 + + prompt 外置:模板经 prompts.load 从 registry(id=tier2.player-system)读,缺失/脏回落内置模板; + persona 经 prompts.render 注入 {{input.persona}}(外置前 persona 未 .strip(),故原样注入,字节不变)。 + """ + template = prompts.load("tier2.player-system", _BUILTIN_PLAYER_TEMPLATE) + return prompts.render(template, {"persona": persona})