lili 596ea13666
Some checks failed
contract-gates / contract-gates (push) Has been cancelled
docs-gate / docs-gate (push) Has been cancelled
feat(cheap-gen): 死圈修复 fix①-A 增量 edit 工具 + fix②-prompt 克隆纪律/check-early
死圈根因(双评审坐实):产物带小红线违规→check FAIL→agent 想修但只有整文件覆盖的
write_file→改3行变重写22KB→MiniMax 漏 path 连撞→stuck 熔断→九门没跑→ok=False。

fix①-A:cheap_run.edit_file(读现文件→校验 old 唯一命中→精确替换→经 write_file
回写继承 L1_FIXED/game_dir 保护)+ cheap_toolkit edit_file 工具(whitelist 把关)注册进
七工具面;edit_file 加入软停 blocked-tools(middleware.py,越软停线同 write_file 拦)。
edit 自身失败面(old 未找到/不唯一)给结构化提示。6 单测(命中/未找到/不唯一/L1拒改/
越界/缺文件)+ 现有 toolkit 8 测全绿。

fix②-prompt(cheap_roles.py):① 工具清单纠正为七工具、明确 edit_file 小改优先(修
之前"没有 edit_file"的错误引导——否则 agent 不会用新工具);② 克隆纪律"rng/nowMs 照
_template 逐字克隆、禁自造 Math.random/Date.now 防御回退、check 函数内裸调照拦";
③ check-early"先写最小核心→立即 check→edit_file 修红线→再扩玩法"。

待续(本计划剩项):fix②-check 报错行号(tools.mjs)、fix③ brief 落 evidence、
fix①-B 共享 tier2 熔断窄口径纠偏(最险另做)、n=5 收敛环(ok=True 判据)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 01:46:22 -07:00

144 lines
7.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。"""
session.last_check = cheap_run.check(session.game_id)
r = session.last_check
return ("PASS" if r["ok"] else "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)