Merge remote-tracking branch 'origin/master' into fix/multi-select
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
"autoCompactTokenLimit": 64000,
|
||||
"toolOutputTokenLimit": 12000,
|
||||
"requestTimeoutMs": 180000,
|
||||
"maxRetries": 0,
|
||||
"maxRetries": 2,
|
||||
"retryBackoffMs": 500
|
||||
},
|
||||
"agentLlm": {},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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}}
|
||||
</style>
|
||||
</head>
|
||||
<body><main>
|
||||
<body><main><img src="assets/art-spritesheet.png" alt="Garden defenders" width="96" height="96">
|
||||
<h1>灵露花园</h1><p>GENARRATIVE_REAL_E2E_VISIBLE</p><p>Goal: defend the garden and win every wave.</p>
|
||||
<div class="toolbar"><button data-playtest-id="start">Start Game</button><button data-playtest-id="speed-up">Speed Up</button><button data-playtest-id="next-level">Next Level</button><button data-playtest-id="restart">Restart</button></div>
|
||||
<div class="plants"><button data-playtest-id="defender-option">露华花</button><button id="thorn">棘刺芽</button></div>
|
||||
|
||||
@@ -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('<title>AI 游戏创作</title>') &&
|
||||
!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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
+208
-100
@@ -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::<Vec<_>>();
|
||||
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<bool, String> {
|
||||
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::<BTreeSet<_>>();
|
||||
@@ -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");
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
+140
-3
@@ -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<String, String> {
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+73
-5
@@ -52,7 +52,7 @@ fn supervisor_collaboration_missing_agent_ids(error: &str) -> Vec<String> {
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.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<String> {
|
||||
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::<Vec<_>>();
|
||||
|
||||
assert_eq!(kept, vec!["design-director"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn isolated_repair_replaces_the_single_accumulated_slot() {
|
||||
let accumulated = vec![collaboration_action(
|
||||
|
||||
+84
@@ -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) =
|
||||
|
||||
@@ -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,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -761,10 +761,7 @@ pub(crate) fn resolve_game_creator_agent_runtime_retry_configuration_at(
|
||||
if binding.parent_run_id.is_some()
|
||||
|| binding.root_agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
||||
|| binding.root_run_id != task.run_id
|
||||
|| !matches!(
|
||||
binding.source.as_str(),
|
||||
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE | AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE
|
||||
)
|
||||
|| !agent_runtime_supervisor_source_is_trusted(&binding.source)
|
||||
{
|
||||
return Err("自主构建 Agent Runtime 重试绑定不是可信 Supervisor 根 Run".to_string());
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+162
@@ -0,0 +1,162 @@
|
||||
use super::main_loop::{
|
||||
await_game_chat_absolute_deadline_at, finish_game_chat_absolute_deadline_timeout_at,
|
||||
game_chat_absolute_deadline_from_bound_at,
|
||||
};
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn game_chat_absolute_deadline_is_root_bound_at_plus_hard_budget() {
|
||||
let now_instant = tokio::time::Instant::now();
|
||||
let bound_at = 10_000;
|
||||
let now_unix = bound_at + 17;
|
||||
|
||||
let deadline = game_chat_absolute_deadline_from_bound_at(now_instant, now_unix, bound_at);
|
||||
|
||||
assert_eq!(
|
||||
deadline.duration_since(now_instant),
|
||||
std::time::Duration::from_secs(GAME_CHAT_FIRST_PLAYABLE_HARD_BUDGET_SECONDS - 17)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn game_chat_absolute_deadline_is_immediate_once_root_budget_is_exhausted() {
|
||||
let now_instant = tokio::time::Instant::now();
|
||||
let bound_at = 20_000;
|
||||
|
||||
let deadline = game_chat_absolute_deadline_from_bound_at(
|
||||
now_instant,
|
||||
bound_at + GAME_CHAT_FIRST_PLAYABLE_HARD_BUDGET_SECONDS + 1,
|
||||
bound_at,
|
||||
);
|
||||
|
||||
assert_eq!(deadline, now_instant);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn game_chat_absolute_deadline_stops_a_never_resolving_in_flight_action() {
|
||||
let deadline = tokio::time::Instant::now() + std::time::Duration::from_millis(20);
|
||||
let started = std::time::Instant::now();
|
||||
|
||||
let result = await_game_chat_absolute_deadline_at(
|
||||
deadline,
|
||||
std::future::pending::<AgentBackgroundTaskOutcome>(),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"never-resolving action must hit the hard deadline"
|
||||
);
|
||||
assert!(
|
||||
started.elapsed() < std::time::Duration::from_secs(1),
|
||||
"deadline test must not hang"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn game_chat_absolute_deadline_returns_an_in_flight_result_before_expiry() {
|
||||
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(1);
|
||||
|
||||
let result = await_game_chat_absolute_deadline_at(deadline, async { "completed" }).await;
|
||||
|
||||
assert_eq!(result.expect("in-flight action completes"), "completed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn game_chat_absolute_deadline_forces_needs_reconciliation_to_failed_before_cleanup() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"genarrative-game-chat-deadline-reconciliation-{}-{}",
|
||||
std::process::id(),
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.expect("system clock")
|
||||
.as_nanos()
|
||||
));
|
||||
init_local_game_project_at(&root, "deadline-reconciliation", "硬截止收尾测试")
|
||||
.expect("project init");
|
||||
let mut runtime = start_game_creator_agent_runtime_task_at(
|
||||
&root,
|
||||
"code-prototype",
|
||||
"执行可能悬挂的首版写入",
|
||||
"game-chat-deadline-reconciliation-run",
|
||||
"agent-ready-task-scheduler",
|
||||
"正在执行首版写入",
|
||||
vec!["执行首版写入".to_string()],
|
||||
)
|
||||
.expect("start runtime");
|
||||
runtime.loop_iteration = 1;
|
||||
let action = AgentRuntimeToolAction {
|
||||
tool: "file.write".to_string(),
|
||||
reason: Some("模拟截止时仍在途的写入".to_string()),
|
||||
input: serde_json::json!({
|
||||
"path": "game/index.html",
|
||||
"content": "<!doctype html><title>deadline</title>"
|
||||
}),
|
||||
};
|
||||
let plan = AgentRuntimeToolPlan {
|
||||
thinking_summary: "准备首版写入".to_string(),
|
||||
plan_update: None,
|
||||
plan: vec!["写入首版".to_string()],
|
||||
actions: vec![action.clone()],
|
||||
response: String::new(),
|
||||
};
|
||||
let project_revision =
|
||||
read_game_creator_agent_runtime_project_revision(&root).expect("read project revision");
|
||||
let repository_fingerprint = build_repository_startup_context_at(&root)
|
||||
.expect("repository context")
|
||||
.fingerprint;
|
||||
let pending = build_game_creator_agent_runtime_pending_tool_action(
|
||||
&root,
|
||||
&runtime,
|
||||
&runtime.current_task,
|
||||
&plan,
|
||||
&[],
|
||||
&project_revision,
|
||||
&repository_fingerprint,
|
||||
&action,
|
||||
0,
|
||||
AGENT_RUNTIME_ACTION_EXECUTION_MODE_AUTO,
|
||||
AGENT_RUNTIME_PENDING_ACTION_STATUS_APPROVED,
|
||||
None,
|
||||
)
|
||||
.expect("build pending action");
|
||||
write_game_creator_agent_runtime_pending_tool_action(&root, &pending)
|
||||
.expect("write pending action");
|
||||
runtime.pending_tool_action = Some(pending.summary());
|
||||
runtime.status = "failed".to_string();
|
||||
runtime.phase = "needs-reconciliation".to_string();
|
||||
runtime.current_action = "等待人工核对在途动作".to_string();
|
||||
runtime.waiting_on = "开发者核对副作用".to_string();
|
||||
runtime.next_step = "核对后恢复".to_string();
|
||||
runtime.error = Some("模拟 needs-reconciliation".to_string());
|
||||
append_game_creator_agent_runtime_task(&root, &runtime).expect("append reconciliation task");
|
||||
write_game_creator_agent_runtime_state(&root, &runtime).expect("write reconciliation state");
|
||||
|
||||
let outcome = finish_game_chat_absolute_deadline_timeout_at(
|
||||
&root,
|
||||
&runtime.agent_id,
|
||||
&runtime.session_id,
|
||||
runtime.clone(),
|
||||
);
|
||||
|
||||
assert!(matches!(outcome, AgentBackgroundTaskOutcome::Finished));
|
||||
let terminal = read_game_creator_agent_runtime_at(&root, &runtime.agent_id)
|
||||
.expect("read terminal runtime")
|
||||
.state;
|
||||
assert_eq!(terminal.run_id, runtime.run_id);
|
||||
assert_eq!(terminal.status, "failed");
|
||||
assert_eq!(terminal.phase, "failed");
|
||||
assert!(terminal.pending_tool_action.is_none());
|
||||
assert!(!game_creator_agent_runtime_pending_tool_action_exists(
|
||||
&root,
|
||||
&runtime.agent_id,
|
||||
&runtime.run_id
|
||||
));
|
||||
assert!(!game_creator_agent_runtime_provider_action_batch_exists(
|
||||
&root,
|
||||
&runtime.agent_id,
|
||||
&runtime.run_id
|
||||
));
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
@@ -25,6 +25,86 @@ fn register_autonomous_recovery_visual_fixture(root: &Path, local_path: &str, ki
|
||||
.expect("register autonomous recovery visual fixture");
|
||||
}
|
||||
|
||||
fn register_game_chat_art_spec_fixture(root: &Path) {
|
||||
image::RgbaImage::from_pixel(4, 4, image::Rgba([90, 140, 220, u8::MAX]))
|
||||
.save(root.join(AGENT_RUNTIME_ART_SPEC_PATH))
|
||||
.expect("write game-chat art spec PNG");
|
||||
register_local_asset_at(
|
||||
root,
|
||||
AGENT_RUNTIME_ART_SPEC_PATH,
|
||||
"icon-spec",
|
||||
"image/png",
|
||||
"canvas",
|
||||
GameCreationAppAssetSource {
|
||||
kind: GameCreationAppAssetSourceKind::Canvas,
|
||||
canvas_project_id: Some("game-chat-canvas".to_string()),
|
||||
resource_id: Some("game-chat-art-spec-resource".to_string()),
|
||||
asset_object_id: Some("game-chat-art-spec-object".to_string()),
|
||||
task_id: Some("art-director".to_string()),
|
||||
prompt: None,
|
||||
model: None,
|
||||
generation_route: Some("/api/external/v1/editor/images/generations".to_string()),
|
||||
generation_kind: Some("spec".to_string()),
|
||||
reference_resource_ids: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("register game-chat art spec");
|
||||
}
|
||||
|
||||
fn queue_game_chat_fast_path_child(
|
||||
root: &Path,
|
||||
root_run_id: &str,
|
||||
root_task: &str,
|
||||
child_id: &str,
|
||||
) -> (AgentRuntimeState, AgentRuntimeState) {
|
||||
let root_session = resolve_agent_conversation_session_id_at(
|
||||
root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.expect("resolve game-chat root session");
|
||||
let root_record = append_unique_game_creator_agent_runtime_pending_task(
|
||||
root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&root_session,
|
||||
root_task,
|
||||
root_run_id,
|
||||
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
|
||||
Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD),
|
||||
None,
|
||||
)
|
||||
.expect("queue game-chat root");
|
||||
let root_state = agent_runtime_state_from_task_record(&root_record);
|
||||
let manifest = read_manifest_for_project(root).expect("read game-chat manifest");
|
||||
let task = manifest
|
||||
.tasks
|
||||
.iter()
|
||||
.find(|task| task.id == child_id)
|
||||
.unwrap_or_else(|| panic!("missing game-chat child task {child_id}"));
|
||||
let child_session = resolve_agent_conversation_session_id_at(root, child_id, None, true)
|
||||
.expect("resolve game-chat child session");
|
||||
let child_record = append_unique_game_creator_agent_runtime_pending_task(
|
||||
root,
|
||||
child_id,
|
||||
&child_session,
|
||||
&render_autonomous_manifest_ready_task_background_prompt(task),
|
||||
&autonomous_manifest_ready_task_run_id(root_run_id, child_id),
|
||||
"agent-ready-task-scheduler",
|
||||
None,
|
||||
Some(&AgentRuntimeTaskLink {
|
||||
parent_agent_id: Some(root_state.agent_id.clone()),
|
||||
parent_run_id: Some(root_state.run_id.clone()),
|
||||
delegation_id: None,
|
||||
}),
|
||||
)
|
||||
.expect("queue game-chat fast-path child");
|
||||
(
|
||||
root_state,
|
||||
agent_runtime_state_from_task_record(&child_record),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn autonomous_parent_keeps_planning_before_scheduling_registered_legacy_derived_visuals() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
@@ -65,7 +145,7 @@ fn prepare_autonomous_completion_evidence(root: &Path, state: &AgentRuntimeState
|
||||
write_local_project_file_at(
|
||||
root,
|
||||
AGENT_RUNTIME_GAME_INDEX_PATH,
|
||||
"<!doctype html><title>可玩塔防</title><canvas></canvas>",
|
||||
&render_game_chat_fast_path_html("可玩塔防"),
|
||||
)
|
||||
.expect("write autonomous game index");
|
||||
for (path, content) in [
|
||||
@@ -84,6 +164,7 @@ fn prepare_autonomous_completion_evidence(root: &Path, state: &AgentRuntimeState
|
||||
}
|
||||
revision
|
||||
};
|
||||
register_game_chat_art_spec_fixture(root);
|
||||
for task in new_game_creation_app_seed_tasks() {
|
||||
update_manifest_task_status_at(root, &task.id, GameCreationAppTaskStatus::Completed)
|
||||
.expect("complete autonomous manifest task");
|
||||
@@ -263,6 +344,214 @@ fn autonomous_visual_ready_tasks_only_require_images_when_editor_api_key_is_conf
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn game_chat_art_director_uses_deterministic_canvas_plan_without_provider_planning() {
|
||||
let _config_guard = crate::tests::write_test_local_config(
|
||||
r#"{"editorApi":{"apiKey":"game-chat-fast-art-key"}}"#.to_string(),
|
||||
);
|
||||
let temporary = tempfile::tempdir().expect("create game-chat art root");
|
||||
let root = temporary.path().join("project");
|
||||
init_local_game_project_at(&root, "game-chat-art-director", "星空飞船收集能量")
|
||||
.expect("init game-chat art project");
|
||||
let (root_state, child_state) = queue_game_chat_fast_path_child(
|
||||
&root,
|
||||
"game-chat-art-director-root",
|
||||
"制作星空飞船收集能量小游戏",
|
||||
"art-director",
|
||||
);
|
||||
let binding = read_game_creator_agent_runtime_run_profile_binding(
|
||||
&root,
|
||||
&root_state.agent_id,
|
||||
&root_state.run_id,
|
||||
)
|
||||
.expect("read game-chat root binding")
|
||||
.expect("game-chat root binding exists");
|
||||
|
||||
let plan = game_chat_fast_path_plan_at(
|
||||
&root,
|
||||
&child_state,
|
||||
&child_state.current_task,
|
||||
binding.bound_at,
|
||||
)
|
||||
.expect("build deterministic art-director plan")
|
||||
.expect("art-director fast path must bypass Provider planning");
|
||||
|
||||
assert_eq!(plan.actions.len(), 1);
|
||||
assert_eq!(plan.actions[0].tool, "canvas.asset_generate");
|
||||
assert_eq!(
|
||||
plan.actions[0].input["outputPath"],
|
||||
AGENT_RUNTIME_ART_SPEC_PATH
|
||||
);
|
||||
assert_eq!(plan.actions[0].input["assetKind"], "icon-spec");
|
||||
assert_eq!(plan.actions[0].input["aspectRatio"], "1:1");
|
||||
assert_eq!(plan.actions[0].input["imageSize"], "1K");
|
||||
assert_eq!(plan.actions[0].input["replaceExisting"], false);
|
||||
let serialized = serde_json::to_string(&plan).expect("serialize deterministic art plan");
|
||||
assert!(!serialized.contains("art-spritesheet"));
|
||||
assert!(!serialized.contains("icon-spritesheets/generations"));
|
||||
assert!(!root.join("assets/manifest.art.json").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn game_chat_art_stage_fails_before_provider_when_editor_api_is_missing() {
|
||||
let _config_guard = crate::tests::write_test_local_config("{}".to_string());
|
||||
let temporary = tempfile::tempdir().expect("create unconfigured game-chat art root");
|
||||
let root = temporary.path().join("project");
|
||||
init_local_game_project_at(&root, "game-chat-art-unconfigured", "太空躲避")
|
||||
.expect("init unconfigured game-chat art project");
|
||||
let (root_state, child_state) = queue_game_chat_fast_path_child(
|
||||
&root,
|
||||
"game-chat-art-unconfigured-root",
|
||||
"制作太空躲避小游戏",
|
||||
"art-director",
|
||||
);
|
||||
let binding = read_game_creator_agent_runtime_run_profile_binding(
|
||||
&root,
|
||||
&root_state.agent_id,
|
||||
&root_state.run_id,
|
||||
)
|
||||
.expect("read game-chat root binding")
|
||||
.expect("game-chat root binding exists");
|
||||
|
||||
let error = game_chat_fast_path_plan_at(
|
||||
&root,
|
||||
&child_state,
|
||||
&child_state.current_task,
|
||||
binding.bound_at,
|
||||
)
|
||||
.expect_err("unconfigured game-chat art must fail closed before Provider planning");
|
||||
assert!(error.contains("External Editor API Key"));
|
||||
assert!(error.contains("平台美术资源"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn game_chat_art_fast_path_idempotently_settles_registered_art_spec() {
|
||||
let _config_guard = crate::tests::write_test_local_config(
|
||||
r#"{"editorApi":{"apiKey":"game-chat-idempotent-art-key"}}"#.to_string(),
|
||||
);
|
||||
|
||||
let art_director_temporary = tempfile::tempdir().expect("create idempotent art-spec root");
|
||||
let art_director_root = art_director_temporary.path().join("project");
|
||||
init_local_game_project_at(
|
||||
&art_director_root,
|
||||
"game-chat-idempotent-art-spec",
|
||||
"海岛收集",
|
||||
)
|
||||
.expect("init idempotent art-spec project");
|
||||
register_game_chat_art_spec_fixture(&art_director_root);
|
||||
let (root_state, art_director_state) = queue_game_chat_fast_path_child(
|
||||
&art_director_root,
|
||||
"game-chat-idempotent-art-spec-root",
|
||||
"制作海岛收集小游戏",
|
||||
"art-director",
|
||||
);
|
||||
let bound_at = read_game_creator_agent_runtime_run_profile_binding(
|
||||
&art_director_root,
|
||||
&root_state.agent_id,
|
||||
&root_state.run_id,
|
||||
)
|
||||
.expect("read idempotent art-spec root binding")
|
||||
.expect("idempotent art-spec root binding exists")
|
||||
.bound_at;
|
||||
let art_director_plan = game_chat_fast_path_plan_at(
|
||||
&art_director_root,
|
||||
&art_director_state,
|
||||
&art_director_state.current_task,
|
||||
bound_at,
|
||||
)
|
||||
.expect("settle registered art spec")
|
||||
.expect("registered art spec must use deterministic settlement");
|
||||
assert!(art_director_plan.actions.is_empty());
|
||||
assert!(art_director_plan.response.contains("已生成并登记"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn game_chat_code_prototype_ignores_art_director_global_revision_before_its_own_mutation() {
|
||||
let _config_guard = crate::tests::write_test_local_config(
|
||||
r#"{"editorApi":{"apiKey":"game-chat-code-revision-key"}}"#.to_string(),
|
||||
);
|
||||
let temporary = tempfile::tempdir().expect("create game-chat code revision root");
|
||||
let root = temporary.path().join("project");
|
||||
init_local_game_project_at(&root, "game-chat-code-revision", "太空飞船收集能量")
|
||||
.expect("init game-chat code revision project");
|
||||
let (root_state, art_director_state) = queue_game_chat_fast_path_child(
|
||||
&root,
|
||||
"game-chat-code-revision-root",
|
||||
"制作太空飞船收集能量小游戏",
|
||||
"art-director",
|
||||
);
|
||||
{
|
||||
let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
|
||||
&root,
|
||||
"test.game-chat.art-director.mutate",
|
||||
)
|
||||
.expect("acquire art-director mutation lock");
|
||||
let revision = prepare_agent_runtime_project_mutation_locked(
|
||||
&root,
|
||||
&art_director_state.agent_id,
|
||||
&art_director_state.run_id,
|
||||
"canvas.asset_generate",
|
||||
)
|
||||
.expect("advance global revision for art-director");
|
||||
assert_eq!(revision, 1);
|
||||
}
|
||||
|
||||
let manifest = read_manifest_for_project(&root).expect("read game-chat manifest");
|
||||
let code_task = manifest
|
||||
.tasks
|
||||
.iter()
|
||||
.find(|task| task.id == "code-prototype")
|
||||
.expect("code-prototype manifest task");
|
||||
let code_session =
|
||||
resolve_agent_conversation_session_id_at(&root, "code-prototype", None, true)
|
||||
.expect("resolve code-prototype session");
|
||||
let code_record = append_unique_game_creator_agent_runtime_pending_task(
|
||||
&root,
|
||||
"code-prototype",
|
||||
&code_session,
|
||||
&render_autonomous_manifest_ready_task_background_prompt(code_task),
|
||||
&autonomous_manifest_ready_task_run_id(&root_state.run_id, "code-prototype"),
|
||||
"agent-ready-task-scheduler",
|
||||
None,
|
||||
Some(&AgentRuntimeTaskLink {
|
||||
parent_agent_id: Some(root_state.agent_id.clone()),
|
||||
parent_run_id: Some(root_state.run_id.clone()),
|
||||
delegation_id: None,
|
||||
}),
|
||||
)
|
||||
.expect("queue code-prototype child after art mutation");
|
||||
let code_state = agent_runtime_state_from_task_record(&code_record);
|
||||
let code_gate = read_game_creator_agent_runtime_verification_gate(
|
||||
&root,
|
||||
&code_state.agent_id,
|
||||
&code_state.run_id,
|
||||
)
|
||||
.expect("read code-prototype gate");
|
||||
assert_eq!(code_gate.mutation_revision, None);
|
||||
assert_eq!(
|
||||
read_game_creator_agent_runtime_project_revision(&root)
|
||||
.expect("read global revision")
|
||||
.revision,
|
||||
1
|
||||
);
|
||||
let bound_at = read_game_creator_agent_runtime_run_profile_binding(
|
||||
&root,
|
||||
&root_state.agent_id,
|
||||
&root_state.run_id,
|
||||
)
|
||||
.expect("read game-chat root binding")
|
||||
.expect("game-chat root binding exists")
|
||||
.bound_at;
|
||||
|
||||
let plan = game_chat_fast_path_plan_at(&root, &code_state, &code_state.current_task, bound_at)
|
||||
.expect("evaluate code-prototype fast path");
|
||||
|
||||
assert!(
|
||||
plan.is_none(),
|
||||
"art-director's global revision must not skip the code Provider"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn autonomous_supervisor_empty_plan_uses_deterministic_final_reply_fallback() {
|
||||
assert_eq!(
|
||||
@@ -277,6 +566,167 @@ fn autonomous_supervisor_empty_plan_uses_deterministic_final_reply_fallback() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn game_chat_single_round_converges_without_another_provider_plan_after_playtest() {
|
||||
const RUN_ID: &str = "game-chat-single-round-convergence";
|
||||
const TASK: &str = "生成一个可玩的原创塔防小游戏,完成一轮后停止并打开预览";
|
||||
let _config_guard = crate::tests::write_test_local_config("{}".to_string());
|
||||
let temporary = tempfile::tempdir().expect("create game-chat convergence root");
|
||||
let root = temporary.path().join("project");
|
||||
init_local_game_project_at(&root, "game-chat-convergence", TASK)
|
||||
.expect("init game-chat convergence project");
|
||||
let session_id = resolve_agent_conversation_session_id_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.expect("resolve game-chat Supervisor session");
|
||||
let task_record = append_unique_game_creator_agent_runtime_pending_task(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&session_id,
|
||||
TASK,
|
||||
RUN_ID,
|
||||
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
|
||||
Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD),
|
||||
None,
|
||||
)
|
||||
.expect("queue game-chat Supervisor task");
|
||||
let mut runtime = agent_runtime_state_from_task_record(&task_record);
|
||||
apply_agent_runtime_plan_update(
|
||||
&mut runtime,
|
||||
&AgentRuntimePlanUpdate {
|
||||
explanation: "模型原本规划了继续迭代".to_string(),
|
||||
steps: vec![
|
||||
AgentRuntimePlanUpdateStep {
|
||||
step: "实现游戏".to_string(),
|
||||
status: AGENT_RUNTIME_PLAN_STATUS_COMPLETED.to_string(),
|
||||
},
|
||||
AgentRuntimePlanUpdateStep {
|
||||
step: "完成试玩".to_string(),
|
||||
status: AGENT_RUNTIME_PLAN_STATUS_IN_PROGRESS.to_string(),
|
||||
},
|
||||
AgentRuntimePlanUpdateStep {
|
||||
step: "继续下一轮".to_string(),
|
||||
status: AGENT_RUNTIME_PLAN_STATUS_PENDING.to_string(),
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
.expect("apply structured game-chat plan");
|
||||
assert!(runtime.plan_revision > 0);
|
||||
let revision = prepare_autonomous_completion_evidence(&root, &runtime);
|
||||
for task in new_game_creation_app_seed_tasks() {
|
||||
if !matches!(
|
||||
task.id.as_str(),
|
||||
"design-director"
|
||||
| "art-director"
|
||||
| "code-director"
|
||||
| "code-prototype"
|
||||
| "preview-readiness"
|
||||
| "preview-playtest"
|
||||
) {
|
||||
update_manifest_task_status_at(&root, &task.id, GameCreationAppTaskStatus::Pending)
|
||||
.unwrap_or_else(|error| panic!("leave non-fast-path task pending: {error}"));
|
||||
}
|
||||
}
|
||||
let mut provider_plan = AgentRuntimeToolPlan {
|
||||
actions: vec![AgentRuntimeToolAction {
|
||||
tool: "agent.run_status".to_string(),
|
||||
reason: Some("模型原本还想继续轮询".to_string()),
|
||||
input: serde_json::json!({}),
|
||||
}],
|
||||
..AgentRuntimeToolPlan::default()
|
||||
};
|
||||
|
||||
let convergence = prepare_game_chat_single_round_convergence_at(
|
||||
&root,
|
||||
&mut runtime,
|
||||
&mut provider_plan,
|
||||
unix_timestamp(),
|
||||
)
|
||||
.expect("prepare deterministic game-chat convergence")
|
||||
.expect("playtest-complete game-chat run must converge");
|
||||
|
||||
assert_eq!(convergence.1, revision);
|
||||
assert!(convergence.0.contains(&format!("revision {revision}")));
|
||||
assert!(provider_plan.actions.is_empty());
|
||||
assert!(runtime
|
||||
.plan_steps
|
||||
.iter()
|
||||
.all(|step| step.status == "completed"));
|
||||
assert_eq!(runtime.current_action, "首个可试玩版本已完成,正在结束本轮");
|
||||
assert!(autonomous_game_build_completion_blocker_at_locked(&root, &runtime).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn game_chat_single_round_cannot_converge_after_the_hard_budget() {
|
||||
const RUN_ID: &str = "game-chat-single-round-hard-budget";
|
||||
const TASK: &str = "生成一个可玩的原创塔防小游戏,并在五分钟内停止";
|
||||
let _config_guard = crate::tests::write_test_local_config("{}".to_string());
|
||||
let temporary = tempfile::tempdir().expect("create game-chat hard budget root");
|
||||
let root = temporary.path().join("project");
|
||||
init_local_game_project_at(&root, "game-chat-hard-budget", TASK)
|
||||
.expect("init game-chat hard budget project");
|
||||
let session_id = resolve_agent_conversation_session_id_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.expect("resolve game-chat Supervisor session");
|
||||
let task_record = append_unique_game_creator_agent_runtime_pending_task(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&session_id,
|
||||
TASK,
|
||||
RUN_ID,
|
||||
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
|
||||
Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD),
|
||||
None,
|
||||
)
|
||||
.expect("queue game-chat Supervisor task");
|
||||
let mut runtime = agent_runtime_state_from_task_record(&task_record);
|
||||
prepare_autonomous_completion_evidence(&root, &runtime);
|
||||
for task in new_game_creation_app_seed_tasks() {
|
||||
if !matches!(
|
||||
task.id.as_str(),
|
||||
"design-director"
|
||||
| "art-director"
|
||||
| "code-director"
|
||||
| "code-prototype"
|
||||
| "preview-readiness"
|
||||
| "preview-playtest"
|
||||
) {
|
||||
update_manifest_task_status_at(&root, &task.id, GameCreationAppTaskStatus::Pending)
|
||||
.unwrap_or_else(|error| panic!("leave non-fast-path task pending: {error}"));
|
||||
}
|
||||
}
|
||||
let binding = read_game_creator_agent_runtime_run_profile_binding(
|
||||
&root,
|
||||
&runtime.agent_id,
|
||||
&runtime.run_id,
|
||||
)
|
||||
.expect("read game-chat root binding")
|
||||
.expect("game-chat root binding exists");
|
||||
let mut provider_plan = AgentRuntimeToolPlan::default();
|
||||
|
||||
let error = prepare_game_chat_single_round_convergence_at(
|
||||
&root,
|
||||
&mut runtime,
|
||||
&mut provider_plan,
|
||||
binding.bound_at + GAME_CHAT_FIRST_PLAYABLE_HARD_BUDGET_SECONDS,
|
||||
)
|
||||
.expect_err("hard-budget-expired game-chat run must not converge");
|
||||
|
||||
assert!(error.starts_with(GAME_CHAT_FIRST_PLAYABLE_HARD_BUDGET_ERROR_PREFIX));
|
||||
assert!(!runtime
|
||||
.observations
|
||||
.iter()
|
||||
.any(|observation| observation.contains("单轮完成门已通过")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn autonomous_specialist_empty_plan_uses_internal_completion_fallback() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) fn autonomous_manifest_parent_wake_error_is_transient(error: &str) -> bool {
|
||||
let normalized = error.to_ascii_lowercase();
|
||||
error.starts_with("项目正在被其他写操作占用:")
|
||||
|| error.contains("另一个程序正在使用此文件")
|
||||
|| normalized.contains("sharing violation")
|
||||
|| normalized.contains("lock violation")
|
||||
|| normalized.contains("os error 32")
|
||||
|| normalized.contains("os error 33")
|
||||
|| normalized.contains("resource temporarily unavailable")
|
||||
|| normalized.contains("would block")
|
||||
|| normalized.contains("timed out")
|
||||
|| normalized.contains("timeout")
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn schedule_waiting_provider_retry_wake_after_lane_release(
|
||||
@@ -109,7 +119,7 @@ async fn drive_waiting_autonomous_manifest_parent_wake_pass(
|
||||
agent_id: &str,
|
||||
run_id: &str,
|
||||
) {
|
||||
for _ in 0..40 {
|
||||
for _ in 0..200 {
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
let Ok(Some(task)) =
|
||||
read_latest_game_creator_agent_runtime_task_by_run_id(root, agent_id, run_id)
|
||||
@@ -124,6 +134,9 @@ async fn drive_waiting_autonomous_manifest_parent_wake_pass(
|
||||
Ok(false) => match autonomous_manifest_dag_in_progress_at(root) {
|
||||
Ok(true) => return,
|
||||
Ok(false) => {}
|
||||
Err(error) if autonomous_manifest_parent_wake_error_is_transient(&error) => {
|
||||
continue;
|
||||
}
|
||||
Err(error) => {
|
||||
let _ = mark_autonomous_manifest_parent_wake_needs_reconciliation_at(
|
||||
root, agent_id, run_id, &error,
|
||||
|
||||
@@ -422,6 +422,108 @@ pub(crate) fn resume_game_creator_agent_background_tasks_at(
|
||||
.map_err(|error| redact_agent_runtime_error(root, &error, 500))
|
||||
}
|
||||
|
||||
pub(crate) fn has_recoverable_game_creator_agent_background_tasks_at(
|
||||
root: &Path,
|
||||
) -> Result<bool, String> {
|
||||
validate_project_root(root)?;
|
||||
|
||||
for relative_directory in [
|
||||
".agent/runtime/finalizations",
|
||||
".agent/runtime/tool-plan-handoffs",
|
||||
".agent/runtime/provider-retries",
|
||||
".agent/runtime/pending-actions",
|
||||
".agent/runtime/parallel-read-batches",
|
||||
".agent/runtime/provider-action-batches",
|
||||
".agent/runtime/cancel",
|
||||
] {
|
||||
if durable_agent_runtime_recovery_directory_has_entries(&root.join(relative_directory)) {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
if durable_process_session_recovery_exists_at(root) {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
for agent_id in collect_game_creator_agent_runtime_agent_ids(root)? {
|
||||
match read_recoverable_game_creator_agent_runtime_task(root, &agent_id) {
|
||||
Ok(Some(_)) | Err(_) => return Ok(true),
|
||||
Ok(None) => {}
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn durable_agent_runtime_recovery_directory_has_entries(directory: &Path) -> bool {
|
||||
let mut pending_directories = vec![directory.to_path_buf()];
|
||||
while let Some(directory) = pending_directories.pop() {
|
||||
let entries = match fs::read_dir(&directory) {
|
||||
Ok(entries) => entries,
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
|
||||
Err(_) => return true,
|
||||
};
|
||||
for entry in entries {
|
||||
let entry = match entry {
|
||||
Ok(entry) => entry,
|
||||
Err(_) => return true,
|
||||
};
|
||||
let file_type = match entry.file_type() {
|
||||
Ok(file_type) => file_type,
|
||||
Err(_) => return true,
|
||||
};
|
||||
if file_type.is_symlink() || !file_type.is_dir() {
|
||||
return true;
|
||||
}
|
||||
pending_directories.push(entry.path());
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn durable_process_session_recovery_exists_at(root: &Path) -> bool {
|
||||
let directory = root.join(".agent/runtime/process-sessions");
|
||||
let entries = match fs::read_dir(&directory) {
|
||||
Ok(entries) => entries,
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return false,
|
||||
Err(_) => return true,
|
||||
};
|
||||
for entry in entries {
|
||||
let entry = match entry {
|
||||
Ok(entry) => entry,
|
||||
Err(_) => return true,
|
||||
};
|
||||
let Some(file_name) = entry.file_name().to_str().map(str::to_string) else {
|
||||
return true;
|
||||
};
|
||||
if !file_name.ends_with(".json") || file_name.ends_with(".output.json") {
|
||||
continue;
|
||||
}
|
||||
match entry.file_type() {
|
||||
Ok(file_type) if !file_type.is_symlink() && file_type.is_file() => {}
|
||||
_ => return true,
|
||||
}
|
||||
let relative_path = format!(".agent/runtime/process-sessions/{file_name}");
|
||||
let record = match read_agent_runtime_json_sidecar_with_max_bytes::<ProcessSessionRecord>(
|
||||
root,
|
||||
&relative_path,
|
||||
"Agent Runtime process session preflight",
|
||||
32 * 1024,
|
||||
) {
|
||||
Ok(Some(record)) => record,
|
||||
Ok(None) | Err(_) => return true,
|
||||
};
|
||||
if record.schema_version != "3" || record.needs_reconciliation {
|
||||
return true;
|
||||
}
|
||||
if !matches!(
|
||||
record.status.as_str(),
|
||||
"exited" | "terminated" | "timed-out" | "output-limit-exceeded" | "failed"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn resume_game_creator_agent_background_tasks_unredacted_at(
|
||||
root: &Path,
|
||||
) -> Result<Vec<AgentRuntimeResult>, String> {
|
||||
|
||||
@@ -119,10 +119,7 @@ pub(crate) fn start_game_creator_supervisor_background_task_for_session_at(
|
||||
source: &str,
|
||||
run_profile: &str,
|
||||
) -> Result<AgentRuntimeResult, String> {
|
||||
if !matches!(
|
||||
source,
|
||||
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE | AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE
|
||||
) {
|
||||
if !agent_runtime_supervisor_source_is_trusted(source) {
|
||||
return Err("Project Supervisor 提交 source 不受信任".to_string());
|
||||
}
|
||||
normalize_agent_runtime_run_profile(Some(run_profile))?;
|
||||
@@ -641,10 +638,7 @@ fn current_autonomous_game_build_root_task_at(
|
||||
&& record.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
&& record.parent_agent_id.is_none()
|
||||
&& record.parent_run_id.is_none()
|
||||
&& matches!(
|
||||
record.source.as_str(),
|
||||
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE | AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE
|
||||
)
|
||||
&& agent_runtime_supervisor_source_is_trusted(&record.source)
|
||||
&& seen_run_ids.insert(record.run_id.clone())
|
||||
{
|
||||
root_run_ids.push(record.run_id.clone());
|
||||
@@ -658,8 +652,41 @@ fn current_autonomous_game_build_root_task_at(
|
||||
.find(|record| record.run_id == *current_run_id))
|
||||
}
|
||||
|
||||
fn autonomous_manifest_ready_task_ids(tasks: &[GameCreationAppTaskState]) -> Vec<String> {
|
||||
new_game_creation_app_seed_tasks()
|
||||
pub(in crate::agent) fn autonomous_manifest_seed_tasks_for_source(
|
||||
source: &str,
|
||||
) -> Vec<GameCreationAppTaskState> {
|
||||
let seed_tasks = new_game_creation_app_seed_tasks();
|
||||
if source.trim() == AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE {
|
||||
seed_tasks
|
||||
.into_iter()
|
||||
.filter_map(|mut task| {
|
||||
let dependencies = match task.id.as_str() {
|
||||
"design-director" => Some(Vec::new()),
|
||||
"art-director" => Some(Vec::new()),
|
||||
"code-director" => Some(Vec::new()),
|
||||
"code-prototype" => Some(vec![
|
||||
"design-director".to_string(),
|
||||
"art-director".to_string(),
|
||||
"code-director".to_string(),
|
||||
]),
|
||||
"preview-readiness" => Some(vec!["code-prototype".to_string()]),
|
||||
"preview-playtest" => Some(vec!["preview-readiness".to_string()]),
|
||||
_ => None,
|
||||
}?;
|
||||
task.dependencies = dependencies;
|
||||
Some(task)
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
seed_tasks
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn autonomous_manifest_ready_task_ids(
|
||||
tasks: &[GameCreationAppTaskState],
|
||||
source: &str,
|
||||
) -> Vec<String> {
|
||||
autonomous_manifest_seed_tasks_for_source(source)
|
||||
.into_iter()
|
||||
.filter_map(|seed_task| {
|
||||
let task = tasks.iter().find(|task| task.id == seed_task.id)?;
|
||||
@@ -669,7 +696,7 @@ fn autonomous_manifest_ready_task_ids(tasks: &[GameCreationAppTaskState]) -> Vec
|
||||
| GameCreationAppTaskStatus::WaitingForConfirmation
|
||||
);
|
||||
(status_is_ready
|
||||
&& task.dependencies.iter().all(|dependency| {
|
||||
&& seed_task.dependencies.iter().all(|dependency| {
|
||||
task_has_status(tasks, dependency, GameCreationAppTaskStatus::Completed)
|
||||
}))
|
||||
.then(|| task.id.clone())
|
||||
@@ -893,9 +920,17 @@ pub(crate) fn schedule_autonomous_game_build_ready_tasks_at(
|
||||
.count();
|
||||
let available = 3usize.saturating_sub(active_count);
|
||||
let seed_task_order = new_game_creation_app_seed_tasks();
|
||||
let allowed_seed_task_ids =
|
||||
autonomous_manifest_seed_tasks_for_source(&parent_binding.source)
|
||||
.into_iter()
|
||||
.map(|task| task.id)
|
||||
.collect::<BTreeSet<_>>();
|
||||
let mut candidates = Vec::new();
|
||||
|
||||
for seed_task in &seed_task_order {
|
||||
if !allowed_seed_task_ids.contains(&seed_task.id) {
|
||||
continue;
|
||||
}
|
||||
let Some(task) = manifest.tasks.iter().find(|task| task.id == seed_task.id) else {
|
||||
continue;
|
||||
};
|
||||
@@ -917,7 +952,7 @@ pub(crate) fn schedule_autonomous_game_build_ready_tasks_at(
|
||||
candidates.push((task.clone(), false));
|
||||
}
|
||||
}
|
||||
for task_id in autonomous_manifest_ready_task_ids(&manifest.tasks)
|
||||
for task_id in autonomous_manifest_ready_task_ids(&manifest.tasks, &parent_binding.source)
|
||||
.into_iter()
|
||||
.take(limit.min(available))
|
||||
{
|
||||
@@ -1133,8 +1168,12 @@ pub(in crate::agent) fn project_autonomous_manifest_ready_task_terminal_at_locke
|
||||
manifest_task,
|
||||
&task_text,
|
||||
)?;
|
||||
let game_chat_requires_visual_asset = root_parent_binding.source
|
||||
== AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE
|
||||
&& manifest_task.id == "art-director";
|
||||
if status == GameCreationAppTaskStatus::Completed
|
||||
&& autonomous_manifest_ready_task_requires_visual_asset(&manifest_task.id)
|
||||
&& (autonomous_manifest_ready_task_requires_visual_asset(&manifest_task.id)
|
||||
|| game_chat_requires_visual_asset)
|
||||
&& !manifest_has_required_visual_asset(root, &manifest, &manifest_task.id)
|
||||
{
|
||||
status = GameCreationAppTaskStatus::Failed;
|
||||
@@ -1166,6 +1205,50 @@ pub(in crate::agent) fn project_autonomous_manifest_ready_task_terminal_at_locke
|
||||
));
|
||||
}
|
||||
}
|
||||
if status == GameCreationAppTaskStatus::Completed
|
||||
&& state.agent_id == "code-prototype"
|
||||
&& root_parent_binding.source == AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE
|
||||
{
|
||||
let revision = read_game_creator_agent_runtime_project_revision(root)?;
|
||||
let child_gate = read_game_creator_agent_runtime_verification_gate(
|
||||
root,
|
||||
&state.agent_id,
|
||||
&state.run_id,
|
||||
)?;
|
||||
if child_gate.verified_revision != Some(revision.revision)
|
||||
|| child_gate.last_verification_tool.as_deref() != Some("game.static_smoke")
|
||||
|| child_gate.last_verification_status.as_deref()
|
||||
!= Some(AGENT_RUNTIME_VERIFICATION_STATUS_PASSED)
|
||||
{
|
||||
return Err("game-chat code-prototype 缺少当前 revision 的静态验证凭证".to_string());
|
||||
}
|
||||
let mut root_gate = read_game_creator_agent_runtime_verification_gate(
|
||||
root,
|
||||
&parent_agent_id,
|
||||
&parent_run_id,
|
||||
)?;
|
||||
root_gate.requires_verification = false;
|
||||
root_gate.mutation_revision = None;
|
||||
root_gate.verified_revision = Some(revision.revision);
|
||||
root_gate.last_mutation_tool = None;
|
||||
root_gate.last_verification_tool = Some("game.static_smoke".to_string());
|
||||
root_gate.last_verification_status =
|
||||
Some(AGENT_RUNTIME_VERIFICATION_STATUS_PASSED.to_string());
|
||||
root_gate.failed_playtest_revision = None;
|
||||
root_gate.updated_at = unix_timestamp();
|
||||
write_game_creator_agent_runtime_verification_gate(root, &root_gate)?;
|
||||
append_agent_db_record(
|
||||
root,
|
||||
serde_json::json!({
|
||||
"recordType": "agent.runtime.game_chat.static_verification_projected",
|
||||
"agentId": state.agent_id,
|
||||
"runId": state.run_id,
|
||||
"parentAgentId": parent_agent_id,
|
||||
"parentRunId": parent_run_id,
|
||||
"revision": revision.revision,
|
||||
}),
|
||||
)?;
|
||||
}
|
||||
update_manifest_task_status_at(root, &state.agent_id, status.clone())?;
|
||||
append_agent_db_record(
|
||||
root,
|
||||
@@ -1183,9 +1266,11 @@ pub(in crate::agent) fn project_autonomous_manifest_ready_task_terminal_at_locke
|
||||
}),
|
||||
)?;
|
||||
let completed = status == GameCreationAppTaskStatus::Completed;
|
||||
let game_chat_single_round =
|
||||
root_parent_binding.source == AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE;
|
||||
let root = root.to_path_buf();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
if completed {
|
||||
if completed && !game_chat_single_round {
|
||||
if let Err(error) = schedule_autonomous_game_build_ready_tasks_at(
|
||||
&root,
|
||||
&parent_agent_id,
|
||||
@@ -1242,9 +1327,17 @@ pub(in crate::agent) fn render_autonomous_manifest_ready_task_background_prompt(
|
||||
} else {
|
||||
""
|
||||
};
|
||||
return format!(
|
||||
let code_visual_asset_requirement = if task.id == "code-prototype"
|
||||
&& editor_api_key_is_configured()
|
||||
{
|
||||
" External Editor API 已配置时必须先调用 asset.list 核对 Canvas 登记与资源有效性:game-chat Run 使用 assets/art-spec.png(icon-spec、images/generations/spec),GUI/CLI Run 使用 assets/art-spritesheet.png;game/index.html 必须通过 HTML、CSS background 或 Canvas drawImage 可见使用对应资源,不得只用 emoji、色块、CSS 绘图或占位文本冒充。"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let owner_prompt = format!(
|
||||
"{base}\n\n这是 autonomous-game-build 的正式 owner 写入任务。必须实际生成并写入非空正式产物:{paths};JSON 文件必须是可解析 JSON,code-prototype 的 game/index.html 不能沿用初始化占位。{publish_package_requirement}任务声明中的视觉图片继续按现有 visual gate 生成、登记并验收。完成修改后按当前 run 的验证门完成验证并直接交付结论;不要调用 task.update,Runtime 会在子 Run 终态后幂等投影 manifest。"
|
||||
);
|
||||
return format!("{owner_prompt}{code_visual_asset_requirement}");
|
||||
}
|
||||
format!(
|
||||
"{base}\n\n这是 autonomous-game-build 的只读协调任务,不要修改项目文件,也不要为了 manifest 内部回执路径写入 memory/、game/、assets/ 或 exports/。只读取当前项目事实,完成方向协调、审查或验收并直接交付结论;不要调用 task.update,Runtime 会在子 Run 终态后幂等投影 manifest。"
|
||||
|
||||
+1062
-10
File diff suppressed because it is too large
Load Diff
+714
-10
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user