lili 14c79dbfdd feat(studio): A11 切片三 M4 模块重生成执行(有界单文件 LLM 重写 game-logic.js)
两段式第二段的 regenerate-module 类:改玩法 = 据 intent 有界重写 game-logic.js,不动其余。

复用机制(参数化、不复制 resume/熔断/收口):
- cheap_studio.run_studio 加 4 个默认 None 可选参(system_prompt/initial_kick/write_whitelist/prepare)
  切到 modify 态,create 路零行为变化(params=None 等价原逻辑)
- cheap_toolkit.build_toolkit 加 write_whitelist(非白名单 basename 写直接拒)
- cheap_roles.build_modify_system_prompt(复用 create 红线契约块)

cheap_modify.execute_regenerate_modify:
- 取 intent(空→failed)→ materialize base 源 → 重写前后对非目标文件算 hash
- 有界重写(写边界收窄到只 game-logic.js)→ 九门
- status=succeeded ⟺ 九门过 ∧ 非目标稳;manifest{file,kind:behavior,intent,changed,untouchedStable}(断言②地基)

worker_service._process_regenerate_job(regen_fn):把 M3 的 regenerate-module 显式 failed 占位换成真执行;
deterministic/create/生成路一字未动。

测试:test_a11_m4_regenerate 10/10(全注入桩零 LLM)+ 全回归独立复跑 73 测试全绿。
一次真 LLM smoke(单跑):intent=连击递增 → status=succeeded、untouchedStable=True、九门 pass、
attempts=1、¥0.31、137s;独立 diff 佐证 5 非目标文件字节相同、仅 game-logic.js 变。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 02:16:22 -07:00

123 lines
5.9 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 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(check), FunctionTool(build), FunctionTool(finish),
]
return Toolkit(tools=tools)