lili 882ca3f357 feat(cheap-gen): W-AXIS 波2 判定语义重置——独立模型玩法判定阻断+拆残笼+判定档切 M3
裁定三落地:accepted = 机械预筛 ∧ 独立模型玩法判定,九门 pass 降为「未见明显死」预筛。

- 波2a 玩法判定器(cheap_verify.py):读 brief+run-summary+全程截图(含新增局中连拍
  midplay-1..N,play.cdp.cjs 只读并发拍、不进任何门判据),布尔裁 broken/hollow/off_brief,
  fail-closed;判定真相层落 evidence/judge.json + verdict.json 写回 accepted/judge 段;
  金标夹具 fixtures/judge-golden/ + judge_golden.py 校准跑批
- 波2b 拆残笼:check 词法 tokenizer 替换 stripCode 正则(字符串内 // 假阴根修,292 在册
  源零差异);prompt v1.6.3 外科清洗(两层奖励→纯设计语/删盲驱动器围设计/删熔断恐吓),
  registry 热取==内置逐字节一致(cheap_roles 内置回退同步);edit_file 空白归一+近似片段提示
- 判定器补修:空输出有界重试一次(glm 思考吃光 max_tokens 时 content 空)+重试放大公式
  反缩 bug 修(旧 min(×2,6000) 大 base 反缩)+finishReason 落盘;imagesSeen 模型自报+
  盲检微扰重掷、两掷均盲=仪器故障 degraded(闲鱼中转静默丢图实锤,不再错杀 hollow)
- 判定档切 MiniMax-M3(创始人 2026-07-10 拍板,裁定三①边界随 v2 plan 修订):
  generation.yaml judge.model+max_tokens=202752(512K 网关实探 400 拒,取实探上限);
  3 例盲案 M3 重判全 accept、¥0.026/局
- 三路消费对齐:CLI 预筛语义修(cheap_studio 喂 verdict.pass 而非 finished,违裁定三的
  放行已纠)/Service 路 apply_gameplay_judge 显式 prefilter 参数/result_out 权威改读
  accepted(无键回落预筛,旧产物兼容)+trace.gameplayJudge additive 透传;
  tier2 gate_judge/genconfig 同步判定配置与消费

测试:test_gameplay_judge 33 项新增,全仓 pytest 480 绿;真判定闭环 accept ¥0.061。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 04:27:18 -07:00

