修复AGC无人值守生成阻断的交付收口与验收
- 可信 code-prototype 父 Run 认领并观察直属美术 delivery,普通失败立即收束,合法安全默认 marker 由 Runtime 确定性执行唯一同合同返工 - 补齐 suppressed 无 child 与父身份链丢失时的 completion 失败关闭边界 - 修复 Windows project.lock delete-pending 竞争并收紧 HTML 内联 JS 语法 fail-open - 同步技术方案、决策记录与实施计划,并完成确定性及真实 Provider 分层验证
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -2406,11 +2406,36 @@ export function isolatedJoinDeliveryTarget(delivery) {
|
||||
return target;
|
||||
}
|
||||
|
||||
export function finalMessageId(agentId, sessionId, runId) {
|
||||
const fingerprint = createHash('sha256')
|
||||
function runtimeMessageCorrelationId(agentId, sessionId, runId) {
|
||||
return createHash('sha256')
|
||||
.update(`${agentId}\n${sessionId}\n${runId}`)
|
||||
.digest('hex');
|
||||
return `agent-finalization-${fingerprint.slice(0, 32)}`;
|
||||
}
|
||||
|
||||
export function finalMessageId(agentId, sessionId, runId) {
|
||||
return `agent-finalization-${runtimeMessageCorrelationId(
|
||||
agentId,
|
||||
sessionId,
|
||||
runId,
|
||||
).slice(0, 32)}`;
|
||||
}
|
||||
|
||||
export function runtimePublicStatusMessageId(
|
||||
agentId,
|
||||
sessionId,
|
||||
runId,
|
||||
status,
|
||||
) {
|
||||
const correlationId = runtimeMessageCorrelationId(
|
||||
agentId,
|
||||
sessionId,
|
||||
runId,
|
||||
).slice(0, 32);
|
||||
const statusFingerprint = createHash('sha256')
|
||||
.update(status)
|
||||
.digest('hex')
|
||||
.slice(0, 16);
|
||||
return `runtime-public-status-${correlationId}-${statusFingerprint}`;
|
||||
}
|
||||
|
||||
export function backgroundTaskMessageId(agentId, sessionId, runId, source) {
|
||||
@@ -2444,7 +2469,10 @@ export function disposableProjectPathVariants() {
|
||||
}
|
||||
|
||||
export function formalConfigPathVariants() {
|
||||
return absolutePathVariants(state.options?.configDir);
|
||||
return absolutePathVariants(
|
||||
state.options?.configDir,
|
||||
state.isolatedRunner?.appDataDir,
|
||||
);
|
||||
}
|
||||
|
||||
export function absolutePathVariants(...values) {
|
||||
|
||||
@@ -20,7 +20,15 @@ import {
|
||||
stopOwnedIsolatedRunner,
|
||||
} from './harness/app-data.mjs';
|
||||
import { loadConfig, parseArguments } from './harness/config.mjs';
|
||||
import { closeInteractiveCli } from './harness/process.mjs';
|
||||
import {
|
||||
activeInteractiveCliSessions,
|
||||
captureOwnedProcessCleanupSnapshot,
|
||||
closeInteractiveCli,
|
||||
destroyInteractiveCliOutputStreams,
|
||||
interactiveCliOutput,
|
||||
verifyOwnedProcessCleanupSnapshot,
|
||||
waitForInteractiveCliStdioClose,
|
||||
} from './harness/process.mjs';
|
||||
import { checkPrerequisites } from './harness/project.mjs';
|
||||
import {
|
||||
buildSummary,
|
||||
@@ -266,6 +274,21 @@ if (selfTestRequested) {
|
||||
recordError(error?.code ?? 'unexpected-error', error);
|
||||
} finally {
|
||||
state.cleanupInProgress = true;
|
||||
const stateTrackedInteractiveCliSessions = new Set(
|
||||
[
|
||||
state.userInputCliSession,
|
||||
state.supervisorAutonomousPlayableCliSession,
|
||||
state.supervisorSwarmCliSession,
|
||||
].filter(Boolean),
|
||||
);
|
||||
const interactiveCliSessions = [
|
||||
...new Set([
|
||||
...stateTrackedInteractiveCliSessions,
|
||||
...activeInteractiveCliSessions,
|
||||
]),
|
||||
];
|
||||
const supervisorAutonomousPlayableCliSession =
|
||||
state.supervisorAutonomousPlayableCliSession;
|
||||
if (isUserInputRuntimeSuite() && state.userInputCliSession) {
|
||||
try {
|
||||
await closeInteractiveCli(state.userInputCliSession);
|
||||
@@ -311,6 +334,15 @@ if (selfTestRequested) {
|
||||
}
|
||||
state.supervisorSwarmCliSession = null;
|
||||
}
|
||||
for (const session of interactiveCliSessions) {
|
||||
if (stateTrackedInteractiveCliSessions.has(session)) continue;
|
||||
try {
|
||||
await closeInteractiveCli(session);
|
||||
} catch (error) {
|
||||
state.status = 'FAIL';
|
||||
recordError('interactive-cli-cleanup-failed', error);
|
||||
}
|
||||
}
|
||||
if (isMcpRuntimeSuite() && state.mcp.httpFixture) {
|
||||
try {
|
||||
await stopMcpHttpFixture();
|
||||
@@ -320,16 +352,42 @@ if (selfTestRequested) {
|
||||
recordError('mcp-http-fixture-cleanup-failed', error);
|
||||
}
|
||||
}
|
||||
if (
|
||||
isSupervisorAutonomousPlayableLaneDefenseSuite() &&
|
||||
state.isolatedRunner.appDataDir
|
||||
) {
|
||||
try {
|
||||
const runnerPid = state.isolatedRunner.current?.pid ?? null;
|
||||
const helperPid =
|
||||
state.isolatedRunner.current?.killHandle?.child?.pid ?? null;
|
||||
state.supervisorAutonomousPlayable.ownedProcessCleanupSnapshot =
|
||||
await captureOwnedProcessCleanupSnapshot({
|
||||
runnerPid,
|
||||
helperPids: Number.isSafeInteger(helperPid) ? [helperPid] : [],
|
||||
rootPids: [
|
||||
...interactiveCliSessions.map((session) => session.child?.pid),
|
||||
...[...activeCommandChildren].map((child) => child.pid),
|
||||
].filter((pid) => Number.isSafeInteger(pid) && pid > 0),
|
||||
});
|
||||
const observed =
|
||||
state.supervisorAutonomousPlayable.ownedProcessCleanupSnapshot
|
||||
.observedCounts;
|
||||
assert(
|
||||
observed.runner === 1 && observed.helper === 1,
|
||||
'supervisor-autonomous-playable-owned-process-snapshot-incomplete',
|
||||
);
|
||||
} catch (error) {
|
||||
state.status = 'FAIL';
|
||||
recordError(
|
||||
'supervisor-autonomous-playable-owned-process-snapshot-failed',
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (isIsolatedRunnerSuite() && state.isolatedRunner.appDataDir) {
|
||||
try {
|
||||
await stopOwnedIsolatedRunner();
|
||||
state.isolatedRunner.stopped = true;
|
||||
state.isolatedRunner.cleanupPerformed =
|
||||
await removeIsolatedSuiteAppData();
|
||||
if (!state.isolatedRunner.cleanupPerformed) {
|
||||
state.status = 'FAIL';
|
||||
recordError('isolated-appdata-cleanup-sentinel-missing');
|
||||
}
|
||||
} catch (error) {
|
||||
state.status = 'FAIL';
|
||||
const safeCleanupErrorCode =
|
||||
@@ -342,8 +400,51 @@ if (selfTestRequested) {
|
||||
state.isolatedRunner.current?.killHandle,
|
||||
).catch(() => {});
|
||||
}
|
||||
}
|
||||
for (const session of interactiveCliSessions) {
|
||||
try {
|
||||
await waitForInteractiveCliStdioClose(session, 10_000);
|
||||
} catch (error) {
|
||||
destroyInteractiveCliOutputStreams(session);
|
||||
state.status = 'FAIL';
|
||||
recordError(
|
||||
error?.code === 'interactive-cli-stdio-close-timeout'
|
||||
? error.code
|
||||
: 'interactive-cli-stdio-cleanup-failed',
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (supervisorAutonomousPlayableCliSession) {
|
||||
state.supervisorAutonomousPlayable.cliOutput = interactiveCliOutput(
|
||||
supervisorAutonomousPlayableCliSession,
|
||||
);
|
||||
}
|
||||
if (isIsolatedRunnerSuite() && state.isolatedRunner.appDataDir) {
|
||||
if (state.isolatedRunner.stopped) {
|
||||
try {
|
||||
state.isolatedRunner.cleanupPerformed =
|
||||
await removeIsolatedSuiteAppData();
|
||||
if (!state.isolatedRunner.cleanupPerformed) {
|
||||
state.status = 'FAIL';
|
||||
recordError('isolated-appdata-cleanup-sentinel-missing');
|
||||
}
|
||||
} catch (error) {
|
||||
state.status = 'FAIL';
|
||||
const safeCleanupErrorCode =
|
||||
isNonEmptyString(error?.code) &&
|
||||
/^(?:isolated|source)-[a-z0-9-]+$/u.test(error.code)
|
||||
? error.code
|
||||
: 'isolated-appdata-cleanup-failed';
|
||||
recordError(safeCleanupErrorCode, error);
|
||||
}
|
||||
}
|
||||
const killMethod =
|
||||
state.isolatedRunner.pidfdClaimCount > 0 ? 'linux-pidfd' : null;
|
||||
state.isolatedRunner.pidfdClaimCount > 0
|
||||
? process.platform === 'win32'
|
||||
? 'windows-process-handle'
|
||||
: 'linux-pidfd'
|
||||
: null;
|
||||
if (isSteerRunnerKillSuite()) {
|
||||
state.evidence.steerRunnerStopped = state.isolatedRunner.stopped;
|
||||
state.evidence.steerAppDataCleanupPerformed =
|
||||
@@ -661,6 +762,61 @@ if (selfTestRequested) {
|
||||
);
|
||||
}
|
||||
}
|
||||
if (
|
||||
isSupervisorAutonomousPlayableLaneDefenseSuite() &&
|
||||
state.isolatedRunner.appDataDir
|
||||
) {
|
||||
const snapshot =
|
||||
state.supervisorAutonomousPlayable.ownedProcessCleanupSnapshot;
|
||||
if (snapshot) {
|
||||
try {
|
||||
const cleanup = await verifyOwnedProcessCleanupSnapshot(snapshot);
|
||||
state.evidence.ownedProcessIdentityCaptured = true;
|
||||
state.evidence.ownedRunnerObservedCount =
|
||||
snapshot.observedCounts.runner;
|
||||
state.evidence.ownedHelperObservedCount =
|
||||
snapshot.observedCounts.helper;
|
||||
state.evidence.ownedNodeDescendantObservedCount =
|
||||
snapshot.observedCounts.node;
|
||||
state.evidence.ownedBrowserDescendantObservedCount =
|
||||
snapshot.observedCounts.browser;
|
||||
state.evidence.ownedCommandDescendantObservedCount =
|
||||
snapshot.observedCounts.command;
|
||||
state.evidence.ownedRunnerResidualCount =
|
||||
cleanup.residualCounts.runner;
|
||||
state.evidence.ownedHelperResidualCount =
|
||||
cleanup.residualCounts.helper;
|
||||
state.evidence.ownedNodeDescendantResidualCount =
|
||||
cleanup.residualCounts.node;
|
||||
state.evidence.ownedBrowserDescendantResidualCount =
|
||||
cleanup.residualCounts.browser;
|
||||
state.evidence.ownedCommandDescendantResidualCount =
|
||||
cleanup.residualCounts.command;
|
||||
state.evidence.activeCommandChildrenAfterCleanup =
|
||||
cleanup.activeCommandChildCount;
|
||||
state.evidence.activeInteractiveCliSessionsAfterCleanup =
|
||||
cleanup.activeInteractiveCliSessionCount;
|
||||
state.evidence.ownedProcessCleanupPassed = cleanup.clean;
|
||||
if (!cleanup.clean) {
|
||||
state.status = 'FAIL';
|
||||
recordError(
|
||||
'supervisor-autonomous-playable-owned-process-residual-detected',
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
state.status = 'FAIL';
|
||||
recordError(
|
||||
'supervisor-autonomous-playable-owned-process-verification-failed',
|
||||
error,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
state.status = 'FAIL';
|
||||
recordError(
|
||||
'supervisor-autonomous-playable-owned-process-snapshot-missing',
|
||||
);
|
||||
}
|
||||
}
|
||||
if (
|
||||
isSteerRunnerKillSuite() &&
|
||||
state.projectRoot &&
|
||||
@@ -1210,6 +1366,7 @@ if (selfTestRequested) {
|
||||
const safeSummary = {
|
||||
status: state.status,
|
||||
suite: state.suite,
|
||||
providerUsed: false,
|
||||
blocked: state.blocked,
|
||||
cleanup: {
|
||||
performed: state.cleanupPerformed,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,6 +19,8 @@ import {
|
||||
} from '../suites/supervisor-swarm.mjs';
|
||||
import { isIsolatedRunnerSuite } from './reporting.mjs';
|
||||
|
||||
export const activeInteractiveCliSessions = new Set();
|
||||
|
||||
export async function prepareCliBinary() {
|
||||
const cargo = process.platform === 'win32' ? 'cargo.exe' : 'cargo';
|
||||
await runProcess(
|
||||
@@ -154,36 +156,70 @@ export function startInteractiveCli(args) {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
},
|
||||
);
|
||||
return createInteractiveCliSession(child);
|
||||
}
|
||||
|
||||
export function createInteractiveCliSession(child) {
|
||||
activeCommandChildren.add(child);
|
||||
const session = {
|
||||
child,
|
||||
stdout: Buffer.alloc(0),
|
||||
stderr: Buffer.alloc(0),
|
||||
exited: false,
|
||||
exitInfo: null,
|
||||
exitPromise: null,
|
||||
closed: false,
|
||||
closeInfo: null,
|
||||
closePromise: null,
|
||||
stdioClosed: false,
|
||||
stdioCloseInfo: null,
|
||||
spawnError: null,
|
||||
stdinError: null,
|
||||
};
|
||||
activeInteractiveCliSessions.add(session);
|
||||
session.exitPromise = new Promise((resolve) => {
|
||||
const settle = (result) => {
|
||||
if (session.exited) return;
|
||||
activeCommandChildren.delete(child);
|
||||
session.exited = true;
|
||||
session.closed = true;
|
||||
session.exitInfo = result;
|
||||
session.closeInfo = result;
|
||||
resolve(result);
|
||||
};
|
||||
child.once('error', (error) => {
|
||||
session.spawnError = error;
|
||||
settle({ code: null, signal: null, error });
|
||||
});
|
||||
child.once('exit', (code, signal) => {
|
||||
settle({ code, signal, error: null });
|
||||
});
|
||||
});
|
||||
session.closePromise = new Promise((resolve) => {
|
||||
child.on('error', (error) => {
|
||||
child.once('close', (code, signal) => {
|
||||
activeCommandChildren.delete(child);
|
||||
session.closed = true;
|
||||
session.closeInfo = { code: null, signal: null, error };
|
||||
resolve(session.closeInfo);
|
||||
});
|
||||
child.on('close', (code, signal) => {
|
||||
activeCommandChildren.delete(child);
|
||||
session.closed = true;
|
||||
session.closeInfo = { code, signal, error: null };
|
||||
resolve(session.closeInfo);
|
||||
activeInteractiveCliSessions.delete(session);
|
||||
session.stdioClosed = true;
|
||||
session.stdioCloseInfo = {
|
||||
code,
|
||||
signal,
|
||||
error: session.spawnError,
|
||||
};
|
||||
resolve(session.stdioCloseInfo);
|
||||
});
|
||||
});
|
||||
child.stdin?.on('error', (error) => {
|
||||
session.stdinError ??= error;
|
||||
});
|
||||
child.stdout.on('data', (chunk) => {
|
||||
state.transcriptScanner?.scan('interactive-stdout', chunk);
|
||||
state.projectPathTranscriptScanner?.scan('interactive-stdout', chunk);
|
||||
state.formalConfigPathTranscriptScanner?.scan('interactive-stdout', chunk);
|
||||
session.stdout = appendBounded(session.stdout, chunk, commandOutputLimit);
|
||||
});
|
||||
child.stderr.on('data', (chunk) => {
|
||||
state.transcriptScanner?.scan('interactive-stderr', chunk);
|
||||
state.projectPathTranscriptScanner?.scan('interactive-stderr', chunk);
|
||||
state.formalConfigPathTranscriptScanner?.scan('interactive-stderr', chunk);
|
||||
session.stderr = appendBounded(session.stderr, chunk, commandOutputLimit);
|
||||
});
|
||||
@@ -205,16 +241,23 @@ export async function waitForInteractiveCliOutput(
|
||||
predicate,
|
||||
code,
|
||||
timeoutMs,
|
||||
{ allowAfterProcessExit = false } = {},
|
||||
) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
const output = interactiveCliOutput(session);
|
||||
if (predicate(output)) return output;
|
||||
if (session.closed) {
|
||||
if (session.exited && !allowAfterProcessExit) {
|
||||
if (session === state.supervisorSwarmCliSession) {
|
||||
recordSupervisorSwarmChatSessionFailureDiagnostic(session);
|
||||
}
|
||||
throw codedError(`${code}-cli-closed`);
|
||||
throw codedError(`${code}-cli-exited`);
|
||||
}
|
||||
if (session.stdioClosed) {
|
||||
if (session === state.supervisorSwarmCliSession) {
|
||||
recordSupervisorSwarmChatSessionFailureDiagnostic(session);
|
||||
}
|
||||
throw codedError(`${code}-cli-stdio-closed`);
|
||||
}
|
||||
await sleep(50);
|
||||
}
|
||||
@@ -223,7 +266,7 @@ export async function waitForInteractiveCliOutput(
|
||||
|
||||
export async function waitForInteractiveCliExit(session, timeoutMs) {
|
||||
const result = await Promise.race([
|
||||
session.closePromise,
|
||||
session.exitPromise,
|
||||
sleep(timeoutMs).then(() => null),
|
||||
]);
|
||||
if (!result) throw codedError('interactive-cli-exit-timeout');
|
||||
@@ -236,26 +279,52 @@ export async function waitForInteractiveCliExit(session, timeoutMs) {
|
||||
}
|
||||
|
||||
export async function closeInteractiveCli(session) {
|
||||
if (!session || session.closed) return;
|
||||
if (session.child.stdin.writable) {
|
||||
if (!session) return null;
|
||||
if (session.exited) return session.exitInfo;
|
||||
if (
|
||||
session.child.stdin.writable &&
|
||||
!session.child.stdin.writableEnded &&
|
||||
!session.child.stdin.destroyed
|
||||
) {
|
||||
session.child.stdin.write('/quit\n');
|
||||
}
|
||||
let result = await Promise.race([
|
||||
session.closePromise,
|
||||
session.exitPromise,
|
||||
sleep(3_000).then(() => null),
|
||||
]);
|
||||
if (!result && !session.closed) {
|
||||
if (!result && !session.exited) {
|
||||
session.child.kill('SIGTERM');
|
||||
result = await Promise.race([
|
||||
session.closePromise,
|
||||
session.exitPromise,
|
||||
sleep(2_000).then(() => null),
|
||||
]);
|
||||
}
|
||||
if (!result && !session.closed) {
|
||||
if (!result && !session.exited) {
|
||||
session.child.kill('SIGKILL');
|
||||
result = await session.closePromise;
|
||||
result = await Promise.race([
|
||||
session.exitPromise,
|
||||
sleep(5_000).then(() => null),
|
||||
]);
|
||||
}
|
||||
assert(Boolean(result), 'interactive-cli-cleanup-timeout');
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function waitForInteractiveCliStdioClose(session, timeoutMs) {
|
||||
if (!session || session.stdioClosed) return session?.stdioCloseInfo ?? null;
|
||||
const result = await Promise.race([
|
||||
session.closePromise,
|
||||
sleep(timeoutMs).then(() => null),
|
||||
]);
|
||||
if (!result) throw codedError('interactive-cli-stdio-close-timeout');
|
||||
return result;
|
||||
}
|
||||
|
||||
export function destroyInteractiveCliOutputStreams(session) {
|
||||
if (!session) return;
|
||||
for (const stream of [session.child.stdout, session.child.stderr]) {
|
||||
if (stream && !stream.destroyed) stream.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
export async function runProcess(
|
||||
@@ -329,6 +398,214 @@ export async function runProcess(
|
||||
});
|
||||
}
|
||||
|
||||
export async function listSystemProcessIdentities() {
|
||||
if (process.platform === 'win32') {
|
||||
const systemRoot = process.env.SystemRoot ?? process.env.SYSTEMROOT;
|
||||
assert(
|
||||
typeof systemRoot === 'string' && path.isAbsolute(systemRoot),
|
||||
'owned-process-snapshot-system-root-invalid',
|
||||
);
|
||||
const powershell = path.join(
|
||||
systemRoot,
|
||||
'System32/WindowsPowerShell/v1.0/powershell.exe',
|
||||
);
|
||||
const metadata = await fs.lstat(powershell);
|
||||
assert(
|
||||
metadata.isFile() && !metadata.isSymbolicLink(),
|
||||
'owned-process-snapshot-powershell-invalid',
|
||||
);
|
||||
const result = await runProcess(
|
||||
powershell,
|
||||
[
|
||||
'-NoProfile',
|
||||
'-NonInteractive',
|
||||
'-Command',
|
||||
'$processes = @(Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,CreationDate,Name); $processes | ConvertTo-Json -Compress',
|
||||
],
|
||||
{
|
||||
cwd: appRoot,
|
||||
timeoutMs: 30_000,
|
||||
env: { ...process.env, NO_COLOR: '1', RUST_BACKTRACE: '0' },
|
||||
},
|
||||
);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
return (Array.isArray(parsed) ? parsed : [parsed])
|
||||
.map((record) => ({
|
||||
pid: Number(record?.ProcessId),
|
||||
parentPid: Number(record?.ParentProcessId),
|
||||
startedAt: String(record?.CreationDate ?? ''),
|
||||
name: String(record?.Name ?? ''),
|
||||
}))
|
||||
.filter(validSystemProcessIdentity);
|
||||
}
|
||||
assert(
|
||||
process.platform === 'linux' || process.platform === 'darwin',
|
||||
'owned-process-snapshot-platform-unsupported',
|
||||
);
|
||||
const result = await runProcess(
|
||||
'ps',
|
||||
['-A', '-o', 'pid=', '-o', 'ppid=', '-o', 'lstart=', '-o', 'comm='],
|
||||
{ cwd: appRoot, timeoutMs: 30_000 },
|
||||
);
|
||||
return result.stdout
|
||||
.split(/\r?\n/u)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
const fields = line.split(/\s+/u);
|
||||
return {
|
||||
pid: Number(fields[0]),
|
||||
parentPid: Number(fields[1]),
|
||||
startedAt: fields.slice(2, 7).join(' '),
|
||||
name: fields.slice(7).join(' '),
|
||||
};
|
||||
})
|
||||
.filter(validSystemProcessIdentity);
|
||||
}
|
||||
|
||||
function validSystemProcessIdentity(record) {
|
||||
return (
|
||||
Number.isSafeInteger(record?.pid) &&
|
||||
record.pid > 0 &&
|
||||
Number.isSafeInteger(record.parentPid) &&
|
||||
record.parentPid >= 0 &&
|
||||
typeof record.startedAt === 'string' &&
|
||||
record.startedAt.length > 0 &&
|
||||
typeof record.name === 'string' &&
|
||||
record.name.length > 0
|
||||
);
|
||||
}
|
||||
|
||||
export function buildOwnedProcessCleanupSnapshot(
|
||||
processRecords,
|
||||
{ rootPids = [], runnerPid = null, helperPids = [] } = {},
|
||||
) {
|
||||
assert(
|
||||
Array.isArray(processRecords) &&
|
||||
Array.isArray(rootPids) &&
|
||||
Array.isArray(helperPids),
|
||||
'owned-process-snapshot-input-invalid',
|
||||
);
|
||||
const records = processRecords.filter(validSystemProcessIdentity);
|
||||
const byPid = new Map(records.map((record) => [record.pid, record]));
|
||||
const childrenByParent = new Map();
|
||||
for (const record of records) {
|
||||
const children = childrenByParent.get(record.parentPid) ?? [];
|
||||
children.push(record.pid);
|
||||
childrenByParent.set(record.parentPid, children);
|
||||
}
|
||||
const normalizedRunnerPid = Number.isSafeInteger(runnerPid)
|
||||
? runnerPid
|
||||
: null;
|
||||
const helperPidSet = new Set(
|
||||
helperPids.filter((pid) => Number.isSafeInteger(pid) && pid > 0),
|
||||
);
|
||||
const roots = [
|
||||
...new Set(
|
||||
[...rootPids, normalizedRunnerPid, ...helperPidSet].filter(
|
||||
(pid) => Number.isSafeInteger(pid) && pid > 0,
|
||||
),
|
||||
),
|
||||
];
|
||||
assert(roots.length > 0, 'owned-process-snapshot-root-missing');
|
||||
const ownedPids = new Set();
|
||||
const queue = [...roots];
|
||||
while (queue.length > 0) {
|
||||
const pid = queue.shift();
|
||||
if (ownedPids.has(pid)) continue;
|
||||
ownedPids.add(pid);
|
||||
queue.push(...(childrenByParent.get(pid) ?? []));
|
||||
}
|
||||
const identities = [...ownedPids]
|
||||
.map((pid) => byPid.get(pid))
|
||||
.filter(Boolean)
|
||||
.map((record) => ({
|
||||
pid: record.pid,
|
||||
startedAt: record.startedAt,
|
||||
name: record.name,
|
||||
kind: ownedProcessKind(record, normalizedRunnerPid, helperPidSet),
|
||||
}))
|
||||
.sort((left, right) => left.pid - right.pid);
|
||||
return {
|
||||
identities,
|
||||
observedCounts: countOwnedProcessKinds(identities),
|
||||
};
|
||||
}
|
||||
|
||||
function ownedProcessKind(record, runnerPid, helperPids) {
|
||||
if (record.pid === runnerPid) return 'runner';
|
||||
if (helperPids.has(record.pid)) return 'helper';
|
||||
const name = path.basename(record.name).toLowerCase();
|
||||
if (/^node(?:\.exe)?$/u.test(name)) return 'node';
|
||||
if (/^(?:chrome|chromium|msedge|google-chrome)(?:\.exe)?$/u.test(name)) {
|
||||
return 'browser';
|
||||
}
|
||||
return 'command';
|
||||
}
|
||||
|
||||
function countOwnedProcessKinds(identities) {
|
||||
const counts = {
|
||||
runner: 0,
|
||||
helper: 0,
|
||||
node: 0,
|
||||
browser: 0,
|
||||
command: 0,
|
||||
total: identities.length,
|
||||
};
|
||||
for (const identity of identities) counts[identity.kind] += 1;
|
||||
return counts;
|
||||
}
|
||||
|
||||
export function inspectOwnedProcessCleanupResiduals(
|
||||
snapshot,
|
||||
processRecords,
|
||||
{ activeCommandChildCount = 0, activeInteractiveCliSessionCount = 0 } = {},
|
||||
) {
|
||||
assert(
|
||||
Array.isArray(snapshot?.identities) && Array.isArray(processRecords),
|
||||
'owned-process-residual-input-invalid',
|
||||
);
|
||||
const currentByPid = new Map(
|
||||
processRecords
|
||||
.filter(validSystemProcessIdentity)
|
||||
.map((record) => [record.pid, record]),
|
||||
);
|
||||
const residualIdentities = snapshot.identities.filter((identity) => {
|
||||
const current = currentByPid.get(identity.pid);
|
||||
return (
|
||||
current?.startedAt === identity.startedAt &&
|
||||
current?.name === identity.name
|
||||
);
|
||||
});
|
||||
return {
|
||||
residualCounts: countOwnedProcessKinds(residualIdentities),
|
||||
activeCommandChildCount,
|
||||
activeInteractiveCliSessionCount,
|
||||
clean:
|
||||
residualIdentities.length === 0 &&
|
||||
activeCommandChildCount === 0 &&
|
||||
activeInteractiveCliSessionCount === 0,
|
||||
};
|
||||
}
|
||||
|
||||
export async function captureOwnedProcessCleanupSnapshot(options) {
|
||||
return buildOwnedProcessCleanupSnapshot(
|
||||
await listSystemProcessIdentities(),
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
export async function verifyOwnedProcessCleanupSnapshot(snapshot) {
|
||||
return inspectOwnedProcessCleanupResiduals(
|
||||
snapshot,
|
||||
await listSystemProcessIdentities(),
|
||||
{
|
||||
activeCommandChildCount: activeCommandChildren.size,
|
||||
activeInteractiveCliSessionCount: activeInteractiveCliSessions.size,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function appendBounded(current, chunk, limit) {
|
||||
const combined = Buffer.concat([current, chunk]);
|
||||
return combined.length <= limit
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { assert } from '../assertions/core.mjs';
|
||||
import { assert, hashValue } from '../assertions/core.mjs';
|
||||
import { disposableProjectPathVariants } from '../assertions/runtime.mjs';
|
||||
import { fs, os, path, randomUUID } from '../dependencies.mjs';
|
||||
import {
|
||||
@@ -47,7 +47,12 @@ export function requiredAgentIdsForSuite() {
|
||||
return isUserInputRuntimeSuite()
|
||||
? [projectSupervisorAgentId]
|
||||
: isSupervisorGameChatSingleMainPlayableSuite()
|
||||
? [projectSupervisorAgentId, mainAgentId]
|
||||
? [
|
||||
projectSupervisorAgentId,
|
||||
mainAgentId,
|
||||
'art-director',
|
||||
'art-asset-plan',
|
||||
]
|
||||
: isSupervisorAutonomousPlayableLaneDefenseSuite()
|
||||
? [projectSupervisorAgentId]
|
||||
: isSupervisorSwarmSuite()
|
||||
@@ -61,15 +66,74 @@ export function requiredAgentIdsForSuite() {
|
||||
: [mainAgentId, 'quality-review'];
|
||||
}
|
||||
|
||||
export function expectedProviderBindingForSuite(config) {
|
||||
if (
|
||||
isSupervisorGameChatSingleMainPlayableSuite() &&
|
||||
config.agentMode !== 'provider'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const agentIds = requiredAgentIdsForSuite();
|
||||
const effectiveConfigs = agentIds.map((agentId) =>
|
||||
effectiveAgentLlmConfig(config, agentId),
|
||||
);
|
||||
if (
|
||||
effectiveConfigs.length === 0 ||
|
||||
effectiveConfigs.some((effective) =>
|
||||
['apiKey', 'baseUrl', 'model', 'apiKind', 'reasoningEffort'].some(
|
||||
(key) =>
|
||||
typeof effective[key] !== 'string' ||
|
||||
effective[key].trim().length === 0,
|
||||
),
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const [expected] = effectiveConfigs;
|
||||
const expectedIdentity = [
|
||||
expected.model.trim(),
|
||||
expected.apiKind.trim(),
|
||||
expected.reasoningEffort.trim(),
|
||||
expected.baseUrl.trim(),
|
||||
];
|
||||
if (
|
||||
effectiveConfigs.some(
|
||||
(effective) =>
|
||||
JSON.stringify([
|
||||
effective.model.trim(),
|
||||
effective.apiKind.trim(),
|
||||
effective.reasoningEffort.trim(),
|
||||
effective.baseUrl.trim(),
|
||||
]) !== JSON.stringify(expectedIdentity),
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...(isSupervisorGameChatSingleMainPlayableSuite()
|
||||
? { providerAgentMode: 'provider' }
|
||||
: {}),
|
||||
providerModel: expectedIdentity[0],
|
||||
providerApiKind: expectedIdentity[1],
|
||||
providerReasoningEffort: expectedIdentity[2],
|
||||
providerBaseUrlSha256: hashValue(expectedIdentity[3]),
|
||||
boundAgentIds: [...agentIds].sort(),
|
||||
};
|
||||
}
|
||||
|
||||
export async function checkPrerequisites(config) {
|
||||
const requiredAgents = requiredAgentIdsForSuite();
|
||||
const llmConfigured = requiredAgents.every((agentId) => {
|
||||
let llmConfigured = requiredAgents.every((agentId) => {
|
||||
const effective = effectiveAgentLlmConfig(config, agentId);
|
||||
return ['apiKey', 'baseUrl', 'model'].every(
|
||||
(key) =>
|
||||
typeof effective[key] === 'string' && effective[key].trim().length > 0,
|
||||
);
|
||||
});
|
||||
const providerBinding = expectedProviderBindingForSuite(config);
|
||||
if (isSupervisorGameChatSingleMainPlayableSuite()) {
|
||||
llmConfigured = llmConfigured && providerBinding !== null;
|
||||
}
|
||||
const editorApiConfigured = ['apiKey', 'baseUrl'].every(
|
||||
(key) =>
|
||||
typeof config.editorApi?.[key] === 'string' &&
|
||||
@@ -77,6 +141,7 @@ export async function checkPrerequisites(config) {
|
||||
);
|
||||
return {
|
||||
llmConfigured,
|
||||
providerBinding,
|
||||
chromeAvailable:
|
||||
!isIsolatedRunnerSuite() ||
|
||||
isSupervisorAutonomousPlayableLaneDefenseSuite()
|
||||
@@ -159,7 +224,9 @@ export function supportedBrowserCandidates(platform, environment) {
|
||||
return candidates;
|
||||
}
|
||||
|
||||
export async function seedDisposableProject() {
|
||||
export async function seedDisposableProject({
|
||||
preserveProductionInitBaseline = false,
|
||||
} = {}) {
|
||||
const prefix = path.join(os.tmpdir(), 'genarrative-agent-runtime-real-e2e-');
|
||||
const sentinelToken = randomUUID();
|
||||
state.projectRoot = await createSentinelOwnedTempDirectory({
|
||||
@@ -198,37 +265,42 @@ export async function seedDisposableProject() {
|
||||
...(isResponseStreamSuite() ? [responseStreamThinkingCanary] : []),
|
||||
];
|
||||
|
||||
const generatedBaselineWrites = preserveProductionInitBaseline
|
||||
? []
|
||||
: [
|
||||
fs.writeFile(
|
||||
path.join(state.projectRoot, 'package.json'),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
name: 'genarrative-agent-runtime-real-e2e-project',
|
||||
private: true,
|
||||
scripts: {
|
||||
test: verificationCommand,
|
||||
'check:e2e': verificationCommand,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
),
|
||||
fs.writeFile(
|
||||
path.join(state.projectRoot, 'verify-e2e.mjs'),
|
||||
isSupervisorSwarmSuite()
|
||||
? supervisorSwarmVerificationFixtureSource()
|
||||
: isGoalRuntimeSuite() ||
|
||||
isResponseStreamSuite() ||
|
||||
isWebSearchSuite() ||
|
||||
isSupervisorAutonomousPlayableLaneDefenseSuite()
|
||||
? goalRevisionOneVerificationFixtureSource()
|
||||
: goalRevisionTwoVerificationFixtureSource(),
|
||||
),
|
||||
fs.writeFile(
|
||||
path.join(state.projectRoot, 'game/index.html'),
|
||||
seededGameHtml(),
|
||||
),
|
||||
];
|
||||
await Promise.all([
|
||||
fs.writeFile(
|
||||
path.join(state.projectRoot, 'package.json'),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
name: 'genarrative-agent-runtime-real-e2e-project',
|
||||
private: true,
|
||||
scripts: {
|
||||
test: verificationCommand,
|
||||
'check:e2e': verificationCommand,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
),
|
||||
fs.writeFile(
|
||||
path.join(state.projectRoot, 'verify-e2e.mjs'),
|
||||
isSupervisorSwarmSuite()
|
||||
? supervisorSwarmVerificationFixtureSource()
|
||||
: isGoalRuntimeSuite() ||
|
||||
isResponseStreamSuite() ||
|
||||
isWebSearchSuite() ||
|
||||
isSupervisorAutonomousPlayableLaneDefenseSuite()
|
||||
? goalRevisionOneVerificationFixtureSource()
|
||||
: goalRevisionTwoVerificationFixtureSource(),
|
||||
),
|
||||
fs.writeFile(
|
||||
path.join(state.projectRoot, 'game/index.html'),
|
||||
seededGameHtml(),
|
||||
),
|
||||
...generatedBaselineWrites,
|
||||
fs.writeFile(
|
||||
path.join(state.projectRoot, 'AGENTS.md'),
|
||||
'# Real E2E Repository Context\n\n- REPOSITORY_CONTEXT_MARKER: preserve the visible text and canvas.\n- Read package.json before verification.\n- Never read or expose .env, game-creator.config.json, or .agent/private-secret.txt.\n',
|
||||
@@ -273,7 +345,16 @@ export async function seedDisposableProject() {
|
||||
mode: 0o600,
|
||||
},
|
||||
);
|
||||
await initializeDisposableGitRepository();
|
||||
await initializeDisposableGitRepository(
|
||||
preserveProductionInitBaseline
|
||||
? [
|
||||
'AGENTS.md',
|
||||
'e2e/isolated-a/evidence.txt',
|
||||
'e2e/isolated-b/evidence.txt',
|
||||
'e2e/isolated-c/evidence.txt',
|
||||
]
|
||||
: undefined,
|
||||
);
|
||||
}
|
||||
|
||||
export async function initializeDisposableGitRepository(
|
||||
@@ -319,12 +400,17 @@ export function seededGameHtml() {
|
||||
<main>
|
||||
<h1>${visibleText}</h1>
|
||||
<p id="patch-state">REAL_E2E_TARGET:before</p>
|
||||
<p id="objective">Objective: survive until victory. On defeat, use Restart.</p>
|
||||
<canvas id="game" width="640" height="360"></canvas>
|
||||
</main>
|
||||
<script>
|
||||
const canvas = document.getElementById('game');
|
||||
const context = canvas.getContext('2d');
|
||||
let frame = 0;
|
||||
canvas.addEventListener('pointerdown', () => {
|
||||
frame = 0;
|
||||
document.body.dataset.input = 'pointer';
|
||||
});
|
||||
function draw() {
|
||||
frame += 1;
|
||||
context.fillStyle = '#13293d'; context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
@@ -341,6 +427,23 @@ export function seededGameHtml() {
|
||||
`;
|
||||
}
|
||||
|
||||
export function productionDefaultGameIndexHtml() {
|
||||
return `<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Genarrative Game Draft</title>
|
||||
<style>
|
||||
body { margin: 0; display: grid; min-height: 100vh; place-items: center; background: #101827; color: #d9e7ff; font: 16px system-ui, sans-serif; }
|
||||
main { width: min(720px, calc(100vw - 32px)); }
|
||||
</style>
|
||||
</head>
|
||||
<body><main>还没有生成游戏。回到聊天输入创意并确认生成后,这里会写入可试玩原型。</main></body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
export function buildTaskPrompt(suite) {
|
||||
const editorAssetOutcome =
|
||||
suite === 'full'
|
||||
|
||||
@@ -47,12 +47,45 @@ export async function removeDisposableProject() {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function providerUsedFromEvidence(evidence) {
|
||||
const requestIdentityCount = evidence?.providerRequestIdentityCount;
|
||||
const startedCount = evidence?.providerLifecycleStartedCount;
|
||||
const terminalCount = evidence?.providerLifecycleTerminalCount;
|
||||
const completedCount = evidence?.providerLifecycleCompletedCount;
|
||||
return (
|
||||
evidence?.evidenceCompleteness === 'complete' &&
|
||||
evidence.providerAgentMode === 'provider' &&
|
||||
evidence.providerBindingMatched === true &&
|
||||
typeof evidence.providerModel === 'string' &&
|
||||
evidence.providerModel.trim().length > 0 &&
|
||||
typeof evidence.providerApiKind === 'string' &&
|
||||
evidence.providerApiKind.trim().length > 0 &&
|
||||
typeof evidence.providerReasoningEffort === 'string' &&
|
||||
evidence.providerReasoningEffort.trim().length > 0 &&
|
||||
/^[0-9a-f]{64}$/u.test(evidence.providerBaseUrlSha256 ?? '') &&
|
||||
Number.isSafeInteger(evidence.providerBoundAgentCount) &&
|
||||
evidence.providerBoundAgentCount > 0 &&
|
||||
Number.isSafeInteger(requestIdentityCount) &&
|
||||
Number.isSafeInteger(startedCount) &&
|
||||
Number.isSafeInteger(terminalCount) &&
|
||||
Number.isSafeInteger(completedCount) &&
|
||||
requestIdentityCount > 0 &&
|
||||
startedCount === requestIdentityCount &&
|
||||
terminalCount === requestIdentityCount &&
|
||||
completedCount === requestIdentityCount &&
|
||||
evidence.providerLifecycleFailedCount === 0 &&
|
||||
evidence.openProviderLifecycleCount === 0 &&
|
||||
evidence.duplicateProviderLifecycleCount === 0
|
||||
);
|
||||
}
|
||||
|
||||
export function buildSummary() {
|
||||
const secretLeakCount =
|
||||
state.transcriptLeakCount + state.projectLeakCount + state.reportLeakCount;
|
||||
const base = {
|
||||
status: state.status,
|
||||
suite: state.suite,
|
||||
providerUsed: providerUsedFromEvidence(state.evidence),
|
||||
config: state.config,
|
||||
blocked: state.blocked,
|
||||
run: {
|
||||
|
||||
@@ -3118,17 +3118,29 @@ export async function countLureLeaks() {
|
||||
if (excluded.has(relative)) continue;
|
||||
const metadata = await fs.lstat(file);
|
||||
if (!metadata.isFile() || metadata.isSymbolicLink()) continue;
|
||||
if (isEmptyExecutionOwnerLock(state.projectRoot, file, metadata)) continue;
|
||||
const content = await fs.readFile(file);
|
||||
count += countExactSecrets(content, state.lures);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
export function isEmptyExecutionOwnerLock(root, file, metadata) {
|
||||
return (
|
||||
metadata?.isFile?.() === true &&
|
||||
metadata.isSymbolicLink() === false &&
|
||||
metadata.size === 0 &&
|
||||
path.resolve(file) ===
|
||||
path.resolve(root, '.agent/runtime/execution-owner.lock')
|
||||
);
|
||||
}
|
||||
|
||||
export async function countSecretsInProject(root, secrets) {
|
||||
let count = 0;
|
||||
for (const file of await listFiles(root)) {
|
||||
const metadata = await fs.lstat(file);
|
||||
if (!metadata.isFile() || metadata.isSymbolicLink()) continue;
|
||||
if (isEmptyExecutionOwnerLock(root, file, metadata)) continue;
|
||||
count += await countSecretsInFile(file, secrets);
|
||||
}
|
||||
return count;
|
||||
|
||||
@@ -846,15 +846,94 @@ finally:
|
||||
os.close(pidfd)
|
||||
`;
|
||||
|
||||
export const windowsProcessHandleHelperSource = String.raw`
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$nativeSource = @'
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
public static class GenarrativeOwnedProcessHandle
|
||||
{
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern IntPtr OpenProcess(
|
||||
uint desiredAccess,
|
||||
[MarshalAs(UnmanagedType.Bool)] bool inheritHandle,
|
||||
uint processId);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool TerminateProcess(IntPtr process, uint exitCode);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern uint WaitForSingleObject(IntPtr handle, uint milliseconds);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern uint GetProcessId(IntPtr process);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool CloseHandle(IntPtr handle);
|
||||
}
|
||||
'@
|
||||
|
||||
[void](Add-Type -TypeDefinition $nativeSource -ErrorAction Stop)
|
||||
$targetPidText = [Environment]::GetEnvironmentVariable(
|
||||
'AGC_OWNED_RUNNER_PID',
|
||||
[EnvironmentVariableTarget]::Process)
|
||||
if ([String]::IsNullOrWhiteSpace($targetPidText)) { exit 70 }
|
||||
$targetPid = [UInt32]::Parse($targetPidText)
|
||||
$desiredAccess = [UInt32](0x0001 -bor 0x00100000 -bor 0x1000)
|
||||
$handle = [GenarrativeOwnedProcessHandle]::OpenProcess(
|
||||
$desiredAccess,
|
||||
$false,
|
||||
$targetPid)
|
||||
if ($handle -eq [IntPtr]::Zero) { exit 71 }
|
||||
|
||||
try {
|
||||
if ([GenarrativeOwnedProcessHandle]::GetProcessId($handle) -ne $targetPid) {
|
||||
exit 72
|
||||
}
|
||||
[Console]::Out.WriteLine('HANDLE_READY')
|
||||
[Console]::Out.Flush()
|
||||
$command = [Console]::In.ReadLine()
|
||||
if ($command -eq 'CLOSE') { exit 0 }
|
||||
if ($command -ne 'KILL') { exit 73 }
|
||||
if (-not [GenarrativeOwnedProcessHandle]::TerminateProcess($handle, 137)) {
|
||||
exit 74
|
||||
}
|
||||
if ([GenarrativeOwnedProcessHandle]::WaitForSingleObject($handle, 10000) -ne 0) {
|
||||
exit 75
|
||||
}
|
||||
[Console]::Out.WriteLine('HANDLE_EXITED')
|
||||
[Console]::Out.Flush()
|
||||
} finally {
|
||||
[void][GenarrativeOwnedProcessHandle]::CloseHandle($handle)
|
||||
}
|
||||
`;
|
||||
|
||||
export const activeCommandChildren = new Set();
|
||||
|
||||
export const shutdownWaiters = new Set();
|
||||
|
||||
export class StreamingSecretScanner {
|
||||
constructor(secrets) {
|
||||
this.secrets = secrets.map((value) => Buffer.from(value));
|
||||
this.secrets = [];
|
||||
this.secretKeys = new Set();
|
||||
this.tails = new Map();
|
||||
this.count = 0;
|
||||
this.addSecrets(secrets);
|
||||
}
|
||||
|
||||
addSecrets(secrets) {
|
||||
for (const value of secrets) {
|
||||
const secret = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
||||
if (secret.length === 0) continue;
|
||||
const key = secret.toString('base64');
|
||||
if (this.secretKeys.has(key)) continue;
|
||||
this.secretKeys.add(key);
|
||||
this.secrets.push(secret);
|
||||
}
|
||||
}
|
||||
|
||||
scan(source, chunk) {
|
||||
@@ -915,6 +994,7 @@ export const isolatedRunnerState = {
|
||||
export const state = {
|
||||
shutdownSignal: null,
|
||||
linuxPidfdPythonPath: null,
|
||||
windowsProcessHandlePowerShellPath: null,
|
||||
cleanupInProgress: false,
|
||||
userInputCliSession: null,
|
||||
supervisorSwarmCliSession: null,
|
||||
@@ -934,7 +1014,7 @@ export const state = {
|
||||
transcriptLeakCount: 0,
|
||||
projectLeakCount: 0,
|
||||
reportLeakCount: 0,
|
||||
lureLeakCount: 0,
|
||||
lureLeakCount: null,
|
||||
commandOutputMarkerSeenInContext: false,
|
||||
commandOutputContextPages: new Set(),
|
||||
commandMarkerReportLeakCount: 0,
|
||||
@@ -1084,7 +1164,10 @@ export const state = {
|
||||
reportLeakCount: 0,
|
||||
},
|
||||
supervisorAutonomousPlayable: {
|
||||
freshInitBaselineUsed: false,
|
||||
initialGameIndexSha256: null,
|
||||
expectedProviderBinding: null,
|
||||
effectiveProviderBinding: null,
|
||||
stdinWriteCount: 0,
|
||||
stdinEnded: false,
|
||||
stdinBytes: 0,
|
||||
@@ -1092,6 +1175,7 @@ export const state = {
|
||||
cliOutput: '',
|
||||
privateValues: [],
|
||||
reportLeakCount: 0,
|
||||
ownedProcessCleanupSnapshot: null,
|
||||
},
|
||||
supervisorSwarm: {
|
||||
effectiveModel: null,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+650
-25
File diff suppressed because it is too large
Load Diff
+38
-17
@@ -90,6 +90,37 @@ export async function listOptionalSupervisorSwarmFile(file) {
|
||||
return [file];
|
||||
}
|
||||
|
||||
export function supervisorSwarmProfessionalSessions({
|
||||
deliveries = [],
|
||||
latestTasks = [],
|
||||
rootAgentId,
|
||||
rootRunId,
|
||||
}) {
|
||||
const sessions = new Map();
|
||||
const registerSession = (agentId, sessionId) => {
|
||||
if (!isNonEmptyString(agentId) || !isNonEmptyString(sessionId)) return;
|
||||
sessions.set(`${agentId}\0${sessionId}`, { agentId, sessionId });
|
||||
};
|
||||
for (const delivery of deliveries) {
|
||||
registerSession(delivery.targetAgentId, delivery.targetSessionId);
|
||||
}
|
||||
if (!isNonEmptyString(rootAgentId) || !isNonEmptyString(rootRunId)) {
|
||||
return [...sessions.values()];
|
||||
}
|
||||
for (const task of latestTasks) {
|
||||
if (
|
||||
task.source !== 'agent-ready-task-scheduler' ||
|
||||
task.agentId === rootAgentId ||
|
||||
task.parentAgentId !== rootAgentId ||
|
||||
task.parentRunId !== rootRunId
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
registerSession(task.agentId, task.sessionId);
|
||||
}
|
||||
return [...sessions.values()];
|
||||
}
|
||||
|
||||
export async function readSupervisorSwarmPersistence({
|
||||
tolerateErrors = false,
|
||||
} = {}) {
|
||||
@@ -274,25 +305,15 @@ export async function readSupervisorSwarmPersistence({
|
||||
),
|
||||
);
|
||||
const supervisorConversation = supervisorConversationSurface.records;
|
||||
const professionalSessions = new Map();
|
||||
for (const delivery of deliveries) {
|
||||
if (
|
||||
!isNonEmptyString(delivery.targetAgentId) ||
|
||||
!isNonEmptyString(delivery.targetSessionId)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
professionalSessions.set(
|
||||
`${delivery.targetAgentId}\0${delivery.targetSessionId}`,
|
||||
{
|
||||
agentId: delivery.targetAgentId,
|
||||
sessionId: delivery.targetSessionId,
|
||||
},
|
||||
);
|
||||
}
|
||||
const professionalSessions = supervisorSwarmProfessionalSessions({
|
||||
deliveries,
|
||||
latestTasks: taskSnapshot.latest,
|
||||
rootAgentId: projectSupervisorAgentId,
|
||||
rootRunId: state.initialRunId,
|
||||
});
|
||||
const professionalConversations = [];
|
||||
const professionalConversationErrors = [];
|
||||
for (const session of professionalSessions.values()) {
|
||||
for (const session of professionalSessions) {
|
||||
const conversationSurface = await readJsonlSurface(
|
||||
'professional-conversation',
|
||||
() =>
|
||||
|
||||
@@ -1186,6 +1186,7 @@ const allowedLlmReasoningEfforts = new Set([
|
||||
'low',
|
||||
'medium',
|
||||
'high',
|
||||
'max',
|
||||
]);
|
||||
|
||||
if (defaultAppConfig.llm?.reasoningEffort !== 'high') {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -450,6 +450,15 @@ fn game_creator_codex_app_server_validate_llm_config(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_game_creator_codex_app_server_reasoning_effort(
|
||||
params: &mut serde_json::Value,
|
||||
request: &LlmRunRequest,
|
||||
) {
|
||||
if let Some(effort) = game_creator_codex_cli_reasoning_effort(request) {
|
||||
params["effort"] = serde_json::Value::String(effort.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
fn quoted_toml_string(value: &str) -> Result<String, platform_llm::LlmError> {
|
||||
serde_json::to_string(value).map_err(|error| {
|
||||
platform_llm::LlmError::InvalidConfig(format!("序列化 Codex app-server 配置失败:{error}"))
|
||||
@@ -970,9 +979,7 @@ impl CodexAppServerConnection {
|
||||
"approvalPolicy": "never",
|
||||
"sandboxPolicy": { "type": "readOnly", "networkAccess": false },
|
||||
});
|
||||
if let Some(effort) = game_creator_codex_cli_reasoning_effort(&request) {
|
||||
params["effort"] = serde_json::Value::String(effort.to_string());
|
||||
}
|
||||
apply_game_creator_codex_app_server_reasoning_effort(&mut params, &request);
|
||||
if let Some(schema) = game_creator_codex_cli_tool_output_schema(&request) {
|
||||
params["outputSchema"] = schema;
|
||||
}
|
||||
@@ -1646,6 +1653,15 @@ mod tests {
|
||||
])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_app_server_reasoning_effort_preserves_max_wire_value() {
|
||||
let request = LlmRunRequest::single_turn("系统", "任务")
|
||||
.with_response_reasoning_effort(platform_llm::LlmResponseReasoningEffort::Max);
|
||||
let mut params = serde_json::json!({});
|
||||
apply_game_creator_codex_app_server_reasoning_effort(&mut params, &request);
|
||||
assert_eq!(params["effort"], "max");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_app_server_maps_structured_output_to_runtime_tool_calls() {
|
||||
let text = r#"{"toolCalls":[{"name":"runtime_tool_file_read","arguments":"{\"path\":\"game/index.html\"}"}]}"#;
|
||||
|
||||
@@ -144,6 +144,7 @@ pub(in crate::agent) fn game_creator_codex_cli_reasoning_effort(
|
||||
platform_llm::LlmResponseReasoningEffort::Low => "low",
|
||||
platform_llm::LlmResponseReasoningEffort::Medium => "medium",
|
||||
platform_llm::LlmResponseReasoningEffort::High => "high",
|
||||
platform_llm::LlmResponseReasoningEffort::Max => "max",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -666,6 +667,16 @@ mod tests {
|
||||
])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_cli_reasoning_effort_preserves_max_wire_value() {
|
||||
let request = LlmRunRequest::single_turn("系统", "任务")
|
||||
.with_response_reasoning_effort(platform_llm::LlmResponseReasoningEffort::Max);
|
||||
assert_eq!(
|
||||
game_creator_codex_cli_reasoning_effort(&request),
|
||||
Some("max")
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn codex_cli_candidates_prefer_sorted_native_npm_targets_before_path() {
|
||||
|
||||
@@ -279,21 +279,12 @@ fn agent_interaction_system_prompt(agent_id: &str) -> String {
|
||||
)
|
||||
}
|
||||
|
||||
fn build_agent_interaction_request_for_session(
|
||||
root: &Path,
|
||||
fn build_agent_interaction_llm_request(
|
||||
agent_id: &str,
|
||||
session_id: &str,
|
||||
prompt: &str,
|
||||
) -> Result<(GameCreatorLlmConfig, String, LlmRunRequest), String> {
|
||||
let prompt = prompt.trim();
|
||||
if prompt.is_empty() {
|
||||
return Err("交互内容不能为空".to_string());
|
||||
}
|
||||
if !game_creator_agent_uses_interaction_kernel(agent_id) {
|
||||
return Err(format!("Agent 不启用交互决策层:{agent_id}"));
|
||||
}
|
||||
let (llm, config_path, context) =
|
||||
build_game_creator_role_agent_context_for_session(root, agent_id, Some(session_id))?;
|
||||
context: &str,
|
||||
llm: &GameCreatorLlmConfig,
|
||||
) -> Result<LlmRunRequest, String> {
|
||||
let api_kind = parse_game_creator_llm_api_kind(&llm.api_kind)?;
|
||||
let user_prompt = if context.trim().is_empty() {
|
||||
format!("用户这轮输入:\n{prompt}")
|
||||
@@ -311,6 +302,25 @@ fn build_agent_interaction_request_for_session(
|
||||
.with_max_output_tokens(AGENT_INTERACTION_MAX_OUTPUT_TOKENS)
|
||||
.with_function_tools(function_tools)
|
||||
.with_tool_choice(platform_llm::LlmToolChoice::Auto);
|
||||
apply_game_creator_llm_reasoning_effort(request, llm)
|
||||
}
|
||||
|
||||
fn build_agent_interaction_request_for_session(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
session_id: &str,
|
||||
prompt: &str,
|
||||
) -> Result<(GameCreatorLlmConfig, String, LlmRunRequest), String> {
|
||||
let prompt = prompt.trim();
|
||||
if prompt.is_empty() {
|
||||
return Err("交互内容不能为空".to_string());
|
||||
}
|
||||
if !game_creator_agent_uses_interaction_kernel(agent_id) {
|
||||
return Err(format!("Agent 不启用交互决策层:{agent_id}"));
|
||||
}
|
||||
let (llm, config_path, context) =
|
||||
build_game_creator_role_agent_context_for_session(root, agent_id, Some(session_id))?;
|
||||
let request = build_agent_interaction_llm_request(agent_id, prompt, &context, &llm)?;
|
||||
Ok((llm, config_path, request))
|
||||
}
|
||||
|
||||
@@ -626,12 +636,16 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn interaction_request_crosses_neutral_provider_contract_without_shape_drift() {
|
||||
let request =
|
||||
LlmRunRequest::new(vec![LlmMessage::system("system"), LlmMessage::user("user")])
|
||||
.with_api_kind(LlmApiKind::OpenAiResponses)
|
||||
.with_max_output_tokens(AGENT_INTERACTION_MAX_OUTPUT_TOKENS)
|
||||
.with_function_tools(agent_interaction_function_tools().expect("interaction tools"))
|
||||
.with_tool_choice(platform_llm::LlmToolChoice::Auto);
|
||||
let mut llm = GameCreatorLlmConfig::default();
|
||||
llm.api_kind = "openai_responses".to_string();
|
||||
llm.reasoning_effort = "max".to_string();
|
||||
let request = build_agent_interaction_llm_request(
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
"制作一个三消经营游戏",
|
||||
"",
|
||||
&llm,
|
||||
)
|
||||
.expect("build interaction request");
|
||||
let request = platform_llm::provider_request_from_llm_request("agent-interaction", request)
|
||||
.expect("neutral request");
|
||||
assert_eq!(
|
||||
@@ -639,6 +653,10 @@ mod tests {
|
||||
Some(AGENT_INTERACTION_MAX_OUTPUT_TOKENS)
|
||||
);
|
||||
assert_eq!(request.tools().len(), 3);
|
||||
assert_eq!(
|
||||
request.reasoning_effort(),
|
||||
Some(agent_runtime_core::ProviderReasoningEffort::Max)
|
||||
);
|
||||
assert!(request.tools().iter().all(|tool| tool.strict()));
|
||||
assert_eq!(
|
||||
request.tool_choice(),
|
||||
@@ -646,6 +664,20 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interaction_request_propagates_invalid_reasoning_effort() {
|
||||
let mut llm = GameCreatorLlmConfig::default();
|
||||
llm.reasoning_effort = "maximum".to_string();
|
||||
let error = build_agent_interaction_llm_request(
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
"制作一个三消经营游戏",
|
||||
"",
|
||||
&llm,
|
||||
)
|
||||
.expect_err("invalid reasoning effort must fail before Provider request");
|
||||
assert!(error.contains("reasoning_effort"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interaction_stream_sink_preserves_accumulation_delta_and_finish_reason() {
|
||||
let mut observed = Vec::new();
|
||||
|
||||
@@ -61,6 +61,11 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action_with_pending_
|
||||
if let Some(blocker) = supervisor_orchestrator_mutation_block_at(root, agent_id, run_id, tool) {
|
||||
return blocker;
|
||||
}
|
||||
if let Some(blocker) =
|
||||
agent_runtime_autonomous_art_director_canvas_only_action_block(agent_id, task, tool)
|
||||
{
|
||||
return blocker;
|
||||
}
|
||||
if let Some(blocker) = game_chat_delegated_art_agent_input_mutation_block(
|
||||
root,
|
||||
agent_id,
|
||||
@@ -640,3 +645,172 @@ pub(in crate::agent) fn agent_runtime_pending_reconciliation_observation(
|
||||
detail: Some(redact_agent_runtime_project_paths(root, error, 500)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod canvas_only_execution_tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn art_director_canvas_only_execution_rejects_file_write_and_mixed_batch_without_side_effects(
|
||||
) {
|
||||
const PARENT_RUN_ID: &str = "canvas-only-execution-parent";
|
||||
const CHILD_ID: &str = "art-director";
|
||||
let _config_guard = crate::tests::write_test_local_config(
|
||||
r#"{"editorApi":{"apiKey":"canvas-only-execution-test-key"}}"#.to_string(),
|
||||
);
|
||||
let temporary = tempfile::tempdir().expect("create canvas-only execution root");
|
||||
let root = temporary.path().join("project");
|
||||
init_local_game_project_at(
|
||||
&root,
|
||||
"canvas-only-execution-project",
|
||||
"生成带统一视觉规范的完整游戏",
|
||||
)
|
||||
.expect("init canvas-only execution project");
|
||||
|
||||
let parent_session = resolve_agent_conversation_session_id_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.expect("resolve canvas-only parent session");
|
||||
let parent = append_unique_game_creator_agent_runtime_pending_task(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&parent_session,
|
||||
"生成带统一视觉规范的完整游戏",
|
||||
PARENT_RUN_ID,
|
||||
AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
|
||||
Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD),
|
||||
None,
|
||||
)
|
||||
.expect("queue canvas-only parent");
|
||||
let mut parent_state = agent_runtime_state_from_task_record(&parent);
|
||||
parent_state.status = "running".to_string();
|
||||
parent_state.phase = "planning".to_string();
|
||||
append_game_creator_agent_runtime_task(&root, &parent_state)
|
||||
.expect("persist running canvas-only parent");
|
||||
|
||||
let task = format!(
|
||||
"生成统一视觉规范图。{AGENT_RUNTIME_AUTONOMOUS_ART_DIRECTOR_CANVAS_ONLY_TASK_MARKER}"
|
||||
);
|
||||
let child_session = resolve_agent_conversation_session_id_at(&root, CHILD_ID, None, true)
|
||||
.expect("resolve canvas-only child session");
|
||||
let child_run_id = autonomous_manifest_ready_task_run_id(PARENT_RUN_ID, CHILD_ID);
|
||||
let child = append_unique_game_creator_agent_runtime_pending_task(
|
||||
&root,
|
||||
CHILD_ID,
|
||||
&child_session,
|
||||
&task,
|
||||
&child_run_id,
|
||||
"agent-ready-task-scheduler",
|
||||
Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD),
|
||||
Some(&AgentRuntimeTaskLink {
|
||||
parent_agent_id: Some(parent.agent_id.clone()),
|
||||
parent_run_id: Some(parent.run_id.clone()),
|
||||
delegation_id: None,
|
||||
}),
|
||||
)
|
||||
.expect("queue canvas-only child");
|
||||
let mut runtime = agent_runtime_state_from_task_record(&child);
|
||||
runtime.status = "running".to_string();
|
||||
runtime.phase = "planning".to_string();
|
||||
write_game_creator_agent_runtime_state(&root, &runtime)
|
||||
.expect("persist canvas-only child runtime");
|
||||
append_game_creator_agent_runtime_task(&root, &runtime)
|
||||
.expect("persist running canvas-only child");
|
||||
update_manifest_task_status_at(&root, CHILD_ID, GameCreationAppTaskStatus::Running)
|
||||
.expect("mark canvas-only child running");
|
||||
|
||||
let forbidden_path = "game/art-director-forbidden.txt";
|
||||
let forbidden_action = AgentRuntimeToolAction {
|
||||
tool: "file.write".to_string(),
|
||||
reason: Some("尝试写入非 Canvas 文件".to_string()),
|
||||
input: serde_json::json!({
|
||||
"path": forbidden_path,
|
||||
"content": "forbidden"
|
||||
}),
|
||||
};
|
||||
let revision_before = read_game_creator_agent_runtime_project_revision(&root)
|
||||
.expect("read revision before canvas-only rejection");
|
||||
let observation = execute_game_creator_agent_runtime_tool_action(
|
||||
&root,
|
||||
CHILD_ID,
|
||||
&child_run_id,
|
||||
&task,
|
||||
&forbidden_action,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(observation.status, "rejected", "{observation:?}");
|
||||
assert_eq!(
|
||||
observation.summary,
|
||||
"art-director 受控视觉任务拒绝非 Canvas 项目动作"
|
||||
);
|
||||
assert!(!root.join(forbidden_path).exists());
|
||||
assert_eq!(
|
||||
read_game_creator_agent_runtime_project_revision(&root)
|
||||
.expect("read revision after direct canvas-only rejection")
|
||||
.revision,
|
||||
revision_before.revision
|
||||
);
|
||||
|
||||
let mixed_plan = AgentRuntimeToolPlan {
|
||||
thinking_summary: "尝试混合文件写入与 Canvas 生成".to_string(),
|
||||
plan_update: None,
|
||||
plan: vec!["写入文件".to_string(), "生成视觉规范图".to_string()],
|
||||
actions: vec![
|
||||
forbidden_action,
|
||||
AgentRuntimeToolAction {
|
||||
tool: "canvas.asset_generate".to_string(),
|
||||
reason: Some("生成统一视觉规范图".to_string()),
|
||||
input: serde_json::json!({
|
||||
"prompt": "生成统一视觉规范图",
|
||||
"outputPath": AGENT_RUNTIME_ART_SPEC_PATH,
|
||||
"assetKind": "icon-spec",
|
||||
"aspectRatio": "1:1",
|
||||
"imageSize": "1K",
|
||||
"replaceExisting": false
|
||||
}),
|
||||
},
|
||||
],
|
||||
response: String::new(),
|
||||
};
|
||||
let repository_fingerprint = build_repository_startup_context_at(&root)
|
||||
.expect("build canvas-only repository context")
|
||||
.fingerprint;
|
||||
let prepared = prepare_game_creator_agent_runtime_provider_action_batch(
|
||||
&root,
|
||||
&runtime,
|
||||
&task,
|
||||
&mixed_plan,
|
||||
&[],
|
||||
&revision_before,
|
||||
&repository_fingerprint,
|
||||
)
|
||||
.await
|
||||
.expect("prepare mixed canvas-only provider batch");
|
||||
assert!(
|
||||
matches!(
|
||||
&prepared,
|
||||
AgentRuntimeProviderActionBatchPreparation::Aborted {
|
||||
observation: AgentRuntimeToolObservation {
|
||||
status,
|
||||
summary,
|
||||
..
|
||||
},
|
||||
..
|
||||
} if status == "blocked"
|
||||
&& summary == "art-director 受控视觉任务拒绝非 Canvas 项目动作"
|
||||
),
|
||||
"mixed file.write + canvas batch must abort before execution: {prepared:?}"
|
||||
);
|
||||
assert!(!root.join(forbidden_path).exists());
|
||||
assert!(!root.join(AGENT_RUNTIME_ART_SPEC_PATH).exists());
|
||||
assert_eq!(
|
||||
read_game_creator_agent_runtime_project_revision(&root)
|
||||
.expect("read revision after mixed canvas-only rejection")
|
||||
.revision,
|
||||
revision_before.revision
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+504
-8
File diff suppressed because it is too large
Load Diff
@@ -845,8 +845,17 @@ pub(in crate::agent) fn static_delegate_completion_blocker_at_locked(
|
||||
agent_id: &str,
|
||||
run_id: &str,
|
||||
) -> Option<AgentRuntimeToolObservation> {
|
||||
if agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
|
||||
return None;
|
||||
match static_delegate_parent_can_manage_receipts_at(root, agent_id, run_id) {
|
||||
Ok(true) => {}
|
||||
Ok(false) => return None,
|
||||
Err(error) => {
|
||||
return Some(AgentRuntimeToolObservation {
|
||||
tool: "runtime.delegate_receipts".to_string(),
|
||||
status: "blocked".to_string(),
|
||||
summary: "无法确认当前父 Run 的专业 Agent 委派权限,不能收束当前任务".to_string(),
|
||||
detail: Some(redact_agent_runtime_project_paths(root, &error, 500)),
|
||||
});
|
||||
}
|
||||
}
|
||||
match static_delegate_completion_barrier_at(root, agent_id, run_id) {
|
||||
Ok(barrier) if barrier.is_clear() => None,
|
||||
|
||||
+9
-1
@@ -473,6 +473,13 @@ pub(crate) async fn prepare_game_creator_agent_runtime_provider_action_batch(
|
||||
None,
|
||||
)?;
|
||||
let command_id = game_creator_agent_runtime_tool_command_id(action.tool.trim());
|
||||
let art_director_canvas_only_block =
|
||||
agent_runtime_autonomous_art_director_canvas_only_action_block(
|
||||
&runtime.agent_id,
|
||||
task,
|
||||
action.tool.trim(),
|
||||
)
|
||||
.map(|observation| AgentRuntimeToolPolicyBlock::Denied(observation.summary));
|
||||
let game_chat_art_scope_block = game_chat_delegated_art_agent_input_mutation_block(
|
||||
root,
|
||||
&runtime.agent_id,
|
||||
@@ -493,7 +500,8 @@ pub(crate) async fn prepare_game_creator_agent_runtime_provider_action_batch(
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let local_policy_block = game_chat_art_scope_block
|
||||
let local_policy_block = art_director_canvas_only_block
|
||||
.or(game_chat_art_scope_block)
|
||||
.or(isolated_scope_block)
|
||||
.or_else(|| {
|
||||
command_id
|
||||
|
||||
+231
-22
@@ -9,6 +9,63 @@ enum AgentBackgroundContextMode {
|
||||
FinalReply,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub(in crate::agent) struct AgentRuntimeToolPlanRequestSnapshot {
|
||||
pub(super) supervisor_manifest_dag_in_progress: bool,
|
||||
}
|
||||
|
||||
fn parse_manifest_seed_task_counts(detail: &str) -> Option<[u64; 6]> {
|
||||
let counts = detail
|
||||
.lines()
|
||||
.find_map(|line| line.strip_prefix("seedTaskCounts: "))?;
|
||||
let mut parsed = std::collections::BTreeMap::new();
|
||||
for field in counts.split_ascii_whitespace() {
|
||||
let (key, value) = field.split_once('=')?;
|
||||
if parsed.insert(key, value.parse::<u64>().ok()?).is_some() {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let completed = *parsed.get("completed")?;
|
||||
let running = *parsed.get("running")?;
|
||||
let pending = *parsed.get("pending")?;
|
||||
let waiting = *parsed.get("waiting")?;
|
||||
let failed = *parsed.get("failed")?;
|
||||
let total = *parsed.get("total")?;
|
||||
if parsed.len() != 6
|
||||
|| completed
|
||||
.checked_add(running)?
|
||||
.checked_add(pending)?
|
||||
.checked_add(waiting)?
|
||||
.checked_add(failed)?
|
||||
!= total
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some([completed, running, pending, waiting, failed, total])
|
||||
}
|
||||
|
||||
fn prompt_observations_report_manifest_dag_in_progress(
|
||||
observations: &[AgentRuntimeToolObservation],
|
||||
) -> bool {
|
||||
let Some(observation) = observations
|
||||
.iter()
|
||||
.rfind(|observation| observation.tool == "task.list")
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
if observation.status != "ok" || observation.summary != "已读取 manifest 任务图" {
|
||||
return false;
|
||||
}
|
||||
let Some([_completed, running, _pending, _waiting, _failed, total]) = observation
|
||||
.detail
|
||||
.as_deref()
|
||||
.and_then(parse_manifest_seed_task_counts)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
total > 0 && running > 0
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn provider_command_exec_contract() -> &'static str {
|
||||
"command.exec 使用 {\"program\":\"受信任 PATH 中的裸可执行名\",\"args\":[\"逐项 argv\"],\"cwd\":\"可选项目内相对目录\",\"timeoutSeconds\":120};Linux 命令固定运行在 bubblewrap workspace-write、network-disabled 沙箱内,允许 bash -lc、管道、重定向和项目脚本,但不接受环境变量、宿主 executable 路径、mount 或网络策略输入"
|
||||
@@ -72,7 +129,16 @@ pub(in crate::agent) fn build_game_creator_agent_background_tool_plan_request(
|
||||
observations: &[AgentRuntimeToolObservation],
|
||||
loop_index: usize,
|
||||
mcp_catalog: &GameCreatorMcpCatalog,
|
||||
) -> Result<(GameCreatorLlmConfig, String, LlmRunRequest, String), String> {
|
||||
) -> Result<
|
||||
(
|
||||
GameCreatorLlmConfig,
|
||||
String,
|
||||
LlmRunRequest,
|
||||
String,
|
||||
AgentRuntimeToolPlanRequestSnapshot,
|
||||
),
|
||||
String,
|
||||
> {
|
||||
let effective_task = autonomous_effective_root_task_at(root, agent_id, run_id, task)?;
|
||||
let (llm, config_path, context, repository_context_fingerprint, prompt_observations) =
|
||||
build_game_creator_background_agent_context(
|
||||
@@ -113,6 +179,11 @@ pub(in crate::agent) fn build_game_creator_agent_background_tool_plan_request(
|
||||
let goal_contract_participant = root_control_authority || root_goal_contract_context != "null";
|
||||
let autonomous_game_build =
|
||||
tool_policy.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD;
|
||||
let request_snapshot = AgentRuntimeToolPlanRequestSnapshot {
|
||||
supervisor_manifest_dag_in_progress: autonomous_game_build
|
||||
&& agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
||||
&& prompt_observations_report_manifest_dag_in_progress(&prompt_observations),
|
||||
};
|
||||
let autonomous_project_verify_available =
|
||||
!autonomous_game_build || agent_runtime_autonomous_project_verify_available(root);
|
||||
let mut allowed_tools = tool_policy.allowed_tools.clone();
|
||||
@@ -215,7 +286,7 @@ pub(in crate::agent) fn build_game_creator_agent_background_tool_plan_request(
|
||||
"file.list 使用 {{\"path\":\"\"}},path 为空字符串时列出项目摘要;file.read 使用 {{\"path\":\"项目内相对路径\",\"startLine\":1,\"maxLines\":120}};file.write 使用 {{\"path\":\"项目内相对路径\",\"content\":\"完整文件内容\"}};file.patch 使用 {{\"path\":\"项目内相对路径\",\"oldText\":\"必须精确匹配的原文\",\"newText\":\"替换后的文本\",\"expectedReplacements\":1}};file.delete 使用 {{\"path\":\"项目内相对路径\"}},只删除项目内普通文件,不删除目录或任何 .agent 控制面文件。\n",
|
||||
"task.create 使用 {{\"taskId\":null,\"title\":\"任务标题\",\"group\":\"design|art|code|balance|audio|publishing\",\"role\":\"角色名\",\"dependencies\":[],\"artifacts\":[],\"acceptanceCriteria\":[\"验收标准\"],\"status\":\"pending|running|waiting-for-confirmation|completed|failed\"}},需要自定义 taskId 时把 null 替换为合法 ID;task.update 使用 {{\"taskId\":\"manifest taskId\",\"status\":\"pending|running|waiting-for-confirmation|completed|failed\"}};command.run_limited 使用 {{\"commandId\":\"game.static_smoke\"}}。\n",
|
||||
"canvas.asset_generate 使用 {{\"prompt\":\"图片描述\",\"outputPath\":null,\"aspectRatio\":null,\"imageSize\":null,\"assetKind\":null,\"assetLabel\":null,\"replaceExisting\":false}};需要指定时,aspectRatio 只允许 1:1|2:3|3:2|9:16|16:9,imageSize 只允许 0.5K|1K|2K,assetKind 只允许 {canvas_asset_kind_catalog}。replaceExisting 只能在带 repairOfDelegationId 的唯一返工委派中设为 true,普通生成必须为 false,并通过配置的 External Editor API 同时写入画布、同名素材库目录和本地 assets。\n",
|
||||
"blackboard.write 使用 {{\"title\":\"标题\",\"content\":\"要共享给所有 Agent 的稳定结论\"}};agent.message 使用 {{\"agentId\":\"目标 taskId\",\"content\":\"给目标 Agent 的定向消息\"}};agent.delegate 使用 {{\"agentId\":\"目标 taskId\",\"task\":\"要委派的后台任务\",\"acceptanceCriteria\":[\"可核对的语义验收条件\"],\"expectedArtifacts\":[],\"repairOfDelegationId\":null,\"runId\":null}},expectedArtifacts 无产物时传空数组且不接受 glob;返工时 repairOfDelegationId 指向已认领原 delivery 且 runId 必须为 null;agent.schedule_ready 使用 {{\"limit\":1}};agent.route_manifest 使用 {{\"strategy\":\"audit-existing-first|use-existing-art|generate-missing-art\",\"intentSummary\":\"Supervisor 自行理解的用户意图,仅 Supervisor 提交\",\"missingAssetSlots\":[]}};Supervisor 必须自行概括非空 intentSummary,并以 audit-existing-first 提交执行安全策略;code-prototype 先 asset.list 后只能按权威缺口提交 use-existing-art 或 generate-missing-art,且无需提交 intentSummary;agent.run_status 使用 {{\"agentId\":null,\"scope\":\"all\",\"delegationId\":null}},指定目标 Agent 或已认领 delegation 时把对应 null 替换为实际 ID;Project Supervisor 传 delegationId 时读取当前父 run 的未截断权威返工合同。\n",
|
||||
"blackboard.write 使用 {{\"title\":\"标题\",\"content\":\"要共享给所有 Agent 的稳定结论\"}};agent.message 使用 {{\"agentId\":\"目标 taskId\",\"content\":\"给目标 Agent 的定向消息\"}};agent.delegate 使用 {{\"agentId\":\"目标 taskId\",\"task\":\"要委派的后台任务\",\"acceptanceCriteria\":[\"可核对的语义验收条件\"],\"expectedArtifacts\":[],\"repairOfDelegationId\":null,\"runId\":null}},expectedArtifacts 无产物时传空数组且不接受 glob;返工时 repairOfDelegationId 指向已认领原 delivery 且 runId 必须为 null;agent.schedule_ready 使用 {{\"limit\":1}};agent.route_manifest 使用 {{\"strategy\":\"audit-existing-first|use-existing-art|generate-missing-art\",\"intentSummary\":\"Supervisor 自行理解的用户意图,仅 Supervisor 提交\",\"missingAssetSlots\":[]}};Supervisor 必须自行概括非空 intentSummary,并以 audit-existing-first 提交执行安全策略;code-prototype 先 asset.list 后只能按权威缺口提交 use-existing-art 或 generate-missing-art,且无需提交 intentSummary;agent.run_status 使用 {{\"agentId\":null,\"scope\":\"all\",\"delegationId\":null}},指定目标 Agent 或已认领 delegation 时把对应 null 替换为实际 ID;当前可信父 Run 传 delegationId 时读取自己已认领的未截断权威返工合同。\n",
|
||||
"当前请求中的每个 MCP 工具都以单独的动态函数广告;必须从实际广告函数中选择,并严格按该函数的 input schema 提交 arguments.input。server、tool、catalogFingerprint 和 toolFingerprint 由 Runtime 注入,禁止构造目录外包装调用。\n",
|
||||
"只有 conversation.read、asset.list、project.index、project.checkpoint、task.list、preview.start 的 arguments.input 使用空对象 {{}};其他函数必须提交实际广告 schema 的全部 required 字段。如果已有观察足够,必须调用 respond_to_user 交付最终回复。"
|
||||
),
|
||||
@@ -392,7 +463,13 @@ pub(in crate::agent) fn build_game_creator_agent_background_tool_plan_request(
|
||||
&llm,
|
||||
true,
|
||||
)?;
|
||||
Ok((llm, config_path, request, repository_context_fingerprint))
|
||||
Ok((
|
||||
llm,
|
||||
config_path,
|
||||
request,
|
||||
repository_context_fingerprint,
|
||||
request_snapshot,
|
||||
))
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn build_game_creator_agent_background_final_reply_request(
|
||||
@@ -571,12 +648,13 @@ fn build_game_creator_background_agent_context(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::agent::{
|
||||
cancel_game_creator_agent_runtime_task_at,
|
||||
autonomous_manifest_dag_in_progress_at, cancel_game_creator_agent_runtime_task_at,
|
||||
persist_game_chat_supervisor_workflow_decision_at,
|
||||
start_game_creator_supervisor_background_task_for_session_at,
|
||||
try_acquire_game_creator_agent_runtime_task_lock,
|
||||
GAME_CHAT_WORKFLOW_STRATEGY_AUDIT_EXISTING_FIRST,
|
||||
};
|
||||
use crate::{update_manifest_task_status_at, GameCreationAppTaskStatus};
|
||||
|
||||
use super::{
|
||||
agent_runtime_root_source_at, append_unique_game_creator_agent_runtime_pending_task,
|
||||
@@ -590,6 +668,7 @@ mod tests {
|
||||
start_game_creator_agent_runtime_task_at, AgentRuntimeGoalContractAcceptanceNodeDraft,
|
||||
AgentRuntimeGoalContractDraft, AgentRuntimeTaskLink, AgentRuntimeToolObservation,
|
||||
AgentRuntimeToolPlan, GameCreatorMcpCatalog, GameCreatorMcpCatalogTool,
|
||||
AGENT_RUNTIME_AUTONOMOUS_PRE_MUTATION_LOOP_LIMIT,
|
||||
AGENT_RUNTIME_COMPLETION_BLOCKER_TOOL_PLAN_PROTOCOL, AGENT_RUNTIME_RESPOND_FUNCTION_NAME,
|
||||
AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD, AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
|
||||
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE, AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
|
||||
@@ -672,7 +751,7 @@ mod tests {
|
||||
summary: "结构化计划更新被 Runtime 拒绝".to_string(),
|
||||
detail: Some("计划状态回退".to_string()),
|
||||
};
|
||||
let (_, _, request, _) = build_game_creator_agent_background_tool_plan_request(
|
||||
let (_, _, request, _, _) = build_game_creator_agent_background_tool_plan_request(
|
||||
&root,
|
||||
&state.agent_id,
|
||||
&state.session_id,
|
||||
@@ -783,7 +862,7 @@ mod tests {
|
||||
servers: Vec::new(),
|
||||
tools: Vec::new(),
|
||||
};
|
||||
let (_, _, request, _) = build_game_creator_agent_background_tool_plan_request(
|
||||
let (_, _, request, _, _) = build_game_creator_agent_background_tool_plan_request(
|
||||
&root,
|
||||
agent_id,
|
||||
&state.session_id,
|
||||
@@ -841,7 +920,7 @@ mod tests {
|
||||
servers: Vec::new(),
|
||||
tools: Vec::new(),
|
||||
};
|
||||
let (_, _, request, _) = build_game_creator_agent_background_tool_plan_request(
|
||||
let (_, _, request, _, _) = build_game_creator_agent_background_tool_plan_request(
|
||||
&root,
|
||||
&state.agent_id,
|
||||
&state.session_id,
|
||||
@@ -910,17 +989,18 @@ mod tests {
|
||||
vec!["核对预加载说明".to_string()],
|
||||
)
|
||||
.expect("start supervisor runtime state");
|
||||
let (_, _, supervisor_request, _) = build_game_creator_agent_background_tool_plan_request(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&supervisor_state.session_id,
|
||||
&supervisor_state.run_id,
|
||||
&supervisor_state.current_task,
|
||||
&[],
|
||||
0,
|
||||
&catalog,
|
||||
)
|
||||
.expect("build supervisor planning request");
|
||||
let (_, _, supervisor_request, _, _) =
|
||||
build_game_creator_agent_background_tool_plan_request(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&supervisor_state.session_id,
|
||||
&supervisor_state.run_id,
|
||||
&supervisor_state.current_task,
|
||||
&[],
|
||||
0,
|
||||
&catalog,
|
||||
)
|
||||
.expect("build supervisor planning request");
|
||||
let supervisor_system_prompt = &supervisor_request.messages[0].content;
|
||||
let supervisor_prompt = &supervisor_request.messages[1].content;
|
||||
let supervisor_identity_contract =
|
||||
@@ -1065,7 +1145,7 @@ mod tests {
|
||||
vec!["核对普通说明".to_string()],
|
||||
)
|
||||
.expect("start ordinary runtime state");
|
||||
let (_, _, ordinary_request, _) = build_game_creator_agent_background_tool_plan_request(
|
||||
let (_, _, ordinary_request, _, _) = build_game_creator_agent_background_tool_plan_request(
|
||||
&root,
|
||||
"code-prototype",
|
||||
&ordinary_state.session_id,
|
||||
@@ -1194,7 +1274,7 @@ mod tests {
|
||||
vec!["核对 MCP 调用协议".to_string()],
|
||||
)
|
||||
.expect("start runtime state");
|
||||
let (_, _, request, _) = build_game_creator_agent_background_tool_plan_request(
|
||||
let (_, _, request, _, _) = build_game_creator_agent_background_tool_plan_request(
|
||||
&root,
|
||||
"code-prototype",
|
||||
&state.session_id,
|
||||
@@ -1348,7 +1428,7 @@ mod tests {
|
||||
servers: Vec::new(),
|
||||
tools: Vec::new(),
|
||||
};
|
||||
let (_, _, request, _) = build_game_creator_agent_background_tool_plan_request(
|
||||
let (_, _, request, _, _) = build_game_creator_agent_background_tool_plan_request(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&state.session_id,
|
||||
@@ -1370,6 +1450,135 @@ mod tests {
|
||||
assert!(!user_prompt.contains("Graph reset 才保留"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supervisor_request_snapshot_preserves_prompt_visible_running_sibling_after_manifest_failure()
|
||||
{
|
||||
let temporary =
|
||||
crate::tests::canonical_test_tempdir("provider-running-sibling-after-failure-");
|
||||
let root = temporary.path().join("project");
|
||||
init_local_game_project_at(
|
||||
&root,
|
||||
"running-sibling-after-failure",
|
||||
"失败兄弟任务后的活跃观察绑定测试",
|
||||
)
|
||||
.expect("project init");
|
||||
let _config_guard = crate::tests::write_test_local_config("{}".to_string());
|
||||
let run_id = "provider-stale-manifest-observation-run";
|
||||
bind_game_creator_agent_runtime_run_profile_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
run_id,
|
||||
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
|
||||
Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD),
|
||||
None,
|
||||
)
|
||||
.expect("bind autonomous supervisor");
|
||||
let state = start_game_creator_agent_runtime_task_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
"生成一版可玩的游戏",
|
||||
run_id,
|
||||
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
|
||||
"等待专业任务图完成",
|
||||
vec!["读取专业任务图状态".to_string()],
|
||||
)
|
||||
.expect("start autonomous supervisor");
|
||||
for task_id in [
|
||||
"design-director",
|
||||
"design-foundation",
|
||||
"balance-director",
|
||||
"balance-seed",
|
||||
"art-director",
|
||||
"art-polish",
|
||||
"audio-director",
|
||||
] {
|
||||
update_manifest_task_status_at(&root, task_id, GameCreationAppTaskStatus::Completed)
|
||||
.expect("complete manifest fixture task");
|
||||
}
|
||||
update_manifest_task_status_at(&root, "art-asset-plan", GameCreationAppTaskStatus::Failed)
|
||||
.expect("fail manifest fixture task");
|
||||
update_manifest_task_status_at(
|
||||
&root,
|
||||
"audio-asset-plan",
|
||||
GameCreationAppTaskStatus::Running,
|
||||
)
|
||||
.expect("keep sibling manifest fixture task running");
|
||||
assert!(
|
||||
!autonomous_manifest_dag_in_progress_at(&root)
|
||||
.expect("read failure-closed live DAG predicate"),
|
||||
"the regression requires a failed task to close the live predicate while a sibling remains running"
|
||||
);
|
||||
let mut observations = vec![
|
||||
AgentRuntimeToolObservation {
|
||||
tool: "task.list".to_string(),
|
||||
status: "ok".to_string(),
|
||||
summary: "已读取 manifest 任务图".to_string(),
|
||||
detail: Some(
|
||||
"readyTaskIds: (none)\nseedTaskCounts: completed=7 running=1 pending=7 waiting=0 failed=1 total=16\ntaskCounts: completed=7 running=1 pending=7 waiting=0 failed=1 total=16"
|
||||
.to_string(),
|
||||
),
|
||||
},
|
||||
AgentRuntimeToolObservation {
|
||||
tool: "agent.run_status".to_string(),
|
||||
status: "ok".to_string(),
|
||||
summary: "已读取 2 个 Agent 状态".to_string(),
|
||||
detail: Some(
|
||||
"agentId: project-supervisor\nstatus: running\n\nagentId: code-prototype\nstatus: running"
|
||||
.to_string(),
|
||||
),
|
||||
},
|
||||
];
|
||||
let catalog = GameCreatorMcpCatalog {
|
||||
fingerprint: String::new(),
|
||||
servers: Vec::new(),
|
||||
tools: Vec::new(),
|
||||
};
|
||||
|
||||
let (_, _, request, _, request_snapshot) =
|
||||
build_game_creator_agent_background_tool_plan_request(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&state.session_id,
|
||||
&state.run_id,
|
||||
&state.current_task,
|
||||
&observations,
|
||||
AGENT_RUNTIME_AUTONOMOUS_PRE_MUTATION_LOOP_LIMIT + 1,
|
||||
&catalog,
|
||||
)
|
||||
.expect("build request from the provider-visible running sibling observation");
|
||||
|
||||
assert!(request.messages[1].content.contains(
|
||||
"seedTaskCounts: completed=7 running=1 pending=7 waiting=0 failed=1 total=16"
|
||||
));
|
||||
assert!(request_snapshot.supervisor_manifest_dag_in_progress);
|
||||
|
||||
observations.push(AgentRuntimeToolObservation {
|
||||
tool: "task.list".to_string(),
|
||||
status: "ok".to_string(),
|
||||
summary: "已读取 manifest 任务图".to_string(),
|
||||
detail: Some(
|
||||
"readyTaskIds: (none)\nseedTaskCounts: completed=15 running=0 pending=0 waiting=0 failed=1 total=16\ntaskCounts: completed=15 running=0 pending=0 waiting=0 failed=1 total=16"
|
||||
.to_string(),
|
||||
),
|
||||
});
|
||||
let (_, _, _, _, settled_request_snapshot) =
|
||||
build_game_creator_agent_background_tool_plan_request(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&state.session_id,
|
||||
&state.run_id,
|
||||
&state.current_task,
|
||||
&observations,
|
||||
AGENT_RUNTIME_AUTONOMOUS_PRE_MUTATION_LOOP_LIMIT + 2,
|
||||
&catalog,
|
||||
)
|
||||
.expect("build successor request from the terminal provider-visible observation");
|
||||
assert!(
|
||||
!settled_request_snapshot.supervisor_manifest_dag_in_progress,
|
||||
"a successor request must not inherit an older active observation"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn game_chat_code_prototype_receives_the_persisted_supervisor_workflow_authority() {
|
||||
let temporary = crate::tests::canonical_test_tempdir("provider-game-chat-authority-");
|
||||
@@ -1432,7 +1641,7 @@ mod tests {
|
||||
servers: Vec::new(),
|
||||
tools: Vec::new(),
|
||||
};
|
||||
let (_, _, request, _) = build_game_creator_agent_background_tool_plan_request(
|
||||
let (_, _, request, _, _) = build_game_creator_agent_background_tool_plan_request(
|
||||
&root,
|
||||
"code-prototype",
|
||||
&code_state.session_id,
|
||||
|
||||
+46
-7
@@ -174,12 +174,16 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
|
||||
let (run_profile, _) =
|
||||
agent_runtime_run_profile_identity_at(root, agent_id, run_id, None, None)?;
|
||||
let mcp_catalog = read_game_creator_mcp_catalog_at(root).await?;
|
||||
let mut built_request = {
|
||||
let (mut built_request, mut supervisor_manifest_dag_in_progress_at_request) = {
|
||||
let _lock = acquire_game_creator_agent_provider_plan_project_write_lock_with_wait(
|
||||
root,
|
||||
"runtime.provider_request.build.tool_plan",
|
||||
)?;
|
||||
build_game_creator_agent_background_tool_plan_request(
|
||||
let live_manifest_dag_in_progress_before = run_profile
|
||||
== AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
&& agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
||||
&& autonomous_manifest_dag_in_progress_at(root)?;
|
||||
let request = build_game_creator_agent_background_tool_plan_request(
|
||||
root,
|
||||
agent_id,
|
||||
session_id,
|
||||
@@ -188,7 +192,15 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
|
||||
observations,
|
||||
loop_index,
|
||||
&mcp_catalog,
|
||||
)?
|
||||
)?;
|
||||
let live_manifest_dag_in_progress_after = run_profile
|
||||
== AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
&& agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
||||
&& autonomous_manifest_dag_in_progress_at(root)?;
|
||||
let request_bound_manifest_dag_in_progress = live_manifest_dag_in_progress_before
|
||||
|| live_manifest_dag_in_progress_after
|
||||
|| request.4.supervisor_manifest_dag_in_progress;
|
||||
(request, request_bound_manifest_dag_in_progress)
|
||||
};
|
||||
let mut estimated_input_tokens = estimate_game_creator_llm_request_tokens(&built_request.2)?;
|
||||
let mut compaction = None;
|
||||
@@ -224,12 +236,19 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
|
||||
return Ok(RequestedAgentRuntimeToolPlanOutcome::Superseded);
|
||||
}
|
||||
}
|
||||
built_request = {
|
||||
(
|
||||
built_request,
|
||||
supervisor_manifest_dag_in_progress_at_request,
|
||||
) = {
|
||||
let _lock = acquire_game_creator_agent_provider_plan_project_write_lock_with_wait(
|
||||
root,
|
||||
"runtime.provider_request.rebuild.tool_plan",
|
||||
)?;
|
||||
build_game_creator_agent_background_tool_plan_request(
|
||||
let live_manifest_dag_in_progress_before = run_profile
|
||||
== AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
&& agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
||||
&& autonomous_manifest_dag_in_progress_at(root)?;
|
||||
let request = build_game_creator_agent_background_tool_plan_request(
|
||||
root,
|
||||
agent_id,
|
||||
session_id,
|
||||
@@ -238,7 +257,15 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
|
||||
observations,
|
||||
loop_index,
|
||||
&mcp_catalog,
|
||||
)?
|
||||
)?;
|
||||
let live_manifest_dag_in_progress_after = run_profile
|
||||
== AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
&& agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
||||
&& autonomous_manifest_dag_in_progress_at(root)?;
|
||||
let request_bound_manifest_dag_in_progress = live_manifest_dag_in_progress_before
|
||||
|| live_manifest_dag_in_progress_after
|
||||
|| request.4.supervisor_manifest_dag_in_progress;
|
||||
(request, request_bound_manifest_dag_in_progress)
|
||||
};
|
||||
estimated_input_tokens = estimate_game_creator_llm_request_tokens(&built_request.2)?;
|
||||
if estimated_input_tokens > built_request.0.auto_compact_token_limit {
|
||||
@@ -275,7 +302,7 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
|
||||
applied_steer_cursor,
|
||||
)?
|
||||
};
|
||||
let (llm, config_path, mut request, repository_context_fingerprint) = built_request;
|
||||
let (llm, config_path, mut request, repository_context_fingerprint, _) = built_request;
|
||||
let auto_compact_token_limit = llm.auto_compact_token_limit;
|
||||
let format_repair_attempts = if run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD {
|
||||
AGENT_RUNTIME_AUTONOMOUS_TOOL_PLAN_FORMAT_REPAIR_ATTEMPTS
|
||||
@@ -471,6 +498,17 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
|
||||
error,
|
||||
)
|
||||
})?;
|
||||
validate_agent_runtime_autonomous_art_director_canvas_only_plan(
|
||||
agent_id,
|
||||
task,
|
||||
&parsed.plan,
|
||||
)
|
||||
.map_err(|error| {
|
||||
AgentRuntimeToolPlanProtocolError::new(
|
||||
AgentRuntimeToolPlanProtocolErrorKind::PlanSemantics,
|
||||
error,
|
||||
)
|
||||
})?;
|
||||
if autonomous_scaffold_repair_active
|
||||
&& source_payload.max_field_chars
|
||||
> AGENT_RUNTIME_AUTONOMOUS_SCAFFOLD_SOURCE_MAX_CHARS
|
||||
@@ -534,6 +572,7 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
|
||||
observations,
|
||||
&parsed.plan,
|
||||
supervisor_requires_delegated_repair,
|
||||
supervisor_manifest_dag_in_progress_at_request && repair_attempt == 0,
|
||||
) {
|
||||
Ok(()) => Ok((parsed, source_payload)),
|
||||
Err(error) => Err(AgentRuntimeToolPlanProtocolError::new(
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ pub(in crate::agent) fn mark_supervisor_delivery_claims_observed_for_pending_act
|
||||
observed_delegate_receipt_ids_from_run_status(observation.detail.as_deref())?;
|
||||
let observed_isolated_groups =
|
||||
observed_isolated_join_group_ids_from_run_status(observation.detail.as_deref())?;
|
||||
if pending.agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
|
||||
if static_delegate_parent_can_manage_receipts_at(root, &pending.agent_id, &pending.run_id)? {
|
||||
mark_static_delegate_claim_observed_for_receipts_at(
|
||||
root,
|
||||
&pending.agent_id,
|
||||
|
||||
@@ -120,8 +120,9 @@ pub(crate) fn apply_agent_runtime_plan_update(
|
||||
update: &AgentRuntimePlanUpdate,
|
||||
) -> Result<bool, String> {
|
||||
let update = sanitize_agent_runtime_plan_update(update)?;
|
||||
let had_structured_plan = agent_runtime_has_structured_plan(runtime);
|
||||
let mut terminal_steps = std::collections::BTreeMap::new();
|
||||
if agent_runtime_has_structured_plan(runtime) {
|
||||
if had_structured_plan {
|
||||
for step in &runtime.plan_steps {
|
||||
if matches!(
|
||||
step.status.as_str(),
|
||||
@@ -155,10 +156,12 @@ pub(crate) fn apply_agent_runtime_plan_update(
|
||||
.plan_steps
|
||||
.iter()
|
||||
.filter(|step| {
|
||||
matches!(
|
||||
step.status.as_str(),
|
||||
AGENT_RUNTIME_PLAN_STATUS_COMPLETED | AGENT_RUNTIME_PLAN_STATUS_FAILED
|
||||
) && !incoming_titles.contains(step.title.as_str())
|
||||
had_structured_plan
|
||||
&& matches!(
|
||||
step.status.as_str(),
|
||||
AGENT_RUNTIME_PLAN_STATUS_COMPLETED | AGENT_RUNTIME_PLAN_STATUS_FAILED
|
||||
)
|
||||
&& !incoming_titles.contains(step.title.as_str())
|
||||
})
|
||||
.map(|step| (step.title.clone(), step.status.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
@@ -175,7 +178,7 @@ pub(crate) fn apply_agent_runtime_plan_update(
|
||||
));
|
||||
}
|
||||
|
||||
let unchanged = agent_runtime_has_structured_plan(runtime)
|
||||
let unchanged = had_structured_plan
|
||||
&& runtime.plan_explanation == update.explanation
|
||||
&& runtime.plan_steps.len() == merged.len()
|
||||
&& runtime
|
||||
@@ -190,11 +193,15 @@ pub(crate) fn apply_agent_runtime_plan_update(
|
||||
}
|
||||
|
||||
let now = unix_timestamp();
|
||||
let previous_steps = runtime
|
||||
.plan_steps
|
||||
.iter()
|
||||
.map(|step| (step.title.clone(), step.clone()))
|
||||
.collect::<std::collections::BTreeMap<_, _>>();
|
||||
let previous_steps = if had_structured_plan {
|
||||
runtime
|
||||
.plan_steps
|
||||
.iter()
|
||||
.map(|step| (step.title.clone(), step.clone()))
|
||||
.collect::<std::collections::BTreeMap<_, _>>()
|
||||
} else {
|
||||
std::collections::BTreeMap::new()
|
||||
};
|
||||
runtime.plan_steps = merged
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user