feat(amodel-gen): Phase 2 批B — SAA 图 amodel 路集成(集成方式C)+ Fix A(validate 尊重 harness ok)

集成方式C:SAA generate 节点 shell-out 已验证 harness `gen.mjs --mode saa`(amodel flag 旁挂)。
- AigcExecutorProperties: saaSourceMode += amodel(Javadoc;默认仍 factory)。
- SaaGenNodes: 新增 runProcessSplit/SplitProcResult(stdout/stderr 分流,末行 JSON 不被 stderr 污染);validateNode amodel 分支 + validateAmodelStaged。
- SaaStudioNodes: generateNode 6 参重载(amodel 短路 generateViaAmodelHarness:argv 透传 brief+key、解析末行 JSON);okElseRepairOrEscalateOrGiveupAmodelAware(validate-ok→play 短路)。
- SaaStudioGraph: assemble/build apiKey/llmBase 透传链 + validate 条件边 amodel-aware + "play"边。
- SaaGraphDispatcher: ensureGraph 传 apiKey/llmBase。

Fix A(本次发现并修的真集成 bug):gen.mjs done-gate 顺序 stage→smoke,smoke 失败 done 被拒但 stage 已落盘陈旧坏 bundle(harness 返 ok:false/stagedDir:null 却留文件)。原 validateAmodelStaged 只查产物存在=误过坏 bundle→路由 play→九门崩+白耗~10min。
修:generate 置 K_AMODEL_GEN_OK(成功 true/失败 false,不再自增 failCount);validate 先尊重该标记(false→救场,不读陈旧产物),failCount 由 validate 统一计一次(与 factory/gamedef「generate 产出、validate 计数」同口径)。

验证(严格):
- mini 权威 mvn test-compile BUILD SUCCESS(main+test)。
- 83/83 SAA 确定性回归单测全绿(门路由18/拓扑30/节点回归14/history重载4/dispatcher9 等)——factory/gamedef 零回归。
- amodel e2e dispatch→generate→validate→路由 实跑通;控制测试 pristine _template scaffold-saa→build→stage→smoke PASS(env/信封/插件正常)。
- Fix A 行为确认:修后坏 bundle 被 validate 正确拒(未到 play);修前 breakout 坏 bundle 到了 play。

未达(解耦·非本集成范围):amodel e2e 仍 0/1——M3 生成质量 boot 崩 `Cannot read properties of undefined (reading 'w')`,跨 breakout+whack 复现;pristine 模板 boot 干净→证 M3 代码问题,非集成/env/harness。属 Phase 3/4 生成质量门(D1=A:factory 默认直到 amodel≥60%)。

零回归:amodel 纯旁挂 flag,saaSourceMode 默认 factory;amodel=false 时全部新分支跳过,新"play"边对 factory/gamedef 不可达。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
lili 2026-06-22 06:21:12 -07:00
parent cd2903baab
commit 3fbb3197f4
6 changed files with 329 additions and 12 deletions

View File

