fc14190f58
Project CI / AI game creator shell Rust shard 1/4 (push) Has been cancelled
Project CI / AI game creator shell Rust shard 2/4 (push) Has been cancelled
Project CI / AI game creator shell Rust shard 3/4 (push) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (push) Has been cancelled
Project CI / AI game creator shell Rust smoke (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
## 背景 切图新增基于连通域的切分后,LLM 仍倾向显式传 `sliceMode=grid`:参数只存在于部分 LLM 可见面、带默认值、没有任何决策规则,生成结果也不回显生效模式。 ## 变更 - 平台:`/api/editor/icon-spritesheets/generations` 与 `/api/external/v1/editor/icon-spritesheets/generations` 把 `sliceMode` 改为必填并移除默认值;缺失、空白或未知取值在引用解析、定价与任何 provider / OSS 副作用之前返回 `400`,错误统一带 `field` 与决策要求。 - 契约:`grid` 必须同时提供 `gridX`/`gridY`,`connected-components` 不接受网格尺寸;`sliceCount` 只约束连通域切分,请求与响应的公开上限统一为 `256`;OpenAPI 去掉默认值并补必填与失败语义。 - AGC:MCP 工具说明去掉默认值并补决策要求,桥接层新增可测试的切分声明校验;原生工具 `canvas.asset_generate` 暴露 `sliceMode/gridX/gridY/sliceCount` 并要求图集显式声明;生成结果回显 `sliceMode/gridX/gridY` 与 `slicePaths`;严格图集在本地提交前校验平台回显与请求声明一致。 - 标准美术包:显式声明 `connected-components` 加 `sliceCount=4`,并在四张 canonical 切片用途映射前校验数量,禁止截断或错位。 - 前端与画板:画板 Agent 工具装配与画板提交计划显式声明连通域切分;前端类型要求显式 `sliceMode` 并在本地校验声明自洽。 - 文档与 Skill:主规范、OpenAPI、AGC Skill、外部编辑器 Skill、里程碑与实施计划、共享决策记录同步更新。 - 测试环境:测试构建对提权 Windows 主机上系统临时目录的所有者偏差做一次性所有者初始化重试,临时目录之外的越权所有者继续失败关闭。 ## 兼容性影响 省略 `sliceMode` 的旧调用方(含已发布但未更新的 AGC 客户端与第三方外部 API 调用方)会在图集生成上收到 `400`;这是本次"不允许默认值"的预期结果,仓库内自有调用方已全部改为显式声明。 ## 验证 - 平台:`slice_mode_must_be_declared_*` 与 OpenAPI 契约测试通过;全量 `cargo test -p api-server` 1043 通过 / 11 失败(`wallet_refund_outbox` 临时文件 `拒绝访问`,已在改动前基线复现,属本机环境)。 - AGC:`slice` 30、`spritesheet` 21、`direct_tools_mcp` 23、`agent_native_tools` 16、`canvas_generation_tests` 83、提示词上限与桥接门禁各 1 条、`cargo check --tests` 全部通过。 - 前端:182 条定向测试与 `typecheck` 通过。 - 门禁:`cargo fmt --check`(两个 workspace)、`check:encoding`、`check:doc-index`、`git diff --check` 通过。 - 未验证:真实 Provider 与浏览器试玩、确定性 e2e 车道;整机全量 AGC 单进程运行在本机受提权 shell 的所有者与时序问题影响,不作为门禁信号。 --------- Co-authored-by: kdletters <61648117+kdletters@users.noreply.github.com> Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/408
3041 lines
108 KiB
JavaScript
3041 lines
108 KiB
JavaScript
import http from 'node:http';
|
||
import { deflateSync } from 'node:zlib';
|
||
|
||
const LOOPBACK_HOST = '127.0.0.1';
|
||
const DEFAULT_FALLBACK_PORTS = Object.freeze(
|
||
Array.from({ length: 128 }, (_, index) => 62_128 + index),
|
||
);
|
||
const CHAT_COMPLETIONS_PATH = '/v1/chat/completions';
|
||
const MAX_REQUEST_BYTES = 16 * 1024 * 1024;
|
||
const PNG_SIGNATURE = Buffer.from([
|
||
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
||
]);
|
||
const PNG_CRC_TABLE = Object.freeze(
|
||
Array.from({ length: 256 }, (_, value) => {
|
||
let crc = value;
|
||
for (let bit = 0; bit < 8; bit += 1) {
|
||
crc = (crc & 1) !== 0 ? 0xedb88320 ^ (crc >>> 1) : crc >>> 1;
|
||
}
|
||
return crc >>> 0;
|
||
}),
|
||
);
|
||
|
||
export const deterministicLaneDefenseModel =
|
||
'deterministic-lane-defense-provider-v1';
|
||
export const deterministicManifestReadyAgentIds = Object.freeze([
|
||
'design-director',
|
||
'design-foundation',
|
||
'balance-director',
|
||
'balance-seed',
|
||
'art-director',
|
||
'art-asset-plan',
|
||
'art-polish',
|
||
'audio-director',
|
||
'audio-asset-plan',
|
||
'code-director',
|
||
'code-prototype',
|
||
'quality-review',
|
||
'preview-readiness',
|
||
'preview-playtest',
|
||
'publish-strategy',
|
||
'publish-package',
|
||
]);
|
||
const deterministicReadOnlyReadyAgentIds = new Set([
|
||
'design-director',
|
||
'balance-director',
|
||
'art-polish',
|
||
'audio-director',
|
||
'code-director',
|
||
'quality-review',
|
||
'preview-readiness',
|
||
'preview-playtest',
|
||
'publish-strategy',
|
||
]);
|
||
// These owner tasks are checked by Runtime's owner-artifact gate. Their
|
||
// request-scoped tool catalog deliberately removes command.run_limited, so a
|
||
// deterministic response must deliver after a successful fixed-path write
|
||
// instead of trying to emit a tool that the runtime did not advertise.
|
||
const deterministicOwnerArtifactValidationAgentIds = new Set([
|
||
'design-foundation',
|
||
'balance-seed',
|
||
'art-asset-plan',
|
||
'audio-asset-plan',
|
||
]);
|
||
const deterministicProjectMutationTools = new Set([
|
||
'file.write',
|
||
'file.patch',
|
||
'file.delete',
|
||
'project.patchset',
|
||
'project.restore',
|
||
'command.exec',
|
||
'command.start',
|
||
'canvas.asset_generate',
|
||
]);
|
||
export const hiddenCanvasCss = '#game{display:none;';
|
||
export const visibleCanvasCss = '#game{display:block;';
|
||
|
||
function pngCrc32(bytes) {
|
||
let crc = 0xffffffff;
|
||
for (const byte of bytes) {
|
||
crc = PNG_CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
|
||
}
|
||
return (crc ^ 0xffffffff) >>> 0;
|
||
}
|
||
|
||
function pngChunk(type, data) {
|
||
const typeBytes = Buffer.from(type, 'ascii');
|
||
const length = Buffer.alloc(4);
|
||
length.writeUInt32BE(data.length);
|
||
const crc = Buffer.alloc(4);
|
||
crc.writeUInt32BE(pngCrc32(Buffer.concat([typeBytes, data])));
|
||
return Buffer.concat([length, typeBytes, data, crc]);
|
||
}
|
||
|
||
function deterministicPng(
|
||
width,
|
||
height,
|
||
{ variant = 0, transparent = false } = {},
|
||
) {
|
||
const stride = width * 4 + 1;
|
||
const pixels = Buffer.alloc(stride * height);
|
||
for (let y = 0; y < height; y += 1) {
|
||
const row = y * stride;
|
||
pixels[row] = 0;
|
||
for (let x = 0; x < width; x += 1) {
|
||
const offset = row + 1 + x * 4;
|
||
const tile = (Math.floor(x / 48) + Math.floor(y / 48)) % 6;
|
||
pixels[offset] = (42 + x + tile * 29 + variant * 13) % 256;
|
||
pixels[offset + 1] = (86 + y * 2 + tile * 17 + variant * 19) % 256;
|
||
pixels[offset + 2] = (118 + x + y + tile * 31 + variant * 23) % 256;
|
||
pixels[offset + 3] =
|
||
transparent && (x + y + variant) % 11 === 0 ? 0 : 255;
|
||
}
|
||
}
|
||
const ihdr = Buffer.alloc(13);
|
||
ihdr.writeUInt32BE(width, 0);
|
||
ihdr.writeUInt32BE(height, 4);
|
||
ihdr[8] = 8;
|
||
ihdr[9] = 6;
|
||
return Buffer.concat([
|
||
PNG_SIGNATURE,
|
||
pngChunk('IHDR', ihdr),
|
||
pngChunk('IDAT', deflateSync(pixels, { level: 6 })),
|
||
pngChunk('IEND', Buffer.alloc(0)),
|
||
]);
|
||
}
|
||
|
||
export function deterministicLaneDefenseInitialHtml() {
|
||
return `<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>灵露花园</title>
|
||
<style>
|
||
*{box-sizing:border-box}body{margin:0;min-height:100vh;background:#f4f8ee;color:#18351f;font:16px system-ui,sans-serif}main{width:min(960px,100%);margin:auto;padding:18px}h1{margin:0 0 4px;font-size:clamp(28px,7vw,46px)}p{margin:4px 0 14px}.toolbar,.plants{display:flex;flex-wrap:wrap;gap:8px;margin:10px 0}button{min-height:44px;border:1px solid #315d35;background:#fff;color:#18351f;padding:9px 14px;font:inherit;font-weight:700;cursor:pointer}button:hover{background:#e6f3dc}.board{display:grid;gap:10px;background:#d9edc8;border:2px solid #315d35;padding:10px}.lane{display:grid;grid-template-columns:repeat(5,1fr);gap:6px}.cell{min-height:54px;background:#eef8e7}.status{font-weight:700;min-height:24px}#game{display:none;width:100%;height:auto;aspect-ratio:20/9;background:#18351f;border:2px solid #315d35}@media(max-width:520px){main{padding:12px}button{flex:1 1 44%}.cell{min-height:44px}}
|
||
</style>
|
||
</head>
|
||
<body><main><img src="../assets/art-spritesheet.png" alt="Garden defenders" width="96" height="96">
|
||
<h1>灵露花园</h1><p>GENARRATIVE_REAL_E2E_VISIBLE</p><p>Goal: defend the garden and win every wave.</p>
|
||
<div class="toolbar"><button data-playtest-id="start">Start Game</button><button data-playtest-id="speed-up">Speed Up</button><button data-playtest-id="next-level">Next Level</button><button data-playtest-id="restart">Restart</button></div>
|
||
<div class="plants"><button data-playtest-id="defender-option">露华花</button><button id="thorn">棘刺芽</button></div>
|
||
<div class="board"><div class="lane"><button class="cell" data-playtest-id="lane-cell" aria-label="Place defender"></button><span class="cell"></span><span class="cell"></span><span class="cell"></span><span class="cell"></span></div><div class="status" id="status">Level 1 ready</div><canvas id="game" width="800" height="360"></canvas></div>
|
||
<script id="playable-web-game-state" type="application/json">{"schemaVersion":"playable-web-game-state.v1","sequence":1,"phase":"ready","level":1,"selectedDefenderId":null,"defenders":[],"enemies":[]}</script>
|
||
<script>
|
||
const surface=document.getElementById('playable-web-game-state'),statusEl=document.getElementById('status'),canvas=document.getElementById('game'),ctx=canvas.getContext('2d');
|
||
const atlasArt=new Image(),playerArt=new Image(),targetArt=new Image(),sceneArt=new Image(),feedbackArt=new Image();atlasArt.src='../assets/art-spritesheet.png';playerArt.src='../assets/art-spritesheet-slices/player.png';targetArt.src='../assets/art-spritesheet-slices/blocks-and-targets.png';sceneArt.src='../assets/art-spritesheet-slices/obstacles-and-scene.png';feedbackArt.src='../assets/art-spritesheet-slices/feedback-effects.png';
|
||
let state={schemaVersion:'playable-web-game-state.v1',sequence:1,phase:'ready',level:1,selectedDefenderId:null,defenders:[],enemies:[]},timers=[];
|
||
function publish(label){state.sequence+=1;surface.textContent=JSON.stringify(state);statusEl.textContent=label||('Level '+state.level+' '+state.phase);}
|
||
function stopTimers(){timers.forEach(clearTimeout);timers=[];}
|
||
function reset(keepLevel=true){stopTimers();const level=keepLevel?state.level:1;state={schemaVersion:'playable-web-game-state.v1',sequence:state.sequence,phase:'ready',level,selectedDefenderId:null,defenders:[],enemies:[]};publish('Level '+level+' ready');}
|
||
document.querySelector('[data-playtest-id="start"]').onclick=()=>{stopTimers();state.phase='playing';state.enemies=[{id:'gloomling-1',lane:0,position:92,health:100,maxHealth:100}];publish('The first wave is moving');};
|
||
document.querySelector('[data-playtest-id="defender-option"]').onclick=()=>{state.selectedDefenderId='nectar-bloom';publish('Nectar Bloom selected');};
|
||
document.getElementById('thorn').onclick=()=>{state.selectedDefenderId='thorn-sentry';publish('Thorn Sentry selected');};
|
||
document.querySelector('[data-playtest-id="lane-cell"]').onclick=()=>{if(!state.selectedDefenderId)return;state.defenders.push({id:state.selectedDefenderId+'-'+(state.defenders.length+1),lane:0});publish('Defender placed');};
|
||
document.querySelector('[data-playtest-id="speed-up"]').onclick=()=>{if(state.phase!=='playing')return;stopTimers();timers.push(setTimeout(()=>{state.enemies[0].position=72;state.enemies[0].health=65;publish('Battle in progress');},120));timers.push(setTimeout(()=>{state.enemies[0].position=48;state.enemies[0].health=20;publish('Gloomling nearly defeated');},300));timers.push(setTimeout(()=>{state.enemies=[];state.phase='won';publish('Level '+state.level+' complete');},560));};
|
||
document.querySelector('[data-playtest-id="next-level"]').onclick=()=>{stopTimers();state.level+=1;state.phase='ready';state.selectedDefenderId=null;state.defenders=[];state.enemies=[];publish('Level '+state.level+' ready');};
|
||
document.querySelector('[data-playtest-id="restart"]').onclick=()=>reset(true);
|
||
function draw(time){ctx.fillStyle='#173b23';ctx.fillRect(0,0,800,360);for(let i=0;i<5;i++){ctx.fillStyle=i%2?'#75ad52':'#86bf5f';ctx.fillRect(i*160,0,158,360)}ctx.fillStyle='#ffe66d';ctx.beginPath();ctx.arc(90,80,30,0,Math.PI*2);ctx.fill();ctx.fillStyle='#fafafa';ctx.font='bold 28px system-ui';ctx.fillText('灵露花园',230,60);ctx.fillStyle='#dbeafe';ctx.fillRect(230+Math.sin(time/350)*30,130,90,100);if(atlasArt.complete&&atlasArt.naturalWidth>0)ctx.drawImage(atlasArt,0,0,256,256,18,220,96,96);if(playerArt.complete&&playerArt.naturalWidth>0)ctx.drawImage(playerArt,140,220,64,64);if(targetArt.complete&&targetArt.naturalWidth>0)ctx.drawImage(targetArt,220,220,64,64);if(sceneArt.complete&&sceneArt.naturalWidth>0)ctx.drawImage(sceneArt,300,220,64,64);if(feedbackArt.complete&&feedbackArt.naturalWidth>0)ctx.drawImage(feedbackArt,380,220,64,64);requestAnimationFrame(draw)}requestAnimationFrame(draw);
|
||
</script></main></body></html>
|
||
`;
|
||
}
|
||
|
||
const deterministicLaneDefenseBalance = Object.freeze({
|
||
schemaVersion: 'lane-defense-balance.v1',
|
||
startingSun: 150,
|
||
defenderCosts: { 'nectar-bloom': 50, 'thorn-sentry': 100 },
|
||
enemyHealth: 100,
|
||
enemySpeed: 20,
|
||
waveTimingsMs: [120, 300, 560],
|
||
scorePerEnemy: 100,
|
||
levelDifficultyMultiplier: 1.18,
|
||
});
|
||
|
||
export function deterministicLaneDefenseCanonicalHtml() {
|
||
const upstreamContract = JSON.stringify({
|
||
schemaVersion: 'lane-defense-upstream-contract.v1',
|
||
design: 'game/game_design.md',
|
||
balance: 'game/balance.json',
|
||
art: 'assets/manifest.art.json',
|
||
audio: 'assets/manifest.audio.json',
|
||
});
|
||
const balance = JSON.stringify(deterministicLaneDefenseBalance);
|
||
return deterministicLaneDefenseInitialHtml()
|
||
.replace(hiddenCanvasCss, visibleCanvasCss)
|
||
.replace(
|
||
'<body><main>',
|
||
'<body><main data-project-contract="game-design+balance+art+audio">',
|
||
)
|
||
.replace(
|
||
'<script id="playable-web-game-state"',
|
||
`<script id="game-project-contract" type="application/json">${upstreamContract}</script>\n<script id="playable-web-game-state"`,
|
||
)
|
||
.replace(
|
||
"let state={schemaVersion:'playable-web-game-state.v1'",
|
||
`const BALANCE=${balance};\nlet state={schemaVersion:'playable-web-game-state.v1'`,
|
||
)
|
||
.replace(
|
||
'health:100,maxHealth:100',
|
||
'health:BALANCE.enemyHealth,maxHealth:BALANCE.enemyHealth',
|
||
)
|
||
.replace('},120));', '},BALANCE.waveTimingsMs[0]));')
|
||
.replace('},300));', '},BALANCE.waveTimingsMs[1]));')
|
||
.replace('},560));', '},BALANCE.waveTimingsMs[2]));');
|
||
}
|
||
|
||
const deterministicProjectMemory = `# 灵露花园项目记忆
|
||
|
||
## 项目目标
|
||
|
||
制作一个移动端优先、桌面端同样可玩的单页植物塔防游戏。玩家通过选择并放置植物阻挡敌人,完成波次后进入下一关。
|
||
|
||
## 长期约束
|
||
|
||
- 保持 lane-defense-v1 状态面和固定试玩控件稳定。
|
||
- 游戏入口是自包含的 \`game/index.html\`,通过本地 HTTP server 运行。
|
||
- 数值来自 \`game/balance.json\`,美术和声音范围分别由对应 manifest 描述。
|
||
- 每次改动都必须保持开始、选择、放置、战斗、胜利、下一关和重开链路可用。
|
||
`;
|
||
|
||
const deterministicGameDesign = `# 灵露花园游戏设计
|
||
|
||
## 核心循环
|
||
|
||
选择原创守卫,消耗灵露放入横向战场,观察敌人推进与受伤,在敌人抵达花园前清空当前波次。
|
||
|
||
## 胜负条件
|
||
|
||
- 当前波次敌人全部被击败时获胜并解锁下一关。
|
||
- 敌人越过最左侧防线时失败;重开保留当前关卡并重置局内状态。
|
||
|
||
## 第一版关卡
|
||
|
||
第一关用于教学选择、放置和加速战斗。后续关卡提高敌人生命和推进节奏,但继续使用相同状态机与输入边界。
|
||
|
||
## 操作与界面
|
||
|
||
横屏界面包含资源区、植物选择区、五格战场、波次状态和开始、加速、下一关、重开按钮。移动端按钮保持至少 44px 的点击高度。
|
||
`;
|
||
|
||
const deterministicAudioManifest = Object.freeze({
|
||
schemaVersion: 'game-audio-manifest.v1',
|
||
bgm: [
|
||
{
|
||
id: 'garden-day-loop',
|
||
usage: '关卡循环背景音乐',
|
||
mood: '轻快、明亮、逐步紧张',
|
||
loop: true,
|
||
status: 'planned',
|
||
},
|
||
],
|
||
sfx: [
|
||
{ id: 'plant-select', usage: '选择植物', status: 'planned' },
|
||
{ id: 'plant-place', usage: '放置植物', status: 'planned' },
|
||
{ id: 'enemy-hit', usage: '敌人受伤', status: 'planned' },
|
||
{ id: 'wave-win', usage: '波次胜利', status: 'planned' },
|
||
],
|
||
});
|
||
|
||
const deterministicPublishReadme = `# 灵露花园
|
||
|
||
一款轻量、可快速上手的横向植物塔防网页游戏。选择植物、布置防线并击退逐步增强的敌人波次。
|
||
|
||
## 操作
|
||
|
||
1. 点击 Start Game 开始当前关卡。
|
||
2. 选择露华花或棘刺芽,再点击战场格放置。
|
||
3. 使用 Speed Up 推进战斗,胜利后点击 Next Level。
|
||
4. 点击 Restart 重置当前关卡。
|
||
|
||
## 标签
|
||
|
||
塔防、策略、休闲、网页游戏、移动端
|
||
|
||
## 导出检查
|
||
|
||
- \`game/index.html\` 可通过本地 HTTP server 打开。
|
||
- 核心循环、固定控件和双视口试玩已纳入验收。
|
||
- 数值、美术和声音 manifest 均随项目交付。
|
||
`;
|
||
|
||
function deterministicArtManifest(
|
||
hasVisualAsset,
|
||
lifecycle = 'initial-delegation',
|
||
) {
|
||
return {
|
||
schemaVersion: 'game-art-manifest.v1',
|
||
lifecycle,
|
||
visualDirection: '明亮花园、清晰轮廓、原创植物守卫与敌人',
|
||
assets: hasVisualAsset
|
||
? [
|
||
{
|
||
id: 'garden-guardians-spritesheet',
|
||
path: 'assets/art-spritesheet.png',
|
||
kind: 'art-spritesheet',
|
||
usage: ['defenders', 'enemies', 'battlefield-ui'],
|
||
source: 'canvas',
|
||
status: 'ready',
|
||
},
|
||
]
|
||
: [],
|
||
pending: hasVisualAsset
|
||
? []
|
||
: [
|
||
{
|
||
id: 'garden-guardians-spritesheet',
|
||
path: 'assets/art-spritesheet.png',
|
||
reason:
|
||
'当前运行未提供 canvas.asset_generate,保留为后续画布生成项',
|
||
},
|
||
],
|
||
};
|
||
}
|
||
|
||
function providerError(code) {
|
||
const error = new Error(code);
|
||
error.code = code;
|
||
return error;
|
||
}
|
||
|
||
function isPlainObject(value) {
|
||
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
||
}
|
||
|
||
function messageText(message) {
|
||
if (typeof message?.content === 'string') return message.content;
|
||
if (!Array.isArray(message?.content)) return '';
|
||
return message.content
|
||
.map((part) => (typeof part?.text === 'string' ? part.text : ''))
|
||
.join('\n');
|
||
}
|
||
|
||
function requestContext(payload) {
|
||
return Array.isArray(payload.messages)
|
||
? payload.messages.map(messageText).join('\n')
|
||
: '';
|
||
}
|
||
|
||
function extractIdentity(context) {
|
||
const agentId =
|
||
context.match(/^- templateAgentId:\s*([a-z0-9-]+)\s*$/imu)?.[1] ??
|
||
context.match(/template taskId=([a-z0-9-]+)/iu)?.[1] ??
|
||
null;
|
||
const runId = context.match(/^- runId:\s*([^\r\n]+?)\s*$/imu)?.[1] ?? null;
|
||
if (!agentId || !runId) throw providerError('provider-identity-missing');
|
||
return { agentId, runId };
|
||
}
|
||
|
||
function currentPlanSteps(context) {
|
||
const marker = '计划进度:';
|
||
const markerIndex = context.lastIndexOf(marker);
|
||
if (markerIndex < 0) return [];
|
||
const steps = [];
|
||
for (const line of context
|
||
.slice(markerIndex + marker.length)
|
||
.split(/\r?\n/u)) {
|
||
const match = line.match(/^- #\d+ \[[^\]]+\] (.+)$/u);
|
||
if (!match) {
|
||
if (steps.length > 0) break;
|
||
continue;
|
||
}
|
||
const title = match[1].split(';', 1)[0].trim();
|
||
if (title) steps.push(title);
|
||
}
|
||
return steps;
|
||
}
|
||
|
||
function advertisedToolNames(payload) {
|
||
return new Set(
|
||
Array.isArray(payload.tools)
|
||
? payload.tools
|
||
.map((tool) => tool?.function?.name)
|
||
.filter((name) => typeof name === 'string' && name.length > 0)
|
||
: [],
|
||
);
|
||
}
|
||
|
||
function isUiPrototypeInspectionRequest(payload, context) {
|
||
const hasInputImage = Array.isArray(payload.messages)
|
||
? payload.messages.some((message) =>
|
||
Array.isArray(message?.content)
|
||
? message.content.some(
|
||
(part) =>
|
||
part?.type === 'image_url' ||
|
||
part?.type === 'input_image' ||
|
||
typeof part?.image_url === 'string' ||
|
||
typeof part?.image_url?.url === 'string',
|
||
)
|
||
: false,
|
||
)
|
||
: false;
|
||
return (
|
||
hasInputImage &&
|
||
context.includes('informationHud') &&
|
||
context.includes('gameplaySurface') &&
|
||
context.includes('objectiveEntities') &&
|
||
context.includes('primaryControls') &&
|
||
context.includes('failureRestartFlow') &&
|
||
context.includes('responsiveLayout') &&
|
||
context.includes('implementationClarity') &&
|
||
context.includes('originalTheme')
|
||
);
|
||
}
|
||
|
||
function runtimeFunction(tool) {
|
||
return `runtime_tool_${tool.replaceAll('.', '_')}`;
|
||
}
|
||
|
||
function nativeAction(tool, reason, input) {
|
||
return { name: runtimeFunction(tool), arguments: { reason, input } };
|
||
}
|
||
|
||
function nativeResponse(response) {
|
||
return { name: 'respond_to_user', arguments: { response } };
|
||
}
|
||
|
||
function goalContractCall() {
|
||
return nativeAction(
|
||
'agent.goal_contract',
|
||
'冻结本轮可玩游戏交付目标与验收标准',
|
||
{
|
||
outcome:
|
||
'交付一个可完整游玩的原创植物塔防网页游戏,并让当前最终 revision 通过自动静态检查和桌面、移动双视口真实试玩。',
|
||
nonNegotiables: [
|
||
'必须形成从开始、放置防御单位、敌人移动与受伤到胜利、下一关和重开的完整闭环',
|
||
'必须在无人确认、无人补充输入和无人 steer 的条件下完成',
|
||
],
|
||
preferences: ['保持单页自包含并提供清晰的桌面与移动端操作反馈'],
|
||
forbiddenAssumptions: [
|
||
'不得把静态页面、占位素材或未执行的试玩声明为完成',
|
||
],
|
||
openQuestions: [],
|
||
acceptanceNodes: [
|
||
{
|
||
criterionId: 'static-current-revision',
|
||
criterion:
|
||
'当前最终 revision 的游戏入口通过可执行 JavaScript、Canvas 与完整玩法合同静态检查',
|
||
required: true,
|
||
requiredEvidence: ['tool:command.run_limited'],
|
||
dependsOn: [],
|
||
},
|
||
{
|
||
criterionId: 'playable-current-revision',
|
||
criterion:
|
||
'当前最终 revision 的完整塔防循环在桌面与移动视口均通过真实浏览器试玩',
|
||
required: true,
|
||
requiredEvidence: ['tool:preview.validate'],
|
||
dependsOn: ['static-current-revision'],
|
||
},
|
||
],
|
||
},
|
||
);
|
||
}
|
||
|
||
function actionHistoryCall() {
|
||
return nativeAction(
|
||
'agent.action_history',
|
||
'读取当前根 Run 的最终静态检查与真实试玩动作回执',
|
||
{
|
||
runId: null,
|
||
actionId: null,
|
||
tool: null,
|
||
status: 'ok',
|
||
limit: 10,
|
||
},
|
||
);
|
||
}
|
||
|
||
function goalContractFingerprint(context) {
|
||
const marker = '[Root Goal Contract]';
|
||
const start = context.lastIndexOf(marker);
|
||
if (start < 0) return null;
|
||
const tail = context.slice(start);
|
||
const end = tail.indexOf('\n\n');
|
||
const goalContractContext = end < 0 ? tail : tail.slice(0, end);
|
||
const matches = new Set(
|
||
[
|
||
...goalContractContext.matchAll(
|
||
/"contractFingerprint"\s*:\s*"([0-9a-f]{64})"/giu,
|
||
),
|
||
].map((match) => match[1].toLowerCase()),
|
||
);
|
||
return matches.size === 1 ? [...matches][0] : null;
|
||
}
|
||
|
||
function finalAcceptanceEvidence(context, expectedRunId) {
|
||
const observation = latestToolObservation(context);
|
||
if (
|
||
observation?.tool !== 'agent.action_history' ||
|
||
observation?.status !== 'ok' ||
|
||
typeof observation.detail !== 'string'
|
||
) {
|
||
throw providerError('provider-final-action-history-observation-invalid');
|
||
}
|
||
let detail;
|
||
try {
|
||
detail = JSON.parse(observation.detail);
|
||
} catch {
|
||
throw providerError('provider-final-action-history-detail-invalid');
|
||
}
|
||
if (
|
||
detail?.runId !== expectedRunId ||
|
||
!Array.isArray(detail?.actions) ||
|
||
detail?.count !== detail.actions.length
|
||
) {
|
||
throw providerError('provider-final-action-history-identity-invalid');
|
||
}
|
||
const evidenceByTool = new Map();
|
||
for (const action of detail.actions) {
|
||
if (
|
||
action?.agentId !== 'project-supervisor' ||
|
||
action?.runId !== expectedRunId ||
|
||
action?.status !== 'ok' ||
|
||
typeof action?.actionId !== 'string' ||
|
||
!/^action-[0-9a-f]{24}$/iu.test(action.actionId) ||
|
||
!['command.run_limited', 'preview.validate'].includes(action?.tool)
|
||
) {
|
||
continue;
|
||
}
|
||
evidenceByTool.set(action.tool, {
|
||
agentId: action.agentId,
|
||
runId: action.runId,
|
||
actionId: action.actionId,
|
||
});
|
||
}
|
||
const staticSmoke = evidenceByTool.get('command.run_limited');
|
||
const preview = evidenceByTool.get('preview.validate');
|
||
if (!staticSmoke || !preview) {
|
||
throw providerError('provider-final-acceptance-evidence-missing');
|
||
}
|
||
return { staticSmoke, preview };
|
||
}
|
||
|
||
function acceptanceUpdateCall(context, runId) {
|
||
const contractFingerprint = goalContractFingerprint(context);
|
||
if (!contractFingerprint) {
|
||
throw providerError('provider-goal-contract-fingerprint-missing');
|
||
}
|
||
const evidence = finalAcceptanceEvidence(context, runId);
|
||
return nativeAction(
|
||
'agent.acceptance_update',
|
||
'提交当前最终 revision 的静态检查与双视口试玩验收证据',
|
||
{
|
||
contractFingerprint,
|
||
evaluations: [
|
||
{
|
||
criterionId: 'static-current-revision',
|
||
status: 'passed',
|
||
evidence: [evidence.staticSmoke],
|
||
summary: '当前最终 revision 已通过 game.static_smoke。',
|
||
},
|
||
{
|
||
criterionId: 'playable-current-revision',
|
||
status: 'passed',
|
||
evidence: [evidence.preview],
|
||
summary: '当前最终 revision 已通过桌面与移动双视口真实试玩。',
|
||
},
|
||
],
|
||
},
|
||
);
|
||
}
|
||
|
||
function completedPlanCall(explanation, steps) {
|
||
return {
|
||
name: 'update_agent_plan',
|
||
arguments: {
|
||
explanation,
|
||
steps: steps.map((step) => ({ step, status: 'completed' })),
|
||
},
|
||
};
|
||
}
|
||
|
||
function completedPlanCallsForContext(context, explanation) {
|
||
const steps = currentPlanSteps(context);
|
||
return steps.length > 0 ? [completedPlanCall(explanation, steps)] : [];
|
||
}
|
||
|
||
function requireAdvertised(tools, calls) {
|
||
for (const call of calls) {
|
||
if (!tools.has(call.name)) {
|
||
throw providerError(`provider-tool-not-advertised:${call.name}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
function chatToolResponse(sequence, model, calls) {
|
||
return {
|
||
id: `chatcmpl-deterministic-${sequence}`,
|
||
object: 'chat.completion',
|
||
model,
|
||
choices: [
|
||
{
|
||
index: 0,
|
||
message: {
|
||
role: 'assistant',
|
||
content: null,
|
||
tool_calls: calls.map((call, index) => ({
|
||
id: `call-deterministic-${sequence}-${index + 1}`,
|
||
type: 'function',
|
||
function: {
|
||
name: call.name,
|
||
arguments: JSON.stringify(call.arguments),
|
||
},
|
||
})),
|
||
},
|
||
finish_reason: 'tool_calls',
|
||
},
|
||
],
|
||
usage: { prompt_tokens: 160, completion_tokens: 48, total_tokens: 208 },
|
||
};
|
||
}
|
||
|
||
function chatTextResponse(sequence, model, content) {
|
||
return {
|
||
id: `chatcmpl-deterministic-${sequence}`,
|
||
object: 'chat.completion',
|
||
model,
|
||
choices: [
|
||
{
|
||
index: 0,
|
||
message: { role: 'assistant', content },
|
||
finish_reason: 'stop',
|
||
},
|
||
],
|
||
usage: { prompt_tokens: 120, completion_tokens: 32, total_tokens: 152 },
|
||
};
|
||
}
|
||
|
||
const projectMutationFunctionNames = new Set(
|
||
[
|
||
'blackboard.write',
|
||
'canvas.asset_generate',
|
||
'command.exec',
|
||
'command.start',
|
||
'file.delete',
|
||
'file.patch',
|
||
'file.write',
|
||
'memory.write',
|
||
'project.git_commit',
|
||
'project.patchset',
|
||
'project.restore',
|
||
'task.create',
|
||
'task.update',
|
||
].map(runtimeFunction),
|
||
);
|
||
|
||
function staticSmokeCall(reason) {
|
||
return nativeAction('command.run_limited', reason, {
|
||
commandId: 'game.static_smoke',
|
||
});
|
||
}
|
||
|
||
function previewCall(reason) {
|
||
return nativeAction('preview.validate', reason, {
|
||
viewports: ['desktop', 'mobile'],
|
||
expectedText: [
|
||
'灵露花园',
|
||
'Start Game',
|
||
'露华花',
|
||
'棘刺芽',
|
||
'Speed Up',
|
||
'Next Level',
|
||
'Restart',
|
||
],
|
||
settleMs: 300,
|
||
failOnConsoleError: true,
|
||
playtestScenario: null,
|
||
});
|
||
}
|
||
|
||
function runStatusCall(reason) {
|
||
return nativeAction('agent.run_status', reason, {
|
||
agentId: null,
|
||
scope: 'all',
|
||
delegationId: null,
|
||
});
|
||
}
|
||
|
||
function fileReadCall(path, reason) {
|
||
return nativeAction('file.read', reason, {
|
||
path,
|
||
startLine: 1,
|
||
maxLines: 120,
|
||
});
|
||
}
|
||
|
||
function fileWriteCall(path, content, reason) {
|
||
return nativeAction('file.write', reason, { path, content });
|
||
}
|
||
|
||
function assetListCall(reason) {
|
||
return nativeAction('asset.list', reason, {});
|
||
}
|
||
|
||
function canvasAssetCall(agentId) {
|
||
if (agentId === 'art-director') {
|
||
return nativeAction('canvas.asset_generate', '生成统一视觉规范图', {
|
||
prompt:
|
||
'原创明亮花园植物塔防游戏的方形图标规范图。统一展示露华花守卫、棘刺芽守卫、雾影兽入侵者、晶苔地块、灵露资源和主要 UI 图标的轮廓、配色、材质与尺寸关系;纯色分区背景,适合后续角色、场景和 UI 共用,不使用任何现有游戏角色、Logo 或受保护视觉语言。',
|
||
outputPath: 'assets/art-spec.png',
|
||
aspectRatio: '1:1',
|
||
imageSize: '1K',
|
||
assetKind: 'icon-spec',
|
||
assetLabel: '游戏统一视觉规范图',
|
||
replaceExisting: false,
|
||
});
|
||
}
|
||
if (agentId === 'design-foundation') {
|
||
return nativeAction('canvas.asset_generate', '生成横屏游戏界面原型', {
|
||
prompt:
|
||
'原创明亮花园植物塔防游戏的 16:9 完整 UI 原型。画面必须清楚展示资源数值栏、带费用的植物卡槽、五列战场网格、敌人来袭方向、波次状态、开始、加速、下一关和重开控件;适合直接指导 HTML/CSS 实现,不使用任何现有游戏角色、Logo 或受保护视觉语言。',
|
||
outputPath: 'assets/ui-prototype.png',
|
||
aspectRatio: '16:9',
|
||
imageSize: '2K',
|
||
assetKind: 'ui-prototype',
|
||
assetLabel: '游戏横屏界面原型图',
|
||
replaceExisting: false,
|
||
});
|
||
}
|
||
return nativeAction('canvas.asset_generate', '生成首版核心美术素材', {
|
||
prompt:
|
||
'原创明亮花园塔防游戏的方形 spritesheet。包含露华花守卫、棘刺芽守卫、雾影兽入侵者、晶苔战场地块、投射物和灵露资源图标;透明或纯色分区背景,轮廓清楚、比例一致,可直接切分用于网页游戏,不使用任何现有游戏角色、单位名、Logo 或受保护视觉语言。',
|
||
outputPath: 'assets/art-spritesheet.png',
|
||
aspectRatio: '1:1',
|
||
imageSize: '1K',
|
||
assetKind: 'art-spritesheet',
|
||
assetLabel: '游戏首版核心美术素材',
|
||
replaceExisting: false,
|
||
sliceMode: 'connected-components',
|
||
});
|
||
}
|
||
|
||
function imageInspectCall() {
|
||
return nativeAction('image.inspect', '检查 UI 原型是否满足实现合同', {
|
||
paths: ['assets/ui-prototype.png'],
|
||
question:
|
||
'核对资源栏、植物卡槽、战场网格、敌人方向、波次状态、主要控件、实现清晰度和原创主题。',
|
||
});
|
||
}
|
||
|
||
function readyTaskContext(context, agentId) {
|
||
return (
|
||
context.includes('处理 manifest ready 任务:') &&
|
||
context.includes(`任务 ID:${agentId}`)
|
||
);
|
||
}
|
||
|
||
function observationContext(context) {
|
||
// The standard tool-plan prompt labels this section "已有工具观察:",
|
||
// while the relaxed autonomous prompt intentionally uses the shorter
|
||
// "已有观察:" label. Keep the parser independent of that presentation
|
||
// detail; otherwise a settled task.list observation is invisible to the
|
||
// deterministic parent and it will poll forever until the run budget ends.
|
||
const markers = ['已有工具观察:', '已有观察:', '工具观察:'];
|
||
let start = -1;
|
||
let markerLength = 0;
|
||
for (const marker of markers) {
|
||
const candidate = context.lastIndexOf(marker);
|
||
if (candidate > start) {
|
||
start = candidate;
|
||
markerLength = marker.length;
|
||
}
|
||
}
|
||
if (start < 0) return '';
|
||
const tail = context.slice(start + markerLength);
|
||
const endMarkers = ['\n\n计划更新约定:', '\n\n工具 input 字段约定:'];
|
||
const ends = endMarkers
|
||
.map((marker) => tail.indexOf(marker))
|
||
.filter((index) => index >= 0);
|
||
const end = ends.length > 0 ? Math.min(...ends) : -1;
|
||
return end < 0 ? tail : tail.slice(0, end);
|
||
}
|
||
|
||
function latestToolObservation(context) {
|
||
const raw = observationContext(context).trim();
|
||
if (!raw.startsWith('[')) return null;
|
||
try {
|
||
const observations = JSON.parse(raw);
|
||
return Array.isArray(observations) ? (observations.at(-1) ?? null) : null;
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function revisionBlockedObservation(context) {
|
||
const observation = latestToolObservation(context);
|
||
return (
|
||
observation?.tool === 'runtime.verification' &&
|
||
observation?.status === 'blocked' &&
|
||
typeof observation?.detail === 'string' &&
|
||
observation.detail.includes('responseRevision=') &&
|
||
observation.detail.includes('currentRevision=')
|
||
);
|
||
}
|
||
|
||
function projectRevisionDriftObservation(context, tool) {
|
||
const observation = latestToolObservation(context);
|
||
const detail =
|
||
typeof observation?.detail === 'string' ? observation.detail : '';
|
||
const hasStructuredDetail =
|
||
detail.includes('projectRevisionDrift=true') &&
|
||
detail.includes('expectedRevision=') &&
|
||
detail.includes('currentRevision=') &&
|
||
detail.includes('replanRequired=true');
|
||
const detailUnavailable =
|
||
observation?.detail == null || detail === '[redacted sensitive context]';
|
||
return (
|
||
observation?.tool === tool &&
|
||
observation?.status === 'blocked' &&
|
||
observation?.summary === '并行项目变更使旧动作过期,旧动作未执行' &&
|
||
(hasStructuredDetail || detailUnavailable)
|
||
);
|
||
}
|
||
|
||
function visualAssetBlockedObservation(context) {
|
||
const observation = latestToolObservation(context);
|
||
return (
|
||
observation?.tool === 'runtime.visual_asset' &&
|
||
observation?.status === 'blocked' &&
|
||
typeof observation?.detail === 'string' &&
|
||
observation.detail.includes('expectedPath=assets/ui-prototype.png') &&
|
||
observation.detail.includes('requiredInspection=image.inspect')
|
||
);
|
||
}
|
||
|
||
function missingGeneratedVisualAssetObservation(context, agentId) {
|
||
const expected = {
|
||
'art-director': {
|
||
path: 'assets/art-spec.png',
|
||
kind: 'icon-spec',
|
||
summary: '统一视觉规范图尚未按正式视觉流程生成并登记,不能完成任务',
|
||
},
|
||
'design-foundation': {
|
||
path: 'assets/ui-prototype.png',
|
||
kind: 'ui-prototype',
|
||
summary: '策划界面原型图尚未按正式视觉流程生成并登记,不能完成任务',
|
||
},
|
||
'art-asset-plan': {
|
||
path: 'assets/art-spritesheet.png',
|
||
kind: 'art-spritesheet',
|
||
summary: '首版美术素材图尚未按正式视觉流程生成并登记,不能完成任务',
|
||
},
|
||
}[agentId];
|
||
if (!expected) return false;
|
||
const observation = latestToolObservation(context);
|
||
return (
|
||
observation?.tool === 'runtime.visual_asset' &&
|
||
observation?.status === 'blocked' &&
|
||
observation?.summary === expected.summary &&
|
||
typeof observation?.detail === 'string' &&
|
||
(observation.detail === '[redacted sensitive context]' ||
|
||
(observation.detail.includes(`expectedPath=${expected.path}`) &&
|
||
observation.detail.includes(`expectedKind=${expected.kind}`)))
|
||
);
|
||
}
|
||
|
||
function specialistVerificationRepairContext(context) {
|
||
return (
|
||
context.includes(
|
||
'当前是 autonomous-game-build 的非只读专业任务,且本人 run 已有 mutation',
|
||
) &&
|
||
context.includes(
|
||
'本次修复的原生工具目录只保留 project.verify 与 command.run_limited',
|
||
)
|
||
);
|
||
}
|
||
|
||
function verifiedDeliveryRepairContext(context) {
|
||
return (
|
||
context.includes('当前 revision 已通过验证') &&
|
||
context.includes('本次修复的原生工具目录只保留 respond_to_user')
|
||
);
|
||
}
|
||
|
||
function transientCommandObservation(context) {
|
||
const observation = latestToolObservation(context);
|
||
const detail =
|
||
typeof observation?.detail === 'string'
|
||
? observation.detail.replaceAll('\\', '/')
|
||
: '';
|
||
return (
|
||
observation?.tool === 'command.run_limited' &&
|
||
((observation?.status === 'blocked' &&
|
||
(detail.includes('repositoryContextDrift=true') ||
|
||
(detail.includes('projectRevisionDrift=true') &&
|
||
detail.includes('replanRequired=true')))) ||
|
||
(observation?.status === 'failed' &&
|
||
detail.includes('.agent/project.lock')))
|
||
);
|
||
}
|
||
|
||
function successfulProjectMutationObservation(context) {
|
||
const observation = latestToolObservation(context);
|
||
return (
|
||
observation?.status === 'ok' &&
|
||
deterministicProjectMutationTools.has(observation?.tool)
|
||
);
|
||
}
|
||
|
||
function observationsIncludeAsset(context, path) {
|
||
const observations = observationContext(context);
|
||
return observations.includes('# 本地项目资产') && observations.includes(path);
|
||
}
|
||
|
||
function completedReadyPlanCalls(context, agentId) {
|
||
const existing = completedPlanCallsForContext(
|
||
context,
|
||
`${agentId} 已完成 manifest ready 任务`,
|
||
);
|
||
if (existing.length > 0) return existing;
|
||
return [
|
||
completedPlanCall(`${agentId} 已完成 manifest ready 任务`, [
|
||
`完成 ${agentId} 正式任务`,
|
||
]),
|
||
];
|
||
}
|
||
|
||
function readyTaskFinalizationCalls(context, agentId, tools) {
|
||
const calls = [];
|
||
if (tools.has('respond_to_user')) {
|
||
if (tools.has('update_agent_plan')) {
|
||
calls.push(...completedReadyPlanCalls(context, agentId));
|
||
}
|
||
calls.push(nativeResponse(`${agentId} 的 manifest ready 任务已经完成。`));
|
||
}
|
||
return calls;
|
||
}
|
||
|
||
export function createDeterministicLaneDefenseRouter({
|
||
apiKey,
|
||
model = deterministicLaneDefenseModel,
|
||
relaxed = false,
|
||
} = {}) {
|
||
if (typeof apiKey !== 'string' || apiKey.length < 16) {
|
||
throw providerError('provider-api-key-invalid');
|
||
}
|
||
const runKinds = new Map();
|
||
const runPhases = new Map();
|
||
const runData = new Map();
|
||
const finalReplyRuns = new Set();
|
||
const readyTaskRunIdsByAgent = new Map();
|
||
const completedReadyTaskRuns = new Set();
|
||
const readyTaskCompletionRetryArmedRuns = new Set();
|
||
const readyTaskCompletionVerificationTools = new Map();
|
||
const readyTaskCompletionRetryCounts = new Map();
|
||
const readyTaskPreCompletionRetryCounts = new Map();
|
||
const readyTaskReplayAvailableRuns = new Set();
|
||
const agentCounts = new Map();
|
||
const relaxedAutonomous = relaxed === true;
|
||
const stats = {
|
||
relaxedAutonomous,
|
||
requestCount: 0,
|
||
contextCompactionRequestCount: 0,
|
||
planningRequestCount: 0,
|
||
finalReplyRequestCount: 0,
|
||
goalContractCount: 0,
|
||
actionHistoryCount: 0,
|
||
acceptanceUpdateCount: 0,
|
||
delegateActionCount: 0,
|
||
runStatusCount: 0,
|
||
sourceWriteCount: 0,
|
||
staticSmokeCount: 0,
|
||
previewValidationCount: 0,
|
||
manifestReadyTaskRunCount: 0,
|
||
manifestReadyTaskCompletionCount: 0,
|
||
manifestReadyTaskFileReadCount: 0,
|
||
manifestReadyTaskFileWriteCount: 0,
|
||
manifestReadyTaskStaticSmokeCount: 0,
|
||
manifestReadyTaskPreviewValidationCount: 0,
|
||
manifestReadyTaskCanvasGenerationCount: 0,
|
||
canonicalCodeRunCount: 0,
|
||
canonicalCodeAssetListCount: 0,
|
||
imageInspectionRequestCount: 0,
|
||
interactionExecuteCount: 0,
|
||
readyTaskRunsByAgent: {},
|
||
readyTaskCompletionsByAgent: {},
|
||
readyTaskCountsByAgent: {},
|
||
unexpectedRequestCount: 0,
|
||
rejectionCodes: {},
|
||
rejections: [],
|
||
byAgent: {},
|
||
};
|
||
let responseSequence = 0;
|
||
let parentStage = relaxedAutonomous ? 'await-manifest' : 'goal-contract';
|
||
|
||
function updateReadyTaskCounts(agentId, kind) {
|
||
const current = stats.readyTaskCountsByAgent[agentId] ?? {
|
||
run: 0,
|
||
completion: 0,
|
||
};
|
||
current[kind] += 1;
|
||
stats.readyTaskCountsByAgent[agentId] = current;
|
||
}
|
||
|
||
function recordReadyTaskRun(agentId, runId) {
|
||
if (
|
||
!relaxedAutonomous &&
|
||
!deterministicManifestReadyAgentIds.includes(agentId)
|
||
) {
|
||
throw providerError(`provider-ready-agent-unsupported:${agentId}`);
|
||
}
|
||
const existingRunId = readyTaskRunIdsByAgent.get(agentId);
|
||
if (!relaxedAutonomous && existingRunId && existingRunId !== runId) {
|
||
throw providerError(`provider-ready-agent-run-duplicate:${agentId}`);
|
||
}
|
||
readyTaskRunIdsByAgent.set(agentId, runId);
|
||
stats.manifestReadyTaskRunCount += 1;
|
||
stats.readyTaskRunsByAgent[agentId] =
|
||
(stats.readyTaskRunsByAgent[agentId] ?? 0) + 1;
|
||
updateReadyTaskCounts(agentId, 'run');
|
||
}
|
||
|
||
function recordReadyTaskCompletion(agentId, runId) {
|
||
if (!relaxedAutonomous && readyTaskRunIdsByAgent.get(agentId) !== runId) {
|
||
throw providerError(`provider-ready-run-identity-invalid:${agentId}`);
|
||
}
|
||
if (!relaxedAutonomous && completedReadyTaskRuns.has(runId)) {
|
||
throw providerError(`provider-ready-run-terminal-duplicate:${agentId}`);
|
||
}
|
||
completedReadyTaskRuns.add(runId);
|
||
readyTaskReplayAvailableRuns.add(runId);
|
||
stats.manifestReadyTaskCompletionCount += 1;
|
||
stats.readyTaskCompletionsByAgent[agentId] =
|
||
(stats.readyTaskCompletionsByAgent[agentId] ?? 0) + 1;
|
||
updateReadyTaskCounts(agentId, 'completion');
|
||
}
|
||
|
||
function ensureReadyTaskRun(runId, agentId) {
|
||
const expectedKind = `manifest-ready:${agentId}`;
|
||
const existingKind = runKinds.get(runId);
|
||
if (!existingKind) {
|
||
recordReadyTaskRun(agentId, runId);
|
||
runKinds.set(runId, expectedKind);
|
||
runPhases.set(runId, 0);
|
||
} else if (existingKind !== expectedKind) {
|
||
throw providerError(
|
||
`provider-ready-run-kind-invalid:${existingKind}:${agentId}`,
|
||
);
|
||
}
|
||
return runPhases.get(runId) ?? 0;
|
||
}
|
||
|
||
function advanceReadyTaskRun(runId, phase) {
|
||
runPhases.set(runId, phase + 1);
|
||
}
|
||
|
||
function readyCallsResponse(agentId, runId, tools, calls) {
|
||
advanceReadyTaskRun(runId, runPhases.get(runId) ?? 0);
|
||
return callsResponse(agentId, tools, calls);
|
||
}
|
||
|
||
function readyTaskCompleteResponse(agentId, runId, tools, context) {
|
||
if (relaxedAutonomous) {
|
||
if (!tools.has('respond_to_user')) {
|
||
throw providerError(
|
||
`provider-relaxed-ready-finalization-tool-missing:${agentId}`,
|
||
);
|
||
}
|
||
if (!completedReadyTaskRuns.has(runId)) {
|
||
recordReadyTaskCompletion(agentId, runId);
|
||
}
|
||
return readyCallsResponse(agentId, runId, tools, [
|
||
nativeResponse(`${agentId} 的任务已完成。`),
|
||
]);
|
||
}
|
||
if (completedReadyTaskRuns.has(runId)) {
|
||
const retryCount = readyTaskCompletionRetryCounts.get(runId) ?? 0;
|
||
const expectedVerificationTool =
|
||
readyTaskCompletionVerificationTools.get(runId) ??
|
||
(agentId === 'preview-playtest'
|
||
? 'preview.validate'
|
||
: 'command.run_limited');
|
||
if (
|
||
tools.size === 1 &&
|
||
tools.has('respond_to_user') &&
|
||
verifiedDeliveryRepairContext(context)
|
||
) {
|
||
readyTaskCompletionRetryArmedRuns.delete(runId);
|
||
readyTaskCompletionVerificationTools.delete(runId);
|
||
readyTaskReplayAvailableRuns.delete(runId);
|
||
return readyCallsResponse(
|
||
agentId,
|
||
runId,
|
||
tools,
|
||
readyTaskFinalizationCalls(context, agentId, tools),
|
||
);
|
||
}
|
||
if (readyTaskCompletionRetryArmedRuns.has(runId)) {
|
||
const observation = latestToolObservation(context);
|
||
if (
|
||
observation?.tool === expectedVerificationTool &&
|
||
observation?.status === 'ok'
|
||
) {
|
||
if (!tools.has('respond_to_user')) {
|
||
throw providerError(
|
||
`provider-ready-retry-finalization-tool-missing:${agentId}`,
|
||
);
|
||
}
|
||
readyTaskCompletionRetryArmedRuns.delete(runId);
|
||
readyTaskCompletionVerificationTools.delete(runId);
|
||
readyTaskReplayAvailableRuns.delete(runId);
|
||
return readyCallsResponse(
|
||
agentId,
|
||
runId,
|
||
tools,
|
||
readyTaskFinalizationCalls(context, agentId, tools),
|
||
);
|
||
}
|
||
if (
|
||
expectedVerificationTool === 'image.inspect' &&
|
||
observation?.tool === 'image.inspect' &&
|
||
['blocked', 'failed'].includes(observation?.status) &&
|
||
tools.has(runtimeFunction('image.inspect'))
|
||
) {
|
||
if (retryCount >= 16) {
|
||
throw providerError(
|
||
`provider-ready-retry-verification-exhausted:${agentId}`,
|
||
);
|
||
}
|
||
readyTaskCompletionRetryCounts.set(runId, retryCount + 1);
|
||
return readyCallsResponse(agentId, runId, tools, [
|
||
imageInspectCall(),
|
||
]);
|
||
}
|
||
const transientCommandFailure = transientCommandObservation(context);
|
||
if (
|
||
transientCommandFailure &&
|
||
tools.has(runtimeFunction('command.run_limited'))
|
||
) {
|
||
if (retryCount >= 16) {
|
||
throw providerError(
|
||
`provider-ready-retry-verification-exhausted:${agentId}`,
|
||
);
|
||
}
|
||
readyTaskCompletionRetryCounts.set(runId, retryCount + 1);
|
||
stats.staticSmokeCount += 1;
|
||
stats.manifestReadyTaskStaticSmokeCount += 1;
|
||
return readyCallsResponse(agentId, runId, tools, [
|
||
staticSmokeCall(`重试验证 ${agentId} 的当前项目 revision`),
|
||
]);
|
||
}
|
||
throw providerError(
|
||
`provider-ready-retry-verification-invalid:${agentId}`,
|
||
);
|
||
}
|
||
const latestObservation = latestToolObservation(context);
|
||
if (
|
||
missingGeneratedVisualAssetObservation(context, agentId) &&
|
||
tools.has(runtimeFunction('canvas.asset_generate'))
|
||
) {
|
||
if (retryCount >= 16) {
|
||
throw providerError(
|
||
`provider-ready-retry-verification-exhausted:${agentId}`,
|
||
);
|
||
}
|
||
readyTaskCompletionRetryCounts.set(runId, retryCount + 1);
|
||
stats.manifestReadyTaskCanvasGenerationCount += 1;
|
||
return readyCallsResponse(agentId, runId, tools, [
|
||
canvasAssetCall(agentId),
|
||
]);
|
||
}
|
||
if (
|
||
agentId === 'design-foundation' &&
|
||
visualAssetBlockedObservation(context) &&
|
||
tools.has(runtimeFunction('image.inspect'))
|
||
) {
|
||
if (retryCount >= 16) {
|
||
throw providerError(
|
||
`provider-ready-retry-verification-exhausted:${agentId}`,
|
||
);
|
||
}
|
||
readyTaskCompletionRetryCounts.set(runId, retryCount + 1);
|
||
readyTaskCompletionRetryArmedRuns.add(runId);
|
||
readyTaskCompletionVerificationTools.set(runId, 'image.inspect');
|
||
return readyCallsResponse(agentId, runId, tools, [imageInspectCall()]);
|
||
}
|
||
const successfulProjectMutation =
|
||
successfulProjectMutationObservation(context);
|
||
const standaloneSuccessfulVerification =
|
||
latestObservation?.tool === expectedVerificationTool &&
|
||
latestObservation?.status === 'ok' &&
|
||
(agentId === 'preview-readiness' ||
|
||
agentId === 'preview-playtest' ||
|
||
!deterministicReadOnlyReadyAgentIds.has(agentId));
|
||
if (standaloneSuccessfulVerification) {
|
||
if (!readyTaskReplayAvailableRuns.delete(runId)) {
|
||
throw providerError(
|
||
`provider-ready-run-terminal-duplicate:${agentId}`,
|
||
);
|
||
}
|
||
if (!tools.has('respond_to_user')) {
|
||
throw providerError(
|
||
`provider-ready-retry-finalization-tool-missing:${agentId}`,
|
||
);
|
||
}
|
||
return readyCallsResponse(
|
||
agentId,
|
||
runId,
|
||
tools,
|
||
readyTaskFinalizationCalls(context, agentId, tools),
|
||
);
|
||
}
|
||
if (
|
||
!revisionBlockedObservation(context) &&
|
||
!specialistVerificationRepairContext(context) &&
|
||
!transientCommandObservation(context) &&
|
||
!successfulProjectMutation
|
||
) {
|
||
throw providerError(`provider-ready-run-terminal-duplicate:${agentId}`);
|
||
}
|
||
if (retryCount >= 16) {
|
||
throw providerError(`provider-ready-run-terminal-duplicate:${agentId}`);
|
||
}
|
||
readyTaskCompletionRetryCounts.set(runId, retryCount + 1);
|
||
readyTaskReplayAvailableRuns.delete(runId);
|
||
let verificationCall = null;
|
||
let verificationTool = null;
|
||
if (
|
||
successfulProjectMutation &&
|
||
tools.has(runtimeFunction('command.run_limited')) &&
|
||
!deterministicReadOnlyReadyAgentIds.has(agentId)
|
||
) {
|
||
verificationCall = staticSmokeCall(
|
||
`重新验证 ${agentId} 并发修改后的当前项目 revision`,
|
||
);
|
||
verificationTool = 'command.run_limited';
|
||
stats.staticSmokeCount += 1;
|
||
stats.manifestReadyTaskStaticSmokeCount += 1;
|
||
} else if (
|
||
agentId === 'preview-playtest' &&
|
||
tools.has(runtimeFunction('preview.validate'))
|
||
) {
|
||
verificationCall = previewCall(
|
||
`重新验证 ${agentId} 的当前项目 revision`,
|
||
);
|
||
verificationTool = 'preview.validate';
|
||
stats.previewValidationCount += 1;
|
||
stats.manifestReadyTaskPreviewValidationCount += 1;
|
||
} else if (
|
||
tools.has(runtimeFunction('command.run_limited')) &&
|
||
(agentId === 'preview-readiness' ||
|
||
!deterministicReadOnlyReadyAgentIds.has(agentId))
|
||
) {
|
||
verificationCall = staticSmokeCall(
|
||
`重新验证 ${agentId} 的当前项目 revision`,
|
||
);
|
||
verificationTool = 'command.run_limited';
|
||
stats.staticSmokeCount += 1;
|
||
stats.manifestReadyTaskStaticSmokeCount += 1;
|
||
}
|
||
if (verificationCall) {
|
||
readyTaskCompletionRetryArmedRuns.add(runId);
|
||
readyTaskCompletionVerificationTools.set(runId, verificationTool);
|
||
return readyCallsResponse(agentId, runId, tools, [verificationCall]);
|
||
}
|
||
if (successfulProjectMutation) {
|
||
throw providerError(
|
||
`provider-ready-mutation-verification-tool-missing:${agentId}`,
|
||
);
|
||
}
|
||
if (!tools.has('respond_to_user')) {
|
||
throw providerError(
|
||
`provider-ready-retry-finalization-tool-missing:${agentId}`,
|
||
);
|
||
}
|
||
readyTaskReplayAvailableRuns.delete(runId);
|
||
return readyCallsResponse(
|
||
agentId,
|
||
runId,
|
||
tools,
|
||
readyTaskFinalizationCalls(context, agentId, tools),
|
||
);
|
||
}
|
||
const preCompletionRetryCount =
|
||
readyTaskPreCompletionRetryCounts.get(runId) ?? 0;
|
||
const mayRunStaticVerification =
|
||
agentId === 'preview-readiness' ||
|
||
!deterministicReadOnlyReadyAgentIds.has(agentId);
|
||
if (
|
||
mayRunStaticVerification &&
|
||
tools.has(runtimeFunction('command.run_limited')) &&
|
||
(specialistVerificationRepairContext(context) ||
|
||
transientCommandObservation(context))
|
||
) {
|
||
if (preCompletionRetryCount >= 16) {
|
||
throw providerError(
|
||
`provider-ready-precompletion-verification-exhausted:${agentId}`,
|
||
);
|
||
}
|
||
readyTaskPreCompletionRetryCounts.set(runId, preCompletionRetryCount + 1);
|
||
stats.staticSmokeCount += 1;
|
||
stats.manifestReadyTaskStaticSmokeCount += 1;
|
||
return readyCallsResponse(agentId, runId, tools, [
|
||
staticSmokeCall(`重试验证 ${agentId} 的当前项目 revision`),
|
||
]);
|
||
}
|
||
const calls = readyTaskFinalizationCalls(context, agentId, tools);
|
||
if (calls.length === 0) {
|
||
throw providerError(
|
||
`provider-ready-finalization-tools-invalid:${agentId}:${[...tools].sort().join(',')}`,
|
||
);
|
||
}
|
||
if (calls.some((call) => call.name === 'respond_to_user')) {
|
||
recordReadyTaskCompletion(agentId, runId);
|
||
}
|
||
return readyCallsResponse(agentId, runId, tools, calls);
|
||
}
|
||
|
||
function manifestTasksCompleted(context) {
|
||
const matches = [
|
||
...observationContext(context).matchAll(
|
||
/seedTaskCounts: completed=(\d+) running=(\d+) pending=(\d+) waiting=(\d+) failed=(\d+) total=(\d+)/g,
|
||
),
|
||
];
|
||
const match = matches.at(-1);
|
||
return (
|
||
match !== undefined &&
|
||
Number(match[1]) === 16 &&
|
||
Number(match[2]) === 0 &&
|
||
Number(match[3]) === 0 &&
|
||
Number(match[4]) === 0 &&
|
||
Number(match[5]) === 0 &&
|
||
Number(match[6]) === 16
|
||
);
|
||
}
|
||
|
||
function manifestTasksSettled(context) {
|
||
const matches = [
|
||
...observationContext(context).matchAll(
|
||
/seedTaskCounts: completed=(\d+) running=(\d+) pending=(\d+) waiting=(\d+) failed=(\d+) total=(\d+)/g,
|
||
),
|
||
];
|
||
const match = matches.at(-1);
|
||
return (
|
||
match !== undefined &&
|
||
Number(match[2]) === 0 &&
|
||
Number(match[3]) === 0 &&
|
||
Number(match[4]) === 0 &&
|
||
Number(match[1]) + Number(match[5]) === Number(match[6])
|
||
);
|
||
}
|
||
|
||
function publishAgentCounts() {
|
||
stats.byAgent = Object.fromEntries(
|
||
[...agentCounts.entries()].sort(([left], [right]) =>
|
||
left.localeCompare(right),
|
||
),
|
||
);
|
||
}
|
||
|
||
function getAgentCounts(agentId) {
|
||
return (
|
||
agentCounts.get(agentId) ?? {
|
||
planning: 0,
|
||
finalReply: 0,
|
||
projectMutation: 0,
|
||
}
|
||
);
|
||
}
|
||
|
||
function recordAgent(agentId, kind) {
|
||
const current = getAgentCounts(agentId);
|
||
current[kind] += 1;
|
||
agentCounts.set(agentId, current);
|
||
publishAgentCounts();
|
||
}
|
||
|
||
function recordEmittedCalls(agentId, calls) {
|
||
const current = getAgentCounts(agentId);
|
||
current.projectMutation += calls.filter((call) =>
|
||
projectMutationFunctionNames.has(call.name),
|
||
).length;
|
||
agentCounts.set(agentId, current);
|
||
publishAgentCounts();
|
||
|
||
for (const call of calls) {
|
||
if (call.name === runtimeFunction('agent.goal_contract')) {
|
||
stats.goalContractCount += 1;
|
||
} else if (call.name === runtimeFunction('agent.action_history')) {
|
||
stats.actionHistoryCount += 1;
|
||
} else if (call.name === runtimeFunction('agent.acceptance_update')) {
|
||
stats.acceptanceUpdateCount += 1;
|
||
} else if (call.name === runtimeFunction('agent.delegate')) {
|
||
stats.delegateActionCount += 1;
|
||
}
|
||
if (
|
||
agentId === 'code-prototype' &&
|
||
call.name === runtimeFunction('asset.list')
|
||
) {
|
||
stats.canonicalCodeAssetListCount += 1;
|
||
}
|
||
}
|
||
}
|
||
|
||
function callsResponse(agentId, tools, calls) {
|
||
requireAdvertised(tools, calls);
|
||
recordEmittedCalls(agentId, calls);
|
||
responseSequence += 1;
|
||
return chatToolResponse(responseSequence, model, calls);
|
||
}
|
||
|
||
function parentCalls(context, tools) {
|
||
if (relaxedAutonomous) {
|
||
if (manifestTasksSettled(context)) {
|
||
if (!tools.has('respond_to_user')) {
|
||
throw providerError('provider-relaxed-parent-respond-tool-missing');
|
||
}
|
||
parentStage = 'done';
|
||
return callsResponse('project-supervisor', tools, [
|
||
nativeResponse('项目任务已经完成,已交回总控。'),
|
||
]);
|
||
}
|
||
if (tools.has(runtimeFunction('task.list'))) {
|
||
const calls = [nativeAction('task.list', '查看并行任务当前状态', {})];
|
||
if (tools.has(runtimeFunction('agent.run_status'))) {
|
||
stats.runStatusCount += 1;
|
||
calls.push(runStatusCall('读取并行任务的最新运行状态'));
|
||
}
|
||
return callsResponse('project-supervisor', tools, calls);
|
||
}
|
||
if (tools.has('respond_to_user')) {
|
||
parentStage = 'done';
|
||
return callsResponse('project-supervisor', tools, [
|
||
nativeResponse('已完成当前自主构建回合。'),
|
||
]);
|
||
}
|
||
throw providerError('provider-relaxed-parent-tools-invalid');
|
||
}
|
||
const goalContractTool = runtimeFunction('agent.goal_contract');
|
||
if (tools.size === 1 && tools.has(goalContractTool)) {
|
||
if (parentStage !== 'goal-contract') {
|
||
throw providerError('provider-goal-contract-out-of-order');
|
||
}
|
||
parentStage = 'await-manifest';
|
||
return callsResponse('project-supervisor', tools, [goalContractCall()]);
|
||
}
|
||
|
||
const advertisedProjectMutation = [...tools].some((tool) =>
|
||
projectMutationFunctionNames.has(tool),
|
||
);
|
||
const advertisedSafeExecution = [
|
||
'agent.goal_contract',
|
||
'task.list',
|
||
'agent.run_status',
|
||
'command.run_limited',
|
||
'preview.validate',
|
||
'agent.action_history',
|
||
'agent.acceptance_update',
|
||
].some((tool) => tools.has(runtimeFunction(tool)));
|
||
if (advertisedProjectMutation && !advertisedSafeExecution) {
|
||
if (
|
||
tools.has('respond_to_user') &&
|
||
context.includes('当前父 run 已进入只编排模式')
|
||
) {
|
||
return callsResponse('project-supervisor', tools, [
|
||
nativeResponse('专业任务尚未收敛,继续等待 Runtime 完成门复核。'),
|
||
]);
|
||
}
|
||
throw providerError('provider-supervisor-direct-mutation-denied');
|
||
}
|
||
|
||
switch (parentStage) {
|
||
case 'await-manifest':
|
||
if (!manifestTasksCompleted(context)) {
|
||
if (
|
||
!tools.has(runtimeFunction('task.list')) ||
|
||
!tools.has(runtimeFunction('agent.run_status'))
|
||
) {
|
||
throw providerError('provider-manifest-wait-tools-invalid');
|
||
}
|
||
stats.runStatusCount += 1;
|
||
return callsResponse('project-supervisor', tools, [
|
||
nativeAction('task.list', '检查正式产物任务图是否已经收敛', {}),
|
||
runStatusCall('等待并读取并行专业任务的最新状态'),
|
||
]);
|
||
}
|
||
parentStage = 'final-static-smoke';
|
||
return parentCalls(context, tools);
|
||
case 'final-static-smoke':
|
||
if (!tools.has(runtimeFunction('command.run_limited'))) {
|
||
throw providerError('provider-final-static-smoke-tool-missing');
|
||
}
|
||
parentStage = 'final-preview';
|
||
stats.staticSmokeCount += 1;
|
||
return callsResponse('project-supervisor', tools, [
|
||
staticSmokeCall('验证全部正式产物整合后的当前 revision'),
|
||
]);
|
||
case 'final-preview': {
|
||
const observation = latestToolObservation(context);
|
||
if (
|
||
observation?.tool !== 'command.run_limited' ||
|
||
observation?.status !== 'ok'
|
||
) {
|
||
throw providerError(
|
||
'provider-final-static-smoke-observation-invalid',
|
||
);
|
||
}
|
||
if (!tools.has(runtimeFunction('preview.validate'))) {
|
||
throw providerError('provider-final-preview-tool-missing');
|
||
}
|
||
parentStage = 'final-action-history';
|
||
stats.previewValidationCount += 1;
|
||
return callsResponse('project-supervisor', tools, [
|
||
previewCall('对最终 current revision 重跑完整真实试玩和双视口检查'),
|
||
]);
|
||
}
|
||
case 'final-action-history': {
|
||
const observation = latestToolObservation(context);
|
||
if (
|
||
observation?.tool !== 'preview.validate' ||
|
||
observation?.status !== 'ok'
|
||
) {
|
||
throw providerError('provider-final-preview-observation-invalid');
|
||
}
|
||
if (!tools.has(runtimeFunction('agent.action_history'))) {
|
||
throw providerError('provider-final-action-history-tool-missing');
|
||
}
|
||
parentStage = 'final-acceptance-update';
|
||
return callsResponse('project-supervisor', tools, [
|
||
actionHistoryCall(),
|
||
]);
|
||
}
|
||
case 'final-acceptance-update':
|
||
if (!tools.has(runtimeFunction('agent.acceptance_update'))) {
|
||
throw providerError('provider-final-acceptance-update-tool-missing');
|
||
}
|
||
{
|
||
const call = acceptanceUpdateCall(
|
||
context,
|
||
extractIdentity(context).runId,
|
||
);
|
||
parentStage = 'final-respond';
|
||
return callsResponse('project-supervisor', tools, [call]);
|
||
}
|
||
case 'final-respond': {
|
||
const observation = latestToolObservation(context);
|
||
if (
|
||
observation?.tool !== 'agent.acceptance_update' ||
|
||
observation?.status !== 'ok'
|
||
) {
|
||
throw providerError('provider-final-acceptance-observation-invalid');
|
||
}
|
||
if (!tools.has('respond_to_user')) {
|
||
throw providerError('provider-final-respond-tool-missing');
|
||
}
|
||
parentStage = 'done';
|
||
return callsResponse('project-supervisor', tools, [
|
||
nativeResponse(
|
||
'可试玩塔防版本已经完成,并通过当前 revision 静态检查、完整交互试玩以及桌面和移动视口验证。',
|
||
),
|
||
]);
|
||
}
|
||
case 'done':
|
||
throw providerError('provider-parent-terminal-duplicate');
|
||
case 'goal-contract':
|
||
throw providerError('provider-goal-contract-required-first');
|
||
default:
|
||
throw providerError(`provider-parent-stage-invalid:${parentStage}`);
|
||
}
|
||
}
|
||
|
||
function readOnlyReadyCalls(agentId, runId, tools, context) {
|
||
const phase = ensureReadyTaskRun(runId, agentId);
|
||
const taskListCall = nativeAction(
|
||
'task.list',
|
||
'读取 manifest 任务图并核对专业边界',
|
||
{},
|
||
);
|
||
const batches = {
|
||
'design-director': [[taskListCall]],
|
||
'balance-director': [
|
||
[
|
||
fileReadCall('game/game_design.md', '读取玩法规格以确定数值口径'),
|
||
fileReadCall('memory/project.md', '读取长期项目约束'),
|
||
],
|
||
],
|
||
'art-polish': [
|
||
[
|
||
fileReadCall(
|
||
'assets/manifest.art.json',
|
||
'检查美术清单是否覆盖首版范围',
|
||
),
|
||
assetListCall('核对清单与真实项目资产'),
|
||
],
|
||
],
|
||
'audio-director': [
|
||
[fileReadCall('game/game_design.md', '读取玩法规格以确定声音方向')],
|
||
],
|
||
'code-director': [
|
||
[
|
||
fileReadCall('game/game_design.md', '读取玩法与界面规格'),
|
||
fileReadCall('game/balance.json', '读取程序必须消费的数值'),
|
||
fileReadCall('assets/manifest.art.json', '读取美术资产范围'),
|
||
],
|
||
[
|
||
fileReadCall('assets/manifest.audio.json', '读取声音事件范围'),
|
||
fileReadCall('game/index.html', '核对当前程序入口与整合边界'),
|
||
],
|
||
],
|
||
'publish-strategy': [
|
||
[
|
||
fileReadCall('game/game_design.md', '读取玩法卖点与操作说明'),
|
||
fileReadCall('game/index.html', '核对最终可玩入口'),
|
||
fileReadCall('assets/manifest.art.json', '核对发布素材范围'),
|
||
],
|
||
],
|
||
};
|
||
const agentBatches = batches[agentId];
|
||
if (!agentBatches) {
|
||
throw providerError(`provider-read-only-agent-unsupported:${agentId}`);
|
||
}
|
||
if (phase < agentBatches.length) {
|
||
const calls = agentBatches[phase];
|
||
stats.manifestReadyTaskFileReadCount += calls.filter(
|
||
(call) => call.name === runtimeFunction('file.read'),
|
||
).length;
|
||
return readyCallsResponse(agentId, runId, tools, calls);
|
||
}
|
||
if (phase === agentBatches.length) {
|
||
return readyTaskCompleteResponse(agentId, runId, tools, context);
|
||
}
|
||
if (phase >= agentBatches.length + 1) {
|
||
return readyTaskCompleteResponse(agentId, runId, tools, context);
|
||
}
|
||
throw providerError(
|
||
`provider-ready-read-only-stage-invalid:${agentId}:${phase}`,
|
||
);
|
||
}
|
||
|
||
function artDirectorReadyCalls(runId, tools, context) {
|
||
const agentId = 'art-director';
|
||
const phase = ensureReadyTaskRun(runId, agentId);
|
||
if (phase === 0) {
|
||
return readyCallsResponse(agentId, runId, tools, [
|
||
assetListCall('核对当前已有美术资产'),
|
||
]);
|
||
}
|
||
if (phase === 1) {
|
||
if (!tools.has(runtimeFunction('canvas.asset_generate'))) {
|
||
throw providerError('provider-ready-art-director-canvas-tool-missing');
|
||
}
|
||
stats.manifestReadyTaskCanvasGenerationCount += 1;
|
||
return readyCallsResponse(agentId, runId, tools, [
|
||
canvasAssetCall(agentId),
|
||
]);
|
||
}
|
||
if (phase === 2) {
|
||
if (
|
||
tools.has('respond_to_user') &&
|
||
!tools.has(runtimeFunction('asset.list'))
|
||
) {
|
||
return readyTaskCompleteResponse(agentId, runId, tools, context);
|
||
}
|
||
return readyCallsResponse(agentId, runId, tools, [
|
||
assetListCall('核对统一视觉规范图已经生成并登记'),
|
||
]);
|
||
}
|
||
if (phase === 3) {
|
||
return readyTaskCompleteResponse(agentId, runId, tools, context);
|
||
}
|
||
if (phase >= 4) {
|
||
return readyTaskCompleteResponse(agentId, runId, tools, context);
|
||
}
|
||
throw providerError(`provider-ready-art-director-stage-invalid:${phase}`);
|
||
}
|
||
|
||
function designFoundationReadyCalls(runId, tools, context) {
|
||
const agentId = 'design-foundation';
|
||
const phase = ensureReadyTaskRun(runId, agentId);
|
||
const hasCanvas = tools.has(runtimeFunction('canvas.asset_generate'));
|
||
if (phase === 0) {
|
||
stats.sourceWriteCount += 1;
|
||
stats.manifestReadyTaskFileWriteCount += 1;
|
||
return readyCallsResponse(agentId, runId, tools, [
|
||
fileWriteCall(
|
||
'memory/project.md',
|
||
deterministicProjectMemory,
|
||
'写入稳定项目目标与长期约束',
|
||
),
|
||
]);
|
||
}
|
||
if (phase === 1) {
|
||
stats.sourceWriteCount += 1;
|
||
stats.manifestReadyTaskFileWriteCount += 1;
|
||
return readyCallsResponse(agentId, runId, tools, [
|
||
fileWriteCall(
|
||
'game/game_design.md',
|
||
deterministicGameDesign,
|
||
'写入核心循环、胜负条件、关卡与界面规格',
|
||
),
|
||
]);
|
||
}
|
||
if (phase === 2) {
|
||
if (hasCanvas) {
|
||
stats.manifestReadyTaskCanvasGenerationCount += 1;
|
||
runData.set(runId, { hasCanvas: true });
|
||
return readyCallsResponse(agentId, runId, tools, [
|
||
canvasAssetCall(agentId),
|
||
]);
|
||
}
|
||
const calls = [
|
||
fileReadCall('memory/project.md', '回读项目记忆产物'),
|
||
fileReadCall('game/game_design.md', '回读玩法规格产物'),
|
||
staticSmokeCall('验证设计产物写入后的当前 revision'),
|
||
];
|
||
stats.manifestReadyTaskFileReadCount += 2;
|
||
stats.staticSmokeCount += 1;
|
||
stats.manifestReadyTaskStaticSmokeCount += 1;
|
||
runData.set(runId, { hasCanvas: false });
|
||
return readyCallsResponse(agentId, runId, tools, calls);
|
||
}
|
||
if (phase === 3) {
|
||
const data = runData.get(runId) ?? { hasCanvas: false };
|
||
if (data.hasCanvas) {
|
||
stats.manifestReadyTaskFileReadCount += 2;
|
||
return readyCallsResponse(agentId, runId, tools, [
|
||
imageInspectCall(),
|
||
fileReadCall('memory/project.md', '回读项目记忆产物'),
|
||
fileReadCall('game/game_design.md', '回读玩法规格产物'),
|
||
]);
|
||
}
|
||
return readyTaskCompleteResponse(agentId, runId, tools, context);
|
||
}
|
||
if (phase === 4) {
|
||
const data = runData.get(runId) ?? { hasCanvas: false };
|
||
if (data.hasCanvas) {
|
||
return readyTaskCompleteResponse(agentId, runId, tools, context);
|
||
}
|
||
return readyTaskCompleteResponse(agentId, runId, tools, context);
|
||
}
|
||
if (phase >= 5) {
|
||
return readyTaskCompleteResponse(agentId, runId, tools, context);
|
||
}
|
||
throw providerError(`provider-ready-design-stage-invalid:${phase}`);
|
||
}
|
||
|
||
function verifiedFileWriterReadyCalls(
|
||
agentId,
|
||
runId,
|
||
tools,
|
||
context,
|
||
{ path, content, writeReason },
|
||
) {
|
||
const phase = ensureReadyTaskRun(runId, agentId);
|
||
const hasManualVerification = tools.has(
|
||
runtimeFunction('command.run_limited'),
|
||
);
|
||
const latestObservation = latestToolObservation(context);
|
||
const latestWriteBlocked =
|
||
latestObservation?.tool === 'file.write' &&
|
||
['blocked', 'failed'].includes(latestObservation?.status);
|
||
if (latestWriteBlocked && tools.has(runtimeFunction('file.write'))) {
|
||
stats.sourceWriteCount += 1;
|
||
stats.manifestReadyTaskFileWriteCount += 1;
|
||
return readyCallsResponse(agentId, runId, tools, [
|
||
fileWriteCall(
|
||
path,
|
||
content,
|
||
`重试写入 ${path} 并交给 Runtime 收束门验证`,
|
||
),
|
||
]);
|
||
}
|
||
// Runtime validates fixed owner artifacts after the owner responds. Once
|
||
// the write itself is accepted, finish the task directly when the manual
|
||
// verification tool is absent. This branch is intentionally limited to
|
||
// the four owner-artifact agents above; code-prototype and publish-package
|
||
// still require their explicit smoke contracts.
|
||
if (
|
||
deterministicOwnerArtifactValidationAgentIds.has(agentId) &&
|
||
!hasManualVerification &&
|
||
latestObservation?.tool === 'file.write' &&
|
||
latestObservation.status === 'ok'
|
||
) {
|
||
const calls = readyTaskFinalizationCalls(context, agentId, tools);
|
||
if (calls.some((call) => call.name === 'respond_to_user')) {
|
||
recordReadyTaskCompletion(agentId, runId);
|
||
}
|
||
return readyCallsResponse(agentId, runId, tools, calls);
|
||
}
|
||
const recovery = runData.get(runId)?.writerRecovery ?? null;
|
||
const observations = observationContext(context);
|
||
if (recovery === 'after-write') {
|
||
const writeOkIndex = observations.lastIndexOf(
|
||
`file.write:ok · 已写入 ${path}`,
|
||
);
|
||
const writeBlockedIndex = Math.max(
|
||
observations.lastIndexOf('file.write:blocked'),
|
||
observations.lastIndexOf('file.write:failed'),
|
||
);
|
||
if (writeBlockedIndex > writeOkIndex) {
|
||
stats.sourceWriteCount += 1;
|
||
stats.manifestReadyTaskFileWriteCount += 1;
|
||
return readyCallsResponse(agentId, runId, tools, [
|
||
fileWriteCall(path, content, `再次修复并写入 ${path}`),
|
||
]);
|
||
}
|
||
runData.set(runId, { writerRecovery: 'after-smoke' });
|
||
if (completedReadyTaskRuns.has(runId)) {
|
||
readyTaskCompletionRetryArmedRuns.add(runId);
|
||
}
|
||
if (!hasManualVerification) {
|
||
runData.delete(runId);
|
||
return readyTaskCompleteResponse(agentId, runId, tools, context);
|
||
}
|
||
stats.staticSmokeCount += 1;
|
||
stats.manifestReadyTaskStaticSmokeCount += 1;
|
||
return readyCallsResponse(agentId, runId, tools, [
|
||
staticSmokeCall(`验证修复写入 ${path} 后的当前 revision`),
|
||
]);
|
||
}
|
||
if (recovery === 'after-smoke') {
|
||
const smokeOkIndex = observations.lastIndexOf(
|
||
'command.run_limited:ok · game.static_smoke 已完成',
|
||
);
|
||
const smokeBlockedIndex = Math.max(
|
||
observations.lastIndexOf('command.run_limited:blocked'),
|
||
observations.lastIndexOf('command.run_limited:failed'),
|
||
);
|
||
if (smokeBlockedIndex > smokeOkIndex) {
|
||
stats.staticSmokeCount += 1;
|
||
stats.manifestReadyTaskStaticSmokeCount += 1;
|
||
return readyCallsResponse(agentId, runId, tools, [
|
||
staticSmokeCall(`重新验证修复写入 ${path} 后的当前 revision`),
|
||
]);
|
||
}
|
||
runData.delete(runId);
|
||
return readyTaskCompleteResponse(agentId, runId, tools, context);
|
||
}
|
||
if (phase === 0) {
|
||
stats.sourceWriteCount += 1;
|
||
stats.manifestReadyTaskFileWriteCount += 1;
|
||
return readyCallsResponse(agentId, runId, tools, [
|
||
fileWriteCall(path, content, writeReason),
|
||
]);
|
||
}
|
||
if (phase === 1) {
|
||
stats.manifestReadyTaskFileReadCount += 1;
|
||
const calls = [fileReadCall(path, `回读并核对 ${path}`)];
|
||
if (hasManualVerification) {
|
||
stats.staticSmokeCount += 1;
|
||
stats.manifestReadyTaskStaticSmokeCount += 1;
|
||
calls.push(staticSmokeCall(`验证 ${path} 写入后的当前 revision`));
|
||
}
|
||
return readyCallsResponse(agentId, runId, tools, calls);
|
||
}
|
||
if (phase === 2) {
|
||
return readyTaskCompleteResponse(agentId, runId, tools, context);
|
||
}
|
||
if (phase >= 3) {
|
||
if (
|
||
!tools.has('respond_to_user') &&
|
||
!tools.has(runtimeFunction('command.run_limited')) &&
|
||
tools.has(runtimeFunction('file.write'))
|
||
) {
|
||
stats.sourceWriteCount += 1;
|
||
stats.manifestReadyTaskFileWriteCount += 1;
|
||
runData.set(runId, { writerRecovery: 'after-write' });
|
||
return readyCallsResponse(agentId, runId, tools, [
|
||
fileWriteCall(path, content, `修复并重新写入 ${path}`),
|
||
]);
|
||
}
|
||
return readyTaskCompleteResponse(agentId, runId, tools, context);
|
||
}
|
||
throw providerError(
|
||
`provider-ready-writer-stage-invalid:${agentId}:${phase}`,
|
||
);
|
||
}
|
||
|
||
function artAssetReadyCalls(runId, tools, context) {
|
||
const agentId = 'art-asset-plan';
|
||
const phase = ensureReadyTaskRun(runId, agentId);
|
||
if (phase === 0) {
|
||
return readyCallsResponse(agentId, runId, tools, [
|
||
assetListCall('先核对首版核心素材是否已经生成和登记'),
|
||
]);
|
||
}
|
||
if (phase === 1) {
|
||
const existing = observationsIncludeAsset(
|
||
context,
|
||
'assets/art-spritesheet.png',
|
||
);
|
||
const hasCanvas = tools.has(runtimeFunction('canvas.asset_generate'));
|
||
const willGenerate = hasCanvas && !existing;
|
||
const hasVisualAsset = existing || willGenerate;
|
||
stats.sourceWriteCount += 1;
|
||
stats.manifestReadyTaskFileWriteCount += 1;
|
||
runData.set(runId, { existing, hasCanvas, willGenerate });
|
||
return readyCallsResponse(agentId, runId, tools, [
|
||
fileWriteCall(
|
||
'assets/manifest.art.json',
|
||
`${JSON.stringify(deterministicArtManifest(hasVisualAsset, 'manifest-ready'), null, 2)}\n`,
|
||
'写入可解析的首版美术资产清单',
|
||
),
|
||
]);
|
||
}
|
||
if (phase === 2) {
|
||
const data = runData.get(runId) ?? {
|
||
existing: false,
|
||
hasCanvas: false,
|
||
willGenerate: false,
|
||
};
|
||
if (data.willGenerate) {
|
||
stats.manifestReadyTaskCanvasGenerationCount += 1;
|
||
return readyCallsResponse(agentId, runId, tools, [
|
||
canvasAssetCall(agentId),
|
||
]);
|
||
}
|
||
const calls = [
|
||
fileReadCall('assets/manifest.art.json', '回读并核对美术清单 JSON'),
|
||
];
|
||
stats.manifestReadyTaskFileReadCount += 1;
|
||
if (data.existing) {
|
||
calls.push(assetListCall('核对新图片已登记到项目资产清单'));
|
||
} else {
|
||
calls.push(staticSmokeCall('验证美术清单写入后的当前 revision'));
|
||
stats.staticSmokeCount += 1;
|
||
stats.manifestReadyTaskStaticSmokeCount += 1;
|
||
}
|
||
return readyCallsResponse(agentId, runId, tools, calls);
|
||
}
|
||
if (phase >= 3) {
|
||
if (
|
||
phase >= 5 &&
|
||
!tools.has('respond_to_user') &&
|
||
!tools.has(runtimeFunction('command.run_limited')) &&
|
||
tools.has(runtimeFunction('file.write'))
|
||
) {
|
||
stats.sourceWriteCount += 1;
|
||
stats.manifestReadyTaskFileWriteCount += 1;
|
||
return readyCallsResponse(agentId, runId, tools, [
|
||
fileWriteCall(
|
||
'assets/manifest.art.json',
|
||
`${JSON.stringify(deterministicArtManifest(true, 'manifest-ready-recovery'), null, 2)}\n`,
|
||
'补写可验证的正式美术任务产物',
|
||
),
|
||
]);
|
||
}
|
||
if (completedReadyTaskRuns.has(runId)) {
|
||
return readyTaskCompleteResponse(agentId, runId, tools, context);
|
||
}
|
||
const data = runData.get(runId) ?? {
|
||
existing: false,
|
||
hasCanvas: false,
|
||
willGenerate: false,
|
||
};
|
||
if (
|
||
data.willGenerate &&
|
||
projectRevisionDriftObservation(context, 'canvas.asset_generate')
|
||
) {
|
||
if (!tools.has(runtimeFunction('canvas.asset_generate'))) {
|
||
throw providerError('provider-ready-art-canvas-tool-missing');
|
||
}
|
||
const retryCount = readyTaskPreCompletionRetryCounts.get(runId) ?? 0;
|
||
if (retryCount >= 16) {
|
||
throw providerError('provider-ready-art-canvas-retry-exhausted');
|
||
}
|
||
readyTaskPreCompletionRetryCounts.set(runId, retryCount + 1);
|
||
stats.manifestReadyTaskCanvasGenerationCount += 1;
|
||
return readyCallsResponse(agentId, runId, tools, [
|
||
canvasAssetCall(agentId),
|
||
]);
|
||
}
|
||
if (data.willGenerate) {
|
||
const observation = latestToolObservation(context);
|
||
if (
|
||
observation?.tool === 'canvas.asset_generate' &&
|
||
observation?.status === 'ok'
|
||
) {
|
||
stats.manifestReadyTaskFileReadCount += 1;
|
||
return readyCallsResponse(agentId, runId, tools, [
|
||
fileReadCall('assets/manifest.art.json', '回读并核对美术清单 JSON'),
|
||
assetListCall('核对新图片已登记到项目资产清单'),
|
||
]);
|
||
}
|
||
if (phase === 3) {
|
||
stats.manifestReadyTaskFileReadCount += 1;
|
||
return readyCallsResponse(agentId, runId, tools, [
|
||
fileReadCall('assets/manifest.art.json', '回读并核对美术清单 JSON'),
|
||
assetListCall('核对新图片已登记到项目资产清单'),
|
||
]);
|
||
}
|
||
}
|
||
return readyTaskCompleteResponse(agentId, runId, tools, context);
|
||
}
|
||
throw providerError(`provider-ready-art-stage-invalid:${phase}`);
|
||
}
|
||
|
||
function canonicalCodeReadyCalls(runId, tools, context) {
|
||
const agentId = 'code-prototype';
|
||
const phase = ensureReadyTaskRun(runId, agentId);
|
||
if (phase === 0) {
|
||
stats.canonicalCodeRunCount += 1;
|
||
stats.manifestReadyTaskFileReadCount += 3;
|
||
return readyCallsResponse(agentId, runId, tools, [
|
||
fileReadCall('memory/project.md', '读取项目长期约束'),
|
||
fileReadCall('game/game_design.md', '读取玩法与界面规格'),
|
||
fileReadCall('game/balance.json', '读取可执行数值参数'),
|
||
]);
|
||
}
|
||
if (phase === 1) {
|
||
stats.manifestReadyTaskFileReadCount += 3;
|
||
return readyCallsResponse(agentId, runId, tools, [
|
||
fileReadCall('assets/manifest.art.json', '读取美术资产清单'),
|
||
fileReadCall('assets/manifest.audio.json', '读取声音事件清单'),
|
||
fileReadCall('game/index.html', '读取当前入口以保留既有试玩行为'),
|
||
assetListCall('核对 Canvas 登记、图集与四类切片的权威资产身份'),
|
||
]);
|
||
}
|
||
if (phase === 2) {
|
||
stats.sourceWriteCount += 1;
|
||
stats.manifestReadyTaskFileWriteCount += 1;
|
||
return readyCallsResponse(agentId, runId, tools, [
|
||
fileWriteCall(
|
||
'game/index.html',
|
||
deterministicLaneDefenseCanonicalHtml(),
|
||
'整合设计、数值、美术和声音合同并重写 canonical 游戏入口',
|
||
),
|
||
]);
|
||
}
|
||
if (phase === 3) {
|
||
stats.staticSmokeCount += 1;
|
||
stats.manifestReadyTaskStaticSmokeCount += 1;
|
||
return readyCallsResponse(agentId, runId, tools, [
|
||
staticSmokeCall('验证整合上游后的 canonical 游戏入口'),
|
||
]);
|
||
}
|
||
if (phase === 4) {
|
||
return readyTaskCompleteResponse(agentId, runId, tools, context);
|
||
}
|
||
if (phase >= 5) {
|
||
return readyTaskCompleteResponse(agentId, runId, tools, context);
|
||
}
|
||
throw providerError(`provider-ready-canonical-code-stage-invalid:${phase}`);
|
||
}
|
||
|
||
function qualityReadyCalls(runId, tools, context) {
|
||
const agentId = 'quality-review';
|
||
const phase = ensureReadyTaskRun(runId, agentId);
|
||
if (phase === 0) {
|
||
stats.manifestReadyTaskFileReadCount += 3;
|
||
return readyCallsResponse(agentId, runId, tools, [
|
||
fileReadCall('game/index.html', '只读检查最终可玩入口'),
|
||
fileReadCall('game/balance.json', '只读检查数值合同'),
|
||
fileReadCall('assets/manifest.art.json', '只读检查美术合同'),
|
||
]);
|
||
}
|
||
if (phase === 1) {
|
||
stats.manifestReadyTaskFileReadCount += 2;
|
||
return readyCallsResponse(agentId, runId, tools, [
|
||
fileReadCall('assets/manifest.audio.json', '只读检查声音合同'),
|
||
fileReadCall('game/game_design.md', '只读检查玩法规格覆盖'),
|
||
]);
|
||
}
|
||
if (phase === 2) {
|
||
return readyTaskCompleteResponse(agentId, runId, tools, context);
|
||
}
|
||
if (phase >= 3) {
|
||
return readyTaskCompleteResponse(agentId, runId, tools, context);
|
||
}
|
||
throw providerError(`provider-ready-quality-stage-invalid:${phase}`);
|
||
}
|
||
|
||
function previewReadyCalls(agentId, runId, tools, context) {
|
||
const phase = ensureReadyTaskRun(runId, agentId);
|
||
if (phase === 0) {
|
||
if (agentId === 'preview-readiness') {
|
||
stats.staticSmokeCount += 1;
|
||
stats.manifestReadyTaskStaticSmokeCount += 1;
|
||
return readyCallsResponse(agentId, runId, tools, [
|
||
staticSmokeCall('执行 canonical 游戏入口静态自检'),
|
||
]);
|
||
}
|
||
stats.previewValidationCount += 1;
|
||
stats.manifestReadyTaskPreviewValidationCount += 1;
|
||
return readyCallsResponse(agentId, runId, tools, [
|
||
previewCall('执行 canonical 游戏入口真实双视口试玩'),
|
||
]);
|
||
}
|
||
if (phase === 1) {
|
||
return readyTaskCompleteResponse(agentId, runId, tools, context);
|
||
}
|
||
if (phase >= 2) {
|
||
return readyTaskCompleteResponse(agentId, runId, tools, context);
|
||
}
|
||
throw providerError(
|
||
`provider-ready-preview-stage-invalid:${agentId}:${phase}`,
|
||
);
|
||
}
|
||
|
||
function relaxedManifestReadyCalls(agentId, runId, tools, context) {
|
||
const phase = ensureReadyTaskRun(runId, agentId);
|
||
const writeSpecs = {
|
||
'code-prototype': {
|
||
path: 'game/index.html',
|
||
content: deterministicLaneDefenseCanonicalHtml(),
|
||
reason: '写入可运行的游戏入口',
|
||
},
|
||
'design-foundation': {
|
||
path: 'memory/project.md',
|
||
content: deterministicProjectMemory,
|
||
reason: '写入项目基础说明',
|
||
},
|
||
'balance-seed': {
|
||
path: 'game/balance.json',
|
||
content: `${JSON.stringify(deterministicLaneDefenseBalance, null, 2)}\n`,
|
||
reason: '写入初版数值',
|
||
},
|
||
'art-asset-plan': {
|
||
path: 'assets/manifest.art.json',
|
||
content: `${JSON.stringify(deterministicArtManifest(false, 'relaxed'), null, 2)}\n`,
|
||
reason: '写入美术清单(平台素材可后续补齐)',
|
||
},
|
||
'audio-asset-plan': {
|
||
path: 'assets/manifest.audio.json',
|
||
content: `${JSON.stringify(deterministicAudioManifest, null, 2)}\n`,
|
||
reason: '写入声音清单',
|
||
},
|
||
'publish-package': {
|
||
path: 'exports/README.md',
|
||
content: deterministicPublishReadme,
|
||
reason: '写入发布说明',
|
||
},
|
||
};
|
||
const spec = writeSpecs[agentId];
|
||
if (phase === 0 && spec && tools.has(runtimeFunction('file.write'))) {
|
||
stats.sourceWriteCount += 1;
|
||
stats.manifestReadyTaskFileWriteCount += 1;
|
||
return readyCallsResponse(agentId, runId, tools, [
|
||
fileWriteCall(spec.path, spec.content, spec.reason),
|
||
]);
|
||
}
|
||
return readyTaskCompleteResponse(agentId, runId, tools, context);
|
||
}
|
||
|
||
function manifestReadyCalls(identity, tools, context) {
|
||
const { agentId, runId } = identity;
|
||
if (relaxedAutonomous) {
|
||
return relaxedManifestReadyCalls(agentId, runId, tools, context);
|
||
}
|
||
if (
|
||
[
|
||
'design-director',
|
||
'balance-director',
|
||
'art-polish',
|
||
'audio-director',
|
||
'code-director',
|
||
'publish-strategy',
|
||
].includes(agentId)
|
||
) {
|
||
return readOnlyReadyCalls(agentId, runId, tools, context);
|
||
}
|
||
if (agentId === 'art-director') {
|
||
return artDirectorReadyCalls(runId, tools, context);
|
||
}
|
||
if (agentId === 'design-foundation') {
|
||
return designFoundationReadyCalls(runId, tools, context);
|
||
}
|
||
if (agentId === 'balance-seed') {
|
||
return verifiedFileWriterReadyCalls(agentId, runId, tools, context, {
|
||
path: 'game/balance.json',
|
||
content: `${JSON.stringify(deterministicLaneDefenseBalance, null, 2)}\n`,
|
||
writeReason: '写入可被程序读取的初版数值参数',
|
||
});
|
||
}
|
||
if (agentId === 'art-asset-plan') {
|
||
return artAssetReadyCalls(runId, tools, context);
|
||
}
|
||
if (agentId === 'audio-asset-plan') {
|
||
return verifiedFileWriterReadyCalls(agentId, runId, tools, context, {
|
||
path: 'assets/manifest.audio.json',
|
||
content: `${JSON.stringify(deterministicAudioManifest, null, 2)}\n`,
|
||
writeReason: '写入 BGM 与关键交互音效需求清单',
|
||
});
|
||
}
|
||
if (agentId === 'code-prototype') {
|
||
return canonicalCodeReadyCalls(runId, tools, context);
|
||
}
|
||
if (agentId === 'quality-review') {
|
||
return qualityReadyCalls(runId, tools, context);
|
||
}
|
||
if (agentId === 'preview-readiness' || agentId === 'preview-playtest') {
|
||
return previewReadyCalls(agentId, runId, tools, context);
|
||
}
|
||
if (agentId === 'publish-package') {
|
||
return verifiedFileWriterReadyCalls(agentId, runId, tools, context, {
|
||
path: 'exports/README.md',
|
||
content: deterministicPublishReadme,
|
||
writeReason: '写入发布标题、简介、操作、标签与导出检查',
|
||
});
|
||
}
|
||
throw providerError(`provider-ready-agent-unsupported:${agentId}`);
|
||
}
|
||
|
||
function finalReply(identity, context) {
|
||
const key = `${identity.agentId}\0${identity.runId}`;
|
||
if (finalReplyRuns.has(key)) {
|
||
throw providerError('provider-duplicate-final-reply-request');
|
||
}
|
||
if (
|
||
!relaxedAutonomous &&
|
||
!context.includes('给用户一个正常中文回复') &&
|
||
!context.includes('给开发者一个正常中文回复')
|
||
) {
|
||
throw providerError('provider-unexpected-text-request');
|
||
}
|
||
if (
|
||
!relaxedAutonomous &&
|
||
identity.agentId === 'project-supervisor' &&
|
||
parentStage !== 'done'
|
||
) {
|
||
throw providerError('provider-parent-final-reply-before-acceptance');
|
||
}
|
||
finalReplyRuns.add(key);
|
||
const content =
|
||
identity.agentId === 'project-supervisor'
|
||
? '项目已经生成并完成真实试玩验证:植物可以选择和放置,敌人会移动并受伤,关卡可以胜利、进入下一关并重新开始。'
|
||
: identity.agentId === 'quality-review'
|
||
? '已完成独立验收重点核对,后续以真实交互和双视口浏览器结果作为最终依据。'
|
||
: '专业实现任务已经完成,修改和静态验证结果已交回总控。';
|
||
responseSequence += 1;
|
||
return chatTextResponse(responseSequence, model, content);
|
||
}
|
||
|
||
function route({ authorization, payload }) {
|
||
stats.requestCount += 1;
|
||
let diagnostic = {
|
||
agentId: 'unknown',
|
||
tools: [],
|
||
delegatedPlaytestRepair: false,
|
||
responsePlanRepair: false,
|
||
verifiedDeliveryRepair: false,
|
||
readOnlyDeliveryRepair: false,
|
||
genericFormatRepair: false,
|
||
};
|
||
try {
|
||
if (authorization !== `Bearer ${apiKey}`) {
|
||
throw providerError('provider-authorization-invalid');
|
||
}
|
||
if (
|
||
!isPlainObject(payload) ||
|
||
payload.model !== model ||
|
||
payload.stream === true
|
||
) {
|
||
throw providerError('provider-request-shape-invalid');
|
||
}
|
||
const context = requestContext(payload);
|
||
const tools = advertisedToolNames(payload);
|
||
if (
|
||
context.includes('统一的 Agent interaction loop') &&
|
||
context.includes('用户这轮输入:') &&
|
||
tools.has('runtime_execute')
|
||
) {
|
||
if (payload.reasoning_effort !== 'max') {
|
||
throw providerError('provider-interaction-reasoning-effort-invalid');
|
||
}
|
||
if (stats.interactionExecuteCount !== 0) {
|
||
throw providerError('provider-interaction-execute-duplicate');
|
||
}
|
||
if (
|
||
stats.manifestReadyTaskRunCount !== 0 ||
|
||
stats.manifestReadyTaskCompletionCount !== 0
|
||
) {
|
||
throw providerError('provider-interaction-execute-out-of-order');
|
||
}
|
||
stats.planningRequestCount += 1;
|
||
stats.interactionExecuteCount += 1;
|
||
recordAgent('project-supervisor', 'planning');
|
||
return callsResponse('project-supervisor', tools, [
|
||
{ name: 'runtime_execute', arguments: {} },
|
||
]);
|
||
}
|
||
if (isUiPrototypeInspectionRequest(payload, context)) {
|
||
stats.imageInspectionRequestCount += 1;
|
||
responseSequence += 1;
|
||
return chatTextResponse(
|
||
responseSequence,
|
||
model,
|
||
JSON.stringify({
|
||
checks: {
|
||
informationHud: true,
|
||
gameplaySurface: true,
|
||
objectiveEntities: true,
|
||
primaryControls: true,
|
||
failureRestartFlow: true,
|
||
responsiveLayout: true,
|
||
implementationClarity: true,
|
||
originalTheme: true,
|
||
},
|
||
issues: [],
|
||
summary: '确定性 UI 原型覆盖完整玩法界面实现合同。',
|
||
}),
|
||
);
|
||
}
|
||
if (context.includes('你负责压缩 Agent 的旧历史')) {
|
||
stats.contextCompactionRequestCount += 1;
|
||
responseSequence += 1;
|
||
return chatTextResponse(
|
||
responseSequence,
|
||
model,
|
||
'用户要求生成可直接试玩的植物塔防游戏。静态专业委派已经完成;正式 manifest 任务图正在按依赖执行,完成后仍需运行最终静态检查和双视口交互试玩。',
|
||
);
|
||
}
|
||
const identity = extractIdentity(context);
|
||
const latestObservation = latestToolObservation(context);
|
||
diagnostic = {
|
||
agentId: identity.agentId,
|
||
tools: [...tools].sort(),
|
||
latestObservationTool: latestObservation?.tool ?? null,
|
||
latestObservationStatus: latestObservation?.status ?? null,
|
||
revisionBlockedObservation: revisionBlockedObservation(context),
|
||
revisionBlockedContext:
|
||
context.includes('responseRevision=') &&
|
||
context.includes('currentRevision='),
|
||
specialistVerificationRepair:
|
||
specialistVerificationRepairContext(context),
|
||
delegatedPlaytestRepair:
|
||
context.includes('当前父 run 已进入只编排模式') ||
|
||
context.includes('后续修复委派'),
|
||
responsePlanRepair:
|
||
context.includes('只保留 update_agent_plan 与 respond_to_user') ||
|
||
context.includes('结构化计划仍未完成'),
|
||
verifiedDeliveryRepair:
|
||
context.includes('当前 revision 已通过验证') &&
|
||
context.includes('只保留 respond_to_user'),
|
||
readOnlyDeliveryRepair:
|
||
context.includes('当前专业合同明确要求只读交付') ||
|
||
context.includes('不允许修改项目'),
|
||
genericFormatRepair: context.includes('请修复格式'),
|
||
};
|
||
if (tools.size === 0) {
|
||
stats.finalReplyRequestCount += 1;
|
||
recordAgent(identity.agentId, 'finalReply');
|
||
return finalReply(identity, context);
|
||
}
|
||
stats.planningRequestCount += 1;
|
||
recordAgent(identity.agentId, 'planning');
|
||
if (readyTaskContext(context, identity.agentId)) {
|
||
return manifestReadyCalls(identity, tools, context);
|
||
}
|
||
if (identity.agentId === 'project-supervisor') {
|
||
return parentCalls(context, tools);
|
||
}
|
||
throw providerError(`provider-agent-unsupported:${identity.agentId}`);
|
||
} catch (error) {
|
||
stats.unexpectedRequestCount += 1;
|
||
const code =
|
||
typeof error?.code === 'string'
|
||
? error.code
|
||
: 'provider-request-rejected';
|
||
stats.rejectionCodes[code] = (stats.rejectionCodes[code] ?? 0) + 1;
|
||
if (stats.rejections.length < 8) {
|
||
stats.rejections.push({ ...diagnostic, code });
|
||
}
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
return Object.freeze({
|
||
route,
|
||
getStats() {
|
||
return structuredClone(stats);
|
||
},
|
||
});
|
||
}
|
||
|
||
function listen(server, host, port) {
|
||
return new Promise((resolve, reject) => {
|
||
const onError = (error) => {
|
||
cleanup();
|
||
reject(error);
|
||
};
|
||
const onListening = () => {
|
||
cleanup();
|
||
resolve();
|
||
};
|
||
const cleanup = () => {
|
||
server.off('error', onError);
|
||
server.off('listening', onListening);
|
||
};
|
||
server.once('error', onError);
|
||
server.once('listening', onListening);
|
||
server.listen({ host, port, exclusive: true });
|
||
});
|
||
}
|
||
|
||
async function bindLoopback(server, fallbackPorts) {
|
||
try {
|
||
await listen(server, LOOPBACK_HOST, 0);
|
||
return;
|
||
} catch (error) {
|
||
if (error?.code !== 'EADDRINUSE') throw error;
|
||
}
|
||
for (const port of fallbackPorts) {
|
||
try {
|
||
await listen(server, LOOPBACK_HOST, port);
|
||
return;
|
||
} catch (error) {
|
||
if (error?.code !== 'EADDRINUSE') throw error;
|
||
}
|
||
}
|
||
throw providerError('provider-loopback-port-exhausted');
|
||
}
|
||
|
||
function readJsonBody(request) {
|
||
return new Promise((resolve, reject) => {
|
||
const chunks = [];
|
||
let bytes = 0;
|
||
request.on('data', (chunk) => {
|
||
bytes += chunk.length;
|
||
if (bytes > MAX_REQUEST_BYTES) {
|
||
reject(providerError('provider-request-too-large'));
|
||
request.destroy();
|
||
return;
|
||
}
|
||
chunks.push(chunk);
|
||
});
|
||
request.once('end', () => {
|
||
try {
|
||
resolve(JSON.parse(Buffer.concat(chunks).toString('utf8')));
|
||
} catch {
|
||
reject(providerError('provider-request-json-invalid'));
|
||
}
|
||
});
|
||
request.once('error', () =>
|
||
reject(providerError('provider-request-read-failed')),
|
||
);
|
||
});
|
||
}
|
||
|
||
function sendJson(response, statusCode, body) {
|
||
if (response.destroyed) return;
|
||
const bytes = Buffer.from(JSON.stringify(body));
|
||
sendBytes(response, statusCode, 'application/json; charset=utf-8', bytes);
|
||
}
|
||
|
||
function sendBytes(response, statusCode, contentType, bytes) {
|
||
if (response.destroyed) return;
|
||
response.writeHead(statusCode, {
|
||
connection: 'close',
|
||
'content-length': bytes.length,
|
||
'content-type': contentType,
|
||
'x-request-id': `deterministic-${statusCode}`,
|
||
});
|
||
response.end(bytes);
|
||
}
|
||
|
||
// Debug builds intentionally exercise the first-party account surface. The
|
||
// deterministic fixture itself is written against the canonical External
|
||
// Editor contract so that the same responses cover both build modes. Keep
|
||
// this translation local to the fixture; production routing remains owned by
|
||
// the Rust client and the server's public contracts.
|
||
function canonicalCanvasApiPath(pathname) {
|
||
if (pathname.startsWith('/api/external/v1/')) return pathname;
|
||
if (pathname.startsWith('/api/editor/')) {
|
||
return `/api/external/v1${pathname.slice('/api'.length)}`;
|
||
}
|
||
if (pathname.startsWith('/api/assets/')) {
|
||
return `/api/external/v1${pathname.slice('/api'.length)}`;
|
||
}
|
||
const generationJobsPrefix = '/api/runtime/external-generation/jobs/';
|
||
if (pathname.startsWith(generationJobsPrefix)) {
|
||
return `/api/external/v1/generations/${pathname.slice(generationJobsPrefix.length)}`;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function createDeterministicCanvasFixture(apiKey) {
|
||
const projectId = 'deterministic-canvas-project';
|
||
const folderId = 'deterministic-canvas-folder';
|
||
const images = new Map();
|
||
const generationOperations = new Map();
|
||
const uploadedObjects = new Map();
|
||
const registeredResources = new Map();
|
||
const imageCache = new Map();
|
||
const stats = {
|
||
canvasApiRequestCount: 0,
|
||
canvasGenerationRequestCount: 0,
|
||
canvasDownloadRequestCount: 0,
|
||
canvasSliceDownloadRequestCount: 0,
|
||
canvasGeneratedAspectRatios: {},
|
||
};
|
||
let projectTitle = null;
|
||
let folderLabel = null;
|
||
let generationSequence = 0;
|
||
let uploadSequence = 0;
|
||
|
||
function json(statusCode, body) {
|
||
return {
|
||
statusCode,
|
||
contentType: 'application/json; charset=utf-8',
|
||
bytes: Buffer.from(JSON.stringify(body)),
|
||
};
|
||
}
|
||
|
||
function imageForAspectRatio(aspectRatio) {
|
||
const normalized = aspectRatio === '16:9' ? '16:9' : '1:1';
|
||
if (!imageCache.has(normalized)) {
|
||
imageCache.set(
|
||
normalized,
|
||
normalized === '16:9'
|
||
? {
|
||
width: 1280,
|
||
height: 720,
|
||
bytes: deterministicPng(1280, 720),
|
||
}
|
||
: {
|
||
width: 1024,
|
||
height: 1024,
|
||
bytes: deterministicPng(1024, 1024),
|
||
},
|
||
);
|
||
}
|
||
return { aspectRatio: normalized, ...imageCache.get(normalized) };
|
||
}
|
||
|
||
async function handle(request) {
|
||
const parsed = new URL(request.url ?? '/', 'http://127.0.0.1');
|
||
const isSignedImage = parsed.pathname.startsWith('/signed/');
|
||
const canonicalPath = canonicalCanvasApiPath(parsed.pathname);
|
||
const isCanvasApi = canonicalPath !== null;
|
||
// Direct-upload tickets point back at this loopback server. The fixture
|
||
// does not need to inspect multipart bytes; draining the request and
|
||
// acknowledging the configured success status is sufficient because the
|
||
// subsequent confirm call is the authoritative object registration step.
|
||
const isDirectUpload =
|
||
request.method === 'POST' && parsed.pathname === '/' && !isCanvasApi;
|
||
if (!isSignedImage && !isCanvasApi && !isDirectUpload) return null;
|
||
if (isDirectUpload) {
|
||
request.resume();
|
||
return {
|
||
statusCode: 204,
|
||
contentType: 'text/plain; charset=utf-8',
|
||
bytes: Buffer.alloc(0),
|
||
};
|
||
}
|
||
stats.canvasApiRequestCount += 1;
|
||
if (
|
||
!isSignedImage &&
|
||
request.headers.authorization !== `Bearer ${apiKey}`
|
||
) {
|
||
request.resume();
|
||
return json(401, { error: { message: 'unauthorized' } });
|
||
}
|
||
|
||
if (
|
||
request.method === 'GET' &&
|
||
canonicalPath === '/api/external/v1/editor/projects'
|
||
) {
|
||
request.resume();
|
||
return json(200, {
|
||
data: {
|
||
projects: projectTitle ? [{ projectId, title: projectTitle }] : [],
|
||
},
|
||
});
|
||
}
|
||
if (
|
||
request.method === 'POST' &&
|
||
canonicalPath === '/api/external/v1/editor/projects'
|
||
) {
|
||
const body = await readJsonBody(request);
|
||
projectTitle =
|
||
typeof body?.title === 'string' && body.title.trim()
|
||
? body.title.trim()
|
||
: '未命名游戏原型';
|
||
return json(200, {
|
||
data: { project: { projectId, title: projectTitle } },
|
||
});
|
||
}
|
||
if (
|
||
request.method === 'GET' &&
|
||
canonicalPath === '/api/external/v1/editor/assets/library'
|
||
) {
|
||
request.resume();
|
||
return json(200, {
|
||
data: {
|
||
library: {
|
||
folders: folderLabel ? [{ folderId, label: folderLabel }] : [],
|
||
},
|
||
},
|
||
});
|
||
}
|
||
if (
|
||
request.method === 'POST' &&
|
||
canonicalPath === '/api/external/v1/editor/assets/folders'
|
||
) {
|
||
const body = await readJsonBody(request);
|
||
folderLabel =
|
||
typeof body?.label === 'string' && body.label.trim()
|
||
? body.label.trim()
|
||
: '未命名游戏原型';
|
||
return json(200, { data: { folder: { folderId, label: folderLabel } } });
|
||
}
|
||
if (
|
||
request.method === 'POST' &&
|
||
canonicalPath === '/api/external/v1/editor/icon-spritesheets/generations'
|
||
) {
|
||
const idempotencyKey = request.headers['idempotency-key'];
|
||
if (
|
||
typeof idempotencyKey !== 'string' ||
|
||
!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
|
||
idempotencyKey,
|
||
)
|
||
) {
|
||
request.resume();
|
||
return json(400, { error: { message: 'invalid idempotency key' } });
|
||
}
|
||
const body = await readJsonBody(request);
|
||
const aspectRatio = body?.aspectRatio === '16:9' ? '16:9' : '1:1';
|
||
const width = aspectRatio === '16:9' ? 1280 : 1024;
|
||
const height = aspectRatio === '16:9' ? 720 : 1024;
|
||
generationSequence += 1;
|
||
stats.canvasGenerationRequestCount += 1;
|
||
stats.canvasGeneratedAspectRatios[aspectRatio] =
|
||
(stats.canvasGeneratedAspectRatios[aspectRatio] ?? 0) + 1;
|
||
const imageId = `deterministic-${generationSequence}`;
|
||
const objectKey = `generated/deterministic/${imageId}.png`;
|
||
const assetObjectId = `asset-object-${imageId}`;
|
||
const resourceId = `resource-${imageId}`;
|
||
const taskId = `task-${imageId}`;
|
||
images.set(imageId, {
|
||
aspectRatio,
|
||
width,
|
||
height,
|
||
bytes: deterministicPng(width, height, {
|
||
variant: generationSequence,
|
||
transparent: true,
|
||
}),
|
||
objectKey,
|
||
downloadKind: 'generation',
|
||
});
|
||
const sliceNames = ['玩家主体', '目标与危险物', '场景与资源', '反馈特效'];
|
||
const iconImageSrcs = sliceNames.map((name, index) => {
|
||
const sliceImageId = `${imageId}-slice-${index + 1}`;
|
||
const sliceObjectKey = `generated/deterministic/${sliceImageId}.png`;
|
||
const sliceResourceId = `resource-${sliceImageId}`;
|
||
const sliceAssetObjectId = `asset-object-${sliceImageId}`;
|
||
images.set(sliceImageId, {
|
||
aspectRatio: '1:1',
|
||
width: 256,
|
||
height: 256,
|
||
bytes: deterministicPng(256, 256, {
|
||
variant: generationSequence * 10 + index + 1,
|
||
// The canonical art contract requires every independently
|
||
// usable slice to retain transparent pixels. Keep the fixture
|
||
// faithful to the External Editor response instead of making the
|
||
// runtime relax that final validation.
|
||
transparent: true,
|
||
}),
|
||
objectKey: sliceObjectKey,
|
||
downloadKind: 'slice',
|
||
});
|
||
return {
|
||
name,
|
||
imageSrc: `/${sliceObjectKey}`,
|
||
width: 256,
|
||
height: 256,
|
||
projectId,
|
||
taskId,
|
||
sourceResourceId: resourceId,
|
||
resource: {
|
||
resourceId: sliceResourceId,
|
||
projectId,
|
||
imageSrc: `/${sliceObjectKey}`,
|
||
objectKey: sliceObjectKey,
|
||
assetObjectId: sliceAssetObjectId,
|
||
taskId,
|
||
sourceResourceId: resourceId,
|
||
},
|
||
asset: {
|
||
assetId: `asset-${sliceImageId}`,
|
||
assetObjectId: sliceAssetObjectId,
|
||
assetKind: 'art-spritesheet-slice',
|
||
projectId,
|
||
taskId,
|
||
},
|
||
};
|
||
});
|
||
const operationId = `task-${imageId}`;
|
||
generationOperations.set(operationId, {
|
||
spritesheetImageSrc: `/${objectKey}`,
|
||
objectKey,
|
||
assetObjectId,
|
||
resourceId,
|
||
projectId,
|
||
taskId,
|
||
width,
|
||
height,
|
||
sourceType: 'generated',
|
||
prompt:
|
||
body?.iconDescriptions?.join('\n') ??
|
||
'deterministic spritesheet fixture',
|
||
actualPrompt:
|
||
body?.iconDescriptions?.join('\n') ??
|
||
'deterministic spritesheet fixture',
|
||
model: 'deterministic-canvas-v1',
|
||
provider: 'deterministic-loopback',
|
||
sliceMode: 'connected-components',
|
||
spritesheetResource: {
|
||
resourceId,
|
||
projectId,
|
||
imageSrc: `/${objectKey}`,
|
||
objectKey,
|
||
assetObjectId,
|
||
width,
|
||
height,
|
||
sourceType: 'generated',
|
||
taskId,
|
||
},
|
||
spritesheetAsset: {
|
||
assetId: `asset-${imageId}`,
|
||
assetObjectId,
|
||
assetKind: 'art-spritesheet',
|
||
projectId,
|
||
taskId,
|
||
},
|
||
iconImageSrcs,
|
||
});
|
||
return json(202, {
|
||
data: {
|
||
operationId,
|
||
kind: 'editor_icon_spritesheet_generation',
|
||
status: 'queued',
|
||
statusUrl: `/api/external/v1/generations/${operationId}`,
|
||
pollAfterMs: 1,
|
||
updatedAtMicros: generationSequence,
|
||
},
|
||
});
|
||
}
|
||
if (
|
||
request.method === 'POST' &&
|
||
canonicalPath === '/api/external/v1/editor/images/generations'
|
||
) {
|
||
const idempotencyKey = request.headers['idempotency-key'];
|
||
if (
|
||
typeof idempotencyKey !== 'string' ||
|
||
!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
|
||
idempotencyKey,
|
||
)
|
||
) {
|
||
request.resume();
|
||
return json(400, { error: { message: 'invalid idempotency key' } });
|
||
}
|
||
const body = await readJsonBody(request);
|
||
const image = imageForAspectRatio(body?.aspectRatio);
|
||
generationSequence += 1;
|
||
stats.canvasGenerationRequestCount += 1;
|
||
stats.canvasGeneratedAspectRatios[image.aspectRatio] =
|
||
(stats.canvasGeneratedAspectRatios[image.aspectRatio] ?? 0) + 1;
|
||
const imageId = `deterministic-${generationSequence}`;
|
||
const objectKey = `generated/deterministic/${imageId}.png`;
|
||
const assetObjectId = `asset-object-${imageId}`;
|
||
const resourceId = `resource-${imageId}`;
|
||
const assetKind =
|
||
typeof body?.assetKind === 'string' ? body.assetKind : 'game-art';
|
||
images.set(imageId, {
|
||
...image,
|
||
objectKey,
|
||
downloadKind: 'generation',
|
||
});
|
||
const operationId = `task-${imageId}`;
|
||
generationOperations.set(operationId, {
|
||
imageSrc: `/${objectKey}`,
|
||
objectKey,
|
||
assetObjectId,
|
||
width: image.width,
|
||
height: image.height,
|
||
sourceType: 'generated',
|
||
prompt: body?.prompt ?? 'deterministic canvas fixture',
|
||
actualPrompt: body?.prompt ?? 'deterministic canvas fixture',
|
||
model: 'deterministic-canvas-v1',
|
||
provider: 'deterministic-loopback',
|
||
taskId: `task-${imageId}`,
|
||
resource: {
|
||
resourceId,
|
||
projectId,
|
||
imageSrc: `/${objectKey}`,
|
||
objectKey,
|
||
assetObjectId,
|
||
width: image.width,
|
||
height: image.height,
|
||
sourceType: 'generated',
|
||
prompt: body?.prompt ?? 'deterministic canvas fixture',
|
||
actualPrompt: body?.prompt ?? 'deterministic canvas fixture',
|
||
model: 'deterministic-canvas-v1',
|
||
provider: 'deterministic-loopback',
|
||
taskId: `task-${imageId}`,
|
||
assetKind,
|
||
},
|
||
asset: {
|
||
assetId: `asset-${imageId}`,
|
||
assetObjectId,
|
||
assetKind,
|
||
},
|
||
});
|
||
return json(202, {
|
||
data: {
|
||
operationId,
|
||
kind: 'editor_image_generation',
|
||
status: 'queued',
|
||
statusUrl: `/api/external/v1/generations/${operationId}`,
|
||
pollAfterMs: 1,
|
||
updatedAtMicros: generationSequence,
|
||
},
|
||
});
|
||
}
|
||
if (
|
||
request.method === 'GET' &&
|
||
canonicalPath?.startsWith('/api/external/v1/generations/')
|
||
) {
|
||
request.resume();
|
||
const operationId = canonicalPath.slice(
|
||
'/api/external/v1/generations/'.length,
|
||
);
|
||
const result = generationOperations.get(operationId);
|
||
if (!result)
|
||
return json(404, { error: { message: 'operation not found' } });
|
||
return json(200, {
|
||
data: {
|
||
operationId,
|
||
kind: 'editor_image_generation',
|
||
status: 'completed',
|
||
phaseLabel: '图片画布生成图片',
|
||
phaseDetail: '生成已完成。',
|
||
progress: 100,
|
||
result,
|
||
updatedAtMicros: generationSequence,
|
||
},
|
||
});
|
||
}
|
||
if (
|
||
request.method === 'GET' &&
|
||
canonicalPath === '/api/external/v1/assets/read-url'
|
||
) {
|
||
request.resume();
|
||
const objectKey = parsed.searchParams.get('objectKey');
|
||
const entry = [...images.entries()].find(
|
||
([, image]) => image.objectKey === objectKey,
|
||
);
|
||
if (!entry) return json(404, { error: { message: 'image not found' } });
|
||
const [imageId] = entry;
|
||
const host = request.headers.host ?? LOOPBACK_HOST;
|
||
return json(200, {
|
||
data: {
|
||
read: {
|
||
provider: 'deterministic-loopback',
|
||
bucket: 'deterministic',
|
||
endpoint: LOOPBACK_HOST,
|
||
host: LOOPBACK_HOST,
|
||
objectKey,
|
||
expiresAt: '2099-01-01T00:00:00Z',
|
||
signedUrl: `http://${host}/signed/${imageId}.png`,
|
||
},
|
||
},
|
||
});
|
||
}
|
||
|
||
if (
|
||
request.method === 'POST' &&
|
||
canonicalPath === '/api/external/v1/assets/direct-upload-tickets'
|
||
) {
|
||
const body = await readJsonBody(request);
|
||
uploadSequence += 1;
|
||
const pathSegments = Array.isArray(body?.pathSegments)
|
||
? body.pathSegments.filter(
|
||
(segment) => typeof segment === 'string' && segment.trim(),
|
||
)
|
||
: [];
|
||
const objectKey =
|
||
pathSegments.length > 0
|
||
? `${pathSegments.join('/')}/deterministic-reference-${uploadSequence}.png`
|
||
: `generated/deterministic/reference-${uploadSequence}.png`;
|
||
const assetObjectId = `asset-object-reference-${uploadSequence}`;
|
||
uploadedObjects.set(objectKey, { assetObjectId });
|
||
const host = request.headers.host
|
||
? `http://${request.headers.host}`
|
||
: `http://${LOOPBACK_HOST}`;
|
||
return json(200, {
|
||
data: {
|
||
upload: {
|
||
host,
|
||
bucket: 'deterministic',
|
||
objectKey,
|
||
successActionStatus: 204,
|
||
maxSizeBytes: MAX_REQUEST_BYTES,
|
||
formFields: { key: objectKey },
|
||
},
|
||
},
|
||
});
|
||
}
|
||
|
||
if (
|
||
request.method === 'POST' &&
|
||
canonicalPath === '/api/external/v1/assets/objects/confirm'
|
||
) {
|
||
const body = await readJsonBody(request);
|
||
const objectKey =
|
||
typeof body?.objectKey === 'string' && body.objectKey.trim()
|
||
? body.objectKey.trim()
|
||
: null;
|
||
if (!objectKey) {
|
||
return json(400, { error: { message: 'objectKey is required' } });
|
||
}
|
||
const existing = uploadedObjects.get(objectKey);
|
||
const assetObjectId =
|
||
existing?.assetObjectId ?? `asset-object-confirmed-${++uploadSequence}`;
|
||
uploadedObjects.set(objectKey, { assetObjectId });
|
||
return json(200, {
|
||
data: { assetObject: { objectKey, assetObjectId } },
|
||
});
|
||
}
|
||
|
||
if (
|
||
request.method === 'POST' &&
|
||
canonicalPath?.match(
|
||
/^\/api\/external\/v1\/editor\/projects\/[^/]+\/resources$/,
|
||
)
|
||
) {
|
||
const body = await readJsonBody(request);
|
||
const objectKey =
|
||
typeof body?.objectKey === 'string' && body.objectKey.trim()
|
||
? body.objectKey.trim()
|
||
: `resource-${registeredResources.size + 1}`;
|
||
if (!registeredResources.has(objectKey)) {
|
||
registeredResources.set(objectKey, {
|
||
resourceId: `resource-reference-${registeredResources.size + 1}`,
|
||
});
|
||
}
|
||
return json(200, {
|
||
data: {
|
||
resource: {
|
||
...(body && typeof body === 'object' ? body : {}),
|
||
resourceId: registeredResources.get(objectKey).resourceId,
|
||
},
|
||
},
|
||
});
|
||
}
|
||
if (request.method === 'GET' && isSignedImage) {
|
||
request.resume();
|
||
const imageId = parsed.pathname.slice('/signed/'.length, -'.png'.length);
|
||
const image = images.get(imageId);
|
||
if (!image) return json(404, { error: { message: 'image not found' } });
|
||
if (image.downloadKind === 'slice') {
|
||
stats.canvasSliceDownloadRequestCount += 1;
|
||
} else {
|
||
stats.canvasDownloadRequestCount += 1;
|
||
}
|
||
return {
|
||
statusCode: 200,
|
||
contentType: 'image/png',
|
||
bytes: image.bytes,
|
||
};
|
||
}
|
||
request.resume();
|
||
return json(404, { error: { message: 'not found' } });
|
||
}
|
||
|
||
return Object.freeze({
|
||
handle,
|
||
getStats: () => structuredClone(stats),
|
||
});
|
||
}
|
||
|
||
export async function startDeterministicLaneDefenseProvider({
|
||
apiKey,
|
||
model = deterministicLaneDefenseModel,
|
||
relaxed = false,
|
||
fallbackPorts = DEFAULT_FALLBACK_PORTS,
|
||
} = {}) {
|
||
const router = createDeterministicLaneDefenseRouter({
|
||
apiKey,
|
||
model,
|
||
relaxed,
|
||
});
|
||
const canvasFixture = createDeterministicCanvasFixture(apiKey);
|
||
const sockets = new Set();
|
||
let stopped = false;
|
||
let stopPromise = null;
|
||
const server = http.createServer(async (request, response) => {
|
||
try {
|
||
const canvasResponse = await canvasFixture.handle(request);
|
||
if (canvasResponse) {
|
||
sendBytes(
|
||
response,
|
||
canvasResponse.statusCode,
|
||
canvasResponse.contentType,
|
||
canvasResponse.bytes,
|
||
);
|
||
return;
|
||
}
|
||
if (request.method !== 'POST' || request.url !== CHAT_COMPLETIONS_PATH) {
|
||
request.resume();
|
||
sendJson(response, 404, { error: { message: 'not found' } });
|
||
return;
|
||
}
|
||
const payload = await readJsonBody(request);
|
||
const result = router.route({
|
||
authorization: request.headers.authorization,
|
||
payload,
|
||
});
|
||
sendJson(response, 200, result);
|
||
} catch (error) {
|
||
sendJson(response, 422, {
|
||
error: { message: error?.code ?? 'provider-request-rejected' },
|
||
});
|
||
}
|
||
});
|
||
server.on('connection', (socket) => {
|
||
sockets.add(socket);
|
||
socket.once('close', () => sockets.delete(socket));
|
||
socket.on('error', () => {});
|
||
});
|
||
server.on('clientError', (_error, socket) => socket.destroy());
|
||
await bindLoopback(server, fallbackPorts);
|
||
const address = server.address();
|
||
if (
|
||
!address ||
|
||
typeof address === 'string' ||
|
||
address.address !== LOOPBACK_HOST
|
||
) {
|
||
throw providerError('provider-loopback-bind-invalid');
|
||
}
|
||
|
||
return Object.freeze({
|
||
baseUrl: `http://${LOOPBACK_HOST}:${address.port}/v1`,
|
||
editorBaseUrl: `http://${LOOPBACK_HOST}:${address.port}`,
|
||
getStats() {
|
||
return { ...router.getStats(), ...canvasFixture.getStats(), stopped };
|
||
},
|
||
stop() {
|
||
if (stopPromise) return stopPromise;
|
||
stopPromise = new Promise((resolve) => {
|
||
server.close(() => {
|
||
stopped = true;
|
||
resolve();
|
||
});
|
||
for (const socket of sockets) socket.destroy();
|
||
server.closeAllConnections?.();
|
||
});
|
||
return stopPromise;
|
||
},
|
||
});
|
||
}
|