两段式第二段的 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>
123 lines
5.9 KiB
Python
123 lines
5.9 KiB
Python
"""
|
||
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 的 Tier2Session,LittleJS 简化版)。
|
||
|
||
单 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 边界,禁 L1);A11 M4 模块重生成传 {"game-logic.js"},把写边界收窄到只许写玩法文件——
|
||
其余 L3(core/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)
|