B 门(AgentScope 重机器)四单元并行建设,文件不相交,均过 6c6g 静态校验(py_compile +
惰性 import 安全 + 机器门)。真跑收敛/Studio/门验证属 mini-desktop,作为下一步 e2e。
B1 控制面有界 resume(service/control_plane.py · 皇冠):
补上 service/app.py 与 bootstrap.py 文件头标的 followup——CLI run_studio:404-458 的外层有界
resume(M3 看一次 verdict 就停 → 带反馈踹回去续修)此前在服务态(create_app :8200)缺失。
drive_generation() 复用 bootstrap REST 原语 + run 纯代码门:每回合消费 SSE(REPLY_END/
EXCEED_MAX_ITERS)等 chat run 结束 → 独立跑 run.run_gates 机器判门 → 门绿据 on-disk 源工程
重建七要素 + persist 落库 / 否则 verdict_feedback 续修,直到门绿/预算耗尽/熔断。不靠内存
session 判收敛(服务态九工具每回合工厂新建 Tier2Session),贴合「门机器判、控制面绝不自评翻绿」。
C1 AgentScope Studio 观测(observability/studio_sink.py + trace.py):
把统一 trace 事件按 OTLP(经核实 Studio = OpenTelemetry,非私有 REST;HTTP 3000 /v1/traces)
映射成 span 推 Studio,traceId→gen_ai.conversation.id 聚成一条 run。私有 TracerProvider 避免
抢 otel 全局单例。默认关闭(无 TIER2_STUDIO_URL 即 no-op)、observe-only、绝不改 decision。
C2 MCP 工具面(worker/mcp_tools.py + toolkit.py 加性挂点):
图说 E 族——build_toolkit 末尾加配置门控挂点,默认关闭时 build_mcp_clients() 返回 []、
Toolkit(tools, mcps=[]) 与原构造等价,九工具(spike 已验证主链)行为零改。Toolkit 接受 mcps=
形参已对源码核实(tool/_toolkit.py:93)。
C3 基线 client + Prompt 一致性机器门(config.py + genconfig.py + generation.yaml + ci/):
build_baseline_model(Opus 经 new-api Anthropic 原生)供 A 门「证路」,build_model M3 路零回归;
check-prompt-registry.sh 文件级三向一致 + import 级命中 registry,6c6g 实跑 exit 0。
修复 design_team 配置外置兑现(design_team.py):
leader_max_iters/per_expert_cap/timeout_s 改 None→genconfig.get('design_team',...),此前硬编码
默认 240 导致改 generation.yaml 的 timeout 不生效;已验 genconfig 真读到 YAML 值。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
397 lines
22 KiB
Python
397 lines
22 KiB
Python
"""toolkit.py —— tier2 单写 ReAct agent 的九工具面(A7 · toolkit-signatures 契约落地)。
|
|
|
|
接法(AgentScope v2.0.2 源码核验):2.0.2 **没有 register_tool_function**;把 Python 函数变 agent 可调工具
|
|
靠 `FunctionTool(func)` 薄壳(tool/_adapters.py),再经构造器 `Toolkit(tools=[FunctionTool(...), ...])`
|
|
(tool/_toolkit.py)装进 toolkit,最后 `Agent(toolkit=...)`。工具入参 schema 由 AgentScope 从函数签名 +
|
|
docstring 自动抽(tool/_utils.py:_extract_input_schema),故每个工具用**带类型注解的参数 + Google 风格 docstring**。
|
|
|
|
九工具三类(toolkit-signatures A7):
|
|
核心环:write_source / build / headless_check / run_gates / read_verdict / finish
|
|
取证: screenshot / query_asset
|
|
起手收尾:scaffold_init / finish
|
|
|
|
三条边界铁律(焊死,见契约):
|
|
① 单写独占:整工程只一个 agent 写(本 toolkit 绑单 session,不并行);
|
|
② finish 入参 schema = A3 源项目契约(tier2-source-project.schema.json),不另写;
|
|
③ 验收零自评:run_gates/read_verdict 的 verdict 由 judge 纯代码产出,screenshot 只取证不判定。
|
|
|
|
工具返回:统一返回 JSON 字符串(FunctionTool 会包成 agent 可读结果;见 _adapters.py:_convert_func_result_to_chunk)。
|
|
"""
|
|
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
|
|
from agentscope.tool import Toolkit, FunctionTool
|
|
|
|
from . import run
|
|
|
|
# ── 平台锁定文件(写死、agent 不许改;spike feie-002 实证根因 3)──
|
|
# fixture-spec 预建分工:平台预建(装载胶水 + 三系统纯逻辑 + 状态机 + 结算 + latch + readState 导出 +
|
|
# 输入路由/命中映射约定)写死不动;agent 只写表现层 play-scene.js + 数据表 + 新增表现文件。
|
|
# 实证:M3 重写了 main.js(装载胶水)→ 丢了 recHook 透传 → F 门 calls=0、合成不记引擎调用、G/H 门连环挂。
|
|
# 故把 fixture 平台预建文件焊成只读:write_source 改它直接拒,并提示「只改 play-scene.js + 数据表」。
|
|
LOCKED_PLATFORM_FILES = frozenset({
|
|
"src/main.js", # entry · PhaserGameFactory 装载胶水(recHook/caps/scene 接线在此)
|
|
"src/game-core.js", # 三系统编排 + 状态机 + 结算 + latch + _forensicsView/readState 导出
|
|
"src/layout.js", # 棋盘/订单面板布局常量单一源(命中映射 + driver tap 同源坐标)
|
|
"src/data/tables.js", # 数据表适配层(按固定 key 读 datatable.gold.json)
|
|
"src/util/seeded-random.js", # 受控随机源(确定性铁律)
|
|
"src/systems/resource-system.js", # 资源系统纯逻辑(addCoins/consumeIngredient 经 recHook 记调用)
|
|
"src/systems/merge-system.js", # 合成系统纯逻辑(tapCell 两次点同物品合成)
|
|
"src/systems/order-system.js", # 订单系统纯逻辑
|
|
"src/systems/reachability.js", # 可达性纯逻辑
|
|
"src/scenes/play-runtime.js", # 表现层平台锁定运行时插座(输入路由 + 逐帧 core.update tick)
|
|
})
|
|
|
|
|
|
def is_locked_platform_file(rel: str) -> bool:
|
|
"""该工程内相对路径是否平台锁定(写死、agent 不许改)。"""
|
|
return rel in LOCKED_PLATFORM_FILES
|
|
|
|
|
|
def _content_hash(files: list[dict]) -> str:
|
|
"""整工程内容指纹 = sha256(各文件 path+content 规范化拼接)。对齐 A3 contentHash 语义。"""
|
|
h = hashlib.sha256()
|
|
# 按 path 排序后拼接,保证同内容同指纹(规范化)。
|
|
for f in sorted(files or [], key=lambda x: (x or {}).get("path", "")):
|
|
h.update(((f or {}).get("path", "") + "\0").encode("utf-8"))
|
|
h.update(((f or {}).get("content", "") + "\0").encode("utf-8"))
|
|
return h.hexdigest()
|
|
|
|
|
|
class Tier2Session:
|
|
"""单写 session 的可变状态(单 agent 独占;绑 game_id)。
|
|
|
|
工具闭包共享它:write_source 累积 file 树、build/run_gates 记最近结果、finish 据它组装源工程。
|
|
"""
|
|
|
|
def __init__(self, game_id: str, *, entry: str = "src/main.js",
|
|
phaser_version: str = "3.80.1", play_spec: dict | None = None) -> None:
|
|
self.game_id = game_id
|
|
self.entry = entry
|
|
self.phaser_version = phaser_version
|
|
self.play_spec = play_spec or {}
|
|
# path → content(write_source / scaffold_init 累积的工程文件树)。
|
|
self.files: dict[str, str] = {}
|
|
# path → role(对齐 A3 fileTree[].role;scaffold_init 填初始角色,write_source 默认 other)。
|
|
self.roles: dict[str, str] = {}
|
|
self.last_verdict: dict | None = None
|
|
self.last_build: dict | None = None
|
|
self.finished: dict | None = None
|
|
|
|
def file_list(self) -> list[dict]:
|
|
"""当前文件树(供落盘 / contentHash;[{path, content}])。"""
|
|
return [{"path": p, "content": c} for p, c in self.files.items()]
|
|
|
|
def file_tree(self) -> list[dict]:
|
|
"""A3 fileTree 形状([{path, role}])。"""
|
|
return [{"path": p, "role": self.roles.get(p, "other")} for p in self.files]
|
|
|
|
|
|
def build_toolkit(session: Tier2Session) -> Toolkit:
|
|
"""据单 session 造九工具面 Toolkit(FunctionTool + 构造器 tools=,2.0.2 正确姿势)。"""
|
|
|
|
# ── 起手:scaffold_init ──
|
|
async def scaffold_init(template_id: str, brief: str = "", design_docs: str = "") -> str:
|
|
"""开局铺一套先天过 boot 的 Phaser 富游戏骨架(平台预建 ~25%:三系统纯逻辑 + 装载胶水 + 留空数据表)。
|
|
|
|
一次 session 开头调一次。铺的是 fixture 平台预建骨架(先天过 boot),你只需:
|
|
① 把 data/datatable.gold.json 的留空 sentinel 填成本游戏主题的真值(物品/合成链/订单数值);
|
|
② 在 src/scenes/play-scene.js 的 TODO 处填那约 56% 表现层(棋盘渲染/订单面板/HUD/反馈)。
|
|
逻辑命令(core.tapCell/fulfillOrder)、命中映射坐标系、readState 语义约定不要动。
|
|
|
|
Args:
|
|
template_id: 品类模板 id(如 'business-sim' = 经营/合成富游戏 = mini-肥鹅 三系统)。
|
|
brief: 题面(可选)。
|
|
design_docs: 阶段 1 设计结论(可选,作为工程内文档写入 DESIGN.md)。
|
|
"""
|
|
# ① 读品类模板对应的 fixture 平台预建骨架(M4/M1 真实预建,先天过 boot)。
|
|
scaffold = run.load_fixture_scaffold(template_id)
|
|
if scaffold["ok"]:
|
|
# 铺 fixture 骨架进 session(已存在的不覆盖,允许 agent 已先写过个别文件)。
|
|
for f in scaffold["files"]:
|
|
session.files.setdefault(f["path"], f["content"])
|
|
session.roles.setdefault(f["path"], f.get("role", "other"))
|
|
else:
|
|
# 兜底:未知品类 → 最小可被 write_source 覆盖的空骨架(诚实标注,不假装铺了真骨架)。
|
|
fallback = {
|
|
session.entry: (
|
|
"// src/main.js —— Phaser 工程入口(scaffold_init 兜底空骨架;write_source 覆盖)\n"
|
|
"// TODO(agent): 配置 Phaser.Game,挂 scene 树,暴露 readState 语义 state 视图。\n"
|
|
),
|
|
"src/scenes/PlayScene.js": "// Play scene 脚手架;write_source 填多系统表现层。\n",
|
|
"src/data/datatable.json": "{}\n",
|
|
}
|
|
for path, content in fallback.items():
|
|
session.files.setdefault(path, content)
|
|
session.roles.setdefault(
|
|
path, "entry" if path == session.entry
|
|
else "scene" if "/scenes/" in path
|
|
else "config" if "/data/" in path else "other")
|
|
if design_docs:
|
|
session.files["DESIGN.md"] = design_docs
|
|
session.roles["DESIGN.md"] = "other"
|
|
# ② 落盘整骨架(供后续 build/run_gates 子进程读;scaffold 同时拷宿主 index.html 模板)。
|
|
res = run.scaffold(session.game_id, session.file_list())
|
|
msg = (f"fixture 骨架已铺(template={template_id},{len(scaffold['files'])} 文件):"
|
|
"填 data/datatable.gold.json 真值 + play-scene.js 的 TODO 表现层。"
|
|
if scaffold["ok"]
|
|
else f"⚠未知品类 template={template_id}({scaffold['reason']}):已铺最小空骨架,需你 write_source 写全。")
|
|
return json.dumps(
|
|
{"ok": res["ok"], "workdir": res["workdir"], "fileTree": session.file_tree(),
|
|
"scaffoldKind": "fixture" if scaffold["ok"] else "minimal", "message": msg},
|
|
ensure_ascii=False)
|
|
|
|
# ── 核心环:write_source ──
|
|
async def write_source(files: list) -> str:
|
|
"""把一批源文件写进工程(多文件、允许 import;写那约 56% 表现层的主手段)。
|
|
|
|
直接落文件,**不从 markdown 抽代码**(与 Tier0/1 反转)。越界路径(含 '..'/绝对路径)会被拒。
|
|
|
|
Args:
|
|
files: 文件数组,每项 {path: 工程内相对路径(如 'src/scenes/PlayScene.js',禁 '..'),
|
|
content: 文件全文 UTF-8, role?: 文件角色(entry/scene/system/ui/config/lib/other)}。
|
|
"""
|
|
written, errors, locked = [], [], []
|
|
for f in files or []:
|
|
rel = (f or {}).get("path") or ""
|
|
content = (f or {}).get("content")
|
|
if not rel or ".." in rel.split("/") or rel.startswith("/"):
|
|
errors.append({"path": rel, "reason": "非法路径(空/含 '..'/绝对路径)"})
|
|
continue
|
|
if not isinstance(content, str):
|
|
errors.append({"path": rel, "reason": "content 必须是字符串"})
|
|
continue
|
|
# 平台锁定文件保护(根因 3):若改的是写死的平台文件且内容真变了,拒绝并提示。
|
|
# 允许「内容完全相同」的无操作重写(幂等;不阻断 agent 把整树回写)。
|
|
if is_locked_platform_file(rel) and session.files.get(rel) != content:
|
|
locked.append({
|
|
"path": rel,
|
|
"reason": "这是平台锁定文件(装载胶水/三系统纯逻辑/状态机/结算/latch/readState/布局),"
|
|
"写死不许改——改它会破坏引擎接线(如丢 recHook → F 门 calls=0、合成不记调用)。"
|
|
"你只该改 src/scenes/play-scene.js(表现层)+ data/datatable.gold.json(数据表),"
|
|
"或在 src/scenes/ 下新增表现文件。请把这个文件的改动撤回。",
|
|
})
|
|
continue
|
|
session.files[rel] = content
|
|
if (f or {}).get("role"):
|
|
session.roles[rel] = f["role"]
|
|
elif rel not in session.roles:
|
|
session.roles[rel] = "other"
|
|
written.append(rel)
|
|
# 同步落盘(供后续 build/headless_check 子进程读)。
|
|
run.scaffold(session.game_id, session.file_list())
|
|
return json.dumps(
|
|
{"ok": len(errors) == 0 and len(locked) == 0,
|
|
"written": written, "errors": errors, "lockedRejected": locked},
|
|
ensure_ascii=False)
|
|
|
|
# ── 核心环:validate_datatable(数据表 schema/可达性预检)──
|
|
async def validate_datatable() -> str:
|
|
"""校验 data/datatable.gold.json 是否符合平台锁定 schema + 富游戏可达性不变量(合成链非空/DAG/订单可达)。
|
|
|
|
强烈建议填完数据表后、build 前先调它。失败会精确告诉你平台读哪些 key、你缺/错了哪个
|
|
(平台锁定文件 src/data/tables.js 按固定 key 消费数据表;自创 schema 会让合成系统空转、三联动门全挂)。
|
|
注:build 也会自动跑这道预检,不过提前调能省一整轮。
|
|
"""
|
|
dv = run.validate_datatable(session.game_id)
|
|
return json.dumps(
|
|
{"ok": dv["ok"], "errors": dv["errors"],
|
|
"expectedSchemaHint": dv.get("expectedSchemaHint", "")},
|
|
ensure_ascii=False)
|
|
|
|
# ── 核心环:build ──
|
|
async def build() -> str:
|
|
"""esbuild 多文件打包当前 src/ 工程(入口 src/main.js,产 iife bundle)。
|
|
|
|
构建失败返回 ok=false + 完整 esbuild 日志,据 log 改源重试。
|
|
"""
|
|
res = run.build(session.game_id, entry=session.entry)
|
|
session.last_build = res
|
|
# 失败 log 可能很长,截断喂回(避免撑爆上下文;完整在落盘日志)。
|
|
return json.dumps(
|
|
{"ok": res["ok"], "bundlePath": res["bundlePath"],
|
|
"durationMs": res["durationMs"], "log": (res["log"] or "")[:1500]},
|
|
ensure_ascii=False)
|
|
|
|
# ── 核心环:headless_check ──
|
|
async def headless_check() -> str:
|
|
"""全套真玩门前的便宜快筛(无头:语法 / bundle 非空)。
|
|
|
|
这是快筛、不是验收门 —— ok 不代表过 L1,只是值不值得上真玩门。结果不入硬判。
|
|
"""
|
|
res = run.headless_check(session.game_id)
|
|
return json.dumps({"ok": res["ok"], "signals": res["signals"], "log": res["log"]},
|
|
ensure_ascii=False)
|
|
|
|
# ── 核心环:run_gates ──
|
|
async def run_gates() -> str:
|
|
"""真浏览器真玩,跑 L1 九门 + 富游戏三门,返回机器判的 verdict(零自评)。
|
|
|
|
verdict 由 judge 纯代码产出,你不给自己打分;据返回的失败门改源。
|
|
"""
|
|
res = run.run_gates(session.game_id, session.play_spec)
|
|
session.last_verdict = res["verdict"]
|
|
# 给 agent 的回喂:verdict 摘要 + 失败门反馈(人读)。
|
|
feedback = run.verdict_feedback(res["verdict"], res.get("log", ""))
|
|
v = res["verdict"] or {}
|
|
l1 = ((v.get("layerResults") or {}).get("L1") or {})
|
|
return json.dumps(
|
|
{"rc": res["rc"], "decision": v.get("decision"),
|
|
"structureOk": v.get("structureOk"), "runnableOk": v.get("runnableOk"),
|
|
"l1Passed": l1.get("passed"), "feedbackText": feedback},
|
|
ensure_ascii=False)
|
|
|
|
# ── 核心环:read_verdict ──
|
|
async def read_verdict() -> str:
|
|
"""读回上次 run_gates 的裁决与失败门摘要(只读,不重跑门)。"""
|
|
v = session.last_verdict
|
|
if not v:
|
|
return json.dumps({"available": False,
|
|
"feedbackText": "尚无 verdict;先调 run_gates。"}, ensure_ascii=False)
|
|
l1 = ((v.get("layerResults") or {}).get("L1") or {})
|
|
return json.dumps(
|
|
{"available": True, "decision": v.get("decision"),
|
|
"structureOk": v.get("structureOk"), "runnableOk": v.get("runnableOk"),
|
|
"l1Passed": l1.get("passed"),
|
|
"feedbackText": run.verdict_feedback(v)},
|
|
ensure_ascii=False)
|
|
|
|
# ── 取证:screenshot ──
|
|
async def screenshot(phase: str = "play", label: str = "") -> str:
|
|
"""读取 CDP harness 真玩时落盘的那一帧游戏截图(喂给 L3 软检与人工终审/debug)。
|
|
|
|
边界铁律:截图只取证、**绝不进硬门判定**。L1/L2 判定零截图参与。
|
|
|
|
真截图由 run_gates 时的 CDP harness(play-phaser.cdp.cjs)在真浏览器里真截并落盘到
|
|
evidence/{first-paint.png, after-play.png}(mini-desktop 真跑;6c6g 无 chrome 时不产图)。
|
|
本工具据 phase 把『约定时相』映射到 harness 实际落盘的那两张真图(不再凭空拼一个不存在的路径):
|
|
- 'title'/'first'/'boot' → first-paint.png(首帧渲染快照);
|
|
- 其余('play'/'gameover'/…) → after-play.png(驱动真玩后快照)。
|
|
返回 exists 字段表明真图是否已落盘——只有 run_gates 真跑过、且在 mini-desktop 有 chrome 时才为 True。
|
|
|
|
Args:
|
|
phase: 截哪个时相(如 'title'/'play'/'gameover')。
|
|
label: 截图标签(可选;仅作回显,不改落点——落点由 harness 真截的固定文件名决定)。
|
|
"""
|
|
# 真截图由 CDP harness 在真玩时落盘(U3/U6,mini-desktop);本工具只读那张真图的真实路径,不再返回桩路径。
|
|
ev_dir = run._workdir(session.game_id) / "evidence"
|
|
# 约定时相 → harness 真落盘的固定文件名(harness saveScreenshot 的两个落点)。
|
|
p = (phase or "play").lower()
|
|
shot_name = "first-paint.png" if p in ("title", "first", "boot", "firstpaint", "first-paint") else "after-play.png"
|
|
shot_path = ev_dir / shot_name
|
|
exists = shot_path.exists()
|
|
return json.dumps(
|
|
{"path": str(shot_path), "phase": phase, "label": label,
|
|
"exists": exists,
|
|
"note": ("截图只取证,绝不进 L1/L2 硬门判定(防 Goodhart)。"
|
|
+ ("真图已就位(CDP harness 真截落盘)。" if exists
|
|
else "真图尚未落盘:run_gates 真跑后(mini-desktop chrome)才会产图。"))},
|
|
ensure_ascii=False)
|
|
|
|
# ── 取证:query_asset ──
|
|
async def query_asset(category: str, count: int = 4) -> str:
|
|
"""按类目查可用资产(六类),不感知底下是哪个资产源(provider 可插拔)。
|
|
|
|
只查/取引用,不生成(生成走 B-ASSET-mmx provider)。spike 期走共用资产池占位图。
|
|
|
|
Args:
|
|
category: 资产类目('sprite'|'character'|'effect'|'scene'|'ui'|'music')。
|
|
count: 要几个(默认 4)。
|
|
"""
|
|
# spike 期:返回占位资产引用(真 provider 接入走 A9,本期不感知实现)。
|
|
refs = [{"id": f"placeholder-{category}-{i}", "category": category,
|
|
"ref": f"shared-pool://{category}/{i}", "provider": "placeholder"}
|
|
for i in range(max(1, min(count, 12)))]
|
|
return json.dumps({"assets": refs}, ensure_ascii=False)
|
|
|
|
# ── 核心环 + 收尾:finish ──
|
|
async def finish(entry: str = "src/main.js", phaser_version: str = "") -> str:
|
|
"""ReAct 收敛那一刻的收尾:吐出最终源工程(tier2 源项目契约形状),交付落库。
|
|
|
|
入参 schema 与 A3 源项目契约共用同一份定义(tier2-source-project.schema.json):
|
|
projectType/fileTree/entry/buildProfile/depLock/contentHash/addressing。这里据当前 session
|
|
文件树自动组装这七要素(agent 只需确认 entry / phaser 版本),从源头杜绝交付↔落库漂移(F3)。
|
|
|
|
Args:
|
|
entry: 构建入口(默认 src/main.js;必须命中已写入的某个文件)。
|
|
phaser_version: Phaser 引擎精确版本(默认沿用 session 配置)。
|
|
"""
|
|
if entry not in session.files:
|
|
return json.dumps(
|
|
{"ok": False, "message": f"entry '{entry}' 不在已写入文件树中;先 write_source 写入入口文件。"},
|
|
ensure_ascii=False)
|
|
# finish 门(spike feie-001 根因 1):验收零自评——不许在没真跑过门、或门没绿时就 finish 收尾。
|
|
# 这道门防 agent 看一次 fix 就放弃/或自我宣布做好。门未绿就把失败反馈塞回去,逼它继续修。
|
|
v = session.last_verdict
|
|
if not v:
|
|
return json.dumps(
|
|
{"ok": False, "blockedByGate": True,
|
|
"message": "还没跑过验收门,不能 finish。先 run_gates 真玩,据 verdict 修到门绿再 finish。"},
|
|
ensure_ascii=False)
|
|
l1 = ((v.get("layerResults") or {}).get("L1") or {})
|
|
if v.get("decision") != "accept" or not l1.get("passed"):
|
|
return json.dumps(
|
|
{"ok": False, "blockedByGate": True,
|
|
"decision": v.get("decision"), "l1Passed": l1.get("passed"),
|
|
"feedbackText": run.verdict_feedback(v),
|
|
"message": "验收门未全绿(decision≠accept 或 L1 未过),不能 finish。"
|
|
"请据上面失败门继续 write_source 针对性修,再 run_gates,直到门绿。"},
|
|
ensure_ascii=False)
|
|
files = session.file_list()
|
|
chash = _content_hash(files)
|
|
# 组装 A3 源项目契约形状(七要素):身份/骨架/入口/构建/依赖锁/内容指纹/落库寻址。
|
|
source_project = {
|
|
"schemaVersion": "tier2-1.0",
|
|
"projectType": "tier2-phaser",
|
|
"fileTree": session.file_tree(),
|
|
"entry": entry,
|
|
"buildProfile": {
|
|
"bundler": "esbuild", "format": "iife",
|
|
"globalName": run.DEFAULT_GLOBAL_NAME, "minify": True, "target": "es2019",
|
|
},
|
|
"depLock": {"phaser": phaser_version or session.phaser_version},
|
|
"contentHash": chash,
|
|
"addressing": {"store": "mysql+oss", "fetchById": "contentHash"},
|
|
}
|
|
session.finished = source_project
|
|
# 注:落库 versionId 由后端回填,本工具只交付形状 + contentHash。
|
|
return json.dumps(
|
|
{"ok": True, "contentHash": chash,
|
|
"sourceProject": source_project,
|
|
"message": "源工程已交付(tier2 源项目契约形状);落库 versionId 由后端回填。"},
|
|
ensure_ascii=False)
|
|
|
|
# 九工具经 FunctionTool 包壳,构造器 tools= 装进 Toolkit(2.0.2 唯一姿势,无 register_tool_function)。
|
|
tools = [
|
|
FunctionTool(scaffold_init),
|
|
FunctionTool(write_source),
|
|
FunctionTool(validate_datatable),
|
|
FunctionTool(build),
|
|
FunctionTool(headless_check),
|
|
FunctionTool(run_gates),
|
|
FunctionTool(read_verdict),
|
|
FunctionTool(screenshot),
|
|
FunctionTool(query_asset),
|
|
FunctionTool(finish),
|
|
]
|
|
|
|
# ── 加性挂点(图说 E 族 · 可选 MCP 工具接入)── 默认关闭,九工具行为零改 ──
|
|
# 仅当 generation.yaml 的 mcp 段 enabled=true(或 env TIER2_GEN__MCP__ENABLED)且 servers 非空时,
|
|
# build_mcp_clients 才返回若干【stateless】MCP 客户端,经 Toolkit(mcps=...) 落进保留的 "basic" 组,
|
|
# 让 agent 在九工具之外多一组外部工具(2.0.2 MCP 接入的唯一构造姿势,见 mcp_tools.py 头部 API 核对)。
|
|
# 默认 / 未配置 / 任何异常 → build_mcp_clients 返回 [] → Toolkit(tools=tools, mcps=[]) 与原
|
|
# Toolkit(tools=tools) 字节级一致(构造器 mcps or [] → 同为空 list),九工具(spike 已验证主链)绝不受影响。
|
|
# stateful / stdio 客户端需 await connect(),不能在此同步路径承载——走 mcp_tools.register_mcp_tools(async)。
|
|
# 惰性 import:6c6g 无 agentscope.mcp,故在挂点处函数内 import(import 失败即 no-op,不阻断)。
|
|
mcp_clients: list = []
|
|
try:
|
|
from .mcp_tools import build_mcp_clients
|
|
mcp_clients = build_mcp_clients() # 默认关闭 → []
|
|
except Exception as exc: # noqa: BLE001 —— MCP 接入是加性增强,任何失败都退化为「不挂 MCP」,不连累九工具
|
|
print(f"[tier2-toolkit] MCP 接入挂点异常({type(exc).__name__}: {exc})→ 仅装九工具(无 MCP)。", flush=True)
|
|
mcp_clients = []
|
|
|
|
return Toolkit(tools=tools, mcps=mcp_clients)
|