@ -131,6 +131,106 @@ final class SaaGenNodes {
return new ProcResult(!done, done ? p.exitValue() : -1, output.trim());
}
// ====================== 子进程运行器stdout/stderr 分流版amodel 路专用 ======================
/**
* 分流子进程运行结果{@link #runProcessSplit}amodel 路专用stdout / stderr <b>不合流</b>分别保留
* <p>amodel node harnessgen.mjs --mode saa契约是stdout <b>末行</b>为单行 JSON 结果其它日志走 stderr/前序 stdout
* 若沿用 {@link #runProcess} {@code redirectErrorStream(true)} 合流stderr 日志会与 stdout 末行交织污染 JSON 解析
* 故本版双守护线程各自 drain stdout/stderr 到独立 buffer镜像 {@link #runProcess} 的并发 drain + 超时强杀范式
*/
static final class SplitProcResult {
final boolean timedOut; // true=超时被强杀未在限时内退出
final int exitCode; // 进程退出码timedOut=true 时无意义 -1
final String stdout; // 标准输出不含 stderr
final String stderr; // 标准错误独立
SplitProcResult(boolean timedOut, int exitCode, String stdout, String stderr) {
this.timedOut = timedOut;
this.exitCode = exitCode;
this.stdout = stdout;
this.stderr = stderr;
}
/** 取 stdout 末行非空行amodel harness 契约:末行为单行 JSON 结果)。无则返空串。 */
String lastStdoutLine() {
if (stdout == null || stdout.isEmpty()) {
return "";
}
String[] lines = stdout.split("\\r?\\n");
for (int i = lines.length - 1; i >= 0; i--) {
String ln = lines[i].trim();
if (!ln.isEmpty()) {
return ln;
}
}
return "";
}
}
/**
* 分流子进程运行器amodel 路专用stdout / stderr <b>不合流</b>不调 redirectErrorStream双守护线程各 drain
* 一路防管道死锁限时 {@code waitFor}超时 destroydestroyForcibly二次 waitFor 兜底强杀镜像 {@link #runProcess} 范式
*
* @param pb 已配好 command/directory ProcessBuilder本方法<b></b> redirectErrorStream
* @param timeoutSecs 超时秒数
* @return SplitProcResulttimedOut / exitCode / stdout / stderr 分流
* @throws IOException 进程启动失败
* @throws InterruptedException 等待被中断向上抛由调用方按节点语义处理
*/
static SplitProcResult runProcessSplit(ProcessBuilder pb, long timeoutSecs) throws IOException, InterruptedException {
// 不调 redirectErrorStreamstdout/stderr 分流amodel harness 末行 JSON 不被 stderr 日志污染
Process p = pb.start();
ByteArrayOutputStream outBuf = new ByteArrayOutputStream();
ByteArrayOutputStream errBuf = new ByteArrayOutputStream();
// 双并发 drain各自把一路输出抽干到独立 buffer确保子进程写任一管道永不阻塞修复 readAll/waitFor 死锁
Thread outDrain = drainTo(p.getInputStream(), outBuf, "saa-proc-drain-out");
Thread errDrain = drainTo(p.getErrorStream(), errBuf, "saa-proc-drain-err");
outDrain.start();
errDrain.start();
boolean done = p.waitFor(timeoutSecs, TimeUnit.SECONDS);
if (!done) {
// 超时先温和 destroy再强杀二次短等确保回收 runProcess 同款
p.destroy();
if (!p.waitFor(2, TimeUnit.SECONDS)) {
p.destroyForcibly();
p.waitFor(2, TimeUnit.SECONDS);
}
}
// 等两路 drain 线程收尾各最多 2s取出已抽到的输出
outDrain.join(2000);
errDrain.join(2000);
String stdout;
synchronized (outBuf) {
stdout = outBuf.toString(StandardCharsets.UTF_8);
}
String stderr;
synchronized (errBuf) {
stderr = errBuf.toString(StandardCharsets.UTF_8);
}
return new SplitProcResult(!done, done ? p.exitValue() : -1, stdout.trim(), stderr.trim());
}
/** 建一个守护 drain 线程:把 {@code in} 抽干到 {@code buf}(同步写,防并发交织)。供 {@link #runProcessSplit} 双路各起一条。 */
private static Thread drainTo(InputStream in, ByteArrayOutputStream buf, String name) {
Thread t = new Thread(() -> {
try (InputStream src = in) {
byte[] chunk = new byte[8192];
int n;
while ((n = src.read(chunk)) != -1) {
synchronized (buf) {
buf.write(chunk, 0, n);
}
}
} catch (IOException ignore) {
// 进程结束/管道关闭引发的读异常可忽略已尽量抽干
}
}, name);
t.setDaemon(true);
return t;
}
// ====================== validate 节点 ======================
// 静态契约扫描 validate.py:55-77 语义+ node --check ESM validate.py:80-100
@ -243,6 +343,11 @@ final class SaaGenNodes {
*/
static NodeAction validateNode(Path gameRuntimeRoot, String sourceMode) {
return (OverAllState s) -> {
// amodel 集成方式 Cgenerate shell-out harness 自跑 scaffold/build/stage/smoke
// validate 只轻断言 staged 产物存在 + __GameBundle不要求 source-project schema 工件 follow-up
if ("amodel".equals(sourceMode)) {
return validateAmodelStaged(s, gameRuntimeRoot);
}
if ("gamedef".equals(sourceMode)) {
return validateGameDef(s, gameRuntimeRoot);
}
@ -306,6 +411,47 @@ final class SaaGenNodes {
return validateOut(s, errs);
}
/**
* amodel validate集成方式 C·精简harnessgen.mjs --mode saa已在子进程内 scaffold/build/stage/smoke-boot 完毕
* 两步门<b>先尊重 generate harness ok 信号</b>{@link SaaStudioNodes#K_AMODEL_GEN_OK}
* <b>harness 成功才做产物轻断言</b>staged {@code games/_wg1-gen/<gameId>/bundle.iife.js} 存在且文本含 {@code __GameBundle}
* 全过 validateOk:true图侧路由短路 scaffold/asset/build 直奔 play任一不过 errs 回喂 repair复用 {@link #validateOut} failCount 一次
*
* <p><b>为何不可省2026-06-22 验收门修</b>gen.mjs done-gate 顺序是 stagesmokesmoke 失败 done 被拒但 <b>stage 已落盘陈旧坏 bundle</b>
* harness ok:false/stagedDir:null 却违约留文件若只做查产物存在会误过该坏 bundle 路由 play 九门 A_boot 必崩 + 白耗一次约 10min 真玩
* 实证e2e打砖块harness ok:false staged bundle 放行九门 A_bootreading 'w'harness 自身 smoke 同款崩证非误判
*
* <p><b>本期边界follow-up</b>不要求 source-project schema 工件A-model 终态是 src/ 多文件工程非结构化 JSON
* 该校验留 follow-up plan 决策 3validate amodel一致
*/
private static Map<String, Object> validateAmodelStaged(OverAllState s, Path gameRuntimeRoot) {
String gameId = s.value(SaaStudioNodes.K_GAME_ID, String.class).orElse("");
List<String> errs = new ArrayList<>();
// 先尊重 generate harness ok 信号harness ok=false 时可能留陈旧坏 bundlegen.mjs stagesmokesmoke 失败仍落了盘
// 只查产物存在会误过 generate 失败直接判 fail救场不读陈旧产物failCount validateOut 统一计一次
boolean genOk = s.value(SaaStudioNodes.K_AMODEL_GEN_OK, Boolean.class).orElse(false);
if (!genOk) {
String fb = s.value(SaaStudioNodes.K_FEEDBACK, String.class).orElse("amodel harness 生成失败(ok=false)");
errs.add("amodel generate 未合格harness ok=false跳过 staged 产物校验直接救场):" + fb);
return validateOut(s, errs);
}
// harness 成功做产物轻断言staged 产物路径与 harness 落点对齐games/_wg1-gen/<gameId>/bundle.iife.jsgen.mjs stage 后的信封产物
Path bundle = gameRuntimeRoot.resolve("games").resolve("_wg1-gen").resolve(gameId).resolve("bundle.iife.js");
if (!Files.exists(bundle)) {
errs.add("amodel staged 产物缺失:未见 " + bundle + "harness 未成功 stage bundle.iife.js");
return validateOut(s, errs);
}
try {
String text = new String(Files.readAllBytes(bundle), StandardCharsets.UTF_8);
if (!text.contains(GLOBAL_NAME)) {
errs.add("amodel staged 产物不含全局名 " + GLOBAL_NAME + "(疑似信封未注入/产物损坏)");
}
} catch (IOException e) {
errs.add("amodel staged 产物读取失败:" + e.getMessage());
}
return validateOut(s, errs);
}
/** 统一 validate 输出(成功=validateOk:true失败=feedback + 清 player 软门 + 计 failCount。 */
private static Map<String, Object> validateOut(OverAllState s, List<String> errs) {
Map<String, Object> out = new HashMap<>();

View File

@ -414,11 +414,13 @@ public class SaaGraphDispatcher implements GenerationDispatcher {
}
// 固定架构 B2透传救场阶梯 stage2 额外救场轮saaStage2ExtraRepairs factory-based build 重载recursionLimit 5+3 重算
// Plan A/U2historyEnabled=anthropicanthropic generate 跨轮保留 thinking 历史 + 连续对话救场openai 路关字节零变
// amodel 透传 apiKey/llmBase generate 节点 shell-out harness 子进程 env NEWAPI_KEY/NEWAPI_BASE_URLfactory/gamedef 路忽略
compiledGraph = SaaStudioGraph.build(mf, models, gameRuntimeRoot,
properties.getSaaMaxRepairs(), properties.getSaaMaxPlayerRounds(),
properties.getSaaStage2ExtraRepairs(),
properties.getSaaSourceMode(), // plan U3factory(默认,iife) | gamedef(真结构化源)
properties.getSaaSourceMode(), // plan U3factory(默认,iife) | gamedef(真结构化源) | amodel(shell-out harness)
anthropic, // plan U2historyEnabled anthropic 路开连续对话历史+thinking 保留
properties.getApiKey(), properties.getLlmBase(), // amodel harness new-api 密钥/host factory/gamedef 忽略
saverConfig, observationRegistry);
log.info("[saa-dispatch] SAA 图构造完成缓存复用protocol={}, maxRepairs={}, stage2Extra={}, maxPlayerRounds={}, "
+ "upstream={}, checkpoint={}, observation={}",

View File

@ -424,7 +424,8 @@ public final class SaaStudioGraph {
*/
private static StateGraph assemble(RoleModelFactory mf, Models models, Path gameRuntimeRoot,
int maxRepairs, int maxPlayerRounds, int stage2ExtraRepairs,
String sourceMode, boolean historyEnabled)
String sourceMode, boolean historyEnabled,
String apiKey, String llmBase)
throws GraphStateException {
// == per-role 采样面temperature/maxTokens逐字对齐 Python mf 工厂建 ChatModelopenai 默认路字节等价anthropic flag 旁挂==
// designPython config.build_model 不传 temperature 服务端默认 temperature=null不下发
@ -465,7 +466,8 @@ public final class SaaStudioGraph {
codeFallbackModel, fixFallbackModel,
models.code(), models.fix(), models.codeStage2(), models.fixStage2(),
models.codeFallback(), models.fixFallback()),
gameRuntimeRoot, sourceMode, historyEnabled))) // U2anthropic history 连续对话救场openai 路关字节零变
gameRuntimeRoot, sourceMode, historyEnabled,
apiKey, llmBase))) // U2anthropic history 连续对话救场openai 路关字节零变apiKey/llmBase amodel shell-out harness
.addNode("validate", node_async(SaaGenNodes.validateNode(gameRuntimeRoot, sourceMode)))
.addNode("scaffold", node_async(SaaGenNodes.scaffoldNode(gameRuntimeRoot)))
.addNode("asset", node_async(SaaStudioNodes.assetNode()))
@ -497,10 +499,13 @@ public final class SaaStudioGraph {
edge_async(SaaStudioNodes.renderRouter()),
Map.of("classify", "classify", "modify", "modify"))
// validate 失败回边救场阶梯okscaffold / repair / escalate / giveup
// amodel-awareamodel harness 已自跑 scaffold/build/stage/smoke validate-ok 短路到 play scaffold/asset/build
// factory/gamedef ok scaffold运行时零变"play" 边对其不可达
.addConditionalEdges("validate",
edge_async(SaaStudioNodes.okElseRepairOrEscalateOrGiveup(
SaaStudioNodes.K_VALIDATE_OK, maxRepairs, stage2ExtraRepairs)),
Map.of("ok", "scaffold", "repair", "repair", "escalate", "escalate", "giveup", "giveup"))
edge_async(SaaStudioNodes.okElseRepairOrEscalateOrGiveupAmodelAware(
SaaStudioNodes.K_VALIDATE_OK, maxRepairs, stage2ExtraRepairs, sourceMode)),
Map.of("ok", "scaffold", "play", "play",
"repair", "repair", "escalate", "escalate", "giveup", "giveup"))
// build 失败回边救场阶梯okplay / repair / escalate / giveup
.addConditionalEdges("build",
edge_async(SaaStudioNodes.okElseRepairOrEscalateOrGiveup(
@ -675,8 +680,29 @@ public final class SaaStudioGraph {
String sourceMode, boolean historyEnabled,
SaverConfig saverConfig, ObservationRegistry observationRegistry)
throws GraphStateException {
// apiKey/llmBase 默认空factory/gamedef 路忽略测试/OpenAiApi 入口走此amodel 路经下方 12 参重载显式带 key/base
return build(mf, models, gameRuntimeRoot, maxRepairs, maxPlayerRounds, stage2ExtraRepairs,
sourceMode, historyEnabled, "", "", saverConfig, observationRegistry);
}
/**
* 组装并编译全图amodel apiKey/llmBase 透传全量重载<b>布线唯一源仍是 {@link #assemble}</b>
*
* <p>apiKey/llmBase {@code sourceMode=amodel} 路用 {@link #assemble}{@link SaaStudioNodes#generateNode}
* 透传 shell-out harness 子进程 env {@code NEWAPI_KEY}/{@code NEWAPI_BASE_URL}factory/gamedef 路忽略字节零变
* dispatcher {@code saaSourceMode=amodel} 调本重载传 {@code properties.getApiKey()}/{@code properties.getLlmBase()}
*
* @param apiKey new-api 密钥amodel harness 子进程 env NEWAPI_KEYfactory/gamedef 路传空即可
* @param llmBase new-api host amodel harness 子进程 env NEWAPI_BASE_URLfactory/gamedef 路传空即可
*/
public static CompiledGraph build(RoleModelFactory mf, Models models, Path gameRuntimeRoot,
int maxRepairs, int maxPlayerRounds, int stage2ExtraRepairs,
String sourceMode, boolean historyEnabled,
String apiKey, String llmBase,
SaverConfig saverConfig, ObservationRegistry observationRegistry)
throws GraphStateException {
StateGraph graph = assemble(mf, models, gameRuntimeRoot, maxRepairs, maxPlayerRounds,
stage2ExtraRepairs, sourceMode, historyEnabled);
stage2ExtraRepairs, sourceMode, historyEnabled, apiKey, llmBase);
CompileConfig.Builder cc = CompileConfig.builder()
.recursionLimit(recursionLimitFor(maxRepairs, stage2ExtraRepairs));

View File

@ -19,6 +19,7 @@ import org.springframework.ai.content.Media;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.util.MimeTypeUtils;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
@ -84,6 +85,7 @@ final class SaaStudioNodes {
static final String K_FEEDBACK = "feedback"; // 上轮失败回喂generate 注入 prompt失败路由写
static final String K_PLAYER_FEEDBACK = "playerFeedback"; // player 软门体验问题与失败 feedback 解耦避免串味
static final String K_VALIDATE_OK = "validateOk"; // validate 节点产出SaaGenNodes
static final String K_AMODEL_GEN_OK = "amodelGenOk"; // amodel generate(shell-out harness) 成功标记validate 据此放行/救场集成方式C 修陈旧坏 bundle 漏判
static final String K_BUILD_OK = "buildOk"; // build 节点产出
static final String K_PLAY_PASS = "playPass"; // play 九门是否全过
static final String K_VERDICT = "verdict"; // 九门 verdictJSON
@ -126,7 +128,7 @@ final class SaaStudioNodes {
/** state key 全集KeyStrategyFactory 注册用,全 ReplaceStrategy 覆盖语义)。 */
static final String[] ALL_KEYS = {
K_BRIEF, K_ENRICHED, K_DESIGN_TEXT, K_GATESPEC, K_GATESPEC_ERROR, K_PLAY_SPEC, K_FACTORY_SRC, K_GAME_ID,
K_ROLE, K_REPAIR_COUNT, K_PLAYER_ROUND, K_FEEDBACK, K_VALIDATE_OK, K_BUILD_OK,
K_ROLE, K_REPAIR_COUNT, K_PLAYER_ROUND, K_FEEDBACK, K_VALIDATE_OK, K_AMODEL_GEN_OK, K_BUILD_OK,
K_PLAY_PASS, K_VERDICT, K_PLAYER_PANEL, K_PLAYER_FEEDBACK, K_TOKENS_IN, K_TOKENS_OUT,
K_TOKENS_BY_MODEL, K_ATTEMPTS,
K_STATUS, K_FAILURE, K_ENGINE_BUNDLE,
@ -718,7 +720,8 @@ final class SaaStudioNodes {
static NodeAction generateNode(GenModels gm, Path gameRuntimeRoot, String sourceMode) {
// 默认 openai history 现行行为字节不变anthropic 路经 4 参重载传 historyEnabled=true
return generateNode(gm, gameRuntimeRoot, sourceMode, false);
// apiKey/llmBase amodel shell-out harness factory/gamedef 路忽略此重载给空串amodel 路不经此入口
return generateNode(gm, gameRuntimeRoot, sourceMode, false, "", "");
}
/**
@ -733,7 +736,27 @@ final class SaaStudioNodes {
* @param historyEnabled true=anthropic 连续对话历史+thinking 保留false=openai 每轮 [system,user]字节零变
*/
static NodeAction generateNode(GenModels gm, Path gameRuntimeRoot, String sourceMode, boolean historyEnabled) {
// 4 参重载既有 factory/gamedef 调用方 + 历史测试用apiKey/llmBase 给空串amodel 路不经此入口
return generateNode(gm, gameRuntimeRoot, sourceMode, historyEnabled, "", "");
}
/**
* generate 节点amodel apiKey/llmBase 透传全量重载{@code sourceMode=amodel} 时闭包最前短路为 shell-out harness
* {@link #generateViaAmodelHarness}apiKey/llmBase 落子进程 envfactory/gamedef 路原码原样在其后<b>运行时零侵入</b>apiKey/llmBase 忽略
*
* @param historyEnabled true=anthropic 连续对话历史+thinking 保留false=openai 每轮 [system,user]字节零变
* @param apiKey amodel shell-out harness 透传为子进程 env {@code NEWAPI_KEY}factory/gamedef 路忽略
* @param llmBase amodel shell-out harness 透传为子进程 env {@code NEWAPI_BASE_URL}factory/gamedef 路忽略
*/
static NodeAction generateNode(GenModels gm, Path gameRuntimeRoot, String sourceMode, boolean historyEnabled,
String apiKey, String llmBase) {
// amodel 旁挂分支集成方式 C闭包外先判命中则整条 generate 短路为 shell-out harnessfactory/gamedef 原码零侵入
boolean amodel = "amodel".equals(sourceMode);
return (OverAllState s) -> {
// amodel generate 短路 shell-out 已验证 node harnessharness 内自跑 scaffold/build/stage/smoke不走下方 factory/gamedef LLM 编排
if (amodel) {
return generateViaAmodelHarness(s, gameRuntimeRoot, apiKey, llmBase);
}
String role = s.value(K_ROLE, "code");
boolean isFix = "fix".equals(role);
// P1-5modelTier=stage2escalate 升档后 stage2 强档模型否则用 stage1模型对象与模型名选择同口径
@ -860,6 +883,97 @@ final class SaaStudioNodes {
};
}
/**
* amodel generate集成方式 Cshell-out 已验证 node harness {@code tools/amodel-gen/gen.mjs --mode saa}
* harness 在子进程内自跑 scaffold/build/stage/smoke-bootA-model 手写 LittleJS src/ 多文件工程产线
* 图侧本方法只<b>透传 brief+key 起进程解析 stdout 末行 JSON按结果写 state</b>不接 LLM 编排/历史/救场升档amodel 不走 design assetSpec/asset 节点已确认
*
* <p><b>透传链plan 决策 1</b>apiKey/llmBase buildassemblegenerateNode 显式带入落子进程 env
* {@code NEWAPI_KEY}/{@code NEWAPI_BASE_URL}gen.mjsm3.mjs 据此连 new-apiNEWAPI_BASE_URL=host m3.mjs 自拼 /v1
*
* <p><b>超时plan 决策 2·最小</b>{@code runProcessSplit} 超时 900s不做 deadline--stateTODO(follow-up)把单局剩余预算传入 harness
*
* <p><b>失败兜底对齐现 generate 全档失败路</b>harness 失败/超时<b>不裸退图</b> {@link #K_FEEDBACK} fail 摘要 + stderr 末段
* + {@link #K_AMODEL_GEN_OK}=false成功则 true<b>验收门修2026-06-22</b>validate 据此标记判 fail/放行
* gen.mjs done-gate 顺序是 stagesmokesmoke 失败 done 被拒但 <b>stage 已落盘陈旧坏 bundle</b>harness ok:false/stagedDir:null
* 却违约留下文件 validate 只查产物存在会误过该坏 bundle浪费一次九门 playfailCount <b>不在此自增</b>
* validate 失败统一计一次 factory/gamedefgenerate 产出validate 计数同口径杜绝双重计数提前 escalate/giveup
*
* <p><b>shell 注入安全</b>argv 独立逐项传非拼 shell 字符串brief 内任意字符无需转义
*
* @param s 当前 state gameId/enriched/brief/feedback
* @param gameRuntimeRoot game-runtime 子进程 cwdgen.mjs/tools/node_modules/games 在此
* @param apiKey new-api 密钥落子进程 env NEWAPI_KEY
* @param llmBase new-api host 落子进程 env NEWAPI_BASE_URL
*/
private static Map<String, Object> generateViaAmodelHarness(OverAllState s, Path gameRuntimeRoot,
String apiKey, String llmBase) {
Map<String, Object> out = new HashMap<>();
String gameId = s.value(K_GAME_ID, String.class).orElse("");
// brief 优先取 enricheddesign 富化稿amodel 路通常无 design缺则退原始 brief
String enriched = s.value(K_ENRICHED, String.class).orElse(s.value(K_BRIEF, String.class).orElse(""));
String feedback = s.value(K_FEEDBACK, String.class).orElse(null);
// 上轮失败反馈拼进 brief 末尾救场重生成时让 harness/模型看到失败原因无反馈则原样
String briefText = (feedback != null && !feedback.isEmpty())
? enriched + "\n\n## 上轮失败反馈(请修复)\n" + feedback
: enriched;
try {
// argv 独立逐项传shell 注入安全brief 任意字符无需转义cwd=game-runtime
ProcessBuilder pb = new ProcessBuilder(
"node", "tools/amodel-gen/gen.mjs", "--mode", "saa",
"--game-id", gameId, "--brief", briefText);
pb.directory(gameRuntimeRoot.toFile());
// 透传 new-api 凭据/端点到子进程 envgen.mjsm3.mjs 消费
pb.environment().put("NEWAPI_KEY", apiKey == null ? "" : apiKey);
pb.environment().put("NEWAPI_BASE_URL", llmBase == null ? "" : llmBase);
// 决策 2最小超时 900sdeadline--state follow-up
// TODO(follow-up·deadline)把单局剩余预算perBriefSec - 已耗算成 timeoutSecs 传入而非恒 900
SaaGenNodes.SplitProcResult r = SaaGenNodes.runProcessSplit(pb, 900);
// 解析 stdout 末行 JSONamodel harness CLI 契约末行单行 {ok,gameId,stagedDir,fail}
JsonNode env = SaaPrompts.looseParse(r.lastStdoutLine());
boolean ok = !r.timedOut && env != null && env.path("ok").asBoolean(false);
if (ok) {
// 成功harness stage bundle.iife.js + smoke 绿validate 仅轻断言其在场记一条 attemptrole=amodel
out.put(K_AMODEL_GEN_OK, true); // 放行信号generate 真成功validate 才认 staged 产物防陈旧坏 bundle 漏到 play
appendAttempt(s, out, MAPPER.createObjectNode()
.put("role", "amodel").put("model", "amodel-harness")
.set("usage", MAPPER.createObjectNode().put("in", 0).put("out", 0))); // token best-effort harness 内部消耗未回传
return out;
}
// 失败harness 未合格 done / 超时
String fail = r.timedOut
? "harness 超时900s"
: (env != null ? env.path("fail").asText("harness 未合格 done") : "harness 无可解析 stdout 末行 JSON");
out.put(K_FEEDBACK, "amodel harness 失败:" + fail + " | " + tailStr(r.stderr, 1500));
// generate 失败 amodelGenOk=falsevalidate 据此判 fail救场failCount validate 失败统一计一次勿在此重计
out.put(K_AMODEL_GEN_OK, false);
appendAttempt(s, out, MAPPER.createObjectNode()
.put("role", "amodel").put("model", "amodel-harness").put("stage_fail", "amodel")
.set("usage", MAPPER.createObjectNode().put("in", 0).put("out", 0)));
return out;
} catch (IOException | InterruptedException e) {
// 子进程启动/等待异常同样不裸退图 feedback + failCount下游救场
if (e instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
out.put(K_FEEDBACK, "amodel harness 子进程异常:" + trunc(e.getMessage(), 300));
out.put(K_AMODEL_GEN_OK, false); // 同上标记 generate 失败failCount validate 统一计一次
appendAttempt(s, out, MAPPER.createObjectNode()
.put("role", "amodel").put("model", "amodel-harness").put("stage_fail", "amodel")
.set("usage", MAPPER.createObjectNode().put("in", 0).put("out", 0)));
return out;
}
}
/** 取字符串尾部 n 字amodel stderr 摘要:错误通常在末段,故取 tail 而非 {@link #trunc} 的 head。 */
private static String tailStr(String s, int n) {
if (s == null) {
return "";
}
return s.length() <= n ? s : s.substring(s.length() - n);
}
/**
* P1-5 {@code (role, modelTier)} 二维选 generate 实际所用模型名 {@link #generateNode} 内模型对象选择同口径
* <p>抽成纯静态方法便于<b>无网络</b>断言升档后真切到 stage2 强档模型topology 测试调本方法即可无需构造 ChatModel
@ -1882,6 +1996,28 @@ final class SaaStudioNodes {
};
}
/**
* validate 失败回边amodel-aware 变体集成方式 C {@link #okElseRepairOrEscalateOrGiveup}
* 仅当 {@code sourceMode=amodel} 且基判据返回 {@code "ok"} 时改返 {@code "play"}amodel harness 已自跑
* scaffold/build/stage/smokevalidate-ok <b>直奔 play 九门</b>跳过 scaffold/asset/build其余一律原样透传基判据
* factory/gamedef validate-okscaffold<b>运行时零变</b>fail 分支 repair/escalate/giveup 不受影响
*
* @param okKey 成功标志 state key此处恒 {@link #K_VALIDATE_OK}
* @param maxRepairs stage1 失败阈
* @param stage2ExtraRepairs stage2 额外失败阈
* @param sourceMode 生成产物形态factory|gamedef|amodel amodel ok 改路由到 play
*/
static EdgeAction okElseRepairOrEscalateOrGiveupAmodelAware(String okKey, int maxRepairs,
int stage2ExtraRepairs, String sourceMode) {
EdgeAction base = okElseRepairOrEscalateOrGiveup(okKey, maxRepairs, stage2ExtraRepairs);
boolean amodel = "amodel".equals(sourceMode);
return (OverAllState s) -> {
String r = base.apply(s);
// amodel validate-ok 短路到 play scaffold/asset/build amodel 或非 ok 一律原样factory 不变
return (amodel && "ok".equals(r)) ? "play" : r;
};
}
/**
* validate/build 失败回边共用判据对齐蓝图 §3.1ok? 下一步 : repairCount>=N? giveup : repair
*

View File

@ -192,7 +192,11 @@ public class AigcExecutorProperties {
/**
* SAA 生成产物形态plan 2026-06-18-001 U3additive 灰度{@code factory}=现行 iife factorySrcgenerate ```js 工厂
* validate 12 禁词静态门<b>默认·行为字节不变</b>{@code gamedef}=真结构化 gameDefinitiongenerate 产声明式源落
* {@code sourceProject}validate build-from-source.mjs 校验+装配出工厂改源不改包/可维护结构化源范式上产线
* {@code sourceProject}validate build-from-source.mjs 校验+装配出工厂改源不改包/可维护结构化源范式上产线
* {@code amodel}=A-model 手写 LittleJS src/ 多文件工程generate 短路为 <b>shell-out 已验证 node harness</b>
* {@code game-runtime/tools/amodel-gen/gen.mjs --mode saa}harness 内自跑 scaffold/build/stage/smoke-boot
* 图侧 generate 只透传 brief+key 起子进程validate 只轻断言 staged {@code bundle.iife.js} __GameBundle
* 路由短路 scaffold/asset/build 直奔 play纯旁挂 flag 分支factory/gamedef 路运行时零侵入
* <b>cutover 量化门 = gamedef SaaFullGraphE2eTest 过门率 60%iife 现基线</b>达标才切默认 dispatcher=saa 生效
*/
private String saaSourceMode = "factory";

View File

@ -60,8 +60,11 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
* <p><b>003-U1 量测 flags</b>
* <ul>
* <li>{@code -Dsaa.e2e.sourceMode}默认 {@code factory}量哪条生成路{@code factory}=iife 工厂旧路现行字节零变基线
* {@code gamedef}=真结构化 gameDefinition cutover 60% 量测对象<b>修发现</b>旧版未 setSaaSourceMode量的是 iife 旧路非 gamedef
* 故量 gamedef 真基线<b>必须</b> {@code -Dsaa.e2e.sourceMode=gamedef}</li>
* {@code gamedef}=真结构化 gameDefinition cutover 60% 量测对象{@code amodel}=A-model 手写 LittleJS src/ 多文件工程路
* generate 短路 shell-out node harness {@code tools/amodel-gen/gen.mjs --mode saa}harness 内自跑 scaffold/build/stage/smoke
* 图侧 validate 轻断言 staged bundle 后短路 scaffold/asset/build 直奔 play 九门<b>amodel 变体无结构改动</b>
* {@code setSaaSourceMode} 透传{@code -Dsaa.e2e.sourceMode=amodel} 即可驱动 game-runtime tools/amodel-gen + node
* <b>修发现</b>旧版未 setSaaSourceMode量的是 iife 旧路非 gamedef故量 gamedef/amodel 真基线<b>必须</b>显式 {@code -Dsaa.e2e.sourceMode=<gamedef|amodel>}</li>
* <li>{@code -Dsaa.e2e.concurrency}默认 1后台并发度 K1=串行生产字节零变基线&gt;1 job 并行真玩各占错开端口对
* K=1 vs K=N verdict 对等校准延 003-U5 真跑期</li>
* <li>{@code -Dsaa.e2e.briefFile}可选外部 brief 文件行式去空白/跳空行与 {@code #} 注释