diff --git a/apps/ai-game-creator-shell/game-creator.config.json b/apps/ai-game-creator-shell/game-creator.config.json
index 41f155eb7..6e225ac31 100644
--- a/apps/ai-game-creator-shell/game-creator.config.json
+++ b/apps/ai-game-creator-shell/game-creator.config.json
@@ -11,7 +11,7 @@
"autoCompactTokenLimit": 64000,
"toolOutputTokenLimit": 12000,
"requestTimeoutMs": 180000,
- "maxRetries": 0,
+ "maxRetries": 2,
"retryBackoffMs": 500
},
"agentLlm": {},
diff --git a/apps/ai-game-creator-shell/package.json b/apps/ai-game-creator-shell/package.json
index 879b1dbab..e2982bbc3 100644
--- a/apps/ai-game-creator-shell/package.json
+++ b/apps/ai-game-creator-shell/package.json
@@ -4,8 +4,8 @@
"version": "0.1.0",
"type": "module",
"scripts": {
- "dev": "npm --prefix ../.. exec tauri -- dev",
- "game-chat": "npm --prefix ../.. exec tauri -- dev -- -- --game-chat",
+ "dev": "node scripts/start-tauri-dev.mjs",
+ "game-chat": "node scripts/start-tauri-dev.mjs --game-chat",
"dev-server": "node scripts/start-dev-server.mjs",
"dev-stack": "node scripts/start-dev-stack.mjs",
"build": "npm --prefix ../.. exec tauri -- build",
diff --git a/apps/ai-game-creator-shell/scripts/check-config.mjs b/apps/ai-game-creator-shell/scripts/check-config.mjs
index 2f858797a..df99a183c 100644
--- a/apps/ai-game-creator-shell/scripts/check-config.mjs
+++ b/apps/ai-game-creator-shell/scripts/check-config.mjs
@@ -452,10 +452,7 @@ async function runConfigWizardRegressionChecks() {
assert.equal(fs.existsSync(missingLinkedConfigDir), false);
assert.equal(
await assertSafeGameCreatorConfigDestination(missingLinkedConfigDir),
- path.join(
- fs.realpathSync.native(realConfigAncestor),
- 'missing-appdata',
- ),
+ path.join(fs.realpathSync.native(realConfigAncestor), 'missing-appdata'),
);
const gitRoot = path.join(testRoot, 'tracked-repository');
@@ -1260,6 +1257,21 @@ if (
);
}
+if (packageConfig.scripts?.dev !== 'node scripts/start-tauri-dev.mjs') {
+ throw new Error(
+ 'AI game creator shell dev must run through the managed Tauri dev launcher',
+ );
+}
+
+if (
+ packageConfig.scripts?.['game-chat'] !==
+ 'node scripts/start-tauri-dev.mjs --game-chat'
+) {
+ throw new Error(
+ 'AI game creator shell game-chat must run through the managed Tauri dev launcher',
+ );
+}
+
const gameChatInitialUrlApply =
'apply_game_chat_initial_window_url(tauri_context.config_mut(), options)';
const gameChatInitialUrlApplyIndexes = Array.from(
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
index 2a3f86b18..98bf22ef3 100644
--- a/apps/ai-game-creator-shell/scripts/deterministic-lane-defense-provider.mjs
+++ b/apps/ai-game-creator-shell/scripts/deterministic-lane-defense-provider.mjs
@@ -120,7 +120,7 @@ export function deterministicLaneDefenseInitialHtml() {
*{box-sizing:border-box}body{margin:0;min-height:100vh;background:#f4f8ee;color:#18351f;font:16px system-ui,sans-serif}main{width:min(960px,100%);margin:auto;padding:18px}h1{margin:0 0 4px;font-size:clamp(28px,7vw,46px)}p{margin:4px 0 14px}.toolbar,.plants{display:flex;flex-wrap:wrap;gap:8px;margin:10px 0}button{min-height:44px;border:1px solid #315d35;background:#fff;color:#18351f;padding:9px 14px;font:inherit;font-weight:700;cursor:pointer}button:hover{background:#e6f3dc}.board{display:grid;gap:10px;background:#d9edc8;border:2px solid #315d35;padding:10px}.lane{display:grid;grid-template-columns:repeat(5,1fr);gap:6px}.cell{min-height:54px;background:#eef8e7}.status{font-weight:700;min-height:24px}#game{display:none;width:100%;height:auto;aspect-ratio:20/9;background:#18351f;border:2px solid #315d35}@media(max-width:520px){main{padding:12px}button{flex:1 1 44%}.cell{min-height:44px}}
-
+
灵露花园
GENARRATIVE_REAL_E2E_VISIBLE
Goal: defend the garden and win every wave.
diff --git a/apps/ai-game-creator-shell/scripts/start-dev-stack.mjs b/apps/ai-game-creator-shell/scripts/start-dev-stack.mjs
index 91bf6d1b2..81fae0bee 100644
--- a/apps/ai-game-creator-shell/scripts/start-dev-stack.mjs
+++ b/apps/ai-game-creator-shell/scripts/start-dev-stack.mjs
@@ -1,6 +1,7 @@
import { spawn } from 'node:child_process';
import { existsSync, readFileSync } from 'node:fs';
import http from 'node:http';
+import net from 'node:net';
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
@@ -134,6 +135,21 @@ async function readExistingViteServer() {
return httpGetText(viteUrl);
}
+function isVitePortListening() {
+ return new Promise((resolveRequest) => {
+ const socket = net.connect({ host: viteHost, port: vitePort });
+ socket.once('connect', () => {
+ socket.destroy();
+ resolveRequest(true);
+ });
+ socket.once('error', () => resolveRequest(false));
+ socket.setTimeout(1000, () => {
+ socket.destroy();
+ resolveRequest(true);
+ });
+ });
+}
+
function isAiGameCreatorServer(response) {
return (
response &&
@@ -144,17 +160,6 @@ function isAiGameCreatorServer(response) {
);
}
-async function isExistingViteProxyReady() {
- const response = await httpGetText(`${viteUrl}api/auth/me`, 2000);
- return Boolean(
- response &&
- response.statusCode >= 200 &&
- response.statusCode < 500 &&
- !response.body.includes('AI 游戏创作') &&
- !response.body.includes('/src/main.tsx'),
- );
-}
-
async function readExistingViteMarker() {
const response = await httpGetText(viteMarkerUrl, 2000);
if (!response || response.statusCode !== 200) {
@@ -167,24 +172,49 @@ async function readExistingViteMarker() {
}
}
-async function isExistingVitePairedWithBackend(apiTarget) {
- const marker = await readExistingViteMarker();
- return Boolean(
- marker &&
- marker.schemaVersion === 1 &&
- marker.app === 'ai-game-creator-shell' &&
- marker.apiTarget === apiTarget,
+async function preflightExistingVite({
+ readServer = readExistingViteServer,
+ portListening = isVitePortListening,
+ readMarker = readExistingViteMarker,
+} = {}) {
+ const existing = await readServer();
+ if (!existing) {
+ if (await portListening()) {
+ throw new Error(
+ `${viteUrl} is already in use by a non-HTTP or unrecognized server. Stop it before starting Tauri dev.`,
+ );
+ }
+ return { status: 'available', apiTarget: '' };
+ }
+
+ if (!isAiGameCreatorServer(existing)) {
+ throw new Error(
+ `${viteUrl} is already in use by another server. Stop it before starting Tauri dev.`,
+ );
+ }
+
+ const marker = await readMarker();
+ const markerApiTarget =
+ marker?.schemaVersion === 1 &&
+ marker?.app === 'ai-game-creator-shell' &&
+ typeof marker?.apiTarget === 'string'
+ ? marker.apiTarget
+ : '';
+ const actualTarget = markerApiTarget || 'unknown';
+ throw new Error(
+ `${viteUrl} is already running with API target ${actualTarget}. Its owning worktree cannot be proven, so it will not be reused. Stop that Vite dev server before starting Tauri dev.`,
);
}
function spawnChild(command, args, options, spawnImpl = spawn) {
- const useShell = process.platform === 'win32';
+ const isPosix = process.platform !== 'win32';
+ const useShell = options.shell ?? !isPosix;
const child = spawnImpl(command, args, {
...options,
shell: useShell,
// POSIX 下让每个长驻服务拥有独立进程组,退出时可以连同 npm、
// Node、Cargo 及其子进程一起清理,避免残留订阅任务持续刷日志。
- detached: !useShell,
+ detached: isPosix,
stdio: 'inherit',
});
const lifecycle = {
@@ -192,7 +222,7 @@ function spawnChild(command, args, options, spawnImpl = spawn) {
promise: null,
// detached 子进程在 POSIX 下以自身 PID 作为 PGID。leader 退出后
// child.pid 仍是清理其后代的唯一稳定句柄,必须随生命周期保留。
- processGroupId: !useShell && Number.isInteger(child.pid) ? child.pid : null,
+ processGroupId: isPosix && Number.isInteger(child.pid) ? child.pid : null,
};
lifecycle.promise = new Promise((resolveLifecycle) => {
child.once('error', (error) => {
@@ -270,6 +300,142 @@ function stopChild(child, signal = 'SIGTERM') {
}
}
+function isProcessGroupAlive(processGroupId, killImpl = process.kill) {
+ if (!Number.isInteger(processGroupId)) {
+ return false;
+ }
+ try {
+ killImpl(-processGroupId, 0);
+ return true;
+ } catch (error) {
+ return error?.code !== 'ESRCH';
+ }
+}
+
+async function waitUntil(check, timeoutMs, pollIntervalMs = 25) {
+ const deadline = Date.now() + timeoutMs;
+ while (Date.now() < deadline) {
+ if (await check()) {
+ return true;
+ }
+ await new Promise((resolveWait) => setTimeout(resolveWait, pollIntervalMs));
+ }
+ return check();
+}
+
+function runWindowsTaskkill(
+ processId,
+ { spawnImpl = spawn, timeoutMs = 5000 } = {},
+) {
+ return new Promise((resolveRequest) => {
+ const taskkill = spawnImpl(
+ 'taskkill.exe',
+ ['/PID', String(processId), '/T', '/F'],
+ {
+ shell: false,
+ stdio: 'ignore',
+ windowsHide: true,
+ },
+ );
+ let settled = false;
+ const timeout = setTimeout(() => {
+ try {
+ taskkill.kill('SIGKILL');
+ } catch {
+ // ignore taskkill timeout races
+ }
+ finish({ timedOut: true, code: null, error: null });
+ }, timeoutMs);
+ const finish = (result) => {
+ if (settled) {
+ return;
+ }
+ settled = true;
+ clearTimeout(timeout);
+ resolveRequest(result);
+ };
+ taskkill.once('error', (error) =>
+ finish({ timedOut: false, code: null, error }),
+ );
+ taskkill.once('exit', (code) =>
+ finish({ timedOut: false, code: code ?? 0, error: null }),
+ );
+ });
+}
+
+async function terminateChildTree(
+ child,
+ {
+ platform = process.platform,
+ gracefulTimeoutMs = 2500,
+ forceTimeoutMs = 2000,
+ killImpl = process.kill,
+ taskkillImpl = runWindowsTaskkill,
+ } = {},
+) {
+ if (!child) {
+ return { stopped: true, forced: false };
+ }
+
+ if (platform === 'win32') {
+ if (!Number.isInteger(child.pid)) {
+ stopChild(child, 'SIGTERM');
+ return { stopped: true, forced: false };
+ }
+ const result = await taskkillImpl(child.pid);
+ return {
+ stopped:
+ !result?.timedOut &&
+ !result?.error &&
+ [0, 128].includes(result?.code ?? 0),
+ forced: true,
+ result,
+ };
+ }
+
+ const processGroupId = childLifecycles.get(child)?.processGroupId;
+ if (!Number.isInteger(processGroupId)) {
+ stopChild(child, 'SIGTERM');
+ const lifecycle = childLifecycles.get(child);
+ if (lifecycle) {
+ await Promise.race([
+ lifecycle.promise,
+ new Promise((resolveWait) =>
+ setTimeout(resolveWait, gracefulTimeoutMs),
+ ),
+ ]);
+ }
+ if (child.exitCode == null && child.signalCode == null) {
+ stopChild(child, 'SIGKILL');
+ return { stopped: false, forced: true };
+ }
+ return { stopped: true, forced: false };
+ }
+
+ stopChild(child, 'SIGTERM');
+ if (
+ await waitUntil(
+ () => !isProcessGroupAlive(processGroupId, killImpl),
+ gracefulTimeoutMs,
+ )
+ ) {
+ return { stopped: true, forced: false };
+ }
+
+ try {
+ killImpl(-processGroupId, 'SIGKILL');
+ } catch (error) {
+ if (error?.code !== 'ESRCH') {
+ return { stopped: false, forced: true, error };
+ }
+ }
+ const stopped = await waitUntil(
+ () => !isProcessGroupAlive(processGroupId, killImpl),
+ forceTimeoutMs,
+ );
+ return { stopped, forced: true };
+}
+
async function waitForBackendReady(backendChild, timeoutMs = 600_000) {
const startedAt = Date.now();
while (Date.now() - startedAt < timeoutMs) {
@@ -340,19 +506,9 @@ async function startVite(apiTarget) {
const existing = await readExistingViteServer();
if (existing) {
- if (
- isAiGameCreatorServer(existing) &&
- (await isExistingVitePairedWithBackend(apiTarget)) &&
- (await isExistingViteProxyReady())
- ) {
- console.log(
- `[ai-game-creator-shell] reuse existing Vite dev server ${viteUrl}`,
- );
- return null;
- }
if (isAiGameCreatorServer(existing)) {
throw new Error(
- `${viteUrl} is already running, but its /api proxy is not connected to the paired backend. Stop it before starting Tauri dev.`,
+ `${viteUrl} is already running and cannot be safely reused. Stop it before starting Tauri dev.`,
);
}
throw new Error(
@@ -384,6 +540,7 @@ async function main() {
}
try {
+ await preflightExistingVite();
const backend = await ensureBackend({
onBackendChild(child) {
backendChild = child;
@@ -422,6 +579,10 @@ async function main() {
);
return 1;
} finally {
+ await Promise.all([
+ terminateChildTree(viteChild),
+ terminateChildTree(backendChild),
+ ]);
for (const [signal, handler] of signalHandlers) {
process.off(signal, handler);
}
@@ -439,10 +600,13 @@ export {
ensureBackend,
formatChildFailure,
isDirectModuleExecution,
+ preflightExistingVite,
readChildFailure,
resolveBackendTargetsFromState,
+ runWindowsTaskkill,
spawnChild,
stopChild,
+ terminateChildTree,
waitForBackendReady,
waitForChildTermination,
};
diff --git a/apps/ai-game-creator-shell/scripts/start-tauri-dev.mjs b/apps/ai-game-creator-shell/scripts/start-tauri-dev.mjs
new file mode 100644
index 000000000..5c6bba17e
--- /dev/null
+++ b/apps/ai-game-creator-shell/scripts/start-tauri-dev.mjs
@@ -0,0 +1,128 @@
+import { resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+import {
+ preflightExistingVite,
+ spawnChild,
+ stopChild,
+ terminateChildTree,
+ waitForChildTermination,
+} from './start-dev-stack.mjs';
+
+const appRoot = fileURLToPath(new URL('..', import.meta.url));
+const repoRoot = resolve(appRoot, '../..');
+const tauriCliPath = resolve(repoRoot, 'node_modules/@tauri-apps/cli/tauri.js');
+
+function parseLauncherArguments(argv) {
+ const args = [...argv];
+ const gameChat = args[0] === '--game-chat';
+ if (gameChat) {
+ args.shift();
+ }
+ return { gameChat, args };
+}
+
+function buildTauriArguments(argv) {
+ const { gameChat, args } = parseLauncherArguments(argv);
+ if (gameChat) {
+ return ['dev', '--', '--', '--game-chat', ...args];
+ }
+ return ['dev', ...args];
+}
+
+function spawnTauriCli(argv) {
+ return spawnChild(process.execPath, [tauriCliPath, ...argv], {
+ cwd: appRoot,
+ shell: false,
+ });
+}
+
+async function runTauriDev(
+ argv = process.argv.slice(2),
+ {
+ preflight = preflightExistingVite,
+ spawnCli = spawnTauriCli,
+ waitForCli = waitForChildTermination,
+ terminateTree = terminateChildTree,
+ } = {},
+) {
+ await preflight();
+
+ const tauriArguments = buildTauriArguments(argv);
+ const child = spawnCli(tauriArguments);
+ let resolveShutdown;
+ let shutdownSignal = '';
+ let repeatedSignal = false;
+ const shutdownRequested = new Promise((resolveRequest) => {
+ resolveShutdown = resolveRequest;
+ });
+ const signalHandlers = new Map();
+
+ for (const signal of ['SIGINT', 'SIGTERM']) {
+ const handler = () => {
+ if (!shutdownSignal) {
+ shutdownSignal = signal;
+ stopChild(child, 'SIGTERM');
+ resolveShutdown(signal);
+ return;
+ }
+ repeatedSignal = true;
+ stopChild(child, 'SIGKILL');
+ };
+ signalHandlers.set(signal, handler);
+ process.on(signal, handler);
+ }
+
+ try {
+ const childResult = waitForCli(child);
+ const outcome = await Promise.race([
+ childResult.then((failure) => ({ type: 'exit', failure })),
+ shutdownRequested.then((signal) => ({ type: 'signal', signal })),
+ ]);
+ const cleanup = await terminateTree(child, {
+ gracefulTimeoutMs: repeatedSignal ? 0 : 2500,
+ });
+ if (!cleanup.stopped) {
+ console.error(
+ '[ai-game-creator-shell] Tauri dev exited, but its process tree could not be fully stopped.',
+ );
+ return 1;
+ }
+
+ if (outcome.type === 'signal') {
+ return 1;
+ }
+ const { failure } = outcome;
+ return failure.type === 'error' || failure.signal ? 1 : (failure.code ?? 0);
+ } finally {
+ for (const [signal, handler] of signalHandlers) {
+ process.off(signal, handler);
+ }
+ }
+}
+
+function isDirectModuleExecution() {
+ return Boolean(
+ process.argv[1] &&
+ resolve(process.argv[1]) === fileURLToPath(import.meta.url),
+ );
+}
+
+export {
+ buildTauriArguments,
+ isDirectModuleExecution,
+ parseLauncherArguments,
+ runTauriDev,
+ spawnTauriCli,
+};
+
+if (isDirectModuleExecution()) {
+ try {
+ process.exitCode = await runTauriDev();
+ } catch (error) {
+ console.error(
+ `[ai-game-creator-shell] ${error instanceof Error ? error.message : String(error)}`,
+ );
+ process.exitCode = 1;
+ }
+}
diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/role_briefs.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/role_briefs.rs
index c15f1bee5..2139ddfe1 100644
--- a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/role_briefs.rs
+++ b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/role_briefs.rs
@@ -367,10 +367,26 @@ pub(crate) fn has_game_creator_agent_llm_override(
config: &GameCreatorAppConfig,
agent_id: &str,
) -> bool {
- config
- .agent_llm
- .get(agent_id)
- .is_some_and(|patch| !is_empty_game_creator_llm_patch(patch))
+ config.agent_llm.get(agent_id).is_some_and(|patch| {
+ if is_empty_game_creator_llm_patch(patch) {
+ return false;
+ }
+ let only_canonical_reasoning_default = patch.api_key.is_none()
+ && patch.base_url.is_none()
+ && patch.model.is_none()
+ && patch.api_kind.is_none()
+ && patch.stream.is_none()
+ && patch.web_search_enabled.is_none()
+ && patch.context_window_tokens.is_none()
+ && patch.auto_compact_token_limit.is_none()
+ && patch.tool_output_token_limit.is_none()
+ && patch.request_timeout_ms.is_none()
+ && patch.max_retries.is_none()
+ && patch.retry_backoff_ms.is_none()
+ && patch.reasoning_effort.as_deref()
+ == game_creator_llm_agent_default_reasoning_effort(agent_id);
+ !only_canonical_reasoning_default
+ })
}
pub(crate) async fn request_agent_role_brief_with_config(
diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/action_execution.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/action_execution.rs
index 2149ad925..cbaaae623 100644
--- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/action_execution.rs
+++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/action_execution.rs
@@ -283,7 +283,7 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action_with_pending_
&action_fingerprint,
pending_action,
false,
- || observe_agent_runtime_task_list(root),
+ || observe_agent_runtime_task_list(root, agent_id, run_id),
),
"task.create" => observe_agent_runtime_task_create(root, agent_id, &action.input),
"task.update" => observe_agent_runtime_task_update(root, agent_id, &action.input),
@@ -414,7 +414,9 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action_with_pending_
action_id,
&action.input,
),
- "agent.schedule_ready" => observe_agent_runtime_schedule_ready_tasks(root, &action.input),
+ "agent.schedule_ready" => {
+ observe_agent_runtime_schedule_ready_tasks(root, agent_id, run_id, &action.input)
+ }
"agent.action_history" => observe_agent_runtime_project_snapshot_with_lock(
root,
agent_id,
diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/autonomous_policy.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/autonomous_policy.rs
index eaee84273..8c8c4f998 100644
--- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/autonomous_policy.rs
+++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/autonomous_policy.rs
@@ -262,11 +262,15 @@ fn autonomous_initial_delegate_expected_artifacts(
pub(in crate::agent) fn validate_agent_runtime_autonomous_initial_collaboration_contract(
plan: &AgentRuntimeToolPlan,
) -> Result<(), String> {
- let mut code_prototype = None;
- let mut quality_review = None;
+ let mut design_director = None;
let mut art_director = None;
- let mut art_asset_plan = None;
+ let mut code_director = None;
for action in &plan.actions {
+ if action.tool.trim() == "agent.spawn_isolated" {
+ return Err(autonomous_initial_collaboration_contract_error(
+ "首批只允许激活 design-director、art-director、code-director,不得启动 isolated child",
+ ));
+ }
let Some(input) = autonomous_initial_delegate_input(action)? else {
continue;
};
@@ -284,11 +288,14 @@ pub(in crate::agent) fn validate_agent_runtime_autonomous_initial_collaboration_
));
};
let slot = match target_agent_id {
- "code-prototype" => &mut code_prototype,
- AGENT_RUNTIME_QUALITY_REVIEW_AGENT_ID => &mut quality_review,
+ "design-director" => &mut design_director,
"art-director" => &mut art_director,
- "art-asset-plan" => &mut art_asset_plan,
- _ => continue,
+ "code-director" => &mut code_director,
+ _ => {
+ return Err(autonomous_initial_collaboration_contract_error(format!(
+ "首批只允许激活 design-director、art-director、code-director,不得委派底层 Agent:{target_agent_id}"
+ )));
+ }
};
if slot.replace(input).is_some() {
return Err(autonomous_initial_collaboration_contract_error(format!(
@@ -297,111 +304,80 @@ pub(in crate::agent) fn validate_agent_runtime_autonomous_initial_collaboration_
}
}
- let code_prototype = code_prototype.ok_or_else(|| {
- autonomous_initial_collaboration_contract_error("首批缺少 code-prototype 委派")
+ let design_director = design_director.ok_or_else(|| {
+ autonomous_initial_collaboration_contract_error("首批缺少 design-director 委派")
})?;
- let code_task = autonomous_initial_delegate_task(code_prototype, "code-prototype")?;
- let code_criteria = agent_runtime_tool_input_string_list(
- &serde_json::Value::Object(code_prototype.clone()),
+ let design_task = autonomous_initial_delegate_task(design_director, "design-director")?;
+ let design_criteria = agent_runtime_tool_input_string_list(
+ &serde_json::Value::Object(design_director.clone()),
&["acceptanceCriteria", "acceptance_criteria", "criteria"],
);
- if std::iter::once(code_task.as_str())
+ if !std::iter::once(design_task.as_str())
+ .chain(design_criteria.iter().map(String::as_str))
+ .any(agent_runtime_task_explicitly_requires_read_only_delivery)
+ {
+ return Err(autonomous_initial_collaboration_contract_error(
+ "首批 design-director task 或 acceptanceCriteria 必须显式声明只读且不得修改项目",
+ ));
+ }
+ let design_artifacts =
+ autonomous_initial_delegate_expected_artifacts(design_director, "design-director")?;
+ if !design_artifacts.is_empty() {
+ return Err(autonomous_initial_collaboration_contract_error(
+ "首批 design-director 的 expectedArtifacts 必须为 []",
+ ));
+ }
+
+ let art_director = art_director.ok_or_else(|| {
+ autonomous_initial_collaboration_contract_error("首批缺少 art-director 委派")
+ })?;
+ let art_task = autonomous_initial_delegate_task(art_director, "art-director")?;
+ let art_criteria = agent_runtime_tool_input_string_list(
+ &serde_json::Value::Object(art_director.clone()),
+ &["acceptanceCriteria", "acceptance_criteria", "criteria"],
+ );
+ if std::iter::once(art_task.as_str())
+ .chain(art_criteria.iter().map(String::as_str))
+ .any(agent_runtime_task_explicitly_requires_read_only_delivery)
+ {
+ return Err(autonomous_initial_collaboration_contract_error(
+ "首批 art-director 必须是非只读规范图生成任务",
+ ));
+ }
+ let art_artifacts =
+ autonomous_initial_delegate_expected_artifacts(art_director, "art-director")?;
+ if !art_artifacts
+ .iter()
+ .any(|path| path == "assets/art-spec.png")
+ {
+ return Err(autonomous_initial_collaboration_contract_error(
+ "首批 art-director 的 expectedArtifacts 必须包含 assets/art-spec.png",
+ ));
+ }
+
+ let code_director = code_director.ok_or_else(|| {
+ autonomous_initial_collaboration_contract_error("首批缺少 code-director 委派")
+ })?;
+ let code_task = autonomous_initial_delegate_task(code_director, "code-director")?;
+ let code_criteria = agent_runtime_tool_input_string_list(
+ &serde_json::Value::Object(code_director.clone()),
+ &["acceptanceCriteria", "acceptance_criteria", "criteria"],
+ );
+ if !std::iter::once(code_task.as_str())
.chain(code_criteria.iter().map(String::as_str))
.any(agent_runtime_task_explicitly_requires_read_only_delivery)
{
return Err(autonomous_initial_collaboration_contract_error(
- "首批 code-prototype 必须是非只读实现任务",
+ "首批 code-director task 或 acceptanceCriteria 必须显式声明只读且不得修改项目",
));
}
let code_artifacts =
- autonomous_initial_delegate_expected_artifacts(code_prototype, "code-prototype")?;
- if !code_artifacts
- .iter()
- .any(|path| path == AGENT_RUNTIME_GAME_INDEX_PATH)
- {
- return Err(autonomous_initial_collaboration_contract_error(format!(
- "首批 code-prototype 的 expectedArtifacts 必须包含 {AGENT_RUNTIME_GAME_INDEX_PATH}"
- )));
- }
-
- let quality_review = quality_review.ok_or_else(|| {
- autonomous_initial_collaboration_contract_error("首批缺少 quality-review 委派")
- })?;
- let quality_task =
- autonomous_initial_delegate_task(quality_review, AGENT_RUNTIME_QUALITY_REVIEW_AGENT_ID)?;
- let quality_criteria = agent_runtime_tool_input_string_list(
- &serde_json::Value::Object(quality_review.clone()),
- &["acceptanceCriteria", "acceptance_criteria", "criteria"],
- );
- if !std::iter::once(quality_task.as_str())
- .chain(quality_criteria.iter().map(String::as_str))
- .any(agent_runtime_task_explicitly_requires_read_only_delivery)
- {
+ autonomous_initial_delegate_expected_artifacts(code_director, "code-director")?;
+ if !code_artifacts.is_empty() {
return Err(autonomous_initial_collaboration_contract_error(
- "首批 quality-review task 或 acceptanceCriteria 必须显式声明只读且不得修改项目",
+ "首批 code-director 的 expectedArtifacts 必须为 []",
));
}
- let quality_artifacts = autonomous_initial_delegate_expected_artifacts(
- quality_review,
- AGENT_RUNTIME_QUALITY_REVIEW_AGENT_ID,
- )?;
- if !quality_artifacts.is_empty() {
- return Err(autonomous_initial_collaboration_contract_error(
- "首批 quality-review 的 expectedArtifacts 必须为 []",
- ));
- }
- if let Some(art_director) = art_director {
- let art_task = autonomous_initial_delegate_task(art_director, "art-director")?;
- let art_criteria = agent_runtime_tool_input_string_list(
- &serde_json::Value::Object(art_director.clone()),
- &["acceptanceCriteria", "acceptance_criteria", "criteria"],
- );
- if std::iter::once(art_task.as_str())
- .chain(art_criteria.iter().map(String::as_str))
- .any(agent_runtime_task_explicitly_requires_read_only_delivery)
- {
- return Err(autonomous_initial_collaboration_contract_error(
- "首批 art-director 必须是非只读规范图生成任务",
- ));
- }
- let art_artifacts =
- autonomous_initial_delegate_expected_artifacts(art_director, "art-director")?;
- if !art_artifacts
- .iter()
- .any(|path| path == "assets/art-spec.png")
- {
- return Err(autonomous_initial_collaboration_contract_error(
- "首批 art-director 的 expectedArtifacts 必须包含 assets/art-spec.png",
- ));
- }
- }
- if let Some(art_asset_plan) = art_asset_plan {
- let art_task = autonomous_initial_delegate_task(art_asset_plan, "art-asset-plan")?;
- let art_criteria = agent_runtime_tool_input_string_list(
- &serde_json::Value::Object(art_asset_plan.clone()),
- &["acceptanceCriteria", "acceptance_criteria", "criteria"],
- );
- if std::iter::once(art_task.as_str())
- .chain(art_criteria.iter().map(String::as_str))
- .any(agent_runtime_task_explicitly_requires_read_only_delivery)
- {
- return Err(autonomous_initial_collaboration_contract_error(
- "首批 art-asset-plan 必须是非只读美术生成任务",
- ));
- }
- let art_artifacts =
- autonomous_initial_delegate_expected_artifacts(art_asset_plan, "art-asset-plan")?;
- let missing_artifacts = ["assets/manifest.art.json", "assets/art-spritesheet.png"]
- .into_iter()
- .filter(|required| !art_artifacts.iter().any(|path| path == required))
- .collect::>();
- if !missing_artifacts.is_empty() {
- return Err(autonomous_initial_collaboration_contract_error(format!(
- "首批 art-asset-plan 的 expectedArtifacts 缺少:{}",
- missing_artifacts.join(", ")
- )));
- }
- }
Ok(())
}
@@ -560,7 +536,34 @@ pub(in crate::agent) fn autonomous_manifest_dag_in_progress_at(
root: &Path,
) -> Result {
let manifest = read_manifest_for_project(root)?;
- let seed_task_ids = new_game_creation_app_seed_tasks()
+ let source = read_game_creator_agent_runtime_at(root, GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID)
+ .ok()
+ .filter(|runtime| {
+ runtime.state.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
+ && agent_runtime_supervisor_source_is_trusted(&runtime.state.source)
+ })
+ .map(|runtime| runtime.state.source)
+ .or_else(|| {
+ let path = game_creator_agent_runtime_task_path(
+ root,
+ GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
+ );
+ read_all_game_creator_agent_runtime_tasks(&path)
+ .ok()
+ .map(latest_game_creator_agent_runtime_tasks)
+ .and_then(|records| {
+ records.into_iter().rev().find_map(|record| {
+ (record.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
+ && record.parent_run_id.is_none()
+ && agent_runtime_supervisor_source_is_trusted(&record.source))
+ .then_some(record.source)
+ })
+ })
+ })
+ .ok_or_else(|| {
+ "无法解析当前自主构建根 Run 的可信 source,拒绝按 GUI 完整 DAG 回退".to_string()
+ })?;
+ let seed_task_ids = autonomous_manifest_seed_tasks_for_source(&source)
.into_iter()
.map(|task| task.id)
.collect::>();
@@ -1479,6 +1482,24 @@ pub(in crate::agent) fn restrict_agent_runtime_supervisor_collaboration_repair_t
Ok(())
}
+pub(in crate::agent) fn restrict_agent_runtime_autonomous_initial_collaboration_repair_tools(
+ request: &mut LlmRunRequest,
+) -> Result<(), String> {
+ let delegate_function = native_runtime_function_name("agent.delegate")
+ .ok_or_else(|| "无法生成 autonomous 首批协作修复工具名:agent.delegate".to_string())?;
+ request
+ .function_tools
+ .retain(|tool| tool.name == delegate_function);
+ if !request
+ .function_tools
+ .iter()
+ .any(|tool| tool.name == delegate_function)
+ {
+ return Err("autonomous 首批协作修复工具目录缺少 agent.delegate".to_string());
+ }
+ Ok(())
+}
+
pub(in crate::agent) fn agent_runtime_protocol_error_requires_supervisor_collaboration_repair(
error: &str,
) -> bool {
@@ -1495,6 +1516,93 @@ pub(in crate::agent) fn agent_runtime_protocol_error_requires_supervisor_collabo
mod tests {
use super::*;
+ fn autonomous_initial_delegate(
+ agent_id: &str,
+ expected_artifacts: &[&str],
+ ) -> AgentRuntimeToolAction {
+ let read_only_planner = matches!(agent_id, "design-director" | "code-director");
+ AgentRuntimeToolAction {
+ tool: "agent.delegate".to_string(),
+ reason: Some("建立首批 Leader 规划".to_string()),
+ input: serde_json::json!({
+ "agentId": agent_id,
+ "task": if read_only_planner {
+ format!("由 {agent_id} 只读完成首轮专业规划,不得修改项目")
+ } else {
+ format!("由 {agent_id} 完成首轮专业交付")
+ },
+ "acceptanceCriteria": if read_only_planner {
+ vec!["只读给出可供后续底层 Agent 按需执行的规划,不得修改项目"]
+ } else {
+ vec!["视觉规范可供后续底层 Agent 按需执行"]
+ },
+ "expectedArtifacts": expected_artifacts,
+ "repairOfDelegationId": null,
+ "runId": null,
+ }),
+ }
+ }
+
+ fn autonomous_initial_leader_plan() -> AgentRuntimeToolPlan {
+ AgentRuntimeToolPlan {
+ thinking_summary: "首批只激活程策美 Leader".to_string(),
+ plan_update: None,
+ plan: Vec::new(),
+ actions: vec![
+ autonomous_initial_delegate("design-director", &[]),
+ autonomous_initial_delegate("art-director", &["assets/art-spec.png"]),
+ autonomous_initial_delegate("code-director", &[]),
+ ],
+ response: String::new(),
+ }
+ }
+
+ #[test]
+ fn autonomous_initial_collaboration_accepts_only_three_leaders() {
+ validate_agent_runtime_autonomous_initial_collaboration_contract(
+ &autonomous_initial_leader_plan(),
+ )
+ .expect("three leader initial contract");
+ }
+
+ #[test]
+ fn autonomous_initial_collaboration_rejects_bottom_agent() {
+ let mut plan = autonomous_initial_leader_plan();
+ plan.actions[2] =
+ autonomous_initial_delegate("code-prototype", &[AGENT_RUNTIME_GAME_INDEX_PATH]);
+
+ let error = validate_agent_runtime_autonomous_initial_collaboration_contract(&plan)
+ .expect_err("bottom agent must be rejected");
+
+ assert!(error.contains("不得委派底层 Agent:code-prototype"));
+ }
+
+ #[test]
+ fn autonomous_initial_collaboration_rejects_isolated_child() {
+ let mut plan = autonomous_initial_leader_plan();
+ plan.actions.push(AgentRuntimeToolAction {
+ tool: "agent.spawn_isolated".to_string(),
+ reason: None,
+ input: serde_json::json!({"children": [], "joinMode": "all"}),
+ });
+
+ let error = validate_agent_runtime_autonomous_initial_collaboration_contract(&plan)
+ .expect_err("isolated child must be rejected");
+
+ assert!(error.contains("不得启动 isolated child"));
+ }
+
+ #[test]
+ fn autonomous_initial_collaboration_requires_leader_artifacts() {
+ let mut plan = autonomous_initial_leader_plan();
+ plan.actions[1] = autonomous_initial_delegate("art-director", &[]);
+
+ let error = validate_agent_runtime_autonomous_initial_collaboration_contract(&plan)
+ .expect_err("art artifact must be required");
+
+ assert!(error.contains("expectedArtifacts 必须包含 assets/art-spec.png"));
+ }
+
#[test]
fn autonomous_manifest_dag_waits_only_after_seed_execution_starts() {
let temporary = tempfile::tempdir().expect("create manifest DAG policy root");
diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/parallel_read.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/parallel_read.rs
index 19de323b5..a85194f36 100644
--- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/parallel_read.rs
+++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/parallel_read.rs
@@ -115,7 +115,7 @@ pub(in crate::agent) fn execute_game_creator_agent_runtime_parallel_safe_read_at
"git.inspect" => observe_agent_runtime_git_inspect(root, &action.input),
"file.list" => observe_agent_runtime_file_list(root, &action.input),
"file.read" => observe_agent_runtime_file(root, &action.input),
- "task.list" => observe_agent_runtime_task_list(root),
+ "task.list" => observe_agent_runtime_task_list(root, agent_id, run_id),
_ => AgentRuntimeToolObservation {
tool: tool.to_string(),
status: "rejected".to_string(),
diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/project_gates.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/project_gates.rs
index 18fc74e29..804deadb6 100644
--- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/project_gates.rs
+++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/project_gates.rs
@@ -655,6 +655,20 @@ pub(in crate::agent) fn supervisor_collaboration_policy_completion_blocker_at_lo
if agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
return None;
}
+ if read_game_creator_agent_runtime_run_profile_binding(root, agent_id, run_id)
+ .ok()
+ .flatten()
+ .is_some_and(|binding| {
+ binding.profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
+ && binding.source == AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE
+ && binding.root_agent_id == binding.agent_id
+ && binding.root_run_id == binding.run_id
+ })
+ {
+ // game-chat 首版由 source-aware manifest scheduler 固定编排
+ // code -> static smoke -> playtest,不再要求 Provider 建立额外委派波。
+ return None;
+ }
let policy = match resolve_supervisor_collaboration_policy_for_run_at(root, agent_id, run_id) {
Ok(resolution) => resolution.policy,
Err(error) => {
diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_request_builders.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_request_builders.rs
index bc244bcba..383a7c415 100644
--- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_request_builders.rs
+++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_request_builders.rs
@@ -2,6 +2,43 @@ use super::*;
const AGENT_RUNTIME_COMPLETION_BLOCKER_TOOL_PLAN_PROTOCOL: &str = "通用完成阻断规则:如果最新 observation 的 tool 为 runtime.autonomous_completion 且 status 为 blocked,本轮禁止直接调用 respond_to_user,也禁止在 legacy response 中填写最终回复;必须先读取该 observation.detail 的 nextRequiredAction,并据此调用合适的读取、修复和验证工具。只有完成要求的动作、取得后续可信 observation 且完成门禁不再阻断后,才能给最终回复;不得反复提交 final response,也不得按项目正文硬编码某一种 blocker 的处理方式。";
+const GAME_CHAT_CODE_PROTOTYPE_FAST_PATH_PROMPT: &str = "game-chat 首版使用五分钟快车道。当前任务的第一目标是在一次 Provider planning 内产出首个完整可玩版本:如果最新 observation 尚未显示 game/index.html 已由本 run 写入,本响应必须直接调用一次 file.write,把完整、自包含、可运行的 game/index.html 一次写完;禁止先调用读取、搜索、任务查询、委派、只更新计划或提交半成品。HTML 必须满足下方固定试玩合同,包含真实 Canvas 游戏循环、键盘与触控输入、开始、主要操作、重开、胜负状态和移动端布局;可以采用保守的原创玩法默认值。已登记的平台视觉规范图 ../assets/art-spec.png 是首版必需资源,必须在主要游戏画面中显著可见使用:至少把规范图实际绘制为主要背景,并从规范图中绘制玩家角色和目标实体。禁止仅放置隐藏 img、透明或屏外元素、微小水印、不可见预加载或只在源码中引用;也禁止用纯几何图形冒充平台图片使用。若规范图无法加载,游戏必须明确失败关闭,不能退回纯 Canvas 几何兜底。一次写入后不要继续扩写功能;Runtime 会在下一步自动执行静态自检并在通过后立即试玩。";
+
+fn game_chat_fast_path_prompt_for_root_source(
+ agent_id: &str,
+ root_source: &str,
+) -> Option<&'static str> {
+ (agent_id.trim() == "code-prototype"
+ && root_source.trim() == AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE)
+ .then_some(GAME_CHAT_CODE_PROTOTYPE_FAST_PATH_PROMPT)
+}
+
+pub(in crate::agent) fn agent_runtime_root_source_at(
+ root: &Path,
+ agent_id: &str,
+ run_id: &str,
+) -> Result {
+ let binding = read_game_creator_agent_runtime_run_profile_binding(root, agent_id, run_id)?
+ .ok_or_else(|| "Agent Runtime 缺少 Run Profile 绑定,无法解析 root source".to_string())?;
+ if binding.root_agent_id == binding.agent_id && binding.root_run_id == binding.run_id {
+ return Ok(binding.source);
+ }
+ let root_binding = read_game_creator_agent_runtime_run_profile_binding(
+ root,
+ &binding.root_agent_id,
+ &binding.root_run_id,
+ )?
+ .ok_or_else(|| "Agent Runtime 缺少 root Run Profile 绑定".to_string())?;
+ if root_binding.agent_id != binding.root_agent_id
+ || root_binding.run_id != binding.root_run_id
+ || root_binding.root_agent_id != root_binding.agent_id
+ || root_binding.root_run_id != root_binding.run_id
+ {
+ return Err("Agent Runtime root Run Profile 绑定身份不一致".to_string());
+ }
+ Ok(root_binding.source)
+}
+
fn game_creator_agent_context_preload_notice(agent_id: &str) -> &'static str {
if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
"下方已预加载有界仓库启动上下文、Supervisor 当前 Session、legacy 项目对话、项目记忆、黑板和资产摘要;源码正文仍只能通过已获准工具读取"
@@ -126,6 +163,11 @@ pub(in crate::agent) fn build_game_creator_agent_background_tool_plan_request(
system_prompt.push_str(
"\n\n当前 Run Profile 为 autonomous-game-build。不得调用 user.input_request,也不得为了等待确认而中断;对不改变核心目标的缺失细节,直接采用可逆、保守且可试玩的默认值。只使用当前 autoTools 推进项目内实现、委派和验证,不得请求 project.git_commit、command.exec、command.start、command.stdin、command.terminate 或其他仍需确认的动作。Project Supervisor 必须持续编排到最小可玩闭环通过 Runtime 完成门禁;专业 Agent 必须完成自己的合同并把结果交回父 Run。",
);
+ if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
+ system_prompt.push_str(
+ "\n\nautonomous-game-build 的正式 manifest 任务图是唯一首轮专业执行链。不得在 manifest 之前另行创建 code-prototype、quality-review、art-director、design-foundation 或 art-asset-plan 的首批 agent.delegate;这些角色会由 Runtime 按 manifest 依赖顺序调度。没有待认领的显式返工合同时也不得额外委派。请直接推进/观察 manifest,Runtime 会在你尝试收束时调度 ready task,并在任务图完成前阻止最终交付。",
+ );
+ }
system_prompt.push_str(&format!(
"\n\n自主构建专业 Agent 在首次项目修改前最多允许 {AGENT_RUNTIME_AUTONOMOUS_PRE_MUTATION_LOOP_LIMIT} 轮 planning 探索。达到上限后,本响应必须直接调用 file.write、file.patch、file.delete、project.patchset、project.restore、canvas.asset_generate 等实际项目修改工具;若当前专业合同确实只要求只读验收,则必须调用 respond_to_user 交付结论。不得继续只调用 update_agent_plan、读取、搜索、状态查询或空验证。"
));
@@ -137,6 +179,15 @@ pub(in crate::agent) fn build_game_creator_agent_background_tool_plan_request(
system_prompt.push_str(playtest_contract);
system_prompt.push_str(" 只有当前 revision 通过 game.static_smoke,并由 preview.validate 对上述固定状态面和控件完成真实浏览器动作后,Runtime 才允许最终回复;不要伪造已通过 observation。");
}
+ if autonomous_game_build {
+ let root_source = agent_runtime_root_source_at(root, agent_id, run_id)?;
+ if let Some(fast_path_prompt) =
+ game_chat_fast_path_prompt_for_root_source(agent_id, &root_source)
+ {
+ system_prompt.push_str("\n\n");
+ system_prompt.push_str(fast_path_prompt);
+ }
+ }
let mut request = LlmRunRequest::new(vec![
LlmMessage::system(system_prompt),
LlmMessage::user(prompt),
@@ -344,10 +395,13 @@ pub(in crate::agent) fn build_game_creator_background_agent_context(
#[cfg(test)]
mod tests {
use super::{
+ agent_runtime_root_source_at, bind_game_creator_agent_runtime_run_profile_at,
build_game_creator_agent_background_tool_plan_request,
- game_creator_agent_context_preload_notice, init_local_game_project_at,
- start_game_creator_agent_runtime_task_at, GameCreatorMcpCatalog,
- AGENT_RUNTIME_COMPLETION_BLOCKER_TOOL_PLAN_PROTOCOL,
+ game_chat_fast_path_prompt_for_root_source, game_creator_agent_context_preload_notice,
+ init_local_game_project_at, start_game_creator_agent_runtime_task_at, AgentRuntimeTaskLink,
+ GameCreatorMcpCatalog, AGENT_RUNTIME_COMPLETION_BLOCKER_TOOL_PLAN_PROTOCOL,
+ AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD, AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
+ AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE, AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
};
@@ -451,4 +505,87 @@ mod tests {
assert!(protocol.contains("不得反复提交 final response"));
assert!(protocol.contains("不得按项目正文硬编码"));
}
+
+ #[test]
+ fn game_chat_fast_path_prompt_forces_one_shot_playable_write_only_for_code_agent() {
+ let prompt = game_chat_fast_path_prompt_for_root_source(
+ "code-prototype",
+ AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
+ )
+ .expect("game-chat code fast path prompt");
+
+ assert!(prompt.contains("五分钟快车道"));
+ assert!(prompt.contains("一次 Provider planning"));
+ assert!(prompt.contains("直接调用一次 file.write"));
+ assert!(prompt.contains("禁止先调用读取、搜索、任务查询、委派"));
+ assert!(prompt.contains("../assets/art-spec.png"));
+ assert!(prompt.contains("平台视觉规范图"));
+ assert!(prompt.contains("主要背景"));
+ assert!(prompt.contains("玩家角色和目标实体"));
+ assert!(prompt.contains("禁止仅放置隐藏 img"));
+ assert!(prompt.contains("不能退回纯 Canvas 几何兜底"));
+ assert!(!prompt.contains("art-spritesheet.png"));
+ assert!(game_chat_fast_path_prompt_for_root_source(
+ "quality-review",
+ AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
+ )
+ .is_none());
+ assert!(game_chat_fast_path_prompt_for_root_source(
+ "code-prototype",
+ AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
+ )
+ .is_none());
+ assert!(game_chat_fast_path_prompt_for_root_source(
+ "code-prototype",
+ AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
+ )
+ .is_none());
+ assert!(game_chat_fast_path_prompt_for_root_source(
+ "code-prototype",
+ "agent-background-task",
+ )
+ .is_none());
+ }
+
+ #[test]
+ fn root_source_resolver_uses_root_binding_for_game_chat_child() {
+ let temporary = tempfile::tempdir().expect("temporary project root");
+ let root = temporary.path().join("project");
+ init_local_game_project_at(&root, "root-source-project", "root source test")
+ .expect("project init");
+ let parent = bind_game_creator_agent_runtime_run_profile_at(
+ &root,
+ GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
+ "root-source-game-chat-run",
+ AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
+ Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD),
+ None,
+ )
+ .expect("bind game-chat root profile");
+ let child_link = AgentRuntimeTaskLink {
+ parent_agent_id: Some(parent.agent_id.clone()),
+ parent_run_id: Some(parent.run_id.clone()),
+ delegation_id: Some("root-source-game-chat-child-delegation".to_string()),
+ };
+ let child = bind_game_creator_agent_runtime_run_profile_at(
+ &root,
+ "code-prototype",
+ "root-source-game-chat-child",
+ "agent-ready-task-scheduler",
+ None,
+ Some(&child_link),
+ )
+ .expect("bind game-chat child profile");
+
+ assert_eq!(
+ agent_runtime_root_source_at(&root, &parent.agent_id, &parent.run_id)
+ .expect("resolve root source"),
+ AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE
+ );
+ assert_eq!(
+ agent_runtime_root_source_at(&root, &child.agent_id, &child.run_id)
+ .expect("resolve child root source"),
+ AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE
+ );
+ }
}
diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_tool_plan.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_tool_plan.rs
index 21336aba1..556316ca5 100644
--- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_tool_plan.rs
+++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_tool_plan.rs
@@ -52,7 +52,7 @@ fn supervisor_collaboration_missing_agent_ids(error: &str) -> Vec {
.collect::>()
})
.unwrap_or_default();
- for agent_id in ["code-prototype", "quality-review", "art-asset-plan"] {
+ for agent_id in ["design-director", "art-director", "code-director"] {
if error.contains(&format!("缺少 {agent_id} 委派"))
&& !missing.iter().any(|value| value == agent_id)
{
@@ -62,6 +62,31 @@ fn supervisor_collaboration_missing_agent_ids(error: &str) -> Vec {
missing
}
+fn is_autonomous_initial_leader_delegate_action(action: &AgentRuntimeToolAction) -> bool {
+ if action.tool.trim() != "agent.delegate" {
+ return false;
+ }
+ let Some(input) = action.input.as_object() else {
+ return false;
+ };
+ if input
+ .get("repairOfDelegationId")
+ .or_else(|| input.get("repair_of_delegation_id"))
+ .and_then(serde_json::Value::as_str)
+ .is_some_and(|value| !value.trim().is_empty())
+ {
+ return false;
+ }
+ matches!(
+ input
+ .get("agentId")
+ .or_else(|| input.get("agent_id"))
+ .and_then(serde_json::Value::as_str)
+ .map(str::trim),
+ Some("design-director" | "art-director" | "code-director")
+ )
+}
+
fn restrict_supervisor_collaboration_repair_to_missing_agents(
request: &mut LlmRunRequest,
error: &str,
@@ -843,9 +868,22 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
if force_supervisor_initial_collaboration {
supervisor_collaboration_repair_active = true;
if let Some(actions) = supervisor_collaboration_candidate_actions.take() {
- supervisor_collaboration_repair_actions = actions;
+ supervisor_collaboration_repair_actions =
+ if run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD {
+ actions
+ .into_iter()
+ .filter(is_autonomous_initial_leader_delegate_action)
+ .collect()
+ } else {
+ actions
+ };
}
restrict_agent_runtime_supervisor_collaboration_repair_tools(&mut request)?;
+ if run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD {
+ restrict_agent_runtime_autonomous_initial_collaboration_repair_tools(
+ &mut request,
+ )?;
+ }
restrict_supervisor_collaboration_repair_to_missing_agents(
&mut request,
&protocol_error,
@@ -854,7 +892,7 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
== AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
{
format!(
- "上一条输出不符合工具计划协议:{protocol_error}\n本次修复的原生工具目录只保留首批协作工具。必须在同一响应一次性建立完整首批合同:code-prototype 必须是非只读实现任务且 expectedArtifacts 包含 game/index.html;quality-review 的 task 必须显式声明只读、不得修改项目,且 expectedArtifacts 必须为 [];如当前 policy 的 requiredStaticAgentIds 包含 art-director,必须加入非只读规范图委派且 expectedArtifacts 包含 assets/art-spec.png;如包含 design-foundation,必须加入非只读设计委派且 expectedArtifacts 包含 memory/project.md、game/game_design.md 与 assets/ui-prototype.png;如包含 art-asset-plan,还必须加入非只读美术生成委派且 expectedArtifacts 同时包含 assets/manifest.art.json 与 assets/art-spritesheet.png。所有静态委派都使用 agent.delegate,repairOfDelegationId=null、runId=null;如当前 policy 还要求 isolated,再在同批补齐 agent.spawn_isolated。不得更新计划、读取、搜索、查询状态、修改项目或返回最终回复。不要解释,不要 markdown,不要代码围栏。"
+ "上一条输出不符合工具计划协议:{protocol_error}\n本次修复的原生工具目录只保留 agent.delegate。必须在同一响应一次性建立完整首批合同,且只允许以下三个非 repair 委派,各出现一次:design-director 与 code-director 的 task 或 acceptanceCriteria 必须显式声明只读且不得修改项目,expectedArtifacts 必须为 [];art-director 必须是非只读规范图生成任务,expectedArtifacts 必须包含 assets/art-spec.png。三者都必须提供非空 task、1-8 条 acceptanceCriteria,并设置 repairOfDelegationId=null、runId=null。不得委派 code-prototype、quality-review、design-foundation、art-asset-plan 或其它底层 Agent,不得调用 agent.spawn_isolated,不得更新计划、读取、搜索、查询状态、修改项目或返回最终回复。不要解释,不要 markdown,不要代码围栏。"
)
} else {
format!(
@@ -1081,12 +1119,42 @@ mod supervisor_collaboration_repair_tests {
.is_empty());
assert_eq!(
supervisor_collaboration_missing_agent_ids(
- "Project Supervisor 首批协作不满足项目合同:missingStaticAgents=code-prototype,quality-review · isolatedChildrenTotal=0"
+ "Project Supervisor 首批协作不满足项目合同:missingStaticAgents=design-director,art-director,code-director · isolatedChildrenTotal=0"
),
- vec!["code-prototype".to_string(), "quality-review".to_string()]
+ vec![
+ "design-director".to_string(),
+ "art-director".to_string(),
+ "code-director".to_string(),
+ ]
);
}
+ #[test]
+ fn autonomous_initial_repair_keeps_only_leader_delegates() {
+ let actions = [
+ collaboration_action(
+ "agent.delegate",
+ serde_json::json!({"agentId": "design-director", "repairOfDelegationId": null}),
+ ),
+ collaboration_action(
+ "agent.delegate",
+ serde_json::json!({"agentId": "code-prototype", "repairOfDelegationId": null}),
+ ),
+ collaboration_action(
+ "agent.spawn_isolated",
+ serde_json::json!({"children": [], "joinMode": "all"}),
+ ),
+ ];
+
+ let kept = actions
+ .iter()
+ .filter(|action| is_autonomous_initial_leader_delegate_action(action))
+ .map(|action| action.input["agentId"].as_str().expect("leader id"))
+ .collect::>();
+
+ assert_eq!(kept, vec!["design-director"]);
+ }
+
#[test]
fn isolated_repair_replaces_the_single_accumulated_slot() {
let accumulated = vec![collaboration_action(
diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/response_stream_tests.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/response_stream_tests.rs
index ccba17a4f..ba11e9fd4 100644
--- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/response_stream_tests.rs
+++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/response_stream_tests.rs
@@ -501,6 +501,90 @@ fn response_stream_finalization_commits_exactly_one_canonical_assistant() {
assert_eq!(assistants, vec![response]);
}
+#[test]
+fn non_stream_professional_final_reply_remains_queryable_after_later_project_revision() {
+ assert!(
+ !GameCreatorLlmConfig::default().stream,
+ "the production default exercises the non-stream final-reply path"
+ );
+ let project = tempfile::tempdir().expect("create non-stream professional reply project");
+ let root = project.path();
+ init_local_game_project_at(
+ root,
+ "non-stream-professional-reply",
+ "非流式专业 Agent 回复",
+ )
+ .expect("initialize non-stream professional reply project");
+ let mut state = start_game_creator_agent_runtime_task_at(
+ root,
+ "art-director",
+ "生成 game-chat 首版统一视觉规范",
+ "non-stream-art-director-run",
+ "agent-delegate",
+ "整理专业 Agent 最终回复",
+ vec!["生成并登记统一视觉规范图".to_string()],
+ )
+ .expect("start non-stream professional runtime");
+ state.parent_agent_id = Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string());
+ state.parent_run_id = Some("game-chat-parent-run".to_string());
+ state.delegation_id = Some("game-chat-art-director-delegation".to_string());
+ state.loop_iteration = 1;
+ state.status = "running".to_string();
+ state.phase = "response".to_string();
+ state.current_action = "直接采用非流式最终回复".to_string();
+ state.waiting_on = "finalization 持久化".to_string();
+ state.next_step = "提交 durable response stream 投影".to_string();
+ state.updated_at = unix_timestamp();
+ append_game_creator_agent_runtime_task(root, &state)
+ .expect("append non-stream professional runtime task");
+ write_game_creator_agent_runtime_state(root, &state)
+ .expect("write non-stream professional runtime state");
+
+ let response_revision = read_game_creator_agent_runtime_project_revision(root)
+ .expect("read non-stream response revision")
+ .revision;
+ assert!(
+ read_game_creator_agent_runtime_response_stream_at(root, &state.agent_id, &state.run_id,)
+ .expect("read absent pre-finalization response stream")
+ .is_none(),
+ "stream=false must enter finalization without a pre-existing stream sidecar"
+ );
+
+ let response = "美术 Agent:统一视觉规范图已生成并登记。";
+ let completed = finish_game_creator_agent_background_runtime_turn_at(
+ root,
+ state.clone(),
+ response,
+ response_revision,
+ &[],
+ )
+ .expect("finalize non-stream professional reply");
+ assert!(matches!(
+ completed,
+ AgentBackgroundFinalizationOutcome::Completed(_)
+ ));
+
+ let mut later_revision = read_game_creator_agent_runtime_project_revision(root)
+ .expect("read project revision before later stage mutation");
+ later_revision.revision = later_revision.revision.saturating_add(1);
+ later_revision.updated_at = unix_timestamp();
+ write_game_creator_agent_runtime_project_revision(root, &later_revision)
+ .expect("simulate a later game-chat stage advancing project revision");
+
+ let queried = read_game_creator_agent_runtime_at(root, &state.agent_id)
+ .expect("query completed professional runtime after revision advance");
+ let stream = queried
+ .response_stream
+ .expect("durable professional final reply remains queryable");
+ assert_eq!(
+ stream.status,
+ AGENT_RUNTIME_RESPONSE_STREAM_STATUS_COMMITTED
+ );
+ assert_eq!(stream.request_kind, "final-reply");
+ assert_eq!(stream.response_revision, response_revision);
+ assert_eq!(stream.accumulated_text, response);
+}
+
#[test]
fn finalization_cleanup_closes_entire_tool_plan_repair_chain_before_removal() {
let (project, state, response_revision, snapshot) =
diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver.rs
index fa86b1a46..bb36a99d4 100644
--- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver.rs
+++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver.rs
@@ -100,6 +100,16 @@ pub(crate) const AGENT_RUNTIME_ISOLATED_CHILD_SOURCE: &str = "agent-isolated-chi
pub(crate) const AGENT_RUNTIME_ISOLATED_JOIN_SOURCE: &str = "agent-isolated-join";
pub(crate) const AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE: &str = "project-supervisor-gui";
pub(crate) const AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE: &str = "project-supervisor-cli";
+pub(crate) const AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE: &str = "project-supervisor-game-chat";
+
+pub(crate) fn agent_runtime_supervisor_source_is_trusted(source: &str) -> bool {
+ matches!(
+ source.trim(),
+ AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE
+ | AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE
+ | AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE
+ )
+}
pub(super) const AGENT_RUNTIME_RUN_PROFILE_BINDING_SCHEMA_VERSION: &str =
"game-creator-run-profile-binding.v1";
pub(super) const AGENT_RUNTIME_AUTONOMOUS_COMPLETION_CONTRACT_SCHEMA_VERSION: &str =
@@ -196,10 +206,13 @@ pub(super) struct AgentRuntimeAutonomousPlaytestReceipt {
mod entrypoints;
mod finalization;
+mod game_chat_fast_path;
mod interaction;
mod lifecycle_control;
mod main_loop;
#[cfg(test)]
+mod main_loop_deadline_tests;
+#[cfg(test)]
mod main_loop_tests;
mod pending_execution;
mod pending_recovery;
@@ -210,6 +223,7 @@ mod task_start;
pub(in crate::agent) use entrypoints::*;
pub(in crate::agent) use finalization::*;
+pub(in crate::agent) use game_chat_fast_path::*;
pub(in crate::agent) use interaction::*;
pub(in crate::agent) use lifecycle_control::*;
pub(in crate::agent) use main_loop::*;
@@ -268,6 +282,7 @@ pub(crate) use provider_recovery::{
};
pub(crate) use recovery_scan::{
cleanup_game_creator_agent_runtime_completed_finalizations_at,
+ has_recoverable_game_creator_agent_background_tasks_at,
resume_game_creator_agent_background_tasks_at,
resume_game_creator_agent_pending_action_for_agent_at,
wake_pending_game_creator_agent_background_tasks_at,
diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/game_chat_fast_path.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/game_chat_fast_path.rs
new file mode 100644
index 000000000..8a8f51cdc
--- /dev/null
+++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/game_chat_fast_path.rs
@@ -0,0 +1,801 @@
+//! A deterministic, dependency-free game-chat fallback.
+//!
+//! This module deliberately does not start the runtime or write project files. It only
+//! renders a small, self-contained HTML document that the runtime can use when it needs to
+//! make a first playable version available before the normal generation pass finishes.
+
+use super::*;
+
+pub(crate) const GAME_CHAT_FIRST_PLAYABLE_SOFT_BUDGET_SECONDS: u64 = 240;
+pub(crate) const GAME_CHAT_FIRST_PLAYABLE_HARD_BUDGET_SECONDS: u64 = 300;
+pub(crate) const GAME_CHAT_FIRST_PLAYABLE_HARD_BUDGET_ERROR_PREFIX: &str =
+ "game-chat-first-playable-hard-budget-exhausted";
+
+const FALLBACK_THEME_MARKER: &str = "__GAME_CHAT_THEME__";
+const FALLBACK_PLATFORM_ART_MARKER: &str = "__GAME_CHAT_PLATFORM_ART__";
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub(crate) struct GameChatFastPathBudget {
+ pub(crate) root_agent_id: String,
+ pub(crate) root_run_id: String,
+ pub(crate) baseline_revision: u64,
+ pub(crate) elapsed_seconds: u64,
+}
+
+pub(crate) fn game_chat_fast_path_budget_at(
+ root: &Path,
+ agent_id: &str,
+ run_id: &str,
+ now: u64,
+) -> Result