放开 AGC 自主创作流程并补齐全流程执行能力

新增 agc_write_file 直写工具并放宽自主构建执行路径

移除固定任务依赖与确认门槛对并行创作的阻塞

同步 DirectProject、预览、运行状态和 E2E 测试调整
This commit is contained in:
Git Hooks Test
2026-08-28 20:55:23 +08:00
parent e52f43485a
commit 1ba34b8e25
50 changed files with 3094 additions and 797 deletions
@@ -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,
File diff suppressed because it is too large Load Diff
@@ -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` 把文本写入项目相对路径。`../`、绝对路径、`.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,11 @@ 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("切图、资源依赖、规范图和试玩都只是可选工具提示"));
assert!(prompt.contains("不要求调用、固定顺序或特定产物"));
}
#[test]
@@ -4562,6 +4570,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 +4578,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;
@@ -1392,6 +1393,54 @@ 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());
}
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() {
@@ -2192,6 +2241,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
@@ -2400,6 +2450,46 @@ 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>"
);
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,27 @@ 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",
"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 +403,43 @@ 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());
}
// 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 +1053,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 +1175,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 +1191,7 @@ mod tests {
names,
vec![
"agc_read_skill_resource",
"agc_write_file",
"taonier_prepare_game_art",
"agc_generate_image",
"agc_edit_image",
@@ -1208,6 +1274,16 @@ 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": ".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(())
}
pub(in crate::agent) fn build_game_creator_agent_background_tool_plan_request(
root: &Path,
agent_id: &str,
@@ -230,6 +255,47 @@ pub(in crate::agent) fn build_game_creator_agent_background_tool_plan_request(
&& 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
@@ -846,7 +912,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,
@@ -882,7 +948,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", "修复现有游戏")
@@ -954,15 +1020,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", "修复现有游戏")
@@ -1008,7 +1085,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,
@@ -1039,7 +1118,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));
@@ -1048,7 +1127,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 已从工具目录中移除")));
}
@@ -1095,9 +1174,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(
@@ -1147,7 +1226,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",
@@ -1175,13 +1254,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 会在收束门内检查本人正式产物"));
}
}
@@ -1222,7 +1300,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(
@@ -1230,48 +1308,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 收束协议"));
}
@@ -1295,10 +1368,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 完全忽略;只有凭据
@@ -1330,16 +1404,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", "完成可验证游戏")
@@ -1373,25 +1449,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]
@@ -221,6 +221,7 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
let initial_request_slot = format!("loop-{loop_index}-repair-0");
let (run_profile, _) =
agent_runtime_run_profile_identity_at(root, agent_id, run_id, None, None)?;
let relaxed_autonomous = autonomous_relaxed_run_profile(&run_profile);
let plan_root_candidate =
read_game_creator_agent_runtime_run_profile_binding(root, agent_id, run_id)?
.is_some_and(|binding| binding.source == AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE);
@@ -235,8 +236,8 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
root,
"runtime.provider_request.build.tool_plan",
)?;
let live_manifest_dag_in_progress_before = run_profile
== AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
let live_manifest_dag_in_progress_before = !relaxed_autonomous
&& run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
&& autonomous_manifest_dag_in_progress_at(root)?;
let request = build_game_creator_agent_background_tool_plan_request(
@@ -248,13 +249,14 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
observations,
loop_index,
)?;
let live_manifest_dag_in_progress_after = run_profile
== AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
let live_manifest_dag_in_progress_after = !relaxed_autonomous
&& run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
&& autonomous_manifest_dag_in_progress_at(root)?;
let request_bound_manifest_dag_in_progress = live_manifest_dag_in_progress_before
|| live_manifest_dag_in_progress_after
|| request.4.supervisor_manifest_dag_in_progress;
let request_bound_manifest_dag_in_progress = !relaxed_autonomous
&& (live_manifest_dag_in_progress_before
|| live_manifest_dag_in_progress_after
|| request.4.supervisor_manifest_dag_in_progress);
(request, request_bound_manifest_dag_in_progress)
};
let mut estimated_input_tokens = estimate_game_creator_llm_request_tokens(&built_request.2)?;
@@ -299,8 +301,8 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
root,
"runtime.provider_request.rebuild.tool_plan",
)?;
let live_manifest_dag_in_progress_before = run_profile
== AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
let live_manifest_dag_in_progress_before = !relaxed_autonomous
&& run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
&& autonomous_manifest_dag_in_progress_at(root)?;
let request = build_game_creator_agent_background_tool_plan_request(
@@ -312,13 +314,14 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
observations,
loop_index,
)?;
let live_manifest_dag_in_progress_after = run_profile
== AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
let live_manifest_dag_in_progress_after = !relaxed_autonomous
&& run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
&& autonomous_manifest_dag_in_progress_at(root)?;
let request_bound_manifest_dag_in_progress = live_manifest_dag_in_progress_before
|| live_manifest_dag_in_progress_after
|| request.4.supervisor_manifest_dag_in_progress;
let request_bound_manifest_dag_in_progress = !relaxed_autonomous
&& (live_manifest_dag_in_progress_before
|| live_manifest_dag_in_progress_after
|| request.4.supervisor_manifest_dag_in_progress);
(request, request_bound_manifest_dag_in_progress)
};
estimated_input_tokens = estimate_game_creator_llm_request_tokens(&built_request.2)?;
@@ -419,20 +422,23 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
} else {
AGENT_RUNTIME_TOOL_PLAN_FORMAT_REPAIR_ATTEMPTS
};
let task_text_requires_read_only_delivery = run_profile
== AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
let task_text_requires_read_only_delivery = !relaxed_autonomous
&& run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& agent_runtime_task_requires_read_only_delivery(agent_id, task);
let read_only_delivery = run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
let read_only_delivery = !relaxed_autonomous
&& run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& agent_runtime_task_requires_read_only_delivery_at(
root, agent_id, session_id, run_id, task,
)?;
let runtime_owner_artifact_validation_available = run_profile
== AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
let runtime_owner_artifact_validation_available = !relaxed_autonomous
&& run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& autonomous_owner_artifact_validation_available_for_run_at(root, agent_id, run_id)?;
let code_prototype_requires_static_smoke = run_profile
== AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
let code_prototype_requires_static_smoke = !relaxed_autonomous
&& run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& agent_id == "code-prototype";
let verified_delivery = if run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD {
let verified_delivery = if !relaxed_autonomous
&& run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
{
let verification_gate =
read_game_creator_agent_runtime_verification_gate(root, agent_id, run_id)?;
runtime_owner_artifact_validation_available
@@ -444,8 +450,8 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
false
};
let allow_runtime_plan_completion = read_only_delivery || verified_delivery;
let autonomous_project_verify_available = run_profile
!= AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
let autonomous_project_verify_available = relaxed_autonomous
|| run_profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|| (!runtime_owner_artifact_validation_available
&& agent_runtime_autonomous_project_verify_available(root));
let mut autonomous_scaffold_repair_active = false;
@@ -608,6 +614,9 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
parsed
})
.and_then(|parsed| {
if relaxed_autonomous {
return Ok((parsed, None));
}
validate_root_goal_contract_control_plan_at(root, agent_id, run_id, &parsed.plan)
.map_err(|error| {
AgentRuntimeToolPlanProtocolError::new(
@@ -703,7 +712,8 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
});
let parsed = match parsed {
Ok((parsed, source_payload))
if run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD =>
if run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& !relaxed_autonomous =>
{
let verification_gate =
read_game_creator_agent_runtime_verification_gate(root, agent_id, run_id)?;
@@ -745,7 +755,8 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
};
let parsed = match parsed {
Ok((parsed, source_payload))
if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID =>
if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
&& !relaxed_autonomous =>
{
let collaboration_policy =
resolve_supervisor_collaboration_policy_for_run_at(root, agent_id, run_id)?
@@ -965,16 +976,16 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
request
.messages
.push(LlmMessage::assistant(response_preview));
let force_autonomous_pre_mutation = run_profile
== AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
let force_autonomous_pre_mutation = !relaxed_autonomous
&& run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& protocol_error.starts_with(AGENT_RUNTIME_AUTONOMOUS_LIVENESS_ERROR_PREFIX)
&& !request.function_tools.is_empty();
let force_autonomous_read_only_delivery = read_only_delivery
&& (force_autonomous_pre_mutation
|| protocol_error
.starts_with(AGENT_RUNTIME_AUTONOMOUS_READ_ONLY_MUTATION_ERROR_PREFIX));
let force_autonomous_specialist_mutation_only = run_profile
== AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
let force_autonomous_specialist_mutation_only = !relaxed_autonomous
&& run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& !read_only_delivery
&& agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
&& (force_autonomous_pre_mutation
@@ -982,67 +993,67 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
AGENT_RUNTIME_AUTONOMOUS_SPECIALIST_MUTATION_ONLY_REPAIR_ERROR_PREFIX,
))
&& !request.function_tools.is_empty();
let force_autonomous_specialist_verification_only = run_profile
== AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
let force_autonomous_specialist_verification_only = !relaxed_autonomous
&& run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& protocol_error.starts_with(
AGENT_RUNTIME_AUTONOMOUS_SPECIALIST_VERIFICATION_ONLY_REPAIR_ERROR_PREFIX,
)
&& !request.function_tools.is_empty();
let force_autonomous_pending_verification = run_profile
== AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
let force_autonomous_pending_verification = !relaxed_autonomous
&& run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& protocol_error.starts_with(
AGENT_RUNTIME_AUTONOMOUS_PENDING_VERIFICATION_LIVENESS_ERROR_PREFIX,
)
&& !request.function_tools.is_empty();
let force_autonomous_reverify_after_mutation = run_profile
== AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
let force_autonomous_reverify_after_mutation = !relaxed_autonomous
&& run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& protocol_error.starts_with(
AGENT_RUNTIME_AUTONOMOUS_REVERIFY_AFTER_MUTATION_LIVENESS_ERROR_PREFIX,
)
&& !request.function_tools.is_empty();
let force_autonomous_supervisor_delivery_convergence = run_profile
== AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
let force_autonomous_supervisor_delivery_convergence = !relaxed_autonomous
&& run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& protocol_error.starts_with(
AGENT_RUNTIME_AUTONOMOUS_SUPERVISOR_DELIVERY_CONVERGENCE_LIVENESS_ERROR_PREFIX,
)
&& !request.function_tools.is_empty();
let force_autonomous_manifest_dag_wait = run_profile
== AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
let force_autonomous_manifest_dag_wait = !relaxed_autonomous
&& run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& protocol_error.starts_with(
AGENT_RUNTIME_AUTONOMOUS_MANIFEST_DAG_WAIT_LIVENESS_ERROR_PREFIX,
)
&& !request.function_tools.is_empty();
let force_autonomous_preview_after_static = run_profile
== AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
let force_autonomous_preview_after_static = !relaxed_autonomous
&& run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& protocol_error.starts_with(
AGENT_RUNTIME_AUTONOMOUS_PREVIEW_AFTER_STATIC_LIVENESS_ERROR_PREFIX,
)
&& !request.function_tools.is_empty();
let force_autonomous_verified_delivery = run_profile
== AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
let force_autonomous_verified_delivery = !relaxed_autonomous
&& run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& protocol_error.starts_with(
AGENT_RUNTIME_AUTONOMOUS_VERIFIED_DELIVERY_LIVENESS_ERROR_PREFIX,
)
&& !request.function_tools.is_empty();
let force_autonomous_failed_playtest = run_profile
== AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
let force_autonomous_failed_playtest = !relaxed_autonomous
&& run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& protocol_error.starts_with(
AGENT_RUNTIME_AUTONOMOUS_FAILED_PLAYTEST_LIVENESS_ERROR_PREFIX,
)
&& !request.function_tools.is_empty();
let force_autonomous_delegated_playtest_repair = run_profile
== AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
let force_autonomous_delegated_playtest_repair = !relaxed_autonomous
&& run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& protocol_error.starts_with(
AGENT_RUNTIME_AUTONOMOUS_DELEGATED_PLAYTEST_REPAIR_LIVENESS_ERROR_PREFIX,
)
&& !request.function_tools.is_empty();
let force_autonomous_response_plan_completion = run_profile
== AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
let force_autonomous_response_plan_completion = !relaxed_autonomous
&& run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& protocol_error
.starts_with(AGENT_RUNTIME_AUTONOMOUS_RESPONSE_PLAN_LIVENESS_ERROR_PREFIX)
&& !request.function_tools.is_empty();
let force_autonomous_truncated_scaffold = run_profile
== AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
let force_autonomous_truncated_scaffold = !relaxed_autonomous
&& run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& protocol_error
.starts_with(AGENT_RUNTIME_AUTONOMOUS_TRUNCATED_SCAFFOLD_ERROR_PREFIX)
&& !request.function_tools.is_empty();
@@ -58,11 +58,19 @@ pub(in crate::agent) fn autonomous_supervisor_run_status_can_schedule_ready_task
agent_id: &str,
run_id: &str,
) -> Result<bool, String> {
if autonomous_relaxed_run_at(root, agent_id, run_id)
.map_err(|error| format!("读取 Agent Runtime Run Profile 绑定失败:{error}"))?
{
return Ok(true);
}
let (profile, _) = agent_runtime_run_profile_identity_at(root, agent_id, run_id, None, None)
.map_err(|error| format!("读取 Agent Runtime Run Profile 绑定失败:{error}"))?;
if profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD {
return Ok(false);
}
// The autonomous-game-build lane deliberately has no receipt/acceptance
// barrier. Once the profile binding is readable, a supervisor status
// observation may trigger the independent manifest wave immediately.
let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
root,
"runtime.run_status.schedule_ready",
@@ -97,7 +97,7 @@ fn autonomous_run_status_schedule_propagates_profile_binding_read_error() {
}
#[test]
fn autonomous_run_status_schedule_propagates_static_barrier_read_error() {
fn autonomous_run_status_schedule_ignores_static_barrier_read_error_in_relaxed_lane() {
let run_id = "run-status-corrupt-static-barrier";
let root = init_autonomous_run_status_observation_test_project("corrupt-barrier", run_id);
let delivery_dir = root.join(".agent/runtime/delegation-deliveries");
@@ -105,19 +105,19 @@ fn autonomous_run_status_schedule_propagates_static_barrier_read_error() {
std::fs::write(delivery_dir.join("corrupt-delivery.json"), b"{not-json")
.expect("corrupt static delivery");
let error = autonomous_supervisor_run_status_can_schedule_ready_tasks_at(
let can_schedule = autonomous_supervisor_run_status_can_schedule_ready_tasks_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
)
.expect_err("corrupt static barrier must reach pending reconciliation");
assert!(error.contains("读取专业 Agent 静态委派完成屏障失败"));
.expect("relaxed lane must not read the static barrier");
assert!(can_schedule);
std::fs::remove_dir_all(root).ok();
}
#[test]
fn autonomous_run_status_schedule_waits_for_real_static_barrier() {
fn autonomous_run_status_schedule_ignores_real_static_barrier_in_relaxed_lane() {
let run_id = "run-status-waiting-static-barrier";
let root = init_autonomous_run_status_observation_test_project("waiting-barrier", run_id);
let delivery = new_static_delegate_delivery(
@@ -134,12 +134,12 @@ fn autonomous_run_status_schedule_waits_for_real_static_barrier() {
.expect("create waiting static delivery");
assert!(
!autonomous_supervisor_run_status_can_schedule_ready_tasks_at(
autonomous_supervisor_run_status_can_schedule_ready_tasks_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
)
.expect("real static barrier should not be an error")
.expect("relaxed lane should not inspect the static barrier")
);
std::fs::remove_dir_all(root).ok();
@@ -538,14 +538,21 @@ pub(crate) fn agent_runtime_tool_policy_snapshot_for_run_at(
snapshot.auto_tools.push(tool.to_string());
}
}
// The relaxed autonomous lane has no confirmation consumer. Promote
// every remaining confirmation-only tool to auto execution, while
// retaining explicit project/Agent denies above. The narrower role and
// capability gates run before this loop, so a denied tool is never
// resurrected by the promotion.
for tool in std::mem::take(&mut snapshot.confirm_tools) {
if !snapshot
if snapshot
.denied_tools
.iter()
.any(|candidate| candidate == &tool)
{
snapshot.denied_tools.push(tool);
continue;
}
snapshot.auto_tools.retain(|candidate| candidate != &tool);
snapshot.auto_tools.push(tool);
}
Ok(snapshot)
}
@@ -201,6 +201,7 @@ pub(in crate::agent) fn resume_game_creator_agent_finalization_at(
if state.run_id.trim().is_empty() {
return Ok(AgentRuntimeFinalizationResume::NotFound(runtime_lock));
}
let relaxed_autonomous = autonomous_relaxed_profile(&state);
let mut journal =
match read_game_creator_agent_runtime_finalization_journal(root, agent_id, &state.run_id) {
Ok(Some(journal)) => journal,
@@ -243,8 +244,9 @@ pub(in crate::agent) fn resume_game_creator_agent_finalization_at(
)?;
let assistant_exists =
game_creator_agent_runtime_finalization_assistant_exists(root, &journal)?;
match classify_game_creator_agent_runtime_finalization_goal_snapshot_at(root, &journal, &state)?
{
if !relaxed_autonomous {
match classify_game_creator_agent_runtime_finalization_goal_snapshot_at(root, &journal, &state)?
{
AgentRuntimeFinalizationGoalSnapshotRelation::Matches => {}
AgentRuntimeFinalizationGoalSnapshotRelation::StaleRevision {
journal_revision,
@@ -287,8 +289,9 @@ pub(in crate::agent) fn resume_game_creator_agent_finalization_at(
return read_game_creator_agent_runtime_at(root, agent_id)
.map(AgentRuntimeFinalizationResume::Blocked);
}
}
}
if assistant_exists
if !relaxed_autonomous && assistant_exists
&& !state_reconstructed_from_task
&& !game_creator_agent_runtime_finalization_plan_matches_state(&journal, &state)
{
@@ -297,7 +300,7 @@ pub(in crate::agent) fn resume_game_creator_agent_finalization_at(
return read_game_creator_agent_runtime_at(root, agent_id)
.map(AgentRuntimeFinalizationResume::Blocked);
}
if state_reconstructed_from_task && !assistant_exists {
if !relaxed_autonomous && state_reconstructed_from_task && !assistant_exists {
let error = "Agent Runtime finalization 恢复已阻断:Runtime state 缺失,不能从 task record 猜测结构化计划快照";
state.status = "failed".to_string();
state.phase = "needs-reconciliation".to_string();
@@ -392,46 +395,52 @@ pub(in crate::agent) fn resume_game_creator_agent_finalization_at(
));
}
if journal.status == AGENT_RUNTIME_FINALIZATION_STATUS_PREPARED && !assistant_exists {
let current_revision = read_game_creator_agent_runtime_project_revision(root)?;
let blocker = if let Some(blocker) = structured_plan_completion_blocker(&state) {
Some(blocker)
} else if let Some(blocker) =
plan_gdd_completion_blocker_at_locked(root, &journal.agent_id, &journal.run_id)
{
Some(blocker)
} else if let Some(blocker) =
game_creator_agent_goal_completion_blocker_at_locked(root, &state)
{
Some(blocker)
} else if let Some(blocker) =
goal_contract_acceptance_completion_blocker_at_locked(root, &state)
{
Some(blocker)
} else if let Some(blocker) = agent_runtime_non_verification_completion_blocker_at_locked(
root,
&journal.agent_id,
&journal.run_id,
) {
Some(blocker)
} else if let Some(blocker) =
autonomous_game_build_completion_blocker_at_locked(root, &state)
{
Some(blocker)
} else if current_revision.revision != journal.response_revision {
Some(agent_runtime_verification_blocker(
"恢复时最终回复基于的项目 revision 已过期",
format!(
"responseRevision={}, currentRevision={};旧 finalization 已丢弃,将在同一 run 重新规划。",
journal.response_revision, current_revision.revision
),
))
let blocker = if relaxed_autonomous {
// A relaxed finalization journal is valid independently of
// manifest/DAG, project revision and platform verification state.
None
} else {
evaluate_project_verification_completion_at_locked(
root,
let current_revision = read_game_creator_agent_runtime_project_revision(root)?;
if let Some(blocker) = structured_plan_completion_blocker(&state) {
Some(blocker)
} else if let Some(blocker) =
plan_gdd_completion_blocker_at_locked(&root, &journal.agent_id, &journal.run_id)
{
Some(blocker)
} else if let Some(blocker) =
game_creator_agent_goal_completion_blocker_at_locked(&root, &state)
{
Some(blocker)
} else if let Some(blocker) =
goal_contract_acceptance_completion_blocker_at_locked(&root, &state)
{
Some(blocker)
} else if let Some(blocker) = agent_runtime_non_verification_completion_blocker_at_locked(
&root,
&journal.agent_id,
&journal.run_id,
&[],
)?
) {
Some(blocker)
} else if let Some(blocker) =
autonomous_game_build_completion_blocker_at_locked(&root, &state)
{
Some(blocker)
} else if current_revision.revision != journal.response_revision {
Some(agent_runtime_verification_blocker(
"恢复时最终回复基于的项目 revision 已过期",
format!(
"responseRevision={}, currentRevision={};旧 finalization 已丢弃,将在同一 run 重新规划。",
journal.response_revision, current_revision.revision
),
))
} else {
evaluate_project_verification_completion_at_locked(
&root,
&journal.agent_id,
&journal.run_id,
&[],
)?
}
};
if let Some(blocker) = blocker {
remove_game_creator_agent_runtime_finalization_recovery_sidecars(
@@ -172,6 +172,15 @@ pub(in crate::agent) fn pending_action_pre_execution_drift_observation(
root: &Path,
pending: &AgentRuntimePendingToolAction,
) -> Result<Option<AgentRuntimeToolObservation>, String> {
// The autonomous game-build lane deliberately permits independent child
// runs to mutate the same project concurrently. Repository fingerprints,
// project revisions and verification snapshots are delivery-time hints in
// this lane, not a reason to reject an otherwise valid pending action.
// Keep the durable identity/tool-policy checks elsewhere, but do not turn
// a sibling write into a stale-action retry loop.
if autonomous_relaxed_run_profile(&pending.run_profile) {
return Ok(None);
}
if let Some(observation) = pending_repository_context_drift_observation(root, pending)? {
return Ok(Some(observation));
}
@@ -498,12 +498,15 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
let mut final_reply_revision = None;
let mut converged = false;
let mut context_stalled = continuation.context_stalled;
let relaxed_autonomous = autonomous_relaxed_profile(&runtime);
// `project_game_creator_agent_runtime_provider_batch_abort` persists the
// rejection counter together with the rejected observation before this
// terminal transition. A crash between those two durable steps must not
// turn the fifth rejection into a sixth Provider request after recovery.
if plan_submit_business_rejection_limit_reached(runtime.plan_submit_gdd_rejection_count) {
if !relaxed_autonomous
&& plan_submit_business_rejection_limit_reached(runtime.plan_submit_gdd_rejection_count)
{
return match finish_plan_submit_business_rejection_limit_at(&root, &runtime) {
Ok(outcome) => outcome,
Err(error) => fail_game_creator_agent_background_context_at(
@@ -518,7 +521,7 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
// 和上面同理:计数已经随上一轮的 blocker 一起落盘,恢复后不能把第 N 次空转
// 变成第 N+1 次 Provider 请求。
if plan_update_idle_limit_reached(runtime.plan_update_idle_rounds) {
if !relaxed_autonomous && plan_update_idle_limit_reached(runtime.plan_update_idle_rounds) {
return match finish_plan_update_idle_limit_at(&root, &runtime) {
Ok(outcome) => outcome,
Err(error) => fail_game_creator_agent_background_context_at(
@@ -649,82 +652,147 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
}
};
if !consumed_steer {
if let Some(blocker) =
isolated_join_completion_blocker_at(&root, &agent_id, &runtime.run_id)
{
let waits_for_join = blocker
.detail
.as_deref()
.is_some_and(isolated_join_barrier_has_waiting_groups);
if waits_for_join {
if let Err(error) = persist_waiting_isolated_parent_context_at(
&root,
&mut runtime,
&task,
&plan,
&mut observations,
loop_index,
&mut context_tracker,
blocker,
) {
return fail_game_creator_agent_background_context_at(
if !relaxed_autonomous {
if let Some(blocker) =
isolated_join_completion_blocker_at(&root, &agent_id, &runtime.run_id)
{
let waits_for_join = blocker
.detail
.as_deref()
.is_some_and(isolated_join_barrier_has_waiting_groups);
if waits_for_join {
if let Err(error) = persist_waiting_isolated_parent_context_at(
&root,
&agent_id,
&session_id,
runtime,
&format!("持久化动态隔离 Agent all-join 等待状态失败:{error}"),
);
&mut runtime,
&task,
&plan,
&mut observations,
loop_index,
&mut context_tracker,
blocker,
) {
return fail_game_creator_agent_background_context_at(
&root,
&agent_id,
&session_id,
runtime,
&format!("持久化动态隔离 Agent all-join 等待状态失败:{error}"),
);
}
return AgentBackgroundTaskOutcome::WaitingForIsolatedJoin;
}
}
if let Some(blocker) =
static_delegate_completion_blocker_at(&root, &agent_id, &runtime.run_id)
{
let waits_for_delivery = blocker.detail.as_deref().is_some_and(|detail| {
static_delegate_barrier_has_waiting_deliveries(detail)
|| static_delegate_barrier_requires_user_input(detail)
});
if waits_for_delivery {
if let Err(error) = persist_waiting_static_delegate_parent_context_at(
&root,
&mut runtime,
&task,
&plan,
&mut observations,
loop_index,
&mut context_tracker,
blocker,
) {
return fail_game_creator_agent_background_context_at(
&root,
&agent_id,
&session_id,
runtime,
&format!("持久化专业 Agent 回执等待状态失败:{error}"),
);
}
return AgentBackgroundTaskOutcome::WaitingForDelegateReceipts;
}
return AgentBackgroundTaskOutcome::WaitingForIsolatedJoin;
}
}
if let Some(blocker) =
static_delegate_completion_blocker_at(&root, &agent_id, &runtime.run_id)
{
let waits_for_delivery = blocker.detail.as_deref().is_some_and(|detail| {
static_delegate_barrier_has_waiting_deliveries(detail)
|| static_delegate_barrier_requires_user_input(detail)
});
if waits_for_delivery {
if let Err(error) = persist_waiting_static_delegate_parent_context_at(
&root,
&mut runtime,
&task,
&plan,
&mut observations,
loop_index,
&mut context_tracker,
blocker,
) {
return fail_game_creator_agent_background_context_at(
&root,
&agent_id,
&session_id,
runtime,
&format!("持久化专业 Agent 回执等待状态失败:{error}"),
);
}
return AgentBackgroundTaskOutcome::WaitingForDelegateReceipts;
}
}
let autonomous_manifest_parent_can_wait = agent_id
== GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
&& runtime.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
// In the relaxed lane the manifest graph is an opportunistic
// launch list, never a parent-run state machine. Start any
// pending seed tasks now, but deliberately ignore scheduler
// errors and all task/DAG state so one unavailable specialist
// cannot stop the Supervisor's own Provider loop.
if relaxed_autonomous
&& agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
&& !game_creator_agent_runtime_provider_action_batch_exists(
&root,
&agent_id,
&runtime.run_id,
)
&& supervisor_collaboration_policy_completion_blocker_at_locked(
{
if let Err(error) = schedule_autonomous_game_build_ready_tasks_at(
&root,
&agent_id,
&runtime.run_id,
)
.is_none()
&& isolated_join_completion_blocker_at(&root, &agent_id, &runtime.run_id).is_none()
&& static_delegate_completion_blocker_at(&root, &agent_id, &runtime.run_id)
.is_none();
if autonomous_manifest_parent_can_wait {
3,
) {
let _ = append_agent_db_record(
&root,
serde_json::json!({
"recordType": "agent.runtime.autonomous_ready_task.schedule_diagnostic",
"agentId": agent_id,
"runId": runtime.run_id,
"relaxedOrchestration": true,
"errorSha256": format!("{:x}", Sha256::digest(error.as_bytes())),
"errorChars": error.chars().count(),
}),
);
}
}
// The strict/legacy lane retains the existing manifest wait and
// completion semantics. Keeping it physically separate makes it
// impossible for a relaxed run to read the DAG and accidentally
// re-enter `waiting-for-manifest-tasks`.
if !relaxed_autonomous {
let autonomous_root_goal_contract_persisted =
if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
&& runtime.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
{
match autonomous_root_goal_contract_persisted_at(
&root,
&agent_id,
&runtime.run_id,
) {
Ok(value) => value,
Err(error) => {
return fail_game_creator_agent_background_context_at(
&root,
&agent_id,
&session_id,
runtime,
&format!("读取自主构建根 Goal Contract 门失败:{error}"),
);
}
}
} else {
false
};
let autonomous_manifest_parent_can_wait =
autonomous_root_goal_contract_persisted
&& agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
&& runtime.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& !game_creator_agent_runtime_provider_action_batch_exists(
&root,
&agent_id,
&runtime.run_id,
)
&& supervisor_collaboration_policy_completion_blocker_at_locked(
&root,
&agent_id,
&runtime.run_id,
)
.is_none()
&& isolated_join_completion_blocker_at(&root, &agent_id, &runtime.run_id)
.is_none()
&& static_delegate_completion_blocker_at(&root, &agent_id, &runtime.run_id)
.is_none();
if autonomous_manifest_parent_can_wait {
let manifest_state_before_schedule = match autonomous_manifest_dag_state_at(&root) {
Ok(value) => value,
Err(error) => {
@@ -743,11 +811,10 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
// 同理,已有失败任务时只能等待已在途 child 收束或立即安全失败,不能再
// 启动新的 pending sibling 并用它遮蔽原始失败。
let manifest_scheduler_blocked = matches!(
&manifest_state_before_schedule,
AutonomousManifestDagState::Completed
| AutonomousManifestDagState::Failed { .. }
)
|| autonomous_registered_derived_visuals_block_manifest_scheduler_at(
&manifest_state_before_schedule,
AutonomousManifestDagState::Completed
| AutonomousManifestDagState::Failed { .. }
) || autonomous_registered_derived_visuals_block_manifest_scheduler_at(
&root, &runtime,
);
let scheduled_ready_tasks = if manifest_scheduler_blocked {
@@ -846,6 +913,7 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
}
}
}
}
let mut resumed_provider_batch = if game_creator_agent_runtime_provider_action_batch_exists(
&root,
&agent_id,
@@ -1256,7 +1324,8 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
);
}
}
if runtime.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
if !relaxed_autonomous
&& runtime.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& !plan.response.trim().is_empty()
{
let read_only_delivery =
@@ -1564,55 +1633,136 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
// blocked 的 plan_gdd blocker 有三种截然不同的继续推进态,phase 与
// next_step 必须按类型化子状态选,不能回去猜 detail 字符串。
let mut plan_gdd_blocker_kind: Option<PlanGddCompletionBlockerKind> = None;
let completion_blocker = structured_plan_completion_blocker(&runtime)
.or_else(|| {
provider_action_batch_completion_blocker_at_locked(
&root,
&agent_id,
&runtime.run_id,
)
})
.or_else(|| {
plan_gdd_typed_completion_blocker_at_locked(&root, &agent_id, &runtime.run_id)
let completion_blocker = if relaxed_autonomous {
// In the free-form lane only an in-flight provider batch, a
// live process session, or the minimal root-entry check may
// hold the run. Manifest/Goal/acceptance/visual/verification
// receipts are observations, never execution gates.
provider_action_batch_completion_blocker_at_locked(
&root,
&agent_id,
&runtime.run_id,
)
.or_else(|| process_session_completion_blocker_at(&root, &agent_id, &runtime.run_id))
.or_else(|| autonomous_game_build_completion_blocker_at_locked(&root, &runtime))
} else {
structured_plan_completion_blocker(&runtime)
.or_else(|| {
provider_action_batch_completion_blocker_at_locked(
&root,
&agent_id,
&runtime.run_id,
)
})
.or_else(|| {
plan_gdd_typed_completion_blocker_at_locked(
&root,
&agent_id,
&runtime.run_id,
)
.map(|blocker| {
plan_gdd_blocker_kind = Some(blocker.kind);
blocker.observation
})
})
.or_else(|| game_creator_agent_goal_completion_blocker_at_locked(&root, &runtime))
.or_else(|| goal_contract_acceptance_completion_blocker_at_locked(&root, &runtime))
.or_else(|| {
supervisor_collaboration_policy_completion_blocker_at_locked(
&root,
&agent_id,
&runtime.run_id,
)
})
.or_else(|| {
process_session_completion_blocker_at(&root, &agent_id, &runtime.run_id)
})
.or_else(|| isolated_join_completion_blocker_at(&root, &agent_id, &runtime.run_id))
.or_else(|| {
static_delegate_completion_blocker_at(&root, &agent_id, &runtime.run_id)
})
.or_else(|| {
visual_asset_completion_blocker_at_locked(
&root,
&agent_id,
Some(&runtime.run_id),
)
})
.or_else(|| {
project_verification_completion_blocker_at(
&root,
&agent_id,
&runtime.run_id,
&observations,
)
})
.or_else(|| autonomous_game_build_completion_blocker_at_locked(&root, &runtime));
})
.or_else(|| {
game_creator_agent_goal_completion_blocker_at_locked(&root, &runtime)
})
.or_else(|| goal_contract_acceptance_completion_blocker_at_locked(&root, &runtime))
.or_else(|| {
supervisor_collaboration_policy_completion_blocker_at_locked(
&root,
&agent_id,
&runtime.run_id,
)
})
.or_else(|| {
process_session_completion_blocker_at(&root, &agent_id, &runtime.run_id)
})
.or_else(|| {
isolated_join_completion_blocker_at(&root, &agent_id, &runtime.run_id)
})
.or_else(|| {
static_delegate_completion_blocker_at(&root, &agent_id, &runtime.run_id)
})
.or_else(|| {
visual_asset_completion_blocker_at_locked(
&root,
&agent_id,
Some(&runtime.run_id),
)
})
.or_else(|| {
project_verification_completion_blocker_at(
&root,
&agent_id,
&runtime.run_id,
&observations,
)
})
.or_else(|| autonomous_game_build_completion_blocker_at_locked(&root, &runtime))
};
if let Some(blocker) = completion_blocker {
let blocker_summary = blocker.summary();
// code-prototype is allowed to start before the art wave. If
// its only missing completion evidence is the still-running
// art owner, park this child instead of asking the Provider to
// repair a resource that has not been generated yet. A
// failed art owner is terminal for this child and must not
// become an endless waiting loop.
if !relaxed_autonomous && blocker.tool == "runtime.autonomous_completion" {
let art_wait_state =
match autonomous_code_prototype_art_asset_wait_state_at(&root, &runtime) {
Ok(value) => value,
Err(error) => {
return fail_game_creator_agent_background_context_at(
&root,
&agent_id,
&session_id,
runtime,
&format!("读取 code-prototype 美术依赖等待状态失败:{error}"),
);
}
};
match art_wait_state {
AutonomousCodePrototypeArtAssetWaitState::Waiting => {
let next_loop_index = loop_index.saturating_add(1);
if let Err(error) =
persist_waiting_autonomous_manifest_child_context_at(
&root,
&mut runtime,
&task,
&plan,
&mut observations,
next_loop_index,
&mut context_tracker,
blocker,
)
{
return fail_game_creator_agent_background_context_at(
&root,
&agent_id,
&session_id,
runtime,
&format!(
"持久化 code-prototype 美术依赖等待状态失败:{error}"
),
);
}
return AgentBackgroundTaskOutcome::WaitingForManifestTasks;
}
AutonomousCodePrototypeArtAssetWaitState::DependencyFailed(reason) => {
return fail_game_creator_agent_background_context_at(
&root,
&agent_id,
&session_id,
runtime,
&reason,
);
}
AutonomousCodePrototypeArtAssetWaitState::NotWaiting => {}
}
}
if blocker.tool == "runtime.plan_update" {
// 走到这里说明本轮没有任何动作,而且未完成的原因就是计划自己。
// 等委派回执、等 provider 批次、等用户问询都是别的 blocker 类型,
@@ -178,6 +178,51 @@ async fn drive_waiting_autonomous_manifest_parent_wake_pass_with_budget(
reconciliation_delay_ms: u64,
request_deferred_rerun: bool,
) -> Result<(), String> {
if read_latest_game_creator_agent_runtime_task_by_run_id(root, agent_id, run_id)?
.is_some_and(|task| {
task.status == "running"
&& task.phase == "waiting-for-manifest-tasks"
&& autonomous_relaxed_run_profile(&task.run_profile)
})
{
// Free-form autonomous runs never reconcile a manifest barrier. The
// phase can only be legacy durable state, so make a bounded attempt to
// resume it and leave any lane contention for the ordinary recovery
// scan instead of converting it into `needs-reconciliation`.
for _ in 0..max_attempts.max(1) {
if retry_delay_ms > 0 {
tokio::time::sleep(Duration::from_millis(retry_delay_ms)).await;
}
let Some(task) =
read_latest_game_creator_agent_runtime_task_by_run_id(root, agent_id, run_id)?
else {
return Ok(());
};
if task.status != "running" || task.phase != "waiting-for-manifest-tasks" {
return Ok(());
}
match wake_waiting_autonomous_manifest_parent_run_at(root, &task) {
Ok(true) => return Ok(()),
Ok(false) => continue,
Err(error) if autonomous_manifest_parent_wake_error_is_transient(&error) => {
continue;
}
Err(error) => return Err(error),
}
}
return Ok(());
}
// `WaitingForManifestTasks` is also used by a ready-task child that has
// finished its code but is waiting for the art owner. That child must not
// enter the root-only reconciliation protocol below. Its wake is driven
// by the same deterministic parent scheduler once art-asset-plan lands.
if read_latest_game_creator_agent_runtime_task_by_run_id(root, agent_id, run_id)
.ok()
.flatten()
.is_some_and(|task| autonomous_manifest_ready_task_waiting_child_record(&task))
{
return drive_waiting_autonomous_manifest_child_wake_pass(root, agent_id, run_id).await;
}
if let Some(deferred_error) =
read_autonomous_manifest_parent_wake_reconciliation_signal_at(root, agent_id, run_id)?
{
@@ -262,6 +307,97 @@ async fn drive_waiting_autonomous_manifest_parent_wake_pass_with_budget(
.await
}
pub(in crate::agent) fn autonomous_manifest_ready_task_waiting_child_record(
task: &AgentRuntimeTaskRecord,
) -> bool {
task.agent_id == "code-prototype"
&& task.task_id == "code-prototype"
&& task.status == "running"
&& task.phase == "waiting-for-manifest-tasks"
&& task.source == "agent-ready-task-scheduler"
&& task.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& task.parent_agent_id.as_deref() == Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID)
&& task.parent_run_id.as_deref().is_some_and(|value| !value.trim().is_empty())
&& task.delegation_id.is_none()
}
async fn drive_waiting_autonomous_manifest_child_wake_pass(
root: &Path,
agent_id: &str,
run_id: &str,
) -> Result<(), String> {
for _ in 0..AUTONOMOUS_MANIFEST_PARENT_WAKE_MAX_ATTEMPTS {
let Some(task) = read_latest_game_creator_agent_runtime_task_by_run_id(
root, agent_id, run_id,
)? else {
return Ok(());
};
if !autonomous_manifest_ready_task_waiting_child_record(&task) {
return Ok(());
}
let state = agent_runtime_state_from_task_record(&task);
match autonomous_code_prototype_art_asset_wait_state_at(root, &state)? {
AutonomousCodePrototypeArtAssetWaitState::Waiting => {
// The art child will issue the next wake after its terminal
// projection. Do not poll the Provider or spin here.
return Ok(());
}
AutonomousCodePrototypeArtAssetWaitState::DependencyFailed(reason) => {
let Some(runtime_lock) =
try_acquire_game_creator_agent_runtime_task_lock(root, agent_id)?
else {
tokio::time::sleep(Duration::from_millis(
AUTONOMOUS_MANIFEST_PARENT_WAKE_RETRY_DELAY_MS,
))
.await;
continue;
};
let current = read_game_creator_agent_runtime_for_session_at(
root,
agent_id,
Some(&task.session_id),
)?
.state;
if current.run_id == run_id
&& current.phase == "waiting-for-manifest-tasks"
&& current.status == "running"
{
let _ = fail_game_creator_agent_runtime_turn_at(root, current, &reason)?;
}
drop(runtime_lock);
return Ok(());
}
AutonomousCodePrototypeArtAssetWaitState::NotWaiting => {
let parent_agent_id = task
.parent_agent_id
.as_deref()
.ok_or_else(|| "code-prototype 等待态缺少 parentAgentId".to_string())?;
let parent_run_id = task
.parent_run_id
.as_deref()
.ok_or_else(|| "code-prototype 等待态缺少 parentRunId".to_string())?;
let scheduled = schedule_autonomous_game_build_ready_tasks_at(
root,
parent_agent_id,
parent_run_id,
3,
)?;
if !scheduled.is_empty() {
return Ok(());
}
// The parent scheduler may be racing the art terminal
// projection. Give that projection a short bounded window;
// recovery scan remains the durable fallback.
tokio::time::sleep(Duration::from_millis(
AUTONOMOUS_MANIFEST_PARENT_WAKE_RETRY_DELAY_MS,
))
.await;
}
}
}
Ok(())
}
async fn settle_autonomous_manifest_parent_wake_needs_reconciliation_at(
root: &Path,
agent_id: &str,
@@ -1607,6 +1743,70 @@ pub(in crate::agent) fn persist_waiting_autonomous_manifest_parent_context_at(
Ok(())
}
/// Persist the short-lived wait used by a manifest ready-task child whose
/// code is complete but whose art owner has not delivered its assets yet.
/// This deliberately uses the existing `waiting-for-manifest-tasks` phase so
/// restart/recovery and the deterministic child Run ID stay unchanged; the
/// wake dispatcher distinguishes this child from the root Supervisor.
pub(in crate::agent) fn persist_waiting_autonomous_manifest_child_context_at(
root: &Path,
runtime: &mut AgentRuntimeState,
task: &str,
plan: &AgentRuntimeToolPlan,
observations: &mut Vec<AgentRuntimeToolObservation>,
next_loop_index: usize,
context_tracker: &mut AgentRuntimeContextWindowTracker,
blocker: AgentRuntimeToolObservation,
) -> Result<(), String> {
let blocker_summary = blocker.summary();
let blocker_detail = blocker.detail.clone();
runtime.status = "running".to_string();
runtime.phase = "waiting-for-manifest-tasks".to_string();
runtime.current_action = "等待美术资产任务收束".to_string();
runtime.waiting_on = "art-asset-plan 完成并交付真实美术资源".to_string();
runtime.next_step = "美术回执到位后恢复同一个 code-prototype run".to_string();
runtime.observations.push(blocker_summary.clone());
runtime.updated_at = unix_timestamp();
context_tracker.record(&blocker);
observations.push(blocker);
persist_game_creator_agent_runtime_context(
root,
runtime,
task,
plan,
observations,
next_loop_index,
context_tracker,
)?;
append_game_creator_agent_runtime_task(root, runtime)?;
refresh_game_creator_agent_runtime_task_queue(root, runtime)?;
write_game_creator_agent_runtime_state(root, runtime)?;
let _ = append_game_creator_agent_runtime_event(
root,
runtime,
"observation",
"running",
"waiting-for-manifest-tasks",
&blocker_summary,
blocker_detail.as_deref(),
);
let _ = append_agent_db_record(
root,
serde_json::json!({
"recordType": "agent.runtime.autonomous_manifest.child_waiting",
"agentId": runtime.agent_id,
"taskId": runtime.task_id,
"sessionId": runtime.session_id,
"runId": runtime.run_id,
"status": "waiting-for-manifest-tasks",
"waitingOn": "art-asset-plan",
"nextLoopIndex": next_loop_index,
}),
);
emit_game_creator_agent_runtime_update(root, &runtime.agent_id);
Ok(())
}
pub(in crate::agent) fn persist_waiting_isolated_parent_context_at(
root: &Path,
runtime: &mut AgentRuntimeState,
@@ -1612,7 +1612,46 @@ pub(in crate::agent) fn resume_game_creator_agent_background_tasks_unredacted_at
Ok(_) => {}
}
}
if task.phase == "waiting-for-manifest-tasks" {
if task.phase == "waiting-for-manifest-tasks"
&& !autonomous_relaxed_run_profile(&task.run_profile)
{
if autonomous_manifest_ready_task_waiting_child_record(&task) {
let state = agent_runtime_state_from_task_record(&task);
match autonomous_code_prototype_art_asset_wait_state_at(root, &state)? {
AutonomousCodePrototypeArtAssetWaitState::Waiting => {
resumed.push(read_game_creator_agent_runtime_at(root, &agent_id)?);
continue;
}
AutonomousCodePrototypeArtAssetWaitState::DependencyFailed(reason) => {
let _ = fail_game_creator_agent_runtime_turn_at(root, state, &reason)?;
resumed.push(read_game_creator_agent_runtime_at(root, &agent_id)?);
continue;
}
AutonomousCodePrototypeArtAssetWaitState::NotWaiting => {
let parent_agent_id = task.parent_agent_id.as_deref().ok_or_else(|| {
"code-prototype 等待态缺少 parentAgentId".to_string()
})?;
let parent_run_id = task.parent_run_id.as_deref().ok_or_else(|| {
"code-prototype 等待态缺少 parentRunId".to_string()
})?;
// The child execution lane is held by this recovery
// scan. Release it before the parent scheduler tries
// to reacquire the deterministic child lane.
drop(runtime_lock);
let scheduled_ready_tasks =
schedule_autonomous_game_build_ready_tasks_at(
root,
parent_agent_id,
parent_run_id,
3,
)?;
if !scheduled_ready_tasks.is_empty() {
resumed.push(read_game_creator_agent_runtime_at(root, &agent_id)?);
}
continue;
}
}
}
let scheduled_ready_tasks = match schedule_autonomous_game_build_ready_tasks_at(
root,
&task.agent_id,

Some files were not shown because too many files have changed in this diff Show More