diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/autonomous_policy.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/autonomous_policy.rs index 8c8c4f998..d2c62d7d5 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/autonomous_policy.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/autonomous_policy.rs @@ -1609,6 +1609,24 @@ mod tests { let root = temporary.path().join("project"); init_local_game_project_at(&root, "manifest-dag-policy", "测试正式任务图等待") .expect("init manifest DAG policy project"); + let session_id = resolve_agent_conversation_session_id_at( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + None, + true, + ) + .expect("resolve manifest DAG Supervisor session"); + append_unique_game_creator_agent_runtime_pending_task( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + &session_id, + "测试正式任务图等待", + "manifest-dag-policy-run", + AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE, + Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD), + None, + ) + .expect("queue trusted autonomous root task"); assert!(!autonomous_manifest_dag_in_progress_at(&root).expect("read pending DAG")); update_manifest_task_status_at( diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop_tests.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop_tests.rs index 98dfec582..6981f58f4 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop_tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop_tests.rs @@ -937,14 +937,16 @@ async fn missing_completed_visual_asset_fails_same_child_without_retry() { "missing visual output must fail the current logical task" ); - assert!(schedule_autonomous_game_build_ready_tasks_at( + let scheduled_after_failure = schedule_autonomous_game_build_ready_tasks_at( &root, GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, PARENT_RUN_ID, 1, ) - .expect("repeat scheduling after visual failure") - .is_empty()); + .expect("schedule other ready work after visual failure"); + assert!(scheduled_after_failure + .iter() + .all(|scheduled| scheduled.state.agent_id != CHILD_ID)); let records = read_all_game_creator_agent_runtime_tasks(&game_creator_agent_runtime_task_path( &root, CHILD_ID, )) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/pending_execution.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/pending_execution.rs index 385d4066b..672ea98f7 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/pending_execution.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/pending_execution.rs @@ -1,6 +1,74 @@ use super::*; +async fn run_after_pending_stack_boundary( + future: std::pin::Pin + Send + 'static>>, +) -> T +where + T: Send + 'static, +{ + // Debug builds give pending execution, the background main loop, and queue draining large + // poll frames. The boxed future keeps that large frame out of its caller before a joined child + // task gives it an independent poll boundary. JoinSet still aborts the child if its parent + // continuation is dropped. + let mut tasks = tokio::task::JoinSet::new(); + tasks.spawn(future); + match tasks + .join_next() + .await + .expect("pending continuation task must exist") + { + Ok(output) => output, + Err(error) if error.is_panic() => std::panic::resume_unwind(error.into_panic()), + Err(error) => panic!("pending continuation task was cancelled: {error}"), + } +} + +async fn run_game_creator_agent_background_task_after_pending_stack_boundary( + root: PathBuf, + agent_id: String, + task: String, + runtime: AgentRuntimeState, + continuation: AgentRuntimeContinuationContext, +) -> AgentBackgroundTaskOutcome { + run_after_pending_stack_boundary(Box::pin(async move { + run_game_creator_agent_background_task_with_context( + root, + agent_id, + task, + runtime, + continuation, + ) + .await + })) + .await +} + +async fn drain_next_game_creator_agent_background_tasks_after_pending_stack_boundary( + root: PathBuf, + agent_id: String, +) { + run_after_pending_stack_boundary(Box::pin(async move { + drain_next_game_creator_agent_background_tasks(root, agent_id).await; + })) + .await; +} + pub(crate) async fn continue_game_creator_agent_pending_tool_action( + root: PathBuf, + agent_id: String, + pending: AgentRuntimePendingToolAction, + runtime: AgentRuntimeState, +) { + run_after_pending_stack_boundary(Box::pin(async move { + continue_game_creator_agent_pending_tool_action_within_stack_boundary( + root, agent_id, pending, runtime, + ) + .await; + })) + .await; +} + +async fn continue_game_creator_agent_pending_tool_action_within_stack_boundary( root: PathBuf, agent_id: String, mut pending: AgentRuntimePendingToolAction, @@ -61,7 +129,7 @@ pub(crate) async fn continue_game_creator_agent_pending_tool_action( usize::try_from(batch.loop_iteration.saturating_sub(1)).unwrap_or(usize::MAX); continuation.context_stalled = false; continuation.applied_steer_cursor = batch.planned_steer_cursor; - let outcome = run_game_creator_agent_background_task_with_context( + let outcome = run_game_creator_agent_background_task_after_pending_stack_boundary( root.clone(), agent_id.clone(), pending.task.clone(), @@ -70,7 +138,10 @@ pub(crate) async fn continue_game_creator_agent_pending_tool_action( ) .await; if matches!(outcome, AgentBackgroundTaskOutcome::Finished) { - drain_next_game_creator_agent_background_tasks(root, agent_id).await; + drain_next_game_creator_agent_background_tasks_after_pending_stack_boundary( + root, agent_id, + ) + .await; } return; } @@ -79,7 +150,8 @@ pub(crate) async fn continue_game_creator_agent_pending_tool_action( if !has_persisted_terminal_observation && stop_game_creator_agent_runtime_if_cancel_requested(&root, &mut runtime) { - drain_next_game_creator_agent_background_tasks(root, agent_id).await; + drain_next_game_creator_agent_background_tasks_after_pending_stack_boundary(root, agent_id) + .await; return; } if let Err(error) = validate_agent_runtime_pending_context(&root, &runtime, &pending) { @@ -341,14 +413,18 @@ pub(crate) async fn continue_game_creator_agent_pending_tool_action( &pending, &observation, ) { - drain_next_game_creator_agent_background_tasks(root, agent_id).await; + drain_next_game_creator_agent_background_tasks_after_pending_stack_boundary( + root, agent_id, + ) + .await; } return; } if observation.is_waiting_for_confirmation() && stop_game_creator_agent_runtime_if_cancel_requested(&root, &mut runtime) { - drain_next_game_creator_agent_background_tasks(root, agent_id).await; + drain_next_game_creator_agent_background_tasks_after_pending_stack_boundary(root, agent_id) + .await; return; } if observation.is_waiting_for_confirmation() { @@ -525,7 +601,8 @@ pub(crate) async fn continue_game_creator_agent_pending_tool_action( }), ); if stop_game_creator_agent_runtime_if_cancel_requested(&root, &mut runtime) { - drain_next_game_creator_agent_background_tasks(root, agent_id).await; + drain_next_game_creator_agent_background_tasks_after_pending_stack_boundary(root, agent_id) + .await; return; } if !auto_execution { @@ -702,7 +779,10 @@ pub(crate) async fn continue_game_creator_agent_pending_tool_action( runtime, &format!("恢复 Agent Runtime context bundle 失败:{error}"), ); - drain_next_game_creator_agent_background_tasks(root, agent_id).await; + drain_next_game_creator_agent_background_tasks_after_pending_stack_boundary( + root, agent_id, + ) + .await; return; } }; @@ -753,7 +833,10 @@ pub(crate) async fn continue_game_creator_agent_pending_tool_action( runtime, &format!("持久化 Agent Runtime context bundle 失败:{error}"), ); - drain_next_game_creator_agent_background_tasks(root, agent_id).await; + drain_next_game_creator_agent_background_tasks_after_pending_stack_boundary( + root, agent_id, + ) + .await; return; } }; @@ -787,7 +870,7 @@ pub(crate) async fn continue_game_creator_agent_pending_tool_action( return; } } - let outcome = run_game_creator_agent_background_task_with_context( + let outcome = run_game_creator_agent_background_task_after_pending_stack_boundary( root.clone(), agent_id.clone(), pending.task.clone(), @@ -796,7 +879,8 @@ pub(crate) async fn continue_game_creator_agent_pending_tool_action( ) .await; if matches!(outcome, AgentBackgroundTaskOutcome::Finished) { - drain_next_game_creator_agent_background_tasks(root, agent_id).await; + drain_next_game_creator_agent_background_tasks_after_pending_stack_boundary(root, agent_id) + .await; } } diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/supervisor_planning.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/supervisor_planning.rs index 641ce4acf..f2b2e0f69 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/supervisor_planning.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/supervisor_planning.rs @@ -300,55 +300,36 @@ async fn supervisor_autonomous_durable_batch_rejects_invalid_responsibilities_wi let AgentRuntimeProviderActionBatchPreparation::Ready(valid_batch) = preparation else { panic!("valid autonomous responsibilities must form a ready durable batch"); }; - assert_eq!(valid_batch.actions.len(), 2); + assert_eq!(valid_batch.actions.len(), 3); assert!(valid_batch.collaboration_contract.is_some()); let valid_batch_id = valid_batch.batch_id.clone(); - let mut quality_not_read_only = autonomous_initial_responsibility_actions_for_test( - "创建或修改 game/index.html,完成可直接试玩的游戏实现并执行静态验证。", - &["game/index.html"], - "直接修改 game/index.html,修复试玩阻塞并重新验证。", - &[], - ); - quality_not_read_only[1].input["acceptanceCriteria"] = serde_json::json!([ - "直接写入 game/index.html 修复试玩问题", - "修改后执行静态验证并交付新 revision" - ]); + let mut design_not_read_only = valid_autonomous_initial_responsibility_actions_for_test(); + design_not_read_only[0].input["task"] = serde_json::json!("直接修改项目并完成首轮策划实现。"); + design_not_read_only[0].input["acceptanceCriteria"] = serde_json::json!(["直接修改项目文件"]); + let mut design_with_artifacts = valid_autonomous_initial_responsibility_actions_for_test(); + design_with_artifacts[0].input["expectedArtifacts"] = + serde_json::json!(["game/game_design.md"]); + let mut art_missing_spec = valid_autonomous_initial_responsibility_actions_for_test(); + art_missing_spec[1].input["expectedArtifacts"] = serde_json::json!(["assets/art-preview.png"]); + let mut code_with_artifacts = valid_autonomous_initial_responsibility_actions_for_test(); + code_with_artifacts[2].input["expectedArtifacts"] = serde_json::json!(["game/index.html"]); let invalid_cases = vec![ ( - "code-missing-game-index", - "game/index.html", - autonomous_initial_responsibility_actions_for_test( - "创建可直接试玩的游戏实现并执行静态验证。", - &["game/main.js"], - "只读验收 game/index.html 的可玩性;不要修改任何项目文件。", - &[], - ), + "design-not-read-only", + "design-director", + design_not_read_only, ), ( - "code-read-only", - "code-prototype", - autonomous_initial_responsibility_actions_for_test( - "只读检查 game/index.html,不要修改任何项目文件。", - &["game/index.html"], - "只读验收 game/index.html 的可玩性;不要修改任何项目文件。", - &[], - ), - ), - ( - "quality-not-read-only", - "quality-review", - quality_not_read_only, - ), - ( - "quality-with-artifacts", + "design-with-artifacts", "expectedArtifacts", - autonomous_initial_responsibility_actions_for_test( - "创建或修改 game/index.html,完成可直接试玩的游戏实现并执行静态验证。", - &["game/index.html"], - "只读验收 game/index.html 的可玩性;不要修改任何项目文件。", - &["game/index.html"], - ), + design_with_artifacts, + ), + ("art-missing-spec", "assets/art-spec.png", art_missing_spec), + ( + "code-with-artifacts", + "expectedArtifacts", + code_with_artifacts, ), ]; for (case_name, expected_error, actions) in invalid_cases { @@ -389,12 +370,7 @@ async fn supervisor_autonomous_durable_batch_rejects_invalid_responsibilities_wi rewrite_autonomous_responsibility_batch_actions_for_test( &root, &mut legacy_v2_batch, - autonomous_initial_responsibility_actions_for_test( - "创建或修改 game/index.html,完成可直接试玩的游戏实现并执行静态验证。", - &["game/index.html"], - "直接修改 game/index.html,修复试玩阻塞并重新验证。", - &[], - ), + valid_autonomous_initial_responsibility_actions_for_test(), ); let legacy_v2_schema = "game-creator-provider-action-batch.v2"; legacy_v2_batch.schema_version = legacy_v2_schema.to_string(); @@ -600,68 +576,37 @@ async fn autonomous_game_build_repairs_supervisor_failed_playtest_stall_into_mut fs::remove_dir_all(root).ok(); } -fn autonomous_initial_responsibility_actions_for_test( - code_task: &str, - code_artifacts: &[&str], - quality_task: &str, - quality_artifacts: &[&str], -) -> Vec { +fn valid_autonomous_initial_responsibility_actions_for_test() -> Vec { vec![ - AgentRuntimeToolAction { - tool: "agent.delegate".to_string(), - reason: Some("委派程序 Agent 形成可玩入口".to_string()), - input: serde_json::json!({ - "agentId": "code-prototype", - "task": code_task, - "acceptanceCriteria": [ - "game/index.html 必须形成可直接试玩的完整入口", - "程序交付必须完成当前 revision 的静态验证" - ], - "expectedArtifacts": code_artifacts, - "repairOfDelegationId": null, - "runId": null - }), - }, - AgentRuntimeToolAction { - tool: "agent.delegate".to_string(), - reason: Some("委派质量 Agent 独立只读验收".to_string()), - input: serde_json::json!({ - "agentId": "quality-review", - "task": quality_task, - "acceptanceCriteria": [ - "只读核对可玩性、交互闭环和阻塞问题", - "返回可追溯的验收结论,不修改项目文件" - ], - "expectedArtifacts": quality_artifacts, - "repairOfDelegationId": null, - "runId": null - }), - }, + autonomous_initial_leader_responsibility_action_for_test("design-director", &[]), + autonomous_initial_leader_responsibility_action_for_test( + "art-director", + &["assets/art-spec.png"], + ), + autonomous_initial_leader_responsibility_action_for_test("code-director", &[]), ] } -fn valid_autonomous_initial_responsibility_actions_for_test() -> Vec { - autonomous_initial_responsibility_actions_for_test( - "创建或修改 game/index.html,完成可直接试玩的游戏实现并执行静态验证。", - &["game/index.html"], - "只读验收 game/index.html 的可玩性与交互闭环;不要修改任何项目文件。", - &[], - ) -} - -fn autonomous_art_director_responsibility_action_for_test( +fn autonomous_initial_leader_responsibility_action_for_test( + agent_id: &str, expected_artifacts: &[&str], ) -> AgentRuntimeToolAction { + let read_only = matches!(agent_id, "design-director" | "code-director"); AgentRuntimeToolAction { tool: "agent.delegate".to_string(), - reason: Some("委派美术总监生成统一视觉规范图".to_string()), + reason: Some("建立首批 Leader 专业规划".to_string()), input: serde_json::json!({ - "agentId": "art-director", - "task": "生成项目统一视觉规范图并写入项目资产目录。", - "acceptanceCriteria": [ - "使用画布生成接口产出后续 UI 与图集共用的规范图", - "生成结果必须登记为项目本地 icon-spec 资产" - ], + "agentId": agent_id, + "task": if read_only { + format!("由 {agent_id} 只读完成首轮专业规划,不得修改项目") + } else { + "生成首轮统一视觉规范图供后续专业 Agent 使用".to_string() + }, + "acceptanceCriteria": if read_only { + serde_json::json!(["只读输出专业规划,不得修改项目文件"]) + } else { + serde_json::json!(["生成并登记统一视觉规范图"]) + }, "expectedArtifacts": expected_artifacts, "repairOfDelegationId": null, "runId": null @@ -713,9 +658,7 @@ async fn supervisor_autonomous_initial_art_director_requires_canonical_art_spec_ ) .expect("start autonomous Supervisor runtime"); let mut actions = valid_autonomous_initial_responsibility_actions_for_test(); - actions.push(autonomous_art_director_responsibility_action_for_test(&[ - "assets/art-preview.png", - ])); + actions[1].input["expectedArtifacts"] = serde_json::json!(["assets/art-preview.png"]); let plan = supervisor_collaboration_plan_for_test(actions); let revision = read_game_creator_agent_runtime_project_revision(&root) .expect("read art contract project revision"); @@ -834,40 +777,21 @@ async fn supervisor_autonomous_initial_responsibilities_reject_invalid_plans_bef ) .expect("start autonomous Supervisor runtime"); - let code_missing_game_index = autonomous_initial_responsibility_actions_for_test( - "创建可直接试玩的游戏实现并执行静态验证。", - &["game/main.js"], - "只读验收 game/index.html 的可玩性;不要修改任何项目文件。", - &[], - ); - let code_read_only = autonomous_initial_responsibility_actions_for_test( - "只读检查 game/index.html,不要修改任何项目文件。", - &["game/index.html"], - "只读验收 game/index.html 的可玩性;不要修改任何项目文件。", - &[], - ); - let mut quality_not_read_only = autonomous_initial_responsibility_actions_for_test( - "创建或修改 game/index.html,完成可直接试玩的游戏实现并执行静态验证。", - &["game/index.html"], - "直接修改 game/index.html,修复试玩阻塞并重新验证。", - &[], - ); - quality_not_read_only[1].input["acceptanceCriteria"] = serde_json::json!([ - "直接写入 game/index.html 修复试玩问题", - "修改后执行静态验证并交付新 revision" - ]); - let quality_with_artifacts = autonomous_initial_responsibility_actions_for_test( - "创建或修改 game/index.html,完成可直接试玩的游戏实现并执行静态验证。", - &["game/index.html"], - "只读验收 game/index.html 的可玩性;不要修改任何项目文件。", - &["game/index.html"], - ); let valid = valid_autonomous_initial_responsibility_actions_for_test(); + let mut missing_design = valid.clone(); + missing_design.remove(0); + let mut design_not_read_only = valid.clone(); + design_not_read_only[0].input["task"] = serde_json::json!("直接修改项目完成策划实现"); + design_not_read_only[0].input["acceptanceCriteria"] = serde_json::json!(["直接修改项目文件"]); + let mut art_missing_spec = valid.clone(); + art_missing_spec[1].input["expectedArtifacts"] = serde_json::json!(["assets/art-preview.png"]); + let mut code_with_artifacts = valid.clone(); + code_with_artifacts[2].input["expectedArtifacts"] = serde_json::json!(["game/index.html"]); let responses = [ - ("code-missing-game-index", &code_missing_game_index), - ("code-read-only", &code_read_only), - ("quality-not-read-only", &quality_not_read_only), - ("quality-with-artifacts", &quality_with_artifacts), + ("missing-design", &missing_design), + ("design-not-read-only", &design_not_read_only), + ("art-missing-spec", &art_missing_spec), + ("code-with-artifacts", &code_with_artifacts), ("valid-responsibilities", &valid), ] .into_iter() @@ -904,7 +828,13 @@ async fn supervisor_autonomous_initial_responsibilities_reject_invalid_plans_bef .await .expect("repair invalid initial responsibilities") .expect("valid initial responsibility plan"); - assert_eq!(plan.actions, valid); + let mut expected_valid = valid.clone(); + expected_valid.sort_by(|left, right| { + left.input["agentId"] + .as_str() + .cmp(&right.input["agentId"].as_str()) + }); + assert_eq!(plan.actions, expected_valid); assert!(plan.response.is_empty()); let requests = (0..5) 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 47ffc7c36..ff819cc52 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 @@ -5036,24 +5036,36 @@ async fn provider_transient_retry_autonomous_upstream_400_retries_with_bounded_b .expect("project init"); let delegate_function = native_runtime_function_name("agent.delegate").expect("delegate function"); - let code_arguments = serde_json::json!({ - "reason": "委派可玩原型实现", + let design_arguments = serde_json::json!({ + "reason": "委派策划 Leader", "input": { - "agentId": "code-prototype", - "task": "实现可直接试玩的游戏原型", - "acceptanceCriteria": ["项目能够启动并完成最小玩法闭环"], - "expectedArtifacts": ["game/index.html"], + "agentId": "design-director", + "task": "只读拆解首轮玩法目标和专业分工,不得修改项目", + "acceptanceCriteria": ["只读输出策划规划,不得修改项目文件"], + "expectedArtifacts": [], "repairOfDelegationId": null, "runId": null } }) .to_string(); - let quality_arguments = serde_json::json!({ - "reason": "委派独立质量评审", + let art_arguments = serde_json::json!({ + "reason": "委派美术 Leader", "input": { - "agentId": "quality-review", - "task": "只读评审可玩性与闯关闭环,不要修改任何项目文件", - "acceptanceCriteria": ["只读给出阻塞试玩的问题和验收结论"], + "agentId": "art-director", + "task": "生成首轮统一视觉规范图供后续专业 Agent 使用", + "acceptanceCriteria": ["生成并登记统一视觉规范图"], + "expectedArtifacts": ["assets/art-spec.png"], + "repairOfDelegationId": null, + "runId": null + } + }) + .to_string(); + let code_arguments = serde_json::json!({ + "reason": "委派程序 Leader", + "input": { + "agentId": "code-director", + "task": "只读拆解首轮程序实现边界,不得修改项目", + "acceptanceCriteria": ["只读输出程序规划,不得修改项目文件"], "expectedArtifacts": [], "repairOfDelegationId": null, "runId": null @@ -5061,16 +5073,21 @@ async fn provider_transient_retry_autonomous_upstream_400_retries_with_bounded_b }) .to_string(); let recovered_response = native_agent_tool_plan_chat_response_with_calls(vec![ + ( + "call-autonomous-upstream-400-design", + delegate_function.as_str(), + design_arguments, + ), + ( + "call-autonomous-upstream-400-art", + delegate_function.as_str(), + art_arguments, + ), ( "call-autonomous-upstream-400-code", delegate_function.as_str(), code_arguments, ), - ( - "call-autonomous-upstream-400-quality", - delegate_function.as_str(), - quality_arguments, - ), ]); let (request_notice_sender, request_notice_receiver) = mpsc::channel(); let base_url = spawn_mock_llm_upstream_400_then_raw_response( @@ -5156,7 +5173,7 @@ async fn provider_transient_retry_autonomous_upstream_400_retries_with_bounded_b assert!(request_notice_receiver .recv_timeout(Duration::from_millis(100)) .is_err()); - assert_eq!(plan.actions.len(), 2); + assert_eq!(plan.actions.len(), 3); assert!(plan .actions .iter() diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_actions/action_execution.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_actions/action_execution.rs index cd234fa8e..c138e990a 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_actions/action_execution.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_actions/action_execution.rs @@ -707,7 +707,7 @@ async fn background_agent_runtime_can_schedule_ready_manifest_tasks() { )); let scheduled = - schedule_game_creator_agent_ready_tasks_at(&root, 0).expect("schedule ready tasks"); + schedule_game_creator_agent_ready_tasks_at(&root, 1).expect("schedule one ready task"); assert_eq!(scheduled.len(), 1); assert_eq!(scheduled[0].state.agent_id, "design-director"); assert_eq!(scheduled[0].state.source, "agent-ready-task-scheduler"); diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 8770963a3..d78966250 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -3986,6 +3986,14 @@ - 处理:先以 CAS 单独 commit `queued -> executing`,成功后才调 ToolHost;调用返回后再 commit observation。恢复见到 executing 或 ToolHost 返回 Unknown 时只能进入 reconciliation,不得自动重执行。重复 resume 不得继续增 revision 或重复 event。 - 验证:在“ToolHost 已调用、observation commit 失败”处注入故障,序列化快照并用新 engine 重载;断言重复 resume 后 ToolHost 计数仍为 1,且只有显式 reconcile observation 才恢复 running。 +## Runtime pending 恢复不能让大型 async frame 共用默认 worker 栈(2026-08-03) + +- 现象:Supervisor collaboration durable isolated spawn 恢复测试在默认 Tokio worker 栈下稳定 `stack overflow`;单独运行同样失败,提高 `RUST_MIN_STACK` 后通过。 +- 原因:不是业务递归。debug 构建中 pending action continuation、后台 task queue 和 Agent 主循环各自形成大型 async poll frame;恢复路径在同一次 poll 调用链直接进入下一层状态机,累计超过 worker 默认栈。 +- 处理:整个 pending continuation、它进入的后台主循环,以及完成、取消或失败后 drain 同 Agent 后续队列时,都必须跨越独立 Tokio task 轮询边界,使上层 poll 先退栈后再轮询下一层状态机。传入边界的 future 必须先装箱;若泛型 helper 直接持有大型 future,即使随后 `spawn`,调用方 async frame 仍会把它保留在默认 worker 栈上。边界必须保留结构化取消语义;当前使用 boxed future 与 `JoinSet`,父 continuation 被丢弃时同步 abort 子任务。不得只增大 CI 的 `RUST_MIN_STACK`,否则生产默认栈仍可能崩溃。 +- 验证:失败用例必须在未设置 `RUST_MIN_STACK` 时通过;同时覆盖 policy batch 全组、拒绝 pending 后重规划并 drain 下一任务,以及 pending/cancellation 回归,证明恢复不重复生成 isolated spawn、队列继续推进且父任务取消不遗留后台子任务。 +- 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/pending_execution.rs`。 + ## Provider 可扩展不能用一个全局 protocol 枚举代替实例隔离 - 现象:把 `openai_chat / openai_responses / anthropic` 直接当 Provider 身份,注册第二个同协议 endpoint 时发生 ID 冲突;或为方便调用把 API Key、base URL、raw-log 目录放进全局状态,并行请求后日志串目录。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 1fa52c0f1..605d98f1d 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -746,6 +746,7 @@ game-project/ - 开发模式可通过本地项目文件面板执行 `file.list/read/write/delete`,普通用户界面不暴露文件面板。 - 2026-07-17 起,同一 Runtime 文档的“V1.32 Runtime 强制 Supervisor 协作合同”作为 mixed swarm 可靠性事实源。项目可用 `.agent/collaboration-policy.json` 约束首波 static/isolated 模式、数量和 required static Agent;Runtime 在任何 child 副作用前整批校验并把合同指纹固化进 Provider batch v2。当前父 run 一旦形成 delivery/group,正式 `project-supervisor` 默认只负责编排、状态认领和验证,不再直接执行项目 mutation;专业 Agent/isolated child 权限与唯一 Supervisor 最终回复边界保持不变。 - 2026-07-17 V1.32 最终代码已完成独立真实 Provider PASS:首批 mixed batch、三 isolated child、Runner 强杀恢复、专业返工、宿主验证、唯一最终回复与零重复/残留/泄漏同时成立。真实报告计数、隔离重试配置和仍待收敛的 tool-plan repair 成本统一以 Runtime 文档 V1.32 章节与共享决策记录为准。 +- 2026-08-03 恢复执行补充约束:整个 pending action continuation、它进入的后台主循环,以及完成、取消或失败后 drain 同 Agent 后续队列时,都必须跨越独立 Tokio task 轮询边界,不能让 pending executor、task queue 与 Agent 主循环的大型 async poll frame 在同一 worker 调用栈连续嵌套。边界输入必须先装箱,避免泛型 helper 在真正 spawn 前仍把大型 future 保留在调用方 async frame;边界同时必须随父 continuation 取消子任务并保持 durable action、batch、run/session 身份及恢复防重语义,当前使用 boxed future 与 `JoinSet` 承担该约束。CI 和生产均使用默认 worker 栈验证,不以提高 `RUST_MIN_STACK` 代替代码边界。 - 2026-07-18 起,同一 Runtime 文档的“V1.34 动态隔离子 Agent writeScopes 命令绕过封堵”作为 isolated child 的现行能力事实源。在 scope-aware OS sandbox 完成前,动态 child 无条件禁用 `project.verify / project.git_commit / command.exec / command.start / command.stdin / preview.start / agent.delegate / agent.spawn_isolated / project.restore / agent.schedule_ready / canvas.asset_generate / task.create / task.update / blackboard.write` 和全部 MCP;原生工具策略统一显示 `denied`,模板、项目 policy 与用户确认均不能放宽。保留固定只读 `command.run_limited`、同身份 `command.output_read / command.poll / command.terminate`、既有预览的 `preview.validate`,以及严格位于 `writeScopes` 内的 `file.write / file.patch / file.delete / project.patchset`。 - V1.34 的新单动作在 confirmation 和 OS launcher 前拒绝;新多 action 原生 batch 只要含一个 denied member 就在独立 pending-action sidecar、confirmation、OS spawn、revision 和任何成员项目副作用前整批 abort,只保留 `aborted / nextActionIndex=0` batch 事实。旧 pending / approval / batch 真正进入执行器时仍重新应用当前 child 边界,旧 executing 未知结果继续进入既有 reconciliation。该安全收紧由恶意 sibling 写入、策略快照、batch、旧 pending 执行器重验和 isolated/mixed/collaboration/provider-batch 回归证明;不因本切片重跑已通过且 isolated mutation 为 0 的 V1.31/V1.32 外部 Provider suite。通用命令只有在后续 scope-aware OS sandbox 对所有后代强制同一 `writeScopes` 并通过独立决策与测试后才可重新评估开放。 - 2026-07-18 起,同一 Runtime 文档的“V1.35 多 ready isolated all-join 原子认领与恢复”作为 `agent.run_status` 同父 run 多 group 认领的现行事实源。Runtime 按 `delegationGroupId` 排序并一次性预取全部 join 锁;任一后续锁忙时保持零 delivery mutation、零 claim sidecar。全锁就绪后,同一 action 的 durable claim journal 按 `prepared -> committed -> observed` 推进;部分 commit 或 Runner 恢复只能复用该 journal 幂等补齐。只认领可完整放入优先 `readyIsolatedJoins` 观察预算的有序前缀,未观察旧 claim 可由后续 action 完整重放,但不创建第二份 isolated claim。每个 claimed delivery 必须由匹配原 action/group 的 journal 覆盖;无 journal 的旧 delivery 每轮只迁移一个原 action,已有 journal 不得扩写或状态倒退,跨 action group 归属冲突失败关闭。成功 observation 写入 pending sidecar 后只能把本轮完整输出的 claim 标记 `observed`,任一未观察或无 journal claim 继续阻断 finalization;每个 group 审计按 `actionId + delegationGroupId` 唯一,并在 Agent DB 锁内修复 torn tail、全量核对后幂等追加。 diff --git a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md index 943e1857f..cf62e1db0 100644 --- a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md +++ b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md @@ -60,7 +60,7 @@ Linux 本机多用户并发开发时,`npm run dev` 和 `npm run dev:*` 单模 AI 游戏创作客户端使用 `npm run agc`,开发态 game-chat 使用 `npm run agc:game-chat`。两个入口都先由 `apps/ai-game-creator-shell/scripts/start-tauri-dev.mjs` 在 Tauri CLI 启动前检查固定地址 `http://127.0.0.1:3080/`:只有端口空闲时才继续启动。现有 marker 只包含 API target,不能证明监听器属于当前 worktree;即使页面和 target 看似匹配,也不得复用已经存在的 3080。旧 worktree Vite、无响应监听器或非 AGC 服务一律在创建原生窗口前失败关闭,并提示先停止旧服务;启动器不擅自终止无法证明归属的进程。 -Tauri `beforeDevCommand` 默认与客户端构建并行,不能把上述检查只放在 `beforeDevCommand` 内:旧 3080 已就绪时,Tauri 可能先创建加载旧前端的窗口,随后配套后端才因代理不匹配退出。外层启动器会把 Tauri CLI 放入受控进程树;CLI 正常退出、启动失败或收到终止信号后,POSIX 先向保留的 PGID 发送 `SIGTERM`、有界等待后升级 `SIGKILL`,Windows 使用 `taskkill /PID /T /F`。配套后端和 Vite 仍由 `start-dev-stack.mjs` 各自持有,退出时同样有界收束,避免只剩客户端、Runner、Cargo 或旧订阅进程。排障时同时核对 3080 marker、`.app/dev-stack.json` 的实际 API URL 和进程 cwd;不要把“终端已返回”当成客户端及其 Runner 已退出的证据。 +Tauri `beforeDevCommand` 默认与客户端构建并行,不能把上述检查只放在 `beforeDevCommand` 内:旧 3080 已就绪时,Tauri 可能先创建加载旧前端的窗口,随后配套后端才因代理不匹配退出。外层启动器会把 Tauri CLI 放入受控进程树;CLI 正常退出、启动失败或收到终止信号后,POSIX 先向保留的 PGID 发送 `SIGTERM`、有界等待后升级 `SIGKILL`,Windows 使用 `taskkill /PID /T /F`。Linux 容器中的孤儿后代退出后可能暂时保留为 zombie,`kill(-PGID, 0)` 仍会返回成功;启动器必须结合 `/proc//stat` 判断同组是否还存在非 zombie 成员,不能把等待 PID 1 回收误报为清理失败。配套后端和 Vite 仍由 `start-dev-stack.mjs` 各自持有,退出时同样有界收束,避免只剩客户端、Runner、Cargo 或旧订阅进程。排障时同时核对 3080 marker、`.app/dev-stack.json` 的实际 API URL 和进程 cwd;不要把“终端已返回”当成客户端及其 Runner 已退出的证据。 Windows 本地 `npm run dev` / `npm run dev:api-server` / `npm run dev:bgfilter-worker` 会用空的 `RUSTC_WRAPPER` / `CARGO_BUILD_RUSTC_WRAPPER` 覆盖 `server-rs/.cargo/config.toml` 里的 `sccache`,从而直连真实 `rustc`。完整栈和 `dev:api-server` 把 API 与 BgFilter worker 作为一个 Rust 重启单元:源码变化时先停两个进程,再先启动并验活 worker、最后启动并验活 API,避免两个 `cargo run` 并发链接同一个 Windows 可执行文件。不要把 wrapper 绕过值写成 `rustc`;Cargo 会按 wrapper 协议调用 `rustc <真实rustc路径> - ...`,最终报 `multiple input filenames provided` 并导致 api-server 无法启动。排查本地启动失败时,先看 dev 日志是否出现该错误,再确认脚本注入的 wrapper 为空。