diff --git a/apps/ai-game-creator-shell/package.json b/apps/ai-game-creator-shell/package.json index 3f41976b7..ee9c475f1 100644 --- a/apps/ai-game-creator-shell/package.json +++ b/apps/ai-game-creator-shell/package.json @@ -19,6 +19,8 @@ "agent-runtime:mixed-swarm-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-static-isolated-autonomous-chat", "agent-runtime:supervisor-swarm-autonomous-chat-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-autonomous-chat", "agent-runtime:supervisor-autonomous-playable-lane-defense-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-autonomous-playable-lane-defense", + "agent-runtime:supervisor-autonomous-playable-lane-defense-deterministic-e2e": "node scripts/agent-runtime-deterministic-playable-e2e.mjs", + "agent-runtime:supervisor-autonomous-playable-lane-defense-deterministic-self-test": "node scripts/agent-runtime-deterministic-playable-e2e.mjs --self-test", "agent-runtime:supervisor-swarm-transient-retry-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-transient-retry", "agent-runtime:supervisor-swarm-final-reply-transient-retry-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-final-reply-transient-retry", "agent-runtime:supervisor-swarm-tool-plan-handoff-runner-kill-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-tool-plan-handoff-runner-kill", diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-deterministic-playable-e2e.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-deterministic-playable-e2e.mjs new file mode 100644 index 000000000..6e611ff8a --- /dev/null +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-deterministic-playable-e2e.mjs @@ -0,0 +1,395 @@ +import { spawn } from 'node:child_process'; +import { createHash, randomUUID } from 'node:crypto'; +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { + createDeterministicLaneDefenseRouter, + deterministicLaneDefenseInitialHtml, + deterministicLaneDefenseModel, + hiddenCanvasCss, + startDeterministicLaneDefenseProvider, + visibleCanvasCss, +} from './deterministic-lane-defense-provider.mjs'; +import { withLoopbackNoProxy } from './llm-transient-fault-proxy.mjs'; + +const appRoot = path.resolve(fileURLToPath(new URL('..', import.meta.url))); +const repoRoot = path.resolve(appRoot, '../..'); +const realE2eScript = path.join(appRoot, 'scripts/agent-runtime-real-e2e.mjs'); +const suite = 'supervisor-autonomous-playable-lane-defense'; +const wrapperSuite = + 'supervisor-autonomous-playable-lane-defense-deterministic'; +const configFileName = 'game-creator.config.json'; +const configSentinelName = '.deterministic-provider-e2e.json'; +const configSentinelSchema = 'genarrative-deterministic-provider-e2e-config.v1'; +const outputLimit = 32 * 1024 * 1024; + +function hashValue(value) { + return createHash('sha256').update(value).digest('hex'); +} + +function assert(condition, code) { + if (condition) return; + const error = new Error(code); + error.code = code; + throw error; +} + +function parseArguments(args) { + let keepProject = false; + let selfTest = false; + for (const arg of args) { + if (arg === '--keep-project') keepProject = true; + else if (arg === '--self-test') selfTest = true; + else throw new Error('unknown-argument'); + } + assert(!(keepProject && selfTest), 'self-test-keep-project-conflict'); + return { keepProject, selfTest }; +} + +function appendBounded(current, chunk) { + const combined = Buffer.concat([current, chunk]); + if (combined.length > outputLimit) throw new Error('child-output-too-large'); + return combined; +} + +function runChild(args, environment) { + return new Promise((resolve) => { + const child = spawn(process.execPath, args, { + cwd: appRoot, + env: environment, + stdio: ['ignore', 'pipe', 'pipe'], + }); + let stdout = Buffer.alloc(0); + let stderr = Buffer.alloc(0); + let outputError = null; + child.stdout.on('data', (chunk) => { + try { + stdout = appendBounded(stdout, chunk); + } catch (error) { + outputError = error; + child.kill('SIGTERM'); + } + }); + child.stderr.on('data', (chunk) => { + try { + stderr = appendBounded(stderr, chunk); + } catch (error) { + outputError = error; + child.kill('SIGTERM'); + } + }); + child.once('error', (error) => + resolve({ code: null, signal: null, error, stdout, stderr }), + ); + child.once('close', (code, signal) => + resolve({ code, signal, error: outputError, stdout, stderr }), + ); + }); +} + +function safeChildDiagnostic(result) { + return { + exitCode: Number.isInteger(result.code) ? result.code : null, + signal: typeof result.signal === 'string' ? result.signal : null, + errorCode: + typeof result.error?.code === 'string' ? result.error.code : null, + stdoutBytes: result.stdout.length, + stdoutSha256: hashValue(result.stdout), + stderrBytes: result.stderr.length, + stderrSha256: hashValue(result.stderr), + }; +} + +function parseChildReport(result) { + try { + const report = JSON.parse(result.stdout.toString('utf8')); + assert(report && typeof report === 'object', 'child-report-root-invalid'); + return report; + } catch (error) { + if (error?.code) throw error; + const wrapped = new Error('child-report-json-invalid'); + wrapped.code = 'child-report-json-invalid'; + throw wrapped; + } +} + +function expectedProviderStats(stats) { + return ( + stats.requestCount === 17 && + stats.planningRequestCount === 17 && + stats.finalReplyRequestCount === 0 && + stats.initialDelegationCount === 2 && + stats.followupDelegationCount === 1 && + stats.runStatusCount === 2 && + stats.sourceWriteCount === 1 && + stats.sourcePatchCount === 1 && + stats.staticSmokeCount === 4 && + stats.previewValidationCount === 2 && + stats.supervisorDirectMutationAttemptCount === 1 && + stats.unexpectedRequestCount === 0 && + Object.keys(stats.rejectionCodes ?? {}).length === 0 && + stats.byAgent?.['project-supervisor']?.planning === 9 && + stats.byAgent?.['project-supervisor']?.finalReply === 0 && + stats.byAgent?.['code-prototype']?.planning === 6 && + stats.byAgent?.['code-prototype']?.finalReply === 0 && + stats.byAgent?.['quality-review']?.planning === 2 && + stats.byAgent?.['quality-review']?.finalReply === 0 + ); +} + +function syntheticPayload(agentId, runId, tools, extraContext = '') { + return { + model: deterministicLaneDefenseModel, + stream: false, + messages: [ + { + role: 'user', + content: `- templateAgentId: ${agentId}\n- runId: ${runId}\n${extraContext}`, + }, + ], + tools: tools.map((name) => ({ type: 'function', function: { name } })), + }; +} + +function responseFunctionNames(response) { + return response.choices[0].message.tool_calls.map( + (call) => call.function.name, + ); +} + +async function runSelfTest() { + const html = deterministicLaneDefenseInitialHtml(); + assert([...html].length <= 8_000, 'self-test-html-source-budget-invalid'); + assert( + html.includes(hiddenCanvasCss) && + !html.includes(visibleCanvasCss) && + html.includes('playable-web-game-state.v1') && + html.includes('data-playtest-id="next-level"') && + html.includes('Goal: defend the garden and win every wave.') && + html.includes('requestAnimationFrame'), + 'self-test-html-contract-invalid', + ); + const apiKey = `deterministic-self-test-${randomUUID()}`; + const router = createDeterministicLaneDefenseRouter({ apiKey }); + const allTools = [ + 'update_agent_plan', + 'respond_to_user', + 'runtime_tool_agent_delegate', + 'runtime_tool_agent_run_status', + 'runtime_tool_command_run_limited', + 'runtime_tool_file_patch', + 'runtime_tool_file_write', + 'runtime_tool_preview_validate', + ]; + const route = (agentId, runId, tools = allTools, extraContext = '') => + router.route({ + authorization: `Bearer ${apiKey}`, + payload: syntheticPayload(agentId, runId, tools, extraContext), + }); + assert( + responseFunctionNames(route('project-supervisor', 'parent-run')).join( + ',', + ) === 'runtime_tool_agent_delegate,runtime_tool_agent_delegate', + 'self-test-initial-delegation-invalid', + ); + const qualityPlan = + '计划进度:\n- #1 [in_progress] 核对完整玩法\n- #2 [pending] 回传结论\n工具策略:auto=无'; + const builderPlan = + '计划进度:\n- #1 [completed] 生成入口\n- #2 [completed] 静态验证\n- #3 [in_progress] 回传结论\n工具策略:auto=无'; + route('quality-review', 'quality-run', allTools, qualityPlan); + route('code-prototype', 'initial-code-run'); + route('code-prototype', 'initial-code-run'); + assert( + responseFunctionNames( + route('code-prototype', 'initial-code-run', allTools, builderPlan), + ).join(',') === 'update_agent_plan,respond_to_user', + 'self-test-builder-plan-completion-invalid', + ); + assert( + responseFunctionNames( + route('quality-review', 'quality-run', allTools, qualityPlan), + ).join(',') === 'update_agent_plan,respond_to_user', + 'self-test-quality-stale-replan-invalid', + ); + route('project-supervisor', 'parent-run'); + route('project-supervisor', 'parent-run'); + route('project-supervisor', 'parent-run'); + assert( + responseFunctionNames(route('project-supervisor', 'parent-run'))[0] === + 'runtime_tool_file_patch', + 'self-test-forbidden-parent-mutation-invalid', + ); + assert( + responseFunctionNames( + route( + 'project-supervisor', + 'parent-run', + ['runtime_tool_agent_delegate'], + '当前父 run 已进入只编排模式,本次修复的原生工具目录只保留 agent.delegate', + ), + )[0] === 'runtime_tool_agent_delegate', + 'self-test-followup-delegation-invalid', + ); + route('code-prototype', 'repair-code-run'); + route('code-prototype', 'repair-code-run'); + route('code-prototype', 'repair-code-run', allTools, builderPlan); + assert( + responseFunctionNames(route('project-supervisor', 'parent-run')).join( + ',', + ) === 'runtime_tool_command_run_limited,runtime_tool_preview_validate', + 'self-test-repair-verification-batch-invalid', + ); + route('project-supervisor', 'parent-run'); + route('project-supervisor', 'parent-run'); + const stats = router.getStats(); + assert(expectedProviderStats(stats), 'self-test-provider-stats-invalid'); + + const rootPackage = JSON.parse( + await fs.readFile(path.join(repoRoot, 'package.json'), 'utf8'), + ); + const shellPackage = JSON.parse( + await fs.readFile(path.join(appRoot, 'package.json'), 'utf8'), + ); + assert( + shellPackage.scripts?.[ + 'agent-runtime:supervisor-autonomous-playable-lane-defense-deterministic-e2e' + ] === 'node scripts/agent-runtime-deterministic-playable-e2e.mjs' && + rootPackage.scripts?.[ + 'ai-game-creator-shell:agent-runtime:supervisor-autonomous-playable-lane-defense-deterministic-e2e' + ] === + 'npm --prefix apps/ai-game-creator-shell run agent-runtime:supervisor-autonomous-playable-lane-defense-deterministic-e2e --', + 'self-test-package-command-invalid', + ); + return { + status: 'PASS', + suite: `${wrapperSuite}-self-test`, + providerUsed: false, + htmlChars: [...html].length, + providerStats: stats, + packageCommandsRegistered: true, + }; +} + +async function runE2e(options) { + const token = randomUUID(); + const apiKey = `deterministic-runtime-${randomUUID()}`; + const configDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'genarrative-deterministic-provider-config-'), + ); + let provider = null; + let childResult = null; + let childReport = null; + let configRemoved = false; + let providerStats = null; + let failureCode = null; + try { + if (process.platform !== 'win32') await fs.chmod(configDir, 0o700); + await fs.writeFile( + path.join(configDir, configSentinelName), + `${JSON.stringify({ schemaVersion: configSentinelSchema, token })}\n`, + { flag: 'wx', mode: 0o600 }, + ); + provider = await startDeterministicLaneDefenseProvider({ apiKey }); + const config = { + llm: { + apiKey, + baseUrl: provider.baseUrl, + model: deterministicLaneDefenseModel, + apiKind: 'openai_chat', + reasoningEffort: 'low', + stream: false, + requestTimeoutMs: 30_000, + maxRetries: 0, + retryBackoffMs: 100, + }, + }; + await fs.writeFile( + path.join(configDir, configFileName), + `${JSON.stringify(config)}\n`, + { flag: 'wx', mode: 0o600 }, + ); + const childArgs = [ + realE2eScript, + '--suite', + suite, + '--config-dir', + configDir, + ]; + if (options.keepProject) childArgs.push('--keep-project'); + childResult = await runChild( + childArgs, + withLoopbackNoProxy({ ...process.env, NO_COLOR: '1' }), + ); + childReport = parseChildReport(childResult); + } catch (error) { + failureCode = error?.code ?? 'deterministic-e2e-unexpected-error'; + } finally { + if (provider) { + try { + await provider.stop(); + providerStats = provider.getStats(); + } catch { + failureCode ??= 'deterministic-provider-stop-failed'; + } + } + try { + const sentinel = JSON.parse( + await fs.readFile(path.join(configDir, configSentinelName), 'utf8'), + ); + assert( + sentinel.schemaVersion === configSentinelSchema && + sentinel.token === token, + 'deterministic-config-sentinel-invalid', + ); + await fs.rm(configDir, { recursive: true, force: false }); + configRemoved = true; + } catch (error) { + failureCode ??= error?.code ?? 'deterministic-config-cleanup-failed'; + } + } + + const childPassed = + childResult?.code === 0 && + childResult?.signal === null && + !childResult?.error && + childReport?.status === 'PASS' && + childReport?.suite === suite && + childReport?.cleanup?.performed === !options.keepProject && + childReport?.cleanup?.kept === options.keepProject; + const providerPassed = + providerStats?.stopped === true && expectedProviderStats(providerStats); + const status = + !failureCode && childPassed && providerPassed && configRemoved + ? 'PASS' + : 'FAIL'; + if (status !== 'PASS' && !failureCode) { + failureCode = !childPassed + ? 'deterministic-child-e2e-failed' + : !providerPassed + ? 'deterministic-provider-contract-failed' + : 'deterministic-config-not-cleaned'; + } + return { + status, + suite: wrapperSuite, + providerMode: 'deterministic-loopback-openai-chat', + delegatedSuite: suite, + child: childReport, + provider: providerStats, + cleanup: { + providerStopped: providerStats?.stopped === true, + configRemoved, + projectKept: options.keepProject, + }, + childDiagnostic: childResult ? safeChildDiagnostic(childResult) : null, + failureCode, + }; +} + +const options = parseArguments(process.argv.slice(2)); +const report = options.selfTest ? await runSelfTest() : await runE2e(options); +process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); +process.exitCode = report.status === 'PASS' ? 0 : 1; diff --git a/apps/ai-game-creator-shell/scripts/deterministic-lane-defense-provider.mjs b/apps/ai-game-creator-shell/scripts/deterministic-lane-defense-provider.mjs new file mode 100644 index 000000000..d2f5712dc --- /dev/null +++ b/apps/ai-game-creator-shell/scripts/deterministic-lane-defense-provider.mjs @@ -0,0 +1,728 @@ +import http from 'node:http'; + +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; + +export const deterministicLaneDefenseModel = + 'deterministic-lane-defense-provider-v1'; +export const hiddenCanvasCss = '#game{display:none;'; +export const visibleCanvasCss = '#game{display:block;'; + +export function deterministicLaneDefenseInitialHtml() { + return ` + + + +Garden Guardians + + +
+

Garden Guardians

GENARRATIVE_REAL_E2E_VISIBLE

Goal: defend the garden and win every wave.

+
+
+
Level 1 ready
+ +
+`; +} + +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 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 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 }, + }; +} + +function delegateCall(agentId, task, acceptanceCriteria) { + return nativeAction('agent.delegate', `把独立工作交给 ${agentId}`, { + agentId, + task, + acceptanceCriteria, + expectedArtifacts: ['game/index.html'], + repairOfDelegationId: null, + runId: null, + }); +} + +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: [ + 'Garden Guardians', + 'Start Game', + 'Sunflower', + 'Peashooter', + '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 repairPatchCall(reason) { + return nativeAction('file.patch', reason, { + path: 'game/index.html', + oldText: hiddenCanvasCss, + newText: visibleCanvasCss, + expectedReplacements: 1, + }); +} + +export function createDeterministicLaneDefenseRouter({ + apiKey, + model = deterministicLaneDefenseModel, +} = {}) { + if (typeof apiKey !== 'string' || apiKey.length < 16) { + throw providerError('provider-api-key-invalid'); + } + const runKinds = new Map(); + const runPhases = new Map(); + const finalReplyRuns = new Set(); + const agentCounts = new Map(); + const stats = { + requestCount: 0, + planningRequestCount: 0, + finalReplyRequestCount: 0, + initialDelegationCount: 0, + followupDelegationCount: 0, + runStatusCount: 0, + sourceWriteCount: 0, + sourcePatchCount: 0, + staticSmokeCount: 0, + previewValidationCount: 0, + supervisorDirectMutationAttemptCount: 0, + unexpectedRequestCount: 0, + rejectionCodes: {}, + rejections: [], + byAgent: {}, + }; + let responseSequence = 0; + let parentStage = 'initial-delegation'; + let initialBuilderRunId = null; + let repairBuilderRunId = null; + + function recordAgent(agentId, kind) { + const current = agentCounts.get(agentId) ?? { planning: 0, finalReply: 0 }; + current[kind] += 1; + agentCounts.set(agentId, current); + stats.byAgent = Object.fromEntries( + [...agentCounts.entries()].sort(([left], [right]) => + left.localeCompare(right), + ), + ); + } + + function callsResponse(tools, calls) { + requireAdvertised(tools, calls); + responseSequence += 1; + return chatToolResponse(responseSequence, model, calls); + } + + function parentCalls(context, tools) { + const delegateRepairPrompt = + (context.includes('当前父 run 已进入只编排模式') && + context.includes('只保留 agent.delegate')) || + (parentStage === 'await-delegation-repair' && + tools.size === 1 && + tools.has(runtimeFunction('agent.delegate'))); + if (delegateRepairPrompt) { + if (parentStage !== 'await-delegation-repair') { + throw providerError('provider-repair-prompt-out-of-order'); + } + parentStage = 'verify-repair'; + stats.followupDelegationCount += 1; + return callsResponse(tools, [ + delegateCall( + 'code-prototype', + '修复最近一次真实浏览器验证证明的 canvas 不可见问题;保持现有 lane-defense-v1 交互、状态面、关卡推进和重开行为不变,并在修改后通过静态自检。', + [ + 'game/index.html 的 canvas 在桌面与移动视口可见且有非空像素', + 'lane-defense-v1 全部固定试玩断言继续通过', + '修改后的当前 revision 通过 game.static_smoke', + ], + ), + ]); + } + + switch (parentStage) { + case 'initial-delegation': { + parentStage = 'claim-initial'; + stats.initialDelegationCount += 2; + return callsResponse(tools, [ + delegateCall( + 'code-prototype', + '生成一个紧凑但完整的植物塔防网页游戏:支持开始、选择植物、放置防御单位、敌人移动和受伤、胜利、下一关与重开;写入 game/index.html,并在修改后通过静态自检。', + [ + 'game/index.html 提供完整 lane-defense-v1 状态面和六个可点击控件', + '敌人会移动并受伤,关卡可胜利、进入下一关并重开', + '修改后的当前 revision 通过 game.static_smoke', + ], + ), + delegateCall( + 'quality-review', + '独立核对塔防交付合同,确认必须覆盖植物选择、战斗、胜利、下一关、重开、桌面和移动视口;只返回验收重点,不修改项目。', + ['验收结论覆盖完整可玩循环和双视口', '不修改项目文件'], + ), + ]); + } + case 'claim-initial': + parentStage = 'static-initial'; + stats.runStatusCount += 1; + return callsResponse(tools, [runStatusCall('认领两份首轮专业回执')]); + case 'static-initial': + parentStage = 'preview-initial'; + stats.staticSmokeCount += 1; + return callsResponse(tools, [staticSmokeCall('验证初版当前 revision')]); + case 'preview-initial': + parentStage = 'attempt-forbidden-repair'; + stats.previewValidationCount += 1; + return callsResponse(tools, [ + previewCall('真实试玩并检查桌面与移动视口'), + ]); + case 'attempt-forbidden-repair': + parentStage = 'await-delegation-repair'; + stats.supervisorDirectMutationAttemptCount += 1; + return callsResponse(tools, [ + repairPatchCall('根据浏览器失败诊断直接修复不可见 canvas'), + ]); + case 'verify-repair': + parentStage = 'claim-repair'; + stats.staticSmokeCount += 1; + stats.previewValidationCount += 1; + return callsResponse(tools, [ + staticSmokeCall('验证返工后的当前 revision'), + previewCall('重跑完整真实试玩和双视口检查'), + ]); + case 'claim-repair': + parentStage = 'respond'; + stats.runStatusCount += 1; + return callsResponse(tools, [ + runStatusCall('认领通过复验的专业返工回执'), + ]); + case 'respond': + parentStage = 'done'; + return callsResponse(tools, [ + ...completedPlanCallsForContext( + context, + '可试玩项目和全部验证已经完成', + ), + nativeResponse( + '可试玩塔防版本已经完成,并通过静态检查、完整交互试玩以及桌面和移动视口验证。', + ), + ]); + default: + throw providerError(`provider-parent-stage-invalid:${parentStage}`); + } + } + + function codeCalls(runId, tools, context) { + if (!runKinds.has(runId)) { + if (!initialBuilderRunId) { + initialBuilderRunId = runId; + runKinds.set(runId, 'initial-builder'); + } else if (!repairBuilderRunId) { + repairBuilderRunId = runId; + runKinds.set(runId, 'repair-builder'); + } else { + throw providerError('provider-extra-builder-run'); + } + runPhases.set(runId, 0); + } + const kind = runKinds.get(runId); + const phase = runPhases.get(runId); + if (kind === 'initial-builder') { + if (phase === 0) { + runPhases.set(runId, 1); + stats.sourceWriteCount += 1; + return callsResponse(tools, [ + nativeAction('file.write', '写入完整可玩的塔防初版', { + path: 'game/index.html', + content: deterministicLaneDefenseInitialHtml(), + }), + ]); + } + if (phase === 1) { + runPhases.set(runId, 2); + stats.staticSmokeCount += 1; + return callsResponse(tools, [staticSmokeCall('验证初版源码与状态面')]); + } + if (phase === 2) { + runPhases.set(runId, 3); + return callsResponse(tools, [ + ...completedPlanCallsForContext( + context, + '初版实现和静态验证已经完成', + ), + nativeResponse('初版塔防玩法与交互状态面已经生成,并通过静态自检。'), + ]); + } + } + if (kind === 'repair-builder') { + if (phase === 0) { + runPhases.set(runId, 1); + stats.sourcePatchCount += 1; + return callsResponse(tools, [ + repairPatchCall('修复真实浏览器发现的 canvas 可见性'), + ]); + } + if (phase === 1) { + runPhases.set(runId, 2); + stats.staticSmokeCount += 1; + return callsResponse(tools, [staticSmokeCall('验证返工后的源码')]); + } + if (phase === 2) { + runPhases.set(runId, 3); + return callsResponse(tools, [ + ...completedPlanCallsForContext( + context, + '画布可见性返工和静态验证已经完成', + ), + nativeResponse( + '画布可见性已修复,原有完整试玩行为保持不变并通过静态自检。', + ), + ]); + } + } + throw providerError(`provider-builder-stage-invalid:${kind}:${phase}`); + } + + function qualityCalls(runId, tools, context) { + const phase = runPhases.get(runId) ?? 0; + if (phase === 0) { + runKinds.set(runId, 'quality-review'); + runPhases.set(runId, 1); + return callsResponse(tools, [ + ...completedPlanCallsForContext(context, '独立验收重点已经核对'), + nativeResponse( + '验收必须以真实植物选择、放置、战斗推进、胜利、下一关、重开和双视口可见性为准。', + ), + ]); + } + if (phase === 1) { + runPhases.set(runId, 2); + return callsResponse(tools, [ + ...completedPlanCallsForContext( + context, + '项目 revision 更新后已重新核对独立验收重点', + ), + nativeResponse( + '已基于最新项目 revision 复核验收重点,仍需以真实植物选择、放置、战斗推进、胜利、下一关、重开和双视口可见性为准。', + ), + ]); + } + throw providerError(`provider-quality-stage-invalid:${phase}`); + } + + function finalReply(identity, context) { + const key = `${identity.agentId}\0${identity.runId}`; + if (finalReplyRuns.has(key)) { + throw providerError('provider-duplicate-final-reply-request'); + } + if ( + !context.includes('给用户一个正常中文回复') && + !context.includes('给开发者一个正常中文回复') + ) { + throw providerError('provider-unexpected-text-request'); + } + 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 identity = extractIdentity(context); + const tools = advertisedToolNames(payload); + diagnostic = { + agentId: identity.agentId, + tools: [...tools].sort(), + 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 (identity.agentId === 'project-supervisor') { + return parentCalls(context, tools); + } + if (identity.agentId === 'code-prototype') { + return codeCalls(identity.runId, tools, context); + } + if (identity.agentId === 'quality-review') { + return qualityCalls(identity.runId, tools, context); + } + 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)); + response.writeHead(statusCode, { + connection: 'close', + 'content-length': bytes.length, + 'content-type': 'application/json; charset=utf-8', + 'x-request-id': `deterministic-${statusCode}`, + }); + response.end(bytes); +} + +export async function startDeterministicLaneDefenseProvider({ + apiKey, + model = deterministicLaneDefenseModel, + fallbackPorts = DEFAULT_FALLBACK_PORTS, +} = {}) { + const router = createDeterministicLaneDefenseRouter({ apiKey, model }); + const sockets = new Set(); + let stopped = false; + let stopPromise = null; + const server = http.createServer(async (request, response) => { + if (request.method !== 'POST' || request.url !== CHAT_COMPLETIONS_PATH) { + request.resume(); + sendJson(response, 404, { error: { message: 'not found' } }); + return; + } + try { + 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`, + getStats() { + return { ...router.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; + }, + }); +} diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent.rs b/apps/ai-game-creator-shell/src-tauri/src/agent.rs index caf06458d..b6c87603e 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent.rs @@ -17397,6 +17397,74 @@ mod autonomous_completion_contract_tests { .iter() .any(|message| message.role == "assistant" && message.content == response)); } + + #[test] + fn autonomous_playtest_liveness_only_enforces_the_latest_preview_result() { + let verification_gate = AgentRuntimeVerificationGate { + schema_version: AGENT_RUNTIME_VERIFICATION_GATE_SCHEMA_VERSION.to_string(), + project_id: "autonomous-playtest-liveness-project".to_string(), + agent_id: GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string(), + run_id: "autonomous-playtest-liveness-run".to_string(), + requires_verification: false, + mutation_revision: None, + verified_revision: Some(2), + last_mutation_tool: None, + last_verification_tool: Some("game.static_smoke".to_string()), + last_verification_status: Some(AGENT_RUNTIME_VERIFICATION_STATUS_PASSED.to_string()), + failed_playtest_revision: None, + updated_at: 0, + }; + let failed_preview = AgentRuntimeToolObservation { + tool: "preview.validate".to_string(), + status: "failed".to_string(), + summary: "浏览器验证未通过,请根据诊断修复后重试".to_string(), + detail: None, + }; + let passed_preview = AgentRuntimeToolObservation { + tool: "preview.validate".to_string(), + status: "ok".to_string(), + summary: "浏览器验证已通过,已生成桌面与移动证据".to_string(), + detail: None, + }; + let non_progress = || AgentRuntimeToolObservation { + tool: "memory.read".to_string(), + status: "ok".to_string(), + summary: "已读取记忆".to_string(), + detail: None, + }; + let response_plan = AgentRuntimeToolPlan { + response: "项目已完成真实试玩验证。".to_string(), + ..AgentRuntimeToolPlan::default() + }; + + let mut superseded_failure = vec![failed_preview.clone(), passed_preview.clone()]; + superseded_failure.extend((0..4).map(|_| non_progress())); + assert!(validate_agent_runtime_autonomous_plan_liveness( + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + 7, + 2, + &verification_gate, + &superseded_failure, + &response_plan, + true, + ) + .is_ok()); + + let mut latest_failure = vec![passed_preview, failed_preview]; + latest_failure.extend((0..4).map(|_| non_progress())); + let error = validate_agent_runtime_autonomous_plan_liveness( + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + 7, + 2, + &verification_gate, + &latest_failure, + &response_plan, + true, + ) + .expect_err("latest failed preview must still require delegated repair"); + assert!(error + .starts_with(AGENT_RUNTIME_AUTONOMOUS_DELEGATED_PLAYTEST_REPAIR_LIVENESS_ERROR_PREFIX)); + } } fn game_creator_agent_runtime_verification_gate_relative_path( @@ -28438,13 +28506,19 @@ fn validate_agent_runtime_autonomous_plan_liveness( )); } } - let Some(failed_playtest_index) = observations.iter().rposition(|observation| { - observation.tool == "preview.validate" - && observation.status == "failed" - && observation.summary == "浏览器验证未通过,请根据诊断修复后重试" - }) else { + let Some(latest_playtest_index) = observations + .iter() + .rposition(|observation| observation.tool == "preview.validate") + else { return Ok(()); }; + let latest_playtest = &observations[latest_playtest_index]; + if latest_playtest.status != "failed" + || latest_playtest.summary != "浏览器验证未通过,请根据诊断修复后重试" + { + return Ok(()); + } + let failed_playtest_index = latest_playtest_index; let observations_after_failure = &observations[failed_playtest_index + 1..]; if observations_after_failure .iter() diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index e73b194dc..8866c6d97 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -5194,3 +5194,11 @@ - 当前 run 状态与项目历史成果是两个投影:前者继续按当前 `parentRunId` 过滤,后者只从专业 Agent 持久对话中合法 `agent-finalization-<32 lower hex>` assistant 恢复。新 run 失败或待确认不清除旧成果,普通失败 assistant 不得进入资源管理。 - 历史成果读取采用项目内单调合并:新的合法 finalization 可以替换同 Agent 的旧回执,但 Runtime 轮询引发的持久对话瞬时读取失败、空结果或新 run 普通失败消息都不得清空已恢复成果。资源卡必须明确标记“历史成果”,继续与 manifest 正式资产和项目文件区分。 - Tauri `read_local_conversation` 的公开消息 DTO 必须把持久 JSONL 的可选 `messageId` 原样投影给前端;否则真实 finalization 在客户端边界丢失身份,前端只能看到普通 assistant 并把资源区错误显示为 0。旧消息缺少 ID 时保持 `null`,不按正文或时间猜测成果。 + +## 2026-07-21 AI 游戏创作自主可玩项目确定性真实门禁 + +- 决策:新增独立 loopback OpenAI Chat Provider 与 `supervisor-autonomous-playable-lane-defense-deterministic` wrapper,复用正式自主构建 suite、真实 Runner、真实 Runtime 工具、真实 Chrome 双视口和 disposable 项目。Provider 只能返回原生 function calls,不能直接写项目或伪造验证结果;临时配置必须位于仓库外并由 sentinel 约束清理。 +- 协作顺序:Supervisor 首轮并行委派程序与只读验收;验收回复因并行写入变成 stale 时必须基于最新 revision 再规划。首轮浏览器失败后,Supervisor 直接 mutation 必须被 orchestrator-only 策略拒绝,再创建后续程序委派;新 revision 的静态复验和浏览器复验必须放在同一 planning 批次,成功后再认领后续回执,最后才允许唯一 Supervisor 回复。 +- liveness 语义:observation 兜底只判断最新一条 `preview.validate`。最新成功结果会取代同一 run 更早的失败 observation,不能在成功试玩后因历史失败再次强制委派;最新结果仍为失败时继续阻断收束。持久 verification gate 的 `failedPlaytestRevision` 仍是优先事实源,不因本次修正放宽。 +- 验收证据:本轮确定性 wrapper 与正式子 suite 同轮 PASS。Provider `17` 次 planning、异常 `0`;项目 revision `0 -> 2`,`game.static_smoke` 与桌面/移动浏览器通过,`lane-defense-v1` 固定试玩 `37/37`;三份委派回执全部认领,唯一 Supervisor assistant,pending、sidecar、reconciliation、重复、密钥/正文/路径泄漏均为 `0`,隔离 Runner、AppData、配置和项目全部清理。 +- 边界:本门禁证明本地确定性 OpenAI-compatible 路由可以驱动完整正式链路,不代表任何外部 Provider 已通过。外部 Provider 的鉴权、网络与模型行为必须用独立同轮 E2E 记录,不能与本结果拼接。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index d9e67f323..1b98ce07e 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -667,3 +667,6 @@ game-project/ - tool-plan arguments 只允许出现在 `0600` 原子 sidecar 及后续 pending/action batch,不得进入 task/event/Agent DB/CLI/report。公共 protocol/repair 审计共同保存 Agent/task/Session/run/source、loop/repair/slot、响应指纹、Provider request ID SHA-256 和 protocol;protocol 只保存 function call 数量、call ID SHA-256 数组、catalog-bound function names、response ID SHA-256/字符数及 normalization 元数据,repair 只保存 attempt/maxAttempts、协议错误/preview 哈希和 call ID/function name SHA-256,不保存原始 callId/callIds/responseId/providerRequestId,并在 Agent DB append 锁内按完整身份全历史幂等追加。为了保持执行语义,参数禁止静默脱敏;命中密钥、配置痕迹、敏感 JSON key、Provider ID 中的秘密/绝对路径、结构化可执行路径中的项目或其它绝对路径、大小/顺序/身份冲突时直接 reconciliation。源码正文和计划叙述只做密钥检查,不能把 HTML 闭合标签当绝对路径;未闭合 thinking 只留无正文无效元数据并继续 repair。账本保留到 run 终态或明确作废;steer/cancel/漂移/终态清理前先闭合整本账本的实际 requestId,Runner 恢复严格扫描 hash/primary/`.previous`/安全临时文件并回收合法终态残留,确保单动作、多动作、confirmation、协作 batch 与直接回复在下一 durable owner 建立前都有恢复来源;未知、冲突、primary、`.previous` 或损坏账本都阻止 Runner idle shutdown。 - V1.43 的确定性门禁必须覆盖 base handoff 与 repair handoff 两个 lifecycle-completed 前断点,关闭 mock Provider 后恢复零网络、原 requestId 唯一闭合、repair/protocol audit 幂等、唯一 assistant/completed/committed stream及终局零 sidecar。独立非默认真实门禁 `supervisor-swarm-tool-plan-handoff-runner-kill` 已实现并完成 Shell/Root 两级注册:它使用 sentinel-owned sibling AppData 与 metadata-only zero-fault proxy,以每轮随机 capability 严格绑定 project/Agent/run/实际 request slot;只有 tool-plan handoff 原子落盘并回读一致、同一实际 requestId lifecycle 尚未 `completed` 时才 ACK,随后通过 pidfd `SIGKILL` 强杀 suite 自有 Runner。恢复必须证明同一 requestId 唯一闭合且 `networkReplayCount=0`、protocol/repair audit 幂等、handoff 与 durable batch plan fingerprint 对应、恢复消费前 action/pending/delivery/claim 等副作用为 `0`,并在终局把 sidecar、重复记录、临时 capability/Runner/AppData 资源及公共正文、凭据、URL、项目/正式配置路径泄漏全部清零。2026-07-20 的真实外部 Provider 单轮已到达并通过 checkpoint,但随后专业 Agent 连续连接失败使整轮 FAIL;另一独立轮首批工具数不满足 fixture,也未通过。两轮不得拼接,当前仍无该 suite 的完整外部 PASS。Provider 成功到 handoff 原子落盘回读前的 unknown-result 和手动 context-compaction 仍不在本切片承诺内。 - V1.43 当前确定性实现已通过本轮 `tool_plan_handoff_ 44/44`、Supervisor collaboration 相关过滤 `55/55`、权威返工合同 `1/1`,以及 Tauri/Rust 串行全量 `1054 passed / 4 ignored / 0 failed`;Linux `cargo check --tests` 与 `x86_64-pc-windows-gnu cargo check --tests` 均通过。E2E self-test、typecheck、变更脚本 ESLint、encoding 与 `git diff --check` 通过;默认并发全量只作竞态诊断,不替代串行门禁。Supervisor 真实 E2E 报告已把 `toolPlanHandoffSidecarCount` 纳入终局残留。handoff 跨平台存储使用 Unix 固定目录句柄、目录 `flock`、exchange/quarantine 与 Windows 相对父句柄、句柄枚举、独占 temp,不再根据 PID 推断写入方是否存活;主动忽略锁的同 UID 进程仍属于宿主 OS 信任边界。 +- 2026-07-21 起,同一 Runtime 文档的“V1.44 自主可玩塔防确定性真实门禁”增加独立 loopback OpenAI Chat Provider 和 wrapper 命令。Provider 只返回原生 function calls,不直接修改项目、不伪造工具 observation;wrapper 在仓库外创建带 sentinel 的临时配置,复用正式 `supervisor-autonomous-playable-lane-defense` suite,并在终局停止 Provider、删除配置和 disposable 项目。 +- V1.44 固定验证两份首轮并行专业委派、只读验收回复因 revision 更新而重新规划、程序 Agent 写入并通过静态自检、首轮真实浏览器因隐藏 canvas 失败、Supervisor 直接修改被 orchestrator-only 策略拒绝、后续程序委派产生新 revision、同一 planning 批次完成 `game.static_smoke + preview.validate`、成功后再用 `agent.run_status` 认领后续回执,最后只由 Supervisor 回复。试玩 liveness 只以最新一条 `preview.validate` observation 为准:后续成功会取代历史失败,最新失败仍继续强制专业返工;持久 `failedPlaytestRevision` 门禁保持不变。 +- 本轮 V1.44 wrapper 与正式子 suite 均为 **PASS**:Provider 共 `17` 次 planning、异常请求 `0`;项目从 revision `0` 推进到 `2`,最终 `game/index.html` 为 `4924` 字节;`lane-defense-v1` 的植物选择、放置、敌人移动与受伤、胜利、下一关和重开共 `37/37` 断言通过,桌面与移动浏览器验证通过;三份专业回执全部认领,Supervisor assistant 唯一,pending、confirmation、user-input、provider batch/retry/handoff、tool-plan handoff、finalization journal、reconciliation、重复和泄漏计数均为 `0`,隔离 Runner、AppData、配置和项目已清理。该确定性 loopback PASS 不能替代外部 Provider 可用性验收;外部路由仍须单独形成同轮完整 PASS。 diff --git a/package.json b/package.json index 51d5d4287..aee2312a8 100644 --- a/package.json +++ b/package.json @@ -148,6 +148,8 @@ "ai-game-creator-shell:agent-runtime:real-e2e": "npm --prefix apps/ai-game-creator-shell run agent-runtime:real-e2e --", "ai-game-creator-shell:agent-runtime:supervisor-swarm-autonomous-chat-real-e2e": "npm --prefix apps/ai-game-creator-shell run agent-runtime:supervisor-swarm-autonomous-chat-real-e2e --", "ai-game-creator-shell:agent-runtime:supervisor-autonomous-playable-lane-defense-real-e2e": "npm --prefix apps/ai-game-creator-shell run agent-runtime:supervisor-autonomous-playable-lane-defense-real-e2e --", + "ai-game-creator-shell:agent-runtime:supervisor-autonomous-playable-lane-defense-deterministic-e2e": "npm --prefix apps/ai-game-creator-shell run agent-runtime:supervisor-autonomous-playable-lane-defense-deterministic-e2e --", + "ai-game-creator-shell:agent-runtime:supervisor-autonomous-playable-lane-defense-deterministic-self-test": "npm --prefix apps/ai-game-creator-shell run agent-runtime:supervisor-autonomous-playable-lane-defense-deterministic-self-test --", "ai-game-creator-shell:agent-runtime:supervisor-swarm-transient-retry-real-e2e": "npm --prefix apps/ai-game-creator-shell run agent-runtime:supervisor-swarm-transient-retry-real-e2e --", "ai-game-creator-shell:agent-runtime:supervisor-swarm-final-reply-transient-retry-real-e2e": "npm --prefix apps/ai-game-creator-shell run agent-runtime:supervisor-swarm-final-reply-transient-retry-real-e2e --", "ai-game-creator-shell:agent-runtime:supervisor-swarm-tool-plan-handoff-runner-kill-real-e2e": "npm --prefix apps/ai-game-creator-shell run agent-runtime:supervisor-swarm-tool-plan-handoff-runner-kill-real-e2e --",