157 lines
8.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
cheap_toolkit.py — 便宜档 LittleJS 六工具对照源Node prompt.mjs 的 TOOLS + gen.mjs done 门 + tier2 toolkit.py 组装范式)。
工具用 AgentScope FunctionTool 薄壳包 async 闭包函数schema 据类型注解 + Google docstring 自动抽prompt 里不手写),
闭包共享一个 CheapSession 可变状态(对 tier2 toolkit.py 的 Tier2Session。底层read/list/write 走 cheap_run
Python 实现、check/build shell-out node。done 门 = finish 工具(工具内复核 check+build 绿才接受,对 gen.mjs done 门)。
"""
import json
from dataclasses import dataclass
from typing import Optional
import cheap_run
# 单次工具结果注入上限(对 gen.mjs TOOL_RESULT_CAP够一份 skill/api.d.ts控 context
_TOOL_RESULT_CAP = 30000
@dataclass
class CheapSession:
"""便宜档单 run 可变状态闭包(对 tier2 toolkit.py 的 Tier2SessionLittleJS 简化版)。
单 session 绑单 agent、单 run不跨 run 复用(同 tier2 toolkit 的三条边界铁律)。
"""
game_id: str
last_check: Optional[dict] = None
last_build: Optional[dict] = None
finished: Optional[dict] = None # finish 组装的产物摘要None=未收敛,编排层据此判收敛)
def build_toolkit(session: CheapSession, *, write_whitelist=None):
"""组装便宜档 Toolkit对 tier2 toolkit.py build_toolkit。返回 agentscope.tool.Toolkit。
Args:
session: 本 run 可变状态闭包。
write_whitelist: 可选写边界白名单basename 集合。None=create 路不收窄(沿用 cheap_run.write_file
的 L3 边界,禁 L1A11 M4 模块重生成传 {"game-logic.js"},把写边界收窄到只许写玩法文件——
其余 L3core/render/assets连同 L1 一并禁写,确保「只改玩法、不动别处」。
"""
from agentscope.tool import Toolkit, FunctionTool
async def read_file(path: str) -> str:
"""读 repo 内任意文件(只读)。用它读 skill、插件 api.d.ts、_template 范例。先读手册再写码。
Args:
path: repo 相对路径,如 .agents/skills/littlejs-game-dev.md
"""
r = cheap_run.read_file(path)
if not r["ok"]:
return "ERROR: " + r["error"]
prefix = "[内容已截断]\n" if r.get("truncated") else ""
return (prefix + r["content"])[:_TOOL_RESULT_CAP]
async def list_dir(path: str) -> str:
"""列目录(只读),用于发现文件。
Args:
path: repo 相对路径。
"""
r = cheap_run.list_dir(path)
if not r["ok"]:
return "ERROR: " + r["error"]
return "\n".join(r["entries"])[:_TOOL_RESULT_CAP]
async def write_file(path: str, content: str) -> str:
"""写文件到本 run 的 game 目录(仅许写 games/amgen-<id>/ 内的 L3 游戏文件)。整文件覆盖。
Args:
path: 如 game-runtime/games/amgen-<id>/src/game-logic.js
content: 完整文件内容。
"""
# M4 模块重生成写边界白名单生效时basename 不在白名单(即非 game-logic.js的写一律拒——
# 本次只重写玩法文件其余文件保持不变read_file 读它取上下文即可。create 路 white=None 不收窄。
if write_whitelist is not None:
base = path.rsplit("/", 1)[-1]
if base not in write_whitelist:
allowed = "/".join(sorted(write_whitelist))
return (f"ERROR: 本次是「只改玩法」的模块重生成,只许写 {allowed}"
f"{base} 属其余文件,要保持不变、不要写它(需要上下文就 read_file 读它)。")
r = cheap_run.write_file(session.game_id, path, content)
return f"OK {r['bytes']}B 已写" if r["ok"] else "ERROR: " + r["error"]
async def edit_file(path: str, old: str, new: str) -> str:
"""增量改文件:把文件里出现一次的 old 串换成 new。改小处如一个函数几行用它别整文件重写。
old 必须逐字节精确匹配文件原文(含缩进/换行)且唯一命中。找不到/不唯一会明确报错——按提示加长 old 再试。
边界同 write_file只许改 games/amgen-<id>/ 内 L3 游戏文件L1 plumbing 拒改。
Args:
path: 如 game-runtime/games/amgen-<id>/src/game-logic.js
old: 要被替换的原文片段(复制文件里的原样,含缩进)。
new: 替换成的新片段。
"""
# 写边界白名单:与 write_file 同口径M4"只改玩法"时非 game-logic.js 一律拒)。
if write_whitelist is not None:
base = path.rsplit("/", 1)[-1]
if base not in write_whitelist:
allowed = "/".join(sorted(write_whitelist))
return (f"ERROR: 本次是「只改玩法」的模块重生成,只许改 {allowed}"
f"{base} 属其余文件,要保持不变、不要改它。")
r = cheap_run.edit_file(session.game_id, path, old, new)
return r.get("message", "OK edit 已应用") if r["ok"] else "ERROR: " + r["error"]
async def check() -> str:
"""循环内快校验node --check 各 src/*.js + 五法/导出名/红线/形状门 lint。返回 PASS 或错误清单。finish 前必须 PASS。
tsc 类型门W-AXIS 波3·advisory`node tools.mjs check` 会在主判定后附一节 [check:tsc-advisory]。
主判定 FAIL 时它随 output 一并回给模型;主判定 PASS 时——若 advisory 有【真发现】(红线/结构门放行、
但 tsc 抓到的类型 bug如把数字当函数调/拼错本地属性名)——把那一节附在 PASS 后回给模型自纠。
**advisory 绝不改 r["ok"]/不阻断**只是把「红线门漏掉的类型问题」原文喂给模型0 发现/tsc 不可用不附,免噪音)。
"""
session.last_check = cheap_run.check(session.game_id)
r = session.last_check
if r["ok"]:
out = r["output"] or ""
idx = out.find("[check:tsc-advisory]")
adv = out[idx:].strip() if idx >= 0 else ""
# 仅当 advisory 有真发现才附("0 发现"/"不可用" 对模型是噪音,不附);不改 PASS 判定。
surface = bool(adv) and ("发现" in adv) and ("0 发现" not in adv)
return ("PASS" + ("\n" + adv if surface else ""))[:_TOOL_RESULT_CAP]
return ("FAIL:\n" + r["output"])[:_TOOL_RESULT_CAP]
async def build() -> str:
"""循环内 esbuild 打包秒级SAA 信封 __GameBundle。返回 PASS 或编译错误。finish 前必须 PASS。"""
session.last_build = cheap_run.build(session.game_id)
r = session.last_build
return ("PASS" if r["ok"] else "FAIL:\n" + r["output"])[:_TOOL_RESULT_CAP]
async def finish(summary: str) -> str:
"""声明完成。done 门:工具内复核 check 与 build 都 PASS 才接受(不信自评;对 gen.mjs done 门)。
Args:
summary: 一句话:你做了一款什么游戏、核心玩法。
"""
c = cheap_run.check(session.game_id)
b = cheap_run.build(session.game_id) if c["ok"] else {"ok": False, "output": "check 未过,先修 check"}
session.last_check = c
if c["ok"]:
session.last_build = b
if c["ok"] and b["ok"]:
session.finished = {"summary": summary, "gameId": session.game_id}
return json.dumps({"ok": True, "message": "DONE 已接受check+build 绿)"}, ensure_ascii=False)
return json.dumps({
"ok": False, "blockedByGate": True,
"message": ("finish 被拒done 前 check+build 必须都绿。"
f"check={'PASS' if c['ok'] else 'FAIL'} build={'PASS' if b.get('ok') else 'FAIL'}"),
"checkOutput": (c.get("output") or "")[:2000],
"buildOutput": (b.get("output") or "")[:2000],
}, ensure_ascii=False)
tools = [
FunctionTool(read_file), FunctionTool(list_dir), FunctionTool(write_file),
FunctionTool(edit_file), FunctionTool(check), FunctionTool(build), FunctionTool(finish),
]
return Toolkit(tools=tools)