放开 AGC 自主创作流程并补齐全流程执行能力
新增 agc_write_file 直写工具并放宽自主构建执行路径 移除固定任务依赖与确认门槛对并行创作的阻塞 同步 DirectProject、预览、运行状态和 E2E 测试调整
This commit is contained in:
@@ -26,6 +26,11 @@ const wrapperSuite =
|
||||
const configFileName = 'game-creator.config.json';
|
||||
const configSentinelName = '.deterministic-provider-e2e.json';
|
||||
const configSentinelSchema = 'genarrative-deterministic-provider-e2e-config.v1';
|
||||
const platformSessionFixtureEnv =
|
||||
'GENARRATIVE_AGC_PLATFORM_SESSION_FIXTURE';
|
||||
const platformSessionFixtureName = '.deterministic-platform-session.json';
|
||||
const platformSessionFixtureSchema =
|
||||
'genarrative-agc-platform-session-fixture.v1';
|
||||
const outputLimit = 32 * 1024 * 1024;
|
||||
|
||||
function hashValue(value) {
|
||||
@@ -77,6 +82,16 @@ function deterministicRuntimeConfig(apiKey, provider) {
|
||||
};
|
||||
}
|
||||
|
||||
function deterministicPlatformSessionFixture(apiKey, provider) {
|
||||
return {
|
||||
schemaVersion: platformSessionFixtureSchema,
|
||||
userId: 'deterministic-e2e-user',
|
||||
accessToken: apiKey,
|
||||
apiBaseUrl: provider.editorBaseUrl,
|
||||
generation: 1,
|
||||
};
|
||||
}
|
||||
|
||||
function appendBounded(current, chunk) {
|
||||
const combined = Buffer.concat([current, chunk]);
|
||||
if (combined.length > outputLimit) throw new Error('child-output-too-large');
|
||||
@@ -1909,6 +1924,12 @@ async function runE2e(options) {
|
||||
);
|
||||
provider = await startDeterministicLaneDefenseProvider({ apiKey });
|
||||
const config = deterministicRuntimeConfig(apiKey, provider);
|
||||
const fixturePath = path.join(configDir, platformSessionFixtureName);
|
||||
await fs.writeFile(
|
||||
fixturePath,
|
||||
`${JSON.stringify(deterministicPlatformSessionFixture(apiKey, provider))}\n`,
|
||||
{ flag: 'wx', mode: 0o600 },
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(configDir, configFileName),
|
||||
`${JSON.stringify(config)}\n`,
|
||||
@@ -1924,7 +1945,11 @@ async function runE2e(options) {
|
||||
if (options.keepProject) childArgs.push('--keep-project');
|
||||
childResult = await runChild(
|
||||
childArgs,
|
||||
withLoopbackNoProxy({ ...process.env, NO_COLOR: '1' }),
|
||||
withLoopbackNoProxy({
|
||||
...process.env,
|
||||
NO_COLOR: '1',
|
||||
[platformSessionFixtureEnv]: fixturePath,
|
||||
}),
|
||||
);
|
||||
childReport = parseChildReport(childResult);
|
||||
} catch (error) {
|
||||
|
||||
@@ -100,6 +100,14 @@ import { appendBounded, runProcess } from './process.mjs';
|
||||
import { decodeUtf8Fatal, isIsolatedRunnerSuite } from './reporting.mjs';
|
||||
import { killRunnerOnce, readRunnerStatus, runnerBootId } from './runtime.mjs';
|
||||
|
||||
const platformSessionFixtureEnv =
|
||||
'GENARRATIVE_AGC_PLATFORM_SESSION_FIXTURE';
|
||||
const platformSessionFixtureMaxBytes = 16 * 1024;
|
||||
const isolatedPlatformSessionFixtureName =
|
||||
'.deterministic-platform-session.json';
|
||||
const platformSessionFixtureSchema =
|
||||
'genarrative-agc-platform-session-fixture.v1';
|
||||
|
||||
export function isolatedSuiteProtectsSourceAppData() {
|
||||
return (
|
||||
isSupervisorSwarmTransientRetrySuite() ||
|
||||
@@ -498,6 +506,116 @@ export async function verifySourceAppDataDirectoryUntouched() {
|
||||
state.isolatedRunner.sourceAppDataDirectoryUntouched = true;
|
||||
}
|
||||
|
||||
async function readPlatformSessionFixtureForIsolatedSuite(sourceConfigDir) {
|
||||
const rawPath = process.env[platformSessionFixtureEnv];
|
||||
assert(
|
||||
isNonEmptyString(rawPath) && path.isAbsolute(rawPath),
|
||||
'supervisor-autonomous-playable-platform-session-fixture-missing',
|
||||
);
|
||||
const sourceRealPath = await fs.realpath(sourceConfigDir);
|
||||
const requestedPath = path.resolve(rawPath);
|
||||
const requestedMetadata = await fs.lstat(requestedPath).catch((error) => {
|
||||
if (error?.code === 'ENOENT') return null;
|
||||
throw error;
|
||||
});
|
||||
assert(
|
||||
requestedMetadata?.isFile() && !requestedMetadata.isSymbolicLink(),
|
||||
'supervisor-autonomous-playable-platform-session-fixture-not-regular',
|
||||
);
|
||||
assert(
|
||||
requestedMetadata.size <= platformSessionFixtureMaxBytes,
|
||||
'supervisor-autonomous-playable-platform-session-fixture-too-large',
|
||||
);
|
||||
const realPath = await fs.realpath(requestedPath);
|
||||
assert(
|
||||
isPathInside(sourceRealPath, realPath),
|
||||
'supervisor-autonomous-playable-platform-session-fixture-outside-config',
|
||||
);
|
||||
const bytes = await fs.readFile(realPath);
|
||||
assert(
|
||||
bytes.length <= platformSessionFixtureMaxBytes,
|
||||
'supervisor-autonomous-playable-platform-session-fixture-too-large',
|
||||
);
|
||||
let fixture;
|
||||
try {
|
||||
fixture = JSON.parse(decodeUtf8Fatal(bytes, 'platform-session-fixture-invalid-utf8'));
|
||||
} catch (error) {
|
||||
throw codedError(
|
||||
'supervisor-autonomous-playable-platform-session-fixture-invalid',
|
||||
error,
|
||||
);
|
||||
}
|
||||
const expectedKeys = [
|
||||
'schemaVersion',
|
||||
'userId',
|
||||
'accessToken',
|
||||
'apiBaseUrl',
|
||||
'generation',
|
||||
];
|
||||
assert(
|
||||
isPlainObject(fixture) &&
|
||||
JSON.stringify(Object.keys(fixture).sort()) ===
|
||||
JSON.stringify([...expectedKeys].sort()) &&
|
||||
fixture.schemaVersion === platformSessionFixtureSchema &&
|
||||
isNonEmptyString(fixture.userId) &&
|
||||
isNonEmptyString(fixture.accessToken) &&
|
||||
isNonEmptyString(fixture.apiBaseUrl) &&
|
||||
Number.isSafeInteger(fixture.generation) &&
|
||||
fixture.generation > 0,
|
||||
'supervisor-autonomous-playable-platform-session-fixture-invalid',
|
||||
);
|
||||
return {
|
||||
sourcePath: realPath,
|
||||
bytes,
|
||||
fixture,
|
||||
sha256: createHash('sha256').update(bytes).digest('hex'),
|
||||
};
|
||||
}
|
||||
|
||||
async function installPlatformSessionFixtureIntoIsolatedAppData(
|
||||
sourceConfigDir,
|
||||
appDataDir,
|
||||
) {
|
||||
if (!isSupervisorAutonomousPlayableLaneDefenseSuite()) return;
|
||||
const source = await readPlatformSessionFixtureForIsolatedSuite(sourceConfigDir);
|
||||
const isolatedPath = path.join(appDataDir, isolatedPlatformSessionFixtureName);
|
||||
await fs.copyFile(
|
||||
source.sourcePath,
|
||||
isolatedPath,
|
||||
fsConstants.COPYFILE_EXCL | fsConstants.COPYFILE_FICLONE,
|
||||
);
|
||||
await fs.chmod(isolatedPath, 0o600).catch(() => {});
|
||||
const isolatedMetadata = await fs.lstat(isolatedPath);
|
||||
assert(
|
||||
isolatedMetadata.isFile() &&
|
||||
!isolatedMetadata.isSymbolicLink() &&
|
||||
isolatedMetadata.size === source.bytes.length,
|
||||
'supervisor-autonomous-playable-platform-session-fixture-copy-invalid',
|
||||
);
|
||||
const isolatedBytes = await fs.readFile(isolatedPath);
|
||||
assert(
|
||||
createHash('sha256').update(isolatedBytes).digest('hex') === source.sha256,
|
||||
'supervisor-autonomous-playable-platform-session-fixture-copy-mismatch',
|
||||
);
|
||||
state.isolatedRunner.platformSessionFixtureSourcePath = source.sourcePath;
|
||||
state.isolatedRunner.platformSessionFixturePath = isolatedPath;
|
||||
state.isolatedRunner.platformSessionFixtureSha256 = source.sha256;
|
||||
state.isolatedRunner.platformSessionFixturePreviousEnv =
|
||||
Object.prototype.hasOwnProperty.call(process.env, platformSessionFixtureEnv)
|
||||
? process.env[platformSessionFixtureEnv]
|
||||
: undefined;
|
||||
process.env[platformSessionFixtureEnv] = isolatedPath;
|
||||
state.formalConfigPathTranscriptScanner?.addSecrets(
|
||||
absolutePathVariants(source.sourcePath, isolatedPath),
|
||||
);
|
||||
const previousLeakCount = state.transcriptScanner?.count ?? 0;
|
||||
state.secrets = [
|
||||
...new Set([...state.secrets, source.fixture.accessToken]),
|
||||
];
|
||||
rebuildSupervisorSwarmTranscriptScanner();
|
||||
state.transcriptScanner.count = previousLeakCount;
|
||||
}
|
||||
|
||||
export async function prepareIsolatedSuiteAppData({
|
||||
streamAgentId = null,
|
||||
webSearchAgentId = null,
|
||||
@@ -769,6 +887,10 @@ export async function prepareIsolatedSuiteAppData({
|
||||
state.secrets = [...suiteSecrets];
|
||||
rebuildSupervisorSwarmTranscriptScanner();
|
||||
state.transcriptScanner.count = previousLeakCount;
|
||||
await installPlatformSessionFixtureIntoIsolatedAppData(
|
||||
sourceConfigDir,
|
||||
appDataDir,
|
||||
);
|
||||
const unexpectedEndpoint = await fs
|
||||
.lstat(path.join(appDataDir, runnerEndpointFileName))
|
||||
.catch((error) => {
|
||||
@@ -1874,6 +1996,31 @@ export async function verifyIsolatedSuiteConfigLinksUnchanged() {
|
||||
'isolated-source-config-changed-during-suite',
|
||||
);
|
||||
}
|
||||
const fixture = state.isolatedRunner;
|
||||
if (
|
||||
fixture.platformSessionFixtureSourcePath &&
|
||||
fixture.platformSessionFixturePath &&
|
||||
fixture.platformSessionFixtureSha256
|
||||
) {
|
||||
const [sourceMetadata, isolatedMetadata, sourceBytes, isolatedBytes] =
|
||||
await Promise.all([
|
||||
fs.lstat(fixture.platformSessionFixtureSourcePath),
|
||||
fs.lstat(fixture.platformSessionFixturePath),
|
||||
fs.readFile(fixture.platformSessionFixtureSourcePath),
|
||||
fs.readFile(fixture.platformSessionFixturePath),
|
||||
]);
|
||||
assert(
|
||||
sourceMetadata.isFile() &&
|
||||
!sourceMetadata.isSymbolicLink() &&
|
||||
isolatedMetadata.isFile() &&
|
||||
!isolatedMetadata.isSymbolicLink() &&
|
||||
createHash('sha256').update(sourceBytes).digest('hex') ===
|
||||
fixture.platformSessionFixtureSha256 &&
|
||||
createHash('sha256').update(isolatedBytes).digest('hex') ===
|
||||
fixture.platformSessionFixtureSha256,
|
||||
'supervisor-autonomous-playable-platform-session-fixture-changed',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function verifySourceConfigLinkCountsRestored() {
|
||||
@@ -1913,7 +2060,22 @@ export async function removeIsolatedSuiteAppData() {
|
||||
} catch (error) {
|
||||
ownershipError = error;
|
||||
}
|
||||
await fs.rm(appDataDir, { recursive: true, force: false });
|
||||
try {
|
||||
await fs.rm(appDataDir, { recursive: true, force: false });
|
||||
} finally {
|
||||
if (state.isolatedRunner.platformSessionFixtureSourcePath) {
|
||||
const previous = state.isolatedRunner.platformSessionFixturePreviousEnv;
|
||||
if (previous === undefined) {
|
||||
delete process.env[platformSessionFixtureEnv];
|
||||
} else {
|
||||
process.env[platformSessionFixtureEnv] = previous;
|
||||
}
|
||||
}
|
||||
state.isolatedRunner.platformSessionFixturePath = null;
|
||||
state.isolatedRunner.platformSessionFixtureSourcePath = null;
|
||||
state.isolatedRunner.platformSessionFixtureSha256 = null;
|
||||
state.isolatedRunner.platformSessionFixturePreviousEnv = undefined;
|
||||
}
|
||||
state.runtimeConfigDir = state.options.configDir;
|
||||
try {
|
||||
await verifySourceConfigLinkCountsRestored();
|
||||
|
||||
@@ -84,6 +84,14 @@ export function buildCliChildEnvironment() {
|
||||
NO_COLOR: '1',
|
||||
RUST_BACKTRACE: '0',
|
||||
};
|
||||
// The deterministic playable suite copies its account fixture into the
|
||||
// sibling isolated AppData directory. Set the path explicitly here so
|
||||
// every CLI and the Runner it launches use the isolated copy, even if the
|
||||
// parent harness environment was restored or changed after setup.
|
||||
if (state.isolatedRunner.platformSessionFixturePath) {
|
||||
environment.GENARRATIVE_AGC_PLATFORM_SESSION_FIXTURE =
|
||||
state.isolatedRunner.platformSessionFixturePath;
|
||||
}
|
||||
return isSupervisorSwarmTransientRetrySuite()
|
||||
? withLoopbackNoProxy(environment)
|
||||
: environment;
|
||||
|
||||
@@ -241,7 +241,7 @@ export const autonomousCompletionContractSchemaVersion =
|
||||
'game-creator-autonomous-completion-contract.v2';
|
||||
|
||||
export const autonomousPlaytestReceiptSchemaVersion =
|
||||
'game-creator-autonomous-playtest-receipt.v1';
|
||||
'game-creator-autonomous-playtest-receipt.v2';
|
||||
|
||||
export const autonomousGameBuildRunProfile = 'autonomous-game-build';
|
||||
|
||||
@@ -938,6 +938,10 @@ export class BlockedError extends Error {
|
||||
|
||||
export const isolatedRunnerState = {
|
||||
appDataDir: null,
|
||||
platformSessionFixturePath: null,
|
||||
platformSessionFixtureSourcePath: null,
|
||||
platformSessionFixtureSha256: null,
|
||||
platformSessionFixturePreviousEnv: undefined,
|
||||
ownerToken: null,
|
||||
createdAt: 0,
|
||||
current: null,
|
||||
|
||||
+5
@@ -1220,6 +1220,11 @@ export async function validateSupervisorAutonomousPlayableEvidence(
|
||||
agentId: receipt.agentId,
|
||||
runId: receipt.runId,
|
||||
runProfileBindingFingerprint: receipt.runProfileBindingFingerprint,
|
||||
executorAgentId: receipt.executorAgentId,
|
||||
executorRunId: receipt.executorRunId,
|
||||
executorSource: receipt.executorSource,
|
||||
executorRunProfileBindingFingerprint:
|
||||
receipt.executorRunProfileBindingFingerprint,
|
||||
actionId: receipt.actionId,
|
||||
actionFingerprint: receipt.actionFingerprint,
|
||||
revision: receipt.revision,
|
||||
|
||||
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,
|
||||
|
||||
+25
-15
@@ -37,6 +37,7 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action_with_pending_
|
||||
pending_action: Option<&AgentRuntimePendingToolAction>,
|
||||
) -> AgentRuntimeToolObservation {
|
||||
let tool = action.tool.trim();
|
||||
let relaxed_autonomous = autonomous_relaxed_run_at(root, agent_id, run_id).unwrap_or(false);
|
||||
if tool == PLAN_SUBMIT_GDD_TOOL && agent_id.trim() != GAME_CREATOR_PROJECT_PLANNING_AGENT_ID {
|
||||
return AgentRuntimeToolObservation {
|
||||
tool: tool.to_string(),
|
||||
@@ -81,13 +82,19 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action_with_pending_
|
||||
};
|
||||
}
|
||||
}
|
||||
if let Some(blocker) = supervisor_orchestrator_mutation_block_at(root, agent_id, run_id, tool) {
|
||||
return blocker;
|
||||
if !relaxed_autonomous {
|
||||
if let Some(blocker) =
|
||||
supervisor_orchestrator_mutation_block_at(root, agent_id, run_id, tool)
|
||||
{
|
||||
return blocker;
|
||||
}
|
||||
}
|
||||
if let Some(blocker) =
|
||||
agent_runtime_autonomous_art_director_canvas_only_action_block(agent_id, task, tool)
|
||||
{
|
||||
return blocker;
|
||||
if !relaxed_autonomous {
|
||||
if let Some(blocker) =
|
||||
agent_runtime_autonomous_art_director_canvas_only_action_block(agent_id, task, tool)
|
||||
{
|
||||
return blocker;
|
||||
}
|
||||
}
|
||||
let command_id = game_creator_agent_runtime_tool_command_id(tool);
|
||||
if let Some(command_id) = command_id {
|
||||
@@ -379,7 +386,7 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action_with_pending_
|
||||
"command.run_limited" => {
|
||||
observe_agent_runtime_limited_command(root, agent_id, run_id, &action.input)
|
||||
}
|
||||
"preview.start" => observe_agent_runtime_preview_start(root, agent_id),
|
||||
"preview.start" => observe_agent_runtime_preview_start(root, agent_id, run_id),
|
||||
"preview.validate" => {
|
||||
observe_agent_runtime_preview_validate(
|
||||
root,
|
||||
@@ -649,6 +656,7 @@ pub(in crate::agent) fn validate_agent_runtime_project_snapshot_action_after_loc
|
||||
));
|
||||
}
|
||||
let pending = &durable_pending;
|
||||
let relaxed_autonomous = autonomous_relaxed_run_profile(&pending.run_profile);
|
||||
let runtime = match read_game_creator_agent_runtime_at(root, agent_id) {
|
||||
Ok(result) => result.state,
|
||||
Err(error) => {
|
||||
@@ -692,16 +700,18 @@ pub(in crate::agent) fn validate_agent_runtime_project_snapshot_action_after_loc
|
||||
) {
|
||||
return Err(agent_runtime_tool_policy_block_observation(tool, blocked));
|
||||
}
|
||||
match pending_repository_context_drift_observation(root, pending) {
|
||||
Ok(Some(observation)) => return Err(observation),
|
||||
Ok(None) => {}
|
||||
Err(error) => {
|
||||
return Err(agent_runtime_pending_reconciliation_observation(
|
||||
tool, root, &error,
|
||||
));
|
||||
if !relaxed_autonomous {
|
||||
match pending_repository_context_drift_observation(root, pending) {
|
||||
Ok(Some(observation)) => return Err(observation),
|
||||
Ok(None) => {}
|
||||
Err(error) => {
|
||||
return Err(agent_runtime_pending_reconciliation_observation(
|
||||
tool, root, &error,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
if validate_revision_gate {
|
||||
if validate_revision_gate && !relaxed_autonomous {
|
||||
match pending_project_revision_drift_observation(root, pending, true) {
|
||||
Ok(Some(observation)) => return Err(observation),
|
||||
Ok(None) => {}
|
||||
|
||||
+38
-4
@@ -601,6 +601,16 @@ pub(in crate::agent) fn validate_agent_runtime_autonomous_plan_liveness_at(
|
||||
supervisor_requires_delegated_repair: bool,
|
||||
supervisor_manifest_dag_in_progress_at_request: bool,
|
||||
) -> Result<(), String> {
|
||||
// `autonomous-game-build` is the free-form lane. Its manifest entries,
|
||||
// delivery receipts and verification snapshots are advisory context; they
|
||||
// must never turn a perfectly valid Provider plan into a DAG-wait or
|
||||
// repair-only plan. Keep this profile check at the top so future liveness
|
||||
// rules cannot accidentally become a hidden start/completion gate.
|
||||
if read_game_creator_agent_runtime_run_profile_binding(root, agent_id, run_id)?
|
||||
.is_some_and(|binding| autonomous_relaxed_run_profile(&binding.profile))
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
||||
&& autonomous_manifest_dag_in_progress_at(root)?
|
||||
{
|
||||
@@ -783,16 +793,40 @@ fn autonomous_manifest_parent_has_active_ready_task_at(
|
||||
let records =
|
||||
latest_game_creator_agent_runtime_tasks(read_all_game_creator_agent_runtime_tasks(
|
||||
&game_creator_agent_runtime_task_path(root, task_id),
|
||||
)?);
|
||||
)?);
|
||||
for record in records {
|
||||
if record.source != "agent-ready-task-scheduler"
|
||||
|| record.parent_agent_id.as_deref()
|
||||
!= Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID)
|
||||
|| record.parent_run_id.as_deref() != Some(parent_run_id)
|
||||
|| game_creator_agent_runtime_terminal_status(&record).is_some()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// Relaxed autonomous children may use a generated run id and do
|
||||
// not depend on parent/child identity for execution. When the
|
||||
// scheduler can record the optional parent hint, use the durable
|
||||
// binding's root id solely to associate an in-flight child with
|
||||
// this root; never reject or block the child for a mismatch.
|
||||
if record.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD {
|
||||
let parent_hint_matches =
|
||||
record.parent_agent_id.as_deref()
|
||||
== Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID)
|
||||
&& record.parent_run_id.as_deref() == Some(parent_run_id);
|
||||
let binding_root_matches = read_game_creator_agent_runtime_run_profile_binding(
|
||||
root,
|
||||
&record.agent_id,
|
||||
&record.run_id,
|
||||
)?
|
||||
.is_some_and(|binding| binding.root_run_id == parent_run_id);
|
||||
if parent_hint_matches || binding_root_matches {
|
||||
return Ok(true);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if record.parent_agent_id.as_deref()
|
||||
!= Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID)
|
||||
|| record.parent_run_id.as_deref() != Some(parent_run_id)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if record.run_id != autonomous_manifest_ready_task_run_id(parent_run_id, task_id) {
|
||||
return Err(format!(
|
||||
"当前自主构建父 Run 的活跃 child runId 不符合确定性绑定:taskId={task_id}"
|
||||
|
||||
+45
-35
@@ -321,7 +321,6 @@ pub(in crate::agent) fn validate_agent_runtime_pending_tool_action_record(
|
||||
pending.fingerprint_version
|
||||
));
|
||||
}
|
||||
validate_agent_runtime_pending_goal_binding(pending)?;
|
||||
let (run_profile, binding_fingerprint) = agent_runtime_run_profile_identity_at(
|
||||
root,
|
||||
&pending.agent_id,
|
||||
@@ -334,46 +333,50 @@ pub(in crate::agent) fn validate_agent_runtime_pending_tool_action_record(
|
||||
{
|
||||
return Err("Agent Runtime 待确认动作 Run Profile 绑定不匹配".to_string());
|
||||
}
|
||||
match pending.planning_session_binding.as_ref() {
|
||||
Some(binding) => {
|
||||
validate_plan_provider_session_binding(binding).map_err(|error| error.to_string())?;
|
||||
if pending.action.tool.trim() != PLAN_SUBMIT_GDD_TOOL
|
||||
|| binding.agent_id != pending.agent_id
|
||||
|| binding.task_id != pending.task_id
|
||||
|| binding.session_id != pending.session_id
|
||||
|| binding.run_id != pending.run_id
|
||||
|| binding.source != pending.source
|
||||
|| binding.run_profile != pending.run_profile
|
||||
|| binding.run_profile_binding_fingerprint
|
||||
!= pending.run_profile_binding_fingerprint
|
||||
|| binding.applied_steer_cursor != pending.planned_steer_cursor
|
||||
{
|
||||
let relaxed_autonomous = autonomous_relaxed_run_profile(&pending.run_profile);
|
||||
if !relaxed_autonomous {
|
||||
validate_agent_runtime_pending_goal_binding(pending)?;
|
||||
match pending.planning_session_binding.as_ref() {
|
||||
Some(binding) => {
|
||||
validate_plan_provider_session_binding(binding).map_err(|error| error.to_string())?;
|
||||
if pending.action.tool.trim() != PLAN_SUBMIT_GDD_TOOL
|
||||
|| binding.agent_id != pending.agent_id
|
||||
|| binding.task_id != pending.task_id
|
||||
|| binding.session_id != pending.session_id
|
||||
|| binding.run_id != pending.run_id
|
||||
|| binding.source != pending.source
|
||||
|| binding.run_profile != pending.run_profile
|
||||
|| binding.run_profile_binding_fingerprint
|
||||
!= pending.run_profile_binding_fingerprint
|
||||
|| binding.applied_steer_cursor != pending.planned_steer_cursor
|
||||
{
|
||||
return Err(
|
||||
"planning submit standalone pending 与 frozen binding 不一致".to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
None if pending.provider_batch_plan_update.is_none() => {}
|
||||
None => {
|
||||
return Err(
|
||||
"planning submit standalone pending 与 frozen binding 不一致".to_string(),
|
||||
"非 planning standalone pending 不能携带 Provider batch planUpdate".to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
None if pending.provider_batch_plan_update.is_none() => {}
|
||||
None => {
|
||||
return Err(
|
||||
"非 planning standalone pending 不能携带 Provider batch planUpdate".to_string(),
|
||||
);
|
||||
validate_agent_runtime_project_revision(root, &pending.project_revision_before)?;
|
||||
if pending.verification_gate_before.project_id
|
||||
!= game_creator_agent_runtime_context_project_id(root)?
|
||||
|| pending.verification_gate_before.agent_id != pending.agent_id
|
||||
|| pending.verification_gate_before.run_id != pending.run_id
|
||||
{
|
||||
return Err("Agent Runtime 待确认动作的 verification gate 身份不匹配".to_string());
|
||||
}
|
||||
validate_agent_runtime_verification_gate(
|
||||
root,
|
||||
&pending.verification_gate_before,
|
||||
&pending.agent_id,
|
||||
&pending.run_id,
|
||||
)?;
|
||||
}
|
||||
validate_agent_runtime_project_revision(root, &pending.project_revision_before)?;
|
||||
if pending.verification_gate_before.project_id
|
||||
!= game_creator_agent_runtime_context_project_id(root)?
|
||||
|| pending.verification_gate_before.agent_id != pending.agent_id
|
||||
|| pending.verification_gate_before.run_id != pending.run_id
|
||||
{
|
||||
return Err("Agent Runtime 待确认动作的 verification gate 身份不匹配".to_string());
|
||||
}
|
||||
validate_agent_runtime_verification_gate(
|
||||
root,
|
||||
&pending.verification_gate_before,
|
||||
&pending.agent_id,
|
||||
&pending.run_id,
|
||||
)?;
|
||||
if pending.planned_repository_context_fingerprint.len() != 64
|
||||
|| !pending
|
||||
.planned_repository_context_fingerprint
|
||||
@@ -458,6 +461,13 @@ pub(in crate::agent) fn validate_agent_runtime_pending_current_goal_snapshot(
|
||||
root: &Path,
|
||||
pending: &AgentRuntimePendingToolAction,
|
||||
) -> Result<(), String> {
|
||||
// Relaxed autonomous child runs are intentionally independent of the
|
||||
// supervisor's Goal/Acceptance state. A sibling task may advance or
|
||||
// replace that state while this action is waiting for the project lock;
|
||||
// only the action's durable identity and tool policy remain relevant.
|
||||
if autonomous_relaxed_run_profile(&pending.run_profile) {
|
||||
return Ok(());
|
||||
}
|
||||
validate_agent_runtime_pending_goal_binding(pending)?;
|
||||
let current = read_game_creator_agent_goal_at(root, &pending.agent_id, &pending.session_id)?;
|
||||
let Some(expected_goal_id) = pending.goal_id.as_deref() else {
|
||||
|
||||
@@ -24,6 +24,12 @@ pub(in crate::agent) fn supervisor_orchestrator_mutation_block_at(
|
||||
{
|
||||
return None;
|
||||
}
|
||||
// The autonomous game-build profile is intentionally free-form: the
|
||||
// Supervisor may mutate the project directly while specialist tasks run
|
||||
// in parallel. Keep the collaboration policy for the standard profile.
|
||||
if autonomous_relaxed_run_at(root, agent_id, run_id).unwrap_or(false) {
|
||||
return None;
|
||||
}
|
||||
let policy = match resolve_supervisor_collaboration_policy_for_run_at(root, agent_id, run_id) {
|
||||
Ok(resolution) => resolution.policy,
|
||||
Err(error) => {
|
||||
@@ -129,150 +135,13 @@ pub(crate) fn ensure_current_autonomous_ready_child_mutation_at_locked(
|
||||
Ok(agent_id) => agent_id,
|
||||
Err(_) => return Ok(()),
|
||||
};
|
||||
let binding =
|
||||
read_game_creator_agent_runtime_run_profile_binding(root, &normalized_agent_id, run_id)?;
|
||||
let Some(binding) = binding else {
|
||||
let task = read_latest_game_creator_agent_runtime_task_by_run_id(
|
||||
root,
|
||||
&normalized_agent_id,
|
||||
run_id,
|
||||
)?;
|
||||
if task
|
||||
.as_ref()
|
||||
.is_some_and(|task| task.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD)
|
||||
{
|
||||
return Err("autonomous Run 项目修改缺少 Run Profile binding,已失败关闭".to_string());
|
||||
}
|
||||
return Ok(());
|
||||
};
|
||||
if game_creator_agent_runtime_cancel_requested_for(root, &normalized_agent_id, run_id) {
|
||||
return Err("当前 Run 已收到取消请求,禁止继续修改项目".to_string());
|
||||
}
|
||||
if binding.profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD {
|
||||
return Ok(());
|
||||
}
|
||||
if binding.agent_id != normalized_agent_id
|
||||
|| binding.run_id != run_id
|
||||
|| binding.root_agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
||||
{
|
||||
return Err("autonomous Run 项目修改的 Run Profile 绑定身份不一致".to_string());
|
||||
}
|
||||
let task =
|
||||
read_latest_game_creator_agent_runtime_task_by_run_id(root, &normalized_agent_id, run_id)?
|
||||
.ok_or_else(|| {
|
||||
"autonomous Run 项目修改缺少 durable task journal,已失败关闭".to_string()
|
||||
})?;
|
||||
if task.agent_id != binding.agent_id
|
||||
|| task.run_id != binding.run_id
|
||||
|| task.source != binding.source
|
||||
|| task.run_profile != binding.profile
|
||||
|| task.run_profile_binding_fingerprint != binding.binding_fingerprint
|
||||
|| task.parent_agent_id != binding.parent_agent_id
|
||||
|| task.parent_run_id != binding.parent_run_id
|
||||
{
|
||||
return Err("autonomous Run 项目修改的 durable task journal 与绑定不一致".to_string());
|
||||
}
|
||||
let is_root = binding.agent_id == binding.root_agent_id
|
||||
&& binding.run_id == binding.root_run_id
|
||||
&& binding.parent_agent_id.is_none()
|
||||
&& binding.parent_run_id.is_none();
|
||||
if is_root {
|
||||
if task.parent_agent_id.is_some()
|
||||
|| task.parent_run_id.is_some()
|
||||
|| !agent_runtime_supervisor_source_is_trusted(&task.source)
|
||||
{
|
||||
return Err("autonomous 根 Run 项目修改的 durable identity 不一致".to_string());
|
||||
}
|
||||
} else {
|
||||
let parent_agent_id = binding
|
||||
.parent_agent_id
|
||||
.as_deref()
|
||||
.ok_or_else(|| "autonomous 派生 Run 项目修改缺少 parentAgentId".to_string())?;
|
||||
let parent_run_id = binding
|
||||
.parent_run_id
|
||||
.as_deref()
|
||||
.ok_or_else(|| "autonomous 派生 Run 项目修改缺少 parentRunId".to_string())?;
|
||||
let parent_binding = read_game_creator_agent_runtime_run_profile_binding(
|
||||
root,
|
||||
parent_agent_id,
|
||||
parent_run_id,
|
||||
)?
|
||||
.ok_or_else(|| "autonomous 派生 Run 项目修改缺少父 Run Profile binding".to_string())?;
|
||||
if binding.parent_binding_fingerprint.as_deref()
|
||||
!= Some(parent_binding.binding_fingerprint.as_str())
|
||||
|| parent_binding.profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
|| parent_binding.root_agent_id != binding.root_agent_id
|
||||
|| parent_binding.root_run_id != binding.root_run_id
|
||||
{
|
||||
return Err(
|
||||
"autonomous 派生 Run 项目修改的父 binding 或 root identity 不一致".to_string(),
|
||||
);
|
||||
}
|
||||
if binding.source == "agent-ready-task-scheduler" {
|
||||
let state = agent_runtime_state_from_task_record(&task);
|
||||
let ready_binding =
|
||||
autonomous_manifest_ready_task_parent_binding_for_state_at(root, &state)?
|
||||
.ok_or_else(|| {
|
||||
"autonomous ready-task 项目修改缺少确定性父 Run 绑定".to_string()
|
||||
})?;
|
||||
if ready_binding != binding
|
||||
|| state.run_id
|
||||
!= autonomous_manifest_ready_task_run_id(
|
||||
&binding.root_run_id,
|
||||
&normalized_agent_id,
|
||||
)
|
||||
{
|
||||
return Err("autonomous ready-task 项目修改的确定性父子身份不一致".to_string());
|
||||
}
|
||||
} else if binding.source == "agent-delegate"
|
||||
&& task
|
||||
.delegation_id
|
||||
.as_deref()
|
||||
.is_none_or(|delegation_id| delegation_id.trim().is_empty())
|
||||
{
|
||||
return Err("autonomous agent-delegate 项目修改缺少 delegationId".to_string());
|
||||
}
|
||||
}
|
||||
if task.status != "running" || game_creator_agent_runtime_terminal_status(&task).is_some() {
|
||||
return Err("autonomous Run 项目修改要求当前 durable task 仍为 running".to_string());
|
||||
}
|
||||
let current_root = current_autonomous_game_build_root_task_at(root)?
|
||||
.ok_or_else(|| "autonomous Run 项目修改时当前根 Run 已不存在".to_string())?;
|
||||
if current_root.run_id != binding.root_run_id {
|
||||
return Err(format!(
|
||||
"autonomous Run 已被更新根 Run 取代:currentRunId={}",
|
||||
current_root.run_id
|
||||
));
|
||||
}
|
||||
let current_root_binding = read_game_creator_agent_runtime_run_profile_binding(
|
||||
root,
|
||||
¤t_root.agent_id,
|
||||
¤t_root.run_id,
|
||||
)?
|
||||
.ok_or_else(|| "autonomous Run 当前根缺少 Run Profile binding".to_string())?;
|
||||
if current_root.agent_id != binding.root_agent_id
|
||||
|| current_root.source != current_root_binding.source
|
||||
|| current_root.run_profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
|| current_root.parent_agent_id.is_some()
|
||||
|| current_root.parent_run_id.is_some()
|
||||
|| current_root.delegation_id.is_some()
|
||||
|| current_root_binding.agent_id != binding.root_agent_id
|
||||
|| current_root_binding.run_id != binding.root_run_id
|
||||
|| current_root_binding.root_agent_id != current_root_binding.agent_id
|
||||
|| current_root_binding.root_run_id != current_root_binding.run_id
|
||||
|| current_root_binding.parent_agent_id.is_some()
|
||||
|| current_root_binding.parent_run_id.is_some()
|
||||
|| current_root_binding.binding_fingerprint != current_root.run_profile_binding_fingerprint
|
||||
|| (is_root && current_root_binding.binding_fingerprint != binding.binding_fingerprint)
|
||||
{
|
||||
return Err("autonomous Run 当前根 journal 与 binding 不一致".to_string());
|
||||
}
|
||||
if !autonomous_game_build_root_task_is_active(¤t_root) {
|
||||
return Err(format!(
|
||||
"autonomous Run 当前根已不再活跃:status={} phase={}",
|
||||
current_root.status, current_root.phase
|
||||
));
|
||||
}
|
||||
// Relaxed autonomous runs do not require a fixed parent/owner lineage.
|
||||
// The project-root and cancellation checks remain in force, while each
|
||||
// task is free to mutate through the normal tool whitelist even when an
|
||||
// old run has no parent/profile sidecar.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
+16
-6
@@ -476,7 +476,9 @@ pub(crate) async fn prepare_game_creator_agent_runtime_provider_action_batch_wit
|
||||
{
|
||||
return Err("Provider action 批次的 Run Profile 快照已漂移".to_string());
|
||||
}
|
||||
if run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
let relaxed_autonomous = autonomous_relaxed_run_profile(&run_profile);
|
||||
if !relaxed_autonomous
|
||||
&& run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
&& batch_plan
|
||||
.actions
|
||||
.iter()
|
||||
@@ -495,7 +497,9 @@ pub(crate) async fn prepare_game_creator_agent_runtime_provider_action_batch_wit
|
||||
));
|
||||
}
|
||||
|
||||
let collaboration_preflight = if runtime.agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
|
||||
let collaboration_preflight = if relaxed_autonomous {
|
||||
SupervisorCollaborationPreflight::default()
|
||||
} else if runtime.agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
|
||||
let collaboration_policy = resolve_supervisor_collaboration_policy_for_run_at(
|
||||
root,
|
||||
&runtime.agent_id,
|
||||
@@ -514,7 +518,8 @@ pub(crate) async fn prepare_game_creator_agent_runtime_provider_action_batch_wit
|
||||
} else {
|
||||
SupervisorCollaborationPreflight::default()
|
||||
};
|
||||
if run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
if !relaxed_autonomous
|
||||
&& run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
&& collaboration_preflight
|
||||
.contract
|
||||
.as_ref()
|
||||
@@ -534,7 +539,8 @@ pub(crate) async fn prepare_game_creator_agent_runtime_provider_action_batch_wit
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(violation) = collaboration_preflight.violation {
|
||||
if !relaxed_autonomous {
|
||||
if let Some(violation) = collaboration_preflight.violation {
|
||||
return Ok(AgentRuntimeProviderActionBatchPreparation::Blocked(
|
||||
AgentRuntimeToolObservation {
|
||||
tool: "runtime.collaboration_policy".to_string(),
|
||||
@@ -543,6 +549,7 @@ pub(crate) async fn prepare_game_creator_agent_runtime_provider_action_batch_wit
|
||||
detail: Some(violation.detail),
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
if provider_action_batch_is_not_needed(
|
||||
batch_plan.actions.len(),
|
||||
@@ -587,13 +594,16 @@ pub(crate) async fn prepare_game_creator_agent_runtime_provider_action_batch_wit
|
||||
"当前 Agent 身份不允许执行该原始工具".to_string(),
|
||||
)
|
||||
});
|
||||
let art_director_canvas_only_block =
|
||||
let art_director_canvas_only_block = if relaxed_autonomous {
|
||||
None
|
||||
} else {
|
||||
agent_runtime_autonomous_art_director_canvas_only_action_block(
|
||||
&runtime.agent_id,
|
||||
task,
|
||||
action.tool.trim(),
|
||||
)
|
||||
.map(|observation| AgentRuntimeToolPolicyBlock::Denied(observation.summary));
|
||||
.map(|observation| AgentRuntimeToolPolicyBlock::Denied(observation.summary))
|
||||
};
|
||||
let isolated_scope_block = if runtime.agent_id.starts_with("child-") {
|
||||
validate_isolated_agent_tool_scope_at(
|
||||
root,
|
||||
|
||||
+148
-69
@@ -152,6 +152,31 @@ pub(in crate::agent) fn remove_autonomous_art_director_non_canvas_validation_too
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The relaxed autonomous lane is an execution lane, not a platform-quality
|
||||
/// gate. Do not advertise actions whose only purpose is to produce platform
|
||||
/// verification evidence: once exposed, a Provider will commonly spend the
|
||||
/// whole turn running them even though their result is deliberately ignored
|
||||
/// by relaxed completion. Keeping the native implementations available is
|
||||
/// intentional; this only narrows the Provider request catalog.
|
||||
pub(in crate::agent) fn remove_relaxed_autonomous_platform_validation_tools(
|
||||
tools: &mut Vec<LlmFunctionTool>,
|
||||
) -> Result<(), String> {
|
||||
let hidden_function_names = [
|
||||
"project.verify",
|
||||
"command.run_limited",
|
||||
"preview.start",
|
||||
"preview.validate",
|
||||
]
|
||||
.into_iter()
|
||||
.map(|tool| {
|
||||
native_runtime_function_name(tool)
|
||||
.ok_or_else(|| format!("无法生成 relaxed 平台验证工具函数名:{tool}"))
|
||||
})
|
||||
.collect::<Result<BTreeSet<_>, _>>()?;
|
||||
tools.retain(|tool| !hidden_function_names.contains(&tool.name));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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]
|
||||
|
||||
+63
-52
@@ -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();
|
||||
|
||||
+8
@@ -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",
|
||||
|
||||
+7
-7
@@ -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();
|
||||
|
||||
+9
-2
@@ -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
Reference in New Issue
Block a user