From 912234e6def75c7e46612963b76aecfe167e433f Mon Sep 17 00:00:00 2001 From: AIGameCreator App Date: Fri, 10 Jul 2026 05:27:19 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=9D=E7=95=99Agent=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E7=A1=AE=E8=AE=A4=E7=AD=89=E5=BE=85=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后台Runtime命中工具确认策略时停在waiting-for-confirmation。 任务队列和前端状态面板展示等待确认计数。 补充Rust和前端测试覆盖工具确认等待态。 同步AI游戏创作App技术方案和决策记录。 --- .../src-tauri/src/agent.rs | 156 ++++++-- .../src-tauri/src/main.rs | 3 + .../src-tauri/src/tests.rs | 376 +++++++----------- apps/ai-game-creator-shell/src/App.tsx | 9 + .../tests/appSurface.test.ts | 15 +- .../shared-memory/decision-log.md | 1 + ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 3 +- 7 files changed, 280 insertions(+), 283 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent.rs b/apps/ai-game-creator-shell/src-tauri/src/agent.rs index 256229d17..ecee1d7b0 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent.rs @@ -468,8 +468,17 @@ async fn drain_game_creator_agent_background_tasks( first_task: String, first_state: AgentRuntimeState, ) { - run_game_creator_agent_background_task(root.clone(), agent_id.clone(), first_task, first_state) - .await; + if run_game_creator_agent_background_task( + root.clone(), + agent_id.clone(), + first_task, + first_state, + ) + .await + == AgentBackgroundTaskOutcome::WaitingForConfirmation + { + return; + } loop { let Some(next_task) = read_next_pending_game_creator_agent_runtime_task(&root, &agent_id) .ok() @@ -495,13 +504,17 @@ async fn drain_game_creator_agent_background_tasks( } }; let _ = append_game_creator_agent_background_task_started_record(&root, &state); - run_game_creator_agent_background_task( + if run_game_creator_agent_background_task( root.clone(), agent_id.clone(), next_task.task, state, ) - .await; + .await + == AgentBackgroundTaskOutcome::WaitingForConfirmation + { + break; + } } } @@ -510,7 +523,7 @@ async fn run_game_creator_agent_background_task( agent_id: String, task: String, state: AgentRuntimeState, -) { +) -> AgentBackgroundTaskOutcome { let mut runtime = state; let mut plan = AgentRuntimeToolPlan::default(); let mut observations = Vec::new(); @@ -535,7 +548,7 @@ async fn run_game_creator_agent_background_task( Err(error) => { let fallback = default_game_creator_agent_runtime_state(&agent_id, ""); let _ = fail_game_creator_agent_runtime_turn_at(&root, fallback, &error); - return; + return AgentBackgroundTaskOutcome::Finished; } }; @@ -575,7 +588,7 @@ async fn run_game_creator_agent_background_task( }), ); } - return; + return AgentBackgroundTaskOutcome::Finished; } }; @@ -647,7 +660,7 @@ async fn run_game_creator_agent_background_task( Err(error) => { let fallback = default_game_creator_agent_runtime_state(&agent_id, ""); let _ = fail_game_creator_agent_runtime_turn_at(&root, fallback, &error); - return; + return AgentBackgroundTaskOutcome::Finished; } }; let _ = append_game_creator_agent_runtime_event( @@ -665,25 +678,45 @@ async fn run_game_creator_agent_background_task( let observation_summary = observation.summary(); runtime.observations.push(observation_summary.clone()); append_agent_runtime_tool_call_record(&mut runtime, action, &observation); - complete_agent_runtime_active_plan_step( - &mut runtime, - if observation.status == "ok" { - "completed" - } else { - "failed" - }, - &observation_summary, - ); - runtime.waiting_on = "Agent 根据工具观察修正计划".to_string(); - runtime.next_step = "把工具观察交给 Agent 修正计划".to_string(); + if observation.is_waiting_for_confirmation() { + runtime.status = "waiting-for-confirmation".to_string(); + runtime.phase = "waiting-for-confirmation".to_string(); + runtime.current_action = format!("等待确认工具 {}", observation.tool); + runtime.waiting_on = "开发者确认 Agent 工具动作".to_string(); + runtime.next_step = + format!("确认或调整策略后继续 Agent 工具动作:{}", observation.tool); + complete_agent_runtime_active_plan_step( + &mut runtime, + "waiting-for-confirmation", + &observation_summary, + ); + } else { + complete_agent_runtime_active_plan_step( + &mut runtime, + if observation.status == "ok" { + "completed" + } else { + "failed" + }, + &observation_summary, + ); + runtime.waiting_on = "Agent 根据工具观察修正计划".to_string(); + runtime.next_step = "把工具观察交给 Agent 修正计划".to_string(); + } runtime.updated_at = unix_timestamp(); + let _ = append_game_creator_agent_runtime_task(&root, &runtime); + let _ = refresh_game_creator_agent_runtime_task_queue(&root, &mut runtime); let _ = write_game_creator_agent_runtime_state(&root, &runtime); let _ = append_game_creator_agent_runtime_event( &root, &runtime, "observation", runtime.status.as_str(), - "observation", + if observation.is_waiting_for_confirmation() { + "waiting-for-confirmation" + } else { + "observation" + }, observation_summary.as_str(), observation.detail.as_deref(), ); @@ -699,6 +732,20 @@ async fn run_game_creator_agent_background_task( "summary": observation.summary, }), ); + if observation.is_waiting_for_confirmation() { + let _ = append_agent_db_record( + &root, + serde_json::json!({ + "recordType": "agent.runtime.tool_confirmation_required", + "agentId": runtime.agent_id, + "taskId": runtime.task_id, + "runId": runtime.run_id, + "tool": observation.tool, + "summary": observation.summary, + }), + ); + return AgentBackgroundTaskOutcome::WaitingForConfirmation; + } observations.push(observation); } } @@ -721,7 +768,7 @@ async fn run_game_creator_agent_background_task( Err(error) => { let fallback = default_game_creator_agent_runtime_state(&agent_id, ""); let _ = fail_game_creator_agent_runtime_turn_at(&root, fallback, &error); - return; + return AgentBackgroundTaskOutcome::Finished; } }; match request_game_creator_agent_background_final_reply_at( @@ -761,7 +808,7 @@ async fn run_game_creator_agent_background_task( }), ); } - return; + return AgentBackgroundTaskOutcome::Finished; } } }; @@ -804,7 +851,7 @@ async fn run_game_creator_agent_background_task( }), ); } - return; + return AgentBackgroundTaskOutcome::Finished; } } let _ = append_local_conversation_message_at( @@ -828,6 +875,7 @@ async fn run_game_creator_agent_background_task( "responsePreview": runtime.last_response, }), ); + AgentBackgroundTaskOutcome::Finished } const AGENT_RUNTIME_BACKGROUND_LOOP_LIMIT: usize = 3; @@ -838,6 +886,12 @@ const AGENT_RUNTIME_TOOL_OBSERVATION_MAX_CHARS: usize = 900; const AGENT_RUNTIME_TOOL_WRITE_MAX_CHARS: usize = 12_000; pub(crate) const AGENT_RUNTIME_LOCK_STALE_AFTER_SECONDS: u64 = 300; +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum AgentBackgroundTaskOutcome { + Finished, + WaitingForConfirmation, +} + #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct AgentRuntimeToolPlan { @@ -875,6 +929,16 @@ impl AgentRuntimeToolObservation { fn summary(&self) -> String { format!("{}:{} · {}", self.tool, self.status, self.summary) } + + fn is_waiting_for_confirmation(&self) -> bool { + self.status == "waiting-for-confirmation" + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +enum AgentRuntimeToolPolicyBlock { + Denied(String), + RequiresConfirmation(String), } fn append_agent_runtime_tool_call_record( @@ -964,10 +1028,10 @@ fn complete_agent_runtime_active_plan_step( let Some(active_index) = runtime.active_plan_step_index else { return; }; - let status = if status == "failed" { - "failed" - } else { - "completed" + let status = match status { + "failed" => "failed", + "waiting-for-confirmation" => "waiting-for-confirmation", + _ => "completed", }; let now = unix_timestamp(); for step in runtime.plan_steps.iter_mut() { @@ -1200,10 +1264,16 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action( if let Some(blocked) = game_creator_agent_runtime_tool_policy_block(root, agent_id, command_id) { + let (status, summary) = match blocked { + AgentRuntimeToolPolicyBlock::Denied(summary) => ("blocked", summary), + AgentRuntimeToolPolicyBlock::RequiresConfirmation(summary) => { + ("waiting-for-confirmation", summary) + } + }; return AgentRuntimeToolObservation { tool: tool.to_string(), - status: "blocked".to_string(), - summary: blocked, + status: status.to_string(), + summary, detail: None, }; } @@ -1378,14 +1448,14 @@ fn game_creator_agent_runtime_tool_policy_block( root: &Path, agent_id: &str, command_id: &str, -) -> Option { +) -> Option { let view = match read_project_permission_policy_at(root) { Ok(view) => view, - Err(error) => return Some(error), + Err(error) => return Some(AgentRuntimeToolPolicyBlock::Denied(error)), }; let agent_id = match normalize_game_creator_runtime_agent_id(agent_id) { Ok(agent_id) => agent_id, - Err(error) => return Some(error), + Err(error) => return Some(AgentRuntimeToolPolicyBlock::Denied(error)), }; if view .policy @@ -1393,7 +1463,9 @@ fn game_creator_agent_runtime_tool_policy_block( .iter() .any(|command| command == command_id) { - return Some(format!("项目权限策略拒绝执行:{command_id}")); + return Some(AgentRuntimeToolPolicyBlock::Denied(format!( + "项目权限策略拒绝执行:{command_id}" + ))); } if view .policy @@ -1407,7 +1479,9 @@ fn game_creator_agent_runtime_tool_policy_block( }) .unwrap_or(false) { - return Some(format!("Agent 权限策略拒绝执行:{agent_id} / {command_id}")); + return Some(AgentRuntimeToolPolicyBlock::Denied(format!( + "Agent 权限策略拒绝执行:{agent_id} / {command_id}" + ))); } if view .policy @@ -1415,7 +1489,9 @@ fn game_creator_agent_runtime_tool_policy_block( .iter() .any(|command| command == command_id) { - return Some(format!("项目权限策略要求用户确认:{command_id}")); + return Some(AgentRuntimeToolPolicyBlock::RequiresConfirmation(format!( + "项目权限策略要求用户确认:{command_id}" + ))); } if view .policy @@ -1429,9 +1505,9 @@ fn game_creator_agent_runtime_tool_policy_block( }) .unwrap_or(false) { - return Some(format!( + return Some(AgentRuntimeToolPolicyBlock::RequiresConfirmation(format!( "Agent 权限策略要求用户确认:{agent_id} / {command_id}" - )); + ))); } None } @@ -2568,10 +2644,11 @@ fn format_agent_runtime_active_plan_step_observation(state: &AgentRuntimeState) fn format_agent_runtime_task_queue_observation(queue: &AgentRuntimeTaskQueueSummary) -> String { format!( - "total={} pending={} running={} completed={} failed={} latest={}", + "total={} pending={} running={} waiting={} completed={} failed={} latest={}", queue.total, queue.pending, queue.running, + queue.waiting_for_confirmation, queue.completed, queue.failed, queue.latest_run_id.as_deref().unwrap_or("-") @@ -3069,6 +3146,7 @@ fn agent_runtime_next_step_for_phase(phase: &str) -> &'static str { match phase.trim() { "planning" => "等待 Agent 输出计划或回复", "action" => "等待工具观察结果", + "waiting-for-confirmation" => "等待开发者确认工具动作", "response" => "等待 Agent 整理最终回复", "completed" | "idle" => "等待下一轮输入", "failed" => "等待开发者处理失败", @@ -3081,6 +3159,7 @@ fn agent_runtime_waiting_on_for_phase(phase: &str) -> &'static str { "planning" => "Agent 输出计划或回复", "llm" => "Agent LLM 回复", "action" => "工具观察结果", + "waiting-for-confirmation" => "开发者确认 Agent 工具动作", "response" => "Agent 整理最终回复", "completed" | "idle" => "开发者下一轮输入", "failed" => "开发者处理失败", @@ -3616,6 +3695,7 @@ fn summarize_game_creator_agent_runtime_task_queue( match record.status.as_str() { "pending" => summary.pending += 1, "running" => summary.running += 1, + "waiting-for-confirmation" => summary.waiting_for_confirmation += 1, "completed" => summary.completed += 1, "failed" => summary.failed += 1, _ => {} 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 5e1117c93..8c55f7e36 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/main.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/main.rs @@ -260,6 +260,8 @@ struct AgentRuntimeTaskQueueSummary { #[serde(default)] running: u32, #[serde(default)] + waiting_for_confirmation: u32, + #[serde(default)] completed: u32, #[serde(default)] failed: u32, @@ -275,6 +277,7 @@ impl Default for AgentRuntimeTaskQueueSummary { total: 0, pending: 0, running: 0, + waiting_for_confirmation: 0, completed: 0, failed: 0, latest_run_id: None, diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests.rs b/apps/ai-game-creator-shell/src-tauri/src/tests.rs index 37e33647e..4e7d8e39a 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests.rs @@ -66,6 +66,22 @@ fn wait_for_agent_runtime_idle(root: &Path, agent_id: &str) -> AgentRuntimeState runtime } +fn wait_for_agent_runtime_confirmation(root: &Path, agent_id: &str) -> AgentRuntimeState { + let mut runtime = read_game_creator_agent_runtime_at(root, agent_id) + .expect("read runtime while waiting for confirmation") + .state; + for _ in 0..50 { + if runtime.status == "waiting-for-confirmation" { + return runtime; + } + std::thread::sleep(Duration::from_millis(20)); + runtime = read_game_creator_agent_runtime_at(root, agent_id) + .expect("read runtime while waiting for confirmation") + .state; + } + runtime +} + fn write_agent_runtime_task_record_for_test(root: &Path, record: &AgentRuntimeTaskRecord) { let path = root .join(".agent/runtime/tasks") @@ -2499,7 +2515,7 @@ async fn background_agent_runtime_plan_request_includes_same_agent_continuity_co assert!(second_design_request.contains("计划进度:")); assert!(second_design_request.contains("#1 [active] 记录开发者投递的后台任务")); assert!(second_design_request.contains( - "任务队列:total=2 pending=0 running=1 completed=1 failed=0 latest=design-continuity-second" + "任务队列:total=2 pending=0 running=1 waiting=0 completed=1 failed=0 latest=design-continuity-second" )); assert!(second_design_request.contains("最近回复:首轮完成:已经读取连续上下文笔记。")); assert!(second_design_request.contains("最近工具动作")); @@ -2814,10 +2830,7 @@ async fn background_agent_runtime_delegate_respects_project_policy() { "response": "" }) .to_string(); - let base_url = spawn_mock_llm_server_responses_with_capture( - vec![plan_json, "委派需要用户确认。".to_string()], - Some(sender), - ); + let base_url = spawn_mock_llm_server_responses_with_capture(vec![plan_json], Some(sender)); let _config_guard = write_test_local_config(format!( r#"{{ "agentLlm": {{ @@ -2843,25 +2856,25 @@ async fn background_agent_runtime_delegate_respects_project_policy() { .recv_timeout(Duration::from_secs(2)) .expect("design plan llm request"); assert!(plan_request.contains("agent.delegate")); - let final_request = receiver - .recv_timeout(Duration::from_secs(2)) - .expect("design final reply llm request"); - assert!(final_request.contains("项目权限策略要求用户确认:agent.delegate")); - assert!(!final_request.contains("已委派 art-director 后台任务")); + assert!(receiver.recv_timeout(Duration::from_millis(200)).is_err()); - let design_runtime = wait_for_agent_runtime_idle(&root, "design-director"); - assert_eq!(design_runtime.status, "idle"); + let design_runtime = wait_for_agent_runtime_confirmation(&root, "design-director"); + assert_eq!(design_runtime.status, "waiting-for-confirmation"); + assert_eq!(design_runtime.phase, "waiting-for-confirmation"); + assert_eq!(design_runtime.task_queue.waiting_for_confirmation, 1); assert!(design_runtime .tool_policy .confirm_tools .contains(&"agent.delegate".to_string())); assert!(design_runtime.observations.iter().any(|item| { - item.contains("agent.delegate:blocked · 项目权限策略要求用户确认:agent.delegate") + item.contains( + "agent.delegate:waiting-for-confirmation · 项目权限策略要求用户确认:agent.delegate", + ) })); assert!(design_runtime .recent_tool_calls .iter() - .any(|call| { call.tool == "agent.delegate" && call.status == "blocked" })); + .any(|call| call.tool == "agent.delegate" && call.status == "waiting-for-confirmation")); let art_conversation = read_local_conversation_at(&root, Some("art-director")).expect("art conversation"); @@ -3044,10 +3057,7 @@ async fn background_agent_runtime_write_tools_respect_project_policy() { "response": "" }) .to_string(); - let base_url = spawn_mock_llm_server_responses_with_capture( - vec![plan_json, "写入需要用户确认。".to_string()], - Some(sender), - ); + let base_url = spawn_mock_llm_server_responses_with_capture(vec![plan_json], Some(sender)); let _config_guard = write_test_local_config(format!( r#"{{ "agentLlm": {{ @@ -3072,31 +3082,20 @@ async fn background_agent_runtime_write_tools_respect_project_policy() { let _plan_request = receiver .recv_timeout(Duration::from_secs(2)) .expect("plan llm request"); - let final_request = receiver - .recv_timeout(Duration::from_secs(2)) - .expect("final reply llm request"); - assert!(final_request.contains("项目权限策略要求用户确认:memory.write")); - assert!(final_request.contains("项目权限策略要求用户确认:conversation.write")); + assert!(receiver.recv_timeout(Duration::from_millis(200)).is_err()); - let mut runtime = read_game_creator_agent_runtime_at(&root, "design-director") - .expect("read runtime") - .state; - for _ in 0..50 { - if runtime.status == "idle" { - break; - } - std::thread::sleep(Duration::from_millis(20)); - runtime = read_game_creator_agent_runtime_at(&root, "design-director") - .expect("read runtime") - .state; - } - assert_eq!(runtime.status, "idle"); + let runtime = wait_for_agent_runtime_confirmation(&root, "design-director"); + assert_eq!(runtime.status, "waiting-for-confirmation"); + assert_eq!(runtime.task_queue.waiting_for_confirmation, 1); assert!(runtime.observations.iter().any(|item| { - item.contains("blackboard.write:blocked · 项目权限策略要求用户确认:memory.write") - })); - assert!(runtime.observations.iter().any(|item| { - item.contains("agent.message:blocked · 项目权限策略要求用户确认:conversation.write") + item.contains( + "blackboard.write:waiting-for-confirmation · 项目权限策略要求用户确认:memory.write", + ) })); + assert!(!runtime + .observations + .iter() + .any(|item| item.contains("agent.message:"))); let blackboard = fs::read_to_string(root.join(PROJECT_BLACKBOARD_MEMORY_PATH)) .unwrap_or_else(|_| String::new()); assert!(!blackboard.contains("这段黑板内容不应该绕过确认策略")); @@ -3146,10 +3145,7 @@ async fn background_agent_runtime_file_and_memory_writes_respect_project_policy( "response": "" }) .to_string(); - let base_url = spawn_mock_llm_server_responses_with_capture( - vec![plan_json, "写入需要用户确认。".to_string()], - Some(sender), - ); + let base_url = spawn_mock_llm_server_responses_with_capture(vec![plan_json], Some(sender)); let _config_guard = write_test_local_config(format!( r#"{{ "agentLlm": {{ @@ -3174,32 +3170,20 @@ async fn background_agent_runtime_file_and_memory_writes_respect_project_policy( let _plan_request = receiver .recv_timeout(Duration::from_secs(2)) .expect("plan llm request"); - let final_request = receiver - .recv_timeout(Duration::from_secs(2)) - .expect("final reply llm request"); - assert!(final_request.contains("项目权限策略要求用户确认:memory.write")); - assert!(final_request.contains("项目权限策略要求用户确认:file.write")); + assert!(receiver.recv_timeout(Duration::from_millis(200)).is_err()); - let mut runtime = read_game_creator_agent_runtime_at(&root, "design-director") - .expect("read runtime") - .state; - for _ in 0..50 { - if runtime.status == "idle" { - break; - } - std::thread::sleep(Duration::from_millis(20)); - runtime = read_game_creator_agent_runtime_at(&root, "design-director") - .expect("read runtime") - .state; - } - assert_eq!(runtime.status, "idle"); + let runtime = wait_for_agent_runtime_confirmation(&root, "design-director"); + assert_eq!(runtime.status, "waiting-for-confirmation"); + assert_eq!(runtime.task_queue.waiting_for_confirmation, 1); assert!(runtime.observations.iter().any(|item| { - item.contains("memory.write:blocked · 项目权限策略要求用户确认:memory.write") + item.contains( + "memory.write:waiting-for-confirmation · 项目权限策略要求用户确认:memory.write", + ) })); - assert!(runtime + assert!(!runtime .observations .iter() - .any(|item| item.contains("file.write:blocked · 项目权限策略要求用户确认:file.write"))); + .any(|item| item.contains("file.write:"))); let agent_memory = read_local_agent_memory_at(&root, "design-director").expect("agent memory"); assert!(!agent_memory .content @@ -3339,10 +3323,7 @@ async fn background_agent_runtime_task_list_respects_project_policy() { "response": "" }) .to_string(); - let base_url = spawn_mock_llm_server_responses_with_capture( - vec![plan_json, "读取任务图需要用户确认。".to_string()], - Some(sender), - ); + let base_url = spawn_mock_llm_server_responses_with_capture(vec![plan_json], Some(sender)); let _config_guard = write_test_local_config(format!( r#"{{ "agentLlm": {{ @@ -3367,18 +3348,13 @@ async fn background_agent_runtime_task_list_respects_project_policy() { let _plan_request = receiver .recv_timeout(Duration::from_secs(2)) .expect("plan llm request"); - let final_request = receiver - .recv_timeout(Duration::from_secs(2)) - .expect("final reply llm request"); - assert!(final_request.contains("项目权限策略要求用户确认:task.list")); - assert!(!final_request.contains("readyTaskIds:")); - assert!(!final_request.contains("design-director [pending]")); + assert!(receiver.recv_timeout(Duration::from_millis(200)).is_err()); let mut runtime = read_game_creator_agent_runtime_at(&root, "design-director") .expect("read runtime") .state; for _ in 0..50 { - if runtime.status == "idle" { + if runtime.status == "waiting-for-confirmation" { break; } std::thread::sleep(Duration::from_millis(20)); @@ -3386,11 +3362,26 @@ async fn background_agent_runtime_task_list_respects_project_policy() { .expect("read runtime") .state; } - assert_eq!(runtime.status, "idle"); + assert_eq!(runtime.status, "waiting-for-confirmation"); + assert_eq!(runtime.phase, "waiting-for-confirmation"); + assert_eq!(runtime.waiting_on, "开发者确认 Agent 工具动作"); + assert_eq!(runtime.task_queue.waiting_for_confirmation, 1); + assert!(runtime.observations.iter().any(|item| item + .contains("task.list:waiting-for-confirmation · 项目权限策略要求用户确认:task.list"))); assert!(runtime - .observations + .recent_tool_calls .iter() - .any(|item| item.contains("task.list:blocked · 项目权限策略要求用户确认:task.list"))); + .any(|item| item.tool == "task.list" && item.status == "waiting-for-confirmation")); + let runtime_result = + read_game_creator_agent_runtime_at(&root, "design-director").expect("runtime result"); + assert!(runtime_result + .recent_tasks + .iter() + .any(|task| task.run_id == "design-task-list-policy-run" + && task.status == "waiting-for-confirmation" + && task.phase == "waiting-for-confirmation")); + let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db"); + assert!(agent_db.contains("\"recordType\":\"agent.runtime.tool_confirmation_required\"")); fs::remove_dir_all(root).ok(); } @@ -3520,10 +3511,7 @@ async fn background_agent_runtime_task_update_respects_project_policy() { "response": "" }) .to_string(); - let base_url = spawn_mock_llm_server_responses_with_capture( - vec![plan_json, "更新任务状态需要用户确认。".to_string()], - Some(sender), - ); + let base_url = spawn_mock_llm_server_responses_with_capture(vec![plan_json], Some(sender)); let _config_guard = write_test_local_config(format!( r#"{{ "agentLlm": {{ @@ -3548,29 +3536,14 @@ async fn background_agent_runtime_task_update_respects_project_policy() { let _plan_request = receiver .recv_timeout(Duration::from_secs(2)) .expect("plan llm request"); - let final_request = receiver - .recv_timeout(Duration::from_secs(2)) - .expect("final reply llm request"); - assert!(final_request.contains("项目权限策略要求用户确认:task.update")); - assert!(!final_request.contains("任务 art-asset-plan 已更新为 completed")); + assert!(receiver.recv_timeout(Duration::from_millis(200)).is_err()); - let mut runtime = read_game_creator_agent_runtime_at(&root, "art-director") - .expect("read runtime") - .state; - for _ in 0..50 { - if runtime.status == "idle" { - break; - } - std::thread::sleep(Duration::from_millis(20)); - runtime = read_game_creator_agent_runtime_at(&root, "art-director") - .expect("read runtime") - .state; - } - assert_eq!(runtime.status, "idle"); - assert!(runtime - .observations - .iter() - .any(|item| item.contains("task.update:blocked · 项目权限策略要求用户确认:task.update"))); + let runtime = wait_for_agent_runtime_confirmation(&root, "art-director"); + assert_eq!(runtime.status, "waiting-for-confirmation"); + assert_eq!(runtime.task_queue.waiting_for_confirmation, 1); + assert!(runtime.observations.iter().any(|item| item.contains( + "task.update:waiting-for-confirmation · 项目权限策略要求用户确认:task.update" + ))); let manifest_after: Value = serde_json::from_str(&fs::read_to_string(root.join(".agent/manifest.json")).unwrap()) .expect("manifest json"); @@ -3719,10 +3692,7 @@ async fn background_agent_runtime_limited_command_respects_project_policy() { "response": "" }) .to_string(); - let base_url = spawn_mock_llm_server_responses_with_capture( - vec![plan_json, "运行自检需要用户确认。".to_string()], - Some(sender), - ); + let base_url = spawn_mock_llm_server_responses_with_capture(vec![plan_json], Some(sender)); let _config_guard = write_test_local_config(format!( r#"{{ "agentLlm": {{ @@ -3747,27 +3717,13 @@ async fn background_agent_runtime_limited_command_respects_project_policy() { let _plan_request = receiver .recv_timeout(Duration::from_secs(2)) .expect("plan llm request"); - let final_request = receiver - .recv_timeout(Duration::from_secs(2)) - .expect("final reply llm request"); - assert!(final_request.contains("项目权限策略要求用户确认:command.run_limited")); - assert!(!final_request.contains("通过:")); + assert!(receiver.recv_timeout(Duration::from_millis(200)).is_err()); - let mut runtime = read_game_creator_agent_runtime_at(&root, "code-prototype") - .expect("read runtime") - .state; - for _ in 0..50 { - if runtime.status == "idle" { - break; - } - std::thread::sleep(Duration::from_millis(20)); - runtime = read_game_creator_agent_runtime_at(&root, "code-prototype") - .expect("read runtime") - .state; - } - assert_eq!(runtime.status, "idle"); + let runtime = wait_for_agent_runtime_confirmation(&root, "code-prototype"); + assert_eq!(runtime.status, "waiting-for-confirmation"); + assert_eq!(runtime.task_queue.waiting_for_confirmation, 1); assert!(runtime.observations.iter().any(|item| item - .contains("command.run_limited:blocked · 项目权限策略要求用户确认:command.run_limited"))); + .contains("command.run_limited:waiting-for-confirmation · 项目权限策略要求用户确认:command.run_limited"))); assert!(!root.join(".agent/logs/command.log").exists()); fs::remove_dir_all(root).ok(); @@ -3910,10 +3866,7 @@ async fn background_agent_runtime_preview_start_respects_project_policy() { "response": "" }) .to_string(); - let base_url = spawn_mock_llm_server_responses_with_capture( - vec![plan_json, "启动预览需要用户确认。".to_string()], - Some(sender), - ); + let base_url = spawn_mock_llm_server_responses_with_capture(vec![plan_json], Some(sender)); let _config_guard = write_test_local_config(format!( r#"{{ "agentLlm": {{ @@ -3938,30 +3891,14 @@ async fn background_agent_runtime_preview_start_respects_project_policy() { let _plan_request = receiver .recv_timeout(Duration::from_secs(2)) .expect("plan llm request"); - let final_request = receiver - .recv_timeout(Duration::from_secs(2)) - .expect("final reply llm request"); - assert!(final_request.contains("项目权限策略要求用户确认:preview.start")); - assert!(!final_request.contains("preview.start 已启动")); + assert!(receiver.recv_timeout(Duration::from_millis(200)).is_err()); - let mut runtime = read_game_creator_agent_runtime_at(&root, "code-prototype") - .expect("read runtime") - .state; - for _ in 0..50 { - if runtime.status == "idle" { - break; - } - std::thread::sleep(Duration::from_millis(20)); - runtime = read_game_creator_agent_runtime_at(&root, "code-prototype") - .expect("read runtime") - .state; - } - assert_eq!(runtime.status, "idle"); - assert!(runtime - .observations - .iter() - .any(|item| item - .contains("preview.start:blocked · 项目权限策略要求用户确认:preview.start"))); + let runtime = wait_for_agent_runtime_confirmation(&root, "code-prototype"); + assert_eq!(runtime.status, "waiting-for-confirmation"); + assert_eq!(runtime.task_queue.waiting_for_confirmation, 1); + assert!(runtime.observations.iter().any(|item| item.contains( + "preview.start:waiting-for-confirmation · 项目权限策略要求用户确认:preview.start" + ))); assert!(!root.join(".agent/logs/preview.log").exists()); fs::remove_dir_all(root).ok(); @@ -4116,10 +4053,7 @@ async fn background_agent_runtime_asset_generation_respects_project_policy() { "response": "" }) .to_string(); - let llm_base_url = spawn_mock_llm_server_responses_with_capture( - vec![plan_json, "生成素材需要用户确认。".to_string()], - Some(sender), - ); + let llm_base_url = spawn_mock_llm_server_responses_with_capture(vec![plan_json], Some(sender)); fs::create_dir_all(&config_dir).expect("runtime config dir"); fs::write( config_dir.join(GAME_CREATOR_CONFIG_FILE_NAME), @@ -4149,27 +4083,13 @@ async fn background_agent_runtime_asset_generation_respects_project_policy() { let _plan_request = receiver .recv_timeout(Duration::from_secs(2)) .expect("plan llm request"); - let final_request = receiver - .recv_timeout(Duration::from_secs(2)) - .expect("final reply llm request"); - assert!(final_request.contains("项目权限策略要求用户确认:canvas.asset_generate")); - assert!(!final_request.contains("已生成美术素材")); + assert!(receiver.recv_timeout(Duration::from_millis(200)).is_err()); - let mut runtime = read_game_creator_agent_runtime_at(&root, "art-asset-plan") - .expect("read runtime") - .state; - for _ in 0..50 { - if runtime.status == "idle" { - break; - } - std::thread::sleep(Duration::from_millis(20)); - runtime = read_game_creator_agent_runtime_at(&root, "art-asset-plan") - .expect("read runtime") - .state; - } - assert_eq!(runtime.status, "idle"); + let runtime = wait_for_agent_runtime_confirmation(&root, "art-asset-plan"); + assert_eq!(runtime.status, "waiting-for-confirmation"); + assert_eq!(runtime.task_queue.waiting_for_confirmation, 1); assert!(runtime.observations.iter().any(|item| item.contains( - "canvas.asset_generate:blocked · 项目权限策略要求用户确认:canvas.asset_generate" + "canvas.asset_generate:waiting-for-confirmation · 项目权限策略要求用户确认:canvas.asset_generate" ))); let manifest = read_manifest_for_project(&root).expect("manifest"); assert!(manifest.assets.is_empty()); @@ -4206,10 +4126,7 @@ async fn background_agent_runtime_tool_action_respects_confirm_policy() { "response": "" }) .to_string(); - let base_url = spawn_mock_llm_server_responses_with_capture( - vec![plan_json, "读取项目笔记需要先获得用户确认。".to_string()], - Some(sender), - ); + let base_url = spawn_mock_llm_server_responses_with_capture(vec![plan_json], Some(sender)); let _config_guard = write_test_local_config(format!( r#"{{ "agentLlm": {{ @@ -4234,17 +4151,13 @@ async fn background_agent_runtime_tool_action_respects_confirm_policy() { let _plan_request = receiver .recv_timeout(Duration::from_secs(2)) .expect("plan llm request"); - let final_request = receiver - .recv_timeout(Duration::from_secs(2)) - .expect("final reply llm request"); - assert!(final_request.contains("项目权限策略要求用户确认:file.read")); - assert!(!final_request.contains("不应该被读取的文件内容")); + assert!(receiver.recv_timeout(Duration::from_millis(200)).is_err()); let mut runtime = read_game_creator_agent_runtime_at(&root, "design-director") .expect("read runtime") .state; for _ in 0..50 { - if runtime.status == "idle" { + if runtime.status == "waiting-for-confirmation" { break; } std::thread::sleep(Duration::from_millis(20)); @@ -4252,11 +4165,12 @@ async fn background_agent_runtime_tool_action_respects_confirm_policy() { .expect("read runtime") .state; } - assert_eq!(runtime.status, "idle"); - assert!(runtime - .observations - .iter() - .any(|item| item.contains("file.read:blocked · 项目权限策略要求用户确认:file.read"))); + assert_eq!(runtime.status, "waiting-for-confirmation"); + assert_eq!(runtime.phase, "waiting-for-confirmation"); + assert_eq!(runtime.waiting_on, "开发者确认 Agent 工具动作"); + assert_eq!(runtime.task_queue.waiting_for_confirmation, 1); + assert!(runtime.observations.iter().any(|item| item + .contains("file.read:waiting-for-confirmation · 项目权限策略要求用户确认:file.read"))); assert!(!runtime .observations .iter() @@ -4267,9 +4181,13 @@ async fn background_agent_runtime_tool_action_respects_confirm_policy() { .recent_tasks .iter() .any(|task| task.run_id == "design-confirm-run" - && task.status == "completed" - && task.phase == "completed" + && task.status == "waiting-for-confirmation" + && task.phase == "waiting-for-confirmation" && task.task == "后台分析当前玩法循环")); + assert_eq!( + runtime_result.task_queue.waiting_for_confirmation, 1, + "queue summary should expose pending human confirmation" + ); fs::remove_dir_all(root).ok(); } @@ -4404,10 +4322,7 @@ async fn background_agent_runtime_file_list_respects_project_policy() { "response": "" }) .to_string(); - let base_url = spawn_mock_llm_server_responses_with_capture( - vec![plan_json, "列目录需要先获得用户确认。".to_string()], - Some(sender), - ); + let base_url = spawn_mock_llm_server_responses_with_capture(vec![plan_json], Some(sender)); let _config_guard = write_test_local_config(format!( r#"{{ "agentLlm": {{ @@ -4434,23 +4349,17 @@ async fn background_agent_runtime_file_list_respects_project_policy() { .expect("plan llm request"); assert!(plan_request.contains("confirmTools")); assert!(plan_request.contains("file.list")); - let final_request = receiver - .recv_timeout(Duration::from_secs(2)) - .expect("final reply llm request"); - assert!(final_request.contains("项目权限策略要求用户确认:file.list")); - assert!(!final_request.contains("game/blocked-notes.txt")); - assert!(!final_request.contains("不应该被列出的文件内容")); + assert!(receiver.recv_timeout(Duration::from_millis(200)).is_err()); - let runtime = wait_for_agent_runtime_idle(&root, "design-director"); - assert_eq!(runtime.status, "idle"); + let runtime = wait_for_agent_runtime_confirmation(&root, "design-director"); + assert_eq!(runtime.status, "waiting-for-confirmation"); + assert_eq!(runtime.task_queue.waiting_for_confirmation, 1); assert!(runtime .tool_policy .confirm_tools .contains(&"file.list".to_string())); - assert!(runtime - .observations - .iter() - .any(|item| item.contains("file.list:blocked · 项目权限策略要求用户确认:file.list"))); + assert!(runtime.observations.iter().any(|item| item + .contains("file.list:waiting-for-confirmation · 项目权限策略要求用户确认:file.list"))); assert!(!runtime .observations .iter() @@ -4591,10 +4500,7 @@ async fn background_agent_runtime_project_diff_respects_project_policy() { "response": "" }) .to_string(); - let base_url = spawn_mock_llm_server_responses_with_capture( - vec![plan_json, "对比差异需要先获得用户确认。".to_string()], - Some(sender), - ); + let base_url = spawn_mock_llm_server_responses_with_capture(vec![plan_json], Some(sender)); let _config_guard = write_test_local_config(format!( r#"{{ "agentLlm": {{ @@ -4621,22 +4527,18 @@ async fn background_agent_runtime_project_diff_respects_project_policy() { .expect("plan llm request"); assert!(plan_request.contains("confirmTools")); assert!(plan_request.contains("project.diff")); - let final_request = receiver - .recv_timeout(Duration::from_secs(2)) - .expect("final reply llm request"); - assert!(final_request.contains("项目权限策略要求用户确认:project.diff")); - assert!(!final_request.contains("changed: 1")); - assert!(!final_request.contains("game/blocked-notes.txt")); + assert!(receiver.recv_timeout(Duration::from_millis(200)).is_err()); - let runtime = wait_for_agent_runtime_idle(&root, "design-director"); - assert_eq!(runtime.status, "idle"); + let runtime = wait_for_agent_runtime_confirmation(&root, "design-director"); + assert_eq!(runtime.status, "waiting-for-confirmation"); + assert_eq!(runtime.task_queue.waiting_for_confirmation, 1); assert!(runtime .tool_policy .confirm_tools .contains(&"project.diff".to_string())); - assert!(runtime.observations.iter().any( - |item| item.contains("project.diff:blocked · 项目权限策略要求用户确认:project.diff") - )); + assert!(runtime.observations.iter().any(|item| item.contains( + "project.diff:waiting-for-confirmation · 项目权限策略要求用户确认:project.diff" + ))); assert!(!runtime .observations .iter() @@ -4805,10 +4707,7 @@ async fn background_agent_runtime_run_status_respects_project_policy() { "response": "" }) .to_string(); - let base_url = spawn_mock_llm_server_responses_with_capture( - vec![plan_json, "读取 Agent 状态需要先获得用户确认。".to_string()], - Some(sender), - ); + let base_url = spawn_mock_llm_server_responses_with_capture(vec![plan_json], Some(sender)); let _config_guard = write_test_local_config(format!( r#"{{ "agentLlm": {{ @@ -4835,21 +4734,18 @@ async fn background_agent_runtime_run_status_respects_project_policy() { .expect("plan llm request"); assert!(plan_request.contains("confirmTools")); assert!(plan_request.contains("agent.run_status")); - let final_request = receiver - .recv_timeout(Duration::from_secs(2)) - .expect("final reply llm request"); - assert!(final_request.contains("项目权限策略要求用户确认:agent.run_status")); - assert!(!final_request.contains("不应泄露的美术任务")); - assert!(!final_request.contains("不应泄露的当前动作")); + assert!(receiver.recv_timeout(Duration::from_millis(200)).is_err()); - let runtime = wait_for_agent_runtime_idle(&root, "design-director"); - assert_eq!(runtime.status, "idle"); + let runtime = wait_for_agent_runtime_confirmation(&root, "design-director"); + assert_eq!(runtime.status, "waiting-for-confirmation"); + assert_eq!(runtime.task_queue.waiting_for_confirmation, 1); assert!(runtime .tool_policy .confirm_tools .contains(&"agent.run_status".to_string())); - assert!(runtime.observations.iter().any(|item| item - .contains("agent.run_status:blocked · 项目权限策略要求用户确认:agent.run_status"))); + assert!(runtime.observations.iter().any(|item| item.contains( + "agent.run_status:waiting-for-confirmation · 项目权限策略要求用户确认:agent.run_status" + ))); assert!(!runtime .observations .iter() diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index e32f41fda..15bd46a7c 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -288,6 +288,7 @@ interface AgentRuntimeTaskQueueSummary { total: number; pending: number; running: number; + waitingForConfirmation?: number; completed: number; failed: number; latestRunId: string | null; @@ -621,6 +622,7 @@ function normalizeAgentRuntimeState( total: 0, pending: 0, running: 0, + waitingForConfirmation: 0, completed: 0, failed: 0, latestRunId: null, @@ -654,6 +656,8 @@ function agentRuntimeWaitingOnFromPhase(phase: string) { return 'Agent LLM 回复'; case 'action': return '工具观察结果'; + case 'waiting-for-confirmation': + return '开发者确认 Agent 工具动作'; case 'response': return 'Agent 整理最终回复'; case 'completed': @@ -672,6 +676,8 @@ function agentRuntimeNextStepFromPhase(phase: string) { return '等待 Agent 输出计划或回复'; case 'action': return '等待工具观察结果'; + case 'waiting-for-confirmation': + return '等待开发者确认工具动作'; case 'response': return '等待 Agent 整理最终回复'; case 'completed': @@ -708,6 +714,7 @@ function formatAgentRuntimeTaskQueue( const parts = [ `pending ${queue.pending}`, `running ${queue.running}`, + `waiting ${queue.waitingForConfirmation ?? 0}`, `completed ${queue.completed}`, `failed ${queue.failed}`, `total ${queue.total}`, @@ -10467,6 +10474,8 @@ function sameAgentRuntimeTaskQueue( left.total === right.total && left.pending === right.pending && left.running === right.running && + (left.waitingForConfirmation ?? 0) === + (right.waitingForConfirmation ?? 0) && left.completed === right.completed && left.failed === right.failed && left.latestRunId === right.latestRunId && diff --git a/apps/ai-game-creator-shell/tests/appSurface.test.ts b/apps/ai-game-creator-shell/tests/appSurface.test.ts index 64a3d2860..ffdfa152d 100644 --- a/apps/ai-game-creator-shell/tests/appSurface.test.ts +++ b/apps/ai-game-creator-shell/tests/appSurface.test.ts @@ -1662,7 +1662,7 @@ describe('AI 游戏创作 App 界面边界', () => { expect(screen.getByText('Loop:1/3 · 工具预算 3')).not.toBeNull(); expect( screen.getByText( - /任务队列:pending 0 · running 1 · completed 0 · failed 0 · total 1 · latest launcher-agent-task-/, + /任务队列:pending 0 · running 1 · waiting 0 · completed 0 · failed 0 · total 1 · latest launcher-agent-task-/, ), ).not.toBeNull(); expect(screen.getByText('计划进度')).not.toBeNull(); @@ -1921,7 +1921,7 @@ describe('AI 游戏创作 App 界面边界', () => { ).not.toBeNull(); expect( screen.getByText( - /任务队列:pending 1 · running 1 · completed 0 · failed 0 · total 2 · latest launcher-agent-task-/, + /任务队列:pending 1 · running 1 · waiting 0 · completed 0 · failed 0 · total 2 · latest launcher-agent-task-/, ), ).not.toBeNull(); }); @@ -13648,7 +13648,7 @@ describe('AI 游戏创作 App 界面边界', () => { '当前计划步骤:#1 active · 读取项目上下文 · 正在整理目标和约束', ); expect(designCard.textContent).toContain( - '任务队列:pending 1 · running 1 · completed 0 · failed 0 · total 2 · latest runtime-design-director-1', + '任务队列:pending 1 · running 1 · waiting 0 · completed 0 · failed 0 · total 2 · latest runtime-design-director-1', ); expect(designCard.textContent).toContain( '最近任务:pending / queued · 排队补齐世界观拆解', @@ -13694,7 +13694,7 @@ describe('AI 游戏创作 App 界面边界', () => { 'Runtime:idle / completed · Loop 2/3 · 等待下一轮输入 · 等待 开发者下一轮输入 · 下一步 等待下一轮输入 · run runtime-design-director-1', ); expect(designCard.textContent).toContain( - '任务队列:pending 1 · running 0 · completed 1 · failed 0 · total 2 · latest runtime-design-director-1', + '任务队列:pending 1 · running 0 · waiting 0 · completed 1 · failed 0 · total 2 · latest runtime-design-director-1', ); expect(designCard.textContent).toContain( '最近任务:completed / completed · 排队补齐世界观拆解', @@ -18338,6 +18338,13 @@ describe('AI 游戏创作 App 界面边界', () => { expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); + await waitFor(() => { + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_project_permission_policy', + ).length, + ).toBeGreaterThanOrEqual(2); + }); invoke.mockClear(); submitChat('/policy-confirm file.write'); diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index fc6e8901c..b9a1bbdc8 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -4061,6 +4061,7 @@ - 2026-07-09 调整:AI 游戏创作 App 新增 Agent Runtime V1 最小可观测状态。单 Agent 对话和生成 loop 中的角色 brief 必须写 `.agent/runtime/agents/.json` 与 `.agent/runtime/events/.jsonl`,记录 `agentId`、`taskId`、`sessionId`、`runId`、`source`、`status`、`phase`、当前任务 / 动作、计划、观测、允许工具、最近回复和错误;单 Agent 流式聊天事件要回传最新 `runtimeState`,开发单 Agent 聊天页和项目内单 Agent 对话弹窗只读展示该状态,读取失败必须可见提示,不得静默伪装为空状态。`source=agent-chat` 表示开发者单 Agent 对话,`source=generate-draft` 表示生成 loop 角色 brief;carry-over brief 只记录继承和完成,不伪装成重新调用 LLM。Runtime state 写入使用临时文件替换,event JSONL 读取跳过坏行,用户 prompt / 回复摘要进入 runtime 与 `agent.db` 前复用敏感上下文过滤;`.agent/runtime/` 是运行观测状态,不进入项目索引、checkpoint diff 或 restore 删除范围。该层仍是本地 JSONL 状态与事件,不引入 SQLite、常驻独立进程、远程 runner 或可中断上游 LLM 的承诺。 - 2026-07-10 调整:Agent Runtime state 新增 `toolPolicy`,从项目权限策略派生工具级 `allowedTools`、`autoTools`、`confirmTools` 和 `deniedTools`。后台 planning prompt 必须带入该快照,让 Agent 在规划阶段知道工具策略;执行阶段仍由 Runtime 白名单和项目权限 gate 决定。`blackboard.write` 继承 `memory.write` 策略,`agent.message` 继承 `conversation.write` 策略,`agent.delegate` 使用独立 `agent.delegate` 策略。 - 2026-07-10 调整:`.agent/policy.json` 支持 `agentPolicies`,用规范 Agent id 保存单个 Agent 的 `deniedCommands / confirmCommands`。Runtime 计算有效工具策略时把项目级策略和 Agent 级策略叠加,项目级策略继续对所有 Agent 生效,Agent 级策略只能进一步拒绝或要求确认,不能放宽项目级策略;拒绝优先于确认。主聊天新增 `/agent-policy-deny Agent 命令`、`/agent-policy-allow Agent 命令`、`/agent-policy-confirm Agent 命令` 和 `/agent-policy-auto Agent 命令`,继续通过 `project.policy_write` 确认卡写入策略。 +- 2026-07-10 调整:后台 Agent 工具命中确认策略时不再当作 `blocked` observation 继续收尾,而是把当前 Runtime 写成 `status/phase = waiting-for-confirmation`,`waitingOn` 固定为等待开发者确认工具动作,`recentToolCalls`、事件流、任务记录和 `taskQueue.waitingForConfirmation` 都保留该事实;同一 Agent 的后台 drain 暂停,不继续消费后续 pending 任务。命中拒绝策略仍使用 `blocked` observation 交回 Agent 修正计划。 - 2026-07-10 调整:Agent Runtime state 新增 `recentToolCalls`,后台 loop 每次执行白名单工具后记录最近 20 条结构化动作,包含 tool、status、reason、summary、detail 和 updatedAt。状态面板展示最近动作时使用该字段,不解析 observation 文本;写入前继续过滤敏感上下文,不保存原始密钥或任意未过滤输入。 - 2026-07-10 调整:Agent Runtime state 新增 `currentGoal` 和 `waitingOn`。`currentGoal` 固定表达本轮任务目标,`waitingOn` 表达当前等待 LLM、工具观察、开发者输入或失败处理;后台任务生命周期、`agent.run_status` observation、下一轮 planning prompt、开发单 Agent 对话页、项目内 Agent 对话弹窗和主窗口 Agent 状态列表都必须展示同一份目标 / 等待状态。 - 2026-07-10 调整:Agent Runtime state 新增 `loopIteration / maxLoopIterations / toolActionBudget`。后台 Agent loop 每轮规划前刷新当前轮次、最大轮次和每轮工具动作预算;开发窗口 Runtime 面板、主窗口 Agent 状态列表、`agent.run_status` observation 和下一轮 planning prompt 都展示该进度,字段只做运行观测,不改变 loop 上限或权限 gate。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 25844c759..256deba31 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -35,6 +35,7 @@ Agent Runtime 负责: - 后台任务能力:开发窗口单 Agent 聊天和项目内 Agent 对话弹窗可把当前输入投递为单 Agent 后台任务,Tauri 命令 `start_game_creator_agent_runtime_task` 会立即写入该 Agent 的 `.agent/runtime/agents/.json`、`.agent/runtime/events/.jsonl`、`.agent/runtime/tasks/.jsonl` 和 `.agent/conversations/agents/.jsonl`,随后在 App 进程内启动 tokio task 执行最小 Agent loop:每轮让该 Agent 输出 `thinkingSummary / plan / actions / response`,Runtime 按白名单和项目权限策略执行工具动作,写入 `thinking_summary / plan / action / observation / response / error` 事件,再把 observation 放入下一轮 prompt 让 Agent 修正计划、继续行动或用空 actions + response 收束;后台任务最多执行 3 轮 loop,仍未收束时再按最后计划和全部观察生成最终回复并追加回对话。工具箱包含只读工具 `memory.read`、`conversation.read`、`asset.list`、`project.index`、`project.diff`、`file.list`、`file.read`、`agent.run_status`,以及受策略保护的写/运行工具 `memory.write`、`file.write`、`command.run_limited`、`blackboard.write`、`agent.message` 和 `agent.delegate`;`memory.write` 可追加或覆盖本 Agent 私有记忆、项目长期/短期记忆或黑板,`file.write` 只能写项目内相对路径并记录审计,`command.run_limited` 只接受 `game.static_smoke` 并复用本地静态自检安全边界,`blackboard.write` 追加 `memory/blackboard.md`,`agent.message` 给目标 `.agent/conversations/agents/.jsonl` 写入 tool 留言,`agent.delegate` 把任务投递到目标 Agent 的独立后台队列,策略要求确认或拒绝时不执行写入、运行或委派,只把策略结果作为 observation 回给 Agent。`read_game_creator_agent_runtime` 会按 `runId` 去重返回最近任务和最近事件,`read_game_creator_agent_runtimes` 批量读取所有规范 taskId 的 runtime;开发窗口和项目内 Agent 对话弹窗的 Runtime 状态面板展示最近事件、最近任务、当前目标、当前任务、当前动作、等待对象、下一步和运行阶段,主窗口 Agent 状态列表展示当前目标、任务、动作、等待对象和运行阶段摘要。不同 Agent 使用各自 runtime 锁,可以并行运行;同一 Agent 已有运行任务时,新任务会先写成 `pending / queued`,由当前后台 drain 在完成后串行继续执行。该能力仍属于 Runtime V1 的进程内任务,不是独立 OS 进程或跨重启离线常驻 worker。 - 2026-07-10 补充:Agent Runtime state 新增 `toolPolicy`,按当前项目 `.agent/policy.json` 派生工具级 `allowedTools / autoTools / confirmTools / deniedTools` 快照;后台 planning prompt 会带入该快照,让 Agent 在规划时知道哪些工具会自动执行、需要确认或被拒绝。`blackboard.write` 继承 `memory.write` 策略,`agent.message` 继承 `conversation.write` 策略,`agent.delegate` 使用独立 `agent.delegate` 策略;实际执行仍以 Runtime 的白名单和项目权限 gate 为准。 - 2026-07-10 补充:`.agent/policy.json` 新增 `agentPolicies`,可按规范 Agent id 分别配置 `deniedCommands / confirmCommands`。有效策略为“项目级策略 + Agent 级策略”的保守叠加:项目级拒绝 / 确认仍对所有 Agent 生效,Agent 级策略只能进一步限制该 Agent,不能放宽项目级策略,拒绝优先于确认。主聊天新增 `/agent-policy-deny Agent 命令`、`/agent-policy-allow Agent 命令`、`/agent-policy-confirm Agent 命令` 和 `/agent-policy-auto Agent 命令`,写入前仍走 `project.policy_write` 确认卡。 +- 2026-07-10 补充:后台 Agent 工具命中 `confirmCommands` 时不再继续整理最终回复,而是把本轮 Runtime 停在 `status/phase = waiting-for-confirmation`,`waitingOn` 指向“开发者确认 Agent 工具动作”,`recentToolCalls`、事件流、任务记录和 `taskQueue.waitingForConfirmation` 都保留该待确认事实;同一 Agent 的 drain 会暂停,不继续消费后续 pending 任务。命中拒绝策略仍作为 `blocked` observation 交回 Agent 继续修正计划。 - 2026-07-10 补充:Agent Runtime state 新增 `recentToolCalls`,每次后台工具执行后记录最近 20 条结构化工具动作,包含 tool、status、reason、summary、detail 和 updatedAt;开发窗口、项目内 Agent 对话弹窗和主窗口 Agent 状态列表可直接展示“最近动作”,不再只能从 observation 字符串里猜测 action / observation 对应关系。字段只保存过滤后的摘要和观察细节,不保存原始 API Key 或任意未过滤输入。 - 2026-07-10 补充:Agent Runtime state 新增 `currentGoal` 和 `waitingOn`,把本轮目标与当前等待对象从 `currentTask / currentAction / nextStep` 中显式拆出来;后台任务启动、工具 observation、完成和失败都会刷新该状态,开发窗口、项目内 Agent 对话弹窗、主窗口 Agent 状态列表、`agent.run_status` observation 和下一轮 planning prompt 都展示同一份目标 / 等待信息,避免开发者只能从动作文本里猜 Agent 卡在 LLM、工具、同伴还是人工输入。 - 2026-07-10 补充:Agent Runtime state 新增 `loopIteration / maxLoopIterations / toolActionBudget`,结构化记录后台 Agent loop 当前轮次、最大轮次和每轮工具动作预算;开发窗口 Runtime 面板、主窗口 Agent 状态列表、`agent.run_status` observation 和下一轮 planning prompt 都展示该进度,帮助判断 Agent 是刚开始规划、正在 replan,还是接近本轮 loop 上限。该字段只做运行观测,不改变后台 loop 的执行上限或工具权限。 @@ -273,7 +274,7 @@ game-project/ - Tauri Rust 入口保持薄壳:`src-tauri/src/main.rs` 只保留共享类型 / 常量、模块声明、CLI preflight、`tauri::Builder`、运行时配置初始化和 `invoke_handler` 清单;命令行入口放在 `cli.rs`,Tauri command 包装放在 `commands.rs`,运行时配置与 LLM 配置检查放在 `config.rs`,Agent loop 与生成编排放在 `agent.rs`,上传 / 画板 / 平台美术生成接入放在 `assets.rs`,本地项目文件、记忆、对话、权限、checkpoint、manifest 和通用路径工具放在 `project.rs`,本地 HTTP 预览与 preview 命令放在 `preview.rs`,旧窗口兼容命令放在 `windows.rs`,Rust 单测放在 `tests.rs`。后续继续拆分时保持 Tauri command 名、JSON 字段、`.agent/*` 路径和错误语义不变。 - 本地项目初始化会创建 `game/`、`assets/`、`memory/`、`memory/agents/`、`exports/`、`.agent/logs/`,写入 `.agent/manifest.json`,生成 append-only JSONL 本地项目索引 `.agent/agent.db`,并生成默认 `game/index.html`。 - v1 conversation 记录使用 append-only JSONL,每行带 `schemaVersion`、`role`、`content`、`agentId` 和 `updatedAt`,作为聊天历史和单 agent 对话历史的事实源;目录在首次写入时创建。 -- 开发窗口和项目内 Agent 对话弹窗的“后台运行”只启动或排队单 Agent 后台任务,不阻塞等待回复;用户可刷新同一 Agent 对话或 runtime 状态查看进度和结果。后台任务会向 `.agent/runtime/tasks/.jsonl` 追加任务视角记录,任务状态使用 `pending / running / completed / failed`,读取时按 `runId` 去重返回最近任务;runtime state 自身仍可在完成后显示 `idle / completed`,二者语义分开。同一 Agent 的 pending 任务由持有 `.agent/runtime/locks/.lock` 的后台 drain 串行执行,避免同一 Agent 并发抢上下文;不同 Agent 仍可并行。客户端重开项目时会对当前项目路径自动尝试一次 Runtime 恢复;恢复命令必须通过 `agent.resume` 自动权限,默认确认策略下不会静默启动;同一 Agent 同时存在上一进程遗留 `running` 和 `pending` 时,先重接 `running`,再由 drain 继续 `pending`。后台任务的核心 loop 最多 3 轮:每轮把已有 observation 带回 LLM 让 Agent 重新规划,只有 actions 为空且 response 非空时提前收束,否则继续执行白名单工具,跑满后再进入最终回复整理。后台任务完成后会把 assistant 回复追加到对应 `.agent/conversations/agents/.jsonl`,并向 `.agent/agent.db` 写入 `agent.runtime.background_task.queued` / `agent.runtime.background_task` / `agent.runtime.background_task.recovered` / `agent.runtime.tool_observation` / `agent.runtime.memory.write` / `agent.runtime.file.write` / `agent.runtime.command.run_limited` / `agent.runtime.blackboard.write` / `agent.runtime.agent.message` / `agent.runtime.agent.delegate` / `agent.runtime.background_task.completed` / `agent.runtime.background_task.failed` 审计记录。当前工具箱开放只读工具 `memory.read`、`conversation.read`、`asset.list`、`project.index`、`project.diff`、`file.list`、`file.read`、`agent.run_status`,以及受策略保护的写/运行工具 `memory.write`、`file.write`、`command.run_limited`、`blackboard.write`、`agent.message` 和 `agent.delegate`;若项目策略要求确认或拒绝,对应工具不会执行,Runtime 会把策略结果作为 observation 回给 Agent。`toolPolicy` 保存当前工具级权限快照,供 planning prompt 和状态面板展示;`recentToolCalls` 保存最近 20 条结构化工具动作,供状态面板展示最近动作;`.agent/agent.db` 追加写入按整行 JSONL 写入,减少多个 Agent 同时完成时的行交错风险。 +- 开发窗口和项目内 Agent 对话弹窗的“后台运行”只启动或排队单 Agent 后台任务,不阻塞等待回复;用户可刷新同一 Agent 对话或 runtime 状态查看进度和结果。后台任务会向 `.agent/runtime/tasks/.jsonl` 追加任务视角记录,任务状态使用 `pending / running / waiting-for-confirmation / completed / failed`,读取时按 `runId` 去重返回最近任务;runtime state 自身仍可在完成后显示 `idle / completed`,二者语义分开。同一 Agent 的 pending 任务由持有 `.agent/runtime/locks/.lock` 的后台 drain 串行执行,避免同一 Agent 并发抢上下文;不同 Agent 仍可并行;若某个工具动作命中确认策略,该 Agent 会停在 `waiting-for-confirmation` 并暂停继续消费队列,等待后续确认或策略调整。客户端重开项目时会对当前项目路径自动尝试一次 Runtime 恢复;恢复命令必须通过 `agent.resume` 自动权限,默认确认策略下不会静默启动;同一 Agent 同时存在上一进程遗留 `running` 和 `pending` 时,先重接 `running`,再由 drain 继续 `pending`。后台任务的核心 loop 最多 3 轮:每轮把已有 observation 带回 LLM 让 Agent 重新规划,只有 actions 为空且 response 非空时提前收束,否则继续执行白名单工具,跑满后再进入最终回复整理。后台任务完成后会把 assistant 回复追加到对应 `.agent/conversations/agents/.jsonl`,并向 `.agent/agent.db` 写入 `agent.runtime.background_task.queued` / `agent.runtime.background_task` / `agent.runtime.background_task.recovered` / `agent.runtime.tool_observation` / `agent.runtime.tool_confirmation_required` / `agent.runtime.memory.write` / `agent.runtime.file.write` / `agent.runtime.command.run_limited` / `agent.runtime.blackboard.write` / `agent.runtime.agent.message` / `agent.runtime.agent.delegate` / `agent.runtime.background_task.completed` / `agent.runtime.background_task.failed` 审计记录。当前工具箱开放只读工具 `memory.read`、`conversation.read`、`asset.list`、`project.index`、`project.diff`、`file.list`、`file.read`、`agent.run_status`,以及受策略保护的写/运行工具 `memory.write`、`file.write`、`command.run_limited`、`blackboard.write`、`agent.message` 和 `agent.delegate`;若项目策略拒绝,对应工具不会执行,Runtime 会把策略结果作为 observation 回给 Agent 修正计划;若项目策略要求确认,Runtime 会保留待确认状态而不执行该工具。`toolPolicy` 保存当前工具级权限快照,供 planning prompt 和状态面板展示;`recentToolCalls` 保存最近 20 条结构化工具动作,供状态面板展示最近动作;`.agent/agent.db` 追加写入按整行 JSONL 写入,减少多个 Agent 同时完成时的行交错风险。 - 2026-07-10 补充:当前工具箱还开放 `preview.start`,审计记录类型为 `agent.runtime.preview.start`;该工具不会打开任意 URL,只启动当前授权项目的 `127.0.0.1` 本地预览,并和 Tauri 用户命令共用同一个 `PreviewRegistry`。 - 2026-07-10 补充:当前工具箱还开放 `canvas.asset_generate`,审计记录类型为 `agent.runtime.canvas.asset_generate`;该工具只通过配置好的 External Editor API 生成并回流素材,不暴露任意上传 / 任意网络请求能力。 - 2026-07-10 补充:当前工具箱还开放 `task.list`;该工具只读取 manifest 任务图、状态、依赖、产物和 `readyTaskIds`,并受 `task.list` 项目权限策略保护。