合并最新游戏创作目标分支并解决工作台冲突

同步目标分支 game-chat、运行预览与 Agent Runtime 最新改动
同时保留 LocalGamePreviewFrame 与资源画布双模式布局能力
合并双方项目决策记录且不包含本地环境文件
This commit is contained in:
2026-07-30 14:55:19 +08:00
82 changed files with 8886 additions and 660 deletions
@@ -142,6 +142,20 @@ client.generate_image(
)
```
For a transparent game/UI atlas, call the dedicated helper instead of ordinary image generation:
```python
client.generate_icon_spritesheet(
"editor-resource-current-art-spec",
["蛇头四方向", "直身与四种转角", "尾部四方向", "四类可区分食物"],
canvasSession=session,
assetLabel="贪吃蛇透明图集",
screenColor="auto",
)
```
Pass the registered visual-spec resource ID as `reference_image_src`; do not pass the UI prototype or a local path.
Use the helper directly from this skill path, or copy it into the caller's project. Do not change the fixed base URL or move the API Key into environment variables.
Use this shared base:
@@ -326,6 +340,18 @@ Character image generation (including character redraw through `kind: "character
- `sliceWarning` is a separate condition used only when transparent spritesheet post-processing succeeded but automatic slicing failed. Keep `sliceWarning.reason` as the original diagnostic and continue using the complete transparent spritesheet; a UI may add context when displaying it, but must not rewrite the stored reason.
- The service contract keeps `warning` and `sliceWarning` mutually exclusive. As defensive handling for a malformed response containing both, treat the general `warning` as authoritative and do not misclassify the source-preserved result as a slicing-only warning.
For reusable transparent game/UI sheets, do not substitute ordinary image generation merely because it can draw several objects in one image. Use icon spritesheet generation when a stable visual-spec reference and `iconDescriptions` exist; use UI extraction only for an existing annotated UI design. Pass `screenColor: "auto"` unless the art direction requires one of the supported solid chroma colors. A client must verify the returned full sheet really contains transparency before treating it as a transparent spritesheet. If a source-preserved `warning` is present, do not register the opaque provider source as the requested transparent deliverable. When only `sliceWarning` is present, the full transparent sheet remains usable, but no individual slices may be claimed.
## AI Game Creator Canonical Visual DAG
The AI game creator reuses its existing 16-task manifest; do not add a parallel task system or collapse the following artifacts into one ordinary generation request:
1. `art-director` generates `assets/art-spec.png` with `POST /api/external/v1/editor/images/generations`, `kind: "spec"`, and registers it as `assetKind: "icon-spec"`. This is the real visual-spec image. The JSON value in `generationInputs.artSpec` is supporting structured context and does not replace this image.
2. `design-foundation` uses the registered External Editor resource ID for `assets/art-spec.png` in `referenceImageSrcs`, then generates the complete `assets/ui-prototype.png` through `POST /api/external/v1/editor/images/generations` with `kind: "ui-design"`.
3. `art-asset-plan` uses the same registered `assets/art-spec.png` resource ID as the required `referenceImageSrc` for `POST /api/external/v1/editor/icon-spritesheets/generations`, supplies concrete `iconDescriptions`, and registers the transparent full result as `assets/art-spritesheet.png`.
Never use `assets/ui-prototype.png` as the icon spritesheet's visual-spec reference. `POST /api/external/v1/editor/ui-designs/assets/extractions` requires an existing UI design image with red-box annotations; it is not UI generation and is not part of this canonical DAG.
## Guardrails
- Do not invent endpoints outside the OpenAPI, especially internal worker or runtime task-list routes.
@@ -20,6 +20,14 @@ At the start of a new conversation, ask for a canvas name before the first gener
Before generating art assets, normalize the user's request into a current art spec with `assetType`, `subject`, `style`, `palette`, `composition`, `format`, `constraints`, and `references`. Ask follow-up questions only for missing fields that block the selected endpoint. Reuse the current spec automatically when the user asks for another asset without changing style/spec requirements. Put the spec in `generationInputs.artSpec` and summarize it in the prompt when useful.
For the AI game creator's existing 16-task autonomous build, distinguish that JSON art spec from the required visual-spec image and keep this dependency chain:
1. `art-director` -> `assets/art-spec.png` via `POST /api/external/v1/editor/images/generations`, with `kind=spec` and registered `assetKind=icon-spec`.
2. `design-foundation` -> `assets/ui-prototype.png` via the same image generation endpoint with `kind=ui-design`, using the registered art-spec resource ID in `referenceImageSrcs`.
3. `art-asset-plan` -> transparent `assets/art-spritesheet.png` via `POST /api/external/v1/editor/icon-spritesheets/generations`, using the registered art-spec resource ID as `referenceImageSrc` and providing `iconDescriptions`.
Do not use the UI prototype as the spritesheet specification. UI extraction requires a stable source image with red-box annotations and is outside this canonical DAG.
## Intent Routing
Infer the endpoint from the user's description. Do not present this as a menu unless the request is genuinely ambiguous.
@@ -69,8 +77,8 @@ Ask a follow-up only when two routes could both be correct and produce different
| --- | --- | --- | --- |
| Generate image/spec/character/UI/publication material | `POST /api/external/v1/editor/images/generations` | `prompt` | `kind`, `model`, `aspectRatio`, `imageSize`, `size`, `referenceImageSrcs`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` |
| Edit/redraw image | `POST /api/external/v1/editor/images/edits` | `prompt`, `sourceImageSrc` | `referenceImageSrcs`, `model`, `size`, `projectId`, `assetFolderId`, `assetLabel`, `sourceResourceId`, `targetLayerId`, `canvasCompletion` |
| Generate icon spritesheet | `POST /api/external/v1/editor/icon-spritesheets/generations` | `referenceImageSrc`, `iconDescriptions` | `referenceImageSrcs`, `model`, `aspectRatio`, `imageSize`, `projectId`, `assetFolderId`, `canvasCompletion` |
| Extract assets from UI design | `POST /api/external/v1/editor/ui-designs/assets/extractions` | `sourceImageSrc`, `aspectRatio`, `imageSize` | `model`, `referenceImageSrcs`, `projectId`, `assetFolderId`, `spritesheetLabel`, `canvasCompletion` |
| Generate icon spritesheet | `POST /api/external/v1/editor/icon-spritesheets/generations` | `referenceImageSrc`, `iconDescriptions` | `referenceImageSrcs`, `screenColor`, `model`, `aspectRatio`, `imageSize`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
| Extract assets from UI design | `POST /api/external/v1/editor/ui-designs/assets/extractions` | `sourceImageSrc`, `aspectRatio`, `imageSize` | `screenColor`, `model`, `referenceImageSrcs`, `projectId`, `assetFolderId`, `spritesheetLabel`, `canvasCompletion` |
| Generate character animation | `POST /api/external/v1/editor/character-animations/generations` | `sourceLayerId`, `sourceImageSrc`, `sourceWidth`, `sourceHeight`, `promptText`, `resolution`, `ratio`, `frameCount`, `durationSeconds`, `model` | `projectId`, `sourceResourceId`, `canvasCompletion`; then create a library asset from the first returned frame |
| Generate video | `POST /api/external/v1/editor/videos/generations` | `prompt`, `model`, `aspectRatio`, `durationSeconds`, `resolution`, `mode`, `sound` | `referenceImageSrcs`, `referenceVideoSrcs`, `referenceAudioSrcs`, `webSearchEnabled`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
| Generate sound effect | `POST /api/external/v1/editor/audios/sound-effects/generations` | `prompt`, `duration` | `model`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` |
@@ -450,6 +450,29 @@ class GenarrativeExternalClient:
timeout=GENERATION_REQUEST_TIMEOUT_SECONDS,
)
def generate_icon_spritesheet(
self,
reference_image_src: str,
icon_descriptions: list[str],
**fields: Any,
) -> Any:
descriptions = [item.strip() for item in icon_descriptions if item.strip()]
if not descriptions:
raise GenarrativeApiError("icon_descriptions must contain at least one non-empty item")
label = fields.get("assetLabel", "图标图集")
self._apply_canvas_session_fields(fields, label, 1024, 1024)
fields.setdefault("screenColor", "auto")
return self.request_json(
"POST",
"/api/external/v1/editor/icon-spritesheets/generations",
{
"referenceImageSrc": reference_image_src,
"iconDescriptions": descriptions,
**fields,
},
timeout=GENERATION_REQUEST_TIMEOUT_SECONDS,
)
def extract_ui_assets(self, source_image_src: str, image_size: str = "1K", **fields: Any) -> Any:
fields.pop("aspectRatio", None)
self._apply_canvas_session_fields(fields, fields.get("spritesheetLabel", "UI 素材拆分"), 1024, 1024, "spritesheetLabel")
@@ -619,6 +642,17 @@ def _self_test() -> None:
assert calls[0]["body"]["canvasCompletion"]["title"] == "角色呼吸动画"
assert calls[1]["path"] == "/api/external/v1/editor/assets"
assert result["asset"]["assetId"] == "editor-asset-demo"
calls.clear()
client.generate_icon_spritesheet(
"editor-resource-spec",
["蛇头向上", "蛇身直线", "转角", "尾部", "四类食物"],
canvasSession=session,
assetLabel="贪吃蛇透明图集",
)
assert calls[0]["path"] == "/api/external/v1/editor/icon-spritesheets/generations"
assert calls[0]["body"]["referenceImageSrc"] == "editor-resource-spec"
assert calls[0]["body"]["screenColor"] == "auto"
assert calls[0]["body"]["iconDescriptions"][0] == "蛇头向上"
print("self-test ok")
+1
View File
@@ -5,6 +5,7 @@
"type": "module",
"scripts": {
"dev": "npm --prefix ../.. exec tauri -- dev",
"game-chat": "npm --prefix ../.. exec tauri -- dev -- -- --game-chat",
"dev-server": "node scripts/start-dev-server.mjs",
"dev-stack": "node scripts/start-dev-stack.mjs",
"build": "npm --prefix ../.. exec tauri -- build",
@@ -61,6 +61,10 @@ const tauriHandlerSource = fs.readFileSync(
new URL('../src-tauri/src/main.rs', import.meta.url),
'utf8',
);
const tauriWindowSource = fs.readFileSync(
new URL('../src-tauri/src/windows.rs', import.meta.url),
'utf8',
);
const tauriRustSource = readSourceTree(
new URL('../src-tauri/src/', import.meta.url),
'.rs',
@@ -1189,6 +1193,95 @@ if (
);
}
const gameChatInitialUrlApply =
'apply_game_chat_initial_window_url(tauri_context.config_mut(), options)';
const gameChatInitialUrlApplyIndexes = Array.from(
tauriHandlerSource.matchAll(
/apply_game_chat_initial_window_url\(tauri_context\.config_mut\(\), options\)/gu,
),
(match) => match.index,
);
const tauriContextIndex = tauriHandlerSource.indexOf(
'let mut tauri_context = tauri::generate_context!()',
);
const tauriBuilderIndex = tauriHandlerSource.indexOf(
'tauri::Builder::default()',
);
if (
gameChatInitialUrlApplyIndexes.length !== 1 ||
tauriContextIndex === -1 ||
tauriBuilderIndex === -1 ||
gameChatInitialUrlApplyIndexes[0] < tauriContextIndex ||
gameChatInitialUrlApplyIndexes[0] > tauriBuilderIndex
) {
throw new Error(
`AI game creator game-chat URL must be applied exactly once between Context creation and Tauri Builder creation: ${gameChatInitialUrlApply}`,
);
}
const tauriSetupStartIndex = tauriHandlerSource.indexOf('.setup(move |app| {');
const tauriSetupEndIndex = tauriHandlerSource.indexOf(
'.invoke_handler(',
tauriSetupStartIndex,
);
if (tauriSetupStartIndex === -1 || tauriSetupEndIndex === -1) {
throw new Error('AI game creator Tauri setup block is missing');
}
const tauriSetupSource = tauriHandlerSource.slice(
tauriSetupStartIndex,
tauriSetupEndIndex,
);
for (const forbiddenSetupSnippet of [
'game_chat_launch.as_ref()',
'navigate_client_to_game_chat',
'.navigate(',
]) {
if (tauriSetupSource.includes(forbiddenSetupSnippet)) {
throw new Error(
`AI game creator setup must not perform game-chat runtime navigation: ${forbiddenSetupSnippet}`,
);
}
}
for (const forbiddenSnippet of [
'navigate_client_to_game_chat',
'client.url()',
'client.navigate(',
]) {
if (
`${tauriHandlerSource}\n${tauriWindowSource}`.includes(forbiddenSnippet)
) {
throw new Error(
`AI game creator game-chat startup must not navigate an initialized client WebView: ${forbiddenSnippet}`,
);
}
}
if (
/get_webview_window\s*\(\s*['"]client['"]\s*\)/u.test(
`${tauriHandlerSource}\n${tauriWindowSource}`,
)
) {
throw new Error(
'AI game creator game-chat startup must not look up the runtime client WebView',
);
}
for (const requiredSnippet of [
'fn apply_game_chat_initial_window_url(',
'.find(|window| window.label == "client")',
'client.url = game_chat_window_url(',
'.run(tauri_context)',
]) {
if (
!`${tauriHandlerSource}\n${tauriWindowSource}`.includes(requiredSnippet)
) {
throw new Error(
`AI game creator game-chat initial WindowConfig guardrail drifted: ${requiredSnippet}`,
);
}
}
if (
!tauriConfig.build?.beforeBuildCommand?.includes('--config vite.config.ts')
) {
+1
View File
@@ -1476,6 +1476,7 @@ dependencies = [
"chromiumoxide",
"futures",
"http",
"image",
"libc",
"platform-agent",
"platform-llm",
@@ -12,6 +12,7 @@ base64 = "0.22"
chromiumoxide = "0.9.1"
futures = "0.3"
http = "1"
image = { version = "0.25", default-features = false, features = ["png"] }
rmcp = { version = "2.2.0", default-features = false, features = ["client", "reqwest-native-tls", "transport-child-process", "transport-streamable-http-client-reqwest"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
@@ -37,4 +38,4 @@ tauri-plugin-clipboard-manager = "2.3.2"
libc = "0.2"
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_Storage_FileSystem", "Win32_System_JobObjects"] }
windows-sys = { version = "0.61", features = ["Wdk_Storage_FileSystem", "Win32_Foundation", "Win32_Storage_FileSystem", "Win32_System_IO", "Win32_System_JobObjects"] }
@@ -405,9 +405,88 @@ pub(in crate::agent) struct PreparedPlatformArtAssetGeneration {
generated_prompt: Option<String>,
model: Option<String>,
provider: Option<String>,
slice_warning: Option<String>,
generation_route: String,
generation_kind: String,
reference_resource_ids: Vec<String>,
extension: String,
}
fn canonical_art_spec_reference_at(
root: &Path,
expected_canvas_project_id: &str,
) -> Result<String, String> {
let manifest = read_manifest_for_project(root)?;
let source = manifest
.assets
.iter()
.find(|asset| {
asset.local_path == AGENT_RUNTIME_ART_SPEC_PATH
&& asset.kind == "icon-spec"
&& asset.media_type.starts_with("image/")
&& asset.source.kind == GameCreationAppAssetSourceKind::Canvas
})
.ok_or_else(|| {
"派生视觉资产需要先完成并登记 assets/art-spec.png;请等待 art-director 后重试"
.to_string()
})?;
let source_path = resolve_local_project_path(root, &source.local_path)?;
if !source_path.is_file() {
return Err(
"派生视觉资产的规范图 assets/art-spec.png 不存在;请等待 art-director 后重试"
.to_string(),
);
}
if source.source.canvas_project_id.as_deref() != Some(expected_canvas_project_id) {
return Err(
"assets/art-spec.png 不属于当前 External Editor 画布项目,不能作为派生视觉资产参考源"
.to_string(),
);
}
source
.source
.resource_id
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_string)
.ok_or_else(|| {
"assets/art-spec.png 缺少 External Editor 项目资源 ID,不能伪造规范图引用".to_string()
})
}
fn canonical_art_spritesheet_icon_descriptions(prompt: &str) -> Vec<String> {
let project_context = truncate_inline(prompt.trim(), 320);
[
"当前玩法的玩家主体、朝向或动作状态与可组合部件",
"当前玩法的目标物、收集物、敌对实体或危险物",
"当前玩法需要的地块、障碍、资源物件与场景装饰",
"得分、受击、成长、失败、胜利与操作反馈特效",
]
.into_iter()
.map(|category| format!("{category};遵循同一项目视觉规范:{project_context}"))
.collect()
}
fn platform_art_spritesheet_has_transparent_pixels(download: &CanvasResourceDownload) -> bool {
image::load_from_memory(&download.bytes)
.ok()
.map(|image| image.to_rgba8().pixels().any(|pixel| pixel[3] < u8::MAX))
.unwrap_or(false)
}
fn platform_art_generation_postprocess_failure(generated: &serde_json::Value) -> Option<String> {
let warning = generated
.get("warning")
.filter(|warning| !warning.is_null())?;
let code = json_string_field(warning, "code").unwrap_or_else(|| "unknown".to_string());
let reason = json_string_field(warning, "reason")
.unwrap_or_else(|| "透明背景后处理未生成可用衍生物".to_string());
Some(format!(
"平台图片生成完成但透明后处理失败({code}):{reason};provider 源图已由服务端保留,不得登记为透明图集或自动重试"
))
}
pub(in crate::agent) async fn generate_platform_art_asset_with_options_at(
root: &Path,
prompt: &str,
@@ -444,34 +523,69 @@ pub(in crate::agent) async fn request_platform_art_asset_with_options_at(
let canvas_context =
prepare_external_canvas_generation_context(root, &client, &api_base_url, &api_key).await?;
let generation_prompt = build_platform_art_asset_prompt(prompt, briefs, options);
let generation_kind = if options.asset_kind == "ui-prototype" {
"ui-design"
let generation_kind = match options.asset_kind.as_str() {
"ui-prototype" => "ui-design",
"art-spritesheet" => "icon-spritesheet",
_ => "spec",
};
let is_canonical_art_spritesheet = options.asset_kind == "art-spritesheet";
let canonical_reference = matches!(
options.asset_kind.as_str(),
"ui-prototype" | "art-spritesheet"
)
.then(|| canonical_art_spec_reference_at(root, &canvas_context.project_id))
.transpose()?;
let (endpoint, request_body) = if is_canonical_art_spritesheet {
let reference_image_src = canonical_reference
.as_deref()
.ok_or_else(|| "透明美术图集缺少规范图引用".to_string())?;
(
"/api/external/v1/editor/icon-spritesheets/generations",
serde_json::json!({
"referenceImageSrc": reference_image_src,
"iconDescriptions": canonical_art_spritesheet_icon_descriptions(&generation_prompt),
"screenColor": "auto",
"aspectRatio": options.aspect_ratio,
"imageSize": options.image_size,
"assetLabel": options.asset_label,
"projectId": canvas_context.project_id,
"assetFolderId": canvas_context.asset_folder_id,
"generationInputs": {
"artSpec": platform_art_asset_art_spec(options),
},
"canvasCompletion": {
"title": options.asset_label,
"placeholder": external_canvas_placeholder(&options.aspect_ratio),
},
}),
)
} else {
"spec"
(
"/api/external/v1/editor/images/generations",
serde_json::json!({
"prompt": generation_prompt,
"kind": generation_kind,
"aspectRatio": options.aspect_ratio,
"imageSize": options.image_size,
"assetKind": options.asset_kind,
"assetLabel": options.asset_label,
"projectId": canvas_context.project_id,
"assetFolderId": canvas_context.asset_folder_id,
"generationInputs": {
"artSpec": platform_art_asset_art_spec(options),
},
"referenceImageSrcs": canonical_reference.clone().into_iter().collect::<Vec<_>>(),
"canvasCompletion": {
"title": options.asset_label,
"placeholder": external_canvas_placeholder(&options.aspect_ratio),
},
}),
)
};
let response = client
.post(format!(
"{}/api/external/v1/editor/images/generations",
api_base_url
))
.post(format!("{api_base_url}{endpoint}"))
.bearer_auth(&api_key)
.json(&serde_json::json!({
"prompt": generation_prompt,
"kind": generation_kind,
"aspectRatio": options.aspect_ratio,
"imageSize": options.image_size,
"assetKind": options.asset_kind,
"assetLabel": options.asset_label,
"projectId": canvas_context.project_id,
"assetFolderId": canvas_context.asset_folder_id,
"generationInputs": {
"artSpec": platform_art_asset_art_spec(options),
},
"canvasCompletion": {
"title": options.asset_label,
"placeholder": external_canvas_placeholder(&options.aspect_ratio),
},
}))
.json(&request_body)
.send()
.await
.map_err(|error| format!("请求平台图片生成失败:{error}"))?;
@@ -484,12 +598,39 @@ pub(in crate::agent) async fn request_platform_art_asset_with_options_at(
.await
.map_err(|error| format!("解析平台图片生成响应失败:{error}"))?;
let generated = payload.get("data").unwrap_or(&payload);
let download = resolve_canvas_resource_download(&client, &api_base_url, &api_key, generated)
.await?
.ok_or_else(|| "平台图片生成响应缺少可下载图片".to_string())?;
if let Some(error) = platform_art_generation_postprocess_failure(generated) {
return Err(error);
}
let null = serde_json::Value::Null;
let resource = generated.get("resource").unwrap_or(&null);
let asset = generated.get("asset").unwrap_or(&null);
let resource = if is_canonical_art_spritesheet {
generated.get("spritesheetResource").unwrap_or(&null)
} else {
generated.get("resource").unwrap_or(&null)
};
let asset = if is_canonical_art_spritesheet {
generated.get("spritesheetAsset").unwrap_or(&null)
} else {
generated.get("asset").unwrap_or(&null)
};
let download_source = if resource.is_object() {
resource
} else {
generated
};
let download =
resolve_canvas_resource_download(&client, &api_base_url, &api_key, download_source)
.await?
.ok_or_else(|| "平台图片生成响应缺少可下载图片".to_string())?;
if is_canonical_art_spritesheet && !platform_art_spritesheet_has_transparent_pixels(&download) {
return Err(
"External Editor 返回的 art-spritesheet 没有真实透明像素,已拒绝把不透明源图登记为正式图集"
.to_string(),
);
}
let slice_warning = generated
.get("sliceWarning")
.filter(|warning| !warning.is_null())
.and_then(|warning| json_string_field(warning, "reason"));
let resource_id = json_string_field(resource, "resourceId");
let task_id =
json_string_field(generated, "taskId").or_else(|| json_string_field(resource, "taskId"));
@@ -507,8 +648,11 @@ pub(in crate::agent) async fn request_platform_art_asset_with_options_at(
json_string_field(generated, "model").or_else(|| json_string_field(resource, "model"));
let provider = json_string_field(generated, "provider")
.or_else(|| json_string_field(resource, "provider"));
let source_hint = json_string_field(generated, "objectKey")
.or_else(|| json_string_field(generated, "imageSrc"));
let source_hint = json_string_field(resource, "objectKey")
.or_else(|| json_string_field(resource, "imageSrc"))
.or_else(|| json_string_field(generated, "objectKey"))
.or_else(|| json_string_field(generated, "imageSrc"))
.or_else(|| json_string_field(generated, "spritesheetImageSrc"));
let extension = infer_file_extension(source_hint.as_deref(), &download.media_type).to_string();
Ok(PreparedPlatformArtAssetGeneration {
requested_output_path,
@@ -522,6 +666,10 @@ pub(in crate::agent) async fn request_platform_art_asset_with_options_at(
generated_prompt,
model,
provider,
slice_warning,
generation_route: endpoint.to_string(),
generation_kind: generation_kind.to_string(),
reference_resource_ids: canonical_reference.into_iter().collect(),
extension,
})
}
@@ -594,6 +742,10 @@ fn commit_prepared_platform_art_asset_with_before_replace_hook(
generated_prompt,
model,
provider,
slice_warning,
generation_route,
generation_kind,
reference_resource_ids,
extension,
} = prepared;
let file_stem = resource_id
@@ -743,6 +895,9 @@ fn commit_prepared_platform_art_asset_with_before_replace_hook(
task_id: task_id.clone(),
prompt: generated_prompt.clone(),
model: model.clone(),
generation_route: Some(generation_route.clone()),
generation_kind: Some(generation_kind.clone()),
reference_resource_ids: reference_resource_ids.clone(),
},
) {
Ok(registered) => registered,
@@ -776,6 +931,10 @@ fn commit_prepared_platform_art_asset_with_before_replace_hook(
"provider": provider.clone(),
"assetFolderId": canvas_context.asset_folder_id,
"canvasName": canvas_context.canvas_name,
"sliceWarning": slice_warning.clone(),
"generationRoute": generation_route,
"generationKind": generation_kind,
"referenceResourceIds": reference_resource_ids,
}),
)?;
Ok(GeneratedPlatformArtAsset {
@@ -784,31 +943,44 @@ fn commit_prepared_platform_art_asset_with_before_replace_hook(
asset_object_id,
task_id,
model,
slice_warning,
})
}
pub(crate) fn platform_art_asset_art_spec(
options: &PlatformArtAssetGenerationOptions,
) -> serde_json::Value {
if options.asset_kind == "icon-spec" {
return serde_json::json!({
"assetType": "icon-spec",
"subject": "游戏角色、场景、UI 图标与反馈特效的统一视觉规范",
"style": "原创、正交展示、轮廓清楚、材质与光照一致的视觉规范板",
"palette": "统一主色、辅色、强调色和状态色,保证桌面端与移动端可读性",
"composition": "严格 1:1 规范板;分区展示玩家主体、目标物、场景地块、UI 图标、状态反馈与材质色板",
"format": format!("{} {}", options.aspect_ratio, options.image_size),
"constraints": "这是后续完整 UI 与透明图集共同引用的真实规范图;元素不得相互遮挡,不得做成完整游戏截图、海报或无结构概念画;必须原创,不得复刻现有游戏角色、Logo、贴图、标志性轮廓或受保护视觉语言",
"references": [],
});
}
if options.asset_kind == "ui-prototype" {
return serde_json::json!({
"assetType": "ui",
"subject": "完整桌面端游戏 UI 原型,包含 HUD、卡牌控件、战场区和操作控件",
"subject": "严格依据当前项目玩法合同生成的完整游戏 UI 原型,包含状态 HUD、主要可玩区域、关键实体、操作控件、失败与重开流程",
"style": "正视角、清晰分区、可指导 HTML/CSS 实现的高保真 UI/UX mockup",
"palette": "与原创游戏主题一致,文字与控件对比清楚",
"composition": "严格 16:9 单屏界面;顶部资源与波次 HUD,左侧或顶部单位卡槽,中部战场网格,右侧敌人入口,底部或角落放置开始、暂停重开和操作提示",
"composition": "严格 16:9 单屏界面;按当前玩法组织状态 HUD、主要可玩区域、目标或收集物说明、开始/移动/暂停/重开控件,并体现移动端触控布局",
"format": format!("{} {}", options.aspect_ratio, options.image_size),
"constraints": "必须明显展示资源数值、单位卡牌、冷却/费用、波次进度、开始或暂停或重开控件和操作反馈;不得只生成无 HUD 的场景插画、战斗概念图、地图或宣传图;玩法类型只描述功能,不授权复刻现有作品,必须重建原创标题、资源名、单位名、角色轮廓与场景语言不得复刻、翻译或近似改写现有游戏角色、单位名、Logo、贴图、标志性布局或受保护视觉语言",
"constraints": "不得假设项目属于塔防或补入玩法合同中不存在的卡牌、波次、敌人入口等结构;必须清楚展示当前玩法实际需要的信息、实体、操作、失败与重开状态;不得只生成无 HUD 的场景插画、概念图、地图或宣传图;必须重建原创标题、资源名、角色轮廓与场景语言不得复刻、翻译或近似改写现有游戏角色、Logo、贴图、标志性布局或受保护视觉语言",
"references": [],
});
}
serde_json::json!({
"assetType": "art",
"subject": format!("{};使用项目原创命名和原创阵营设计", options.asset_label),
"style": "清晰可切分的原创 Web 游戏素材图集;以非拟人生态构装体、晶体、菌丝、雾气或器物轮廓建立独立视觉身份,避免现有塔防游戏的可识别角色语言",
"composition": format!("{} 游戏素材图集,按单位、敌人、地块和特效分区,留出清楚切分间距", options.aspect_ratio),
"style": "清晰可切分的原创 Web 游戏素材图集;严格沿用当前规范图的轮廓、材质、色板与光照,不预设塔防或其他固定玩法",
"composition": format!("{} 游戏素材图集,按玩家主体及状态、目标或收集物、障碍或场景元素、反馈特效分区,留出清楚切分间距", options.aspect_ratio),
"format": format!("{} {}", options.aspect_ratio, options.image_size),
"constraints": "必须是可见的真实图片产物,不得用纯文本计划代替;禁止带脸向日葵、嘴状豌豆炮管、草坪横排、僵尸人形、现有游戏 Logo、贴图、图标式角色排布、标志性轮廓或近似配色;植物或生态主题不得直接画成大众熟知塔防角色的变体",
"constraints": "必须是可见的真实透明图片产物,不得用纯文本计划代替;只生成当前项目玩法合同需要的实体和反馈,不得擅自加入单位卡牌、波次敌人或其他玩法;禁止现有游戏 Logo、贴图、标志性角色轮廓、图标排布或近似配色",
"references": [],
})
}
@@ -818,9 +990,15 @@ pub(crate) fn build_platform_art_asset_prompt(
briefs: &[AgentGroupBrief],
options: &PlatformArtAssetGenerationOptions,
) -> String {
if options.asset_kind == "icon-spec" {
return format!(
"为这个 Web 小游戏生成一张 1:1 的统一视觉规范图,作为后续 UI 设计图和透明游戏图集的共同权威参考。规范板必须分区展示:玩家主体及其成长形态、核心目标或收集物、场景地块与障碍、HUD/操作图标、得分/受击/胜负反馈、主辅强调色与材质规则。所有元素使用一致的正交视角、轮廓、光照和原创视觉语言,留出清楚间距;不要生成完整游戏截图、海报、黑底图集或纯文字说明。玩法机制只用于理解功能,不授权复刻现有作品。\n\n项目视觉需求:{}",
truncate_prompt_context(prompt.trim())
);
}
if options.asset_kind == "ui-prototype" {
return format!(
"生成一张真正的游戏 UI/UX 原型图,不是场景概念图。画面必须是完整 16:9 桌面端单屏界面,明确可见:顶部资源数值与波次/状态 HUD;单位卡牌及费用、冷却状态;中部战场网格;右侧敌人来袭方向;开始、暂停、重开控件;基础操作提示和点击/资源不足等反馈。使用正视角、清晰分区和可读占位文字,使前端开发可直接据此拆分 HTML/CSS。禁止只画草地、角色和敌人的无 HUD 战斗画面,禁止做海报、地图或纯插画。玩法类型和机制只用于理解功能,不代表允许复刻现有作品;生成前必须把输入中可识别的现有游戏标题、资源、角色或单位名转换为新的原创命名,并重建角色轮廓、配色、场景材质和界面视觉语言。不得在画面中渲染 Sunflower、Peashooter、向日葵、豌豆射手、僵尸等知名塔防元素,也不得使用现有游戏 Logo、贴图、标志性布局或受保护视觉语言。\n\n项目 UI 需求:{}",
"生成一张真正的游戏 UI/UX 原型图,不是场景概念图。必须严格从下方当前项目 UI 需求提取玩法,不得自行假设它属于塔防或加入需求中不存在的单位卡牌、费用、波次、敌人入口等结构。画面是完整 16:9 桌面端单屏界面,并同时明确移动端重排意图;清楚呈现当前玩法所需的分数/资源/生命/局内状态 HUD、主要可玩区域、玩家与目标/收集物/危险物、开始和主要操作、失败状态与重新开始、键盘和触控提示。使用正视角、清晰分区和可读占位文字,使前端开发可直接据此拆分 HTML/CSS。禁止只画无 HUD 场景、概念图、地图、海报或纯插画。玩法机制只用于理解功能,不授权复刻现有作品;必须采用项目已经确定的原创命名角色轮廓、配色、场景材质和界面视觉语言不得使用现有游戏 Logo、贴图、标志性布局或受保护视觉语言。\n\n当前项目 UI 需求:{}",
truncate_prompt_context(prompt.trim())
);
}
@@ -834,7 +1012,7 @@ pub(crate) fn build_platform_art_asset_prompt(
.filter(|markdown| !markdown.is_empty())
.unwrap_or("需要一张可直接用于 Web 小游戏首版原型的核心美术素材。");
format!(
"为 Web 小游戏首版原型生成一张可切分的原创核心美术素材图集,适合放入本地 assets 并被 canvas 游戏直接引用。必须先从美术 brief 提取项目自己的标题、阵营、资源和单位名,并据此创造非拟人、非标志性的新轮廓。即使用户提到植物塔防或相似机制,也禁止画带脸向日葵、嘴状豌豆炮管、草坪横排、僵尸人形、现有游戏 Logo、贴图、标志性角色轮廓、图标式排布或近似配色不得把知名角色改色、换名或机械化后继续使用。优先采用晶体、菌丝、雾气、陶质器物、潮汐生态构装体等与项目设定一致的独立形态,按守卫、敌人、地块和特效分区并留出清楚切分间距。\n用户需求:{}\n美术资产 brief{}",
"为 Web 小游戏首版原型生成一张可切分的原创透明核心美术素材图集,适合放入本地 assets 并被游戏直接引用。严格从用户需求和美术 brief 提取当前项目自己的标题、玩法实体、目标物、收集物、障碍、状态与反馈;不得自行假设为塔防或加入合同中不存在的单位、敌人、波次、卡牌等内容。所有元素沿用当前规范图的轮廓、配色、材质和光照,分区排布并留出清楚切分间距。禁止现有游戏 Logo、贴图、标志性角色轮廓、图标式排布或近似配色不得把知名角色改色、换名或机械化后继续使用。\n用户需求:{}\n美术资产 brief{}",
truncate_inline(prompt, 240),
truncate_prompt_context(art_asset_brief)
)
@@ -843,6 +1021,61 @@ pub(crate) fn build_platform_art_asset_prompt(
#[cfg(test)]
mod canvas_generation_tests {
use super::*;
use image::{codecs::png::PngEncoder, ColorType, ImageEncoder};
fn rgba_test_png(alpha: u8) -> CanvasResourceDownload {
let mut bytes = Vec::new();
PngEncoder::new(&mut bytes)
.write_image(&[12, 34, 56, alpha], 1, 1, ColorType::Rgba8.into())
.expect("encode RGBA fixture");
CanvasResourceDownload {
bytes,
media_type: "image/png".to_string(),
}
}
#[test]
fn canonical_art_spritesheet_requires_real_transparent_pixels() {
assert!(platform_art_spritesheet_has_transparent_pixels(
&rgba_test_png(0)
));
assert!(!platform_art_spritesheet_has_transparent_pixels(
&rgba_test_png(u8::MAX)
));
}
#[test]
fn derived_visual_asset_waits_for_registered_art_spec() {
let temporary = tempfile::tempdir().expect("create art dependency project");
let root = temporary.path();
init_local_game_project_at(root, "art-dependency", "美术依赖测试").expect("init project");
let error = canonical_art_spec_reference_at(root, "canvas-project")
.expect_err("missing art spec must block derived visual generation");
assert!(error.contains("先完成并登记 assets/art-spec.png"));
assert!(error.contains("等待 art-director"));
}
#[test]
fn postprocess_warning_wins_over_slice_warning_and_preserves_reason() {
let payload = serde_json::json!({
"warning": {
"code": "postprocess-failed-source-preserved",
"reason": "抠图服务暂不可用,已保留源图。"
},
"sliceWarning": {
"code": "insufficient-connected-components",
"reason": "不应覆盖通用告警"
}
});
let error = platform_art_generation_postprocess_failure(&payload)
.expect("general warning must be authoritative");
assert!(error.contains("postprocess-failed-source-preserved"));
assert!(error.contains("抠图服务暂不可用,已保留源图。"));
assert!(error.contains("不得登记为透明图集或自动重试"));
assert!(!error.contains("不应覆盖通用告警"));
}
fn replacement_options() -> PlatformArtAssetGenerationOptions {
PlatformArtAssetGenerationOptions {
@@ -878,6 +1111,10 @@ mod canvas_generation_tests {
generated_prompt: Some("原创替换图集".to_string()),
model: Some("test-image-model".to_string()),
provider: Some("test-provider".to_string()),
slice_warning: None,
generation_route: "/api/external/v1/editor/icon-spritesheets/generations".to_string(),
generation_kind: "icon-spritesheet".to_string(),
reference_resource_ids: vec!["art-spec-resource".to_string()],
extension: "png".to_string(),
}
}
File diff suppressed because one or more lines are too long
@@ -44,6 +44,8 @@ pub(in crate::agent) use run_status_observation::*;
pub(in crate::agent) use structured_plan::*;
pub(in crate::agent) use tool_plan_protocol::*;
#[cfg(test)]
pub(crate) use action_audit::agent_runtime_action_receipt_safe_detail_for_owner_for_test;
pub(crate) use action_audit::{
agent_runtime_git_commit_safe_detail_value, agent_runtime_tool_action_fingerprint,
agent_runtime_tool_action_id, agent_runtime_tool_action_input_summary,
@@ -86,6 +88,7 @@ pub(crate) use pending_confirmation_ledger::{
write_game_creator_agent_runtime_tool_confirmation,
};
pub(crate) use project_gates::{
acquire_game_creator_agent_provider_plan_project_write_lock_with_wait,
acquire_game_creator_agent_runtime_project_write_lock_with_wait,
advance_agent_runtime_project_revision_locked,
agent_runtime_observation_advances_project_revision,
@@ -179,6 +179,21 @@ pub(in crate::agent) fn agent_runtime_action_receipt_safe_detail_for_owner(
)
}
#[cfg(test)]
pub(crate) fn agent_runtime_action_receipt_safe_detail_for_owner_for_test(
root: &Path,
receipt_agent_id: &str,
receipt_run_id: &str,
observation: &AgentRuntimeToolObservation,
) -> Option<String> {
agent_runtime_action_receipt_safe_detail_for_owner(
root,
receipt_agent_id,
receipt_run_id,
observation,
)
}
fn agent_runtime_action_receipt_safe_detail_with_owner(
root: &Path,
receipt_owner: Option<(&str, &str)>,
@@ -279,27 +294,58 @@ fn agent_runtime_action_receipt_safe_detail_with_owner(
let validation_profile = detail
.get("validationProfile")
.and_then(serde_json::Value::as_str)
.filter(|value| *value == AGENT_RUNTIME_UI_PROTOTYPE_VALIDATION_PROFILE);
.filter(|value| {
matches!(
*value,
AGENT_RUNTIME_UI_PROTOTYPE_VALIDATION_PROFILE
| AGENT_RUNTIME_UI_PROTOTYPE_LEGACY_VALIDATION_PROFILE
)
});
let (passed, checks, issues) = if validation_profile.is_some() {
if inspection_kind != Some(AGENT_RUNTIME_UI_PROTOTYPE_INSPECTION_KIND) {
return None;
}
let passed = detail.get("passed")?.as_bool()?;
let assessment = AgentRuntimeUiPrototypeAssessment {
checks: serde_json::from_value(detail.get("checks")?.clone()).ok()?,
issues: serde_json::from_value(detail.get("issues")?.clone()).ok()?,
summary: detail.get("conclusion")?.as_str()?.to_string(),
if validation_profile == Some(AGENT_RUNTIME_UI_PROTOTYPE_VALIDATION_PROFILE) {
let assessment = AgentRuntimeUiPrototypeAssessment {
checks: serde_json::from_value(detail.get("checks")?.clone()).ok()?,
issues: serde_json::from_value(detail.get("issues")?.clone()).ok()?,
summary: detail.get("conclusion")?.as_str()?.to_string(),
}
.validate()
.ok()?;
if passed != assessment.passed() || passed != (observation.status == "ok") {
return None;
}
(
Some(passed),
Some(serde_json::to_value(&assessment.checks).ok()?),
Some(serde_json::to_value(&assessment.issues).ok()?),
)
} else {
let checks = serde_json::from_value::<AgentRuntimeUiPrototypeLegacyChecks>(
detail.get("checks")?.clone(),
)
.ok()?;
let issues =
serde_json::from_value::<Vec<String>>(detail.get("issues")?.clone()).ok()?;
if issues.len() > 8
|| issues.iter().any(|issue| {
issue.trim().is_empty()
|| issue.chars().count() > 160
|| sanitize_agent_runtime_text(issue, 160) != *issue
})
|| passed != (checks.all_passed() && issues.is_empty())
|| passed != (observation.status == "ok")
{
return None;
}
(
Some(passed),
Some(serde_json::to_value(&checks).ok()?),
Some(serde_json::to_value(&issues).ok()?),
)
}
.validate()
.ok()?;
if passed != assessment.passed() || passed != (observation.status == "ok") {
return None;
}
(
Some(passed),
Some(serde_json::to_value(&assessment.checks).ok()?),
Some(serde_json::to_value(&assessment.issues).ok()?),
)
} else {
if inspection_kind.is_some()
|| !matches!(detail.get("passed"), None | Some(serde_json::Value::Null))
@@ -264,6 +264,7 @@ pub(in crate::agent) fn validate_agent_runtime_autonomous_initial_collaboration_
) -> Result<(), String> {
let mut code_prototype = None;
let mut quality_review = None;
let mut art_director = None;
let mut art_asset_plan = None;
for action in &plan.actions {
let Some(input) = autonomous_initial_delegate_input(action)? else {
@@ -285,6 +286,7 @@ pub(in crate::agent) fn validate_agent_runtime_autonomous_initial_collaboration_
let slot = match target_agent_id {
"code-prototype" => &mut code_prototype,
AGENT_RUNTIME_QUALITY_REVIEW_AGENT_ID => &mut quality_review,
"art-director" => &mut art_director,
"art-asset-plan" => &mut art_asset_plan,
_ => continue,
};
@@ -348,6 +350,31 @@ pub(in crate::agent) fn validate_agent_runtime_autonomous_initial_collaboration_
"首批 quality-review 的 expectedArtifacts 必须为 []",
));
}
if let Some(art_director) = art_director {
let art_task = autonomous_initial_delegate_task(art_director, "art-director")?;
let art_criteria = agent_runtime_tool_input_string_list(
&serde_json::Value::Object(art_director.clone()),
&["acceptanceCriteria", "acceptance_criteria", "criteria"],
);
if std::iter::once(art_task.as_str())
.chain(art_criteria.iter().map(String::as_str))
.any(agent_runtime_task_explicitly_requires_read_only_delivery)
{
return Err(autonomous_initial_collaboration_contract_error(
"首批 art-director 必须是非只读规范图生成任务",
));
}
let art_artifacts =
autonomous_initial_delegate_expected_artifacts(art_director, "art-director")?;
if !art_artifacts
.iter()
.any(|path| path == "assets/art-spec.png")
{
return Err(autonomous_initial_collaboration_contract_error(
"首批 art-director 的 expectedArtifacts 必须包含 assets/art-spec.png",
));
}
}
if let Some(art_asset_plan) = art_asset_plan {
let art_task = autonomous_initial_delegate_task(art_asset_plan, "art-asset-plan")?;
let art_criteria = agent_runtime_tool_input_string_list(
@@ -1495,10 +1522,38 @@ mod tests {
.expect("resume first manifest task fixture");
for (local_path, kind) in [
("assets/art-spec.png", "icon-spec"),
("assets/ui-prototype.png", "ui-prototype"),
("assets/art-spritesheet.png", "art-spritesheet"),
] {
std::fs::write(root.join(local_path), b"visual fixture").expect("write visual fixture");
let (generation_route, generation_kind, reference_resource_ids) = match kind {
"icon-spec" => (
"/api/external/v1/editor/images/generations",
"spec",
Vec::new(),
),
"ui-prototype" => (
"/api/external/v1/editor/images/generations",
"ui-design",
vec!["resource-icon-spec".to_string()],
),
_ => (
"/api/external/v1/editor/icon-spritesheets/generations",
"icon-spritesheet",
vec!["resource-icon-spec".to_string()],
),
};
let bytes = if kind == "art-spritesheet" {
use image::ImageEncoder;
let mut bytes = Vec::new();
image::codecs::png::PngEncoder::new(&mut bytes)
.write_image(&[12, 34, 56, 0], 1, 1, image::ColorType::Rgba8.into())
.expect("encode transparent visual fixture");
bytes
} else {
b"\x89PNG\r\n\x1a\nfixture".to_vec()
};
std::fs::write(root.join(local_path), bytes).expect("write visual fixture");
register_local_asset_at(
&root,
local_path,
@@ -1513,6 +1568,9 @@ mod tests {
task_id: Some(format!("task-{kind}")),
prompt: Some("测试视觉资产".to_string()),
model: Some("gpt-image-2".to_string()),
generation_route: Some(generation_route.to_string()),
generation_kind: Some(generation_kind.to_string()),
reference_resource_ids,
},
)
.expect("register visual fixture");
@@ -28,18 +28,18 @@ pub(in crate::agent) fn validate_agent_runtime_pending_serialized_content(
) -> Result<(), String> {
let lower = content.to_ascii_lowercase();
let sensitive_rule = [
".env",
"game-creator.config",
"authorization:",
"cookie:",
"api_key",
"apikey",
"api key",
"token=",
"bearer ",
(0, ".env"),
(1, "game-creator.config"),
(2, "authorization:"),
(3, "cookie:"),
(4, "api_key"),
(5, "apikey"),
(7, "token="),
(8, "bearer "),
]
.into_iter()
.position(|marker| lower.contains(marker))
.find_map(|(rule, marker)| lower.contains(marker).then_some(rule))
.or_else(|| agent_runtime_contains_api_key_assignment(&lower).then_some(6))
.or_else(|| (redact_secret_tokens(content) != content).then_some(9));
if let Some(rule) = sensitive_rule {
return Err(format!(
@@ -59,6 +59,154 @@ pub(in crate::agent) fn validate_agent_runtime_pending_serialized_content(
Ok(())
}
fn agent_runtime_contains_api_key_assignment(lower: &str) -> bool {
lower.match_indices("api key").any(|(index, marker)| {
let Some(value) =
agent_runtime_api_key_assignment_value(lower[index + marker.len()..].trim_start())
else {
return false;
};
let value = value
.trim_start()
.trim_start_matches(|character| matches!(character, '"' | '\\'))
.trim_start();
!agent_runtime_api_key_assignment_is_safe_status(value)
})
}
fn agent_runtime_api_key_assignment_value(remainder: &str) -> Option<&str> {
let mut cursor = 0usize;
loop {
if remainder[..cursor].chars().count() > 64 {
return None;
}
let current = &remainder[cursor..];
let trimmed = current.trim_start();
cursor += current.len().saturating_sub(trimmed.len());
let current = &remainder[cursor..];
let character = current.chars().next()?;
if matches!(character, ':' | '=' | '') {
return Some(&current[character.len_utf8()..]);
}
if matches!(character, '*' | '`' | '_' | '~' | '\\' | '\'' | '"' | ']') {
cursor += character.len_utf8();
continue;
}
let closing = match character {
'(' => Some(')'),
'' => Some(''),
'[' => Some(']'),
'【' => Some('】'),
_ => None,
};
if let Some(closing) = closing {
let after_open = &current[character.len_utf8()..];
let closing_index = after_open.find(closing)?;
let qualifier = &after_open[..closing_index];
if qualifier.chars().count() > 32
|| qualifier
.chars()
.any(|value| matches!(value, ':' | '=' | '' | '"' | '\n' | '\r'))
{
return None;
}
cursor += character.len_utf8() + closing_index + closing.len_utf8();
continue;
}
let qualifier = [
"value",
"production",
"development",
"prod",
"test",
"dev",
"",
"生产",
"测试",
"开发",
]
.into_iter()
.find(|qualifier| {
current.strip_prefix(qualifier).is_some_and(|suffix| {
suffix.chars().next().is_none_or(|next| {
next.is_whitespace()
|| matches!(
next,
':' | '=' | '' | '*' | '`' | '_' | '~' | '' | '(' | '[' | '【'
)
})
})
})?;
cursor += qualifier.len();
}
}
fn agent_runtime_api_key_assignment_is_safe_status(value: &str) -> bool {
let value = agent_runtime_serialized_string_value(value)
.trim()
.trim_end_matches(['。', '.']);
if [
"当前未配置",
"未配置",
"没有配置",
"未提供",
"缺失",
"不存在",
"不可用",
"为空",
"禁止",
"不要",
"不得",
"无需",
"not configured",
"unconfigured",
"not available",
"unavailable",
"missing",
"absent",
"none",
"empty",
"not provided",
"do not",
"never",
"disabled",
]
.into_iter()
.any(|safe_status| value == safe_status)
{
return true;
}
matches!(
value,
"当前未配置,请按无密钥路径降级"
| "未配置,请按无密钥路径降级"
| "not configured; use the text-only fallback"
)
}
fn agent_runtime_serialized_string_value(value: &str) -> &str {
for (index, character) in value.char_indices() {
if character != '"' {
continue;
}
let escaped = value[..index]
.as_bytes()
.iter()
.rev()
.take_while(|byte| **byte == b'\\')
.count()
% 2
== 1;
if !escaped {
return &value[..index];
}
}
value
}
pub(crate) fn agent_runtime_contains_secret_key_prefix(content: &str, prefix: &str) -> bool {
content
.match_indices(prefix)
@@ -531,3 +679,57 @@ pub(in crate::agent) fn consume_game_creator_agent_runtime_tool_confirmation(
})?;
Ok(!action_fingerprint.trim().is_empty() && confirmed_fingerprint == action_fingerprint)
}
#[cfg(test)]
mod tests {
use super::validate_agent_runtime_pending_serialized_content;
use std::path::Path;
#[test]
fn pending_content_allows_api_key_security_guidance_without_secret_material() {
for task in [
"继续修复失败的视觉任务;不要读取或暴露 External Editor API Key。",
"External Editor API Key:当前未配置,请按无密钥路径降级。",
"External Editor API Key: not configured; use the text-only fallback.",
"不要读取或暴露 External Editor API Key;失败时:改走文本降级。",
"不要暴露 API Key,说明见 https://example.test/docs",
] {
let content = serde_json::json!({
"action": {
"tool": "agent.delegate",
"input": { "task": task }
}
})
.to_string();
validate_agent_runtime_pending_serialized_content(Path::new("C:\\workspace"), &content)
.expect("natural-language API Key guidance is not secret material");
}
}
#[test]
fn pending_content_still_rejects_api_key_fields_and_secret_tokens() {
let root = Path::new("C:\\workspace");
for (content, rule) in [
(r#"{"apiKey":"plain-secret-material"}"#, 5),
(r#"{"api_key":"plain-secret-material"}"#, 4),
(r#"{"task":"API Key: plain-secret-material"}"#, 6),
(r#"{"task":"API Key = plain-secret-material"}"#, 6),
(r#"{"task":"API Key: none-but-real-secret-material"}"#, 6),
(
r#"{"task":"API Key: not configured; actual value plain-secret-material"}"#,
6,
),
(r#"{"task":"API Key: disabled plain-secret-material"}"#, 6),
(r#"{"task":"**API Key**: plain-secret-material"}"#, 6),
(r#"{"task":"`API Key`: plain-secret-material"}"#, 6),
(r#"{"task":"API Key(生产): plain-secret-material"}"#, 6),
(r#"{"token":"token=plain-secret-material"}"#, 7),
(r#"{"note":"sk-prohibitedsecret"}"#, 9),
] {
let error = validate_agent_runtime_pending_serialized_content(root, content)
.expect_err("sensitive content must remain rejected");
assert!(error.contains(&format!("#{rule}")), "{content}: {error}");
}
}
}
@@ -857,13 +857,13 @@ pub(in crate::agent) fn ui_prototype_visual_inspection_blocker_detail_at_locked(
return Ok(None);
}
Ok(Some(format!(
"expectedPath={expected_path} · resourceBar={} · unitCardTray={} · battlefieldGrid={} · enemyEntryDirection={} · waveStatus={} · primaryControls={} · implementationClarity={} · originalTheme={} · issues={}",
assessment.checks.resource_bar,
assessment.checks.unit_card_tray,
assessment.checks.battlefield_grid,
assessment.checks.enemy_entry_direction,
assessment.checks.wave_status,
"expectedPath={expected_path} · informationHud={} · gameplaySurface={} · objectiveEntities={} · primaryControls={} · failureRestartFlow={} · responsiveLayout={} · implementationClarity={} · originalTheme={} · issues={}",
assessment.checks.information_hud,
assessment.checks.gameplay_surface,
assessment.checks.objective_entities,
assessment.checks.primary_controls,
assessment.checks.failure_restart_flow,
assessment.checks.responsive_layout,
assessment.checks.implementation_clarity,
assessment.checks.original_theme,
assessment.issues.join(""),
@@ -879,6 +879,7 @@ pub(in crate::agent) fn visual_asset_completion_blocker_at_locked(
return None;
}
let (expected_path, expected_kind, label) = match agent_id {
"art-director" => (AGENT_RUNTIME_ART_SPEC_PATH, "icon-spec", "统一视觉规范图"),
"design-foundation" => ("assets/ui-prototype.png", "ui-prototype", "策划界面原型图"),
"art-asset-plan" => (
"assets/art-spritesheet.png",
@@ -898,23 +899,15 @@ pub(in crate::agent) fn visual_asset_completion_blocker_at_locked(
});
}
};
let registered = manifest.assets.iter().any(|asset| {
asset.local_path == expected_path
&& asset.kind == expected_kind
&& asset.media_type.starts_with("image/")
&& asset.source.kind == GameCreationAppAssetSourceKind::Canvas
&& resolve_local_project_path(root, &asset.local_path)
.ok()
.is_some_and(|path| path.is_file())
});
if !registered {
if let Err(error) = validate_manifest_required_visual_asset(root, &manifest, agent_id) {
return Some(AgentRuntimeToolObservation {
tool: "runtime.visual_asset".to_string(),
status: "blocked".to_string(),
summary: format!("{label}尚未生成并登记,不能完成任务"),
summary: format!("{label}尚未按正式视觉流程生成并登记,不能完成任务"),
detail: Some(format!(
"expectedPath={expected_path} · expectedKind={expected_kind} · editorApiKeyConfigured={}",
editor_api_key_is_configured()
"expectedPath={expected_path} · expectedKind={expected_kind} · editorApiKeyConfigured={} · reason={}",
editor_api_key_is_configured(),
redact_agent_runtime_project_paths(root, &error, 300),
)),
});
}
@@ -1318,7 +1311,7 @@ pub(crate) fn acquire_game_creator_agent_runtime_project_write_lock_with_wait(
root: &Path,
command_id: &str,
) -> Result<ProjectWriteLock, String> {
const MAX_ATTEMPTS: usize = 200;
const MAX_ATTEMPTS: usize = 2_000;
for attempt in 0..MAX_ATTEMPTS {
match acquire_project_write_lock(root, command_id) {
Err(error)
@@ -1332,3 +1325,10 @@ pub(crate) fn acquire_game_creator_agent_runtime_project_write_lock_with_wait(
}
unreachable!("project write lock retry loop always returns")
}
pub(crate) fn acquire_game_creator_agent_provider_plan_project_write_lock_with_wait(
root: &Path,
command_id: &str,
) -> Result<ProjectWriteLock, String> {
acquire_game_creator_agent_runtime_project_write_lock_with_wait(root, command_id)
}
@@ -1,5 +1,91 @@
use super::*;
fn supervisor_collaboration_repair_action_key(action: &AgentRuntimeToolAction) -> Option<String> {
match action.tool.trim() {
"agent.delegate" => action
.input
.get("agentId")
.or_else(|| action.input.get("agent_id"))
.and_then(serde_json::Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(|agent_id| format!("delegate:{agent_id}")),
"agent.spawn_isolated" => Some("isolated".to_string()),
_ => None,
}
}
fn merge_supervisor_collaboration_repair_actions(
accumulated: &[AgentRuntimeToolAction],
current: &[AgentRuntimeToolAction],
) -> Vec<AgentRuntimeToolAction> {
let mut merged = accumulated.to_vec();
for action in current {
let Some(key) = supervisor_collaboration_repair_action_key(action) else {
continue;
};
if let Some(existing) = merged.iter_mut().find(|candidate| {
supervisor_collaboration_repair_action_key(candidate).as_deref() == Some(key.as_str())
}) {
*existing = action.clone();
} else {
merged.push(action.clone());
}
}
merged
}
fn supervisor_collaboration_missing_agent_ids(error: &str) -> Vec<String> {
let mut missing = error
.split_once("missingStaticAgents=")
.map(|(_, suffix)| {
suffix
.split(['·', '', ';', '\n'])
.next()
.unwrap_or_default()
.split(',')
.map(str::trim)
.filter(|value| {
!value.is_empty() && *value != "-" && !value.eq_ignore_ascii_case("none")
})
.map(str::to_string)
.collect::<Vec<_>>()
})
.unwrap_or_default();
for agent_id in ["code-prototype", "quality-review", "art-asset-plan"] {
if error.contains(&format!("缺少 {agent_id} 委派"))
&& !missing.iter().any(|value| value == agent_id)
{
missing.push(agent_id.to_string());
}
}
missing
}
fn restrict_supervisor_collaboration_repair_to_missing_agents(
request: &mut LlmRunRequest,
error: &str,
) -> Result<(), String> {
let missing = supervisor_collaboration_missing_agent_ids(error);
if missing.is_empty() {
return Ok(());
}
let delegate_function = native_runtime_function_name("agent.delegate")
.ok_or_else(|| "无法生成 Supervisor 首批委派修复工具名".to_string())?;
let delegate = request
.function_tools
.iter_mut()
.find(|tool| tool.name == delegate_function)
.ok_or_else(|| "Supervisor 首批委派修复工具目录缺少 agent.delegate".to_string())?;
let agent_id = delegate
.parameters
.pointer_mut("/properties/input/properties/agentId")
.and_then(serde_json::Value::as_object_mut)
.ok_or_else(|| "Supervisor 首批委派修复 agent.delegate schema 缺少 agentId".to_string())?;
agent_id.insert("enum".to_string(), serde_json::json!(missing));
Ok(())
}
pub(in crate::agent) fn append_game_creator_agent_tool_plan_audit_idempotent(
root: &Path,
record: serde_json::Value,
@@ -27,6 +113,10 @@ pub(in crate::agent) fn append_game_creator_agent_tool_plan_audit_idempotent(
.get("repairAttempt")
.and_then(serde_json::Value::as_u64)
.ok_or_else(|| "tool-plan 审计缺少 repairAttempt".to_string())?;
record
.get("appliedSteerCursor")
.and_then(serde_json::Value::as_u64)
.ok_or_else(|| "tool-plan 审计缺少 appliedSteerCursor".to_string())?;
if request_slot != format!("loop-{loop_iteration}-repair-{repair_attempt}") {
return Err("tool-plan 审计 requestSlot 与 loop/repair 身份不匹配".to_string());
}
@@ -53,7 +143,7 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
agent_runtime_run_profile_identity_at(root, agent_id, run_id, None, None)?;
let mcp_catalog = read_game_creator_mcp_catalog_at(root).await?;
let mut built_request = {
let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
let _lock = acquire_game_creator_agent_provider_plan_project_write_lock_with_wait(
root,
"runtime.provider_request.build.tool_plan",
)?;
@@ -103,7 +193,7 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
}
}
built_request = {
let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
let _lock = acquire_game_creator_agent_provider_plan_project_write_lock_with_wait(
root,
"runtime.provider_request.rebuild.tool_plan",
)?;
@@ -139,7 +229,7 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
.map(|sidecar| context_compaction_result(sidecar, true));
}
let provider_snapshot = {
let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
let _lock = acquire_game_creator_agent_provider_plan_project_write_lock_with_wait(
root,
"runtime.provider_request.capture.tool_plan",
)?;
@@ -182,6 +272,8 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
!= AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|| agent_runtime_autonomous_project_verify_available(root);
let mut autonomous_scaffold_repair_active = false;
let mut supervisor_collaboration_repair_active = false;
let mut supervisor_collaboration_repair_actions = Vec::new();
for repair_attempt in 0..=format_repair_attempts {
if game_creator_agent_runtime_cancel_requested_for(root, agent_id, run_id) {
return Err("Agent 后台任务已收到取消请求".to_string());
@@ -275,10 +367,29 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
"{:x}",
Sha256::digest(response_handoff.provider_request_id.as_bytes())
);
let mut supervisor_collaboration_candidate_actions = None;
let parsed = parse_game_creator_agent_tool_plan_llm_response_with_catalog_classified(
&response,
&mcp_catalog,
)
.map(|mut parsed| {
let merged = merge_supervisor_collaboration_repair_actions(
if supervisor_collaboration_repair_active {
&supervisor_collaboration_repair_actions
} else {
&[]
},
&parsed.plan.actions,
);
supervisor_collaboration_candidate_actions = Some(merged.clone());
if supervisor_collaboration_repair_active {
parsed.plan.plan_update = None;
parsed.plan.plan.clear();
parsed.plan.response.clear();
parsed.plan.actions = merged;
}
parsed
})
.and_then(|parsed| {
let source_payload = if run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD {
let verification_gate = read_game_creator_agent_runtime_verification_gate(
@@ -522,6 +633,7 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
"loopIteration": loop_index,
"repairAttempt": repair_attempt,
"requestSlot": request_slot,
"appliedSteerCursor": request_snapshot.applied_steer_cursor,
"responseFingerprint": response_fingerprint,
"providerRequestIdSha256": provider_request_id_sha256,
"protocol": protocol,
@@ -586,6 +698,7 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
"loopIteration": loop_index,
"repairAttempt": repair_attempt,
"requestSlot": request_slot,
"appliedSteerCursor": request_snapshot.applied_steer_cursor,
"responseFingerprint": response_fingerprint,
"providerRequestIdSha256": provider_request_id_sha256,
"attempt": next_attempt,
@@ -728,12 +841,20 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
}
}
if force_supervisor_initial_collaboration {
supervisor_collaboration_repair_active = true;
if let Some(actions) = supervisor_collaboration_candidate_actions.take() {
supervisor_collaboration_repair_actions = actions;
}
restrict_agent_runtime_supervisor_collaboration_repair_tools(&mut request)?;
restrict_supervisor_collaboration_repair_to_missing_agents(
&mut request,
&protocol_error,
)?;
let instruction = if run_profile
== AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
{
format!(
"上一条输出不符合工具计划协议:{protocol_error}\n本次修复的原生工具目录只保留首批协作工具。必须在同一响应一次性建立完整首批合同:code-prototype 必须是非只读实现任务且 expectedArtifacts 包含 game/index.htmlquality-review 的 task 必须显式声明只读、不得修改项目,且 expectedArtifacts 必须为 [];如当前 policy 的 requiredStaticAgentIds 包含 art-asset-plan,还必须加入非只读美术生成委派且 expectedArtifacts 同时包含 assets/manifest.art.json 与 assets/art-spritesheet.png。所有静态委派都使用 agent.delegaterepairOfDelegationId=null、runId=null;如当前 policy 还要求 isolated,再在同批补齐 agent.spawn_isolated。不得更新计划、读取、搜索、查询状态、修改项目或返回最终回复。不要解释,不要 markdown,不要代码围栏。"
"上一条输出不符合工具计划协议:{protocol_error}\n本次修复的原生工具目录只保留首批协作工具。必须在同一响应一次性建立完整首批合同:code-prototype 必须是非只读实现任务且 expectedArtifacts 包含 game/index.htmlquality-review 的 task 必须显式声明只读、不得修改项目,且 expectedArtifacts 必须为 [];如当前 policy 的 requiredStaticAgentIds 包含 art-director,必须加入非只读规范图委派且 expectedArtifacts 包含 assets/art-spec.png;如包含 design-foundation,必须加入非只读设计委派且 expectedArtifacts 包含 memory/project.md、game/game_design.md 与 assets/ui-prototype.png;如包含 art-asset-plan,还必须加入非只读美术生成委派且 expectedArtifacts 同时包含 assets/manifest.art.json 与 assets/art-spritesheet.png。所有静态委派都使用 agent.delegaterepairOfDelegationId=null、runId=null;如当前 policy 还要求 isolated,再在同批补齐 agent.spawn_isolated。不得更新计划、读取、搜索、查询状态、修改项目或返回最终回复。不要解释,不要 markdown,不要代码围栏。"
)
} else {
format!(
@@ -939,3 +1060,55 @@ pub(crate) async fn request_game_creator_agent_background_tool_plan_for_test(
}
}
}
#[cfg(test)]
mod supervisor_collaboration_repair_tests {
use super::*;
fn collaboration_action(tool: &str, input: serde_json::Value) -> AgentRuntimeToolAction {
AgentRuntimeToolAction {
tool: tool.to_string(),
reason: None,
input,
}
}
#[test]
fn missing_static_agents_ignores_none_sentinel() {
assert!(supervisor_collaboration_missing_agent_ids(
"Project Supervisor 首批协作不满足项目合同:static=1/2 · missingStaticAgents=none"
)
.is_empty());
assert_eq!(
supervisor_collaboration_missing_agent_ids(
"Project Supervisor 首批协作不满足项目合同:missingStaticAgents=code-prototype,quality-review · isolatedChildrenTotal=0"
),
vec!["code-prototype".to_string(), "quality-review".to_string()]
);
}
#[test]
fn isolated_repair_replaces_the_single_accumulated_slot() {
let accumulated = vec![collaboration_action(
"agent.spawn_isolated",
serde_json::json!({
"children": [{"templateAgentId": "quality-review", "task": "旧检查任务"}],
"joinMode": "all"
}),
)];
let replacement = collaboration_action(
"agent.spawn_isolated",
serde_json::json!({
"children": [{"templateAgentId": "quality-review", "task": "修正后的检查任务"}],
"joinMode": "all"
}),
);
let merged = merge_supervisor_collaboration_repair_actions(
&accumulated,
std::slice::from_ref(&replacement),
);
assert_eq!(merged, vec![replacement]);
}
}
@@ -768,6 +768,101 @@ fn response_stream_committed_checkpoint_recovers_by_idempotent_cleanup() {
.is_none());
}
#[test]
fn completed_orphan_finalization_cleanup_keeps_newer_terminal_run() {
let (project, state, response_revision, snapshot) =
response_stream_fixture("response-stream-completed-orphan-run");
let root = project.path();
let response = "旧 run 已完成,只残留幂等清理 journal。";
write_game_creator_agent_runtime_response_stream_ready_at(
root,
&snapshot,
response_revision,
response,
Some("stop"),
)
.expect("write orphan ready stream");
let outcome = finish_game_creator_agent_background_runtime_turn_with_checkpoint_at(
root,
state.clone(),
response,
response_revision,
&[],
|checkpoint| {
if checkpoint == AgentRuntimeFinalizationCheckpoint::ResponseStreamCommitted {
Err("injected-orphan-after-response-stream-committed".to_string())
} else {
Ok(())
}
},
)
.expect("leave completed orphan finalization");
assert!(matches!(
outcome,
AgentBackgroundFinalizationOutcome::Pending(_)
));
assert!(read_game_creator_agent_runtime_finalization_journal(
root,
&state.agent_id,
&state.run_id,
)
.expect("read retained orphan journal")
.is_some());
let mut stale_stream =
read_game_creator_agent_runtime_response_stream_at(root, &state.agent_id, &state.run_id)
.expect("read committed orphan stream")
.expect("committed orphan stream exists");
stale_stream.status = AGENT_RUNTIME_RESPONSE_STREAM_STATUS_READY.to_string();
stale_stream.request_slot = "final-reply-stale-slot".to_string();
stale_stream.response_revision = response_revision.saturating_sub(1);
stale_stream.accumulated_text = "上一版已过期回复".to_string();
write_game_creator_agent_runtime_response_stream_at(root, &stale_stream)
.expect("replace committed stream with stale ready residue");
let newer_run_id = "response-stream-newer-cancelled-run";
start_game_creator_agent_runtime_task_at(
root,
&state.agent_id,
"更新后的下一轮任务",
newer_run_id,
"agent-background-task",
"等待取消",
vec!["保留新 run 身份".to_string()],
)
.expect("start newer run");
cancel_game_creator_agent_runtime_task_at(root, &state.agent_id, newer_run_id)
.expect("cancel newer run");
assert_eq!(
cleanup_game_creator_agent_runtime_completed_finalizations_at(root)
.expect("clean completed orphan finalization"),
1
);
assert!(read_game_creator_agent_runtime_finalization_journal(
root,
&state.agent_id,
&state.run_id,
)
.expect("read cleaned orphan journal")
.is_none());
let repaired_stream =
read_game_creator_agent_runtime_response_stream_at(root, &state.agent_id, &state.run_id)
.expect("read repaired orphan stream")
.expect("repaired orphan stream exists");
assert_eq!(
repaired_stream.status,
AGENT_RUNTIME_RESPONSE_STREAM_STATUS_COMMITTED
);
assert_eq!(repaired_stream.accumulated_text, response);
assert_eq!(repaired_stream.response_revision, response_revision);
let current = read_game_creator_agent_runtime_at(root, &state.agent_id)
.expect("read newer runtime after orphan cleanup")
.state;
assert_eq!(current.run_id, newer_run_id);
assert_eq!(current.status, "cancelled");
assert_eq!(current.phase, "cancelled");
}
#[tokio::test]
async fn provider_handoff_identity_drift_closes_lifecycle_without_leaking_response() {
let (project, state, _response_revision, snapshot) =
@@ -47,7 +47,10 @@ pub(crate) fn agent_runtime_executable_tools() -> Vec<&'static str> {
}
fn autonomous_game_build_agent_can_generate_canvas_asset(agent_id: &str) -> bool {
matches!(agent_id.trim(), "design-foundation" | "art-asset-plan")
matches!(
agent_id.trim(),
"art-director" | "design-foundation" | "art-asset-plan"
)
}
pub(in crate::agent) fn agent_runtime_tool_policy_snapshot_at(
@@ -238,6 +238,8 @@ pub(crate) use interaction::{
answer_game_creator_agent_runtime_user_input_at, confirm_game_creator_agent_runtime_task_at,
pending_repository_context_drift_observation, reject_game_creator_agent_runtime_task_at,
};
#[cfg(test)]
pub(crate) use lifecycle_control::resolve_game_creator_agent_runtime_retry_configuration_at;
pub(crate) use lifecycle_control::{
append_game_creator_agent_runtime_queued_cancellation,
cancel_game_creator_agent_runtime_task_at,
@@ -253,17 +255,19 @@ pub(crate) use pending_execution::{
pub(crate) use pending_recovery::resume_game_creator_agent_pending_tool_action_at;
#[cfg(test)]
pub(crate) use pending_recovery::resume_game_creator_agent_provider_action_batch_for_test_at;
pub(crate) use provider_recovery::{
autonomous_manifest_parent_wake_error_is_transient,
schedule_waiting_autonomous_manifest_parent_wake_after_lane_release,
schedule_waiting_static_delegate_parent_wake_after_lane_release,
static_delegate_parent_wake_error_is_transient,
};
#[cfg(test)]
pub(crate) use provider_recovery::{
ensure_waiting_provider_retry_records_for_test,
probe_static_delegate_parent_wake_singleflight_coalescing,
};
pub(crate) use provider_recovery::{
schedule_waiting_autonomous_manifest_parent_wake_after_lane_release,
schedule_waiting_static_delegate_parent_wake_after_lane_release,
static_delegate_parent_wake_error_is_transient,
};
pub(crate) use recovery_scan::{
cleanup_game_creator_agent_runtime_completed_finalizations_at,
resume_game_creator_agent_background_tasks_at,
resume_game_creator_agent_pending_action_for_agent_at,
wake_pending_game_creator_agent_background_tasks_at,
@@ -738,6 +738,44 @@ pub(crate) fn append_game_creator_agent_runtime_queued_cancellation(
Ok(())
}
pub(crate) fn resolve_game_creator_agent_runtime_retry_configuration_at(
root: &Path,
task: &AgentRuntimeTaskRecord,
delegated: bool,
) -> Result<(String, String), String> {
let (run_profile, _) = agent_runtime_run_profile_identity_at(
root,
&task.agent_id,
&task.run_id,
Some(&task.run_profile),
Some(&task.run_profile_binding_fingerprint),
)?;
let source = if delegated {
"agent-delegate-retry".to_string()
} else if run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD {
let binding = read_game_creator_agent_runtime_run_profile_binding(
root,
&task.agent_id,
&task.run_id,
)?
.ok_or_else(|| "自主构建 Agent Runtime 重试缺少 Run Profile 绑定".to_string())?;
if binding.parent_run_id.is_some()
|| binding.root_agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|| binding.root_run_id != task.run_id
|| !matches!(
binding.source.as_str(),
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE | AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE
)
{
return Err("自主构建 Agent Runtime 重试绑定不是可信 Supervisor 根 Run".to_string());
}
binding.source
} else {
"agent-background-task".to_string()
};
Ok((run_profile, source))
}
pub(crate) fn retry_game_creator_agent_runtime_task_at(
root: &Path,
agent_id: &str,
@@ -848,11 +886,12 @@ pub(crate) fn retry_game_creator_agent_runtime_task_at(
}),
_ => None,
};
let retry_source = if retry_link.is_some() {
"agent-delegate-retry"
} else {
"agent-background-task"
};
let (retry_run_profile, retry_source) =
resolve_game_creator_agent_runtime_retry_configuration_at(
root,
&task,
retry_link.is_some(),
)?;
let (mut result, actual_retry_run_id) = with_agent_conversation_session_lane_at(
root,
&agent_id,
@@ -864,8 +903,8 @@ pub(crate) fn retry_game_creator_agent_runtime_task_at(
Some(&task.session_id),
&task.task,
&retry_run_id,
retry_source,
Some(&task.run_profile),
&retry_source,
Some(&retry_run_profile),
retry_link.as_ref(),
)
},
@@ -1,5 +1,31 @@
use super::*;
pub(in crate::agent) fn autonomous_registered_derived_visuals_need_repair_at(root: &Path) -> bool {
let Ok(manifest) = read_manifest_for_project(root) else {
return false;
};
[
(
"design-foundation",
"assets/ui-prototype.png",
"ui-prototype",
),
(
"art-asset-plan",
"assets/art-spritesheet.png",
"art-spritesheet",
),
]
.into_iter()
.any(|(task_id, local_path, kind)| {
manifest
.assets
.iter()
.any(|asset| asset.local_path == local_path && asset.kind == kind)
&& validate_manifest_required_visual_asset(root, &manifest, task_id).is_err()
})
}
pub(super) fn game_creator_agent_background_final_reply_fallback(
plan_response: &str,
run_profile: &str,
@@ -225,6 +251,7 @@ pub(in crate::agent) async fn run_game_creator_agent_background_task_pass_with_c
}
let autonomous_manifest_can_wait = agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
&& runtime.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& !autonomous_registered_derived_visuals_need_repair_at(&root)
&& !game_creator_agent_runtime_provider_action_batch_exists(
&root,
&agent_id,
@@ -1089,8 +1116,8 @@ pub(in crate::agent) async fn run_game_creator_agent_background_task_pass_with_c
if agent_id == "design-foundation" {
runtime.current_action = "等待可验收的 UI 原型图".to_string();
runtime.waiting_on =
"图片生成、manifest 登记与 ui-prototype.v1 结构化视觉检查".to_string();
runtime.next_step = "缺图时调用 canvas.asset_generate;已有候选时对 assets/ui-prototype.png 调用 image.inspect;未通过则经 file.delete 权限流程删除后重新生成".to_string();
"图片生成、manifest 登记与 ui-prototype.v2 结构化视觉检查".to_string();
runtime.next_step = "缺图时调用 canvas.asset_generate;已有候选时对 assets/ui-prototype.png 调用 image.inspect;未通过时如实返回 needs-repair,由 Supervisor 认领后发起唯一 repair 原位替换,禁止先删除正式图片".to_string();
} else {
runtime.current_action = "等待实际图片产物".to_string();
runtime.waiting_on = "图片生成确认、配置与本地 manifest 登记".to_string();
@@ -17,11 +17,34 @@ fn register_autonomous_recovery_visual_fixture(root: &Path, local_path: &str, ki
task_id: Some(format!("task-{kind}")),
prompt: None,
model: None,
generation_route: Some("/api/external/v1/editor/images/generations".to_string()),
generation_kind: Some("spec".to_string()),
reference_resource_ids: Vec::new(),
},
)
.expect("register autonomous recovery visual fixture");
}
#[test]
fn autonomous_parent_keeps_planning_before_scheduling_registered_legacy_derived_visuals() {
let root = std::env::temp_dir().join(format!(
"genarrative-agent-main-loop-legacy-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system clock")
.as_nanos()
));
init_local_game_project_at(&root, "legacy-derived-visuals", "旧派生视觉返工门禁")
.expect("project init");
assert!(!autonomous_registered_derived_visuals_need_repair_at(&root));
register_autonomous_recovery_visual_fixture(&root, "assets/ui-prototype.png", "ui-prototype");
assert!(autonomous_registered_derived_visuals_need_repair_at(&root));
fs::remove_dir_all(root).ok();
}
fn prepare_autonomous_completion_evidence(root: &Path, state: &AgentRuntimeState) -> u64 {
let contract = read_autonomous_completion_contract(root, &state.agent_id, &state.run_id)
.expect("read autonomous completion contract")
@@ -205,6 +228,37 @@ fn prepare_autonomous_completion_evidence(root: &Path, state: &AgentRuntimeState
revision
}
#[test]
fn autonomous_visual_ready_tasks_only_require_images_when_editor_api_key_is_configured() {
{
let _config_guard = crate::tests::write_test_local_config("{}".to_string());
assert!(!autonomous_manifest_ready_task_requires_visual_asset(
"art-director"
));
assert!(!autonomous_manifest_ready_task_requires_visual_asset(
"design-foundation"
));
assert!(!autonomous_manifest_ready_task_requires_visual_asset(
"art-asset-plan"
));
}
let _config_guard = crate::tests::write_test_local_config(
r#"{"editorApi":{"apiKey":"visual-ready-task-test-key"}}"#.to_string(),
);
assert!(autonomous_manifest_ready_task_requires_visual_asset(
"art-director"
));
assert!(autonomous_manifest_ready_task_requires_visual_asset(
"design-foundation"
));
assert!(autonomous_manifest_ready_task_requires_visual_asset(
"art-asset-plan"
));
assert!(!autonomous_manifest_ready_task_requires_visual_asset(
"code-prototype"
));
}
#[test]
fn autonomous_supervisor_empty_plan_uses_deterministic_final_reply_fallback() {
assert_eq!(
@@ -324,7 +378,7 @@ fn autonomous_manifest_waiting_context_persists_without_finishing_parent_run() {
async fn missing_completed_visual_asset_fails_same_child_without_retry() {
const PARENT_RUN_ID: &str = "autonomous-visual-recovery-parent";
const PARENT_TASK: &str = "生成完整小游戏并恢复丢失的正式视觉产物";
const CHILD_ID: &str = "design-foundation";
const CHILD_ID: &str = "art-director";
let _config_guard = crate::tests::write_test_local_config(
r#"{"editorApi":{"apiKey":"visual-recovery-test-key"}}"#.to_string(),
);
@@ -358,7 +412,7 @@ async fn missing_completed_visual_asset_fails_same_child_without_retry() {
"# 游戏设计\n\n恢复丢失图片后才能完成。\n",
)
.expect("write game design fixture");
register_autonomous_recovery_visual_fixture(&root, "assets/ui-prototype.png", "ui-prototype");
register_autonomous_recovery_visual_fixture(&root, "assets/art-spec.png", "icon-spec");
update_manifest_task_status_at(
&root,
"design-director",
@@ -400,7 +454,7 @@ async fn missing_completed_visual_asset_fails_same_child_without_retry() {
update_manifest_task_status_at(&root, CHILD_ID, GameCreationAppTaskStatus::Completed)
.expect("project initial visual completion fixture");
fs::remove_file(root.join("assets/ui-prototype.png")).expect("remove registered visual file");
fs::remove_file(root.join("assets/art-spec.png")).expect("remove registered visual file");
let downgraded = read_manifest_for_project(&root).expect("refresh missing visual manifest");
assert_eq!(
downgraded
@@ -595,7 +649,7 @@ async fn autonomous_supervisor_converged_final_reply_deserialize_commits_fallbac
RUN_ID,
)
.expect("read finalization residue");
for _ in 0..250 {
for _ in 0..500 {
if finalization_residue.is_none() {
break;
}
@@ -1,5 +1,9 @@
use super::*;
pub(crate) fn autonomous_manifest_parent_wake_error_is_transient(error: &str) -> bool {
error.starts_with("项目正在被其他写操作占用:")
}
pub(in crate::agent) fn schedule_waiting_provider_retry_wake_after_lane_release(
root: PathBuf,
agent_id: String,
@@ -127,6 +131,9 @@ async fn drive_waiting_autonomous_manifest_parent_wake_pass(
return;
}
},
Err(error) if autonomous_manifest_parent_wake_error_is_transient(&error) => {
continue;
}
Err(error) => {
let _ = mark_autonomous_manifest_parent_wake_needs_reconciliation_at(
root, agent_id, run_id, &error,
@@ -149,6 +149,272 @@ pub(in crate::agent) fn remove_game_creator_agent_runtime_provider_recovery_at(
provider_retry::remove_at(root, agent_id, run_id)
}
pub(crate) fn cleanup_game_creator_agent_runtime_completed_finalizations_at(
root: &Path,
) -> Result<usize, String> {
let journals = list_game_creator_agent_runtime_finalization_journals(root)?;
let mut removed = 0_usize;
for journal in journals {
if journal.status != AGENT_RUNTIME_FINALIZATION_STATUS_RUNTIME_COMPLETED {
continue;
}
let task = read_latest_game_creator_agent_runtime_task_by_run_id(
root,
&journal.agent_id,
&journal.run_id,
)?
.ok_or_else(|| {
format!(
"已完成 finalization 缺少所属任务:agent={} runId={}",
journal.agent_id, journal.run_id
)
})?;
if task.status != "completed"
|| task.phase != "completed"
|| task.agent_id != journal.agent_id
|| task.task_id != journal.task_id
|| task.session_id != journal.session_id
|| task.run_id != journal.run_id
|| task.source != journal.source
|| task.parent_agent_id != journal.parent_agent_id
|| task.parent_run_id != journal.parent_run_id
|| task.delegation_id != journal.delegation_id
|| task.goal_id != journal.goal_id
|| task.goal_revision != journal.goal_revision
{
return Err(format!(
"已完成 finalization 与所属终态任务身份冲突:agent={} runId={}",
journal.agent_id, journal.run_id
));
}
if !game_creator_agent_runtime_finalization_assistant_exists(root, &journal)? {
return Err(format!(
"已完成 finalization 的 assistant 消息缺失:agent={} runId={}",
journal.agent_id, journal.run_id
));
}
let (records, scan_truncated) =
read_agent_db_records_bounded(root, AGENT_RUNTIME_ACTION_HISTORY_MAX_DB_BYTES)?;
let finalization_records = records
.iter()
.filter(|record| {
record
.get("finalizationId")
.and_then(serde_json::Value::as_str)
== Some(journal.finalization_id.as_str())
})
.collect::<Vec<_>>();
let lifecycle_records = finalization_records
.iter()
.filter(|record| {
record.get("recordType").and_then(serde_json::Value::as_str)
== Some(AGENT_RUNTIME_FINALIZATION_LIFECYCLE_RECORD_TYPE)
})
.copied()
.collect::<Vec<_>>();
let lifecycle_stages = lifecycle_records
.iter()
.filter_map(|record| record.get("stage").and_then(serde_json::Value::as_str))
.collect::<Vec<_>>();
let exact_record_count = |record_type: &str| {
finalization_records
.iter()
.filter(|record| {
record.get("recordType").and_then(serde_json::Value::as_str)
== Some(record_type)
&& record.get("agentId").and_then(serde_json::Value::as_str)
== Some(journal.agent_id.as_str())
&& record.get("taskId").and_then(serde_json::Value::as_str)
== Some(journal.task_id.as_str())
&& record.get("sessionId").and_then(serde_json::Value::as_str)
== Some(journal.session_id.as_str())
&& record.get("runId").and_then(serde_json::Value::as_str)
== Some(journal.run_id.as_str())
&& record.get("messageId").and_then(serde_json::Value::as_str)
== Some(journal.message_id.as_str())
})
.count()
};
let conversation_message_count = finalization_records
.iter()
.filter(|record| {
record.get("recordType").and_then(serde_json::Value::as_str)
== Some("conversation.message")
&& record.get("agentId").and_then(serde_json::Value::as_str)
== Some(journal.agent_id.as_str())
&& record.get("sessionId").and_then(serde_json::Value::as_str)
== Some(journal.session_id.as_str())
&& record.get("messageId").and_then(serde_json::Value::as_str)
== Some(journal.message_id.as_str())
&& record.get("role").and_then(serde_json::Value::as_str) == Some("assistant")
})
.count();
let lifecycle_identity_matches = lifecycle_records.iter().all(|record| {
record.get("agentId").and_then(serde_json::Value::as_str)
== Some(journal.agent_id.as_str())
&& record.get("taskId").and_then(serde_json::Value::as_str)
== Some(journal.task_id.as_str())
&& record.get("sessionId").and_then(serde_json::Value::as_str)
== Some(journal.session_id.as_str())
&& record.get("runId").and_then(serde_json::Value::as_str)
== Some(journal.run_id.as_str())
&& record.get("messageId").and_then(serde_json::Value::as_str)
== Some(journal.message_id.as_str())
&& record.get("source").and_then(serde_json::Value::as_str)
== Some(journal.source.as_str())
});
if scan_truncated
|| finalization_records.len() != 7
|| !lifecycle_identity_matches
|| lifecycle_stages
!= [
"prepared",
"assistant-persisted",
"runtime-completed",
"goal-completed",
]
|| conversation_message_count != 1
|| exact_record_count("agent.runtime.completed") != 1
|| exact_record_count("agent.runtime.background_task.completed") != 1
{
return Err(format!(
"已完成 finalization 缺少完整唯一七槽审计:agent={} runId={} scanTruncated={scan_truncated}",
journal.agent_id, journal.run_id
));
}
let mut stream = read_game_creator_agent_runtime_response_stream_at(
root,
&journal.agent_id,
&journal.run_id,
)?
.ok_or_else(|| {
format!(
"已完成 finalization 的 committed 回复流缺失:agent={} runId={}",
journal.agent_id, journal.run_id
)
})?;
let stream_base_identity_conflicts = stream.agent_id != journal.agent_id
|| stream.task_id != journal.task_id
|| stream.session_id != journal.session_id
|| stream.run_id != journal.run_id
|| stream.request_kind != "final-reply";
if stream_base_identity_conflicts {
return Err(format!(
"已完成 finalization 与 committed 回复流身份冲突:agent={} runId={}",
journal.agent_id, journal.run_id
));
}
let stream_matches = |stream: &AgentRuntimeResponseStream| {
stream.status == AGENT_RUNTIME_RESPONSE_STREAM_STATUS_COMMITTED
&& stream.request_slot == journal.response_request_slot
&& stream.applied_steer_cursor == journal.response_steer_cursor
&& stream.response_revision == journal.response_revision
&& stream.accumulated_text == journal.response
};
if !stream_matches(&stream) {
if stream.status == AGENT_RUNTIME_RESPONSE_STREAM_STATUS_COMMITTED {
return Err(format!(
"已完成 finalization 与 committed 回复流身份冲突:agent={} runId={}",
journal.agent_id, journal.run_id
));
}
let _project_lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
root,
"runtime.finalization.completed-stream-repair",
)?;
remove_game_creator_agent_runtime_response_stream_at(
root,
&journal.agent_id,
&journal.run_id,
)?;
let mut state = agent_runtime_state_from_task_record(&task);
state.applied_steer_cursor = journal.response_steer_cursor;
mark_game_creator_agent_runtime_response_stream_committed_at(root, &state, &journal)?;
stream = read_game_creator_agent_runtime_response_stream_at(
root,
&journal.agent_id,
&journal.run_id,
)?
.ok_or_else(|| "已完成 finalization 的回复流修复后缺失".to_string())?;
if !stream_matches(&stream) {
return Err(format!(
"已完成 finalization 的回复流修复后仍冲突:agent={} runId={}",
journal.agent_id, journal.run_id
));
}
let _ = append_agent_db_record(
root,
serde_json::json!({
"recordType": "agent.runtime.background_task.finalization_completed_stream_repaired",
"agentId": journal.agent_id,
"taskId": journal.task_id,
"sessionId": journal.session_id,
"runId": journal.run_id,
"source": journal.source,
}),
);
}
let _project_lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
root,
"runtime.finalization.completed-cleanup",
)?;
remove_game_creator_agent_runtime_finalization_recovery_sidecars(
root,
&journal.agent_id,
&journal.run_id,
)?;
let _ = append_agent_db_record(
root,
serde_json::json!({
"recordType": "agent.runtime.background_task.finalization_completed_cleaned",
"agentId": journal.agent_id,
"taskId": journal.task_id,
"sessionId": journal.session_id,
"runId": journal.run_id,
"source": journal.source,
}),
);
removed = removed.saturating_add(1);
}
Ok(removed)
}
fn current_game_creator_agent_runtime_finalization_exists_at(
root: &Path,
agent_ids: &std::collections::BTreeSet<String>,
) -> Result<bool, String> {
for agent_id in agent_ids {
let (state, _) =
read_game_creator_agent_runtime_state_for_finalization_resume(root, agent_id)?;
if state.run_id.trim().is_empty() {
continue;
}
let path = game_creator_agent_runtime_finalization_path(root, agent_id, &state.run_id);
match fs::symlink_metadata(&path) {
Ok(_) => return Ok(true),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => {
return Err(format!(
"读取当前 Agent Runtime finalization 元数据失败:{}: {error}",
path.display()
));
}
}
let backup_path = agent_runtime_json_sidecar_backup_path(&path);
match fs::symlink_metadata(&backup_path) {
Ok(_) => return Ok(true),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => {
return Err(format!(
"读取当前 Agent Runtime finalization 恢复副本元数据失败:{}: {error}",
backup_path.display()
));
}
}
}
Ok(false)
}
pub(crate) fn resume_game_creator_agent_background_tasks_at(
root: &Path,
) -> Result<Vec<AgentRuntimeResult>, String> {
@@ -165,6 +431,9 @@ pub(in crate::agent) fn resume_game_creator_agent_background_tasks_unredacted_at
return read_game_creator_agent_runtimes_at(root);
}
let agent_ids = collect_game_creator_agent_runtime_agent_ids(root)?;
if !current_game_creator_agent_runtime_finalization_exists_at(root, &agent_ids)? {
cleanup_game_creator_agent_runtime_completed_finalizations_at(root)?;
}
for ledger in tool_plan_handoff::list_at(root)? {
let agent_id = ledger.agent_id();
let run_id = ledger.run_id();
@@ -559,6 +559,7 @@ fn validate_autonomous_game_build_ready_task_parent_at(
root: &Path,
parent_agent_id: &str,
parent_run_id: &str,
require_static_delegate_barrier: bool,
) -> Result<AgentRuntimeRunProfileBinding, String> {
let parent_agent_id = normalize_game_creator_runtime_agent_id(parent_agent_id)?;
if parent_agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
@@ -606,12 +607,14 @@ fn validate_autonomous_game_build_ready_task_parent_at(
&agent_runtime_state_from_task_record(&current_root),
)?
.ok_or_else(|| "autonomous ready-task scheduler 父 Run 缺少完成合同".to_string())?;
let barrier = static_delegate_completion_barrier_at(root, &parent_agent_id, parent_run_id)?;
if !barrier.is_clear() {
return Err(format!(
"autonomous ready-task scheduler 必须等待静态委派完成屏障收束:{}",
barrier.detail()
));
if require_static_delegate_barrier {
let barrier = static_delegate_completion_barrier_at(root, &parent_agent_id, parent_run_id)?;
if !barrier.is_clear() {
return Err(format!(
"autonomous ready-task scheduler 必须等待静态委派完成屏障收束:{}",
barrier.detail()
));
}
}
Ok(binding)
}
@@ -880,6 +883,7 @@ pub(crate) fn schedule_autonomous_game_build_ready_tasks_at(
root,
parent_agent_id,
parent_run_id,
true,
)?;
let manifest = read_manifest_for_project(root)?;
let active_count = manifest
@@ -923,10 +927,12 @@ pub(crate) fn schedule_autonomous_game_build_ready_tasks_at(
}
for (task, transition_to_running) in candidates {
let runtime_lock = try_acquire_game_creator_agent_runtime_task_lock(root, &task.id)?;
let parent_binding = match validate_autonomous_game_build_ready_task_parent_at(
root,
parent_agent_id,
parent_run_id,
true,
) {
Ok(binding) => binding,
Err(error) => {
@@ -940,6 +946,7 @@ pub(crate) fn schedule_autonomous_game_build_ready_tasks_at(
root,
parent_agent_id,
parent_run_id,
true,
) {
let cancelled = AgentRuntimeTaskRecord {
status: "cancelled".to_string(),
@@ -965,7 +972,7 @@ pub(crate) fn schedule_autonomous_game_build_ready_tasks_at(
if game_creator_agent_runtime_terminal_status(&record).is_some() {
terminal_records.push(record.clone());
}
scheduled.push((task, result, record, needs_notification));
scheduled.push((task, result, record, needs_notification, runtime_lock));
}
Err(error) => {
update_manifest_task_status_at(
@@ -995,7 +1002,7 @@ pub(crate) fn schedule_autonomous_game_build_ready_tasks_at(
}
let mut results = Vec::new();
for (task, result, record, needs_notification) in scheduled {
for (task, result, record, needs_notification, runtime_lock) in scheduled {
append_agent_db_record(
root,
serde_json::json!({
@@ -1013,6 +1020,15 @@ pub(crate) fn schedule_autonomous_game_build_ready_tasks_at(
"recovered": needs_notification,
}),
)?;
if game_creator_agent_runtime_terminal_status(&record).is_none() {
if let Some(runtime_lock) = runtime_lock {
spawn_next_game_creator_agent_background_task_drain_with_lock(
root,
&record.agent_id,
runtime_lock,
);
}
}
if needs_notification
&& game_creator_agent_runtime_terminal_status(&record).is_none()
&& notify_external_agent_runner_after_background_task_enqueue(
@@ -1101,6 +1117,7 @@ pub(in crate::agent) fn project_autonomous_manifest_ready_task_terminal_at_locke
root,
&parent_agent_id,
&parent_run_id,
false,
)?;
let manifest = read_manifest_for_project(root)?;
let Some(manifest_task) = manifest.tasks.iter().find(|task| task.id == state.agent_id) else {
@@ -1117,10 +1134,7 @@ pub(in crate::agent) fn project_autonomous_manifest_ready_task_terminal_at_locke
&task_text,
)?;
if status == GameCreationAppTaskStatus::Completed
&& matches!(
manifest_task.id.as_str(),
"design-foundation" | "art-asset-plan"
)
&& autonomous_manifest_ready_task_requires_visual_asset(&manifest_task.id)
&& !manifest_has_required_visual_asset(root, &manifest, &manifest_task.id)
{
status = GameCreationAppTaskStatus::Failed;
@@ -1199,6 +1213,14 @@ pub(in crate::agent) fn project_autonomous_manifest_ready_task_terminal_at_locke
Ok(true)
}
pub(super) fn autonomous_manifest_ready_task_requires_visual_asset(task_id: &str) -> bool {
editor_api_key_is_configured()
&& matches!(
task_id,
"art-director" | "design-foundation" | "art-asset-plan"
)
}
pub(in crate::agent) fn render_autonomous_manifest_ready_task_background_prompt(
task: &GameCreationAppTaskState,
) -> String {
@@ -82,6 +82,7 @@ pub(crate) use run_configuration::{
read_game_creator_agent_runtime_run_profile_binding,
};
pub(crate) use steering::{
acquire_game_creator_agent_runtime_steer_project_write_lock_with_wait,
consume_game_creator_agent_runtime_steers, game_creator_agent_runtime_accepts_steer,
game_creator_agent_runtime_steer_ledger_path,
interrupt_game_creator_agent_runtime_provider_request_at,

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