From 6106b4e67caa6d55feaac374a86aa32ece2b5e64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Wed, 23 Sep 2026 11:12:39 +0800 Subject: [PATCH] =?UTF-8?q?=E9=80=80=E5=BD=B9AGC=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E5=AF=B9=E8=AF=9D=E6=96=9C=E6=9D=A0=E5=91=BD=E4=BB=A4=E4=B8=8E?= =?UTF-8?q?=E7=BB=88=E7=AB=AFswarm=20chat=E5=85=A5=E5=8F=A3=EF=BC=9A?= =?UTF-8?q?=E5=BA=94=E7=94=A8=E4=B8=8ERust=E5=AE=9E=E7=8E=B0=E5=88=A0?= =?UTF-8?q?=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除应用侧 /history 精确匹配分支与 reloadHistory、chatPromptPolish 的 / 前缀绕过、chatCommandMetadata、memoryCommands、projectSummaryConstants 命令清单 - 删除只服务退役摘要面板的 project-summary/*Summaries.ts 与 agentTrace.ts 及对应测试 - 删除 /sync-canvas-project、/read、/trace 草稿回填死链(agentPresentation.ts 与 Rust suggested_canvas_tool_call) - 删除无人调用的 Tauri 命令 get_game_creation_agent_capabilities 与 get_limited_local_commands - 删除 --swarm-chat 入口、SwarmChat 变体、src/swarm_cli.rs 与整个 swarm_cli/ 目录 - 收敛 agent/interaction.rs 至自然语言 steer 决策路径,删除交互内核整层 - 删除 SWARM_TURN_*_ERROR、print_runtime_response_stream_status 及其专属测试 - 更新 ChatMarkdownMessage、chatPromptPolish、rememberCommand 与 appSurface 用例,移除斜杠命令断言 --- .../src-tauri/src/agent/generation.rs | 5 +- .../src/agent/generation/canvas_generation.rs | 27 - .../agent/generation/loop_orchestration.rs | 2 +- .../src/agent/generation/pass_artifacts.rs | 9 +- .../src-tauri/src/agent/interaction.rs | 494 +--- .../src-tauri/src/agent/runtime_protocol.rs | 2 +- .../src-tauri/src/cli.rs | 167 +- .../src-tauri/src/commands.rs | 16 +- .../src-tauri/src/main.rs | 22 +- .../src-tauri/src/swarm_cli.rs | 72 - .../src-tauri/src/swarm_cli/commands.rs | 79 - .../src-tauri/src/swarm_cli/conversation.rs | 337 --- .../src-tauri/src/swarm_cli/goal_commands.rs | 189 -- .../src-tauri/src/swarm_cli/input.rs | 390 --- .../src-tauri/src/swarm_cli/observer.rs | 843 ------ .../src-tauri/src/swarm_cli/report.rs | 285 -- .../src/swarm_cli/terminal_classification.rs | 803 ------ .../src-tauri/src/swarm_cli/tests.rs | 2417 ----------------- .../src-tauri/src/swarm_cli/turn_dispatch.rs | 590 ---- .../src-tauri/src/swarm_cli/turn_wait.rs | 427 --- .../src-tauri/src/tests/project.rs | 52 - .../src-tauri/src/tests/provider.rs | 24 - apps/ai-game-creator-shell/src/App.tsx | 77 +- .../project-summary/agentPresentation.ts | 653 ----- .../project-summary/agentRunSummaries.ts | 311 --- .../features/project-summary/agentTrace.ts | 96 - .../project-summary/chatCommandMetadata.ts | 48 - .../projectArtifactSummaries.ts | 368 --- .../project-summary/projectAssetSummaries.ts | 282 -- .../projectDeliverySummaries.ts | 661 ----- .../projectGuidanceSummaries.ts | 271 -- .../projectOverviewSummaries.ts | 562 ---- .../features/project-summary/projectPath.ts | 12 - .../projectPlanningSummaries.ts | 463 ---- .../projectPlaytestSummaries.ts | 786 ------ .../projectQualitySummaries.ts | 1078 -------- .../projectReadinessSummaries.ts | 811 ------ .../project-summary/projectSummary.ts | 134 - .../projectSummaryConstants.ts | 215 -- .../project-workspace/agentRunTrace.ts | 104 - .../project-workspace/chatPromptPolish.ts | 6 +- .../project-workspace/memoryCommands.ts | 53 - .../project-workspace/projectCommandPolicy.ts | 8 - .../useDirectProjectChatController.ts | 50 - .../tests/ChatMarkdownMessage.test.tsx | 7 +- .../tests/agentRunTrace.test.ts | 155 -- .../tests/agentSwarmTestEntry.test.ts | 1915 ------------- .../tests/agentTraceSummary.test.ts | 284 -- .../tests/appSurface/harness.ts | 16 - .../appSurface/project-conversation.suite.ts | 158 -- .../appSurface/project-development.suite.ts | 166 -- .../tests/chatPromptPolish.test.tsx | 21 +- .../tests/rememberCommand.test.ts | 42 +- 53 files changed, 40 insertions(+), 17025 deletions(-) delete mode 100644 apps/ai-game-creator-shell/src-tauri/src/swarm_cli.rs delete mode 100644 apps/ai-game-creator-shell/src-tauri/src/swarm_cli/commands.rs delete mode 100644 apps/ai-game-creator-shell/src-tauri/src/swarm_cli/conversation.rs delete mode 100644 apps/ai-game-creator-shell/src-tauri/src/swarm_cli/goal_commands.rs delete mode 100644 apps/ai-game-creator-shell/src-tauri/src/swarm_cli/input.rs delete mode 100644 apps/ai-game-creator-shell/src-tauri/src/swarm_cli/observer.rs delete mode 100644 apps/ai-game-creator-shell/src-tauri/src/swarm_cli/report.rs delete mode 100644 apps/ai-game-creator-shell/src-tauri/src/swarm_cli/terminal_classification.rs delete mode 100644 apps/ai-game-creator-shell/src-tauri/src/swarm_cli/tests.rs delete mode 100644 apps/ai-game-creator-shell/src-tauri/src/swarm_cli/turn_dispatch.rs delete mode 100644 apps/ai-game-creator-shell/src-tauri/src/swarm_cli/turn_wait.rs delete mode 100644 apps/ai-game-creator-shell/src/features/project-summary/agentRunSummaries.ts delete mode 100644 apps/ai-game-creator-shell/src/features/project-summary/agentTrace.ts delete mode 100644 apps/ai-game-creator-shell/src/features/project-summary/chatCommandMetadata.ts delete mode 100644 apps/ai-game-creator-shell/src/features/project-summary/projectArtifactSummaries.ts delete mode 100644 apps/ai-game-creator-shell/src/features/project-summary/projectAssetSummaries.ts delete mode 100644 apps/ai-game-creator-shell/src/features/project-summary/projectDeliverySummaries.ts delete mode 100644 apps/ai-game-creator-shell/src/features/project-summary/projectGuidanceSummaries.ts delete mode 100644 apps/ai-game-creator-shell/src/features/project-summary/projectOverviewSummaries.ts delete mode 100644 apps/ai-game-creator-shell/src/features/project-summary/projectPlanningSummaries.ts delete mode 100644 apps/ai-game-creator-shell/src/features/project-summary/projectPlaytestSummaries.ts delete mode 100644 apps/ai-game-creator-shell/src/features/project-summary/projectQualitySummaries.ts delete mode 100644 apps/ai-game-creator-shell/src/features/project-summary/projectReadinessSummaries.ts delete mode 100644 apps/ai-game-creator-shell/src/features/project-summary/projectSummaryConstants.ts delete mode 100644 apps/ai-game-creator-shell/src/features/project-workspace/agentRunTrace.ts delete mode 100644 apps/ai-game-creator-shell/src/features/project-workspace/memoryCommands.ts delete mode 100644 apps/ai-game-creator-shell/tests/agentRunTrace.test.ts delete mode 100644 apps/ai-game-creator-shell/tests/agentSwarmTestEntry.test.ts delete mode 100644 apps/ai-game-creator-shell/tests/agentTraceSummary.test.ts diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/generation.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/generation.rs index 5b09bc5d6..f4be82c32 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/generation.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/generation.rs @@ -71,9 +71,8 @@ pub(crate) use canvas_generation::{ normalize_platform_art_reference_asset_ids, normalize_platform_art_target_category, platform_art_asset_art_spec, platform_art_asset_output_extension_matches, platform_art_runtime_references_match_request_contract, prepare_platform_art_asset_output_path, - project_canvas_asset_media_types, role_has_canvas_assets, suggested_canvas_tool_call, - validate_platform_art_icon_prompt, PlatformArtAssetGenerationOptions, - PLATFORM_ART_ASSET_GENERATION_KINDS, + project_canvas_asset_media_types, role_has_canvas_assets, validate_platform_art_icon_prompt, + PlatformArtAssetGenerationOptions, PLATFORM_ART_ASSET_GENERATION_KINDS, }; #[allow(unused_imports)] pub(crate) use draft_validation::{ diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs index e79f40ac9..e016b9101 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/canvas_generation.rs @@ -315,33 +315,6 @@ pub(crate) fn project_canvas_asset_media_types(root: &Path) -> Vec { .unwrap_or_default() } -pub(crate) fn suggested_canvas_tool_call( - role_brief: &AgentRoleBrief, - input_paths: &[String], - canvas_asset_media_types: &[String], -) -> Option { - if role_brief.status != "completed" - || role_has_canvas_assets(role_brief, canvas_asset_media_types) - { - return None; - } - let tool_id = match ( - role_brief.group_definition.id, - role_brief.role_definition.id, - ) { - ("art", "asset") | ("audio", "sfx") => "agent.tool.suggest.canvas.project_sync", - _ => return None, - }; - Some(GameCreationAgentToolCallTrace { - tool_id: tool_id.to_string(), - status: "suggested".to_string(), - input_paths: input_paths.to_vec(), - output_paths: Vec::new(), - summary: "项目还没有对应类型的画板回流素材;建议用户确认 /sync-canvas-project <画板项目ID> 后同步画板资源到本地 assets/。" - .to_string(), - }) -} - pub(crate) async fn maybe_generate_platform_art_asset_step( root: &Path, prompt: &str, diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/loop_orchestration.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/loop_orchestration.rs index a79d03fbd..72fa03629 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/loop_orchestration.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/loop_orchestration.rs @@ -204,7 +204,7 @@ pub(crate) async fn run_game_creator_agent_loop_at( let platform_art_step = maybe_generate_platform_art_asset_step(root, prompt, &group_briefs, pass, progress) .await; - append_group_brief_steps(root, pass, &agenda.relative_path, &group_briefs, &mut steps); + append_group_brief_steps(pass, &agenda.relative_path, &group_briefs, &mut steps); if let Some(step) = platform_art_step { steps.push(step); } diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/pass_artifacts.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/pass_artifacts.rs index 44a966e00..60e097107 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/pass_artifacts.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/pass_artifacts.rs @@ -395,13 +395,11 @@ pub(crate) fn append_agent_success_memories( } pub(crate) fn append_group_brief_steps( - root: &Path, pass: u8, agenda_relative_path: &str, briefs: &[AgentGroupBrief], steps: &mut Vec, ) { - let canvas_asset_media_types = project_canvas_asset_media_types(root); for brief in briefs { for role_brief in &brief.role_briefs { let input_paths = vec![ @@ -421,7 +419,7 @@ pub(crate) fn append_group_brief_steps( output_paths.push(role_brief.memory_relative_path.clone()); output_paths.push(PROJECT_BLACKBOARD_MEMORY_PATH.to_string()); } - let mut step = with_task_context( + let step = with_task_context( agent_trace_step_owned( pass, &format!( @@ -439,11 +437,6 @@ pub(crate) fn append_group_brief_steps( Some(role_brief.role_definition.task_id), "role-brief", ); - if let Some(tool_call) = - suggested_canvas_tool_call(role_brief, &input_paths, &canvas_asset_media_types) - { - step.tool_calls.push(tool_call); - } steps.push(step); } let role_paths = brief diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/interaction.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/interaction.rs index 8e7390696..397c754f1 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/interaction.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/interaction.rs @@ -1,86 +1,8 @@ use super::*; -use agent_runtime_core::{CapabilityDefinition, CapabilityRegistry}; -const AGENT_INTERACTION_EXECUTE_TOOL: &str = "runtime_execute"; -const AGENT_INTERACTION_RESUME_TOOL: &str = "runtime_resume"; -const AGENT_INTERACTION_PROJECT_LOCATION_TOOL: &str = "project_location"; -const AGENT_INTERACTION_MAX_OUTPUT_TOKENS: u32 = 1_200; -const AGENT_INTERACTION_PROVIDER_INSTANCE_ID: &str = "agc-interaction"; +const AGENT_RUNTIME_STEER_DECISION_MAX_OUTPUT_TOKENS: u32 = 1_200; pub(crate) const AGENT_RUNTIME_STEER_DECISION_TOOL: &str = "runtime_steer_decision"; -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum AgentInteractionToolKind { - Execute, - Resume, - ProjectLocation, -} - -fn agent_interaction_tool_registry() -> Result, String> -{ - let empty_input_schema = || { - serde_json::json!({ - "type": "object", - "properties": {}, - "additionalProperties": false - }) - }; - CapabilityRegistry::try_new([ - CapabilityDefinition::try_new( - AGENT_INTERACTION_EXECUTE_TOOL, - AGENT_INTERACTION_EXECUTE_TOOL, - prompt_text!("interaction.execute_description"), - empty_input_schema(), - AgentInteractionToolKind::Execute, - ) - .map_err(|error| format!("Agent interaction capability 无效:{error}"))?, - CapabilityDefinition::try_new( - AGENT_INTERACTION_RESUME_TOOL, - AGENT_INTERACTION_RESUME_TOOL, - prompt_text!("interaction.resume_description"), - empty_input_schema(), - AgentInteractionToolKind::Resume, - ) - .map_err(|error| format!("Agent interaction capability 无效:{error}"))?, - CapabilityDefinition::try_new( - AGENT_INTERACTION_PROJECT_LOCATION_TOOL, - AGENT_INTERACTION_PROJECT_LOCATION_TOOL, - prompt_text!("interaction.project_location_description"), - empty_input_schema(), - AgentInteractionToolKind::ProjectLocation, - ) - .map_err(|error| format!("Agent interaction capability 无效:{error}"))?, - ]) - .map_err(|error| format!("Agent interaction registry 无效:{error}")) -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub(crate) enum AgentInteractionAction { - Reply(String), - Execute, - Resume, - ProjectLocation, -} - -impl AgentInteractionAction { - pub(crate) fn label(&self) -> &'static str { - match self { - Self::Reply(_) => "reply", - Self::Execute => "execute", - Self::Resume => "resume", - Self::ProjectLocation => "project_location", - } - } -} - -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields, tag = "action", rename_all = "snake_case")] -enum AgentInteractionTextEnvelope { - Reply { reply: String }, - Execute, - Resume, - ProjectLocation, -} - #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields, rename_all = "camelCase")] struct AgentRuntimeSteerDecisionArguments { @@ -148,7 +70,7 @@ fn build_agent_runtime_steer_decision_request( ); let request = LlmRunRequest::single_turn(system, user) .with_api_kind(api_kind) - .with_max_output_tokens(AGENT_INTERACTION_MAX_OUTPUT_TOKENS) + .with_max_output_tokens(AGENT_RUNTIME_STEER_DECISION_MAX_OUTPUT_TOKENS) .with_function_tools(vec![agent_runtime_steer_decision_function_tool()]) .with_tool_choice(platform_llm::LlmToolChoice::Required); let request = apply_game_creator_llm_reasoning_effort(request, &llm)?; @@ -247,253 +169,6 @@ pub(crate) async fn decide_game_creator_agent_runtime_steer_at( ) } -pub(crate) fn game_creator_agent_uses_interaction_kernel(agent_id: &str) -> bool { - if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID { - return true; - } - game_creator_agent_role_definition(agent_id).is_some_and(|(_group, role)| role.id == "director") -} - -fn agent_interaction_function_tools() -> Result, String> { - Ok(agent_interaction_tool_registry()? - .iter() - .map(|definition| { - platform_llm::LlmFunctionTool::new( - definition.function_name(), - definition.description(), - definition.input_schema().clone(), - ) - .with_strict(true) - }) - .collect()) -} - -fn agent_interaction_system_prompt(agent_id: &str) -> String { - let role_prompt = if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID { - game_creator_project_supervisor_chat_system_prompt() - } else { - game_creator_role_agent_chat_system_prompt() - }; - let protocol = prompt_text!("interaction.protocol"); - format!( - prompt_text!("interaction.system"), - protocol = protocol, - role_prompt = role_prompt, - ) -} - -fn build_agent_interaction_llm_request( - agent_id: &str, - prompt: &str, - context: &str, - llm: &GameCreatorLlmConfig, -) -> Result { - let api_kind = parse_game_creator_llm_api_kind(&llm.api_kind)?; - let user_prompt = if context.trim().is_empty() { - format!(prompt_text!("interaction.user"), prompt = prompt,) - } else { - format!( - prompt_text!("interaction.user_with_context"), - context = context, - prompt = prompt, - ) - }; - let function_tools = agent_interaction_function_tools()?; - let request = LlmRunRequest::new(vec![ - LlmMessage::system(agent_interaction_system_prompt(agent_id)), - LlmMessage::user(user_prompt), - ]) - .with_api_kind(api_kind) - .with_max_output_tokens(AGENT_INTERACTION_MAX_OUTPUT_TOKENS) - .with_function_tools(function_tools) - .with_tool_choice(platform_llm::LlmToolChoice::Auto); - apply_game_creator_llm_reasoning_effort(request, llm) -} - -fn build_agent_interaction_request_for_session( - root: &Path, - agent_id: &str, - session_id: &str, - prompt: &str, -) -> Result<(GameCreatorLlmConfig, String, LlmRunRequest), String> { - let prompt = prompt.trim(); - if prompt.is_empty() { - return Err("交互内容不能为空".to_string()); - } - if !game_creator_agent_uses_interaction_kernel(agent_id) { - return Err(format!("Agent 不启用交互决策层:{agent_id}")); - } - let (llm, config_path, context) = - build_game_creator_role_agent_context_for_session(root, agent_id, Some(session_id))?; - let request = build_agent_interaction_llm_request(agent_id, prompt, &context, &llm)?; - Ok((llm, config_path, request)) -} - -pub(crate) async fn decide_game_creator_agent_interaction_turn_for_session_at( - root: &Path, - agent_id: &str, - session_id: &str, - prompt: &str, - mut on_delta: F, -) -> Result -where - F: FnMut(&platform_llm::LlmStreamDelta), -{ - let (llm, config_path, request) = - build_agent_interaction_request_for_session(root, agent_id, session_id, prompt)?; - let api_kind = request.api_kind; - let client = build_game_creator_agent_runtime_llm_client(&llm, &config_path)?; - let llm_provider = client.config().provider(); - let request = platform_llm::provider_request_from_llm_request("agent-interaction", request) - .map_err(|error| format!("{config_path} Agent interaction Provider 请求无效:{error}"))?; - let (registry, target) = platform_llm::build_platform_llm_provider_registry_for_api_kind( - AGENT_INTERACTION_PROVIDER_INSTANCE_ID, - client, - api_kind, - ) - .map_err(|error| format!("{config_path} Agent interaction Provider 注册失败:{error}"))?; - let response = if llm.stream { - let fallback_request = request.clone(); - let sink = AgentInteractionProviderStreamSink { - on_delta: &mut on_delta, - }; - match registry.stream(&target, request, Box::new(sink)).await { - Ok(response) => response, - Err(error) - if matches!( - error.kind(), - agent_runtime_core::ProviderErrorKind::StreamUnavailable - | agent_runtime_core::ProviderErrorKind::EmptyResponse - | agent_runtime_core::ProviderErrorKind::Deserialize - ) => - { - registry - .invoke(&target, fallback_request) - .await - .map_err(|fallback_error| { - format!( - "{config_path} Agent interaction 流式协议不可用且普通请求回退失败:流式错误:{error};普通请求错误:{fallback_error}" - ) - })? - } - Err(error) => { - return Err(format!( - "{config_path} Agent interaction 调用 LLM 失败:{error}" - )); - } - } - } else { - registry - .invoke(&target, request) - .await - .map_err(|error| format!("{config_path} Agent interaction 调用 LLM 失败:{error}"))? - }; - let response = platform_llm::llm_response_from_provider_response(llm_provider, response) - .map_err(|error| format!("{config_path} Agent interaction Provider 响应无效:{error}"))?; - parse_agent_interaction_response(&response) -} - -struct AgentInteractionProviderStreamSink<'a, F> { - on_delta: &'a mut F, -} - -impl agent_runtime_core::ProviderStreamSink for AgentInteractionProviderStreamSink<'_, F> -where - F: FnMut(&platform_llm::LlmStreamDelta), -{ - fn emit( - &mut self, - event: agent_runtime_core::ProviderStreamEvent, - ) -> Result<(), agent_runtime_core::ProviderError> { - if let agent_runtime_core::ProviderStreamEvent::TextDelta { - accumulated_text, - delta_text, - finish_reason, - } = event - { - (self.on_delta)(&platform_llm::LlmStreamDelta { - accumulated_text, - delta_text, - accumulated_reasoning: String::new(), - reasoning_delta: String::new(), - finish_reason, - }); - } - Ok(()) - } -} - -fn parse_agent_interaction_response( - response: &platform_llm::LlmRunResponse, -) -> Result { - if response.tool_calls.len() > 1 { - return Err("Agent interaction 一轮最多只能选择一个宿主工具".to_string()); - } - if let Some(call) = response.tool_calls.first() { - let registry = agent_interaction_tool_registry()?; - let definition = registry - .get_by_function_name(&call.name) - .ok_or_else(|| format!("Agent interaction 返回未知工具:{}", call.name))?; - return match definition.dispatch() { - AgentInteractionToolKind::Execute => { - let arguments = serde_json::from_str::>( - call.arguments.as_str(), - ) - .map_err(|error| format!("runtime_execute 参数无效:{error}"))?; - if !arguments.is_empty() { - return Err("runtime_execute 不接受参数".to_string()); - } - Ok(AgentInteractionAction::Execute) - } - AgentInteractionToolKind::Resume => { - validate_empty_interaction_tool_arguments(call)?; - Ok(AgentInteractionAction::Resume) - } - AgentInteractionToolKind::ProjectLocation => { - validate_empty_interaction_tool_arguments(call)?; - Ok(AgentInteractionAction::ProjectLocation) - } - }; - } - - let text = strip_llm_thinking_blocks(response.text.as_str()); - if let Some(payload) = extract_json_payload(text.as_str()) { - if let Ok(envelope) = serde_json::from_str::(payload) { - return match envelope { - AgentInteractionTextEnvelope::Reply { reply } => { - let reply = reply.trim(); - if reply.is_empty() { - Err("Agent interaction.reply 不能为空".to_string()) - } else { - Ok(AgentInteractionAction::Reply(reply.to_string())) - } - } - AgentInteractionTextEnvelope::Execute => Ok(AgentInteractionAction::Execute), - AgentInteractionTextEnvelope::Resume => Ok(AgentInteractionAction::Resume), - AgentInteractionTextEnvelope::ProjectLocation => { - Ok(AgentInteractionAction::ProjectLocation) - } - }; - } - } - if text.is_empty() { - return Err("Agent interaction 未返回回复或工具调用".to_string()); - } - Ok(AgentInteractionAction::Reply(text)) -} - -fn validate_empty_interaction_tool_arguments( - call: &platform_llm::LlmToolCall, -) -> Result<(), String> { - let arguments = - serde_json::from_str::>(&call.arguments) - .map_err(|error| format!("{} 参数无效:{error}", call.name))?; - if !arguments.is_empty() { - return Err(format!("{} 不接受参数", call.name)); - } - Ok(()) -} - #[cfg(test)] mod tests { use super::*; @@ -523,75 +198,6 @@ mod tests { } } - #[test] - fn interaction_kernel_is_limited_to_supervisor_and_department_directors() { - for agent_id in [ - GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - "design-director", - "balance-director", - "art-director", - "audio-director", - "code-director", - "publish-strategy", - ] { - assert!( - game_creator_agent_uses_interaction_kernel(agent_id), - "{agent_id}" - ); - } - for agent_id in ["design-foundation", "art-asset-plan", "code-prototype"] { - assert!( - !game_creator_agent_uses_interaction_kernel(agent_id), - "{agent_id}" - ); - } - } - - #[test] - fn interaction_response_uses_natural_text_as_direct_reply() { - assert_eq!( - parse_agent_interaction_response(&response("我是项目总控。", Vec::new())).unwrap(), - AgentInteractionAction::Reply("我是项目总控。".to_string()) - ); - } - - #[test] - fn interaction_response_dispatches_registered_execute_tool() { - assert_eq!( - parse_agent_interaction_response(&response( - "", - vec![tool_call(AGENT_INTERACTION_EXECUTE_TOOL, "{}",)], - )) - .unwrap(), - AgentInteractionAction::Execute - ); - } - - #[test] - fn interaction_response_supports_text_protocol_for_non_native_provider() { - assert_eq!( - parse_agent_interaction_response(&response( - r#"{"action":"project_location"}"#, - Vec::new(), - )) - .unwrap(), - AgentInteractionAction::ProjectLocation - ); - } - - #[test] - fn interaction_response_rejects_multiple_host_actions() { - let error = parse_agent_interaction_response(&response( - "", - vec![ - tool_call(AGENT_INTERACTION_RESUME_TOOL, "{}"), - tool_call(AGENT_INTERACTION_PROJECT_LOCATION_TOOL, "{}"), - ], - )) - .expect_err("multiple actions must fail closed"); - assert!(error.contains("最多只能选择一个")); - } - #[test] fn steer_decision_replies_without_interrupting_status_questions() { let decision = parse_agent_runtime_steer_decision_response(&response( @@ -625,100 +231,4 @@ mod tests { assert!(decision.interrupt_current_provider); assert!(decision.reply.contains("改成回合制")); } - - #[test] - fn interaction_registry_derives_unique_strict_function_tools() { - let registry = agent_interaction_tool_registry().expect("interaction registry"); - let tools = agent_interaction_function_tools().expect("interaction tools"); - assert_eq!(tools.len(), registry.len()); - let names = tools - .iter() - .map(|tool| tool.name.as_str()) - .collect::>(); - assert_eq!(names.len(), tools.len()); - assert!(tools.iter().all(|tool| tool.strict)); - assert!(tools.iter().all(|tool| { - tool.parameters["additionalProperties"] == serde_json::json!(false) - && tool.parameters["properties"] == serde_json::json!({}) - })); - } - - #[test] - fn interaction_request_crosses_neutral_provider_contract_without_shape_drift() { - let mut llm = GameCreatorLlmConfig::default(); - llm.api_kind = "openai_responses".to_string(); - llm.reasoning_effort = "max".to_string(); - let request = build_agent_interaction_llm_request( - GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - "制作一个三消经营游戏", - "", - &llm, - ) - .expect("build interaction request"); - let request = platform_llm::provider_request_from_llm_request("agent-interaction", request) - .expect("neutral request"); - assert_eq!( - request.max_output_tokens(), - Some(AGENT_INTERACTION_MAX_OUTPUT_TOKENS) - ); - assert_eq!(request.tools().len(), 3); - assert_eq!( - request.reasoning_effort(), - Some(agent_runtime_core::ProviderReasoningEffort::Max) - ); - assert!(request.tools().iter().all(|tool| tool.strict())); - assert_eq!( - request.tool_choice(), - &agent_runtime_core::ProviderToolChoice::Auto - ); - } - - #[test] - fn interaction_request_propagates_invalid_reasoning_effort() { - let mut llm = GameCreatorLlmConfig::default(); - llm.reasoning_effort = "maximum".to_string(); - let error = build_agent_interaction_llm_request( - GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - "制作一个三消经营游戏", - "", - &llm, - ) - .expect_err("invalid reasoning effort must fail before Provider request"); - assert!(error.contains("reasoning_effort")); - } - - #[test] - fn interaction_stream_sink_preserves_accumulation_delta_and_finish_reason() { - let mut observed = Vec::new(); - let mut callback = |delta: &platform_llm::LlmStreamDelta| observed.push(delta.clone()); - let mut sink = AgentInteractionProviderStreamSink { - on_delta: &mut callback, - }; - agent_runtime_core::ProviderStreamSink::emit( - &mut sink, - agent_runtime_core::ProviderStreamEvent::TextDelta { - accumulated_text: "完成".to_string(), - delta_text: "成".to_string(), - finish_reason: Some("stop".to_string()), - }, - ) - .expect("emit"); - assert_eq!(observed.len(), 1); - assert_eq!(observed[0].accumulated_text, "完成"); - assert_eq!(observed[0].delta_text, "成"); - assert_eq!(observed[0].finish_reason.as_deref(), Some("stop")); - } - - #[test] - fn interaction_response_rejects_arguments_for_host_selected_action() { - let error = parse_agent_interaction_response(&response( - "", - vec![tool_call( - AGENT_INTERACTION_PROJECT_LOCATION_TOOL, - r#"{"path":"/tmp"}"#, - )], - )) - .expect_err("host facts must not accept model-selected arguments"); - assert!(error.contains("不接受参数")); - } } diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol.rs index f35612757..e73fbaada 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol.rs @@ -85,7 +85,7 @@ pub(crate) use run_configuration::{ pub(crate) use steering::{ acquire_game_creator_agent_runtime_steer_project_write_lock_with_wait, append_game_creator_agent_runtime_steer_decision_failure_reply_at, - consume_game_creator_agent_runtime_steers, game_creator_agent_runtime_accepts_steer, + consume_game_creator_agent_runtime_steers, game_creator_agent_runtime_provider_request_count_for_roots, game_creator_agent_runtime_steer_ledger_path, interrupt_game_creator_agent_runtime_provider_for_decided_steer_at, diff --git a/apps/ai-game-creator-shell/src-tauri/src/cli.rs b/apps/ai-game-creator-shell/src-tauri/src/cli.rs index a38a27d76..75a11a58c 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/cli.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/cli.rs @@ -24,13 +24,6 @@ pub(crate) enum CliCommand { task: String, initialize: bool, }, - SwarmChat { - project_path: PathBuf, - parent_agent_id: String, - initialize: bool, - run_profile: String, - supervisor_source: &'static str, - }, AgentEnqueue { project_path: PathBuf, agent_id: String, @@ -136,7 +129,6 @@ impl CliCommand { matches!( self, Self::AgentTask { .. } - | Self::SwarmChat { .. } | Self::AgentEnqueue { .. } | Self::AgentContextCompact { .. } | Self::AgentConfirm { .. } @@ -175,11 +167,6 @@ impl CliCommand { initialize, .. } - | Self::SwarmChat { - project_path, - initialize, - .. - } | Self::AgentEnqueue { project_path, initialize, @@ -725,49 +712,6 @@ pub(crate) fn parse_cli_command(args: &[String]) -> Result, S prompt: prompt.trim().to_string(), })); } - if args.first().map(String::as_str) == Some("--swarm-chat") { - const USAGE: &str = "用法:--swarm-chat [--init] [--autonomous-game-build] <本地项目绝对路径> [parentAgentId]"; - let mut rest = args[1..].to_vec(); - let initialize = if let Some(index) = rest.iter().position(|arg| arg == "--init") { - rest.remove(index); - true - } else { - false - }; - let autonomous_game_build = match rest - .iter() - .filter(|arg| arg.as_str() == "--autonomous-game-build") - .count() - { - 0 => false, - 1 => { - let index = rest - .iter() - .position(|arg| arg == "--autonomous-game-build") - .expect("counted autonomous game build flag"); - rest.remove(index); - true - } - _ => return Err(USAGE.to_string()), - }; - if !(1..=2).contains(&rest.len()) || rest.iter().any(|value| value.trim().is_empty()) { - return Err(USAGE.to_string()); - } - return Ok(Some(CliCommand::SwarmChat { - project_path: PathBuf::from(&rest[0]), - parent_agent_id: rest - .get(1) - .map(|value| value.trim().to_string()) - .unwrap_or_else(|| GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string()), - initialize, - run_profile: if autonomous_game_build { - AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD.to_string() - } else { - AGENT_RUNTIME_RUN_PROFILE_STANDARD.to_string() - }, - supervisor_source: AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE, - })); - } if args.first().map(String::as_str) == Some("--agent-task") { let mut rest = args[1..].to_vec(); let initialize = if let Some(index) = rest.iter().position(|arg| arg == "--init") { @@ -1057,7 +1001,7 @@ pub(crate) fn run_cli_command(command: CliCommand) -> Result<(), String> { } else if terminal.status == "waiting-for-confirmation" { Err("单 Agent 任务正在等待开发者确认,请在开发窗口继续".to_string()) } else if terminal.status == "waiting-for-user-input" { - Err("单 Agent 任务正在等待用户回答,请使用 agc:chat 继续".to_string()) + Err("单 Agent 任务正在等待用户回答,请在开发窗口继续".to_string()) } else { Err(format!( "单 Agent 任务未完成:{} / {}", @@ -1065,23 +1009,6 @@ pub(crate) fn run_cli_command(command: CliCommand) -> Result<(), String> { )) } } - CliCommand::SwarmChat { - project_path, - parent_agent_id, - initialize, - run_profile, - supervisor_source, - } => { - let project_path = canonicalize_cli_path(&project_path, "本地项目路径", initialize)?; - require_external_agent_runner_for_cli_runtime_write(&project_path)?; - initialize_cli_agent_project(&project_path, initialize)?; - run_game_creator_swarm_chat_at( - &project_path, - &parent_agent_id, - &run_profile, - supervisor_source, - ) - } CliCommand::AgentEnqueue { project_path, agent_id, @@ -1954,34 +1881,6 @@ mod tests { assert!(read_cli_agent_goal_payload(&mut oversized).is_err()); } - #[test] - fn parses_swarm_chat_and_requires_external_config_dir() { - let project_path = std::env::current_dir().expect("current directory"); - let mut command = parse_cli_command(&[ - "--swarm-chat".to_string(), - "--init".to_string(), - project_path.display().to_string(), - "code-prototype".to_string(), - ]) - .expect("parse swarm chat") - .expect("swarm chat command"); - - assert_eq!( - command, - CliCommand::SwarmChat { - project_path, - parent_agent_id: "code-prototype".to_string(), - initialize: true, - run_profile: AGENT_RUNTIME_RUN_PROFILE_STANDARD.to_string(), - supervisor_source: AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE, - } - ); - assert!(command.requires_external_agent_runner()); - let error = prepare_cli_command_paths(&mut command, None) - .expect_err("swarm chat must require config dir"); - assert!(error.contains("--config-dir")); - } - #[test] fn parses_idle_runner_shutdown_without_starting_a_new_runner() { let mut command = parse_cli_command(&["--runner-shutdown-if-idle".to_string()]) @@ -2035,68 +1934,4 @@ mod tests { ]) .is_err()); } - - #[test] - fn swarm_chat_defaults_to_project_supervisor() { - let project_path = std::env::current_dir().expect("current directory"); - let command = parse_cli_command(&[ - "--swarm-chat".to_string(), - project_path.display().to_string(), - ]) - .expect("parse supervisor chat") - .expect("supervisor chat command"); - - assert_eq!( - command, - CliCommand::SwarmChat { - project_path, - parent_agent_id: GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string(), - initialize: false, - run_profile: AGENT_RUNTIME_RUN_PROFILE_STANDARD.to_string(), - supervisor_source: AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE, - } - ); - } - - #[test] - fn swarm_chat_autonomous_game_build_flag_selects_autonomous_profile() { - let project_path = std::env::current_dir().expect("current directory"); - let command = parse_cli_command(&[ - "--swarm-chat".to_string(), - project_path.display().to_string(), - "--autonomous-game-build".to_string(), - ]) - .expect("parse autonomous supervisor chat") - .expect("autonomous supervisor chat command"); - - assert_eq!( - command, - CliCommand::SwarmChat { - project_path, - parent_agent_id: GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string(), - initialize: false, - run_profile: AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD.to_string(), - supervisor_source: AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE, - } - ); - } - - #[test] - fn swarm_chat_rejects_missing_or_extra_arguments() { - assert!(parse_cli_command(&["--swarm-chat".to_string()]).is_err()); - assert!(parse_cli_command(&[ - "--swarm-chat".to_string(), - "/tmp/game-project".to_string(), - "code-prototype".to_string(), - "extra".to_string(), - ]) - .is_err()); - assert!(parse_cli_command(&[ - "--swarm-chat".to_string(), - "--autonomous-game-build".to_string(), - "--autonomous-game-build".to_string(), - "/tmp/game-project".to_string(), - ]) - .is_err()); - } } diff --git a/apps/ai-game-creator-shell/src-tauri/src/commands.rs b/apps/ai-game-creator-shell/src-tauri/src/commands.rs index fa4163a4d..4c60b2b7c 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/commands.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/commands.rs @@ -1345,6 +1345,9 @@ pub(crate) async fn chat_with_game_creator_role_agent_stream( } #[tauri::command] +#[allow(dead_code)] +// Tauri IPC 入口:前端暂无调用方,内部实现 `*_at` 仍被 `--agent-steer`、goal 与测试使用。 +// 保留注册以维持既有 App IPC 表面;重接前端入口或删除属于单独的 native 能力取舍。 pub(crate) fn start_game_creator_agent_runtime_task( project_path: String, agent_id: String, @@ -1547,6 +1550,9 @@ pub(crate) fn clear_game_creator_agent_goal( } #[tauri::command] +#[allow(dead_code)] +// Tauri IPC 入口:前端暂无调用方,内部实现 `*_at` 仍被 `--agent-steer`、goal 与测试使用。 +// 保留注册以维持既有 App IPC 表面;重接前端入口或删除属于单独的 native 能力取舍。 pub(crate) async fn steer_game_creator_agent_runtime_task( project_path: String, agent_id: String, @@ -5301,16 +5307,6 @@ pub(crate) fn open_canvas_project( Ok(OpenCanvasProjectResult { url }) } -#[tauri::command] -pub(crate) fn get_game_creation_agent_capabilities() -> Vec { - GAME_CREATION_AGENT_CAPABILITIES.to_vec() -} - -#[tauri::command] -pub(crate) fn get_limited_local_commands() -> Vec { - GAME_CREATION_APP_LIMITED_RUN_COMMANDS.to_vec() -} - #[tauri::command] pub(crate) fn run_limited_local_command( project_path: String, diff --git a/apps/ai-game-creator-shell/src-tauri/src/main.rs b/apps/ai-game-creator-shell/src-tauri/src/main.rs index 151a18cfe..f151f543d 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/main.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/main.rs @@ -36,20 +36,18 @@ use shared_contracts::game_creation_app::{ game_creation_app_asset_effective_category, new_game_creation_app_manifest, new_game_creation_app_seed_tasks, normalize_game_creation_app_asset_tags, validate_game_iteration_versions, GameCreationAgentArtifactTrace, - GameCreationAgentCapabilityDescriptor, GameCreationAgentPassPlanTrace, - GameCreationAgentRepairRouteTrace, GameCreationAgentRunStep, + GameCreationAgentPassPlanTrace, GameCreationAgentRepairRouteTrace, GameCreationAgentRunStep, GameCreationAgentRunTaskGraphTrace, GameCreationAgentRunTrace, GameCreationAgentToolCallTrace, GameCreationAppAgentGroup, GameCreationAppAssetKind, GameCreationAppAssetManifestEntry, GameCreationAppAssetSource, GameCreationAppAssetSourceKind, GameCreationAppCommandRunState, - GameCreationAppCommandRunStatus, GameCreationAppLimitedRunCommandDescriptor, - GameCreationAppManifest, GameCreationAppPermission, GameCreationAppPreviewState, - GameCreationAppPreviewStatus, GameCreationAppTaskState, GameCreationAppTaskStatus, - GameIterationVersion, GameIterationVersionCreatedReason, GameIterationVersionResourceBinding, - ProjectResourceCanvasLayout, ProjectResourceCanvasLayoutMode, ProjectResourceCanvasPosition, + GameCreationAppCommandRunStatus, 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, + GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, GAME_CREATION_AGENT_TOOL_CALL_MAX, + GAME_CREATION_APP_COMMANDS, GAME_CREATION_RESOURCE_LAYOUT_SCHEMA_VERSION, }; // `Emitter` 同时被 `use super::*` 的子模块依赖(通知、Agent 事件等都从 crate 根取该 trait), // 不要因为根模块自身不再直接 `.emit(..)` 就删掉它。 @@ -151,7 +149,6 @@ mod repository_context; mod resource_inspect; mod resource_preview_scheduler; mod runner; -mod swarm_cli; mod template_library; mod tool_plan_handoff; mod user_input; @@ -193,7 +190,6 @@ use repository_context::*; use resource_inspect::*; use resource_preview_scheduler::*; use runner::*; -use swarm_cli::*; use template_library::*; use user_input::*; use windows::*; @@ -2682,8 +2678,6 @@ fn main() { start_local_project_asset_generation, list_local_project_asset_generations, open_canvas_project, - get_game_creation_agent_capabilities, - get_limited_local_commands, run_limited_local_command, append_local_permission_log, list_local_project_files, diff --git a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli.rs b/apps/ai-game-creator-shell/src-tauri/src/swarm_cli.rs deleted file mode 100644 index 9bbdae1bb..000000000 --- a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli.rs +++ /dev/null @@ -1,72 +0,0 @@ -use super::*; -use std::collections::{BTreeMap, BTreeSet}; -use std::io::{BufRead, Write}; -use std::sync::mpsc::{self, Receiver, RecvTimeoutError}; -use std::time::{Duration, Instant}; - -mod commands; -mod conversation; -mod goal_commands; -mod input; -mod observer; -mod report; -mod terminal_classification; -mod turn_dispatch; -mod turn_wait; - -use commands::*; -use conversation::*; -use goal_commands::*; -use input::*; -use observer::*; -use report::*; -use terminal_classification::*; -use turn_dispatch::*; -use turn_wait::*; - -#[cfg(test)] -mod tests; - -pub(crate) fn run_game_creator_swarm_chat_at( - root: &Path, - parent_agent_id: &str, - run_profile: &str, - supervisor_source: &'static str, -) -> Result<(), String> { - let (input_tx, input_rx) = mpsc::channel(); - std::thread::spawn(move || { - let stdin = std::io::stdin(); - let mut input = stdin.lock(); - loop { - let mut line = String::new(); - match input.read_line(&mut line) { - Ok(0) => { - let _ = input_tx.send(SwarmInputEvent::Eof); - break; - } - Ok(_) => { - if input_tx - .send(SwarmInputEvent::Line(line.trim().to_string())) - .is_err() - { - break; - } - } - Err(error) => { - let _ = input_tx.send(SwarmInputEvent::Error(error.to_string())); - break; - } - } - } - }); - let stdout = std::io::stdout(); - let mut output = stdout.lock(); - run_game_creator_swarm_chat_with_input( - root, - parent_agent_id, - run_profile, - supervisor_source, - &input_rx, - &mut output, - ) -} diff --git a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/commands.rs b/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/commands.rs deleted file mode 100644 index 4593c8f11..000000000 --- a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/commands.rs +++ /dev/null @@ -1,79 +0,0 @@ -use super::*; - -pub(super) fn print_swarm_agents(root: &Path, output: &mut W) -> Result<(), String> { - writeln!(output, "静态 Agent:").map_err(|error| format!("写入终端失败:{error}"))?; - for group in GAME_CREATOR_AGENT_GROUP_DEFINITIONS { - for role in group.roles { - writeln!( - output, - "- {} / {}: {} ({})", - group.label, role.role, role.task_id, role.id - ) - .map_err(|error| format!("写入终端失败:{error}"))?; - } - } - let dynamic = read_game_creator_agent_runtimes_at(root)? - .into_iter() - .filter(|runtime| runtime.state.agent_id.starts_with("child-")) - .collect::>(); - if !dynamic.is_empty() { - writeln!(output, "动态隔离 Agent:").map_err(|error| format!("写入终端失败:{error}"))?; - for runtime in dynamic { - writeln!( - output, - "- {} <- {} run={} delegation={} status={}/{}", - runtime.state.agent_id, - runtime - .state - .parent_agent_id - .as_deref() - .unwrap_or("unknown"), - runtime.state.run_id, - runtime.state.delegation_id.as_deref().unwrap_or("unknown"), - runtime.state.status, - runtime.state.phase - ) - .map_err(|error| format!("写入终端失败:{error}"))?; - } - } - Ok(()) -} - -pub(super) fn print_swarm_status(root: &Path, output: &mut W) -> Result<(), String> { - let runtimes = read_game_creator_agent_runtimes_at(root)?; - for runtime in runtimes.iter().filter(|runtime| { - !runtime.state.run_id.is_empty() - || runtime.task_queue.pending > 0 - || runtime.task_queue.running > 0 - || runtime.task_queue.waiting_for_confirmation > 0 - || runtime.task_queue.waiting_for_user_input > 0 - }) { - print_runtime_state(&runtime.state, &runtime.task_queue, output)?; - print_runtime_response_stream_status(runtime.response_stream.as_ref(), output)?; - } - if runtimes - .iter() - .all(|runtime| runtime.state.run_id.is_empty()) - { - writeln!(output, "当前没有 Agent Runtime 记录。") - .map_err(|error| format!("写入终端失败:{error}"))?; - } - Ok(()) -} - -pub(super) fn print_runtime_response_stream_status( - stream: Option<&AgentRuntimeResponseStream>, - output: &mut W, -) -> Result<(), String> { - let Some(stream) = stream else { - return Ok(()); - }; - writeln!( - output, - "[回复流] status={} sequence={} chars={}", - stream.status, - stream.sequence, - stream.accumulated_text.chars().count() - ) - .map_err(|error| format!("写入终端失败:{error}")) -} diff --git a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/conversation.rs b/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/conversation.rs deleted file mode 100644 index a2afff4c8..000000000 --- a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/conversation.rs +++ /dev/null @@ -1,337 +0,0 @@ -use super::*; - -pub(super) const SWARM_CHAT_HISTORY_LIMIT: usize = 50; - -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] -pub(super) struct SwarmTurnConversationMetrics { - pub(super) new_assistant_message_count: usize, - pub(super) final_reply_chars: usize, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub(super) struct SwarmRecoveredAssistant { - pub(super) run_id: String, - pub(super) finalization_id: String, - pub(super) message_id: String, - pub(super) content: String, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub(super) struct SwarmTurnConversationBaseline { - pub(super) previous_message_count: usize, - pub(super) parent_run_id: String, - pub(super) recovered_assistant: Option, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub(super) struct SwarmTurnConversationSnapshot { - pub(super) metrics: SwarmTurnConversationMetrics, - pub(super) final_reply: Option, - pub(super) recovered_before_observation: bool, -} - -pub(super) fn handle_swarm_context_compaction( - root: &Path, - parent_agent_id: &str, - output: &mut W, -) -> Result<(), String> { - let conversation = read_local_conversation_for_session_at(root, Some(parent_agent_id), None)?; - match compact_external_agent_runner_context( - root, - parent_agent_id, - conversation.session_id.as_deref(), - ) { - Ok(result) => writeln!( - output, - "[上下文压缩] revision={} reused={} estimated={}->{} covered={}/{}/{}", - result.revision, - result.reused, - result.estimated_tokens_before, - result.estimated_tokens_after, - result.covered_agent_messages, - result.covered_project_messages, - result.covered_observations, - ), - Err(error) => writeln!(output, "[上下文压缩失败] {error}"), - } - .map_err(|error| format!("写入终端失败:{error}")) -} - -pub(super) fn print_conversation_history( - root: &Path, - parent_agent_id: &str, - output: &mut W, -) -> Result<(), String> { - let conversation = read_local_conversation_for_session_at(root, Some(parent_agent_id), None)?; - writeln!( - output, - "[历史] session={} messages={}", - conversation.session_id.as_deref().unwrap_or("unknown"), - conversation.messages.len() - ) - .map_err(|error| format!("写入终端失败:{error}"))?; - let start = conversation - .messages - .len() - .saturating_sub(SWARM_CHAT_HISTORY_LIMIT); - for message in &conversation.messages[start..] { - let label = if message.role == "assistant" { - "Agent" - } else if message.role == "user" { - "你" - } else { - message.role.as_str() - }; - writeln!(output, "{label}> {}", message.content) - .map_err(|error| format!("写入终端失败:{error}"))?; - } - Ok(()) -} - -pub(super) fn new_swarm_turn_conversation_baseline( - previous_message_count: usize, - parent_run_id: impl Into, -) -> SwarmTurnConversationBaseline { - SwarmTurnConversationBaseline { - previous_message_count, - parent_run_id: parent_run_id.into(), - recovered_assistant: None, - } -} - -pub(super) fn capture_recovered_swarm_assistant_at( - root: &Path, - parent_agent_id: &str, - session_id: &str, - baseline: &mut SwarmTurnConversationBaseline, -) -> Result<(), String> { - if baseline.parent_run_id.trim().is_empty() || baseline.recovered_assistant.is_some() { - return Ok(()); - } - let conversation = - read_local_conversation_for_session_at(root, Some(parent_agent_id), Some(session_id))?; - if baseline.previous_message_count > conversation.messages.len() { - return Err("Swarm turn 对话 baseline 超出当前 Session 消息数".to_string()); - } - if baseline.previous_message_count < conversation.messages.len() { - return Ok(()); - } - let Some(journal) = read_game_creator_agent_runtime_finalization_journal( - root, - parent_agent_id, - &baseline.parent_run_id, - )? - else { - return Ok(()); - }; - if journal.agent_id != parent_agent_id - || journal.session_id != session_id - || journal.run_id != baseline.parent_run_id - { - return Err("Swarm 恢复 finalization 与目标 parent Session/run 不匹配".to_string()); - } - if !game_creator_agent_runtime_finalization_assistant_exists(root, &journal)? { - return Ok(()); - } - baseline.recovered_assistant = Some(SwarmRecoveredAssistant { - run_id: journal.run_id, - finalization_id: journal.finalization_id, - message_id: journal.message_id, - content: journal.response, - }); - Ok(()) -} - -pub(super) fn read_turn_conversation_snapshot( - root: &Path, - parent_agent_id: &str, - session_id: &str, - baseline: &SwarmTurnConversationBaseline, -) -> Result { - let conversation = - read_local_conversation_for_session_at(root, Some(parent_agent_id), Some(session_id))?; - if baseline.previous_message_count > conversation.messages.len() { - return Err("Swarm turn 对话 baseline 超出当前 Session 消息数".to_string()); - } - let target_message_id = game_creator_agent_runtime_finalization_message_id( - parent_agent_id, - session_id, - &baseline.parent_run_id, - ); - if let Some(journal) = read_game_creator_agent_runtime_finalization_journal( - root, - parent_agent_id, - &baseline.parent_run_id, - )? { - if journal.agent_id != parent_agent_id - || journal.session_id != session_id - || journal.run_id != baseline.parent_run_id - || journal.message_id != target_message_id - { - return Err("Swarm finalization 与目标 parent Session/run 不匹配".to_string()); - } - } - let (mut metrics, final_reply) = summarize_scoped_new_assistant_messages( - conversation - .messages - .iter() - .skip(baseline.previous_message_count) - .map(|message| { - ( - message.role.as_str(), - message.content.as_str(), - message.message_id.as_deref(), - ) - }), - Some(&target_message_id), - ); - let mut final_reply = final_reply.map(str::to_string); - let recovered_before_observation = baseline.recovered_assistant.is_some(); - if let Some(recovered) = baseline.recovered_assistant.as_ref() { - if recovered.run_id != baseline.parent_run_id - || recovered.finalization_id.trim().is_empty() - || recovered.message_id.trim().is_empty() - { - return Err("Swarm 恢复 assistant 身份不完整".to_string()); - } - if metrics.new_assistant_message_count != 0 { - return Err("Swarm 恢复 assistant 与 baseline 后的新回复重叠".to_string()); - } - metrics.new_assistant_message_count = metrics.new_assistant_message_count.saturating_add(1); - if final_reply.is_none() { - metrics.final_reply_chars = recovered.content.chars().count(); - final_reply = Some(recovered.content.clone()); - } - } - Ok(SwarmTurnConversationSnapshot { - metrics, - final_reply, - recovered_before_observation, - }) -} - -pub(super) fn read_turn_conversation_metrics( - root: &Path, - parent_agent_id: &str, - session_id: &str, - baseline: &SwarmTurnConversationBaseline, -) -> Result { - read_turn_conversation_snapshot(root, parent_agent_id, session_id, baseline) - .map(|snapshot| snapshot.metrics) -} - -pub(super) fn summarize_new_assistant_messages<'a>( - messages: impl IntoIterator, -) -> (SwarmTurnConversationMetrics, Option<&'a str>) { - summarize_scoped_new_assistant_messages( - messages - .into_iter() - .map(|(role, content)| (role, content, None)), - None, - ) -} - -pub(super) fn summarize_scoped_new_assistant_messages<'a>( - messages: impl IntoIterator)>, - target_message_id: Option<&str>, -) -> (SwarmTurnConversationMetrics, Option<&'a str>) { - let mut scoped_count = 0; - let mut scoped_reply = None; - let mut legacy_count = 0; - let mut legacy_reply = None; - let mut identified_assistant_exists = false; - for (role, content, message_id) in messages { - if role != "assistant" { - continue; - } - match message_id { - Some(message_id) if target_message_id == Some(message_id) => { - identified_assistant_exists = true; - scoped_count += 1; - scoped_reply = Some(content); - } - None => { - legacy_count += 1; - legacy_reply = Some(content); - } - Some(_) => identified_assistant_exists = true, - } - } - let (new_assistant_message_count, final_reply) = - if target_message_id.is_some() && (scoped_count > 0 || identified_assistant_exists) { - (scoped_count, scoped_reply) - } else { - (legacy_count, legacy_reply) - }; - ( - SwarmTurnConversationMetrics { - new_assistant_message_count, - final_reply_chars: final_reply.map_or(0, |reply| reply.chars().count()), - }, - final_reply, - ) -} - -pub(super) fn print_new_parent_reply( - root: &Path, - parent_agent_id: &str, - session_id: &str, - baseline: &SwarmTurnConversationBaseline, - output: &mut W, - observer: &mut SwarmRuntimeObserver, -) -> Result { - let snapshot = read_turn_conversation_snapshot(root, parent_agent_id, session_id, baseline)?; - observer.close_response_line(output)?; - if snapshot.recovered_before_observation { - writeln!(output, "[本轮结束] 父 Agent 回复已在恢复前持久化。") - .map_err(|error| format!("写入终端失败:{error}"))?; - } else { - print_settled_parent_reply_for_run( - parent_agent_id, - session_id, - Some(&baseline.parent_run_id), - snapshot.final_reply.as_deref(), - observer, - output, - )?; - } - Ok(snapshot.metrics) -} - -pub(super) fn print_settled_parent_reply( - parent_agent_id: &str, - session_id: &str, - reply: Option<&str>, - observer: &SwarmRuntimeObserver, - output: &mut W, -) -> Result<(), String> { - print_settled_parent_reply_for_run(parent_agent_id, session_id, None, reply, observer, output) -} - -pub(super) fn print_settled_parent_reply_for_run( - parent_agent_id: &str, - session_id: &str, - target_run_id: Option<&str>, - reply: Option<&str>, - observer: &SwarmRuntimeObserver, - output: &mut W, -) -> Result<(), String> { - let Some(reply) = reply else { - return writeln!(output, "[本轮结束] 父 Agent 未产生新的最终回复。") - .map_err(|error| format!("写入终端失败:{error}")); - }; - let stream_belongs_to_target = target_run_id.is_none_or(|target_run_id| { - observer - .response_streams - .get(parent_agent_id) - .is_some_and(|cursor| cursor.identity.run_id == target_run_id) - }); - if stream_belongs_to_target - && observer.parent_reply_was_fully_streamed(parent_agent_id, session_id, reply) - { - writeln!(output, "[本轮结束] 父 Agent 回复已完整流式输出。") - .map_err(|error| format!("写入终端失败:{error}")) - } else { - writeln!(output, "\nAgent> {reply}").map_err(|error| format!("写入终端失败:{error}")) - } -} diff --git a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/goal_commands.rs b/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/goal_commands.rs deleted file mode 100644 index 4f3e6c5ce..000000000 --- a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/goal_commands.rs +++ /dev/null @@ -1,189 +0,0 @@ -use super::*; - -#[derive(Debug, Eq, PartialEq)] -pub(super) enum SwarmGoalCommand { - Status, - Start(String), - Edit(String), - Pause, - Resume, - Clear, -} - -#[derive(Debug, Eq, PartialEq)] -pub(super) struct SwarmGoalObservation { - pub(super) session_id: String, - pub(super) run_id: String, - pub(super) previous_message_count: usize, -} - -pub(super) fn handle_swarm_goal_command( - root: &Path, - parent_agent_id: &str, - command: SwarmGoalCommand, - output: &mut W, -) -> Result, String> { - match execute_swarm_goal_command(root, parent_agent_id, command, output) { - Ok(observation) => Ok(observation), - Err(error) => { - print_swarm_goal_error(output, &error)?; - Ok(None) - } - } -} - -pub(super) fn execute_swarm_goal_command( - root: &Path, - parent_agent_id: &str, - command: SwarmGoalCommand, - output: &mut W, -) -> Result, String> { - let conversation = read_local_conversation_for_session_at(root, Some(parent_agent_id), None)?; - let session_id = conversation - .session_id - .clone() - .ok_or_else(|| "父 Agent 当前 Session 缺失".to_string())?; - let previous_message_count = conversation.messages.len(); - let current_goal = read_game_creator_agent_goal_at(root, parent_agent_id, &session_id)?; - let project_path = root.display().to_string(); - - match command { - SwarmGoalCommand::Status => { - print_swarm_goal_status(&session_id, current_goal.as_ref(), output)?; - Ok(None) - } - SwarmGoalCommand::Start(outcome) => { - let requested_run_id = format!("swarm-goal-{parent_agent_id}-{}", unix_millis()); - let result = start_game_creator_agent_goal( - project_path, - parent_agent_id.to_string(), - Some(session_id.clone()), - outcome.clone(), - Vec::new(), - vec![outcome], - requested_run_id, - )?; - print_swarm_goal_mutation("已启动", &result, output)?; - Ok(Some(SwarmGoalObservation { - session_id, - run_id: result.goal.run_id.clone(), - previous_message_count, - })) - } - SwarmGoalCommand::Edit(outcome) => { - let goal = current_goal.ok_or_else(|| "当前 Session 没有 Agent Goal".to_string())?; - let result = edit_game_creator_agent_goal( - project_path, - parent_agent_id.to_string(), - session_id.clone(), - goal.goal_id, - goal.revision, - outcome.clone(), - Vec::new(), - vec![outcome], - )?; - print_swarm_goal_mutation("已编辑", &result, output)?; - Ok( - (result.goal.status == AGENT_GOAL_STATUS_ACTIVE).then_some(SwarmGoalObservation { - session_id, - run_id: result.goal.run_id.clone(), - previous_message_count, - }), - ) - } - SwarmGoalCommand::Pause => { - let goal = current_goal.ok_or_else(|| "当前 Session 没有 Agent Goal".to_string())?; - let result = pause_game_creator_agent_goal( - project_path, - parent_agent_id.to_string(), - session_id, - goal.goal_id, - goal.revision, - )?; - print_swarm_goal_mutation("已暂停", &result, output)?; - Ok(None) - } - SwarmGoalCommand::Resume => { - let goal = current_goal.ok_or_else(|| "当前 Session 没有 Agent Goal".to_string())?; - let result = resume_game_creator_agent_goal( - project_path, - parent_agent_id.to_string(), - session_id.clone(), - goal.goal_id, - goal.revision, - )?; - print_swarm_goal_mutation("已恢复", &result, output)?; - Ok(Some(SwarmGoalObservation { - session_id, - run_id: result.goal.run_id.clone(), - previous_message_count, - })) - } - SwarmGoalCommand::Clear => { - let goal = current_goal.ok_or_else(|| "当前 Session 没有 Agent Goal".to_string())?; - let result = clear_game_creator_agent_goal( - project_path, - parent_agent_id.to_string(), - session_id, - goal.goal_id, - goal.revision, - )?; - print_swarm_goal_mutation("已清理", &result, output)?; - Ok(None) - } - } -} - -pub(super) fn print_swarm_goal_status( - session_id: &str, - goal: Option<&AgentGoalRecord>, - output: &mut W, -) -> Result<(), String> { - let Some(goal) = goal else { - return writeln!(output, "[Goal] session={session_id} 当前尚未设置持久目标。") - .map_err(|error| format!("写入终端失败:{error}")); - }; - writeln!( - output, - "[Goal] session={} goal={} run={} revision={} status={}", - goal.session_id, goal.goal_id, goal.run_id, goal.revision, goal.status - ) - .and_then(|_| writeln!(output, "[Goal 目标] {}", goal.outcome)) - .map_err(|error| format!("写入终端失败:{error}"))?; - for constraint in &goal.constraints { - writeln!(output, "[Goal 约束] {constraint}") - .map_err(|error| format!("写入终端失败:{error}"))?; - } - for verification in &goal.verification { - writeln!(output, "[Goal 完成标准] {verification}") - .map_err(|error| format!("写入终端失败:{error}"))?; - } - if let Some(error) = goal.error.as_deref() { - writeln!(output, "[Goal 错误] {error}") - .map_err(|write_error| format!("写入终端失败:{write_error}"))?; - } - Ok(()) -} - -pub(super) fn print_swarm_goal_mutation( - action: &str, - result: &AgentGoalMutationResult, - output: &mut W, -) -> Result<(), String> { - writeln!( - output, - "[Goal {action}] goal={} run={} revision={} status={} providerInterrupted={}", - result.goal.goal_id, - result.goal.run_id, - result.goal.revision, - result.goal.status, - result.provider_interrupted - ) - .map_err(|error| format!("写入终端失败:{error}"))?; - print_swarm_goal_status(&result.goal.session_id, Some(&result.goal), output) -} - -pub(super) fn print_swarm_goal_error(output: &mut W, error: &str) -> Result<(), String> { - writeln!(output, "[Goal 失败] {error}") - .map_err(|write_error| format!("写入终端失败:{write_error}")) -} diff --git a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/input.rs b/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/input.rs deleted file mode 100644 index fdcc3ba13..000000000 --- a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/input.rs +++ /dev/null @@ -1,390 +0,0 @@ -use super::*; - -#[derive(Debug, Eq, PartialEq)] -pub(super) enum SwarmChatInput { - Help, - Agents, - Status, - History, - Compact, - Goal(SwarmGoalCommand), - InvalidGoal(String), - Resume, - Quit, - Message(String), -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub(super) enum SwarmChatFlow { - Continue, - Exit, -} - -pub(super) enum SwarmInputEvent { - Line(String), - Eof, - Error(String), -} - -pub(super) enum SwarmPromptDecision { - Approve, - Reject, - Deferred, - InputClosed, - Quit, -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub(super) enum SwarmNewRunLaunch<'a> { - ProjectSupervisor { - source: &'static str, - run_profile: &'a str, - }, - ExplicitParentDebug, -} - -impl SwarmNewRunLaunch<'_> { - pub(super) fn expected_parent_source(self) -> Option<&'static str> { - match self { - Self::ProjectSupervisor { .. } | Self::ExplicitParentDebug => None, - } - } -} - -pub(super) fn resolve_swarm_new_run_launch<'a>( - parent_agent_id: &str, - run_profile: &'a str, - supervisor_source: &'static str, -) -> Result, String> { - if !matches!( - run_profile, - AGENT_RUNTIME_RUN_PROFILE_STANDARD | AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD - ) { - return Err(format!("不支持的 Agent Runtime Run Profile:{run_profile}")); - } - if parent_agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID { - if supervisor_source != AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE { - return Err(format!( - "不支持的 Project Supervisor source:{supervisor_source}" - )); - } - return Ok(SwarmNewRunLaunch::ProjectSupervisor { - source: supervisor_source, - run_profile, - }); - } - if supervisor_source != AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE { - return Err(format!( - "受限 Supervisor source 仅支持 project-supervisor 总控入口:{supervisor_source}" - )); - } - if run_profile != AGENT_RUNTIME_RUN_PROFILE_STANDARD { - return Err("--autonomous-game-build 仅支持 project-supervisor 总控入口".to_string()); - } - Ok(SwarmNewRunLaunch::ExplicitParentDebug) -} - -pub(super) fn run_game_creator_swarm_chat_with_input( - root: &Path, - parent_agent_id: &str, - run_profile: &str, - supervisor_source: &'static str, - input: &Receiver, - output: &mut W, -) -> Result<(), String> { - let parent_agent_id = parent_agent_id.trim(); - if parent_agent_id.is_empty() { - return Err("parentAgentId 不能为空".to_string()); - } - let new_run_launch = - resolve_swarm_new_run_launch(parent_agent_id, run_profile, supervisor_source)?; - let expected_parent_source = new_run_launch.expected_parent_source(); - enforce_project_permission_policy(root, "conversation.read")?; - enforce_project_permission_policy(root, "conversation.write")?; - enforce_project_permission_policy(root, "agent.run_status")?; - enforce_project_permission_policy(root, "agent.compact")?; - enforce_project_permission_policy(root, "agent.resume")?; - let _ = read_local_conversation_for_session_at(root, Some(parent_agent_id), None)?; - let existing_runtimes = read_game_creator_agent_runtimes_at(root)?; - - writeln!(output, "Agent Swarm Chat") - .and_then(|_| writeln!(output, "项目:{}", root.display())) - .and_then(|_| writeln!(output, "父 Agent:{parent_agent_id}")) - .and_then(|_| writeln!(output, "输入 /help 查看命令。")) - .map_err(|error| format!("写入终端失败:{error}"))?; - print_conversation_history(root, parent_agent_id, output)?; - - let active_conversation = - read_local_conversation_for_session_at(root, Some(parent_agent_id), None)?; - let active_session_id = active_conversation - .session_id - .as_deref() - .ok_or_else(|| "父 Agent 当前 Session 缺失".to_string())?; - let matching_parent_is_busy = swarm_parent_runtime( - parent_agent_id, - active_session_id, - run_profile, - expected_parent_source, - &existing_runtimes, - ) - .is_some_and(runtime_is_busy); - if matching_parent_is_busy { - writeln!( - output, - "[恢复扫描] 检测到未收束 Runtime;输入 /resume 继续观察,新消息会进入该 run 的 steer 队列。" - ) - .map_err(|error| format!("写入终端失败:{error}"))?; - } - - loop { - write!(output, "\n你> ").map_err(|error| format!("写入终端失败:{error}"))?; - output - .flush() - .map_err(|error| format!("刷新终端失败:{error}"))?; - let Some(line) = receive_swarm_chat_line(input)? else { - writeln!(output, "\n已退出 Agent Swarm Chat。") - .map_err(|error| format!("写入终端失败:{error}"))?; - return Ok(()); - }; - let Some(command) = parse_swarm_chat_input(&line) else { - continue; - }; - match command { - SwarmChatInput::Help => print_swarm_chat_help(output)?, - SwarmChatInput::Agents => print_swarm_agents(root, output)?, - SwarmChatInput::Status => print_swarm_status(root, output)?, - SwarmChatInput::History => print_conversation_history(root, parent_agent_id, output)?, - SwarmChatInput::Compact => { - handle_swarm_context_compaction(root, parent_agent_id, output)? - } - SwarmChatInput::Goal(command) => { - let mut observer = SwarmRuntimeObserver::seed(root)?; - let Some(observation) = - handle_swarm_goal_command(root, parent_agent_id, command, output)? - else { - continue; - }; - let conversation_baseline = new_swarm_turn_conversation_baseline( - observation.previous_message_count, - &observation.run_id, - ); - let outcome = wait_for_swarm_turn( - root, - parent_agent_id, - &observation.session_id, - run_profile, - expected_parent_source, - conversation_baseline, - input, - output, - &mut observer, - SWARM_CHAT_POLL_INTERVAL, - SWARM_CHAT_SETTLE_WINDOW, - )?; - if outcome == SwarmTurnOutcome::Quit { - return print_swarm_chat_exit(output); - } - print_turn_outcome(outcome, output)?; - } - SwarmChatInput::InvalidGoal(error) => print_swarm_goal_error(output, &error)?, - SwarmChatInput::Resume => { - let before = - read_local_conversation_for_session_at(root, Some(parent_agent_id), None)?; - let session_id = before - .session_id - .as_deref() - .ok_or_else(|| "父 Agent 当前 Session 缺失".to_string())?; - if handle_swarm_resume_turn( - root, - parent_agent_id, - session_id, - run_profile, - expected_parent_source, - before.messages.len(), - input, - output, - )? == SwarmChatFlow::Exit - { - return Ok(()); - } - } - SwarmChatInput::Quit => { - writeln!( - output, - "已退出 Agent Swarm Chat;后台 Runner 和已投递任务保持运行。" - ) - .map_err(|error| format!("写入终端失败:{error}"))?; - return Ok(()); - } - SwarmChatInput::Message(message) => { - let before = - read_local_conversation_for_session_at(root, Some(parent_agent_id), None)?; - let session_id = before - .session_id - .as_deref() - .ok_or_else(|| "父 Agent 当前 Session 缺失".to_string())?; - if handle_swarm_user_turn( - root, - parent_agent_id, - session_id, - run_profile, - new_run_launch, - &message, - input, - output, - )? == SwarmChatFlow::Exit - { - return Ok(()); - } - } - } - } -} - -pub(super) fn receive_swarm_chat_line( - input: &Receiver, -) -> Result, String> { - match input.recv() { - Ok(SwarmInputEvent::Line(line)) => Ok(Some(line)), - Ok(SwarmInputEvent::Eof) | Err(_) => Ok(None), - Ok(SwarmInputEvent::Error(error)) => Err(format!("读取终端输入失败:{error}")), - } -} - -pub(super) fn prompt_swarm_decision( - root: &Path, - parent_agent_id: &str, - input: &Receiver, - output: &mut W, - prompt: &str, -) -> Result { - write!(output, "{prompt}").map_err(|error| format!("写入终端失败:{error}"))?; - output - .flush() - .map_err(|error| format!("刷新终端失败:{error}"))?; - loop { - let Some(line) = receive_swarm_chat_line(input)? else { - return Ok(SwarmPromptDecision::InputClosed); - }; - match line.to_ascii_lowercase().as_str() { - "approve" | "yes" | "y" | "批准" => return Ok(SwarmPromptDecision::Approve), - "reject" | "no" | "n" | "拒绝" => return Ok(SwarmPromptDecision::Reject), - "/quit" | "/exit" => return Ok(SwarmPromptDecision::Quit), - _ => { - if let Some(command) = parse_swarm_chat_input(&line) { - match command { - SwarmChatInput::Goal(command) => { - let _ = - handle_swarm_goal_command(root, parent_agent_id, command, output)?; - return Ok(SwarmPromptDecision::Deferred); - } - SwarmChatInput::InvalidGoal(error) => { - print_swarm_goal_error(output, &error)?; - } - SwarmChatInput::Compact => { - handle_swarm_context_compaction(root, parent_agent_id, output)?; - return Ok(SwarmPromptDecision::Deferred); - } - _ => {} - } - } - write!(output, "请输入 approve 或 reject:") - .map_err(|error| format!("写入终端失败:{error}"))?; - output - .flush() - .map_err(|error| format!("刷新终端失败:{error}"))?; - } - } - } -} - -pub(super) fn print_swarm_chat_exit(output: &mut W) -> Result<(), String> { - writeln!( - output, - "已退出 Agent Swarm Chat;后台 Runner 和已投递任务保持运行。" - ) - .map_err(|error| format!("写入终端失败:{error}")) -} - -pub(super) fn parse_swarm_chat_input(input: &str) -> Option { - let input = input.trim(); - if input.is_empty() { - return None; - } - if let Some(rest) = input.strip_prefix("/goal") { - if rest.is_empty() { - return Some(SwarmChatInput::Goal(SwarmGoalCommand::Status)); - } - if !rest.chars().next().is_some_and(char::is_whitespace) { - return Some(SwarmChatInput::InvalidGoal( - "未知 /goal 命令;输入 /help 查看支持的 Goal 命令。".to_string(), - )); - } - return Some(parse_swarm_goal_command(rest.trim())); - } - Some(match input { - "/help" => SwarmChatInput::Help, - "/agents" => SwarmChatInput::Agents, - "/status" => SwarmChatInput::Status, - "/history" => SwarmChatInput::History, - "/compact" => SwarmChatInput::Compact, - "/resume" => SwarmChatInput::Resume, - "/quit" | "/exit" => SwarmChatInput::Quit, - value => SwarmChatInput::Message(value.to_string()), - }) -} - -pub(super) fn parse_swarm_goal_command(input: &str) -> SwarmChatInput { - if input.is_empty() || input == "status" { - return SwarmChatInput::Goal(SwarmGoalCommand::Status); - } - if input == "pause" { - return SwarmChatInput::Goal(SwarmGoalCommand::Pause); - } - if input == "resume" { - return SwarmChatInput::Goal(SwarmGoalCommand::Resume); - } - if input == "clear" { - return SwarmChatInput::Goal(SwarmGoalCommand::Clear); - } - if let Some(outcome) = input.strip_prefix("edit") { - if outcome.is_empty() { - return SwarmChatInput::InvalidGoal("用法:/goal edit <目标>".to_string()); - } - if outcome.chars().next().is_some_and(char::is_whitespace) { - let outcome = outcome.trim(); - return if outcome.is_empty() { - SwarmChatInput::InvalidGoal("用法:/goal edit <目标>".to_string()) - } else { - SwarmChatInput::Goal(SwarmGoalCommand::Edit(outcome.to_string())) - }; - } - } - for command in ["status", "pause", "resume", "clear"] { - if input - .strip_prefix(command) - .is_some_and(|rest| rest.chars().next().is_some_and(char::is_whitespace)) - { - return SwarmChatInput::InvalidGoal(format!("/goal {command} 不接受额外参数")); - } - } - SwarmChatInput::Goal(SwarmGoalCommand::Start(input.to_string())) -} - -pub(super) fn print_swarm_chat_help(output: &mut W) -> Result<(), String> { - writeln!(output, "/agents 查看静态 Agent 与动态 child") - .and_then(|_| writeln!(output, "/status 查看全部 Runtime 状态")) - .and_then(|_| writeln!(output, "/history 查看父 Agent 当前 Session 历史")) - .and_then(|_| writeln!(output, "/compact 压缩父 Agent 当前空闲 Session 历史")) - .and_then(|_| writeln!(output, "/resume 继续观察当前 Session 的未收束 Runtime")) - .and_then(|_| writeln!(output, "/goal <目标> 启动当前 Session 的持久 Goal")) - .and_then(|_| writeln!(output, "/goal 查看当前 Goal")) - .and_then(|_| writeln!(output, "/goal status 查看当前 Goal")) - .and_then(|_| writeln!(output, "/goal edit <目标> 编辑当前 Goal")) - .and_then(|_| writeln!(output, "/goal pause 暂停当前 Goal")) - .and_then(|_| writeln!(output, "/goal resume 恢复当前 Goal")) - .and_then(|_| writeln!(output, "/goal clear 清理当前 Goal")) - .and_then(|_| writeln!(output, "/help 查看命令")) - .and_then(|_| writeln!(output, "/quit 退出终端观察客户端")) - .map_err(|error| format!("写入终端失败:{error}")) -} diff --git a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/observer.rs b/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/observer.rs deleted file mode 100644 index f728282f7..000000000 --- a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/observer.rs +++ /dev/null @@ -1,843 +0,0 @@ -use super::*; - -pub(super) const SWARM_CHAT_PLAN_STEP_LIMIT: usize = 8; - -#[derive(Default)] -pub(super) struct SwarmRuntimeObserver { - pub(super) state_signatures: BTreeMap, - pub(super) seen_events: BTreeSet, - pub(super) handled_confirmations: BTreeSet, - pub(super) handled_user_input_requests: BTreeSet, - pub(super) user_input_response_ids: BTreeMap, - pub(super) response_streams: BTreeMap, - pub(super) open_response_line: Option, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub(super) struct SwarmResponseStreamIdentity { - pub(super) task_id: String, - pub(super) session_id: String, - pub(super) run_id: String, - pub(super) request_slot: String, - pub(super) applied_steer_cursor: u64, - pub(super) response_revision: u64, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub(super) struct SwarmResponseStreamCursor { - pub(super) identity: SwarmResponseStreamIdentity, - pub(super) session_id: String, - pub(super) sequence: u64, - pub(super) status: String, - pub(super) accumulated_text: String, - pub(super) printed_accumulated_text: Option, - pub(super) connected: bool, - pub(super) rejected_snapshot: Option, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub(super) struct SwarmResponseStreamLine { - pub(super) agent_id: String, - pub(super) identity: SwarmResponseStreamIdentity, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub(super) struct SwarmRejectedResponseStreamSnapshot { - pub(super) session_id: String, - pub(super) sequence: u64, - pub(super) status: String, - pub(super) accumulated_text: String, -} - -pub(super) enum SwarmConfirmationResolution { - None, - Handled, - InputClosed, - Quit, -} - -impl SwarmResponseStreamIdentity { - pub(super) fn from_stream(stream: &AgentRuntimeResponseStream) -> Self { - Self { - task_id: stream.task_id.clone(), - session_id: stream.session_id.clone(), - run_id: stream.run_id.clone(), - request_slot: stream.request_slot.clone(), - applied_steer_cursor: stream.applied_steer_cursor, - response_revision: stream.response_revision, - } - } -} - -impl SwarmResponseStreamCursor { - pub(super) fn seeded(stream: &AgentRuntimeResponseStream) -> Self { - Self { - identity: SwarmResponseStreamIdentity::from_stream(stream), - session_id: stream.session_id.clone(), - sequence: stream.sequence, - status: stream.status.clone(), - accumulated_text: stream.accumulated_text.clone(), - printed_accumulated_text: stream.accumulated_text.is_empty().then(String::new), - connected: true, - rejected_snapshot: None, - } - } - - pub(super) fn fresh(stream: &AgentRuntimeResponseStream) -> Self { - Self { - identity: SwarmResponseStreamIdentity::from_stream(stream), - session_id: stream.session_id.clone(), - sequence: stream.sequence, - status: stream.status.clone(), - accumulated_text: stream.accumulated_text.clone(), - printed_accumulated_text: None, - connected: true, - rejected_snapshot: None, - } - } -} - -impl SwarmRejectedResponseStreamSnapshot { - pub(super) fn from_stream(stream: &AgentRuntimeResponseStream) -> Self { - Self { - session_id: stream.session_id.clone(), - sequence: stream.sequence, - status: stream.status.clone(), - accumulated_text: stream.accumulated_text.clone(), - } - } -} - -pub(super) fn swarm_response_stream_is_printable(stream: &AgentRuntimeResponseStream) -> bool { - matches!( - stream.status.as_str(), - AGENT_RUNTIME_RESPONSE_STREAM_STATUS_STREAMING | AGENT_RUNTIME_RESPONSE_STREAM_STATUS_READY - ) -} - -pub(super) fn swarm_response_stream_identity_reset_reason( - previous: &SwarmResponseStreamIdentity, - current: &SwarmResponseStreamIdentity, -) -> &'static str { - if previous.run_id != current.run_id { - "new-run" - } else if previous.session_id != current.session_id { - "new-session" - } else if previous.task_id != current.task_id { - "new-task" - } else if previous.applied_steer_cursor != current.applied_steer_cursor { - "new-steer-cursor" - } else if previous.request_slot != current.request_slot { - "new-request-slot" - } else { - "new-response-revision" - } -} - -impl SwarmRuntimeObserver { - pub(super) fn seed(root: &Path) -> Result { - let mut observer = Self::default(); - for runtime in read_game_creator_agent_runtimes_at(root)? { - observer.state_signatures.insert( - runtime.state.agent_id.clone(), - runtime_state_signature(&runtime.state, &runtime.task_queue), - ); - if let Some(stream) = runtime.response_stream.as_ref() { - observer.response_streams.insert( - runtime.state.agent_id.clone(), - SwarmResponseStreamCursor::seeded(stream), - ); - } - for event in runtime.recent_events { - observer.seen_events.insert(runtime_event_key(&event)); - } - } - Ok(observer) - } - - pub(super) fn print_changes( - &mut self, - runtimes: &[AgentRuntimeResult], - output: &mut W, - ) -> Result { - let mut changed = false; - for runtime in runtimes { - let has_runtime_history = !runtime.state.run_id.is_empty() - || runtime.task_queue.total > 0 - || !runtime.recent_events.is_empty(); - if has_runtime_history { - let signature = runtime_state_signature(&runtime.state, &runtime.task_queue); - if self.state_signatures.get(&runtime.state.agent_id) != Some(&signature) { - changed = true; - self.close_response_line(output)?; - self.state_signatures - .insert(runtime.state.agent_id.clone(), signature); - print_runtime_state(&runtime.state, &runtime.task_queue, output)?; - } - } - for event in &runtime.recent_events { - if self.seen_events.insert(runtime_event_key(event)) { - changed = true; - self.close_response_line(output)?; - writeln!( - output, - "[事件] {} {} {}/{} {}{}", - event.agent_id, - event.event_type, - event.status, - event.phase, - event.summary, - event - .detail - .as_deref() - .map(|detail| format!(" | {detail}")) - .unwrap_or_default() - ) - .map_err(|error| format!("写入终端失败:{error}"))?; - } - } - changed |= self.observe_response_stream( - &runtime.state.agent_id, - runtime.response_stream.as_ref(), - output, - )?; - } - Ok(changed) - } - - pub(super) fn observe_response_stream( - &mut self, - agent_id: &str, - stream: Option<&AgentRuntimeResponseStream>, - output: &mut W, - ) -> Result { - let previous = self.response_streams.remove(agent_id); - let Some(stream) = stream else { - let Some(mut cursor) = previous else { - return Ok(false); - }; - let changed = cursor.connected; - if changed { - cursor.connected = false; - if self.response_line_matches(agent_id, &cursor.identity) { - self.close_response_line(output)?; - } - } - self.response_streams.insert(agent_id.to_string(), cursor); - return Ok(changed); - }; - - let printable = swarm_response_stream_is_printable(stream); - let Some(previous) = previous else { - let reason = if stream.sequence == 0 && stream.accumulated_text.is_empty() { - "new-stream" - } else { - "reconnect" - }; - self.print_response_stream_reset(agent_id, stream, reason, output)?; - let mut cursor = SwarmResponseStreamCursor::fresh(stream); - if printable { - self.print_response_stream_full(agent_id, stream, &mut cursor, output)?; - } - self.response_streams.insert(agent_id.to_string(), cursor); - return Ok(true); - }; - - let identity = SwarmResponseStreamIdentity::from_stream(stream); - if previous.identity != identity { - let reason = swarm_response_stream_identity_reset_reason(&previous.identity, &identity); - self.print_response_stream_reset(agent_id, stream, reason, output)?; - let mut cursor = SwarmResponseStreamCursor::fresh(stream); - if printable { - self.print_response_stream_full(agent_id, stream, &mut cursor, output)?; - } - self.response_streams.insert(agent_id.to_string(), cursor); - return Ok(true); - } - - let exact_snapshot = previous.session_id == stream.session_id - && previous.sequence == stream.sequence - && previous.status == stream.status - && previous.accumulated_text == stream.accumulated_text; - let unchanged = previous.connected && exact_snapshot; - if unchanged { - self.response_streams.insert(agent_id.to_string(), previous); - return Ok(false); - } - - let non_monotonic_reason = if previous.session_id != stream.session_id { - Some("identity-conflict") - } else if stream.sequence < previous.sequence { - Some("sequence-rollback") - } else if stream.sequence == previous.sequence && !exact_snapshot { - Some("sequence-conflict") - } else { - None - }; - if let Some(reason) = non_monotonic_reason { - let rejected = SwarmRejectedResponseStreamSnapshot::from_stream(stream); - if previous.rejected_snapshot.as_ref() == Some(&rejected) { - self.response_streams.insert(agent_id.to_string(), previous); - return Ok(false); - } - self.print_response_stream_reset(agent_id, stream, reason, output)?; - let mut cursor = previous; - cursor.connected = false; - cursor.rejected_snapshot = Some(rejected); - self.response_streams.insert(agent_id.to_string(), cursor); - return Ok(true); - } - - let prefix_continuation = stream - .accumulated_text - .strip_prefix(&previous.accumulated_text); - let reset_reason = if !previous.connected { - Some("reconnect") - } else if prefix_continuation.is_none() { - Some("non-prefix-correction") - } else { - None - }; - if let Some(reason) = reset_reason { - self.print_response_stream_reset(agent_id, stream, reason, output)?; - } - - let mut cursor = SwarmResponseStreamCursor::fresh(stream); - if printable { - let previous_printed = previous.printed_accumulated_text.as_deref(); - let reset_requires_full = reset_reason.is_some_and(|reason| reason != "reconnect") - && previous_printed != Some(stream.accumulated_text.as_str()); - if previous_printed == Some(stream.accumulated_text.as_str()) { - cursor.printed_accumulated_text = Some(stream.accumulated_text.clone()); - } else if reset_requires_full { - self.print_response_stream_full(agent_id, stream, &mut cursor, output)?; - } else if let Some(suffix) = prefix_continuation - .filter(|_| previous_printed == Some(previous.accumulated_text.as_str())) - { - self.write_response_stream_chunk(agent_id, &identity, suffix, output)?; - cursor.printed_accumulated_text = Some(stream.accumulated_text.clone()); - } else { - self.print_response_stream_full(agent_id, stream, &mut cursor, output)?; - } - } else { - cursor.printed_accumulated_text = previous - .printed_accumulated_text - .filter(|printed| printed == &stream.accumulated_text); - if self.response_line_matches(agent_id, &identity) { - self.close_response_line(output)?; - } - } - self.response_streams.insert(agent_id.to_string(), cursor); - Ok(true) - } - - pub(super) fn print_response_stream_full( - &mut self, - agent_id: &str, - stream: &AgentRuntimeResponseStream, - cursor: &mut SwarmResponseStreamCursor, - output: &mut W, - ) -> Result<(), String> { - self.write_response_stream_chunk( - agent_id, - &cursor.identity, - &stream.accumulated_text, - output, - )?; - cursor.printed_accumulated_text = Some(stream.accumulated_text.clone()); - Ok(()) - } - - pub(super) fn print_response_stream_reset( - &mut self, - agent_id: &str, - stream: &AgentRuntimeResponseStream, - reason: &str, - output: &mut W, - ) -> Result<(), String> { - self.close_response_line(output)?; - writeln!( - output, - "[回复流重置] agent={} run={} requestSlot={} revision={} sequence={} status={} chars={} reason={}", - agent_id, - stream.run_id, - stream.request_slot, - stream.response_revision, - stream.sequence, - stream.status, - stream.accumulated_text.chars().count(), - reason - ) - .map_err(|error| format!("写入终端失败:{error}")) - } - - pub(super) fn write_response_stream_chunk( - &mut self, - agent_id: &str, - identity: &SwarmResponseStreamIdentity, - chunk: &str, - output: &mut W, - ) -> Result<(), String> { - if chunk.is_empty() { - return Ok(()); - } - if !self.response_line_matches(agent_id, identity) { - self.close_response_line(output)?; - write!(output, "Agent[{agent_id}]> {chunk}") - .map_err(|error| format!("写入终端失败:{error}"))?; - self.open_response_line = Some(SwarmResponseStreamLine { - agent_id: agent_id.to_string(), - identity: identity.clone(), - }); - } else { - write!(output, "{chunk}").map_err(|error| format!("写入终端失败:{error}"))?; - } - output - .flush() - .map_err(|error| format!("刷新终端失败:{error}")) - } - - pub(super) fn response_line_matches( - &self, - agent_id: &str, - identity: &SwarmResponseStreamIdentity, - ) -> bool { - self.open_response_line - .as_ref() - .is_some_and(|line| line.agent_id == agent_id && line.identity == *identity) - } - - pub(super) fn close_response_line(&mut self, output: &mut W) -> Result<(), String> { - if self.open_response_line.take().is_some() { - writeln!(output).map_err(|error| format!("写入终端失败:{error}"))?; - output - .flush() - .map_err(|error| format!("刷新终端失败:{error}"))?; - } - Ok(()) - } - - pub(super) fn parent_reply_was_fully_streamed( - &self, - parent_agent_id: &str, - session_id: &str, - reply: &str, - ) -> bool { - self.response_streams - .get(parent_agent_id) - .is_some_and(|cursor| { - cursor.session_id == session_id - && cursor.accumulated_text == reply - && cursor.printed_accumulated_text.as_deref() == Some(reply) - && matches!( - cursor.status.as_str(), - AGENT_RUNTIME_RESPONSE_STREAM_STATUS_STREAMING - | AGENT_RUNTIME_RESPONSE_STREAM_STATUS_READY - | AGENT_RUNTIME_RESPONSE_STREAM_STATUS_COMMITTED - ) - }) - } - - pub(super) fn resolve_confirmations( - &mut self, - root: &Path, - parent_agent_id: &str, - runtimes: &[AgentRuntimeResult], - input: &Receiver, - output: &mut W, - ) -> Result { - for runtime in runtimes { - if runtime.state.status != "waiting-for-confirmation" { - continue; - } - let Some(pending) = runtime.state.pending_tool_action.as_ref() else { - continue; - }; - let key = format!( - "{}:{}:{}", - runtime.state.agent_id, runtime.state.run_id, pending.action_id - ); - if self.handled_confirmations.contains(&key) { - continue; - } - self.close_response_line(output)?; - writeln!( - output, - "\n[待确认] agent={} run={} action={} tool={}", - runtime.state.agent_id, runtime.state.run_id, pending.action_id, pending.tool - ) - .and_then(|_| { - if let Some(summary) = pending.input_summary.as_deref() { - writeln!(output, "输入摘要:{summary}") - } else { - Ok(()) - } - }) - .and_then(|_| write!(output, "输入 approve 或 reject:")) - .map_err(|error| format!("写入终端失败:{error}"))?; - let decision = prompt_swarm_decision(root, parent_agent_id, input, output, "")?; - let approved = match decision { - SwarmPromptDecision::Approve => true, - SwarmPromptDecision::Reject => false, - SwarmPromptDecision::Deferred => return Ok(SwarmConfirmationResolution::Handled), - SwarmPromptDecision::InputClosed => { - return Ok(SwarmConfirmationResolution::InputClosed) - } - SwarmPromptDecision::Quit => return Ok(SwarmConfirmationResolution::Quit), - }; - let project_path = root.display().to_string(); - if approved { - confirm_game_creator_agent_runtime_task( - project_path, - runtime.state.agent_id.clone(), - runtime.state.run_id.clone(), - pending.action_id.clone(), - "Agent Swarm Chat 终端批准".to_string(), - )?; - writeln!(output, "[已批准] {}", pending.action_id) - .map_err(|error| format!("写入终端失败:{error}"))?; - } else { - reject_game_creator_agent_runtime_task( - project_path, - runtime.state.agent_id.clone(), - runtime.state.run_id.clone(), - pending.action_id.clone(), - "Agent Swarm Chat 终端拒绝".to_string(), - )?; - writeln!(output, "[已拒绝] {}", pending.action_id) - .map_err(|error| format!("写入终端失败:{error}"))?; - } - self.handled_confirmations.insert(key); - return Ok(SwarmConfirmationResolution::Handled); - } - Ok(SwarmConfirmationResolution::None) - } - - pub(super) fn resolve_user_input_requests( - &mut self, - root: &Path, - parent_agent_id: &str, - runtimes: &[AgentRuntimeResult], - input: &Receiver, - output: &mut W, - ) -> Result { - for runtime in runtimes { - let Some(request) = runtime.user_input_request.as_ref() else { - continue; - }; - if runtime.state.agent_id != parent_agent_id - || runtime.state.status != "waiting-for-user-input" - { - continue; - } - let key = format!( - "{}:{}:{}", - runtime.state.agent_id, runtime.state.run_id, request.request_id - ); - if self.handled_user_input_requests.contains(&key) { - continue; - } - self.close_response_line(output)?; - writeln!( - output, - "\n[Needs input] agent={} run={} request={}", - runtime.state.agent_id, runtime.state.run_id, request.request_id - ) - .map_err(|error| format!("写入终端失败:{error}"))?; - let mut answers = BTreeMap::new(); - for question in &request.questions { - writeln!(output, "\n{}:{}", question.header, question.question) - .map_err(|error| format!("写入终端失败:{error}"))?; - for (index, option) in question.options.iter().enumerate() { - writeln!( - output, - " {}. {} - {}", - index + 1, - option.label, - option.description - ) - .map_err(|error| format!("写入终端失败:{error}"))?; - } - loop { - write!( - output, - "请选择 1-{},或直接输入其他答案:", - question.options.len() - ) - .map_err(|error| format!("写入终端失败:{error}"))?; - output - .flush() - .map_err(|error| format!("刷新终端失败:{error}"))?; - let Some(line) = receive_swarm_chat_line(input)? else { - return Ok(SwarmConfirmationResolution::InputClosed); - }; - if matches!(line.as_str(), "/quit" | "/exit") { - return Ok(SwarmConfirmationResolution::Quit); - } - if line == "/status" { - print_swarm_status(root, output)?; - continue; - } - if line == "/history" { - print_conversation_history(root, parent_agent_id, output)?; - continue; - } - let answer = line - .parse::() - .ok() - .and_then(|index| index.checked_sub(1)) - .and_then(|index| question.options.get(index)) - .map(|option| option.label.clone()) - .unwrap_or_else(|| line.trim().to_string()); - if answer.is_empty() { - writeln!(output, "回答不能为空。") - .map_err(|error| format!("写入终端失败:{error}"))?; - continue; - } - answers.insert(question.id.clone(), answer); - break; - } - } - let response_id = self - .user_input_response_ids - .entry(key.clone()) - .or_insert_with(|| { - format!("swarm-user-input-{}-{}", request.request_id, unix_millis()) - }) - .clone(); - answer_game_creator_agent_runtime_user_input_at( - root, - &runtime.state.agent_id, - &runtime.state.run_id, - &request.action_id, - &request.request_id, - &response_id, - answers, - )?; - writeln!(output, "[已回答] {}", request.request_id) - .map_err(|error| format!("写入终端失败:{error}"))?; - self.handled_user_input_requests.insert(key); - return Ok(SwarmConfirmationResolution::Handled); - } - Ok(SwarmConfirmationResolution::None) - } -} - -pub(super) fn print_runtime_state( - state: &AgentRuntimeState, - queue: &AgentRuntimeTaskQueueSummary, - output: &mut W, -) -> Result<(), String> { - let relation = state - .parent_agent_id - .as_deref() - .map(|parent| { - format!( - " parent={parent} delegation={}", - state.delegation_id.as_deref().unwrap_or("unknown") - ) - }) - .unwrap_or_default(); - writeln!( - output, - "[状态] {} {}/{} run={} queue={}/{}/{}/{}{} | {}", - state.agent_id, - state.status, - state.phase, - state.run_id, - queue.pending, - queue.running, - queue.waiting_for_confirmation, - queue.waiting_for_user_input, - relation, - state.current_action - ) - .map_err(|error| format!("写入终端失败:{error}"))?; - - let completed = state - .plan_steps - .iter() - .filter(|step| step.status == "completed") - .count(); - let current_step = runtime_current_plan_step(state) - .map(|step| { - format!( - "#{} [{}] {}", - step.index.saturating_add(1), - runtime_cli_value(&step.status), - runtime_cli_value(&step.title) - ) - }) - .unwrap_or_else(|| "-".to_string()); - writeln!( - output, - "[计划] revision={} completed={}/{} current={} | waiting={} | next={}", - state.plan_revision, - completed, - state.plan_steps.len(), - current_step, - runtime_cli_value(&state.waiting_on), - runtime_cli_value(&state.next_step) - ) - .map_err(|error| format!("写入终端失败:{error}"))?; - if !state.plan_explanation.trim().is_empty() { - writeln!( - output, - " [计划说明] {}", - runtime_cli_value(&state.plan_explanation) - ) - .map_err(|error| format!("写入终端失败:{error}"))?; - } - writeln!( - output, - "[上下文] estimated={}/{} actual={}/{}/{} compaction={} last={}", - state.context_usage.estimated_input_tokens, - state.context_usage.auto_compact_token_limit, - state - .context_usage - .last_prompt_tokens - .map(|value| value.to_string()) - .unwrap_or_else(|| "-".to_string()), - state - .context_usage - .last_completion_tokens - .map(|value| value.to_string()) - .unwrap_or_else(|| "-".to_string()), - state - .context_usage - .last_total_tokens - .map(|value| value.to_string()) - .unwrap_or_else(|| "-".to_string()), - state.context_usage.compaction_revision, - state - .context_usage - .last_compacted_at - .map(|value| value.to_string()) - .unwrap_or_else(|| "-".to_string()), - ) - .map_err(|error| format!("写入终端失败:{error}"))?; - - for step in state.plan_steps.iter().take(SWARM_CHAT_PLAN_STEP_LIMIT) { - writeln!( - output, - " [计划步骤] #{} [{}] {}", - step.index.saturating_add(1), - runtime_cli_value(&step.status), - runtime_cli_value(&step.title) - ) - .map_err(|error| format!("写入终端失败:{error}"))?; - } - if state.plan_steps.len() > SWARM_CHAT_PLAN_STEP_LIMIT { - writeln!( - output, - " [计划] 另有 {} 条步骤未显示", - state.plan_steps.len() - SWARM_CHAT_PLAN_STEP_LIMIT - ) - .map_err(|error| format!("写入终端失败:{error}"))?; - } - Ok(()) -} - -pub(super) fn runtime_current_plan_step( - state: &AgentRuntimeState, -) -> Option<&AgentRuntimePlanStep> { - state - .active_plan_step_index - .and_then(|active_index| { - state - .plan_steps - .iter() - .find(|step| step.index == active_index) - .or_else(|| state.plan_steps.get(active_index as usize)) - }) - .or_else(|| { - state.plan_steps.iter().find(|step| { - matches!( - step.status.as_str(), - "active" | "in_progress" | "running" | "waiting-for-confirmation" - ) - }) - }) - .or_else(|| { - state - .plan_steps - .iter() - .find(|step| step.status == "pending") - }) -} - -pub(super) fn runtime_cli_value(value: &str) -> &str { - let value = value.trim(); - if value.is_empty() { - "-" - } else { - value - } -} - -pub(super) fn runtime_state_signature( - state: &AgentRuntimeState, - queue: &AgentRuntimeTaskQueueSummary, -) -> String { - let completed_plan_steps = state - .plan_steps - .iter() - .filter(|step| step.status == "completed") - .count(); - let current_plan_step = runtime_current_plan_step(state) - .map(|step| format!("{}:{}:{}", step.index, step.status, step.title)) - .unwrap_or_default(); - let mut signature = format!( - "{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}", - state.run_id, - state.status, - state.phase, - state.current_action, - state.updated_at, - queue.pending, - queue.running, - queue.waiting_for_confirmation, - queue.waiting_for_user_input, - queue.updated_at, - state.plan_revision, - state - .active_plan_step_index - .map(|index| index.to_string()) - .unwrap_or_default(), - completed_plan_steps, - state.plan_steps.len(), - current_plan_step, - state.waiting_on, - state.next_step - ); - signature.push(':'); - signature.push_str(&state.plan_explanation); - signature.push(':'); - signature.push_str(&format!( - "{}:{}:{:?}:{:?}:{}:{:?}", - state.context_usage.estimated_input_tokens, - state.context_usage.auto_compact_token_limit, - state.context_usage.last_prompt_tokens, - state.context_usage.last_completion_tokens, - state.context_usage.compaction_revision, - state.context_usage.last_compacted_at, - )); - signature -} - -pub(super) fn runtime_event_key(event: &AgentRuntimeEvent) -> String { - format!( - "{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}", - event.agent_id, - event.task_id, - event.session_id, - event.run_id, - event.event_type, - event.action_id.as_deref().unwrap_or_default(), - event.status, - event.phase, - event.updated_at, - event.summary, - event.detail.as_deref().unwrap_or_default() - ) -} diff --git a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/report.rs b/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/report.rs deleted file mode 100644 index a587ca7c0..000000000 --- a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/report.rs +++ /dev/null @@ -1,285 +0,0 @@ -use super::*; - -pub(super) const SWARM_TURN_REPORT_PREFIX: &str = "[turn.report] "; -pub(super) const SWARM_TURN_REPORT_SCHEMA_VERSION: &str = "game-creator-swarm-turn-report.v1"; -pub(super) const SWARM_TURN_FAILED_ERROR: &str = "swarm-turn-failed"; -pub(super) const SWARM_TURN_INCOMPLETE_ERROR: &str = "swarm-turn-incomplete"; -pub(super) const SWARM_TURN_RECONCILIATION_ERROR: &str = "swarm-turn-needs-reconciliation"; - -#[derive(Debug, Eq, PartialEq)] -pub(super) enum SwarmTurnOutcome { - Settled(SwarmTurnReport), - Failed { - agent_ids: Vec, - report: SwarmTurnReport, - }, - Incomplete { - reasons: Vec, - report: SwarmTurnReport, - }, - NeedsReconciliation { - agent_ids: Vec, - report: SwarmTurnReport, - }, - Quit, -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize)] -#[serde(rename_all = "kebab-case")] -pub(super) enum SwarmTurnReportOutcome { - Settled, - Failed, - Incomplete, - NeedsReconciliation, -} - -#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] -#[serde(rename_all = "camelCase")] -pub(super) struct SwarmTurnReport { - pub(super) schema_version: &'static str, - pub(super) outcome: SwarmTurnReportOutcome, - pub(super) parent_agent_id: String, - pub(super) session_id: String, - pub(super) parent_run_id: Option, - pub(super) runtime_count: usize, - pub(super) busy_runtime_count: usize, - pub(super) pending_task_count: u64, - pub(super) running_task_count: u64, - pub(super) waiting_for_confirmation_count: u64, - pub(super) waiting_for_user_input_count: u64, - pub(super) new_assistant_message_count: usize, - pub(super) final_reply_chars: usize, - pub(super) reconciliation_agent_count: usize, -} - -#[derive(Default)] -struct SwarmTurnRuntimeMetrics { - runtime_count: usize, - busy_runtime_count: usize, - pending_task_count: u64, - running_task_count: u64, - waiting_for_confirmation_count: u64, - waiting_for_user_input_count: u64, -} - -struct SwarmTurnRuntimeSnapshot { - agent_id: String, - run_id: String, - status: String, - phase: String, - updated_at: u64, -} - -fn runtime_state_belongs_to_turn( - state: &AgentRuntimeState, - parent_agent_id: &str, - session_id: &str, - parent_run_id: &str, -) -> bool { - (state.agent_id == parent_agent_id - && state.session_id == session_id - && state.run_id == parent_run_id) - || (state.parent_agent_id.as_deref() == Some(parent_agent_id) - && state.parent_run_id.as_deref() == Some(parent_run_id)) -} - -fn runtime_task_belongs_to_turn( - task: &AgentRuntimeTaskRecord, - parent_agent_id: &str, - session_id: &str, - parent_run_id: &str, -) -> bool { - (task.agent_id == parent_agent_id - && task.session_id == session_id - && task.run_id == parent_run_id) - || (task.parent_agent_id.as_deref() == Some(parent_agent_id) - && task.parent_run_id.as_deref() == Some(parent_run_id)) -} - -fn upsert_turn_runtime_snapshot( - snapshots: &mut Vec, - candidate: SwarmTurnRuntimeSnapshot, -) { - if candidate.run_id.trim().is_empty() { - return; - } - if let Some(existing) = snapshots.iter_mut().find(|snapshot| { - snapshot.agent_id == candidate.agent_id && snapshot.run_id == candidate.run_id - }) { - if candidate.updated_at >= existing.updated_at { - *existing = candidate; - } - } else { - snapshots.push(candidate); - } -} - -fn scoped_swarm_turn_runtime_metrics( - parent_agent_id: &str, - session_id: &str, - parent_run_id: Option<&str>, - runtimes: &[AgentRuntimeResult], -) -> Result { - let Some(parent_run_id) = parent_run_id else { - return Ok(SwarmTurnRuntimeMetrics::default()); - }; - let mut snapshots = Vec::new(); - for runtime in runtimes { - let journal_tasks = if runtime.task_path.trim().is_empty() { - None - } else { - Some(read_all_game_creator_agent_runtime_tasks(Path::new( - &runtime.task_path, - ))?) - }; - let tasks = journal_tasks.as_deref().unwrap_or(&runtime.recent_tasks); - for task in tasks { - if runtime_task_belongs_to_turn(task, parent_agent_id, session_id, parent_run_id) { - upsert_turn_runtime_snapshot( - &mut snapshots, - SwarmTurnRuntimeSnapshot { - agent_id: task.agent_id.clone(), - run_id: task.run_id.clone(), - status: task.status.clone(), - phase: task.phase.clone(), - updated_at: task.updated_at, - }, - ); - } - } - if runtime_state_belongs_to_turn(&runtime.state, parent_agent_id, session_id, parent_run_id) - { - upsert_turn_runtime_snapshot( - &mut snapshots, - SwarmTurnRuntimeSnapshot { - agent_id: runtime.state.agent_id.clone(), - run_id: runtime.state.run_id.clone(), - status: runtime.state.status.clone(), - phase: runtime.state.phase.clone(), - updated_at: runtime.state.updated_at, - }, - ); - } - } - - let mut metrics = SwarmTurnRuntimeMetrics { - runtime_count: snapshots.len(), - ..SwarmTurnRuntimeMetrics::default() - }; - for snapshot in snapshots { - if matches!( - snapshot.status.as_str(), - "pending" - | "running" - | "waiting-for-confirmation" - | "waiting-for-user-input" - | "cancelling" - ) || snapshot.phase == "needs-reconciliation" - { - metrics.busy_runtime_count += 1; - } - match snapshot.status.as_str() { - "pending" => metrics.pending_task_count += 1, - "running" => metrics.running_task_count += 1, - "waiting-for-confirmation" => metrics.waiting_for_confirmation_count += 1, - "waiting-for-user-input" => metrics.waiting_for_user_input_count += 1, - _ => {} - } - } - Ok(metrics) -} - -pub(super) fn build_swarm_turn_report( - outcome: SwarmTurnReportOutcome, - parent_agent_id: &str, - session_id: &str, - expected_parent_run_id: Option<&str>, - runtimes: &[AgentRuntimeResult], - conversation_metrics: SwarmTurnConversationMetrics, - reconciliation_agent_count: usize, -) -> Result { - let parent_run_id = expected_parent_run_id - .map(str::trim) - .filter(|run_id| !run_id.is_empty()) - .map(str::to_string) - .or_else(|| { - runtimes - .iter() - .find(|runtime| { - runtime.state.agent_id == parent_agent_id - && runtime.state.session_id == session_id - }) - .map(|runtime| runtime.state.run_id.trim()) - .filter(|run_id| !run_id.is_empty()) - .map(str::to_string) - }); - let runtime_metrics = scoped_swarm_turn_runtime_metrics( - parent_agent_id, - session_id, - parent_run_id.as_deref(), - runtimes, - )?; - Ok(SwarmTurnReport { - schema_version: SWARM_TURN_REPORT_SCHEMA_VERSION, - outcome, - parent_agent_id: parent_agent_id.to_string(), - session_id: session_id.to_string(), - parent_run_id, - runtime_count: runtime_metrics.runtime_count, - busy_runtime_count: runtime_metrics.busy_runtime_count, - pending_task_count: runtime_metrics.pending_task_count, - running_task_count: runtime_metrics.running_task_count, - waiting_for_confirmation_count: runtime_metrics.waiting_for_confirmation_count, - waiting_for_user_input_count: runtime_metrics.waiting_for_user_input_count, - new_assistant_message_count: conversation_metrics.new_assistant_message_count, - final_reply_chars: conversation_metrics.final_reply_chars, - reconciliation_agent_count, - }) -} - -pub(super) fn print_turn_outcome( - outcome: SwarmTurnOutcome, - output: &mut W, -) -> Result<(), String> { - match outcome { - SwarmTurnOutcome::Settled(report) => print_swarm_turn_report(&report, output), - SwarmTurnOutcome::Failed { agent_ids, report } => { - writeln!( - output, - "[已失败] 以下 Runtime 到达失败终态:{}", - agent_ids.join(", ") - ) - .map_err(|error| format!("写入终端失败:{error}"))?; - print_swarm_turn_report(&report, output) - } - SwarmTurnOutcome::Incomplete { reasons, report } => { - writeln!( - output, - "[未完成] 当前 turn 未满足可信终态:{}", - reasons.join(", ") - ) - .map_err(|error| format!("写入终端失败:{error}"))?; - print_swarm_turn_report(&report, output) - } - SwarmTurnOutcome::NeedsReconciliation { agent_ids, report } => { - writeln!( - output, - "[已阻断] 以下 Agent 需要人工 reconciliation:{}", - agent_ids.join(", ") - ) - .map_err(|error| format!("写入终端失败:{error}"))?; - print_swarm_turn_report(&report, output) - } - SwarmTurnOutcome::Quit => Ok(()), - } -} - -pub(super) fn print_swarm_turn_report( - report: &SwarmTurnReport, - output: &mut W, -) -> Result<(), String> { - let json = serde_json::to_string(report) - .map_err(|error| format!("序列化 turn report 失败:{error}"))?; - writeln!(output, "{SWARM_TURN_REPORT_PREFIX}{json}") - .map_err(|error| format!("写入终端失败:{error}")) -} diff --git a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/terminal_classification.rs b/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/terminal_classification.rs deleted file mode 100644 index 40129d9ec..000000000 --- a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/terminal_classification.rs +++ /dev/null @@ -1,803 +0,0 @@ -use super::*; - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub(super) enum SwarmTurnTerminalClassification { - Settled, - Failed, - Incomplete, -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub(super) enum SwarmSpecialistFailureDisposition { - Recoverable, - Failed, - Incomplete, -} - -#[derive(Default)] -pub(super) struct SwarmTerminalFailureScan { - pub(super) failed_agents: Vec, - pub(super) incomplete_reasons: Vec, - pub(super) reconciliation_agents: Vec, -} - -pub(super) fn swarm_parent_runtime<'a>( - parent_agent_id: &str, - session_id: &str, - run_profile: &str, - expected_source: Option<&str>, - runtimes: &'a [AgentRuntimeResult], -) -> Option<&'a AgentRuntimeResult> { - runtimes.iter().find(|runtime| { - runtime.state.agent_id == parent_agent_id - && runtime.state.session_id == session_id - && runtime.state.run_profile == run_profile - && expected_source.is_none_or(|source| runtime.state.source == source) - }) -} - -pub(super) fn swarm_parent_runtime_for_run<'a>( - parent_agent_id: &str, - session_id: &str, - run_profile: &str, - expected_source: Option<&str>, - expected_run_id: &str, - runtimes: &'a [AgentRuntimeResult], -) -> Option<&'a AgentRuntimeResult> { - swarm_parent_runtime( - parent_agent_id, - session_id, - run_profile, - expected_source, - runtimes, - ) - .filter(|runtime| runtime.state.run_id == expected_run_id) -} - -pub(super) fn swarm_parent_steer_target<'a>( - parent_agent_id: &str, - session_id: &str, - run_profile: &str, - expected_source: Option<&str>, - expected_run_id: Option<&str>, - runtimes: &'a [AgentRuntimeResult], -) -> Option<&'a AgentRuntimeResult> { - swarm_parent_runtime( - parent_agent_id, - session_id, - run_profile, - expected_source, - runtimes, - ) - .filter(|runtime| { - expected_run_id.is_none_or(|run_id| runtime.state.run_id == run_id) - && game_creator_agent_runtime_accepts_steer(&runtime.state) - }) -} - -pub(super) fn matching_pending_swarm_run_id<'a>( - runtime: &'a AgentRuntimeResult, - session_id: &str, - run_profile: &str, - expected_source: Option<&str>, - message: &str, -) -> Option<&'a str> { - let message = message.trim(); - runtime - .recent_tasks - .iter() - .rev() - .find(|task| { - task.session_id == session_id - && task.run_profile == run_profile - && expected_source.is_none_or(|source| task.source == source) - && task.status == "pending" - && task.phase == "queued" - && task.task.trim() == message - }) - .map(|task| task.run_id.as_str()) -} - -pub(super) fn next_pending_swarm_task<'a>( - runtime: &'a AgentRuntimeResult, - session_id: &str, - run_profile: &str, - expected_source: Option<&str>, -) -> Option<&'a AgentRuntimeTaskRecord> { - runtime.recent_tasks.iter().find(|task| { - task.session_id == session_id - && task.run_profile == run_profile - && expected_source.is_none_or(|source| task.source == source) - && task.status == "pending" - && task.phase == "queued" - }) -} - -pub(super) fn swarm_parent_task_for_run<'a>( - parent_agent_id: &str, - session_id: &str, - run_profile: &str, - expected_source: Option<&str>, - expected_run_id: &str, - runtimes: &'a [AgentRuntimeResult], -) -> Option<&'a AgentRuntimeTaskRecord> { - swarm_parent_runtime( - parent_agent_id, - session_id, - run_profile, - expected_source, - runtimes, - ) - .and_then(|runtime| { - runtime.recent_tasks.iter().find(|task| { - task.agent_id == parent_agent_id - && task.session_id == session_id - && task.run_profile == run_profile - && expected_source.is_none_or(|source| task.source == source) - && task.run_id == expected_run_id - }) - }) -} - -pub(super) fn runtime_result_from_task_record(task: &AgentRuntimeTaskRecord) -> AgentRuntimeResult { - let mut state = default_game_creator_agent_runtime_state(&task.agent_id, &task.run_id); - state.task_id = task.task_id.clone(); - state.session_id = task.session_id.clone(); - state.source = task.source.clone(); - state.run_profile = task.run_profile.clone(); - state.run_profile_binding_fingerprint = task.run_profile_binding_fingerprint.clone(); - state.parent_agent_id = task.parent_agent_id.clone(); - state.parent_run_id = task.parent_run_id.clone(); - state.delegation_id = task.delegation_id.clone(); - state.goal_id = task.goal_id.clone(); - state.goal_revision = task.goal_revision; - state.goal_status = task.goal_status.clone(); - state.current_task = task.task.clone(); - state.status = task.status.clone(); - state.phase = task.phase.clone(); - state.current_action = task.current_action.clone(); - state.error = task.error.clone(); - state.updated_at = task.updated_at; - AgentRuntimeResult { - state, - accepted_run_id: None, - session_path: String::new(), - event_path: String::new(), - task_path: String::new(), - task_queue: AgentRuntimeTaskQueueSummary::default(), - recent_events: Vec::new(), - recent_tasks: vec![task.clone()], - response_stream: None, - user_input_request: None, - } -} - -pub(super) fn swarm_parent_runtime_snapshot_for_run( - root: &Path, - parent_agent_id: &str, - session_id: &str, - run_profile: &str, - expected_source: Option<&str>, - expected_run_id: &str, - runtimes: &[AgentRuntimeResult], -) -> Result, String> { - if let Some(runtime) = swarm_parent_runtime_for_run( - parent_agent_id, - session_id, - run_profile, - expected_source, - expected_run_id, - runtimes, - ) { - return Ok(Some(runtime.clone())); - } - if let Some(task) = read_latest_game_creator_agent_runtime_task_by_run_id( - root, - parent_agent_id, - expected_run_id, - )? { - if task.agent_id != parent_agent_id - || task.session_id != session_id - || task.run_profile != run_profile - || expected_source.is_some_and(|source| task.source != source) - || task.run_id != expected_run_id - { - return Err("Swarm parent task journal 与目标 Session/run 不匹配".to_string()); - } - return Ok(Some(runtime_result_from_task_record(&task))); - } - Ok(swarm_parent_task_for_run( - parent_agent_id, - session_id, - run_profile, - expected_source, - expected_run_id, - runtimes, - ) - .map(runtime_result_from_task_record)) -} - -pub(super) fn swarm_turn_is_busy( - root: &Path, - parent_agent_id: &str, - session_id: &str, - run_profile: &str, - expected_source: Option<&str>, - expected_run_id: &str, - runtimes: &[AgentRuntimeResult], -) -> Result { - let Some(parent) = swarm_parent_runtime_snapshot_for_run( - root, - parent_agent_id, - session_id, - run_profile, - expected_source, - expected_run_id, - runtimes, - )? - else { - return Ok(false); - }; - Ok(matches!( - parent.state.status.as_str(), - "pending" - | "running" - | "waiting-for-confirmation" - | "waiting-for-user-input" - | "cancelling" - ) || parent.state.phase == "needs-reconciliation") -} - -pub(super) fn swarm_current_runtimes_for_run( - parent_agent_id: &str, - session_id: &str, - run_profile: &str, - expected_source: Option<&str>, - expected_run_id: &str, - runtimes: &[AgentRuntimeResult], -) -> Vec { - runtimes - .iter() - .filter(|runtime| { - (runtime.state.agent_id == parent_agent_id - && runtime.state.session_id == session_id - && runtime.state.run_profile == run_profile - && expected_source.is_none_or(|source| runtime.state.source == source) - && runtime.state.run_id == expected_run_id) - || (runtime.state.parent_agent_id.as_deref() == Some(parent_agent_id) - && runtime.state.parent_run_id.as_deref() == Some(expected_run_id)) - }) - .cloned() - .collect() -} - -pub(super) fn runtime_terminal_failure_kind(runtime: &AgentRuntimeResult) -> Option<&'static str> { - if runtime.state.phase == "needs-reconciliation" { - None - } else if runtime.state.phase == "budget-exhausted" { - Some("budget-exhausted") - } else if runtime.state.status == "cancelled" || runtime.state.phase == "cancelled" { - Some("cancelled") - } else if runtime.state.phase == "completion-contract-failed" { - Some("completion-contract-failed") - } else if runtime.state.status == "failed" || runtime.state.phase == "failed" { - Some("failed") - } else { - None - } -} - -pub(super) fn parent_runtime_is_active(runtime: &AgentRuntimeResult) -> bool { - runtime.state.phase != "needs-reconciliation" - && (matches!( - runtime.state.status.as_str(), - "pending" - | "running" - | "waiting-for-confirmation" - | "waiting-for-user-input" - | "cancelling" - ) || runtime.recent_tasks.iter().any(|task| { - task.run_id == runtime.state.run_id - && matches!( - task.status.as_str(), - "pending" - | "running" - | "waiting-for-confirmation" - | "waiting-for-user-input" - | "cancelling" - ) - })) -} - -pub(super) fn static_delegate_delivery_has_repairable_contract( - delivery: &StaticDelegateDeliveryRecord, -) -> bool { - delivery.repair_of_delegation_id.is_none() - && delivery.status != StaticDelegateDeliveryStatus::Suppressed - && (!delivery.acceptance_criteria.is_empty() || !delivery.expected_artifacts.is_empty()) - && delivery.structured_result.as_ref().is_none_or(|result| { - result.contract_status == StaticDelegateContractStatus::NeedsRepair - }) -} - -pub(super) fn classify_failed_specialist( - parent: &AgentRuntimeResult, - child: &AgentRuntimeResult, - delivery: Option<&StaticDelegateDeliveryRecord>, - successful_repair: bool, -) -> SwarmSpecialistFailureDisposition { - let delivery_matches = delivery.is_some_and(|delivery| { - child.state.source == "agent-delegate" - && child.state.parent_agent_id.as_deref() == Some(parent.state.agent_id.as_str()) - && child.state.parent_run_id.as_deref() == Some(parent.state.run_id.as_str()) - && child.state.delegation_id.as_deref() == Some(delivery.delegation_id.as_str()) - && delivery.parent_agent_id == parent.state.agent_id - && delivery.parent_session_id == parent.state.session_id - && delivery.parent_run_id == parent.state.run_id - && delivery.target_agent_id == child.state.agent_id - && delivery.target_session_id == child.state.session_id - && delivery.target_run_id == child.state.run_id - }); - if !delivery_matches { - return SwarmSpecialistFailureDisposition::Failed; - } - let delivery = delivery.expect("matching delivery exists"); - if !static_delegate_delivery_has_repairable_contract(delivery) { - return SwarmSpecialistFailureDisposition::Failed; - } - if successful_repair { - return SwarmSpecialistFailureDisposition::Recoverable; - } - if parent_runtime_is_active(parent) { - SwarmSpecialistFailureDisposition::Recoverable - } else { - SwarmSpecialistFailureDisposition::Incomplete - } -} - -pub(super) fn original_delivery_has_successful_repair( - original: &StaticDelegateDeliveryRecord, - claimed_deliveries: &[StaticDelegateDeliveryRecord], -) -> bool { - original.repair_of_delegation_id.is_none() - && !original - .structured_result - .as_ref() - .is_some_and(|result| result.contract_status.is_unknown()) - && claimed_deliveries.iter().any(|candidate| { - candidate.repair_of_delegation_id.as_deref() == Some(original.delegation_id.as_str()) - && candidate.terminal_status.as_deref() == Some("completed") - && candidate.structured_result.as_ref().is_some_and(|result| { - result.contract_status == StaticDelegateContractStatus::EvidenceReady - }) - }) -} - -pub(super) fn scan_swarm_terminal_failures_at( - root: &Path, - parent_agent_id: &str, - session_id: &str, - run_profile: &str, - expected_source: Option<&str>, - expected_parent_run_id: &str, - runtimes: &[AgentRuntimeResult], -) -> SwarmTerminalFailureScan { - let mut scan = SwarmTerminalFailureScan::default(); - let canonical_parent = swarm_parent_runtime_for_run( - parent_agent_id, - session_id, - run_profile, - expected_source, - expected_parent_run_id, - runtimes, - ); - let journal_parent_task = match read_latest_game_creator_agent_runtime_task_by_run_id( - root, - parent_agent_id, - expected_parent_run_id, - ) { - Ok(Some(task)) - if task.agent_id == parent_agent_id - && task.session_id == session_id - && task.run_profile == run_profile - && expected_source.is_none_or(|source| task.source == source) - && task.run_id == expected_parent_run_id => - { - Some(task) - } - Ok(Some(_)) => { - scan.reconciliation_agents.push(parent_agent_id.to_string()); - return scan; - } - Ok(None) => None, - Err(_) => { - scan.reconciliation_agents.push(parent_agent_id.to_string()); - return scan; - } - }; - let parent_task = journal_parent_task.as_ref().or_else(|| { - swarm_parent_task_for_run( - parent_agent_id, - session_id, - run_profile, - expected_source, - expected_parent_run_id, - runtimes, - ) - }); - if canonical_parent.is_none() - && parent_task.is_none_or(|task| game_creator_agent_runtime_terminal_status(task).is_none()) - { - return scan; - } - let parent = canonical_parent - .cloned() - .or_else(|| parent_task.map(runtime_result_from_task_record)) - .expect("target parent runtime or terminal task exists"); - let claimed_deliveries = match claimed_static_delegate_deliveries_at( - root, - &parent.state.agent_id, - &parent.state.run_id, - ) { - Ok(deliveries) => deliveries, - Err(_) => { - scan.reconciliation_agents - .push(parent.state.agent_id.clone()); - return scan; - } - }; - if let Some(kind) = runtime_terminal_failure_kind(&parent) { - scan.failed_agents - .push(format!("{}:{kind}", parent.state.agent_id)); - } - let task_matches_parent = |task: &AgentRuntimeTaskRecord| { - task.source == "agent-delegate" - && task.parent_agent_id.as_deref() == Some(parent.state.agent_id.as_str()) - && task.parent_run_id.as_deref() == Some(parent.state.run_id.as_str()) - }; - let mut specialist_agent_ids = runtimes - .iter() - .map(|runtime| runtime.state.agent_id.clone()) - .filter(|agent_id| agent_id != &parent.state.agent_id) - .collect::>(); - specialist_agent_ids.extend( - claimed_deliveries - .iter() - .map(|delivery| delivery.target_agent_id.clone()), - ); - - // recent_tasks remains a compatibility fallback for in-memory callers, while every - // discoverable specialist journal below overwrites it with append-order latest records. - let mut latest_child_tasks_by_identity = - BTreeMap::<(String, String), AgentRuntimeTaskRecord>::new(); - for runtime in runtimes { - for task in runtime - .recent_tasks - .iter() - .filter(|task| task_matches_parent(task)) - { - latest_child_tasks_by_identity - .insert((task.agent_id.clone(), task.run_id.clone()), task.clone()); - } - } - for agent_id in specialist_agent_ids { - let path = game_creator_agent_runtime_task_path(root, &agent_id); - let tasks = match read_all_game_creator_agent_runtime_tasks(&path) { - Ok(tasks) => tasks, - Err(_) => { - scan.reconciliation_agents.push(agent_id); - continue; - } - }; - for task in tasks.into_iter().filter(|task| task_matches_parent(task)) { - if task.agent_id != agent_id { - scan.reconciliation_agents.push(agent_id.clone()); - continue; - } - latest_child_tasks_by_identity - .insert((task.agent_id.clone(), task.run_id.clone()), task); - } - } - - let mut failed_children_by_identity = latest_child_tasks_by_identity - .into_iter() - .filter_map(|(identity, task)| { - let runtime = runtime_result_from_task_record(&task); - runtime_terminal_failure_kind(&runtime).map(|_| (identity, runtime)) - }) - .collect::>(); - for runtime in runtimes.iter().filter(|runtime| { - runtime.state.source == "agent-delegate" - && runtime.state.parent_agent_id.as_deref() == Some(parent.state.agent_id.as_str()) - && runtime.state.parent_run_id.as_deref() == Some(parent.state.run_id.as_str()) - }) { - if runtime_terminal_failure_kind(runtime).is_some() { - failed_children_by_identity.insert( - (runtime.state.agent_id.clone(), runtime.state.run_id.clone()), - runtime.clone(), - ); - } - } - for child in failed_children_by_identity.values() { - let Some(delegation_id) = child - .state - .delegation_id - .as_deref() - .filter(|value| !value.is_empty()) - else { - scan.failed_agents.push(format!( - "{}:{}", - child.state.agent_id, - runtime_terminal_failure_kind(child).unwrap_or("failed") - )); - continue; - }; - let delivery = match read_static_delegate_delivery_at(root, delegation_id) { - Ok(Some(delivery)) => delivery, - Ok(None) | Err(_) => { - scan.reconciliation_agents - .push(child.state.agent_id.clone()); - continue; - } - }; - let successful_repair = - original_delivery_has_successful_repair(&delivery, &claimed_deliveries); - match classify_failed_specialist(&parent, child, Some(&delivery), successful_repair) { - SwarmSpecialistFailureDisposition::Recoverable => {} - SwarmSpecialistFailureDisposition::Failed => scan.failed_agents.push(format!( - "{}:{}", - child.state.agent_id, - runtime_terminal_failure_kind(child).unwrap_or("failed") - )), - SwarmSpecialistFailureDisposition::Incomplete => scan - .incomplete_reasons - .push(format!("repair-required:{}", child.state.agent_id)), - } - } - scan.failed_agents.sort(); - scan.failed_agents.dedup(); - scan.incomplete_reasons.sort(); - scan.incomplete_reasons.dedup(); - scan.reconciliation_agents.sort(); - scan.reconciliation_agents.dedup(); - scan -} - -pub(super) fn swarm_unhandled_interaction_reasons( - parent_agent_id: &str, - runtimes: &[AgentRuntimeResult], - input_closed: bool, -) -> Vec { - let mut reasons = Vec::new(); - for runtime in runtimes { - let waiting_for_confirmation = runtime.state.status == "waiting-for-confirmation" - || runtime.state.pending_tool_action.is_some() - || runtime.task_queue.waiting_for_confirmation > 0; - let waiting_for_user_input = runtime.state.status == "waiting-for-user-input" - || runtime.user_input_request.is_some() - || runtime.task_queue.waiting_for_user_input > 0; - if input_closed && waiting_for_confirmation { - reasons.push(format!("pending-confirmation:{}", runtime.state.agent_id)); - } - if waiting_for_user_input && (input_closed || runtime.state.agent_id != parent_agent_id) { - reasons.push(format!("pending-user-input:{}", runtime.state.agent_id)); - } - } - reasons.sort(); - reasons.dedup(); - reasons -} - -pub(super) fn parent_runtime_completed(runtime: &AgentRuntimeResult) -> bool { - runtime.state.phase == "completed" - && matches!(runtime.state.status.as_str(), "idle" | "completed") -} - -pub(super) fn classify_swarm_turn_terminal( - parent: Option<&AgentRuntimeResult>, - conversation_metrics: SwarmTurnConversationMetrics, - failed_runtime_count: usize, - pending_interaction_count: usize, - completion_blocker_count: usize, -) -> SwarmTurnTerminalClassification { - if failed_runtime_count > 0 - || parent.is_some_and(|runtime| runtime_terminal_failure_kind(runtime).is_some()) - { - return SwarmTurnTerminalClassification::Failed; - } - if parent.is_none_or(|runtime| !parent_runtime_completed(runtime)) - || pending_interaction_count > 0 - || completion_blocker_count > 0 - || conversation_metrics.new_assistant_message_count != 1 - || conversation_metrics.final_reply_chars == 0 - { - return SwarmTurnTerminalClassification::Incomplete; - } - SwarmTurnTerminalClassification::Settled -} - -pub(super) fn append_swarm_terminal_snapshot_reasons( - reasons: &mut Vec, - parent: Option<&AgentRuntimeResult>, - conversation_metrics: SwarmTurnConversationMetrics, -) { - match parent { - None => reasons.push("parent-runtime-missing".to_string()), - Some(parent) if !parent_runtime_completed(parent) => reasons.push(format!( - "parent-not-completed:{}:{}", - parent.state.status, parent.state.phase - )), - Some(_) => {} - } - if conversation_metrics.new_assistant_message_count != 1 { - reasons.push(format!( - "assistant-count={}", - conversation_metrics.new_assistant_message_count - )); - } else if conversation_metrics.final_reply_chars == 0 { - reasons.push("assistant-empty".to_string()); - } - reasons.sort(); - reasons.dedup(); -} - -pub(super) fn swarm_parent_completion_contract_blockers_at( - root: &Path, - parent: &AgentRuntimeResult, -) -> Vec { - let mut blockers = Vec::new(); - let agent_id = parent.state.agent_id.as_str(); - let run_id = parent.state.run_id.as_str(); - if let Some(blocker) = structured_plan_completion_blocker(&parent.state) { - blockers.push(blocker.tool); - } - if parent.state.goal_id.is_some() - && !matches!( - parent.state.goal_status.as_deref(), - Some(AGENT_GOAL_STATUS_COMPLETED | AGENT_GOAL_STATUS_CLEARED) - ) - { - blockers.push("runtime.goal".to_string()); - } - if parent.state.pending_tool_action.is_some() { - blockers.push("runtime.pending_tool_action".to_string()); - } - match crate::provider_retry::read_for_run_at(root, agent_id, run_id) { - Ok(None) => {} - Ok(Some(_)) | Err(_) => blockers.push("runtime.provider_retry".to_string()), - } - let provider_action_batch_path = - game_creator_agent_runtime_provider_action_batch_path(root, agent_id, run_id); - if provider_action_batch_path.exists() - || agent_runtime_json_sidecar_backup_path(&provider_action_batch_path).exists() - { - blockers.push("runtime.provider_action_batch".to_string()); - } - let finalization_path = game_creator_agent_runtime_finalization_path(root, agent_id, run_id); - if finalization_path.exists() - || agent_runtime_json_sidecar_backup_path(&finalization_path).exists() - { - blockers.push("runtime.finalization".to_string()); - } - if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID { - match resolve_supervisor_collaboration_policy_for_run_at(root, agent_id, run_id) { - Ok(resolution) => { - match read_supervisor_collaboration_state_at(root, agent_id, run_id) { - Ok(state) => { - if supervisor_collaboration_completion_gap(&resolution.policy, &state) - .is_some() - { - blockers.push("runtime.collaboration_policy".to_string()); - } - } - Err(_) => blockers.push("runtime.collaboration_policy".to_string()), - } - } - Err(_) => blockers.push("runtime.collaboration_policy".to_string()), - } - } - if let Some(blocker) = process_session_completion_blocker_at(root, agent_id, run_id) { - blockers.push(blocker.tool); - } - if let Some(blocker) = isolated_join_completion_blocker_at(root, agent_id, run_id) { - blockers.push(blocker.tool); - } - if let Some(blocker) = static_delegate_completion_blocker_at(root, agent_id, run_id) { - blockers.push(blocker.tool); - } - if let Some(blocker) = project_verification_completion_blocker_at(root, agent_id, run_id, &[]) { - blockers.push(blocker.tool); - } - blockers.sort(); - blockers.dedup(); - blockers -} - -pub(super) fn swarm_reconciliation_agents(runtimes: &[AgentRuntimeResult]) -> Vec { - runtimes - .iter() - .filter(|runtime| { - runtime.state.phase == "needs-reconciliation" - || (runtime.state.status == "waiting-for-confirmation" - && runtime.state.pending_tool_action.is_none()) - || (runtime.state.status == "waiting-for-user-input" - && runtime.user_input_request.is_none()) - }) - .map(|runtime| runtime.state.agent_id.clone()) - .collect() -} - -pub(super) fn build_reconciliation_turn_outcome( - root: &Path, - parent_agent_id: &str, - session_id: &str, - conversation_baseline: &SwarmTurnConversationBaseline, - runtimes: &[AgentRuntimeResult], - mut agent_ids: Vec, -) -> Result { - agent_ids.sort(); - agent_ids.dedup(); - let conversation_metrics = - read_turn_conversation_metrics(root, parent_agent_id, session_id, conversation_baseline)?; - let report = build_swarm_turn_report( - SwarmTurnReportOutcome::NeedsReconciliation, - parent_agent_id, - session_id, - Some(&conversation_baseline.parent_run_id), - runtimes, - conversation_metrics, - agent_ids.len(), - )?; - Ok(SwarmTurnOutcome::NeedsReconciliation { agent_ids, report }) -} - -pub(super) fn build_failed_turn_outcome( - root: &Path, - parent_agent_id: &str, - session_id: &str, - conversation_baseline: &SwarmTurnConversationBaseline, - runtimes: &[AgentRuntimeResult], - mut agent_ids: Vec, -) -> Result { - agent_ids.sort(); - agent_ids.dedup(); - let conversation_metrics = - read_turn_conversation_metrics(root, parent_agent_id, session_id, conversation_baseline)?; - let report = build_swarm_turn_report( - SwarmTurnReportOutcome::Failed, - parent_agent_id, - session_id, - Some(&conversation_baseline.parent_run_id), - runtimes, - conversation_metrics, - 0, - )?; - Ok(SwarmTurnOutcome::Failed { agent_ids, report }) -} - -pub(super) fn build_incomplete_turn_outcome( - root: &Path, - parent_agent_id: &str, - session_id: &str, - conversation_baseline: &SwarmTurnConversationBaseline, - runtimes: &[AgentRuntimeResult], - mut reasons: Vec, -) -> Result { - reasons.sort(); - reasons.dedup(); - if reasons.is_empty() { - reasons.push("terminal-contract-not-proven".to_string()); - } - let conversation_metrics = - read_turn_conversation_metrics(root, parent_agent_id, session_id, conversation_baseline)?; - let report = build_swarm_turn_report( - SwarmTurnReportOutcome::Incomplete, - parent_agent_id, - session_id, - Some(&conversation_baseline.parent_run_id), - runtimes, - conversation_metrics, - 0, - )?; - Ok(SwarmTurnOutcome::Incomplete { reasons, report }) -} diff --git a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/tests.rs b/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/tests.rs deleted file mode 100644 index 9c3c1aedf..000000000 --- a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/tests.rs +++ /dev/null @@ -1,2417 +0,0 @@ -use super::*; - -fn runtime(status: &str, phase: &str, pending: u32) -> AgentRuntimeResult { - let state = serde_json::from_value::(serde_json::json!({ - "agentId": "code-prototype", - "runId": "run-test", - "status": status, - "phase": phase, - })) - .expect("deserialize runtime fixture"); - let mut task_queue = AgentRuntimeTaskQueueSummary::default(); - task_queue.pending = pending; - AgentRuntimeResult { - state, - accepted_run_id: None, - session_path: String::new(), - event_path: String::new(), - task_path: String::new(), - task_queue, - recent_events: Vec::new(), - recent_tasks: Vec::new(), - response_stream: None, - user_input_request: None, - } -} - -fn response_stream( - request_slot: &str, - response_revision: u64, - sequence: u64, - status: &str, - accumulated_text: &str, -) -> AgentRuntimeResponseStream { - AgentRuntimeResponseStream { - schema_version: "game-creator-runtime-response-stream.v1".to_string(), - agent_id: "code-prototype".to_string(), - task_id: "code-prototype".to_string(), - session_id: "session-test".to_string(), - run_id: "run-test".to_string(), - request_kind: "final-reply".to_string(), - request_slot: request_slot.to_string(), - applied_steer_cursor: 0, - response_revision, - sequence, - status: status.to_string(), - accumulated_text: accumulated_text.to_string(), - finish_reason: None, - started_at: 100, - updated_at: 100 + sequence, - } -} - -fn runtime_with_response_stream(stream: AgentRuntimeResponseStream) -> AgentRuntimeResult { - let mut snapshot = runtime("running", "response", 0); - snapshot.state.session_id = stream.session_id.clone(); - snapshot.response_stream = Some(stream); - snapshot -} - -#[test] -fn new_supervisor_runs_fix_cli_source_and_select_requested_profile() { - for run_profile in [ - AGENT_RUNTIME_RUN_PROFILE_STANDARD, - AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD, - ] { - assert_eq!( - resolve_swarm_new_run_launch( - GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - run_profile, - AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE, - ) - .expect("resolve supervisor launch"), - SwarmNewRunLaunch::ProjectSupervisor { - source: AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE, - run_profile, - } - ); - } -} - -#[test] -fn explicit_parent_debug_keeps_standard_profile_only() { - assert_eq!( - resolve_swarm_new_run_launch( - "code-prototype", - AGENT_RUNTIME_RUN_PROFILE_STANDARD, - AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE, - ) - .expect("resolve explicit parent debug launch"), - SwarmNewRunLaunch::ExplicitParentDebug, - ); - assert!(resolve_swarm_new_run_launch( - "code-prototype", - AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD, - AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE, - ) - .expect_err("autonomous profile must stay on the supervisor root run") - .contains(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID)); - assert!(resolve_swarm_new_run_launch( - GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - "unsupported", - AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE, - ) - .is_err()); -} - -#[test] -fn same_run_steer_preserves_bound_autonomous_profile() { - let root = std::env::temp_dir().join(format!( - "swarm-cli-profile-steer-{}-{}", - std::process::id(), - unix_millis() - )); - init_local_game_project_at(&root, "project-swarm-profile", "Swarm Profile Steer") - .expect("initialize profile steer project"); - let run_id = "swarm-profile-steer-run"; - let binding = bind_game_creator_agent_runtime_run_profile_at( - &root, - GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - run_id, - AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE, - Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD), - None, - ) - .expect("bind autonomous supervisor profile"); - let state = start_game_creator_agent_runtime_task_at( - &root, - GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - "生成一版可试玩项目", - run_id, - AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE, - "准备自主构建", - vec!["实现并验证最小可玩闭环".to_string()], - ) - .expect("start autonomous supervisor runtime"); - - let steered = steer_game_creator_agent_runtime_task_at( - &root, - GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - &state.session_id, - run_id, - "swarm-profile-steer-1", - "保持当前目标并补充触屏操作", - "swarm-cli", - ) - .expect("steer autonomous supervisor runtime"); - - assert_eq!(steered.runtime.state.run_id, run_id); - assert_eq!( - steered.runtime.state.run_profile, - AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD - ); - assert_eq!( - steered.runtime.state.run_profile_binding_fingerprint, - binding.binding_fingerprint - ); - fs::remove_dir_all(root).ok(); -} - -#[test] -fn cross_profile_steer_is_rejected_before_persistent_side_effects() { - let root = std::env::temp_dir().join(format!( - "swarm-cli-profile-mismatch-{}-{}", - std::process::id(), - unix_millis() - )); - init_local_game_project_at(&root, "project-swarm-mismatch", "Swarm Profile Mismatch") - .expect("initialize profile mismatch project"); - let run_id = "swarm-profile-standard-run"; - let state = start_game_creator_agent_runtime_task_at( - &root, - GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - "等待开发者确认的标准任务", - run_id, - AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE, - "准备标准模式运行", - vec!["等待确认".to_string()], - ) - .expect("start standard supervisor runtime"); - assert_eq!( - state.run_profile, AGENT_RUNTIME_RUN_PROFILE_STANDARD, - "fixture must stay standard" - ); - let conversation_before = read_local_conversation_for_session_at( - &root, - Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID), - Some(&state.session_id), - ) - .expect("read conversation before rejected steer"); - - let error = steer_game_creator_agent_runtime_task_for_profile_at( - &root, - GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - &state.session_id, - run_id, - "swarm-profile-mismatch-steer", - "切换为自主构建并继续", - Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD), - "swarm-cli", - ) - .expect_err("cross-profile steer must fail closed"); - assert!(error.contains("Run Profile"), "unexpected error: {error}"); - assert!(!game_creator_agent_runtime_steer_ledger_path( - &root, - GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - run_id, - ) - .exists()); - let conversation_after = read_local_conversation_for_session_at( - &root, - Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID), - Some(&state.session_id), - ) - .expect("read conversation after rejected steer"); - assert_eq!(conversation_after.messages, conversation_before.messages); - let persisted = - read_game_creator_agent_runtime_at(&root, GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID) - .expect("read runtime after rejected steer"); - assert_eq!(persisted.state.run_id, run_id); - assert_eq!( - persisted.state.run_profile, - AGENT_RUNTIME_RUN_PROFILE_STANDARD - ); - fs::remove_dir_all(root).ok(); -} - -#[test] -fn natural_language_is_not_preclassified_by_cli_keywords() { - for message in [ - "你是谁", - "项目在哪里", - "实现登录页并运行测试", - "不要运行任何命令,只解释架构", - "你能做什么,并顺便修复这个问题", - "继续刚才的任务", - ] { - assert_eq!( - parse_swarm_chat_input(message), - Some(SwarmChatInput::Message(message.to_string())), - "{message} must enter the unified Agent interaction loop" - ); - } -} - -#[test] -fn resume_is_an_explicit_control_command() { - assert_eq!( - parse_swarm_chat_input("/resume"), - Some(SwarmChatInput::Resume) - ); -} - -#[test] -fn natural_language_resume_without_active_runtime_starts_a_new_run() { - assert_eq!( - normalize_interaction_action_without_active_runtime(AgentInteractionAction::Resume), - AgentInteractionAction::Execute - ); - assert_eq!( - normalize_interaction_action_without_active_runtime(AgentInteractionAction::Reply( - "记得之前的工作".to_string() - )), - AgentInteractionAction::Reply("记得之前的工作".to_string()) - ); -} - -#[test] -fn parent_runtime_matching_is_scoped_to_requested_profile() { - let mut standard = runtime("running", "planning", 0); - standard.state.agent_id = GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string(); - standard.state.session_id = "session-profile-match".to_string(); - standard.state.run_id = "run-standard".to_string(); - standard.state.run_profile = AGENT_RUNTIME_RUN_PROFILE_STANDARD.to_string(); - let mut autonomous = standard.clone(); - autonomous.state.run_id = "run-autonomous".to_string(); - autonomous.state.run_profile = AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD.to_string(); - let runtimes = vec![standard, autonomous]; - - assert_eq!( - swarm_parent_runtime( - GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - "session-profile-match", - AGENT_RUNTIME_RUN_PROFILE_STANDARD, - None, - &runtimes, - ) - .map(|runtime| runtime.state.run_id.as_str()), - Some("run-standard") - ); - assert_eq!( - swarm_parent_runtime( - GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - "session-profile-match", - AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD, - None, - &runtimes, - ) - .map(|runtime| runtime.state.run_id.as_str()), - Some("run-autonomous") - ); -} - -#[test] -fn completed_parent_with_pending_task_is_busy_but_not_a_steer_target() { - let session_id = "session-pending-after-completed"; - let run_profile = AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD; - let mut completed = runtime("idle", "completed", 1); - completed.state.agent_id = GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string(); - completed.state.session_id = session_id.to_string(); - completed.state.run_id = "run-completed-before-pending".to_string(); - completed.state.run_profile = run_profile.to_string(); - completed.recent_tasks.push( - serde_json::from_value(serde_json::json!({ - "agentId": GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - "taskId": GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - "sessionId": session_id, - "runId": "run-pending-after-completed", - "source": AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE, - "runProfile": run_profile, - "task": "继续补齐游戏功能", - "status": "pending", - "phase": "queued" - })) - .expect("deserialize pending task fixture"), - ); - let runtimes = vec![completed]; - - assert!(runtime_is_busy(&runtimes[0])); - assert!( - swarm_parent_steer_target( - GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - session_id, - run_profile, - None, - None, - &runtimes, - ) - .is_none(), - "queued work must not make the completed canonical run steerable" - ); - assert_eq!( - matching_pending_swarm_run_id( - &runtimes[0], - session_id, - run_profile, - None, - "继续补齐游戏功能", - ), - Some("run-pending-after-completed") - ); -} - -#[test] -fn new_turn_uses_accepted_run_id_instead_of_stale_canonical_state() { - let mut started = runtime("cancelled", "cancelled", 1); - started.state.agent_id = GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string(); - started.state.run_id = "run-old-cancelled".to_string(); - started.accepted_run_id = Some("run-new-pending".to_string()); - - assert_eq!( - accepted_swarm_run_id(&started, "run-requested"), - "run-new-pending" - ); - started.accepted_run_id = None; - assert_eq!( - accepted_swarm_run_id(&started, "run-requested"), - "run-requested" - ); -} - -#[test] -fn queued_start_returns_actual_accepted_run_id_after_collision() { - let root = std::env::temp_dir().join(format!( - "swarm-cli-accepted-run-id-{}-{}", - std::process::id(), - unix_millis() - )); - init_local_game_project_at(&root, "project-accepted-run-id", "Accepted run ID") - .expect("initialize accepted run ID project"); - let runtime_lock = try_acquire_game_creator_agent_runtime_task_lock( - &root, - GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - ) - .expect("acquire supervisor runtime lock") - .expect("supervisor runtime lock available"); - - let first = start_game_creator_supervisor_background_task_for_session_at( - &root, - None, - "第一条排队任务", - "run-collision", - AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE, - AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD, - ) - .expect("queue first colliding run"); - let second = start_game_creator_supervisor_background_task_for_session_at( - &root, - None, - "第二条排队任务", - "run-collision", - AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE, - AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD, - ) - .expect("queue second colliding run"); - - assert_eq!(first.accepted_run_id.as_deref(), Some("run-collision")); - let second_run_id = second - .accepted_run_id - .as_deref() - .expect("second accepted run ID"); - assert_ne!(second_run_id, "run-collision"); - assert!(second_run_id.starts_with("run-collision-dup-")); - let serialized = serde_json::to_value(&second).expect("serialize queued start result"); - assert_eq!(serialized["acceptedRunId"], second_run_id); - assert!(serialized.get("accepted_run_id").is_none()); - - drop(runtime_lock); - fs::remove_dir_all(root).ok(); -} - -#[test] -fn completed_target_turn_settles_after_canonical_advances_to_next_run() { - let root = std::env::temp_dir().join(format!( - "swarm-cli-consecutive-snapshot-{}-{}", - std::process::id(), - unix_millis() - )); - init_local_game_project_at( - &root, - "project-consecutive-snapshot", - "Consecutive run snapshot", - ) - .expect("initialize consecutive snapshot project"); - let session_id = "session-consecutive-runs"; - let run_profile = AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD; - let completed_task = serde_json::from_value::(serde_json::json!({ - "agentId": GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - "taskId": GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - "sessionId": session_id, - "runId": "run-target-completed", - "source": AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE, - "runProfile": run_profile, - "task": "先完成这一轮", - "status": "completed", - "phase": "completed", - "currentAction": "本轮已经完成" - })) - .expect("deserialize completed target task"); - let next_task = serde_json::from_value::(serde_json::json!({ - "agentId": GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - "taskId": GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - "sessionId": session_id, - "runId": "run-next-running", - "source": AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE, - "runProfile": run_profile, - "task": "随后执行下一轮", - "status": "running", - "phase": "planning", - "currentAction": "下一轮正在执行" - })) - .expect("deserialize next running task"); - let mut current = runtime("running", "planning", 0); - current.state.agent_id = GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string(); - current.state.session_id = session_id.to_string(); - current.state.run_id = "run-next-running".to_string(); - current.state.run_profile = run_profile.to_string(); - let task_path = - game_creator_agent_runtime_task_path(&root, GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID); - fs::create_dir_all(task_path.parent().expect("task journal parent")) - .expect("create task journal parent"); - fs::write( - &task_path, - format!( - "{}\n{}\n", - serde_json::to_string(&completed_task).expect("serialize completed target task"), - serde_json::to_string(&next_task).expect("serialize next target task"), - ), - ) - .expect("persist full task journal"); - current.recent_tasks = vec![next_task]; - let runtimes = vec![current]; - - assert!(!swarm_turn_is_busy( - &root, - GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - session_id, - run_profile, - None, - "run-target-completed", - &runtimes, - ) - .expect("read completed target busy state")); - assert!(swarm_turn_is_busy( - &root, - GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - session_id, - run_profile, - None, - "run-next-running", - &runtimes, - ) - .expect("read next target busy state")); - let snapshot = swarm_parent_runtime_snapshot_for_run( - &root, - GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - session_id, - run_profile, - None, - "run-target-completed", - &runtimes, - ) - .expect("read target task journal") - .expect("recover completed target from task journal"); - assert!(parent_runtime_completed(&snapshot)); - - fs::remove_dir_all(root).ok(); -} - -#[test] -fn running_parent_remains_the_only_valid_swarm_steer_target() { - let session_id = "session-running-steer"; - let run_profile = AGENT_RUNTIME_RUN_PROFILE_STANDARD; - let mut running = runtime("running", "waiting-for-provider-retry", 0); - running.state.agent_id = GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string(); - running.state.session_id = session_id.to_string(); - running.state.run_id = "run-running-steer".to_string(); - running.state.run_profile = run_profile.to_string(); - let runtimes = vec![running]; - - assert_eq!( - swarm_parent_steer_target( - GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - session_id, - run_profile, - None, - Some("run-running-steer"), - &runtimes, - ) - .map(|runtime| runtime.state.run_id.as_str()), - Some("run-running-steer") - ); - assert!(swarm_parent_steer_target( - GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - session_id, - run_profile, - None, - Some("another-run"), - &runtimes, - ) - .is_none()); -} - -#[test] -fn parses_chat_commands_without_stealing_normal_messages() { - assert_eq!(parse_swarm_chat_input(" "), None); - assert_eq!( - parse_swarm_chat_input("/agents"), - Some(SwarmChatInput::Agents) - ); - assert_eq!(parse_swarm_chat_input("/exit"), Some(SwarmChatInput::Quit)); - assert_eq!( - parse_swarm_chat_input("/status"), - Some(SwarmChatInput::Status) - ); - assert_eq!( - parse_swarm_chat_input("/compact"), - Some(SwarmChatInput::Compact) - ); - assert_eq!( - parse_swarm_chat_input("让策划和程序并行检查玩法"), - Some(SwarmChatInput::Message( - "让策划和程序并行检查玩法".to_string() - )) - ); -} - -#[test] -fn parses_goal_commands_and_keeps_goal_namespace_out_of_messages() { - assert_eq!( - parse_swarm_chat_input("/goal"), - Some(SwarmChatInput::Goal(SwarmGoalCommand::Status)) - ); - assert_eq!( - parse_swarm_chat_input("/goal status"), - Some(SwarmChatInput::Goal(SwarmGoalCommand::Status)) - ); - assert_eq!( - parse_swarm_chat_input("/goal 完成可玩的战斗循环"), - Some(SwarmChatInput::Goal(SwarmGoalCommand::Start( - "完成可玩的战斗循环".to_string() - ))) - ); - assert_eq!( - parse_swarm_chat_input("/goal edit 增加键盘与触屏验收"), - Some(SwarmChatInput::Goal(SwarmGoalCommand::Edit( - "增加键盘与触屏验收".to_string() - ))) - ); - assert_eq!( - parse_swarm_chat_input("/goal pause"), - Some(SwarmChatInput::Goal(SwarmGoalCommand::Pause)) - ); - assert_eq!( - parse_swarm_chat_input("/goal resume"), - Some(SwarmChatInput::Goal(SwarmGoalCommand::Resume)) - ); - assert_eq!( - parse_swarm_chat_input("/goal clear"), - Some(SwarmChatInput::Goal(SwarmGoalCommand::Clear)) - ); - - for invalid in [ - "/goal-status", - "/goal/status", - "/goal edit", - "/goal pause now", - ] { - assert!(matches!( - parse_swarm_chat_input(invalid), - Some(SwarmChatInput::InvalidGoal(_)) - )); - assert!(!matches!( - parse_swarm_chat_input(invalid), - Some(SwarmChatInput::Message(_)) - )); - } -} - -#[test] -fn swarm_help_lists_the_complete_goal_control_surface() { - let mut output = Vec::new(); - print_swarm_chat_help(&mut output).expect("print swarm help"); - let output = String::from_utf8(output).expect("help output is utf-8"); - - for command in [ - "/status", - "/compact", - "/goal <目标>", - "/goal status", - "/goal edit <目标>", - "/goal pause", - "/goal resume", - "/goal clear", - ] { - assert!(output.contains(command), "missing help command: {command}"); - } -} - -#[test] -fn goal_status_prints_identity_outcome_and_completion_standard() { - let goal = AgentGoalRecord { - schema_version: AGENT_GOAL_SCHEMA_VERSION.to_string(), - project_id: "project-1".to_string(), - goal_id: "goal-1".to_string(), - agent_id: "project-supervisor".to_string(), - session_id: "session-1".to_string(), - run_id: "run-1".to_string(), - revision: 3, - status: AGENT_GOAL_STATUS_ACTIVE.to_string(), - outcome: "完成首个可玩版本".to_string(), - constraints: vec!["不新增平行 Runtime".to_string()], - verification: vec!["键盘与触屏均可完成一局".to_string()], - completion_evidence: Vec::new(), - response_fingerprint: None, - created_at: 1, - pause_requested_at: None, - paused_at: None, - completed_at: None, - cleared_at: None, - error: None, - updated_at: 2, - }; - let mut output = Vec::new(); - print_swarm_goal_status("session-1", Some(&goal), &mut output).expect("print goal status"); - let output = String::from_utf8(output).expect("goal output is utf-8"); - - assert!(output.contains("goal=goal-1 run=run-1 revision=3 status=active")); - assert!(output.contains("[Goal 目标] 完成首个可玩版本")); - assert!(output.contains("[Goal 约束] 不新增平行 Runtime")); - assert!(output.contains("[Goal 完成标准] 键盘与触屏均可完成一局")); -} - -#[test] -fn swarm_stays_busy_for_active_queue_and_reconciliation() { - assert!(runtimes_are_busy(&[runtime("running", "planning", 0)])); - assert!(runtimes_are_busy(&[runtime("idle", "completed", 1)])); - assert!(runtimes_are_busy(&[runtime( - "failed", - "needs-reconciliation", - 0 - )])); - assert!(!runtimes_are_busy(&[runtime("idle", "completed", 0)])); - assert!(!runtimes_are_busy(&[runtime("failed", "failed", 0)])); -} - -#[test] -fn active_turn_eof_closes_input_once_without_requesting_quit() { - let mut input_closed = false; - let mut observer = SwarmRuntimeObserver::default(); - let mut output = Vec::new(); - - mark_swarm_turn_input_closed(&mut input_closed, &mut observer, &mut output) - .expect("close active turn input"); - mark_swarm_turn_input_closed(&mut input_closed, &mut observer, &mut output) - .expect("repeat closed input is idempotent"); - - assert!(input_closed); - let output = String::from_utf8(output).expect("input close output is utf-8"); - assert_eq!(output.matches("[输入已关闭]").count(), 1); - assert!(output.contains("继续运行,等待可信终态")); - assert!(!output.contains("已退出 Agent Swarm Chat")); -} - -#[test] -fn active_turn_eof_keeps_observing_until_parent_completes() { - let root = std::env::temp_dir().join(format!( - "swarm-cli-eof-active-{}-{}", - std::process::id(), - unix_millis() - )); - init_local_game_project_at(&root, "project-swarm-eof", "Swarm EOF active turn") - .expect("initialize EOF project"); - for group in GAME_CREATOR_AGENT_GROUP_DEFINITIONS { - for role in group.roles { - let idle = default_game_creator_agent_runtime_state(role.task_id, "run-eof-idle"); - write_game_creator_agent_runtime_state(&root, &idle) - .expect("persist valid idle specialist state"); - } - } - let parent_agent_id = GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID; - let before = append_local_conversation_message_at( - &root, - Some(parent_agent_id), - LocalConversationMessage { - role: "user".to_string(), - content: "继续完成当前项目".to_string(), - agent_id: Some(parent_agent_id.to_string()), - }, - ) - .expect("append turn user message"); - let session_id = before.session_id.clone().expect("active parent session"); - let mut parent = runtime("running", "planning", 0).state; - parent.agent_id = parent_agent_id.to_string(); - parent.task_id = parent_agent_id.to_string(); - parent.session_id = session_id.clone(); - parent.run_id = "run-eof-active".to_string(); - parent.source = "agent-background-task".to_string(); - parent.current_task = "继续完成当前项目".to_string(); - write_game_creator_agent_runtime_state(&root, &parent).expect("persist active parent"); - let conversation_baseline = - new_swarm_turn_conversation_baseline(before.messages.len(), &parent.run_id); - - let completion_root = root.clone(); - let completion_session_id = session_id.clone(); - let completion = std::thread::spawn(move || { - std::thread::sleep(Duration::from_millis(15)); - append_local_conversation_message_for_session_at( - &completion_root, - Some(parent_agent_id), - Some(&completion_session_id), - LocalConversationMessage { - role: "assistant".to_string(), - content: "已完成可信终态".to_string(), - agent_id: Some(parent_agent_id.to_string()), - }, - ) - .expect("append terminal assistant message"); - parent.status = "idle".to_string(); - parent.phase = "completed".to_string(); - parent.updated_at = unix_timestamp(); - write_game_creator_agent_runtime_state(&completion_root, &parent) - .expect("persist completed parent"); - }); - let (tx, rx) = mpsc::channel(); - tx.send(SwarmInputEvent::Eof).expect("send active turn EOF"); - let mut observer = SwarmRuntimeObserver::default(); - let mut output = Vec::new(); - - let outcome = wait_for_swarm_turn( - &root, - parent_agent_id, - &session_id, - AGENT_RUNTIME_RUN_PROFILE_STANDARD, - None, - conversation_baseline, - &rx, - &mut output, - &mut observer, - Duration::from_millis(2), - Duration::from_millis(8), - ) - .expect("observe active turn after EOF"); - completion.join().expect("join completion writer"); - - let output = String::from_utf8(output).expect("EOF turn output is utf-8"); - let runtime_diagnostics = read_game_creator_agent_runtimes_at(&root) - .expect("read terminal runtime diagnostics") - .into_iter() - .filter(|runtime| runtime.state.phase == "needs-reconciliation") - .map(|runtime| { - format!( - "{}:{}", - runtime.state.agent_id, - runtime.state.error.unwrap_or_default() - ) - }) - .collect::>(); - assert!( - matches!(outcome, SwarmTurnOutcome::Settled(_)), - "unexpected outcome: {outcome:?}; diagnostics={runtime_diagnostics:?}; output={output}" - ); - assert!(output.contains("[输入已关闭]")); - assert!(output.contains("已完成可信终态")); - assert!(!output.contains("已退出 Agent Swarm Chat")); - - fs::remove_dir_all(root).ok(); -} - -#[test] -fn recovered_prebaseline_assistant_counts_once_without_duplicate_output() { - let root = std::env::temp_dir().join(format!( - "swarm-cli-recovered-assistant-{}-{}", - std::process::id(), - unix_millis() - )); - init_local_game_project_at( - &root, - "project-swarm-recovered-assistant", - "Swarm recovered assistant", - ) - .expect("initialize recovered assistant project"); - let parent_agent_id = GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID; - let conversation = append_local_conversation_message_at( - &root, - Some(parent_agent_id), - LocalConversationMessage { - role: "assistant".to_string(), - content: "已在恢复阶段持久化".to_string(), - agent_id: Some(parent_agent_id.to_string()), - }, - ) - .expect("append recovered assistant"); - let session_id = conversation.session_id.expect("active parent session"); - let mut baseline = - new_swarm_turn_conversation_baseline(conversation.messages.len(), "run-recovered"); - baseline.recovered_assistant = Some(SwarmRecoveredAssistant { - run_id: "run-recovered".to_string(), - finalization_id: "finalization-recovered".to_string(), - message_id: "message-recovered".to_string(), - content: "已在恢复阶段持久化".to_string(), - }); - - let snapshot = read_turn_conversation_snapshot(&root, parent_agent_id, &session_id, &baseline) - .expect("read recovered conversation snapshot"); - assert_eq!(snapshot.metrics.new_assistant_message_count, 1); - assert_eq!( - snapshot.metrics.final_reply_chars, - "已在恢复阶段持久化".chars().count() - ); - assert_eq!(snapshot.final_reply.as_deref(), Some("已在恢复阶段持久化")); - assert!(snapshot.recovered_before_observation); - - let mut output = Vec::new(); - let mut observer = SwarmRuntimeObserver::default(); - let metrics = print_new_parent_reply( - &root, - parent_agent_id, - &session_id, - &baseline, - &mut output, - &mut observer, - ) - .expect("print recovered parent reply"); - assert_eq!(metrics, snapshot.metrics); - let output = String::from_utf8(output).expect("recovered output is utf-8"); - assert!(output.contains("父 Agent 回复已在恢复前持久化")); - assert!(!output.contains("已在恢复阶段持久化")); - - fs::remove_dir_all(root).ok(); -} - -#[test] -fn recovered_assistant_cannot_overlap_a_new_terminal_reply() { - let root = std::env::temp_dir().join(format!( - "swarm-cli-recovered-overlap-{}-{}", - std::process::id(), - unix_millis() - )); - init_local_game_project_at( - &root, - "project-swarm-recovered-overlap", - "Swarm recovered overlap", - ) - .expect("initialize recovered overlap project"); - let parent_agent_id = GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID; - let before = - read_local_conversation_for_session_at(root.as_path(), Some(parent_agent_id), None) - .expect("read initial conversation"); - let session_id = before.session_id.expect("active parent session"); - append_local_conversation_message_for_session_at( - &root, - Some(parent_agent_id), - Some(&session_id), - LocalConversationMessage { - role: "assistant".to_string(), - content: "baseline 后的新回复".to_string(), - agent_id: Some(parent_agent_id.to_string()), - }, - ) - .expect("append new terminal reply"); - let mut baseline = new_swarm_turn_conversation_baseline(before.messages.len(), "run-overlap"); - baseline.recovered_assistant = Some(SwarmRecoveredAssistant { - run_id: "run-overlap".to_string(), - finalization_id: "finalization-overlap".to_string(), - message_id: "message-overlap".to_string(), - content: "恢复回复".to_string(), - }); - - let error = read_turn_conversation_snapshot(&root, parent_agent_id, &session_id, &baseline) - .expect_err("recovered and new assistant replies must not be double counted"); - assert!(error.contains("与 baseline 后的新回复重叠")); - - fs::remove_dir_all(root).ok(); -} - -#[test] -fn conversation_snapshot_scopes_consecutive_run_replies_by_message_id() { - let target_message_id = game_creator_agent_runtime_finalization_message_id( - GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - "session-consecutive", - "run-b", - ); - let next_message_id = game_creator_agent_runtime_finalization_message_id( - GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - "session-consecutive", - "run-c", - ); - let (metrics, reply) = summarize_scoped_new_assistant_messages( - [ - ( - "assistant", - "B 的最终回复", - Some(target_message_id.as_str()), - ), - ("assistant", "C 的最终回复", Some(next_message_id.as_str())), - ], - Some(&target_message_id), - ); - assert_eq!(metrics.new_assistant_message_count, 1); - assert_eq!(metrics.final_reply_chars, "B 的最终回复".chars().count()); - assert_eq!(reply, Some("B 的最终回复")); - - let (missing_metrics, missing_reply) = summarize_scoped_new_assistant_messages( - [("assistant", "C 的最终回复", Some(next_message_id.as_str()))], - Some(&target_message_id), - ); - assert_eq!(missing_metrics, SwarmTurnConversationMetrics::default()); - assert_eq!(missing_reply, None); - - let (legacy_metrics, legacy_reply) = summarize_scoped_new_assistant_messages( - [("assistant", "旧格式回复", None)], - Some(&target_message_id), - ); - assert_eq!(legacy_metrics.new_assistant_message_count, 1); - assert_eq!(legacy_reply, Some("旧格式回复")); -} - -#[test] -fn wait_for_turn_keeps_consecutive_run_reply_and_report_scoped() { - let root = std::env::temp_dir().join(format!( - "swarm-cli-consecutive-wait-{}-{}", - std::process::id(), - unix_millis() - )); - init_local_game_project_at(&root, "project-consecutive-wait", "Consecutive wait") - .expect("initialize consecutive wait project"); - let parent_agent_id = GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID; - let before = append_local_conversation_message_at( - &root, - Some(parent_agent_id), - LocalConversationMessage { - role: "user".to_string(), - content: "先完成 B".to_string(), - agent_id: None, - }, - ) - .expect("append B user turn"); - let session_id = before.session_id.expect("active supervisor session"); - let baseline = new_swarm_turn_conversation_baseline(before.messages.len(), "run-b"); - for (run_id, reply) in [("run-b", "B 的最终回复"), ("run-c", "C 的最终回复")] { - append_local_conversation_message_for_session_idempotent_at( - &root, - Some(parent_agent_id), - Some(&session_id), - LocalConversationMessage { - role: "assistant".to_string(), - content: reply.to_string(), - agent_id: None, - }, - &game_creator_agent_runtime_finalization_message_id( - parent_agent_id, - &session_id, - run_id, - ), - ) - .expect("append consecutive assistant reply"); - } - - let task_path = game_creator_agent_runtime_task_path(&root, parent_agent_id); - fs::create_dir_all(task_path.parent().expect("consecutive journal parent")) - .expect("create consecutive journal parent"); - let tasks = [("run-b", "先完成 B"), ("run-c", "再完成 C")] - .into_iter() - .map(|(run_id, task)| { - serde_json::from_value::(serde_json::json!({ - "agentId": parent_agent_id, - "taskId": parent_agent_id, - "sessionId": session_id, - "runId": run_id, - "source": AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE, - "runProfile": AGENT_RUNTIME_RUN_PROFILE_STANDARD, - "task": task, - "status": "completed", - "phase": "completed", - "currentAction": "本轮已完成", - "updatedAt": if run_id == "run-b" { 100 } else { 200 } - })) - .expect("deserialize consecutive task") - }) - .collect::>(); - fs::write( - &task_path, - tasks - .iter() - .map(|task| serde_json::to_string(task).expect("serialize consecutive task")) - .collect::>() - .join("\n") - + "\n", - ) - .expect("persist consecutive task journal"); - let mut current = default_game_creator_agent_runtime_state(parent_agent_id, "run-c"); - current.session_id = session_id.clone(); - current.source = AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE.to_string(); - current.run_profile = AGENT_RUNTIME_RUN_PROFILE_STANDARD.to_string(); - current.status = "idle".to_string(); - current.phase = "completed".to_string(); - current.current_task = "再完成 C".to_string(); - current.current_action = "C 已完成".to_string(); - current.updated_at = 200; - write_game_creator_agent_runtime_state(&root, ¤t) - .expect("persist consecutive canonical state"); - - let (_tx, rx) = mpsc::channel(); - let mut observer = SwarmRuntimeObserver::default(); - let mut output = Vec::new(); - let outcome = wait_for_swarm_turn( - &root, - parent_agent_id, - &session_id, - AGENT_RUNTIME_RUN_PROFILE_STANDARD, - None, - baseline, - &rx, - &mut output, - &mut observer, - Duration::from_millis(1), - Duration::ZERO, - ) - .expect("settle B after canonical advanced to C"); - let SwarmTurnOutcome::Settled(report) = outcome else { - panic!("B must settle independently: {outcome:?}"); - }; - assert_eq!(report.parent_run_id.as_deref(), Some("run-b")); - assert_eq!(report.new_assistant_message_count, 1); - assert_eq!(report.runtime_count, 1); - let output = String::from_utf8(output).expect("consecutive wait output is utf-8"); - assert!(output.contains("B 的最终回复")); - assert!(!output.contains("C 的最终回复")); - - fs::remove_dir_all(root).ok(); -} - -#[test] -fn confirmation_prompt_propagates_eof_as_closed_input() { - let (tx, rx) = mpsc::channel(); - tx.send(SwarmInputEvent::Eof).expect("send eof"); - let mut output = Vec::new(); - - let decision = prompt_swarm_decision( - Path::new("."), - GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - &rx, - &mut output, - "confirm> ", - ) - .expect("EOF is a turn input state, not an error"); - - assert!(matches!(decision, SwarmPromptDecision::InputClosed)); -} - -#[test] -fn terminal_classifier_requires_completed_parent_unique_reply_and_clear_contract() { - let mut parent = runtime("idle", "completed", 0); - parent.state.agent_id = GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string(); - parent.state.session_id = "session-terminal".to_string(); - parent.state.run_id = "run-terminal".to_string(); - let unique_reply = SwarmTurnConversationMetrics { - new_assistant_message_count: 1, - final_reply_chars: 12, - }; - - assert_eq!( - classify_swarm_turn_terminal(Some(&parent), unique_reply, 0, 0, 0), - SwarmTurnTerminalClassification::Settled - ); - assert_eq!( - classify_swarm_turn_terminal(Some(&parent), unique_reply, 0, 1, 0), - SwarmTurnTerminalClassification::Incomplete - ); - assert_eq!( - classify_swarm_turn_terminal(Some(&parent), unique_reply, 0, 0, 1), - SwarmTurnTerminalClassification::Incomplete - ); - assert_eq!( - classify_swarm_turn_terminal( - Some(&parent), - SwarmTurnConversationMetrics::default(), - 0, - 0, - 0, - ), - SwarmTurnTerminalClassification::Incomplete - ); - assert_eq!( - classify_swarm_turn_terminal( - Some(&parent), - SwarmTurnConversationMetrics { - new_assistant_message_count: 2, - final_reply_chars: 12, - }, - 0, - 0, - 0, - ), - SwarmTurnTerminalClassification::Incomplete - ); - assert_eq!( - classify_swarm_turn_terminal(None, unique_reply, 0, 0, 0), - SwarmTurnTerminalClassification::Incomplete - ); -} - -#[test] -fn terminal_classifier_fails_parent_failure_cancel_and_budget_exhaustion() { - let metrics = SwarmTurnConversationMetrics { - new_assistant_message_count: 1, - final_reply_chars: 8, - }; - for (status, phase) in [ - ("failed", "failed"), - ("cancelled", "cancelled"), - ("failed", "budget-exhausted"), - ] { - let parent = runtime(status, phase, 0); - assert_eq!( - classify_swarm_turn_terminal(Some(&parent), metrics, 0, 0, 0), - SwarmTurnTerminalClassification::Failed, - "parent {status}/{phase} must fail closed" - ); - } - let completed = runtime("idle", "completed", 0); - assert_eq!( - classify_swarm_turn_terminal(Some(&completed), metrics, 1, 0, 0), - SwarmTurnTerminalClassification::Failed - ); -} - -#[test] -fn pending_interactions_never_form_a_settled_snapshot() { - let mut parent = runtime("waiting-for-user-input", "waiting-for-user-input", 0); - parent.state.agent_id = GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string(); - let mut child = runtime("waiting-for-user-input", "waiting-for-user-input", 0); - child.state.agent_id = "code-prototype".to_string(); - let mut confirmation = runtime("waiting-for-confirmation", "waiting-for-confirmation", 0); - confirmation.state.agent_id = "quality-review".to_string(); - - assert!(swarm_unhandled_interaction_reasons( - GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - &[parent.clone()], - false, - ) - .is_empty()); - let child_reasons = swarm_unhandled_interaction_reasons( - GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - &[child], - false, - ); - assert_eq!(child_reasons, vec!["pending-user-input:code-prototype"]); - let closed_reasons = swarm_unhandled_interaction_reasons( - GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - &[parent, confirmation], - true, - ); - assert!(closed_reasons - .iter() - .any(|reason| reason == "pending-user-input:project-supervisor")); - assert!(closed_reasons - .iter() - .any(|reason| reason == "pending-confirmation:quality-review")); -} - -#[test] -fn original_specialist_failure_is_recoverable_but_repair_failure_closes() { - let mut parent = runtime("running", "waiting-for-delegate-receipts", 0); - parent.state.agent_id = GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string(); - parent.state.session_id = "session-parent".to_string(); - parent.state.run_id = "run-parent".to_string(); - let mut child = runtime("failed", "failed", 0); - child.state.agent_id = "code-prototype".to_string(); - child.state.session_id = "session-child".to_string(); - child.state.run_id = "run-child".to_string(); - child.state.source = "agent-delegate".to_string(); - child.state.parent_agent_id = Some(parent.state.agent_id.clone()); - child.state.parent_run_id = Some(parent.state.run_id.clone()); - child.state.delegation_id = Some("delivery-original".to_string()); - let acceptance = vec!["交付可运行原型".to_string()]; - let original = new_static_delegate_delivery_with_contract( - &parent.state.agent_id, - &parent.state.session_id, - &parent.state.run_id, - "action-original", - "delivery-original", - &child.state.agent_id, - &child.state.session_id, - &child.state.run_id, - &acceptance, - &[], - None, - ); - - assert_eq!( - classify_failed_specialist(&parent, &child, Some(&original), false), - SwarmSpecialistFailureDisposition::Recoverable - ); - let mut completed_parent = parent.clone(); - completed_parent.state.status = "idle".to_string(); - completed_parent.state.phase = "completed".to_string(); - assert_eq!( - classify_failed_specialist(&completed_parent, &child, Some(&original), false), - SwarmSpecialistFailureDisposition::Incomplete - ); - - child.state.run_id = "run-repair".to_string(); - child.state.delegation_id = Some("delivery-repair".to_string()); - let repair = new_static_delegate_delivery_with_contract( - &parent.state.agent_id, - &parent.state.session_id, - &parent.state.run_id, - "action-repair", - "delivery-repair", - &child.state.agent_id, - &child.state.session_id, - &child.state.run_id, - &acceptance, - &[], - Some("delivery-original"), - ); - assert_eq!( - classify_failed_specialist(&parent, &child, Some(&repair), false), - SwarmSpecialistFailureDisposition::Failed - ); - - let mut successful_repair = repair.clone(); - successful_repair.terminal_status = Some("completed".to_string()); - let mut evidence_ready = StaticDelegateStructuredResult::default(); - evidence_ready.contract_status = StaticDelegateContractStatus::EvidenceReady; - successful_repair.structured_result = Some(evidence_ready); - assert!(original_delivery_has_successful_repair( - &original, - &[successful_repair.clone()] - )); - - let mut unknown_original = original.clone(); - let mut unknown_result = StaticDelegateStructuredResult::default(); - unknown_result.contract_status = - StaticDelegateContractStatus::Unknown("future-contract-status".to_string()); - unknown_original.structured_result = Some(unknown_result); - assert!( - !original_delivery_has_successful_repair(&unknown_original, &[successful_repair]), - "a newer contract status must not be classified as already repaired" - ); -} - -#[test] -fn observer_failure_scan_waits_for_original_repair_and_fails_repair_child() { - let root = std::env::temp_dir().join(format!( - "swarm-cli-repair-scan-{}-{}", - std::process::id(), - unix_millis() - )); - init_local_game_project_at(&root, "project-swarm-repair", "Swarm repair scan") - .expect("initialize repair scan project"); - let mut parent = runtime("running", "waiting-for-delegate-receipts", 0); - parent.state.agent_id = GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string(); - parent.state.session_id = "session-parent".to_string(); - parent.state.run_id = "run-parent".to_string(); - let mut child = runtime("failed", "failed", 0); - child.state.agent_id = "code-prototype".to_string(); - child.state.session_id = "session-child".to_string(); - child.state.run_id = "run-child".to_string(); - child.state.source = "agent-delegate".to_string(); - child.state.parent_agent_id = Some(parent.state.agent_id.clone()); - child.state.parent_run_id = Some(parent.state.run_id.clone()); - child.state.delegation_id = Some("delivery-original".to_string()); - let acceptance = vec!["交付可运行原型".to_string()]; - let original = new_static_delegate_delivery_with_contract( - &parent.state.agent_id, - &parent.state.session_id, - &parent.state.run_id, - "action-original", - "delivery-original", - &child.state.agent_id, - &child.state.session_id, - &child.state.run_id, - &acceptance, - &[], - None, - ); - create_or_read_static_delegate_delivery_at(&root, &original) - .expect("persist original delivery"); - - let original_scan = scan_swarm_terminal_failures_at( - &root, - &parent.state.agent_id, - &parent.state.session_id, - AGENT_RUNTIME_RUN_PROFILE_STANDARD, - None, - &parent.state.run_id, - &[parent.clone(), child.clone()], - ); - assert!(original_scan.failed_agents.is_empty()); - assert!(original_scan.incomplete_reasons.is_empty()); - assert!(original_scan.reconciliation_agents.is_empty()); - - child.state.run_id = "run-repair".to_string(); - child.state.delegation_id = Some("delivery-repair".to_string()); - let repair = new_static_delegate_delivery_with_contract( - &parent.state.agent_id, - &parent.state.session_id, - &parent.state.run_id, - "action-repair", - "delivery-repair", - &child.state.agent_id, - &child.state.session_id, - &child.state.run_id, - &acceptance, - &[], - Some("delivery-original"), - ); - create_or_read_static_delegate_delivery_at(&root, &repair).expect("persist repair delivery"); - let repair_scan = scan_swarm_terminal_failures_at( - &root, - &parent.state.agent_id, - &parent.state.session_id, - AGENT_RUNTIME_RUN_PROFILE_STANDARD, - None, - &parent.state.run_id, - &[parent.clone(), child], - ); - assert_eq!(repair_scan.failed_agents, vec!["code-prototype:failed"]); - assert!(repair_scan.incomplete_reasons.is_empty()); - assert!(repair_scan.reconciliation_agents.is_empty()); - - fs::remove_dir_all(root).ok(); -} - -#[test] -fn failure_scan_reads_all_historical_runs_for_the_same_specialist() { - let root = std::env::temp_dir().join(format!( - "swarm-cli-historical-repair-scan-{}-{}", - std::process::id(), - unix_millis() - )); - init_local_game_project_at(&root, "project-historical-repair", "Historical repair scan") - .expect("initialize historical repair project"); - let mut parent = runtime("running", "waiting-for-delegate-receipts", 0); - parent.state.agent_id = GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string(); - parent.state.session_id = "session-historical-parent".to_string(); - parent.state.run_id = "run-historical-parent".to_string(); - let acceptance = vec!["交付可运行原型".to_string()]; - let original = new_static_delegate_delivery_with_contract( - &parent.state.agent_id, - &parent.state.session_id, - &parent.state.run_id, - "action-historical-original", - "delivery-historical-original", - "code-prototype", - "session-historical-child", - "run-historical-original", - &acceptance, - &[], - None, - ); - let repair = new_static_delegate_delivery_with_contract( - &parent.state.agent_id, - &parent.state.session_id, - &parent.state.run_id, - "action-historical-repair", - "delivery-historical-repair", - "code-prototype", - "session-historical-child", - "run-historical-repair", - &acceptance, - &[], - Some("delivery-historical-original"), - ); - let quality_failure = new_static_delegate_delivery_with_contract( - &parent.state.agent_id, - &parent.state.session_id, - &parent.state.run_id, - "action-historical-quality", - "delivery-historical-quality", - "quality-review", - "session-historical-quality", - "run-historical-repair", - &acceptance, - &[], - Some("delivery-historical-quality-original"), - ); - create_or_read_static_delegate_delivery_at(&root, &original) - .expect("persist historical original delivery"); - create_or_read_static_delegate_delivery_at(&root, &repair) - .expect("persist historical repair delivery"); - create_or_read_static_delegate_delivery_at(&root, &quality_failure) - .expect("persist historical quality delivery"); - - let original_task = serde_json::from_value::(serde_json::json!({ - "agentId": "code-prototype", - "taskId": "code-prototype", - "sessionId": "session-historical-child", - "runId": "run-historical-original", - "source": "agent-delegate", - "runProfile": AGENT_RUNTIME_RUN_PROFILE_STANDARD, - "parentAgentId": GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - "parentRunId": "run-historical-parent", - "delegationId": "delivery-historical-original", - "task": "原始委派", - "status": "failed", - "phase": "failed", - "currentAction": "原始委派失败", - "updatedAt": 100 - })) - .expect("deserialize historical original task"); - let repair_task = serde_json::from_value::(serde_json::json!({ - "agentId": "code-prototype", - "taskId": "code-prototype", - "sessionId": "session-historical-child", - "runId": "run-historical-repair", - "source": "agent-delegate", - "runProfile": AGENT_RUNTIME_RUN_PROFILE_STANDARD, - "parentAgentId": GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - "parentRunId": "run-historical-parent", - "delegationId": "delivery-historical-repair", - "task": "修复委派", - "status": "failed", - "phase": "completion-contract-failed", - "currentAction": "修复交付未通过合同", - "updatedAt": 200 - })) - .expect("deserialize historical repair task"); - let task_path = game_creator_agent_runtime_task_path(&root, "code-prototype"); - fs::create_dir_all(task_path.parent().expect("specialist journal parent")) - .expect("create specialist journal parent"); - fs::write( - &task_path, - format!( - "{}\n{}\n", - serde_json::to_string(&original_task).expect("serialize original task"), - serde_json::to_string(&repair_task).expect("serialize repair task"), - ), - ) - .expect("persist specialist task journal"); - - let quality_task = serde_json::from_value::(serde_json::json!({ - "agentId": "quality-review", - "taskId": "quality-review", - "sessionId": "session-historical-quality", - "runId": "run-historical-repair", - "source": "agent-delegate", - "runProfile": AGENT_RUNTIME_RUN_PROFILE_STANDARD, - "parentAgentId": GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - "parentRunId": "run-historical-parent", - "delegationId": "delivery-historical-quality", - "task": "质量修复复验", - "status": "failed", - "phase": "failed", - "currentAction": "质量修复失败", - "updatedAt": 210 - })) - .expect("deserialize historical quality task"); - let quality_task_path = game_creator_agent_runtime_task_path(&root, "quality-review"); - fs::create_dir_all(quality_task_path.parent().expect("quality journal parent")) - .expect("create quality journal parent"); - fs::write( - &quality_task_path, - format!( - "{}\n", - serde_json::to_string(&quality_task).expect("serialize quality task"), - ), - ) - .expect("persist quality task journal"); - - let mut current_specialist = runtime("running", "planning", 0); - current_specialist.state.agent_id = "code-prototype".to_string(); - current_specialist.state.session_id = "session-historical-child".to_string(); - current_specialist.state.run_id = "run-historical-repair".to_string(); - current_specialist.state.source = "agent-delegate".to_string(); - current_specialist.state.parent_agent_id = Some(parent.state.agent_id.clone()); - current_specialist.state.parent_run_id = Some(parent.state.run_id.clone()); - current_specialist.state.updated_at = 50; - let mut current_quality = runtime("idle", "completed", 0); - current_quality.state.agent_id = "quality-review".to_string(); - current_quality.state.run_id = "run-later-quality".to_string(); - let scan = scan_swarm_terminal_failures_at( - &root, - &parent.state.agent_id, - &parent.state.session_id, - AGENT_RUNTIME_RUN_PROFILE_STANDARD, - None, - &parent.state.run_id, - &[parent.clone(), current_specialist, current_quality], - ); - assert_eq!( - scan.failed_agents, - vec![ - "code-prototype:completion-contract-failed", - "quality-review:failed", - ] - ); - assert!(scan.incomplete_reasons.is_empty()); - assert!(scan.reconciliation_agents.is_empty()); - - fs::remove_dir_all(root).ok(); -} - -#[test] -fn failure_scan_ignores_cancelled_parent_and_children_from_another_run() { - let root = std::env::temp_dir().join(format!( - "swarm-cli-cross-run-failure-scan-{}-{}", - std::process::id(), - unix_millis() - )); - init_local_game_project_at(&root, "project-cross-run-scan", "Cross-run failure scan") - .expect("initialize cross-run failure scan project"); - let session_id = "session-cross-run"; - let run_profile = AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD; - let mut old_parent = runtime("cancelled", "cancelled", 1); - old_parent.state.agent_id = GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string(); - old_parent.state.session_id = session_id.to_string(); - old_parent.state.run_id = "run-old-cancelled".to_string(); - old_parent.state.run_profile = run_profile.to_string(); - - let mut old_child = runtime("failed", "budget-exhausted", 0); - old_child.state.agent_id = "art-asset-plan".to_string(); - old_child.state.session_id = "session-old-child".to_string(); - old_child.state.run_id = "run-old-child".to_string(); - old_child.state.source = "agent-delegate".to_string(); - old_child.state.parent_agent_id = Some(old_parent.state.agent_id.clone()); - old_child.state.parent_run_id = Some(old_parent.state.run_id.clone()); - - let runtimes = vec![old_parent.clone(), old_child]; - let new_turn_scan = scan_swarm_terminal_failures_at( - &root, - &old_parent.state.agent_id, - session_id, - run_profile, - None, - "run-new-pending", - &runtimes, - ); - assert!(new_turn_scan.failed_agents.is_empty()); - assert!(new_turn_scan.incomplete_reasons.is_empty()); - assert!(new_turn_scan.reconciliation_agents.is_empty()); - - let old_turn_scan = scan_swarm_terminal_failures_at( - &root, - &old_parent.state.agent_id, - session_id, - run_profile, - None, - &old_parent.state.run_id, - &runtimes, - ); - assert!(old_turn_scan - .failed_agents - .iter() - .any(|agent| agent == "project-supervisor:cancelled")); - assert!(old_turn_scan - .failed_agents - .iter() - .any(|agent| agent == "art-asset-plan:budget-exhausted")); - - fs::remove_dir_all(root).ok(); -} - -#[test] -fn missing_confirmation_sidecar_is_reported_as_reconciliation() { - let broken = runtime("waiting-for-confirmation", "waiting-for-confirmation", 0); - assert_eq!( - swarm_reconciliation_agents(&[broken]), - vec!["code-prototype".to_string()] - ); -} - -#[test] -fn turn_report_counts_runtime_and_conversation_snapshots() { - let mut parent = runtime("running", "response", 0); - parent.state.agent_id = "project-supervisor".to_string(); - parent.state.session_id = "session-report".to_string(); - parent.state.run_id = "run-parent".to_string(); - - let mut pending_child = runtime("pending", "queued", 0); - pending_child.state.agent_id = "child-code".to_string(); - pending_child.state.run_id = "run-child-pending".to_string(); - pending_child.state.parent_agent_id = Some("project-supervisor".to_string()); - pending_child.state.parent_run_id = Some("run-parent".to_string()); - - let mut confirmation_child = runtime("waiting-for-confirmation", "waiting-for-confirmation", 0); - confirmation_child.state.agent_id = "child-design".to_string(); - confirmation_child.state.run_id = "run-child-confirmation".to_string(); - confirmation_child.state.parent_agent_id = Some("project-supervisor".to_string()); - confirmation_child.state.parent_run_id = Some("run-parent".to_string()); - - let mut input_child = runtime("waiting-for-user-input", "waiting-for-user-input", 0); - input_child.state.agent_id = "child-test".to_string(); - input_child.state.run_id = "run-child-input".to_string(); - input_child.state.parent_agent_id = Some("project-supervisor".to_string()); - input_child.state.parent_run_id = Some("run-parent".to_string()); - - let mut next_turn = runtime("running", "planning", 0); - next_turn.state.agent_id = "project-supervisor".to_string(); - next_turn.state.session_id = "session-report".to_string(); - next_turn.state.run_id = "run-next-turn".to_string(); - let runtimes = vec![ - parent, - pending_child, - confirmation_child, - input_child, - next_turn, - ]; - let (conversation_metrics, final_reply) = summarize_new_assistant_messages([ - ("user", "请继续"), - ("assistant", "阶段回复"), - ("tool", "PRIVATE_OBSERVATION"), - ("assistant", "最终🙂"), - ]); - assert_eq!(final_reply, Some("最终🙂")); - - let report = build_swarm_turn_report( - SwarmTurnReportOutcome::NeedsReconciliation, - "project-supervisor", - "session-report", - Some("run-parent"), - &runtimes, - conversation_metrics, - 1, - ) - .expect("build scoped turn report"); - - assert_eq!(report.schema_version, SWARM_TURN_REPORT_SCHEMA_VERSION); - assert_eq!(report.outcome, SwarmTurnReportOutcome::NeedsReconciliation); - assert_eq!(report.parent_agent_id, "project-supervisor"); - assert_eq!(report.session_id, "session-report"); - assert_eq!(report.parent_run_id.as_deref(), Some("run-parent")); - assert_eq!(report.runtime_count, 4); - assert_eq!(report.busy_runtime_count, 4); - assert_eq!(report.pending_task_count, 1); - assert_eq!(report.running_task_count, 1); - assert_eq!(report.waiting_for_confirmation_count, 1); - assert_eq!(report.waiting_for_user_input_count, 1); - assert_eq!(report.new_assistant_message_count, 2); - assert_eq!(report.final_reply_chars, "最终🙂".chars().count()); - assert_eq!(report.reconciliation_agent_count, 1); -} - -#[test] -fn turn_report_prefers_expected_run_over_stale_canonical_state() { - let root = std::env::temp_dir().join(format!( - "swarm-cli-report-full-journal-{}-{}", - std::process::id(), - unix_millis() - )); - init_local_game_project_at(&root, "project-report-journal", "Report full journal") - .expect("initialize report journal project"); - let mut old_parent = runtime("cancelled", "cancelled", 1); - old_parent.state.agent_id = GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string(); - old_parent.state.session_id = "session-report-stale".to_string(); - old_parent.state.run_id = "run-old-cancelled".to_string(); - let task_path = - game_creator_agent_runtime_task_path(&root, GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID); - fs::create_dir_all(task_path.parent().expect("report journal parent")) - .expect("create report journal parent"); - let pending_task = serde_json::from_value::(serde_json::json!({ - "agentId": GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - "taskId": GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - "sessionId": "session-report-stale", - "runId": "run-new-pending", - "source": AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE, - "runProfile": AGENT_RUNTIME_RUN_PROFILE_STANDARD, - "task": "等待下一轮", - "status": "pending", - "phase": "queued", - "currentAction": "等待 Runner", - "updatedAt": 200 - })) - .expect("deserialize report pending task"); - fs::write( - &task_path, - format!( - "{}\n", - serde_json::to_string(&pending_task).expect("serialize report pending task"), - ), - ) - .expect("persist report task journal"); - old_parent.task_path = task_path.to_string_lossy().into_owned(); - - let report = build_swarm_turn_report( - SwarmTurnReportOutcome::Failed, - GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - "session-report-stale", - Some("run-new-pending"), - &[old_parent], - SwarmTurnConversationMetrics::default(), - 0, - ) - .expect("build stale canonical turn report"); - - assert_eq!(report.parent_run_id.as_deref(), Some("run-new-pending")); - assert_eq!(report.runtime_count, 1); - assert_eq!(report.pending_task_count, 1); - - fs::remove_dir_all(root).ok(); -} - -#[test] -fn turn_report_json_is_single_line_and_omits_sensitive_bodies_and_paths() { - let sensitive_reply = concat!( - "PRIVATE_REPLY_BODY\n", - "/private/project/root ", - "prompt=DO_NOT_LEAK observation=DO_NOT_LEAK CREDENTIAL_SENTINEL" - ); - let (conversation_metrics, _) = - summarize_new_assistant_messages([("assistant", sensitive_reply)]); - let report = build_swarm_turn_report( - SwarmTurnReportOutcome::Settled, - "project-supervisor", - "session-safe", - None, - &[], - conversation_metrics, - 0, - ) - .expect("build safe turn report"); - let json = serde_json::to_string(&report).expect("serialize turn report"); - let value = serde_json::from_str::(&json).expect("parse turn report"); - let object = value.as_object().expect("turn report is an object"); - - assert_eq!(json.lines().count(), 1); - assert_eq!(object.len(), 14); - for key in [ - "schemaVersion", - "outcome", - "parentAgentId", - "sessionId", - "parentRunId", - "runtimeCount", - "busyRuntimeCount", - "pendingTaskCount", - "runningTaskCount", - "waitingForConfirmationCount", - "waitingForUserInputCount", - "newAssistantMessageCount", - "finalReplyChars", - "reconciliationAgentCount", - ] { - assert!(object.contains_key(key), "turn report omitted {key}"); - } - assert_eq!( - value["schemaVersion"], - serde_json::json!(SWARM_TURN_REPORT_SCHEMA_VERSION) - ); - assert_eq!(value["outcome"], serde_json::json!("settled")); - assert_eq!(value["parentRunId"], serde_json::Value::Null); - assert_eq!(value["newAssistantMessageCount"], serde_json::json!(1)); - assert_eq!( - value["finalReplyChars"], - serde_json::json!(sensitive_reply.chars().count()) - ); - for forbidden in [ - "PRIVATE_REPLY_BODY", - "/private/project/root", - "DO_NOT_LEAK", - "CREDENTIAL_SENTINEL", - ] { - assert!(!json.contains(forbidden), "report leaked {forbidden}"); - } -} - -#[test] -fn turn_outcome_prints_all_terminal_reports_but_not_quit() { - let metrics = SwarmTurnConversationMetrics { - new_assistant_message_count: 1, - final_reply_chars: 4, - }; - let settled_report = build_swarm_turn_report( - SwarmTurnReportOutcome::Settled, - "project-supervisor", - "session-settled", - None, - &[], - metrics, - 0, - ) - .expect("build settled turn report"); - let mut settled_output = Vec::new(); - print_turn_outcome( - SwarmTurnOutcome::Settled(settled_report), - &mut settled_output, - ) - .expect("print settled report"); - let settled_output = String::from_utf8(settled_output).expect("settled output is utf-8"); - assert_eq!(settled_output.lines().count(), 1); - assert!(settled_output.starts_with(SWARM_TURN_REPORT_PREFIX)); - assert!(settled_output.contains("\"outcome\":\"settled\"")); - - let failed_report = build_swarm_turn_report( - SwarmTurnReportOutcome::Failed, - "project-supervisor", - "session-failed", - None, - &[], - metrics, - 0, - ) - .expect("build failed turn report"); - let mut failed_output = Vec::new(); - print_turn_outcome( - SwarmTurnOutcome::Failed { - agent_ids: vec!["project-supervisor:budget-exhausted".to_string()], - report: failed_report, - }, - &mut failed_output, - ) - .expect("print failed report"); - let failed_output = String::from_utf8(failed_output).expect("failed output is utf-8"); - assert!(failed_output.starts_with("[已失败]")); - assert!(failed_output.contains("\"outcome\":\"failed\"")); - - let incomplete_report = build_swarm_turn_report( - SwarmTurnReportOutcome::Incomplete, - "project-supervisor", - "session-incomplete", - None, - &[], - metrics, - 0, - ) - .expect("build incomplete turn report"); - let mut incomplete_output = Vec::new(); - print_turn_outcome( - SwarmTurnOutcome::Incomplete { - reasons: vec!["assistant-count=0".to_string()], - report: incomplete_report, - }, - &mut incomplete_output, - ) - .expect("print incomplete report"); - let incomplete_output = - String::from_utf8(incomplete_output).expect("incomplete output is utf-8"); - assert!(incomplete_output.starts_with("[未完成]")); - assert!(incomplete_output.contains("\"outcome\":\"incomplete\"")); - - let reconciliation_report = build_swarm_turn_report( - SwarmTurnReportOutcome::NeedsReconciliation, - "project-supervisor", - "session-reconciliation", - None, - &[], - metrics, - 2, - ) - .expect("build reconciliation turn report"); - let mut reconciliation_output = Vec::new(); - print_turn_outcome( - SwarmTurnOutcome::NeedsReconciliation { - agent_ids: vec!["code-prototype".to_string(), "external-runner".to_string()], - report: reconciliation_report, - }, - &mut reconciliation_output, - ) - .expect("print reconciliation report"); - let reconciliation_output = - String::from_utf8(reconciliation_output).expect("reconciliation output is utf-8"); - let lines = reconciliation_output.lines().collect::>(); - assert_eq!(lines.len(), 2); - assert_eq!( - lines[0], - "[已阻断] 以下 Agent 需要人工 reconciliation:code-prototype, external-runner" - ); - assert!(lines[1].starts_with(SWARM_TURN_REPORT_PREFIX)); - assert!(lines[1].contains("\"outcome\":\"needs-reconciliation\"")); - assert!(lines[1].contains("\"reconciliationAgentCount\":2")); - - let mut quit_output = Vec::new(); - print_turn_outcome(SwarmTurnOutcome::Quit, &mut quit_output).expect("ignore quit"); - assert!(quit_output.is_empty()); -} - -#[test] -fn blank_agent_snapshots_do_not_reset_the_settle_window() { - let mut blank = runtime("idle", "idle", 0); - blank.state.run_id.clear(); - blank.state.updated_at = 100; - let mut observer = SwarmRuntimeObserver::default(); - let mut output = Vec::new(); - assert!(!observer - .print_changes(&[blank.clone()], &mut output) - .expect("observe first blank snapshot")); - blank.state.updated_at = 101; - assert!(!observer - .print_changes(&[blank], &mut output) - .expect("observe refreshed blank snapshot")); - assert!(output.is_empty()); -} - -#[test] -fn runtime_plan_revision_and_current_step_change_state_signature() { - let mut snapshot = runtime("running", "planning", 0); - snapshot.state.updated_at = 100; - snapshot.task_queue.updated_at = 100; - snapshot.state.plan_revision = 1; - snapshot.state.plan_steps = vec![ - AgentRuntimePlanStep { - index: 0, - title: "读取现有 CLI".to_string(), - status: "in_progress".to_string(), - detail: None, - updated_at: 100, - }, - AgentRuntimePlanStep { - index: 1, - title: "补充计划展示".to_string(), - status: "pending".to_string(), - detail: None, - updated_at: 100, - }, - ]; - snapshot.state.active_plan_step_index = Some(0); - - let initial = runtime_state_signature(&snapshot.state, &snapshot.task_queue); - snapshot.state.plan_revision = 2; - let revised = runtime_state_signature(&snapshot.state, &snapshot.task_queue); - assert_ne!(initial, revised); - - snapshot.state.plan_steps[0].title = "核对现有 CLI".to_string(); - let current_step_changed = runtime_state_signature(&snapshot.state, &snapshot.task_queue); - assert_ne!(revised, current_step_changed); - - snapshot.state.plan_steps[0].status = "completed".to_string(); - snapshot.state.plan_steps[1].status = "in_progress".to_string(); - snapshot.state.active_plan_step_index = Some(1); - let advanced = runtime_state_signature(&snapshot.state, &snapshot.task_queue); - assert_ne!(current_step_changed, advanced); -} - -#[test] -fn runtime_plan_output_is_bounded_and_omits_private_observations() { - let mut snapshot = runtime("running", "planning", 0); - snapshot.state.plan_revision = 7; - snapshot.state.plan_explanation = "已完成读取,进入验证".to_string(); - snapshot.state.current_action = "展示持久计划".to_string(); - snapshot.state.waiting_on = "开发者确认".to_string(); - snapshot.state.next_step = "运行 focused cargo test".to_string(); - snapshot.state.observations = vec![ - "PRIVATE_OBSERVATION_SENTINEL".to_string(), - "PRIVATE_DETAIL_SENTINEL".to_string(), - ]; - snapshot.state.plan_steps = (0..10) - .map(|index| AgentRuntimePlanStep { - index, - title: format!("计划步骤 {}", index + 1), - status: match index { - 0 | 1 => "completed", - 2 => "in_progress", - _ => "pending", - } - .to_string(), - detail: Some(format!("PRIVATE_STEP_DETAIL_{index}")), - updated_at: 100, - }) - .collect(); - snapshot.state.active_plan_step_index = Some(2); - - let mut output = Vec::new(); - print_runtime_state(&snapshot.state, &snapshot.task_queue, &mut output) - .expect("print runtime plan progress"); - let output = String::from_utf8(output).expect("runtime output is utf-8"); - - assert!(output.contains( - "[计划] revision=7 completed=2/10 current=#3 [in_progress] 计划步骤 3 | waiting=开发者确认 | next=运行 focused cargo test" - )); - assert!(output.contains("[计划说明] 已完成读取,进入验证")); - assert_eq!(output.matches("[计划步骤]").count(), 8); - assert!(output.contains("[计划步骤] #8 [pending] 计划步骤 8")); - assert!(output.contains("另有 2 条步骤未显示")); - assert!(!output.contains("计划步骤 9")); - assert!(!output.contains("PRIVATE_OBSERVATION_SENTINEL")); - assert!(!output.contains("PRIVATE_DETAIL_SENTINEL")); - assert!(!output.contains("PRIVATE_STEP_DETAIL")); -} - -#[test] -fn response_stream_prints_only_monotonic_utf8_suffixes() { - let mut observer = SwarmRuntimeObserver::default(); - let mut output = Vec::new(); - let mut snapshot = runtime_with_response_stream(response_stream( - "slot-1", - 7, - 0, - AGENT_RUNTIME_RESPONSE_STREAM_STATUS_STREAMING, - "", - )); - - assert!(observer - .print_changes(&[snapshot.clone()], &mut output) - .expect("observe empty response stream")); - snapshot.response_stream = Some(response_stream( - "slot-1", - 7, - 1, - AGENT_RUNTIME_RESPONSE_STREAM_STATUS_STREAMING, - "你", - )); - assert!(observer - .print_changes(&[snapshot.clone()], &mut output) - .expect("observe first utf-8 suffix")); - snapshot.response_stream = Some(response_stream( - "slot-1", - 7, - 2, - AGENT_RUNTIME_RESPONSE_STREAM_STATUS_READY, - "你好🙂", - )); - assert!(observer - .print_changes(&[snapshot.clone()], &mut output) - .expect("observe second utf-8 suffix")); - assert!(!observer - .print_changes(&[snapshot], &mut output) - .expect("ignore duplicate snapshot")); - observer - .close_response_line(&mut output) - .expect("close response line"); - - let output = String::from_utf8(output).expect("stream output is utf-8"); - assert!(output.contains("Agent[code-prototype]> 你好🙂")); - assert_eq!(output.matches("Agent[code-prototype]>").count(), 1); - assert!(!output.contains("你你好")); -} - -#[test] -fn response_stream_resets_for_non_prefix_and_new_request_slot() { - let mut observer = SwarmRuntimeObserver::default(); - let mut output = Vec::new(); - let mut snapshot = runtime_with_response_stream(response_stream( - "slot-1", - 9, - 1, - AGENT_RUNTIME_RESPONSE_STREAM_STATUS_STREAMING, - "旧稿", - )); - observer - .print_changes(&[snapshot.clone()], &mut output) - .expect("observe initial stream"); - - snapshot.response_stream = Some(response_stream( - "slot-1", - 9, - 2, - AGENT_RUNTIME_RESPONSE_STREAM_STATUS_READY, - "修正版", - )); - observer - .print_changes(&[snapshot.clone()], &mut output) - .expect("observe non-prefix correction"); - snapshot.response_stream = Some(response_stream( - "slot-2", - 9, - 1, - AGENT_RUNTIME_RESPONSE_STREAM_STATUS_READY, - "最终版", - )); - observer - .print_changes(&[snapshot], &mut output) - .expect("observe new request slot"); - observer - .close_response_line(&mut output) - .expect("close response line"); - - let output = String::from_utf8(output).expect("stream output is utf-8"); - assert!(output.contains("reason=non-prefix-correction")); - assert!(output.contains("reason=new-request-slot")); - assert_eq!(output.matches("旧稿").count(), 1); - assert_eq!(output.matches("修正版").count(), 1); - assert_eq!(output.matches("最终版").count(), 1); -} - -#[test] -fn response_stream_resets_sequence_for_same_run_steer_cursor() { - let mut observer = SwarmRuntimeObserver::default(); - let mut output = Vec::new(); - let mut initial = response_stream( - "slot-1", - 9, - 4, - AGENT_RUNTIME_RESPONSE_STREAM_STATUS_STREAMING, - "纠偏前回复", - ); - initial.applied_steer_cursor = 1; - observer - .print_changes(&[runtime_with_response_stream(initial)], &mut output) - .expect("observe pre-steer stream"); - - let mut steered = response_stream( - "slot-1", - 9, - 1, - AGENT_RUNTIME_RESPONSE_STREAM_STATUS_READY, - "纠偏后回复", - ); - steered.applied_steer_cursor = 2; - observer - .print_changes(&[runtime_with_response_stream(steered)], &mut output) - .expect("observe same-run stream after steer"); - observer - .close_response_line(&mut output) - .expect("close steered response line"); - - let output = String::from_utf8(output).expect("steer output is utf-8"); - assert!(output.contains("reason=new-steer-cursor")); - assert!(!output.contains("reason=sequence-rollback")); - assert_eq!(output.matches("纠偏前回复").count(), 1); - assert_eq!(output.matches("纠偏后回复").count(), 1); -} - -#[test] -fn response_stream_reconnects_without_repeating_body_and_rejects_sequence_rollback() { - let mut observer = SwarmRuntimeObserver::default(); - let mut output = Vec::new(); - let mut snapshot = runtime_with_response_stream(response_stream( - "slot-1", - 11, - 3, - AGENT_RUNTIME_RESPONSE_STREAM_STATUS_STREAMING, - "已输出", - )); - observer - .print_changes(&[snapshot.clone()], &mut output) - .expect("observe initial stream"); - - snapshot.response_stream = None; - assert!(observer - .print_changes(&[snapshot.clone()], &mut output) - .expect("observe disconnect")); - snapshot.response_stream = Some(response_stream( - "slot-1", - 11, - 3, - AGENT_RUNTIME_RESPONSE_STREAM_STATUS_STREAMING, - "已输出", - )); - assert!(observer - .print_changes(&[snapshot.clone()], &mut output) - .expect("observe reconnect")); - - snapshot.response_stream = Some(response_stream( - "slot-1", - 11, - 2, - AGENT_RUNTIME_RESPONSE_STREAM_STATUS_STREAMING, - "回退正文", - )); - assert!(observer - .print_changes(&[snapshot.clone()], &mut output) - .expect("report sequence rollback")); - assert!(!observer - .print_changes(&[snapshot], &mut output) - .expect("deduplicate repeated rollback")); - - let cursor = observer - .response_streams - .get("code-prototype") - .expect("response cursor"); - assert_eq!(cursor.sequence, 3); - assert_eq!(cursor.accumulated_text, "已输出"); - assert_eq!(cursor.printed_accumulated_text.as_deref(), Some("已输出")); - - let recovered = runtime_with_response_stream(response_stream( - "slot-1", - 11, - 4, - AGENT_RUNTIME_RESPONSE_STREAM_STATUS_READY, - "已输出继续", - )); - assert!(observer - .print_changes(&[recovered], &mut output) - .expect("resume from accepted high-water mark")); - observer - .close_response_line(&mut output) - .expect("close recovered response line"); - - let output = String::from_utf8(output).expect("stream output is utf-8"); - assert!(output.contains("reason=reconnect")); - assert!(output.contains("reason=sequence-rollback")); - assert_eq!(output.matches("已输出").count(), 1); - assert_eq!(output.matches("继续").count(), 1); - assert!(!output.contains("回退正文")); -} - -#[test] -fn settled_parent_reply_is_not_repeated_after_complete_stream() { - let mut observer = SwarmRuntimeObserver::default(); - let mut output = Vec::new(); - let mut snapshot = runtime_with_response_stream(response_stream( - "slot-1", - 13, - 4, - AGENT_RUNTIME_RESPONSE_STREAM_STATUS_READY, - "权威最终回复", - )); - observer - .print_changes(&[snapshot.clone()], &mut output) - .expect("observe complete stream"); - snapshot.response_stream = Some(response_stream( - "slot-1", - 13, - 5, - AGENT_RUNTIME_RESPONSE_STREAM_STATUS_COMMITTED, - "权威最终回复", - )); - observer - .print_changes(&[snapshot], &mut output) - .expect("observe committed stream without printing body"); - observer - .close_response_line(&mut output) - .expect("close response line"); - print_settled_parent_reply( - "code-prototype", - "session-test", - Some("权威最终回复"), - &observer, - &mut output, - ) - .expect("settle streamed reply"); - let (conversation_metrics, _) = - summarize_new_assistant_messages([("assistant", "权威最终回复")]); - let report = build_swarm_turn_report( - SwarmTurnReportOutcome::Settled, - "code-prototype", - "session-test", - None, - &[], - conversation_metrics, - 0, - ) - .expect("build streamed reply turn report"); - print_turn_outcome(SwarmTurnOutcome::Settled(report), &mut output) - .expect("print settled report after stream"); - - let output = String::from_utf8(output).expect("settle output is utf-8"); - assert_eq!(output.matches("权威最终回复").count(), 1); - assert!(output.contains("父 Agent 回复已完整流式输出")); - assert!(output.contains(SWARM_TURN_REPORT_PREFIX)); - - let mut next_run_stream = response_stream( - "slot-next", - 14, - 1, - AGENT_RUNTIME_RESPONSE_STREAM_STATUS_READY, - "下一轮回复", - ); - next_run_stream.run_id = "run-next".to_string(); - let mut next_run_observer = SwarmRuntimeObserver::default(); - let mut next_run_output = Vec::new(); - next_run_observer - .print_changes( - &[runtime_with_response_stream(next_run_stream)], - &mut next_run_output, - ) - .expect("observe next run stream"); - print_settled_parent_reply_for_run( - "code-prototype", - "session-test", - Some("run-target"), - Some("上一轮最终回复"), - &next_run_observer, - &mut next_run_output, - ) - .expect("next run stream must not suppress target reply"); - let next_run_output = String::from_utf8(next_run_output).expect("next run output is utf-8"); - assert!(next_run_output.contains("Agent> 上一轮最终回复")); - - let mut fallback = Vec::new(); - print_settled_parent_reply( - "code-prototype", - "session-test", - Some("未流过的权威回复"), - &SwarmRuntimeObserver::default(), - &mut fallback, - ) - .expect("print authoritative fallback"); - let fallback = String::from_utf8(fallback).expect("fallback output is utf-8"); - assert!(fallback.contains("Agent> 未流过的权威回复")); -} - -#[test] -fn response_stream_status_reports_only_status_sequence_and_char_count() { - let body = "PRIVATE_RESPONSE_BODY"; - let stream = response_stream( - "slot-private", - 17, - 8, - AGENT_RUNTIME_RESPONSE_STREAM_STATUS_READY, - body, - ); - let mut output = Vec::new(); - print_runtime_response_stream_status(Some(&stream), &mut output) - .expect("print response stream status"); - let output = String::from_utf8(output).expect("status output is utf-8"); - - assert_eq!( - output.trim(), - format!( - "[回复流] status=ready sequence=8 chars={}", - body.chars().count() - ) - ); - assert!(!output.contains(body)); - assert!(!output.contains("slot-private")); -} - -#[test] -fn input_channel_preserves_lines_and_eof() { - let (tx, rx) = mpsc::channel(); - tx.send(SwarmInputEvent::Line("hello swarm".to_string())) - .expect("send line"); - tx.send(SwarmInputEvent::Eof).expect("send eof"); - assert_eq!( - receive_swarm_chat_line(&rx).expect("read line").as_deref(), - Some("hello swarm") - ); - assert_eq!(receive_swarm_chat_line(&rx).expect("read eof"), None); -} - -#[test] -fn confirmation_prompt_defers_to_bare_goal_status_without_deciding_action() { - let root = std::env::temp_dir().join(format!( - "swarm-goal-confirmation-{}-{}", - std::process::id(), - unix_millis() - )); - init_local_game_project_at(&root, "project-1", "Goal 确认提示测试") - .expect("initialize Goal prompt project"); - let (tx, rx) = mpsc::channel(); - tx.send(SwarmInputEvent::Line("/goal".to_string())) - .expect("send bare Goal status"); - let mut output = Vec::new(); - - let decision = prompt_swarm_decision( - &root, - GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - &rx, - &mut output, - "", - ) - .expect("handle Goal status during confirmation"); - assert!(matches!(decision, SwarmPromptDecision::Deferred)); - let output = String::from_utf8(output).expect("prompt output is utf-8"); - assert!(output.contains("当前尚未设置持久目标")); - assert!(!output.contains("[已批准]")); - assert!(!output.contains("[已拒绝]")); - - fs::remove_dir_all(root).ok(); -} - -#[test] -fn event_deduplication_keeps_phase_and_detail_changes() { - let base = serde_json::json!({ - "agentId": "code-prototype", - "taskId": "task-1", - "sessionId": "session-1", - "runId": "run-1", - "eventType": "observation", - "status": "running", - "phase": "action", - "summary": "工具观察", - "detail": "第一条", - "updatedAt": 100, - }); - let first = - serde_json::from_value::(base.clone()).expect("deserialize first event"); - let mut changed = base; - changed["phase"] = serde_json::json!("observation"); - changed["detail"] = serde_json::json!("第二条"); - let second = - serde_json::from_value::(changed).expect("deserialize second event"); - assert_ne!(runtime_event_key(&first), runtime_event_key(&second)); -} diff --git a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/turn_dispatch.rs b/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/turn_dispatch.rs deleted file mode 100644 index 0254f6301..000000000 --- a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/turn_dispatch.rs +++ /dev/null @@ -1,590 +0,0 @@ -use super::*; - -pub(super) fn handle_swarm_user_turn( - root: &Path, - parent_agent_id: &str, - session_id: &str, - run_profile: &str, - new_run_launch: SwarmNewRunLaunch<'_>, - message: &str, - input: &Receiver, - output: &mut W, -) -> Result { - let expected_parent_source = new_run_launch.expected_parent_source(); - let before = - read_local_conversation_for_session_at(root, Some(parent_agent_id), Some(session_id))?; - let active_goal_run_id = - if let Some(goal) = read_game_creator_agent_goal_at(root, parent_agent_id, session_id)? { - match goal.status.as_str() { - AGENT_GOAL_STATUS_ACTIVE => Some(goal.run_id), - AGENT_GOAL_STATUS_PAUSE_REQUESTED | AGENT_GOAL_STATUS_PAUSED => { - print_swarm_goal_error(output, "当前 Goal 已暂停;请先输入 /goal resume。")?; - return Ok(SwarmChatFlow::Continue); - } - AGENT_GOAL_STATUS_CLEARING => { - print_swarm_goal_error(output, "当前 Goal 正在清理,暂不接受新消息。")?; - return Ok(SwarmChatFlow::Continue); - } - AGENT_GOAL_STATUS_NEEDS_RECONCILIATION => { - print_swarm_goal_error( - output, - "当前 Goal 需要人工 reconciliation,暂不接受新消息。", - )?; - return Ok(SwarmChatFlow::Continue); - } - AGENT_GOAL_STATUS_COMPLETED | AGENT_GOAL_STATUS_CLEARED => None, - status => { - print_swarm_goal_error( - output, - &format!("当前 Goal 状态未知,已阻止发送:{status}"), - )?; - return Ok(SwarmChatFlow::Continue); - } - } - } else { - None - }; - - let mut runtimes = read_game_creator_agent_runtimes_at(root)?; - if let Some(runtime) = swarm_parent_steer_target( - parent_agent_id, - session_id, - run_profile, - expected_parent_source, - active_goal_run_id.as_deref(), - &runtimes, - ) { - return steer_and_wait_for_swarm_turn( - root, - parent_agent_id, - session_id, - run_profile, - expected_parent_source, - &runtime.state.run_id, - message, - before.messages.len(), - input, - output, - if active_goal_run_id.is_some() { - "Goal 已追加" - } else { - "运行中输入已排队" - }, - ); - } - - let pending_message_is_latest = before - .messages - .last() - .is_some_and(|item| item.role == "user" && item.content.trim() == message.trim()); - let matching_pending_run_id = pending_message_is_latest - .then(|| { - swarm_parent_runtime( - parent_agent_id, - session_id, - run_profile, - expected_parent_source, - &runtimes, - ) - }) - .flatten() - .and_then(|runtime| { - matching_pending_swarm_run_id( - runtime, - session_id, - run_profile, - expected_parent_source, - message, - ) - }) - .map(str::to_string); - let parent_has_queued_work = swarm_parent_runtime( - parent_agent_id, - session_id, - run_profile, - expected_parent_source, - &runtimes, - ) - .is_some_and(runtime_is_busy); - if parent_has_queued_work { - require_external_agent_runner_for_cli_runtime_write(root)?; - resume_game_creator_agent_background_tasks_at(root)?; - runtimes = read_game_creator_agent_runtimes_at(root)?; - - if let Some(run_id) = matching_pending_run_id { - writeln!( - output, - "[恢复] 该消息已在 run={run_id} 落盘,继续观察原任务,不重复追加。" - ) - .map_err(|error| format!("写入终端失败:{error}"))?; - let conversation_baseline = new_swarm_turn_conversation_baseline( - before.messages.len().saturating_sub(1), - &run_id, - ); - return wait_and_print_swarm_turn( - root, - parent_agent_id, - session_id, - run_profile, - expected_parent_source, - conversation_baseline, - input, - output, - ); - } - - if let Some(runtime) = swarm_parent_steer_target( - parent_agent_id, - session_id, - run_profile, - expected_parent_source, - active_goal_run_id.as_deref(), - &runtimes, - ) { - return steer_and_wait_for_swarm_turn( - root, - parent_agent_id, - session_id, - run_profile, - expected_parent_source, - &runtime.state.run_id, - message, - before.messages.len(), - input, - output, - if active_goal_run_id.is_some() { - "Goal 已恢复并追加" - } else { - "排队任务已恢复,输入已追加" - }, - ); - } - } - - if let Some(goal_run_id) = active_goal_run_id { - print_swarm_goal_error( - output, - &format!( - "当前 Goal run={goal_run_id} 没有可追加的运行态;请先输入 /resume 检查恢复结果。" - ), - )?; - return Ok(SwarmChatFlow::Continue); - } - - let action = if swarm_turn_uses_interaction_kernel(parent_agent_id, expected_parent_source) { - let Some(runtime_lock) = - try_acquire_game_creator_agent_runtime_task_lock(root, parent_agent_id)? - else { - let current_runtimes = read_game_creator_agent_runtimes_at(root)?; - if let Some(runtime) = swarm_parent_steer_target( - parent_agent_id, - session_id, - run_profile, - expected_parent_source, - None, - ¤t_runtimes, - ) { - return steer_and_wait_for_swarm_turn( - root, - parent_agent_id, - session_id, - run_profile, - expected_parent_source, - &runtime.state.run_id, - message, - before.messages.len(), - input, - output, - "运行中输入已排队", - ); - } - return Err(format!( - "Agent 交互锁已被占用但没有可识别的活动 Runtime:{parent_agent_id}" - )); - }; - let action_result = - decide_interaction_action(root, parent_agent_id, session_id, message, output); - let persistence_result = match &action_result { - Ok(AgentInteractionAction::Reply(reply)) => { - persist_swarm_reply(root, parent_agent_id, session_id, message, reply) - } - Ok(AgentInteractionAction::ProjectLocation) => { - let reply = format!("当前 CLI 会话绑定的项目目录是:{}", root.display()); - writeln!(output, "陶泥儿> {reply}") - .map_err(|error| format!("写入终端失败:{error}")) - .and_then(|_| { - persist_swarm_reply(root, parent_agent_id, session_id, message, &reply) - }) - } - _ => Ok(()), - }; - spawn_next_game_creator_agent_background_task_drain_with_lock( - root, - parent_agent_id, - runtime_lock, - ); - persistence_result?; - action_result? - } else { - AgentInteractionAction::Execute - }; - let action = normalize_interaction_action_without_active_runtime(action); - writeln!(output, "[意图] {}", action.label()) - .map_err(|error| format!("写入终端失败:{error}"))?; - match action { - AgentInteractionAction::Reply(reply) => { - debug_assert!(!reply.trim().is_empty()); - Ok(SwarmChatFlow::Continue) - } - AgentInteractionAction::ProjectLocation => Ok(SwarmChatFlow::Continue), - AgentInteractionAction::Execute => start_and_wait_for_swarm_turn( - root, - parent_agent_id, - session_id, - run_profile, - new_run_launch, - message, - before.messages.len(), - input, - output, - ), - AgentInteractionAction::Resume => handle_swarm_resume_turn( - root, - parent_agent_id, - session_id, - run_profile, - expected_parent_source, - before.messages.len(), - input, - output, - ), - } -} - -pub(super) fn swarm_turn_uses_interaction_kernel( - parent_agent_id: &str, - _expected_parent_source: Option<&str>, -) -> bool { - game_creator_agent_uses_interaction_kernel(parent_agent_id) -} - -pub(super) fn normalize_interaction_action_without_active_runtime( - action: AgentInteractionAction, -) -> AgentInteractionAction { - match action { - AgentInteractionAction::Resume => AgentInteractionAction::Execute, - action => action, - } -} - -fn decide_interaction_action( - root: &Path, - parent_agent_id: &str, - session_id: &str, - message: &str, - output: &mut W, -) -> Result { - writeln!(output, "[决策] Agent 正在判断直接回复或调用持久能力。") - .map_err(|error| format!("写入终端失败:{error}"))?; - let mut printed_chars = 0usize; - let mut reply_started = false; - let mut protocol_buffered = false; - let action = - tauri::async_runtime::block_on(decide_game_creator_agent_interaction_turn_for_session_at( - root, - parent_agent_id, - session_id, - message, - |delta| { - if protocol_buffered { - return; - } - let trimmed = delta.accumulated_text.trim_start(); - if !reply_started && (trimmed.starts_with('{') || trimmed.starts_with("```")) { - protocol_buffered = true; - return; - } - if delta.accumulated_text.len() <= printed_chars { - return; - } - if !reply_started { - let _ = write!(output, "陶泥儿> "); - reply_started = true; - } - let chunk = &delta.accumulated_text[printed_chars..]; - let _ = write!(output, "{chunk}"); - let _ = output.flush(); - printed_chars = delta.accumulated_text.len(); - }, - ))?; - if let AgentInteractionAction::Reply(reply) = &action { - if reply.len() > printed_chars { - if !reply_started { - write!(output, "陶泥儿> ").map_err(|error| format!("写入终端失败:{error}"))?; - } - write!(output, "{}", &reply[printed_chars..]) - .map_err(|error| format!("写入终端失败:{error}"))?; - } - writeln!(output).map_err(|error| format!("写入终端失败:{error}"))?; - } else if reply_started { - writeln!(output).map_err(|error| format!("写入终端失败:{error}"))?; - } - Ok(action) -} - -fn persist_swarm_reply( - root: &Path, - parent_agent_id: &str, - session_id: &str, - message: &str, - reply: &str, -) -> Result<(), String> { - with_agent_conversation_session_lane_at( - root, - parent_agent_id, - "Swarm interaction 回复落盘", - || { - append_local_conversation_message_for_session_at( - root, - Some(parent_agent_id), - Some(session_id), - LocalConversationMessage { - role: "user".to_string(), - content: message.to_string(), - agent_id: Some(parent_agent_id.to_string()), - }, - )?; - append_local_conversation_message_for_session_at( - root, - Some(parent_agent_id), - Some(session_id), - LocalConversationMessage { - role: "assistant".to_string(), - content: reply.to_string(), - agent_id: Some(parent_agent_id.to_string()), - }, - )?; - Ok(()) - }, - ) -} - -fn steer_and_wait_for_swarm_turn( - root: &Path, - parent_agent_id: &str, - session_id: &str, - run_profile: &str, - expected_parent_source: Option<&str>, - run_id: &str, - message: &str, - previous_message_count: usize, - input: &Receiver, - output: &mut W, - label: &str, -) -> Result { - require_external_agent_runner_for_cli_runtime_write(root)?; - let steer_id = format!("swarm-steer-{}", unix_millis()); - let steer_source = expected_parent_source.map(str::to_string); - let result = tauri::async_runtime::block_on(steer_game_creator_agent_runtime_task( - root.display().to_string(), - parent_agent_id.to_string(), - session_id.to_string(), - run_id.to_string(), - steer_id.clone(), - message.to_string(), - Some(run_profile.to_string()), - steer_source, - ))?; - writeln!( - output, - "[{label}] run={} steer={} providerInterrupted={}", - run_id, steer_id, result.provider_interrupted - ) - .map_err(|error| format!("写入终端失败:{error}"))?; - let conversation_baseline = - new_swarm_turn_conversation_baseline(previous_message_count, run_id); - wait_and_print_swarm_turn( - root, - parent_agent_id, - session_id, - run_profile, - expected_parent_source, - conversation_baseline, - input, - output, - ) -} - -fn start_and_wait_for_swarm_turn( - root: &Path, - parent_agent_id: &str, - session_id: &str, - run_profile: &str, - new_run_launch: SwarmNewRunLaunch<'_>, - task: &str, - previous_message_count: usize, - input: &Receiver, - output: &mut W, -) -> Result { - require_external_agent_runner_for_cli_runtime_write(root)?; - let requested_run_id = format!("swarm-{parent_agent_id}-{}", unix_millis()); - let started = match new_run_launch { - SwarmNewRunLaunch::ProjectSupervisor { - source, - run_profile, - } => start_game_creator_supervisor_background_task_for_session_at( - root, - Some(session_id), - task, - &requested_run_id, - source, - run_profile, - )?, - SwarmNewRunLaunch::ExplicitParentDebug => start_game_creator_agent_runtime_task( - root.display().to_string(), - parent_agent_id.to_string(), - Some(session_id.to_string()), - task.to_string(), - requested_run_id.clone(), - )?, - }; - let accepted_run_id = accepted_swarm_run_id(&started, &requested_run_id); - writeln!( - output, - "[已投递] agent={} session={} run={}", - parent_agent_id, started.state.session_id, accepted_run_id - ) - .map_err(|error| format!("写入终端失败:{error}"))?; - let conversation_baseline = - new_swarm_turn_conversation_baseline(previous_message_count, accepted_run_id); - wait_and_print_swarm_turn( - root, - parent_agent_id, - session_id, - run_profile, - new_run_launch.expected_parent_source(), - conversation_baseline, - input, - output, - ) -} - -pub(super) fn accepted_swarm_run_id( - started: &AgentRuntimeResult, - requested_run_id: &str, -) -> String { - started - .accepted_run_id - .as_deref() - .map(str::trim) - .filter(|run_id| !run_id.is_empty()) - .unwrap_or(requested_run_id) - .to_string() -} - -pub(super) fn handle_swarm_resume_turn( - root: &Path, - parent_agent_id: &str, - session_id: &str, - run_profile: &str, - expected_parent_source: Option<&str>, - previous_message_count: usize, - input: &Receiver, - output: &mut W, -) -> Result { - require_external_agent_runner_for_cli_runtime_write(root)?; - let resumed = resume_game_creator_agent_background_tasks_at(root)?; - if !resumed.is_empty() { - writeln!(output, "[恢复扫描] 已检查 {} 个 Runtime。", resumed.len()) - .map_err(|error| format!("写入终端失败:{error}"))?; - } - let current_runtimes = read_game_creator_agent_runtimes_at(root)?; - let Some(parent) = swarm_parent_runtime( - parent_agent_id, - session_id, - run_profile, - expected_parent_source, - ¤t_runtimes, - ) else { - writeln!(output, "[恢复] 当前 Session 没有可恢复的运行任务。") - .map_err(|error| format!("写入终端失败:{error}"))?; - return Ok(SwarmChatFlow::Continue); - }; - let pending = next_pending_swarm_task(parent, session_id, run_profile, expected_parent_source); - let (target_run_id, target_status, target_phase) = - if game_creator_agent_runtime_accepts_steer(&parent.state) - || parent.state.status == "pending" - { - ( - parent.state.run_id.as_str(), - parent.state.status.as_str(), - parent.state.phase.as_str(), - ) - } else if let Some(task) = pending { - ( - task.run_id.as_str(), - task.status.as_str(), - task.phase.as_str(), - ) - } else { - writeln!(output, "[恢复] 当前 Session 没有可恢复的运行任务。") - .map_err(|error| format!("写入终端失败:{error}"))?; - return Ok(SwarmChatFlow::Continue); - }; - writeln!( - output, - "[恢复] 继续观察 run={} status={} phase={}", - target_run_id, target_status, target_phase - ) - .map_err(|error| format!("写入终端失败:{error}"))?; - let mut conversation_baseline = - new_swarm_turn_conversation_baseline(previous_message_count, target_run_id); - capture_recovered_swarm_assistant_at( - root, - parent_agent_id, - session_id, - &mut conversation_baseline, - )?; - wait_and_print_swarm_turn( - root, - parent_agent_id, - session_id, - run_profile, - expected_parent_source, - conversation_baseline, - input, - output, - ) -} - -fn wait_and_print_swarm_turn( - root: &Path, - parent_agent_id: &str, - session_id: &str, - run_profile: &str, - expected_parent_source: Option<&str>, - conversation_baseline: SwarmTurnConversationBaseline, - input: &Receiver, - output: &mut W, -) -> Result { - let mut observer = SwarmRuntimeObserver::seed(root)?; - let outcome = wait_for_swarm_turn( - root, - parent_agent_id, - session_id, - run_profile, - expected_parent_source, - conversation_baseline, - input, - output, - &mut observer, - SWARM_CHAT_POLL_INTERVAL, - SWARM_CHAT_SETTLE_WINDOW, - )?; - if outcome == SwarmTurnOutcome::Quit { - print_swarm_chat_exit(output)?; - return Ok(SwarmChatFlow::Exit); - } - print_turn_outcome(outcome, output)?; - Ok(SwarmChatFlow::Continue) -} diff --git a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/turn_wait.rs b/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/turn_wait.rs deleted file mode 100644 index e1d820bd6..000000000 --- a/apps/ai-game-creator-shell/src-tauri/src/swarm_cli/turn_wait.rs +++ /dev/null @@ -1,427 +0,0 @@ -use super::*; - -pub(super) const SWARM_CHAT_POLL_INTERVAL: Duration = Duration::from_millis(250); -pub(super) const SWARM_CHAT_SETTLE_WINDOW: Duration = Duration::from_millis(1_500); - -#[derive(Debug, Eq, PartialEq)] -pub(super) struct SwarmTurnObservation { - pub(super) outcome: SwarmTurnOutcome, - pub(super) input_closed: bool, -} - -pub(super) fn wait_for_swarm_turn( - root: &Path, - parent_agent_id: &str, - session_id: &str, - run_profile: &str, - expected_parent_source: Option<&str>, - conversation_baseline: SwarmTurnConversationBaseline, - input: &Receiver, - output: &mut W, - observer: &mut SwarmRuntimeObserver, - poll_interval: Duration, - settle_window: Duration, -) -> Result { - let mut stable_since: Option = None; - let mut recovery_scan_required = true; - let mut last_runner_check = Instant::now(); - let mut input_closed = false; - loop { - let runtimes = read_game_creator_agent_runtimes_at(root)?; - let turn_runtimes = swarm_current_runtimes_for_run( - parent_agent_id, - session_id, - run_profile, - expected_parent_source, - &conversation_baseline.parent_run_id, - &runtimes, - ); - let changed = observer.print_changes(&turn_runtimes, output)?; - if changed { - stable_since = None; - } - let mut reconciliation = swarm_reconciliation_agents(&turn_runtimes); - if !reconciliation.is_empty() { - observer.close_response_line(output)?; - return build_reconciliation_turn_outcome( - root, - parent_agent_id, - session_id, - &conversation_baseline, - &runtimes, - reconciliation, - ); - } - if !input_closed { - match observer.resolve_confirmations( - root, - parent_agent_id, - &turn_runtimes, - input, - output, - )? { - SwarmConfirmationResolution::Handled => { - stable_since = None; - recovery_scan_required = true; - continue; - } - SwarmConfirmationResolution::InputClosed => { - mark_swarm_turn_input_closed(&mut input_closed, observer, output)?; - stable_since = None; - continue; - } - SwarmConfirmationResolution::Quit => return Ok(SwarmTurnOutcome::Quit), - SwarmConfirmationResolution::None => {} - } - match observer.resolve_user_input_requests( - root, - parent_agent_id, - &turn_runtimes, - input, - output, - )? { - SwarmConfirmationResolution::Handled => { - stable_since = None; - recovery_scan_required = true; - continue; - } - SwarmConfirmationResolution::InputClosed => { - mark_swarm_turn_input_closed(&mut input_closed, observer, output)?; - stable_since = None; - continue; - } - SwarmConfirmationResolution::Quit => return Ok(SwarmTurnOutcome::Quit), - SwarmConfirmationResolution::None => {} - } - } - let pending_interactions = - swarm_unhandled_interaction_reasons(parent_agent_id, &turn_runtimes, input_closed); - if !pending_interactions.is_empty() { - observer.close_response_line(output)?; - return build_incomplete_turn_outcome( - root, - parent_agent_id, - session_id, - &conversation_baseline, - &runtimes, - pending_interactions, - ); - } - let failure_scan = scan_swarm_terminal_failures_at( - root, - parent_agent_id, - session_id, - run_profile, - expected_parent_source, - &conversation_baseline.parent_run_id, - &runtimes, - ); - if !failure_scan.reconciliation_agents.is_empty() { - observer.close_response_line(output)?; - return build_reconciliation_turn_outcome( - root, - parent_agent_id, - session_id, - &conversation_baseline, - &runtimes, - failure_scan.reconciliation_agents, - ); - } - if !failure_scan.failed_agents.is_empty() { - observer.close_response_line(output)?; - return build_failed_turn_outcome( - root, - parent_agent_id, - session_id, - &conversation_baseline, - &runtimes, - failure_scan.failed_agents, - ); - } - if !failure_scan.incomplete_reasons.is_empty() { - observer.close_response_line(output)?; - return build_incomplete_turn_outcome( - root, - parent_agent_id, - session_id, - &conversation_baseline, - &runtimes, - failure_scan.incomplete_reasons, - ); - } - if last_runner_check.elapsed() >= Duration::from_secs(2) { - let runner = read_external_agent_runner_status(); - last_runner_check = Instant::now(); - if swarm_turn_is_busy( - root, - parent_agent_id, - session_id, - run_profile, - expected_parent_source, - &conversation_baseline.parent_run_id, - &runtimes, - )? && (!runner.enabled || !runner.running) - { - reconciliation.push("external-runner".to_string()); - observer.close_response_line(output)?; - return build_reconciliation_turn_outcome( - root, - parent_agent_id, - session_id, - &conversation_baseline, - &runtimes, - reconciliation, - ); - } - } - if swarm_turn_is_busy( - root, - parent_agent_id, - session_id, - run_profile, - expected_parent_source, - &conversation_baseline.parent_run_id, - &runtimes, - )? { - stable_since = None; - recovery_scan_required = true; - } else { - let since = stable_since.get_or_insert_with(Instant::now); - if since.elapsed() >= settle_window { - if recovery_scan_required { - observer.close_response_line(output)?; - writeln!( - output, - "[收束] Runtime 已空闲,检查待发布的 receipt / join。" - ) - .map_err(|error| format!("写入终端失败:{error}"))?; - output - .flush() - .map_err(|error| format!("刷新终端失败:{error}"))?; - resume_game_creator_agent_background_tasks_at(root)?; - recovery_scan_required = false; - stable_since = Some(Instant::now()); - continue; - } - let conversation_metrics = read_turn_conversation_metrics( - root, - parent_agent_id, - session_id, - &conversation_baseline, - )?; - let parent_runtime_is_current = swarm_parent_runtime_for_run( - parent_agent_id, - session_id, - run_profile, - expected_parent_source, - &conversation_baseline.parent_run_id, - &runtimes, - ) - .is_some(); - let parent_runtime = swarm_parent_runtime_snapshot_for_run( - root, - parent_agent_id, - session_id, - run_profile, - expected_parent_source, - &conversation_baseline.parent_run_id, - &runtimes, - )?; - let completion_blockers = if parent_runtime_is_current { - parent_runtime - .as_ref() - .map(|parent| swarm_parent_completion_contract_blockers_at(root, parent)) - .unwrap_or_else(|| vec!["parent-runtime-missing".to_string()]) - } else if parent_runtime - .as_ref() - .is_some_and(parent_runtime_completed) - { - Vec::new() - } else { - vec!["parent-runtime-missing".to_string()] - }; - match classify_swarm_turn_terminal( - parent_runtime.as_ref(), - conversation_metrics, - 0, - 0, - completion_blockers.len(), - ) { - SwarmTurnTerminalClassification::Settled => { - let printed_metrics = print_new_parent_reply( - root, - parent_agent_id, - session_id, - &conversation_baseline, - output, - observer, - )?; - if printed_metrics != conversation_metrics { - return build_incomplete_turn_outcome( - root, - parent_agent_id, - session_id, - &conversation_baseline, - &runtimes, - vec!["conversation-changed-before-settle".to_string()], - ); - } - let report = build_swarm_turn_report( - SwarmTurnReportOutcome::Settled, - parent_agent_id, - session_id, - Some(&conversation_baseline.parent_run_id), - &runtimes, - conversation_metrics, - 0, - )?; - return Ok(SwarmTurnOutcome::Settled(report)); - } - SwarmTurnTerminalClassification::Failed => { - return build_failed_turn_outcome( - root, - parent_agent_id, - session_id, - &conversation_baseline, - &runtimes, - vec![format!( - "{}:{}", - parent_agent_id, - parent_runtime - .as_ref() - .map(|runtime| runtime.state.phase.as_str()) - .unwrap_or("missing") - )], - ); - } - SwarmTurnTerminalClassification::Incomplete => { - let mut reasons = completion_blockers; - append_swarm_terminal_snapshot_reasons( - &mut reasons, - parent_runtime.as_ref(), - conversation_metrics, - ); - return build_incomplete_turn_outcome( - root, - parent_agent_id, - session_id, - &conversation_baseline, - &runtimes, - reasons, - ); - } - } - } - } - if input_closed { - std::thread::sleep(poll_interval); - continue; - } - match input.recv_timeout(poll_interval) { - Ok(SwarmInputEvent::Line(line)) => { - let Some(command) = parse_swarm_chat_input(&line) else { - continue; - }; - observer.close_response_line(output)?; - match command { - SwarmChatInput::Quit => return Ok(SwarmTurnOutcome::Quit), - SwarmChatInput::Help => print_swarm_chat_help(output)?, - SwarmChatInput::Agents => print_swarm_agents(root, output)?, - SwarmChatInput::Status => print_swarm_status(root, output)?, - SwarmChatInput::History => { - print_conversation_history(root, parent_agent_id, output)? - } - SwarmChatInput::Compact => { - handle_swarm_context_compaction(root, parent_agent_id, output)? - } - SwarmChatInput::Goal(command) => { - let _ = handle_swarm_goal_command(root, parent_agent_id, command, output)?; - stable_since = None; - recovery_scan_required = true; - } - SwarmChatInput::InvalidGoal(error) => print_swarm_goal_error(output, &error)?, - SwarmChatInput::Resume => { - writeln!(output, "[恢复] 当前已经在观察这个 Runtime。") - .map_err(|error| format!("写入终端失败:{error}"))?; - } - SwarmChatInput::Message(message) => { - if let Some(parent) = swarm_parent_steer_target( - parent_agent_id, - session_id, - run_profile, - expected_parent_source, - Some(&conversation_baseline.parent_run_id), - &runtimes, - ) { - let steer_id = format!("swarm-steer-{}", unix_millis()); - let result = tauri::async_runtime::block_on( - steer_game_creator_agent_runtime_task( - root.display().to_string(), - parent_agent_id.to_string(), - parent.state.session_id.clone(), - parent.state.run_id.clone(), - steer_id.clone(), - message, - Some(run_profile.to_string()), - None, - ), - )?; - writeln!( - output, - "[已追加] run={} steer={} providerInterrupted={}", - parent.state.run_id, steer_id, result.provider_interrupted - ) - .map_err(|error| format!("写入终端失败:{error}"))?; - } else { - writeln!(output, "[暂未发送] 子 Agent 尚未收束,请稍后重发。") - .map_err(|error| format!("写入终端失败:{error}"))?; - } - stable_since = None; - recovery_scan_required = true; - } - } - } - Ok(SwarmInputEvent::Eof) | Err(RecvTimeoutError::Disconnected) => { - mark_swarm_turn_input_closed(&mut input_closed, observer, output)?; - } - Ok(SwarmInputEvent::Error(error)) => { - observer.close_response_line(output)?; - return Err(format!("读取终端输入失败:{error}")); - } - Err(RecvTimeoutError::Timeout) => {} - } - } -} - -pub(super) fn runtimes_are_busy(runtimes: &[AgentRuntimeResult]) -> bool { - runtimes.iter().any(runtime_is_busy) -} - -pub(super) fn runtime_is_busy(runtime: &AgentRuntimeResult) -> bool { - matches!( - runtime.state.status.as_str(), - "pending" - | "running" - | "waiting-for-confirmation" - | "waiting-for-user-input" - | "cancelling" - ) || runtime.state.phase == "needs-reconciliation" - || runtime.task_queue.pending > 0 - || runtime.task_queue.running > 0 - || runtime.task_queue.waiting_for_confirmation > 0 - || runtime.task_queue.waiting_for_user_input > 0 -} - -pub(super) fn mark_swarm_turn_input_closed( - input_closed: &mut bool, - observer: &mut SwarmRuntimeObserver, - output: &mut W, -) -> Result<(), String> { - if *input_closed { - return Ok(()); - } - *input_closed = true; - observer.close_response_line(output)?; - writeln!(output, "[输入已关闭] 当前 turn 继续运行,等待可信终态。") - .map_err(|error| format!("写入终端失败:{error}")) -} diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/project.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/project.rs index 6e9abb9e8..f70834828 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/project.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/project.rs @@ -1929,58 +1929,6 @@ async fn generate_local_game_draft_fails_after_max_passes_without_final_artifact fs::remove_dir_all(root).ok(); } -#[test] -fn canvas_sync_suggestion_is_media_type_aware() { - let art_group = GAME_CREATOR_AGENT_GROUP_DEFINITIONS - .iter() - .find(|definition| definition.id == "art") - .copied() - .expect("art group"); - let audio_group = GAME_CREATOR_AGENT_GROUP_DEFINITIONS - .iter() - .find(|definition| definition.id == "audio") - .copied() - .expect("audio group"); - let art_role = ART_AGENT_ROLES - .iter() - .find(|role| role.id == "asset") - .copied() - .expect("art asset role"); - let audio_role = AUDIO_AGENT_ROLES - .iter() - .find(|role| role.id == "sfx") - .copied() - .expect("audio sfx role"); - let art_brief = AgentRoleBrief { - group_definition: art_group, - role_definition: art_role, - markdown: String::new(), - relative_path: ".agent/passes/pass-1/groups/art/asset.md".to_string(), - memory_relative_path: agent_role_memory_relative_path(art_group, art_role), - status: "completed".to_string(), - tool_id: art_role.tool_id.to_string(), - summary: String::new(), - }; - let audio_brief = AgentRoleBrief { - group_definition: audio_group, - role_definition: audio_role, - markdown: String::new(), - relative_path: ".agent/passes/pass-1/groups/audio/sfx.md".to_string(), - memory_relative_path: agent_role_memory_relative_path(audio_group, audio_role), - status: "completed".to_string(), - tool_id: audio_role.tool_id.to_string(), - summary: String::new(), - }; - let input_paths = vec![".agent/manifest.json".to_string()]; - let image_canvas_assets = vec!["image/png".to_string()]; - let audio_canvas_assets = vec!["audio/wav".to_string()]; - - assert!(suggested_canvas_tool_call(&art_brief, &input_paths, &image_canvas_assets).is_none()); - assert!(suggested_canvas_tool_call(&audio_brief, &input_paths, &image_canvas_assets).is_some()); - assert!(suggested_canvas_tool_call(&audio_brief, &input_paths, &audio_canvas_assets).is_none()); - assert!(suggested_canvas_tool_call(&art_brief, &input_paths, &audio_canvas_assets).is_some()); -} - #[test] fn init_local_game_project_creates_manifest_and_dirs() { let root = unique_project_path(); diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/provider.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/provider.rs index 77e12f38c..aeb59c559 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/provider.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/provider.rs @@ -6461,31 +6461,7 @@ async fn agent_loop_writes_spec_findings_and_retries_generator() { .iter() .any(|step| step["agent"] == "数值组 / Difficulty")); assert!(steps.iter().any(|step| step["agent"] == "美术组 / Asset")); - assert!(steps.iter().any(|step| { - step["agent"] == "美术组 / Asset" - && step["toolCalls"] - .as_array() - .unwrap() - .iter() - .any(|tool_call| { - tool_call["toolId"] == "agent.tool.suggest.canvas.project_sync" - && tool_call["status"] == "suggested" - }) - })); assert!(steps.iter().any(|step| step["agent"] == "音乐组 / SFX")); - assert!(steps.iter().any(|step| { - step["agent"] == "音乐组 / SFX" - && step["toolCalls"] - .as_array() - .unwrap() - .iter() - .any(|tool_call| { - tool_call["toolId"] == "agent.tool.suggest.canvas.project_sync" - && tool_call["summary"] - .as_str() - .is_some_and(|summary| summary.contains("/sync-canvas-project")) - }) - })); assert!(steps.iter().any(|step| step["agent"] == "程序组 / Code")); assert!(steps.iter().any(|step| step["agent"] == "运营组 / Publish")); assert!(steps diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index 2403d3913..ab0f9c102 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -40,7 +40,6 @@ import type { LocalGameProjectRevisionStatus, LocalPreviewResult, LocalPreviewStatus, - LocalProjectFileResult, LocalProjectKind, PendingUiConfirmation, ProjectPermissionPolicyView, @@ -67,19 +66,13 @@ import { writeRecentWorkspace, } from './features/app-shell/model'; import { WorkspaceLauncherShell } from './features/app-shell/WorkspaceLauncher'; -import { - projectAgentRuntimeSummaries, - summarizeAgentRunCompletionForChat, -} from './features/project-summary/agentPresentation'; +import { projectAgentRuntimeSummaries } from './features/project-summary/agentPresentation'; import { isAbsoluteProjectPath, projectPathHasControlCharacter, } from './features/project-summary/projectSummary'; -import { parseAgentRunTrace } from './features/project-workspace/agentRunTrace'; import { importDesignFiles } from './features/project-workspace/importDesignFiles'; -import { parseRememberInput } from './features/project-workspace/memoryCommands'; import { - isAgentTraceFilePath, needsInitializedChatProject, resolveChatProjectPath, } from './features/project-workspace/projectCommandPolicy'; @@ -146,17 +139,9 @@ function isPersistableDirectCodexConversationMessage(message: ChatMessage) { */ export { AuthenticatedClient } from './app/AuthenticatedClient'; -export { - deriveAgentStatusCards, - summarizeAgentAudit, - summarizeAgentRunTrace, -} from './features/project-summary/agentPresentation'; +export { deriveAgentStatusCards } from './features/project-summary/agentPresentation'; export { isAbsoluteProjectPath } from './features/project-summary/projectSummary'; -export { - needsInitializedChatProject, - parseRememberInput, - resolveChatProjectPath, -}; +export { needsInitializedChatProject, resolveChatProjectPath }; export function WorkspaceLauncher(props: WorkspaceLauncherProps) { return ; @@ -857,7 +842,7 @@ export function App({ localProjectPathRef.current === initialProjectPath && !planningStartMode ) { - void refreshAgentRunTrace(initialProjectPath); + void refreshAgentRuntimes(initialProjectPath); } }); // Initial project opening is guarded by initialProjectOpenedRef. @@ -1512,7 +1497,7 @@ export function App({ } void loadProjectConversation(openedProject.projectPath); if (!directProjectMode) { - void refreshAgentRunTrace(openedProject.projectPath); + void refreshAgentRuntimes(openedProject.projectPath); } } catch (error) { if (projectScopeVersionRef.current !== projectScopeVersion) { @@ -1730,9 +1715,8 @@ export function App({ * * `activate_local_game_preview` 是 Rust 侧的「这条预览还活着、且属于这个项目」闸门: * 它只核对内存 registry 里的状态并回传可用的 loopback 地址,前端据此进客户端运行视图。 - * 同一个动作过去由工作台壳的 `/preview open` 聊天命令承担,那条命令链随 Supervisor - * 前端链路一起退役,行为改由「运行」入口承接,结果经 DirectProject 聊天的 `announce` - * 交给聊天自己的消息流。返回 `null` 表示没有可复用的活体预览,调用方照旧重启预览。 + * 行为由「运行」入口承接,结果经 DirectProject 聊天的 `announce` 交给聊天自己的消息流。 + * 返回 `null` 表示没有可复用的活体预览,调用方照旧重启预览。 */ async function activateRunningPreview( invoke: TauriInvoke, @@ -1795,7 +1779,7 @@ export function App({ updateClientPreview(previewResult); void refreshManifest(nextProjectPath); if (!directProjectMode) { - void refreshAgentRunTrace(nextProjectPath); + void refreshAgentRuntimes(nextProjectPath); } if (announceToChat) { announceProjectChatMessage( @@ -1810,36 +1794,6 @@ export function App({ } } - async function loadAgentRunTraceFile( - relativePath: string, - nextProjectPath = resolveChatProjectPath(localProject) ?? '', - ) { - const invoke = resolveTauriInvoke(); - if (!invoke) { - return null; - } - if (!nextProjectPath) { - return null; - } - - try { - const result = await invoke( - 'read_local_project_file', - { - projectPath: nextProjectPath, - relativePath, - commandId: isAgentTraceFilePath(relativePath) - ? 'agent.trace_read' - : 'file.read', - }, - ); - const trace = parseAgentRunTrace(result.content); - return summarizeAgentRunCompletionForChat(trace); - } catch { - return null; - } - } - function rememberAgentRuntimeState(runtime: AgentRuntimeState | null) { if (!runtime) { return; @@ -1955,21 +1909,6 @@ export function App({ } } - async function refreshAgentRunTrace( - nextProjectPath = resolveChatProjectPath(localProject) ?? '', - ) { - if (!nextProjectPath) { - await refreshAgentRuntimes(nextProjectPath); - return null; - } - const summary = await loadAgentRunTraceFile( - '.agent/run.latest.json', - nextProjectPath, - ); - await refreshAgentRuntimes(nextProjectPath); - return summary; - } - const professionalResultCandidates = taskRowsFromManifest(manifest).map( (task) => ({ agentId: agentConversationId(task), diff --git a/apps/ai-game-creator-shell/src/features/project-summary/agentPresentation.ts b/apps/ai-game-creator-shell/src/features/project-summary/agentPresentation.ts index 77f8045af..1f7d05317 100644 --- a/apps/ai-game-creator-shell/src/features/project-summary/agentPresentation.ts +++ b/apps/ai-game-creator-shell/src/features/project-summary/agentPresentation.ts @@ -1,24 +1,14 @@ import { - GAME_CREATION_AGENT_CAPABILITIES, - GAME_CREATION_APP_COMMANDS, type GameCreationAgentRunStep, type GameCreationAgentRunTrace, - type GameCreationAgentToolCallTrace, - type GameCreationAppAgentGroup, type GameCreationAppManifest, - type GameCreationAppTaskState, type GameCreationAppTaskStatus, - selectGameCreationAppReadyTasks, } from '../../../../../packages/shared/src/contracts/gameCreationApp'; import type { AgentRuntimeState, - AgentRuntimeTaskQueueSummary, - AgentRuntimeTaskRecord, AgentStatusCard, AgentTaskGraphState, GameCreatorLlmConfigStatus, - LocalProjectFileEntry, - ProjectPermissionPolicy, } from '../../app/types'; import type { ProjectAgentRuntimeSummary } from '../../view/project-development'; import { @@ -34,18 +24,6 @@ import { projectRuntimeVisibleError, taskRowsFromManifest, } from '../agent-runtime'; -import { - formatAgentRunStatus, - formatProjectPolicyCommandList, - formatTraceRepairRoutes, - formatTraceTaskId, - formatTraceTaskIds, - isSafeProjectRelativePath, - previewStatusLabels, - readableArtifactPathFromAgentRunTrace, - taskGroupLabels, - taskStatusLabels, -} from './projectSummary'; export function taskStatusFromTraceStep( stepStatus: string | undefined, @@ -357,316 +335,6 @@ export function projectAgentRuntimeSummaries( }); } -export function sameStringArray(left: string[], right: string[]) { - return ( - left.length === right.length && - left.every((value, index) => value === right[index]) - ); -} - -export function sameAgentRuntimeTaskQueue( - left: AgentRuntimeTaskQueueSummary | null, - right: AgentRuntimeTaskQueueSummary | null, -) { - if (!left || !right) { - return left === right; - } - return ( - left.total === right.total && - left.pending === right.pending && - left.running === right.running && - (left.waitingForConfirmation ?? 0) === - (right.waitingForConfirmation ?? 0) && - (left.waitingForUserInput ?? 0) === (right.waitingForUserInput ?? 0) && - (left.cancelled ?? 0) === (right.cancelled ?? 0) && - left.completed === right.completed && - left.failed === right.failed && - left.latestRunId === right.latestRunId && - left.updatedAt === right.updatedAt - ); -} - -export function sameAgentRuntimeTasks( - left: AgentRuntimeTaskRecord[], - right: AgentRuntimeTaskRecord[], -) { - return ( - left.length === right.length && - left.every((task, index) => { - const other = right[index]; - return ( - other && - task.runId === other.runId && - task.source === other.source && - task.parentAgentId === other.parentAgentId && - task.parentRunId === other.parentRunId && - task.delegationId === other.delegationId && - task.status === other.status && - task.phase === other.phase && - task.task === other.task && - task.currentAction === other.currentAction && - task.terminalDetail === other.terminalDetail && - task.error === other.error && - task.updatedAt === other.updatedAt - ); - }) - ); -} - -export function hasProjectFile(files: LocalProjectFileEntry[], path: string) { - return files.some((file) => file.kind === 'file' && file.path === path); -} - -export function hasTracePath( - trace: GameCreationAgentRunTrace | null, - path: string, -) { - if (!trace) { - return false; - } - return ( - trace.artifacts.some((artifact) => artifact.path === path) || - trace.steps.some( - (step) => - step.inputPaths.includes(path) || step.outputPaths.includes(path), - ) - ); -} - -export function formatAuditStatus( - ok: boolean, - okLabel = '通过', - failLabel = '待补', -) { - return ok ? okLabel : failLabel; -} - -export function summarizeAuditLoopTrace( - trace: GameCreationAgentRunTrace | null, -) { - if (!trace) { - return '- Loop trace:待生成 · 还没有 .agent/run.latest.json'; - } - - const hasEvaluatorPassed = trace.steps.some( - (step) => step.agent === 'Evaluator' && step.status === 'passed', - ); - const failed = - trace.status === 'failed' || - Boolean(trace.error) || - trace.stopReason === 'failed' || - trace.stopReason === 'max-passes-exhausted'; - const passed = - !failed && - hasEvaluatorPassed && - ['passed', 'preview-running', 'preview-stopped'].includes(trace.status); - const needsRevision = - !failed && - (trace.status === 'needs-revision' || - trace.stopReason === 'evaluator-needs-revision'); - const label = failed - ? '未通过' - : passed - ? '通过' - : needsRevision - ? '需返工' - : trace.status === 'running' - ? '运行中' - : trace.status === 'artifacts-written' - ? '待自检' - : hasEvaluatorPassed - ? '待核对' - : '缺步骤'; - - return `- Loop trace:${label} · run ${trace.runId},${trace.passes}/${trace.maxPasses} 轮,${trace.toolCallCount}/${trace.maxToolCalls} 次工具调用,${trace.stopReason}`; -} - -export function summarizeAgentAudit( - nextManifest: GameCreationAppManifest, - nextProjectPath: string, - files: LocalProjectFileEntry[], - trace: GameCreationAgentRunTrace | null, - commandLogContent = '', -) { - const tasks = taskRowsFromManifest(nextManifest); - const groups = new Set(tasks.map((task) => task.group)); - const groupIds = Object.keys(taskGroupLabels) as GameCreationAppAgentGroup[]; - const configuredGroups = groupIds.filter((group) => groups.has(group)); - const traceGroups = new Set( - trace?.steps - .map((step) => step.group) - .filter((group): group is GameCreationAppAgentGroup => Boolean(group)) ?? - [], - ); - const collaborationGroups = groupIds.filter((group) => - traceGroups.has(group), - ); - const completedCount = tasks.filter( - (task) => task.status === 'completed', - ).length; - const readyTasks = selectGameCreationAppReadyTasks({ tasks }); - const readySummary = - readyTasks.length > 0 - ? readyTasks - .slice(0, 3) - .map((task) => `${taskGroupLabels[task.group]} / ${task.role}`) - .join(';') - : '暂无'; - const traceAgents = new Set(trace?.steps.map((step) => step.agent) ?? []); - const hasCoreLoop = - trace !== null && - ['Planner', 'Orchestrator', 'Generator', 'Evaluator'].every((agent) => - traceAgents.has(agent), - ); - const finalArtifacts = [ - 'game/index.html', - 'game/game_design.md', - 'game/balance.json', - 'assets/manifest.art.json', - 'assets/manifest.audio.json', - 'exports/README.md', - ].filter((path) => hasProjectFile(files, path) || hasTracePath(trace, path)); - const hasShortMemory = - hasProjectFile(files, 'memory/session.md') || - hasTracePath(trace, 'memory/session.md'); - const hasLongMemory = - hasProjectFile(files, 'memory/project.md') || - hasTracePath(trace, 'memory/project.md'); - const hasBlackboardMemory = - hasProjectFile(files, 'memory/blackboard.md') || - hasTracePath(trace, 'memory/blackboard.md'); - const hasAgentPrivateMemory = - files.some((file) => file.path.startsWith('memory/agents/')) || - (trace?.artifacts.some((artifact) => - artifact.path.startsWith('memory/agents/'), - ) ?? - false) || - (trace?.steps.some((step) => - [...step.inputPaths, ...step.outputPaths].some((path) => - path.startsWith('memory/agents/'), - ), - ) ?? - false); - const hasCanvasAsset = nextManifest.assets.some( - (asset) => asset.source.kind === 'canvas', - ); - const suggestedCanvasSync = - trace?.steps.some((step) => - step.toolCalls.some((toolCall) => - toolCall.toolId.includes('canvas.project_sync'), - ), - ) ?? false; - const commandRuns = nextManifest.commandRuns ?? []; - const hasPermissionPending = commandLogContent.includes('permission.pending'); - const hasPermissionDecision = - commandLogContent.includes('permission.confirm') || - commandLogContent.includes('permission.cancel'); - const hasAutoPermissionLog = commandLogContent.includes('command.auto'); - const hasCommandLog = - (hasPermissionPending && hasPermissionDecision) || hasAutoPermissionLog; - const preview = nextManifest.preview; - const previewSummary = - preview?.status === 'running' && preview.url - ? preview.url - : preview - ? previewStatusLabels[preview.status] - : '未启动'; - - return [ - 'Agent v1 审计:', - `目录:${nextProjectPath}`, - `- 用户面:通过 · 普通窗口只保留聊天、上传和确认卡片;开发面板只在 dev 模式出现`, - `- 能力/命令契约:通过 · ${GAME_CREATION_AGENT_CAPABILITIES.length} 项能力,${GAME_CREATION_APP_COMMANDS.length} 个内置命令`, - `- 6 组任务配置:${formatAuditStatus(configuredGroups.length === groupIds.length)} · ${configuredGroups - .map((group) => taskGroupLabels[group]) - .join('、')};角色任务 ${tasks.length} 个`, - `- 6 组协作证据:${formatAuditStatus( - collaborationGroups.length === groupIds.length, - '通过', - trace ? '缺组' : '待生成', - )} · ${ - collaborationGroups.length > 0 - ? collaborationGroups.map((group) => taskGroupLabels[group]).join('、') - : '还没有 Agent run trace' - }`, - `- 任务拆分/编排:${formatAuditStatus(tasks.length > 0)} · 已完成 ${completedCount}/${tasks.length};下一步 ${readySummary}`, - summarizeAuditLoopTrace(trace), - `- Planner/Orchestrator/Generator/Evaluator:${formatAuditStatus( - hasCoreLoop, - '通过', - trace ? '缺步骤' : '待生成', - )}`, - `- 返工路由/Carry-over:${formatAuditStatus( - Boolean( - trace && - (trace.taskGraph.repairRoutes.length > 0 || - trace.taskGraph.carriedTaskIds.length > 0 || - trace.passPlans.some((plan) => plan.mode === 'repair')), - ), - '通过', - trace ? '待触发' : '待生成', - )}`, - `- 记忆:${formatAuditStatus( - hasShortMemory && - hasLongMemory && - hasBlackboardMemory && - hasAgentPrivateMemory, - )} · session ${hasShortMemory ? '有' : '无'},project ${ - hasLongMemory ? '有' : '无' - },blackboard ${hasBlackboardMemory ? '有' : '无'},agent ${ - hasAgentPrivateMemory ? '有' : '无' - }`, - `- 本地产物:${formatAuditStatus(finalArtifacts.length > 0)} · ${ - finalArtifacts.join(',') || '无' - }`, - `- 本地 HTTP 预览:${formatAuditStatus( - preview?.status === 'running', - '通过', - '待启动', - )} · ${previewSummary}`, - `- 画板回流:${formatAuditStatus( - hasCanvasAsset, - '通过', - suggestedCanvasSync ? '待同步' : '待接入资产', - )} · ${ - hasCanvasAsset - ? `${nextManifest.assets.filter((asset) => asset.source.kind === 'canvas').length} 个 canvas 资产` - : suggestedCanvasSync - ? '建议 /sync-canvas-project <画板项目ID>' - : '暂无 canvas 来源资产' - }`, - `- 权限 Gate/命令日志:${formatAuditStatus(hasCommandLog)} · ${ - hasPermissionPending && hasPermissionDecision - ? '.agent/logs/command.log 含 pending/decision' - : hasAutoPermissionLog - ? '.agent/logs/command.log 含 auto 权限记录' - : commandRuns.length > 0 || - hasProjectFile(files, '.agent/logs/command.log') - ? '缺 permission.pending 或确认/取消记录' - : '无命令记录' - }`, - '完整 trace:/trace', - ].join('\n'); -} - -export function isMissingAgentRunTraceError(message: string) { - return ( - message.includes('.agent/run.latest.json') && - (message.includes('No such file') || - message.includes('os error 2') || - message.includes('找不到')) - ); -} - -export function formatAgentRunControlError(action: string, message: string) { - if (!isMissingAgentRunTraceError(message)) { - return message; - } - return action === 'status' - ? '暂无最近 Agent run。先生成一次游戏草案后再查看状态。' - : '暂无可控制的 Agent run。先生成一次游戏草案后再操作。'; -} - export function formatCodexRuntimeCapabilities( status: Pick< GameCreatorLlmConfigStatus, @@ -681,34 +349,6 @@ export function formatCodexRuntimeCapabilities( ].join(','); } -export function llmStatusForAgentCard( - status: GameCreatorLlmConfigStatus | null, - agent: Pick, -) { - return ( - status?.agents?.find( - (candidate) => - candidate.agentId === agent.taskId || candidate.agentId === agent.id, - ) ?? null - ); -} - -export function formatAgentCardLlmStatus( - status: GameCreatorLlmConfigStatus | null, - agent: AgentStatusCard, -) { - const agentStatus = llmStatusForAgentCard(status, agent); - if (!agentStatus) { - return null; - } - return [ - `智能服务:${agentStatus.configured ? '已连接' : '未就绪'}`, - `流式${agentStatus.stream ? '开' : '关'}`, - `联网检索${agentStatus.webSearchEnabled ? '开' : '关'}`, - `账号状态${visibleLlmCredentialState(agentStatus.accountCredentialState)}`, - ].join(' · '); -} - export function formatAgentCardRuntimeStatus(agent: AgentStatusCard) { if (!agent.runtimeStatus) { return null; @@ -744,296 +384,3 @@ export function formatAgentCardRuntimeStatus(agent: AgentStatusCard) { .filter(Boolean) .join(' · '); } - -export function formatAgentPolicySummary(policy: ProjectPermissionPolicy) { - const entries = Object.entries(policy.agentPolicies ?? {}); - if (entries.length === 0) { - return '无 Agent 独立策略'; - } - return entries - .slice(0, 4) - .map( - ([agentId, agentPolicy]) => - `${agentId} 拒绝:${formatProjectPolicyCommandList( - agentPolicy.deniedCommands, - )} 确认:${formatProjectPolicyCommandList(agentPolicy.confirmCommands)}`, - ) - .join(';'); -} - -function visibleLlmCredentialState(state: string | undefined) { - switch (state) { - case 'ready': - case 'available': - return '已就绪'; - case 'login_required': - return '需要登录'; - case 'permission_denied': - return '权限不足'; - case 'revoked': - return '需要重新授权'; - default: - return '暂不可用'; - } -} - -export const agentRoleMemoryFileNames: Record = { - Director: 'director.md', - Gameplay: 'gameplay.md', - Difficulty: 'difficulty.md', - Asset: 'asset.md', - Polish: 'polish.md', - SFX: 'sfx.md', - Code: 'code.md', - Review: 'review.md', - Preview: 'preview.md', - Playtest: 'playtest.md', - Publish: 'publish.md', -}; - -export function agentMemoryReadDraftFromTask(task: GameCreationAppTaskState) { - const fileName = agentRoleMemoryFileNames[task.role]; - if (!fileName) { - return null; - } - return { - task, - path: `memory/agents/${task.group}/${fileName}`, - }; -} - -export function agentMemoryReadDraftsFromManifest( - nextManifest: GameCreationAppManifest, -) { - return taskRowsFromManifest(nextManifest) - .map(agentMemoryReadDraftFromTask) - .filter((draft): draft is NonNullable => draft !== null); -} - -export function agentConversationReadDraftFromTask( - task: GameCreationAppTaskState, -) { - return { - task, - path: `.agent/conversations/agents/${agentConversationId(task)}.jsonl`, - }; -} - -export function agentConversationReadDraftsFromManifest( - nextManifest: GameCreationAppManifest, -) { - return taskRowsFromManifest(nextManifest).map( - agentConversationReadDraftFromTask, - ); -} - -export function formatTraceTaskWaves( - waves: string[][], - tasks: GameCreationAppTaskState[], -) { - return ( - waves - .map((wave) => - wave.map((taskId) => formatTraceTaskId(taskId, tasks)).join(' + '), - ) - .join(' / ') || 'none' - ); -} - -export function summarizeSuggestedToolCalls(trace: GameCreationAgentRunTrace) { - const suggestedToolCalls = trace.steps - .flatMap((step) => step.toolCalls) - .filter( - (toolCall) => - toolCall.status === 'suggested' || - toolCall.toolId.startsWith('agent.tool.suggest.'), - ); - const lines = suggestedToolCalls - .slice(0, 5) - .map((toolCall) => `- ${toolCall.toolId}: ${toolCall.summary}`); - if (suggestedToolCalls.length > lines.length) { - lines.push(`- 还有 ${suggestedToolCalls.length - lines.length} 个建议命令`); - } - return lines.join('\n'); -} - -export function commandDraftFromSuggestedToolCall( - toolCall: GameCreationAgentToolCallTrace, -) { - if ( - toolCall.toolId.includes('canvas.project_sync') && - (toolCall.status === 'suggested' || - toolCall.toolId.startsWith('agent.tool.suggest.')) - ) { - return '/sync-canvas-project '; - } - return null; -} - -export function commandDraftFromAgentRunTrace( - trace: GameCreationAgentRunTrace, -) { - return ( - trace.steps - .flatMap((step) => step.toolCalls) - .map(commandDraftFromSuggestedToolCall) - .find((commandDraft) => commandDraft !== null) ?? null - ); -} - -export function readablePassArtifactsFromAgentRunTrace( - trace: GameCreationAgentRunTrace, -) { - return trace.artifacts.filter( - (artifact) => - artifact.path.startsWith('.agent/passes/') && - isSafeProjectRelativePath(artifact.path), - ); -} - -export function summarizeLlmConversation(trace: GameCreationAgentRunTrace) { - const llmSteps = trace.steps.filter((step) => - step.toolCalls.some((toolCall) => toolCall.toolId.startsWith('llm.')), - ); - const visibleLlmSteps = llmSteps.slice(-6); - const lines = visibleLlmSteps.map((step) => { - const toolIds = step.toolCalls - .filter((toolCall) => toolCall.toolId.startsWith('llm.')) - .map((toolCall) => toolCall.toolId) - .join(', '); - return `- ${step.agent} #${step.pass} · ${step.status} · ${step.phase} · ${toolIds}`; - }); - if (llmSteps.length > visibleLlmSteps.length) { - lines.push( - `- 还有 ${llmSteps.length - visibleLlmSteps.length} 个较早 LLM 步骤`, - ); - } - return lines.join('\n'); -} - -export function summarizeAgentRunTrace(trace: GameCreationAgentRunTrace) { - const llmConversation = summarizeLlmConversation(trace); - const visibleRecentSteps = trace.steps.slice(-5); - const recentStepLines = visibleRecentSteps.map( - (step) => `- ${step.agent} #${step.pass} · ${step.status} · ${step.phase}`, - ); - if (trace.steps.length > visibleRecentSteps.length) { - recentStepLines.push( - `- 还有 ${trace.steps.length - visibleRecentSteps.length} 个较早步骤`, - ); - } - const recentSteps = recentStepLines.join('\n'); - const taskCounts = trace.taskGraph.tasks.reduce< - Record - >( - (current, task) => { - current[task.status] += 1; - return current; - }, - { - pending: 0, - running: 0, - 'waiting-for-confirmation': 0, - completed: 0, - failed: 0, - }, - ); - const taskSummary = ( - [ - 'completed', - 'waiting-for-confirmation', - 'running', - 'pending', - 'failed', - ] as const - ) - .filter((status) => taskCounts[status] > 0) - .map((status) => `${taskStatusLabels[status]} ${taskCounts[status]}`) - .join(','); - const repair = formatTraceRepairRoutes( - trace.taskGraph.repairRoutes, - trace.taskGraph.tasks, - ); - const suggestedTools = summarizeSuggestedToolCalls(trace); - const allPassPlans = trace.passPlans ?? []; - const visiblePassPlans = allPassPlans.slice(-3); - const passPlanLines = visiblePassPlans.map((plan) => { - const waves = formatTraceTaskWaves( - plan.dependencyWaves, - trace.taskGraph.tasks, - ); - return [ - `- pass ${plan.pass} · ${plan.mode}`, - `active ${formatTraceTaskIds(plan.activeTaskIds, trace.taskGraph.tasks)}`, - `carry ${formatTraceTaskIds(plan.carriedTaskIds, trace.taskGraph.tasks)}`, - `waves ${waves}`, - plan.repairFocus.length > 0 - ? `repair ${plan.repairFocus.join(';')}` - : null, - plan.repairRoutes.length > 0 - ? `routes ${formatTraceRepairRoutes(plan.repairRoutes, trace.taskGraph.tasks)}` - : null, - ] - .filter(Boolean) - .join(' · '); - }); - if (allPassPlans.length > visiblePassPlans.length) { - passPlanLines.push( - `- 还有 ${allPassPlans.length - visiblePassPlans.length} 个较早轮次`, - ); - } - const passPlans = passPlanLines.join('\n'); - const visibleArtifacts = trace.artifacts.slice(-5); - const artifactLines = visibleArtifacts.map( - (artifact) => `- ${artifact.path} · ${artifact.checksum}`, - ); - if (trace.artifacts.length > visibleArtifacts.length) { - artifactLines.push( - `- 还有 ${trace.artifacts.length - visibleArtifacts.length} 个较早产物`, - ); - } - const artifacts = artifactLines.join('\n'); - - return [ - `Run:${trace.runId}`, - `状态:${formatAgentRunStatus(trace)}`, - `工具调用:${trace.toolCallCount}/${trace.maxToolCalls}`, - `下一步:${trace.nextStep}`, - llmConversation ? `LLM 对话:\n${llmConversation}` : null, - taskSummary ? `任务:${taskSummary}` : null, - `active 任务:${formatTraceTaskIds( - trace.taskGraph.activeTaskIds, - trace.taskGraph.tasks, - )}`, - `carry-over 任务:${formatTraceTaskIds( - trace.taskGraph.carriedTaskIds, - trace.taskGraph.tasks, - )}`, - trace.taskGraph.repairFocus.length > 0 - ? `返工焦点:${trace.taskGraph.repairFocus.join(';')}` - : null, - repair ? `返工路线:${repair}` : null, - suggestedTools ? `建议命令:\n${suggestedTools}` : null, - passPlans ? `编排轮次:\n${passPlans}` : null, - artifacts ? `产物快照:\n${artifacts}` : null, - recentSteps ? `最近步骤:\n${recentSteps}` : null, - ] - .filter(Boolean) - .join('\n'); -} - -export function summarizeAgentRunCompletionForChat( - trace: GameCreationAgentRunTrace, -) { - const suggestedCommand = commandDraftFromAgentRunTrace(trace); - const readableArtifactPath = readableArtifactPathFromAgentRunTrace(trace); - const artifactReadCommand = readableArtifactPath - ? `/read ${readableArtifactPath}` - : undefined; - return { - text: `${summarizeAgentRunTrace(trace)}\n完整 trace:/trace`, - draftCommand: suggestedCommand ?? artifactReadCommand, - draftCommandLabel: - suggestedCommand || !artifactReadCommand ? undefined : '读取首个产物', - }; -} diff --git a/apps/ai-game-creator-shell/src/features/project-summary/agentRunSummaries.ts b/apps/ai-game-creator-shell/src/features/project-summary/agentRunSummaries.ts deleted file mode 100644 index 9d037d9d8..000000000 --- a/apps/ai-game-creator-shell/src/features/project-summary/agentRunSummaries.ts +++ /dev/null @@ -1,311 +0,0 @@ -import { - type GameCreationAgentRunTrace, - type GameCreationAppManifest, - type GameCreationAppTaskState, - selectGameCreationAppReadyTasks, -} from '../../../../../packages/shared/src/contracts/gameCreationApp'; -import { - type AgentRunHistoryItem, - type AgentStatusCard, -} from '../../app/types'; -import { taskRowsFromManifest } from '../agent-runtime'; -import { - formatAgentRunStatus, - formatTraceRepairRoutes, - isAgentReviewStep, - isAgentRunTracePassed, -} from './agentTrace'; -import { isSafeProjectRelativePath } from './projectPath'; -import { previewStatusLabels } from './projectSummaryConstants'; - -export function summarizeAgentRunBudget( - trace: GameCreationAgentRunTrace | null, -) { - if (!trace) { - return { - text: '运行预算:\n- 最近 Run:暂无\n- 建议:/next', - draftCommand: '/next', - draftCommandLabel: '查看下一步', - }; - } - - const remainingPasses = Math.max(trace.maxPasses - trace.passes, 0); - const remainingToolCalls = Math.max( - trace.maxToolCalls - trace.toolCallCount, - 0, - ); - const blocked = - trace.lifecycleStatus === 'killed' || - trace.status === 'failed' || - trace.status === 'needs-revision' || - trace.stopReason === 'max-passes-exhausted' || - remainingPasses === 0 || - remainingToolCalls === 0; - const draftCommand = blocked - ? '/review' - : isAgentRunTracePassed(trace) - ? '/publish' - : '/trace'; - - return { - text: [ - '运行预算:', - `- Run:${trace.runId} · ${formatAgentRunStatus(trace)}`, - `- 轮次:已用 ${trace.passes}/${trace.maxPasses} · 剩余 ${remainingPasses}`, - `- 工具调用:已用 ${trace.toolCallCount}/${trace.maxToolCalls} · 剩余 ${remainingToolCalls}`, - `- 下一步:${trace.nextStep}`, - `- 建议:${draftCommand}`, - ].join('\n'), - draftCommand, - draftCommandLabel: - draftCommand === '/review' - ? '查看评审' - : draftCommand === '/publish' - ? '查看发布准备' - : '查看 trace', - }; -} - -export function summarizeAgentReviewState( - trace: GameCreationAgentRunTrace | null, -) { - if (!trace) { - return { - text: '评审状态:暂无最近 trace', - draftCommand: '/next', - draftCommandLabel: '查看下一步', - }; - } - - const reviewSteps = trace.steps.filter(isAgentReviewStep); - const visibleReviewSteps = reviewSteps.slice(-3); - const reviewStepLines = visibleReviewSteps.map((step) => { - const outputPaths = step.outputPaths.filter(isSafeProjectRelativePath); - return [ - `- ${step.agent} #${step.pass} · ${step.status} · ${step.summary}`, - outputPaths.length > 0 ? `输出 ${outputPaths.join(', ')}` : null, - ] - .filter(Boolean) - .join(' · '); - }); - if (reviewSteps.length > visibleReviewSteps.length) { - reviewStepLines.push( - `- 还有 ${reviewSteps.length - visibleReviewSteps.length} 个较早评审步骤`, - ); - } - const repair = formatTraceRepairRoutes( - trace.taskGraph.repairRoutes, - trace.taskGraph.tasks, - ); - const needsResume = - trace.lifecycleStatus === 'killed' || - trace.status === 'failed' || - trace.status === 'needs-revision' || - trace.stopReason === 'max-passes-exhausted'; - const evaluatorState = isAgentRunTracePassed(trace) - ? '通过' - : needsResume - ? '需返工' - : '未通过'; - - return { - text: [ - '评审状态:', - `- Run:${trace.runId} · ${formatAgentRunStatus(trace)}`, - `- Evaluator:${evaluatorState}`, - `- 返工焦点:${ - trace.taskGraph.repairFocus.length > 0 - ? trace.taskGraph.repairFocus.join(';') - : '暂无' - }`, - `- 返工路线:${repair || '暂无'}`, - `- 下一步:${trace.nextStep}`, - '- 评审记录:/read .agent/findings.md', - reviewStepLines.length > 0 - ? `- 最近评审步骤:\n${reviewStepLines.join('\n')}` - : '- 最近评审步骤:暂无', - ].join('\n'), - draftCommand: needsResume ? '/agent-resume ' : '/read .agent/findings.md', - draftCommandLabel: needsResume ? '继续修复' : '读取评审记录', - }; -} - -export function summarizeProjectContextSources( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const tasks = taskRowsFromManifest(nextManifest); - const llmInputPaths = trace - ? Array.from( - new Set( - trace.steps - .filter((step) => - step.toolCalls.some((toolCall) => - toolCall.toolId.startsWith('llm.'), - ), - ) - .flatMap((step) => step.inputPaths) - .filter(isSafeProjectRelativePath), - ), - ).slice(0, 8) - : []; - const inputPathLines = llmInputPaths.map( - (path) => `- ${path}:/read ${path}`, - ); - const draftCommand = - llmInputPaths.length > 0 - ? `/read ${llmInputPaths[0]}` - : '/memory blackboard'; - - return { - text: [ - '上下文来源:', - '- 项目对话:/history', - '- 短期记忆:/memory short', - '- 长期记忆:/memory long', - '- 项目黑板:/memory blackboard', - `- Agent 对话:${tasks.length} 个 · /agent-conversations`, - `- Agent 私有记忆:${tasks.length} 个 · /agent-memories`, - '- 项目 manifest:/read .agent/manifest.json', - trace ? `- 最近 Run:${trace.runId} · /trace` : '- 最近 Run:暂无', - inputPathLines.length > 0 - ? `最近 LLM 输入:\n${inputPathLines.join('\n')}` - : '最近 LLM 输入:暂无', - ].join('\n'), - draftCommand, - draftCommandLabel: llmInputPaths.length > 0 ? '读取首个上下文' : '查看黑板', - }; -} - -export function summarizeProjectTimeline( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const commandRuns = (nextManifest.commandRuns ?? []).slice(-5); - const commandLines = commandRuns.map((commandRun) => { - const logSuffix = isSafeProjectRelativePath(commandRun.logPath) - ? ` · 日志 /read ${commandRun.logPath}` - : ''; - return `- 命令 ${commandRun.commandId} · ${ - commandRun.status === 'completed' ? '完成' : '失败' - }${logSuffix}`; - }); - const visibleSteps = trace?.steps.slice(-6) ?? []; - const stepLines = visibleSteps.map((step) => { - const outputPaths = step.outputPaths - .filter(isSafeProjectRelativePath) - .slice(0, 3); - return [ - `- ${step.agent} #${step.pass} / ${step.phase} · ${step.status} · ${step.summary}`, - outputPaths.length > 0 ? `输出 ${outputPaths.join(', ')}` : null, - ] - .filter(Boolean) - .join(' · '); - }); - const latestSafeLogPath = [...commandRuns] - .reverse() - .find((commandRun) => - isSafeProjectRelativePath(commandRun.logPath), - )?.logPath; - const draftCommand = latestSafeLogPath - ? `/read ${latestSafeLogPath}` - : trace - ? '/trace' - : '/history'; - - return { - text: [ - '项目时间线:', - `- Run:${trace ? `${trace.runId} · ${formatAgentRunStatus(trace)}` : '暂无最近 run'}`, - commandLines.length > 0 - ? `- 最近命令:\n${commandLines.join('\n')}` - : '- 最近命令:暂无', - stepLines.length > 0 - ? `- 最近步骤:\n${stepLines.join('\n')}` - : '- 最近步骤:暂无', - ].join('\n'), - draftCommand, - draftCommandLabel: latestSafeLogPath - ? '读取最近日志' - : trace - ? '查看 trace' - : '查看历史', - }; -} - -export function summarizeProjectHandoff( - nextManifest: GameCreationAppManifest, - nextProjectPath: string, - trace: GameCreationAgentRunTrace | null, - history: AgentRunHistoryItem[], - agents: AgentStatusCard[], -) { - const tasks = taskRowsFromManifest(nextManifest); - const completedCount = tasks.filter( - (task) => task.status === 'completed', - ).length; - const manifestTasksById = new Map(tasks.map((task) => [task.id, task])); - const traceTasksById = new Map( - trace?.taskGraph.tasks.map((task) => [task.id, task]) ?? [], - ); - const traceReadyTasks = - trace?.taskGraph.readyTaskIds - .map( - (taskId) => traceTasksById.get(taskId) ?? manifestTasksById.get(taskId), - ) - .filter((task): task is GameCreationAppTaskState => Boolean(task)) ?? []; - const readyTasks = - traceReadyTasks.length > 0 - ? traceReadyTasks - : selectGameCreationAppReadyTasks({ tasks }); - const failedTasks = tasks.filter((task) => task.status === 'failed'); - const sourceCounts = nextManifest.assets.reduce( - (counts, asset) => { - counts[asset.source.kind] += 1; - return counts; - }, - { uploaded: 0, generated: 0, canvas: 0 }, - ); - const preview = nextManifest.preview; - const previewSummary = - preview?.status === 'running' && preview.url - ? `运行中 ${preview.url}` - : preview - ? previewStatusLabels[preview.status] - : '未启动'; - const commandRuns = nextManifest.commandRuns ?? []; - const latestCommandRun = commandRuns[commandRuns.length - 1]; - const evidenceAgentCount = agents.filter( - (agent) => agent.hasRecentEvidence, - ).length; - const activeAgentCount = agents.filter( - (agent) => agent.taskGraphState === 'active', - ).length; - const runSummary = trace - ? `${trace.runId} · ${formatAgentRunStatus(trace)} · next ${trace.nextStep}` - : history[0] - ? `无 latest,最近历史 ${history[0].trace.runId} · ${formatAgentRunStatus(history[0].trace)}` - : '暂无 run'; - const readySummary = - readyTasks.length > 0 - ? readyTasks - .slice(0, 3) - .map((task) => `${task.group}/${task.role} ${task.title}`) - .join(';') - : '暂无'; - const failedSummary = - failedTasks.length > 0 - ? failedTasks - .slice(0, 3) - .map((task) => `${task.group}/${task.role} ${task.title}`) - .join(';') - : '暂无'; - - return `项目交接:\n- 项目:${nextManifest.name}\n- 目录:${nextProjectPath}\n- Run:${runSummary}\n- 任务:完成 ${completedCount}/${tasks.length} · ready ${readyTasks.length} · 失败 ${failedTasks.length}\n- Ready:${readySummary}\n- 失败项:${failedSummary}\n- 资产:${nextManifest.assets.length} 个 · 上传 ${sourceCounts.uploaded} / 生成 ${sourceCounts.generated} / 画板 ${sourceCounts.canvas}\n- 预览:${previewSummary}\n- Agent:${evidenceAgentCount}/${agents.length} 有运行证据 · active ${activeAgentCount}\n- 历史:已加载 ${history.length} 个 run\n- 最近命令:${ - latestCommandRun - ? `${latestCommandRun.commandId} · ${ - latestCommandRun.status === 'completed' ? '完成' : '失败' - }` - : '暂无' - }`; -} diff --git a/apps/ai-game-creator-shell/src/features/project-summary/agentTrace.ts b/apps/ai-game-creator-shell/src/features/project-summary/agentTrace.ts deleted file mode 100644 index 2a619e13a..000000000 --- a/apps/ai-game-creator-shell/src/features/project-summary/agentTrace.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { - type GameCreationAgentRepairRouteTrace, - type GameCreationAgentRunStep, - type GameCreationAgentRunTrace, - type GameCreationAppTaskState, -} from '../../../../../packages/shared/src/contracts/gameCreationApp'; -import { isSafeProjectRelativePath } from './projectPath'; -import { taskGroupLabels } from './projectSummaryConstants'; - -export function isAgentRunTracePassed(trace: GameCreationAgentRunTrace | null) { - return ( - trace?.status === 'passed' || - trace?.status === 'artifacts-written' || - trace?.stopReason === 'evaluator-passed' - ); -} - -export function isAgentReviewStep(step: GameCreationAgentRunStep) { - return ( - step.agent.toLowerCase().includes('evaluator') || - step.phase === 'evaluation' || - step.phase === 'evaluate' || - step.taskId === 'quality-review' - ); -} - -export function isPlaytestTraceStep(step: GameCreationAgentRunStep) { - return ( - step.agent.toLowerCase().includes('playtest') || - step.phase === 'playtest' || - step.taskId === 'preview-playtest' || - step.toolCalls.some( - (toolCall) => - toolCall.toolId === 'game.static_smoke' || - toolCall.toolId.startsWith('preview.'), - ) - ); -} - -export function formatAgentRunStatus(trace: GameCreationAgentRunTrace) { - const status = trace.lifecycleStatus - ? `${trace.status} / ${trace.lifecycleStatus}` - : trace.status; - return `${status} · ${trace.passes}/${trace.maxPasses} 轮 · ${trace.stopReason}`; -} - -export function formatTraceTaskId( - taskId: string, - tasks: GameCreationAppTaskState[], -) { - const task = tasks.find((candidate) => candidate.id === taskId); - if (!task) { - return taskId; - } - return `${taskGroupLabels[task.group]} / ${task.role} ${task.title}(${task.id})`; -} - -export function formatTraceTaskIds( - taskIds: string[], - tasks: GameCreationAppTaskState[], -) { - return taskIds.length > 0 - ? taskIds.map((taskId) => formatTraceTaskId(taskId, tasks)).join(', ') - : 'none'; -} - -export function formatTraceRepairRoutes( - routes: GameCreationAgentRepairRouteTrace[], - tasks: GameCreationAppTaskState[], -) { - const visibleRoutes = routes - .slice(0, 3) - .map( - (route) => `${route.reason}: ${formatTraceTaskIds(route.taskIds, tasks)}`, - ); - if (routes.length > visibleRoutes.length) { - visibleRoutes.push(`还有 ${routes.length - visibleRoutes.length} 条路线`); - } - return visibleRoutes.join(';'); -} - -export function readableArtifactPathFromAgentRunTrace( - trace: GameCreationAgentRunTrace, -) { - return trace.artifacts.find((artifact) => - isSafeProjectRelativePath(artifact.path), - )?.path; -} - -export function readableArtifactsFromAgentRunTrace( - trace: GameCreationAgentRunTrace, -) { - return trace.artifacts.filter((artifact) => - isSafeProjectRelativePath(artifact.path), - ); -} diff --git a/apps/ai-game-creator-shell/src/features/project-summary/chatCommandMetadata.ts b/apps/ai-game-creator-shell/src/features/project-summary/chatCommandMetadata.ts deleted file mode 100644 index df190e75e..000000000 --- a/apps/ai-game-creator-shell/src/features/project-summary/chatCommandMetadata.ts +++ /dev/null @@ -1,48 +0,0 @@ -export function missingChatCommandArgumentMessage(prompt: string) { - switch (prompt) { - case '/project': - return '格式:/project /绝对路径'; - case '/generate': - case '/draft': - return '格式:/generate 创作想法'; - case '/diff': - return '格式:/diff checkpoint-id'; - case '/restore': - return '格式:/restore checkpoint-id'; - case '/policy-deny': - return '格式:/policy-deny file.write'; - case '/policy-allow': - return '格式:/policy-allow file.write'; - case '/policy-confirm': - return '格式:/policy-confirm project.index'; - case '/policy-auto': - return '格式:/policy-auto project.index'; - case '/agent-policy-deny': - return '格式:/agent-policy-deny design-director file.read'; - case '/agent-policy-allow': - return '格式:/agent-policy-allow design-director file.read'; - case '/agent-policy-confirm': - return '格式:/agent-policy-confirm design-director memory.write'; - case '/agent-policy-auto': - return '格式:/agent-policy-auto design-director memory.write'; - case '/read': - return '格式:/read game/index.html'; - case '/asset-register': - return '格式:/asset-register assets/hero.png [kind] [mediaType]'; - case '/remember': - return '请提供要追加的记忆内容。'; - case '/memory-set': - return '请提供要保存的记忆内容。'; - case '/canvas': - case '/sync-canvas-project': - return '请提供画板项目 ID。'; - case '/generate-art': - return '请提供美术生成提示词。'; - case '/import-canvas-asset': - return '格式:/import-canvas-asset assets/hero.png 画板项目ID 资源ID|object:资产对象ID'; - case '/import-canvas-export': - return '格式:/import-canvas-export /绝对/画板素材.zip 画板项目ID'; - default: - return null; - } -} diff --git a/apps/ai-game-creator-shell/src/features/project-summary/projectArtifactSummaries.ts b/apps/ai-game-creator-shell/src/features/project-summary/projectArtifactSummaries.ts deleted file mode 100644 index ce4906830..000000000 --- a/apps/ai-game-creator-shell/src/features/project-summary/projectArtifactSummaries.ts +++ /dev/null @@ -1,368 +0,0 @@ -import { - type LocalProjectCheckpointResult, - type LocalProjectCheckpointSummary, - type LocalProjectDiffResult, - type LocalProjectExportPackageResult, - type LocalProjectExportPackagesResult, - type LocalProjectFileEntry, - type LocalProjectFileResult, - type LocalProjectIndexResult, - type ProjectAssetDraft, - type ProjectFileActionDraft, - type ProjectPermissionPolicyView, -} from '../../app/types'; -import { - commonAgentRunSupportReadDrafts, - commonProjectArtifactReadDrafts, - commonProjectInternalReadDrafts, - commonProjectLogReadDrafts, -} from './projectSummaryConstants'; - -export function summarizeProjectFiles(files: LocalProjectFileEntry[]) { - if (files.length === 0) { - return '本地项目还没有文件。'; - } - - const visibleFiles = files.slice(0, 40); - const lines = visibleFiles.map((file) => - file.kind === 'directory' ? `- ${file.path}/` : `- ${file.path}`, - ); - if (files.length > visibleFiles.length) { - lines.push(`- 还有 ${files.length - visibleFiles.length} 项`); - } - return `本地项目文件:\n${lines.join('\n')}`; -} - -export function summarizeProjectIndex(result: LocalProjectIndexResult) { - const visibleFiles = result.files.slice(0, 12); - const lines = visibleFiles.map((file) => `- ${file.path} · ${file.size}B`); - if (result.files.length > visibleFiles.length) { - lines.push(`- 还有 ${result.files.length - visibleFiles.length} 项`); - } - return [ - `索引:${result.fileCount} 个文件,${result.totalBytes}B`, - `路径:${result.indexPath}`, - lines.join('\n'), - ] - .filter(Boolean) - .join('\n'); -} - -export function summarizeProjectCheckpoint( - result: LocalProjectCheckpointResult, -) { - return [ - `已保存 checkpoint:${result.checkpointId}`, - `文件:${result.fileCount} 个,${result.totalBytes}B`, - `路径:${result.checkpointPath}`, - ].join('\n'); -} - -export function summarizeProjectExportPackage( - result: LocalProjectExportPackageResult, -) { - return [ - `已导出本地试玩包:${result.packageRelativePath}`, - `文件:${result.fileCount} 个,${result.totalBytes}B`, - `路径:${result.packagePath}`, - ].join('\n'); -} - -export function summarizeProjectExportPackages( - result: LocalProjectExportPackagesResult, -) { - if (result.packages.length === 0) { - return '本地试玩包:暂无。输入 /export 导出当前可试玩原型。'; - } - const visiblePackages = result.packages.slice(0, 8); - const lines = visiblePackages.map( - (item) => `- ${item.packageRelativePath} · ${item.totalBytes}B`, - ); - if (result.packages.length > visiblePackages.length) { - lines.push( - `- 还有 ${result.packages.length - visiblePackages.length} 个更早试玩包`, - ); - } - return `本地试玩包:\n${lines.join('\n')}`; -} - -export function checkpointIdFromManifestPath(path: string) { - const match = path.match(/^\.agent\/checkpoints\/([^/]+)\/manifest\.json$/); - return match?.[1] ?? null; -} - -export function isCheckpointManifestFile(file: LocalProjectFileEntry) { - return file.kind === 'file' && checkpointIdFromManifestPath(file.path); -} - -export function sortCheckpointManifestFiles(files: LocalProjectFileEntry[]) { - return files.filter(isCheckpointManifestFile).sort((left, right) => { - const modifiedDelta = (right.modifiedAt ?? 0) - (left.modifiedAt ?? 0); - return modifiedDelta || right.path.localeCompare(left.path); - }); -} - -export function summarizeProjectCheckpoints( - checkpoints: LocalProjectCheckpointSummary[], - hiddenCount: number, -) { - if (checkpoints.length === 0) { - return '还没有 checkpoint。输入 /checkpoint 保存当前项目快照。'; - } - const lines = checkpoints.map((checkpoint) => - [ - `- ${checkpoint.checkpointId}`, - `${checkpoint.fileCount} 个文件`, - `${checkpoint.totalBytes}B`, - checkpoint.createdAt ? `createdAt ${checkpoint.createdAt}` : null, - `/diff ${checkpoint.checkpointId}`, - `/restore ${checkpoint.checkpointId}`, - ] - .filter(Boolean) - .join(' · '), - ); - if (hiddenCount > 0) { - lines.push(`- 还有 ${hiddenCount} 个更早 checkpoint`); - } - return `最近 checkpoint:\n${lines.join('\n')}`; -} - -export function checkpointSummaryFromManifest( - file: LocalProjectFileEntry, - content: string, -): LocalProjectCheckpointSummary { - const fallbackId = checkpointIdFromManifestPath(file.path) ?? file.path; - try { - const parsed: unknown = JSON.parse(content); - const data = - parsed && typeof parsed === 'object' - ? (parsed as { - checkpointId?: unknown; - createdAt?: unknown; - files?: unknown; - }) - : {}; - const files = Array.isArray(data.files) ? data.files : []; - const manifestFileCount = (data as { fileCount?: unknown }).fileCount; - const fileCount = - files.length > 0 - ? files.length - : typeof manifestFileCount === 'number' && manifestFileCount >= 0 - ? manifestFileCount - : 0; - const totalBytes = files.reduce((sum, item) => { - if (!item || typeof item !== 'object') { - return sum; - } - const size = (item as { size?: unknown }).size; - return sum + (typeof size === 'number' && size > 0 ? size : 0); - }, 0); - const manifestTotalBytes = (data as { totalBytes?: unknown }).totalBytes; - const resolvedTotalBytes = - totalBytes > 0 - ? totalBytes - : typeof manifestTotalBytes === 'number' && manifestTotalBytes >= 0 - ? manifestTotalBytes - : 0; - return { - checkpointId: - typeof data.checkpointId === 'string' && data.checkpointId.trim() - ? data.checkpointId - : fallbackId, - path: file.path, - fileCount, - totalBytes: resolvedTotalBytes, - createdAt: - typeof data.createdAt === 'number' || typeof data.createdAt === 'string' - ? String(data.createdAt) - : '', - modifiedAt: file.modifiedAt, - }; - } catch { - return { - checkpointId: fallbackId, - path: file.path, - fileCount: 0, - totalBytes: 0, - createdAt: '', - modifiedAt: file.modifiedAt, - }; - } -} - -export function summarizeProjectDiff(result: LocalProjectDiffResult) { - const section = (label: string, files: Array<{ path: string }>) => { - if (files.length === 0) { - return null; - } - const visibleFiles = files.slice(0, 20); - const lines = visibleFiles.map((file) => `- ${file.path}`); - if (files.length > visibleFiles.length) { - lines.push(`- 还有 ${files.length - visibleFiles.length} 项`); - } - return `${label}:\n${lines.join('\n')}`; - }; - return ( - [ - `checkpoint:${result.checkpointId}`, - section('新增', result.added), - section('变更', result.changed), - section('删除', result.deleted), - ] - .filter(Boolean) - .join('\n') || '无差异。' - ); -} - -export function summarizeProjectPolicy(view: ProjectPermissionPolicyView) { - const agentPolicies = view.policy.agentPolicies ?? {}; - const agentPolicyLines = Object.entries(agentPolicies) - .slice(0, 8) - .map( - ([agentId, policy]) => - `Agent ${agentId}:拒绝 ${formatProjectPolicyCommandList( - policy.deniedCommands, - )};确认 ${formatProjectPolicyCommandList(policy.confirmCommands)}`, - ); - if (Object.keys(agentPolicies).length > agentPolicyLines.length) { - agentPolicyLines.push( - `Agent 策略还有 ${Object.keys(agentPolicies).length - agentPolicyLines.length} 项`, - ); - } - return [ - `策略:${view.path}`, - `拒绝:${formatProjectPolicyCommandList(view.policy.deniedCommands)}`, - `确认:${formatProjectPolicyCommandList(view.policy.confirmCommands)}`, - ...agentPolicyLines, - ].join('\n'); -} - -export function formatProjectPolicyCommandList(values: string[]) { - if (values.length === 0) { - return '无'; - } - const visibleValues = values.slice(0, 12); - return [ - visibleValues.join('、'), - values.length > visibleValues.length - ? `还有 ${values.length - visibleValues.length} 项` - : null, - ] - .filter(Boolean) - .join('、'); -} - -export function formatCanvasAssetSource(source: { - canvasProjectId: string; - canvasAssetId: string; - canvasAssetObjectId?: string; -}) { - const assetReference = source.canvasAssetObjectId - ? `object:${source.canvasAssetObjectId}` - : source.canvasAssetId; - return `${source.canvasProjectId} / ${assetReference || '未提供资产 ID'}`; -} - -export function summarizeProjectFileContent(result: LocalProjectFileResult) { - const limit = 4000; - const content = - result.content.length > limit - ? `${result.content.slice(0, limit)}\n...已截断 ${ - result.content.length - limit - } 字符` - : result.content; - - const visibleContent = content || '空文件'; - let longestBacktickRun = 0; - for (const match of visibleContent.matchAll(/`+/gu)) { - longestBacktickRun = Math.max(longestBacktickRun, match[0].length); - } - const fence = '`'.repeat(Math.max(3, longestBacktickRun + 1)); - return `文件:${result.path}\n\n${fence}text\n${visibleContent}\n${fence}`; -} - -export function inferProjectFileAssetDraft( - localPath: string, -): ProjectAssetDraft { - const extension = localPath.split('.').pop()?.toLowerCase() ?? ''; - if ( - ['png', 'jpg', 'jpeg', 'webp', 'gif', 'svg', 'avif'].includes(extension) - ) { - const normalizedExtension = extension === 'jpg' ? 'jpeg' : extension; - return { - localPath, - kind: 'image', - mediaType: - extension === 'svg' ? 'image/svg+xml' : `image/${normalizedExtension}`, - }; - } - if (['mp3', 'wav', 'ogg', 'm4a', 'flac'].includes(extension)) { - return { - localPath, - kind: 'audio', - mediaType: extension === 'm4a' ? 'audio/mp4' : `audio/${extension}`, - }; - } - if (['mp4', 'webm', 'mov'].includes(extension)) { - return { - localPath, - kind: 'video', - mediaType: extension === 'mov' ? 'video/quicktime' : `video/${extension}`, - }; - } - if (extension === 'json') { - return { localPath, kind: 'document', mediaType: 'application/json' }; - } - if (extension === 'html') { - return { localPath, kind: 'document', mediaType: 'text/html' }; - } - if (['txt', 'md', 'csv'].includes(extension)) { - return { localPath, kind: 'document', mediaType: 'text/plain' }; - } - // 判不出内容类型就写 `unknown`(→「待归类」),与 Rust 侧 `uploaded_asset_kind` 同口径: - // `/asset-register` 的 kind 会原样进 `register_local_asset` 的严格解析,写 `data` / `asset` - // 这类非 canonical 值只会被判成"认不出"并留痕,不能靠它表达"这是数据 / 这是资产"。 - return { localPath, kind: 'unknown', mediaType: 'application/octet-stream' }; -} - -export function projectAssetDraftCommand(draft: ProjectAssetDraft) { - return `/asset-register ${draft.localPath} ${draft.kind} ${draft.mediaType}`; -} - -export function projectFileActionDrafts( - localPath: string, -): ProjectFileActionDraft { - return { - readCommand: `/read ${localPath}`, - assetCommand: projectAssetDraftCommand( - inferProjectFileAssetDraft(localPath), - ), - }; -} - -export function summarizeCommonProjectArtifactReadDrafts() { - return `常用生成产物:\n${commonProjectArtifactReadDrafts - .map( - (artifact) => - `- ${artifact.label} · ${artifact.path} · /read ${artifact.path}`, - ) - .join('\n')}`; -} - -export function summarizeCommonProjectLogReadDrafts() { - return `常用日志读取命令:\n${commonProjectLogReadDrafts - .map((log) => `- ${log.label} · ${log.path} · /read ${log.path}`) - .join('\n')}`; -} - -export function summarizeAgentRunSupportFileReadDrafts() { - return `Agent 运行辅助文件读取命令:\n${commonAgentRunSupportReadDrafts - .map((file) => `- ${file.label} · ${file.path} · /read ${file.path}`) - .join('\n')}`; -} - -export function summarizeProjectInternalReadDrafts() { - return `项目内部真相源读取命令:\n${commonProjectInternalReadDrafts - .map((file) => `- ${file.label} · ${file.path} · /read ${file.path}`) - .join('\n')}`; -} diff --git a/apps/ai-game-creator-shell/src/features/project-summary/projectAssetSummaries.ts b/apps/ai-game-creator-shell/src/features/project-summary/projectAssetSummaries.ts deleted file mode 100644 index 3afb4fde7..000000000 --- a/apps/ai-game-creator-shell/src/features/project-summary/projectAssetSummaries.ts +++ /dev/null @@ -1,282 +0,0 @@ -import { - type GameCreationAppAssetSourceKind, - type GameCreationAppManifest, - isGameCreationAppAssetAudioKind, - isGameCreationAppAssetVisualKind, - selectGameCreationAppReadyTasks, -} from '../../../../../packages/shared/src/contracts/gameCreationApp'; -import { taskRowsFromManifest } from '../agent-runtime'; -import { isSafeProjectRelativePath } from './projectPath'; -import { - assetSourceKindLabels, - taskGroupLabels, - taskStatusLabels, -} from './projectSummaryConstants'; - -export function summarizeProjectAssets(nextManifest: GameCreationAppManifest) { - if (nextManifest.assets.length === 0) { - return '本地项目还没有登记资产。'; - } - - const visibleAssets = nextManifest.assets.slice(0, 20); - const lines = visibleAssets.map( - (asset) => - `- ${asset.kind} · ${asset.localPath} · ${asset.source.kind}${ - asset.source.canvasProjectId - ? ` · 画板 ${asset.source.canvasProjectId}` - : '' - }`, - ); - if (nextManifest.assets.length > visibleAssets.length) { - lines.push( - `- 还有 ${nextManifest.assets.length - visibleAssets.length} 个资产`, - ); - } - return `本地项目资产:\n${lines.join('\n')}`; -} - -export function summarizeProjectAssetCredits( - nextManifest: GameCreationAppManifest, -) { - if (nextManifest.assets.length === 0) { - return { - text: [ - '素材署名:', - '- 当前资产:暂无登记资产', - '- 来源清单:暂无', - '- 需要确认:上传素材授权;生成素材模型;画板资源来源', - '- 建议:/assets', - ].join('\n'), - draftCommand: '/assets', - draftCommandLabel: '查看资产', - }; - } - - const sourceCounts = nextManifest.assets.reduce( - (counts, asset) => { - counts[asset.source.kind] += 1; - return counts; - }, - { uploaded: 0, generated: 0, canvas: 0 } satisfies Record< - GameCreationAppAssetSourceKind, - number - >, - ); - const visibleAssets = nextManifest.assets.slice(0, 10); - const lines = visibleAssets.map((asset) => { - const sourceLabel = assetSourceKindLabels[asset.source.kind]; - const sourceDetail = - asset.source.kind === 'canvas' - ? `画板 ${asset.source.canvasProjectId ?? '未记录'}` - : asset.source.kind === 'generated' - ? `生成${asset.source.model ? ` ${asset.source.model}` : ''}` - : '用户上传'; - return `- ${asset.localPath} · ${asset.mediaType} · ${sourceLabel} · ${sourceDetail}`; - }); - if (nextManifest.assets.length > visibleAssets.length) { - lines.push( - `- 还有 ${nextManifest.assets.length - visibleAssets.length} 个资产`, - ); - } - const sourceSummary = ( - Object.keys(sourceCounts) as GameCreationAppAssetSourceKind[] - ) - .filter((source) => sourceCounts[source] > 0) - .map((source) => `${assetSourceKindLabels[source]} ${sourceCounts[source]}`) - .join(' / '); - - return { - text: [ - '素材署名:', - `- 当前资产:${nextManifest.assets.length} 个`, - `- 来源分布:${sourceSummary || '暂无'}`, - `- 来源清单:\n${lines.join('\n')}`, - '- 需要确认:上传素材授权;生成素材模型;画板资源来源;本地试玩包保留来源口径', - '- 参考:/assets;/art;/audio;/listing', - '- 建议:/assets', - ].join('\n'), - draftCommand: '/assets', - draftCommandLabel: '查看资产', - }; -} - -export function isProjectAudioAsset( - asset: GameCreationAppManifest['assets'][number], -) { - const mediaType = asset.mediaType.toLowerCase(); - return ( - mediaType.startsWith('audio/') || - isGameCreationAppAssetAudioKind(asset.kind) - ); -} - -export function isProjectVisualAsset( - asset: GameCreationAppManifest['assets'][number], -) { - const mediaType = asset.mediaType.toLowerCase(); - return ( - mediaType.startsWith('image/') || - mediaType.startsWith('video/') || - mediaType === 'application/vnd.genarrative.image-sequence' || - isGameCreationAppAssetVisualKind(asset.kind) - ); -} - -export function summarizeProjectVisualAssets( - nextManifest: GameCreationAppManifest, -) { - const visualAssets = nextManifest.assets.filter(isProjectVisualAsset); - if (visualAssets.length === 0) { - return { - text: [ - '美术素材:暂无登记图片、视频或序列帧。', - '可先生成首版美术,或同步已有画板项目资源。', - ].join('\n'), - draftCommand: '/generate-art 首版核心美术素材', - draftCommandLabel: '生成美术', - }; - } - - const sourceCounts = visualAssets.reduce( - (counts, asset) => { - counts[asset.source.kind] += 1; - return counts; - }, - { uploaded: 0, generated: 0, canvas: 0 } satisfies Record< - GameCreationAppAssetSourceKind, - number - >, - ); - const visibleAssets = visualAssets.slice(0, 8); - const lines = visibleAssets.map( - (asset) => - `- ${asset.localPath} · ${asset.mediaType} · ${ - assetSourceKindLabels[asset.source.kind] - }${ - asset.source.canvasProjectId - ? ` · 画板 ${asset.source.canvasProjectId}` - : '' - }`, - ); - if (visualAssets.length > visibleAssets.length) { - lines.push( - `- 还有 ${visualAssets.length - visibleAssets.length} 个美术素材`, - ); - } - const hasCanvasVisualAsset = visualAssets.some( - (asset) => asset.source.kind === 'canvas', - ); - - return { - text: [ - `美术素材:${visualAssets.length} 个`, - `来源:${(Object.keys(sourceCounts) as GameCreationAppAssetSourceKind[]) - .filter((source) => sourceCounts[source] > 0) - .map( - (source) => - `${assetSourceKindLabels[source]} ${sourceCounts[source]}`, - ) - .join('、')}`, - hasCanvasVisualAsset - ? '画板来源:已接入' - : '画板来源:暂无 · 建议 /generate-art 首版核心美术素材', - lines.join('\n'), - ].join('\n'), - draftCommand: hasCanvasVisualAsset - ? '/read assets/manifest.art.json' - : '/generate-art 首版核心美术素材', - draftCommandLabel: hasCanvasVisualAsset ? '读美术清单' : '生成美术', - }; -} - -export function summarizeProjectAudioAssets( - nextManifest: GameCreationAppManifest, -) { - const audioAssets = nextManifest.assets.filter(isProjectAudioAsset); - if (audioAssets.length === 0) { - return { - text: [ - '音频素材:暂无登记音频。', - '可先登记项目内音效,或把已有画板音频作为素材导入。', - ].join('\n'), - draftCommand: '/asset-register assets/audio/sfx.wav audio audio/wav', - draftCommandLabel: '登记音效', - }; - } - - const sourceCounts = audioAssets.reduce( - (counts, asset) => { - counts[asset.source.kind] += 1; - return counts; - }, - { uploaded: 0, generated: 0, canvas: 0 } satisfies Record< - GameCreationAppAssetSourceKind, - number - >, - ); - const visibleAssets = audioAssets.slice(0, 8); - const lines = visibleAssets.map( - (asset) => - `- ${asset.localPath} · ${asset.mediaType} · ${ - assetSourceKindLabels[asset.source.kind] - }${ - asset.source.canvasProjectId - ? ` · 画板 ${asset.source.canvasProjectId}` - : '' - }`, - ); - if (audioAssets.length > visibleAssets.length) { - lines.push( - `- 还有 ${audioAssets.length - visibleAssets.length} 个音频素材`, - ); - } - - return { - text: [ - `音频素材:${audioAssets.length} 个`, - `来源:${(Object.keys(sourceCounts) as GameCreationAppAssetSourceKind[]) - .filter((source) => sourceCounts[source] > 0) - .map( - (source) => - `${assetSourceKindLabels[source]} ${sourceCounts[source]}`, - ) - .join('、')}`, - lines.join('\n'), - ].join('\n'), - draftCommand: '/read assets/manifest.audio.json', - draftCommandLabel: '读音频清单', - }; -} - -export function firstReadableProjectAssetPath( - nextManifest: GameCreationAppManifest, -) { - return nextManifest.assets.find((asset) => - isSafeProjectRelativePath(asset.localPath), - )?.localPath; -} - -export function summarizeProjectTasks(nextManifest: GameCreationAppManifest) { - const tasks = taskRowsFromManifest(nextManifest); - if (tasks.length === 0) { - return '还没有任务拆分。'; - } - const readyTasks = selectGameCreationAppReadyTasks({ - tasks, - }); - const readySummary = - readyTasks.length > 0 - ? `\n下一步:${readyTasks - .map((task) => `${taskGroupLabels[task.group]} / ${task.role}`) - .join(';')}` - : '\n下一步:等待确认或暂无可执行任务'; - - return `任务拆分:\n${tasks - .map( - (task) => - `- ${taskGroupLabels[task.group]} / ${task.role}:${task.title} · ${ - taskStatusLabels[task.status] - } -> ${task.artifacts.join(', ')}`, - ) - .join('\n')}${readySummary}`; -} diff --git a/apps/ai-game-creator-shell/src/features/project-summary/projectDeliverySummaries.ts b/apps/ai-game-creator-shell/src/features/project-summary/projectDeliverySummaries.ts deleted file mode 100644 index 0e3d4091c..000000000 --- a/apps/ai-game-creator-shell/src/features/project-summary/projectDeliverySummaries.ts +++ /dev/null @@ -1,661 +0,0 @@ -import { - type GameCreationAgentRunTrace, - type GameCreationAppAgentGroup, - type GameCreationAppManifest, - type GameCreationAppTaskState, - selectGameCreationAppReadyTasks, -} from '../../../../../packages/shared/src/contracts/gameCreationApp'; -import { taskRowsFromManifest } from '../agent-runtime'; -import { - formatAgentRunStatus, - isAgentRunTracePassed, - readableArtifactsFromAgentRunTrace, -} from './agentTrace'; -import { - isProjectAudioAsset, - isProjectVisualAsset, -} from './projectAssetSummaries'; -import { - previewStatusLabels, - taskGroupLabels, - taskStatusLabels, -} from './projectSummaryConstants'; - -export function summarizeProjectReleaseNotes( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const preview = nextManifest.preview; - const previewRunning = preview?.status === 'running' && preview.url; - const previewSummary = previewRunning - ? `运行中 ${preview.url}` - : preview - ? previewStatusLabels[preview.status] - : '未启动'; - const tracePassed = isAgentRunTracePassed(trace); - const blockedTrace = - trace?.lifecycleStatus === 'killed' || - trace?.status === 'failed' || - trace?.status === 'needs-revision' || - trace?.stopReason === 'max-passes-exhausted'; - const goal = - nextManifest.goal?.trim() || - trace?.goal?.trim() || - trace?.taskGraph.goal?.trim() || - '暂无'; - const artifacts = trace ? readableArtifactsFromAgentRunTrace(trace) : []; - const artifactSummary = - artifacts.length > 0 - ? artifacts - .slice(0, 4) - .map((artifact) => artifact.path) - .join(';') - : '暂无'; - const visualAssetCount = - nextManifest.assets.filter(isProjectVisualAsset).length; - const audioAssetCount = - nextManifest.assets.filter(isProjectAudioAsset).length; - const hasPublishReadme = - trace?.artifacts.some( - (artifact) => artifact.path === 'exports/README.md', - ) ?? false; - - let draftCommand = '/media-kit'; - let draftCommandLabel = '准备资料包'; - if (!trace) { - draftCommand = '/next'; - draftCommandLabel = '查看下一步'; - } else if (blockedTrace) { - draftCommand = '/review'; - draftCommandLabel = '查看评审'; - } else if (!tracePassed) { - draftCommand = '/trace'; - draftCommandLabel = '查看 trace'; - } else if (!previewRunning) { - draftCommand = '/run'; - draftCommandLabel = '启动试玩'; - } else if (hasPublishReadme) { - draftCommand = '/read exports/README.md'; - draftCommandLabel = '读发布说明'; - } - - return { - text: [ - '试玩更新说明:', - `- 项目:${nextManifest.name}`, - `- 一句话:${goal}`, - `- 当前版本:本地 Web 原型 · 小范围试玩 · 预览 ${previewSummary}`, - `- Run:${ - trace - ? `${trace.runId} · ${formatAgentRunStatus(trace)}` - : '暂无最近 run' - }`, - `- 本轮变化:${tracePassed ? '可试玩版本已通过 Evaluator' : trace ? '仍需返工或复查' : '待生成首个版本'}`, - `- 主要产物:${artifactSummary}`, - `- 素材变化:视觉素材 ${visualAssetCount} 个;音频素材 ${audioAssetCount} 个`, - '- 玩家可见说明:玩法目标;操作方式;胜负 / 重开反馈;当前已知限制', - '- 已知限制:本地原型;不承诺账号、云存档、排行榜、付费或长期兼容', - '- 搭配:/changes;/media-kit;/post;/store;/share', - '- 边界:只准备试玩更新说明;不读取文件;不启动或打开预览;不导出试玩包;不上传云端;不发布作品;不写项目', - `- 建议:${draftCommand}`, - ].join('\n'), - draftCommand, - draftCommandLabel, - }; -} - -export function summarizeProjectKnownIssues( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const preview = nextManifest.preview; - const previewRunning = preview?.status === 'running' && preview.url; - const previewSummary = previewRunning - ? `运行中 ${preview.url}` - : preview - ? previewStatusLabels[preview.status] - : '未启动'; - const tracePassed = isAgentRunTracePassed(trace); - const blockedTrace = - trace?.lifecycleStatus === 'killed' || - trace?.status === 'failed' || - trace?.status === 'needs-revision' || - trace?.stopReason === 'max-passes-exhausted'; - const traceTasks = trace?.taskGraph.tasks ?? []; - const taskSource = - traceTasks.length > 0 ? traceTasks : taskRowsFromManifest(nextManifest); - const failedTasks = taskSource.filter((task) => task.status === 'failed'); - const latestFailedTask = failedTasks[0] ?? null; - const knownIssueSummary = latestFailedTask - ? `失败任务 ${failedTasks.length} 个:${taskGroupLabels[latestFailedTask.group]} / ${latestFailedTask.role} ${latestFailedTask.title}(${latestFailedTask.id})` - : blockedTrace && trace - ? `最近 run 需返工:${trace.status} / ${trace.stopReason}` - : '暂无明确失败任务;仍按早期原型标注限制'; - - let draftCommand = '/share'; - let draftCommandLabel = '准备交付'; - if (!trace) { - draftCommand = '/next'; - draftCommandLabel = '查看下一步'; - } else if (blockedTrace) { - draftCommand = '/review'; - draftCommandLabel = '查看评审'; - } else if (!tracePassed) { - draftCommand = '/trace'; - draftCommandLabel = '查看 trace'; - } else if (!previewRunning) { - draftCommand = '/run'; - draftCommandLabel = '启动试玩'; - } - - return { - text: [ - '已知问题清单:', - `- 项目:${nextManifest.name}`, - `- 当前状态:${ - trace - ? `${trace.runId} · ${formatAgentRunStatus(trace)}` - : '暂无最近 run' - };预览 ${previewSummary}`, - `- 已知问题:${knownIssueSummary}`, - '- 试玩限制:本地 Web 原型;小范围 5-10 分钟试玩;不承诺账号、云存档、排行榜、付费或长期兼容', - '- 反馈入口:客户端问题由自动诊断提示;整体体验走 /feedback;版本变化走 /release-notes', - '- 发送前检查:可试玩状态先 /run;交付口径看 /share;对外资料看 /media-kit', - '- 边界:只准备已知问题清单;不读取文件;不启动或打开预览;不导出试玩包;不上传云端;不发布作品;不写项目', - `- 建议:${draftCommand}`, - ].join('\n'), - draftCommand, - draftCommandLabel, - }; -} - -export function summarizeProjectAcceptanceCriteria( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const tasks = taskRowsFromManifest(nextManifest); - const manifestTasksById = new Map(tasks.map((task) => [task.id, task])); - const traceTasksById = new Map( - trace?.taskGraph.tasks.map((task) => [task.id, task]) ?? [], - ); - const selectedTasks: Array<{ - task: GameCreationAppTaskState; - marker: string; - }> = []; - const seenTaskIds = new Set(); - const addTask = (taskId: string, marker: string) => { - if (seenTaskIds.has(taskId)) { - return; - } - const task = traceTasksById.get(taskId) ?? manifestTasksById.get(taskId); - if (!task) { - return; - } - selectedTasks.push({ task, marker }); - seenTaskIds.add(task.id); - }; - - trace?.taskGraph.activeTaskIds.forEach((taskId) => addTask(taskId, 'active')); - trace?.taskGraph.carriedTaskIds.forEach((taskId) => addTask(taskId, 'carry')); - trace?.taskGraph.readyTaskIds.forEach((taskId) => addTask(taskId, 'ready')); - tasks - .filter((task) => task.status === 'failed') - .forEach((task) => addTask(task.id, '失败')); - - if (selectedTasks.length === 0) { - selectGameCreationAppReadyTasks({ tasks }).forEach((task) => - addTask(task.id, 'ready'), - ); - } - - if (selectedTasks.length === 0) { - tasks - .filter((task) => task.status !== 'completed') - .slice(0, 3) - .forEach((task) => addTask(task.id, taskStatusLabels[task.status])); - } - - const visibleTasks = selectedTasks.slice(0, 6); - const taskLines = visibleTasks.map(({ task, marker }) => { - const criteria = - task.acceptanceCriteria.length > 0 - ? task.acceptanceCriteria.join(';') - : '暂无'; - const artifacts = - task.artifacts.length > 0 - ? task.artifacts.slice(0, 3).join(', ') - : '暂无'; - return `- ${marker}:${taskGroupLabels[task.group]} / ${task.role} ${task.title}(${task.id}) · ${taskStatusLabels[task.status]} · 验收:${criteria} · 产物:${artifacts}`; - }); - if (selectedTasks.length > visibleTasks.length) { - taskLines.push( - `- 还有 ${selectedTasks.length - visibleTasks.length} 个任务`, - ); - } - - return { - text: [ - '当前验收标准:', - trace ? `- Run:${trace.runId} · ${formatAgentRunStatus(trace)}` : null, - taskLines.length > 0 ? taskLines.join('\n') : '- 暂无待验收任务', - ] - .filter(Boolean) - .join('\n'), - draftCommand: '/tasks', - draftCommandLabel: '查看任务', - }; -} - -export function summarizeProjectTodoList( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const manifestTasks = taskRowsFromManifest(nextManifest); - const traceTasks = trace?.taskGraph.tasks ?? []; - const taskSource = traceTasks.length > 0 ? traceTasks : manifestTasks; - const manifestTasksById = new Map( - manifestTasks.map((task) => [task.id, task]), - ); - const taskSourceById = new Map(taskSource.map((task) => [task.id, task])); - const selectedTasks: Array<{ - task: GameCreationAppTaskState; - marker: string; - }> = []; - const seenTaskIds = new Set(); - const addTask = (taskId: string, marker: string) => { - if (seenTaskIds.has(taskId)) { - return; - } - const task = taskSourceById.get(taskId) ?? manifestTasksById.get(taskId); - if (!task) { - return; - } - selectedTasks.push({ task, marker }); - seenTaskIds.add(task.id); - }; - - taskSource - .filter((task) => task.status === 'failed') - .forEach((task) => addTask(task.id, '失败')); - trace?.taskGraph.activeTaskIds.forEach((taskId) => addTask(taskId, 'active')); - trace?.taskGraph.carriedTaskIds.forEach((taskId) => addTask(taskId, 'carry')); - trace?.taskGraph.readyTaskIds.forEach((taskId) => addTask(taskId, 'ready')); - - if (selectedTasks.length === 0) { - selectGameCreationAppReadyTasks({ tasks: manifestTasks }).forEach((task) => - addTask(task.id, 'ready'), - ); - } - - if (selectedTasks.length === 0) { - taskSource - .filter((task) => task.status !== 'completed') - .slice(0, 5) - .forEach((task) => addTask(task.id, taskStatusLabels[task.status])); - } - - const visibleTasks = selectedTasks.slice(0, 5); - const taskLines = visibleTasks.map(({ task, marker }, index) => { - const acceptance = - task.acceptanceCriteria.length > 0 ? task.acceptanceCriteria[0] : '暂无'; - const artifact = task.artifacts[0] ?? '暂无'; - return `- ${index + 1}. ${marker}:${taskGroupLabels[task.group]} / ${task.role} ${task.title}(${task.id}) · ${taskStatusLabels[task.status]} · 验收:${acceptance} · 产物:${artifact}`; - }); - if (selectedTasks.length > visibleTasks.length) { - taskLines.push( - `- 还有 ${selectedTasks.length - visibleTasks.length} 个候选任务`, - ); - } - - const blockedTrace = - trace?.lifecycleStatus === 'killed' || - trace?.status === 'failed' || - trace?.status === 'needs-revision' || - trace?.stopReason === 'max-passes-exhausted'; - let draftCommand = selectedTasks.length > 0 ? '/tasks' : '/next'; - let draftCommandLabel = selectedTasks.length > 0 ? '查看任务' : '查看下一步'; - if (blockedTrace) { - draftCommand = '/review'; - draftCommandLabel = '查看评审'; - } - - return { - text: [ - '下一轮小步:', - `- 项目:${nextManifest.name}`, - trace ? `- Run:${trace.runId} · ${formatAgentRunStatus(trace)}` : null, - trace?.nextStep ? `- 编排下一步:${trace.nextStep}` : null, - taskLines.length > 0 - ? `- 小步清单:\n${taskLines.join('\n')}` - : '- 小步清单:暂无待处理任务', - '- 边界:只整理下一步;不读取任务文件;不启动 run;不修改项目', - `- 建议:${draftCommand}`, - ] - .filter(Boolean) - .join('\n'), - draftCommand, - draftCommandLabel, - }; -} - -export function summarizeProjectNextRoundPlan( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const manifestTasks = taskRowsFromManifest(nextManifest); - const traceTasks = trace?.taskGraph.tasks ?? []; - const taskSource = traceTasks.length > 0 ? traceTasks : manifestTasks; - const manifestTasksById = new Map( - manifestTasks.map((task) => [task.id, task]), - ); - const taskSourceById = new Map(taskSource.map((task) => [task.id, task])); - const selectedTasks: Array<{ - task: GameCreationAppTaskState; - marker: string; - }> = []; - const seenTaskIds = new Set(); - const addTask = (taskId: string, marker: string) => { - if (seenTaskIds.has(taskId)) { - return; - } - const task = taskSourceById.get(taskId) ?? manifestTasksById.get(taskId); - if (!task) { - return; - } - selectedTasks.push({ task, marker }); - seenTaskIds.add(task.id); - }; - - taskSource - .filter((task) => task.status === 'failed') - .forEach((task) => addTask(task.id, '失败')); - trace?.taskGraph.activeTaskIds.forEach((taskId) => addTask(taskId, 'active')); - trace?.taskGraph.carriedTaskIds.forEach((taskId) => addTask(taskId, 'carry')); - trace?.taskGraph.readyTaskIds.forEach((taskId) => addTask(taskId, 'ready')); - - if (selectedTasks.length === 0) { - selectGameCreationAppReadyTasks({ tasks: manifestTasks }).forEach((task) => - addTask(task.id, 'ready'), - ); - } - - if (selectedTasks.length === 0) { - taskSource - .filter((task) => task.status !== 'completed') - .slice(0, 6) - .forEach((task) => addTask(task.id, taskStatusLabels[task.status])); - } - - const groups: GameCreationAppAgentGroup[] = [ - 'design', - 'art', - 'code', - 'balance', - 'audio', - 'publishing', - ]; - const groupLines = groups.flatMap((group) => { - const groupTasks = selectedTasks.filter(({ task }) => task.group === group); - return groupTasks.slice(0, 2).map(({ task, marker }) => { - const acceptance = task.acceptanceCriteria[0] ?? '暂无'; - return `- ${taskGroupLabels[group]}:${marker} · ${task.role} ${task.title}(${task.id}) · 验收:${acceptance}`; - }); - }); - const selectedGroups = groups.filter((group) => - selectedTasks.some(({ task }) => task.group === group), - ); - const idleGroups = groups.filter((group) => !selectedGroups.includes(group)); - const firstTask = selectedTasks[0]?.task ?? null; - const blockedTrace = - trace?.lifecycleStatus === 'killed' || - trace?.status === 'failed' || - trace?.status === 'needs-revision' || - trace?.stopReason === 'max-passes-exhausted'; - const draftCommand = blockedTrace - ? '/review' - : firstTask - ? `/agent-resume 下一轮计划:${taskGroupLabels[firstTask.group]} / ${firstTask.role} ${firstTask.title}` - : '/next'; - const draftCommandLabel = blockedTrace - ? '查看评审' - : firstTask - ? '继续执行计划' - : '查看下一步'; - - return { - text: [ - '下一轮分工计划:', - `- 项目:${nextManifest.name}`, - trace ? `- Run:${trace.runId} · ${formatAgentRunStatus(trace)}` : null, - trace?.nextStep ? `- 编排焦点:${trace.nextStep}` : null, - selectedGroups.length > 0 - ? `- 协作顺序:${selectedGroups - .map((group) => taskGroupLabels[group]) - .join(' -> ')}` - : '- 协作顺序:暂无', - groupLines.length > 0 - ? `- 分工:\n${groupLines.join('\n')}` - : '- 分工:暂无待接手任务', - idleGroups.length > 0 - ? `- 空档组:${idleGroups - .map((group) => taskGroupLabels[group]) - .join('、')}` - : '- 空档组:暂无', - '- 边界:只整理下一轮分工;不读取任务文件;不启动 run;不修改项目', - `- 建议:${draftCommand}`, - ] - .filter(Boolean) - .join('\n'), - draftCommand, - draftCommandLabel, - }; -} - -export function summarizeProjectSpecSheet( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const tasks = taskRowsFromManifest(nextManifest); - const taskArtifactPaths = new Set(tasks.flatMap((task) => task.artifacts)); - const tracePathEntries = - trace?.steps.flatMap((step) => [...step.inputPaths, ...step.outputPaths]) ?? - []; - const tracePaths = new Set([ - ...(trace?.artifacts.map((artifact) => artifact.path) ?? []), - ...tracePathEntries, - ]); - const specItems = [ - { label: 'Planner 规格', path: '.agent/spec.md', group: 'design' }, - { label: '玩法设计', path: 'game/game_design.md', group: 'design' }, - { label: '数值表', path: 'game/balance.json', group: 'balance' }, - { label: '美术清单', path: 'assets/manifest.art.json', group: 'art' }, - { label: '音频清单', path: 'assets/manifest.audio.json', group: 'audio' }, - { label: '发布说明', path: 'exports/README.md', group: 'publishing' }, - ] as const; - const itemLines = specItems.map((item) => { - const groupTasks = tasks.filter((task) => task.group === item.group); - const completedCount = groupTasks.filter( - (task) => task.status === 'completed', - ).length; - const status = tracePaths.has(item.path) - ? '已出现在最近 run' - : taskArtifactPaths.has(item.path) - ? '任务声明' - : '待补齐'; - const taskSummary = - groupTasks.length > 0 - ? ` · ${taskGroupLabels[item.group]}任务 ${completedCount}/${groupTasks.length}` - : ''; - return `- ${item.label}:${item.path} · ${status}${taskSummary}`; - }); - const firstReadablePath = - specItems.find((item) => tracePaths.has(item.path))?.path ?? - specItems.find((item) => taskArtifactPaths.has(item.path))?.path ?? - null; - const draftCommand = firstReadablePath - ? `/read ${firstReadablePath}` - : '/next'; - - return { - text: [ - '创作规格包:', - `- 项目:${nextManifest.name}`, - `- 目标:${nextManifest.goal || trace?.goal || '暂无'}`, - trace ? `- Run:${trace.runId} · ${formatAgentRunStatus(trace)}` : null, - `- 规格清单:\n${itemLines.join('\n')}`, - '- 关联:/goal;/rules;/balance;/art;/audio;/publish', - '- 边界:只整理规格产物状态;不读取规格文件;不启动预览;不写项目', - `- 建议:${draftCommand}`, - ] - .filter(Boolean) - .join('\n'), - draftCommand, - draftCommandLabel: firstReadablePath ? '读取规格' : '查看下一步', - }; -} - -export function summarizeProjectGroupProgress( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const tasks = taskRowsFromManifest(nextManifest); - const traceTasks = trace?.taskGraph.tasks ?? []; - const taskSource = traceTasks.length > 0 ? traceTasks : tasks; - const activeTaskIds = new Set(trace?.taskGraph.activeTaskIds ?? []); - const carriedTaskIds = new Set(trace?.taskGraph.carriedTaskIds ?? []); - const readyTaskIds = new Set( - trace - ? trace.taskGraph.readyTaskIds - : selectGameCreationAppReadyTasks({ tasks }).map((task) => task.id), - ); - const groups: GameCreationAppAgentGroup[] = [ - 'design', - 'art', - 'code', - 'balance', - 'audio', - 'publishing', - ]; - const lines = groups.map((group) => { - const groupTasks = taskSource.filter((task) => task.group === group); - const completedCount = groupTasks.filter( - (task) => task.status === 'completed', - ).length; - const failedCount = groupTasks.filter( - (task) => task.status === 'failed', - ).length; - const activeCount = groupTasks.filter((task) => - activeTaskIds.has(task.id), - ).length; - const carriedCount = groupTasks.filter((task) => - carriedTaskIds.has(task.id), - ).length; - const readyTasks = groupTasks.filter((task) => readyTaskIds.has(task.id)); - const nextTask = - readyTasks[0] ?? - groupTasks.find((task) => activeTaskIds.has(task.id)) ?? - null; - const nextSummary = nextTask - ? `${nextTask.role} ${nextTask.title}` - : '暂无'; - - return `- ${taskGroupLabels[group]}:完成 ${completedCount}/${groupTasks.length} · active ${activeCount} · carry ${carriedCount} · ready ${readyTasks.length} · 失败 ${failedCount} · 下一步 ${nextSummary}`; - }); - const latestPassPlan = trace?.passPlans.slice(-1)[0] ?? null; - - return { - text: [ - '专业组进度:', - trace ? `- Run:${trace.runId} · ${formatAgentRunStatus(trace)}` : null, - latestPassPlan - ? `- 最近编排:第 ${latestPassPlan.pass} 轮 · ${latestPassPlan.mode} · ${latestPassPlan.summary}` - : null, - lines.join('\n'), - ] - .filter(Boolean) - .join('\n'), - draftCommand: '/tasks', - draftCommandLabel: '查看任务', - }; -} - -export function summarizeProjectBalanceState( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const manifestTasks = taskRowsFromManifest(nextManifest); - const traceTasks = trace?.taskGraph.tasks ?? []; - const tasks = manifestTasks.map( - (task) => traceTasks.find((traceTask) => traceTask.id === task.id) ?? task, - ); - const balanceTasks = tasks.filter( - (task) => task.group === 'balance' || task.id.startsWith('balance-'), - ); - const readyTaskIds = new Set( - trace - ? trace.taskGraph.readyTaskIds - : selectGameCreationAppReadyTasks({ tasks }).map((task) => task.id), - ); - const activeTaskIds = new Set(trace?.taskGraph.activeTaskIds ?? []); - const carriedTaskIds = new Set(trace?.taskGraph.carriedTaskIds ?? []); - const balanceArtifact = - trace?.artifacts.find( - (artifact) => artifact.path === 'game/balance.json', - ) ?? null; - const latestBalanceStep = - trace?.steps - .filter( - (step) => - step.group === 'balance' || step.taskId?.startsWith('balance-'), - ) - .slice(-1)[0] ?? null; - const taskLines = balanceTasks.map((task) => { - const markers = [taskStatusLabels[task.status]]; - if (readyTaskIds.has(task.id)) { - markers.push('ready'); - } - if (activeTaskIds.has(task.id)) { - markers.push('active'); - } - if (carriedTaskIds.has(task.id)) { - markers.push('carry'); - } - return `- ${task.id}:${task.role} ${task.title} · ${markers.join(' / ')}`; - }); - const criteriaLines = balanceTasks.flatMap((task) => - task.acceptanceCriteria.map((criterion) => `- ${task.id}:${criterion}`), - ); - - const draftCommand = balanceArtifact - ? '/read game/balance.json' - : trace - ? '/agent-resume 数值调整:前 30 秒更易上手;得分反馈更明显;失败后重开节奏更快' - : '/next'; - - return { - text: [ - '数值状态:', - `- 项目:${nextManifest.name}`, - taskLines.length > 0 - ? `- 数值任务:\n${taskLines.join('\n')}` - : '- 数值任务:暂无', - criteriaLines.length > 0 - ? `- 数值口径:\n${criteriaLines.join('\n')}` - : '- 数值口径:暂无', - `- 数值表:${balanceArtifact ? 'game/balance.json · 已生成' : 'game/balance.json · 待生成'}`, - `- 最近数值步骤:${ - latestBalanceStep - ? `${latestBalanceStep.agent} #${latestBalanceStep.pass} · ${latestBalanceStep.status} · ${latestBalanceStep.summary}` - : '暂无' - }`, - '- 试玩关联:/playtest;/feedback', - `- 建议:${draftCommand}`, - ].join('\n'), - draftCommand, - draftCommandLabel: balanceArtifact - ? '读数值表' - : trace - ? '填写数值反馈' - : '查看下一步', - }; -} diff --git a/apps/ai-game-creator-shell/src/features/project-summary/projectGuidanceSummaries.ts b/apps/ai-game-creator-shell/src/features/project-summary/projectGuidanceSummaries.ts deleted file mode 100644 index d5f8b35ee..000000000 --- a/apps/ai-game-creator-shell/src/features/project-summary/projectGuidanceSummaries.ts +++ /dev/null @@ -1,271 +0,0 @@ -import { - type GameCreationAgentRunTrace, - type GameCreationAppAssetSourceKind, - type GameCreationAppManifest, - selectGameCreationAppReadyTasks, -} from '../../../../../packages/shared/src/contracts/gameCreationApp'; -import { type AgentStatusCard } from '../../app/types'; -import { taskRowsFromManifest } from '../agent-runtime'; -import { isAgentRunTracePassed } from './agentTrace'; -import { - isProjectAudioAsset, - isProjectVisualAsset, -} from './projectAssetSummaries'; -import { assetSourceKindLabels } from './projectSummaryConstants'; - -export function summarizeNextProjectActions( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const suggestions: Array<{ label: string; command?: string }> = []; - const addSuggestion = (label: string, command?: string) => { - if (command && suggestions.some((item) => item.command === command)) { - return; - } - suggestions.push({ label, command }); - }; - - const preview = nextManifest.preview; - if (!trace) { - addSuggestion('直接输入一句游戏需求,确认后生成首版原型'); - } else if ( - trace.lifecycleStatus === 'killed' || - trace.status === 'failed' || - trace.stopReason === 'max-passes-exhausted' - ) { - addSuggestion('补充说明并继续最近 run', '/agent-resume '); - } else if ( - trace.status === 'passed' || - trace.status === 'artifacts-written' || - trace.stopReason === 'evaluator-passed' - ) { - addSuggestion( - preview?.status === 'running' && preview.url - ? '打开当前本地预览' - : '运行自检并启动本地预览', - preview?.status === 'running' && preview.url ? '/open-preview' : '/run', - ); - addSuggestion('导出本地试玩包', '/export'); - addSuggestion('查看本地试玩包', '/exports'); - } else { - addSuggestion('查看最近 loop 进展', '/trace'); - } - addSuggestion('查看创作目标', '/goal'); - addSuggestion('查看普通用户操作导引', '/guide'); - addSuggestion('查看项目进度', '/progress'); - addSuggestion('查看创作规格包', '/spec'); - addSuggestion('查看本轮 MVP 范围', '/mvp'); - addSuggestion('查看试玩定位与卖点', '/pitch'); - addSuggestion('准备 30 秒试玩讲解稿', '/demo'); - addSuggestion('查看玩法操作与规则', '/rules'); - addSuggestion('查看新手引导检查', '/tutorial'); - addSuggestion('查看移动试玩检查', '/mobile'); - addSuggestion('准备兼容性说明', '/compatibility'); - addSuggestion('查看可读性与无障碍检查', '/accessibility'); - addSuggestion('查看本地化与文案检查', '/localization'); - addSuggestion('查看性能与加载检查', '/performance'); - addSuggestion('查看试玩前打磨清单', '/polish'); - addSuggestion('查看数值与难度口径', '/balance'); - addSuggestion('查看当前阻塞项', '/blockers'); - addSuggestion('查看试玩就绪度', '/ready'); - addSuggestion('查看验证证据台账', '/evidence'); - addSuggestion('查看 Agent 智能服务状态', '/llm-routes'); - addSuggestion('查看任务依赖链', '/deps'); - addSuggestion('准备下一轮改版说明', '/revise'); - addSuggestion('查看隐私与导出边界', '/privacy'); - addSuggestion('查看首批试玩对象', '/audience'); - addSuggestion('准备试玩邀请文案', '/invite'); - addSuggestion('准备试玩问卷问题', '/survey'); - addSuggestion('准备封面与缩略图检查', '/cover'); - addSuggestion('准备宣传截图清单', '/screenshots'); - addSuggestion('准备试玩短视频脚本', '/trailer'); - addSuggestion('准备试玩常见问答', '/faq'); - addSuggestion('准备社区发布文案', '/post'); - addSuggestion('准备上架资料清单', '/store'); - addSuggestion('准备媒体资料包清单', '/media-kit'); - addSuggestion('准备试玩更新说明', '/release-notes'); - addSuggestion('准备已知问题清单', '/known-issues'); - - const readyTasks = selectGameCreationAppReadyTasks({ - tasks: taskRowsFromManifest(nextManifest), - }); - if (readyTasks.length > 0) { - addSuggestion(`查看 ${readyTasks.length} 个 ready 任务`, '/tasks'); - addSuggestion('查看当前任务验收标准', '/criteria'); - addSuggestion('查看专业组进度', '/groups'); - } else { - addSuggestion('查看任务拆分和等待项', '/tasks'); - addSuggestion('查看当前任务验收标准', '/criteria'); - addSuggestion('查看专业组进度', '/groups'); - } - addSuggestion('查看质量检查清单', '/qa'); - addSuggestion('查看最近生成变更', '/changes'); - addSuggestion('查看下一轮分工计划', '/plan'); - addSuggestion('查看下一轮小步清单', '/todo'); - - if (nextManifest.assets.length > 0) { - addSuggestion(`查看 ${nextManifest.assets.length} 个本地资产`, '/assets'); - addSuggestion('查看素材署名与来源', '/credits'); - addSuggestion( - nextManifest.assets.some(isProjectVisualAsset) - ? '查看美术素材' - : '生成或同步美术素材', - '/art', - ); - addSuggestion( - nextManifest.assets.some(isProjectAudioAsset) - ? '查看音频素材' - : '登记或导入音频素材', - '/audio', - ); - } else { - addSuggestion( - '登记本地素材或同步画板资源', - '/asset-register assets/hero.png image image/png', - ); - addSuggestion('同步已有画板项目资源', '/sync-canvas-project '); - addSuggestion('查看素材署名与来源', '/credits'); - addSuggestion('生成或同步美术素材', '/art'); - addSuggestion('登记或导入音频素材', '/audio'); - } - - if (trace) { - addSuggestion('查看发布准备清单', '/publish'); - addSuggestion('准备作品页文案清单', '/listing'); - addSuggestion('查看试玩状态', '/playtest'); - addSuggestion('准备手动测试计划', '/test-plan'); - addSuggestion('准备试玩反馈', '/feedback'); - addSuggestion('准备复玩观察清单', '/retention'); - addSuggestion('准备试玩交付清单', '/share'); - addSuggestion('查看最近 run 预算', '/budget'); - addSuggestion('查看评审和返工焦点', '/review'); - addSuggestion('查看生成上下文来源', '/context'); - addSuggestion('查看项目活动时间线', '/timeline'); - addSuggestion('查看最近 trace 摘要', '/trace'); - addSuggestion('列出最近 Run 产物', '/run-artifacts'); - addSuggestion('列出 Agent 轮次产物', '/passes'); - addSuggestion('列出 Agent 运行辅助文件', '/run-files'); - } - addSuggestion('打开产物命令列表', '/artifacts'); - addSuggestion('列出内部真相源读取命令', '/internals'); - addSuggestion('打开日志命令列表', '/logs'); - - const firstCommand = suggestions.find((item) => item.command); - return { - text: `下一步建议:\n${suggestions - .map( - (item) => `- ${item.label}${item.command ? `:${item.command}` : ''}`, - ) - .join('\n')}`, - draftCommand: firstCommand?.command, - draftCommandLabel: firstCommand?.label, - }; -} - -export function summarizeProjectUserGuide( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const preview = nextManifest.preview; - const previewRunning = preview?.status === 'running' && preview.url; - const tracePassed = isAgentRunTracePassed(trace); - const blockedTrace = - trace?.lifecycleStatus === 'killed' || - trace?.status === 'failed' || - trace?.status === 'needs-revision' || - trace?.stopReason === 'max-passes-exhausted'; - const exported = (nextManifest.commandRuns ?? []).some( - (commandRun) => - commandRun.commandId === 'project.export_package' && - commandRun.status === 'completed', - ); - - let stage = '未开始'; - let nextAction = '先确认一句游戏目标,再生成首版原型。'; - let recommendedCommands = ['/brief', '/mvp', '/next']; - - if (blockedTrace) { - stage = '需修复'; - nextAction = '先看评审和阻塞,再把修复说明交回 agent。'; - recommendedCommands = ['/review', '/todo', '/plan']; - } else if (exported) { - stage = '已导出'; - nextAction = '先检查试玩包和交付材料,再发给测试者。'; - recommendedCommands = ['/exports', '/share', '/listing']; - } else if (previewRunning) { - stage = '可预览'; - nextAction = '先打开本地预览试玩一轮,再记录反馈。'; - recommendedCommands = ['/open-preview', '/test-plan', '/feedback']; - } else if (tracePassed) { - stage = '可导出'; - nextAction = '先运行自检并启动本地预览,通过后再导出试玩包。'; - recommendedCommands = ['/run', '/test-plan', '/share']; - } else if (trace) { - stage = '生成中/待验收'; - nextAction = '先看最近 loop 和下一轮小步,再决定是否继续。'; - recommendedCommands = ['/trace', '/todo', '/plan']; - } - - const draftCommand = recommendedCommands[0]; - - return { - text: [ - '使用导引:', - `- 项目:${nextManifest.name}`, - `- 当前阶段:${stage}`, - `- 现在先做:${nextAction}`, - `- 推荐命令:${recommendedCommands.join(' / ')}`, - '- 边界:只给操作导引;不读取文件;不启动 run;不启动预览;不写项目', - ].join('\n'), - draftCommand, - draftCommandLabel: '执行导引建议', - }; -} - -export function summarizeMainProjectHeader( - nextManifest: GameCreationAppManifest, - agents: AgentStatusCard[], -) { - const tasks = taskRowsFromManifest(nextManifest); - const completedCount = tasks.filter( - (task) => task.status === 'completed', - ).length; - const readyTaskIds = new Set( - selectGameCreationAppReadyTasks({ tasks }).map((task) => task.id), - ); - for (const agent of agents) { - if (agent.taskGraphState === 'ready') { - readyTaskIds.add(agent.taskId); - } - } - const sourceCounts = nextManifest.assets.reduce( - (counts, asset) => { - counts[asset.source.kind] += 1; - return counts; - }, - { - uploaded: 0, - generated: 0, - canvas: 0, - } satisfies Record, - ); - const sourceSummary = ( - Object.keys(sourceCounts) as GameCreationAppAssetSourceKind[] - ) - .filter((source) => sourceCounts[source] > 0) - .map((source) => `${assetSourceKindLabels[source]} ${sourceCounts[source]}`) - .join(' / '); - const commandRuns = nextManifest.commandRuns ?? []; - const latestCommandRun = commandRuns[commandRuns.length - 1]; - return [ - `任务:已完成 ${completedCount}/${tasks.length} · ready ${readyTaskIds.size}`, - `资产:${nextManifest.assets.length} 个${ - sourceSummary ? ` · ${sourceSummary}` : '' - }`, - latestCommandRun - ? `最近命令:${latestCommandRun.commandId} ${ - latestCommandRun.status === 'completed' ? '完成' : '失败' - }` - : '最近命令:暂无', - ].join(' · '); -} diff --git a/apps/ai-game-creator-shell/src/features/project-summary/projectOverviewSummaries.ts b/apps/ai-game-creator-shell/src/features/project-summary/projectOverviewSummaries.ts deleted file mode 100644 index 5860bfd3a..000000000 --- a/apps/ai-game-creator-shell/src/features/project-summary/projectOverviewSummaries.ts +++ /dev/null @@ -1,562 +0,0 @@ -import { - type GameCreationAgentRunTrace, - type GameCreationAppManifest, - type GameCreationAppTaskStatus, - selectGameCreationAppReadyTasks, -} from '../../../../../packages/shared/src/contracts/gameCreationApp'; -import { taskRowsFromManifest } from '../agent-runtime'; -import { - formatAgentRunStatus, - isAgentRunTracePassed, - isPlaytestTraceStep, -} from './agentTrace'; -import { - isProjectAudioAsset, - isProjectVisualAsset, -} from './projectAssetSummaries'; -import { - previewStatusLabels, - taskGroupLabels, - taskStatusLabels, -} from './projectSummaryConstants'; - -export function summarizeProjectStatus( - nextManifest: GameCreationAppManifest, - nextProjectPath: string, -) { - const tasks = taskRowsFromManifest(nextManifest); - const counts = tasks.reduce>( - (current, task) => { - current[task.status] += 1; - return current; - }, - { - pending: 0, - running: 0, - 'waiting-for-confirmation': 0, - completed: 0, - failed: 0, - }, - ); - const taskSummary = ( - [ - 'completed', - 'waiting-for-confirmation', - 'running', - 'pending', - 'failed', - ] as const - ) - .filter((status) => counts[status] > 0) - .map((status) => `${taskStatusLabels[status]} ${counts[status]}`) - .join(','); - const preview = nextManifest.preview; - const commandRuns = nextManifest.commandRuns ?? []; - const latestCommandRun = commandRuns[commandRuns.length - 1]; - const previewSummary = - preview?.status === 'running' && preview.url - ? `运行中 ${preview.url}` - : preview - ? previewStatusLabels[preview.status] - : '未启动'; - - return [ - `项目:${nextManifest.name}`, - `目录:${nextProjectPath}`, - `任务:${taskSummary || '无任务'}`, - `资产:${nextManifest.assets.length} 个`, - `预览:${previewSummary}`, - latestCommandRun - ? `最近命令:${latestCommandRun.commandId} · ${ - latestCommandRun.status === 'completed' ? '完成' : '失败' - }` - : null, - ] - .filter(Boolean) - .join('\n'); -} - -export function summarizeProjectBrief( - nextManifest: GameCreationAppManifest, - nextProjectPath: string, - trace: GameCreationAgentRunTrace | null, -) { - const tasks = taskRowsFromManifest(nextManifest); - const completedCount = tasks.filter( - (task) => task.status === 'completed', - ).length; - const failedCount = tasks.filter((task) => task.status === 'failed').length; - const readyCount = selectGameCreationAppReadyTasks({ tasks }).length; - const sourceCounts = nextManifest.assets.reduce( - (counts, asset) => { - counts[asset.source.kind] += 1; - return counts; - }, - { uploaded: 0, generated: 0, canvas: 0 }, - ); - const assetSummary = - nextManifest.assets.length > 0 - ? `${nextManifest.assets.length} 个 · 上传 ${sourceCounts.uploaded} / 生成 ${sourceCounts.generated} / 画板 ${sourceCounts.canvas}` - : '暂无'; - const preview = nextManifest.preview; - const previewSummary = - preview?.status === 'running' && preview.url - ? `运行中 ${preview.url}` - : preview - ? previewStatusLabels[preview.status] - : '未启动'; - const commandRuns = nextManifest.commandRuns ?? []; - const latestCommandRun = commandRuns[commandRuns.length - 1]; - const runSummary = trace - ? `${trace.runId} · ${trace.status}${ - trace.lifecycleStatus ? ` / ${trace.lifecycleStatus}` : '' - } · ${trace.passes}/${trace.maxPasses} 轮 · ${trace.stopReason}` - : '暂无最近 run'; - - return `项目简报:\n- 项目:${nextManifest.name}\n- 目录:${nextProjectPath}\n- 任务:完成 ${completedCount}/${tasks.length} · ready ${readyCount} · 失败 ${failedCount}\n- 资产:${assetSummary}\n- 最近 Run:${runSummary}\n- 预览:${previewSummary}\n- 最近命令:${ - latestCommandRun - ? `${latestCommandRun.commandId} · ${ - latestCommandRun.status === 'completed' ? '完成' : '失败' - }` - : '暂无' - }`; -} - -export function summarizeProjectGoal( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const manifestGoal = nextManifest.goal?.trim() || ''; - const runGoal = trace?.goal?.trim() || ''; - const taskGraphGoal = trace?.taskGraph.goal?.trim() || ''; - const draftCommand = trace ? '/agent-resume 细化目标:' : '/next'; - - return { - text: [ - '创作目标:', - `- 项目:${nextManifest.name}`, - `- Manifest:${manifestGoal || '暂无'}`, - trace ? `- Run:${trace.runId} · ${formatAgentRunStatus(trace)}` : null, - `- Run 目标:${runGoal || '暂无'}`, - `- 任务图目标:${taskGraphGoal || '暂无'}`, - '- 上下文:/context', - `- 建议:${draftCommand}`, - ] - .filter(Boolean) - .join('\n'), - draftCommand, - draftCommandLabel: trace ? '补充目标' : '查看下一步', - }; -} - -export function summarizeProjectProgress( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const manifestTasks = taskRowsFromManifest(nextManifest); - const traceTasks = trace?.taskGraph.tasks ?? []; - const traceTasksById = new Map(traceTasks.map((task) => [task.id, task])); - const tasks = manifestTasks.map( - (task) => traceTasksById.get(task.id) ?? task, - ); - const taskIds = new Set(tasks.map((task) => task.id)); - for (const task of traceTasks) { - if (!taskIds.has(task.id)) { - tasks.push(task); - taskIds.add(task.id); - } - } - const completedCount = tasks.filter( - (task) => task.status === 'completed', - ).length; - const failedCount = tasks.filter((task) => task.status === 'failed').length; - const readyCount = - trace?.taskGraph.readyTaskIds.filter((taskId) => taskIds.has(taskId)) - .length ?? selectGameCreationAppReadyTasks({ tasks }).length; - const progressPercent = - tasks.length > 0 ? Math.round((completedCount / tasks.length) * 100) : 0; - const preview = nextManifest.preview; - const previewRunning = preview?.status === 'running' && preview.url; - const previewSummary = previewRunning - ? `运行中 ${preview.url}` - : preview - ? previewStatusLabels[preview.status] - : '未启动'; - const commandRuns = nextManifest.commandRuns ?? []; - const staticSmokePassed = commandRuns.some( - (commandRun) => - commandRun.commandId === 'game.static_smoke' && - commandRun.status === 'completed', - ); - const exported = commandRuns.some( - (commandRun) => - commandRun.commandId === 'project.export_package' && - commandRun.status === 'completed', - ); - const visualAssetCount = - nextManifest.assets.filter(isProjectVisualAsset).length; - const audioAssetCount = - nextManifest.assets.filter(isProjectAudioAsset).length; - const tracePassed = isAgentRunTracePassed(trace); - const blockedTrace = - trace?.lifecycleStatus === 'killed' || - trace?.status === 'failed' || - trace?.status === 'needs-revision' || - trace?.stopReason === 'max-passes-exhausted'; - - let phase = '准备生成'; - let draftCommand = '/guide'; - let draftCommandLabel = '查看导引'; - - if (blockedTrace) { - phase = '需修复'; - draftCommand = '/review'; - draftCommandLabel = '查看评审'; - } else if (exported) { - phase = '已导出'; - draftCommand = '/share'; - draftCommandLabel = '准备交付'; - } else if (previewRunning) { - phase = '试玩中'; - draftCommand = '/test-plan'; - draftCommandLabel = '准备测试'; - } else if (tracePassed) { - phase = '已生成'; - draftCommand = '/run'; - draftCommandLabel = '启动预览'; - } else if (trace) { - phase = '生成中/待验收'; - draftCommand = readyCount > 0 ? '/todo' : '/trace'; - draftCommandLabel = readyCount > 0 ? '查看小步' : '查看 trace'; - } - - return { - text: [ - '项目进度:', - `- 项目:${nextManifest.name}`, - `- 当前阶段:${phase}`, - `- 任务完成度:${completedCount}/${tasks.length} · ${progressPercent}% · ready ${readyCount} · 失败 ${failedCount}`, - trace - ? `- 最近 Run:${trace.runId} · ${formatAgentRunStatus(trace)}` - : '- 最近 Run:暂无', - `- 预览:${previewSummary}`, - `- 素材:共 ${nextManifest.assets.length} 个 · 美术 ${visualAssetCount} · 音频 ${audioAssetCount}`, - `- 交付:自检 ${staticSmokePassed ? '已通过' : '未通过'} · 试玩包 ${exported ? '已导出' : '未导出'}`, - '- 边界:只整理项目进度;不读取文件;不启动 run;不启动预览;不导出试玩包;不写项目', - `- 建议:${draftCommand}`, - ].join('\n'), - draftCommand, - draftCommandLabel, - }; -} - -export function summarizeProjectMvpScope( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const tasks = taskRowsFromManifest(nextManifest); - const completedCount = tasks.filter( - (task) => task.status === 'completed', - ).length; - const failedCount = tasks.filter((task) => task.status === 'failed').length; - const readyCount = selectGameCreationAppReadyTasks({ tasks }).length; - const preview = nextManifest.preview; - const previewRunning = preview?.status === 'running' && preview.url; - const previewSummary = previewRunning - ? `运行中 ${preview.url}` - : preview - ? previewStatusLabels[preview.status] - : '未启动'; - const tracePassed = isAgentRunTracePassed(trace); - const blockedTrace = - trace?.lifecycleStatus === 'killed' || - trace?.status === 'failed' || - trace?.status === 'needs-revision' || - trace?.stopReason === 'max-passes-exhausted'; - const exported = (nextManifest.commandRuns ?? []).some( - (commandRun) => - commandRun.commandId === 'project.export_package' && - commandRun.status === 'completed', - ); - const goal = - nextManifest.goal?.trim() || - trace?.goal?.trim() || - trace?.taskGraph.goal?.trim() || - '暂无'; - - let draftCommand = '/next'; - let draftCommandLabel = '查看下一步'; - if (blockedTrace) { - draftCommand = '/review'; - draftCommandLabel = '查看评审'; - } else if (trace && !tracePassed) { - draftCommand = readyCount > 0 ? '/criteria' : '/trace'; - draftCommandLabel = readyCount > 0 ? '查看验收' : '查看 trace'; - } else if (tracePassed && !previewRunning) { - draftCommand = '/run'; - draftCommandLabel = '启动预览'; - } else if (tracePassed && !exported) { - draftCommand = '/export'; - draftCommandLabel = '导出试玩包'; - } else if (exported) { - draftCommand = '/exports'; - draftCommandLabel = '查看试玩包'; - } - - return { - text: [ - 'MVP 范围:', - `- 项目:${nextManifest.name}`, - `- 目标:${goal}`, - `- MVP 内:可运行 Web 原型;基础输入 / 胜负 / 重开;本地预览;本地试玩包`, - trace - ? `- 当前状态:最近 run ${trace.runId} · ${formatAgentRunStatus(trace)}` - : '- 当前状态:暂无最近 run', - `- 任务:完成 ${completedCount}/${tasks.length} · ready ${readyCount} · 失败 ${failedCount}`, - `- 预览:${previewSummary}`, - `- 资产:${nextManifest.assets.length} 个`, - `- 试玩包:${exported ? '已导出' : tracePassed ? '待导出' : '待原型通过'}`, - '- 先不做:云同步;Unity/Godot;插件市场;任意 shell;深度资产精修', - `- 建议:${draftCommand}`, - ].join('\n'), - draftCommand, - draftCommandLabel, - }; -} - -export function summarizeProjectPitch( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const preview = nextManifest.preview; - const previewRunning = preview?.status === 'running' && preview.url; - const previewSummary = previewRunning - ? `运行中 ${preview.url}` - : preview - ? previewStatusLabels[preview.status] - : '未启动'; - const tracePassed = isAgentRunTracePassed(trace); - const blockedTrace = - trace?.lifecycleStatus === 'killed' || - trace?.status === 'failed' || - trace?.status === 'needs-revision' || - trace?.stopReason === 'max-passes-exhausted'; - const goal = - nextManifest.goal?.trim() || - trace?.goal?.trim() || - trace?.taskGraph.goal?.trim() || - '暂无'; - - let draftCommand = '/mvp'; - let draftCommandLabel = '查看 MVP 范围'; - if (blockedTrace) { - draftCommand = '/review'; - draftCommandLabel = '查看评审'; - } else if (!trace) { - draftCommand = '/mvp'; - draftCommandLabel = '查看 MVP 范围'; - } else if (!tracePassed) { - draftCommand = '/trace'; - draftCommandLabel = '查看 trace'; - } else if (previewRunning) { - draftCommand = '/open-preview'; - draftCommandLabel = '打开预览'; - } else { - draftCommand = '/run'; - draftCommandLabel = '启动预览'; - } - - return { - text: [ - '试玩定位:', - `- 项目:${nextManifest.name}`, - `- 一句话:${goal}`, - '- 核心乐趣:快速验证目标、操作反馈、胜负结果和重开节奏', - `- 当前可演示:${ - tracePassed - ? `最近 run 已通过${trace?.runId ? ` ${trace.runId}` : ''}` - : trace - ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}` - : '暂无最近 run' - };预览 ${previewSummary}`, - '- 讲给测试者:先说明目标,再说明操作,然后看 30 秒内是否能理解胜负和重开', - '- 不承诺:云发布;深度美术精修;账号体系;排行榜;长期运营包装', - '- 参考:/mvp;/rules;/playtest;/listing', - `- 建议:${draftCommand}`, - ].join('\n'), - draftCommand, - draftCommandLabel, - }; -} - -export function summarizeProjectDemoScript( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const preview = nextManifest.preview; - const previewRunning = preview?.status === 'running' && preview.url; - const previewSummary = previewRunning - ? `运行中 ${preview.url}` - : preview - ? previewStatusLabels[preview.status] - : '未启动'; - const tracePassed = isAgentRunTracePassed(trace); - const blockedTrace = - trace?.lifecycleStatus === 'killed' || - trace?.status === 'failed' || - trace?.status === 'needs-revision' || - trace?.stopReason === 'max-passes-exhausted'; - const goal = - nextManifest.goal?.trim() || - trace?.goal?.trim() || - trace?.taskGraph.goal?.trim() || - '暂无'; - const latestPlaytestStep = - trace?.steps.filter(isPlaytestTraceStep).slice(-1)[0] ?? null; - - let draftCommand = '/test-plan'; - let draftCommandLabel = '准备测试计划'; - if (blockedTrace) { - draftCommand = '/review'; - draftCommandLabel = '查看评审'; - } else if (trace && !tracePassed) { - draftCommand = '/trace'; - draftCommandLabel = '查看 trace'; - } else if (previewRunning) { - draftCommand = '/open-preview'; - draftCommandLabel = '打开预览'; - } else if (tracePassed) { - draftCommand = '/run'; - draftCommandLabel = '启动预览'; - } - - return { - text: [ - '试玩讲解稿:', - `- 项目:${nextManifest.name}`, - `- 30 秒开场:这是《${nextManifest.name}》,目标是${goal}`, - '- 讲解顺序:目标 -> 操作 -> 反馈 -> 胜负 -> 重开', - '- 口播稿:先看目标提示,尝试移动/点击完成核心动作;看到得分、受击或状态反馈后,继续到胜利或失败;结束后确认能否一键重开', - `- 当前演示状态:${ - tracePassed - ? `最近 run 已通过${trace?.runId ? ` ${trace.runId}` : ''}` - : trace - ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}` - : '暂无最近 run' - };预览 ${previewSummary}`, - `- 最近试玩证据:${ - latestPlaytestStep - ? `${latestPlaytestStep.agent} #${latestPlaytestStep.pass} · ${latestPlaytestStep.status} · ${latestPlaytestStep.summary}` - : '暂无' - }`, - '- 收反馈:操作是否明白;节奏是否太快;胜负是否清楚;视觉 / 音效是否帮助理解', - '- 边界:只准备试玩讲解;不启动预览;不导出试玩包;不发布作品', - '- 参考:/rules;/test-plan;/feedback;/share', - `- 建议:${draftCommand}`, - ].join('\n'), - draftCommand, - draftCommandLabel, - }; -} - -export function summarizeProjectControlGuide( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const manifestTasks = taskRowsFromManifest(nextManifest); - const traceTasks = trace?.taskGraph.tasks ?? []; - const tasks = manifestTasks.map( - (task) => traceTasks.find((traceTask) => traceTask.id === task.id) ?? task, - ); - const tasksById = new Map(tasks.map((task) => [task.id, task])); - const readyTaskIds = new Set( - trace - ? trace.taskGraph.readyTaskIds - : selectGameCreationAppReadyTasks({ tasks }).map((task) => task.id), - ); - const activeTaskIds = new Set(trace?.taskGraph.activeTaskIds ?? []); - const carriedTaskIds = new Set(trace?.taskGraph.carriedTaskIds ?? []); - const formatTaskLine = (taskId: string) => { - const task = tasksById.get(taskId); - if (!task) { - return null; - } - const markers = [taskStatusLabels[task.status]]; - if (readyTaskIds.has(task.id)) { - markers.push('ready'); - } - if (activeTaskIds.has(task.id)) { - markers.push('active'); - } - if (carriedTaskIds.has(task.id)) { - markers.push('carry'); - } - return `- ${task.id}:${taskGroupLabels[task.group]} / ${task.role} ${task.title} · ${markers.join(' / ')}`; - }; - const taskLines = [ - 'design-foundation', - 'code-prototype', - 'preview-readiness', - 'preview-playtest', - ] - .map(formatTaskLine) - .filter(Boolean); - const hasDesignArtifact = - trace?.artifacts.some( - (artifact) => artifact.path === 'game/game_design.md', - ) ?? false; - const hasGameArtifact = - trace?.artifacts.some( - (artifact) => - artifact.path === 'game/index.html' || artifact.path === 'game/', - ) ?? false; - const latestControlStep = - trace?.steps - .filter( - (step) => - step.taskId === 'code-prototype' || - step.taskId === 'preview-readiness' || - step.taskId === 'preview-playtest' || - step.group === 'code' || - step.phase === 'generate' || - step.phase === 'playtest' || - step.toolCalls.some( - (toolCall) => - toolCall.toolId === 'game.static_smoke' || - toolCall.toolId.startsWith('preview.'), - ), - ) - .slice(-1)[0] ?? null; - const draftCommand = hasDesignArtifact - ? '/read game/game_design.md' - : trace - ? '/agent-resume 操作说明:在首屏明确移动/点击操作、胜负目标、失败后重开方式' - : '/next'; - - return { - text: [ - '玩法操作:', - `- 项目:${nextManifest.name}`, - `- 目标:${nextManifest.goal ?? trace?.goal ?? trace?.taskGraph.goal ?? '暂无'}`, - '- 核心口径:目标;操作;胜负;重开;本地预览', - `- 规则来源:game/game_design.md · ${hasDesignArtifact ? '已生成' : '未见 trace 产物'}`, - `- 原型入口:game/index.html · ${hasGameArtifact ? '已生成' : '未见 trace 产物'}`, - taskLines.length > 0 - ? `- 任务状态:\n${taskLines.join('\n')}` - : '- 任务状态:暂无', - `- 最近程序/试玩步骤:${ - latestControlStep - ? `${latestControlStep.agent} #${latestControlStep.pass} · ${latestControlStep.status} · ${latestControlStep.summary}` - : '暂无' - }`, - '- 相关命令:/mvp;/playtest;/feedback', - `- 建议:${draftCommand}`, - ].join('\n'), - draftCommand, - draftCommandLabel: hasDesignArtifact - ? '读取玩法设计' - : trace - ? '补充操作说明' - : '查看下一步', - }; -} diff --git a/apps/ai-game-creator-shell/src/features/project-summary/projectPath.ts b/apps/ai-game-creator-shell/src/features/project-summary/projectPath.ts index d8a54f66a..5808f0235 100644 --- a/apps/ai-game-creator-shell/src/features/project-summary/projectPath.ts +++ b/apps/ai-game-creator-shell/src/features/project-summary/projectPath.ts @@ -37,15 +37,3 @@ export function projectPathsMatchForInvalidation( } return normalize(eventPath) === normalize(activePath); } - -export function isSafeProjectRelativePath(value: string) { - const path = value.trim(); - return ( - !!path && - !isAbsoluteProjectPath(path) && - !projectPathHasControlCharacter(path) && - !path.includes('\\') && - !path.includes(':') && - path.split('/').every((part) => part && part !== '.' && part !== '..') - ); -} diff --git a/apps/ai-game-creator-shell/src/features/project-summary/projectPlanningSummaries.ts b/apps/ai-game-creator-shell/src/features/project-summary/projectPlanningSummaries.ts deleted file mode 100644 index 02e69c3d5..000000000 --- a/apps/ai-game-creator-shell/src/features/project-summary/projectPlanningSummaries.ts +++ /dev/null @@ -1,463 +0,0 @@ -import { - type GameCreationAgentRunTrace, - type GameCreationAppAssetSourceKind, - type GameCreationAppManifest, - type GameCreationAppTaskState, - selectGameCreationAppReadyTasks, -} from '../../../../../packages/shared/src/contracts/gameCreationApp'; -import { taskRowsFromManifest } from '../agent-runtime'; -import { - formatAgentRunStatus, - formatTraceTaskId, - isAgentReviewStep, - isAgentRunTracePassed, - isPlaytestTraceStep, - readableArtifactsFromAgentRunTrace, -} from './agentTrace'; -import { - isProjectAudioAsset, - isProjectVisualAsset, -} from './projectAssetSummaries'; -import { - assetSourceKindLabels, - previewStatusLabels, -} from './projectSummaryConstants'; - -export function summarizeProjectEvidenceLedger( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const commandRuns = nextManifest.commandRuns ?? []; - const preview = nextManifest.preview; - const previewRunning = preview?.status === 'running' && preview.url; - const previewSummary = previewRunning - ? `运行中 ${preview.url}` - : preview - ? previewStatusLabels[preview.status] - : '未启动'; - const tracePassed = isAgentRunTracePassed(trace); - const staticSmokePassed = - commandRuns.some( - (commandRun) => - commandRun.commandId === 'game.static_smoke' && - commandRun.status === 'completed', - ) || - Boolean( - trace?.steps.some((step) => - step.toolCalls.some( - (toolCall) => - toolCall.toolId === 'game.static_smoke' && toolCall.status === 'ok', - ), - ), - ); - const latestExportCommand = - [...commandRuns] - .reverse() - .find( - (commandRun) => commandRun.commandId === 'project.export_package', - ) ?? null; - const latestFailedCommand = - [...commandRuns] - .reverse() - .find((commandRun) => commandRun.status === 'failed') ?? null; - const latestReviewStep = - trace?.steps.filter(isAgentReviewStep).slice(-1)[0] ?? null; - const latestPlaytestStep = - trace?.steps.filter(isPlaytestTraceStep).slice(-1)[0] ?? null; - const readableArtifacts = trace - ? readableArtifactsFromAgentRunTrace(trace) - : []; - const hasGameEntry = - trace?.artifacts.some((artifact) => artifact.path === 'game/index.html') ?? - false; - const hasVisualAsset = nextManifest.assets.some(isProjectVisualAsset); - const hasAudioAsset = nextManifest.assets.some(isProjectAudioAsset); - const evidenceLines = [ - trace - ? `- Run trace:已有 ${trace.runId} · ${formatAgentRunStatus(trace)}` - : '- Run trace:缺失', - `- Evaluator:${ - tracePassed - ? '通过' - : trace - ? `未通过 ${trace.status} / ${trace.stopReason}` - : '缺失' - }${latestReviewStep ? ` · ${latestReviewStep.summary}` : ''}`, - `- 静态自检:${staticSmokePassed ? '已有通过证据' : '缺失通过证据'}`, - `- 预览:${previewSummary}`, - `- 试玩包:${ - latestExportCommand?.status === 'completed' - ? '已有导出记录' - : latestExportCommand?.status === 'failed' - ? '最近导出失败' - : '缺失' - }`, - `- 入口产物:${hasGameEntry ? 'game/index.html 已在 trace 产物中' : '缺失 trace 产物证据'}`, - `- 资产:${nextManifest.assets.length} 个 · 美术 ${ - hasVisualAsset ? '有' : '缺' - } · 音频 ${hasAudioAsset ? '有' : '可后补'}`, - `- 可读产物:${readableArtifacts.length} 个`, - latestPlaytestStep - ? `- 最近试玩:${latestPlaytestStep.agent} #${latestPlaytestStep.pass} · ${latestPlaytestStep.status} · ${latestPlaytestStep.summary}` - : '- 最近试玩:暂无', - latestFailedCommand - ? `- 最近失败命令:${latestFailedCommand.commandId}` - : '- 最近失败命令:暂无', - ]; - const gaps: Array<{ text: string; command: string }> = []; - if (!trace) { - gaps.push({ text: '缺少最近 run trace', command: '/next' }); - } else if (!tracePassed) { - gaps.push({ text: 'Evaluator 尚未通过', command: '/review' }); - } - if (!staticSmokePassed) { - gaps.push({ text: '缺少静态自检通过证据', command: '/run' }); - } - if (!previewRunning) { - gaps.push({ text: '本地预览未运行', command: '/run' }); - } - if (tracePassed && latestExportCommand?.status !== 'completed') { - gaps.push({ text: '缺少本地试玩包导出记录', command: '/export' }); - } - if (!hasVisualAsset) { - gaps.push({ text: '缺少可复用美术素材', command: '/art' }); - } - if (latestFailedCommand) { - gaps.push({ text: '存在失败命令需要查看日志', command: '/logs' }); - } - const firstGap = gaps[0] ?? null; - - return { - text: [ - '验证证据台账:', - `- 项目:${nextManifest.name}`, - ...evidenceLines, - gaps.length > 0 - ? `- 缺口:\n${gaps - .map((gap) => `- ${gap.text} · 建议 ${gap.command}`) - .join('\n')}` - : '- 缺口:暂无关键缺口', - '- 边界:只整理当前已加载证据;不读取文件;不启动或打开预览;不导出试玩包;不写项目', - `- 建议:${firstGap?.command ?? '/ready'}`, - ].join('\n'), - draftCommand: firstGap?.command ?? '/ready', - draftCommandLabel: firstGap ? '补齐首个证据缺口' : '查看就绪度', - }; -} - -export function summarizeProjectDependencyMap( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const manifestTasks = taskRowsFromManifest(nextManifest); - const traceTasks = trace?.taskGraph.tasks ?? []; - const taskSource = traceTasks.length > 0 ? traceTasks : manifestTasks; - const manifestTasksById = new Map( - manifestTasks.map((task) => [task.id, task]), - ); - const taskSourceById = new Map(taskSource.map((task) => [task.id, task])); - const resolvedTask = (taskId: string) => - taskSourceById.get(taskId) ?? manifestTasksById.get(taskId) ?? null; - const completedTaskIds = new Set( - taskSource - .filter((task) => task.status === 'completed') - .map((task) => task.id), - ); - const readyTaskIds = - trace?.taskGraph.readyTaskIds ?? - selectGameCreationAppReadyTasks({ tasks: manifestTasks }).map( - (task) => task.id, - ); - const activeTaskIds = trace?.taskGraph.activeTaskIds ?? []; - const carriedTaskIds = trace?.taskGraph.carriedTaskIds ?? []; - const blockedTasks = taskSource - .filter((task) => task.status !== 'completed') - .map((task) => ({ - task, - missingDependencies: task.dependencies.filter( - (dependencyId) => !completedTaskIds.has(dependencyId), - ), - })) - .filter((entry) => entry.missingDependencies.length > 0); - const readyLines = readyTaskIds - .map((taskId) => resolvedTask(taskId)) - .filter((task): task is GameCreationAppTaskState => Boolean(task)) - .slice(0, 4) - .map((task) => { - const dependencies = - task.dependencies.length > 0 - ? task.dependencies - .map((dependencyId) => - formatTraceTaskId(dependencyId, taskSource), - ) - .join(';') - : '无'; - return `- ${formatTraceTaskId(task.id, taskSource)} · 依赖:${dependencies}`; - }); - const blockedLines = blockedTasks.slice(0, 5).map((entry) => { - const missing = entry.missingDependencies - .map((dependencyId) => formatTraceTaskId(dependencyId, taskSource)) - .join(';'); - return `- ${formatTraceTaskId(entry.task.id, taskSource)} · 等待:${missing}`; - }); - if (blockedTasks.length > blockedLines.length) { - blockedLines.push( - `- 还有 ${blockedTasks.length - blockedLines.length} 个等待依赖的任务`, - ); - } - - let draftCommand = '/tasks'; - let draftCommandLabel = '查看任务'; - if (activeTaskIds.length > 0 || carriedTaskIds.length > 0) { - draftCommand = '/todo'; - draftCommandLabel = '查看小步清单'; - } else if (readyTaskIds.length > 0) { - draftCommand = '/criteria'; - draftCommandLabel = '查看验收标准'; - } else if (blockedTasks.length === 0) { - draftCommand = '/next'; - draftCommandLabel = '查看下一步'; - } - - return { - text: [ - '任务依赖链:', - trace ? `- Run:${trace.runId} · ${formatAgentRunStatus(trace)}` : null, - `- 状态:active ${activeTaskIds.length} / carry ${carriedTaskIds.length} / ready ${readyTaskIds.length} / 等待依赖 ${blockedTasks.length}`, - readyLines.length > 0 - ? `- 可执行任务:\n${readyLines.join('\n')}` - : '- 可执行任务:暂无', - blockedLines.length > 0 - ? `- 依赖等待:\n${blockedLines.join('\n')}` - : '- 依赖等待:暂无', - '- 边界:只整理任务依赖;不读取任务文件;不启动 run;不修改项目', - `- 建议:${draftCommand}`, - ] - .filter(Boolean) - .join('\n'), - draftCommand, - draftCommandLabel, - }; -} - -export function summarizeProjectRevisionDraft( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const manifestTasks = taskRowsFromManifest(nextManifest); - const traceTasks = trace?.taskGraph.tasks ?? []; - const taskSource = traceTasks.length > 0 ? traceTasks : manifestTasks; - const manifestTasksById = new Map( - manifestTasks.map((task) => [task.id, task]), - ); - const taskSourceById = new Map(taskSource.map((task) => [task.id, task])); - const resolvedTask = (taskId: string) => - taskSourceById.get(taskId) ?? manifestTasksById.get(taskId) ?? null; - const failedTasks = taskSource.filter((task) => task.status === 'failed'); - const activeTasks = - trace?.taskGraph.activeTaskIds - .map(resolvedTask) - .filter((task): task is GameCreationAppTaskState => Boolean(task)) ?? []; - const carriedTasks = - trace?.taskGraph.carriedTaskIds - .map(resolvedTask) - .filter((task): task is GameCreationAppTaskState => Boolean(task)) ?? []; - const readyTasks = - trace?.taskGraph.readyTaskIds - .map(resolvedTask) - .filter((task): task is GameCreationAppTaskState => Boolean(task)) ?? - selectGameCreationAppReadyTasks({ tasks: manifestTasks }); - const commandRuns = nextManifest.commandRuns ?? []; - const latestExportCommand = - [...commandRuns] - .reverse() - .find( - (commandRun) => commandRun.commandId === 'project.export_package', - ) ?? null; - const preview = nextManifest.preview; - const previewRunning = preview?.status === 'running' && preview.url; - const tracePassed = isAgentRunTracePassed(trace); - const blockedTrace = - trace?.lifecycleStatus === 'killed' || - trace?.status === 'failed' || - trace?.status === 'needs-revision' || - trace?.stopReason === 'max-passes-exhausted'; - const latestReviewStep = - trace?.steps.filter(isAgentReviewStep).slice(-1)[0] ?? null; - const latestPlaytestStep = - trace?.steps.filter(isPlaytestTraceStep).slice(-1)[0] ?? null; - const revisionItems: string[] = []; - const addRevisionItem = (item: string) => { - if (!revisionItems.includes(item)) { - revisionItems.push(item); - } - }; - - if (!trace) { - addRevisionItem('明确首版核心玩法、可玩目标和验收口径'); - } else { - if (trace.taskGraph.repairFocus.length > 0) { - addRevisionItem( - `处理返工焦点:${trace.taskGraph.repairFocus.join(';')}`, - ); - } - if (failedTasks.length > 0) { - addRevisionItem( - `修复失败任务:${formatTraceTaskId(failedTasks[0]!.id, taskSource)}`, - ); - } - if (activeTasks.length > 0) { - addRevisionItem( - `继续 active 任务:${formatTraceTaskId(activeTasks[0]!.id, taskSource)}`, - ); - } - if (carriedTasks.length > 0) { - addRevisionItem( - `承接 carry 任务:${formatTraceTaskId(carriedTasks[0]!.id, taskSource)}`, - ); - } - if (readyTasks.length > 0) { - addRevisionItem( - `推进 ready 任务:${formatTraceTaskId(readyTasks[0]!.id, taskSource)}`, - ); - } - if (blockedTrace && latestReviewStep) { - addRevisionItem(`按评审修复:${latestReviewStep.summary}`); - } - if (latestPlaytestStep && latestPlaytestStep.status !== 'completed') { - addRevisionItem(`补试玩问题:${latestPlaytestStep.summary}`); - } - if (tracePassed && !previewRunning) { - addRevisionItem('补齐本地试玩:启动预览并验证首屏'); - } - if (tracePassed && latestExportCommand?.status !== 'completed') { - addRevisionItem('交付:导出本地试玩包'); - } - } - - if (revisionItems.length === 0) { - addRevisionItem('做一轮小步打磨,优先提升可玩性和交付清晰度'); - } - - const keepItem = tracePassed - ? '保留当前已通过的核心玩法和可运行入口' - : '保留当前创作目标、已有任务拆分和已生成资产'; - const adjustItems = revisionItems.slice(0, 2); - const addItems: string[] = []; - if (tracePassed && !previewRunning) { - addItems.push('补一次本地预览验证'); - } - if (tracePassed && latestExportCommand?.status !== 'completed') { - addItems.push('补导出本地试玩包'); - } - if (addItems.length === 0) { - addItems.push('补清楚下一轮验收证据'); - } - const acceptanceItem = '通过 /ready、/qa 和 /changes 复查'; - const draftCommand = `/agent-resume 改版说明:保留${keepItem};调整${adjustItems.join( - ';', - )};新增${addItems.join(';')};验收${acceptanceItem}`; - - return { - text: [ - '改版草稿:', - `- 项目:${nextManifest.name}`, - trace ? `- Run:${trace.runId} · ${formatAgentRunStatus(trace)}` : null, - latestReviewStep - ? `- 最近评审:${latestReviewStep.agent} #${latestReviewStep.pass} · ${latestReviewStep.status} · ${latestReviewStep.summary}` - : '- 最近评审:暂无', - latestPlaytestStep - ? `- 最近试玩:${latestPlaytestStep.agent} #${latestPlaytestStep.pass} · ${latestPlaytestStep.status} · ${latestPlaytestStep.summary}` - : '- 最近试玩:暂无', - `- 保留项:${keepItem}`, - `- 调整项:${adjustItems.join(';')}`, - `- 新增项:${addItems.join(';')}`, - `- 验收口径:${acceptanceItem}`, - `- 优先依据:\n${revisionItems.map((item) => `- ${item}`).join('\n')}`, - '- 参考命令:/ready;/deps;/qa;/changes', - `- 草稿:${draftCommand}`, - '- 边界:只准备改版说明;不继续 run;不读取文件;不启动预览;不导出试玩包;不写项目', - `- 建议:${draftCommand}`, - ] - .filter(Boolean) - .join('\n'), - draftCommand, - draftCommandLabel: '填入改版说明', - }; -} - -export function summarizeProjectPrivacyBoundary( - nextManifest: GameCreationAppManifest, - projectPath: string, - trace: GameCreationAgentRunTrace | null, -) { - const sourceCounts = nextManifest.assets.reduce( - (counts, asset) => { - counts[asset.source.kind] += 1; - return counts; - }, - { - uploaded: 0, - generated: 0, - canvas: 0, - } satisfies Record, - ); - const sourceSummary = - (Object.keys(sourceCounts) as GameCreationAppAssetSourceKind[]) - .filter((source) => sourceCounts[source] > 0) - .map( - (source) => `${assetSourceKindLabels[source]} ${sourceCounts[source]}`, - ) - .join(' / ') || '暂无'; - const commandRuns = nextManifest.commandRuns ?? []; - const latestExportCommand = - [...commandRuns] - .reverse() - .find( - (commandRun) => commandRun.commandId === 'project.export_package', - ) ?? null; - const preview = nextManifest.preview; - const previewRunning = preview?.status === 'running' && preview.url; - const previewSummary = previewRunning - ? `本机预览 ${preview.url}` - : preview - ? previewStatusLabels[preview.status] - : '未启动'; - const draftCommand = - latestExportCommand?.status === 'completed' - ? '/exports' - : nextManifest.assets.length > 0 - ? '/credits' - : '/config'; - const draftCommandLabel = - draftCommand === '/exports' - ? '查看试玩包' - : draftCommand === '/credits' - ? '查看素材来源' - : '打开配置'; - - return { - text: [ - '隐私与导出边界:', - `- 项目:${nextManifest.name}`, - `- 本地目录:${projectPath}`, - '- 智能服务凭据由服务端按登录账号管理;不进入客户端、manifest、trace、聊天、导出包或项目文件', - `- 本地预览:${previewSummary};仅限 127.0.0.1 本机访问`, - `- 试玩包:${ - latestExportCommand?.status === 'completed' ? '最近已导出' : '尚未导出' - };只应包含 game/**、assets/** 和 exports/README.md`, - `- 内部文件:.agent/**、memory/**、日志、trace、配置和密钥不得进入试玩包`, - `- 素材来源:${nextManifest.assets.length} 个;${sourceSummary}`, - `- Trace:${ - trace - ? `${trace.runId} · ${trace.artifacts.length} 个内部产物记录` - : '暂无最近 run' - };只通过 /trace 或 /internals 查看,不作为交付内容`, - '- 交付前建议:/credits;/ready;/export;/exports', - '- 边界:只整理隐私与交付口径;不读取文件;不导出;不启动预览;不写项目', - `- 建议:${draftCommand}`, - ].join('\n'), - draftCommand, - draftCommandLabel, - }; -} diff --git a/apps/ai-game-creator-shell/src/features/project-summary/projectPlaytestSummaries.ts b/apps/ai-game-creator-shell/src/features/project-summary/projectPlaytestSummaries.ts deleted file mode 100644 index 53c928a77..000000000 --- a/apps/ai-game-creator-shell/src/features/project-summary/projectPlaytestSummaries.ts +++ /dev/null @@ -1,786 +0,0 @@ -import { - type GameCreationAgentRunTrace, - type GameCreationAppManifest, - type GameCreationAppTaskState, -} from '../../../../../packages/shared/src/contracts/gameCreationApp'; -import { taskRowsFromManifest } from '../agent-runtime'; -import { - formatAgentRunStatus, - isAgentRunTracePassed, - isPlaytestTraceStep, -} from './agentTrace'; -import { - isProjectAudioAsset, - isProjectVisualAsset, -} from './projectAssetSummaries'; -import { - assetSourceKindLabels, - previewStatusLabels, - taskGroupLabels, - taskStatusLabels, -} from './projectSummaryConstants'; - -export function summarizeProjectAudienceGuide( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const manifestTasks = taskRowsFromManifest(nextManifest); - const traceTasks = trace?.taskGraph.tasks ?? []; - const taskSource = traceTasks.length > 0 ? traceTasks : manifestTasks; - const previewReadinessTask = taskSource.find( - (task) => task.id === 'preview-readiness', - ); - const previewPlaytestTask = taskSource.find( - (task) => task.id === 'preview-playtest', - ); - const previewTaskLines = [previewReadinessTask, previewPlaytestTask] - .filter((task): task is GameCreationAppTaskState => Boolean(task)) - .map( - (task) => - `- ${task.id}:${taskGroupLabels[task.group]} / ${task.role} ${task.title} · ${taskStatusLabels[task.status]}`, - ); - const preview = nextManifest.preview; - const previewRunning = preview?.status === 'running' && preview.url; - const previewSummary = previewRunning - ? `运行中 ${preview.url}` - : preview - ? previewStatusLabels[preview.status] - : '未启动'; - const tracePassed = isAgentRunTracePassed(trace); - const blockedTrace = - trace?.lifecycleStatus === 'killed' || - trace?.status === 'failed' || - trace?.status === 'needs-revision' || - trace?.stopReason === 'max-passes-exhausted'; - const goal = - nextManifest.goal?.trim() || - trace?.goal?.trim() || - trace?.taskGraph.goal?.trim() || - '暂无'; - const latestPlaytestStep = - trace?.steps.filter(isPlaytestTraceStep).slice(-1)[0] ?? null; - - let draftCommand = '/feedback'; - let draftCommandLabel = '准备反馈'; - if (!trace) { - draftCommand = '/next'; - draftCommandLabel = '查看下一步'; - } else if (blockedTrace) { - draftCommand = '/review'; - draftCommandLabel = '查看评审'; - } else if (!tracePassed) { - draftCommand = '/trace'; - draftCommandLabel = '查看 trace'; - } else if (!previewRunning) { - draftCommand = '/run'; - draftCommandLabel = '启动试玩'; - } - - return { - text: [ - '首批试玩对象:', - `- 项目:${nextManifest.name}`, - `- 目标:${goal}`, - `- 当前状态:${ - tracePassed - ? `最近 run 已通过${trace?.runId ? ` ${trace.runId}` : ''}` - : trace - ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}` - : '暂无最近 run' - };预览 ${previewSummary}`, - '- 先测人群:创作者自测 1 轮;熟悉目标的同事 1-2 人;完全没看过项目的人 3-5 人;至少 1 位移动/触屏用户', - '- 第一批测试者:3-5 人;每人 5-10 分钟;先看能否独立理解', - '- 观察重点:30 秒能否理解目标;输入是否顺;胜负/重开是否明确;难度是否过早劝退;视觉/音效是否干扰', - previewTaskLines.length > 0 - ? `- 试玩任务:\n${previewTaskLines.join('\n')}` - : '- 试玩任务:暂无', - `- 最近试玩证据:${ - latestPlaytestStep - ? `${latestPlaytestStep.agent} #${latestPlaytestStep.pass} · ${latestPlaytestStep.status} · ${latestPlaytestStep.summary}` - : '暂无' - }`, - '- 暂不面向:公开发布、付费用户、大规模投放、儿童/无障碍等强承诺场景', - '- 参考:/playtest;/test-plan;/feedback;/share', - '- 边界:只整理首批试玩对象;不读取文件;不启动或打开预览;不导出试玩包;不写项目', - `- 建议:${draftCommand}`, - ].join('\n'), - draftCommand, - draftCommandLabel, - }; -} - -export function summarizeProjectPlaytestInvite( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const preview = nextManifest.preview; - const previewRunning = preview?.status === 'running' && preview.url; - const previewSummary = previewRunning - ? `运行中 ${preview.url}` - : preview - ? previewStatusLabels[preview.status] - : '未启动'; - const tracePassed = isAgentRunTracePassed(trace); - const blockedTrace = - trace?.lifecycleStatus === 'killed' || - trace?.status === 'failed' || - trace?.status === 'needs-revision' || - trace?.stopReason === 'max-passes-exhausted'; - const goal = - nextManifest.goal?.trim() || - trace?.goal?.trim() || - trace?.taskGraph.goal?.trim() || - '暂无'; - - let draftCommand = '/feedback'; - let draftCommandLabel = '准备反馈'; - if (!trace) { - draftCommand = '/next'; - draftCommandLabel = '查看下一步'; - } else if (blockedTrace) { - draftCommand = '/review'; - draftCommandLabel = '查看评审'; - } else if (!tracePassed) { - draftCommand = '/trace'; - draftCommandLabel = '查看 trace'; - } else if (!previewRunning) { - draftCommand = '/run'; - draftCommandLabel = '启动试玩'; - } - - return { - text: [ - '试玩邀请:', - `- 项目:${nextManifest.name}`, - `- 目标:${goal}`, - `- 当前状态:${ - tracePassed - ? `最近 run 已通过${trace?.runId ? ` ${trace.runId}` : ''}` - : trace - ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}` - : '暂无最近 run' - };预览 ${previewSummary}`, - '- 邀请对象:先发 3-5 人;优先熟人/同事/没看过项目的人;暂不公开发布或大规模投放', - '- 邀请文案:我做了一个早期 Web 小游戏原型,想请你花 5-10 分钟试玩。重点不是评价完成度,而是看 30 秒内能否理解目标、操作是否顺、胜负和重开是否清楚。试玩后请反馈:哪里没看懂、哪里卡住、还想不想再来一局。', - previewRunning - ? '- 发送前:本地预览已运行,可配合 /open-preview' - : '- 发送前:先 /run 启动本地预览,再把本地试玩方式发给测试者', - '- 收反馈:让测试者按 /feedback 的三类模板回收;需要交付包时再看 /share', - '- 参考:/audience;/test-plan;/feedback;/share', - '- 边界:只准备邀请文案;不读取文件;不启动或打开预览;不导出试玩包;不写项目', - `- 建议:${draftCommand}`, - ].join('\n'), - draftCommand, - draftCommandLabel, - }; -} - -export function summarizeProjectBugReport( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const preview = nextManifest.preview; - const previewRunning = preview?.status === 'running' && preview.url; - const previewSummary = previewRunning - ? `运行中 ${preview.url}` - : preview - ? previewStatusLabels[preview.status] - : '未启动'; - const tracePassed = isAgentRunTracePassed(trace); - const blockedTrace = - trace?.lifecycleStatus === 'killed' || - trace?.status === 'failed' || - trace?.status === 'needs-revision' || - trace?.stopReason === 'max-passes-exhausted'; - const latestPlaytestStep = - trace?.steps.filter(isPlaytestTraceStep).slice(-1)[0] ?? null; - - let draftCommand = '/agent-resume 缺陷修复:'; - let draftCommandLabel = '填写缺陷修复'; - if (!trace) { - draftCommand = '/next'; - draftCommandLabel = '查看下一步'; - } else if (blockedTrace) { - draftCommand = '/review'; - draftCommandLabel = '查看评审'; - } else if (!tracePassed) { - draftCommand = '/trace'; - draftCommandLabel = '查看 trace'; - } else if (!previewRunning) { - draftCommand = '/run'; - draftCommandLabel = '启动试玩'; - } - - return { - text: [ - '缺陷记录:', - `- 项目:${nextManifest.name}`, - trace ? `- Run:${trace.runId} · ${formatAgentRunStatus(trace)}` : null, - `- 复现入口:预览 ${previewSummary}${previewRunning ? ' · /open-preview' : ' · 建议 /run'}`, - `- 最近试玩证据:${ - latestPlaytestStep - ? `${latestPlaytestStep.agent} #${latestPlaytestStep.pass} · ${latestPlaytestStep.status} · ${latestPlaytestStep.summary}` - : '暂无' - }`, - '- 记录模板:问题一句话;复现步骤 1/2/3;期望结果;实际结果;设备/输入方式;严重度 阻断/高/中/低;附件 截图/录屏/日志时间点', - '- 优先级口径:阻断无法进入首局;高影响胜负或重开;中影响理解或手感;低为包装和文字问题', - '- 转修复草稿:/agent-resume 缺陷修复:现象…;复现…;期望…;实际…', - '- 参考:/test-plan;/feedback;/review;/logs', - '- 边界:只准备缺陷记录模板;不读取文件;不启动或打开预览;不导出试玩包;不写项目', - `- 建议:${draftCommand}`, - ] - .filter(Boolean) - .join('\n'), - draftCommand, - draftCommandLabel, - }; -} - -export function summarizeProjectPlaytestSurvey( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const preview = nextManifest.preview; - const previewRunning = preview?.status === 'running' && preview.url; - const previewSummary = previewRunning - ? `运行中 ${preview.url}` - : preview - ? previewStatusLabels[preview.status] - : '未启动'; - const tracePassed = isAgentRunTracePassed(trace); - const blockedTrace = - trace?.lifecycleStatus === 'killed' || - trace?.status === 'failed' || - trace?.status === 'needs-revision' || - trace?.stopReason === 'max-passes-exhausted'; - const goal = - nextManifest.goal?.trim() || - trace?.goal?.trim() || - trace?.taskGraph.goal?.trim() || - '暂无'; - - let draftCommand = '/invite'; - let draftCommandLabel = '准备邀请'; - if (!trace) { - draftCommand = '/next'; - draftCommandLabel = '查看下一步'; - } else if (blockedTrace) { - draftCommand = '/review'; - draftCommandLabel = '查看评审'; - } else if (!tracePassed) { - draftCommand = '/trace'; - draftCommandLabel = '查看 trace'; - } else if (!previewRunning) { - draftCommand = '/run'; - draftCommandLabel = '启动试玩'; - } - - return { - text: [ - '试玩问卷:', - `- 项目:${nextManifest.name}`, - `- 目标:${goal}`, - `- 当前状态:${ - tracePassed - ? `最近 run 已通过${trace?.runId ? ` ${trace.runId}` : ''}` - : trace - ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}` - : '暂无最近 run' - };预览 ${previewSummary}`, - '- 使用场景:发给首批 3-5 位测试者;每人 5-10 分钟;先自由玩一局再回答', - '- 问题清单:1. 30 秒内你觉得目标是什么?2. 第一次操作哪里最卡?3. 胜负/重开是否清楚?4. 难度/节奏感觉如何?5. 最想保留和最想改的各一项?', - '- 记录格式:每题 1-5 分 + 一句话;补充设备、输入方式、是否愿意再玩一局', - '- 追踪方式:客户端问题由自动诊断提示;整体反馈走 /feedback;下一轮改动走 /revise', - '- 参考:/invite;/audience;/feedback', - '- 边界:只准备试玩问卷;不读取文件;不启动或打开预览;不导出试玩包;不写项目', - `- 建议:${draftCommand}`, - ].join('\n'), - draftCommand, - draftCommandLabel, - }; -} - -export function summarizeProjectCoverChecklist( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const preview = nextManifest.preview; - const previewRunning = preview?.status === 'running' && preview.url; - const previewSummary = previewRunning - ? `运行中 ${preview.url}` - : preview - ? previewStatusLabels[preview.status] - : '未启动'; - const tracePassed = isAgentRunTracePassed(trace); - const blockedTrace = - trace?.lifecycleStatus === 'killed' || - trace?.status === 'failed' || - trace?.status === 'needs-revision' || - trace?.stopReason === 'max-passes-exhausted'; - const visualAssets = nextManifest.assets.filter(isProjectVisualAsset); - const coverCandidate = - visualAssets.find( - (asset) => - asset.source.kind === 'canvas' || asset.source.kind === 'generated', - ) ?? - visualAssets[0] ?? - null; - const canvasOrGeneratedCount = visualAssets.filter( - (asset) => - asset.source.kind === 'canvas' || asset.source.kind === 'generated', - ).length; - const coverCandidateSummary = coverCandidate - ? `${coverCandidate.localPath} · ${coverCandidate.mediaType} · ${assetSourceKindLabels[coverCandidate.source.kind]}` - : '暂无 · 先用 /screenshots 或 /art 准备'; - - let draftCommand = coverCandidate ? '/listing' : '/art'; - let draftCommandLabel = coverCandidate ? '准备作品页' : '查看美术素材'; - if (!trace) { - draftCommand = '/next'; - draftCommandLabel = '查看下一步'; - } else if (blockedTrace) { - draftCommand = '/review'; - draftCommandLabel = '查看评审'; - } else if (!tracePassed) { - draftCommand = '/trace'; - draftCommandLabel = '查看 trace'; - } else if (!previewRunning) { - draftCommand = '/run'; - draftCommandLabel = '启动试玩'; - } - - return { - text: [ - '封面与缩略图:', - `- 项目:${nextManifest.name}`, - `- 当前状态:${ - tracePassed - ? `最近 run 已通过${trace?.runId ? ` ${trace.runId}` : ''}` - : trace - ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}` - : '暂无最近 run' - };预览 ${previewSummary}`, - `- 可用素材:视觉素材 ${visualAssets.length} 个;画板/生成候选 ${canvasOrGeneratedCount} 个;总资产 ${nextManifest.assets.length} 个`, - `- 封面候选:${coverCandidateSummary}`, - '- 用途尺寸:作品页封面 16:9;社区缩略图 1:1;移动首屏 9:16', - '- 选择口径:优先展示核心玩法状态;避免内部路径、调试面板、密钥配置或纯空场景', - '- 补齐路径:有可试玩时先 /screenshots;缺美术时 /art;作品页文案走 /listing', - '- 参考:/screenshots;/listing;/media-kit;/credits', - '- 边界:只准备封面与缩略图检查;不截屏;不裁剪;不读取文件;不启动或打开预览;不导出试玩包;不上传云端;不发布作品;不写项目', - `- 建议:${draftCommand}`, - ].join('\n'), - draftCommand, - draftCommandLabel, - }; -} - -export function summarizeProjectScreenshotChecklist( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const preview = nextManifest.preview; - const previewRunning = preview?.status === 'running' && preview.url; - const previewSummary = previewRunning - ? `运行中 ${preview.url}` - : preview - ? previewStatusLabels[preview.status] - : '未启动'; - const tracePassed = isAgentRunTracePassed(trace); - const blockedTrace = - trace?.lifecycleStatus === 'killed' || - trace?.status === 'failed' || - trace?.status === 'needs-revision' || - trace?.stopReason === 'max-passes-exhausted'; - const visualAssetCount = - nextManifest.assets.filter(isProjectVisualAsset).length; - - let draftCommand = '/listing'; - let draftCommandLabel = '准备作品页'; - if (!trace) { - draftCommand = '/next'; - draftCommandLabel = '查看下一步'; - } else if (blockedTrace) { - draftCommand = '/review'; - draftCommandLabel = '查看评审'; - } else if (!tracePassed) { - draftCommand = '/trace'; - draftCommandLabel = '查看 trace'; - } else if (!previewRunning) { - draftCommand = '/run'; - draftCommandLabel = '启动试玩'; - } - - return { - text: [ - '宣传截图:', - `- 项目:${nextManifest.name}`, - `- 当前状态:${ - tracePassed - ? `最近 run 已通过${trace?.runId ? ` ${trace.runId}` : ''}` - : trace - ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}` - : '暂无最近 run' - };预览 ${previewSummary}`, - `- 可用素材:视觉素材 ${visualAssetCount} 个;总资产 ${nextManifest.assets.length} 个`, - '- 截图目标:封面一张;核心操作一张;胜负/重开一张;移动或窄屏一张;异常/空状态不作为首批宣传图', - '- 拍摄顺序:先确认 /run 可试玩;进入第一局 10-30 秒;截核心交互;再截结算或失败反馈', - '- 命名建议:exports/screenshots/cover.png;gameplay.png;result.png;mobile.png', - '- 文案搭配:每张图只配一句卖点;作品页标题和标签继续走 /listing', - '- 参考:/listing;/publish;/share;/credits', - '- 边界:只准备截图清单;不截屏;不读取文件;不启动或打开预览;不导出试玩包;不写项目', - `- 建议:${draftCommand}`, - ].join('\n'), - draftCommand, - draftCommandLabel, - }; -} - -export function summarizeProjectTrailerScript( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const preview = nextManifest.preview; - const previewRunning = preview?.status === 'running' && preview.url; - const previewSummary = previewRunning - ? `运行中 ${preview.url}` - : preview - ? previewStatusLabels[preview.status] - : '未启动'; - const tracePassed = isAgentRunTracePassed(trace); - const blockedTrace = - trace?.lifecycleStatus === 'killed' || - trace?.status === 'failed' || - trace?.status === 'needs-revision' || - trace?.stopReason === 'max-passes-exhausted'; - const goal = - nextManifest.goal?.trim() || - trace?.goal?.trim() || - trace?.taskGraph.goal?.trim() || - '暂无'; - const visualAssetCount = - nextManifest.assets.filter(isProjectVisualAsset).length; - const audioAssetCount = - nextManifest.assets.filter(isProjectAudioAsset).length; - - let draftCommand = '/share'; - let draftCommandLabel = '准备交付'; - if (!trace) { - draftCommand = '/next'; - draftCommandLabel = '查看下一步'; - } else if (blockedTrace) { - draftCommand = '/review'; - draftCommandLabel = '查看评审'; - } else if (!tracePassed) { - draftCommand = '/trace'; - draftCommandLabel = '查看 trace'; - } else if (!previewRunning) { - draftCommand = '/run'; - draftCommandLabel = '启动试玩'; - } - - return { - text: [ - '试玩短视频:', - `- 项目:${nextManifest.name}`, - `- 目标:${goal}`, - `- 当前状态:${ - tracePassed - ? `最近 run 已通过${trace?.runId ? ` ${trace.runId}` : ''}` - : trace - ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}` - : '暂无最近 run' - };预览 ${previewSummary}`, - `- 可用素材:视觉素材 ${visualAssetCount} 个;音频素材 ${audioAssetCount} 个;总资产 ${nextManifest.assets.length} 个`, - '- 15 秒结构:0-3 秒首屏目标;3-8 秒核心操作;8-12 秒胜负 / 重开;12-15 秒结尾 CTA', - '- 镜头清单:标题 / 目标提示;玩家第一次操作;得分或失败反馈;重开按钮;结尾试玩邀请', - '- 口播节奏:一句玩法目标;一句操作说明;一句邀请试玩和反馈', - '- 录制提示:先确认 /run 可试玩;横屏或竖屏只选一种;不露内部路径、调试面板或密钥配置', - '- 参考:/screenshots;/listing;/share;/publish', - '- 边界:只准备试玩短视频脚本;不录屏;不读取文件;不启动或打开预览;不导出试玩包;不写项目', - `- 建议:${draftCommand}`, - ].join('\n'), - draftCommand, - draftCommandLabel, - }; -} - -export function summarizeProjectPlaytestFaq( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const preview = nextManifest.preview; - const previewRunning = preview?.status === 'running' && preview.url; - const previewSummary = previewRunning - ? `运行中 ${preview.url}` - : preview - ? previewStatusLabels[preview.status] - : '未启动'; - const tracePassed = isAgentRunTracePassed(trace); - const blockedTrace = - trace?.lifecycleStatus === 'killed' || - trace?.status === 'failed' || - trace?.status === 'needs-revision' || - trace?.stopReason === 'max-passes-exhausted'; - const goal = - nextManifest.goal?.trim() || - trace?.goal?.trim() || - trace?.taskGraph.goal?.trim() || - '暂无'; - - let draftCommand = '/share'; - let draftCommandLabel = '准备交付'; - if (!trace) { - draftCommand = '/next'; - draftCommandLabel = '查看下一步'; - } else if (blockedTrace) { - draftCommand = '/review'; - draftCommandLabel = '查看评审'; - } else if (!tracePassed) { - draftCommand = '/trace'; - draftCommandLabel = '查看 trace'; - } else if (!previewRunning) { - draftCommand = '/run'; - draftCommandLabel = '启动试玩'; - } - - return { - text: [ - '试玩 FAQ:', - `- 项目:${nextManifest.name}`, - `- 目标:${goal}`, - `- 当前状态:${ - tracePassed - ? `最近 run 已通过${trace?.runId ? ` ${trace.runId}` : ''}` - : trace - ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}` - : '暂无最近 run' - };预览 ${previewSummary}`, - '- 问答清单:1. 这是什么?2. 怎么开始和重开?3. 需要反馈什么?4. 打不开或卡住怎么办?5. 能不能转发或公开?', - '- 回答口径:早期本地 Web 原型;5-10 分钟试玩;重点反馈目标理解、操作手感、难度、bug 和还想不想再玩', - '- 测试者提醒:先自由玩一局;不要评价完成度;问卷走 /survey;客户端问题由自动诊断提示', - '- 交付搭配:/invite;/share;/screenshots;/trailer', - '- 边界:只准备试玩常见问答;不读取文件;不启动或打开预览;不导出试玩包;不写项目', - `- 建议:${draftCommand}`, - ].join('\n'), - draftCommand, - draftCommandLabel, - }; -} - -export function summarizeProjectCommunityPost( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const preview = nextManifest.preview; - const previewRunning = preview?.status === 'running' && preview.url; - const previewSummary = previewRunning - ? `运行中 ${preview.url}` - : preview - ? previewStatusLabels[preview.status] - : '未启动'; - const tracePassed = isAgentRunTracePassed(trace); - const blockedTrace = - trace?.lifecycleStatus === 'killed' || - trace?.status === 'failed' || - trace?.status === 'needs-revision' || - trace?.stopReason === 'max-passes-exhausted'; - const goal = - nextManifest.goal?.trim() || - trace?.goal?.trim() || - trace?.taskGraph.goal?.trim() || - '暂无'; - const visualAssetCount = - nextManifest.assets.filter(isProjectVisualAsset).length; - - let draftCommand = '/store'; - let draftCommandLabel = '准备上架'; - if (!trace) { - draftCommand = '/next'; - draftCommandLabel = '查看下一步'; - } else if (blockedTrace) { - draftCommand = '/review'; - draftCommandLabel = '查看评审'; - } else if (!tracePassed) { - draftCommand = '/trace'; - draftCommandLabel = '查看 trace'; - } else if (!previewRunning) { - draftCommand = '/run'; - draftCommandLabel = '启动试玩'; - } - - return { - text: [ - '社区发布文案:', - `- 项目:${nextManifest.name}`, - `- 一句话:${goal}`, - `- 当前状态:${ - tracePassed - ? `最近 run 已通过${trace?.runId ? ` ${trace.runId}` : ''}` - : trace - ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}` - : '暂无最近 run' - };预览 ${previewSummary}`, - `- 素材准备:视觉素材 ${visualAssetCount} 个;配图走 /screenshots;短视频走 /trailer`, - `- 短文案:我做了一个早期 Web 小游戏原型《${nextManifest.name}》,核心目标是${goal}。想找 3-5 位朋友试玩 5 分钟,重点看能不能理解目标、操作顺不顺、还想不想再来一局。`, - '- 长文案结构:一句玩法目标;一张截图或短视频;试玩方式;希望收到的三类反馈;已知限制', - '- 标签建议:#Web小游戏 #原型试玩 #AI游戏创作 #本地试玩', - '- CTA:愿意试玩请回复;遇到问题按 /faq 的口径反馈,客户端问题会自动提示', - '- 参考:/faq;/screenshots;/trailer;/store;/share', - '- 边界:只准备社区发布文案;不上传云端;不发布作品;不读取文件;不启动或打开预览;不导出试玩包;不写项目', - `- 建议:${draftCommand}`, - ].join('\n'), - draftCommand, - draftCommandLabel, - }; -} - -export function summarizeProjectStoreChecklist( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const preview = nextManifest.preview; - const previewRunning = preview?.status === 'running' && preview.url; - const previewSummary = previewRunning - ? `运行中 ${preview.url}` - : preview - ? previewStatusLabels[preview.status] - : '未启动'; - const tracePassed = isAgentRunTracePassed(trace); - const blockedTrace = - trace?.lifecycleStatus === 'killed' || - trace?.status === 'failed' || - trace?.status === 'needs-revision' || - trace?.stopReason === 'max-passes-exhausted'; - const goal = - nextManifest.goal?.trim() || - trace?.goal?.trim() || - trace?.taskGraph.goal?.trim() || - '暂无'; - const visualAssetCount = - nextManifest.assets.filter(isProjectVisualAsset).length; - const audioAssetCount = - nextManifest.assets.filter(isProjectAudioAsset).length; - const hasPublishReadme = - trace?.artifacts.some( - (artifact) => artifact.path === 'exports/README.md', - ) ?? false; - - let draftCommand = '/listing'; - let draftCommandLabel = '准备作品页'; - if (!trace) { - draftCommand = '/next'; - draftCommandLabel = '查看下一步'; - } else if (blockedTrace) { - draftCommand = '/review'; - draftCommandLabel = '查看评审'; - } else if (!tracePassed) { - draftCommand = '/trace'; - draftCommandLabel = '查看 trace'; - } else if (!previewRunning) { - draftCommand = '/run'; - draftCommandLabel = '启动试玩'; - } else if (hasPublishReadme) { - draftCommand = '/read exports/README.md'; - draftCommandLabel = '读发布说明'; - } - - return { - text: [ - '上架资料:', - `- 项目:${nextManifest.name}`, - `- 一句话:${goal}`, - `- 当前状态:${ - tracePassed - ? `最近 run 已通过${trace?.runId ? ` ${trace.runId}` : ''}` - : trace - ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}` - : '暂无最近 run' - };预览 ${previewSummary}`, - `- 资产概况:视觉 ${visualAssetCount} 个;音频 ${audioAssetCount} 个;总资产 ${nextManifest.assets.length} 个`, - `- 必备资料:作品页文案 /listing;宣传截图 /screenshots;素材署名 /credits;隐私边界 /privacy;试玩包 /export`, - `- 发布说明:${hasPublishReadme ? 'exports/README.md · 已生成' : '待生成 · 先看 /publish 或 /listing'}`, - '- 首发范围:本地 Web 原型;小规模试玩;免费体验;不承诺账号、云存档、排行榜或付费', - '- 上架前检查:30 秒玩法可懂;首屏不空白;重开清楚;截图不含内部路径;素材来源可说明', - '- 参考:/publish;/listing;/screenshots;/credits;/privacy;/share', - '- 边界:只准备上架资料清单;不上传云端;不发布作品;不读取文件;不启动或打开预览;不导出试玩包;不写项目', - `- 建议:${draftCommand}`, - ].join('\n'), - draftCommand, - draftCommandLabel, - }; -} - -export function summarizeProjectMediaKit( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const preview = nextManifest.preview; - const previewRunning = preview?.status === 'running' && preview.url; - const previewSummary = previewRunning - ? `运行中 ${preview.url}` - : preview - ? previewStatusLabels[preview.status] - : '未启动'; - const tracePassed = isAgentRunTracePassed(trace); - const blockedTrace = - trace?.lifecycleStatus === 'killed' || - trace?.status === 'failed' || - trace?.status === 'needs-revision' || - trace?.stopReason === 'max-passes-exhausted'; - const goal = - nextManifest.goal?.trim() || - trace?.goal?.trim() || - trace?.taskGraph.goal?.trim() || - '暂无'; - const visualAssetCount = - nextManifest.assets.filter(isProjectVisualAsset).length; - const audioAssetCount = - nextManifest.assets.filter(isProjectAudioAsset).length; - const hasPublishReadme = - trace?.artifacts.some( - (artifact) => artifact.path === 'exports/README.md', - ) ?? false; - - let draftCommand = '/screenshots'; - let draftCommandLabel = '准备截图'; - if (!trace) { - draftCommand = '/next'; - draftCommandLabel = '查看下一步'; - } else if (blockedTrace) { - draftCommand = '/review'; - draftCommandLabel = '查看评审'; - } else if (!tracePassed) { - draftCommand = '/trace'; - draftCommandLabel = '查看 trace'; - } else if (!previewRunning) { - draftCommand = '/run'; - draftCommandLabel = '启动试玩'; - } else if (hasPublishReadme) { - draftCommand = '/read exports/README.md'; - draftCommandLabel = '读发布说明'; - } - - return { - text: [ - '媒体资料包:', - `- 项目:${nextManifest.name}`, - `- 一句话:${goal}`, - `- 当前状态:${ - tracePassed - ? `最近 run 已通过${trace?.runId ? ` ${trace.runId}` : ''}` - : trace - ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}` - : '暂无最近 run' - };预览 ${previewSummary}`, - `- 素材概况:视觉素材 ${visualAssetCount} 个;音频素材 ${audioAssetCount} 个;总资产 ${nextManifest.assets.length} 个;发布说明 ${hasPublishReadme ? 'exports/README.md · 已生成' : '待生成'}`, - '- 资料清单:作品页 /listing;宣传截图 /screenshots;短视频 /trailer;FAQ /faq;社区文案 /post;上架清单 /store', - '- 缺口优先级:先跑 /run 确认可试玩;再补 /screenshots 和 /trailer;最后整理 /post 与 /store', - '- 打包顺序:1. 确认首屏和核心玩法;2. 准备截图 / 视频 / FAQ;3. 汇总署名、隐私和发布说明', - '- 参考:/screenshots;/trailer;/listing;/faq;/post;/store;/share', - '- 边界:只准备媒体资料包清单;不截屏;不录屏;不读取文件;不启动或打开预览;不导出试玩包;不上传云端;不发布作品;不写项目', - `- 建议:${draftCommand}`, - ].join('\n'), - draftCommand, - draftCommandLabel, - }; -} diff --git a/apps/ai-game-creator-shell/src/features/project-summary/projectQualitySummaries.ts b/apps/ai-game-creator-shell/src/features/project-summary/projectQualitySummaries.ts deleted file mode 100644 index d95a6428f..000000000 --- a/apps/ai-game-creator-shell/src/features/project-summary/projectQualitySummaries.ts +++ /dev/null @@ -1,1078 +0,0 @@ -import { - type GameCreationAgentRunTrace, - type GameCreationAppManifest, - type GameCreationAppTaskState, - selectGameCreationAppReadyTasks, -} from '../../../../../packages/shared/src/contracts/gameCreationApp'; -import { taskRowsFromManifest } from '../agent-runtime'; -import { - formatAgentRunStatus, - isAgentRunTracePassed, - readableArtifactsFromAgentRunTrace, -} from './agentTrace'; -import { - isProjectAudioAsset, - isProjectVisualAsset, -} from './projectAssetSummaries'; -import { - previewStatusLabels, - taskGroupLabels, - taskStatusLabels, -} from './projectSummaryConstants'; - -export function summarizeProjectTutorialGuide( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const preview = nextManifest.preview; - const previewRunning = preview?.status === 'running' && preview.url; - const previewSummary = previewRunning - ? `运行中 ${preview.url}` - : preview - ? previewStatusLabels[preview.status] - : '未启动'; - const tracePassed = isAgentRunTracePassed(trace); - const goal = - nextManifest.goal?.trim() || - trace?.goal?.trim() || - trace?.taskGraph.goal?.trim() || - '暂无'; - const manifestTasks = taskRowsFromManifest(nextManifest); - const traceTasks = trace?.taskGraph.tasks ?? []; - const tasks = manifestTasks.map( - (task) => traceTasks.find((traceTask) => traceTask.id === task.id) ?? task, - ); - const playtestTask = - tasks.find((task) => task.id === 'preview-playtest') ?? null; - const hasGameArtifact = - trace?.artifacts.some( - (artifact) => - artifact.path === 'game/index.html' || artifact.path === 'game/', - ) ?? false; - const latestTutorialStep = - trace?.steps - .filter( - (step) => - step.taskId === 'design-foundation' || - step.taskId === 'preview-readiness' || - step.taskId === 'preview-playtest' || - step.phase === 'generate' || - step.phase === 'playtest' || - step.toolCalls.some( - (toolCall) => - toolCall.toolId === 'game.static_smoke' || - toolCall.toolId.startsWith('preview.'), - ), - ) - .slice(-1)[0] ?? null; - - let draftCommand = '/rules'; - let draftCommandLabel = '查看玩法规则'; - if (trace && !tracePassed) { - draftCommand = '/review'; - draftCommandLabel = '查看评审'; - } else if (tracePassed && !hasGameArtifact) { - draftCommand = - '/agent-resume 新手引导:在首屏加入目标、操作、反馈、失败重开提示'; - draftCommandLabel = '补充新手引导'; - } else if (previewRunning) { - draftCommand = '/open-preview'; - draftCommandLabel = '打开预览'; - } else if (tracePassed) { - draftCommand = '/run'; - draftCommandLabel = '启动预览'; - } - - return { - text: [ - '新手引导:', - `- 项目:${nextManifest.name}`, - `- 首屏目标:${goal}`, - '- 首局 30 秒:看到目标;尝试操作;收到反馈;理解失败/胜利;能重开', - `- 当前证据:原型入口 ${hasGameArtifact ? '已生成' : '未见 trace 产物'};${ - tracePassed - ? `最近 run 已通过${trace?.runId ? ` ${trace.runId}` : ''}` - : trace - ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}` - : '暂无最近 run' - };预览 ${previewSummary}`, - `- 试玩任务:${ - playtestTask - ? `${taskGroupLabels[playtestTask.group]} / ${playtestTask.role} ${playtestTask.title} · ${taskStatusLabels[playtestTask.status]}` - : '暂无' - }`, - `- 最近引导证据:${ - latestTutorialStep - ? `${latestTutorialStep.agent} #${latestTutorialStep.pass} · ${latestTutorialStep.status} · ${latestTutorialStep.summary}` - : '暂无' - }`, - '- 需要补齐:首屏目标提示;操作提示;碰撞/得分反馈;失败或胜利提示;重开按钮', - '- 参考:/rules;/playtest;/feedback;/pitch', - `- 建议:${draftCommand}`, - ].join('\n'), - draftCommand, - draftCommandLabel, - }; -} - -export function summarizeProjectMobilePlaytestGuide( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const preview = nextManifest.preview; - const previewRunning = preview?.status === 'running' && preview.url; - const previewSummary = previewRunning - ? `运行中 ${preview.url}` - : preview - ? previewStatusLabels[preview.status] - : '未启动'; - const tracePassed = isAgentRunTracePassed(trace); - const goal = - nextManifest.goal?.trim() || - trace?.goal?.trim() || - trace?.taskGraph.goal?.trim() || - '暂无'; - const manifestTasks = taskRowsFromManifest(nextManifest); - const traceTasks = trace?.taskGraph.tasks ?? []; - const tasks = manifestTasks.map( - (task) => traceTasks.find((traceTask) => traceTask.id === task.id) ?? task, - ); - const relevantTasks = ['code-prototype', 'preview-playtest'] - .map((taskId) => tasks.find((task) => task.id === taskId)) - .filter((task): task is NonNullable => Boolean(task)); - const taskLines = relevantTasks.map( - (task) => - `- ${task.id}:${taskGroupLabels[task.group]} / ${task.role} ${task.title} · ${taskStatusLabels[task.status]}`, - ); - const hasGameArtifact = - trace?.artifacts.some( - (artifact) => - artifact.path === 'game/index.html' || artifact.path === 'game/', - ) ?? false; - const latestMobileStep = - trace?.steps - .filter( - (step) => - step.taskId === 'code-prototype' || - step.taskId === 'preview-readiness' || - step.taskId === 'preview-playtest' || - step.group === 'code' || - step.phase === 'generate' || - step.phase === 'playtest' || - step.toolCalls.some( - (toolCall) => - toolCall.toolId === 'game.static_smoke' || - toolCall.toolId.startsWith('preview.'), - ), - ) - .slice(-1)[0] ?? null; - - let draftCommand = '/rules'; - let draftCommandLabel = '查看玩法规则'; - if (!trace) { - draftCommand = '/next'; - draftCommandLabel = '查看下一步'; - } else if (!tracePassed) { - draftCommand = '/review'; - draftCommandLabel = '查看评审'; - } else if (tracePassed && !hasGameArtifact) { - draftCommand = - '/agent-resume 移动试玩:补充触屏操作、响应式画布、横竖屏提示、重开按钮'; - draftCommandLabel = '补充移动试玩'; - } else if (previewRunning) { - draftCommand = '/open-preview'; - draftCommandLabel = '打开预览'; - } else { - draftCommand = '/run'; - draftCommandLabel = '启动预览'; - } - - return { - text: [ - '移动试玩:', - `- 项目:${nextManifest.name}`, - `- 目标:${goal}`, - '- 输入方式:键盘 / 触屏都应能完成核心循环', - `- 当前证据:原型入口 ${hasGameArtifact ? '已生成' : '未见 trace 产物'};${ - tracePassed - ? `最近 run 已通过${trace?.runId ? ` ${trace.runId}` : ''}` - : trace - ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}` - : '暂无最近 run' - };预览 ${previewSummary}`, - '- 移动检查:触屏操作;响应式画布;横竖屏提示;按钮尺寸;失败/胜利重开', - taskLines.length > 0 - ? `- 关联任务:\n${taskLines.join('\n')}` - : '- 关联任务:暂无', - `- 最近移动相关步骤:${ - latestMobileStep - ? `${latestMobileStep.agent} #${latestMobileStep.pass} · ${latestMobileStep.status} · ${latestMobileStep.summary}` - : '暂无' - }`, - '- 参考:/rules;/tutorial;/playtest;/feedback', - `- 建议:${draftCommand}`, - ].join('\n'), - draftCommand, - draftCommandLabel, - }; -} - -export function summarizeProjectCompatibilityNotes( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const preview = nextManifest.preview; - const previewRunning = preview?.status === 'running' && preview.url; - const previewSummary = previewRunning - ? `运行中 ${preview.url}` - : preview - ? previewStatusLabels[preview.status] - : '未启动'; - const tracePassed = isAgentRunTracePassed(trace); - const blockedTrace = - trace?.lifecycleStatus === 'killed' || - trace?.status === 'failed' || - trace?.status === 'needs-revision' || - trace?.stopReason === 'max-passes-exhausted'; - const goal = - nextManifest.goal?.trim() || - trace?.goal?.trim() || - trace?.taskGraph.goal?.trim() || - '暂无'; - const commandRuns = nextManifest.commandRuns ?? []; - const staticSmokePassed = - commandRuns.some( - (commandRun) => - commandRun.commandId === 'game.static_smoke' && - commandRun.status === 'completed', - ) || - Boolean( - trace?.steps.some((step) => - step.toolCalls.some( - (toolCall) => - toolCall.toolId === 'game.static_smoke' && toolCall.status === 'ok', - ), - ), - ); - const inputSummary = tracePassed - ? '键盘优先;触屏按 /mobile 复查' - : '待原型通过后复查键盘 / 触屏'; - - let draftCommand = '/mobile'; - let draftCommandLabel = '查看移动试玩'; - if (!trace) { - draftCommand = '/next'; - draftCommandLabel = '查看下一步'; - } else if (blockedTrace) { - draftCommand = '/review'; - draftCommandLabel = '查看评审'; - } else if (!tracePassed) { - draftCommand = '/trace'; - draftCommandLabel = '查看 trace'; - } else if (!previewRunning) { - draftCommand = '/run'; - draftCommandLabel = '启动试玩'; - } - - return { - text: [ - '兼容性说明:', - `- 项目:${nextManifest.name}`, - `- 目标:${goal}`, - `- 当前状态:${ - tracePassed - ? `最近 run 已通过${trace?.runId ? ` ${trace.runId}` : ''}` - : trace - ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}` - : '暂无最近 run' - };预览 ${previewSummary}`, - `- 自检:${staticSmokePassed ? 'game.static_smoke 已通过' : '未见静态自检通过'}`, - `- 输入兼容:${inputSummary}`, - '- 推荐环境:桌面 Chrome / Edge 最新版;本机 127.0.0.1 预览;移动浏览器只做早期体验', - '- 不承诺:旧浏览器、低端设备、离线模式、云存档、账号同步、手柄或多端数据一致', - '- 反馈口径:设备 / 浏览器 / 输入方式 / 截图或录屏;客户端检测到问题时会自动提示', - '- 参考:/mobile;/accessibility;/performance;/known-issues', - '- 边界:只准备兼容性说明;不读取文件;不启动或打开预览;不导出试玩包;不上传云端;不发布作品;不写项目', - `- 建议:${draftCommand}`, - ].join('\n'), - draftCommand, - draftCommandLabel, - }; -} - -export function summarizeProjectAccessibilityGuide( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const preview = nextManifest.preview; - const previewRunning = preview?.status === 'running' && preview.url; - const previewSummary = previewRunning - ? `运行中 ${preview.url}` - : preview - ? previewStatusLabels[preview.status] - : '未启动'; - const tracePassed = isAgentRunTracePassed(trace); - const goal = - nextManifest.goal?.trim() || - trace?.goal?.trim() || - trace?.taskGraph.goal?.trim() || - '暂无'; - const manifestTasks = taskRowsFromManifest(nextManifest); - const traceTasks = trace?.taskGraph.tasks ?? []; - const tasks = manifestTasks.map( - (task) => traceTasks.find((traceTask) => traceTask.id === task.id) ?? task, - ); - const relevantTasks = [ - 'code-prototype', - 'quality-review', - 'preview-readiness', - 'preview-playtest', - ] - .map((taskId) => tasks.find((task) => task.id === taskId)) - .filter((task): task is NonNullable => Boolean(task)); - const taskLines = relevantTasks.map( - (task) => - `- ${task.id}:${taskGroupLabels[task.group]} / ${task.role} ${task.title} · ${taskStatusLabels[task.status]}`, - ); - const hasGameArtifact = - trace?.artifacts.some( - (artifact) => - artifact.path === 'game/index.html' || artifact.path === 'game/', - ) ?? false; - const latestAccessibilityStep = - trace?.steps - .filter( - (step) => - step.taskId === 'code-prototype' || - step.taskId === 'quality-review' || - step.taskId === 'preview-readiness' || - step.taskId === 'preview-playtest' || - step.group === 'code' || - step.phase === 'evaluation' || - step.phase === 'playtest' || - step.toolCalls.some( - (toolCall) => - toolCall.toolId === 'game.static_smoke' || - toolCall.toolId === 'agent.evaluate' || - toolCall.toolId.startsWith('preview.'), - ), - ) - .slice(-1)[0] ?? null; - - let draftCommand = '/rules'; - let draftCommandLabel = '查看玩法规则'; - if (!trace) { - draftCommand = '/next'; - draftCommandLabel = '查看下一步'; - } else if (!tracePassed) { - draftCommand = '/review'; - draftCommandLabel = '查看评审'; - } else if (tracePassed && !hasGameArtifact) { - draftCommand = - '/agent-resume 可读性与无障碍:补充文字对比、清晰按钮标签、键盘等价操作、非颜色唯一反馈、静音可玩'; - draftCommandLabel = '补充无障碍'; - } else if (previewRunning) { - draftCommand = '/open-preview'; - draftCommandLabel = '打开预览'; - } else { - draftCommand = '/run'; - draftCommandLabel = '启动预览'; - } - - return { - text: [ - '可读性与无障碍:', - `- 项目:${nextManifest.name}`, - `- 目标:${goal}`, - '- 检查范围:文字可读;颜色对比;按钮/状态命名;键盘等价操作;可见焦点;非颜色唯一反馈;静音可玩', - `- 当前证据:原型入口 ${hasGameArtifact ? '已生成' : '未见 trace 产物'};${ - tracePassed - ? `最近 run 已通过${trace?.runId ? ` ${trace.runId}` : ''}` - : trace - ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}` - : '暂无最近 run' - };预览 ${previewSummary}`, - '- 补齐项:文字对比;清晰按钮标签;键盘等价操作;非颜色唯一反馈;静音可玩', - taskLines.length > 0 - ? `- 关联任务:\n${taskLines.join('\n')}` - : '- 关联任务:暂无', - `- 最近无障碍相关步骤:${ - latestAccessibilityStep - ? `${latestAccessibilityStep.agent} #${latestAccessibilityStep.pass} · ${latestAccessibilityStep.status} · ${latestAccessibilityStep.summary}` - : '暂无' - }`, - '- 参考:/rules;/mobile;/tutorial;/qa;/playtest', - `- 建议:${draftCommand}`, - ].join('\n'), - draftCommand, - draftCommandLabel, - }; -} - -export function summarizeProjectLocalizationChecklist( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const preview = nextManifest.preview; - const previewRunning = preview?.status === 'running' && preview.url; - const previewSummary = previewRunning - ? `运行中 ${preview.url}` - : preview - ? previewStatusLabels[preview.status] - : '未启动'; - const tracePassed = isAgentRunTracePassed(trace); - const goal = - nextManifest.goal?.trim() || - trace?.goal?.trim() || - trace?.taskGraph.goal?.trim() || - '暂无'; - const manifestTasks = taskRowsFromManifest(nextManifest); - const traceTasks = trace?.taskGraph.tasks ?? []; - const tasks = manifestTasks.map( - (task) => traceTasks.find((traceTask) => traceTask.id === task.id) ?? task, - ); - const relevantTasks = [ - 'design-foundation', - 'code-prototype', - 'quality-review', - 'publish-package', - ] - .map((taskId) => tasks.find((task) => task.id === taskId)) - .filter((task): task is NonNullable => Boolean(task)); - const taskLines = relevantTasks.map( - (task) => - `- ${task.id}:${taskGroupLabels[task.group]} / ${task.role} ${task.title} · ${taskStatusLabels[task.status]}`, - ); - const hasPublishArtifact = - trace?.artifacts.some( - (artifact) => artifact.path === 'exports/README.md', - ) ?? false; - const latestCopyStep = - trace?.steps - .filter( - (step) => - step.group === 'design' || - step.group === 'code' || - step.group === 'publishing' || - step.phase === 'evaluation' || - step.taskId === 'design-foundation' || - step.taskId === 'quality-review' || - step.taskId === 'publish-package', - ) - .slice(-1)[0] ?? null; - - let draftCommand = '/next'; - let draftCommandLabel = '查看下一步'; - if (trace && !tracePassed) { - draftCommand = '/review'; - draftCommandLabel = '查看评审'; - } else if (hasPublishArtifact) { - draftCommand = '/read exports/README.md'; - draftCommandLabel = '读发布说明'; - } else if (tracePassed) { - draftCommand = - '/agent-resume 本地化与文案:统一标题、按钮、状态提示、失败胜利文案、发布简介'; - draftCommandLabel = '补充文案'; - } - - return { - text: [ - '本地化与文案:', - `- 项目:${nextManifest.name}`, - `- 目标:${goal}`, - '- 默认语言:简体中文;首版不承诺多语言', - '- 文案范围:标题;目标提示;操作按钮;状态反馈;失败/胜利;重开;发布简介', - `- 当前证据:${ - tracePassed - ? `最近 run 已通过${trace?.runId ? ` ${trace.runId}` : ''}` - : trace - ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}` - : '暂无最近 run' - };预览 ${previewSummary};发布说明 ${ - hasPublishArtifact ? '已生成' : '未见 trace 产物' - }`, - taskLines.length > 0 - ? `- 关联任务:\n${taskLines.join('\n')}` - : '- 关联任务:暂无', - '- 检查口径:短句优先;动词一致;玩家术语统一;错误提示可复现;UI 文案避免开发解释', - '- 暂不做:英日韩等多语言包;自动翻译;地区化素材;语音本地化;商店长文案 A/B', - `- 最近文案相关步骤:${ - latestCopyStep - ? `${latestCopyStep.agent} #${latestCopyStep.pass} · ${latestCopyStep.status} · ${latestCopyStep.summary}` - : '暂无' - }`, - '- 参考:/rules;/tutorial;/listing;/faq;/known-issues', - '- 边界:只整理本地化与文案检查;不读取文件;不启动或打开预览;不导出试玩包;不上传云端;不发布作品;不写项目', - `- 建议:${draftCommand}`, - ].join('\n'), - draftCommand, - draftCommandLabel, - }; -} - -export function summarizeProjectPerformanceCheck( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const preview = nextManifest.preview; - const previewRunning = preview?.status === 'running' && preview.url; - const previewSummary = previewRunning - ? `运行中 ${preview.url}` - : preview - ? previewStatusLabels[preview.status] - : '未启动'; - const tracePassed = isAgentRunTracePassed(trace); - const goal = - nextManifest.goal?.trim() || - trace?.goal?.trim() || - trace?.taskGraph.goal?.trim() || - '暂无'; - const artifacts = trace ? readableArtifactsFromAgentRunTrace(trace) : []; - const totalArtifactBytes = artifacts.reduce( - (total, artifact) => total + artifact.sizeBytes, - 0, - ); - const visibleArtifacts = artifacts.slice(0, 6); - const artifactLines = visibleArtifacts.map( - (artifact) => - `- ${artifact.path} · ${artifact.sizeBytes}B · ${artifact.checksum}`, - ); - if (artifacts.length > visibleArtifacts.length) { - artifactLines.push( - `- 还有 ${artifacts.length - visibleArtifacts.length} 个产物`, - ); - } - const manifestTasks = taskRowsFromManifest(nextManifest); - const traceTasks = trace?.taskGraph.tasks ?? []; - const tasks = manifestTasks.map( - (task) => traceTasks.find((traceTask) => traceTask.id === task.id) ?? task, - ); - const relevantTasks = [ - 'code-prototype', - 'preview-readiness', - 'preview-playtest', - ] - .map((taskId) => tasks.find((task) => task.id === taskId)) - .filter((task): task is NonNullable => Boolean(task)); - const taskLines = relevantTasks.map( - (task) => - `- ${task.id}:${taskGroupLabels[task.group]} / ${task.role} ${task.title} · ${taskStatusLabels[task.status]}`, - ); - const hasGameArtifact = - trace?.artifacts.some( - (artifact) => - artifact.path === 'game/index.html' || artifact.path === 'game/', - ) ?? false; - const latestPerformanceStep = - trace?.steps - .filter( - (step) => - step.taskId === 'code-prototype' || - step.taskId === 'preview-readiness' || - step.taskId === 'preview-playtest' || - step.group === 'code' || - step.phase === 'generate' || - step.phase === 'playtest' || - step.toolCalls.some( - (toolCall) => - toolCall.toolId === 'game.static_smoke' || - toolCall.toolId.startsWith('preview.'), - ), - ) - .slice(-1)[0] ?? null; - - let draftCommand = '/next'; - let draftCommandLabel = '查看下一步'; - if (trace && !tracePassed) { - draftCommand = '/review'; - draftCommandLabel = '查看评审'; - } else if (artifacts.length > 0) { - draftCommand = '/run-artifacts'; - draftCommandLabel = '列出 Run 产物'; - } else if (previewRunning) { - draftCommand = '/open-preview'; - draftCommandLabel = '打开预览'; - } else if (tracePassed) { - draftCommand = '/run'; - draftCommandLabel = '启动预览'; - } - - return { - text: [ - '性能与加载:', - `- 项目:${nextManifest.name}`, - `- 目标:${goal}`, - `- 当前证据:${ - tracePassed - ? `最近 run 已通过${trace?.runId ? ` ${trace.runId}` : ''}` - : trace - ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}` - : '暂无最近 run' - };预览 ${previewSummary};入口 ${hasGameArtifact ? '已生成' : '未见 trace 产物'};产物 ${artifacts.length} 个 / ${totalArtifactBytes}B;资产 ${nextManifest.assets.length} 个`, - '- 检查范围:入口 HTML 自包含;首屏不空白;素材体积;主循环稳定;无远程依赖;预览启动', - artifactLines.length > 0 - ? `- 关键产物:\n${artifactLines.join('\n')}` - : '- 关键产物:暂无', - taskLines.length > 0 - ? `- 关联任务:\n${taskLines.join('\n')}` - : '- 关联任务:暂无', - `- 最近性能相关步骤:${ - latestPerformanceStep - ? `${latestPerformanceStep.agent} #${latestPerformanceStep.pass} · ${latestPerformanceStep.status} · ${latestPerformanceStep.summary}` - : '暂无' - }`, - '- 参考:/run-artifacts;/playtest;/qa;/export', - `- 建议:${draftCommand}`, - ].join('\n'), - draftCommand, - draftCommandLabel, - }; -} - -export function summarizeProjectPolishChecklist( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const preview = nextManifest.preview; - const previewRunning = preview?.status === 'running' && preview.url; - const previewSummary = previewRunning - ? `运行中 ${preview.url}` - : preview - ? previewStatusLabels[preview.status] - : '未启动'; - const tracePassed = isAgentRunTracePassed(trace); - const goal = - nextManifest.goal?.trim() || - trace?.goal?.trim() || - trace?.taskGraph.goal?.trim() || - '暂无'; - const visualAssetCount = - nextManifest.assets.filter(isProjectVisualAsset).length; - const audioAssetCount = - nextManifest.assets.filter(isProjectAudioAsset).length; - const latestSmokeRun = - [...(nextManifest.commandRuns ?? [])] - .reverse() - .find((run) => run.commandId === 'game.static_smoke') ?? null; - const smokeSummary = latestSmokeRun - ? latestSmokeRun.status === 'completed' - ? '已通过' - : '失败' - : '暂无'; - const manifestTasks = taskRowsFromManifest(nextManifest); - const traceTasks = trace?.taskGraph.tasks ?? []; - const tasks = manifestTasks.map( - (task) => traceTasks.find((traceTask) => traceTask.id === task.id) ?? task, - ); - const polishTaskIds = [ - 'art-polish', - 'audio-asset-plan', - 'code-prototype', - 'quality-review', - 'preview-readiness', - 'preview-playtest', - 'publish-package', - ]; - const taskLines = polishTaskIds - .map((taskId) => tasks.find((task) => task.id === taskId)) - .filter((task): task is NonNullable => Boolean(task)) - .map( - (task) => - `- ${task.id}:${taskGroupLabels[task.group]} / ${task.role} ${task.title} · ${taskStatusLabels[task.status]}`, - ); - const latestPolishStep = - trace?.steps - .filter( - (step) => - (step.taskId && polishTaskIds.includes(step.taskId)) || - step.phase === 'evaluation' || - step.phase === 'playtest' || - step.group === 'art' || - step.group === 'code' || - step.group === 'publishing' || - step.toolCalls.some( - (toolCall) => - toolCall.toolId === 'agent.evaluate' || - toolCall.toolId === 'game.static_smoke' || - toolCall.toolId.startsWith('preview.'), - ), - ) - .slice(-1)[0] ?? null; - - let draftCommand = - '/agent-resume 打磨:补齐新手引导、触屏操作、可读性、性能、素材署名和试玩反馈'; - let draftCommandLabel = '补充打磨'; - if (!trace) { - draftCommand = '/next'; - draftCommandLabel = '查看下一步'; - } else if (!tracePassed) { - draftCommand = '/review'; - draftCommandLabel = '查看评审'; - } else if (previewRunning) { - draftCommand = '/feedback'; - draftCommandLabel = '准备反馈'; - } - - return { - text: [ - '试玩前打磨:', - `- 项目:${nextManifest.name}`, - `- 目标:${goal}`, - `- 当前证据:${ - tracePassed - ? `最近 run 已通过${trace?.runId ? ` ${trace.runId}` : ''}` - : trace - ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}` - : '暂无最近 run' - };预览 ${previewSummary};自检 ${smokeSummary};资产 ${ - nextManifest.assets.length - } 个(美术 ${visualAssetCount} / 音频 ${audioAssetCount})`, - '- 打磨范围:新手引导;移动试玩;可读性与无障碍;性能与加载;美术 / 音频素材;试玩反馈', - '- 推荐顺序:/tutorial -> /mobile -> /accessibility -> /performance -> /credits -> /feedback', - taskLines.length > 0 - ? `- 关联任务:\n${taskLines.join('\n')}` - : '- 关联任务:暂无', - `- 最近打磨相关步骤:${ - latestPolishStep - ? `${latestPolishStep.agent} #${latestPolishStep.pass} · ${latestPolishStep.status} · ${latestPolishStep.summary}` - : '暂无' - }`, - '- 边界:只整理试玩前打磨清单;不读取文件;不启动预览;不导出试玩包;不写项目', - `- 建议:${draftCommand}`, - ].join('\n'), - draftCommand, - draftCommandLabel, - }; -} - -export function summarizeProjectRisks( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const risks: Array<{ text: string; command?: string }> = []; - const addRisk = (text: string, command?: string) => { - if (command && risks.some((risk) => risk.command === command)) { - return; - } - risks.push({ text, command }); - }; - - const tasks = taskRowsFromManifest(nextManifest); - const failedTasks = tasks.filter((task) => task.status === 'failed'); - const readyTasks = selectGameCreationAppReadyTasks({ tasks }); - const commandRuns = nextManifest.commandRuns ?? []; - const latestCommandRun = commandRuns[commandRuns.length - 1]; - const preview = nextManifest.preview; - const tracePassed = - trace?.status === 'passed' || - trace?.status === 'artifacts-written' || - trace?.stopReason === 'evaluator-passed'; - const hasCanvasImageAsset = nextManifest.assets.some( - (asset) => - asset.source.kind === 'canvas' && - (asset.mediaType.startsWith('image/') || - asset.mediaType === 'application/vnd.genarrative.image-sequence'), - ); - - if (!trace) { - addRisk('暂无最近 Agent run,当前项目还缺少生成闭环证据。', '/next'); - } else if ( - trace.lifecycleStatus === 'killed' || - trace.status === 'failed' || - trace.stopReason === 'max-passes-exhausted' - ) { - addRisk( - `最近 run 未完成:${trace.status} / ${trace.stopReason}。`, - '/agent-resume ', - ); - } else if (!tracePassed) { - addRisk( - `最近 run 尚未通过 Evaluator:${trace.status} / ${trace.stopReason}。`, - '/trace', - ); - } - - if (failedTasks.length > 0) { - addRisk(`有 ${failedTasks.length} 个任务处于失败状态。`, '/tasks'); - } - - if (latestCommandRun?.status === 'failed') { - addRisk(`最近命令 ${latestCommandRun.commandId} 失败。`, '/logs'); - } - - if (tracePassed && !(preview?.status === 'running' && preview.url)) { - addRisk('最近 run 已通过,但当前本地预览未运行。', '/run'); - } - - if (readyTasks.length > 0) { - addRisk(`还有 ${readyTasks.length} 个 ready 任务等待处理。`, '/tasks'); - } - - if (nextManifest.assets.length === 0) { - addRisk( - '暂无本地资产,首版原型可能缺少可复用素材。', - '/asset-register assets/hero.png image image/png', - ); - } else if (!hasCanvasImageAsset) { - addRisk( - '暂无画板来源图片资产,美术组可能只能先使用占位素材。', - '/sync-canvas-project ', - ); - } - - const firstAction = risks.find((risk) => risk.command); - return { - text: - risks.length > 0 - ? `项目风险:\n${risks - .map( - (risk) => - `- ${risk.text}${risk.command ? ` 建议:${risk.command}` : ''}`, - ) - .join('\n')}` - : '项目风险:\n- 暂未发现需要立即处理的风险。', - draftCommand: firstAction?.command, - draftCommandLabel: firstAction ? '处理首个风险' : undefined, - }; -} - -export function summarizeProjectBlockers( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const blockers: Array<{ text: string; command?: string }> = []; - const addBlocker = (text: string, command?: string) => { - if (command && blockers.some((blocker) => blocker.command === command)) { - return; - } - blockers.push({ text, command }); - }; - - const manifestTasks = taskRowsFromManifest(nextManifest); - const traceTasks = trace?.taskGraph.tasks ?? []; - const taskSource = traceTasks.length > 0 ? traceTasks : manifestTasks; - const taskSourceById = new Map(taskSource.map((task) => [task.id, task])); - const failedTasks = taskSource.filter((task) => task.status === 'failed'); - const readyTasks = - trace?.taskGraph.readyTaskIds - .map((taskId) => taskSourceById.get(taskId)) - .filter((task): task is GameCreationAppTaskState => Boolean(task)) ?? - selectGameCreationAppReadyTasks({ tasks: manifestTasks }); - const commandRuns = nextManifest.commandRuns ?? []; - const latestCommandRun = commandRuns[commandRuns.length - 1] ?? null; - const latestExportCommand = - [...commandRuns] - .reverse() - .find( - (commandRun) => commandRun.commandId === 'project.export_package', - ) ?? null; - const preview = nextManifest.preview; - const previewRunning = preview?.status === 'running' && preview.url; - const tracePassed = isAgentRunTracePassed(trace); - const hasVisualAsset = nextManifest.assets.some(isProjectVisualAsset); - - if (!trace) { - addBlocker('暂无最近 Agent run,缺少可验原型证据。', '/next'); - } else if ( - trace.lifecycleStatus === 'killed' || - trace.status === 'failed' || - trace.status === 'needs-revision' || - trace.stopReason === 'max-passes-exhausted' - ) { - addBlocker( - `最近 run 阻塞:${trace.status} / ${trace.stopReason}。`, - '/review', - ); - } else if (!tracePassed) { - addBlocker( - `最近 run 尚未通过:${trace.status} / ${trace.stopReason}。`, - '/trace', - ); - } - - if (failedTasks.length > 0) { - const firstFailed = failedTasks[0]; - if (firstFailed) { - addBlocker( - `失败任务 ${failedTasks.length} 个:${firstFailed.title}(${firstFailed.id})。`, - '/tasks', - ); - } - } - - if (latestCommandRun?.status === 'failed') { - addBlocker(`最近命令失败:${latestCommandRun.commandId}。`, '/logs'); - } - - if (tracePassed && !previewRunning) { - addBlocker('原型已通过,但本地预览未运行。', '/run'); - } - - if (tracePassed && latestExportCommand?.status !== 'completed') { - addBlocker('原型已通过,但本地试玩包尚未导出。', '/export'); - } - - if (readyTasks.length > 0) { - const firstReady = readyTasks[0]; - if (firstReady) { - addBlocker( - `ready 任务 ${readyTasks.length} 个:${taskGroupLabels[firstReady.group]} / ${firstReady.role} ${firstReady.title}(${firstReady.id})。`, - '/todo', - ); - } - } - - if (!hasVisualAsset) { - addBlocker('暂无可用美术素材,首版试玩可能只能使用占位。', '/art'); - } - - const firstAction = blockers.find((blocker) => blocker.command); - const blockerLines = - blockers.length > 0 - ? blockers.map( - (blocker) => - `- ${blocker.text}${blocker.command ? ` 建议:${blocker.command}` : ''}`, - ) - : ['- 暂未发现会阻断 MVP 试玩的事项。']; - - return { - text: [ - '当前阻塞项:', - `- 项目:${nextManifest.name}`, - trace ? `- Run:${trace.runId} · ${formatAgentRunStatus(trace)}` : null, - ...blockerLines, - '- 边界:只整理阻塞项;不读取文件;不启动预览;不导出试玩包;不写项目', - `- 建议:${firstAction?.command ?? '/next'}`, - ] - .filter(Boolean) - .join('\n'), - draftCommand: firstAction?.command ?? '/next', - draftCommandLabel: firstAction ? '处理首个阻塞' : '查看下一步', - }; -} - -export function summarizeProjectPlaytestReadiness( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const manifestTasks = taskRowsFromManifest(nextManifest); - const traceTasks = trace?.taskGraph.tasks ?? []; - const taskSource = traceTasks.length > 0 ? traceTasks : manifestTasks; - const taskSourceById = new Map(taskSource.map((task) => [task.id, task])); - const failedTasks = taskSource.filter((task) => task.status === 'failed'); - const readyTasks = - trace?.taskGraph.readyTaskIds - .map((taskId) => taskSourceById.get(taskId)) - .filter((task): task is GameCreationAppTaskState => Boolean(task)) ?? - selectGameCreationAppReadyTasks({ tasks: manifestTasks }); - const commandRuns = nextManifest.commandRuns ?? []; - const latestStaticSmokeCommand = - [...commandRuns] - .reverse() - .find((commandRun) => commandRun.commandId === 'game.static_smoke') ?? - null; - const latestExportCommand = - [...commandRuns] - .reverse() - .find( - (commandRun) => commandRun.commandId === 'project.export_package', - ) ?? null; - const preview = nextManifest.preview; - const previewRunning = preview?.status === 'running' && preview.url; - const previewSummary = previewRunning - ? `运行中 ${preview.url}` - : preview - ? previewStatusLabels[preview.status] - : '未启动'; - const tracePassed = isAgentRunTracePassed(trace); - const blockedTrace = - trace?.lifecycleStatus === 'killed' || - trace?.status === 'failed' || - trace?.status === 'needs-revision' || - trace?.stopReason === 'max-passes-exhausted'; - const staticSmokePassed = - latestStaticSmokeCommand?.status === 'completed' || - Boolean( - trace?.steps.some((step) => - step.toolCalls.some( - (toolCall) => - toolCall.toolId === 'game.static_smoke' && toolCall.status === 'ok', - ), - ), - ); - const exportReady = latestExportCommand?.status === 'completed'; - const hasVisualAsset = nextManifest.assets.some(isProjectVisualAsset); - const hasAudioAsset = nextManifest.assets.some(isProjectAudioAsset); - - let draftCommand = '/share'; - let draftCommandLabel = '准备交付'; - if (!trace) { - draftCommand = '/next'; - draftCommandLabel = '查看下一步'; - } else if (blockedTrace) { - draftCommand = '/review'; - draftCommandLabel = '查看评审'; - } else if (!tracePassed) { - draftCommand = '/trace'; - draftCommandLabel = '查看 trace'; - } else if (failedTasks.length > 0) { - draftCommand = '/tasks'; - draftCommandLabel = '查看任务'; - } else if (!staticSmokePassed || !previewRunning) { - draftCommand = '/run'; - draftCommandLabel = '启动试玩'; - } else if (!exportReady) { - draftCommand = '/export'; - draftCommandLabel = '导出试玩包'; - } else if (readyTasks.length > 0) { - draftCommand = '/todo'; - draftCommandLabel = '查看小步清单'; - } else if (!hasVisualAsset) { - draftCommand = '/art'; - draftCommandLabel = '查看美术'; - } - - const verdict = - tracePassed && previewRunning && staticSmokePassed && exportReady - ? '可交给测试者' - : tracePassed - ? '接近可测,先补齐预览 / 自检 / 试玩包' - : trace - ? '暂不建议交付,先处理最近 run' - : '暂不建议交付,先生成可验原型'; - - return { - text: [ - '试玩就绪度:', - `- 项目:${nextManifest.name}`, - trace ? `- Run:${trace.runId} · ${formatAgentRunStatus(trace)}` : null, - `- 原型:${ - tracePassed - ? '最近 run 已通过' - : trace - ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}` - : '暂无最近 run' - }`, - `- 预览:${previewSummary}`, - `- 自检:${ - staticSmokePassed - ? '已通过' - : latestStaticSmokeCommand - ? `${latestStaticSmokeCommand.commandId} ${latestStaticSmokeCommand.status}` - : '暂无' - }`, - `- 试玩包:${exportReady ? '已导出' : '未导出'}`, - `- 任务:失败 ${failedTasks.length} / ready ${readyTasks.length}`, - `- 素材:美术 ${hasVisualAsset ? '已有' : '缺少'} / 音频 ${ - hasAudioAsset ? '已有' : '可后补' - }`, - `- 结论:${verdict}`, - '- 边界:只判断就绪度;不读取文件;不启动预览;不导出试玩包;不写项目', - `- 建议:${draftCommand}`, - ] - .filter(Boolean) - .join('\n'), - draftCommand, - draftCommandLabel, - }; -} diff --git a/apps/ai-game-creator-shell/src/features/project-summary/projectReadinessSummaries.ts b/apps/ai-game-creator-shell/src/features/project-summary/projectReadinessSummaries.ts deleted file mode 100644 index 189a705f0..000000000 --- a/apps/ai-game-creator-shell/src/features/project-summary/projectReadinessSummaries.ts +++ /dev/null @@ -1,811 +0,0 @@ -import { - type GameCreationAgentRunTrace, - type GameCreationAppAssetSourceKind, - type GameCreationAppManifest, - selectGameCreationAppReadyTasks, -} from '../../../../../packages/shared/src/contracts/gameCreationApp'; -import { taskRowsFromManifest } from '../agent-runtime'; -import { - formatAgentRunStatus, - isAgentReviewStep, - isAgentRunTracePassed, - isPlaytestTraceStep, - readableArtifactsFromAgentRunTrace, -} from './agentTrace'; -import { - isProjectAudioAsset, - isProjectVisualAsset, -} from './projectAssetSummaries'; -import { isSafeProjectRelativePath } from './projectPath'; -import { - previewStatusLabels, - taskGroupLabels, - taskStatusLabels, -} from './projectSummaryConstants'; - -export function summarizeProjectPublishReadiness( - nextManifest: GameCreationAppManifest, - nextProjectPath: string, - trace: GameCreationAgentRunTrace | null, -) { - const tasks = taskRowsFromManifest(nextManifest); - const completedCount = tasks.filter( - (task) => task.status === 'completed', - ).length; - const failedCount = tasks.filter((task) => task.status === 'failed').length; - const readyCount = selectGameCreationAppReadyTasks({ tasks }).length; - const tracePassed = isAgentRunTracePassed(trace); - const preview = nextManifest.preview; - const previewRunning = preview?.status === 'running' && preview.url; - const previewSummary = previewRunning - ? `运行中 ${preview.url}` - : preview - ? previewStatusLabels[preview.status] - : '未启动'; - const sourceCounts = nextManifest.assets.reduce( - (counts, asset) => { - counts[asset.source.kind] += 1; - return counts; - }, - { uploaded: 0, generated: 0, canvas: 0 } satisfies Record< - GameCreationAppAssetSourceKind, - number - >, - ); - const audioAssetCount = - nextManifest.assets.filter(isProjectAudioAsset).length; - const commandRuns = nextManifest.commandRuns ?? []; - const latestCommandRun = commandRuns[commandRuns.length - 1] ?? null; - const hasPublishReadme = - trace?.artifacts.some( - (artifact) => artifact.path === 'exports/README.md', - ) ?? false; - - let draftCommand = '/export'; - let draftCommandLabel = '导出试玩包'; - if (!trace) { - draftCommand = '/next'; - draftCommandLabel = '查看下一步'; - } else if ( - trace.lifecycleStatus === 'killed' || - trace.status === 'failed' || - trace.stopReason === 'max-passes-exhausted' - ) { - draftCommand = '/agent-resume '; - draftCommandLabel = '继续最近 run'; - } else if (!tracePassed) { - draftCommand = '/trace'; - draftCommandLabel = '查看 trace'; - } else if (!previewRunning) { - draftCommand = '/run'; - draftCommandLabel = '启动预览'; - } - - return { - text: [ - '发布准备:', - `- 项目:${nextManifest.name}`, - `- 目录:${nextProjectPath}`, - `- 原型:${ - tracePassed - ? `最近 run 已通过 ${trace?.runId ?? ''}`.trim() - : trace - ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}` - : '暂无最近 run' - }`, - `- 预览:${previewSummary}${previewRunning ? '' : ' · 建议 /run'}`, - `- 任务:完成 ${completedCount}/${tasks.length} · ready ${readyCount} · 失败 ${failedCount}`, - `- 资产:${nextManifest.assets.length} 个 · 上传 ${sourceCounts.uploaded} / 生成 ${sourceCounts.generated} / 画板 ${sourceCounts.canvas}`, - `- 音频:${audioAssetCount > 0 ? `${audioAssetCount} 个` : '暂无 · 建议 /audio'}`, - `- 包装:${ - hasPublishReadme - ? '最近 Run 包含 exports/README.md · /read exports/README.md' - : '可用 /artifacts 查看发布说明草稿' - }`, - `- 试玩包:${ - tracePassed - ? '可执行 /export 生成本地 ZIP' - : '等待最近 run 通过后再导出' - }`, - latestCommandRun?.status === 'failed' - ? `- 阻塞:最近命令 ${latestCommandRun.commandId} 失败 · /logs` - : null, - ] - .filter(Boolean) - .join('\n'), - draftCommand, - draftCommandLabel, - }; -} - -export function summarizeProjectListingDraft( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const manifestTasks = taskRowsFromManifest(nextManifest); - const traceTasks = trace?.taskGraph.tasks ?? []; - const tasks = manifestTasks.map( - (task) => traceTasks.find((traceTask) => traceTask.id === task.id) ?? task, - ); - const publishingTasks = tasks.filter( - (task) => task.group === 'publishing' || task.id.startsWith('publish-'), - ); - const readyTaskIds = new Set( - trace - ? trace.taskGraph.readyTaskIds - : selectGameCreationAppReadyTasks({ tasks }).map((task) => task.id), - ); - const activeTaskIds = new Set(trace?.taskGraph.activeTaskIds ?? []); - const carriedTaskIds = new Set(trace?.taskGraph.carriedTaskIds ?? []); - const taskLines = publishingTasks.map((task) => { - const markers = [taskStatusLabels[task.status]]; - if (readyTaskIds.has(task.id)) { - markers.push('ready'); - } - if (activeTaskIds.has(task.id)) { - markers.push('active'); - } - if (carriedTaskIds.has(task.id)) { - markers.push('carry'); - } - return `- ${task.id}:${task.role} ${task.title} · ${markers.join(' / ')}`; - }); - const visualAssets = nextManifest.assets.filter(isProjectVisualAsset); - const usableVisualAssetCount = visualAssets.filter( - (asset) => - asset.source.kind === 'canvas' || asset.source.kind === 'generated', - ).length; - const hasPublishReadme = - trace?.artifacts.some( - (artifact) => artifact.path === 'exports/README.md', - ) ?? false; - const latestPublishingStep = - trace?.steps - .filter( - (step) => - step.group === 'publishing' || - step.taskId?.startsWith('publish-') || - step.outputPaths.includes('exports/README.md'), - ) - .slice(-1)[0] ?? null; - const goal = - nextManifest.goal?.trim() || - trace?.goal?.trim() || - trace?.taskGraph.goal?.trim() || - '暂无'; - const blockedTrace = - trace?.lifecycleStatus === 'killed' || - trace?.status === 'failed' || - trace?.status === 'needs-revision' || - trace?.stopReason === 'max-passes-exhausted'; - - let draftCommand = '/publish'; - let draftCommandLabel = '查看发布准备'; - if (hasPublishReadme) { - draftCommand = '/read exports/README.md'; - draftCommandLabel = '读发布说明'; - } else if (blockedTrace) { - draftCommand = '/review'; - draftCommandLabel = '查看评审'; - } else if (!trace) { - draftCommand = '/next'; - draftCommandLabel = '查看下一步'; - } else if (visualAssets.length === 0) { - draftCommand = '/art'; - draftCommandLabel = '补齐美术素材'; - } - - return { - text: [ - '作品页草稿:', - `- 标题:${nextManifest.name}`, - `- 一句话卖点:${goal}`, - taskLines.length > 0 - ? `- 发布任务:\n${taskLines.join('\n')}` - : '- 发布任务:暂无', - `- 封面素材:${ - visualAssets.length > 0 - ? `${visualAssets.length} 个视觉素材 · 可用 ${usableVisualAssetCount} 个画板 / 生成来源` - : '暂无 · 建议 /art' - }`, - `- 说明文案:${ - hasPublishReadme - ? 'exports/README.md · 已生成' - : '待从发布包装生成 · /publish' - }`, - '- 标签口径:玩法类型;视觉风格;难度 / 节奏;本地可玩', - `- 最近运营步骤:${ - latestPublishingStep - ? `${latestPublishingStep.agent} #${latestPublishingStep.pass} · ${latestPublishingStep.status} · ${latestPublishingStep.summary}` - : '暂无' - }`, - '- 边界:只整理作品页文案和封面需求;不上传云端;不发布作品', - `- 建议:${draftCommand}`, - ].join('\n'), - draftCommand, - draftCommandLabel, - }; -} - -export function summarizeProjectPlaytestState( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const tasks = taskRowsFromManifest(nextManifest); - const traceTasks = trace?.taskGraph.tasks ?? []; - const playtestTask = - traceTasks.find((task) => task.id === 'preview-playtest') ?? - tasks.find((task) => task.id === 'preview-playtest') ?? - null; - const readyTaskIds = new Set( - trace - ? trace.taskGraph.readyTaskIds - : selectGameCreationAppReadyTasks({ tasks }).map((task) => task.id), - ); - const preview = nextManifest.preview; - const previewRunning = preview?.status === 'running' && preview.url; - const previewSummary = previewRunning - ? `运行中 ${preview.url}` - : preview - ? previewStatusLabels[preview.status] - : '未启动'; - const tracePassed = isAgentRunTracePassed(trace); - const blockedTrace = - trace?.lifecycleStatus === 'killed' || - trace?.status === 'failed' || - trace?.status === 'needs-revision' || - trace?.stopReason === 'max-passes-exhausted'; - const latestPlaytestStep = - trace?.steps.filter(isPlaytestTraceStep).slice(-1)[0] ?? null; - - let draftCommand = '/next'; - let draftCommandLabel = '查看下一步'; - if (blockedTrace) { - draftCommand = '/review'; - draftCommandLabel = '查看评审'; - } else if (trace && !tracePassed) { - draftCommand = '/trace'; - draftCommandLabel = '查看 trace'; - } else if (previewRunning) { - draftCommand = '/open-preview'; - draftCommandLabel = '打开预览'; - } else if (tracePassed) { - draftCommand = '/run'; - draftCommandLabel = '启动试玩'; - } - - const taskMarkers: string[] = []; - if (playtestTask) { - taskMarkers.push(taskStatusLabels[playtestTask.status]); - if (readyTaskIds.has(playtestTask.id)) { - taskMarkers.push('ready'); - } - if (trace?.taskGraph.activeTaskIds.includes(playtestTask.id)) { - taskMarkers.push('active'); - } - if (trace?.taskGraph.carriedTaskIds.includes(playtestTask.id)) { - taskMarkers.push('carry'); - } - } - - return { - text: [ - '试玩状态:', - `- 原型:${ - tracePassed - ? `最近 run 已通过 ${trace?.runId ?? ''}`.trim() - : trace - ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}` - : '暂无最近 run' - }`, - `- 预览:${previewSummary}${ - previewRunning ? ' · 建议 /open-preview' : ' · 建议 /run' - }`, - `- Playtest 任务:${ - playtestTask - ? `${taskGroupLabels[playtestTask.group]} / ${playtestTask.role} ${playtestTask.title} · ${taskMarkers.join(' / ')}` - : '暂无 preview-playtest 任务' - }`, - `- 最近试玩步骤:${ - latestPlaytestStep - ? `${latestPlaytestStep.agent} #${latestPlaytestStep.pass} · ${latestPlaytestStep.status} · ${latestPlaytestStep.summary}` - : '暂无' - }`, - '- 试玩日志:/read .agent/logs/preview.log', - ].join('\n'), - draftCommand, - draftCommandLabel, - }; -} - -export function summarizeProjectManualTestPlan( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const tasks = taskRowsFromManifest(nextManifest); - const traceTasks = trace?.taskGraph.tasks ?? []; - const taskRows = tasks.map( - (task) => traceTasks.find((traceTask) => traceTask.id === task.id) ?? task, - ); - const taskLines = ['preview-readiness', 'preview-playtest'] - .map((taskId) => taskRows.find((task) => task.id === taskId)) - .filter((task): task is NonNullable => Boolean(task)) - .map( - (task) => - `- ${task.id}:${taskGroupLabels[task.group]} / ${task.role} ${task.title} · ${taskStatusLabels[task.status]}`, - ); - const preview = nextManifest.preview; - const previewRunning = preview?.status === 'running' && preview.url; - const previewSummary = previewRunning - ? `运行中 ${preview.url}` - : preview - ? previewStatusLabels[preview.status] - : '未启动'; - const tracePassed = isAgentRunTracePassed(trace); - const blockedTrace = - trace?.lifecycleStatus === 'killed' || - trace?.status === 'failed' || - trace?.status === 'needs-revision' || - trace?.stopReason === 'max-passes-exhausted'; - const goal = - nextManifest.goal?.trim() || - trace?.goal?.trim() || - trace?.taskGraph.goal?.trim() || - '暂无'; - const hasGameArtifact = - trace?.artifacts.some( - (artifact) => - artifact.path === 'game/index.html' || artifact.path === 'game/', - ) ?? false; - const latestPlaytestStep = - trace?.steps.filter(isPlaytestTraceStep).slice(-1)[0] ?? null; - - let draftCommand = '/next'; - let draftCommandLabel = '查看下一步'; - if (blockedTrace) { - draftCommand = '/review'; - draftCommandLabel = '查看评审'; - } else if (trace && !tracePassed) { - draftCommand = '/trace'; - draftCommandLabel = '查看 trace'; - } else if (previewRunning) { - draftCommand = '/open-preview'; - draftCommandLabel = '打开预览'; - } else if (tracePassed) { - draftCommand = '/run'; - draftCommandLabel = '启动试玩'; - } - - return { - text: [ - '手动测试计划:', - `- 项目:${nextManifest.name}`, - `- 目标:${goal}`, - `- 当前证据:${ - tracePassed - ? `最近 run 已通过${trace?.runId ? ` ${trace.runId}` : ''}` - : trace - ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}` - : '暂无最近 run' - };预览 ${previewSummary};入口 ${hasGameArtifact ? '已生成' : '未见 trace 产物'}`, - '- 用例:\n1. 启动预览:/run 后确认首屏不空白\n2. 30 秒理解:目标、操作、得分/失败和重开可见\n3. 输入验证:键盘/点击/触屏至少一种可完成核心动作\n4. 结局验证:胜利或失败后可重开\n5. 回归检查:/mobile;/accessibility;/performance;/audio', - taskLines.length > 0 - ? `- 关联任务:\n${taskLines.join('\n')}` - : '- 关联任务:暂无', - `- 最近试玩证据:${ - latestPlaytestStep - ? `${latestPlaytestStep.agent} #${latestPlaytestStep.pass} · ${latestPlaytestStep.status} · ${latestPlaytestStep.summary}` - : '暂无' - }`, - '- 记录反馈:/feedback', - `- 建议:${draftCommand}`, - ].join('\n'), - draftCommand, - draftCommandLabel, - }; -} - -export function summarizeProjectFeedbackPrompt( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const preview = nextManifest.preview; - const previewRunning = preview?.status === 'running' && preview.url; - const previewSummary = previewRunning - ? `运行中 ${preview.url}` - : preview - ? previewStatusLabels[preview.status] - : '未启动'; - const tracePassed = isAgentRunTracePassed(trace); - const blockedTrace = - trace?.lifecycleStatus === 'killed' || - trace?.status === 'failed' || - trace?.status === 'needs-revision' || - trace?.stopReason === 'max-passes-exhausted'; - - let draftCommand = '/next'; - let draftCommandLabel = '查看下一步'; - if (blockedTrace) { - draftCommand = '/review'; - draftCommandLabel = '查看评审'; - } else if (tracePassed && !previewRunning) { - draftCommand = '/run'; - draftCommandLabel = '启动预览'; - } else if (trace) { - draftCommand = '/agent-resume 试玩反馈:'; - draftCommandLabel = '填写试玩反馈'; - } - - return { - text: [ - '试玩反馈:', - `- 项目:${nextManifest.name}`, - trace ? `- Run:${trace.runId} · ${formatAgentRunStatus(trace)}` : null, - `- 预览:${previewSummary}${previewRunning ? '' : ' · 建议 /run'}`, - '- 反馈方向:操作手感;胜负目标;难度;视觉 / 音效;重开路径', - '- 反馈模板:/agent-resume 试玩反馈:保留…;调整…;新增…', - '- 参考:/playtest;/qa;/changes', - `- 建议:${draftCommand}`, - ] - .filter(Boolean) - .join('\n'), - draftCommand, - draftCommandLabel, - }; -} - -export function summarizeProjectRetentionSignals( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const preview = nextManifest.preview; - const previewRunning = preview?.status === 'running' && preview.url; - const previewSummary = previewRunning - ? `运行中 ${preview.url}` - : preview - ? previewStatusLabels[preview.status] - : '未启动'; - const tracePassed = isAgentRunTracePassed(trace); - const blockedTrace = - trace?.lifecycleStatus === 'killed' || - trace?.status === 'failed' || - trace?.status === 'needs-revision' || - trace?.stopReason === 'max-passes-exhausted'; - const commandRuns = nextManifest.commandRuns ?? []; - const staticSmokePassed = - commandRuns.some( - (commandRun) => - commandRun.commandId === 'game.static_smoke' && - commandRun.status === 'completed', - ) || - Boolean( - trace?.steps.some((step) => - step.toolCalls.some( - (toolCall) => - toolCall.toolId === 'game.static_smoke' && toolCall.status === 'ok', - ), - ), - ); - const latestExportCommand = - [...commandRuns] - .reverse() - .find( - (commandRun) => commandRun.commandId === 'project.export_package', - ) ?? null; - const packageSummary = - latestExportCommand?.status === 'completed' - ? '最近导出完成' - : latestExportCommand?.status === 'failed' - ? '最近导出失败' - : tracePassed - ? '待导出' - : '等待原型通过'; - const goal = - nextManifest.goal?.trim() || - trace?.goal?.trim() || - trace?.taskGraph.goal?.trim() || - '暂无'; - const latestPlaytestStep = - trace?.steps.filter(isPlaytestTraceStep).slice(-1)[0] ?? null; - const hasReleaseNotes = - trace?.artifacts.some( - (artifact) => artifact.path === 'exports/README.md', - ) ?? false; - - let draftCommand = '/next'; - let draftCommandLabel = '查看下一步'; - if (blockedTrace) { - draftCommand = '/review'; - draftCommandLabel = '查看评审'; - } else if (trace && !tracePassed) { - draftCommand = '/trace'; - draftCommandLabel = '查看 trace'; - } else if (tracePassed && !previewRunning) { - draftCommand = '/run'; - draftCommandLabel = '启动试玩'; - } else if (trace) { - draftCommand = '/feedback'; - draftCommandLabel = '准备反馈'; - } - - return { - text: [ - '复玩观察:', - `- 项目:${nextManifest.name}`, - `- 目标:${goal}`, - `- 当前状态:${ - tracePassed - ? `最近 run 已通过${trace?.runId ? ` ${trace.runId}` : ''}` - : trace - ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}` - : '暂无最近 run' - };预览 ${previewSummary};自检 ${ - staticSmokePassed ? '已通过' : '未见通过' - };试玩包 ${packageSummary}`, - '- 首轮样本:3-5 名测试者;每人 5-10 分钟;先不解释玩法,观察是否能自己完成首局', - '- 复玩信号:是否主动重开;失败后是否理解原因;第二局是否更快进入目标;是否愿意换难度/角色/关卡;是否能说出想保留的一点', - `- 资产与包装:素材 ${nextManifest.assets.length} 个;发布说明 ${ - hasReleaseNotes ? '已生成' : '未见 trace 产物' - }`, - `- 最近试玩证据:${ - latestPlaytestStep - ? `${latestPlaytestStep.agent} #${latestPlaytestStep.pass} · ${latestPlaytestStep.status} · ${latestPlaytestStep.summary}` - : '暂无' - }`, - '- 记录模板:保留 1 项;调弱/调强 1 项;新增 1 项;必须修 1 项;是否愿意再玩一局', - '- 暂不做:真实埋点;留存报表;用户画像;A/B 实验;排行榜或账号留存', - '- 参考:/playtest;/feedback;/survey;/share;/known-issues', - '- 边界:只准备复玩观察清单;不读取文件;不启动或打开预览;不导出试玩包;不上传云端;不发布作品;不写项目', - `- 建议:${draftCommand}`, - ].join('\n'), - draftCommand, - draftCommandLabel, - }; -} - -export function summarizeProjectShareHandoff( - nextManifest: GameCreationAppManifest, - nextProjectPath: string, - trace: GameCreationAgentRunTrace | null, -) { - const tracePassed = isAgentRunTracePassed(trace); - const blockedTrace = - trace?.lifecycleStatus === 'killed' || - trace?.status === 'failed' || - trace?.status === 'needs-revision' || - trace?.stopReason === 'max-passes-exhausted'; - const preview = nextManifest.preview; - const previewRunning = preview?.status === 'running' && preview.url; - const previewSummary = previewRunning - ? `运行中 ${preview.url}` - : preview - ? previewStatusLabels[preview.status] - : '未启动'; - const commandRuns = nextManifest.commandRuns ?? []; - const latestExportCommand = - [...commandRuns] - .reverse() - .find( - (commandRun) => commandRun.commandId === 'project.export_package', - ) ?? null; - const packageSummary = - latestExportCommand?.status === 'completed' - ? '最近导出完成 · /exports' - : latestExportCommand?.status === 'failed' - ? '最近导出失败 · /logs' - : tracePassed - ? '待导出 · /export' - : '等待最近 run 通过'; - - let draftCommand = '/next'; - let draftCommandLabel = '查看下一步'; - if (blockedTrace) { - draftCommand = '/review'; - draftCommandLabel = '查看评审'; - } else if (!trace) { - draftCommand = '/next'; - draftCommandLabel = '查看下一步'; - } else if (!tracePassed) { - draftCommand = '/trace'; - draftCommandLabel = '查看 trace'; - } else if (latestExportCommand?.status === 'completed') { - draftCommand = '/exports'; - draftCommandLabel = '查看试玩包'; - } else { - draftCommand = '/export'; - draftCommandLabel = '导出试玩包'; - } - - return { - text: [ - '试玩交付:', - `- 项目:${nextManifest.name}`, - `- 目录:${nextProjectPath}`, - `- 原型:${ - tracePassed - ? `最近 run 已通过 ${trace?.runId ?? ''}`.trim() - : trace - ? `最近 run 未通过 ${trace.status} / ${trace.stopReason}` - : '暂无最近 run' - }`, - `- 本地预览:${previewSummary}${previewRunning ? ' · /open-preview' : ' · /run'}`, - `- 本地试玩包:${packageSummary}`, - '- 给测试者:玩法目标 / 操作 / 胜负 / 重开口径见 /rules', - '- 反馈收集:/feedback', - '- 交付边界:本地 ZIP 和本地预览;不上传云端;不生成公开分享链接', - `- 建议:${draftCommand}`, - ].join('\n'), - draftCommand, - draftCommandLabel, - }; -} - -export function summarizeProjectQualityCheck( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - const tasks = taskRowsFromManifest(nextManifest); - const completedCount = tasks.filter( - (task) => task.status === 'completed', - ).length; - const failedTasks = tasks.filter((task) => task.status === 'failed'); - const readyCount = selectGameCreationAppReadyTasks({ tasks }).length; - const commandRuns = nextManifest.commandRuns ?? []; - const latestCommandRun = commandRuns[commandRuns.length - 1] ?? null; - const preview = nextManifest.preview; - const previewRunning = preview?.status === 'running' && preview.url; - const tracePassed = isAgentRunTracePassed(trace); - const blockedTrace = - trace?.lifecycleStatus === 'killed' || - trace?.status === 'failed' || - trace?.status === 'needs-revision' || - trace?.stopReason === 'max-passes-exhausted'; - const staticSmokePassed = - commandRuns.some( - (commandRun) => - commandRun.commandId === 'game.static_smoke' && - commandRun.status === 'completed', - ) || - Boolean( - trace?.steps.some((step) => - step.toolCalls.some( - (toolCall) => - toolCall.toolId === 'game.static_smoke' && toolCall.status === 'ok', - ), - ), - ); - const latestReviewStep = - trace?.steps.filter(isAgentReviewStep).slice(-1)[0] ?? null; - const latestPlaytestStep = - trace?.steps.filter(isPlaytestTraceStep).slice(-1)[0] ?? null; - const evaluatorSummary = !trace - ? '暂无' - : tracePassed - ? '通过' - : blockedTrace - ? '需返工' - : '未通过'; - const playtestSummary = previewRunning - ? `预览运行中 ${preview.url}` - : tracePassed - ? '待启动预览' - : '等待原型通过'; - - let draftCommand = '/publish'; - let draftCommandLabel = '查看发布准备'; - if (!trace) { - draftCommand = '/next'; - draftCommandLabel = '查看下一步'; - } else if (blockedTrace) { - draftCommand = '/review'; - draftCommandLabel = '查看评审'; - } else if (failedTasks.length > 0) { - draftCommand = '/tasks'; - draftCommandLabel = '查看任务'; - } else if (!tracePassed) { - draftCommand = '/trace'; - draftCommandLabel = '查看 trace'; - } else if (!staticSmokePassed || !previewRunning) { - draftCommand = '/playtest'; - draftCommandLabel = '查看试玩状态'; - } - - return { - text: [ - '质量检查:', - trace ? `- Run:${trace.runId} · ${formatAgentRunStatus(trace)}` : null, - `- Evaluator:${evaluatorSummary}${ - latestReviewStep ? ` · ${latestReviewStep.summary}` : '' - }`, - `- 任务:完成 ${completedCount}/${tasks.length} · ready ${readyCount} · 失败 ${failedTasks.length}`, - `- 静态自检:${staticSmokePassed ? '通过' : '未运行'}`, - `- 试玩:${playtestSummary}`, - `- 最近试玩步骤:${ - latestPlaytestStep - ? `${latestPlaytestStep.agent} #${latestPlaytestStep.pass} · ${latestPlaytestStep.status} · ${latestPlaytestStep.summary}` - : '暂无' - }`, - `- 产物:${trace ? `${trace.artifacts.length} 个` : '暂无'}`, - latestCommandRun?.status === 'failed' - ? `- 阻塞:最近命令 ${latestCommandRun.commandId} 失败 · /logs` - : null, - `- 建议:${draftCommand}`, - ] - .filter(Boolean) - .join('\n'), - draftCommand, - draftCommandLabel, - }; -} - -export function summarizeProjectRecentChanges( - nextManifest: GameCreationAppManifest, - trace: GameCreationAgentRunTrace | null, -) { - if (!trace) { - return { - text: '最近变更:\n- 最近 Run:暂无\n- 建议:/next', - draftCommand: '/next', - draftCommandLabel: '查看下一步', - }; - } - - const artifacts = readableArtifactsFromAgentRunTrace(trace); - const visibleArtifacts = artifacts.slice(0, 6); - const artifactLines = visibleArtifacts.map( - (artifact) => - `- ${artifact.path} · ${artifact.sizeBytes}B · ${artifact.checksum}`, - ); - if (artifacts.length > visibleArtifacts.length) { - artifactLines.push( - `- 还有 ${artifacts.length - visibleArtifacts.length} 个产物`, - ); - } - const outputPathLines = trace.steps - .slice(-4) - .map((step) => { - const outputPaths = step.outputPaths - .filter(isSafeProjectRelativePath) - .slice(0, 3); - if (outputPaths.length === 0) { - return null; - } - return `- ${step.agent} #${step.pass} · ${step.status} · ${outputPaths.join(', ')}`; - }) - .filter(Boolean); - const commandRuns = nextManifest.commandRuns ?? []; - const latestCommandRun = commandRuns[commandRuns.length - 1] ?? null; - const preferredArtifact = - artifacts.find((artifact) => artifact.path === 'game/index.html') ?? - artifacts[0] ?? - null; - const draftCommand = preferredArtifact - ? `/read ${preferredArtifact.path}` - : '/run-artifacts'; - - return { - text: [ - '最近变更:', - `- Run:${trace.runId} · ${formatAgentRunStatus(trace)}`, - `- 可验产物:${artifacts.length} 个`, - artifactLines.length > 0 - ? `- 关键产物:\n${artifactLines.join('\n')}` - : '- 关键产物:暂无可读取产物', - outputPathLines.length > 0 - ? `- 最近输出:\n${outputPathLines.join('\n')}` - : '- 最近输出:暂无', - `- 当前资产:${nextManifest.assets.length} 个`, - latestCommandRun - ? `- 最近命令:${latestCommandRun.commandId} · ${ - latestCommandRun.status === 'completed' ? '完成' : '失败' - }` - : '- 最近命令:暂无', - '- 全部产物:/run-artifacts', - '- Trace:/trace', - '- 真实差异:/checkpoints 后 /diff checkpoint-id', - `- 建议:${draftCommand}`, - ].join('\n'), - draftCommand, - draftCommandLabel: preferredArtifact ? '读取首个产物' : '列出 Run 产物', - }; -} diff --git a/apps/ai-game-creator-shell/src/features/project-summary/projectSummary.ts b/apps/ai-game-creator-shell/src/features/project-summary/projectSummary.ts index 97b981535..eefb6306d 100644 --- a/apps/ai-game-creator-shell/src/features/project-summary/projectSummary.ts +++ b/apps/ai-game-creator-shell/src/features/project-summary/projectSummary.ts @@ -1,139 +1,5 @@ -export { - summarizeAgentReviewState, - summarizeAgentRunBudget, - summarizeProjectContextSources, - summarizeProjectHandoff, - summarizeProjectTimeline, -} from './agentRunSummaries'; -export { - formatAgentRunStatus, - formatTraceRepairRoutes, - formatTraceTaskId, - formatTraceTaskIds, - isAgentReviewStep, - isAgentRunTracePassed, - isPlaytestTraceStep, - readableArtifactPathFromAgentRunTrace, - readableArtifactsFromAgentRunTrace, -} from './agentTrace'; -export { missingChatCommandArgumentMessage } from './chatCommandMetadata'; -export { - checkpointIdFromManifestPath, - checkpointSummaryFromManifest, - formatCanvasAssetSource, - formatProjectPolicyCommandList, - inferProjectFileAssetDraft, - isCheckpointManifestFile, - projectAssetDraftCommand, - projectFileActionDrafts, - sortCheckpointManifestFiles, - summarizeAgentRunSupportFileReadDrafts, - summarizeCommonProjectArtifactReadDrafts, - summarizeCommonProjectLogReadDrafts, - summarizeProjectCheckpoint, - summarizeProjectCheckpoints, - summarizeProjectDiff, - summarizeProjectExportPackage, - summarizeProjectExportPackages, - summarizeProjectFileContent, - summarizeProjectFiles, - summarizeProjectIndex, - summarizeProjectInternalReadDrafts, - summarizeProjectPolicy, -} from './projectArtifactSummaries'; -export { - firstReadableProjectAssetPath, - isProjectAudioAsset, - isProjectVisualAsset, - summarizeProjectAssetCredits, - summarizeProjectAssets, - summarizeProjectAudioAssets, - summarizeProjectTasks, - summarizeProjectVisualAssets, -} from './projectAssetSummaries'; -export { - summarizeProjectAcceptanceCriteria, - summarizeProjectBalanceState, - summarizeProjectGroupProgress, - summarizeProjectKnownIssues, - summarizeProjectNextRoundPlan, - summarizeProjectReleaseNotes, - summarizeProjectSpecSheet, - summarizeProjectTodoList, -} from './projectDeliverySummaries'; -export { - summarizeMainProjectHeader, - summarizeNextProjectActions, - summarizeProjectUserGuide, -} from './projectGuidanceSummaries'; -export { - summarizeProjectBrief, - summarizeProjectControlGuide, - summarizeProjectDemoScript, - summarizeProjectGoal, - summarizeProjectMvpScope, - summarizeProjectPitch, - summarizeProjectProgress, - summarizeProjectStatus, -} from './projectOverviewSummaries'; export { isAbsoluteProjectPath, - isSafeProjectRelativePath, projectPathHasControlCharacter, projectPathsMatchForInvalidation, } from './projectPath'; -export { - summarizeProjectDependencyMap, - summarizeProjectEvidenceLedger, - summarizeProjectPrivacyBoundary, - summarizeProjectRevisionDraft, -} from './projectPlanningSummaries'; -export { - summarizeProjectAudienceGuide, - summarizeProjectBugReport, - summarizeProjectCommunityPost, - summarizeProjectCoverChecklist, - summarizeProjectMediaKit, - summarizeProjectPlaytestFaq, - summarizeProjectPlaytestInvite, - summarizeProjectPlaytestSurvey, - summarizeProjectScreenshotChecklist, - summarizeProjectStoreChecklist, - summarizeProjectTrailerScript, -} from './projectPlaytestSummaries'; -export { - summarizeProjectAccessibilityGuide, - summarizeProjectBlockers, - summarizeProjectCompatibilityNotes, - summarizeProjectLocalizationChecklist, - summarizeProjectMobilePlaytestGuide, - summarizeProjectPerformanceCheck, - summarizeProjectPlaytestReadiness, - summarizeProjectPolishChecklist, - summarizeProjectRisks, - summarizeProjectTutorialGuide, -} from './projectQualitySummaries'; -export { - summarizeProjectFeedbackPrompt, - summarizeProjectListingDraft, - summarizeProjectManualTestPlan, - summarizeProjectPlaytestState, - summarizeProjectPublishReadiness, - summarizeProjectQualityCheck, - summarizeProjectRecentChanges, - summarizeProjectRetentionSignals, - summarizeProjectShareHandoff, -} from './projectReadinessSummaries'; -export { - agentTaskGraphStateLabels, - assetSourceKindLabels, - capabilityAreaLabels, - chatCommandHelp, - commonAgentRunSupportReadDrafts, - commonProjectArtifactReadDrafts, - commonProjectInternalReadDrafts, - commonProjectLogReadDrafts, - previewStatusLabels, - taskGroupLabels, - taskStatusLabels, -} from './projectSummaryConstants'; diff --git a/apps/ai-game-creator-shell/src/features/project-summary/projectSummaryConstants.ts b/apps/ai-game-creator-shell/src/features/project-summary/projectSummaryConstants.ts deleted file mode 100644 index c2f43bc5c..000000000 --- a/apps/ai-game-creator-shell/src/features/project-summary/projectSummaryConstants.ts +++ /dev/null @@ -1,215 +0,0 @@ -import { - GAME_CREATION_AGENT_CAPABILITIES, - type GameCreationAppAgentGroup, - type GameCreationAppAssetSourceKind, - type GameCreationAppPreviewStatus, - type GameCreationAppTaskStatus, -} from '../../../../../packages/shared/src/contracts/gameCreationApp'; -import { type AgentTaskGraphState } from '../../app/types'; - -export const commonProjectArtifactReadDrafts = [ - { label: '读入口', path: 'game/index.html' }, - { label: '读设计', path: 'game/game_design.md' }, - { label: '读数值', path: 'game/balance.json' }, - { label: '读美术清单', path: 'assets/manifest.art.json' }, - { label: '读音频清单', path: 'assets/manifest.audio.json' }, - { label: '读发布说明', path: 'exports/README.md' }, -] as const; - -export const commonProjectLogReadDrafts = [ - { label: '读命令日志', path: '.agent/logs/command.log' }, - { label: '读预览日志', path: '.agent/logs/preview.log' }, - { label: '读 Agent 日志', path: '.agent/logs/agent.log' }, -] as const; - -export const commonAgentRunSupportReadDrafts = [ - { label: '读输出流', path: '.agent/output.jsonl' }, - { label: '读活动流', path: '.agent/activity.jsonl' }, - { label: '读上下文包', path: '.agent/context.bundle.json' }, -] as const; - -export const commonProjectInternalReadDrafts = [ - { label: '读 manifest', path: '.agent/manifest.json' }, - { label: '读 run 指针', path: '.agent/run.latest.json' }, - { label: '读规格', path: '.agent/spec.md' }, - { label: '读评审', path: '.agent/findings.md' }, - { label: '读权限策略', path: '.agent/policy.json' }, - { label: '读项目索引', path: '.agent/project.index.json' }, - { label: '读本地索引流', path: '.agent/agent.db' }, - { label: '读项目对话', path: '.agent/conversations/project.jsonl' }, -] as const; - -export const taskGroupLabels: Record = { - design: '设计实现组', - art: '美术组', - code: '程序组', - balance: '数值组', - audio: '音乐组', - publishing: '运营组', -}; - -export const taskStatusLabels: Record = { - pending: '待处理', - running: '运行中', - 'waiting-for-confirmation': '待确认', - completed: '已完成', - failed: '失败', -}; - -export const agentTaskGraphStateLabels: Record = { - active: '本轮 active', - carried: 'carry-over', - ready: 'ready', -}; - -export const previewStatusLabels: Record = - { - stopped: '未启动', - starting: '启动中', - running: '运行中', - failed: '失败', - }; - -export const assetSourceKindLabels: Record< - GameCreationAppAssetSourceKind, - string -> = { - uploaded: '上传', - generated: '生成', - canvas: '画板', -}; - -export const capabilityAreaLabels: Record< - (typeof GAME_CREATION_AGENT_CAPABILITIES)[number]['area'], - string -> = { - user: '用户入口', - 'agent-runtime': 'Agent Runtime', - 'local-runtime': '本地运行', - 'dev-runtime': '开发支撑', -}; - -export const chatCommandHelp = [ - '直接输入普通文本:和主聊天 Agent 对话', - '/generate 创作想法:生成本地游戏草案', - '/project /绝对路径:设置本地项目目录', - '/config:打开运行时配置', - '/llm-status:检查 LLM 配置', - '/llm-routes:查看 Agent 智能服务状态', - '/capabilities:查看 Agent 能力清单', - '/audit:审计当前项目的 Agent 能力证据', - '/status:查看项目状态', - '/brief:生成当前项目简报', - '/goal:查看创作目标', - '/progress:查看项目进度', - '/spec:查看创作规格包', - '/mvp:查看本轮最小可玩范围', - '/pitch:查看试玩定位与卖点', - '/demo:准备 30 秒试玩讲解稿', - '/rules:查看玩法操作与规则', - '/tutorial:查看新手引导检查', - '/mobile:查看移动试玩检查', - '/compatibility:准备兼容性说明', - '/accessibility:查看可读性与无障碍检查', - '/localization:查看本地化与文案检查', - '/performance:查看性能与加载检查', - '/polish:查看试玩前打磨清单', - '/risks:查看当前项目风险', - '/blockers:查看当前阻塞项', - '/ready:查看试玩就绪度', - '/evidence:查看当前验证证据台账', - '/deps:查看任务依赖链', - '/revise:准备下一轮改版说明草稿', - '/privacy:查看隐私与导出边界', - '/audience:查看首批试玩对象', - '/invite:准备试玩邀请文案', - '/survey:准备试玩问卷问题', - '/cover:准备封面与缩略图检查', - '/screenshots:准备宣传截图清单', - '/trailer:准备试玩短视频脚本', - '/faq:准备试玩常见问答', - '/post:准备社区发布文案', - '/store:准备上架资料清单', - '/media-kit:准备媒体资料包清单', - '/release-notes:准备试玩更新说明', - '/known-issues:准备已知问题清单', - '/criteria:查看当前任务验收标准', - '/groups:查看专业组进度', - '/balance:查看数值与难度口径', - '/budget:查看最近 run 预算', - '/qa:查看质量检查清单', - '/changes:查看最近生成变更', - '/review:查看 Evaluator 评审和返工焦点', - '/context:查看生成上下文来源', - '/timeline:查看项目活动时间线', - '/handoff:生成当前项目交接摘要', - '/next:查看下一步建议', - '/guide:查看普通用户操作导引', - '/plan:查看下一轮分工计划', - '/todo:查看下一轮小步清单', - '/publish:查看发布准备清单', - '/listing:准备作品页文案清单', - '/playtest:查看试玩状态与下一步', - '/test-plan:准备手动测试计划', - '/feedback:准备试玩反馈和修改说明', - '/retention:准备首轮复玩/留存观察清单', - '/share:准备试玩交付清单', - '/open-project:在系统文件管理器中显示项目目录', - '/switch-project:回到首页项目组切换工作区', - '/index:刷新本地项目索引', - '/checkpoint:保存本地项目快照', - '/checkpoints:列出最近 checkpoint', - '/diff checkpoint-id:对比 checkpoint', - '/restore checkpoint-id:回滚项目文件到 checkpoint', - '/policy:查看项目权限策略', - '/policy-deny 命令:拒绝项目内某个内置命令', - '/policy-allow 命令:移除项目内某个命令拒绝项', - '/policy-confirm 命令:执行前每次确认', - '/policy-auto 命令:恢复自动执行', - '/agent-policy-deny Agent 命令:拒绝某个 Agent 调用工具', - '/agent-policy-allow Agent 命令:移除某个 Agent 的拒绝项', - '/agent-policy-confirm Agent 命令:某个 Agent 调用工具前要求确认', - '/agent-policy-auto Agent 命令:恢复某个 Agent 自动执行', - '/tasks:查看任务拆分', - '/agents:查看每个 Agent 的当前状态', - '/agent-conversations:列出 Agent 对话读取命令', - '/agent-memories:列出 Agent 私有记忆读取命令', - '/trace 或 /loop:查看最近一次 Agent loop trace', - '/agent-status:查看最近 run 生命周期', - '/agent-kill:标记最近 run 为 killed', - '/agent-retry:用最近 run 目标重新运行一次', - '/agent-resume [说明]:带说明继续运行最近 run 目标', - '/history:重新读取当前项目对话历史', - '/files:列出本地项目文件', - '/assets:列出本地项目资产', - '/credits:查看素材署名与来源', - '/art:查看美术素材与下一步草稿', - '/audio:查看音频素材与下一步草稿', - '/artifacts:列出常用生成产物读取命令', - '/run-artifacts:列出最近 Run 产物读取命令', - '/passes:列出 Agent 轮次产物读取命令', - '/runs:列出已加载 Run 历史读取命令', - '/run-files:列出 Agent 运行辅助文件读取命令', - '/internals:列出项目内部真相源读取命令', - '/logs:列出常用日志读取命令', - '/asset-register 路径 [kind] [mediaType]:登记项目内已有资产', - '/read 路径:读取本地项目内文本文件', - '/run:运行自检,启动本地 HTTP 预览并载入客户端运行视图', - '/export:导出本地试玩包', - '/exports:列出本地试玩包', - '/preview:启动本地 HTTP 预览并载入客户端运行视图', - '/open-preview:打开当前本地预览', - '/preview-status:查看预览状态', - '/preview-stop:停止预览', - '/memory [short|long|blackboard]:查看短期、长期或黑板记忆', - '/remember [short|long|blackboard] 内容:追加短期、长期或黑板记忆', - '/memory-set [short|long|blackboard] 内容:覆盖保存对应记忆', - '/forget-memory [short|long|blackboard]:删除对应记忆', - '/commands:查看可运行的受限命令白名单', - '/smoke:运行静态入口自检', - '/canvas 画板项目ID:打开本机画板项目', - '/sync-canvas-project 画板项目ID:同步画板项目资源到本地资产', - '/generate-art 提示词:通过平台 External Editor API 生成首版美术素材', - '/import-canvas-asset 本地路径 画板项目ID 资源ID|object:资产对象ID:登记画板来源资产', - '/import-canvas-export /绝对/导出.zip 画板项目ID:导入画板素材导出包', -]; diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/agentRunTrace.ts b/apps/ai-game-creator-shell/src/features/project-workspace/agentRunTrace.ts deleted file mode 100644 index f6fed0f77..000000000 --- a/apps/ai-game-creator-shell/src/features/project-workspace/agentRunTrace.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { - createGameCreationAppSeedTasks, - GAME_CREATION_AGENT_RUN_MAX_PASSES, - GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, - GAME_CREATION_AGENT_TOOL_CALL_MAX, - type GameCreationAgentRunTrace, -} from '../../../../../packages/shared/src/contracts/gameCreationApp'; - -function normalizeTraceArray(value: T[] | null | undefined, fallback: T[]) { - if (value == null) { - return fallback; - } - if (!Array.isArray(value)) { - throw new Error('Agent run trace 格式不正确'); - } - return value; -} - -export function parseAgentRunTrace(content: string): GameCreationAgentRunTrace { - const parsed = JSON.parse(content) as GameCreationAgentRunTrace; - if ( - parsed.schemaVersion !== GAME_CREATION_AGENT_RUN_SCHEMA_VERSION || - !Array.isArray(parsed.steps) - ) { - throw new Error('Agent run trace 格式不正确'); - } - parsed.artifacts = normalizeTraceArray(parsed.artifacts, []); - parsed.taskGraph ??= { - goal: parsed.goal, - readyTaskIds: [], - activeTaskIds: [], - carriedTaskIds: [], - repairFocus: [], - repairRoutes: [], - tasks: createGameCreationAppSeedTasks(), - }; - if (typeof parsed.taskGraph !== 'object' || Array.isArray(parsed.taskGraph)) { - throw new Error('Agent run trace 格式不正确'); - } - parsed.steps.forEach((step) => { - step.inputPaths = normalizeTraceArray(step.inputPaths, []); - step.outputPaths = normalizeTraceArray(step.outputPaths, []); - step.toolCalls = normalizeTraceArray(step.toolCalls, []); - }); - parsed.maxPasses ??= GAME_CREATION_AGENT_RUN_MAX_PASSES; - parsed.toolCallCount ??= parsed.steps.reduce( - (count, step) => count + step.toolCalls.length, - 0, - ); - parsed.maxToolCalls ??= GAME_CREATION_AGENT_TOOL_CALL_MAX; - parsed.stopReason ??= parsed.status; - parsed.taskGraph.readyTaskIds = normalizeTraceArray( - parsed.taskGraph.readyTaskIds, - [], - ); - parsed.taskGraph.activeTaskIds = normalizeTraceArray( - parsed.taskGraph.activeTaskIds, - [], - ); - parsed.taskGraph.carriedTaskIds = normalizeTraceArray( - parsed.taskGraph.carriedTaskIds, - [], - ); - parsed.taskGraph.repairFocus = normalizeTraceArray( - parsed.taskGraph.repairFocus, - [], - ); - parsed.taskGraph.repairRoutes = normalizeTraceArray( - parsed.taskGraph.repairRoutes, - [], - ); - parsed.taskGraph.repairRoutes.forEach((route) => { - route.taskIds = normalizeTraceArray(route.taskIds, []); - }); - parsed.taskGraph.tasks = normalizeTraceArray( - parsed.taskGraph.tasks, - createGameCreationAppSeedTasks(), - ); - parsed.passPlans = normalizeTraceArray(parsed.passPlans, []); - parsed.passPlans.forEach((plan) => { - plan.activeTaskIds = normalizeTraceArray(plan.activeTaskIds, []); - plan.carriedTaskIds = normalizeTraceArray(plan.carriedTaskIds, []); - plan.dependencyWaves = normalizeTraceArray(plan.dependencyWaves, []); - plan.dependencyWaves.forEach((wave) => { - if (!Array.isArray(wave)) { - throw new Error('Agent run trace 格式不正确'); - } - }); - plan.repairFocus = normalizeTraceArray(plan.repairFocus, []); - plan.repairRoutes = normalizeTraceArray(plan.repairRoutes, []); - plan.repairRoutes.forEach((route) => { - route.taskIds = normalizeTraceArray(route.taskIds, []); - }); - }); - return parsed; -} - -export function gameDraftStartedMessage() { - return [ - '开始调用 LLM:Planner 正在整理规格。', - '随后 Orchestrator 编排 6 组角色 brief,Generator 生成代码和资产清单,Evaluator 做质量评审。', - '完成后会把 run trace 和本地产物摘要发回这里。', - ].join('\n'); -} diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/chatPromptPolish.ts b/apps/ai-game-creator-shell/src/features/project-workspace/chatPromptPolish.ts index d30127da5..d426335ac 100644 --- a/apps/ai-game-creator-shell/src/features/project-workspace/chatPromptPolish.ts +++ b/apps/ai-game-creator-shell/src/features/project-workspace/chatPromptPolish.ts @@ -68,8 +68,7 @@ export function chatPromptDraftKey( * 发送前提醒判据(全部满足才提醒): * 1. 提醒没有被用户在偏好里关掉(本机 localStorage); * 2. 当前草稿指纹不等于「本轮已确认草稿」指纹 —— 即本轮还没有润色过、也没有选过「使用原文提交」; - * 3. 纯文本(trim 后)长度达到 {@link CHAT_PROMPT_POLISH_REMINDER_MIN_TEXT_LENGTH}; - * 4. 草稿不是以 `/` 开头的命令 —— 命令走直通路径,不参与提醒。 + * 3. 纯文本(trim 后)长度达到 {@link CHAT_PROMPT_POLISH_REMINDER_MIN_TEXT_LENGTH}。 */ export function shouldRemindChatPromptPolish({ content, @@ -89,9 +88,6 @@ export function shouldRemindChatPromptPolish({ if (text.length < CHAT_PROMPT_POLISH_REMINDER_MIN_TEXT_LENGTH) { return false; } - if (text.startsWith('/')) { - return false; - } return chatPromptDraftKey(content) !== acknowledgedDraftKey; } diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/memoryCommands.ts b/apps/ai-game-creator-shell/src/features/project-workspace/memoryCommands.ts deleted file mode 100644 index f803aa0cf..000000000 --- a/apps/ai-game-creator-shell/src/features/project-workspace/memoryCommands.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { MemoryScope } from '../../app/types'; - -export function memoryScopeLabel(scope: MemoryScope) { - if (scope === 'short') { - return '短期'; - } - return scope === 'blackboard' ? '黑板' : '长期'; -} - -export function memoryScopePath(scope: MemoryScope) { - if (scope === 'short') { - return 'memory/session.md'; - } - return scope === 'blackboard' ? 'memory/blackboard.md' : 'memory/project.md'; -} - -export function parseMemoryScope(value: string): MemoryScope { - const scope = value.trim().toLowerCase(); - if (scope === 'short' || scope === '短期' || scope === 'session') { - return 'short'; - } - if (scope === 'blackboard' || scope === '黑板') { - return 'blackboard'; - } - return 'long'; -} - -export function parseRememberInput(value: string): { - scope: MemoryScope; - content: string; -} { - const input = value.trim(); - const [first = '', ...rest] = input.split(/\s+/); - const lower = first.toLowerCase(); - if ( - [ - 'short', - '短期', - 'session', - 'long', - '长期', - 'project', - 'blackboard', - '黑板', - ].includes(lower) - ) { - return { - scope: parseMemoryScope(first), - content: rest.join(' ').trim(), - }; - } - return { scope: 'long', content: input }; -} diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/projectCommandPolicy.ts b/apps/ai-game-creator-shell/src/features/project-workspace/projectCommandPolicy.ts index 152837459..ff92e5353 100644 --- a/apps/ai-game-creator-shell/src/features/project-workspace/projectCommandPolicy.ts +++ b/apps/ai-game-creator-shell/src/features/project-workspace/projectCommandPolicy.ts @@ -18,14 +18,6 @@ export function resolveChatProjectPath( return projectPath; } -export function isAgentTraceFilePath(value: string) { - const path = value.trim(); - return ( - path === '.agent/run.latest.json' || - (path.startsWith('.agent/runs/') && path.endsWith('.json')) - ); -} - export function needsInitializedChatProject( commandId: GameCreationAppCommandDescriptor['id'], ) { diff --git a/apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectChatController.ts b/apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectChatController.ts index d368e8bb5..f20a51515 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectChatController.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectChatController.ts @@ -330,12 +330,6 @@ export function useDirectProjectChatController({ } return false; } - const prompt = directCodexContentToPromptText(content, assets).trim(); - if (prompt === '/history') { - clearPendingInput(); - void reloadHistory(); - return true; - } const clientTurnId = createDirectProjectTurnId(); const userItem = directCodexUserItemFromContent( content, @@ -613,49 +607,6 @@ export function useDirectProjectChatController({ } } - async function reloadHistory() { - const nextProjectPath = projectPath; - const invoke = resolveTauriInvoke(); - if (!nextProjectPath || !invoke || historyLoadingRef.current) return; - const allowed = await ensureConversationReadAllowed({ - projectPath: nextProjectPath, - onConfirmed: () => { - void reloadHistory(); - }, - }); - if (!allowed || projectPathRef.current !== nextProjectPath) return; - historyLoadingRef.current = true; - try { - const pages = await readDirectHistoryPages({ - existingEntries: directEntries, - beforeItemId: null, - readSlice: (beforeItemId) => - invoke( - 'read_direct_project_history_slice', - { - projectPath: nextProjectPath, - limit: CONVERSATION_INITIAL_VISIBLE_COUNT, - ...(beforeItemId ? { beforeItemId } : {}), - }, - ), - }); - if (projectPathRef.current !== nextProjectPath) return; - mergeHistoryPages(pages); - if (pages.error) throw pages.error; - setStatusNotice('已读取项目对话历史'); - } catch (error) { - if (projectPathRef.current === nextProjectPath) { - setStatusNotice( - `读取项目对话历史失败:${ - error instanceof Error ? error.message : String(error) - }`, - ); - } - } finally { - historyLoadingRef.current = false; - } - } - async function loadEarlierHistory() { const nextProjectPath = projectPath; if ( @@ -715,7 +666,6 @@ export function useDirectProjectChatController({ loadEarlierHistory, localMessages, queuedTurns, - reloadHistory, removeAttachment, startInitialTurn: startTurn, statusNotice, diff --git a/apps/ai-game-creator-shell/tests/ChatMarkdownMessage.test.tsx b/apps/ai-game-creator-shell/tests/ChatMarkdownMessage.test.tsx index 2b4768e04..dd92a0023 100644 --- a/apps/ai-game-creator-shell/tests/ChatMarkdownMessage.test.tsx +++ b/apps/ai-game-creator-shell/tests/ChatMarkdownMessage.test.tsx @@ -236,13 +236,16 @@ describe('ChatMarkdownMessage', () => { it('用户消息保持纯文本,不解析 Markdown', () => { const { container } = render( - , + , ); expect(container.querySelector('strong')).toBeNull(); expect(container.querySelector('code')).toBeNull(); expect(container.textContent).toContain('**不要解析**'); - expect(container.textContent).toContain('`/read game`'); + expect(container.textContent).toContain('`game/index.html`'); }); it('不输出原始 HTML、可点击链接或图片节点', () => { diff --git a/apps/ai-game-creator-shell/tests/agentRunTrace.test.ts b/apps/ai-game-creator-shell/tests/agentRunTrace.test.ts deleted file mode 100644 index 8f7d8183f..000000000 --- a/apps/ai-game-creator-shell/tests/agentRunTrace.test.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { - createGameCreationAppSeedTasks, - GAME_CREATION_AGENT_RUN_MAX_PASSES, - GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, - GAME_CREATION_AGENT_TOOL_CALL_MAX, -} from '../../../packages/shared/src/contracts/gameCreationApp'; -import { formatAgentRunStatus } from '../src/features/project-summary/agentTrace'; -import { parseAgentRunTrace } from '../src/features/project-workspace/agentRunTrace'; - -const TRACE_FORMAT_ERROR = 'Agent run trace 格式不正确'; - -/** - * 旧 trace(`876529e66` 之前的 App 命令聊天会在面板里渲染它):步骤没有路径与工具数组、 - * 缺 `artifacts` / `taskGraph` 以外的归一化字段。这里保留同一份夹具,只是改成直接断言 - * 归一化函数的输出,不再渲染整个 App。 - */ -function legacyTraceFixture(): Record { - return { - schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, - runId: 'run-legacy-trace', - commandId: 'game.generate_draft', - status: 'running', - passes: 1, - stopReason: 'planning', - goal: '做一个厨房弹幕游戏', - coordination: 'legacy', - steps: [legacyStep()], - taskGraph: { - goal: '做一个厨房弹幕游戏', - activeTaskIds: ['code-prototype'], - tasks: createGameCreationAppSeedTasks(), - }, - passPlans: [ - { - pass: 1, - mode: 'repair', - summary: '旧 pass plan', - activeTaskIds: ['code-prototype'], - }, - ], - nextStep: 'continue', - error: null, - updatedAt: 1, - }; -} - -function legacyStep(overrides: Record = {}) { - return { - pass: 1, - agent: 'Generator', - phase: 'generate', - taskId: 'code-prototype', - group: 'code', - role: 'Code', - status: 'completed', - summary: '旧 trace 没有路径和工具数组', - ...overrides, - }; -} - -function traceJson(overrides: Record = {}) { - return JSON.stringify({ ...legacyTraceFixture(), ...overrides }); -} - -describe('Agent run trace 归一化与展示', () => { - it('normalizes a legacy trace before it reaches the renderers', () => { - const trace = parseAgentRunTrace(traceJson()); - - expect(trace.steps[0]!.inputPaths).toEqual([]); - expect(trace.steps[0]!.outputPaths).toEqual([]); - expect(trace.steps[0]!.toolCalls).toEqual([]); - expect(trace.artifacts).toEqual([]); - expect(trace.maxPasses).toBe(GAME_CREATION_AGENT_RUN_MAX_PASSES); - expect(trace.maxToolCalls).toBe(GAME_CREATION_AGENT_TOOL_CALL_MAX); - expect(trace.toolCallCount).toBe(0); - expect(trace.taskGraph.readyTaskIds).toEqual([]); - expect(trace.taskGraph.carriedTaskIds).toEqual([]); - expect(trace.taskGraph.repairFocus).toEqual([]); - expect(trace.taskGraph.repairRoutes).toEqual([]); - expect(trace.passPlans[0]!.carriedTaskIds).toEqual([]); - expect(trace.passPlans[0]!.dependencyWaves).toEqual([]); - expect(trace.passPlans[0]!.repairFocus).toEqual([]); - expect(trace.passPlans[0]!.repairRoutes).toEqual([]); - }); - - it('counts tool calls from the steps when the legacy trace omits the total', () => { - const trace = parseAgentRunTrace( - traceJson({ - steps: [ - legacyStep({ toolCalls: [{ toolId: 'file.read' }] }), - legacyStep({ - taskId: 'quality-review', - toolCalls: [ - { toolId: 'game.static_smoke' }, - { toolId: 'game.run_local' }, - ], - }), - ], - }), - ); - - expect(trace.toolCallCount).toBe(3); - }); - - it('falls back to the seed task graph when the trace predates task graphs', () => { - const fixture = legacyTraceFixture(); - delete fixture.taskGraph; - - const trace = parseAgentRunTrace(JSON.stringify(fixture)); - - expect(trace.taskGraph.goal).toBe('做一个厨房弹幕游戏'); - expect(trace.taskGraph.tasks.length).toBe( - createGameCreationAppSeedTasks().length, - ); - expect(trace.taskGraph.activeTaskIds).toEqual([]); - }); - - it('rejects malformed traces instead of rendering a half-parsed panel', () => { - expect(() => parseAgentRunTrace(traceJson({ steps: null }))).toThrow( - TRACE_FORMAT_ERROR, - ); - expect(() => - parseAgentRunTrace(traceJson({ schemaVersion: 'legacy' })), - ).toThrow(TRACE_FORMAT_ERROR); - expect(() => parseAgentRunTrace(traceJson({ artifacts: {} }))).toThrow( - TRACE_FORMAT_ERROR, - ); - expect(() => parseAgentRunTrace(traceJson({ taskGraph: [] }))).toThrow( - TRACE_FORMAT_ERROR, - ); - expect(() => - parseAgentRunTrace( - traceJson({ - passPlans: [ - { pass: 1, mode: 'repair', dependencyWaves: [['a'], 'b'] }, - ], - }), - ), - ).toThrow(TRACE_FORMAT_ERROR); - }); - - it('formats the run status with and without a lifecycle status', () => { - const running = parseAgentRunTrace(traceJson()); - expect(formatAgentRunStatus(running)).toBe('running · 1/3 轮 · planning'); - - const passed = parseAgentRunTrace( - traceJson({ status: 'passed', passes: 2, lifecycleStatus: 'completed' }), - ); - expect(formatAgentRunStatus(passed)).toBe( - 'passed / completed · 2/3 轮 · planning', - ); - }); -}); diff --git a/apps/ai-game-creator-shell/tests/agentSwarmTestEntry.test.ts b/apps/ai-game-creator-shell/tests/agentSwarmTestEntry.test.ts deleted file mode 100644 index 5498797fd..000000000 --- a/apps/ai-game-creator-shell/tests/agentSwarmTestEntry.test.ts +++ /dev/null @@ -1,1915 +0,0 @@ -import { spawn } from 'node:child_process'; -import { - lstat, - mkdir, - mkdtemp, - readdir, - readFile, - realpath, - rm, - symlink, - writeFile, -} from 'node:fs/promises'; -import os from 'node:os'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { deflateSync } from 'node:zlib'; - -import { describe, expect, it } from 'vitest'; - -import { - appIdentifier, - buildCargoCliArguments, - buildMissingConfigWizardArguments, - canPromptForMissingRuntimeConfig, - childExitWithTimeout, - cleanupSwarmTestProject, - cleanupSwarmTestRuntimeConfig, - configFileName, - defaultRealSwarmTestTask, - defaultRuntimeConfigDirCandidates, - discoverRuntimeConfigDir, - hasConfiguredEditorApiKey, - hasGeneratedGameEntry, - hasIncompleteArtifactMarker, - inspectSwarmProjectArtifacts, - localConfigFileName, - nextSwarmAutoPilotReply, - parseRunnerShutdownOutput, - parseSettledSwarmTurnReport, - parseSwarmTestArguments, - prepareSwarmTestProject, - prepareSwarmTestRuntimeConfig, - removeDirectoryWithTimeout, - requiredSwarmManifestTaskIds, - resolveSwarmTestTimeoutMs, - runnerEndpointFileName, - shouldStartPersistentPreview, - swarmAutoPilotShouldCloseInput, - swarmAutoPilotSitsAtPrompt, - terminateChildTree, - testProjectPrefix, - testProjectSentinelName, - testProjectSentinelSchema, - testRuntimeConfigPrefix, - testRuntimeConfigSentinelName, - testRuntimeConfigSentinelSchema, - ungeneratedGameEntryMarker, - validatePngBytes, - validatePreviewUrl, - validateSwarmProjectArtifacts, -} from '../scripts/agent-swarm-test-chat.mjs'; -import { - buildGameCreatorWizardConfig, - gameCreatorProviderPresets, - normalizeWizardBaseUrl, - parseConfigWizardArguments, - readGameCreatorWizardConfigState, - resolveGameCreatorAppConfigDir, - writeGameCreatorConfigAtomically, - writeGameCreatorWizardConfig, -} from '../scripts/game-creator-config-wizard.mjs'; - -const appRoot = path.resolve(fileURLToPath(new URL('..', import.meta.url))); - -// Windows 下「私有路径加固」每次都会拉起真实 powershell.exe 去设置 DACL(本机实测约 0.6 秒 / 次)。 -// 只校验配置分层、隔离与清理语义的用例统一传入这份 no-op 桩;真正覆盖私有 ACL 的用例才走真实实现。 -const skippedWindowsAclOptions = - process.platform === 'win32' ? { secureWindowsPath: async () => {} } : {}; - -async function withTemporaryRoot( - run: (root: string) => Promise, -): Promise { - const root = await mkdtemp( - path.join(os.tmpdir(), 'genarrative-swarm-entry-test-'), - ); - try { - return await run(root); - } finally { - await rm(root, { recursive: true, force: true }); - } -} - -async function pathExists(targetPath: string): Promise { - return lstat(targetPath).then( - () => true, - (error: NodeJS.ErrnoException) => { - if (error.code === 'ENOENT') return false; - throw error; - }, - ); -} - -const minimumFormalArtifactContents: Record = { - 'memory/project.md': - '# Project\n\n原创项目目标、世界观、资源与单位命名均已确定。\n', - 'game/game_design.md': - '# Game design\n\n玩家放置原创守卫完成波次,胜利后进入下一关,并可随时重新开始。\n', - 'game/balance.json': '{"speed":1,"waves":3}\n', - 'assets/manifest.art.json': - '{"assets":[{"path":"assets/art-spritesheet.png"}]}\n', - 'assets/manifest.audio.json': '{"assets":[],"status":"planned"}\n', - 'game/index.html': - '\n', - 'exports/README.md': - '# Export\n\n项目已完成静态检查与桌面、移动双视口试玩。\n', -}; - -const fixturePngSignature = Buffer.from([ - 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, -]); - -function fixtureCrc32(bytes: Buffer): number { - let value = 0xffffffff; - for (const byte of bytes) { - value ^= byte; - for (let bit = 0; bit < 8; bit += 1) { - value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1; - } - } - return (value ^ 0xffffffff) >>> 0; -} - -function fixturePngChunk(type: string, data: Buffer): Buffer { - const typeBytes = Buffer.from(type, 'ascii'); - const chunk = Buffer.alloc(12 + data.length); - chunk.writeUInt32BE(data.length, 0); - typeBytes.copy(chunk, 4); - data.copy(chunk, 8); - chunk.writeUInt32BE( - fixtureCrc32(Buffer.concat([typeBytes, data])), - 8 + data.length, - ); - return chunk; -} - -function fixturePng( - width: number, - height: number, - { invalidFilter = false, trailingCompressedBytes = false } = {}, -): Buffer { - const ihdr = Buffer.alloc(13); - ihdr.writeUInt32BE(width, 0); - ihdr.writeUInt32BE(height, 4); - ihdr[8] = 1; - ihdr[9] = 0; - const rowBytes = Math.ceil(width / 8); - const scanlines = Buffer.alloc((rowBytes + 1) * height); - let pseudoRandom = 0x12345678; - for (let row = 0; row < height; row += 1) { - const rowOffset = row * (rowBytes + 1); - scanlines[rowOffset] = invalidFilter && row === 0 ? 5 : 0; - for (let column = 0; column < rowBytes; column += 1) { - pseudoRandom = (Math.imul(pseudoRandom, 1664525) + 1013904223) >>> 0; - scanlines[rowOffset + column + 1] = pseudoRandom >>> 24; - } - } - return Buffer.concat([ - fixturePngSignature, - fixturePngChunk('IHDR', ihdr), - fixturePngChunk( - 'IDAT', - trailingCompressedBytes - ? Buffer.concat([deflateSync(scanlines), Buffer.from('junk')]) - : deflateSync(scanlines), - ), - fixturePngChunk('IEND', Buffer.alloc(0)), - ]); -} - -function fixtureIndexedPng({ - includePalette = true, - duplicatePalette = false, - unknownCriticalChunk = false, -} = {}): Buffer { - const ihdr = Buffer.alloc(13); - ihdr.writeUInt32BE(1, 0); - ihdr.writeUInt32BE(1, 4); - ihdr[8] = 8; - ihdr[9] = 3; - const palette = fixturePngChunk('PLTE', Buffer.from([0, 0, 0])); - return Buffer.concat([ - fixturePngSignature, - fixturePngChunk('IHDR', ihdr), - ...(includePalette ? [palette] : []), - ...(duplicatePalette ? [palette] : []), - ...(unknownCriticalChunk ? [fixturePngChunk('ABCD', Buffer.alloc(0))] : []), - fixturePngChunk('IDAT', deflateSync(Buffer.from([0, 0]))), - fixturePngChunk('IEND', Buffer.alloc(0)), - ]); -} - -async function writeMinimumFormalArtifacts(root: string): Promise { - for (const [relativePath, content] of Object.entries( - minimumFormalArtifactContents, - )) { - const targetPath = path.join(root, ...relativePath.split('/')); - await mkdir(path.dirname(targetPath), { recursive: true }); - await writeFile(targetPath, content); - } - const revision = 8; - const reportPath = - '.agent/runtime/browser-validations/publish-package/test-run/8/validation.json'; - const desktopPath = - '.agent/runtime/browser-validations/publish-package/test-run/8/desktop.png'; - const mobilePath = - '.agent/runtime/browser-validations/publish-package/test-run/8/mobile.png'; - await mkdir(path.join(root, '.agent', 'runtime'), { recursive: true }); - await writeFile( - path.join(root, '.agent', 'manifest.json'), - `${JSON.stringify({ - tasks: requiredSwarmManifestTaskIds.map((id) => ({ - id, - status: 'completed', - })), - })}\n`, - ); - await writeFile( - path.join(root, '.agent', 'runtime', 'project-revision.json'), - `${JSON.stringify({ revision })}\n`, - ); - const evidenceDirectory = path.dirname(path.join(root, reportPath)); - await mkdir(evidenceDirectory, { recursive: true }); - await writeFile(path.join(root, desktopPath), fixturePng(1280, 720)); - await writeFile(path.join(root, mobilePath), fixturePng(390, 844)); - await writeFile( - path.join(root, reportPath), - `${JSON.stringify({ - passed: true, - playtest: { passed: true }, - viewportResults: [ - { viewport: 'desktop', passed: true }, - { viewport: 'mobile', passed: true }, - ], - })}\n`, - ); - await writeFile( - path.join(root, '.agent', 'agent.db'), - [ - JSON.stringify({ - recordType: 'agent.runtime.command.run_limited', - commandId: 'game.static_smoke', - status: 'completed', - revision, - }), - JSON.stringify({ - recordType: 'agent.runtime.preview.validation', - passed: true, - playtestPassed: true, - revision, - reportPath, - screenshots: [desktopPath, mobilePath], - updatedAt: 1, - }), - '', - ].join('\n'), - ); -} - -async function writeReadyTaskExactlyOnceEvidence( - root: string, - parentRunId: string, -): Promise { - const databasePath = path.join(root, '.agent', 'agent.db'); - const database = await readFile(databasePath, 'utf8'); - const records: Array> = []; - for (const taskId of requiredSwarmManifestTaskIds) { - const runId = `autonomous-ready-${taskId}-fixture`; - const taskDirectory = path.join(root, '.agent', 'runtime', 'tasks'); - await mkdir(taskDirectory, { recursive: true }); - await writeFile( - path.join(taskDirectory, `${taskId}.jsonl`), - `${JSON.stringify({ - agentId: taskId, - taskId, - runId, - source: 'agent-ready-task-scheduler', - runProfile: 'autonomous-game-build', - parentAgentId: 'project-supervisor', - parentRunId, - status: 'completed', - phase: 'completed', - })}\n`, - ); - records.push( - { - recordType: 'agent.runtime.background_task', - agentId: taskId, - taskId, - runId, - source: 'agent-ready-task-scheduler', - }, - { - recordType: 'agent.runtime.background_task.completed', - agentId: taskId, - taskId, - runId, - source: 'agent-ready-task-scheduler', - }, - { - recordType: 'agent.runtime.autonomous_ready_task.manifest_projected', - agentId: taskId, - taskId, - runId, - source: 'agent-ready-task-scheduler', - parentAgentId: 'project-supervisor', - parentRunId, - terminalPhase: 'completed', - manifestStatus: 'completed', - }, - ); - } - await writeFile( - databasePath, - `${database.trimEnd()}\n${records.map((record) => JSON.stringify(record)).join('\n')}\n`, - ); -} - -describe('terminal configuration wizard arguments', () => { - it('parses the supported options and documented defaults', () => { - const configDir = path.resolve('fixture-config'); - - expect(parseConfigWizardArguments([])).toEqual({ - configDir: null, - configureOnly: false, - help: false, - }); - expect( - parseConfigWizardArguments([ - '--config-dir', - configDir, - '--configure-only', - '--help', - ]), - ).toEqual({ configDir, configureOnly: true, help: true }); - expect(parseConfigWizardArguments(['-h']).help).toBe(true); - }); - - it.each([ - ['separate API key argument', ['--api-key', 'fixture-secret'], 'API Key'], - ['inline API key argument', ['--api-key=fixture-secret'], 'API Key'], - ['relative config directory', ['--config-dir', 'relative'], '绝对路径'], - ['missing config directory', ['--config-dir'], '缺少目录路径'], - [ - 'duplicate config directory', - ['--config-dir', '/first', '--config-dir', '/second'], - '只能指定一次', - ], - ['unknown option', ['--unknown'], '未知选项'], - ])('rejects %s', (_label, args, marker) => { - expect(() => parseConfigWizardArguments(args)).toThrow(marker); - }); -}); - -describe('terminal configuration wizard AppData paths', () => { - it.each([ - { - label: 'Linux XDG config', - context: { - platform: 'linux', - environment: { XDG_CONFIG_HOME: '/fixture/xdg' }, - homeDirectory: '/fixture/home', - }, - candidate: path.posix.join('/fixture/xdg', appIdentifier), - }, - { - label: 'macOS Application Support', - context: { - platform: 'darwin', - environment: {}, - homeDirectory: '/Users/fixture', - }, - candidate: path.posix.join( - '/Users/fixture', - 'Library', - 'Application Support', - appIdentifier, - ), - }, - { - label: 'Windows roaming AppData', - context: { - platform: 'win32', - environment: { - APPDATA: 'C:\\Users\\fixture\\AppData\\Roaming', - LOCALAPPDATA: 'C:\\Users\\fixture\\AppData\\Local', - }, - homeDirectory: 'C:\\Users\\fixture', - }, - candidate: path.win32.join( - 'C:\\Users\\fixture\\AppData\\Roaming', - appIdentifier, - ), - }, - ])( - 'resolves the GUI-compatible $label directory', - ({ context, candidate }) => { - expect(resolveGameCreatorAppConfigDir(context)).toBe(candidate); - }, - ); - - it('uses an explicit absolute directory and rejects a relative one', () => { - const explicitConfigDir = path.resolve('explicit-config'); - - expect(resolveGameCreatorAppConfigDir({ explicitConfigDir })).toBe( - explicitConfigDir, - ); - expect(() => - resolveGameCreatorAppConfigDir({ explicitConfigDir: 'relative-config' }), - ).toThrow('绝对路径'); - }); -}); - -describe('terminal configuration wizard providers', () => { - it('publishes the supported provider presets exactly', () => { - expect(gameCreatorProviderPresets).toEqual([ - { - id: 'openai', - label: 'OpenAI', - baseUrl: 'https://api.openai.com/v1', - model: 'gpt-4.1', - apiKind: 'openai_responses', - }, - { - id: 'deepseek', - label: 'DeepSeek', - baseUrl: 'https://api.deepseek.com', - model: 'deepseek-chat', - apiKind: 'openai_chat', - }, - { - id: 'anthropic', - label: 'Anthropic', - baseUrl: 'https://api.anthropic.com', - model: 'claude-3-5-sonnet-latest', - apiKind: 'anthropic', - }, - { - id: 'ark', - label: '火山 Ark', - baseUrl: 'https://ark.cn-beijing.volces.com/api/v3', - model: 'doubao-seed-1-6', - apiKind: 'openai_chat', - }, - { - id: 'custom', - label: '自定义', - baseUrl: '', - model: '', - apiKind: 'openai_chat', - }, - ]); - expect(Object.isFrozen(gameCreatorProviderPresets)).toBe(true); - }); -}); - -describe('terminal configuration wizard config merge', () => { - it('updates the default LLM while retaining per-Agent and Editor API config', () => { - const existingConfig = { - schemaVersion: 'game-creator-config.v2', - agentMode: 'codex_app_server', - llm: { - apiKey: 'old-fixture-secret', - model: 'old-model', - maxRetries: 2, - }, - agentLlm: { - 'project-supervisor': { providerId: 'supervisor-provider' }, - }, - editorApi: { - apiKey: 'fixture-editor-secret', - baseUrl: 'https://editor.example.test/v1', - }, - }; - const originalConfig = structuredClone(existingConfig); - - const merged = buildGameCreatorWizardConfig(existingConfig, { - apiKey: ' new-fixture-secret ', - baseUrl: 'https://llm.example.test/v1/', - model: ' fixture-model ', - apiKind: 'openai_chat', - }); - - expect(merged).toEqual({ - ...originalConfig, - agentMode: 'provider', - llm: { - apiKey: 'new-fixture-secret', - model: 'fixture-model', - maxRetries: 2, - baseUrl: 'https://llm.example.test/v1', - apiKind: 'openai_chat', - reasoningEffort: 'high', - }, - }); - expect(merged.agentLlm).toEqual(originalConfig.agentLlm); - expect(merged.editorApi).toEqual(originalConfig.editorApi); - expect(existingConfig).toEqual(originalConfig); - }); - - it('uses provider-default reasoning for Anthropic', () => { - expect( - buildGameCreatorWizardConfig( - { llm: { webSearchEnabled: true } }, - { - apiKey: 'fixture-secret', - baseUrl: 'https://api.anthropic.com/', - model: 'claude-fixture', - apiKind: 'anthropic', - }, - ).llm, - ).toMatchObject({ reasoningEffort: 'default', webSearchEnabled: false }); - }); -}); - -describe('terminal configuration wizard URL safety', () => { - it.each([ - ['https://provider.example.test/v1///', 'https://provider.example.test/v1'], - ['http://127.0.0.1:8080/v1/', 'http://127.0.0.1:8080/v1'], - ['http://localhost:8080/', 'http://localhost:8080'], - ['http://[::1]:8080/v1/', 'http://[::1]:8080/v1'], - ])('accepts %s', (input, expected) => { - expect(normalizeWizardBaseUrl(input)).toBe(expected); - }); - - it.each([ - '', - 'not-a-url', - 'http://provider.example.test/v1', - 'ftp://provider.example.test/v1', - 'https://user:password@provider.example.test/v1', - 'https://provider.example.test/v1?token=fixture', - 'https://provider.example.test/v1#fragment', - ])('rejects unsafe Base URL %s', (input) => { - expect(() => normalizeWizardBaseUrl(input)).toThrowError(); - }); -}); - -describe('terminal configuration wizard persistence', () => { - it('atomically replaces a private AppData config file', async () => { - await withTemporaryRoot(async (root) => { - const configDir = path.join(root, 'nested', appIdentifier); - const configPath = path.join(configDir, configFileName); - const firstConfig = { llm: { model: 'first-fixture-model' } }; - const finalConfig = { - llm: { model: 'final-fixture-model', apiKey: 'fixture-secret' }, - agentLlm: { 'project-supervisor': { model: 'agent-fixture-model' } }, - }; - - await expect( - writeGameCreatorConfigAtomically( - configPath, - firstConfig, - skippedWindowsAclOptions, - ), - ).resolves.toBe(configPath); - const firstMetadata = await lstat(configPath); - await expect( - writeGameCreatorConfigAtomically( - configPath, - finalConfig, - skippedWindowsAclOptions, - ), - ).resolves.toBe(configPath); - - const [directoryMetadata, finalMetadata, entries, contents] = - await Promise.all([ - lstat(configDir), - lstat(configPath), - readdir(configDir), - readFile(configPath, 'utf8'), - ]); - expect(JSON.parse(contents)).toEqual(finalConfig); - expect(contents.endsWith('\n')).toBe(true); - expect(entries).toEqual([configFileName]); - expect(finalMetadata.isFile()).toBe(true); - expect(finalMetadata.isSymbolicLink()).toBe(false); - if (process.platform !== 'win32') { - expect(directoryMetadata.mode & 0o077).toBe(0); - expect(finalMetadata.mode & 0o077).toBe(0); - expect([finalMetadata.dev, finalMetadata.ino]).not.toEqual([ - firstMetadata.dev, - firstMetadata.ino, - ]); - } - }); - }); - - it('moves the default LLM to primary config and lets later GUI saves win', async () => { - await withTemporaryRoot(async (root) => { - const configDir = path.join(root, appIdentifier); - const primaryPath = path.join(configDir, configFileName); - const localPath = path.join(configDir, localConfigFileName); - await mkdir(configDir); - await writeFile( - primaryPath, - '{"llm":{"model":"primary","requestTimeoutMs":12345},"editorApi":{"apiKey":"canvas"}}\n', - ); - await writeFile( - localPath, - '{"llm":{"model":"stale-local","stream":true},"agentLlm":{"planner":{"model":"planner"}}}\n', - ); - - const state = await readGameCreatorWizardConfigState( - configDir, - skippedWindowsAclOptions, - ); - expect(state.configPath).toBe(primaryPath); - expect(state.effectiveConfig.llm.model).toBe('stale-local'); - const wizardConfig = buildGameCreatorWizardConfig(state.writeConfig, { - apiKey: 'wizard-key', - baseUrl: 'https://provider.example.test/v1', - model: 'wizard-model', - apiKind: 'openai_chat', - }); - await writeGameCreatorWizardConfig( - state, - wizardConfig, - skippedWindowsAclOptions, - ); - - const sanitizedLocal = JSON.parse(await readFile(localPath, 'utf8')); - expect(sanitizedLocal.llm).toBeUndefined(); - expect(sanitizedLocal.agentLlm.planner.model).toBe('planner'); - const afterWizard = await readGameCreatorWizardConfigState( - configDir, - skippedWindowsAclOptions, - ); - expect(afterWizard.effectiveConfig.llm).toMatchObject({ - apiKey: 'wizard-key', - model: 'wizard-model', - stream: true, - requestTimeoutMs: 12345, - }); - - const guiConfig = JSON.parse(await readFile(primaryPath, 'utf8')); - guiConfig.llm = { - ...guiConfig.llm, - apiKey: 'gui-key', - model: 'gui-model', - }; - await writeGameCreatorConfigAtomically( - primaryPath, - guiConfig, - skippedWindowsAclOptions, - ); - const afterGui = await readGameCreatorWizardConfigState( - configDir, - skippedWindowsAclOptions, - ); - expect(afterGui.effectiveConfig.llm.apiKey).toBe('gui-key'); - expect(afterGui.effectiveConfig.llm.model).toBe('gui-model'); - }); - }); -}); - -describe('Swarm test argument parsing', () => { - it('returns the documented defaults', () => { - expect(parseSwarmTestArguments([])).toEqual({ - configDir: null, - projectDir: null, - keepProject: false, - openBrowser: true, - task: null, - timeoutMinutes: null, - dryRun: false, - help: false, - }); - }); - - it('parses every supported option', () => { - const configDir = path.resolve('fixture-config'); - const projectDir = path.resolve('fixture-project'); - - expect( - parseSwarmTestArguments([ - '--config-dir', - configDir, - '--project-dir', - projectDir, - '--keep-project', - '--no-open', - '--task', - '生成一款可试玩的塔防游戏', - '--timeout-minutes', - '75', - '--dry-run', - '--help', - ]), - ).toEqual({ - configDir, - projectDir, - keepProject: true, - openBrowser: false, - task: '生成一款可试玩的塔防游戏', - timeoutMinutes: 75, - dryRun: true, - help: true, - }); - expect(parseSwarmTestArguments(['-h']).help).toBe(true); - expect(() => parseSwarmTestArguments(['--task', ''])).toThrowError(); - }); - - it('auto-answers only the confirmation and choice prompts', () => { - expect( - nextSwarmAutoPilotReply( - '[待确认] agent=project-supervisor run=r action=a tool=agent.delegate\n输入 approve 或 reject:', - ), - ).toBe('approve'); - expect(nextSwarmAutoPilotReply('请选择 1-3,或直接输入其他答案:')).toBe( - '1', - ); - // 提示词已经被上一轮消费掉、不在缓冲末尾时不得重复应答。 - expect( - nextSwarmAutoPilotReply('输入 approve 或 reject:\n[已批准] action-1\n'), - ).toBeNull(); - expect( - nextSwarmAutoPilotReply('[状态] project-supervisor running'), - ).toBeNull(); - }); - - it('closes the driven stdin only at the prompt that follows submission', () => { - // CLI 先打印提示符再读行:第一个「你>」是用来读我们这条任务的,此时本轮还没 - // 开始跑。在那里收 stdin,确认卡弹出来时已经是 EOF,自动应答器根本没机会回 - // approve——实测就这样把一轮跑成了 pending-confirmation。 - expect(swarmAutoPilotSitsAtPrompt('你> ')).toBe(true); - expect(swarmAutoPilotShouldCloseInput('你> ', 0)).toBe(false); - expect(swarmAutoPilotShouldCloseInput('你> ', 1)).toBe(true); - // 总控判定直接回复那条路径没有 turn 回执,同样靠这个提示符收口。 - expect(swarmAutoPilotShouldCloseInput('[意图] reply\n\n你> ', 1)).toBe( - true, - ); - // 提示符不在缓冲末尾、或本轮还在推进时不得收 stdin。 - expect( - swarmAutoPilotSitsAtPrompt( - '你> [决策] Agent 正在判断直接回复或调用持久能力。', - ), - ).toBe(false); - expect( - swarmAutoPilotSitsAtPrompt('[状态] project-supervisor running/planning'), - ).toBe(false); - }); - - it('keeps persistent preview only for manual chat mode', () => { - expect(shouldStartPersistentPreview(parseSwarmTestArguments([]))).toBe( - true, - ); - expect( - shouldStartPersistentPreview( - parseSwarmTestArguments(['--task', '生成一款可试玩的塔防游戏']), - ), - ).toBe(false); - }); - - it('applies a bounded default only to non-interactive tasks', () => { - expect(resolveSwarmTestTimeoutMs(parseSwarmTestArguments([]))).toBeNull(); - expect( - resolveSwarmTestTimeoutMs( - parseSwarmTestArguments(['--task', '生成原创塔防游戏']), - ), - ).toBe(50 * 60_000); - expect( - resolveSwarmTestTimeoutMs( - parseSwarmTestArguments(['--timeout-minutes', '12']), - ), - ).toBe(12 * 60_000); - }); - - it.each([ - { - label: 'duplicate config directory', - args: ['--config-dir', '/first', '--config-dir', '/second'], - marker: '--config-dir', - }, - { - label: 'duplicate project directory', - args: ['--project-dir', '/first', '--project-dir', '/second'], - marker: '--project-dir', - }, - { - label: 'missing config directory', - args: ['--config-dir'], - marker: '--config-dir', - }, - { - label: 'missing project directory', - args: ['--project-dir', '--dry-run'], - marker: '--project-dir', - }, - { - label: 'duplicate timeout', - args: ['--timeout-minutes', '10', '--timeout-minutes', '20'], - marker: '--timeout-minutes', - }, - { - label: 'invalid timeout', - args: ['--timeout-minutes', '0'], - marker: '1-1440', - }, - { - label: 'unknown option', - args: ['--unsupported'], - marker: '--unsupported', - }, - ])('rejects $label', ({ args, marker }) => { - expect(() => parseSwarmTestArguments(args)).toThrow(marker); - }); -}); - -describe('runtime config directory candidates', () => { - it('uses an absolute Linux XDG config root', () => { - expect( - defaultRuntimeConfigDirCandidates({ - platform: 'linux', - environment: { XDG_CONFIG_HOME: '/fixture/xdg' }, - homeDirectory: '/fixture/home', - }), - ).toEqual([path.posix.join('/fixture/xdg', appIdentifier)]); - }); - - it('falls back to the Linux home config root', () => { - expect( - defaultRuntimeConfigDirCandidates({ - platform: 'linux', - environment: { XDG_CONFIG_HOME: 'relative-xdg' }, - homeDirectory: '/fixture/home', - }), - ).toEqual([path.posix.join('/fixture/home', '.config', appIdentifier)]); - }); - - it('uses the macOS Application Support directory', () => { - expect( - defaultRuntimeConfigDirCandidates({ - platform: 'darwin', - environment: {}, - homeDirectory: '/Users/fixture', - }), - ).toEqual([ - path.posix.join( - '/Users/fixture', - 'Library', - 'Application Support', - appIdentifier, - ), - ]); - }); - - it('uses both Windows roaming and local AppData directories', () => { - const appData = 'C:\\Users\\fixture\\AppData\\Roaming'; - const localAppData = 'C:\\Users\\fixture\\AppData\\Local'; - - expect( - defaultRuntimeConfigDirCandidates({ - platform: 'win32', - environment: { - APPDATA: appData, - LOCALAPPDATA: localAppData, - }, - homeDirectory: 'C:\\Users\\fixture', - }), - ).toEqual([ - path.win32.join(appData, appIdentifier), - path.win32.join(localAppData, appIdentifier), - ]); - }); -}); - -describe('runtime config discovery', () => { - it('passes an empty explicit config directory through the TTY wizard only', () => { - const explicitConfigDir = path.resolve('empty-explicit-config'); - const argumentsForWizard = - buildMissingConfigWizardArguments(explicitConfigDir); - - expect(argumentsForWizard.slice(1)).toEqual([ - '--configure-only', - '--config-dir', - explicitConfigDir, - ]); - expect(argumentsForWizard[0]).toMatch(/game-creator-config-wizard\.mjs$/u); - expect(canPromptForMissingRuntimeConfig(true, true)).toBe(true); - expect(canPromptForMissingRuntimeConfig(false, true)).toBe(false); - expect(canPromptForMissingRuntimeConfig(true, false)).toBe(false); - }); - - it('discovers an explicitly selected config directory', async () => { - await withTemporaryRoot(async (root) => { - const configDir = path.join(root, 'explicit-config'); - await mkdir(configDir); - await writeFile(path.join(configDir, configFileName), '{}\n'); - - await expect( - discoverRuntimeConfigDir(configDir, { - platform: 'linux', - environment: { XDG_CONFIG_HOME: path.join(root, 'unused') }, - homeDirectory: path.join(root, 'unused-home'), - }), - ).resolves.toBe(await realpath(configDir)); - }); - }); - - it('discovers a config directory from an isolated XDG root', async () => { - await withTemporaryRoot(async (root) => { - const xdgRoot = path.join(root, 'xdg'); - const configDir = path.join(xdgRoot, appIdentifier); - await mkdir(configDir, { recursive: true }); - await writeFile(path.join(configDir, configFileName), '{}\n'); - - await expect( - discoverRuntimeConfigDir(null, { - platform: 'linux', - environment: { XDG_CONFIG_HOME: xdgRoot }, - homeDirectory: path.join(root, 'unused-home'), - }), - ).resolves.toBe(await realpath(configDir)); - }); - }); - - it.skipIf(process.platform === 'win32')( - 'rejects symlinked and non-file config entries', - async () => { - await withTemporaryRoot(async (root) => { - const target = path.join(root, 'config-target.json'); - await writeFile(target, '{}\n'); - - const symlinkConfigDir = path.join(root, 'symlink-config'); - await mkdir(symlinkConfigDir); - await symlink(target, path.join(symlinkConfigDir, configFileName)); - await expect( - discoverRuntimeConfigDir(symlinkConfigDir), - ).rejects.toThrow(configFileName); - - const directoryConfigDir = path.join(root, 'directory-config'); - await mkdir(path.join(directoryConfigDir, configFileName), { - recursive: true, - }); - await expect( - discoverRuntimeConfigDir(directoryConfigDir), - ).rejects.toThrow(configFileName); - }); - }, - ); - - it('rejects a relative explicit config directory', async () => { - await expect(discoverRuntimeConfigDir('relative-config')).rejects.toThrow( - '--config-dir', - ); - }); -}); - -describe('isolated Swarm runtime config', () => { - it('privately copies only active config files and removes the owned directory', async () => { - await withTemporaryRoot(async (root) => { - const sourceConfigDir = path.join(root, 'source-config'); - await mkdir(sourceConfigDir); - await writeFile( - path.join(sourceConfigDir, configFileName), - '{"llm":{"apiKey":"fixture-credential"}}\n', - { mode: 0o600 }, - ); - await writeFile( - path.join(sourceConfigDir, localConfigFileName), - '{"llm":{"model":"fixture-model"}}\n', - { mode: 0o600 }, - ); - await writeFile( - path.join(sourceConfigDir, runnerEndpointFileName), - '{"mustNotCopy":true}\n', - ); - await writeFile( - path.join(sourceConfigDir, 'agent-runner.lock'), - 'must-not-copy\n', - ); - await writeFile( - path.join(sourceConfigDir, `.${configFileName}.previous`), - 'must-not-copy\n', - ); - const sourceMetadata = await lstat( - path.join(sourceConfigDir, configFileName), - ); - - const runtimeConfig = await prepareSwarmTestRuntimeConfig( - sourceConfigDir, - root, - skippedWindowsAclOptions, - ); - const runtimeEntries = (await readdir(runtimeConfig.path)).sort(); - const runtimeDirectoryMetadata = await lstat(runtimeConfig.path); - const primaryMetadata = await lstat( - path.join(runtimeConfig.path, configFileName), - ); - const localMetadata = await lstat( - path.join(runtimeConfig.path, localConfigFileName), - ); - const sentinel = JSON.parse( - await readFile( - path.join(runtimeConfig.path, testRuntimeConfigSentinelName), - 'utf8', - ), - ); - - expect(runtimeConfig.sourcePath).toBe(await realpath(sourceConfigDir)); - expect(path.basename(runtimeConfig.path)).toMatch( - new RegExp(`^${testRuntimeConfigPrefix}`), - ); - expect(runtimeEntries).toEqual( - [ - configFileName, - localConfigFileName, - testRuntimeConfigSentinelName, - ].sort(), - ); - expect( - await readFile(path.join(runtimeConfig.path, configFileName), 'utf8'), - ).toBe('{"llm":{"apiKey":"fixture-credential"}}\n'); - expect( - await readFile( - path.join(runtimeConfig.path, localConfigFileName), - 'utf8', - ), - ).toBe('{"llm":{"model":"fixture-model"}}\n'); - expect(sentinel).toEqual({ - schemaVersion: testRuntimeConfigSentinelSchema, - token: runtimeConfig.sentinelToken, - }); - if (process.platform !== 'win32') { - expect(runtimeDirectoryMetadata.mode & 0o077).toBe(0); - expect(primaryMetadata.mode & 0o077).toBe(0); - expect(localMetadata.mode & 0o077).toBe(0); - expect([primaryMetadata.dev, primaryMetadata.ino]).not.toEqual([ - sourceMetadata.dev, - sourceMetadata.ino, - ]); - } - expect( - await pathExists(path.join(runtimeConfig.path, runnerEndpointFileName)), - ).toBe(false); - - await expect(cleanupSwarmTestRuntimeConfig(runtimeConfig)).resolves.toBe( - true, - ); - expect(await pathExists(runtimeConfig.path)).toBe(false); - expect(await pathExists(sourceConfigDir)).toBe(true); - expect( - await readFile(path.join(sourceConfigDir, configFileName), 'utf8'), - ).toBe('{"llm":{"apiKey":"fixture-credential"}}\n'); - }); - }); - - it('rejects a symlinked local override without leaving a temporary directory', async () => { - if (process.platform === 'win32') return; - await withTemporaryRoot(async (root) => { - const sourceConfigDir = path.join(root, 'source-config'); - const localTarget = path.join(root, 'local-target.json'); - await mkdir(sourceConfigDir); - await writeFile(path.join(sourceConfigDir, configFileName), '{}\n'); - await writeFile(localTarget, '{}\n'); - await symlink( - localTarget, - path.join(sourceConfigDir, localConfigFileName), - ); - - await expect( - prepareSwarmTestRuntimeConfig( - sourceConfigDir, - root, - skippedWindowsAclOptions, - ), - ).rejects.toThrow(localConfigFileName); - expect( - (await readdir(root)).filter((entry) => - entry.startsWith(testRuntimeConfigPrefix), - ), - ).toEqual([]); - }); - }); - - it('refuses to delete an isolated config while its Runner endpoint exists', async () => { - await withTemporaryRoot(async (root) => { - const sourceConfigDir = path.join(root, 'source-config'); - await mkdir(sourceConfigDir); - await writeFile(path.join(sourceConfigDir, configFileName), '{}\n'); - const runtimeConfig = await prepareSwarmTestRuntimeConfig( - sourceConfigDir, - root, - skippedWindowsAclOptions, - ); - const endpointPath = path.join( - runtimeConfig.path, - runnerEndpointFileName, - ); - await writeFile(endpointPath, '{}\n'); - - await expect( - cleanupSwarmTestRuntimeConfig(runtimeConfig), - ).rejects.toThrow('Runner'); - expect(await pathExists(runtimeConfig.path)).toBe(true); - - await rm(endpointPath); - await expect(cleanupSwarmTestRuntimeConfig(runtimeConfig)).resolves.toBe( - true, - ); - }); - }); -}); - -describe('Swarm test project ownership', () => { - it('creates a sentinel-owned project and removes it during cleanup', async () => { - await withTemporaryRoot(async (root) => { - const project = await prepareSwarmTestProject(null, root); - const sentinelPath = path.join(project.path, testProjectSentinelName); - const sentinelMetadata = await lstat(sentinelPath); - const sentinel = JSON.parse(await readFile(sentinelPath, 'utf8')); - - expect(project.owned).toBe(true); - expect(project.sentinelToken).toEqual(expect.any(String)); - expect(path.basename(project.path)).toMatch( - new RegExp(`^${testProjectPrefix}`), - ); - expect(sentinelMetadata.isFile()).toBe(true); - expect(sentinelMetadata.isSymbolicLink()).toBe(false); - expect(sentinel).toEqual({ - schemaVersion: testProjectSentinelSchema, - token: project.sentinelToken, - }); - - await expect(cleanupSwarmTestProject(project)).resolves.toBe(true); - expect(await pathExists(project.path)).toBe(false); - }); - }); - - it('refuses cleanup after the sentinel identity is changed', async () => { - await withTemporaryRoot(async (root) => { - const project = await prepareSwarmTestProject(null, root); - await writeFile( - path.join(project.path, testProjectSentinelName), - `${JSON.stringify({ - schemaVersion: testProjectSentinelSchema, - token: 'changed-token', - })}\n`, - ); - - await expect(cleanupSwarmTestProject(project)).rejects.toThrowError(); - expect(await pathExists(project.path)).toBe(true); - }); - }); - - it('keeps an explicit empty directory unowned', async () => { - await withTemporaryRoot(async (root) => { - const explicitProjectDir = path.join(root, 'explicit-project'); - await mkdir(explicitProjectDir); - - const project = await prepareSwarmTestProject(explicitProjectDir, root); - - expect(project).toEqual({ - path: await realpath(explicitProjectDir), - owned: false, - sentinelToken: null, - }); - await expect(cleanupSwarmTestProject(project)).resolves.toBe(false); - expect(await readdir(explicitProjectDir)).toEqual([]); - }); - }); - - it('rejects a non-empty uninitialized explicit directory', async () => { - await withTemporaryRoot(async (root) => { - const explicitProjectDir = path.join(root, 'uninitialized-project'); - await mkdir(explicitProjectDir); - await writeFile(path.join(explicitProjectDir, 'existing.txt'), 'fixture'); - - await expect( - prepareSwarmTestProject(explicitProjectDir, root), - ).rejects.toThrow('--project-dir'); - }); - }); -}); - -describe('cargo CLI argument construction', () => { - it('uses the shell manifest and separates cargo from application arguments', () => { - const cliArguments = ['--config-dir', 'fixture-config', '--llm-status']; - const cargoArguments = buildCargoCliArguments(cliArguments); - - // Assert the separator invariants rather than fixed positions: cargo flags - // may be added before `--`, but everything after it must reach the CLI - // unchanged, and the manifest must stay this shell's own. - const separatorIndex = cargoArguments.indexOf('--'); - expect(cargoArguments[0]).toBe('run'); - expect(separatorIndex).toBeGreaterThan(0); - expect(cargoArguments.slice(separatorIndex + 1)).toEqual(cliArguments); - - const manifestIndex = cargoArguments.indexOf('--manifest-path'); - expect(manifestIndex).toBeGreaterThan(0); - expect(manifestIndex).toBeLessThan(separatorIndex); - expect(path.isAbsolute(cargoArguments[manifestIndex + 1])).toBe(true); - expect(path.relative(appRoot, cargoArguments[manifestIndex + 1])).toBe( - path.join('src-tauri', 'Cargo.toml'), - ); - }); - - it('keeps cargo build chatter out of the run output', () => { - // The dead-code warnings are reprinted on every spawn; without --quiet they - // bury the swarm output this script exists to surface. - const cargoArguments = buildCargoCliArguments(['--llm-status']); - const separatorIndex = cargoArguments.indexOf('--'); - - expect(cargoArguments.slice(0, separatorIndex)).toContain('--quiet'); - }); - - it('parses the idle Runner shutdown marker', () => { - expect(parseRunnerShutdownOutput('runner.stopped=true\n')).toBe(true); - expect(parseRunnerShutdownOutput('runner.stopped=false\n')).toBe(false); - expect(() => parseRunnerShutdownOutput('runner.status=unknown\n')).toThrow( - 'stopped', - ); - }); -}); - -describe('bounded process-tree termination', () => { - it('kills a POSIX process group after its leader exits with stdio still open', async () => { - if (process.platform === 'win32') return; - const child = spawn( - process.execPath, - [ - '-e', - `const { spawn } = require('node:child_process'); -const grandchild = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], { - stdio: 'inherit', -}); -grandchild.once('spawn', () => { - process.stdout.write('grandchild-ready\\n'); - setTimeout(() => process.exit(0), 50); -}); -grandchild.once('error', () => process.exit(2));`, - ], - { detached: true, stdio: ['ignore', 'pipe', 'pipe'] }, - ); - try { - child.stdout.setEncoding('utf8'); - const grandchildReady = new Promise((resolve, reject) => { - let output = ''; - child.stdout.on('data', (chunk) => { - output += chunk; - if (output.includes('grandchild-ready')) resolve(); - }); - child.once('error', reject); - }); - child.stderr.resume(); - const leaderExited = new Promise((resolve, reject) => { - child.once('error', reject); - child.once('exit', () => resolve()); - }); - await Promise.all([leaderExited, grandchildReady]); - expect(child.exitCode).toBe(0); - const startedAt = Date.now(); - await expect( - childExitWithTimeout(child, 20, 'fixture process tree', { - graceMs: 50, - forceWaitMs: 500, - }), - ).rejects.toMatchObject({ code: 'AGC_CHILD_TIMEOUT' }); - expect(Date.now() - startedAt).toBeLessThan(2_000); - } finally { - await terminateChildTree(child, 'SIGKILL', true); - } - }); - - it('bounds recursive cleanup in an independently terminable child', async () => { - if (process.platform === 'win32') return; - const target = await mkdtemp( - path.join(os.tmpdir(), 'genarrative-bounded-cleanup-test-'), - ); - try { - await expect( - removeDirectoryWithTimeout(target, { - timeoutMs: 20, - childProgram: 'setInterval(() => {}, 1000);', - }), - ).rejects.toMatchObject({ code: 'AGC_CHILD_TIMEOUT' }); - expect(await pathExists(target)).toBe(true); - } finally { - await rm(target, { recursive: true, force: true }); - } - }); -}); - -describe('non-interactive Swarm turn report validation', () => { - const settledReport = { - schemaVersion: 'game-creator-swarm-turn-report.v1', - outcome: 'settled', - parentAgentId: 'project-supervisor', - sessionId: 'agent-session-project-supervisor', - parentRunId: 'swarm-project-supervisor-fixture', - runtimeCount: 5, - busyRuntimeCount: 0, - pendingTaskCount: 0, - runningTaskCount: 0, - waitingForConfirmationCount: 0, - waitingForUserInputCount: 0, - newAssistantMessageCount: 1, - finalReplyChars: 128, - reconciliationAgentCount: 0, - }; - const outputFor = (...reports: unknown[]) => - [ - '[状态] 正在收束', - ...reports.map((report) => `[turn.report] ${JSON.stringify(report)}`), - '[完成] 本轮结束', - ].join('\n'); - - it('accepts exactly one settled report with no remaining work', () => { - expect(parseSettledSwarmTurnReport(outputFor(settledReport))).toEqual( - settledReport, - ); - }); - - it.each([ - ['missing', '[状态] 没有报告'], - ['duplicate', outputFor(settledReport, settledReport)], - ['malformed JSON', '[turn.report] {invalid'], - ['non-object JSON', '[turn.report] []'], - [ - 'incomplete shape', - outputFor( - (({ finalReplyChars: _removed, ...report }) => report)(settledReport), - ), - ], - ['unknown shape', outputFor({ ...settledReport, unexpected: true })], - ])('rejects a %s report', (_label, output) => { - expect(() => parseSettledSwarmTurnReport(output)).toThrowError(); - }); - - it.each(['failed', 'incomplete', 'needs-reconciliation'])( - 'rejects the %s outcome', - (outcome) => { - expect(() => - parseSettledSwarmTurnReport(outputFor({ ...settledReport, outcome })), - ).toThrow(`outcome=${outcome}`); - }, - ); - - it.each([ - 'busyRuntimeCount', - 'pendingTaskCount', - 'runningTaskCount', - 'waitingForConfirmationCount', - 'waitingForUserInputCount', - 'reconciliationAgentCount', - ])('rejects non-zero %s', (field) => { - expect(() => - parseSettledSwarmTurnReport(outputFor({ ...settledReport, [field]: 1 })), - ).toThrow(field); - }); - - it.each([ - ['no Runtime', { runtimeCount: 0 }], - ['no assistant reply', { newAssistantMessageCount: 0 }], - ['multiple assistant replies', { newAssistantMessageCount: 2 }], - ['empty final reply', { finalReplyChars: 0 }], - ['implausibly large final reply', { finalReplyChars: 1_000_001 }], - ['fractional count', { pendingTaskCount: 0.5 }], - ['missing parent run', { parentRunId: null }], - ])('rejects %s', (_label, overrides) => { - expect(() => - parseSettledSwarmTurnReport( - outputFor({ ...settledReport, ...overrides }), - ), - ).toThrowError(); - }); -}); - -describe('generated game entry validation', () => { - it('rejects the initializer placeholder and accepts generated HTML', async () => { - await withTemporaryRoot(async (root) => { - const gameDir = path.join(root, 'game'); - const gameEntry = path.join(gameDir, 'index.html'); - await mkdir(gameDir); - - expect(await hasGeneratedGameEntry(root)).toBe(false); - await writeFile( - gameEntry, - `
${ungeneratedGameEntryMarker}
`, - ); - expect(await hasGeneratedGameEntry(root)).toBe(false); - - await writeFile(gameEntry, ''); - expect(await hasGeneratedGameEntry(root)).toBe(true); - }); - }); -}); - -describe('formal Swarm project artifact validation', () => { - it('reports every missing required artifact by relative path', async () => { - await withTemporaryRoot(async (root) => { - const inspection = await inspectSwarmProjectArtifacts(root); - - expect(inspection.valid).toBe(false); - expect(inspection.invalidPaths).toEqual([ - ...Object.keys(minimumFormalArtifactContents), - '.agent/manifest.json', - '.agent/runtime/project-revision.json', - '.agent/agent.db', - ]); - const validationError = await validateSwarmProjectArtifacts(root).then( - () => null, - (error: Error) => error, - ); - expect(validationError).toBeInstanceOf(Error); - for (const relativePath of Object.keys(minimumFormalArtifactContents)) { - expect(validationError?.message).toContain(relativePath); - } - - await mkdir(path.join(root, 'exports')); - await writeFile(path.join(root, 'exports', 'README.md'), ' \n'); - const emptyInspection = await inspectSwarmProjectArtifacts(root); - expect(emptyInspection.issues).toContainEqual({ - path: 'exports/README.md', - reason: '空文件', - }); - - if (process.platform !== 'win32') { - const target = path.join(root, 'project-target.md'); - await mkdir(path.join(root, 'memory')); - await writeFile(target, '# Outside artifact path\n'); - await symlink(target, path.join(root, 'memory', 'project.md')); - const symlinkInspection = await inspectSwarmProjectArtifacts(root); - expect(symlinkInspection.issues).toContainEqual({ - path: 'memory/project.md', - reason: '不是无符号链接普通文件', - }); - } - }); - }); - - it('rejects each malformed JSON artifact', async () => { - await withTemporaryRoot(async (root) => { - await writeMinimumFormalArtifacts(root); - const invalidJsonPaths = [ - 'game/balance.json', - 'assets/manifest.art.json', - 'assets/manifest.audio.json', - ]; - await Promise.all( - invalidJsonPaths.map((relativePath) => - writeFile(path.join(root, ...relativePath.split('/')), '{invalid'), - ), - ); - - const inspection = await inspectSwarmProjectArtifacts(root); - - expect(inspection.invalidPaths).toEqual(invalidJsonPaths); - expect(inspection.issues).toEqual( - invalidJsonPaths.map((relativePath) => ({ - path: relativePath, - reason: 'JSON 无法解析', - })), - ); - }); - }); - - it('rejects placeholder Markdown and empty JSON objects', async () => { - await withTemporaryRoot(async (root) => { - await writeMinimumFormalArtifacts(root); - const projectMemoryPath = path.join(root, 'memory', 'project.md'); - await writeFile(projectMemoryPath, '# TODO\n\n待补充项目说明。\n'); - let inspection = await inspectSwarmProjectArtifacts(root); - expect(inspection.issues).toContainEqual({ - path: 'memory/project.md', - reason: '仍包含占位标记', - }); - - await writeFile( - projectMemoryPath, - minimumFormalArtifactContents['memory/project.md'], - ); - const gameEntryPath = path.join(root, 'game', 'index.html'); - await writeFile( - gameEntryPath, - '\n', - ); - inspection = await inspectSwarmProjectArtifacts(root); - expect(inspection.issues).toContainEqual({ - path: 'game/index.html', - reason: '仍包含占位标记', - }); - - await writeFile( - gameEntryPath, - minimumFormalArtifactContents['game/index.html'], - ); - await writeFile(path.join(root, 'game', 'balance.json'), '{}\n'); - inspection = await inspectSwarmProjectArtifacts(root); - expect(inspection.issues).toContainEqual({ - path: 'game/balance.json', - reason: 'JSON 必须是非空对象', - }); - }); - }); - - it.each([ - ['TODO', 'TODO: 补齐导出说明'], - ['TBD', 'TBD - export notes'], - ['placeholder', 'This is a placeholder document.'], - ['coming soon', 'Release notes are coming soon.'], - ['lorem ipsum', 'Lorem ipsum dolor sit amet.'], - ['待补充', '导出说明待补充。'], - ['待完善', '移动端结论待完善。'], - ['占位', '本文件仅供流程占位。'], - ['尚未完成', '最终试玩尚未完成。'], - ['稍后补充', '截图说明稍后补充。'], - ['待填写', '版本信息待填写。'], - ['待验证', '桌面视口待验证。'], - ['待复核', '最终结论待复核。'], - ['待确认', '发布范围待确认。'], - ['待定', '交付日期待定。'], - ['unchecked checklist', '- [ ] 补齐移动端试玩记录'], - ])('rejects exports/README.md containing %s', async (_label, marker) => { - expect(hasIncompleteArtifactMarker(`# Export\n\n${marker}\n`)).toBe(true); - - await withTemporaryRoot(async (root) => { - await writeMinimumFormalArtifacts(root); - await writeFile( - path.join(root, 'exports', 'README.md'), - `# Export\n\n当前导出流程记录如下。\n\n${marker}\n`, - ); - - const inspection = await inspectSwarmProjectArtifacts(root); - - expect(inspection.issues).toContainEqual({ - path: 'exports/README.md', - reason: '仍包含占位标记', - }); - }); - }); - - it('accepts complete formal Markdown and checked task lists', async () => { - const completeReadme = [ - '# Export', - '', - '- [x] 桌面视口试玩验证已经通过', - '- [X] 移动视口试玩验证已经通过', - '', - '版本信息已经填写,静态检查、人工复核和发布范围确认均已完成。', - '', - ].join('\n'); - expect(hasIncompleteArtifactMarker(completeReadme)).toBe(false); - - await withTemporaryRoot(async (root) => { - await writeMinimumFormalArtifacts(root); - await writeFile(path.join(root, 'exports', 'README.md'), completeReadme); - - await expect(validateSwarmProjectArtifacts(root)).resolves.toMatchObject({ - valid: true, - }); - }); - }); - - it('requires all 16 manifest tasks and browser evidence for the current revision', async () => { - await withTemporaryRoot(async (root) => { - await writeMinimumFormalArtifacts(root); - const manifestPath = path.join(root, '.agent', 'manifest.json'); - const manifest = JSON.parse(await readFile(manifestPath, 'utf8')); - manifest.tasks[0].status = 'running'; - await writeFile(manifestPath, `${JSON.stringify(manifest)}\n`); - - let inspection = await inspectSwarmProjectArtifacts(root); - expect(inspection.issues).toContainEqual({ - path: '.agent/manifest.json', - reason: '固定 16 个正式任务未全部且仅完成一次', - }); - - manifest.tasks[0].status = 'completed'; - await writeFile(manifestPath, `${JSON.stringify(manifest)}\n`); - await writeFile( - path.join(root, '.agent', 'runtime', 'project-revision.json'), - '{"revision":9}\n', - ); - inspection = await inspectSwarmProjectArtifacts(root); - expect(inspection.issues).toContainEqual({ - path: '.agent/agent.db', - reason: '缺少当前 revision 的桌面与移动试玩通过凭证', - }); - }); - }); - - it('binds all 16 task lifecycles exactly once to the settled parent run', async () => { - await withTemporaryRoot(async (root) => { - const parentRunId = 'swarm-project-supervisor-exactly-once'; - await writeMinimumFormalArtifacts(root); - await writeReadyTaskExactlyOnceEvidence(root, parentRunId); - - await expect( - validateSwarmProjectArtifacts(root, { parentRunId }), - ).resolves.toMatchObject({ valid: true }); - - const databasePath = path.join(root, '.agent', 'agent.db'); - const baselineDatabase = await readFile(databasePath, 'utf8'); - const failedThenCompletedTaskId = requiredSwarmManifestTaskIds[0]; - await writeFile( - databasePath, - `${baselineDatabase}${JSON.stringify({ - recordType: 'agent.runtime.background_task.failed', - agentId: failedThenCompletedTaskId, - taskId: failedThenCompletedTaskId, - runId: `autonomous-ready-${failedThenCompletedTaskId}-fixture`, - source: 'agent-ready-task-scheduler', - })}\n`, - ); - const failedThenCompleted = await inspectSwarmProjectArtifacts(root, { - parentRunId, - }); - expect(failedThenCompleted.issues).toContainEqual({ - path: `.agent/runtime/tasks/${failedThenCompletedTaskId}.jsonl`, - reason: `正式任务 ${failedThenCompletedTaskId} 未在当前父 Run 中恰好启动并完成一次`, - }); - await writeFile(databasePath, baselineDatabase); - - const duplicateTaskId = requiredSwarmManifestTaskIds[0]; - await writeFile( - databasePath, - `${baselineDatabase}${JSON.stringify({ - recordType: 'agent.runtime.background_task', - agentId: duplicateTaskId, - taskId: duplicateTaskId, - runId: `autonomous-ready-${duplicateTaskId}-fixture`, - source: 'agent-ready-task-scheduler', - })}\n`, - ); - const duplicate = await inspectSwarmProjectArtifacts(root, { - parentRunId, - }); - expect(duplicate.issues).toContainEqual({ - path: `.agent/runtime/tasks/${duplicateTaskId}.jsonl`, - reason: `正式任务 ${duplicateTaskId} 未在当前父 Run 中恰好启动并完成一次`, - }); - - const secondRunTaskId = requiredSwarmManifestTaskIds[1]; - const journalPath = path.join( - root, - '.agent', - 'runtime', - 'tasks', - `${secondRunTaskId}.jsonl`, - ); - await writeFile( - journalPath, - `${await readFile(journalPath, 'utf8')}${JSON.stringify({ - agentId: secondRunTaskId, - taskId: secondRunTaskId, - runId: `autonomous-ready-${secondRunTaskId}-second-attempt`, - source: 'agent-ready-task-scheduler', - runProfile: 'autonomous-game-build', - parentAgentId: 'project-supervisor', - parentRunId, - status: 'completed', - phase: 'completed', - })}\n`, - ); - const secondRun = await inspectSwarmProjectArtifacts(root, { - parentRunId, - }); - expect(secondRun.invalidPaths).toContain( - `.agent/runtime/tasks/${secondRunTaskId}.jsonl`, - ); - }); - }); - - it('rejects a stale static smoke record from an older revision', async () => { - await withTemporaryRoot(async (root) => { - await writeMinimumFormalArtifacts(root); - const databasePath = path.join(root, '.agent', 'agent.db'); - const records = (await readFile(databasePath, 'utf8')) - .trim() - .split('\n') - .map((line) => JSON.parse(line)); - records[0].revision = 7; - await writeFile( - databasePath, - `${records.map((record) => JSON.stringify(record)).join('\n')}\n`, - ); - - const inspection = await inspectSwarmProjectArtifacts(root); - expect(inspection.issues).toContainEqual({ - path: '.agent/agent.db', - reason: '缺少当前 revision 的静态检查通过凭证', - }); - }); - }); - - it('requires valid image files only when the editor API key is configured', async () => { - await withTemporaryRoot(async (root) => { - await writeMinimumFormalArtifacts(root); - const configDir = path.join(root, 'config'); - await mkdir(configDir); - await writeFile( - path.join(configDir, configFileName), - '{"editorApi":{"apiKey":" "}}\n', - ); - expect(await hasConfiguredEditorApiKey(configDir)).toBe(false); - await expect(validateSwarmProjectArtifacts(root)).resolves.toMatchObject({ - valid: true, - requireEditorImages: false, - }); - - await writeFile( - path.join(configDir, localConfigFileName), - '{"editorApi":{"apiKey":"fixture-editor-key"}}\n', - ); - expect(await hasConfiguredEditorApiKey(configDir)).toBe(true); - await expect( - validateSwarmProjectArtifacts(root, { requireEditorImages: true }), - ).rejects.toThrow('assets/ui-prototype.png'); - - await writeFile( - path.join(root, 'assets', 'ui-prototype.png'), - Buffer.from('not-an-image'), - ); - await writeFile( - path.join(root, 'assets', 'art-spritesheet.png'), - Buffer.from([0xff, 0xd8, 0xff, 0xe0]), - ); - await expect( - validateSwarmProjectArtifacts(root, { requireEditorImages: true }), - ).rejects.toThrow('assets/ui-prototype.png'); - - await writeFile( - path.join(root, 'assets', 'ui-prototype.png'), - Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), - ); - await expect( - validateSwarmProjectArtifacts(root, { requireEditorImages: true }), - ).rejects.toThrow('assets/ui-prototype.png'); - - const headerOnlyPng = Buffer.alloc(1_024); - fixturePngSignature.copy(headerOnlyPng); - headerOnlyPng.writeUInt32BE(13, 8); - headerOnlyPng.write('IHDR', 12, 'ascii'); - headerOnlyPng.writeUInt32BE(1_600, 16); - headerOnlyPng.writeUInt32BE(900, 20); - await writeFile( - path.join(root, 'assets', 'ui-prototype.png'), - headerOnlyPng, - ); - await expect( - validateSwarmProjectArtifacts(root, { requireEditorImages: true }), - ).rejects.toThrow('assets/ui-prototype.png'); - - await writeFile( - path.join(root, 'assets', 'ui-prototype.png'), - fixturePng(1_600, 900), - ); - await writeFile( - path.join(root, 'assets', 'art-spritesheet.png'), - fixturePng(1_024, 1_024), - ); - await expect( - validateSwarmProjectArtifacts(root, { requireEditorImages: true }), - ).resolves.toMatchObject({ valid: true, requireEditorImages: true }); - }); - }); - - it('validates chunk CRC, zlib scanlines, filters, and screenshot PNGs', async () => { - const validPng = fixturePng(1_600, 900); - expect(validatePngBytes(validPng)).toEqual({ width: 1_600, height: 900 }); - - const crcCorrupted = Buffer.from(validPng); - crcCorrupted[42] ^= 0xff; - expect(() => validatePngBytes(crcCorrupted)).toThrow('CRC'); - expect(() => validatePngBytes(validPng.subarray(0, -1))).toThrowError(); - expect(() => - validatePngBytes(fixturePng(320, 180, { invalidFilter: true })), - ).toThrow('filter byte'); - expect(() => - validatePngBytes(fixturePng(320, 180, { trailingCompressedBytes: true })), - ).toThrow('zlib'); - expect(() => - validatePngBytes(fixtureIndexedPng({ includePalette: false })), - ).toThrow('PLTE'); - expect(() => - validatePngBytes(fixtureIndexedPng({ duplicatePalette: true })), - ).toThrow('PLTE'); - expect(() => - validatePngBytes(fixtureIndexedPng({ unknownCriticalChunk: true })), - ).toThrow('critical chunk'); - expect(validatePngBytes(fixtureIndexedPng())).toEqual({ - width: 1, - height: 1, - }); - - await withTemporaryRoot(async (root) => { - await writeMinimumFormalArtifacts(root); - const desktopScreenshot = path.join( - root, - '.agent', - 'runtime', - 'browser-validations', - 'publish-package', - 'test-run', - '8', - 'desktop.png', - ); - const fakeScreenshot = Buffer.alloc(2_048); - fixturePngSignature.copy(fakeScreenshot); - await writeFile(desktopScreenshot, fakeScreenshot); - - const inspection = await inspectSwarmProjectArtifacts(root); - expect(inspection.issues).toContainEqual({ - path: '.agent/agent.db', - reason: '缺少 desktop 试玩截图', - }); - }); - }); - - it('rejects non-PNG bytes at the fixed PNG artifact paths', async () => { - await withTemporaryRoot(async (root) => { - await writeMinimumFormalArtifacts(root); - await writeFile( - path.join(root, 'assets', 'ui-prototype.png'), - Buffer.from('RIFF\x04\x00\x00\x00WEBP', 'binary'), - ); - await writeFile( - path.join(root, 'assets', 'art-spritesheet.png'), - Buffer.from('GIF89a', 'ascii'), - ); - - await expect( - validateSwarmProjectArtifacts(root, { requireEditorImages: true }), - ).rejects.toThrow('assets/ui-prototype.png'); - }); - }); -}); - -describe('preview URL validation', () => { - it('accepts an HTTP URL on the numeric loopback host', () => { - const previewUrl = 'http://127.0.0.1:4173/'; - - expect(validatePreviewUrl(previewUrl)).toBe(previewUrl); - }); - - it.each([ - 'https://127.0.0.1:4173/', - 'http://localhost:4173/', - 'http://[::1]:4173/', - 'http://192.0.2.1:4173/', - 'http://127.0.0.1.example:4173/', - 'http://127.0.0.1/', - 'http://127.0.0.1:4173/play', - 'http://127.0.0.1:4173/?mode=test', - 'http://127.0.0.1:4173/#ready', - 'http://user@127.0.0.1:4173/', - 'file:///fixture/game/index.html', - 'not-a-url', - ])('rejects %s', (previewUrl) => { - expect(() => validatePreviewUrl(previewUrl)).toThrowError(); - }); -}); - -describe('package script registration', () => { - it('registers the root and app config and test commands exactly', async () => { - const [rootPackage, appPackage, checkConfigSource] = await Promise.all( - [ - new URL('../../../package.json', import.meta.url), - new URL('../package.json', import.meta.url), - new URL('../scripts/check-config.mjs', import.meta.url), - ].map(async (packageUrl) => - packageUrl.pathname.endsWith('.json') - ? JSON.parse(await readFile(packageUrl, 'utf8')) - : readFile(packageUrl, 'utf8'), - ), - ); - - expect(rootPackage.scripts?.['agc:config']).toBe( - 'npm --prefix apps/ai-game-creator-shell run config --', - ); - expect(rootPackage.scripts?.['agc:test']).toBe( - 'npm --prefix apps/ai-game-creator-shell run agent-runtime:supervisor-autonomous-playable-lane-defense-deterministic-e2e --', - ); - expect(rootPackage.scripts?.['agc:test:chat']).toBe( - 'npm --prefix apps/ai-game-creator-shell run test:chat --', - ); - expect(rootPackage.scripts?.['agc:test:chat:manual']).toBe( - 'npm --prefix apps/ai-game-creator-shell run test:chat:manual --', - ); - expect(appPackage.scripts?.['test:chat']).toBe( - `node scripts/agent-swarm-test-chat.mjs --task "${defaultRealSwarmTestTask}" --no-open`, - ); - expect(appPackage.scripts?.config).toBe( - 'node scripts/game-creator-config-wizard.mjs', - ); - expect(appPackage.scripts?.['test:chat:manual']).toBe( - 'node scripts/agent-swarm-test-chat.mjs', - ); - expect(checkConfigSource).toMatch( - /packageConfig\.scripts\?\.config\s*!==\s*'node scripts\/game-creator-config-wizard\.mjs'/u, - ); - expect(checkConfigSource).toMatch( - /rootPackageConfig\.scripts\?\.\['agc:config'\]\s*!==\s*'npm --prefix apps\/ai-game-creator-shell run config --'/u, - ); - const wrapperSource = await readFile( - new URL('../scripts/run-cli-with-config.mjs', import.meta.url), - 'utf8', - ); - expect(wrapperSource).toContain('resolveGameCreatorAppConfigDir'); - expect(wrapperSource).toContain( - "'--config-dir', resolveGameCreatorAppConfigDir()", - ); - }); -}); diff --git a/apps/ai-game-creator-shell/tests/agentTraceSummary.test.ts b/apps/ai-game-creator-shell/tests/agentTraceSummary.test.ts deleted file mode 100644 index a49f3db21..000000000 --- a/apps/ai-game-creator-shell/tests/agentTraceSummary.test.ts +++ /dev/null @@ -1,284 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { - createGameCreationAppSeedTasks, - GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, - type GameCreationAgentRunTrace, -} from '../../../packages/shared/src/contracts/gameCreationApp'; -import { summarizeAgentRunTrace } from '../src/App'; - -describe('AI 游戏创作 Agent loop 摘要', () => { - it('shows repair loop state in the chat trace summary', () => { - const tasks = createGameCreationAppSeedTasks(); - tasks[0]!.status = 'completed'; - tasks[1]!.status = 'completed'; - - const trace: GameCreationAgentRunTrace = { - schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, - runId: 'run-test', - commandId: 'game.generate_draft', - status: 'needs-revision', - lifecycleStatus: 'pending', - passes: 2, - maxPasses: 3, - toolCallCount: 12, - maxToolCalls: 128, - stopReason: 'max-passes-exhausted', - goal: '做一个弹幕厨房游戏', - coordination: 'Planner -> Orchestrator -> Generator -> Evaluator', - steps: [ - ...Array.from({ length: 7 }, (_, index) => ({ - pass: 1, - agent: `LLM-${index + 1}`, - phase: 'llm', - taskId: null, - group: null, - role: null, - status: 'completed', - inputPaths: [], - outputPaths: [], - summary: `LLM 调用 ${index + 1}`, - toolCalls: [ - { - toolId: `llm.call.${index + 1}`, - status: 'ok', - inputPaths: [], - outputPaths: [], - summary: `LLM 工具 ${index + 1}`, - }, - ], - })), - { - pass: 2, - agent: 'Orchestrator', - phase: 'plan', - taskId: 'code-director', - group: 'code', - role: 'Code', - status: 'completed', - inputPaths: ['.agent/findings.md'], - outputPaths: ['.agent/passes/pass-2/task-graph.json'], - summary: '重跑程序链路及下游发布包装', - toolCalls: [], - }, - ...Array.from({ length: 4 }, (_, index) => ({ - pass: 2, - agent: `Bridge-${index + 1}`, - phase: 'handoff', - taskId: null, - group: null, - role: null, - status: 'completed', - inputPaths: [], - outputPaths: [], - summary: `中间步骤 ${index + 1}`, - toolCalls: [], - })), - { - pass: 2, - agent: '美术组 / Asset', - phase: 'role-brief', - taskId: 'art-asset-plan', - group: 'art', - role: 'Asset', - status: 'completed', - inputPaths: ['.agent/manifest.json'], - outputPaths: ['.agent/passes/pass-2/groups/art/asset.md'], - summary: '需要回流画板角色素材', - toolCalls: [ - { - toolId: 'agent.role.brief.art.asset', - status: 'completed', - inputPaths: ['.agent/manifest.json'], - outputPaths: ['.agent/passes/pass-2/groups/art/asset.md'], - summary: '规划角色资产', - }, - { - toolId: 'agent.tool.suggest.canvas.project_sync', - status: 'suggested', - inputPaths: ['.agent/manifest.json'], - outputPaths: [], - summary: - '项目还没有画板回流资产;建议用户确认 /sync-canvas-project <画板项目ID>。', - }, - ...Array.from({ length: 5 }, (_, index) => ({ - toolId: `agent.tool.suggest.extra.${index + 1}`, - status: 'suggested', - inputPaths: ['.agent/manifest.json'], - outputPaths: [], - summary: `额外建议命令 ${index + 1}`, - })), - ], - }, - ], - artifacts: [ - { - path: '.agent/older-artifact.json', - sizeBytes: 64, - checksum: 'fnv1a64:older', - }, - { - path: '.agent/passes/pass-2/task-graph.json', - sizeBytes: 128, - checksum: 'fnv1a64:test', - }, - ...Array.from({ length: 4 }, (_, index) => ({ - path: `.agent/passes/pass-2/artifact-${index + 1}.json`, - sizeBytes: 128 + index, - checksum: `fnv1a64:artifact-${index + 1}`, - })), - ], - taskGraph: { - goal: '做一个弹幕厨房游戏', - readyTaskIds: ['code-director'], - activeTaskIds: ['code-director', 'quality-review', 'publish-package'], - carriedTaskIds: ['design-director'], - repairFocus: ['gameHtml 缺少 canvas'], - repairRoutes: [ - { - issue: 'gameHtml 缺少 canvas', - taskIds: [ - 'code-director', - 'quality-review', - 'preview-readiness', - 'publish-package', - ], - reason: 'code-repair+dependency-impact', - }, - { - issue: '缺少输入绑定', - taskIds: ['code-director'], - reason: 'input-binding', - }, - { - issue: '缺少胜负条件', - taskIds: ['quality-review'], - reason: 'win-condition', - }, - { - issue: '缺少发布说明', - taskIds: ['publish-package'], - reason: 'publish-readme', - }, - ], - tasks, - }, - passPlans: [ - { - pass: 1, - mode: 'initial', - summary: '第 1 轮全量调度', - activeTaskIds: ['design-director', 'code-director'], - carriedTaskIds: [], - dependencyWaves: [['design-director'], ['code-director']], - repairFocus: [], - repairRoutes: [], - }, - { - pass: 2, - mode: 'repair', - summary: '第 2 轮重跑程序链路及下游发布包装', - activeTaskIds: ['code-director', 'quality-review', 'publish-package'], - carriedTaskIds: ['design-director'], - dependencyWaves: [ - ['code-director'], - ['quality-review'], - ['publish-package'], - ], - repairFocus: ['gameHtml 缺少 canvas'], - repairRoutes: [ - { - issue: 'gameHtml 缺少 canvas', - taskIds: [ - 'code-director', - 'quality-review', - 'preview-readiness', - 'publish-package', - ], - reason: 'code-repair+dependency-impact', - }, - { - issue: '缺少输入绑定', - taskIds: ['code-director'], - reason: 'input-binding', - }, - { - issue: '缺少胜负条件', - taskIds: ['quality-review'], - reason: 'win-condition', - }, - { - issue: '缺少发布说明', - taskIds: ['publish-package'], - reason: 'publish-readme', - }, - ], - }, - { - pass: 3, - mode: 'repair', - summary: '第 3 轮复核', - activeTaskIds: ['quality-review'], - carriedTaskIds: ['design-director'], - dependencyWaves: [['quality-review']], - repairFocus: [], - repairRoutes: [], - }, - { - pass: 4, - mode: 'repair', - summary: '第 4 轮收尾', - activeTaskIds: ['publish-package'], - carriedTaskIds: [], - dependencyWaves: [['publish-package']], - repairFocus: [], - repairRoutes: [], - }, - ], - nextStep: 'repair-next-pass', - error: null, - updatedAt: 1, - }; - - const summary = summarizeAgentRunTrace(trace); - - expect(summary).toContain( - 'needs-revision / pending · 2/3 轮 · max-passes-exhausted', - ); - expect(summary).toContain('工具调用:12/128'); - expect(summary).toContain('任务:已完成 2'); - expect(summary).toContain( - 'active 任务:程序组 / Director 拆解程序实现(code-director), 程序组 / Review 执行质量评审(quality-review), 运营组 / Publish 整理发布包装(publish-package)', - ); - expect(summary).toContain( - 'carry-over 任务:设计实现组 / Director 拆解创作方向(design-director)', - ); - expect(summary).toContain('返工焦点:gameHtml 缺少 canvas'); - expect(summary).toContain( - '返工路线:code-repair+dependency-impact: 程序组 / Director 拆解程序实现(code-director), 程序组 / Review 执行质量评审(quality-review), 程序组 / Preview 执行静态自检(preview-readiness), 运营组 / Publish 整理发布包装(publish-package)', - ); - expect(summary).toContain('还有 1 条路线'); - expect(summary).not.toContain('publish-readme:'); - expect(summary).toContain('建议命令:'); - expect(summary).toContain('agent.tool.suggest.canvas.project_sync'); - expect(summary).toContain('/sync-canvas-project <画板项目ID>'); - expect(summary).toContain('agent.tool.suggest.extra.4'); - expect(summary).not.toContain('agent.tool.suggest.extra.5'); - expect(summary).toContain('还有 1 个建议命令'); - expect(summary).toContain('编排轮次:'); - expect(summary).toContain( - 'pass 2 · repair · active 程序组 / Director 拆解程序实现(code-director), 程序组 / Review 执行质量评审(quality-review), 运营组 / Publish 整理发布包装(publish-package) · carry 设计实现组 / Director 拆解创作方向(design-director) · waves 程序组 / Director 拆解程序实现(code-director) / 程序组 / Review 执行质量评审(quality-review) / 运营组 / Publish 整理发布包装(publish-package) · repair gameHtml 缺少 canvas · routes code-repair+dependency-impact: 程序组 / Director 拆解程序实现(code-director), 程序组 / Review 执行质量评审(quality-review), 程序组 / Preview 执行静态自检(preview-readiness), 运营组 / Publish 整理发布包装(publish-package)', - ); - expect(summary).not.toContain('pass 1 · initial'); - expect(summary).toContain('还有 1 个较早轮次'); - expect(summary).toContain('.agent/passes/pass-2/task-graph.json'); - expect(summary).not.toContain('.agent/older-artifact.json'); - expect(summary).toContain('还有 1 个较早产物'); - expect(summary).not.toContain('Orchestrator #2 · completed · plan'); - expect(summary).toContain('Bridge-1 #2 · completed · handoff'); - expect(summary).toContain('还有 8 个较早步骤'); - expect(summary).toContain('LLM-2 #1 · completed · llm · llm.call.2'); - expect(summary).not.toContain('LLM-1 #1 · completed · llm · llm.call.1'); - expect(summary).toContain('还有 1 个较早 LLM 步骤'); - }); -}); diff --git a/apps/ai-game-creator-shell/tests/appSurface/harness.ts b/apps/ai-game-creator-shell/tests/appSurface/harness.ts index a1ca82d1b..9322d1f5f 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/harness.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/harness.ts @@ -136,7 +136,6 @@ import { deriveAgentStatusCards, WorkspaceLauncher, } from '../../src/App'; -import { projectNameFromPath } from '../../src/features/agent-runtime/model'; import ProjectDevelopmentView from '../../src/view/project-development'; const testAuthUser: AuthUser = { @@ -314,11 +313,6 @@ async function setComposerText(element: HTMLElement, value: string) { await settleComposer(); } -async function submitChat(value: string) { - await setComposerText(screen.getByLabelText('创作想法'), value); - fireEvent.click(screen.getByRole('button', { name: '发送' })); -} - // 同一张资源卡上的按钮变少了:卡片本体只有「选中资源」与媒体播放钮两个(原先右上角 // 那个 @ 引用圆钮已挪进选中工具条)。选中按钮的可访问名模板固定为 // `选中资源:<分类标签> <资源文件名>`;分类标签可能自带空格(例如 `UI 交互`), @@ -1394,14 +1388,6 @@ function createProjectChatRuntimeHarness({ }; } -async function openMainProject(projectPath: string) { - await submitChat(`/project ${projectPath}`); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText(`已打开:${projectNameFromPath(projectPath)}`), - ).not.toBeNull(); -} - export function installResizeObserverStub() { let observerCount = 0; let observerDisconnected = false; @@ -1552,7 +1538,6 @@ export { it, mockRoleAgentReply, nativeClipboardMock, - openMainProject, openResourceFilterPanel, pickProjectFromLauncher, planningResponseStream, @@ -1568,7 +1553,6 @@ export { roleAgentMockReply, screen, setComposerText, - submitChat, TEST_LOCAL_PROJECT_PATH, testAuthUser, vi, diff --git a/apps/ai-game-creator-shell/tests/appSurface/project-conversation.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/project-conversation.suite.ts index bc3191d9a..61c65cead 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/project-conversation.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/project-conversation.suite.ts @@ -13,7 +13,6 @@ import { renderAppAt, roleAgentMockReply, screen, - submitChat, vi, waitFor, within, @@ -79,163 +78,6 @@ export function registerProjectConversationTests() { ); }); - it.skip('loads project conversation history after opening from chat command', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'read_local_conversation') { - return { - path: '/tmp/authorized-game/.agent/conversations/project.jsonl', - agentId: null, - messages: [ - { - schemaVersion: 'game-creator-conversation.v1', - role: 'user', - content: '历史需求:保留弹幕厨房', - agentId: null, - updatedAt: 1, - }, - { - schemaVersion: 'game-creator-conversation.v1', - role: 'assistant', - content: '历史回复:继续做第二版', - agentId: null, - updatedAt: 2, - }, - ], - }; - } - if (command === 'read_local_project_file') { - throw new Error('missing trace'); - } - if (command === 'list_local_project_files') { - return { projectPath: String(args?.projectPath ?? ''), files: [] }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect(await screen.findByText('历史需求:保留弹幕厨房')).not.toBeNull(); - expect(screen.getByText('历史回复:继续做第二版')).not.toBeNull(); - expect( - screen.queryByText('已设置本地项目:/tmp/authorized-game'), - ).toBeNull(); - expect(invoke).toHaveBeenCalledWith('read_local_conversation', { - projectPath: '/tmp/authorized-game', - agentId: null, - }); - expect(invoke).not.toHaveBeenCalledWith( - 'append_local_conversation_message', - expect.anything(), - ); - }); - - it.skip('reloads project conversation history from chat on demand', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'chat_with_game_creator_agent') { - return { - replyText: `主聊天回复:${String(args?.prompt ?? '')}`, - }; - } - if (command === 'read_local_conversation') { - return { - path: '/tmp/authorized-game/.agent/conversations/project.jsonl', - agentId: null, - messages: [ - { - schemaVersion: 'game-creator-conversation.v1', - role: 'user', - content: '重载历史需求', - agentId: null, - updatedAt: 1, - }, - { - schemaVersion: 'game-creator-conversation.v1', - role: 'assistant', - content: '重载历史回复', - agentId: null, - updatedAt: 2, - }, - ], - }; - } - if (command === 'read_local_project_file') { - throw new Error('missing trace'); - } - if (command === 'list_local_project_files') { - return { projectPath: String(args?.projectPath ?? ''), files: [] }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect(await screen.findByText('重载历史需求')).not.toBeNull(); - - await submitChat('临时未保存的输入'); - expect(await screen.findByText('临时未保存的输入')).not.toBeNull(); - await submitChat('/history'); - - expect(await screen.findByText('重载历史回复')).not.toBeNull(); - expect(screen.queryByText('临时未保存的输入')).toBeNull(); - expect(screen.getByText('已读取项目对话历史:2 条')).not.toBeNull(); - - await submitChat('另一条临时未保存的输入'); - expect(await screen.findByText('另一条临时未保存的输入')).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '历史' })); - - expect(await screen.findByText('重载历史回复')).not.toBeNull(); - expect(screen.queryByText('另一条临时未保存的输入')).toBeNull(); - expect(invoke).toHaveBeenCalledWith('read_local_conversation', { - projectPath: '/tmp/authorized-game', - agentId: null, - }); - }); - it.skip('requires confirmation before reading a specific agent conversation when policy asks for it', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', diff --git a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts index 119b1463f..75667a54d 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts @@ -32,12 +32,9 @@ import { act, cleanup, createGameCreationAppManifest, - createGameCreationAppSeedTasks, expect, findResourceSelectButton, fireEvent, - GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, - type GameCreationAgentRunTrace, getResourceSelectButton, installResizeObserverStub, it, @@ -6732,169 +6729,6 @@ export function registerProjectAgentStatusTests() { ).toBe(false); }); - it.skip('confirms before refreshing agents when trace read policy requires it', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const trace: GameCreationAgentRunTrace = { - schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, - runId: 'run-agent-refresh-confirm', - commandId: 'game.generate_draft', - status: 'running', - passes: 1, - maxPasses: 3, - toolCallCount: 1, - maxToolCalls: 128, - stopReason: 'running', - goal: '做一个厨房弹幕游戏', - coordination: 'Planner', - steps: [], - artifacts: [], - taskGraph: { - goal: '做一个厨房弹幕游戏', - readyTaskIds: [], - activeTaskIds: [], - carriedTaskIds: [], - repairFocus: [], - repairRoutes: [], - tasks: createGameCreationAppSeedTasks(), - }, - passPlans: [], - nextStep: 'continue', - error: null, - updatedAt: 1, - }; - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_local_conversation') { - return { - path: '/tmp/authorized-game/.agent/conversations/project.jsonl', - agentId: null, - messages: [], - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy: { - deniedCommands: [], - confirmCommands: ['agent.trace_read'], - }, - }; - } - if (command === 'read_local_project_file') { - return { - path: '.agent/run.latest.json', - absolutePath: `${String(args?.projectPath ?? '')}/.agent/run.latest.json`, - content: JSON.stringify(trace), - }; - } - if (command === 'list_local_project_files') { - return { projectPath: String(args?.projectPath ?? ''), files: [] }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game'); - invoke.mockClear(); - - fireEvent.click(screen.getByRole('button', { name: '刷新 Agent' })); - - expect(await screen.findByText('agent.trace_read')).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'read_local_project_file', - expect.anything(), - ); - - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - await waitFor(() => { - expect(invoke).toHaveBeenCalledWith('read_local_project_file', { - projectPath: '/tmp/authorized-game', - relativePath: '.agent/run.latest.json', - commandId: 'agent.trace_read', - }); - }); - }); - - it.skip('cancels agent run trace refresh policy confirmation from the panel', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_local_conversation') { - return { - path: '/tmp/authorized-game/.agent/conversations/project.jsonl', - agentId: null, - messages: [], - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy: { - deniedCommands: [], - confirmCommands: ['agent.trace_read'], - }, - }; - } - if (command === 'read_local_project_file') { - throw new Error('should wait for trace confirmation'); - } - if (command === 'list_local_project_files') { - return { projectPath: String(args?.projectPath ?? ''), files: [] }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game'); - invoke.mockClear(); - - fireEvent.click(screen.getByRole('button', { name: '刷新 Agent' })); - - const traceReadCommand = await screen.findByText('agent.trace_read'); - fireEvent.click( - within( - traceReadCommand.closest('.pending-command') as HTMLElement, - ).getByRole('button', { name: '取消' }), - ); - - expect( - await screen.findByText('run: 已取消读取 Agent trace'), - ).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'read_local_project_file', - expect.anything(), - ); - }); - /** * 资源总览(main 态)必须为**每个非空栏目**挂载真实卡片本体。 * diff --git a/apps/ai-game-creator-shell/tests/chatPromptPolish.test.tsx b/apps/ai-game-creator-shell/tests/chatPromptPolish.test.tsx index c179df4fc..4e9456d6d 100644 --- a/apps/ai-game-creator-shell/tests/chatPromptPolish.test.tsx +++ b/apps/ai-game-creator-shell/tests/chatPromptPolish.test.tsx @@ -203,15 +203,6 @@ describe('发送前提醒判据', () => { reminderDisabled: false, }), ).toBe(false); - const command = `/${'长'.repeat(60)}`; - expect( - shouldRemindChatPromptPolish({ - content: textContent(command), - prompt: command, - acknowledgedDraftKey: null, - reminderDisabled: false, - }), - ).toBe(false); }); test('changes the draft key when the references change', () => { @@ -494,22 +485,12 @@ describe('聊天输入区 AI 润色与发送前提醒', () => { }); }); - test('does not hold short prompts or slash commands', () => { + test('does not hold short prompts', () => { const shortSubmit = renderComposer({ initialText: '做个跳跃游戏' }); fireEvent.click(sendButton()); expect(screen.queryByRole('dialog', { name: '发送前提醒' })).toBeNull(); expect(shortSubmit).toHaveBeenCalledWith({ content: textContent('做个跳跃游戏'), }); - - cleanup(); - const commandSubmit = renderComposer({ - initialText: `/${'命令'.repeat(40)}`, - }); - fireEvent.click(sendButton()); - expect(screen.queryByRole('dialog', { name: '发送前提醒' })).toBeNull(); - expect(commandSubmit).toHaveBeenCalledWith({ - content: textContent(`/${'命令'.repeat(40)}`), - }); }); }); diff --git a/apps/ai-game-creator-shell/tests/rememberCommand.test.ts b/apps/ai-game-creator-shell/tests/rememberCommand.test.ts index 95f4ed246..0ab3b9fd8 100644 --- a/apps/ai-game-creator-shell/tests/rememberCommand.test.ts +++ b/apps/ai-game-creator-shell/tests/rememberCommand.test.ts @@ -9,11 +9,10 @@ import { deriveAgentStatusCards, isAbsoluteProjectPath, needsInitializedChatProject, - parseRememberInput, resolveChatProjectPath, } from '../src/App'; -describe('AI 游戏创作聊天记忆命令', () => { +describe('AI 游戏创作项目路径与 Agent 状态卡', () => { it('recognizes local project absolute paths across desktop platforms', () => { expect(isAbsoluteProjectPath('/tmp/game')).toBe(true); expect(isAbsoluteProjectPath('C:\\Games\\demo')).toBe(true); @@ -21,34 +20,7 @@ describe('AI 游戏创作聊天记忆命令', () => { expect(isAbsoluteProjectPath('relative-game')).toBe(false); }); - it('defaults /remember to long memory and supports short and blackboard scopes', () => { - expect(parseRememberInput('主角喜欢反弹弹幕')).toEqual({ - scope: 'long', - content: '主角喜欢反弹弹幕', - }); - expect(parseRememberInput('short 本轮先修输入手感')).toEqual({ - scope: 'short', - content: '本轮先修输入手感', - }); - expect(parseRememberInput('长期 保留厨房主题')).toEqual({ - scope: 'long', - content: '保留厨房主题', - }); - expect(parseRememberInput('project 覆盖后的长期设定')).toEqual({ - scope: 'long', - content: '覆盖后的长期设定', - }); - expect(parseRememberInput('blackboard 共享美术约束')).toEqual({ - scope: 'blackboard', - content: '共享美术约束', - }); - expect(parseRememberInput('黑板 统一使用俯视角')).toEqual({ - scope: 'blackboard', - content: '统一使用俯视角', - }); - }); - - it('requires an initialized local project path before chat memory commands', () => { + it('requires an initialized local project path', () => { expect(resolveChatProjectPath(null)).toBeNull(); expect(resolveChatProjectPath({ projectPath: '/tmp/game' })).toBe( '/tmp/game', @@ -57,17 +29,9 @@ describe('AI 游戏创作聊天记忆命令', () => { expect( resolveChatProjectPath({ projectPath: '/tmp/bad\u0007game' }), ).toBeNull(); - expect(parseRememberInput('long')).toEqual({ - scope: 'long', - content: '', - }); - expect(parseRememberInput('short 覆盖本轮上下文')).toEqual({ - scope: 'short', - content: '覆盖本轮上下文', - }); }); - it('requires a project before chat commands write or run local artifacts', () => { + it('requires a project before local artifacts are written or run', () => { expect(needsInitializedChatProject('game.generate_draft')).toBe(true); expect(needsInitializedChatProject('asset.upload')).toBe(true); expect(needsInitializedChatProject('agent.kill')).toBe(true);