From 8c9693a907ef416cb9678863f5f5080b210cc255 Mon Sep 17 00:00:00 2001 From: AIGameCreator App Date: Fri, 10 Jul 2026 06:26:29 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81Agent=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E7=A1=AE=E8=AE=A4=E5=90=8E=E7=BB=A7=E7=BB=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增一次性工具确认票据并在Runtime权限gate中按run消费。 提供确认待确认后台任务的Tauri命令和开发面板按钮。 确认后把原waiting run标记为confirmed并投递新run继续执行。 补充确认续跑测试并同步Agent Runtime文档。 --- .../src-tauri/src/agent.rs | 242 +++++++++++++++++- .../src-tauri/src/commands.rs | 22 ++ .../src-tauri/src/main.rs | 1 + .../src-tauri/src/tests.rs | 112 ++++++++ apps/ai-game-creator-shell/src/App.tsx | 99 ++++++- .../shared-memory/decision-log.md | 1 + ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 3 +- 7 files changed, 474 insertions(+), 6 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 778f18bc2..fe6c63c4b 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent.rs @@ -375,6 +375,19 @@ fn start_game_creator_agent_background_task_with_run_id_at( agent_id: &str, task: &str, run_id: &str, +) -> Result<(AgentRuntimeResult, String), String> { + start_game_creator_agent_background_task_with_confirmed_tool_at( + root, agent_id, task, run_id, None, "", + ) +} + +fn start_game_creator_agent_background_task_with_confirmed_tool_at( + root: &Path, + agent_id: &str, + task: &str, + run_id: &str, + confirmed_command_id: Option<&str>, + confirmation_note: &str, ) -> Result<(AgentRuntimeResult, String), String> { let agent_id = normalize_game_creator_runtime_agent_id(agent_id)?; validate_project_root(root)?; @@ -408,6 +421,15 @@ fn start_game_creator_agent_background_task_with_run_id_at( "task": pending_task.task, }), )?; + if let Some(command_id) = confirmed_command_id { + write_game_creator_agent_runtime_tool_confirmation( + root, + &agent_id, + &run_id, + command_id, + confirmation_note, + )?; + } let Some(runtime_lock) = try_acquire_game_creator_agent_runtime_task_lock(root, &agent_id)? else { let result = read_game_creator_agent_runtime_at(root, &agent_id)?; @@ -592,6 +614,102 @@ pub(crate) fn retry_game_creator_agent_runtime_task_at( Ok(result) } +pub(crate) fn confirm_game_creator_agent_runtime_task_at( + root: &Path, + agent_id: &str, + run_id: &str, + next_run_id: &str, + note: &str, +) -> Result { + let agent_id = normalize_game_creator_runtime_agent_id(agent_id)?; + validate_project_root(root)?; + if run_id.trim().is_empty() { + return Err("Agent Runtime runId 不能为空".to_string()); + } + let target_run_id = normalize_game_creator_agent_runtime_run_id(&agent_id, run_id); + let task = + read_latest_game_creator_agent_runtime_task_by_run_id(root, &agent_id, &target_run_id)? + .ok_or_else(|| format!("未找到 Agent Runtime 任务:{target_run_id}"))?; + if task.status != "waiting-for-confirmation" { + return Err(format!( + "Agent Runtime 任务不在待确认状态,不能确认继续:{target_run_id}" + )); + } + let runtime = read_game_creator_agent_runtime_at(root, &agent_id)?.state; + if runtime.run_id != target_run_id || runtime.status != "waiting-for-confirmation" { + return Err(format!( + "Agent Runtime 当前状态不是该待确认 run:{target_run_id}" + )); + } + let tool_call = runtime + .recent_tool_calls + .iter() + .rev() + .find(|call| call.status == "waiting-for-confirmation") + .ok_or_else(|| "未找到待确认工具动作".to_string())?; + let command_id = game_creator_agent_runtime_tool_command_id(&tool_call.tool) + .ok_or_else(|| format!("待确认工具不在白名单中:{}", tool_call.tool))?; + let note = sanitize_agent_runtime_text(note, 240); + let confirmed_task = if note.trim().is_empty() { + format!( + "继续已确认的后台任务:{}\n\n开发者已确认工具动作:{}。", + task.task, tool_call.tool + ) + } else { + format!( + "继续已确认的后台任务:{}\n\n开发者已确认工具动作:{}。确认说明:{}", + task.task, tool_call.tool, note + ) + }; + let requested_run_id = if next_run_id.trim().is_empty() { + format!("{target_run_id}-confirm-{}", unix_timestamp()) + } else { + next_run_id.trim().to_string() + }; + let (result, confirmed_run_id) = + start_game_creator_agent_background_task_with_confirmed_tool_at( + root, + &agent_id, + &confirmed_task, + &requested_run_id, + Some(command_id), + ¬e, + )?; + append_game_creator_agent_runtime_task_record( + root, + &AgentRuntimeTaskRecord { + schema_version: AGENT_RUNTIME_SCHEMA_VERSION.to_string(), + agent_id: task.agent_id.clone(), + task_id: task.task_id.clone(), + session_id: task.session_id.clone(), + run_id: task.run_id.clone(), + source: task.source.clone(), + task: task.task.clone(), + status: "completed".to_string(), + phase: "confirmed".to_string(), + current_action: format!("已确认 {},继续 run {}", tool_call.tool, confirmed_run_id), + error: None, + updated_at: unix_timestamp(), + }, + )?; + append_agent_db_record( + root, + serde_json::json!({ + "recordType": "agent.runtime.tool_confirmation.approved", + "agentId": task.agent_id, + "taskId": task.task_id, + "sessionId": task.session_id, + "runId": task.run_id, + "confirmedRunId": confirmed_run_id, + "tool": tool_call.tool, + "commandId": command_id, + "note": note, + }), + )?; + emit_game_creator_agent_runtime_update(root, &agent_id); + read_game_creator_agent_runtime_at(root, &agent_id).or(Ok(result)) +} + fn game_creator_agent_background_task_default_plan() -> Vec { vec![ "记录开发者投递的后台任务".to_string(), @@ -845,9 +963,14 @@ async fn run_game_creator_agent_background_task( if stop_game_creator_agent_runtime_if_cancel_requested(&root, &mut runtime) { return AgentBackgroundTaskOutcome::Finished; } - let observation = - execute_game_creator_agent_runtime_tool_action(&root, &agent_id, &task, action) - .await; + let observation = execute_game_creator_agent_runtime_tool_action( + &root, + &agent_id, + runtime.run_id.as_str(), + &task, + action, + ) + .await; if stop_game_creator_agent_runtime_if_cancel_requested(&root, &mut runtime) { return AgentBackgroundTaskOutcome::Finished; } @@ -1445,6 +1568,7 @@ fn parse_game_creator_agent_tool_plan_response( pub(crate) async fn execute_game_creator_agent_runtime_tool_action( root: &Path, agent_id: &str, + run_id: &str, task: &str, action: &AgentRuntimeToolAction, ) -> AgentRuntimeToolObservation { @@ -1452,7 +1576,7 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action( let command_id = game_creator_agent_runtime_tool_command_id(tool); if let Some(command_id) = command_id { if let Some(blocked) = - game_creator_agent_runtime_tool_policy_block(root, agent_id, command_id) + game_creator_agent_runtime_tool_policy_block(root, agent_id, run_id, command_id) { let (status, summary) = match blocked { AgentRuntimeToolPolicyBlock::Denied(summary) => ("blocked", summary), @@ -1533,6 +1657,101 @@ fn game_creator_agent_runtime_tool_command_id(tool: &str) -> Option<&'static str } } +fn agent_runtime_confirmation_path_component(value: &str, fallback: &str) -> String { + let normalized = value + .trim() + .chars() + .map(|character| { + if character.is_ascii_alphanumeric() + || character == '-' + || character == '_' + || character == '.' + { + character + } else { + '-' + } + }) + .collect::(); + let normalized = normalized.trim_matches('-'); + if normalized.is_empty() { + fallback.to_string() + } else { + truncate_agent_runtime_text(normalized, 160) + } +} + +fn game_creator_agent_runtime_tool_confirmation_path( + root: &Path, + agent_id: &str, + run_id: &str, + command_id: &str, +) -> PathBuf { + root.join(".agent/runtime/confirmations") + .join(agent_runtime_confirmation_path_component(agent_id, "agent")) + .join(agent_runtime_confirmation_path_component(run_id, "run")) + .join(format!( + "{}.json", + agent_runtime_confirmation_path_component(command_id, "command") + )) +} + +fn write_game_creator_agent_runtime_tool_confirmation( + root: &Path, + agent_id: &str, + run_id: &str, + command_id: &str, + note: &str, +) -> Result<(), String> { + let path = + game_creator_agent_runtime_tool_confirmation_path(root, agent_id, run_id, command_id); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).map_err(|error| { + format!( + "创建 Agent Runtime 工具确认目录失败:{}: {error}", + parent.display() + ) + })?; + } + let payload = serde_json::json!({ + "schemaVersion": AGENT_RUNTIME_SCHEMA_VERSION, + "agentId": agent_id, + "runId": run_id, + "commandId": command_id, + "note": sanitize_agent_runtime_text(note, 240), + "updatedAt": unix_timestamp(), + }); + let content = serde_json::to_string_pretty(&payload) + .map_err(|error| format!("序列化 Agent Runtime 工具确认失败:{error}"))?; + fs::write(&path, content).map_err(|error| { + format!( + "写入 Agent Runtime 工具确认失败:{}: {error}", + path.display() + ) + }) +} + +fn consume_game_creator_agent_runtime_tool_confirmation( + root: &Path, + agent_id: &str, + run_id: &str, + command_id: &str, +) -> Result { + if run_id.trim().is_empty() { + return Ok(false); + } + let path = + game_creator_agent_runtime_tool_confirmation_path(root, agent_id, run_id, command_id); + match fs::remove_file(&path) { + Ok(()) => Ok(true), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false), + Err(error) => Err(format!( + "消费 Agent Runtime 工具确认失败:{}: {error}", + path.display() + )), + } +} + fn agent_runtime_executable_tools() -> Vec<&'static str> { vec![ "memory.read", @@ -1637,6 +1856,7 @@ fn agent_runtime_effective_tool_policy_at( fn game_creator_agent_runtime_tool_policy_block( root: &Path, agent_id: &str, + run_id: &str, command_id: &str, ) -> Option { let view = match read_project_permission_policy_at(root) { @@ -1679,6 +1899,13 @@ fn game_creator_agent_runtime_tool_policy_block( .iter() .any(|command| command == command_id) { + match consume_game_creator_agent_runtime_tool_confirmation( + root, &agent_id, run_id, command_id, + ) { + Ok(true) => return None, + Ok(false) => {} + Err(error) => return Some(AgentRuntimeToolPolicyBlock::Denied(error)), + } return Some(AgentRuntimeToolPolicyBlock::RequiresConfirmation(format!( "项目权限策略要求用户确认:{command_id}" ))); @@ -1695,6 +1922,13 @@ fn game_creator_agent_runtime_tool_policy_block( }) .unwrap_or(false) { + match consume_game_creator_agent_runtime_tool_confirmation( + root, &agent_id, run_id, command_id, + ) { + Ok(true) => return None, + Ok(false) => {} + Err(error) => return Some(AgentRuntimeToolPolicyBlock::Denied(error)), + } return Some(AgentRuntimeToolPolicyBlock::RequiresConfirmation(format!( "Agent 权限策略要求用户确认:{agent_id} / {command_id}" ))); 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 2a9217ba7..2d5400461 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/commands.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/commands.rs @@ -426,6 +426,28 @@ pub(crate) fn retry_game_creator_agent_runtime_task( ) } +#[tauri::command] +pub(crate) fn confirm_game_creator_agent_runtime_task( + project_path: String, + agent_id: String, + run_id: String, + next_run_id: String, + note: String, +) -> Result { + let root = Path::new(project_path.trim()); + enforce_project_permission_policy(root, "conversation.read")?; + enforce_project_permission_policy(root, "conversation.write")?; + enforce_project_permission_policy(root, "agent.run_status")?; + enforce_project_auto_permission_policy(root, "agent.resume")?; + confirm_game_creator_agent_runtime_task_at( + root, + agent_id.trim(), + run_id.trim(), + next_run_id.trim(), + note.trim(), + ) +} + #[tauri::command] pub(crate) fn read_game_creator_agent_runtime( 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 140a0b23d..955240158 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/main.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/main.rs @@ -1202,6 +1202,7 @@ fn main() { start_game_creator_agent_runtime_task, cancel_game_creator_agent_runtime_task, retry_game_creator_agent_runtime_task, + confirm_game_creator_agent_runtime_task, read_game_creator_agent_runtime, read_game_creator_agent_runtimes, resume_game_creator_agent_runtime_tasks, 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 8c50f872b..67b21efe0 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests.rs @@ -1640,6 +1640,7 @@ async fn background_agent_runtime_tool_action_respects_agent_policy() { let observation = execute_game_creator_agent_runtime_tool_action( &root, "design-director", + "direct-policy-test-run", "读取设计笔记", &AgentRuntimeToolAction { tool: "file.read".to_string(), @@ -4360,6 +4361,117 @@ async fn background_agent_runtime_tool_action_respects_confirm_policy() { fs::remove_dir_all(root).ok(); } +#[tokio::test] +async fn background_agent_runtime_can_confirm_and_continue_waiting_tool_action() { + let root = unique_project_path(); + init_local_game_project_at(&root, "project-1", "月光厨房").expect("project init"); + fs::write(root.join("game/notes.txt"), "核心循环笔记").expect("write notes"); + write_project_permission_policy_at( + &root, + ProjectPermissionPolicy { + denied_commands: Vec::new(), + confirm_commands: vec!["file.read".to_string()], + agent_policies: BTreeMap::new(), + }, + ) + .expect("write policy"); + let (sender, receiver) = mpsc::channel(); + let read_plan = serde_json::json!({ + "thinkingSummary": "需要读项目笔记", + "plan": ["读取项目笔记", "回复开发者"], + "actions": [ + { + "tool": "file.read", + "reason": "确认项目笔记", + "input": { "path": "game/notes.txt" } + } + ], + "response": "" + }) + .to_string(); + let final_plan = serde_json::json!({ + "thinkingSummary": "已经拿到项目笔记", + "plan": [], + "actions": [], + "response": "确认后已读取笔记:核心循环笔记。" + }) + .to_string(); + let base_url = spawn_mock_llm_server_responses_with_capture( + vec![read_plan.clone(), read_plan, final_plan], + Some(sender), + ); + let _config_guard = write_test_local_config(format!( + r#"{{ + "agentLlm": {{ + "design-director": {{ + "apiKey": "design-key", + "baseUrl": {base_url:?}, + "model": "design-runtime-model", + "apiKind": "openai_responses" + }} + }} +}}"# + )); + + start_game_creator_agent_background_task_at( + &root, + "design-director", + "后台分析当前玩法循环", + "design-confirm-run", + ) + .expect("start background task"); + + receiver + .recv_timeout(Duration::from_secs(2)) + .expect("first plan llm request"); + let waiting_runtime = wait_for_agent_runtime_confirmation(&root, "design-director"); + assert_eq!(waiting_runtime.status, "waiting-for-confirmation"); + + let confirmed = confirm_game_creator_agent_runtime_task_at( + &root, + "design-director", + "design-confirm-run", + "design-confirm-run-approved", + "允许读取项目笔记", + ) + .expect("confirm waiting task"); + assert_eq!(confirmed.state.run_id, "design-confirm-run-approved"); + receiver + .recv_timeout(Duration::from_secs(2)) + .expect("confirmed run plan request"); + receiver + .recv_timeout(Duration::from_secs(2)) + .expect("confirmed run final plan request"); + + let runtime = wait_for_agent_runtime_idle(&root, "design-director"); + assert_eq!(runtime.status, "idle"); + assert_eq!( + runtime.last_response.as_deref(), + Some("确认后已读取笔记:核心循环笔记。") + ); + assert_eq!(runtime.task_queue.waiting_for_confirmation, 0); + assert!(runtime + .observations + .iter() + .any(|item| item.contains("file.read:ok"))); + 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-confirm-run" + && task.status == "completed" + && task.phase == "confirmed" + })); + assert!(runtime_result.recent_tasks.iter().any(|task| { + task.run_id == "design-confirm-run-approved" && task.status == "completed" + })); + let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db"); + assert!(agent_db.contains("\"recordType\":\"agent.runtime.tool_confirmation.approved\"")); + assert!(agent_db.contains("\"confirmedRunId\":\"design-confirm-run-approved\"")); + assert!(agent_db.contains("\"tool\":\"file.read\"")); + + fs::remove_dir_all(root).ok(); +} + #[tokio::test] async fn background_agent_runtime_can_list_project_files() { let root = unique_project_path(); diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index 6d92f2d0b..003e0be39 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -772,18 +772,24 @@ function agentRuntimeCanRetry(status: string) { return ['cancelled', 'failed', 'completed', 'idle'].includes(status); } +function agentRuntimeCanConfirm(status: string) { + return status === 'waiting-for-confirmation'; +} + function AgentRuntimeStatusPanel({ runtime, error, controlBusy = false, onCancelRuntimeTask, onRetryRuntimeTask, + onConfirmRuntimeTask, }: { runtime: AgentRuntimeState | null; error?: string | null; controlBusy?: boolean; onCancelRuntimeTask?: (runId: string) => void; onRetryRuntimeTask?: (runId: string) => void; + onConfirmRuntimeTask?: (runId: string) => void; }) { if (!runtime && error) { return ( @@ -818,14 +824,27 @@ function AgentRuntimeStatusPanel({ Boolean(runtime.runId) && agentRuntimeCanRetry(runtime.status) && Boolean(onRetryRuntimeTask); + const canConfirm = + Boolean(runtime.runId) && + agentRuntimeCanConfirm(runtime.status) && + Boolean(onConfirmRuntimeTask); return (
{`${runtime.status} / ${runtime.phase}`} {runtime.sessionId}
- {onCancelRuntimeTask || onRetryRuntimeTask ? ( + {onCancelRuntimeTask || onRetryRuntimeTask || onConfirmRuntimeTask ? (
+