feat(tier2): B门-配置外置——生成器旋钮 + prompt 全部数据驱动(调生成=改数据重跑、不改码)

让"调生成"从改代码变成改数据,直接给"优化 20% 过门率"的迭代提速。默认值=现有硬编码值、行为字节不变,改外部数据才生效。

【生成配置层】genconfig.py + tier2/config/generation.yaml:
把硬编码旋钮(model 选择 / 熔断·预算 rmb_hard_limit=3.0 / 迭代 writer_max_iters=40 / 工作室 Team leader_max_iters=12·per_expert_cap=2 / 品类参数)
抽成中央 YAML、运行时读(env TIER2_GENCONFIG 覆盖 + 缺省回落内置默认);config/middleware/studio/design_team 改读 genconfig。
回归:4 关键旋钮默认取值 == 改造前硬编码值;整 worker 包编译过。
(未externalize:fallback_tree ★阈值 + harness HUMAN_MIN_PATIENCE 门阈值——门阈值次要、gates 现 advisory,留后续。)

【Prompt 外置】prompts.py + contracts/prompts/09-tier2-richgame/(8 .md)+ registry.yaml:
tier2 的 8 条 system prompt(writer/design-leader/4 专家/player/design-system)外置到 Prompt Registry(契约#8),
运行时按 id 读 + 三级回落(env > registry .md > 内置原文)+ best-effort 不中断;{{input.*}} 字符串替换(避开裸 JSON 花括号噎 format)。
roles.py 改读 registry 但对外接口字节不变(studio/design_team call site 零改)。
回归:13/13 产物逐字节 == 改造前(含带参 design_leader/writer 四种入参组合);registry 缺失回落内置原文。Tier0/1 既有 8 条 prompt 零改动(red line)。

全 tier2/+contracts/prompts/ 内、零碰 Tier0/1、compileall + forbidden-import 通过。注:两 workflow 中 U1(genconfig)结构化报告解析失败(基建层),代码已落盘且主会话直接复核(编译+默认值回归过)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
zizi 2026-06-24 05:32:37 +00:00
parent 5b35e61f89
commit c9dbd50d80
17 changed files with 1433 additions and 115 deletions

View File

@ -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.0tier2 prompt 经 Registry 外置首版(正文 = roles.py 内置原文逐字节对齐,默认行为不变)。
stage: "09-tier2-richgame"
owner: WS2
tier: tier2
# engineAgentScope 2.0.2 ReActM3 走 new-api Anthropic 原生路);具体 model 由 worker/config 参数化prompt 不写死。
engine: agentscope-react
---
你是富游戏工作室里的【经济数值设计师】。给你题面(及系统设计师已给的系统骨架,若附),你只负责经济与数值这一面:
1. 数据表:列出驱动游戏的数据表(物品表/合成链/订单模板/平衡数值),给具体条目数与示例值。
2. 经济流转:资源怎么进、怎么耗、金币怎么涨,给关键单价/产出/成本的具体数,让经济能自洽地转。
3. 胜负条件:怎样算赢、怎样算输——必须有失败态(破产/连续流失/资源耗尽任一),给数值阈值。
4. 难度曲线:开局到中期的压力怎么递增(订单变难/成本上升),给一两档具体数。
【写法铁律】只产出你这一域的设计结论,写成给工程师读的紧凑中文条目/短散文:逻辑连续、给具体数值和规格、能被下游直接照着实现。禁三样:① 元叙述(别写「本设计讲什么/下面介绍/综上」这类讲述文档自身的话,直接陈述内容);② 自造唬人新词、堆缩写黑话(项目既有术语如 phase/latch/scene 可用);③ 套话空强调(去掉「至关重要/本质上/归根结底/值得注意的是」)。绝不写代码、不写 Phaser/esbuild 实现细节。控制在 ~180 字内,聚焦决策。

View File

@ -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.0tier2 prompt 经 Registry 外置首版(正文 = roles.py 内置原文逐字节对齐,默认行为不变)。
stage: "09-tier2-richgame"
owner: WS2
tier: tier2
# engineAgentScope 2.0.2 ReActM3 走 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}}

View File

@ -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.0tier2 prompt 经 Registry 外置首版(正文 = roles.py 内置原文逐字节对齐,默认行为不变)。
stage: "09-tier2-richgame"
owner: WS2
tier: tier2
# engineAgentScope 2.0.2 ReActM3 走 new-api Anthropic 原生路);具体 model 由 worker/config 参数化prompt 不写死。
engine: agentscope-react
---
你是富游戏工作室里的【玩法关卡设计师】。给你题面(及系统/经济设计,若附),你只负责玩法与节奏这一面:
1. 玩法节奏:一局大致怎么推进(开局教学→中期张力→收尾),玩家的关键决策点是什么。
2. 关卡/阶段编排:若分阶段(如订单批次/解锁档位),各阶段的目标与触发条件。
3. 操作手感:玩家主要靠什么操作(点击合成/拖拽/点单),命中要直觉、反馈要即时。
4. 失败-重试体验:输了之后玩家看到什么、能不能快速再来,别让失败态变成死局白屏。
【写法铁律】只产出你这一域的设计结论,写成给工程师读的紧凑中文条目/短散文:逻辑连续、给具体数值和规格、能被下游直接照着实现。禁三样:① 元叙述(别写「本设计讲什么/下面介绍/综上」这类讲述文档自身的话,直接陈述内容);② 自造唬人新词、堆缩写黑话(项目既有术语如 phase/latch/scene 可用);③ 套话空强调(去掉「至关重要/本质上/归根结底/值得注意的是」)。绝不写代码、不写 Phaser/esbuild 实现细节。控制在 ~180 字内,聚焦决策。

View File

@ -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.0tier2 prompt 经 Registry 外置首版(正文 = roles.py 内置原文逐字节对齐,默认行为不变)。
stage: "09-tier2-richgame"
owner: WS2
tier: tier2
# engineAgentScope 2.0.2 ReActM3 走 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 字内,聚焦决策。

View File

@ -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.0tier2 prompt 经 Registry 外置首版(正文 = roles.py 内置原文逐字节对齐,默认行为不变)。
stage: "09-tier2-richgame"
owner: WS2
tier: tier2
# engineAgentScope 2.0.2 ReActM3 走 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 实现细节。

View File

@ -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.0tier2 prompt 经 Registry 外置首版(正文 = roles.py 内置原文逐字节对齐,默认行为不变)。
stage: "09-tier2-richgame"
owner: WS2
tier: tier2
# engineAgentScope 2.0.2 ReActM3 走 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 字内,聚焦决策。

View File

@ -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.0tier2 prompt 经 Registry 外置首版(正文 = roles.py 内置原文逐字节对齐,默认行为不变)。
stage: "09-tier2-richgame"
owner: WS2
tier: tier2
# engineAgentScope 2.0.2 ReActM3 走 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":"一句话总评"}。

View File

@ -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.0tier2 prompt 经 Registry 外置首版(正文 = roles.py 内置原文逐字节对齐,默认行为不变)。
stage: "09-tier2-richgame"
owner: WS2
tier: tier2
# engineAgentScope 2.0.2 ReActM3 走 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。

View File

@ -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 富游戏自治生成线 prompt09-tier2-richgamerange=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

View File

@ -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<r<0.9)
ctx_reserve_ratio: 0.1 # 历史压缩保留最近上下文比例(须 < trigger_ratio)
# ── budget · ¥ 累进硬闸 + 四道熔断 + 软刹(消费方:worker/middleware.py)────────────────────
# 这一区是「自治多轮 ReAct 不能无限烧钱」的硬地板。调它们 = 调成本上限与失控保护强度。
budget:
rmb_hard_limit: 3.0 # ★ 单局 ¥ 硬上限(越「已花+本次预估」即 fail-closed 抛熔断);0号 spike 观测约 ¥0.x~¥1,留 ~3×头寸
max_tool_calls: 60 # step_cap 步数硬顶(工具调用总次数;C3 建议单系统≤8/整局≤40,这是工具粒度总闸)
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×硬顶时往 system prompt 注入收敛提醒)
group_ratio: 1.0 # new-api 分组倍率(¥ 折算用)
est_prompt_tokens: 12000 # ¥ 闸首调保守预估的 prompt token 量级(系统提示+历史+设计稿;调用后按实测覆盖)
est_completion_tokens: 2000 # ¥ 闸首调保守预估的补全 token 量级(一轮补全+tool_use)
# ── iteration · 单写 ReAct 轮数 + 外层有界 resume(消费方:worker/agent_loop/studio.py)──────────
# 这一区是「优化过门率」最直接的着力点之一:放开/收紧自治轮数与续修次数。
iteration:
writer_max_iters: 40 # 单写 ReAct 放开的最大轮数(C3 建议整局 ≤40;放太大烧 token,收太小修不完)
max_resumes: 6 # 外层有界自纠 resume 上限(agent 过早停下、门未绿且有预算时,带反馈再踹回去续修的次数)
# ── design_team · 阶段 1 工作室星形多 agent 设计预算(消费方:worker/agent_loop/design_team.py)──
# 设计阶段是过门头号杠杆(图说 C1);这一区控制设计团队烧多少 token、跑多久。
design_team:
leader_max_iters: 12 # leader ReAct 总轮数上限(= 调四专家 + 汇总的天花板;默认够各专家 ≤2 次 + 汇总)
per_expert_cap: 2 # 单专家被 leader 调用次数上限(防 leader 反复刷同一专家烧 token)
timeout_s: 240.0 # 整设计团队墙钟硬超时(秒;超时 degrade 回单 agent 设计,不中断主链)
# ── gates · 退路树四阈值 + harness 门阈值 ──────────────────────────────────────────────
# 退路树阈值消费方:worker/fallback_tree.py(go/no-go 判定,纯分析侧,import 时读)。
# harness 阈值消费方:tier2/harness/play-phaser.cdp.cjs(Node;经 env TIER2_GEN__GATES__* 注入,
# 或读 genconfig.export_for_harness() 导出的 JSON;.cjs 端按「env > 内置默认」消费,默认 = 现值)。
# ★ 这些多为 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 对齐)

View File

@ -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

View File

@ -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 交付。"

View File

@ -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,

View File

@ -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__<AREA>__<KEY>(全大写双下划线分隔)临时压一个值不必改文件,
适合这一批我想试 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>(双下划线分隔 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__<AREA>__<KEY>(全大写双下划线分隔);
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 导出自测全部通过。")

View File

@ -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,

View File

@ -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-governanceprompt 即第 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.<key>}}( contracts/prompts 既有范本orchestrator/prompts.py 同款),render()
替换注意:tier2 的若干 prompt 正文含 JSON 花括号( player_system {"fun":...}), render
**字符串精确替换**(只替换 {{input.<key>}} 字面),绝不用 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 → <repo-root>,再进 contracts/prompts/。
# 与 genconfig 的「从 __file__ 推 tier2/ 根」同源口径(这里多上溯一级到 repo 根)。
_REPO_ROOT = Path(__file__).resolve().parents[3]
_DEFAULT_PROMPTS_DIR = _REPO_ROOT / "contracts" / "prompts"
# {{input.<key>}} 占位符(兼容花括号内有空白的写法 {{ 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.<key>}} 占位符替换成 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.<key>}} 占位符的模板(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 + 空目录降级自测全部通过。")

View File

@ -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})