lili db3c42c5de feat(amodel): 运行时日志基建(ctx.log+插件自带+harness捕获) + 九门A(play-spec透传)+B2(F_wiring) + 预算
创始人指令:游戏要有日志,读日志诊断运行时问题,模板+可选插件自带日志。

日志基建:
- plugin.js:受控面 ctx 加 log(tag,...args) → window.__gameLog 环形(800)+ console;用受控 time.nowMs。
- boot-game-host.js:B2 插件 Proxy wrap 顺手记插件语义调用进 __gameLog(跳 draw/update/current/get 等高频)→ 插件自带日志免逐个改;recHook 仍记 __engineCalls 供 F_wiring。
- _template game-logic.js:ctx.log 生命周期(init/scene play/over)→ 模板自带。
- play.mjs smokeBoot:抓 __gameLog 写 staged evidence/game-log.json + 末12条进 out.logTail(boot 成败都抓,失败时最有诊断价值)。
- 实证:scaffold→smoke logtest → logCount:165、game-log.json 有 [p:sceneFsm:define/start]+[game:init];读日志诊断可用。

九门对齐(C 之 A+B2,recon ad3c564):
- A(SaaGenNodes validateAmodelStaged):amodel 路 validate-ok→play 短路了 scaffoldNode(唯一写 play-spec.json 处)→ 补写 play-spec.json(从 design 产 K_PLAY_SPEC)→ 九门可驱动(治 E_live/G_input/H_progress 机制)。
- B2(boot-game-host):插件 recHook wrap → F_wiring callCount>0(实证 e2e 从 [E_live,F_wiring] → [E_live,H_progress],F_wiring 已过)。叠加式,factory/gamedef 零回归。

预算(gen.mjs):maxSteps 45→70 / maxWallMs 12→20min —— SAA design 富化 brief 比 raw(7步)需更多步。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 22:21:29 -07:00

230 lines
11 KiB
JavaScript
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.

