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.
+
+
+
+
+
+`;
+}
+
+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 --",