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>
This commit is contained in:
parent
51230f1347
commit
14c79dbfdd
@ -22,12 +22,19 @@ best-effort 铁律:apply 失败 / 重建失败 → 返 failed + 诊断,绝不抛
|
||||
不碰 L1 plumbing。
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import cheap_run
|
||||
|
||||
# A11 M4 模块重生成:behavior 在便宜档映射到玩法文件(KTD4)——只重写它,其余文件保持不变。
|
||||
_BEHAVIOR_TARGET = "game-logic.js"
|
||||
_TARGET_REL = "src/game-logic.js"
|
||||
# 断言②非目标稳定:game_dir 根下要 track 稳定性的 L1 入口/页面(src/*.js 用 glob,build 衍生物 bundle.iife.js 不 track)。
|
||||
_ROOT_STABLE = ("index.html", "entry-bundle.js", "entry.js")
|
||||
|
||||
# 调数值(config/level)寻址的 L3 文件优先序:核心数值集中在 core.js,找不到再搜其它 L3。
|
||||
_CONFIG_FILE_ORDER = ("core.js", "game-logic.js", "render.js", "balance.js")
|
||||
# 换资产寻址的 L3 文件:资产清单统一在 assets.js。
|
||||
@ -324,3 +331,170 @@ def execute_deterministic_modify(game_id: str, source_project, modify_patch,
|
||||
return {"status": status, "manifest": manifest, "verdict": verdict,
|
||||
"stage": "play", "error": None if status == "succeeded" else "九门未通过",
|
||||
"smokeOk": sm.get("ok"), "playOk": pr.get("ok")}
|
||||
|
||||
|
||||
# ───────────────────────── ④ execute_regenerate_modify(模块重生成 · 改玩法)─────────────────────────
|
||||
|
||||
def _hash_file(p: Path) -> str:
|
||||
"""文件内容 sha256(断言②的稳定性指纹)。"""
|
||||
return hashlib.sha256(p.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def _snapshot_nontarget_hashes(game_id: str) -> dict:
|
||||
"""对 game_dir 内**非目标源文件**算 hash 快照(断言②地基)。返回 {rel_path: sha256}。
|
||||
|
||||
非目标 = 除 game-logic.js 外的全部源文件:src/ 下其余 .js(core/render/assets/balance + L1 的
|
||||
host-config/game)+ 根下 L1 入口/页面(index.html/entry-bundle.js/entry.js)。**不 track build 衍生物**
|
||||
(bundle.iife.js 每次 build 重生成,track 它会假性不稳)。重写前后各拍一次、逐键比对即知非目标有没有被误伤。
|
||||
"""
|
||||
gd = cheap_run.game_dir(game_id)
|
||||
out = {}
|
||||
src = gd / "src"
|
||||
if src.is_dir():
|
||||
for p in sorted(src.glob("*.js")):
|
||||
if p.name == _BEHAVIOR_TARGET:
|
||||
continue # 目标文件本身不算「非目标」
|
||||
out[f"src/{p.name}"] = _hash_file(p)
|
||||
for name in _ROOT_STABLE:
|
||||
p = gd / name
|
||||
if p.is_file():
|
||||
out[name] = _hash_file(p)
|
||||
return out
|
||||
|
||||
|
||||
def _regen_manifest(intent: str, *, changed, stable) -> dict:
|
||||
"""模块重生成改动清单(单条 dict,区别确定性类的多条 list):哪个文件 / 哪类 / 意图 / 变没变 / 非目标稳没稳。"""
|
||||
return {"file": _TARGET_REL, "kind": "behavior", "intent": intent,
|
||||
"changed": changed, "untouchedStable": stable}
|
||||
|
||||
|
||||
def _regen_why(gates_pass: bool, untouched_stable: bool) -> str:
|
||||
"""失败诊断文案:断言②泄漏优先(误伤非目标比九门不过更严重)。"""
|
||||
if not untouched_stable:
|
||||
return "断言②失败:模块重生成误伤了非目标文件(core/render/assets/host-config 等应保持不变)"
|
||||
if not gates_pass:
|
||||
return "九门未通过"
|
||||
return "模块重生成失败"
|
||||
|
||||
|
||||
def _noop_prepare(game_id: str) -> dict:
|
||||
"""run_studio 的 prepare 注入位:game_dir 已由 execute_regenerate_modify 预备(scaffold+materialize base 源),
|
||||
不要再 scaffold 覆盖掉 base 源。返回 scaffold 同形契约 {ok,output}。"""
|
||||
return {"ok": True, "output": "game_dir 已由 execute_regenerate_modify 预备(scaffold+materialize base 源)"}
|
||||
|
||||
|
||||
def _default_rewrite(game_id: str, intent: str, *, port: int = 4320, cdp_port: int = 9222) -> dict:
|
||||
"""默认重写器:跑 cheap_studio.run_studio 的 modify 态(自定义 system prompt + kick + 写边界白名单 +
|
||||
prepare=noop[game_dir 已就绪]),复用同一套 ReAct + resume 熔断 + 三层校验收口。返回 run-summary(含九门 verdict)。
|
||||
|
||||
懒导入 cheap_studio/cheap_roles:只在真运行时装配 tier2 框架 + key,单测注入 rewrite_fn 不触发本函数。
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
import cheap_roles
|
||||
import cheap_studio
|
||||
|
||||
sys_prompt = cheap_roles.build_modify_system_prompt(game_id, intent)
|
||||
g = f"game-runtime/games/amgen-{game_id}"
|
||||
kick = (f"{g}/src/game-logic.js 现在是一款能跑能玩的游戏。请**只重写它**来实现这个调整:「{intent}」。"
|
||||
f"先 read_file 读当前的 {g}/src/game-logic.js 看现状,再据调整意图最小改写;"
|
||||
"core/render/assets/host-config 等其余文件保持不变、别写它们(写会被拒)。"
|
||||
"check 与 build 都绿了就立即 finish。")
|
||||
# 复用既有 max_iters(40)/max_resumes(6) resume 熔断,不新造。
|
||||
return asyncio.run(cheap_studio.run_studio(
|
||||
game_id, intent, system_prompt=sys_prompt, initial_kick=kick,
|
||||
write_whitelist={_BEHAVIOR_TARGET}, prepare=_noop_prepare,
|
||||
port=port, cdp_port=cdp_port))
|
||||
|
||||
|
||||
def execute_regenerate_modify(game_id: str, source_project, modify_patch,
|
||||
*, rewrite_fn=None, port: int = 4320, cdp_port: int = 9222) -> dict:
|
||||
"""模块重生成执行(改玩法):取 base 源 + intent → 有界单文件重写 game-logic.js → 九门 → 断言②非目标稳定 + 改动清单。
|
||||
|
||||
流程:取 intent(空→failed 不重写)→ 取 base 源 files(无源→failed)→ scaffold-if-needed + materialize
|
||||
base 源(同 deterministic 口径)→ 重写前对目标+非目标算 hash → 有界重写 game-logic.js(复用 cheap_studio
|
||||
ReAct+resume+三层校验,写边界收窄到只许写 game-logic.js)→ 重写后再算 hash 判 changed / untouchedStable →
|
||||
据九门 verdict + 断言②判 status。best-effort:任一步异常→failed+诊断,绝不抛崩 worker。
|
||||
|
||||
Args:
|
||||
game_id: 本 run gameId。
|
||||
source_project: base 源工程(dict 或 2.0 JSON 串,有 files=路径→文本)。
|
||||
modify_patch: 改动意图,形如 {"target":{"kind":"behavior"},"payload":{"intent":"<改写意图>"}}。
|
||||
rewrite_fn: (game_id, intent, *, port, cdp_port)->run_summary 的重写器注入位(单测桩免真 LLM/esbuild/Chrome);
|
||||
默认 _default_rewrite(真跑 cheap_studio modify 态)。
|
||||
port/cdp_port: serve/CDP 端口(透传 run_studio 收口)。
|
||||
|
||||
Returns:
|
||||
{"status":"succeeded|failed", "stage":..., "manifest":{...}, "verdict":九门verdict|None,
|
||||
"summary":run-summary|None, "error":str|None}。status=succeeded ⟺ 九门过 ∧ 断言②非目标稳定。
|
||||
manifest = {"file":"src/game-logic.js","kind":"behavior","intent":intent,"changed":bool,"untouchedStable":bool}。
|
||||
"""
|
||||
# 1. 取 intent(改写意图);空 → failed,不重写(省一切重活)。
|
||||
patch = _as_dict(modify_patch)
|
||||
payload = patch.get("payload") if isinstance(patch.get("payload"), dict) else {}
|
||||
intent = payload.get("intent")
|
||||
intent = intent.strip() if isinstance(intent, str) else ""
|
||||
if not intent:
|
||||
log(f"模块重生成 intent 为空 → failed 不重写 game_id={game_id}")
|
||||
return {"status": "failed", "stage": "intent",
|
||||
"manifest": _regen_manifest("", changed=False, stable=None),
|
||||
"verdict": None, "summary": None,
|
||||
"error": "modifyPatch.payload.intent 为空(改写意图缺失),不重写"}
|
||||
|
||||
# 2. 取 base 源 files;无源 → failed(无可改源)。
|
||||
sp = _as_dict(source_project)
|
||||
base_files = sp.get("files") if isinstance(sp.get("files"), dict) else None
|
||||
if not base_files:
|
||||
log(f"模块重生成无 base 源 files → failed game_id={game_id}")
|
||||
return {"status": "failed", "stage": "intent",
|
||||
"manifest": _regen_manifest(intent, changed=False, stable=None),
|
||||
"verdict": None, "summary": None,
|
||||
"error": "base 源工程无 files(无可改源)"}
|
||||
|
||||
try:
|
||||
# 3. prep:缺 plumbing 才 scaffold(补 L1 entry-bundle/index.html)→ materialize base 源覆盖 src/(同 deterministic 口径)。
|
||||
gd = cheap_run.game_dir(game_id)
|
||||
if not (gd / "entry-bundle.js").exists():
|
||||
sc = cheap_run.scaffold(game_id)
|
||||
if not sc["ok"]:
|
||||
log(f"模块重生成 scaffold 失败 game_id={game_id}: {sc.get('output', '')[:300]}")
|
||||
return {"status": "failed", "stage": "scaffold",
|
||||
"manifest": _regen_manifest(intent, changed=False, stable=None),
|
||||
"verdict": None, "summary": None,
|
||||
"error": "scaffold 失败:" + sc.get("output", "")}
|
||||
materialize_source_project(game_id, base_files)
|
||||
|
||||
# 4. 断言②地基:重写前对目标 + 非目标算 hash 快照。
|
||||
target_p = gd / _TARGET_REL
|
||||
target_before = _hash_file(target_p) if target_p.is_file() else None
|
||||
nontarget_before = _snapshot_nontarget_hashes(game_id)
|
||||
|
||||
# 5. 有界单文件重写 game-logic.js(默认 cheap_studio modify 态;rewrite_fn 可注入桩)。
|
||||
fn = rewrite_fn or _default_rewrite
|
||||
run_summary = fn(game_id, intent, port=port, cdp_port=cdp_port)
|
||||
run_summary = run_summary if isinstance(run_summary, dict) else {}
|
||||
|
||||
# 6. 断言②:重写后再算 hash,判目标变没变 + 非目标稳没稳。
|
||||
target_after = _hash_file(target_p) if target_p.is_file() else None
|
||||
nontarget_after = _snapshot_nontarget_hashes(game_id)
|
||||
changed = target_before != target_after
|
||||
untouched_stable = nontarget_before == nontarget_after
|
||||
manifest = _regen_manifest(intent, changed=changed, stable=untouched_stable)
|
||||
except Exception as e: # noqa: BLE001 — 重写/落盘/hash 任一异常 best-effort 兜底,不抛崩 worker
|
||||
log(f"模块重生成异常 game_id={game_id}: {type(e).__name__}: {e}")
|
||||
return {"status": "failed", "stage": "code",
|
||||
"manifest": _regen_manifest(intent, changed=False, stable=None),
|
||||
"verdict": None, "summary": None,
|
||||
"error": f"模块重生成异常:{type(e).__name__}: {e}"}
|
||||
|
||||
# 7. 据九门 verdict + 断言②判 status:九门过 ∧ 非目标稳 才算成功(误伤非目标=边界泄漏,即便九门过也判失败)。
|
||||
verdict_full = run_summary.get("verdictFull") if isinstance(run_summary.get("verdictFull"), dict) else None
|
||||
vb = run_summary.get("verdict") if isinstance(run_summary.get("verdict"), dict) else None
|
||||
gates_pass = bool(vb and vb.get("pass") is True)
|
||||
status = "succeeded" if (gates_pass and untouched_stable) else "failed"
|
||||
stage = run_summary.get("stage") or "code"
|
||||
log(f"模块重生成完成 game_id={game_id} status={status} changed={changed} "
|
||||
f"untouchedStable={untouched_stable} gatesPass={gates_pass} stage={stage}")
|
||||
return {"status": status, "stage": stage, "manifest": manifest,
|
||||
"verdict": verdict_full, "summary": run_summary,
|
||||
"error": None if status == "succeeded" else _regen_why(gates_pass, untouched_stable)}
|
||||
|
||||
@ -72,3 +72,46 @@ def build_system_prompt(game_id: str) -> str:
|
||||
"""
|
||||
g = f"game-runtime/games/amgen-{game_id}"
|
||||
return _SYSTEM_PROMPT.replace("⟦G⟧", g)
|
||||
|
||||
|
||||
def _contract_block() -> str:
|
||||
"""从 create system prompt 运行时切出「红线 + 完成判据 + 现在开始」通用工程契约块。
|
||||
|
||||
红线(入口契约五法 / 插件 API / 两条 footgun / 输入契约 / 完成判据)对 create 与 modify 两条 prompt
|
||||
完全通用——是复利资产,单一事实源。modify prompt 复用它而非另抄一份(运行时切片,不改动 create 字面量,
|
||||
create 路一字不动)。锚 `【红线` 标记到末尾;找不到(罕见,被改坏)兜底返全文。
|
||||
"""
|
||||
idx = _SYSTEM_PROMPT.find("【红线")
|
||||
return _SYSTEM_PROMPT[idx:] if idx >= 0 else _SYSTEM_PROMPT
|
||||
|
||||
|
||||
def build_modify_system_prompt(game_id: str, intent: str) -> str:
|
||||
"""A11 M4 模块重生成(改玩法)的 system prompt:已能玩的游戏 → 只重写 game-logic.js 实现 intent。
|
||||
|
||||
与 create prompt 的两点差异:① 开头不是「从零造」而是「在已能玩的游戏上最小改写」;② 写边界收窄到
|
||||
**只许写 game-logic.js**,core/render/assets/host-config 等其余文件保持不变、不要动(写会被工具拒)。
|
||||
红线契约块(入口五法 / 插件 API / footgun / 输入)经 _contract_block() 复用 create 同一份,不另抄。
|
||||
|
||||
Args:
|
||||
game_id: 本 run 的游戏 id。
|
||||
intent: 用户的改写意图(M2 判意图产出的 modifyPatch.payload.intent)。
|
||||
|
||||
Returns:
|
||||
modify system prompt 字符串(⟦G⟧ 已据 game_id 替换)。
|
||||
"""
|
||||
head = f"""你是 A-model 游戏调整 agent。⟦G⟧/ 下已经是一款**能跑能玩**的便宜档 LittleJS 小游戏(多文件 src/ 工程)。
|
||||
|
||||
【本次任务 = 只改玩法,不是从零造】
|
||||
- **唯一任务:只重写 ⟦G⟧/src/game-logic.js**(玩法本体),来实现这个调整:「{intent}」。
|
||||
- **其余文件一律保持不变、绝不要写**:src/core.js(数值)/ src/render.js(画面)/ src/assets.js(资产)/ src/host-config.js / src/game.js / index.html / entry-bundle.js 都不要动——写它们会被工具直接拒绝。你**可以 read_file 读** core/render/assets 取上下文,但**只能 write game-logic.js**。
|
||||
- 这是已能玩的游戏的**最小改写**:先 read_file 读当前的 ⟦G⟧/src/game-logic.js 看现在怎么实现,再据上面的调整意图改写它;别推倒重来、别改 brief/调整没要求的东西。
|
||||
|
||||
【先读(必须;别凭记忆猜 API)】
|
||||
1. read_file('{SKILL_PATH}') —— code 层作业手册(结构/边界/红线/插件 API 速查)。
|
||||
2. read_file('⟦G⟧/src/game-logic.js') —— 当前玩法实现(你的改写起点)。
|
||||
3. 用到某插件先 read_file 它的 api.d.ts 看精确签名(如 game-runtime/src/plugins/scene-fsm/api.d.ts)。
|
||||
|
||||
【步骤】
|
||||
A 读手册 + 读当前 game-logic.js → B 定位「{intent}」要改玩法的哪一处 → C 读要用的插件 api.d.ts → D write_file 改写 **game-logic.js(只此一个文件)** 实现调整 → E 调 check,有错改到 PASS → F 调 build,有错改到 PASS → G check+build 都绿后调 finish。"""
|
||||
g = f"game-runtime/games/amgen-{game_id}"
|
||||
return (head + "\n\n" + _contract_block()).replace("⟦G⟧", g)
|
||||
|
||||
@ -109,24 +109,34 @@ def build_trace_source(verdict, driver_type, attempts, stage, model) -> dict:
|
||||
|
||||
|
||||
async def run_studio(game_id, brief, *, max_iters=40, max_resumes=6, max_tokens=16000,
|
||||
port=4320, cdp_port=9222, run_gates=True):
|
||||
port=4320, cdp_port=9222, run_gates=True,
|
||||
system_prompt=None, initial_kick=None, write_whitelist=None, prepare=None):
|
||||
"""跑便宜档一局生成(scaffold → ReAct 写 src/ → done 门 → 收口 stage+smoke+九门)。返回 run-summary dict。
|
||||
|
||||
run_gates=True(默认,spike 单局):收口跑全套 stage→smoke→ensure_play_spec→九门 play。
|
||||
run_gates=False(generation-only,给 compare_node 对照用):收口只到 stage+smoke 为止,**不**自动产 play-spec、
|
||||
**不**跑内部九门 play —— 由调用方在生成与 play 之间注入金标 spec 再单独 play,把驱动器从对照变量里摘掉
|
||||
(Codex C1:run_studio 内部已 play,对照需把生成与 play 拆开)。
|
||||
|
||||
A11 M4 模块重生成复用本编排(同一 ReAct + resume + 熔断 + 三层校验收口,不另造),靠四个可选参切到 modify 态,
|
||||
都默认 None=create 原行为(create 路零改动):
|
||||
· system_prompt:None=create 的 build_system_prompt;modify 传 build_modify_system_prompt(只改玩法)。
|
||||
· initial_kick:None=create 的「按 brief 造游戏」首条 kick;modify 传「只重写 game-logic.js 实现 intent」。
|
||||
· write_whitelist:None=create 不收窄;modify 传 {"game-logic.js"} 把写边界收窄到只许写玩法文件。
|
||||
· prepare:None=create 的 scaffold(clone _template);modify 传「已由上游 scaffold+materialize base 源」
|
||||
的 noop(game_dir 已就绪、不要再 scaffold 覆盖掉 base 源)。签名同 scaffold:(game_id)->{ok,output}。
|
||||
"""
|
||||
t0 = time.time()
|
||||
_rec(f"model={_bootstrap.SPIKE_MODEL} id={game_id} brief=「{brief}」max_iters={max_iters} max_resumes={max_resumes}")
|
||||
|
||||
sc = cheap_run.scaffold(game_id)
|
||||
prep = prepare or cheap_run.scaffold # create=scaffold clone _template;modify=上游已预备的 noop
|
||||
sc = prep(game_id)
|
||||
if not sc["ok"]:
|
||||
return {"ok": False, "gameId": game_id, "fail": "scaffold 失败:" + sc["output"]}
|
||||
_rec(f"scaffold amgen-{game_id}(clone _template + SAA 信封)")
|
||||
return {"ok": False, "gameId": game_id, "fail": "准备失败:" + sc["output"]}
|
||||
_rec(f"准备就绪 amgen-{game_id}({'scaffold clone _template + SAA 信封' if prepare is None else 'modify: base 源已预备'})")
|
||||
|
||||
session = CheapSession(game_id=game_id)
|
||||
toolkit = build_toolkit(session)
|
||||
toolkit = build_toolkit(session, write_whitelist=write_whitelist) # modify 收窄到只许写 game-logic.js
|
||||
model = _bootstrap.build_cheap_model(max_tokens=max_tokens)
|
||||
# 熔断:便宜档开 ¥ 累进硬闸(M1 U4 / KTD5)+ 设便宜档专属 ¥10 硬上限(区别 tier2 的 ¥3 genconfig 默认)。
|
||||
# on_model_call 在每次裸模型调用前按「已花 + 本次预估」判、越线即 fail-closed 抛 Tier2CircuitBreak(budget);
|
||||
@ -136,7 +146,7 @@ async def run_studio(game_id, brief, *, max_iters=40, max_resumes=6, max_tokens=
|
||||
|
||||
writer = Agent(
|
||||
name="cheap-writer",
|
||||
system_prompt=build_system_prompt(game_id),
|
||||
system_prompt=system_prompt or build_system_prompt(game_id), # modify 传 build_modify_system_prompt
|
||||
model=model,
|
||||
toolkit=toolkit,
|
||||
middlewares=[tracer, breaker], # trace 外层、熔断内层(列表序=洋葱序)
|
||||
@ -145,8 +155,9 @@ async def run_studio(game_id, brief, *, max_iters=40, max_resumes=6, max_tokens=
|
||||
context_config=config.build_context_config(), # 历史压缩
|
||||
)
|
||||
|
||||
kick = (f"请按这个 brief 造一款游戏:「{brief}」。先 read_file 读手册(.agents/skills/littlejs-game-dev.md)"
|
||||
"和你的起点 game-logic.js 再动手;核心玩法实现完、check 与 build 都绿了就立即 finish。")
|
||||
kick = initial_kick or (
|
||||
f"请按这个 brief 造一款游戏:「{brief}」。先 read_file 读手册(.agents/skills/littlejs-game-dev.md)"
|
||||
"和你的起点 game-logic.js 再动手;核心玩法实现完、check 与 build 都绿了就立即 finish。")
|
||||
breaker_tripped = None
|
||||
attempts = 0
|
||||
try:
|
||||
|
||||
@ -29,8 +29,15 @@ class CheapSession:
|
||||
finished: Optional[dict] = None # finish 组装的产物摘要(None=未收敛,编排层据此判收敛)
|
||||
|
||||
|
||||
def build_toolkit(session: CheapSession):
|
||||
"""组装便宜档 Toolkit(对 tier2 toolkit.py build_toolkit)。返回 agentscope.tool.Toolkit。"""
|
||||
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:
|
||||
@ -63,6 +70,14 @@ def build_toolkit(session: CheapSession):
|
||||
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"]
|
||||
|
||||
|
||||
@ -247,15 +247,20 @@ def test_worker_deterministic_route_success_carries_bundle_and_source():
|
||||
assert "sourceProject" in p, "成功路 best-effort 带新版源(供下一次链式改)"
|
||||
|
||||
|
||||
def test_worker_regenerate_module_explicit_failed():
|
||||
"""modifyMode=regenerate-module(M4 未实现)→ 显式 failed + 发 failed 回调,绝不静默吞。"""
|
||||
def test_worker_regenerate_module_routes_to_executor_no_llm():
|
||||
"""modifyMode=regenerate-module(M4 已实现)→ 走 execute_regenerate_modify、不再占位 failed。
|
||||
|
||||
无 sourceProject/modifyPatch → intent 空,executor 早失败(零 LLM、不 scaffold/不重写)→ 仍发 failed 回调
|
||||
(任务不挂 RUNNING)+ 失败原因进 trace(不静默吞)。验 regen 分流已接真执行器。完整 regen 单测见 test_a11_m4。
|
||||
"""
|
||||
captured = {}
|
||||
state = W.WorkerState(send_fn=lambda u, p, s: captured.update(payload=p) or (200, ""))
|
||||
state = W.WorkerState(send_fn=lambda u, p, s: captured.update(payload=p) or (200, ""),
|
||||
profile_fn=lambda j, g: None) # 默认 regen_fn → 真 execute_regenerate_modify(intent 空零 LLM)
|
||||
job = {"job_id": "rm1", "traceId": "rm1", "modifyMode": "regenerate-module", "gameId": "g",
|
||||
"brief": "x", "callback": {"target": "http://cb"}}
|
||||
out = W.process_job(state, job)
|
||||
assert out["status"] == "failed" and out["mode"] == "regenerate-module"
|
||||
assert captured["payload"]["status"] == "failed", "应发 failed 回调(任务不挂 RUNNING)"
|
||||
"brief": "x", "callback": {"target": "http://cb"}} # 无 sourceProject/modifyPatch
|
||||
W.process_job(state, job)
|
||||
assert captured["payload"]["status"] == "failed", "intent 空 → failed 回调(任务不挂 RUNNING)"
|
||||
assert captured["payload"]["trace"]["modifyError"], "失败原因附进 trace(不静默吞)"
|
||||
|
||||
|
||||
def _boom_modify(*a):
|
||||
|
||||
275
cheap-worker/tests/test_a11_m4_regenerate.py
Normal file
275
cheap-worker/tests/test_a11_m4_regenerate.py
Normal file
@ -0,0 +1,275 @@
|
||||
"""test_a11_m4_regenerate.py — A11 切片三 M4「模块重生成执行」(改玩法)单测(全注入桩、零真 LLM)。
|
||||
|
||||
守的不变量:
|
||||
· execute_regenerate_modify:有界单文件重写 game-logic.js,复用 cheap_studio resume + 三层校验,
|
||||
但写边界收窄到只许写 game-logic.js;断言②对非目标文件 hash 稳定性把关。
|
||||
- 注入假 rewrite(改 game-logic.js)→ manifest.changed=true、非目标稳(untouchedStable=true)、走到九门 verdict;
|
||||
- intent 空 → failed 不重写(不调重写器);
|
||||
- 假 rewrite 篡改非目标 → untouchedStable=false(断言②真能抓误伤)+ status failed;
|
||||
- 无 base 源 / 重写异常 → best-effort failed,不抛。
|
||||
· worker 接线:modifyMode=regenerate-module → 走 execute_regenerate_modify(不再占位 failed);
|
||||
manifest(单 dict)附进 trace、成功带新版 sourceProject + 富 run-summary(cost/attempts)。
|
||||
|
||||
跑:cheap-worker/.venv/bin/python cheap-worker/tests/test_a11_m4_regenerate.py
|
||||
"""
|
||||
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1])) # → cheap-worker/
|
||||
import cheap_run # noqa: E402
|
||||
import cheap_modify as M # noqa: E402
|
||||
import worker_service as W # noqa: E402
|
||||
import dedup # noqa: E402
|
||||
|
||||
# game_dir 重定向到临时目录:materialize / 接线落盘免污染真 games/(cheap_modify、worker_service 同引用一个
|
||||
# cheap_run 模块对象 → patch 模块属性即对二者生效;与 test_a11_m3 同范式)。
|
||||
_TMP_GAMES = Path(tempfile.mkdtemp(prefix="a11-m4-games-"))
|
||||
cheap_run.game_dir = lambda gid: _TMP_GAMES / f"amgen-{gid}" # noqa: E731
|
||||
# 防 D9 dedup 写真登记表(本测不走 create 路、稳妥起见仍重定向)。
|
||||
dedup.DEDUP_REGISTRY = Path(tempfile.mkdtemp(prefix="a11m4-dedup-")) / "_dedup_registry.jsonl"
|
||||
|
||||
_GOOD_BUNDLE = "var __GameBundle=(function(){return{bootGameHost(){}}})();"
|
||||
_BASE_LOGIC = "// base game-logic\nexport function createGame({ plugins, bundle, viewport }) { return {}; }\n"
|
||||
|
||||
|
||||
def _setup_base(game_id: str):
|
||||
"""预备一个「已能玩」的便宜档产物:写好 L1 根件(entry-bundle/index,让 scaffold 跳过)+ 返回 base 源 dict。
|
||||
|
||||
base 源 files 含目标 game-logic.js + 两个非目标(core 数值 / host-config L1);execute_regenerate_modify 会
|
||||
materialize 这些到 game_dir。返回 (game_dir, source_project_dict)。
|
||||
"""
|
||||
gd = cheap_run.game_dir(game_id)
|
||||
(gd / "src").mkdir(parents=True, exist_ok=True)
|
||||
# 预置 entry-bundle.js 让 execute 的 scaffold-if-needed 跳过(单测不 shell-out node scaffold)。
|
||||
(gd / "entry-bundle.js").write_text("// L1 entry-bundle\n", encoding="utf-8")
|
||||
(gd / "index.html").write_text("<html><body>g</body></html>", encoding="utf-8")
|
||||
sp = {"files": {
|
||||
"src/game-logic.js": _BASE_LOGIC,
|
||||
"src/core.js": "export const SPEED = 5;\n", # 非目标(数值)
|
||||
"src/host-config.js": "// L1 host-config plumbing\n", # 非目标(L1)
|
||||
}}
|
||||
return gd, sp
|
||||
|
||||
|
||||
# ───────────────────────── ① execute_regenerate_modify ─────────────────────────
|
||||
|
||||
def _fake_rewrite_ok(game_id, intent, **kw):
|
||||
"""假重写器:只改 game-logic.js(模拟 agent 有界重写),返回九门桩 run-summary(verdict pass)。"""
|
||||
gd = cheap_run.game_dir(game_id)
|
||||
(gd / "src" / "game-logic.js").write_text(
|
||||
f"// rewritten for: {intent}\nexport function createGame({{ plugins, bundle, viewport }}) {{ return {{}}; }}\n",
|
||||
encoding="utf-8")
|
||||
return {"verdict": {"pass": True, "failedGates": []},
|
||||
"verdictFull": {"pass": True, "guards": {}},
|
||||
"attempts": 3, "stage": "play", "costRmb": 0.5,
|
||||
"models": {"code": "MiniMax-M3"}, "gameId": game_id}
|
||||
|
||||
|
||||
def _fake_rewrite_tamper(game_id, intent, **kw):
|
||||
"""假重写器(误伤):改了 game-logic.js,**还动了非目标 core.js**——断言②应抓到。"""
|
||||
gd = cheap_run.game_dir(game_id)
|
||||
(gd / "src" / "game-logic.js").write_text("// rewritten\nexport function createGame(o){ return {}; }\n", encoding="utf-8")
|
||||
(gd / "src" / "core.js").write_text("export const SPEED = 999;\n", encoding="utf-8") # 误伤非目标
|
||||
return {"verdict": {"pass": True, "failedGates": []}, "verdictFull": {"pass": True, "guards": {}},
|
||||
"attempts": 2, "stage": "play", "gameId": game_id}
|
||||
|
||||
|
||||
def test_regenerate_changed_and_nontarget_stable():
|
||||
"""假 rewrite 改 game-logic → changed=true、非目标稳(untouchedStable=true)、走到九门 verdict、status succeeded。"""
|
||||
gid = "m4ok"
|
||||
_, sp = _setup_base(gid)
|
||||
patch = {"target": {"kind": "behavior"}, "payload": {"intent": "把命中加分从固定改成连击递增"}}
|
||||
out = M.execute_regenerate_modify(gid, sp, patch, rewrite_fn=_fake_rewrite_ok)
|
||||
assert out["status"] == "succeeded", out # 九门桩 pass ∧ 非目标稳 → succeeded
|
||||
man = out["manifest"]
|
||||
assert man["file"] == "src/game-logic.js" and man["kind"] == "behavior"
|
||||
assert man["intent"] == "把命中加分从固定改成连击递增"
|
||||
assert man["changed"] is True, "game-logic 被重写 → changed"
|
||||
assert man["untouchedStable"] is True, "非目标(core/host-config/entry/index)未动 → stable"
|
||||
assert out["verdict"] == {"pass": True, "guards": {}}, "九门 verdict 透传(走到 verdict)"
|
||||
assert out["stage"] == "play"
|
||||
assert out["summary"]["attempts"] == 3, "富 run-summary 透传(cost/attempts 在)"
|
||||
# 非目标文件内容确实没变。
|
||||
assert (cheap_run.game_dir(gid) / "src" / "core.js").read_text(encoding="utf-8") == "export const SPEED = 5;\n"
|
||||
|
||||
|
||||
def test_regenerate_empty_intent_failed_no_rewrite():
|
||||
"""intent 空(空串/缺 intent/缺 payload)→ failed 不重写(不调用重写器)。"""
|
||||
gid = "m4empty"
|
||||
_, sp = _setup_base(gid)
|
||||
called = {}
|
||||
|
||||
def _spy(game_id, intent, **kw):
|
||||
called["x"] = True
|
||||
return {"verdict": {"pass": True}}
|
||||
|
||||
for patch in ({"target": {"kind": "behavior"}, "payload": {"intent": " "}},
|
||||
{"target": {"kind": "behavior"}, "payload": {"intent": ""}},
|
||||
{"target": {"kind": "behavior"}, "payload": {}},
|
||||
{"target": {"kind": "behavior"}}):
|
||||
out = M.execute_regenerate_modify(gid, sp, patch, rewrite_fn=_spy)
|
||||
assert out["status"] == "failed", out
|
||||
assert out["stage"] == "intent"
|
||||
assert "intent" in (out["error"] or "")
|
||||
assert out["manifest"]["untouchedStable"] is None, "没重写 → 稳定性未知=None"
|
||||
assert "x" not in called, "intent 空绝不应调用重写器"
|
||||
|
||||
|
||||
def test_regenerate_tamper_nontarget_caught_by_assert2():
|
||||
"""假 rewrite 篡改非目标(core.js)→ untouchedStable=false(断言②真能抓误伤)+ 即便九门过也判 failed。"""
|
||||
gid = "m4tamper"
|
||||
_, sp = _setup_base(gid)
|
||||
patch = {"target": {"kind": "behavior"}, "payload": {"intent": "改玩法但误伤了 core"}}
|
||||
out = M.execute_regenerate_modify(gid, sp, patch, rewrite_fn=_fake_rewrite_tamper)
|
||||
assert out["manifest"]["changed"] is True, "game-logic 仍被改"
|
||||
assert out["manifest"]["untouchedStable"] is False, "断言②应抓到非目标 core.js 被改"
|
||||
assert out["status"] == "failed", "误伤非目标=边界泄漏,即便九门桩 pass 也判失败"
|
||||
assert "断言②" in (out["error"] or "") or "非目标" in (out["error"] or "")
|
||||
|
||||
|
||||
def test_regenerate_no_source_failed():
|
||||
"""无 base 源 files → failed(无可改源),不重写。"""
|
||||
patch = {"target": {"kind": "behavior"}, "payload": {"intent": "随便改"}}
|
||||
out = M.execute_regenerate_modify("m4nosrc", {"files": {}}, patch, rewrite_fn=_fake_rewrite_ok)
|
||||
assert out["status"] == "failed" and "源" in (out["error"] or "")
|
||||
out2 = M.execute_regenerate_modify("m4nosrc2", "{bad json", patch, rewrite_fn=_fake_rewrite_ok)
|
||||
assert out2["status"] == "failed", "坏 source_project 容错 failed,不抛"
|
||||
|
||||
|
||||
def test_regenerate_rewrite_exception_best_effort():
|
||||
"""重写器抛异常 → best-effort failed + 诊断,绝不抛崩。"""
|
||||
gid = "m4boom"
|
||||
_, sp = _setup_base(gid)
|
||||
|
||||
def _boom(game_id, intent, **kw):
|
||||
raise RuntimeError("LLM exploded")
|
||||
|
||||
out = M.execute_regenerate_modify(gid, sp, {"target": {"kind": "behavior"}, "payload": {"intent": "x"}},
|
||||
rewrite_fn=_boom)
|
||||
assert out["status"] == "failed" and "异常" in (out["error"] or "")
|
||||
assert out["stage"] == "code"
|
||||
|
||||
|
||||
def test_regenerate_gates_fail_is_plumbing_pass():
|
||||
"""九门桩不过(verdict pass=false)但非目标稳 → status failed,但 changed/stable 正确(M4 验 plumbing,质量归 agent)。"""
|
||||
gid = "m4gatefail"
|
||||
_, sp = _setup_base(gid)
|
||||
|
||||
def _rewrite_gatefail(game_id, intent, **kw):
|
||||
gd = cheap_run.game_dir(game_id)
|
||||
(gd / "src" / "game-logic.js").write_text("// rewritten low quality\n", encoding="utf-8")
|
||||
return {"verdict": {"pass": False, "failedGates": ["H_progress"]},
|
||||
"verdictFull": {"pass": False, "guards": {"H_progress": {"pass": False}}},
|
||||
"attempts": 6, "stage": "play", "gameId": game_id}
|
||||
|
||||
out = M.execute_regenerate_modify(gid, sp, {"target": {"kind": "behavior"}, "payload": {"intent": "改玩法"}},
|
||||
rewrite_fn=_rewrite_gatefail)
|
||||
assert out["status"] == "failed" and out["error"] == "九门未通过"
|
||||
assert out["manifest"]["changed"] is True and out["manifest"]["untouchedStable"] is True, "plumbing 仍通(改了+非目标稳)"
|
||||
|
||||
|
||||
# ───────────────────────── ② worker 接线 ─────────────────────────
|
||||
|
||||
def test_worker_regenerate_route_success_carries_manifest_bundle_source():
|
||||
"""regenerate-module 成功 → result-out 带 engineBundle + manifest(单 dict)进 trace + 新版 sourceProject + 富 trace。"""
|
||||
gid = "m4wok"
|
||||
gd = cheap_run.game_dir(gid)
|
||||
(gd / "src").mkdir(parents=True, exist_ok=True)
|
||||
(gd / "bundle.iife.js").write_text(_GOOD_BUNDLE, encoding="utf-8") # 成功路需 engineBundle
|
||||
(gd / "src" / "game-logic.js").write_text("// rewritten\n", encoding="utf-8")
|
||||
captured = {}
|
||||
fake = {"status": "succeeded", "stage": "play",
|
||||
"manifest": {"file": "src/game-logic.js", "kind": "behavior", "intent": "连击递增",
|
||||
"changed": True, "untouchedStable": True},
|
||||
"verdict": {"pass": True, "guards": {}},
|
||||
"summary": {"verdict": {"pass": True, "failedGates": []}, "verdictFull": {"pass": True, "guards": {}},
|
||||
"attempts": 4, "stage": "play", "costRmb": 0.7, "models": {"code": "MiniMax-M3"}, "gameId": gid},
|
||||
"error": None}
|
||||
profile = {"tickModel": "realtime", "inputModel": "discrete-choice", "progressModel": "metric"}
|
||||
state = W.WorkerState(regen_fn=lambda *a: fake,
|
||||
send_fn=lambda u, p, s: captured.update(payload=p) or (200, ""),
|
||||
profile_fn=lambda j, g: profile)
|
||||
job = {"job_id": "rmok", "traceId": "rmok", "modifyMode": "regenerate-module", "gameId": gid,
|
||||
"sourceProject": "{}", "modifyPatch": "{}", "brief": "连击小游戏", "callback": {"target": "http://cb"}}
|
||||
W.process_job(state, job)
|
||||
p = captured["payload"]
|
||||
assert p["status"] == "succeeded"
|
||||
assert "__GameBundle" in p["engineBundle"], "成功路携重建后的 bundle"
|
||||
man = p["trace"]["modifyManifest"]
|
||||
assert man["untouchedStable"] is True and man["changed"] is True, "manifest(单 dict)进 trace"
|
||||
assert man["kind"] == "behavior"
|
||||
assert "sourceProject" in p, "成功路 best-effort 带新版源(供链式改)"
|
||||
assert p["trace"]["attempts"] == 4, "富 run-summary → trace.attempts"
|
||||
assert p["trace"]["cost"]["totalRmb"] == 0.7, "富 run-summary → trace.cost"
|
||||
|
||||
|
||||
def test_worker_regenerate_route_failed_attaches_error_and_manifest():
|
||||
"""regenerate-module 失败(误伤非目标)→ failed 回调 + manifest/error 进 trace(不静默吞)。"""
|
||||
gid = "m4wfail"
|
||||
captured = {}
|
||||
fake = {"status": "failed", "stage": "play",
|
||||
"manifest": {"file": "src/game-logic.js", "kind": "behavior", "intent": "x",
|
||||
"changed": True, "untouchedStable": False},
|
||||
"verdict": {"pass": True, "guards": {}}, "summary": None,
|
||||
"error": "断言②失败:模块重生成误伤了非目标文件"}
|
||||
state = W.WorkerState(regen_fn=lambda *a: fake,
|
||||
send_fn=lambda u, p, s: captured.update(payload=p) or (200, ""),
|
||||
profile_fn=lambda j, g: None)
|
||||
job = {"job_id": "rmf", "traceId": "rmf", "modifyMode": "regenerate-module", "gameId": gid,
|
||||
"sourceProject": "{}", "modifyPatch": "{}", "brief": "x", "callback": {"target": "http://cb"}}
|
||||
out = W.process_job(state, job)
|
||||
assert out["status"] == "failed", "无 bundle + 执行器 failed → failed"
|
||||
assert captured["payload"]["trace"]["modifyManifest"]["untouchedStable"] is False
|
||||
assert captured["payload"]["trace"]["modifyError"], "失败原因附进 trace"
|
||||
assert "sourceProject" not in captured["payload"], "失败路不带新版源"
|
||||
|
||||
|
||||
def _boom_regen(*a):
|
||||
raise AssertionError("deterministic/create 路不应调用 regen_fn")
|
||||
|
||||
|
||||
def test_worker_deterministic_unaffected_by_regen_wiring():
|
||||
"""modifyMode=deterministic 仍走 modify_fn、不触 regen_fn(regen 接线对 deterministic 零副作用)。"""
|
||||
gid = "m4det"
|
||||
gd = cheap_run.game_dir(gid)
|
||||
(gd / "src").mkdir(parents=True, exist_ok=True)
|
||||
(gd / "bundle.iife.js").write_text(_GOOD_BUNDLE, encoding="utf-8")
|
||||
captured = {}
|
||||
det = {"status": "succeeded",
|
||||
"manifest": [{"file": "src/core.js", "kind": "config", "key": "SPEED", "old": "5", "new": "8", "found": True}],
|
||||
"verdict": {"pass": True, "guards": {}}, "error": None, "stage": "play"}
|
||||
state = W.WorkerState(modify_fn=lambda *a: det, regen_fn=_boom_regen,
|
||||
send_fn=lambda u, p, s: captured.update(payload=p) or (200, ""),
|
||||
profile_fn=lambda j, g: None)
|
||||
job = {"job_id": "det1", "traceId": "det1", "modifyMode": "deterministic", "gameId": gid,
|
||||
"sourceProject": "{}", "modifyPatch": "{}", "brief": "x", "callback": {"target": "http://cb"}}
|
||||
W.process_job(state, job)
|
||||
assert captured["payload"]["status"] == "succeeded"
|
||||
assert isinstance(captured["payload"]["trace"]["modifyManifest"], list), "deterministic manifest 仍是 list"
|
||||
|
||||
|
||||
def test_worker_unknown_mode_still_failed():
|
||||
"""未知 modifyMode → 显式 failed 兜底回调(不静默吞;regen 接线后未知 mode 行为不变)。"""
|
||||
captured = {}
|
||||
state = W.WorkerState(send_fn=lambda u, p, s: captured.update(payload=p) or (200, ""))
|
||||
job = {"job_id": "unk1", "traceId": "unk1", "modifyMode": "teleport", "gameId": "g",
|
||||
"brief": "x", "callback": {"target": "http://cb"}}
|
||||
out = W.process_job(state, job)
|
||||
assert out["status"] == "failed" and out["mode"] == "teleport"
|
||||
assert captured["payload"]["status"] == "failed"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
_fns = [v for k, v in sorted(globals().items()) if k.startswith("test_") and callable(v)]
|
||||
_failed = 0
|
||||
for _fn in _fns:
|
||||
try:
|
||||
_fn()
|
||||
print(f" PASS {_fn.__name__}")
|
||||
except Exception as e: # noqa: BLE001
|
||||
_failed += 1
|
||||
print(f" FAIL {_fn.__name__}: {type(e).__name__}: {e}")
|
||||
print(f"\n[test_a11_m4_regenerate] {len(_fns) - _failed}/{len(_fns)} passed")
|
||||
sys.exit(1 if _failed else 0)
|
||||
@ -149,7 +149,7 @@ class WorkerState:
|
||||
"""进程级状态:配置 + 有界队列 + 可注入依赖 + job_id 去重。"""
|
||||
|
||||
def __init__(self, *, callback_secret: str = "", queue_maxsize: int = DEFAULT_QUEUE_MAXSIZE,
|
||||
run_fn=None, send_fn=None, profile_fn=None, classify_fn=None, modify_fn=None):
|
||||
run_fn=None, send_fn=None, profile_fn=None, classify_fn=None, modify_fn=None, regen_fn=None):
|
||||
self.callback_secret = callback_secret
|
||||
self.queue: queue.Queue = queue.Queue(maxsize=queue_maxsize)
|
||||
self.run_fn = run_fn or _default_run_fn # job -> (summary, game_dir)
|
||||
@ -159,6 +159,9 @@ class WorkerState:
|
||||
# A11 切片三 M3:确定性类执行器(game_id, sourceProject, modifyPatch) -> 执行结果。
|
||||
# None=懒绑 cheap_modify.execute_deterministic_modify(单测注入桩免真 esbuild/Chrome)。
|
||||
self.modify_fn = modify_fn
|
||||
# A11 切片三 M4:模块重生成执行器(game_id, sourceProject, modifyPatch) -> 执行结果。
|
||||
# None=懒绑 cheap_modify.execute_regenerate_modify(单测注入桩免真 LLM/esbuild/Chrome)。
|
||||
self.regen_fn = regen_fn
|
||||
self._seen: set = set() # job_id 去重(防同 job 重投重跑)
|
||||
self._lock = threading.Lock()
|
||||
|
||||
@ -245,18 +248,23 @@ def _modify_summary(result: dict, game_id: str) -> dict:
|
||||
|
||||
|
||||
def _process_modify_job(state: WorkerState, job: dict, modify: dict) -> dict:
|
||||
"""A11 调整路:deterministic 走确定性执行(改→重建→九门);regenerate-module 留 M4;未知 mode → failed。
|
||||
"""A11 调整路:deterministic 走确定性执行(改→重建→九门);regenerate-module 走模块重生成(M4);未知 mode → failed。
|
||||
|
||||
deterministic:execute_deterministic_modify(注入 modify_fn 可桩)→ 组 result-out(manifest 附进 trace、
|
||||
best-effort 带新版 sourceProject 供链式改)→ 回调。regenerate-module/未知:显式 failed + 日志(不静默吞)。
|
||||
best-effort 带新版 sourceProject 供链式改)→ 回调。regenerate-module:_process_regenerate_job(M4 有界重写
|
||||
game-logic.js)。未知 mode:显式 failed + 日志(不静默吞)。
|
||||
"""
|
||||
trace_id = job.get("traceId") or job.get("job_id")
|
||||
mode = modify.get("mode")
|
||||
game_id = job.get("gameId") or job.get("job_id")
|
||||
|
||||
# A11 M4:模块重生成(改玩法)分流——有界单文件重写 game-logic.js,复用 cheap_studio resume + 三层校验。
|
||||
if mode == "regenerate-module":
|
||||
return _process_regenerate_job(state, job, modify)
|
||||
|
||||
if mode != "deterministic":
|
||||
# regenerate-module 属 M4(模块重生成,要 LLM)、未知 mode 都显式 failed,绝不静默吞(错误路红线)。
|
||||
why = "regenerate-module 属 M4、未实现" if mode == "regenerate-module" else f"未知 modifyMode={mode}"
|
||||
# 未知 mode 显式 failed,绝不静默吞(错误路红线)。
|
||||
why = f"未知 modifyMode={mode}"
|
||||
log(f"调整路不支持的 mode → 发兜底 failed trace_id={trace_id}: {why}")
|
||||
_send_failed(state, job, why)
|
||||
return {"status": "failed", "mode": mode, "error": why}
|
||||
@ -301,6 +309,83 @@ def _process_modify_job(state: WorkerState, job: dict, modify: dict) -> dict:
|
||||
return payload
|
||||
|
||||
|
||||
def _regen_summary(result: dict, game_id: str) -> dict:
|
||||
"""把 execute_regenerate_modify 结果转成 build_result_out 吃的 run-summary。
|
||||
|
||||
复用重写产出的富 run-summary(cost/models/attempts/wallSec/stage/driverType,供 trace 完整);**但
|
||||
verdict.pass 以执行器最终判定为准**(= 九门过 ∧ 断言②非目标稳定)——否则「九门过但误伤了非目标」会被
|
||||
build_result_out 读 run-summary 原始 verdict.pass=True 误判成功,与执行器 status=failed 矛盾。
|
||||
早失败(无重写、result.summary=None)→ 用最小 failed summary 兜底。
|
||||
"""
|
||||
pass_ = result.get("status") == "succeeded"
|
||||
run_summary = result.get("summary")
|
||||
verdict_full = result.get("verdict") if isinstance(result.get("verdict"), dict) else None
|
||||
failed = []
|
||||
if verdict_full:
|
||||
guards = verdict_full.get("guards") or {}
|
||||
failed = [k for k, g in guards.items() if isinstance(g, dict) and g.get("pass") is False]
|
||||
if isinstance(run_summary, dict):
|
||||
s = dict(run_summary) # 浅拷贝富字段(cost/models/attempts/wallSec/stage/gameId/driverType)
|
||||
else:
|
||||
s = {"gameId": game_id, "attempts": 1, "stage": result.get("stage", "code")}
|
||||
# verdict 以执行器最终判定为准(含断言②);verdictFull 透传供 trace.sevenGateVerdict(后端 D11)。
|
||||
s["verdict"] = {"pass": pass_, "failedGates": failed}
|
||||
s["verdictFull"] = verdict_full
|
||||
s["gameId"] = game_id
|
||||
return s
|
||||
|
||||
|
||||
def _process_regenerate_job(state: WorkerState, job: dict, modify: dict) -> dict:
|
||||
"""A11 M4 模块重生成(改玩法):execute_regenerate_modify 有界重写 game-logic.js → 九门 → 改动清单 +
|
||||
断言②非目标稳定。manifest 进 trace、best-effort 带新版 sourceProject(同 deterministic 口径)。
|
||||
|
||||
与 deterministic 路两点差异:① 执行器是 regen_fn(LLM 重写、有 resume 轮),summary 取重写富 run-summary
|
||||
(cost/models/attempts 完整)而非 attempts=1 的 _modify_summary;② manifest 是单条 dict(非 list)、含
|
||||
changed/untouchedStable。
|
||||
"""
|
||||
trace_id = job.get("traceId") or job.get("job_id")
|
||||
game_id = job.get("gameId") or job.get("job_id")
|
||||
|
||||
import cheap_run
|
||||
|
||||
regen_fn = state.regen_fn
|
||||
if regen_fn is None:
|
||||
import cheap_modify
|
||||
regen_fn = cheap_modify.execute_regenerate_modify
|
||||
|
||||
result = regen_fn(game_id, modify.get("sourceProject"), modify.get("modifyPatch"))
|
||||
summary = _regen_summary(result, game_id)
|
||||
game_dir = cheap_run.game_dir(game_id)
|
||||
|
||||
# best-effort:重写成功则把新版源回传(供下一次链式改用 base);派生失败只记日志、不阻断。
|
||||
source_project = None
|
||||
if result.get("status") == "succeeded":
|
||||
try:
|
||||
profile = state.profile_fn(job, game_dir)
|
||||
if profile:
|
||||
source_project = result_out.build_source_project(game_dir, profile)
|
||||
except Exception as e: # noqa: BLE001 — sourceProject 非承重,派生失败不阻断回调
|
||||
log(f"模块重生成 sourceProject 派生失败(省略,不阻断)trace_id={trace_id}: {type(e).__name__}: {e}")
|
||||
|
||||
payload = result_out.build_result_out(job, summary, game_dir, source_project=source_project)
|
||||
# 改动清单 manifest 附进 trace(后端 trace 是开放 Map、忽略未知键;供前端展示「改了玩法、非目标稳没稳」+ 排障)。
|
||||
manifest = result.get("manifest")
|
||||
if manifest is not None:
|
||||
payload.setdefault("trace", {})["modifyManifest"] = manifest
|
||||
if result.get("error"):
|
||||
payload.setdefault("trace", {})["modifyError"] = result["error"]
|
||||
|
||||
callback_url = (job.get("callback") or {}).get("target")
|
||||
if callback_url:
|
||||
http_status, body = state.send_fn(callback_url, payload, state.callback_secret)
|
||||
log(f"模块重生成回调完成 trace_id={trace_id}, status={payload['status']}, callbackHttp={http_status}, "
|
||||
f"changed={(manifest or {}).get('changed')}, untouchedStable={(manifest or {}).get('untouchedStable')}, "
|
||||
f"body={str(body)[:160]}")
|
||||
else:
|
||||
log(f"模块重生成 job 无 callback.target,跳过回调 trace_id={trace_id}, status={payload['status']}")
|
||||
return payload
|
||||
|
||||
|
||||
def _send_failed(state: WorkerState, job: dict, reason: str) -> None:
|
||||
"""生成异常兜底:发 failed result-out,避免任务挂 RUNNING(对执行器 callbackFailed 语义)。"""
|
||||
trace_id = job.get("traceId") or job.get("job_id")
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
---
|
||||
title: A11 对话式调整回路(试玩后纠错)细化执行 plan
|
||||
status: 已批(创始人 2026-06-29)· 执行中(M1✅ M2✅ M3✅ → M4)
|
||||
status: 已批(创始人 2026-06-29)· 执行中(M1✅ M2✅ M3✅ M4✅ → M5)
|
||||
date: 2026-06-28
|
||||
topic: studio-A11-调整回路
|
||||
canonical: false # 切片三统领在 plan①;本档是「切片三·A11」的可执行细化,不另立 SoT;评审后把 A11 设计面蒸馏回 docs/architecture 生成引擎域(兑现 plan① 收口 TODO⑥)
|
||||
@ -297,6 +297,15 @@ stateDiagram-v2
|
||||
- **九门 smoke(主会话独立复跑,本机 Chrome 免 LLM)**:取 `amgen-bake-click-score-0` base 源 → config 改 `ROUND_MS 30000→19000` → **status=succeeded、stage=play、verdictPass=true、manifest found=true、落盘 core.js=19000、运行时 remainMs=19000**(改的那一处真在游戏里生效并过九门)。坐实"改规范工程一处→重建→过九门"全链路。
|
||||
- **M3 收口**:确定性类执行端到端通(零 LLM);改动清单为 M5 断言①就位。
|
||||
|
||||
**M4 模块重生成执行(完成、绿,2026-06-29)· 有界单文件 LLM 重写**
|
||||
|
||||
- **复用机制(参数化、不复制 resume/熔断/收口)**:`cheap_studio.run_studio` 加 4 个默认 None 可选参(system_prompt/initial_kick/write_whitelist/prepare)切到 modify 态,**create 路零行为变化**(核 diff 确认 params=None 等价原逻辑、仅日志串变);`cheap_toolkit.build_toolkit` 加 write_whitelist(非白名单 basename 写直接拒);`cheap_roles.build_modify_system_prompt`(复用 create 红线契约块)。
|
||||
- **`execute_regenerate_modify`**:取 intent(空→failed)→ materialize base 源 → 重写前后对**非目标文件**算 hash → 有界重写(写边界收窄到只 `game-logic.js`,agent 可读其余取上下文)→ 九门;status=succeeded ⟺ 九门过 ∧ 非目标稳;manifest `{file:game-logic.js, kind:behavior, intent, changed, untouchedStable}`(断言②地基)。
|
||||
- **worker 接线**:`_process_regenerate_job`(regen_fn),把 M3 的 regenerate-module 显式 failed 占位换成真执行;deterministic/create/生成路一字未动。
|
||||
- **测试**:`test_a11_m4_regenerate` 10/10(全注入桩零真 LLM,含"篡改非目标→断言②抓+判 failed")+ 全回归(m1/m2/m3/worker_service/toolkit/roles)主会话独立复跑 **73 测试全绿**。
|
||||
- **一次真 LLM smoke(子代理单跑,本机 Chrome + new-api M3)**:base=`amgen-bake-click-score-0`、intent="把命中加分从固定改成连击递增" → **status=succeeded、untouchedStable=True、九门 verdict.pass=True、attempts=1、¥0.31、137s、无熔断**;独立 diff 佐证 5 个非目标文件字节相同、仅 game-logic.js 变且真实现连击递增。**M4 plumbing 通**(重写发生 + 写边界守住 + 非目标稳 + 九门跑到 verdict);此次质量也恰好达标(质量归 agent 层、不在 M4 判据)。
|
||||
- **M4 收口**:两档执行(deterministic + regenerate-module)全落地;断言②地基(非目标 hash 稳)就位。
|
||||
|
||||
## 状态
|
||||
|
||||
双评审已过(发现全处置)→ 待创始人批 → 执行(M1→M5 顺序,M1 为 plan① 所称"首个里程碑")。
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user