Merge branch 'master' into codex/rust-agent
Project CI / Repository checks (pull_request) Successful in 2m29s
Project CI / Backend tests (pull_request) Failing after 5m1s
Project CI / Frontend tests (pull_request) Successful in 3m13s
Project CI / Native shell tests (pull_request) Failing after 8m23s

This commit is contained in:
2026-08-31 10:50:50 +08:00
73 changed files with 3504 additions and 873 deletions
+9
View File
@@ -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">
+37
View File
@@ -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 -1
View File
@@ -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,
@@ -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
View File
@@ -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,
@@ -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) => {}
@@ -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}"
@@ -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,
&current_root.agent_id,
&current_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(&current_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(())
}
@@ -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,
@@ -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