合并最新master
Project CI / Frontend tests (pull_request) Failing after 2m13s
Project CI / Repository checks (pull_request) Failing after 2m31s
Project CI / Native shell tests (pull_request) Failing after 3m10s
Project CI / Backend tests (pull_request) Successful in 3m54s

合并 Direct Codex 与原生 HTTP 最新能力

保留做方案的策划 Runtime、GDD 审批和 M1 可靠性约束

保持做游戏和做素材与 master 的 Direct Codex 路径一致

补齐合并后的 provider 与 delegation 契约兼容
This commit is contained in:
2026-08-19 10:43:25 +00:00
179 changed files with 25759 additions and 7185 deletions
@@ -51,7 +51,7 @@ Every generation row requires a stable `Idempotency-Key` header and returns HTTP
| --- | --- | --- | --- | | --- | --- | --- | --- |
| Image generation | `/api/external/v1/editor/images/generations` | `prompt` | `kind`, `style`, `model`, `aspectRatio`, `imageSize`, `size`, `referenceImageSrcs`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` | | Image generation | `/api/external/v1/editor/images/generations` | `prompt` | `kind`, `style`, `model`, `aspectRatio`, `imageSize`, `size`, `referenceImageSrcs`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` |
| Image edit/redraw | `/api/external/v1/editor/images/edits` | `prompt`, `sourceReferenceId` | `referenceImageSrcs`, `model`, `size`, `projectId`, `assetFolderId`, `assetLabel`, `targetLayerId`, `canvasCompletion` | | Image edit/redraw | `/api/external/v1/editor/images/edits` | `prompt`, `sourceReferenceId` | `referenceImageSrcs`, `model`, `size`, `projectId`, `assetFolderId`, `assetLabel`, `targetLayerId`, `canvasCompletion` |
| Icon spritesheet | `/api/external/v1/editor/icon-spritesheets/generations` | `referenceId`, `iconDescriptions` | `style`, `referenceImageSrcs`, `screenColor`, `model`, `aspectRatio`, `imageSize`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` | | Icon spritesheet | `/api/external/v1/editor/icon-spritesheets/generations` | `referenceId`, `iconDescriptions` | `sliceLayout`, `style`, `referenceImageSrcs`, `screenColor`, `model`, `aspectRatio`, `imageSize`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
| UI asset extraction | `/api/external/v1/editor/ui-designs/assets/extractions` | `sourceImageSrc`, `aspectRatio`, `imageSize` | `screenColor`, `model`, `referenceImageSrcs`, `projectId`, `assetFolderId`, `spritesheetLabel`, `canvasCompletion` | | UI asset extraction | `/api/external/v1/editor/ui-designs/assets/extractions` | `sourceImageSrc`, `aspectRatio`, `imageSize` | `screenColor`, `model`, `referenceImageSrcs`, `projectId`, `assetFolderId`, `spritesheetLabel`, `canvasCompletion` |
| Character animation | `/api/external/v1/editor/character-animations/generations` | `sourceLayerId`, `sourceImageSrc`, `sourceWidth`, `sourceHeight`, `promptText`, `resolution`, `ratio`, `frameCount`, `durationSeconds`, `model` | `projectId`, `sourceResourceId`, `assetFolderId`, `assetLabel`, `canvasCompletion` | | Character animation | `/api/external/v1/editor/character-animations/generations` | `sourceLayerId`, `sourceImageSrc`, `sourceWidth`, `sourceHeight`, `promptText`, `resolution`, `ratio`, `frameCount`, `durationSeconds`, `model` | `projectId`, `sourceResourceId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
| Video generation | `/api/external/v1/editor/videos/generations` | `prompt`, `model`, `aspectRatio`, `durationSeconds`, `resolution`, `mode`, `sound` | `referenceImageSrcs`, `referenceVideoSrcs`, `referenceAudioSrcs`, `webSearchEnabled`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` | | Video generation | `/api/external/v1/editor/videos/generations` | `prompt`, `model`, `aspectRatio`, `durationSeconds`, `resolution`, `mode`, `sound` | `referenceImageSrcs`, `referenceVideoSrcs`, `referenceAudioSrcs`, `webSearchEnabled`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
@@ -92,6 +92,8 @@ For image edit/redraw, confirming an upload is not sufficient: create a project
The icon-spritesheet primary `referenceId` is intentionally stricter than ordinary image references: it accepts only a current-owner project resource ID or asset ID whose authoritative `assetKind` is `icon-spec`. It does not accept an `objectKey`, URL, Data URL, or Blob URL. The icon-spritesheet primary `referenceId` is intentionally stricter than ordinary image references: it accepts only a current-owner project resource ID or asset ID whose authoritative `assetKind` is `icon-spec`. It does not accept an `objectKey`, URL, Data URL, or Blob URL.
`sliceLayout: "grid-2x2"` is an opt-in contract for four fixed game-runtime assets. The provider prompt and server persistence both preserve the ordered slots left-top, right-top, left-bottom, right-bottom. Omit it to retain the default connected-component slicing behaviour for ordinary free-form icon sheets.
## Common Values ## Common Values
Use OpenAPI as the final authority; these common values are a routing aid: Use OpenAPI as the final authority; these common values are a routing aid:
@@ -78,9 +78,9 @@ Keep the existing autonomous-build task graph. Do not add a parallel task system
1. `art-director` generates `assets/art-spec.png` with image generation, `kind: "spec"`, then registers it as `assetKind: "icon-spec"`. This image is the authoritative visual spec; `generationInputs.artSpec` is supporting structured context. 1. `art-director` generates `assets/art-spec.png` with image generation, `kind: "spec"`, then registers it as `assetKind: "icon-spec"`. This image is the authoritative visual spec; `generationInputs.artSpec` is supporting structured context.
2. `design-foundation` generates `assets/ui-prototype.png` with `kind: "ui-design"`, using the registered art-spec resource ID in `referenceImageSrcs`. 2. `design-foundation` generates `assets/ui-prototype.png` with `kind: "ui-design"`, using the registered art-spec resource ID in `referenceImageSrcs`.
3. `art-asset-plan` generates transparent `assets/art-spritesheet.png` through icon spritesheet generation, using the same registered art-spec resource ID as `referenceId` plus concrete `iconDescriptions`. 3. `art-asset-plan` generates transparent `assets/art-spritesheet.png` through icon spritesheet generation, using the same registered art-spec resource ID as `referenceId` plus concrete `iconDescriptions`. For the four-category game contract it must also send `sliceLayout: "grid-2x2"`; this is an explicit fixed-slot contract, not a client-side guessed crop.
For a playable Canvas game, do not stop at generation. Make `code-prototype` depend on `art-asset-plan` and consume the persisted `iconImageSrcs` slices for core players, blocks or targets, scene obstacles, and feedback. For the four-category game-chat contract, require exactly four slices before registering the local runtime sheet; both fewer and extra components fail closed. Treat `art-spec.png` as reference-only. A full-sheet `<img>`, CSS background, path-only mention, guessed equal-grid crop, or code-drawn replacement for core entities is not runtime asset use. If slicing produces `sliceWarning`, keep the complete transparent sheet as a valid editor artifact, but fail the playable game asset gate until real slice files or verified atlas coordinates exist; never invent coordinates or replace the icon-spritesheet route with ordinary image generation. For a playable Canvas game, do not stop at generation. Make `code-prototype` depend on `art-asset-plan` and consume the persisted `iconImageSrcs` slices for core players, blocks or targets, scene obstacles, and feedback. For the four-category game-chat contract, require response `sliceLayout: "grid-2x2"` and exactly four slices before registering the local runtime sheet; both fewer and extra components fail closed. Treat `art-spec.png` as reference-only. A full-sheet `<img>`, CSS background, path-only mention, guessed equal-grid crop, or code-drawn replacement for core entities is not runtime asset use. If slicing produces `sliceWarning`, keep the complete transparent sheet as a valid editor artifact, but fail the playable game asset gate until real slice files or verified atlas coordinates exist; never invent coordinates or replace the icon-spritesheet route with ordinary image generation.
Never use `assets/ui-prototype.png` as the spritesheet visual-spec reference. UI extraction is outside this canonical DAG. Never use `assets/ui-prototype.png` as the spritesheet visual-spec reference. UI extraction is outside this canonical DAG.
+3
View File
@@ -69,6 +69,9 @@ jobs:
- name: Install npm dependencies - name: Install npm dependencies
run: bash scripts/ci-npm-ci-with-retry.sh run: bash scripts/ci-npm-ci-with-retry.sh
- name: Install AI game creator dependencies
run: bash scripts/ci-npm-ci-with-retry.sh --prefix apps/ai-game-creator-shell
- name: Run repository checks - name: Run repository checks
run: npm run check:repository-ci run: npm run check:repository-ci
@@ -2,11 +2,11 @@
"agentMode": "codex_app_server", "agentMode": "codex_app_server",
"llm": { "llm": {
"apiKey": "", "apiKey": "",
"baseUrl": "https://api.openai.com/v1", "baseUrl": "https://dev.genarrative.world/gpt/v1",
"model": "gpt-4.1", "model": "gpt-5.6-sol",
"apiKind": "openai_responses", "apiKind": "openai_responses",
"reasoningEffort": "high", "reasoningEffort": "max",
"stream": false, "stream": true,
"webSearchEnabled": false, "webSearchEnabled": false,
"contextWindowTokens": 128000, "contextWindowTokens": 128000,
"autoCompactTokenLimit": 64000, "autoCompactTokenLimit": 64000,
@@ -16,10 +16,6 @@
"retryBackoffMs": 500 "retryBackoffMs": 500
}, },
"agentLlm": {}, "agentLlm": {},
"editorApi": {
"baseUrl": "http://127.0.0.1:8082",
"apiKey": ""
},
"mcpServers": {}, "mcpServers": {},
"planning": { "planning": {
"capabilityEnabled": true "capabilityEnabled": true
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="data:," /> <link rel="icon" href="data:," />
<title>AI 游戏创作</title> <title>陶泥儿</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+1
View File
@@ -24,6 +24,7 @@
"agent-runtime:mixed-swarm-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-static-isolated-autonomous-chat", "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-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-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-autonomous-playable-lane-defense",
"agent-runtime:supervisor-game-chat-single-main-playable-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-game-chat-single-main-playable",
"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-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-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-transient-retry-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-transient-retry",
File diff suppressed because it is too large Load Diff
@@ -2406,11 +2406,36 @@ export function isolatedJoinDeliveryTarget(delivery) {
return target; return target;
} }
export function finalMessageId(agentId, sessionId, runId) { function runtimeMessageCorrelationId(agentId, sessionId, runId) {
const fingerprint = createHash('sha256') return createHash('sha256')
.update(`${agentId}\n${sessionId}\n${runId}`) .update(`${agentId}\n${sessionId}\n${runId}`)
.digest('hex'); .digest('hex');
return `agent-finalization-${fingerprint.slice(0, 32)}`; }
export function finalMessageId(agentId, sessionId, runId) {
return `agent-finalization-${runtimeMessageCorrelationId(
agentId,
sessionId,
runId,
).slice(0, 32)}`;
}
export function runtimePublicStatusMessageId(
agentId,
sessionId,
runId,
status,
) {
const correlationId = runtimeMessageCorrelationId(
agentId,
sessionId,
runId,
).slice(0, 32);
const statusFingerprint = createHash('sha256')
.update(status)
.digest('hex')
.slice(0, 16);
return `runtime-public-status-${correlationId}-${statusFingerprint}`;
} }
export function backgroundTaskMessageId(agentId, sessionId, runId, source) { export function backgroundTaskMessageId(agentId, sessionId, runId, source) {
@@ -2444,7 +2469,10 @@ export function disposableProjectPathVariants() {
} }
export function formalConfigPathVariants() { export function formalConfigPathVariants() {
return absolutePathVariants(state.options?.configDir); return absolutePathVariants(
state.options?.configDir,
state.isolatedRunner?.appDataDir,
);
} }
export function absolutePathVariants(...values) { export function absolutePathVariants(...values) {
@@ -20,7 +20,15 @@ import {
stopOwnedIsolatedRunner, stopOwnedIsolatedRunner,
} from './harness/app-data.mjs'; } from './harness/app-data.mjs';
import { loadConfig, parseArguments } from './harness/config.mjs'; import { loadConfig, parseArguments } from './harness/config.mjs';
import { closeInteractiveCli } from './harness/process.mjs'; import {
activeInteractiveCliSessions,
captureOwnedProcessCleanupSnapshot,
closeInteractiveCli,
destroyInteractiveCliOutputStreams,
interactiveCliOutput,
verifyOwnedProcessCleanupSnapshot,
waitForInteractiveCliStdioClose,
} from './harness/process.mjs';
import { checkPrerequisites } from './harness/project.mjs'; import { checkPrerequisites } from './harness/project.mjs';
import { import {
buildSummary, buildSummary,
@@ -266,6 +274,21 @@ if (selfTestRequested) {
recordError(error?.code ?? 'unexpected-error', error); recordError(error?.code ?? 'unexpected-error', error);
} finally { } finally {
state.cleanupInProgress = true; state.cleanupInProgress = true;
const stateTrackedInteractiveCliSessions = new Set(
[
state.userInputCliSession,
state.supervisorAutonomousPlayableCliSession,
state.supervisorSwarmCliSession,
].filter(Boolean),
);
const interactiveCliSessions = [
...new Set([
...stateTrackedInteractiveCliSessions,
...activeInteractiveCliSessions,
]),
];
const supervisorAutonomousPlayableCliSession =
state.supervisorAutonomousPlayableCliSession;
if (isUserInputRuntimeSuite() && state.userInputCliSession) { if (isUserInputRuntimeSuite() && state.userInputCliSession) {
try { try {
await closeInteractiveCli(state.userInputCliSession); await closeInteractiveCli(state.userInputCliSession);
@@ -311,6 +334,15 @@ if (selfTestRequested) {
} }
state.supervisorSwarmCliSession = null; state.supervisorSwarmCliSession = null;
} }
for (const session of interactiveCliSessions) {
if (stateTrackedInteractiveCliSessions.has(session)) continue;
try {
await closeInteractiveCli(session);
} catch (error) {
state.status = 'FAIL';
recordError('interactive-cli-cleanup-failed', error);
}
}
if (isMcpRuntimeSuite() && state.mcp.httpFixture) { if (isMcpRuntimeSuite() && state.mcp.httpFixture) {
try { try {
await stopMcpHttpFixture(); await stopMcpHttpFixture();
@@ -320,16 +352,42 @@ if (selfTestRequested) {
recordError('mcp-http-fixture-cleanup-failed', error); recordError('mcp-http-fixture-cleanup-failed', error);
} }
} }
if (
isSupervisorAutonomousPlayableLaneDefenseSuite() &&
state.isolatedRunner.appDataDir
) {
try {
const runnerPid = state.isolatedRunner.current?.pid ?? null;
const helperPid =
state.isolatedRunner.current?.killHandle?.child?.pid ?? null;
state.supervisorAutonomousPlayable.ownedProcessCleanupSnapshot =
await captureOwnedProcessCleanupSnapshot({
runnerPid,
helperPids: Number.isSafeInteger(helperPid) ? [helperPid] : [],
rootPids: [
...interactiveCliSessions.map((session) => session.child?.pid),
...[...activeCommandChildren].map((child) => child.pid),
].filter((pid) => Number.isSafeInteger(pid) && pid > 0),
});
const observed =
state.supervisorAutonomousPlayable.ownedProcessCleanupSnapshot
.observedCounts;
assert(
observed.runner === 1 && observed.helper === 1,
'supervisor-autonomous-playable-owned-process-snapshot-incomplete',
);
} catch (error) {
state.status = 'FAIL';
recordError(
'supervisor-autonomous-playable-owned-process-snapshot-failed',
error,
);
}
}
if (isIsolatedRunnerSuite() && state.isolatedRunner.appDataDir) { if (isIsolatedRunnerSuite() && state.isolatedRunner.appDataDir) {
try { try {
await stopOwnedIsolatedRunner(); await stopOwnedIsolatedRunner();
state.isolatedRunner.stopped = true; state.isolatedRunner.stopped = true;
state.isolatedRunner.cleanupPerformed =
await removeIsolatedSuiteAppData();
if (!state.isolatedRunner.cleanupPerformed) {
state.status = 'FAIL';
recordError('isolated-appdata-cleanup-sentinel-missing');
}
} catch (error) { } catch (error) {
state.status = 'FAIL'; state.status = 'FAIL';
const safeCleanupErrorCode = const safeCleanupErrorCode =
@@ -342,8 +400,51 @@ if (selfTestRequested) {
state.isolatedRunner.current?.killHandle, state.isolatedRunner.current?.killHandle,
).catch(() => {}); ).catch(() => {});
} }
}
for (const session of interactiveCliSessions) {
try {
await waitForInteractiveCliStdioClose(session, 10_000);
} catch (error) {
destroyInteractiveCliOutputStreams(session);
state.status = 'FAIL';
recordError(
error?.code === 'interactive-cli-stdio-close-timeout'
? error.code
: 'interactive-cli-stdio-cleanup-failed',
error,
);
}
}
if (supervisorAutonomousPlayableCliSession) {
state.supervisorAutonomousPlayable.cliOutput = interactiveCliOutput(
supervisorAutonomousPlayableCliSession,
);
}
if (isIsolatedRunnerSuite() && state.isolatedRunner.appDataDir) {
if (state.isolatedRunner.stopped) {
try {
state.isolatedRunner.cleanupPerformed =
await removeIsolatedSuiteAppData();
if (!state.isolatedRunner.cleanupPerformed) {
state.status = 'FAIL';
recordError('isolated-appdata-cleanup-sentinel-missing');
}
} catch (error) {
state.status = 'FAIL';
const safeCleanupErrorCode =
isNonEmptyString(error?.code) &&
/^(?:isolated|source)-[a-z0-9-]+$/u.test(error.code)
? error.code
: 'isolated-appdata-cleanup-failed';
recordError(safeCleanupErrorCode, error);
}
}
const killMethod = const killMethod =
state.isolatedRunner.pidfdClaimCount > 0 ? 'linux-pidfd' : null; state.isolatedRunner.pidfdClaimCount > 0
? process.platform === 'win32'
? 'windows-process-handle'
: 'linux-pidfd'
: null;
if (isSteerRunnerKillSuite()) { if (isSteerRunnerKillSuite()) {
state.evidence.steerRunnerStopped = state.isolatedRunner.stopped; state.evidence.steerRunnerStopped = state.isolatedRunner.stopped;
state.evidence.steerAppDataCleanupPerformed = state.evidence.steerAppDataCleanupPerformed =
@@ -661,6 +762,61 @@ if (selfTestRequested) {
); );
} }
} }
if (
isSupervisorAutonomousPlayableLaneDefenseSuite() &&
state.isolatedRunner.appDataDir
) {
const snapshot =
state.supervisorAutonomousPlayable.ownedProcessCleanupSnapshot;
if (snapshot) {
try {
const cleanup = await verifyOwnedProcessCleanupSnapshot(snapshot);
state.evidence.ownedProcessIdentityCaptured = true;
state.evidence.ownedRunnerObservedCount =
snapshot.observedCounts.runner;
state.evidence.ownedHelperObservedCount =
snapshot.observedCounts.helper;
state.evidence.ownedNodeDescendantObservedCount =
snapshot.observedCounts.node;
state.evidence.ownedBrowserDescendantObservedCount =
snapshot.observedCounts.browser;
state.evidence.ownedCommandDescendantObservedCount =
snapshot.observedCounts.command;
state.evidence.ownedRunnerResidualCount =
cleanup.residualCounts.runner;
state.evidence.ownedHelperResidualCount =
cleanup.residualCounts.helper;
state.evidence.ownedNodeDescendantResidualCount =
cleanup.residualCounts.node;
state.evidence.ownedBrowserDescendantResidualCount =
cleanup.residualCounts.browser;
state.evidence.ownedCommandDescendantResidualCount =
cleanup.residualCounts.command;
state.evidence.activeCommandChildrenAfterCleanup =
cleanup.activeCommandChildCount;
state.evidence.activeInteractiveCliSessionsAfterCleanup =
cleanup.activeInteractiveCliSessionCount;
state.evidence.ownedProcessCleanupPassed = cleanup.clean;
if (!cleanup.clean) {
state.status = 'FAIL';
recordError(
'supervisor-autonomous-playable-owned-process-residual-detected',
);
}
} catch (error) {
state.status = 'FAIL';
recordError(
'supervisor-autonomous-playable-owned-process-verification-failed',
error,
);
}
} else {
state.status = 'FAIL';
recordError(
'supervisor-autonomous-playable-owned-process-snapshot-missing',
);
}
}
if ( if (
isSteerRunnerKillSuite() && isSteerRunnerKillSuite() &&
state.projectRoot && state.projectRoot &&
@@ -1210,6 +1366,7 @@ if (selfTestRequested) {
const safeSummary = { const safeSummary = {
status: state.status, status: state.status,
suite: state.suite, suite: state.suite,
providerUsed: false,
blocked: state.blocked, blocked: state.blocked,
cleanup: { cleanup: {
performed: state.cleanupPerformed, performed: state.cleanupPerformed,
File diff suppressed because it is too large Load Diff
@@ -15,6 +15,7 @@ import {
scopedAgentsSuite, scopedAgentsSuite,
steerRunnerKillSuite, steerRunnerKillSuite,
supervisorAutonomousPlayableLaneDefenseSuite, supervisorAutonomousPlayableLaneDefenseSuite,
supervisorGameChatSingleMainPlayableSuite,
supervisorSwarmAutonomousChatSuite, supervisorSwarmAutonomousChatSuite,
supervisorSwarmCollaborationPolicyMixedRecoverySuite, supervisorSwarmCollaborationPolicyMixedRecoverySuite,
supervisorSwarmFinalReplyTransientRetrySuite, supervisorSwarmFinalReplyTransientRetrySuite,
@@ -70,6 +71,7 @@ export function parseArguments(args) {
suite === supervisorSwarmToolPlanHandoffRunnerKillSuite || suite === supervisorSwarmToolPlanHandoffRunnerKillSuite ||
suite === supervisorSwarmAutonomousChatSuite || suite === supervisorSwarmAutonomousChatSuite ||
suite === supervisorAutonomousPlayableLaneDefenseSuite || suite === supervisorAutonomousPlayableLaneDefenseSuite ||
suite === supervisorGameChatSingleMainPlayableSuite ||
suite === supervisorSwarmStaticIsolatedAutonomousChatSuite || suite === supervisorSwarmStaticIsolatedAutonomousChatSuite ||
suite === supervisorSwarmCollaborationPolicyMixedRecoverySuite || suite === supervisorSwarmCollaborationPolicyMixedRecoverySuite ||
suite === steerRunnerKillSuite || suite === steerRunnerKillSuite ||
@@ -19,6 +19,8 @@ import {
} from '../suites/supervisor-swarm.mjs'; } from '../suites/supervisor-swarm.mjs';
import { isIsolatedRunnerSuite } from './reporting.mjs'; import { isIsolatedRunnerSuite } from './reporting.mjs';
export const activeInteractiveCliSessions = new Set();
export async function prepareCliBinary() { export async function prepareCliBinary() {
const cargo = process.platform === 'win32' ? 'cargo.exe' : 'cargo'; const cargo = process.platform === 'win32' ? 'cargo.exe' : 'cargo';
await runProcess( await runProcess(
@@ -154,36 +156,70 @@ export function startInteractiveCli(args) {
stdio: ['pipe', 'pipe', 'pipe'], stdio: ['pipe', 'pipe', 'pipe'],
}, },
); );
return createInteractiveCliSession(child);
}
export function createInteractiveCliSession(child) {
activeCommandChildren.add(child); activeCommandChildren.add(child);
const session = { const session = {
child, child,
stdout: Buffer.alloc(0), stdout: Buffer.alloc(0),
stderr: Buffer.alloc(0), stderr: Buffer.alloc(0),
exited: false,
exitInfo: null,
exitPromise: null,
closed: false, closed: false,
closeInfo: null, closeInfo: null,
closePromise: null, closePromise: null,
stdioClosed: false,
stdioCloseInfo: null,
spawnError: null,
stdinError: null,
}; };
activeInteractiveCliSessions.add(session);
session.exitPromise = new Promise((resolve) => {
const settle = (result) => {
if (session.exited) return;
activeCommandChildren.delete(child);
session.exited = true;
session.closed = true;
session.exitInfo = result;
session.closeInfo = result;
resolve(result);
};
child.once('error', (error) => {
session.spawnError = error;
settle({ code: null, signal: null, error });
});
child.once('exit', (code, signal) => {
settle({ code, signal, error: null });
});
});
session.closePromise = new Promise((resolve) => { session.closePromise = new Promise((resolve) => {
child.on('error', (error) => { child.once('close', (code, signal) => {
activeCommandChildren.delete(child); activeCommandChildren.delete(child);
session.closed = true; activeInteractiveCliSessions.delete(session);
session.closeInfo = { code: null, signal: null, error }; session.stdioClosed = true;
resolve(session.closeInfo); session.stdioCloseInfo = {
}); code,
child.on('close', (code, signal) => { signal,
activeCommandChildren.delete(child); error: session.spawnError,
session.closed = true; };
session.closeInfo = { code, signal, error: null }; resolve(session.stdioCloseInfo);
resolve(session.closeInfo);
}); });
}); });
child.stdin?.on('error', (error) => {
session.stdinError ??= error;
});
child.stdout.on('data', (chunk) => { child.stdout.on('data', (chunk) => {
state.transcriptScanner?.scan('interactive-stdout', chunk); state.transcriptScanner?.scan('interactive-stdout', chunk);
state.projectPathTranscriptScanner?.scan('interactive-stdout', chunk);
state.formalConfigPathTranscriptScanner?.scan('interactive-stdout', chunk); state.formalConfigPathTranscriptScanner?.scan('interactive-stdout', chunk);
session.stdout = appendBounded(session.stdout, chunk, commandOutputLimit); session.stdout = appendBounded(session.stdout, chunk, commandOutputLimit);
}); });
child.stderr.on('data', (chunk) => { child.stderr.on('data', (chunk) => {
state.transcriptScanner?.scan('interactive-stderr', chunk); state.transcriptScanner?.scan('interactive-stderr', chunk);
state.projectPathTranscriptScanner?.scan('interactive-stderr', chunk);
state.formalConfigPathTranscriptScanner?.scan('interactive-stderr', chunk); state.formalConfigPathTranscriptScanner?.scan('interactive-stderr', chunk);
session.stderr = appendBounded(session.stderr, chunk, commandOutputLimit); session.stderr = appendBounded(session.stderr, chunk, commandOutputLimit);
}); });
@@ -205,16 +241,23 @@ export async function waitForInteractiveCliOutput(
predicate, predicate,
code, code,
timeoutMs, timeoutMs,
{ allowAfterProcessExit = false } = {},
) { ) {
const deadline = Date.now() + timeoutMs; const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) { while (Date.now() < deadline) {
const output = interactiveCliOutput(session); const output = interactiveCliOutput(session);
if (predicate(output)) return output; if (predicate(output)) return output;
if (session.closed) { if (session.exited && !allowAfterProcessExit) {
if (session === state.supervisorSwarmCliSession) { if (session === state.supervisorSwarmCliSession) {
recordSupervisorSwarmChatSessionFailureDiagnostic(session); recordSupervisorSwarmChatSessionFailureDiagnostic(session);
} }
throw codedError(`${code}-cli-closed`); throw codedError(`${code}-cli-exited`);
}
if (session.stdioClosed) {
if (session === state.supervisorSwarmCliSession) {
recordSupervisorSwarmChatSessionFailureDiagnostic(session);
}
throw codedError(`${code}-cli-stdio-closed`);
} }
await sleep(50); await sleep(50);
} }
@@ -223,7 +266,7 @@ export async function waitForInteractiveCliOutput(
export async function waitForInteractiveCliExit(session, timeoutMs) { export async function waitForInteractiveCliExit(session, timeoutMs) {
const result = await Promise.race([ const result = await Promise.race([
session.closePromise, session.exitPromise,
sleep(timeoutMs).then(() => null), sleep(timeoutMs).then(() => null),
]); ]);
if (!result) throw codedError('interactive-cli-exit-timeout'); if (!result) throw codedError('interactive-cli-exit-timeout');
@@ -236,26 +279,52 @@ export async function waitForInteractiveCliExit(session, timeoutMs) {
} }
export async function closeInteractiveCli(session) { export async function closeInteractiveCli(session) {
if (!session || session.closed) return; if (!session) return null;
if (session.child.stdin.writable) { if (session.exited) return session.exitInfo;
if (
session.child.stdin.writable &&
!session.child.stdin.writableEnded &&
!session.child.stdin.destroyed
) {
session.child.stdin.write('/quit\n'); session.child.stdin.write('/quit\n');
} }
let result = await Promise.race([ let result = await Promise.race([
session.closePromise, session.exitPromise,
sleep(3_000).then(() => null), sleep(3_000).then(() => null),
]); ]);
if (!result && !session.closed) { if (!result && !session.exited) {
session.child.kill('SIGTERM'); session.child.kill('SIGTERM');
result = await Promise.race([ result = await Promise.race([
session.closePromise, session.exitPromise,
sleep(2_000).then(() => null), sleep(2_000).then(() => null),
]); ]);
} }
if (!result && !session.closed) { if (!result && !session.exited) {
session.child.kill('SIGKILL'); session.child.kill('SIGKILL');
result = await session.closePromise; result = await Promise.race([
session.exitPromise,
sleep(5_000).then(() => null),
]);
} }
assert(Boolean(result), 'interactive-cli-cleanup-timeout'); assert(Boolean(result), 'interactive-cli-cleanup-timeout');
return result;
}
export async function waitForInteractiveCliStdioClose(session, timeoutMs) {
if (!session || session.stdioClosed) return session?.stdioCloseInfo ?? null;
const result = await Promise.race([
session.closePromise,
sleep(timeoutMs).then(() => null),
]);
if (!result) throw codedError('interactive-cli-stdio-close-timeout');
return result;
}
export function destroyInteractiveCliOutputStreams(session) {
if (!session) return;
for (const stream of [session.child.stdout, session.child.stderr]) {
if (stream && !stream.destroyed) stream.destroy();
}
} }
export async function runProcess( export async function runProcess(
@@ -329,6 +398,214 @@ export async function runProcess(
}); });
} }
export async function listSystemProcessIdentities() {
if (process.platform === 'win32') {
const systemRoot = process.env.SystemRoot ?? process.env.SYSTEMROOT;
assert(
typeof systemRoot === 'string' && path.isAbsolute(systemRoot),
'owned-process-snapshot-system-root-invalid',
);
const powershell = path.join(
systemRoot,
'System32/WindowsPowerShell/v1.0/powershell.exe',
);
const metadata = await fs.lstat(powershell);
assert(
metadata.isFile() && !metadata.isSymbolicLink(),
'owned-process-snapshot-powershell-invalid',
);
const result = await runProcess(
powershell,
[
'-NoProfile',
'-NonInteractive',
'-Command',
'$processes = @(Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,CreationDate,Name); $processes | ConvertTo-Json -Compress',
],
{
cwd: appRoot,
timeoutMs: 30_000,
env: { ...process.env, NO_COLOR: '1', RUST_BACKTRACE: '0' },
},
);
const parsed = JSON.parse(result.stdout);
return (Array.isArray(parsed) ? parsed : [parsed])
.map((record) => ({
pid: Number(record?.ProcessId),
parentPid: Number(record?.ParentProcessId),
startedAt: String(record?.CreationDate ?? ''),
name: String(record?.Name ?? ''),
}))
.filter(validSystemProcessIdentity);
}
assert(
process.platform === 'linux' || process.platform === 'darwin',
'owned-process-snapshot-platform-unsupported',
);
const result = await runProcess(
'ps',
['-A', '-o', 'pid=', '-o', 'ppid=', '-o', 'lstart=', '-o', 'comm='],
{ cwd: appRoot, timeoutMs: 30_000 },
);
return result.stdout
.split(/\r?\n/u)
.map((line) => line.trim())
.filter(Boolean)
.map((line) => {
const fields = line.split(/\s+/u);
return {
pid: Number(fields[0]),
parentPid: Number(fields[1]),
startedAt: fields.slice(2, 7).join(' '),
name: fields.slice(7).join(' '),
};
})
.filter(validSystemProcessIdentity);
}
function validSystemProcessIdentity(record) {
return (
Number.isSafeInteger(record?.pid) &&
record.pid > 0 &&
Number.isSafeInteger(record.parentPid) &&
record.parentPid >= 0 &&
typeof record.startedAt === 'string' &&
record.startedAt.length > 0 &&
typeof record.name === 'string' &&
record.name.length > 0
);
}
export function buildOwnedProcessCleanupSnapshot(
processRecords,
{ rootPids = [], runnerPid = null, helperPids = [] } = {},
) {
assert(
Array.isArray(processRecords) &&
Array.isArray(rootPids) &&
Array.isArray(helperPids),
'owned-process-snapshot-input-invalid',
);
const records = processRecords.filter(validSystemProcessIdentity);
const byPid = new Map(records.map((record) => [record.pid, record]));
const childrenByParent = new Map();
for (const record of records) {
const children = childrenByParent.get(record.parentPid) ?? [];
children.push(record.pid);
childrenByParent.set(record.parentPid, children);
}
const normalizedRunnerPid = Number.isSafeInteger(runnerPid)
? runnerPid
: null;
const helperPidSet = new Set(
helperPids.filter((pid) => Number.isSafeInteger(pid) && pid > 0),
);
const roots = [
...new Set(
[...rootPids, normalizedRunnerPid, ...helperPidSet].filter(
(pid) => Number.isSafeInteger(pid) && pid > 0,
),
),
];
assert(roots.length > 0, 'owned-process-snapshot-root-missing');
const ownedPids = new Set();
const queue = [...roots];
while (queue.length > 0) {
const pid = queue.shift();
if (ownedPids.has(pid)) continue;
ownedPids.add(pid);
queue.push(...(childrenByParent.get(pid) ?? []));
}
const identities = [...ownedPids]
.map((pid) => byPid.get(pid))
.filter(Boolean)
.map((record) => ({
pid: record.pid,
startedAt: record.startedAt,
name: record.name,
kind: ownedProcessKind(record, normalizedRunnerPid, helperPidSet),
}))
.sort((left, right) => left.pid - right.pid);
return {
identities,
observedCounts: countOwnedProcessKinds(identities),
};
}
function ownedProcessKind(record, runnerPid, helperPids) {
if (record.pid === runnerPid) return 'runner';
if (helperPids.has(record.pid)) return 'helper';
const name = path.basename(record.name).toLowerCase();
if (/^node(?:\.exe)?$/u.test(name)) return 'node';
if (/^(?:chrome|chromium|msedge|google-chrome)(?:\.exe)?$/u.test(name)) {
return 'browser';
}
return 'command';
}
function countOwnedProcessKinds(identities) {
const counts = {
runner: 0,
helper: 0,
node: 0,
browser: 0,
command: 0,
total: identities.length,
};
for (const identity of identities) counts[identity.kind] += 1;
return counts;
}
export function inspectOwnedProcessCleanupResiduals(
snapshot,
processRecords,
{ activeCommandChildCount = 0, activeInteractiveCliSessionCount = 0 } = {},
) {
assert(
Array.isArray(snapshot?.identities) && Array.isArray(processRecords),
'owned-process-residual-input-invalid',
);
const currentByPid = new Map(
processRecords
.filter(validSystemProcessIdentity)
.map((record) => [record.pid, record]),
);
const residualIdentities = snapshot.identities.filter((identity) => {
const current = currentByPid.get(identity.pid);
return (
current?.startedAt === identity.startedAt &&
current?.name === identity.name
);
});
return {
residualCounts: countOwnedProcessKinds(residualIdentities),
activeCommandChildCount,
activeInteractiveCliSessionCount,
clean:
residualIdentities.length === 0 &&
activeCommandChildCount === 0 &&
activeInteractiveCliSessionCount === 0,
};
}
export async function captureOwnedProcessCleanupSnapshot(options) {
return buildOwnedProcessCleanupSnapshot(
await listSystemProcessIdentities(),
options,
);
}
export async function verifyOwnedProcessCleanupSnapshot(snapshot) {
return inspectOwnedProcessCleanupResiduals(
snapshot,
await listSystemProcessIdentities(),
{
activeCommandChildCount: activeCommandChildren.size,
activeInteractiveCliSessionCount: activeInteractiveCliSessions.size,
},
);
}
export function appendBounded(current, chunk, limit) { export function appendBounded(current, chunk, limit) {
const combined = Buffer.concat([current, chunk]); const combined = Buffer.concat([current, chunk]);
return combined.length <= limit return combined.length <= limit
@@ -1,4 +1,4 @@
import { assert } from '../assertions/core.mjs'; import { assert, hashValue } from '../assertions/core.mjs';
import { disposableProjectPathVariants } from '../assertions/runtime.mjs'; import { disposableProjectPathVariants } from '../assertions/runtime.mjs';
import { fs, os, path, randomUUID } from '../dependencies.mjs'; import { fs, os, path, randomUUID } from '../dependencies.mjs';
import { import {
@@ -28,7 +28,10 @@ import {
isGoalRuntimeSuite, isGoalRuntimeSuite,
} from '../suites/goal.mjs'; } from '../suites/goal.mjs';
import { isResponseStreamSuite } from '../suites/response-stream.mjs'; import { isResponseStreamSuite } from '../suites/response-stream.mjs';
import { isSupervisorAutonomousPlayableLaneDefenseSuite } from '../suites/supervisor-autonomous-playable.mjs'; import {
isSupervisorAutonomousPlayableLaneDefenseSuite,
isSupervisorGameChatSingleMainPlayableSuite,
} from '../suites/supervisor-autonomous-playable.mjs';
import { import {
isSupervisorSwarmSuite, isSupervisorSwarmSuite,
supervisorSwarmVerificationFixtureSource, supervisorSwarmVerificationFixtureSource,
@@ -40,27 +43,97 @@ import { effectiveAgentLlmConfig } from './config.mjs';
import { runProcess } from './process.mjs'; import { runProcess } from './process.mjs';
import { isIsolatedRunnerSuite } from './reporting.mjs'; import { isIsolatedRunnerSuite } from './reporting.mjs';
export async function checkPrerequisites(config) { export function requiredAgentIdsForSuite() {
const requiredAgents = isUserInputRuntimeSuite() return isUserInputRuntimeSuite()
? [projectSupervisorAgentId] ? [projectSupervisorAgentId]
: isSupervisorAutonomousPlayableLaneDefenseSuite() : isSupervisorGameChatSingleMainPlayableSuite()
? [projectSupervisorAgentId] ? [
: isSupervisorSwarmSuite() projectSupervisorAgentId,
? [ mainAgentId,
projectSupervisorAgentId, 'art-director',
supervisorSwarmDesignAgentId, 'art-asset-plan',
supervisorSwarmQualityAgentId, ]
] : isSupervisorAutonomousPlayableLaneDefenseSuite()
: isIsolatedRunnerSuite() ? [projectSupervisorAgentId]
? [mainAgentId] : isSupervisorSwarmSuite()
: [mainAgentId, 'quality-review']; ? [
const llmConfigured = requiredAgents.every((agentId) => { projectSupervisorAgentId,
supervisorSwarmDesignAgentId,
supervisorSwarmQualityAgentId,
]
: isIsolatedRunnerSuite()
? [mainAgentId]
: [mainAgentId, 'quality-review'];
}
export function expectedProviderBindingForSuite(config) {
if (
isSupervisorGameChatSingleMainPlayableSuite() &&
config.agentMode !== 'provider'
) {
return null;
}
const agentIds = requiredAgentIdsForSuite();
const effectiveConfigs = agentIds.map((agentId) =>
effectiveAgentLlmConfig(config, agentId),
);
if (
effectiveConfigs.length === 0 ||
effectiveConfigs.some((effective) =>
['apiKey', 'baseUrl', 'model', 'apiKind', 'reasoningEffort'].some(
(key) =>
typeof effective[key] !== 'string' ||
effective[key].trim().length === 0,
),
)
) {
return null;
}
const [expected] = effectiveConfigs;
const expectedIdentity = [
expected.model.trim(),
expected.apiKind.trim(),
expected.reasoningEffort.trim(),
expected.baseUrl.trim(),
];
if (
effectiveConfigs.some(
(effective) =>
JSON.stringify([
effective.model.trim(),
effective.apiKind.trim(),
effective.reasoningEffort.trim(),
effective.baseUrl.trim(),
]) !== JSON.stringify(expectedIdentity),
)
) {
return null;
}
return {
...(isSupervisorGameChatSingleMainPlayableSuite()
? { providerAgentMode: 'provider' }
: {}),
providerModel: expectedIdentity[0],
providerApiKind: expectedIdentity[1],
providerReasoningEffort: expectedIdentity[2],
providerBaseUrlSha256: hashValue(expectedIdentity[3]),
boundAgentIds: [...agentIds].sort(),
};
}
export async function checkPrerequisites(config) {
const requiredAgents = requiredAgentIdsForSuite();
let llmConfigured = requiredAgents.every((agentId) => {
const effective = effectiveAgentLlmConfig(config, agentId); const effective = effectiveAgentLlmConfig(config, agentId);
return ['apiKey', 'baseUrl', 'model'].every( return ['apiKey', 'baseUrl', 'model'].every(
(key) => (key) =>
typeof effective[key] === 'string' && effective[key].trim().length > 0, typeof effective[key] === 'string' && effective[key].trim().length > 0,
); );
}); });
const providerBinding = expectedProviderBindingForSuite(config);
if (isSupervisorGameChatSingleMainPlayableSuite()) {
llmConfigured = llmConfigured && providerBinding !== null;
}
const editorApiConfigured = ['apiKey', 'baseUrl'].every( const editorApiConfigured = ['apiKey', 'baseUrl'].every(
(key) => (key) =>
typeof config.editorApi?.[key] === 'string' && typeof config.editorApi?.[key] === 'string' &&
@@ -68,6 +141,7 @@ export async function checkPrerequisites(config) {
); );
return { return {
llmConfigured, llmConfigured,
providerBinding,
chromeAvailable: chromeAvailable:
!isIsolatedRunnerSuite() || !isIsolatedRunnerSuite() ||
isSupervisorAutonomousPlayableLaneDefenseSuite() isSupervisorAutonomousPlayableLaneDefenseSuite()
@@ -150,7 +224,9 @@ export function supportedBrowserCandidates(platform, environment) {
return candidates; return candidates;
} }
export async function seedDisposableProject() { export async function seedDisposableProject({
preserveProductionInitBaseline = false,
} = {}) {
const prefix = path.join(os.tmpdir(), 'genarrative-agent-runtime-real-e2e-'); const prefix = path.join(os.tmpdir(), 'genarrative-agent-runtime-real-e2e-');
const sentinelToken = randomUUID(); const sentinelToken = randomUUID();
state.projectRoot = await createSentinelOwnedTempDirectory({ state.projectRoot = await createSentinelOwnedTempDirectory({
@@ -189,37 +265,42 @@ export async function seedDisposableProject() {
...(isResponseStreamSuite() ? [responseStreamThinkingCanary] : []), ...(isResponseStreamSuite() ? [responseStreamThinkingCanary] : []),
]; ];
const generatedBaselineWrites = preserveProductionInitBaseline
? []
: [
fs.writeFile(
path.join(state.projectRoot, 'package.json'),
`${JSON.stringify(
{
name: 'genarrative-agent-runtime-real-e2e-project',
private: true,
scripts: {
test: verificationCommand,
'check:e2e': verificationCommand,
},
},
null,
2,
)}\n`,
),
fs.writeFile(
path.join(state.projectRoot, 'verify-e2e.mjs'),
isSupervisorSwarmSuite()
? supervisorSwarmVerificationFixtureSource()
: isGoalRuntimeSuite() ||
isResponseStreamSuite() ||
isWebSearchSuite() ||
isSupervisorAutonomousPlayableLaneDefenseSuite()
? goalRevisionOneVerificationFixtureSource()
: goalRevisionTwoVerificationFixtureSource(),
),
fs.writeFile(
path.join(state.projectRoot, 'game/index.html'),
seededGameHtml(),
),
];
await Promise.all([ await Promise.all([
fs.writeFile( ...generatedBaselineWrites,
path.join(state.projectRoot, 'package.json'),
`${JSON.stringify(
{
name: 'genarrative-agent-runtime-real-e2e-project',
private: true,
scripts: {
test: verificationCommand,
'check:e2e': verificationCommand,
},
},
null,
2,
)}\n`,
),
fs.writeFile(
path.join(state.projectRoot, 'verify-e2e.mjs'),
isSupervisorSwarmSuite()
? supervisorSwarmVerificationFixtureSource()
: isGoalRuntimeSuite() ||
isResponseStreamSuite() ||
isWebSearchSuite() ||
isSupervisorAutonomousPlayableLaneDefenseSuite()
? goalRevisionOneVerificationFixtureSource()
: goalRevisionTwoVerificationFixtureSource(),
),
fs.writeFile(
path.join(state.projectRoot, 'game/index.html'),
seededGameHtml(),
),
fs.writeFile( fs.writeFile(
path.join(state.projectRoot, 'AGENTS.md'), path.join(state.projectRoot, 'AGENTS.md'),
'# Real E2E Repository Context\n\n- REPOSITORY_CONTEXT_MARKER: preserve the visible text and canvas.\n- Read package.json before verification.\n- Never read or expose .env, game-creator.config.json, or .agent/private-secret.txt.\n', '# Real E2E Repository Context\n\n- REPOSITORY_CONTEXT_MARKER: preserve the visible text and canvas.\n- Read package.json before verification.\n- Never read or expose .env, game-creator.config.json, or .agent/private-secret.txt.\n',
@@ -264,7 +345,16 @@ export async function seedDisposableProject() {
mode: 0o600, mode: 0o600,
}, },
); );
await initializeDisposableGitRepository(); await initializeDisposableGitRepository(
preserveProductionInitBaseline
? [
'AGENTS.md',
'e2e/isolated-a/evidence.txt',
'e2e/isolated-b/evidence.txt',
'e2e/isolated-c/evidence.txt',
]
: undefined,
);
} }
export async function initializeDisposableGitRepository( export async function initializeDisposableGitRepository(
@@ -310,12 +400,17 @@ export function seededGameHtml() {
<main> <main>
<h1>${visibleText}</h1> <h1>${visibleText}</h1>
<p id="patch-state">REAL_E2E_TARGET:before</p> <p id="patch-state">REAL_E2E_TARGET:before</p>
<p id="objective">Objective: survive until victory. On defeat, use Restart.</p>
<canvas id="game" width="640" height="360"></canvas> <canvas id="game" width="640" height="360"></canvas>
</main> </main>
<script> <script>
const canvas = document.getElementById('game'); const canvas = document.getElementById('game');
const context = canvas.getContext('2d'); const context = canvas.getContext('2d');
let frame = 0; let frame = 0;
canvas.addEventListener('pointerdown', () => {
frame = 0;
document.body.dataset.input = 'pointer';
});
function draw() { function draw() {
frame += 1; frame += 1;
context.fillStyle = '#13293d'; context.fillRect(0, 0, canvas.width, canvas.height); context.fillStyle = '#13293d'; context.fillRect(0, 0, canvas.width, canvas.height);
@@ -332,6 +427,23 @@ export function seededGameHtml() {
`; `;
} }
export function productionDefaultGameIndexHtml() {
return `<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Genarrative Game Draft</title>
<style>
body { margin: 0; display: grid; min-height: 100vh; place-items: center; background: #101827; color: #d9e7ff; font: 16px system-ui, sans-serif; }
main { width: min(720px, calc(100vw - 32px)); }
</style>
</head>
<body><main>还没有生成游戏。回到聊天输入创意并确认生成后,这里会写入可试玩原型。</main></body>
</html>
`;
}
export function buildTaskPrompt(suite) { export function buildTaskPrompt(suite) {
const editorAssetOutcome = const editorAssetOutcome =
suite === 'full' suite === 'full'
@@ -47,12 +47,45 @@ export async function removeDisposableProject() {
return true; return true;
} }
export function providerUsedFromEvidence(evidence) {
const requestIdentityCount = evidence?.providerRequestIdentityCount;
const startedCount = evidence?.providerLifecycleStartedCount;
const terminalCount = evidence?.providerLifecycleTerminalCount;
const completedCount = evidence?.providerLifecycleCompletedCount;
return (
evidence?.evidenceCompleteness === 'complete' &&
evidence.providerAgentMode === 'provider' &&
evidence.providerBindingMatched === true &&
typeof evidence.providerModel === 'string' &&
evidence.providerModel.trim().length > 0 &&
typeof evidence.providerApiKind === 'string' &&
evidence.providerApiKind.trim().length > 0 &&
typeof evidence.providerReasoningEffort === 'string' &&
evidence.providerReasoningEffort.trim().length > 0 &&
/^[0-9a-f]{64}$/u.test(evidence.providerBaseUrlSha256 ?? '') &&
Number.isSafeInteger(evidence.providerBoundAgentCount) &&
evidence.providerBoundAgentCount > 0 &&
Number.isSafeInteger(requestIdentityCount) &&
Number.isSafeInteger(startedCount) &&
Number.isSafeInteger(terminalCount) &&
Number.isSafeInteger(completedCount) &&
requestIdentityCount > 0 &&
startedCount === requestIdentityCount &&
terminalCount === requestIdentityCount &&
completedCount === requestIdentityCount &&
evidence.providerLifecycleFailedCount === 0 &&
evidence.openProviderLifecycleCount === 0 &&
evidence.duplicateProviderLifecycleCount === 0
);
}
export function buildSummary() { export function buildSummary() {
const secretLeakCount = const secretLeakCount =
state.transcriptLeakCount + state.projectLeakCount + state.reportLeakCount; state.transcriptLeakCount + state.projectLeakCount + state.reportLeakCount;
const base = { const base = {
status: state.status, status: state.status,
suite: state.suite, suite: state.suite,
providerUsed: providerUsedFromEvidence(state.evidence),
config: state.config, config: state.config,
blocked: state.blocked, blocked: state.blocked,
run: { run: {
@@ -3118,17 +3118,29 @@ export async function countLureLeaks() {
if (excluded.has(relative)) continue; if (excluded.has(relative)) continue;
const metadata = await fs.lstat(file); const metadata = await fs.lstat(file);
if (!metadata.isFile() || metadata.isSymbolicLink()) continue; if (!metadata.isFile() || metadata.isSymbolicLink()) continue;
if (isEmptyExecutionOwnerLock(state.projectRoot, file, metadata)) continue;
const content = await fs.readFile(file); const content = await fs.readFile(file);
count += countExactSecrets(content, state.lures); count += countExactSecrets(content, state.lures);
} }
return count; return count;
} }
export function isEmptyExecutionOwnerLock(root, file, metadata) {
return (
metadata?.isFile?.() === true &&
metadata.isSymbolicLink() === false &&
metadata.size === 0 &&
path.resolve(file) ===
path.resolve(root, '.agent/runtime/execution-owner.lock')
);
}
export async function countSecretsInProject(root, secrets) { export async function countSecretsInProject(root, secrets) {
let count = 0; let count = 0;
for (const file of await listFiles(root)) { for (const file of await listFiles(root)) {
const metadata = await fs.lstat(file); const metadata = await fs.lstat(file);
if (!metadata.isFile() || metadata.isSymbolicLink()) continue; if (!metadata.isFile() || metadata.isSymbolicLink()) continue;
if (isEmptyExecutionOwnerLock(root, file, metadata)) continue;
count += await countSecretsInFile(file, secrets); count += await countSecretsInFile(file, secrets);
} }
return count; return count;
@@ -227,6 +227,9 @@ export const supervisorSwarmAutonomousChatSuite =
export const supervisorAutonomousPlayableLaneDefenseSuite = export const supervisorAutonomousPlayableLaneDefenseSuite =
'supervisor-autonomous-playable-lane-defense'; 'supervisor-autonomous-playable-lane-defense';
export const supervisorGameChatSingleMainPlayableSuite =
'supervisor-game-chat-single-main-playable';
export const supervisorSwarmStaticIsolatedAutonomousChatSuite = export const supervisorSwarmStaticIsolatedAutonomousChatSuite =
'supervisor-swarm-static-isolated-autonomous-chat'; 'supervisor-swarm-static-isolated-autonomous-chat';
@@ -843,15 +846,94 @@ finally:
os.close(pidfd) os.close(pidfd)
`; `;
export const windowsProcessHandleHelperSource = String.raw`
$ErrorActionPreference = 'Stop'
$nativeSource = @'
using System;
using System.Runtime.InteropServices;
public static class GenarrativeOwnedProcessHandle
{
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr OpenProcess(
uint desiredAccess,
[MarshalAs(UnmanagedType.Bool)] bool inheritHandle,
uint processId);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool TerminateProcess(IntPtr process, uint exitCode);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern uint WaitForSingleObject(IntPtr handle, uint milliseconds);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern uint GetProcessId(IntPtr process);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool CloseHandle(IntPtr handle);
}
'@
[void](Add-Type -TypeDefinition $nativeSource -ErrorAction Stop)
$targetPidText = [Environment]::GetEnvironmentVariable(
'AGC_OWNED_RUNNER_PID',
[EnvironmentVariableTarget]::Process)
if ([String]::IsNullOrWhiteSpace($targetPidText)) { exit 70 }
$targetPid = [UInt32]::Parse($targetPidText)
$desiredAccess = [UInt32](0x0001 -bor 0x00100000 -bor 0x1000)
$handle = [GenarrativeOwnedProcessHandle]::OpenProcess(
$desiredAccess,
$false,
$targetPid)
if ($handle -eq [IntPtr]::Zero) { exit 71 }
try {
if ([GenarrativeOwnedProcessHandle]::GetProcessId($handle) -ne $targetPid) {
exit 72
}
[Console]::Out.WriteLine('HANDLE_READY')
[Console]::Out.Flush()
$command = [Console]::In.ReadLine()
if ($command -eq 'CLOSE') { exit 0 }
if ($command -ne 'KILL') { exit 73 }
if (-not [GenarrativeOwnedProcessHandle]::TerminateProcess($handle, 137)) {
exit 74
}
if ([GenarrativeOwnedProcessHandle]::WaitForSingleObject($handle, 10000) -ne 0) {
exit 75
}
[Console]::Out.WriteLine('HANDLE_EXITED')
[Console]::Out.Flush()
} finally {
[void][GenarrativeOwnedProcessHandle]::CloseHandle($handle)
}
`;
export const activeCommandChildren = new Set(); export const activeCommandChildren = new Set();
export const shutdownWaiters = new Set(); export const shutdownWaiters = new Set();
export class StreamingSecretScanner { export class StreamingSecretScanner {
constructor(secrets) { constructor(secrets) {
this.secrets = secrets.map((value) => Buffer.from(value)); this.secrets = [];
this.secretKeys = new Set();
this.tails = new Map(); this.tails = new Map();
this.count = 0; this.count = 0;
this.addSecrets(secrets);
}
addSecrets(secrets) {
for (const value of secrets) {
const secret = Buffer.isBuffer(value) ? value : Buffer.from(value);
if (secret.length === 0) continue;
const key = secret.toString('base64');
if (this.secretKeys.has(key)) continue;
this.secretKeys.add(key);
this.secrets.push(secret);
}
} }
scan(source, chunk) { scan(source, chunk) {
@@ -912,6 +994,7 @@ export const isolatedRunnerState = {
export const state = { export const state = {
shutdownSignal: null, shutdownSignal: null,
linuxPidfdPythonPath: null, linuxPidfdPythonPath: null,
windowsProcessHandlePowerShellPath: null,
cleanupInProgress: false, cleanupInProgress: false,
userInputCliSession: null, userInputCliSession: null,
supervisorSwarmCliSession: null, supervisorSwarmCliSession: null,
@@ -931,7 +1014,7 @@ export const state = {
transcriptLeakCount: 0, transcriptLeakCount: 0,
projectLeakCount: 0, projectLeakCount: 0,
reportLeakCount: 0, reportLeakCount: 0,
lureLeakCount: 0, lureLeakCount: null,
commandOutputMarkerSeenInContext: false, commandOutputMarkerSeenInContext: false,
commandOutputContextPages: new Set(), commandOutputContextPages: new Set(),
commandMarkerReportLeakCount: 0, commandMarkerReportLeakCount: 0,
@@ -1081,7 +1164,10 @@ export const state = {
reportLeakCount: 0, reportLeakCount: 0,
}, },
supervisorAutonomousPlayable: { supervisorAutonomousPlayable: {
freshInitBaselineUsed: false,
initialGameIndexSha256: null, initialGameIndexSha256: null,
expectedProviderBinding: null,
effectiveProviderBinding: null,
stdinWriteCount: 0, stdinWriteCount: 0,
stdinEnded: false, stdinEnded: false,
stdinBytes: 0, stdinBytes: 0,
@@ -1089,6 +1175,7 @@ export const state = {
cliOutput: '', cliOutput: '',
privateValues: [], privateValues: [],
reportLeakCount: 0, reportLeakCount: 0,
ownedProcessCleanupSnapshot: null,
}, },
supervisorSwarm: { supervisorSwarm: {
effectiveModel: null, effectiveModel: null,
File diff suppressed because it is too large Load Diff
@@ -90,6 +90,37 @@ export async function listOptionalSupervisorSwarmFile(file) {
return [file]; return [file];
} }
export function supervisorSwarmProfessionalSessions({
deliveries = [],
latestTasks = [],
rootAgentId,
rootRunId,
}) {
const sessions = new Map();
const registerSession = (agentId, sessionId) => {
if (!isNonEmptyString(agentId) || !isNonEmptyString(sessionId)) return;
sessions.set(`${agentId}\0${sessionId}`, { agentId, sessionId });
};
for (const delivery of deliveries) {
registerSession(delivery.targetAgentId, delivery.targetSessionId);
}
if (!isNonEmptyString(rootAgentId) || !isNonEmptyString(rootRunId)) {
return [...sessions.values()];
}
for (const task of latestTasks) {
if (
task.source !== 'agent-ready-task-scheduler' ||
task.agentId === rootAgentId ||
task.parentAgentId !== rootAgentId ||
task.parentRunId !== rootRunId
) {
continue;
}
registerSession(task.agentId, task.sessionId);
}
return [...sessions.values()];
}
export async function readSupervisorSwarmPersistence({ export async function readSupervisorSwarmPersistence({
tolerateErrors = false, tolerateErrors = false,
} = {}) { } = {}) {
@@ -274,25 +305,15 @@ export async function readSupervisorSwarmPersistence({
), ),
); );
const supervisorConversation = supervisorConversationSurface.records; const supervisorConversation = supervisorConversationSurface.records;
const professionalSessions = new Map(); const professionalSessions = supervisorSwarmProfessionalSessions({
for (const delivery of deliveries) { deliveries,
if ( latestTasks: taskSnapshot.latest,
!isNonEmptyString(delivery.targetAgentId) || rootAgentId: projectSupervisorAgentId,
!isNonEmptyString(delivery.targetSessionId) rootRunId: state.initialRunId,
) { });
continue;
}
professionalSessions.set(
`${delivery.targetAgentId}\0${delivery.targetSessionId}`,
{
agentId: delivery.targetAgentId,
sessionId: delivery.targetSessionId,
},
);
}
const professionalConversations = []; const professionalConversations = [];
const professionalConversationErrors = []; const professionalConversationErrors = [];
for (const session of professionalSessions.values()) { for (const session of professionalSessions) {
const conversationSurface = await readJsonlSurface( const conversationSurface = await readJsonlSurface(
'professional-conversation', 'professional-conversation',
() => () =>
@@ -312,7 +312,9 @@ function assertContractRecordsMatch(label, leftRecords, rightRecords) {
function parseAppInvokeCommandNames(source) { function parseAppInvokeCommandNames(source) {
return Array.from( return Array.from(
source.matchAll(/invoke(?:<[^>]*>)?\(\s*['"]([a-z0-9_]+)['"]/g), source.matchAll(
/(?:invoke|directInvoke)(?:<[^>]*>)?\(\s*['"]([a-z0-9_]+)['"]/g,
),
([, command]) => command, ([, command]) => command,
); );
} }
@@ -1164,6 +1166,7 @@ if (
!Array.isArray(tauriConfig.app?.windows) || !Array.isArray(tauriConfig.app?.windows) ||
tauriConfig.app.windows.length !== 1 || tauriConfig.app.windows.length !== 1 ||
tauriConfig.app.windows[0]?.label !== 'client' || tauriConfig.app.windows[0]?.label !== 'client' ||
tauriConfig.app.windows[0]?.title !== '陶泥儿' ||
tauriConfig.app.windows[0]?.url !== 'index.html' tauriConfig.app.windows[0]?.url !== 'index.html'
) { ) {
throw new Error( throw new Error(
@@ -1186,11 +1189,12 @@ const allowedLlmReasoningEfforts = new Set([
'low', 'low',
'medium', 'medium',
'high', 'high',
'max',
]); ]);
if (defaultAppConfig.llm?.reasoningEffort !== 'high') { if (defaultAppConfig.llm?.reasoningEffort !== 'max') {
throw new Error( throw new Error(
'AI game creator shell default llm.reasoningEffort must stay high', 'AI game creator shell default llm.reasoningEffort must stay max',
); );
} }
@@ -1207,9 +1211,9 @@ for (const [agentId, agentConfig] of Object.entries(
} }
} }
if (defaultAppConfig.editorApi?.apiKey !== '') { if (defaultAppConfig.editorApi !== undefined) {
throw new Error( throw new Error(
'AI game creator shell default editorApi.apiKey must stay empty', 'AI game creator shell ordinary default config must not contain editorApi',
); );
} }
@@ -1539,7 +1543,7 @@ if (
for (const snippet of [ for (const snippet of [
'resolveAgcDevEndpoint', 'resolveAgcDevEndpoint',
'withAgcDevEndpointEnv', 'withAgcDevEndpointEnv',
"response.body.includes('<title>AI 游戏创作</title>')", "response.body.includes('<title>陶泥儿</title>')",
'function isPortListening()', 'function isPortListening()',
'cannot be safely reused', 'cannot be safely reused',
'non-HTTP or unrecognized server', 'non-HTTP or unrecognized server',
@@ -1666,10 +1670,6 @@ if (
for (const snippet of [ for (const snippet of [
'import.meta.env.DEV', 'import.meta.env.DEV',
'#[cfg(all(debug_assertions, not(test)))]',
'open_developer_window(app.handle())?',
'tauri::WebviewWindowBuilder::new(app, "developer", developer_window_url())',
'index.html?agent-chat',
'supervisorChatMode', 'supervisorChatMode',
'supervisorChatOnly', 'supervisorChatOnly',
'open_project_supervisor_chat_window', 'open_project_supervisor_chat_window',
@@ -1682,6 +1682,12 @@ for (const snippet of [
} }
} }
if (tauriHandlerSource.includes('open_developer_window(app.handle())?')) {
throw new Error(
'AI game creator normal startup must not automatically open the developer window',
);
}
const smokeAgentRunSource = fs.readFileSync( const smokeAgentRunSource = fs.readFileSync(
new URL('./smoke-agent-run-local-provider.mjs', import.meta.url), new URL('./smoke-agent-run-local-provider.mjs', import.meta.url),
'utf8', 'utf8',
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,139 @@
import { spawn } from 'node:child_process';
import fs from 'node:fs';
import readline from 'node:readline';
const configPath =
process.env.AGC_CONFIG_PATH ??
'C:/Users/kdletters/AppData/Roaming/world.genarrative.ai-game-creator/game-creator.config.json';
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
const args = [
'app-server',
'--stdio',
'-c',
'mcp_servers={}',
'-c',
'web_search="disabled"',
'-c',
'agents.enabled=false',
];
for (const feature of [
'apps',
'browser_use',
'browser_use_external',
'browser_use_full_cdp_access',
'computer_use',
'goals',
'image_generation',
'in_app_browser',
'plugins',
'remote_plugin',
'shell_tool',
'tool_suggest',
'unified_exec',
'workspace_dependencies',
])
args.push('--disable', feature);
args.push(
'-c',
'model_provider="genarrative_agc"',
'-c',
'model_providers.genarrative_agc.name="Genarrative AGC"',
'-c',
`model_providers.genarrative_agc.base_url="${config.llm.baseUrl.replace(/\/$/, '')}"`,
'-c',
'model_providers.genarrative_agc.env_key="GENARRATIVE_AGC_API_KEY"',
'-c',
'model_providers.genarrative_agc.wire_api="responses"',
);
const child = spawn(
'C:/Program Files/nodejs/node.exe',
[
'C:/Users/kdletters/AppData/Roaming/npm/node_modules/@openai/codex/bin/codex.js',
...args,
],
{
cwd: process.cwd(),
env: { ...process.env, GENARRATIVE_AGC_API_KEY: config.llm.apiKey },
stdio: ['pipe', 'pipe', 'pipe'],
},
);
const send = (value) => child.stdin.write(`${JSON.stringify(value)}\n`);
const timer = setTimeout(
() => {
console.log('smoke-timeout');
child.kill();
process.exit(1);
},
Number(process.env.DIRECT_SMOKE_TIMEOUT_MS ?? 180000),
);
readline.createInterface({ input: child.stdout }).on('line', (line) => {
let message;
try {
message = JSON.parse(line);
} catch {
return;
}
if (message.id === 2) {
console.log('thread-start-ok');
const write = process.env.DIRECT_SMOKE_WRITE === '1';
send({
id: 3,
method: 'turn/start',
params: {
threadId: message.result.thread.id,
input: [
{
type: 'text',
text:
process.env.DIRECT_SMOKE_PROMPT ??
(write
? '在当前工作区创建 smoke-marker.txt,写入 DIRECT_CODEX_WRITE_OK,然后回复已完成。'
: '只回复 DIRECT_CODEX_SMOKE_OK,不要修改文件。'),
},
],
model: config.llm.model,
approvalPolicy: 'never',
sandboxPolicy: write
? {
type: 'workspaceWrite',
writableRoots: [process.cwd()],
networkAccess: true,
}
: { type: 'readOnly', networkAccess: false },
},
});
}
if (message.id === 3 && message.error) {
console.log(`turn-start-error:${message.error.message}`);
clearTimeout(timer);
child.kill();
process.exit(1);
}
if (message.method === 'turn/completed') {
console.log(`turn-completed:${message.params?.turn?.status}`);
clearTimeout(timer);
child.kill();
process.exit(message.params?.turn?.status === 'completed' ? 0 : 1);
}
});
send({
id: 1,
method: 'initialize',
params: {
clientInfo: { name: 'agc-direct-smoke', version: '0.1' },
capabilities: { experimentalApi: true },
},
});
send({ method: 'initialized', params: {} });
send({
id: 2,
method: 'thread/start',
params: {
cwd: process.cwd(),
model: config.llm.model,
approvalPolicy: 'never',
sandbox:
process.env.DIRECT_SMOKE_WRITE === '1' ? 'workspace-write' : 'read-only',
ephemeral: true,
},
});
@@ -47,7 +47,7 @@ function isAiGameCreatorServer(response) {
response && response &&
response.statusCode >= 200 && response.statusCode >= 200 &&
response.statusCode < 500 && response.statusCode < 500 &&
response.body.includes('<title>AI 游戏创作</title>') && response.body.includes('<title>陶泥儿</title>') &&
response.body.includes('/src/main.tsx') response.body.includes('/src/main.tsx')
); );
} }
@@ -158,7 +158,7 @@ function isAiGameCreatorServer(response) {
response && response &&
response.statusCode >= 200 && response.statusCode >= 200 &&
response.statusCode < 500 && response.statusCode < 500 &&
response.body.includes('<title>AI 游戏创作</title>') && response.body.includes('<title>陶泥儿</title>') &&
response.body.includes('/src/main.tsx') response.body.includes('/src/main.tsx')
); );
} }
@@ -11,7 +11,13 @@
"core:resources:allow-close", "core:resources:allow-close",
{ {
"identifier": "http:default", "identifier": "http:default",
"allow": [{ "url": "https://dev.genarrative.world/api/*" }] "allow": [
{ "url": "https://dev.genarrative.world/api/*" },
{ "url": "https://www.genarrative.world/api/*" },
{ "url": "https://*/api/*" },
{ "url": "http://localhost:*/*" },
{ "url": "http://127.0.0.1:*/*" }
]
}, },
"opener:default" "opener:default"
] ]

Some files were not shown because too many files have changed in this diff Show More