/**
* gen.mjs — agentic A-model 生成 orchestrator(L2 ReAct 循环 · spike)
* owner:amodel-gen spike
*
* 流程:scaffold(_template→amgen-<id>) → ReAct(M3 + 5 工具,done 门=check+build 绿)
* → play(循环外·权威门) → 落 verdict / run-log / run-summary。
*
* 用法(cwd=game-runtime,mini-desktop):
* spike:node tools/amodel-gen/gen.mjs --id run1 --brief "..." [--max-steps 40] [--port 4320] [--cdp 9222]
* SAA :node tools/amodel-gen/gen.mjs --mode saa --game-id <gameId> --brief "..."(__GameBundle 信封 + stage 到 _wg1-gen/ + done 门含 smoke-boot;stdout 末行 JSON)
*
* 注:本文件是 harness 侧,允许 Date.now/setTimeout(红线只约束游戏 src)。
*/
'use strict';
import { writeFileSync, mkdirSync } from 'node:fs';
import { join } from 'node:path';
import { scaffold, scaffoldSaa, readFileTool, writeFileTool, listDirTool, check, build, buildSaa, stage, gameDir } from './tools.mjs';
import { chat, MODEL } from './m3.mjs';
import { TOOLS, buildSystemPrompt } from './prompt.mjs';
import { play, smokeBoot } from './play.mjs';
function arg(name, def) {
const i = process.argv.indexOf(`--${name}`);
return i >= 0 ? process.argv[i + 1] : def;
}
const TOOL_RESULT_CAP = 30000; // 单次工具结果注入上限(够一份 skill/api.d.ts,控 context)
/** 历史压缩:折叠旧的大 tool 结果 / write 内容,控 token(主流 context 工程:循环越长越要压)。占位 <400 字符 → 幂等。 */
function compactHistory(messages, keepLast = 8) {
const head = 2; // system + 首个 user-brief 保全
const tailStart = Math.max(head, messages.length - keepLast);
for (let i = head; i < tailStart; i++) {
const m = messages[i];
if (!m) continue;
if (m.role === 'tool' && typeof m.content === 'string' && m.content.length > 400) {
m.content = '[旧工具结果已折叠省 token;需要可重新 read_file]';
}
if (m.role === 'assistant' && Array.isArray(m.tool_calls)) {
for (const c of m.tool_calls) {
if (c.function && c.function.name === 'write_file' && c.function.arguments && c.function.arguments.length > 400) {
try {
const a = JSON.parse(c.function.arguments);
c.function.arguments = JSON.stringify({ path: a.path, content: '[旧 write 内容已折叠;文件在磁盘,需要可重新 read_file]' });
} catch { /* 坏 JSON 略过 */ }
}
}
}
}
}
async function main() {
const mode = arg('mode', 'spike'); // 'spike'(默认,_template/__gameBooted 信封)| 'saa'(__GameBundle 信封 + stage 到 _wg1-gen)
const isSaa = mode === 'saa';
const id = isSaa ? arg('game-id', arg('id', 'run1')) : arg('id', 'run1');
const brief = arg('brief', '一个简单的点击得分小游戏');
const maxSteps = Number(arg('max-steps', '70')); // 默认 70:raw brief 7 步(3层架构 newarch1),但 SAA design 富化 brief(juice/音效/屏震/gatespec)需更多步;留余量
const maxWallMs = Number(arg('max-wall-ms', '1200000')); // 单次 attempt 墙钟上限 20min:design 富化 brief + M3 大上下文 turn 慢,raw 102s 但富化需更久;bound thrash 而不误杀富化正常 run
const port = Number(arg('port', '4320'));
const cdpPort = Number(arg('cdp', '9222'));
// 模式分派:SAA 用 __GameBundle 信封 + buildSaa + stage + smoke;spike 用 _template 信封 + build + play。
const doScaffold = isSaa ? () => scaffoldSaa(id) : () => scaffold(id);
const doBuild = isSaa ? () => buildSaa(id) : () => build(id);
const log = [];
const t0 = Date.now();
const rec = (m) => {
const line = `[+${((Date.now() - t0) / 1000).toFixed(1)}s] ${m}`;
console.log(line);
log.push(line);
};
rec(`model=${MODEL} mode=${mode} id=${id} brief=「${brief}」maxSteps=${maxSteps}`);
rec(`scaffold amgen-${id}(clone _template${isSaa ? ' + SAA 信封' : ''})`);
doScaffold();
const messages = [
{ role: 'system', content: buildSystemPrompt(id) },
{ role: 'user', content: `请按这个 brief 造一款游戏:「${brief}」。先读手册再动手。` },
];
let lastCheck = null;
let lastBuild = null;
let doneSummary = null;
let finished = false;
let nudgedDone = false;
let lastSmoke = null;
let steps = 0;
let totalTokens = 0;
const callSig = new Map(); // loop-detection:同一 (name,args) 计数
while (!finished && steps < maxSteps) {
if (Date.now() - t0 > maxWallMs) { rec(`wall-time cap ${Math.round(maxWallMs / 1000)}s 超时,截停(防 thrash;失败交 repair fresh 重试)`); break; }
steps++;
compactHistory(messages); // 每轮先压旧大块,控 token
let resp;
try {
resp = await chat(messages, TOOLS);
} catch (e) {
rec(`step${steps} M3 调用失败: ${e.message}`);
break;
}
totalTokens += (resp.usage && resp.usage.total_tokens) || 0;
const msg = resp.message;
messages.push(msg);
const calls = msg.tool_calls || [];
if (calls.length === 0) {
rec(`step${steps}: 无 tool_calls(finish=${resp.finishReason}; content="${(msg.content || '').slice(0, 80)}")`);
messages.push({ role: 'user', content: '请用工具继续(read_file/write_file/check/build);全绿后调 done。' });
continue;
}
for (const c of calls) {
const name = c.function.name;
let a = {};
try { a = JSON.parse(c.function.arguments || '{}'); } catch { /* 容错:坏 JSON 当空参 */ }
const sig = name + ':' + (c.function.arguments || '');
callSig.set(sig, (callSig.get(sig) || 0) + 1);
let result;
if (name === 'read_file') {
const r = readFileTool(a.path);
result = r.ok ? (r.truncated ? '[内容已截断]\n' : '') + r.content : 'ERROR: ' + r.error;
rec(`read ${a.path} ${r.ok ? 'ok' : 'ERR ' + r.error}`);
} else if (name === 'list_dir') {
const r = listDirTool(a.path);
result = r.ok ? r.entries.join('\n') : 'ERROR: ' + r.error;
rec(`list ${a.path} ${r.ok ? 'ok' : 'ERR'}`);
} else if (name === 'write_file') {
const r = writeFileTool(id, a.path, a.content || '');
result = r.ok ? `OK ${r.bytes}B 已写` : 'ERROR: ' + r.error;
rec(`write ${a.path} ${r.ok ? r.bytes + 'B' : 'ERR ' + r.error}`);
} else if (name === 'check') {
lastCheck = check(id);
result = lastCheck.ok ? 'PASS' : 'FAIL:\n' + lastCheck.errors.join('\n');
rec(`check ${lastCheck.ok ? 'PASS' : 'FAIL(' + lastCheck.errors.length + ')'}`);
} else if (name === 'build') {
lastBuild = doBuild();
result = lastBuild.ok ? 'PASS' : 'FAIL:\n' + lastBuild.error;
rec(`build ${lastBuild.ok ? 'PASS' : 'FAIL'}`);
} else if (name === 'done') {
// done 门:复核 check+build 必须都绿;SAA 模式再加 stage + 同进程 smoke-boot(治 review HIGH-1:boot 失败挡在 generate 内,不漏到 SAA play)。
const c2 = check(id);
const b2 = c2.ok ? doBuild() : { ok: false, error: 'check 未过,先修 check' };
lastCheck = c2; if (c2.ok) lastBuild = b2;
let gate = c2.ok && b2.ok;
let gateMsg = `check=${c2.ok ? 'PASS' : 'FAIL: ' + c2.errors.join('; ')} build=${b2.ok ? 'PASS' : 'FAIL: ' + (b2.error || '')}`;
if (gate && isSaa) {
const st = stage(id);
if (!st.ok) { gate = false; gateMsg += ` stage=FAIL: ${st.error}`; }
else {
rec('done 门:stage OK → smoke-boot…');
lastSmoke = await smokeBoot(id, { port, cdpPort });
if (!lastSmoke.ok) { gate = false; gateMsg += ` smoke=FAIL(booted=${lastSmoke.booted} framesAdvanced=${lastSmoke.framesAdvanced} err=${lastSmoke.bootErr || '-'})`; }
else { gateMsg += ' stage=OK smoke=PASS'; }
}
}
if (gate) {
doneSummary = a.summary || '';
result = 'DONE 已接受' + (isSaa ? '(已 stage + smoke-boot 绿)' : '');
rec(`done 接受: ${doneSummary}`);
messages.push({ role: 'tool', tool_call_id: c.id, content: result });
finished = true;
break;
}
result = `DONE 被拒:${isSaa ? 'done 前 check+build+stage+smoke 必须都绿' : 'done 前 check+build 必须都绿'}。${gateMsg}`;
rec('done 被拒(' + gateMsg + ')');
} else {
result = 'ERROR: 未知工具 ' + name;
}
messages.push({ role: 'tool', tool_call_id: c.id, content: String(result).slice(0, TOOL_RESULT_CAP) });
}
// check+build 都绿 → 一次性强提醒 done(治"绿了还乱写测试/额外文件")
if (!finished && !nudgedDone && lastCheck && lastCheck.ok && lastBuild && lastBuild.ok) {
messages.push({ role: 'user', content: 'check 与 build 都已 PASS。若已实现 brief 的核心玩法,请立即调 done(summary 一句话);不要再写测试或额外文件。' });
nudgedDone = true;
rec('已提示 done(check+build 绿)');
}
// loop-detection:同一调用重复过多 → 截停(防卡死烧 token)
if ([...callSig.values()].some((n) => n > 6)) {
rec('loop-detection:同一工具调用重复过多,截停');
break;
}
}
// 收口:SAA 模式已在 done 门 stage+smoke(SAA play 九门是 batch B 的 Java 节点职责,本 harness 不跑);spike 模式跑循环外真玩门。
let verdict = null;
if (finished && !isSaa) {
rec('play(循环外·权威门:boot+截图+交互 = 能跑能玩)...');
try {
verdict = await play(id, { port, cdpPort });
rec(`play ${verdict.pass ? 'PASS(能跑)' : 'FAIL'} — ${verdict.notes.join(' | ')}`);
} catch (e) {
rec('play 异常: ' + (e.message || e));
}
} else if (finished && isSaa) {
rec(`SAA 模式收口:已 stage → games/_wg1-gen/${id}/ + smoke-boot 绿;SAA play 九门交 batch B Java 节点。`);
} else {
rec('未合格 done,跳过收口(本 run 未产出合格游戏)');
}
const summary = {
id, mode, brief, model: MODEL, steps, finished, doneSummary,
check: lastCheck ? { ok: lastCheck.ok, errors: lastCheck.errors } : null,
build: lastBuild ? { ok: lastBuild.ok } : null,
smoke: lastSmoke ? { ok: lastSmoke.ok, booted: lastSmoke.booted, framesAdvanced: lastSmoke.framesAdvanced, bootErr: lastSmoke.bootErr } : null,
verdict: verdict ? { pass: verdict.pass, notes: verdict.notes, error: verdict.error || null } : null,
stagedDir: isSaa && finished ? `games/_wg1-gen/${id}` : null,
approxTokens: totalTokens,
wallSec: Number(((Date.now() - t0) / 1000).toFixed(1)),
};
const evDir = join(gameDir(id), 'evidence');
mkdirSync(evDir, { recursive: true });
writeFileSync(join(evDir, 'run-log.txt'), log.join('\n'));
writeFileSync(join(evDir, 'run-summary.json'), JSON.stringify(summary, null, 2));
console.log('\n=== RUN SUMMARY ===');
console.log(JSON.stringify(summary, null, 2));
// SAA↔harness CLI 契约(§5):stdout 末行单行 JSON,SAA generate 节点专用 runner 解析。
if (isSaa) {
console.log(JSON.stringify({ ok: !!finished, gameId: id, stagedDir: finished ? `games/_wg1-gen/${id}` : null, fail: finished ? null : 'agent 未合格 done' }));
}
}
main().catch((e) => { console.error('gen.mjs 顶层异常:', e); process.exit(1); });