Merge branch 'master' into codex/rust-agent
This commit is contained in:
@@ -362,6 +362,10 @@ export interface AdminEditorAssetPayload {
|
||||
assetKind?: string | null;
|
||||
generationInputs?: Record<string, unknown> | null;
|
||||
sourceResourceId?: string | null;
|
||||
sourceImageSrc?: string | null;
|
||||
sourceObjectKey?: string | null;
|
||||
sourceAssetObjectId?: string | null;
|
||||
sourceLabel?: string | null;
|
||||
thumbnailSrc?: string | null;
|
||||
generationCostMudPoints: number;
|
||||
createdAt: string;
|
||||
@@ -404,6 +408,11 @@ export interface AdminEditorShowcaseAssetPayload {
|
||||
model?: string | null;
|
||||
provider?: string | null;
|
||||
taskId?: string | null;
|
||||
sourceResourceId?: string | null;
|
||||
sourceImageSrc?: string | null;
|
||||
sourceObjectKey?: string | null;
|
||||
sourceAssetObjectId?: string | null;
|
||||
sourceLabel?: string | null;
|
||||
assetKind?: string | null;
|
||||
generationInputs?: Record<string, unknown> | null;
|
||||
thumbnailSrc?: string | null;
|
||||
|
||||
@@ -25,6 +25,11 @@ export interface AdminPreviewableEditorAsset {
|
||||
thumbnailSrc?: string | null;
|
||||
imageSequenceFrames?: AdminEditorImageSequenceFramePayload[] | null;
|
||||
imageSequenceDurationMs?: number | null;
|
||||
sourceResourceId?: string | null;
|
||||
sourceImageSrc?: string | null;
|
||||
sourceObjectKey?: string | null;
|
||||
sourceAssetObjectId?: string | null;
|
||||
sourceLabel?: string | null;
|
||||
}
|
||||
|
||||
export function AdminEditorAssetThumbnail({
|
||||
@@ -70,6 +75,15 @@ export function AdminEditorAssetPreviewDialog({
|
||||
token: string;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const sourceEntry = entry.sourceImageSrc?.trim()
|
||||
? {
|
||||
assetId: entry.sourceResourceId ?? `${entry.assetId}-source`,
|
||||
label: entry.sourceLabel?.trim() || '原图',
|
||||
imageSrc: entry.sourceImageSrc,
|
||||
objectKey: entry.sourceObjectKey,
|
||||
}
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="admin-confirm-backdrop" role="presentation">
|
||||
<section
|
||||
@@ -91,13 +105,19 @@ export function AdminEditorAssetPreviewDialog({
|
||||
<X size={17} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
{sourceEntry ? (
|
||||
<div className="admin-asset-query-source-preview">
|
||||
<h4>原图</h4>
|
||||
<AdminEditorAssetPreviewMedia entry={sourceEntry} token={token} />
|
||||
</div>
|
||||
) : null}
|
||||
<AdminEditorAssetPreviewMedia entry={entry} token={token} />
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AdminEditorAssetPreviewMedia({
|
||||
export function AdminEditorAssetPreviewMedia({
|
||||
entry,
|
||||
token,
|
||||
}: {
|
||||
@@ -716,7 +736,7 @@ function isAdminImageSequenceFrameUrlUsable(
|
||||
): cached is AdminImageSequenceFrameCacheEntry {
|
||||
return Boolean(
|
||||
cached?.resolvedUrl &&
|
||||
(cached.expiresAtMs === null || cached.expiresAtMs > Date.now()),
|
||||
(cached.expiresAtMs === null || cached.expiresAtMs > Date.now()),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import type {
|
||||
} from '../api/adminApiTypes';
|
||||
import {
|
||||
AdminEditorAssetPreviewDialog,
|
||||
AdminEditorAssetPreviewMedia,
|
||||
AdminEditorAssetThumbnail,
|
||||
} from '../components/AdminEditorAssetMedia';
|
||||
import { AdminUserReferenceButton } from '../components/AdminUserReferenceButton';
|
||||
@@ -561,14 +562,34 @@ function AdminAssetDetailDialog({
|
||||
</button>
|
||||
</div>
|
||||
<div className="admin-asset-query-detail-layout">
|
||||
<button
|
||||
className="admin-asset-query-thumb-button admin-asset-query-detail-thumb-button"
|
||||
title="预览素材"
|
||||
type="button"
|
||||
onClick={() => onPreview(entry)}
|
||||
>
|
||||
<AdminEditorAssetThumbnail entry={entry} token={token} />
|
||||
</button>
|
||||
<div className="admin-asset-query-detail-media">
|
||||
{entry.sourceImageSrc?.trim() ? (
|
||||
<div className="admin-asset-query-detail-media-card">
|
||||
<h4>原图</h4>
|
||||
<AdminEditorAssetPreviewMedia
|
||||
entry={{
|
||||
assetId:
|
||||
entry.sourceResourceId ?? `${entry.assetId}-source`,
|
||||
label: entry.sourceLabel?.trim() || '原图',
|
||||
imageSrc: entry.sourceImageSrc,
|
||||
objectKey: entry.sourceObjectKey,
|
||||
}}
|
||||
token={token}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="admin-asset-query-detail-media-card">
|
||||
<h4>当前结果</h4>
|
||||
<button
|
||||
className="admin-asset-query-thumb-button admin-asset-query-detail-thumb-button"
|
||||
title="预览素材"
|
||||
type="button"
|
||||
onClick={() => onPreview(entry)}
|
||||
>
|
||||
<AdminEditorAssetThumbnail entry={entry} token={token} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<dl className="admin-info-list admin-detail-list">
|
||||
<AdminInfoItem label="作者">
|
||||
<div className="admin-inline-identity">
|
||||
|
||||
@@ -17,6 +17,7 @@ import type {
|
||||
} from '../api/adminApiTypes';
|
||||
import {
|
||||
AdminEditorAssetPreviewDialog,
|
||||
AdminEditorAssetPreviewMedia,
|
||||
AdminEditorAssetThumbnail,
|
||||
} from '../components/AdminEditorAssetMedia';
|
||||
import { AdminUserReferenceButton } from '../components/AdminUserReferenceButton';
|
||||
@@ -709,18 +710,38 @@ function AdminShowcaseDetailDialog({
|
||||
</button>
|
||||
</div>
|
||||
<div className="admin-asset-query-detail-layout">
|
||||
<button
|
||||
className="admin-asset-query-thumb-button admin-asset-query-detail-thumb-button"
|
||||
title="预览素材"
|
||||
type="button"
|
||||
onClick={() => onPreview(entry)}
|
||||
>
|
||||
<AdminEditorAssetThumbnail
|
||||
entry={entry}
|
||||
token={token}
|
||||
altPrefix="精选素材"
|
||||
/>
|
||||
</button>
|
||||
<div className="admin-asset-query-detail-media">
|
||||
{entry.sourceImageSrc?.trim() ? (
|
||||
<div className="admin-asset-query-detail-media-card">
|
||||
<h4>原图</h4>
|
||||
<AdminEditorAssetPreviewMedia
|
||||
entry={{
|
||||
assetId:
|
||||
entry.sourceResourceId ?? `${entry.assetId}-source`,
|
||||
label: entry.sourceLabel?.trim() || '原图',
|
||||
imageSrc: entry.sourceImageSrc,
|
||||
objectKey: entry.sourceObjectKey,
|
||||
}}
|
||||
token={token}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="admin-asset-query-detail-media-card">
|
||||
<h4>当前结果</h4>
|
||||
<button
|
||||
className="admin-asset-query-thumb-button admin-asset-query-detail-thumb-button"
|
||||
title="预览素材"
|
||||
type="button"
|
||||
onClick={() => onPreview(entry)}
|
||||
>
|
||||
<AdminEditorAssetThumbnail
|
||||
entry={entry}
|
||||
token={token}
|
||||
altPrefix="精选素材"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<dl className="admin-info-list admin-detail-list">
|
||||
<AdminInfoItem label="作者">
|
||||
<div className="admin-inline-identity">
|
||||
|
||||
@@ -1655,6 +1655,18 @@ button:disabled {
|
||||
width: min(100%, 860px);
|
||||
}
|
||||
|
||||
.admin-asset-query-source-preview {
|
||||
margin: 0 20px 16px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.admin-asset-query-source-preview h4 {
|
||||
margin: 0 0 8px;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.admin-asset-query-prompt-dialog .admin-panel-heading > div,
|
||||
.admin-asset-query-detail-dialog .admin-panel-heading > div,
|
||||
.admin-asset-query-preview-dialog .admin-panel-heading > div {
|
||||
@@ -1683,6 +1695,27 @@ button:disabled {
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.admin-asset-query-detail-media {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.admin-asset-query-detail-media-card {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.admin-asset-query-detail-media-card h4 {
|
||||
margin: 0;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.admin-asset-query-detail-media-card .admin-asset-query-preview-media {
|
||||
width: 220px;
|
||||
height: 220px;
|
||||
}
|
||||
|
||||
.admin-asset-query-detail-thumb-button .admin-asset-query-thumb {
|
||||
width: 220px;
|
||||
height: 220px;
|
||||
@@ -2332,6 +2365,10 @@ button:disabled {
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
.admin-asset-query-detail-media {
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.admin-dashboard-tabs {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@genarrative/ai-game-creator-shell",
|
||||
"private": true,
|
||||
"version": "0.1.10",
|
||||
"version": "0.1.12",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "node scripts/start-tauri-dev.mjs",
|
||||
|
||||
@@ -26,6 +26,11 @@ const wrapperSuite =
|
||||
const configFileName = 'game-creator.config.json';
|
||||
const configSentinelName = '.deterministic-provider-e2e.json';
|
||||
const configSentinelSchema = 'genarrative-deterministic-provider-e2e-config.v1';
|
||||
const platformSessionFixtureEnv =
|
||||
'GENARRATIVE_AGC_PLATFORM_SESSION_FIXTURE';
|
||||
const platformSessionFixtureName = '.deterministic-platform-session.json';
|
||||
const platformSessionFixtureSchema =
|
||||
'genarrative-agc-platform-session-fixture.v1';
|
||||
const outputLimit = 32 * 1024 * 1024;
|
||||
|
||||
function hashValue(value) {
|
||||
@@ -77,6 +82,16 @@ function deterministicRuntimeConfig(apiKey, provider) {
|
||||
};
|
||||
}
|
||||
|
||||
function deterministicPlatformSessionFixture(apiKey, provider) {
|
||||
return {
|
||||
schemaVersion: platformSessionFixtureSchema,
|
||||
userId: 'deterministic-e2e-user',
|
||||
accessToken: apiKey,
|
||||
apiBaseUrl: provider.editorBaseUrl,
|
||||
generation: 1,
|
||||
};
|
||||
}
|
||||
|
||||
function appendBounded(current, chunk) {
|
||||
const combined = Buffer.concat([current, chunk]);
|
||||
if (combined.length > outputLimit) throw new Error('child-output-too-large');
|
||||
@@ -1909,6 +1924,12 @@ async function runE2e(options) {
|
||||
);
|
||||
provider = await startDeterministicLaneDefenseProvider({ apiKey });
|
||||
const config = deterministicRuntimeConfig(apiKey, provider);
|
||||
const fixturePath = path.join(configDir, platformSessionFixtureName);
|
||||
await fs.writeFile(
|
||||
fixturePath,
|
||||
`${JSON.stringify(deterministicPlatformSessionFixture(apiKey, provider))}\n`,
|
||||
{ flag: 'wx', mode: 0o600 },
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(configDir, configFileName),
|
||||
`${JSON.stringify(config)}\n`,
|
||||
@@ -1924,7 +1945,11 @@ async function runE2e(options) {
|
||||
if (options.keepProject) childArgs.push('--keep-project');
|
||||
childResult = await runChild(
|
||||
childArgs,
|
||||
withLoopbackNoProxy({ ...process.env, NO_COLOR: '1' }),
|
||||
withLoopbackNoProxy({
|
||||
...process.env,
|
||||
NO_COLOR: '1',
|
||||
[platformSessionFixtureEnv]: fixturePath,
|
||||
}),
|
||||
);
|
||||
childReport = parseChildReport(childResult);
|
||||
} catch (error) {
|
||||
|
||||
@@ -100,6 +100,14 @@ import { appendBounded, runProcess } from './process.mjs';
|
||||
import { decodeUtf8Fatal, isIsolatedRunnerSuite } from './reporting.mjs';
|
||||
import { killRunnerOnce, readRunnerStatus, runnerBootId } from './runtime.mjs';
|
||||
|
||||
const platformSessionFixtureEnv =
|
||||
'GENARRATIVE_AGC_PLATFORM_SESSION_FIXTURE';
|
||||
const platformSessionFixtureMaxBytes = 16 * 1024;
|
||||
const isolatedPlatformSessionFixtureName =
|
||||
'.deterministic-platform-session.json';
|
||||
const platformSessionFixtureSchema =
|
||||
'genarrative-agc-platform-session-fixture.v1';
|
||||
|
||||
export function isolatedSuiteProtectsSourceAppData() {
|
||||
return (
|
||||
isSupervisorSwarmTransientRetrySuite() ||
|
||||
@@ -498,6 +506,116 @@ export async function verifySourceAppDataDirectoryUntouched() {
|
||||
state.isolatedRunner.sourceAppDataDirectoryUntouched = true;
|
||||
}
|
||||
|
||||
async function readPlatformSessionFixtureForIsolatedSuite(sourceConfigDir) {
|
||||
const rawPath = process.env[platformSessionFixtureEnv];
|
||||
assert(
|
||||
isNonEmptyString(rawPath) && path.isAbsolute(rawPath),
|
||||
'supervisor-autonomous-playable-platform-session-fixture-missing',
|
||||
);
|
||||
const sourceRealPath = await fs.realpath(sourceConfigDir);
|
||||
const requestedPath = path.resolve(rawPath);
|
||||
const requestedMetadata = await fs.lstat(requestedPath).catch((error) => {
|
||||
if (error?.code === 'ENOENT') return null;
|
||||
throw error;
|
||||
});
|
||||
assert(
|
||||
requestedMetadata?.isFile() && !requestedMetadata.isSymbolicLink(),
|
||||
'supervisor-autonomous-playable-platform-session-fixture-not-regular',
|
||||
);
|
||||
assert(
|
||||
requestedMetadata.size <= platformSessionFixtureMaxBytes,
|
||||
'supervisor-autonomous-playable-platform-session-fixture-too-large',
|
||||
);
|
||||
const realPath = await fs.realpath(requestedPath);
|
||||
assert(
|
||||
isPathInside(sourceRealPath, realPath),
|
||||
'supervisor-autonomous-playable-platform-session-fixture-outside-config',
|
||||
);
|
||||
const bytes = await fs.readFile(realPath);
|
||||
assert(
|
||||
bytes.length <= platformSessionFixtureMaxBytes,
|
||||
'supervisor-autonomous-playable-platform-session-fixture-too-large',
|
||||
);
|
||||
let fixture;
|
||||
try {
|
||||
fixture = JSON.parse(decodeUtf8Fatal(bytes, 'platform-session-fixture-invalid-utf8'));
|
||||
} catch (error) {
|
||||
throw codedError(
|
||||
'supervisor-autonomous-playable-platform-session-fixture-invalid',
|
||||
error,
|
||||
);
|
||||
}
|
||||
const expectedKeys = [
|
||||
'schemaVersion',
|
||||
'userId',
|
||||
'accessToken',
|
||||
'apiBaseUrl',
|
||||
'generation',
|
||||
];
|
||||
assert(
|
||||
isPlainObject(fixture) &&
|
||||
JSON.stringify(Object.keys(fixture).sort()) ===
|
||||
JSON.stringify([...expectedKeys].sort()) &&
|
||||
fixture.schemaVersion === platformSessionFixtureSchema &&
|
||||
isNonEmptyString(fixture.userId) &&
|
||||
isNonEmptyString(fixture.accessToken) &&
|
||||
isNonEmptyString(fixture.apiBaseUrl) &&
|
||||
Number.isSafeInteger(fixture.generation) &&
|
||||
fixture.generation > 0,
|
||||
'supervisor-autonomous-playable-platform-session-fixture-invalid',
|
||||
);
|
||||
return {
|
||||
sourcePath: realPath,
|
||||
bytes,
|
||||
fixture,
|
||||
sha256: createHash('sha256').update(bytes).digest('hex'),
|
||||
};
|
||||
}
|
||||
|
||||
async function installPlatformSessionFixtureIntoIsolatedAppData(
|
||||
sourceConfigDir,
|
||||
appDataDir,
|
||||
) {
|
||||
if (!isSupervisorAutonomousPlayableLaneDefenseSuite()) return;
|
||||
const source = await readPlatformSessionFixtureForIsolatedSuite(sourceConfigDir);
|
||||
const isolatedPath = path.join(appDataDir, isolatedPlatformSessionFixtureName);
|
||||
await fs.copyFile(
|
||||
source.sourcePath,
|
||||
isolatedPath,
|
||||
fsConstants.COPYFILE_EXCL | fsConstants.COPYFILE_FICLONE,
|
||||
);
|
||||
await fs.chmod(isolatedPath, 0o600).catch(() => {});
|
||||
const isolatedMetadata = await fs.lstat(isolatedPath);
|
||||
assert(
|
||||
isolatedMetadata.isFile() &&
|
||||
!isolatedMetadata.isSymbolicLink() &&
|
||||
isolatedMetadata.size === source.bytes.length,
|
||||
'supervisor-autonomous-playable-platform-session-fixture-copy-invalid',
|
||||
);
|
||||
const isolatedBytes = await fs.readFile(isolatedPath);
|
||||
assert(
|
||||
createHash('sha256').update(isolatedBytes).digest('hex') === source.sha256,
|
||||
'supervisor-autonomous-playable-platform-session-fixture-copy-mismatch',
|
||||
);
|
||||
state.isolatedRunner.platformSessionFixtureSourcePath = source.sourcePath;
|
||||
state.isolatedRunner.platformSessionFixturePath = isolatedPath;
|
||||
state.isolatedRunner.platformSessionFixtureSha256 = source.sha256;
|
||||
state.isolatedRunner.platformSessionFixturePreviousEnv =
|
||||
Object.prototype.hasOwnProperty.call(process.env, platformSessionFixtureEnv)
|
||||
? process.env[platformSessionFixtureEnv]
|
||||
: undefined;
|
||||
process.env[platformSessionFixtureEnv] = isolatedPath;
|
||||
state.formalConfigPathTranscriptScanner?.addSecrets(
|
||||
absolutePathVariants(source.sourcePath, isolatedPath),
|
||||
);
|
||||
const previousLeakCount = state.transcriptScanner?.count ?? 0;
|
||||
state.secrets = [
|
||||
...new Set([...state.secrets, source.fixture.accessToken]),
|
||||
];
|
||||
rebuildSupervisorSwarmTranscriptScanner();
|
||||
state.transcriptScanner.count = previousLeakCount;
|
||||
}
|
||||
|
||||
export async function prepareIsolatedSuiteAppData({
|
||||
streamAgentId = null,
|
||||
webSearchAgentId = null,
|
||||
@@ -769,6 +887,10 @@ export async function prepareIsolatedSuiteAppData({
|
||||
state.secrets = [...suiteSecrets];
|
||||
rebuildSupervisorSwarmTranscriptScanner();
|
||||
state.transcriptScanner.count = previousLeakCount;
|
||||
await installPlatformSessionFixtureIntoIsolatedAppData(
|
||||
sourceConfigDir,
|
||||
appDataDir,
|
||||
);
|
||||
const unexpectedEndpoint = await fs
|
||||
.lstat(path.join(appDataDir, runnerEndpointFileName))
|
||||
.catch((error) => {
|
||||
@@ -1874,6 +1996,31 @@ export async function verifyIsolatedSuiteConfigLinksUnchanged() {
|
||||
'isolated-source-config-changed-during-suite',
|
||||
);
|
||||
}
|
||||
const fixture = state.isolatedRunner;
|
||||
if (
|
||||
fixture.platformSessionFixtureSourcePath &&
|
||||
fixture.platformSessionFixturePath &&
|
||||
fixture.platformSessionFixtureSha256
|
||||
) {
|
||||
const [sourceMetadata, isolatedMetadata, sourceBytes, isolatedBytes] =
|
||||
await Promise.all([
|
||||
fs.lstat(fixture.platformSessionFixtureSourcePath),
|
||||
fs.lstat(fixture.platformSessionFixturePath),
|
||||
fs.readFile(fixture.platformSessionFixtureSourcePath),
|
||||
fs.readFile(fixture.platformSessionFixturePath),
|
||||
]);
|
||||
assert(
|
||||
sourceMetadata.isFile() &&
|
||||
!sourceMetadata.isSymbolicLink() &&
|
||||
isolatedMetadata.isFile() &&
|
||||
!isolatedMetadata.isSymbolicLink() &&
|
||||
createHash('sha256').update(sourceBytes).digest('hex') ===
|
||||
fixture.platformSessionFixtureSha256 &&
|
||||
createHash('sha256').update(isolatedBytes).digest('hex') ===
|
||||
fixture.platformSessionFixtureSha256,
|
||||
'supervisor-autonomous-playable-platform-session-fixture-changed',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function verifySourceConfigLinkCountsRestored() {
|
||||
@@ -1913,7 +2060,22 @@ export async function removeIsolatedSuiteAppData() {
|
||||
} catch (error) {
|
||||
ownershipError = error;
|
||||
}
|
||||
await fs.rm(appDataDir, { recursive: true, force: false });
|
||||
try {
|
||||
await fs.rm(appDataDir, { recursive: true, force: false });
|
||||
} finally {
|
||||
if (state.isolatedRunner.platformSessionFixtureSourcePath) {
|
||||
const previous = state.isolatedRunner.platformSessionFixturePreviousEnv;
|
||||
if (previous === undefined) {
|
||||
delete process.env[platformSessionFixtureEnv];
|
||||
} else {
|
||||
process.env[platformSessionFixtureEnv] = previous;
|
||||
}
|
||||
}
|
||||
state.isolatedRunner.platformSessionFixturePath = null;
|
||||
state.isolatedRunner.platformSessionFixtureSourcePath = null;
|
||||
state.isolatedRunner.platformSessionFixtureSha256 = null;
|
||||
state.isolatedRunner.platformSessionFixturePreviousEnv = undefined;
|
||||
}
|
||||
state.runtimeConfigDir = state.options.configDir;
|
||||
try {
|
||||
await verifySourceConfigLinkCountsRestored();
|
||||
|
||||
@@ -84,6 +84,14 @@ export function buildCliChildEnvironment() {
|
||||
NO_COLOR: '1',
|
||||
RUST_BACKTRACE: '0',
|
||||
};
|
||||
// The deterministic playable suite copies its account fixture into the
|
||||
// sibling isolated AppData directory. Set the path explicitly here so
|
||||
// every CLI and the Runner it launches use the isolated copy, even if the
|
||||
// parent harness environment was restored or changed after setup.
|
||||
if (state.isolatedRunner.platformSessionFixturePath) {
|
||||
environment.GENARRATIVE_AGC_PLATFORM_SESSION_FIXTURE =
|
||||
state.isolatedRunner.platformSessionFixturePath;
|
||||
}
|
||||
return isSupervisorSwarmTransientRetrySuite()
|
||||
? withLoopbackNoProxy(environment)
|
||||
: environment;
|
||||
|
||||
@@ -241,7 +241,7 @@ export const autonomousCompletionContractSchemaVersion =
|
||||
'game-creator-autonomous-completion-contract.v2';
|
||||
|
||||
export const autonomousPlaytestReceiptSchemaVersion =
|
||||
'game-creator-autonomous-playtest-receipt.v1';
|
||||
'game-creator-autonomous-playtest-receipt.v2';
|
||||
|
||||
export const autonomousGameBuildRunProfile = 'autonomous-game-build';
|
||||
|
||||
@@ -938,6 +938,10 @@ export class BlockedError extends Error {
|
||||
|
||||
export const isolatedRunnerState = {
|
||||
appDataDir: null,
|
||||
platformSessionFixturePath: null,
|
||||
platformSessionFixtureSourcePath: null,
|
||||
platformSessionFixtureSha256: null,
|
||||
platformSessionFixturePreviousEnv: undefined,
|
||||
ownerToken: null,
|
||||
createdAt: 0,
|
||||
current: null,
|
||||
|
||||
+5
@@ -1220,6 +1220,11 @@ export async function validateSupervisorAutonomousPlayableEvidence(
|
||||
agentId: receipt.agentId,
|
||||
runId: receipt.runId,
|
||||
runProfileBindingFingerprint: receipt.runProfileBindingFingerprint,
|
||||
executorAgentId: receipt.executorAgentId,
|
||||
executorRunId: receipt.executorRunId,
|
||||
executorSource: receipt.executorSource,
|
||||
executorRunProfileBindingFingerprint:
|
||||
receipt.executorRunProfileBindingFingerprint,
|
||||
actionId: receipt.actionId,
|
||||
actionFingerprint: receipt.actionFingerprint,
|
||||
revision: receipt.revision,
|
||||
|
||||
@@ -1533,12 +1533,12 @@ if (
|
||||
}
|
||||
|
||||
if (
|
||||
tauriConfig.version !== '0.1.10' ||
|
||||
packageConfig.version !== '0.1.10' ||
|
||||
cargoPackageVersion !== '0.1.10'
|
||||
tauriConfig.version !== '0.1.12' ||
|
||||
packageConfig.version !== '0.1.12' ||
|
||||
cargoPackageVersion !== '0.1.12'
|
||||
) {
|
||||
throw new Error(
|
||||
'AI game creator standard release must remain version 0.1.10',
|
||||
'AI game creator standard release must remain version 0.1.12',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -1703,7 +1703,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "genarrative-ai-game-creator-shell"
|
||||
version = "0.1.10"
|
||||
version = "0.1.12"
|
||||
dependencies = [
|
||||
"agent-runtime-core",
|
||||
"axum",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "genarrative-ai-game-creator-shell"
|
||||
version = "0.1.10"
|
||||
version = "0.1.12"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ const MAX_DIRECT_HOME_ATTACHMENT_MEDIA_TYPE_CHARS: usize = 96;
|
||||
const MIN_DIRECT_CLIENT_TURN_ID_CHARS: usize = 6;
|
||||
const MAX_DIRECT_CLIENT_TURN_ID_CHARS: usize = 160;
|
||||
const DIRECT_TAONIER_IDENTITY_GUIDANCE: &str = "对外身份合同:你是“陶泥儿”,是 Genarrative 的游戏创作助手。用户询问你是谁、你的名称或能力时,以陶泥儿的身份回答;不要把 Codex、ChatGPT、OpenAI、模型、通用 AI 助手或内部执行智能体当作自己的名称或对外身份。Codex app-server 仅是客户端内部执行技术;只有用户明确询问底层实现时才可如实说明,同时仍以陶泥儿自称。";
|
||||
const DIRECT_AGC_ENGINEERING_GUIDANCE: &str = "AGC 工程合同:当前 Codex cwd 就是用户选择的整个项目目录(工作区根),游戏源码、素材、音效和资源全部直接放在该根下;原生文件工具、原生 patch 和命令参数中的文件路径必须相对于当前 cwd:合法写法是 `index.html`、`style.css`、`game.js`、`assets/hero.png`,禁止写 `../`、项目根绝对路径或任何其它父目录路径;`game/...` 仅用于兼容旧项目结构,不是当前 cwd 的强制布局。`.agent/`、`.git/`、密钥文件和 Runtime 控制面由客户端维护,不能请求扩权或直接改写。DirectProject 提供 Codex 原生文件、搜索、命令、图片查看、Skill,以及经审核的 `agc_tools` MCP;普通单张图片、角色图、视觉规范图、UI 设计图和发布宣传图使用 `agc_tools.agc_generate_image`,已有图片修改使用 `agc_tools.agc_edit_image`,完整游戏美术包和 canonical 切片才使用 `agc_tools.taonier_prepare_game_art`,视频、角色动画、音效、背景音乐、浏览器试玩、资源登记和受控联网搜索等带 AGC 账本的动作也使用 `agc_tools`。按用户意图自行选择并执行,不要把普通图片误报成只能生成美术包,也不要等待 Supervisor、harness 或宿主规划器。不要读取或输出凭据、Token、Cookie、auth.json、.env 或宿主私密路径。项目锁、付费提交、幂等键、下载校验和客户端投影仍由客户端确定性掌管。游戏文件真实变化后由客户端登记资源和版本,Codex 不直接保存或伪造项目版本。";
|
||||
const DIRECT_AGC_ENGINEERING_GUIDANCE: &str = "AGC 工程合同(仅说明项目边界,不是流程门槛):当前 Codex cwd 是用户选择的项目目录(工作区根),源码、素材、音效和其它资源按项目现有结构放置;先按需读取当前 cwd 下适用的 `AGENTS.md`、README 或项目说明,把它们当作项目规范参考。原生文件工具、patch 和命令参数使用 cwd 相对路径,例如 `index.html`、`style.css`、`game.js`、`assets/hero.png`;如果 Codex 原生文件修改不可用,可以按需用客户端 `agc_write_file` 把文本写入项目相对路径。调用 `agc_write_file` 时,content 必须是目标文件的完整原始 UTF-8 正文;不得把 command.exec 的 Exit code、Wall time、Output 包装、终端日志或解释文字一起复制进 content,命令结果只能用于判断,不能当作文件正文。`../`、绝对路径、`.agent/`、`.git/`、密钥文件和 Runtime 控制面属于客户端边界,不能请求扩权或直接改写。DirectProject 提供 Codex 原生文件、搜索、命令、图片查看、Skill,以及经审核的 `agc_tools` MCP。你可以按需选择这些能力:`agc_write_file` 写入代码、配置、资源依赖清单或说明文件;`agc_generate_image` 生成普通图片、角色图、视觉规范图(icon-spec)、UI 设计图或发布宣传图;`agc_edit_image` 修改已登记图片;`taonier_prepare_game_art` 准备完整游戏美术包及可用的 canonical 切片;`agc_list_registered_assets`、`agc_list_project_files`、`agc_list_account_assets`、`agc_import_account_assets` 用于发现和接入资源依赖;`agc_create_or_derive_resource` 用于视频、角色动画、音效或背景音乐;`agc_browser_playtest` 用于需要时的本地试玩观察;`agc_read_skill_resource` 用于按需读取审核 Skill。切图、资源依赖、规范图和试玩都只是可选工具提示,不要求调用、固定顺序或特定产物,AGC 不会据此替你拆任务、编排 DAG、做强验收或阻止继续执行;不要等待 Supervisor、harness 或宿主规划器。不要读取或输出凭据、Token、Cookie、auth.json、.env 或宿主私密路径;项目锁、付费提交、幂等键、下载校验和客户端投影由客户端处理。游戏文件真实变化后客户端可登记资源和版本,Codex 不直接保存或伪造项目版本。";
|
||||
const DIRECT_CODEX_ART_SPEC_ASSET_PATH: &str = "assets/art-spec.png";
|
||||
const DIRECT_CODEX_BACKGROUND_ASSET_PATH: &str = "assets/direct-game-background.png";
|
||||
const DIRECT_CODEX_SPRITESHEET_ASSET_PATH: &str = "assets/art-spritesheet.png";
|
||||
@@ -3840,6 +3840,12 @@ pub(crate) async fn run_direct_game_creator_turn_at(
|
||||
root: &Path,
|
||||
prompt: &str,
|
||||
) -> Result<String, String> {
|
||||
// The CLI entry point does not receive the GUI's clientTurnId. Still arm
|
||||
// one invocation identity so an otherwise optional AGC generation tool
|
||||
// cannot fail merely because the request came through the CLI. This is
|
||||
// local execution identity only; it does not create a Runtime task or DAG.
|
||||
let invocation_id = format!("direct-cli-{}", unix_millis());
|
||||
let _invocation = DirectTaonierActiveInvocationGuard::enter(root, &invocation_id)?;
|
||||
run_direct_game_creator_turn_at_with_creation_type(root, prompt, None).await
|
||||
}
|
||||
|
||||
@@ -4398,9 +4404,13 @@ mod tests {
|
||||
assert!(!prompt.contains("你是 Codex"));
|
||||
assert!(prompt.contains("不要等待 Supervisor"));
|
||||
assert!(prompt.contains("提示词与技能"));
|
||||
assert!(prompt.contains("合法写法是 `index.html`、`style.css`、`game.js`"));
|
||||
assert!(prompt.contains("用户选择的整个项目目录"));
|
||||
assert!(prompt.contains("禁止写 `../`"));
|
||||
assert!(prompt.contains("AGC 工程合同(仅说明项目边界,不是流程门槛)"));
|
||||
assert!(prompt.contains("先按需读取当前 cwd 下适用的 `AGENTS.md`"));
|
||||
assert!(prompt.contains("agc_write_file"));
|
||||
assert!(prompt.contains("content 必须是目标文件的完整原始 UTF-8 正文"));
|
||||
assert!(prompt.contains("不得把 command.exec 的 Exit code、Wall time、Output 包装"));
|
||||
assert!(prompt.contains("切图、资源依赖、规范图和试玩都只是可选工具提示"));
|
||||
assert!(prompt.contains("不要求调用、固定顺序或特定产物"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -4562,6 +4572,7 @@ mod tests {
|
||||
let root = tempfile::tempdir().expect("temp dir");
|
||||
let prompt =
|
||||
build_direct_codex_system_prompt(root.path()).expect("build direct system prompt");
|
||||
assert!(prompt.contains("agc_write_file"));
|
||||
assert!(prompt.contains("agc_tools.taonier_prepare_game_art"));
|
||||
assert!(prompt.contains("agc_tools.agc_generate_image"));
|
||||
assert!(prompt.contains("agc_tools.agc_browser_playtest"));
|
||||
@@ -4569,6 +4580,7 @@ mod tests {
|
||||
assert!(!prompt.contains("客户端会在系统上下文提供有界的当前游戏文件快照"));
|
||||
assert!(prompt.contains("Codex 不直接保存或伪造项目版本"));
|
||||
assert!(prompt.contains("普通对话直接回答且不触碰工作区"));
|
||||
assert!(prompt.contains("切图、资源依赖、规范图和试玩都只是可选工具提示"));
|
||||
assert!(prompt.contains("用户不需要、也不得向你提供、配置、粘贴或创建 API Key"));
|
||||
assert!(prompt.contains("工具返回 401/403 时,只说明 AGC 客户端登录或权限状态异常并停止"));
|
||||
assert!(!prompt.contains("Use real platform assets only"));
|
||||
|
||||
@@ -13,7 +13,8 @@ use unicode_normalization::UnicodeNormalization;
|
||||
pub(crate) const DIRECT_TOOL_BRIDGE_PROTOCOL: &str = "genarrative-agc-tool-bridge.v1";
|
||||
pub(crate) const DIRECT_TOOL_BRIDGE_URL_ENV: &str = "GENARRATIVE_AGC_TOOL_BRIDGE_URL";
|
||||
|
||||
const DIRECT_TOOL_BRIDGE_MAX_REQUEST_BYTES: usize = 16 * 1024;
|
||||
const DIRECT_TOOL_BRIDGE_MAX_REQUEST_BYTES: usize = 2 * 1024 * 1024;
|
||||
const DIRECT_TOOL_BRIDGE_MAX_WRITE_CONTENT_BYTES: usize = 1_500_000;
|
||||
const DIRECT_TOOL_BRIDGE_MAX_ART_BRIEF_CHARS: usize = 4_000;
|
||||
const DIRECT_TOOL_BRIDGE_MAX_IMAGE_PROMPT_CHARS: usize = 32_000;
|
||||
const DIRECT_TOOL_BRIDGE_MAX_IMAGE_BYTES: u64 = 6 * 1024 * 1024;
|
||||
@@ -28,6 +29,23 @@ const DIRECT_TOOL_BRIDGE_MAX_RESOURCE_CALLS_PER_TURN: usize = 4;
|
||||
const DIRECT_TOOL_BRIDGE_MAX_ACCOUNT_ASSET_ID_CHARS: usize = 512;
|
||||
const DIRECT_TOOL_BRIDGE_MAX_LOCAL_ASSET_PATH_CHARS: usize = 512;
|
||||
|
||||
pub(crate) fn reject_command_output_wrapper(content: &str) -> Result<(), String> {
|
||||
let mut lines = content.trim_start_matches('\u{feff}').lines();
|
||||
let exit_line = lines.next().map(str::trim).unwrap_or_default();
|
||||
let wall_time_line = lines.next().map(str::trim).unwrap_or_default();
|
||||
let output_line = lines.next().map(str::trim).unwrap_or_default();
|
||||
if exit_line.starts_with("Exit code:")
|
||||
&& wall_time_line.starts_with("Wall time:")
|
||||
&& output_line.eq_ignore_ascii_case("Output:")
|
||||
{
|
||||
return Err(
|
||||
"工具参数 content 不能包含 command.exec 的 Exit code/Wall time/Output 包装;请只传原始 UTF-8 文件正文"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct DirectToolBridgeState {
|
||||
root: PathBuf,
|
||||
turn_authorization: StdMutex<DirectToolBridgeTurnAuthorization>,
|
||||
@@ -1391,6 +1409,55 @@ fn bridge_list_project_files(root: &Path, arguments: &Value) -> Value {
|
||||
}
|
||||
}
|
||||
|
||||
fn bridge_write_file(root: &Path, arguments: &Value) -> Value {
|
||||
let result = (|| {
|
||||
bridge_reject_unknown_fields(arguments, &["path", "content"])?;
|
||||
enforce_project_permission_policy(root, "file.write")?;
|
||||
let raw_path = bridge_bounded_string(
|
||||
arguments,
|
||||
"path",
|
||||
DIRECT_TOOL_BRIDGE_MAX_LOCAL_ASSET_PATH_CHARS,
|
||||
)?;
|
||||
let path = normalize_relative_path(&raw_path)?;
|
||||
if bridge_project_file_is_hidden_control_path(&path)
|
||||
|| reject_sensitive_project_file_read(&path).is_err()
|
||||
{
|
||||
return Err("工具参数 path 不得访问受保护项目控制面".to_string());
|
||||
}
|
||||
let content = arguments
|
||||
.get("content")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or_else(|| "工具参数 content 必须是字符串".to_string())?;
|
||||
if content.len() > DIRECT_TOOL_BRIDGE_MAX_WRITE_CONTENT_BYTES {
|
||||
return Err(format!(
|
||||
"工具参数 content 超过 {} bytes",
|
||||
DIRECT_TOOL_BRIDGE_MAX_WRITE_CONTENT_BYTES
|
||||
));
|
||||
}
|
||||
if content.chars().any(|character| character == '\0') {
|
||||
return Err("工具参数 content 不能包含 NUL".to_string());
|
||||
}
|
||||
reject_command_output_wrapper(content)?;
|
||||
let _lock = acquire_project_write_lock(root, "direct-codex.file.write")?;
|
||||
let written = write_local_project_file_at(root, &path, content)?;
|
||||
let revision = advance_agent_runtime_project_revision_locked(root)?;
|
||||
Ok::<_, String>(json!({
|
||||
"status": "completed",
|
||||
"path": written.path,
|
||||
"bytes": content.len(),
|
||||
"revision": revision,
|
||||
}))
|
||||
})();
|
||||
match result {
|
||||
Ok(result) => bridge_tool_result(result.to_string(), Vec::new(), false),
|
||||
Err(error) => bridge_tool_result(
|
||||
redact_agent_runtime_error(root, &error, 480),
|
||||
Vec::new(),
|
||||
true,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn bridge_safe_account_asset_projection(asset: &Value) -> Option<Value> {
|
||||
let asset_id = asset.get("assetId").and_then(Value::as_str)?;
|
||||
if asset_id.trim().is_empty() {
|
||||
@@ -2191,6 +2258,7 @@ async fn handle_direct_tool_bridge(
|
||||
bridge_list_registered_assets(&state.root, &request.arguments)
|
||||
}
|
||||
"agc_list_project_files" => bridge_list_project_files(&state.root, &request.arguments),
|
||||
"agc_write_file" => bridge_write_file(&state.root, &request.arguments),
|
||||
"agc_list_account_assets" => bridge_list_account_assets(&state, &request.arguments).await,
|
||||
"agc_import_account_assets" => {
|
||||
bridge_import_account_assets(&state, &request.arguments).await
|
||||
@@ -2399,6 +2467,58 @@ mod tests {
|
||||
assert_eq!(importability.get("assets/vector.svg"), Some(&false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bridge_write_file_writes_project_relative_text_without_runtime_tasks() {
|
||||
let temporary = tempfile::tempdir().expect("create direct write root");
|
||||
init_local_game_project_at(temporary.path(), "direct-write", "Direct 写入工具测试")
|
||||
.expect("initialize direct write root");
|
||||
let result = bridge_write_file(
|
||||
temporary.path(),
|
||||
&json!({
|
||||
"path": "game/index.html",
|
||||
"content": "<!doctype html><button>写入成功</button>"
|
||||
}),
|
||||
);
|
||||
assert_eq!(result.get("isError").and_then(Value::as_bool), Some(false));
|
||||
let payload: Value = serde_json::from_str(
|
||||
result
|
||||
.pointer("/content/0/text")
|
||||
.and_then(Value::as_str)
|
||||
.expect("direct write result text"),
|
||||
)
|
||||
.expect("parse direct write result");
|
||||
assert_eq!(payload["status"], "completed");
|
||||
assert_eq!(payload["path"], "game/index.html");
|
||||
assert_eq!(
|
||||
fs::read_to_string(temporary.path().join("game/index.html")).expect("read written"),
|
||||
"<!doctype html><button>写入成功</button>"
|
||||
);
|
||||
let wrapped = bridge_write_file(
|
||||
temporary.path(),
|
||||
&json!({
|
||||
"path": "game/index.html",
|
||||
"content": "Exit code: 0\nWall time: 0.1 seconds\nOutput:\n<!doctype html><button>错误包装</button>"
|
||||
}),
|
||||
);
|
||||
assert_eq!(wrapped.get("isError").and_then(Value::as_bool), Some(true));
|
||||
assert_eq!(
|
||||
fs::read_to_string(temporary.path().join("game/index.html")).expect("read unchanged"),
|
||||
"<!doctype html><button>写入成功</button>"
|
||||
);
|
||||
assert!(
|
||||
bridge_write_file(
|
||||
temporary.path(),
|
||||
&json!({
|
||||
"path": ".agent/manifest.json",
|
||||
"content": "{}"
|
||||
}),
|
||||
)
|
||||
.get("isError")
|
||||
.and_then(Value::as_bool)
|
||||
== Some(true)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resource_request_uuid_is_stable_v4_and_domain_separated() {
|
||||
let operation = direct_resource_request_uuid("turn-1", "operation", "abc");
|
||||
|
||||
@@ -6,12 +6,13 @@ use std::path::{Path, PathBuf};
|
||||
pub(crate) const DIRECT_TOOLS_MCP_MODE_FLAG: &str = "--agc-direct-tools-mcp";
|
||||
pub(crate) const DIRECT_TOOLS_MCP_CONTROLLED_WEB_SEARCH_ENV: &str =
|
||||
"AGC_CONTROLLED_WEB_SEARCH_ENABLED";
|
||||
const DIRECT_TOOLS_MCP_MAX_REQUEST_BYTES: usize = 1024 * 1024;
|
||||
const DIRECT_TOOLS_MCP_MAX_REQUEST_BYTES: usize = 2 * 1024 * 1024;
|
||||
const DIRECT_TOOLS_MCP_MAX_ART_BRIEF_CHARS: usize = 4_000;
|
||||
const DIRECT_TOOLS_MCP_MAX_IMAGE_PROMPT_CHARS: usize = 32_000;
|
||||
const DIRECT_TOOLS_MCP_MAX_SEARCH_QUERY_CHARS: usize = 400;
|
||||
const DIRECT_TOOLS_MCP_MAX_RESOURCE_PROMPT_CHARS: usize = 4_000;
|
||||
const DIRECT_TOOLS_MCP_MAX_RESOURCE_NAME_CHARS: usize = 120;
|
||||
const DIRECT_TOOLS_MCP_MAX_WRITE_CONTENT_BYTES: usize = 1_500_000;
|
||||
const DIRECT_TOOLS_MCP_MAX_BRIDGE_RESPONSE_BYTES: usize = 32 * 1024 * 1024;
|
||||
|
||||
pub(crate) fn direct_tools_mcp_mode_requested(args: &[String]) -> bool {
|
||||
@@ -62,6 +63,28 @@ fn direct_tools_mcp_specs_for(controlled_web_search: bool) -> Value {
|
||||
"additionalProperties": false
|
||||
}
|
||||
}),
|
||||
json!({
|
||||
"name": "agc_write_file",
|
||||
"description": "把文本写入当前 AGC 项目的相对路径。Codex 可以按需使用它直接推进代码、配置、资源依赖或说明文件;客户端只负责项目路径和基本控制面边界,不要求固定文件、任务顺序、验证或完成回执。",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 512,
|
||||
"description": "当前项目根下的相对路径,例如 game/index.html、assets/manifest.json 或 data/gameplay-spec.md"
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "目标文件的完整原始 UTF-8 正文;不要包含 command.exec 的 Exit code、Wall time、Output 包装、终端日志或解释文字",
|
||||
"maxLength": DIRECT_TOOLS_MCP_MAX_WRITE_CONTENT_BYTES
|
||||
}
|
||||
},
|
||||
"required": ["path", "content"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}),
|
||||
json!({
|
||||
"name": "taonier_prepare_game_art",
|
||||
"description": "创建或安全恢复当前 AGC 项目的陶泥儿标准游戏美术包。付费提交、幂等键、operation 恢复、来源校验、下载解码和登记均由客户端确定性执行。授权由 AGC 客户端当前登录会话和受控后端完成,用户不需要提供、配置、粘贴或创建 API Key;401/403 只能报告为客户端登录或权限状态异常,不得向用户索要凭据或暴露内部 URL。regenerate 还必须通过客户端对当前用户消息签发的单回合稳定调用授权;模型参数和 MCP 自动批准本身不构成替换授权。仅在用户意图确实需要新美术时调用。",
|
||||
@@ -381,6 +404,44 @@ fn call_agc_read_skill_resource(arguments: &Value) -> Value {
|
||||
}
|
||||
}
|
||||
|
||||
async fn call_agc_write_file(arguments: &Value) -> Value {
|
||||
if let Err(error) = validate_write_file_arguments(arguments) {
|
||||
return mcp_tool_result(error, Vec::new(), true);
|
||||
}
|
||||
call_client_tool_bridge("agc_write_file", arguments).await
|
||||
}
|
||||
|
||||
fn validate_write_file_arguments(arguments: &Value) -> Result<(), String> {
|
||||
validate_tool_object_fields(arguments, &["path", "content"])?;
|
||||
let path = bounded_tool_string(arguments, "path", 512)?;
|
||||
if path.split('/').any(|part| {
|
||||
part.eq_ignore_ascii_case(".agent")
|
||||
|| part.eq_ignore_ascii_case(".git")
|
||||
|| part.eq_ignore_ascii_case(".codex")
|
||||
|| part.eq_ignore_ascii_case(".hermes")
|
||||
|| part.eq_ignore_ascii_case("node_modules")
|
||||
}) {
|
||||
return Err("工具参数 path 不得访问受保护项目控制面".to_string());
|
||||
}
|
||||
let content = arguments
|
||||
.get("content")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or_else(|| "工具参数 content 必须是字符串".to_string())?;
|
||||
if content.len() > DIRECT_TOOLS_MCP_MAX_WRITE_CONTENT_BYTES {
|
||||
return Err(format!(
|
||||
"工具参数 content 超过 {} bytes",
|
||||
DIRECT_TOOLS_MCP_MAX_WRITE_CONTENT_BYTES
|
||||
));
|
||||
}
|
||||
if content.chars().any(|character| character == '\0') {
|
||||
return Err("工具参数 content 不能包含 NUL".to_string());
|
||||
}
|
||||
reject_command_output_wrapper(content)?;
|
||||
// Keep path normalization in the client bridge as the final authority;
|
||||
// this early check only gives Codex a quick, deterministic argument error.
|
||||
normalize_relative_path(&path).map(|_| ())
|
||||
}
|
||||
|
||||
fn mcp_success(id: Value, result: Value) -> Value {
|
||||
json!({ "jsonrpc": "2.0", "id": id, "result": result })
|
||||
}
|
||||
@@ -994,6 +1055,7 @@ async fn handle_direct_tools_mcp_request(_root: &Path, request: Value) -> Option
|
||||
.unwrap_or_else(|| json!({}));
|
||||
let result = match tool {
|
||||
"agc_read_skill_resource" => call_agc_read_skill_resource(&arguments),
|
||||
"agc_write_file" => call_agc_write_file(&arguments).await,
|
||||
"taonier_prepare_game_art" => call_taonier_prepare_game_art(&arguments).await,
|
||||
"agc_generate_image" => call_agc_generate_image(&arguments).await,
|
||||
"agc_edit_image" => call_agc_edit_image(&arguments).await,
|
||||
@@ -1115,6 +1177,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn tool_catalog_preserves_reviewed_resource_contracts() {
|
||||
assert!(
|
||||
DIRECT_TOOLS_MCP_MAX_REQUEST_BYTES
|
||||
> DIRECT_TOOLS_MCP_MAX_WRITE_CONTENT_BYTES + 1024,
|
||||
"MCP request envelope must fit the advertised file-write payload"
|
||||
);
|
||||
let specs = direct_tools_mcp_specs();
|
||||
let names = specs["tools"]
|
||||
.as_array()
|
||||
@@ -1126,6 +1193,7 @@ mod tests {
|
||||
names,
|
||||
vec![
|
||||
"agc_read_skill_resource",
|
||||
"agc_write_file",
|
||||
"taonier_prepare_game_art",
|
||||
"agc_generate_image",
|
||||
"agc_edit_image",
|
||||
@@ -1208,6 +1276,26 @@ mod tests {
|
||||
assert!(resource_tool["inputSchema"]["required"]
|
||||
.as_array()
|
||||
.is_some_and(|required| required.iter().any(|field| field == "assetName")));
|
||||
assert!(validate_write_file_arguments(&json!({
|
||||
"path": "game/index.html",
|
||||
"content": "<html></html>"
|
||||
}))
|
||||
.is_ok());
|
||||
assert!(validate_write_file_arguments(&json!({
|
||||
"path": "game/index.html",
|
||||
"content": "Exit code: 0\nWall time: 0.1 seconds\nOutput:\n<html></html>"
|
||||
}))
|
||||
.is_err());
|
||||
assert!(validate_write_file_arguments(&json!({
|
||||
"path": "notes.txt",
|
||||
"content": "说明:Exit code 只是一段普通文本"
|
||||
}))
|
||||
.is_ok());
|
||||
assert!(validate_write_file_arguments(&json!({
|
||||
"path": ".agent/manifest.json",
|
||||
"content": "{}"
|
||||
}))
|
||||
.is_err());
|
||||
assert_eq!(
|
||||
tool_art_preparation_mode(&json!({})).expect("safe default"),
|
||||
"reuse-or-create"
|
||||
|
||||
@@ -25,6 +25,7 @@ pub(crate) use canvas_generation::{
|
||||
};
|
||||
pub(in crate::agent) use canvas_generation::{
|
||||
commit_prepared_platform_art_asset_at,
|
||||
commit_prepared_platform_art_asset_strict_slices_at,
|
||||
generate_platform_art_asset_with_retained_runtime_options_at,
|
||||
generate_platform_art_asset_with_runtime_options_at,
|
||||
platform_art_generation_error_result_unknown, register_existing_platform_art_slices_at,
|
||||
|
||||
+25
-15
@@ -37,6 +37,7 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action_with_pending_
|
||||
pending_action: Option<&AgentRuntimePendingToolAction>,
|
||||
) -> AgentRuntimeToolObservation {
|
||||
let tool = action.tool.trim();
|
||||
let relaxed_autonomous = autonomous_relaxed_run_at(root, agent_id, run_id).unwrap_or(false);
|
||||
if tool == PLAN_SUBMIT_GDD_TOOL && agent_id.trim() != GAME_CREATOR_PROJECT_PLANNING_AGENT_ID {
|
||||
return AgentRuntimeToolObservation {
|
||||
tool: tool.to_string(),
|
||||
@@ -81,13 +82,19 @@ 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 !relaxed_autonomous {
|
||||
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 !relaxed_autonomous {
|
||||
if let Some(blocker) =
|
||||
agent_runtime_autonomous_art_director_canvas_only_action_block(agent_id, task, tool)
|
||||
{
|
||||
return blocker;
|
||||
}
|
||||
}
|
||||
let command_id = game_creator_agent_runtime_tool_command_id(tool);
|
||||
if let Some(command_id) = command_id {
|
||||
@@ -379,7 +386,7 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action_with_pending_
|
||||
"command.run_limited" => {
|
||||
observe_agent_runtime_limited_command(root, agent_id, run_id, &action.input)
|
||||
}
|
||||
"preview.start" => observe_agent_runtime_preview_start(root, agent_id),
|
||||
"preview.start" => observe_agent_runtime_preview_start(root, agent_id, run_id),
|
||||
"preview.validate" => {
|
||||
observe_agent_runtime_preview_validate(
|
||||
root,
|
||||
@@ -649,6 +656,7 @@ pub(in crate::agent) fn validate_agent_runtime_project_snapshot_action_after_loc
|
||||
));
|
||||
}
|
||||
let pending = &durable_pending;
|
||||
let relaxed_autonomous = autonomous_relaxed_run_profile(&pending.run_profile);
|
||||
let runtime = match read_game_creator_agent_runtime_at(root, agent_id) {
|
||||
Ok(result) => result.state,
|
||||
Err(error) => {
|
||||
@@ -692,16 +700,18 @@ pub(in crate::agent) fn validate_agent_runtime_project_snapshot_action_after_loc
|
||||
) {
|
||||
return Err(agent_runtime_tool_policy_block_observation(tool, blocked));
|
||||
}
|
||||
match pending_repository_context_drift_observation(root, pending) {
|
||||
Ok(Some(observation)) => return Err(observation),
|
||||
Ok(None) => {}
|
||||
Err(error) => {
|
||||
return Err(agent_runtime_pending_reconciliation_observation(
|
||||
tool, root, &error,
|
||||
));
|
||||
if !relaxed_autonomous {
|
||||
match pending_repository_context_drift_observation(root, pending) {
|
||||
Ok(Some(observation)) => return Err(observation),
|
||||
Ok(None) => {}
|
||||
Err(error) => {
|
||||
return Err(agent_runtime_pending_reconciliation_observation(
|
||||
tool, root, &error,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
if validate_revision_gate {
|
||||
if validate_revision_gate && !relaxed_autonomous {
|
||||
match pending_project_revision_drift_observation(root, pending, true) {
|
||||
Ok(Some(observation)) => return Err(observation),
|
||||
Ok(None) => {}
|
||||
|
||||
+38
-4
@@ -601,6 +601,16 @@ pub(in crate::agent) fn validate_agent_runtime_autonomous_plan_liveness_at(
|
||||
supervisor_requires_delegated_repair: bool,
|
||||
supervisor_manifest_dag_in_progress_at_request: bool,
|
||||
) -> Result<(), String> {
|
||||
// `autonomous-game-build` is the free-form lane. Its manifest entries,
|
||||
// delivery receipts and verification snapshots are advisory context; they
|
||||
// must never turn a perfectly valid Provider plan into a DAG-wait or
|
||||
// repair-only plan. Keep this profile check at the top so future liveness
|
||||
// rules cannot accidentally become a hidden start/completion gate.
|
||||
if read_game_creator_agent_runtime_run_profile_binding(root, agent_id, run_id)?
|
||||
.is_some_and(|binding| autonomous_relaxed_run_profile(&binding.profile))
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
||||
&& autonomous_manifest_dag_in_progress_at(root)?
|
||||
{
|
||||
@@ -783,16 +793,40 @@ fn autonomous_manifest_parent_has_active_ready_task_at(
|
||||
let records =
|
||||
latest_game_creator_agent_runtime_tasks(read_all_game_creator_agent_runtime_tasks(
|
||||
&game_creator_agent_runtime_task_path(root, task_id),
|
||||
)?);
|
||||
)?);
|
||||
for record in records {
|
||||
if record.source != "agent-ready-task-scheduler"
|
||||
|| record.parent_agent_id.as_deref()
|
||||
!= Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID)
|
||||
|| record.parent_run_id.as_deref() != Some(parent_run_id)
|
||||
|| game_creator_agent_runtime_terminal_status(&record).is_some()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// Relaxed autonomous children may use a generated run id and do
|
||||
// not depend on parent/child identity for execution. When the
|
||||
// scheduler can record the optional parent hint, use the durable
|
||||
// binding's root id solely to associate an in-flight child with
|
||||
// this root; never reject or block the child for a mismatch.
|
||||
if record.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD {
|
||||
let parent_hint_matches =
|
||||
record.parent_agent_id.as_deref()
|
||||
== Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID)
|
||||
&& record.parent_run_id.as_deref() == Some(parent_run_id);
|
||||
let binding_root_matches = read_game_creator_agent_runtime_run_profile_binding(
|
||||
root,
|
||||
&record.agent_id,
|
||||
&record.run_id,
|
||||
)?
|
||||
.is_some_and(|binding| binding.root_run_id == parent_run_id);
|
||||
if parent_hint_matches || binding_root_matches {
|
||||
return Ok(true);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if record.parent_agent_id.as_deref()
|
||||
!= Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID)
|
||||
|| record.parent_run_id.as_deref() != Some(parent_run_id)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if record.run_id != autonomous_manifest_ready_task_run_id(parent_run_id, task_id) {
|
||||
return Err(format!(
|
||||
"当前自主构建父 Run 的活跃 child runId 不符合确定性绑定:taskId={task_id}"
|
||||
|
||||
+45
-35
@@ -321,7 +321,6 @@ pub(in crate::agent) fn validate_agent_runtime_pending_tool_action_record(
|
||||
pending.fingerprint_version
|
||||
));
|
||||
}
|
||||
validate_agent_runtime_pending_goal_binding(pending)?;
|
||||
let (run_profile, binding_fingerprint) = agent_runtime_run_profile_identity_at(
|
||||
root,
|
||||
&pending.agent_id,
|
||||
@@ -334,46 +333,50 @@ pub(in crate::agent) fn validate_agent_runtime_pending_tool_action_record(
|
||||
{
|
||||
return Err("Agent Runtime 待确认动作 Run Profile 绑定不匹配".to_string());
|
||||
}
|
||||
match pending.planning_session_binding.as_ref() {
|
||||
Some(binding) => {
|
||||
validate_plan_provider_session_binding(binding).map_err(|error| error.to_string())?;
|
||||
if pending.action.tool.trim() != PLAN_SUBMIT_GDD_TOOL
|
||||
|| binding.agent_id != pending.agent_id
|
||||
|| binding.task_id != pending.task_id
|
||||
|| binding.session_id != pending.session_id
|
||||
|| binding.run_id != pending.run_id
|
||||
|| binding.source != pending.source
|
||||
|| binding.run_profile != pending.run_profile
|
||||
|| binding.run_profile_binding_fingerprint
|
||||
!= pending.run_profile_binding_fingerprint
|
||||
|| binding.applied_steer_cursor != pending.planned_steer_cursor
|
||||
{
|
||||
let relaxed_autonomous = autonomous_relaxed_run_profile(&pending.run_profile);
|
||||
if !relaxed_autonomous {
|
||||
validate_agent_runtime_pending_goal_binding(pending)?;
|
||||
match pending.planning_session_binding.as_ref() {
|
||||
Some(binding) => {
|
||||
validate_plan_provider_session_binding(binding).map_err(|error| error.to_string())?;
|
||||
if pending.action.tool.trim() != PLAN_SUBMIT_GDD_TOOL
|
||||
|| binding.agent_id != pending.agent_id
|
||||
|| binding.task_id != pending.task_id
|
||||
|| binding.session_id != pending.session_id
|
||||
|| binding.run_id != pending.run_id
|
||||
|| binding.source != pending.source
|
||||
|| binding.run_profile != pending.run_profile
|
||||
|| binding.run_profile_binding_fingerprint
|
||||
!= pending.run_profile_binding_fingerprint
|
||||
|| binding.applied_steer_cursor != pending.planned_steer_cursor
|
||||
{
|
||||
return Err(
|
||||
"planning submit standalone pending 与 frozen binding 不一致".to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
None if pending.provider_batch_plan_update.is_none() => {}
|
||||
None => {
|
||||
return Err(
|
||||
"planning submit standalone pending 与 frozen binding 不一致".to_string(),
|
||||
"非 planning standalone pending 不能携带 Provider batch planUpdate".to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
None if pending.provider_batch_plan_update.is_none() => {}
|
||||
None => {
|
||||
return Err(
|
||||
"非 planning standalone pending 不能携带 Provider batch planUpdate".to_string(),
|
||||
);
|
||||
validate_agent_runtime_project_revision(root, &pending.project_revision_before)?;
|
||||
if pending.verification_gate_before.project_id
|
||||
!= game_creator_agent_runtime_context_project_id(root)?
|
||||
|| pending.verification_gate_before.agent_id != pending.agent_id
|
||||
|| pending.verification_gate_before.run_id != pending.run_id
|
||||
{
|
||||
return Err("Agent Runtime 待确认动作的 verification gate 身份不匹配".to_string());
|
||||
}
|
||||
validate_agent_runtime_verification_gate(
|
||||
root,
|
||||
&pending.verification_gate_before,
|
||||
&pending.agent_id,
|
||||
&pending.run_id,
|
||||
)?;
|
||||
}
|
||||
validate_agent_runtime_project_revision(root, &pending.project_revision_before)?;
|
||||
if pending.verification_gate_before.project_id
|
||||
!= game_creator_agent_runtime_context_project_id(root)?
|
||||
|| pending.verification_gate_before.agent_id != pending.agent_id
|
||||
|| pending.verification_gate_before.run_id != pending.run_id
|
||||
{
|
||||
return Err("Agent Runtime 待确认动作的 verification gate 身份不匹配".to_string());
|
||||
}
|
||||
validate_agent_runtime_verification_gate(
|
||||
root,
|
||||
&pending.verification_gate_before,
|
||||
&pending.agent_id,
|
||||
&pending.run_id,
|
||||
)?;
|
||||
if pending.planned_repository_context_fingerprint.len() != 64
|
||||
|| !pending
|
||||
.planned_repository_context_fingerprint
|
||||
@@ -458,6 +461,13 @@ pub(in crate::agent) fn validate_agent_runtime_pending_current_goal_snapshot(
|
||||
root: &Path,
|
||||
pending: &AgentRuntimePendingToolAction,
|
||||
) -> Result<(), String> {
|
||||
// Relaxed autonomous child runs are intentionally independent of the
|
||||
// supervisor's Goal/Acceptance state. A sibling task may advance or
|
||||
// replace that state while this action is waiting for the project lock;
|
||||
// only the action's durable identity and tool policy remain relevant.
|
||||
if autonomous_relaxed_run_profile(&pending.run_profile) {
|
||||
return Ok(());
|
||||
}
|
||||
validate_agent_runtime_pending_goal_binding(pending)?;
|
||||
let current = read_game_creator_agent_goal_at(root, &pending.agent_id, &pending.session_id)?;
|
||||
let Some(expected_goal_id) = pending.goal_id.as_deref() else {
|
||||
|
||||
@@ -24,6 +24,12 @@ pub(in crate::agent) fn supervisor_orchestrator_mutation_block_at(
|
||||
{
|
||||
return None;
|
||||
}
|
||||
// The autonomous game-build profile is intentionally free-form: the
|
||||
// Supervisor may mutate the project directly while specialist tasks run
|
||||
// in parallel. Keep the collaboration policy for the standard profile.
|
||||
if autonomous_relaxed_run_at(root, agent_id, run_id).unwrap_or(false) {
|
||||
return None;
|
||||
}
|
||||
let policy = match resolve_supervisor_collaboration_policy_for_run_at(root, agent_id, run_id) {
|
||||
Ok(resolution) => resolution.policy,
|
||||
Err(error) => {
|
||||
@@ -129,150 +135,13 @@ pub(crate) fn ensure_current_autonomous_ready_child_mutation_at_locked(
|
||||
Ok(agent_id) => agent_id,
|
||||
Err(_) => return Ok(()),
|
||||
};
|
||||
let binding =
|
||||
read_game_creator_agent_runtime_run_profile_binding(root, &normalized_agent_id, run_id)?;
|
||||
let Some(binding) = binding else {
|
||||
let task = read_latest_game_creator_agent_runtime_task_by_run_id(
|
||||
root,
|
||||
&normalized_agent_id,
|
||||
run_id,
|
||||
)?;
|
||||
if task
|
||||
.as_ref()
|
||||
.is_some_and(|task| task.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD)
|
||||
{
|
||||
return Err("autonomous Run 项目修改缺少 Run Profile binding,已失败关闭".to_string());
|
||||
}
|
||||
return Ok(());
|
||||
};
|
||||
if game_creator_agent_runtime_cancel_requested_for(root, &normalized_agent_id, run_id) {
|
||||
return Err("当前 Run 已收到取消请求,禁止继续修改项目".to_string());
|
||||
}
|
||||
if binding.profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD {
|
||||
return Ok(());
|
||||
}
|
||||
if binding.agent_id != normalized_agent_id
|
||||
|| binding.run_id != run_id
|
||||
|| binding.root_agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
||||
{
|
||||
return Err("autonomous Run 项目修改的 Run Profile 绑定身份不一致".to_string());
|
||||
}
|
||||
let task =
|
||||
read_latest_game_creator_agent_runtime_task_by_run_id(root, &normalized_agent_id, run_id)?
|
||||
.ok_or_else(|| {
|
||||
"autonomous Run 项目修改缺少 durable task journal,已失败关闭".to_string()
|
||||
})?;
|
||||
if task.agent_id != binding.agent_id
|
||||
|| task.run_id != binding.run_id
|
||||
|| task.source != binding.source
|
||||
|| task.run_profile != binding.profile
|
||||
|| task.run_profile_binding_fingerprint != binding.binding_fingerprint
|
||||
|| task.parent_agent_id != binding.parent_agent_id
|
||||
|| task.parent_run_id != binding.parent_run_id
|
||||
{
|
||||
return Err("autonomous Run 项目修改的 durable task journal 与绑定不一致".to_string());
|
||||
}
|
||||
let is_root = binding.agent_id == binding.root_agent_id
|
||||
&& binding.run_id == binding.root_run_id
|
||||
&& binding.parent_agent_id.is_none()
|
||||
&& binding.parent_run_id.is_none();
|
||||
if is_root {
|
||||
if task.parent_agent_id.is_some()
|
||||
|| task.parent_run_id.is_some()
|
||||
|| !agent_runtime_supervisor_source_is_trusted(&task.source)
|
||||
{
|
||||
return Err("autonomous 根 Run 项目修改的 durable identity 不一致".to_string());
|
||||
}
|
||||
} else {
|
||||
let parent_agent_id = binding
|
||||
.parent_agent_id
|
||||
.as_deref()
|
||||
.ok_or_else(|| "autonomous 派生 Run 项目修改缺少 parentAgentId".to_string())?;
|
||||
let parent_run_id = binding
|
||||
.parent_run_id
|
||||
.as_deref()
|
||||
.ok_or_else(|| "autonomous 派生 Run 项目修改缺少 parentRunId".to_string())?;
|
||||
let parent_binding = read_game_creator_agent_runtime_run_profile_binding(
|
||||
root,
|
||||
parent_agent_id,
|
||||
parent_run_id,
|
||||
)?
|
||||
.ok_or_else(|| "autonomous 派生 Run 项目修改缺少父 Run Profile binding".to_string())?;
|
||||
if binding.parent_binding_fingerprint.as_deref()
|
||||
!= Some(parent_binding.binding_fingerprint.as_str())
|
||||
|| parent_binding.profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
|| parent_binding.root_agent_id != binding.root_agent_id
|
||||
|| parent_binding.root_run_id != binding.root_run_id
|
||||
{
|
||||
return Err(
|
||||
"autonomous 派生 Run 项目修改的父 binding 或 root identity 不一致".to_string(),
|
||||
);
|
||||
}
|
||||
if binding.source == "agent-ready-task-scheduler" {
|
||||
let state = agent_runtime_state_from_task_record(&task);
|
||||
let ready_binding =
|
||||
autonomous_manifest_ready_task_parent_binding_for_state_at(root, &state)?
|
||||
.ok_or_else(|| {
|
||||
"autonomous ready-task 项目修改缺少确定性父 Run 绑定".to_string()
|
||||
})?;
|
||||
if ready_binding != binding
|
||||
|| state.run_id
|
||||
!= autonomous_manifest_ready_task_run_id(
|
||||
&binding.root_run_id,
|
||||
&normalized_agent_id,
|
||||
)
|
||||
{
|
||||
return Err("autonomous ready-task 项目修改的确定性父子身份不一致".to_string());
|
||||
}
|
||||
} else if binding.source == "agent-delegate"
|
||||
&& task
|
||||
.delegation_id
|
||||
.as_deref()
|
||||
.is_none_or(|delegation_id| delegation_id.trim().is_empty())
|
||||
{
|
||||
return Err("autonomous agent-delegate 项目修改缺少 delegationId".to_string());
|
||||
}
|
||||
}
|
||||
if task.status != "running" || game_creator_agent_runtime_terminal_status(&task).is_some() {
|
||||
return Err("autonomous Run 项目修改要求当前 durable task 仍为 running".to_string());
|
||||
}
|
||||
let current_root = current_autonomous_game_build_root_task_at(root)?
|
||||
.ok_or_else(|| "autonomous Run 项目修改时当前根 Run 已不存在".to_string())?;
|
||||
if current_root.run_id != binding.root_run_id {
|
||||
return Err(format!(
|
||||
"autonomous Run 已被更新根 Run 取代:currentRunId={}",
|
||||
current_root.run_id
|
||||
));
|
||||
}
|
||||
let current_root_binding = read_game_creator_agent_runtime_run_profile_binding(
|
||||
root,
|
||||
¤t_root.agent_id,
|
||||
¤t_root.run_id,
|
||||
)?
|
||||
.ok_or_else(|| "autonomous Run 当前根缺少 Run Profile binding".to_string())?;
|
||||
if current_root.agent_id != binding.root_agent_id
|
||||
|| current_root.source != current_root_binding.source
|
||||
|| current_root.run_profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
|| current_root.parent_agent_id.is_some()
|
||||
|| current_root.parent_run_id.is_some()
|
||||
|| current_root.delegation_id.is_some()
|
||||
|| current_root_binding.agent_id != binding.root_agent_id
|
||||
|| current_root_binding.run_id != binding.root_run_id
|
||||
|| current_root_binding.root_agent_id != current_root_binding.agent_id
|
||||
|| current_root_binding.root_run_id != current_root_binding.run_id
|
||||
|| current_root_binding.parent_agent_id.is_some()
|
||||
|| current_root_binding.parent_run_id.is_some()
|
||||
|| current_root_binding.binding_fingerprint != current_root.run_profile_binding_fingerprint
|
||||
|| (is_root && current_root_binding.binding_fingerprint != binding.binding_fingerprint)
|
||||
{
|
||||
return Err("autonomous Run 当前根 journal 与 binding 不一致".to_string());
|
||||
}
|
||||
if !autonomous_game_build_root_task_is_active(¤t_root) {
|
||||
return Err(format!(
|
||||
"autonomous Run 当前根已不再活跃:status={} phase={}",
|
||||
current_root.status, current_root.phase
|
||||
));
|
||||
}
|
||||
// Relaxed autonomous runs do not require a fixed parent/owner lineage.
|
||||
// The project-root and cancellation checks remain in force, while each
|
||||
// task is free to mutate through the normal tool whitelist even when an
|
||||
// old run has no parent/profile sidecar.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
+16
-6
@@ -476,7 +476,9 @@ pub(crate) async fn prepare_game_creator_agent_runtime_provider_action_batch_wit
|
||||
{
|
||||
return Err("Provider action 批次的 Run Profile 快照已漂移".to_string());
|
||||
}
|
||||
if run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
let relaxed_autonomous = autonomous_relaxed_run_profile(&run_profile);
|
||||
if !relaxed_autonomous
|
||||
&& run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
&& batch_plan
|
||||
.actions
|
||||
.iter()
|
||||
@@ -495,7 +497,9 @@ pub(crate) async fn prepare_game_creator_agent_runtime_provider_action_batch_wit
|
||||
));
|
||||
}
|
||||
|
||||
let collaboration_preflight = if runtime.agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
|
||||
let collaboration_preflight = if relaxed_autonomous {
|
||||
SupervisorCollaborationPreflight::default()
|
||||
} else if runtime.agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
|
||||
let collaboration_policy = resolve_supervisor_collaboration_policy_for_run_at(
|
||||
root,
|
||||
&runtime.agent_id,
|
||||
@@ -514,7 +518,8 @@ pub(crate) async fn prepare_game_creator_agent_runtime_provider_action_batch_wit
|
||||
} else {
|
||||
SupervisorCollaborationPreflight::default()
|
||||
};
|
||||
if run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
if !relaxed_autonomous
|
||||
&& run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
&& collaboration_preflight
|
||||
.contract
|
||||
.as_ref()
|
||||
@@ -534,7 +539,8 @@ pub(crate) async fn prepare_game_creator_agent_runtime_provider_action_batch_wit
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(violation) = collaboration_preflight.violation {
|
||||
if !relaxed_autonomous {
|
||||
if let Some(violation) = collaboration_preflight.violation {
|
||||
return Ok(AgentRuntimeProviderActionBatchPreparation::Blocked(
|
||||
AgentRuntimeToolObservation {
|
||||
tool: "runtime.collaboration_policy".to_string(),
|
||||
@@ -543,6 +549,7 @@ pub(crate) async fn prepare_game_creator_agent_runtime_provider_action_batch_wit
|
||||
detail: Some(violation.detail),
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
if provider_action_batch_is_not_needed(
|
||||
batch_plan.actions.len(),
|
||||
@@ -587,13 +594,16 @@ pub(crate) async fn prepare_game_creator_agent_runtime_provider_action_batch_wit
|
||||
"当前 Agent 身份不允许执行该原始工具".to_string(),
|
||||
)
|
||||
});
|
||||
let art_director_canvas_only_block =
|
||||
let art_director_canvas_only_block = if relaxed_autonomous {
|
||||
None
|
||||
} else {
|
||||
agent_runtime_autonomous_art_director_canvas_only_action_block(
|
||||
&runtime.agent_id,
|
||||
task,
|
||||
action.tool.trim(),
|
||||
)
|
||||
.map(|observation| AgentRuntimeToolPolicyBlock::Denied(observation.summary));
|
||||
.map(|observation| AgentRuntimeToolPolicyBlock::Denied(observation.summary))
|
||||
};
|
||||
let isolated_scope_block = if runtime.agent_id.starts_with("child-") {
|
||||
validate_isolated_agent_tool_scope_at(
|
||||
root,
|
||||
|
||||
+148
-69
@@ -152,6 +152,31 @@ pub(in crate::agent) fn remove_autonomous_art_director_non_canvas_validation_too
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The relaxed autonomous lane is an execution lane, not a platform-quality
|
||||
/// gate. Do not advertise actions whose only purpose is to produce platform
|
||||
/// verification evidence: once exposed, a Provider will commonly spend the
|
||||
/// whole turn running them even though their result is deliberately ignored
|
||||
/// by relaxed completion. Keeping the native implementations available is
|
||||
/// intentional; this only narrows the Provider request catalog.
|
||||
pub(in crate::agent) fn remove_relaxed_autonomous_platform_validation_tools(
|
||||
tools: &mut Vec<LlmFunctionTool>,
|
||||
) -> Result<(), String> {
|
||||
let hidden_function_names = [
|
||||
"project.verify",
|
||||
"command.run_limited",
|
||||
"preview.start",
|
||||
"preview.validate",
|
||||
]
|
||||
.into_iter()
|
||||
.map(|tool| {
|
||||
native_runtime_function_name(tool)
|
||||
.ok_or_else(|| format!("无法生成 relaxed 平台验证工具函数名:{tool}"))
|
||||
})
|
||||
.collect::<Result<BTreeSet<_>, _>>()?;
|
||||
tools.retain(|tool| !hidden_function_names.contains(&tool.name));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_game_creator_agent_background_tool_plan_request_at(
|
||||
root: &Path,
|
||||
project_lock: Option<&ProjectWriteLock>,
|
||||
@@ -231,6 +256,47 @@ fn build_game_creator_agent_background_tool_plan_request_at(
|
||||
&& agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
||||
&& prompt_observations_report_manifest_dag_in_progress(&prompt_observations),
|
||||
};
|
||||
|
||||
if autonomous_game_build {
|
||||
// Autonomous game builds use the normal native tool catalog and a
|
||||
// short task/context prompt. Goal Contract, Acceptance Graph,
|
||||
// owner-artifact and preview wording belongs to the optional
|
||||
// acceptance layer; it must not steer the Provider into repair loops
|
||||
// before any project work has happened.
|
||||
let relaxed_prompt = format!(
|
||||
"你正在执行一个自主游戏构建任务。请按自己的判断规划并直接调用当前广告的原生工具完成目标;任务可以与其它 Agent 并行,依赖只作为参考,不要等待或索要平台资产/验收回执。已有观察只代表已发生的事实,完成后直接调用 respond_to_user。\n\n运行上下文:\n{context}\n\n任务:\n{effective_task}\n\n已有观察:\n{observations_json}"
|
||||
);
|
||||
let mut function_tools = build_agent_runtime_native_function_tools_for_agent(agent_id)?;
|
||||
remove_relaxed_autonomous_platform_validation_tools(&mut function_tools)?;
|
||||
// Platform-backed generation remains an optional capability. A
|
||||
// relaxed run may proceed with all ordinary project tools when no
|
||||
// editor session is configured, but it must not advertise a paid
|
||||
// Canvas action that cannot succeed.
|
||||
if !editor_api_key_is_configured() {
|
||||
let canvas_function = native_runtime_function_name("canvas.asset_generate")
|
||||
.ok_or_else(|| "无法生成画布素材工具函数名".to_string())?;
|
||||
function_tools.retain(|tool| tool.name != canvas_function);
|
||||
}
|
||||
let request = LlmRunRequest::new(vec![
|
||||
LlmMessage::system(
|
||||
"你是 Genarrative AGC 的自主执行 Agent。保持在项目根目录内工作,使用可用工具完成实际任务;不要把流程合同、固定 owner、DAG 顺序或平台产物当作启动条件。",
|
||||
),
|
||||
LlmMessage::user(relaxed_prompt),
|
||||
])
|
||||
.with_api_kind(parse_game_creator_llm_api_kind(&llm.api_kind)? )
|
||||
.with_max_output_tokens(AGENT_RUNTIME_TOOL_PLAN_MAX_OUTPUT_TOKENS)
|
||||
.with_response_text_verbosity(platform_llm::LlmResponseTextVerbosity::Low)
|
||||
.with_function_tools(function_tools)
|
||||
.with_tool_choice(platform_llm::LlmToolChoice::Required);
|
||||
let request = apply_game_creator_llm_reasoning_effort(request, &llm)?.with_web_search(false);
|
||||
return Ok((
|
||||
llm,
|
||||
config_path,
|
||||
request,
|
||||
repository_context_fingerprint,
|
||||
request_snapshot,
|
||||
));
|
||||
}
|
||||
let runtime_owner_artifact_validation_available = autonomous_game_build
|
||||
&& autonomous_owner_artifact_validation_available_for_run_at(root, agent_id, run_id)?;
|
||||
let autonomous_project_verify_available = !runtime_owner_artifact_validation_available
|
||||
@@ -913,7 +979,7 @@ mod tests {
|
||||
game_creator_agent_runtime_run_profile_binding_path,
|
||||
game_creator_project_supervisor_chat_system_prompt, init_local_game_project_at,
|
||||
new_game_creation_app_seed_tasks, provider_command_exec_contract,
|
||||
provider_command_start_contract, render_autonomous_manifest_ready_task_background_prompt,
|
||||
provider_command_start_contract, render_relaxed_autonomous_manifest_ready_task_background_prompt,
|
||||
required_runtime_prompt_section, start_game_creator_agent_runtime_task_at,
|
||||
AgentRuntimeGoalContractAcceptanceNodeDraft, AgentRuntimeGoalContractDraft,
|
||||
AgentRuntimeTaskLink, AgentRuntimeToolObservation, AgentRuntimeToolPlan,
|
||||
@@ -949,7 +1015,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejected_plan_update_forces_request_scoped_mutation_catalog() {
|
||||
fn relaxed_request_keeps_general_catalog_after_plan_rejection() {
|
||||
let directory = crate::tests::canonical_test_tempdir("provider-plan-rejection-repair-");
|
||||
let root = directory.path().join("project");
|
||||
init_local_game_project_at(&root, "plan-rejection-repair", "修复现有游戏")
|
||||
@@ -1021,15 +1087,26 @@ mod tests {
|
||||
.as_str()
|
||||
));
|
||||
assert!(names.contains(AGENT_RUNTIME_RESPOND_FUNCTION_NAME));
|
||||
assert!(!names.contains(AGENT_RUNTIME_UPDATE_PLAN_FUNCTION_NAME));
|
||||
// A rejected/empty plan is only an observation in the free-form lane;
|
||||
// it must not turn the next request into a narrow repair state machine.
|
||||
assert!(names.contains(AGENT_RUNTIME_UPDATE_PLAN_FUNCTION_NAME));
|
||||
assert!(!request_advertises_native_tool(&request, "project.verify"));
|
||||
assert!(!request_advertises_native_tool(
|
||||
&request,
|
||||
"command.run_limited"
|
||||
));
|
||||
assert!(request
|
||||
.messages
|
||||
.iter()
|
||||
.any(|message| message.content.contains("依赖只作为参考")));
|
||||
assert!(!request
|
||||
.messages
|
||||
.iter()
|
||||
.any(|message| message.content.contains("runtime.plan_update 被拒绝")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn idle_plan_update_rounds_drop_the_plan_tool_from_the_request_catalog() {
|
||||
fn relaxed_request_keeps_plan_tool_after_idle_rounds() {
|
||||
let directory = crate::tests::canonical_test_tempdir("provider-plan-idle-repair-");
|
||||
let root = directory.path().join("project");
|
||||
init_local_game_project_at(&root, "plan-idle-repair", "修复现有游戏")
|
||||
@@ -1075,7 +1152,9 @@ mod tests {
|
||||
)
|
||||
.expect("create goal contract");
|
||||
|
||||
// 没有空转计数时 update_agent_plan 必须还在,否则这条判据就等于永远生效。
|
||||
// Relaxed orchestration does not convert an idle planning counter into
|
||||
// a tool-removal gate; the Provider remains free to choose its next
|
||||
// action.
|
||||
let (_, _, baseline, _, _) = build_game_creator_agent_background_tool_plan_request(
|
||||
&root,
|
||||
&state.agent_id,
|
||||
@@ -1106,7 +1185,7 @@ mod tests {
|
||||
2,
|
||||
)
|
||||
.expect("build idle-repair request");
|
||||
assert!(!request
|
||||
assert!(request
|
||||
.function_tools
|
||||
.iter()
|
||||
.any(|tool| tool.name == AGENT_RUNTIME_UPDATE_PLAN_FUNCTION_NAME));
|
||||
@@ -1115,7 +1194,7 @@ mod tests {
|
||||
.function_tools
|
||||
.iter()
|
||||
.any(|tool| tool.name == AGENT_RUNTIME_RESPOND_FUNCTION_NAME));
|
||||
assert!(request.messages.iter().any(|message| message
|
||||
assert!(!request.messages.iter().any(|message| message
|
||||
.content
|
||||
.contains("update_agent_plan 已从工具目录中移除")));
|
||||
}
|
||||
@@ -1162,9 +1241,9 @@ mod tests {
|
||||
.into_iter()
|
||||
.find(|task| task.id == agent_id)
|
||||
.unwrap_or_else(|| panic!("missing seed task {agent_id}"));
|
||||
let task = render_autonomous_manifest_ready_task_background_prompt(&seed_task);
|
||||
let task = render_relaxed_autonomous_manifest_ready_task_background_prompt(&seed_task);
|
||||
assert!(
|
||||
task.contains("这是 autonomous-game-build"),
|
||||
task.contains("这是并行自主执行任务"),
|
||||
"ready task prompt lost autonomous overlay: {task}"
|
||||
);
|
||||
let state = start_game_creator_agent_runtime_task_at(
|
||||
@@ -1214,7 +1293,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_dag_pre_code_owner_requests_do_not_advertise_manual_verification() {
|
||||
fn relaxed_pre_code_requests_use_the_same_free_form_prompt() {
|
||||
let _config_guard = crate::tests::write_test_local_config("{}".to_string());
|
||||
for (index, agent_id) in [
|
||||
"design-foundation",
|
||||
@@ -1242,13 +1321,12 @@ mod tests {
|
||||
));
|
||||
let system_prompt = &request.messages[0].content;
|
||||
let user_prompt = &request.messages[1].content;
|
||||
assert!(system_prompt.contains("固定 owner 写入后直接交付"));
|
||||
assert!(system_prompt.contains("Runtime 会在收束门内检查本人正式产物"));
|
||||
assert!(user_prompt.contains("固定 owner 收束协议"));
|
||||
assert!(user_prompt.contains("当前请求不广告 project.verify 或 command.run_limited"));
|
||||
assert!(!user_prompt.contains("project.verify 使用"));
|
||||
assert!(!user_prompt.contains("command.run_limited 使用"));
|
||||
assert!(user_prompt.contains("完成本人固定路径的正式产物后直接调用 respond_to_user"));
|
||||
assert!(system_prompt.contains("自主执行 Agent"));
|
||||
assert!(system_prompt.contains("不要把流程合同、固定 owner、DAG 顺序或平台产物当作启动条件"));
|
||||
assert!(user_prompt.contains("依赖只作为参考"));
|
||||
assert!(user_prompt.contains("不要等待或索要平台资产/验收回执"));
|
||||
assert!(!user_prompt.contains("固定 owner 收束协议"));
|
||||
assert!(!user_prompt.contains("Runtime 会在收束门内检查本人正式产物"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1289,7 +1367,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn playable_and_late_stage_requests_keep_their_existing_verification_boundaries() {
|
||||
fn relaxed_playable_and_late_stage_requests_skip_platform_validation_tools() {
|
||||
let _config_guard = crate::tests::write_test_local_config("{}".to_string());
|
||||
|
||||
let code = build_autonomous_ready_child_request(
|
||||
@@ -1297,48 +1375,43 @@ mod tests {
|
||||
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
|
||||
"code-prototype",
|
||||
);
|
||||
assert!(request_advertises_native_tool(&code, "command.run_limited"));
|
||||
assert!(code.messages[0]
|
||||
.content
|
||||
.contains("程序 owner 必须对可玩入口执行 game.static_smoke"));
|
||||
assert!(code.messages[1]
|
||||
.content
|
||||
.contains("必须对可玩入口执行 game.static_smoke"));
|
||||
for tool in ["project.verify", "command.run_limited", "preview.start", "preview.validate"] {
|
||||
assert!(!request_advertises_native_tool(&code, tool));
|
||||
}
|
||||
assert!(request_advertises_native_tool(&code, "file.write"));
|
||||
assert!(code.messages[0].content.contains("自主执行 Agent"));
|
||||
assert!(code.messages[1].content.contains("不要等待或索要平台资产/验收回执"));
|
||||
|
||||
let readiness = build_autonomous_ready_child_request(
|
||||
"preview-readiness",
|
||||
AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
|
||||
"preview-readiness",
|
||||
);
|
||||
assert!(request_advertises_native_tool(
|
||||
&readiness,
|
||||
"command.run_limited"
|
||||
));
|
||||
assert!(readiness.messages[0]
|
||||
.content
|
||||
.contains("必须对最终 revision 执行 game.static_smoke,不执行 preview.validate"));
|
||||
assert!(readiness.messages[1]
|
||||
.content
|
||||
.contains("且只能是 command.run_limited(commandId=game.static_smoke)"));
|
||||
for tool in ["project.verify", "command.run_limited", "preview.start", "preview.validate"] {
|
||||
assert!(!request_advertises_native_tool(&readiness, tool));
|
||||
}
|
||||
assert!(request_advertises_native_tool(&readiness, "file.read"));
|
||||
assert!(readiness.messages[0].content.contains("自主执行 Agent"));
|
||||
assert!(readiness.messages[1].content.contains("不要等待或索要平台资产/验收回执"));
|
||||
|
||||
let publish = build_autonomous_ready_child_request(
|
||||
"publish-package",
|
||||
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
|
||||
"publish-package",
|
||||
);
|
||||
assert!(!request_advertises_native_tool(&publish, "project.verify"));
|
||||
assert!(request_advertises_native_tool(
|
||||
&publish,
|
||||
"command.run_limited"
|
||||
));
|
||||
for tool in ["project.verify", "command.run_limited", "preview.start", "preview.validate"] {
|
||||
assert!(!request_advertises_native_tool(&publish, tool));
|
||||
}
|
||||
assert!(request_advertises_native_tool(&publish, "file.write"));
|
||||
let publish_prompts = publish
|
||||
.messages
|
||||
.iter()
|
||||
.map(|message| message.content.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
assert!(publish_prompts.contains("不在前置固定 owner 的内部产物验证范围内"));
|
||||
assert!(!publish_prompts.contains("当前固定 owner 写入后直接交付"));
|
||||
assert!(publish_prompts.contains("自主执行 Agent"));
|
||||
assert!(publish_prompts.contains("依赖只作为参考"));
|
||||
assert!(!publish_prompts.contains("不在前置固定 owner 的内部产物验证范围内"));
|
||||
assert!(!publish_prompts.contains("固定 owner 收束协议"));
|
||||
}
|
||||
|
||||
@@ -1362,10 +1435,11 @@ mod tests {
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
assert!(
|
||||
prompts.contains("无生图凭据只读协调任务"),
|
||||
prompts.contains("自主执行 Agent"),
|
||||
"unexpected no-key art-director prompts: {prompts}"
|
||||
);
|
||||
assert!(prompts.contains("不调用 canvas.asset_generate"));
|
||||
assert!(prompts.contains("不要等待或索要平台资产/验收回执"));
|
||||
assert!(!prompts.contains("无生图凭据只读协调任务"));
|
||||
}
|
||||
// debug 构建下 editor_api_mode() 恒为 PlatformAccount,配置里的
|
||||
// editorApi.apiKey 会被 editor_api_key_is_configured 完全忽略;只有凭据
|
||||
@@ -1397,16 +1471,18 @@ mod tests {
|
||||
"
|
||||
",
|
||||
);
|
||||
assert!(prompts.contains("非只读视觉规范生成任务"));
|
||||
assert!(prompts
|
||||
.contains(crate::agent::AGENT_RUNTIME_AUTONOMOUS_ART_DIRECTOR_CANVAS_ONLY_TASK_MARKER));
|
||||
assert!(prompts.contains("assets/art-spec.png"));
|
||||
assert!(prompts.contains("会同时提交当前 run 的 mutation 与验证凭证"));
|
||||
assert!(prompts.contains("自主执行 Agent"));
|
||||
assert!(prompts.contains("依赖只作为参考"));
|
||||
assert!(!prompts.contains("非只读视觉规范生成任务"));
|
||||
assert!(!prompts.contains(
|
||||
crate::agent::AGENT_RUNTIME_AUTONOMOUS_ART_DIRECTOR_CANVAS_ONLY_TASK_MARKER
|
||||
));
|
||||
assert!(!prompts.contains("会同时提交当前 run 的 mutation 与验证凭证"));
|
||||
assert!(!prompts.contains("无生图凭据只读协调任务"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trusted_root_supervisor_first_turn_only_receives_goal_contract_tool() {
|
||||
fn relaxed_root_supervisor_receives_general_execution_catalog_first_turn() {
|
||||
let directory = crate::tests::canonical_test_tempdir("provider-goal-control-");
|
||||
let root = directory.path().join("project");
|
||||
init_local_game_project_at(&root, "goal-control-project", "完成可验证游戏")
|
||||
@@ -1440,25 +1516,28 @@ mod tests {
|
||||
0,
|
||||
)
|
||||
.expect("build trusted root request");
|
||||
let prompt = &request.messages[1].content;
|
||||
assert!(prompt.contains("动态目标协议:agent.goal_contract"));
|
||||
assert!(prompt.contains("固定规则、关键词、资产探测和专家建议只能作为上下文"));
|
||||
assert!(prompt.contains("未提交的 passed 节点保持不变"));
|
||||
assert_eq!(request.function_tools.len(), 1);
|
||||
assert_eq!(
|
||||
native_input_required_fields(&request, "agent.goal_contract"),
|
||||
[
|
||||
"outcome",
|
||||
"nonNegotiables",
|
||||
"preferences",
|
||||
"forbiddenAssumptions",
|
||||
"openQuestions",
|
||||
"acceptanceNodes"
|
||||
]
|
||||
);
|
||||
assert!(request.messages.iter().any(|message| message
|
||||
.content
|
||||
.contains("本轮唯一可用工具是 agent.goal_contract")));
|
||||
// The autonomous execution marker lives in the system message; the
|
||||
// user message carries only the task-specific runtime context.
|
||||
let prompt = &request.messages[0].content;
|
||||
assert!(prompt.contains("自主执行 Agent"), "unexpected relaxed root prompt: {prompt}");
|
||||
assert!(prompt.contains("不要把流程合同、固定 owner、DAG 顺序或平台产物当作启动条件"));
|
||||
assert!(request.function_tools.len() > 1);
|
||||
for tool in [
|
||||
"agent.goal_contract",
|
||||
"task.list",
|
||||
"agent.run_status",
|
||||
"file.patch",
|
||||
] {
|
||||
assert!(
|
||||
request_advertises_native_tool(&request, tool),
|
||||
"relaxed root must advertise {tool}"
|
||||
);
|
||||
}
|
||||
assert!(request
|
||||
.function_tools
|
||||
.iter()
|
||||
.any(|function| function.name == AGENT_RUNTIME_RESPOND_FUNCTION_NAME));
|
||||
assert!(!prompt.contains("本轮唯一可用工具是 agent.goal_contract"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user