#![cfg_attr(all(not(dev), target_os = "windows"), windows_subsystem = "windows")] use std::collections::BTreeMap; use std::fs; use std::fs::{File, OpenOptions}; use std::io::{BufRead, BufReader, Read, Seek, SeekFrom, Write}; use std::net::{TcpListener, TcpStream}; use std::path::{Path, PathBuf}; use std::process::Command; use std::sync::atomic::AtomicBool; use std::sync::{mpsc, Arc, Mutex, OnceLock}; use std::thread; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use futures::StreamExt; use platform_agent::{ build_game_creation_seed_task_graph, plan_game_creation_agent_pass, route_game_creation_repair_issues, }; use platform_llm::{ LlmApiKind, LlmClient, LlmConfig, LlmMessage, LlmMessageContentPart, LlmProvider, LlmRunRequest, DEFAULT_RETRY_BACKOFF_MS, }; use reqwest::header; use serde::{Deserialize, Serialize}; use sha2::Digest; use shared_contracts::game_creation_app::{ new_game_creation_app_manifest, new_game_creation_app_seed_tasks, validate_game_iteration_versions, GameCreationAgentArtifactTrace, GameCreationAgentCapabilityDescriptor, GameCreationAgentPassPlanTrace, GameCreationAgentRepairRouteTrace, GameCreationAgentRunStep, GameCreationAgentRunTaskGraphTrace, GameCreationAgentRunTrace, GameCreationAgentToolCallTrace, GameCreationAppAgentGroup, GameCreationAppAssetManifestEntry, GameCreationAppAssetSource, GameCreationAppAssetSourceKind, GameCreationAppCommandRunState, GameCreationAppCommandRunStatus, GameCreationAppLimitedRunCommandDescriptor, GameCreationAppManifest, GameCreationAppPermission, GameCreationAppPreviewState, GameCreationAppPreviewStatus, GameCreationAppTaskState, GameCreationAppTaskStatus, GameIterationVersion, GameIterationVersionCreatedReason, GameIterationVersionResourceBinding, ProjectResourceCanvasLayout, ProjectResourceCanvasLayoutMode, ProjectResourceCanvasPosition, UpdateProjectResourceCanvasLayoutResult, UpdateProjectResourceCanvasLayoutStatus, GAME_CREATION_AGENT_CAPABILITIES, GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, GAME_CREATION_AGENT_TOOL_CALL_MAX, GAME_CREATION_APP_COMMANDS, GAME_CREATION_APP_LIMITED_RUN_COMMANDS, GAME_CREATION_RESOURCE_LAYOUT_SCHEMA_VERSION, }; use tauri::{Emitter, Manager}; use tauri_plugin_dialog::DialogExt; use tauri_plugin_opener::OpenerExt; const AGC_UPDATE_OSS_HOST: &str = "agc-dev.oss-rg-china-mainland.aliyuncs.com"; const AGC_UPDATE_MAX_DOWNLOAD_BYTES: u64 = 512 * 1024 * 1024; const AGC_UPDATE_DOWNLOAD_PROGRESS_EVENT: &str = "agc-update-download-progress"; fn build_agc_update_download_client() -> reqwest::Client { reqwest::Client::new() } #[derive(Clone, Debug, Serialize)] #[serde(rename_all = "camelCase")] struct AgcUpdateDownloadProgress { downloaded_bytes: u64, total_bytes: Option, } #[cfg(windows)] fn launch_agc_installer(path: &Path, relaunch_path: &Path) -> Result<(), String> { use std::os::windows::process::CommandExt; let executable = path.to_string_lossy().replace('\'', "''"); let relaunch_executable = relaunch_path.to_string_lossy().replace('\'', "''"); let script = format!( "$ErrorActionPreference = 'Stop'; $installer = Start-Process -Verb RunAs -Wait -PassThru -FilePath '{executable}' -ArgumentList @('/S'); if ($installer.ExitCode -eq 0 -and (Test-Path -LiteralPath '{relaunch_executable}')) {{ Start-Process -FilePath '{relaunch_executable}' }}; exit $installer.ExitCode" ); Command::new("powershell.exe") .args([ "-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command", script.as_str(), ]) .creation_flags(0x0800_0000) .spawn() .map(|_| ()) .map_err(|error| format!("无法启动更新安装程序:{error}")) } #[cfg(not(windows))] fn launch_agc_installer(path: &Path, _relaunch_path: &Path) -> Result<(), String> { Command::new(path) .arg("/S") .spawn() .map(|_| ()) .map_err(|error| format!("无法启动更新安装程序:{error}")) } #[tauri::command] async fn download_agc_update( app: tauri::AppHandle, download_url: String, expected_sha256: Option, expected_size: Option, ) -> Result { let parsed = url::Url::parse(download_url.trim()).map_err(|_| "更新下载地址无效".to_string())?; if parsed.scheme() != "https" || parsed.host_str() != Some(AGC_UPDATE_OSS_HOST) { return Err("更新下载地址必须来自受信任的 OSS".to_string()); } let encoded_filename = parsed .path_segments() .and_then(|segments| segments.last()) .filter(|value| !value.is_empty()) .ok_or_else(|| "更新下载地址缺少文件名".to_string())? .to_string(); let filename = percent_encoding::percent_decode_str(&encoded_filename) .decode_utf8() .map_err(|_| "更新文件名无效".to_string())? .into_owned(); if filename.contains('/') || filename.contains('\\') || filename.contains("..") { return Err("更新文件名无效".to_string()); } if filename.is_empty() || filename.len() > 128 { return Err("更新文件名无效".to_string()); } let response = build_agc_update_download_client() .get(parsed) .send() .await .map_err(|_| "下载更新失败".to_string())?; if !response.status().is_success() { return Err("下载更新失败".to_string()); } if response .content_length() .is_some_and(|length| length > AGC_UPDATE_MAX_DOWNLOAD_BYTES) { return Err("更新文件超过大小限制".to_string()); } let download_dir = app .path() .temp_dir() .map_err(|_| "无法定位临时目录".to_string())? .join("genarrative-agc-update"); fs::create_dir_all(&download_dir).map_err(|_| "无法创建临时目录".to_string())?; let target = download_dir.join(&filename); let temporary = download_dir.join(format!( "{}.{}.download", filename, uuid::Uuid::new_v4().simple() )); let mut file = File::create(&temporary).map_err(|_| "保存更新文件失败".to_string())?; let mut hasher = sha2::Sha256::new(); let total_bytes = response.content_length(); let mut downloaded_bytes = 0_u64; let _ = app.emit( AGC_UPDATE_DOWNLOAD_PROGRESS_EVENT, AgcUpdateDownloadProgress { downloaded_bytes, total_bytes, }, ); let mut stream = response.bytes_stream(); while let Some(chunk_result) = stream.next().await { let chunk = match chunk_result { Ok(chunk) => chunk, Err(_) => { let _ = fs::remove_file(&temporary); return Err("读取更新文件失败".to_string()); } }; downloaded_bytes = match downloaded_bytes.checked_add(chunk.len() as u64) { Some(value) if value <= AGC_UPDATE_MAX_DOWNLOAD_BYTES => value, _ => { let _ = fs::remove_file(&temporary); return Err("更新文件超过大小限制".to_string()); } }; hasher.update(&chunk); if file.write_all(&chunk).is_err() { let _ = fs::remove_file(&temporary); return Err("保存更新文件失败".to_string()); } let _ = app.emit( AGC_UPDATE_DOWNLOAD_PROGRESS_EVENT, AgcUpdateDownloadProgress { downloaded_bytes, total_bytes, }, ); } if file.flush().is_err() { let _ = fs::remove_file(&temporary); return Err("保存更新文件失败".to_string()); } drop(file); if let Some(expected_size) = expected_size { if downloaded_bytes != expected_size { let _ = fs::remove_file(&temporary); return Err("更新文件大小校验失败".to_string()); } } if let Some(expected_sha256) = expected_sha256 { let expected_sha256 = expected_sha256.trim().to_ascii_lowercase(); if !expected_sha256.bytes().all(|byte| byte.is_ascii_hexdigit()) || expected_sha256.len() != 64 { let _ = fs::remove_file(&temporary); return Err("更新文件摘要无效".to_string()); } let actual = format!("{:x}", hasher.finalize()); if actual != expected_sha256 { let _ = fs::remove_file(&temporary); return Err("更新文件完整性校验失败".to_string()); } } if target.exists() { let _ = fs::remove_file(&target); } if let Err(error) = fs::rename(&temporary, &target) { let _ = fs::remove_file(&temporary); return Err(format!("提交更新文件失败:{error}")); } let relaunch_path = std::env::current_exe().map_err(|error| format!("无法定位客户端程序:{error}"))?; launch_agc_installer(&target, &relaunch_path)?; app.exit(0); Ok(target.to_string_lossy().into_owned()) } // 调试落盘模块(保存 LLM 原始输出 / 失败输入,排查截断、空返回等)放在 debug_drafts.rs。 // 用 #[cfg] 编译期门控:仅开发(debug)且非测试构建编入;生产 release 与 cargo test 下整体剔除。 mod agent; mod agent_native_tools; mod assets; mod browser; mod cli; mod client_extensions; mod collaboration; mod command_exec; mod command_output; mod command_sandbox; mod command_sandbox_trampoline; mod commands; mod config; mod context_compaction; mod context_menu; #[cfg(all(debug_assertions, not(test)))] mod debug; mod delegation; mod git_inspect; mod goal; mod http_client; mod image_inspect; mod isolated_agent; mod patchset; mod platform_session; mod preview; mod process_session; mod process_session_bridge; mod project; mod provider_handoff; mod provider_retry; mod repository_context; mod resource_inspect; mod resource_preview_scheduler; mod runner; mod swarm_cli; mod tool_plan_handoff; mod user_input; mod windows; use agent::*; use agent_native_tools::*; use assets::*; use browser::*; use cli::*; use client_extensions::*; use collaboration::*; use command_exec::*; use command_output::*; use command_sandbox::*; use commands::*; use config::*; use context_compaction::*; use delegation::*; use git_inspect::*; use goal::*; use image_inspect::*; use isolated_agent::*; use patchset::*; use platform_session::*; use preview::*; use process_session::*; use project::*; use repository_context::*; use resource_inspect::*; use resource_preview_scheduler::*; use runner::*; use swarm_cli::*; use user_input::*; use windows::*; #[tauri::command] async fn suggest_ui_design_semantic( project_path: String, state: ui_editor::state::State, ) -> Result, String> { ui_editor::commands::suggest_ui_design_semantic_impl(project_path, state).await } #[tauri::command] async fn recognize_ui( project_path: String, state: ui_editor::state::State, ) -> Result { ui_editor::commands::recognize_ui_impl(project_path, state).await } #[tauri::command] async fn merge_ui(state: ui_editor::state::State) -> Result { ui_editor::commands::merge_ui_impl(state).await } #[tauri::command] async fn bind_components( project_path: String, state: ui_editor::state::State, sprite_ids: Vec, ) -> Result { ui_editor::commands::bind_components_impl(project_path, state, sprite_ids).await } #[tauri::command] fn load_ui_design_state( input: ui_editor::persistence::LoadUiDesignStateInput, ) -> Result { let root = Path::new(input.project_path.trim()); enforce_project_permission_policy(root, "asset.list")?; ui_editor::persistence::load_ui_design_state_at(input) } #[tauri::command] fn save_ui_design_state( input: ui_editor::persistence::SaveUiDesignStateInput, ) -> Result { let root = Path::new(input.project_path.trim()); enforce_project_permission_policy(root, "asset.register")?; ui_editor::persistence::save_ui_design_state_at(input) } #[tauri::command] fn generate_ui_design_code( input: ui_editor::persistence::GenerateUiDesignCodeInput, ) -> Result { let root = Path::new(input.project_path.trim()); enforce_project_permission_policy(root, "file.write")?; ui_editor::persistence::generate_ui_design_code_at(input) } #[tauri::command] fn ensure_ui_design_resource_for_prototype( input: ui_editor::resource_bridge::EnsureUiDesignResourceForPrototypeInput, ) -> Result { ui_editor::resource_bridge::ensure_ui_design_resource_for_prototype(input) } #[derive(Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct InitLocalProjectResult { project_path: String, manifest_path: String, manifest: GameCreationAppManifest, } #[derive(Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct LocalProjectDirectoryStatus { project_path: String, exists: bool, is_directory: bool, is_game_creator_project: bool, is_godot_project: bool, godot_project_root: Option, project_name: Option, modified_at: Option, manifest_error: Option, recent_run_status: Option, recent_run_stop_reason: Option, } #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct LocalPreviewResult { url: String, port: u16, root: String, } #[derive(Clone, Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct LocalPreviewStatus { status: String, url: Option, port: Option, root: Option, } #[derive(Clone, Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct LocalGameProjectRevisionStatus { revision: u64, } #[derive(Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct GenerateLocalGameDraftResult { project_path: String, game_index_path: String, design_path: String, short_memory_path: String, long_memory_path: String, manifest: GameCreationAppManifest, } #[derive(Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct GameCreatorChatAgentReply { reply_text: String, } #[derive(Clone, Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct GameCreatorRoleAgentChatStreamEvent { project_path: String, agent_id: String, run_id: String, status: String, delta_text: String, accumulated_text: String, finish_reason: Option, session_id: Option, runtime_status: Option, runtime_phase: Option, runtime_summary: Option, runtime_state: Option, } #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct AgentRuntimeState { #[serde(default)] schema_version: String, #[serde(default)] agent_id: String, #[serde(default)] task_id: String, #[serde(default)] session_id: String, #[serde(default)] run_id: String, #[serde(default)] source: String, #[serde(default = "default_agent_runtime_run_profile")] run_profile: String, #[serde(default)] run_profile_binding_fingerprint: String, #[serde(default)] parent_agent_id: Option, #[serde(default)] parent_run_id: Option, #[serde(default)] delegation_id: Option, #[serde(default)] status: String, #[serde(default)] phase: String, #[serde(default)] current_task: String, #[serde(default)] current_goal: String, #[serde(default)] goal_id: Option, #[serde(default)] goal_revision: u64, #[serde(default)] goal_status: Option, #[serde(default)] goal_outcome: Option, #[serde(default)] goal_constraints: Vec, #[serde(default)] goal_verification: Vec, #[serde(default)] current_action: String, #[serde(default)] waiting_on: String, #[serde(default)] next_step: String, #[serde(default)] loop_iteration: u32, /// Consecutive strict Fast-GDD submit rejections for this exact planning /// child run. It is Runtime-owned durable state so a restart cannot turn /// an invalid-provider-output loop back into an unbounded retry. #[serde(default)] plan_submit_gdd_rejection_count: u32, /// Consecutive rounds where this run produced no action and no structured /// plan step advance. Runtime-owned durable state so a runner restart /// cannot launder an explanation-only planning loop back into an unbounded /// Provider spend. #[serde(default)] plan_update_idle_rounds: u32, /// Consecutive final replies this run had refused by a completion blocker. /// Runtime-owned durable state for the same reason as the two counters /// above: a blocker the model cannot satisfy is a livelock, and a runner /// restart must not launder it back into unbounded Provider spend. #[serde(default)] stale_finalization_rounds: u32, #[serde(default)] max_loop_iterations: u32, #[serde(default)] tool_action_budget: u32, #[serde(default)] plan_revision: u64, #[serde(default)] plan_explanation: String, #[serde(default)] plan: Vec, #[serde(default)] plan_steps: Vec, #[serde(default)] active_plan_step_index: Option, #[serde(default)] observations: Vec, #[serde(default)] recent_tool_calls: Vec, #[serde(default)] pending_tool_action: Option, #[serde(default)] task_queue: AgentRuntimeTaskQueueSummary, #[serde(default)] allowed_tools: Vec, #[serde(default)] tool_policy: AgentRuntimeToolPolicySnapshot, #[serde(default)] applied_steer_cursor: u64, #[serde(default)] applied_steer_refs: Vec, #[serde(default)] queued_steer_count: u32, #[serde(default)] context_usage: AgentRuntimeContextUsage, #[serde(default)] last_response: Option, #[serde(default)] error: Option, #[serde(default)] started_at: u64, #[serde(default)] updated_at: u64, } #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct AgentRuntimeContextUsage { #[serde(default)] estimated_input_tokens: u64, #[serde(default)] auto_compact_token_limit: u64, #[serde(default)] last_prompt_tokens: Option, #[serde(default)] last_completion_tokens: Option, #[serde(default)] last_total_tokens: Option, #[serde(default)] compaction_revision: u64, #[serde(default)] compaction_count: u64, #[serde(default)] last_compaction_trigger: Option, #[serde(default)] last_compacted_at: Option, } #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct AgentRuntimeSteerRef { #[serde(default)] steer_id: String, #[serde(default)] sequence: u64, #[serde(default)] message_id: String, #[serde(default)] instruction_sha256: String, #[serde(default)] content_chars: u32, } #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct AgentRuntimeToolPolicySnapshot { #[serde(default = "default_agent_runtime_run_profile")] run_profile: String, #[serde(default)] run_profile_binding_fingerprint: String, #[serde(default)] allowed_tools: Vec, #[serde(default)] auto_tools: Vec, #[serde(default)] confirm_tools: Vec, #[serde(default)] denied_tools: Vec, #[serde(default)] updated_at: u64, } impl Default for AgentRuntimeToolPolicySnapshot { fn default() -> Self { Self { run_profile: default_agent_runtime_run_profile(), run_profile_binding_fingerprint: String::new(), allowed_tools: Vec::new(), auto_tools: Vec::new(), confirm_tools: Vec::new(), denied_tools: Vec::new(), updated_at: 0, } } } #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct AgentRuntimeToolCallRecord { #[serde(default)] action_id: Option, #[serde(default)] tool: String, #[serde(default)] status: String, #[serde(default)] action_fingerprint: Option, #[serde(default)] input_summary: Option, #[serde(default)] reason: Option, #[serde(default)] summary: String, #[serde(default)] detail: Option, #[serde(default)] updated_at: u64, } #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct AgentRuntimePendingToolActionSummary { #[serde(default)] action_id: String, #[serde(default)] action_fingerprint: String, #[serde(default)] tool: String, #[serde(default)] input_summary: Option, #[serde(default)] reason: Option, #[serde(default)] requested_at: u64, } #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct AgentRuntimePlanStep { #[serde(default)] index: u32, #[serde(default)] title: String, #[serde(default)] status: String, #[serde(default)] detail: Option, #[serde(default)] updated_at: u64, } #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct AgentRuntimeTaskQueueSummary { #[serde(default)] total: u32, #[serde(default)] pending: u32, #[serde(default)] running: u32, #[serde(default)] waiting_for_confirmation: u32, #[serde(default)] waiting_for_user_input: u32, #[serde(default)] paused: u32, #[serde(default)] cancelled: u32, #[serde(default)] completed: u32, #[serde(default)] failed: u32, #[serde(default)] latest_run_id: Option, #[serde(default)] updated_at: u64, } impl Default for AgentRuntimeTaskQueueSummary { fn default() -> Self { Self { total: 0, pending: 0, running: 0, waiting_for_confirmation: 0, waiting_for_user_input: 0, paused: 0, cancelled: 0, completed: 0, failed: 0, latest_run_id: None, updated_at: 0, } } } #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct AgentRuntimeEvent { #[serde(default)] schema_version: String, #[serde(default)] agent_id: String, #[serde(default)] task_id: String, #[serde(default)] session_id: String, #[serde(default)] run_id: String, #[serde(default)] source: String, #[serde(default)] event_type: String, #[serde(default)] event_id: String, #[serde(default)] action_id: Option, #[serde(default)] status: String, #[serde(default)] phase: String, #[serde(default)] summary: String, #[serde(default)] public_text: Option, #[serde(default)] detail: Option, #[serde(default)] updated_at: u64, } #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(deny_unknown_fields, rename_all = "camelCase")] struct AgentRuntimeResponseStream { schema_version: String, agent_id: String, task_id: String, session_id: String, run_id: String, request_kind: String, request_slot: String, applied_steer_cursor: u64, response_revision: u64, sequence: u64, status: String, accumulated_text: String, finish_reason: Option, started_at: u64, updated_at: u64, } #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct AgentRuntimeTaskRecord { #[serde(default)] schema_version: String, #[serde(default)] agent_id: String, #[serde(default)] task_id: String, #[serde(default)] session_id: String, #[serde(default)] run_id: String, #[serde(default)] source: String, #[serde(default = "default_agent_runtime_run_profile")] run_profile: String, #[serde(default)] run_profile_binding_fingerprint: String, #[serde(default)] parent_agent_id: Option, #[serde(default)] parent_run_id: Option, #[serde(default)] delegation_id: Option, #[serde(default)] goal_id: Option, #[serde(default)] goal_revision: u64, #[serde(default)] goal_status: Option, #[serde(default)] task: String, #[serde(default)] status: String, #[serde(default)] phase: String, #[serde(default)] current_action: String, #[serde(default)] terminal_detail: Option, #[serde(default)] error: Option, #[serde(default)] updated_at: u64, } #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(deny_unknown_fields, rename_all = "camelCase")] struct AgentGoalRecord { schema_version: String, project_id: String, goal_id: String, agent_id: String, session_id: String, run_id: String, revision: u64, status: String, outcome: String, constraints: Vec, verification: Vec, #[serde(default)] completion_evidence: Vec, #[serde(default)] response_fingerprint: Option, created_at: u64, #[serde(default)] pause_requested_at: Option, #[serde(default)] paused_at: Option, #[serde(default)] completed_at: Option, #[serde(default)] cleared_at: Option, #[serde(default)] error: Option, updated_at: u64, } #[derive(Clone, Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct AgentGoalMutationResult { goal: AgentGoalRecord, runtime: AgentRuntimeResult, provider_interrupted: bool, } #[derive(Clone, Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct AgentRuntimeResult { state: AgentRuntimeState, #[serde(skip_serializing_if = "Option::is_none")] accepted_run_id: Option, session_path: String, event_path: String, task_path: String, task_queue: AgentRuntimeTaskQueueSummary, recent_events: Vec, recent_tasks: Vec, response_stream: Option, user_input_request: Option, } #[derive(Clone, Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct AgentRuntimeSteerResult { runtime: AgentRuntimeResult, steer_id: String, sequence: u64, status: String, provider_interrupted: bool, #[serde(skip_serializing_if = "Option::is_none")] assistant_reply: Option, #[serde(skip_serializing_if = "Option::is_none")] interrupt_decision: Option, #[serde(skip_serializing_if = "Option::is_none")] decision_reason: Option, } #[derive(Clone, Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct GameCreatorAgentRuntimeUpdateEvent { project_path: String, agent_id: String, run_id: String, status: String, phase: String, manifest_invalidated: bool, runtime: AgentRuntimeResult, } #[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] struct GameCreatorManifestInvalidatedEvent { project_path: String, agent_id: String, } #[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] struct GameCreatorManifestInvalidationRelayEnvelope { token: String, event: GameCreatorManifestInvalidatedEvent, } #[derive(Clone, Debug, Eq, PartialEq)] struct GameCreatorManifestInvalidationEventSink { port: u16, token: String, } #[derive(Clone, Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct GameCreatorAgentProgressEvent { project_path: String, stage: String, message: String, } #[derive(Clone, Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct GameCreatorDirectTurnUpdateEvent { project_path: String, turn_id: String, sequence: u64, status: String, activity: Option, accumulated_text: Option, updated_at: u64, } #[derive(Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct GameCreatorLlmConfigStatus { agent_mode: String, configured: bool, account_credential_state: String, official_route_locked: bool, reasoning_effort: String, stream: bool, web_search_enabled: bool, context_window_tokens: u64, auto_compact_token_limit: u64, tool_output_token_limit: u64, request_timeout_ms: u64, max_retries: u32, retry_backoff_ms: u64, error: Option, agents: Vec, } #[derive(Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct GameCreatorAgentLlmConfigStatus { agent_mode: String, agent_id: String, label: String, configured: bool, account_credential_state: String, official_route_locked: bool, reasoning_effort: String, stream: bool, web_search_enabled: bool, context_window_tokens: u64, auto_compact_token_limit: u64, tool_output_token_limit: u64, request_timeout_ms: u64, max_retries: u32, retry_backoff_ms: u64, error: Option, } #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] struct GameCreatorAppConfigFile { #[serde(skip_serializing_if = "Option::is_none")] schema_version: Option, #[serde(skip_serializing_if = "Option::is_none")] agent_mode: Option, llm: Option, agent_llm: Option>, editor_api: Option, planning: Option, } #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] struct GameCreatorPlanningConfigFile { #[serde(skip_serializing_if = "Option::is_none")] capability_enabled: Option, } #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] struct GameCreatorLlmConfigFile { #[serde(skip_serializing_if = "Option::is_none")] api_key: Option, #[serde(skip_serializing_if = "Option::is_none")] base_url: Option, #[serde(skip_serializing_if = "Option::is_none")] model: Option, #[serde(skip_serializing_if = "Option::is_none")] api_kind: Option, #[serde(skip_serializing_if = "Option::is_none")] reasoning_effort: Option, #[serde(skip_serializing_if = "Option::is_none")] stream: Option, #[serde(skip_serializing_if = "Option::is_none")] web_search_enabled: Option, #[serde(skip_serializing_if = "Option::is_none")] context_window_tokens: Option, #[serde(skip_serializing_if = "Option::is_none")] auto_compact_token_limit: Option, #[serde(skip_serializing_if = "Option::is_none")] tool_output_token_limit: Option, #[serde(skip_serializing_if = "Option::is_none")] request_timeout_ms: Option, #[serde(skip_serializing_if = "Option::is_none")] max_retries: Option, #[serde(skip_serializing_if = "Option::is_none")] retry_backoff_ms: Option, } #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] struct GameCreatorEditorApiConfigFile { base_url: Option, api_key: Option, } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] struct GameCreatorAppConfig { #[serde(default = "default_game_creator_app_config_schema_version")] schema_version: String, #[serde(default = "default_game_creator_agent_mode")] agent_mode: String, llm: GameCreatorLlmConfig, #[serde(default)] agent_llm: BTreeMap, editor_api: GameCreatorEditorApiConfig, #[serde(default)] planning: GameCreatorPlanningConfig, } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] struct GameCreatorPlanningConfig { #[serde(default = "default_game_creator_planning_capability_enabled")] capability_enabled: bool, } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] struct GameCreatorLlmConfig { api_key: String, base_url: String, model: String, api_kind: String, reasoning_effort: String, stream: bool, web_search_enabled: bool, #[serde(default = "default_game_creator_llm_context_window_tokens")] context_window_tokens: u64, #[serde(default = "default_game_creator_llm_auto_compact_token_limit")] auto_compact_token_limit: u64, #[serde(default = "default_game_creator_llm_tool_output_token_limit")] tool_output_token_limit: u64, request_timeout_ms: u64, max_retries: u32, retry_backoff_ms: u64, } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] struct GameCreatorEditorApiConfig { base_url: String, api_key: String, } #[derive(Clone, Debug, Serialize)] #[serde(rename_all = "camelCase")] struct GameCreatorAppConfigView { path: String, config: GameCreatorAppConfig, } #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct AgentRuntimeContextCompactionResult { agent_id: String, session_id: String, run_id: Option, trigger: String, revision: u64, estimated_tokens_before: u64, estimated_tokens_after: u64, prompt_tokens: Option, completion_tokens: Option, total_tokens: Option, covered_agent_messages: u64, covered_project_messages: u64, covered_observations: u64, reused: bool, compacted_at: u64, } #[derive(Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct AgentRunControlResult { run_id: String, status: String, lifecycle_status: String, next_step: String, message: String, activity_path: String, output_path: String, context_bundle_path: String, } #[derive(Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct UploadLocalAssetResult { id: String, local_path: String, absolute_path: String, manifest_path: String, } #[derive(Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct CreateUiDesignResourceResult { asset: UploadLocalAssetResult, manifest: GameCreationAppManifest, committed_project_revision: u64, } #[derive(Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct ImportCanvasExportResult { import_root: String, metadata_path: String, imported_count: usize, assets: Vec, } #[derive(Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct SyncCanvasProjectAssetsResult { canvas_project_id: String, import_root: String, imported_count: usize, assets: Vec, } #[derive(Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct LocalImportResult { assets: Vec, } type RemoteImportResult = LocalImportResult; #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct ImportedAsset { id: String, local_path: String, asset_kind: Option, } #[derive(Debug, Eq, PartialEq)] struct GeneratedPlatformArtAssetSlice { name: String, width: u32, height: u32, local_path: String, resource_id: Option, asset_object_id: Option, content_sha256: String, pixel_sha256: String, } #[derive(Debug, Eq, PartialEq)] struct GeneratedPlatformArtAsset { asset: UploadLocalAssetResult, slices: Vec, resource_id: Option, asset_object_id: Option, task_id: Option, model: Option, warning: Option, slice_warning: Option, } #[derive(Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct OpenCanvasProjectResult { url: String, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct CanvasExportMetadata { project_title: String, exported_at: String, layers: Vec, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct CanvasExportLayerMetadata { title: String, file: Option, visible: CanvasExportVisibleMetadata, export_error: Option, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct CanvasExportVisibleMetadata { #[serde(rename = "type")] layer_type: String, model: String, task: String, object: String, } #[derive(Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct LocalGameMemoryResult { scope: String, path: String, content: String, exists: bool, } #[derive(Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct LocalAgentMemoryResult { task_id: String, path: String, content: String, exists: bool, } #[derive(Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct LimitedLocalCommandResult { command_id: String, status: String, output: String, log_path: String, updated_at: u64, } #[derive(Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct LocalProjectFileEntry { path: String, kind: String, size: u64, modified_at: u64, } #[derive(Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct ListLocalProjectFilesResult { project_path: String, files: Vec, } #[derive(Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct LocalProjectFileResult { path: String, absolute_path: String, content: String, } #[derive(Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct LocalProjectFileMutationResult { path: String, absolute_path: String, deleted: bool, } #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct LocalConversationMessage { role: String, content: String, agent_id: Option, } #[derive(Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct LocalConversationMessageRecord { schema_version: String, role: String, content: String, agent_id: Option, message_id: Option, updated_at: u64, } #[derive(Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct LocalConversationResult { path: String, agent_id: Option, session_id: Option, messages: Vec, } #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct AgentConversationSessionRecord { session_id: String, title: String, created_at: u64, updated_at: u64, archived_at: Option, message_count: u64, legacy: bool, #[serde(default, skip_serializing_if = "Option::is_none")] forked_from_session_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] forked_message_count: Option, } #[derive(Clone, Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct AgentConversationSessionListResult { path: String, agent_id: String, active_session_id: String, sessions: Vec, } #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct ProjectPermissionPolicy { denied_commands: Vec, confirm_commands: Vec, #[serde(default)] agent_policies: BTreeMap, } #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct ProjectAgentPermissionPolicy { #[serde(default)] denied_commands: Vec, #[serde(default)] confirm_commands: Vec, } #[derive(Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct ProjectPermissionPolicyView { path: String, policy: ProjectPermissionPolicy, } #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct LocalProjectIndexedFile { path: String, size: u64, checksum: String, } #[derive(Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct LocalProjectIndexResult { project_path: String, index_path: String, file_count: usize, total_bytes: u64, files: Vec, } #[derive(Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct LocalProjectCheckpointResult { checkpoint_id: String, checkpoint_path: String, file_count: usize, total_bytes: u64, } #[derive(Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct LocalProjectExportPackageResult { package_path: String, package_relative_path: String, file_count: usize, total_bytes: u64, } #[derive(Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct LocalProjectExportPackageSummary { package_path: String, package_relative_path: String, total_bytes: u64, modified_at: u64, } #[derive(Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct LocalProjectExportPackagesResult { project_path: String, packages: Vec, } #[derive(Clone, Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct LocalProjectDiffEntry { path: String, status: String, } #[derive(Clone, Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct LocalProjectDiffResult { checkpoint_id: String, added: Vec, changed: Vec, deleted: Vec, } #[derive(Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct LocalProjectRestoreResult { checkpoint_id: String, restored_count: usize, deleted_count: usize, } const DEFAULT_GAME_INDEX_HTML: &str = r#" Genarrative Game Draft
还没有生成游戏。回到聊天输入创意并确认生成后,这里会写入可试玩原型。
"#; const DEFAULT_EDITOR_BASE_URL: &str = "http://127.0.0.1:3000"; const GAME_CREATOR_CONFIG_FILE_NAME: &str = "game-creator.config.json"; const GAME_CREATOR_LOCAL_CONFIG_FILE_NAME: &str = "game-creator.config.local.json"; const GAME_CREATOR_AGENT_MODE_CODEX_APP_SERVER: &str = "codex_app_server"; const GAME_CREATOR_AGENT_MODE_CODEX_CLI: &str = "codex_cli"; const GAME_CREATOR_AGENT_MODE_PROVIDER: &str = "provider"; const GAME_CREATOR_APP_CONFIG_SCHEMA_VERSION: &str = "game-creator-config.v2"; const DEFAULT_GAME_CREATOR_LLM_BASE_URL: &str = "https://dev.genarrative.world/gpt/v1"; const DEFAULT_GAME_CREATOR_LLM_MODEL: &str = "gpt-5.6-sol"; const DEFAULT_GAME_CREATOR_LLM_API_KIND: &str = "openai_responses"; const DEFAULT_GAME_CREATOR_LLM_REASONING_EFFORT: &str = "max"; const DEFAULT_GAME_CREATOR_LLM_CONTEXT_WINDOW_TOKENS: u64 = 128_000; const DEFAULT_GAME_CREATOR_LLM_AUTO_COMPACT_TOKEN_LIMIT: u64 = 64_000; const DEFAULT_GAME_CREATOR_LLM_TOOL_OUTPUT_TOKEN_LIMIT: u64 = 12_000; const DEFAULT_GAME_CREATOR_LLM_MAX_RETRIES: u32 = 2; fn default_game_creator_agent_mode() -> String { GAME_CREATOR_AGENT_MODE_CODEX_APP_SERVER.to_string() } fn default_game_creator_app_config_schema_version() -> String { GAME_CREATOR_APP_CONFIG_SCHEMA_VERSION.to_string() } fn default_game_creator_llm_context_window_tokens() -> u64 { DEFAULT_GAME_CREATOR_LLM_CONTEXT_WINDOW_TOKENS } fn default_game_creator_llm_auto_compact_token_limit() -> u64 { DEFAULT_GAME_CREATOR_LLM_AUTO_COMPACT_TOKEN_LIMIT } fn default_game_creator_llm_tool_output_token_limit() -> u64 { DEFAULT_GAME_CREATOR_LLM_TOOL_OUTPUT_TOKEN_LIMIT } fn default_game_creator_planning_capability_enabled() -> bool { true } const DEFAULT_CANVAS_SYNC_API_BASE_URL: &str = "https://dev.genarrative.world"; const DEFAULT_GAME_CREATOR_APP_CONFIG_JSON: &str = include_str!("../../game-creator.config.json"); const GAME_CREATOR_LLM_MAX_OUTPUT_TOKENS: u32 = 320000; const GAME_CREATOR_CHAT_AGENT_MAX_OUTPUT_TOKENS: u32 = 1800; const GAME_CREATOR_PLANNER_MAX_OUTPUT_TOKENS: u32 = 900; const GAME_CREATOR_ROLE_AGENT_MAX_OUTPUT_TOKENS: u32 = 1200; const GAME_CREATOR_REQUIRED_LLM_AGENT_IDS: [&str; 3] = [ GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, "planner", "generator", ]; const MIN_GAME_CREATOR_LLM_REQUEST_TIMEOUT_MS: u64 = 1_000; const GAME_CREATOR_LLM_REQUEST_TIMEOUT_MS: u64 = 180_000; const GAME_CREATOR_AGENT_LOOP_MAX_PASSES: u8 = 3; const GAME_CREATOR_AGENT_TOOL_CALL_MAX: u16 = GAME_CREATION_AGENT_TOOL_CALL_MAX; const GAME_CREATOR_AGENT_RUN_HISTORY_MAX_COUNT: usize = 100; const GAME_CREATOR_AGENT_DB_SCHEMA_VERSION: &str = "game-creator-agent-db.v1"; const PROJECT_BLACKBOARD_MEMORY_PATH: &str = "memory/blackboard.md"; const PROJECT_PERMISSION_POLICY_PATH: &str = ".agent/policy.json"; const PROJECT_INDEX_PATH: &str = ".agent/project.index.json"; const PROJECT_WRITE_LOCK_PATH: &str = ".agent/project.lock"; const LOCAL_CONVERSATION_SCHEMA_VERSION: &str = "game-creator-conversation.v1"; const AGENT_CONVERSATION_SESSION_SCHEMA_VERSION: &str = "game-creator-agent-sessions.v1"; const AGENT_RUNTIME_SCHEMA_VERSION: &str = "game-creator-agent-runtime.v1"; const AGENT_RUNTIME_RUN_PROFILE_STANDARD: &str = "standard"; const AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD: &str = "autonomous-game-build"; const AGENT_RUNTIME_AUTONOMOUS_SOURCE_FIELD_MAX_CHARS: usize = 8_000; const AGENT_RUNTIME_AUTONOMOUS_SOURCE_TOTAL_MAX_CHARS: usize = 10_000; const AGENT_RUNTIME_RECENT_EVENT_LIMIT: usize = 20; const AGENT_RUNTIME_RECENT_TASK_LIMIT: usize = 12; const GAME_CREATOR_CONVERSATION_CONTEXT_MAX_MESSAGES: usize = 12; fn default_agent_runtime_run_profile() -> String { AGENT_RUNTIME_RUN_PROFILE_STANDARD.to_string() } const MAX_CANVAS_EXPORT_FILES: usize = 500; const MAX_CANVAS_EXPORT_BYTES: u64 = 512 * 1024 * 1024; const MAX_PROJECT_EXPORT_PACKAGE_FILES: usize = 1200; const MAX_PROJECT_EXPORT_PACKAGE_BYTES: u64 = 512 * 1024 * 1024; const GAME_CREATOR_AGENT_ARTIFACT_PATHS: [&str; 15] = [ ".agent/agent.db", ".agent/spec.md", ".agent/findings.md", ".agent/logs/agent.log", ".agent/logs/command.log", ".agent/logs/preview.log", "memory/session.md", "memory/project.md", PROJECT_BLACKBOARD_MEMORY_PATH, "game/game_design.md", "game/balance.json", "assets/manifest.art.json", "assets/manifest.audio.json", "exports/README.md", "game/index.html", ]; static GAME_CREATOR_RUNTIME_CONFIG_DIR: OnceLock>> = OnceLock::new(); impl Default for GameCreatorAppConfig { fn default() -> Self { let mut llm = GameCreatorLlmConfig::default(); // DirectProject is the shipped product route, so the application-level // default enables the controlled AGC search tool. The bare // GameCreatorLlmConfig default remains conservative for legacy callers. llm.web_search_enabled = true; Self { schema_version: default_game_creator_app_config_schema_version(), agent_mode: default_game_creator_agent_mode(), llm, agent_llm: BTreeMap::new(), editor_api: GameCreatorEditorApiConfig::default(), planning: GameCreatorPlanningConfig::default(), } } } impl Default for GameCreatorPlanningConfig { fn default() -> Self { Self { capability_enabled: default_game_creator_planning_capability_enabled(), } } } impl Default for GameCreatorLlmConfig { fn default() -> Self { Self { api_key: String::new(), base_url: DEFAULT_GAME_CREATOR_LLM_BASE_URL.to_string(), model: DEFAULT_GAME_CREATOR_LLM_MODEL.to_string(), api_kind: DEFAULT_GAME_CREATOR_LLM_API_KIND.to_string(), reasoning_effort: DEFAULT_GAME_CREATOR_LLM_REASONING_EFFORT.to_string(), stream: true, web_search_enabled: false, context_window_tokens: DEFAULT_GAME_CREATOR_LLM_CONTEXT_WINDOW_TOKENS, auto_compact_token_limit: DEFAULT_GAME_CREATOR_LLM_AUTO_COMPACT_TOKEN_LIMIT, tool_output_token_limit: DEFAULT_GAME_CREATOR_LLM_TOOL_OUTPUT_TOKEN_LIMIT, request_timeout_ms: GAME_CREATOR_LLM_REQUEST_TIMEOUT_MS, max_retries: DEFAULT_GAME_CREATOR_LLM_MAX_RETRIES, retry_backoff_ms: DEFAULT_RETRY_BACKOFF_MS, } } } impl Default for GameCreatorEditorApiConfig { fn default() -> Self { Self { base_url: DEFAULT_CANVAS_SYNC_API_BASE_URL.to_string(), api_key: String::new(), } } } #[derive(Clone, Copy, Debug)] struct AgentRoleDefinition { id: &'static str, role: &'static str, task_id: &'static str, tool_id: &'static str, brief_path_name: &'static str, } #[derive(Clone, Copy, Debug)] struct AgentGroupDefinition { id: &'static str, label: &'static str, role: &'static str, brief_path_name: &'static str, roles: &'static [AgentRoleDefinition], } include!(concat!(env!("OUT_DIR"), "/agent_runtime_prompt_bundle.rs")); const GAME_CREATOR_LEGACY_CHAT_AGENT_CONFIG_ID: &str = "chat"; struct GameCreatorLlmAgentStatusDefinition { agent_id: String, label: String, } fn game_creator_llm_agent_status_definitions() -> Vec { let mut agents = vec![ GameCreatorLlmAgentStatusDefinition { agent_id: GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string(), label: format!("{} Agent", PROJECT_SUPERVISOR_AGENT_DEFINITION.label), }, GameCreatorLlmAgentStatusDefinition { agent_id: "planner".to_string(), label: "Planner".to_string(), }, GameCreatorLlmAgentStatusDefinition { agent_id: "orchestrator".to_string(), label: "Orchestrator".to_string(), }, GameCreatorLlmAgentStatusDefinition { agent_id: "generator".to_string(), label: "Generator".to_string(), }, GameCreatorLlmAgentStatusDefinition { agent_id: "evaluator".to_string(), label: "Evaluator".to_string(), }, ]; for group in GAME_CREATOR_AGENT_GROUP_DEFINITIONS { for role in group.roles { if agents.iter().any(|agent| agent.agent_id == role.task_id) { continue; } agents.push(GameCreatorLlmAgentStatusDefinition { agent_id: role.task_id.to_string(), label: format!("{} / {}", group.label, role.role), }); } } agents } #[derive(Clone, Debug)] struct AgentPassArtifactPaths { draft_json: String, design_markdown: String, balance_json: String, art_manifest_json: String, audio_manifest_json: String, publish_readme: String, game_html: String, handoff_markdown: String, } #[derive(Clone, Debug)] struct AgentRoleBrief { group_definition: AgentGroupDefinition, role_definition: AgentRoleDefinition, markdown: String, relative_path: String, memory_relative_path: String, status: String, tool_id: String, summary: String, } #[derive(Clone, Debug)] struct AgentGroupBrief { definition: AgentGroupDefinition, markdown: String, relative_path: String, role_briefs: Vec, } #[derive(Clone, Debug)] struct AgentPassAgenda { relative_path: String, task_graph_relative_path: String, active_task_ids: Vec, carried_task_ids: Vec, dependency_waves: Vec>, repair_focus: Vec, repair_routes: Vec, summary: String, } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] struct LlmGameDraft { title: String, design_markdown: String, balance: serde_json::Value, art_manifest: serde_json::Value, audio_manifest: serde_json::Value, publish_readme: String, handoffs: Vec, game_html: String, handoff_summary: String, } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] struct LlmAgentHandoff { group: String, role: String, summary: String, outputs: Vec, next: String, } const DIAGNOSTIC_LOG_MAX_BYTES: u64 = 256 * 1024; static DIAGNOSTIC_LOG_LOCK: OnceLock> = OnceLock::new(); static STARTUP_PANIC_LOG_PATH: OnceLock = OnceLock::new(); static STARTUP_ERROR_DIALOG_SHOWN: AtomicBool = AtomicBool::new(false); fn diagnostic_timestamp() -> u64 { SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_secs() } fn append_bounded_diagnostic_line_with_limit( path: &Path, line: &str, max_bytes: u64, ) -> std::io::Result<()> { let _guard = DIAGNOSTIC_LOG_LOCK .get_or_init(|| Mutex::new(())) .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()); if let Some(parent) = path.parent() { fs::create_dir_all(parent)?; } let mut file = open_secure_diagnostic_log(path)?; if file.metadata()?.len() >= max_bytes { file.seek(SeekFrom::Start(0)).map_err(|error| { std::io::Error::new(error.kind(), format!("seek current log: {error}")) })?; let mut previous_content = Vec::new(); std::io::Read::by_ref(&mut file) .take(max_bytes.saturating_add(1)) .read_to_end(&mut previous_content) .map_err(|error| { std::io::Error::new(error.kind(), format!("read current log: {error}")) })?; let previous_path = path.with_extension("previous.log"); let mut previous = open_secure_diagnostic_log(&previous_path)?; previous.set_len(0).map_err(|error| { std::io::Error::new(error.kind(), format!("truncate previous log: {error}")) })?; previous.write_all(&previous_content).map_err(|error| { std::io::Error::new(error.kind(), format!("write previous log: {error}")) })?; previous.flush().map_err(|error| { std::io::Error::new(error.kind(), format!("flush previous log: {error}")) })?; file.set_len(0).map_err(|error| { std::io::Error::new(error.kind(), format!("truncate current log: {error}")) })?; } file.seek(SeekFrom::End(0)) .map_err(|error| std::io::Error::new(error.kind(), format!("seek log end: {error}")))?; writeln!(file, "{} {line}", diagnostic_timestamp())?; file.flush() } fn open_secure_diagnostic_log(path: &Path) -> std::io::Result { match fs::symlink_metadata(path) { Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_file() => { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, "diagnostic log must be a regular file", )); } Ok(_) => {} Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} Err(error) => return Err(error), } let mut options = OpenOptions::new(); options.read(true).write(true).create(true); #[cfg(unix)] { use std::os::unix::fs::OpenOptionsExt; options.mode(0o600).custom_flags(libc::O_NOFOLLOW); } #[cfg(windows)] { use std::os::windows::fs::OpenOptionsExt; const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000; options.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT); } let file = options.open(path)?; let metadata = file.metadata()?; if !metadata.is_file() { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, "diagnostic log must be a regular file", )); } #[cfg(unix)] { use std::os::unix::fs::MetadataExt; if metadata.nlink() != 1 { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, "diagnostic log must not be a hardlink", )); } } #[cfg(windows)] crate::runner::validate_windows_regular_file_handle(&file, "diagnostic log") .map_err(std::io::Error::other)?; Ok(file) } pub(crate) fn append_bounded_diagnostic_line(path: &Path, line: &str) -> std::io::Result<()> { append_bounded_diagnostic_line_with_limit(path, line, DIAGNOSTIC_LOG_MAX_BYTES) } fn redact_windows_absolute_paths(value: &str) -> String { let bytes = value.as_bytes(); let mut output = String::with_capacity(value.len()); let mut cursor = 0; while cursor < bytes.len() { let previous_allows_drive_path = cursor == 0 || !bytes[cursor - 1].is_ascii_alphanumeric(); let is_drive_path = previous_allows_drive_path && cursor + 2 < bytes.len() && bytes[cursor].is_ascii_alphabetic() && bytes[cursor + 1] == b':' && matches!(bytes[cursor + 2], b'\\' | b'/'); if !is_drive_path { let ch = value[cursor..] .chars() .next() .expect("valid character boundary"); output.push(ch); cursor += ch.len_utf8(); continue; } output.push_str(""); cursor += 3; while cursor < bytes.len() && !bytes[cursor].is_ascii_whitespace() && !matches!(bytes[cursor], b'\"' | b'\'' | b',' | b';') { cursor += 1; } } output } fn redact_unix_absolute_paths(value: &str) -> String { let chars = value.chars().collect::>(); let mut output = String::with_capacity(value.len()); let mut cursor = 0; while cursor < chars.len() { let previous_allows_path = cursor == 0 || chars[cursor - 1].is_whitespace() || matches!(chars[cursor - 1], '=' | '(' | ':' | ':'); let is_url_separator = chars.get(cursor + 1) == Some(&'/'); if chars[cursor] != '/' || !previous_allows_path || is_url_separator { output.push(chars[cursor]); cursor += 1; continue; } output.push_str(""); cursor += 1; while cursor < chars.len() && !chars[cursor].is_whitespace() && !matches!(chars[cursor], '"' | '\'' | ',' | ';') { cursor += 1; } } output } pub(crate) fn sanitize_diagnostic_message(value: &str, private_root: Option<&Path>) -> String { let mut sanitized = value.replace(['\r', '\n'], " "); if let Some(root) = private_root { let root = root.to_string_lossy(); if !root.is_empty() { sanitized = sanitized.replace(root.as_ref(), ""); } } let lowercase = sanitized.to_ascii_lowercase(); if [ "authorization", "bearer ", "api_key", "apikey", "api key", "x-api-key", "token=", "token:", "credential", ] .iter() .any(|marker| lowercase.contains(marker)) { return "".to_string(); } sanitized = redact_unix_absolute_paths(&redact_windows_absolute_paths(&sanitized)); sanitized.chars().take(2_048).collect() } fn show_startup_error_dialog(log_path: &Path) { eprintln!("Genarrative startup failed; see {}", log_path.display()); } #[derive(Clone, Debug)] struct GameCreatorAgentLoopResult { run_id: String, draft: LlmGameDraft, spec_markdown: String, findings_markdown: String, passes: u8, steps: Vec, } fn game_creator_gui_run_event_requests_runner_shutdown(event: &tauri::RunEvent) -> bool { matches!(event, tauri::RunEvent::Exit) } #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum GameCreatorGuiRunnerShutdownOutcome { NotRequested, Requested, Failed(GameCreatorGuiRunnerShutdownFailure), } #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum GameCreatorGuiRunnerShutdownFailure { EndpointUnavailable, RunnerUnresponsive, ProcessIdentity, PlatformUnsupported, LockTimeout, Other, } impl GameCreatorGuiRunnerShutdownFailure { fn code(self) -> &'static str { match self { Self::EndpointUnavailable => "endpoint_unavailable", Self::RunnerUnresponsive => "runner_unresponsive", Self::ProcessIdentity => "process_identity", Self::PlatformUnsupported => "platform_unsupported", Self::LockTimeout => "lock_timeout", Self::Other => "other", } } } fn classify_game_creator_gui_runner_shutdown_error( error: &str, ) -> GameCreatorGuiRunnerShutdownFailure { if error.contains("进程启动身份") || error.contains("pid 已") || error.contains("pidfd") || error.contains("进程句柄") { GameCreatorGuiRunnerShutdownFailure::ProcessIdentity } else if error.contains("当前平台不支持") || error.contains("macOS 不提供") { GameCreatorGuiRunnerShutdownFailure::PlatformUnsupported } else if error.contains("实例锁") || error.contains("owner 锁") { GameCreatorGuiRunnerShutdownFailure::LockTimeout } else if error.contains("endpoint") { GameCreatorGuiRunnerShutdownFailure::EndpointUnavailable } else if error.contains("响应") || error.contains("连接 Agent Runner") { GameCreatorGuiRunnerShutdownFailure::RunnerUnresponsive } else { GameCreatorGuiRunnerShutdownFailure::Other } } fn resolve_game_creator_gui_runner_shutdown( event: &tauri::RunEvent, shutdown: F, ) -> GameCreatorGuiRunnerShutdownOutcome where F: FnOnce() -> Result<(), String>, { if !game_creator_gui_run_event_requests_runner_shutdown(event) { return GameCreatorGuiRunnerShutdownOutcome::NotRequested; } match shutdown() { Ok(()) => GameCreatorGuiRunnerShutdownOutcome::Requested, Err(error) => GameCreatorGuiRunnerShutdownOutcome::Failed( classify_game_creator_gui_runner_shutdown_error(&error), ), } } fn handle_game_creator_gui_run_event(event: &tauri::RunEvent) { if matches!(event, tauri::RunEvent::Exit) { if let Err(error) = agent::shutdown_game_creator_codex_app_servers() { eprintln!("agent.direct_codex.gui_exit.shutdown_failed: {error}"); } } match resolve_game_creator_gui_runner_shutdown(event, shutdown_external_agent_runner) { GameCreatorGuiRunnerShutdownOutcome::NotRequested => {} GameCreatorGuiRunnerShutdownOutcome::Requested => { eprintln!("agent.runner.gui_exit.shutdown_requested") } GameCreatorGuiRunnerShutdownOutcome::Failed(failure) => { eprintln!("agent.runner.gui_exit.shutdown_failed.{}", failure.code()) } } } /// Tauri 的全局异步 runtime 默认由 `TokioRuntime::new()` 建出来,worker 线程吃 /// tokio 默认栈。Runtime 的 agent turn 调用链深到本仓库另一处专门给自己的后台 /// 线程配了 AGENT_RUNTIME_BACKGROUND_WORKER_STACK_BYTES;但凡经 async_runtime::spawn /// 派发的活(例如静态委派的父 run 唤醒)都落在这些默认栈的 worker 上,同一段代码 /// 在那里直接 `thread 'tokio-rt-worker' has overflowed its stack` 把整个进程 abort /// 掉——现场表现是父 run 认领委派回执那一刻 Agent Runner 无声消失,调用方只看到 /// 连接超时。这里在任何异步派发之前把全局 runtime 换成同样栈尺寸的实例。 /// /// `async_runtime::set` 只接受 handle 且要求底层 Runtime 常驻,所以这里刻意泄漏。 fn build_agent_runtime_async_runtime() -> Result { tokio::runtime::Builder::new_multi_thread() .enable_all() .thread_stack_size(crate::agent::AGENT_RUNTIME_BACKGROUND_WORKER_STACK_BYTES) .build() .map_err(|error| format!("创建全局异步 runtime 失败:{error}")) } #[cfg(not(test))] fn install_agent_runtime_async_runtime_with_deep_stack() { let runtime = match build_agent_runtime_async_runtime() { Ok(runtime) => runtime, Err(error) => { eprintln!("agent.runner.failed: {error}"); std::process::exit(1); } }; tauri::async_runtime::set(runtime.handle().clone()); // 进程生命周期内必须持有,否则 handle 立即失效。 Box::leak(Box::new(runtime)); } #[cfg(windows)] fn run_windows_acl_repair_if_requested(args: &[String]) -> Option { let [command, path, target_user_sid_flag, target_user_sid, authorization_flag, nonce, scope_flag, scope_value] = args else { if args.first().map(String::as_str) == Some("--repair-private-acl") { eprintln!( "用法:--repair-private-acl <路径> --target-user-sid --authorization <票据> --scope " ); return Some(1); } return None; }; if command != "--repair-private-acl" || target_user_sid_flag != "--target-user-sid" || authorization_flag != "--authorization" || scope_flag != "--scope" { eprintln!( "用法:--repair-private-acl <路径> --target-user-sid --authorization <票据> --scope " ); return Some(1); } let path = std::path::PathBuf::from(path); let scope = match config::parse_windows_acl_repair_scope(scope_value) { Ok(scope) => scope, Err(error) => { eprintln!("{error}"); return Some(1); } }; let result = config::consume_windows_acl_repair_authorization(&path, target_user_sid, nonce, scope) .and_then(|()| { config::repair_game_creator_private_acl_for_user_sid(&path, target_user_sid) }); match result { Ok(()) => Some(0), Err(error) => { eprintln!("AGC ACL 提权修复失败:{error}"); Some(1) } } } #[cfg(test)] mod async_runtime_stack_tests { /// 每帧固定占 16 KiB,用 black_box 挡住优化,让递归深度直接换算成栈用量。 fn consume_stack(depth: usize) -> u64 { let mut frame = [0_u8; 16 * 1024]; frame[depth % frame.len()] = depth as u8; let sum = std::hint::black_box(&frame) .iter() .map(|byte| u64::from(*byte)) .sum::(); if depth == 0 { sum } else { sum + consume_stack(depth - 1) } } /// 全局异步 runtime 的 worker 必须和 Runtime 自己的后台线程用同一份栈预算。 /// 掉了 thread_stack_size 时这条不是断言失败而是整个测试进程被 abort——这正是 /// 线上的失效形态:Agent Runner 在父 run 认领委派回执时无声消失。 #[test] fn async_runtime_workers_hold_a_call_chain_that_overflows_the_default_stack() { const FRAMES: usize = 192; // 192 × 16 KiB = 3 MiB,超出 tokio 默认栈,远低于 16 MiB let runtime = super::build_agent_runtime_async_runtime().expect("build async runtime"); let handled = runtime.block_on(async { tokio::spawn(async { consume_stack(FRAMES - 1) }).await }); assert!(handled.is_ok(), "{handled:?}"); } } #[cfg(not(test))] fn main() { install_agent_runtime_async_runtime_with_deep_stack(); let mut args = std::env::args().skip(1).collect::>(); if let Some(exit_code) = run_direct_tools_mcp_if_requested(&args) { std::process::exit(exit_code); } #[cfg(windows)] if let Some(exit_code) = run_windows_acl_repair_if_requested(&args) { std::process::exit(exit_code); } #[cfg(target_os = "linux")] if command_sandbox_trampoline::is_trampoline_mode(&args) { match command_sandbox_trampoline::run_trampoline() { Ok(exit_code) => std::process::exit(exit_code), Err(_) => std::process::exit(125), } } #[cfg(target_os = "linux")] if command_sandbox_trampoline::is_process_session_trampoline_mode(&args) { match command_sandbox_trampoline::run_process_session_trampoline() { Ok(exit_code) => std::process::exit(exit_code), Err(_) => std::process::exit(125), } } #[cfg(target_os = "linux")] if is_process_session_child_mode(&args) { match run_process_session_child(&args) { Ok(exit_code) => std::process::exit(exit_code), Err(_) => std::process::exit(125), } } let runtime_config_dir = match take_cli_runtime_config_dir(&mut args) { Ok(config_dir) => config_dir, Err(error) => { eprintln!("{error}"); std::process::exit(1); } }; if args.first().map(String::as_str) == Some("--agent-runner") { let gui_owner_required = match args.as_slice() { [_] => false, [_, option] if option == "--gui-owner-required" => true, _ => { eprintln!( "用法:--agent-runner [--gui-owner-required] --config-dir " ); std::process::exit(1); } }; let Some(config_dir) = runtime_config_dir else { eprintln!("Agent Runner 必须显式传入 --config-dir "); std::process::exit(1); }; if let Err(error) = load_platform_session_fixture_from_env(&config_dir) { eprintln!("agent.runner.failed: {error}"); std::process::exit(1); } set_game_creator_runtime_config_dir(config_dir.clone()); if let Err(error) = run_external_agent_runner_server(config_dir, gui_owner_required) { eprintln!("agent.runner.failed: {error}"); std::process::exit(1); } return; } match parse_cli_command(&args) { Ok(Some(mut command)) => { let config_dir = match prepare_cli_command_paths(&mut command, runtime_config_dir.as_deref()) { Ok(config_dir) => config_dir, Err(error) => { eprintln!("agent.runner.failed: {error}"); std::process::exit(1); } }; if let Some(config_dir) = config_dir { if let Err(error) = load_platform_session_fixture_from_env(&config_dir) { eprintln!("agent.runner.failed: {error}"); std::process::exit(1); } if command.requires_external_agent_runner() { let configured = if command.is_read_only_status() { configure_external_agent_runner_read_only(&config_dir) } else { configure_external_agent_runner(&config_dir) }; if let Err(error) = configured { eprintln!("agent.runner.failed: {error}"); std::process::exit(1); } } set_game_creator_runtime_config_dir(config_dir); } if command.requires_started_external_agent_runner() { if let Err(error) = ensure_external_agent_runner_started() { eprintln!("agent.runner.failed: {error}"); std::process::exit(1); } } if let Err(error) = run_cli_command(command) { eprintln!("agent.run.failed: {error}"); std::process::exit(1); } return; } Ok(None) => {} Err(error) => { eprintln!("{error}"); std::process::exit(1); } } if let Some(config_dir) = runtime_config_dir { set_game_creator_runtime_config_dir(config_dir); } let mut tauri_context = tauri::generate_context!(); let startup_log: Option = None; let setup_log = startup_log.clone(); let app = tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_http::init()) .plugin(tauri_plugin_clipboard_manager::init()) .plugin(context_menu::init()) .manage(game_creator_preview_registry()) .manage(ProjectResourcePreviewReadManager::default()) .setup(move |app| { if let Some(path) = setup_log.as_deref() { let _ = append_bounded_diagnostic_line(path, "startup.setup.begin"); let _ = append_bounded_diagnostic_line(path, "startup.appdata.configure.begin"); } configure_game_creator_runtime_config_dir(app.handle()).inspect_err(|error| { if let Some(path) = setup_log.as_deref() { let details = sanitize_diagnostic_message(&error.to_string(), path.parent()); let _ = append_bounded_diagnostic_line( path, &format!("startup.appdata.configure.failed details={details}"), ); show_startup_error_dialog(path); } })?; if let Some(path) = setup_log.as_deref() { let _ = append_bounded_diagnostic_line(path, "startup.appdata.configure.complete"); } let config_dir = game_creator_runtime_config_dir().ok_or_else(|| { let error = std::io::Error::new( std::io::ErrorKind::NotFound, "客户端 AppData 配置目录未初始化", ); if let Some(path) = setup_log.as_deref() { let _ = append_bounded_diagnostic_line( path, "startup.appdata.resolve.failed details=config-dir-uninitialized", ); show_startup_error_dialog(path); } error })?; load_platform_session_fixture_from_env(&config_dir).map_err(|error| { std::io::Error::new( std::io::ErrorKind::PermissionDenied, format!("加载平台登录态测试 fixture 失败:{error}"), ) })?; if let Some(path) = setup_log.as_deref() { let _ = append_bounded_diagnostic_line(path, "startup.runner.configure.begin"); } configure_external_agent_runner(&config_dir) .inspect_err(|error| { if let Some(path) = setup_log.as_deref() { let details = sanitize_diagnostic_message(error, Some(config_dir.as_path())); let _ = append_bounded_diagnostic_line( path, &format!("startup.runner.configure.failed details={details}"), ); show_startup_error_dialog(path); } }) .map_err(|error| { std::io::Error::new( std::io::ErrorKind::Other, format!("配置 Agent Runner 失败:{error}"), ) })?; if let Some(path) = setup_log.as_deref() { let _ = append_bounded_diagnostic_line(path, "startup.runner.configure.complete"); } let gui_owner_lock = acquire_external_agent_runner_gui_owner_lock(&config_dir) .inspect_err(|error| { if let Some(path) = setup_log.as_deref() { let details = sanitize_diagnostic_message(error, Some(config_dir.as_path())); let _ = append_bounded_diagnostic_line( path, &format!("startup.runner.owner-lock.failed details={details}"), ); show_startup_error_dialog(path); } }) .map_err(|error| { std::io::Error::new( std::io::ErrorKind::AlreadyExists, format!("获取 GUI owner 锁失败:{error}"), ) })?; let gui_owner_epoch = gui_owner_lock.owner_epoch().to_string(); app.manage(gui_owner_lock); if let Some(path) = setup_log.as_deref() { let _ = append_bounded_diagnostic_line(path, "startup.runner.start.begin"); } set_game_creator_agent_runtime_update_app_handle(app.handle().clone()); let manifest_event_sink = start_game_creator_manifest_invalidation_event_sink(app.handle().clone())?; attach_external_agent_runner_gui_owner(&manifest_event_sink, &gui_owner_epoch) .inspect_err(|error| { if let Some(path) = setup_log.as_deref() { let details = sanitize_diagnostic_message(error, Some(config_dir.as_path())); let _ = append_bounded_diagnostic_line( path, &format!("startup.runner.attach-owner.failed details={details}"), ); show_startup_error_dialog(path); } }) .map_err(|error| { std::io::Error::new( std::io::ErrorKind::Other, format!("绑定 Agent Runner GUI owner 失败:{error}"), ) })?; if let Some(path) = setup_log.as_deref() { let _ = append_bounded_diagnostic_line(path, "startup.runner.start.complete"); } if let Some(path) = setup_log.as_deref() { let _ = append_bounded_diagnostic_line(path, "startup.setup.complete"); } Ok(()) }) .invoke_handler(tauri::generate_handler![ create_automatic_local_game_project, init_local_game_project, import_local_godot_project, is_local_project_directory_non_empty, inspect_local_project_directory, pick_local_project_directory, pick_local_file, pick_client_extension_file, pick_client_extension_directory, list_client_extensions, import_client_extension, set_client_extension_enabled, rename_client_extension, remove_client_extension, open_local_project_directory, open_local_project_plan_gdd_markdown, control_agent_run, generate_local_game_draft, chat_with_game_creator_agent, chat_with_game_creator_role_agent, chat_with_game_creator_role_agent_stream, chat_with_game_creator_direct_codex, start_game_creator_agent_runtime_task, start_game_creator_supervisor_runtime_task, compact_game_creator_agent_runtime_context, read_game_creator_agent_goal, start_game_creator_agent_goal, edit_game_creator_agent_goal, pause_game_creator_agent_goal, resume_game_creator_agent_goal, clear_game_creator_agent_goal, steer_game_creator_agent_runtime_task, cancel_game_creator_agent_runtime_task, retry_game_creator_agent_runtime_task, confirm_retry_game_creator_agent_runtime_task, confirm_game_creator_agent_runtime_task, reject_game_creator_agent_runtime_task, answer_game_creator_agent_runtime_user_input, decide_game_creator_plan_gdd, hydrate_game_creator_plan_gdd_state, read_game_creator_agent_runtime, read_game_creator_agent_runtimes, resume_game_creator_agent_runtime_tasks, confirm_resume_game_creator_agent_runtime_tasks, schedule_game_creator_agent_ready_tasks, check_game_creator_llm_config, read_platform_account_session_generation, install_platform_account_session, clear_platform_account_session, read_game_creator_app_config, write_game_creator_app_config, upload_local_asset, register_local_asset, create_ui_design_resource, derive_local_project_resource, list_pending_local_project_resource_edits, resume_local_project_resource_edit, request_local_project_resource_edit_service_identity_confirmation, confirm_local_project_resource_edit_service_identity, archive_failed_local_project_resource_edit, normalize_local_project_raster_resource, import_canvas_asset, import_canvas_export, sync_canvas_project_assets, import_ui_editor_assets, prepare_ui_editor_project_fonts, read_ui_editor_font_bytes, check_ui_editor_font_glyph_coverage, suggest_ui_design_semantic, recognize_ui, merge_ui, bind_components, load_ui_design_state, save_ui_design_state, generate_ui_design_code, ensure_ui_design_resource_for_prototype, generate_platform_art_asset, open_canvas_project, get_game_creation_agent_capabilities, get_limited_local_commands, run_limited_local_command, append_local_permission_log, list_local_project_files, import_local_project_image_assets, read_local_project_file, read_local_project_image_preview, read_local_project_text_preview, read_local_project_media_preview, cancel_local_project_resource_preview_scope, write_local_project_file, delete_local_project_file, read_local_game_memory, read_local_agent_memory, write_local_agent_memory, write_local_game_memory, delete_local_game_memory, list_game_creator_agent_sessions, create_game_creator_agent_session, fork_game_creator_agent_session, set_active_game_creator_agent_session, archive_game_creator_agent_session, read_local_conversation, append_local_conversation_message, build_local_project_index, create_local_project_checkpoint, export_local_project_package, list_local_project_export_packages, diff_local_project_checkpoint, restore_local_project_checkpoint, read_project_permission_policy, write_project_permission_policy, open_game_creator_workspace_window, open_game_creator_launcher_window, open_project_supervisor_chat_window, start_local_game_preview, activate_local_game_preview, stop_local_game_preview, stop_local_game_preview_if_matches, get_local_game_preview_status, read_local_project_resource_canvas_layout, read_local_project_resource_graph, update_local_project_resource_canvas_layout, create_local_project_asset_canvas_draft, read_local_project_asset_canvas_draft, discover_local_project_asset_canvas_draft, update_local_project_asset_canvas_draft, acknowledge_local_project_asset_canvas_candidate_layers, import_local_project_asset_canvas_images, store_local_project_asset_canvas_media, stage_local_project_asset_canvas_image, finalize_local_project_asset_canvas_generation_failure, archive_failed_local_project_asset_canvas_generation, generate_local_project_asset_canvas_image, recover_local_project_asset_canvas_generations, confirm_local_project_asset_canvas_generation_service_identity, read_local_project_asset_canvas_media, discard_local_project_asset_canvas_draft, recover_local_project_asset_canvas_transactions, commit_local_project_asset, commit_local_project_asset_canvas_candidate, get_local_game_project_revision, get_local_game_manifest, download_agc_update, ]) .build(tauri_context); let app = match app { Ok(app) => { if let Some(path) = startup_log.as_deref() { let _ = append_bounded_diagnostic_line(path, "startup.build.complete"); } app } Err(error) => { if let Some(path) = startup_log.as_deref() { let details = sanitize_diagnostic_message(&error.to_string(), path.parent()); let _ = append_bounded_diagnostic_line( path, &format!("startup.build.failed details={details}"), ); show_startup_error_dialog(path); } eprintln!("failed to build Genarrative AI Game Creator shell: {error}"); std::process::exit(1); } }; app.run(move |_app_handle, event| handle_game_creator_gui_run_event(&event)); } #[cfg(test)] mod diagnostic_log_tests { use super::*; #[test] fn bounded_diagnostic_log_rotates_and_keeps_only_one_previous_file() { let directory = tempfile::tempdir().expect("create diagnostics directory"); let path = directory.path().join("startup.log"); let first_record = "x".repeat(128); append_bounded_diagnostic_line_with_limit(&path, &first_record, 64) .expect("write first record"); append_bounded_diagnostic_line_with_limit(&path, "second-record", 64) .expect("rotate diagnostic log"); let current = fs::read_to_string(&path).expect("read current diagnostic log"); let previous = fs::read_to_string(path.with_extension("previous.log")) .expect("read previous diagnostic log"); assert!(current.contains("second-record")); assert!(previous.contains(&"x".repeat(32))); } #[test] fn diagnostic_message_redacts_sensitive_values_and_absolute_paths() { assert_eq!( sanitize_diagnostic_message("Authorization: Bearer secret", None), "" ); assert_eq!( sanitize_diagnostic_message(r"failed at C:\private\project\game.json", None), "failed at " ); assert_eq!( sanitize_diagnostic_message("failed at /home/example/private/game.json", None), "failed at " ); } #[test] fn diagnostic_log_rejects_hardlink_targets_including_rotation_backup() { let directory = tempfile::tempdir().expect("create diagnostics directory"); let outside = directory.path().join("outside.txt"); fs::write(&outside, "outside-unchanged").expect("write outside target"); let path = directory.path().join("startup.log"); fs::hard_link(&outside, &path).expect("create diagnostic hardlink"); assert!(append_bounded_diagnostic_line(&path, "must-not-write").is_err()); assert_eq!( fs::read_to_string(&outside).expect("read outside target"), "outside-unchanged" ); fs::remove_file(&path).expect("remove diagnostic hardlink"); fs::write(&path, "rotate-me").expect("write diagnostic file"); let previous = path.with_extension("previous.log"); fs::hard_link(&outside, &previous).expect("create previous hardlink"); assert!(append_bounded_diagnostic_line_with_limit(&path, "blocked", 1).is_err()); assert_eq!( fs::read_to_string(&outside).expect("read outside target after rotation"), "outside-unchanged" ); } #[cfg(unix)] #[test] fn diagnostic_log_rejects_symlink_targets() { use std::os::unix::fs::symlink; let directory = tempfile::tempdir().expect("create diagnostics directory"); let outside = directory.path().join("outside.txt"); fs::write(&outside, "outside-unchanged").expect("write outside target"); let path = directory.path().join("startup.log"); symlink(&outside, &path).expect("create diagnostic symlink"); assert!(append_bounded_diagnostic_line(&path, "must-not-write").is_err()); assert_eq!( fs::read_to_string(&outside).expect("read outside target"), "outside-unchanged" ); } #[cfg(windows)] #[test] fn diagnostic_log_rejects_windows_symlink_or_reparse_targets_when_supported() { use std::os::windows::fs::symlink_file; let directory = tempfile::tempdir().expect("create diagnostics directory"); let outside = directory.path().join("outside.txt"); fs::write(&outside, "outside-unchanged").expect("write outside target"); let path = directory.path().join("startup.log"); if symlink_file(&outside, &path).is_err() { return; } assert!(append_bounded_diagnostic_line(&path, "must-not-write").is_err()); assert_eq!( fs::read_to_string(&outside).expect("read outside target"), "outside-unchanged" ); } } #[cfg(test)] mod update_client_tests { use super::*; use std::io::{Read, Write}; #[tokio::test] async fn update_download_client_omits_agc_marker() { let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind update fixture"); let address = listener.local_addr().expect("update fixture address"); let server = std::thread::spawn(move || { let (mut stream, _) = listener.accept().expect("accept update request"); stream .set_read_timeout(Some(std::time::Duration::from_secs(2))) .expect("set update fixture timeout"); let mut bytes = Vec::new(); let mut buffer = [0_u8; 1024]; while !bytes.windows(4).any(|window| window == b"\r\n\r\n") { let read = stream.read(&mut buffer).expect("read update request"); assert!(read > 0, "update request closed before headers"); bytes.extend_from_slice(&buffer[..read]); } stream .write_all( b"HTTP/1.1 204 No Content\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", ) .expect("write update response"); String::from_utf8_lossy(&bytes).into_owned() }); let client = build_agc_update_download_client(); let response = client .get(format!("http://{address}/update.exe")) .send() .await .expect("send update request"); let request = server.join().expect("join update fixture"); assert_eq!(response.status(), reqwest::StatusCode::NO_CONTENT); assert!(!request .to_ascii_lowercase() .contains("x-genarrative-client:")); } } #[cfg(test)] mod tests; pub mod ui_editor;