lili 675a8e688b feat(cheap-worker): 便宜档 Node→Python 重写进 AgentScope spike — 范式可移植坐实
WU-A 迁移面 spike:便宜档生成核心(多轮 ReAct + 五工具沙箱 + done 门)用 Python 在新建 cheap-worker/
重写成 AgentScope agent,import 复用 tier2 框架层(model client/四道熔断/历史压缩/observability),
check/build/九门 shell-out 现有 node/cjs(不重写 esbuild/CDP/形状门)。

- U1 地基:跨包 sys.path import tier2 worker + 代理旁路 + key 从内网文档注入 env + M3 装配
- U2 五工具:read/list/write Python 实现 + check/build shell-out(形状门三态与 Node 一致)
- U3 LittleJS system prompt:prompt.mjs Python 化(两条 footgun 正反例 + 输入契约)
- U4 scaffold/stage/play 薄壳 + ensure_play_spec(据 state 形态产 play-spec 治链路假绿)
- U5 ReAct 主编排:内置 Agent+ReActConfig+外层有界 resume + done 门 + 收口 stage/smoke/九门
- U6 对照 Node:产物形态 + 九门逐门 + 过门率对比

验收:同 brief Python 新路 vs Node 旧路,产物都真 src/ 6 文件、九门都 9/9 全 PASS、过门率 1.0=1.0
→ spikePass=True。端到端单局 attempt 1 一次收敛、148s。落点 cheap-worker/ 对 tier2/amodel-gen
零改动(结构性零回归)。测试 38/38 全绿。

origin: docs/plans/2026-06-26-001-feat-cheap-worker-python-spike-plan.md

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 02:42:14 -07:00

108 lines
4.8 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 的 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):
"""组装便宜档 Toolkit(对 tier2 toolkit.py build_toolkit)。返回 agentscope.tool.Toolkit。"""
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: 完整文件内容。
"""
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)