From a5c85c912ba70ea19e772397d066bee7ee4484f5 Mon Sep 17 00:00:00 2001 From: AIGameCreator App Date: Fri, 10 Jul 2026 09:01:32 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=91=E5=AE=9AAgent=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E7=A1=AE=E8=AE=A4=E5=88=B0=E7=B2=BE=E7=A1=AE=E5=8A=A8=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Runtime 为工具输入生成 SHA-256 指纹和安全摘要 确认票据仅放行同一 Agent、run、command 和输入 开发窗口接通待确认动作的确认继续入口 补充动作变更阻断和界面确认回归测试 同步 Runtime V1 技术方案和项目记忆 --- .../src-tauri/Cargo.lock | 1 + .../src-tauri/Cargo.toml | 1 + .../src-tauri/src/agent.rs | 216 ++++++++++++++++-- .../src-tauri/src/commands.rs | 3 +- .../src-tauri/src/main.rs | 4 + .../src-tauri/src/tests.rs | 152 +++++++++++- apps/ai-game-creator-shell/src/App.tsx | 66 ++++++ .../tests/appSurface.test.ts | 190 ++++++++++++++- .../shared-memory/decision-log.md | 4 +- ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 6 +- 10 files changed, 613 insertions(+), 30 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/Cargo.lock b/apps/ai-game-creator-shell/src-tauri/Cargo.lock index d71f6667e..76beeef19 100644 --- a/apps/ai-game-creator-shell/src-tauri/Cargo.lock +++ b/apps/ai-game-creator-shell/src-tauri/Cargo.lock @@ -1314,6 +1314,7 @@ dependencies = [ "reqwest 0.12.28", "serde", "serde_json", + "sha2", "shared-contracts", "tauri", "tauri-build", diff --git a/apps/ai-game-creator-shell/src-tauri/Cargo.toml b/apps/ai-game-creator-shell/src-tauri/Cargo.toml index 77de932fa..ef895cd29 100644 --- a/apps/ai-game-creator-shell/src-tauri/Cargo.toml +++ b/apps/ai-game-creator-shell/src-tauri/Cargo.toml @@ -10,6 +10,7 @@ tauri-build = { version = "2.6.2", features = [] } [dependencies] serde = { version = "1", features = ["derive"] } serde_json = "1" +sha2 = "0.10" platform-llm = { path = "../../../server-rs/crates/platform-llm" } platform-agent = { path = "../../../server-rs/crates/platform-agent" } reqwest = { version = "0.12", default-features = false, features = ["json", "native-tls"] } 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 7f7a72e14..50d88e804 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent.rs @@ -1,4 +1,5 @@ use super::*; +use sha2::{Digest, Sha256}; static GAME_CREATOR_AGENT_RUNTIME_UPDATE_APP_HANDLE: OnceLock = OnceLock::new(); @@ -377,7 +378,7 @@ fn start_game_creator_agent_background_task_with_run_id_at( run_id: &str, ) -> Result<(AgentRuntimeResult, String), String> { start_game_creator_agent_background_task_with_confirmed_tool_at( - root, agent_id, task, run_id, None, "", + root, agent_id, task, run_id, None, None, "", ) } @@ -387,6 +388,7 @@ fn start_game_creator_agent_background_task_with_confirmed_tool_at( task: &str, run_id: &str, confirmed_command_id: Option<&str>, + confirmed_action_fingerprint: Option<&str>, confirmation_note: &str, ) -> Result<(AgentRuntimeResult, String), String> { start_game_creator_agent_background_task_with_source_at( @@ -396,6 +398,7 @@ fn start_game_creator_agent_background_task_with_confirmed_tool_at( run_id, "agent-background-task", confirmed_command_id, + confirmed_action_fingerprint, confirmation_note, ) } @@ -407,6 +410,7 @@ fn start_game_creator_agent_background_task_with_source_at( run_id: &str, source: &str, confirmed_command_id: Option<&str>, + confirmed_action_fingerprint: Option<&str>, confirmation_note: &str, ) -> Result<(AgentRuntimeResult, String), String> { let agent_id = normalize_game_creator_runtime_agent_id(agent_id)?; @@ -447,11 +451,15 @@ fn start_game_creator_agent_background_task_with_source_at( }), )?; if let Some(command_id) = confirmed_command_id { + let action_fingerprint = confirmed_action_fingerprint + .filter(|value| !value.trim().is_empty()) + .ok_or_else(|| "Agent Runtime 工具确认缺少动作指纹".to_string())?; write_game_creator_agent_runtime_tool_confirmation( root, &agent_id, &run_id, command_id, + action_fingerprint, confirmation_note, )?; } @@ -522,6 +530,7 @@ pub(crate) fn schedule_game_creator_agent_ready_tasks_at( &run_id, "agent-ready-task-scheduler", None, + None, "", ) { Ok((result, actual_run_id)) => { @@ -776,6 +785,12 @@ pub(crate) fn confirm_game_creator_agent_runtime_task_at( .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 action_fingerprint = tool_call + .action_fingerprint + .as_deref() + .filter(|value| !value.trim().is_empty()) + .ok_or_else(|| "待确认工具动作缺少精确指纹,请重新提交该任务".to_string())?; + let input_summary = tool_call.input_summary.clone(); let note = sanitize_agent_runtime_text(note, 240); let confirmed_task = if note.trim().is_empty() { format!( @@ -800,6 +815,7 @@ pub(crate) fn confirm_game_creator_agent_runtime_task_at( &confirmed_task, &requested_run_id, Some(command_id), + Some(action_fingerprint), ¬e, )?; append_game_creator_agent_runtime_task_record( @@ -830,6 +846,8 @@ pub(crate) fn confirm_game_creator_agent_runtime_task_at( "confirmedRunId": confirmed_run_id, "tool": tool_call.tool, "commandId": command_id, + "actionFingerprint": action_fingerprint, + "inputSummary": input_summary, "note": note, }), )?; @@ -1103,7 +1121,7 @@ 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); + append_agent_runtime_tool_call_record(&root, &mut runtime, action, &observation); if observation.is_waiting_for_confirmation() { runtime.status = "waiting-for-confirmation".to_string(); runtime.phase = "waiting-for-confirmation".to_string(); @@ -1167,6 +1185,8 @@ async fn run_game_creator_agent_background_task( "taskId": runtime.task_id, "runId": runtime.run_id, "tool": observation.tool, + "actionFingerprint": agent_runtime_tool_action_fingerprint(action), + "inputSummary": agent_runtime_tool_action_input_summary(&root, action), "summary": observation.summary, }), ); @@ -1382,6 +1402,7 @@ enum AgentRuntimeToolPolicyBlock { } fn append_agent_runtime_tool_call_record( + root: &Path, runtime: &mut AgentRuntimeState, action: &AgentRuntimeToolAction, observation: &AgentRuntimeToolObservation, @@ -1389,6 +1410,8 @@ fn append_agent_runtime_tool_call_record( runtime.recent_tool_calls.push(AgentRuntimeToolCallRecord { tool: observation.tool.clone(), status: observation.status.clone(), + action_fingerprint: Some(agent_runtime_tool_action_fingerprint(action)), + input_summary: agent_runtime_tool_action_input_summary(root, action), reason: action .reason .as_deref() @@ -1411,6 +1434,112 @@ fn append_agent_runtime_tool_call_record( } } +fn agent_runtime_tool_action_fingerprint(action: &AgentRuntimeToolAction) -> String { + let payload = serde_json::json!({ + "tool": action.tool.trim(), + "input": &action.input, + }); + let encoded = serde_json::to_vec(&payload).unwrap_or_default(); + format!("{:x}", Sha256::digest(encoded)) +} + +fn agent_runtime_tool_action_input_summary( + root: &Path, + action: &AgentRuntimeToolAction, +) -> Option { + let input = &action.input; + let tool = action.tool.trim(); + let text = |keys: &[&str]| agent_runtime_tool_input_text(input, keys); + let relative_path = |keys: &[&str]| { + let value = text(keys); + if Path::new(&value).is_absolute() { + "[absolute path rejected]".to_string() + } else { + value + } + }; + let chars = |keys: &[&str]| { + keys.iter() + .find_map(|key| input.get(*key).and_then(|value| value.as_str())) + .map(|value| value.chars().count()) + .unwrap_or(0) + }; + let list_len = |keys: &[&str]| { + keys.iter() + .find_map(|key| input.get(*key).and_then(|value| value.as_array())) + .map(Vec::len) + .unwrap_or(0) + }; + let summary = match tool { + "memory.read" => format!("scope={}", text(&["scope"])), + "memory.write" => format!( + "scope={} · mode={} · title={} · contentChars={}", + text(&["scope"]), + text(&["mode"]), + text(&["title"]), + chars(&["content"]) + ), + "project.restore" | "project.diff" => format!( + "checkpointId={}", + text(&["checkpointId", "checkpoint_id", "id"]) + ), + "file.list" | "file.read" => format!("path={}", relative_path(&["path"])), + "file.write" => format!( + "path={} · contentChars={}", + relative_path(&["path"]), + chars(&["content"]) + ), + "task.create" => format!( + "taskId={} · title={} · group={} · role={} · dependencies={} · artifacts={} · criteria={}", + text(&["taskId", "task_id", "id"]), + text(&["title"]), + text(&["group"]), + text(&["role"]), + list_len(&["dependencies"]), + list_len(&["artifacts"]), + list_len(&["acceptanceCriteria", "acceptance_criteria"]) + ), + "task.update" => format!( + "taskId={} · status={}", + text(&["taskId", "task_id", "id"]), + text(&["status"]) + ), + "command.run_limited" => format!( + "commandId={}", + text(&["commandId", "command_id", "id"]) + ), + "canvas.asset_generate" => format!("promptChars={}", chars(&["prompt"])), + "blackboard.write" => format!( + "title={} · contentChars={}", + text(&["title"]), + chars(&["content"]) + ), + "agent.message" => format!( + "agentId={} · contentChars={}", + text(&["agentId", "agent_id", "targetAgentId", "target_agent_id"]), + chars(&["content"]) + ), + "agent.delegate" => format!( + "agentId={} · runId={} · taskChars={}", + text(&["agentId", "agent_id", "targetAgentId", "target_agent_id"]), + text(&["runId", "run_id"]), + chars(&["task"]) + ), + "agent.schedule_ready" => format!( + "limit={}", + input.get("limit").and_then(|value| value.as_u64()).unwrap_or(1) + ), + "agent.run_status" => format!( + "scope={} · agentId={}", + text(&["scope"]), + text(&["agentId", "agent_id", "targetAgentId", "target_agent_id"]) + ), + _ => String::new(), + }; + let summary = redact_agent_runtime_project_paths(root, &summary, 320); + (!summary.trim().is_empty()).then_some(summary) +} + fn update_agent_runtime_plan_steps(runtime: &mut AgentRuntimeState, plan: Vec) { runtime.plan = plan .into_iter() @@ -1700,11 +1829,16 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action( action: &AgentRuntimeToolAction, ) -> AgentRuntimeToolObservation { let tool = action.tool.trim(); + let action_fingerprint = agent_runtime_tool_action_fingerprint(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, run_id, command_id) - { + if let Some(blocked) = game_creator_agent_runtime_tool_policy_block( + root, + agent_id, + run_id, + command_id, + &action_fingerprint, + ) { let (status, summary) = match blocked { AgentRuntimeToolPolicyBlock::Denied(summary) => ("blocked", summary), AgentRuntimeToolPolicyBlock::RequiresConfirmation(summary) => { @@ -1836,6 +1970,7 @@ fn write_game_creator_agent_runtime_tool_confirmation( agent_id: &str, run_id: &str, command_id: &str, + action_fingerprint: &str, note: &str, ) -> Result<(), String> { let path = @@ -1853,6 +1988,7 @@ fn write_game_creator_agent_runtime_tool_confirmation( "agentId": agent_id, "runId": run_id, "commandId": command_id, + "actionFingerprint": action_fingerprint, "note": sanitize_agent_runtime_text(note, 240), "updatedAt": unix_timestamp(), }); @@ -1871,20 +2007,40 @@ fn consume_game_creator_agent_runtime_tool_confirmation( agent_id: &str, run_id: &str, command_id: &str, + action_fingerprint: &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!( + let content = match fs::read_to_string(&path) { + Ok(content) => content, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(false), + Err(error) => { + return Err(format!( + "读取 Agent Runtime 工具确认失败:{}: {error}", + path.display() + )) + } + }; + let payload = serde_json::from_str::(&content).map_err(|error| { + format!( + "解析 Agent Runtime 工具确认失败:{}: {error}", + path.display() + ) + })?; + let confirmed_fingerprint = payload + .get("actionFingerprint") + .and_then(|value| value.as_str()) + .unwrap_or_default(); + fs::remove_file(&path).map_err(|error| { + format!( "消费 Agent Runtime 工具确认失败:{}: {error}", path.display() - )), - } + ) + })?; + Ok(!action_fingerprint.trim().is_empty() && confirmed_fingerprint == action_fingerprint) } fn agent_runtime_executable_tools() -> Vec<&'static str> { @@ -1997,6 +2153,7 @@ fn game_creator_agent_runtime_tool_policy_block( agent_id: &str, run_id: &str, command_id: &str, + action_fingerprint: &str, ) -> Option { let view = match read_project_permission_policy_at(root) { Ok(view) => view, @@ -2039,7 +2196,11 @@ fn game_creator_agent_runtime_tool_policy_block( .any(|command| command == command_id) { match consume_game_creator_agent_runtime_tool_confirmation( - root, &agent_id, run_id, command_id, + root, + &agent_id, + run_id, + command_id, + action_fingerprint, ) { Ok(true) => return None, Ok(false) => {} @@ -2062,7 +2223,11 @@ 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, + root, + &agent_id, + run_id, + command_id, + action_fingerprint, ) { Ok(true) => return None, Ok(false) => {} @@ -3517,12 +3682,23 @@ fn format_agent_runtime_task_observation(task: &AgentRuntimeTaskRecord) -> Strin } fn format_agent_runtime_tool_call_observation(call: &AgentRuntimeToolCallRecord) -> String { - format!( + let mut output = format!( "{} / {} / {}", call.tool, call.status, sanitize_agent_runtime_text(&call.summary, 120) - ) + ); + if let Some(input_summary) = call + .input_summary + .as_deref() + .filter(|value| !value.trim().is_empty()) + { + output.push_str(&format!( + " / target={}", + sanitize_agent_runtime_text(input_summary, 160) + )); + } + output } fn agent_runtime_tool_input_text(input: &serde_json::Value, keys: &[&str]) -> String { @@ -4993,6 +5169,16 @@ fn render_agent_runtime_prompt_context(root: &Path, agent_id: &str) -> Result, + #[serde(default)] + input_summary: Option, + #[serde(default)] reason: Option, #[serde(default)] summary: String, 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 4d17ad882..133b710f5 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests.rs @@ -4897,7 +4897,7 @@ async fn background_agent_runtime_can_confirm_and_continue_waiting_tool_action() &root, ProjectPermissionPolicy { denied_commands: Vec::new(), - confirm_commands: vec!["file.read".to_string()], + confirm_commands: vec!["file.read".to_string(), "agent.resume".to_string()], agent_policies: BTreeMap::new(), }, ) @@ -4953,13 +4953,31 @@ async fn background_agent_runtime_can_confirm_and_continue_waiting_tool_action() .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 pending_tool_call = waiting_runtime + .recent_tool_calls + .iter() + .rev() + .find(|call| call.status == "waiting-for-confirmation") + .expect("pending tool call"); + assert_eq!( + pending_tool_call.input_summary.as_deref(), + Some("path=game/notes.txt") + ); + assert_eq!( + pending_tool_call + .action_fingerprint + .as_deref() + .expect("action fingerprint") + .len(), + 64 + ); - let confirmed = confirm_game_creator_agent_runtime_task_at( - &root, - "design-director", - "design-confirm-run", - "design-confirm-run-approved", - "允许读取项目笔记", + let confirmed = confirm_game_creator_agent_runtime_task( + root.to_string_lossy().into_owned(), + "design-director".to_string(), + "design-confirm-run".to_string(), + "design-confirm-run-approved".to_string(), + "允许读取项目笔记".to_string(), ) .expect("confirm waiting task"); assert_eq!(confirmed.state.run_id, "design-confirm-run-approved"); @@ -4995,6 +5013,126 @@ async fn background_agent_runtime_can_confirm_and_continue_waiting_tool_action() 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\"")); + assert!(agent_db.contains("\"actionFingerprint\":")); + assert!(agent_db.contains("\"inputSummary\":\"path=game/notes.txt\"")); + + fs::remove_dir_all(root).ok(); +} + +#[tokio::test] +async fn background_agent_runtime_confirmation_is_bound_to_exact_tool_input() { + 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 approved notes"); + fs::write(root.join("game/other.txt"), "未批准读取的笔记").expect("write changed 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 approved_plan = serde_json::json!({ + "thinkingSummary": "需要读已批准的项目笔记", + "plan": ["读取已批准笔记"], + "actions": [ + { + "tool": "file.read", + "reason": "读取已批准目标", + "input": { "path": "game/notes.txt" } + } + ], + "response": "" + }) + .to_string(); + let changed_plan = serde_json::json!({ + "thinkingSummary": "改为读取另一个文件", + "plan": ["读取未批准目标"], + "actions": [ + { + "tool": "file.read", + "reason": "尝试复用旧确认读取不同目标", + "input": { "path": "game/other.txt" } + } + ], + "response": "" + }) + .to_string(); + let base_url = spawn_mock_llm_server_responses_with_capture( + vec![approved_plan, changed_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-exact-confirm-run", + ) + .expect("start background task"); + + receiver + .recv_timeout(Duration::from_secs(2)) + .expect("first plan llm request"); + let first_waiting = wait_for_agent_runtime_confirmation(&root, "design-director"); + assert_eq!(first_waiting.run_id, "design-exact-confirm-run"); + + confirm_game_creator_agent_runtime_task_at( + &root, + "design-director", + "design-exact-confirm-run", + "design-exact-confirm-approved", + "只允许读取 game/notes.txt", + ) + .expect("confirm exact action"); + receiver + .recv_timeout(Duration::from_secs(2)) + .expect("changed plan llm request"); + assert!(receiver.recv_timeout(Duration::from_millis(200)).is_err()); + + let changed_waiting = wait_for_agent_runtime_confirmation(&root, "design-director"); + assert_eq!(changed_waiting.run_id, "design-exact-confirm-approved"); + assert_eq!(changed_waiting.status, "waiting-for-confirmation"); + assert!(!changed_waiting + .observations + .iter() + .any(|item| item.contains("file.read:ok"))); + let changed_tool_call = changed_waiting + .recent_tool_calls + .iter() + .rev() + .find(|call| call.status == "waiting-for-confirmation") + .expect("changed pending tool call"); + assert_eq!( + changed_tool_call.input_summary.as_deref(), + Some("path=game/other.txt") + ); + assert_ne!( + changed_tool_call.action_fingerprint, + first_waiting + .recent_tool_calls + .iter() + .rev() + .find(|call| call.status == "waiting-for-confirmation") + .and_then(|call| call.action_fingerprint.clone()) + ); + let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db"); + assert!(!agent_db.contains("未批准读取的笔记")); fs::remove_dir_all(root).ok(); } diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index 593122c0e..41253d2c6 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -270,6 +270,8 @@ interface AgentRuntimeToolPolicySnapshot { interface AgentRuntimeToolCallRecord { tool: string; status: string; + actionFingerprint?: string | null; + inputSummary?: string | null; reason: string | null; summary: string; detail: string | null; @@ -926,6 +928,7 @@ function AgentRuntimeStatusPanel({ key={`${runtime.sessionId}-tool-${toolCall.updatedAt}-${index}`} > {`${toolCall.tool} · ${toolCall.status} · ${toolCall.summary}`} + {toolCall.inputSummary ? ` · 目标:${toolCall.inputSummary}` : ''} {toolCall.reason ? ` · ${toolCall.reason}` : ''} ))} @@ -3934,6 +3937,66 @@ export function WorkspaceLauncher({ } } + async function handleAgentChatConfirmRuntimeTask(runId: string) { + const projectPathForChat = validateAgentChatProjectPath(); + const agent = selectedLauncherAgentChatAgent(); + if (!projectPathForChat || !agent || !runId || agentChatBackgroundBusy) { + return; + } + const llmWarning = getCurrentAgentChatLlmWarning(agent); + if (llmWarning) { + setAgentChatStatus(llmWarning); + return; + } + const invoke = resolveTauriInvoke(); + if (!invoke) { + setAgentChatStatus('需要在 Tauri App 内运行'); + return; + } + const saveVersion = agentChatLoadVersionRef.current + 1; + agentChatLoadVersionRef.current = saveVersion; + setAgentChatBackgroundBusy(true); + setAgentChatStatus('正在确认并继续 Agent 后台任务'); + try { + const runtime = await invoke( + 'confirm_game_creator_agent_runtime_task', + { + projectPath: projectPathForChat, + agentId: agent.id, + runId, + nextRunId: createAgentChatRunId('launcher-agent-confirm'), + note: '开发者已确认待执行工具动作', + }, + ); + if (agentChatLoadVersionRef.current !== saveVersion) { + return; + } + setAgentChatRuntime(agentRuntimeStateFromResult(runtime)); + setAgentChatRuntimeError(''); + const conversation = await invoke( + 'read_local_conversation', + { + projectPath: projectPathForChat, + agentId: agent.id, + }, + ); + if (agentChatLoadVersionRef.current !== saveVersion) { + return; + } + setAgentChatMessages(conversation.messages); + setAgentChatStatus(agentRuntimeStartStatus(runtime)); + } catch (error) { + if (agentChatLoadVersionRef.current !== saveVersion) { + return; + } + setAgentChatStatus(error instanceof Error ? error.message : String(error)); + } finally { + if (agentChatLoadVersionRef.current === saveVersion) { + setAgentChatBackgroundBusy(false); + } + } + } + const projectRows = recentWorkspaces.map((workspace) => { const directoryStatus = recentWorkspaceStatuses[workspace]; const isPendingStatus = directoryStatus === undefined; @@ -4608,6 +4671,9 @@ export function WorkspaceLauncher({ onRetryRuntimeTask={(runId) => void handleAgentChatRetryRuntimeTask(runId) } + onConfirmRuntimeTask={(runId) => + void handleAgentChatConfirmRuntimeTask(runId) + } />
diff --git a/apps/ai-game-creator-shell/tests/appSurface.test.ts b/apps/ai-game-creator-shell/tests/appSurface.test.ts index 434123083..0595badf9 100644 --- a/apps/ai-game-creator-shell/tests/appSurface.test.ts +++ b/apps/ai-game-creator-shell/tests/appSurface.test.ts @@ -1436,6 +1436,8 @@ describe('AI 游戏创作 App 界面边界', () => { { tool: 'file.read', status: 'ok', + actionFingerprint: 'a'.repeat(64), + inputSummary: 'path=game/notes.txt', reason: '读取笔记', summary: '已读取 game/notes.txt', detail: 'game/notes.txt: 连续上下文笔记', @@ -1690,7 +1692,9 @@ describe('AI 游戏创作 App 界面边界', () => { expect(screen.getByText('file.read:ok · 已读取 game/notes.txt')).not.toBeNull(); expect(screen.getByText('最近动作')).not.toBeNull(); expect( - screen.getByText('file.read · ok · 已读取 game/notes.txt · 读取笔记'), + screen.getByText( + 'file.read · ok · 已读取 game/notes.txt · 目标:path=game/notes.txt · 读取笔记', + ), ).not.toBeNull(); expect(screen.getByText('最近事件')).not.toBeNull(); expect( @@ -1780,6 +1784,188 @@ describe('AI 游戏创作 App 界面边界', () => { ).not.toBeNull(); }); + it('confirms the exact pending tool action from the developer agent window', async () => { + const waitingRuntimeState = { + schemaVersion: 'game-creator-agent-runtime.v1', + agentId: 'design-director', + taskId: 'design-director', + sessionId: 'agent-session-design-director', + runId: 'launcher-agent-waiting', + source: 'agent-background-task', + status: 'waiting-for-confirmation', + phase: 'waiting-for-confirmation', + currentTask: '读取角色规范笔记', + currentGoal: '确认角色规范依据', + currentAction: '等待确认工具 file.read', + waitingOn: '开发者确认 Agent 工具动作', + nextStep: '确认或调整策略后继续 Agent 工具动作:file.read', + loopIteration: 1, + maxLoopIterations: 3, + toolActionBudget: 3, + plan: ['读取项目笔记'], + observations: ['file.read:waiting-for-confirmation'], + recentToolCalls: [ + { + tool: 'file.read', + status: 'waiting-for-confirmation', + actionFingerprint: 'b'.repeat(64), + inputSummary: 'path=game/notes.txt', + reason: '读取角色规范依据', + summary: '项目权限策略要求用户确认:file.read', + detail: null, + updatedAt: 5000, + }, + ], + taskQueue: { + total: 1, + pending: 0, + running: 0, + waitingForConfirmation: 1, + cancelled: 0, + completed: 0, + failed: 0, + latestRunId: 'launcher-agent-waiting', + updatedAt: 5000, + }, + allowedTools: ['file.read'], + lastResponse: null, + error: null, + updatedAt: 5000, + }; + const waitingTask = { + schemaVersion: 'game-creator-agent-runtime.v1', + agentId: 'design-director', + taskId: 'design-director', + sessionId: 'agent-session-design-director', + runId: 'launcher-agent-waiting', + source: 'agent-background-task', + task: '读取角色规范笔记', + status: 'waiting-for-confirmation', + phase: 'waiting-for-confirmation', + currentAction: '等待确认工具 file.read', + error: null, + updatedAt: 5000, + }; + const runtimeResult = { + state: waitingRuntimeState, + sessionPath: + '/tmp/authorized-game/.agent/runtime/agents/design-director.json', + eventPath: + '/tmp/authorized-game/.agent/runtime/events/design-director.jsonl', + taskPath: + '/tmp/authorized-game/.agent/runtime/tasks/design-director.jsonl', + taskQueue: waitingRuntimeState.taskQueue, + recentEvents: [], + recentTasks: [waitingTask], + }; + const invoke = vi.fn( + async (command: string, args?: Record) => { + if (command === 'check_game_creator_llm_config') { + return { + configured: true, + apiKeyPresent: true, + baseUrl: 'https://llm.example.test/v1', + model: 'gpt-5.5', + apiKind: 'openai_chat', + stream: true, + error: null, + agents: [ + { + agentId: 'design-director', + label: '拆解创作方向', + configured: true, + apiKeyPresent: true, + baseUrl: 'https://llm.example.test/v1', + model: 'gpt-5.5', + apiKind: 'openai_chat', + stream: true, + error: null, + }, + ], + }; + } + if (command === 'read_local_conversation') { + return { + path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl', + agentId: args?.agentId, + messages: [], + }; + } + if (command === 'read_game_creator_agent_runtime') { + return runtimeResult; + } + if (command === 'confirm_game_creator_agent_runtime_task') { + const runId = String(args?.nextRunId ?? 'launcher-agent-confirmed'); + const taskQueue = { + ...waitingRuntimeState.taskQueue, + running: 1, + waitingForConfirmation: 0, + latestRunId: runId, + }; + return { + ...runtimeResult, + state: { + ...waitingRuntimeState, + runId, + status: 'running', + phase: 'planning', + currentAction: '生成 Agent 工具计划(第 1 轮)', + waitingOn: 'Agent 输出计划或回复', + taskQueue, + }, + taskQueue, + recentTasks: [ + { + ...waitingTask, + runId, + status: 'running', + phase: 'planning', + }, + ], + }; + } + throw new Error(`unexpected invoke ${command}`); + }, + ); + window.__TAURI__ = { core: { invoke } }; + renderLauncherAgentChatAt('/?agent-chat'); + + fireEvent.change(screen.getByLabelText('Agent 聊天项目目录'), { + target: { value: '/tmp/authorized-game' }, + }); + fireEvent.click(screen.getByRole('button', { name: '读取历史' })); + + expect( + await screen.findByText( + 'waiting-for-confirmation / waiting-for-confirmation', + ), + ).not.toBeNull(); + expect( + screen.getByText( + 'file.read · waiting-for-confirmation · 项目权限策略要求用户确认:file.read · 目标:path=game/notes.txt · 读取角色规范依据', + ), + ).not.toBeNull(); + const runtimeActions = screen.getByLabelText('Agent Runtime 操作'); + const confirmButton = within(runtimeActions).getByRole('button', { + name: '确认继续', + }) as HTMLButtonElement; + expect(confirmButton.disabled).toBe(false); + fireEvent.click(confirmButton); + + await waitFor(() => { + expect(invoke).toHaveBeenCalledWith( + 'confirm_game_creator_agent_runtime_task', + expect.objectContaining({ + projectPath: '/tmp/authorized-game', + agentId: 'design-director', + runId: 'launcher-agent-waiting', + note: '开发者已确认待执行工具动作', + }), + ); + }); + expect(await screen.findByText('running / planning')).not.toBeNull(); + }); + it('shows queued developer agent background tasks when the agent is already running', async () => { const runningRuntimeState = { schemaVersion: 'game-creator-agent-runtime.v1', @@ -18566,7 +18752,7 @@ describe('AI 游戏创作 App 界面边界', () => { expect( await screen.findByText( - '当前仅支持确认 project.index、project.status、project.checkpoint、project.diff、project.restore、project.export_package、project.export_list、file.list、file.read、memory.read、memory.write、memory.delete、asset.register、asset.list、task.list、task.update、agent.run_status、agent.kill、agent.retry、agent.resume、agent.delegate、agent.audit、agent.trace_read、preview.status、preview.start、preview.open、preview.stop、canvas.project_sync、canvas.asset_import、canvas.asset_generate、canvas.export_import、conversation.read 和 conversation.write。', + '当前仅支持确认 project.index、project.status、project.checkpoint、project.diff、project.restore、project.export_package、project.export_list、file.list、file.read、memory.read、memory.write、memory.delete、asset.register、asset.list、task.list、task.create、task.update、agent.run_status、agent.kill、agent.retry、agent.resume、agent.delegate、agent.schedule_ready、agent.audit、agent.trace_read、preview.status、preview.start、preview.open、preview.stop、canvas.project_sync、canvas.asset_import、canvas.asset_generate、canvas.export_import、conversation.read 和 conversation.write。', ), ).not.toBeNull(); expect(screen.queryByText('project.policy_write')).toBeNull(); diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 75c2f8b03..1dd9adbfb 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -4070,10 +4070,10 @@ - 2026-07-10 调整:Agent Runtime 后台任务的 `runId` 是同一 Agent 任务历史的身份,不允许复用覆盖。`start_game_creator_agent_runtime_task`、`agent.delegate` 和 retry 进入后台队列前会读取该 Agent 全量 task JSONL 历史;若调用方传入的规范化 runId 已存在,Runtime 自动追加 `-dup--` 生成实际 runId。任务队列、delegate observation 和 `agent.db` 审计都必须使用实际 runId,避免 `latest_game_creator_agent_runtime_tasks` 按 runId 去重时折叠掉不同任务。 - 2026-07-10 调整:Agent Runtime 的 `memory.write scope=agent` 只能写当前 Agent 自己的私有记忆。若 action 指定其他 `agentId / targetAgentId`,Runtime 返回 `blocked` observation,不写目标 Agent 私有记忆、不写 `agent.runtime.memory.write` 审计;跨 Agent 共享稳定结论必须走 `blackboard.write`,给单个 Agent 留上下文必须走 `agent.message`。 - 2026-07-10 调整:Agent Runtime 和本地对话使用 append-only JSONL 作为事实源时,进程内必须按目标文件路径串行追加整行。`.agent/agent.db`、`.agent/conversations/**/*.jsonl`、`.agent/runtime/events/*.jsonl`、`.agent/runtime/tasks/*.jsonl`、`.agent/activity.jsonl` 和 `.agent/output.jsonl` 统一走共享追加 helper,避免多个后台 Agent 并行完成时 JSON record 与换行交错。 -- 2026-07-10 调整:Agent Runtime 待确认工具动作支持确认后继续。开发者确认 `waiting-for-confirmation` run 时,Runtime 为新 run 写入一次性 `.agent/runtime/confirmations///.json` 票据并重新入队;工具权限 gate 在 deny 之后、confirm 阶段消费该票据,只放行对应 Agent/run/command 一次。原 waiting run 追加 `completed/confirmed` 任务记录,避免队列长期显示等待确认;审计记录写 `agent.runtime.tool_confirmation.approved`。 +- 2026-07-10 调整:Agent Runtime 待确认工具动作支持确认后继续。开发者确认 `waiting-for-confirmation` run 时,Runtime 为新 run 写入一次性 `.agent/runtime/confirmations///.json` 票据并重新入队;票据同时保存工具名与输入 JSON 的 SHA-256 `actionFingerprint`,权限 gate 在 deny 之后、confirm 阶段只放行对应 Agent/run/command/fingerprint 一次,模型若把同一工具改成其他路径、checkpoint 或目标 Agent,旧票据立即失效并重新进入待确认。`recentToolCalls` 和确认审计只额外保存安全 `inputSummary`,例如相对路径、checkpoint id、目标 Agent 或内容字符数,不保存待写正文、消息正文、素材 prompt 或 API Key。开发窗口必须把 `onConfirmRuntimeTask` 接入真实“确认继续”按钮;该显式确认命令允许 `agent.resume` 处于 confirm,只继续服从 deny,自动重启恢复仍要求 `agent.resume` 为 auto。原 waiting run 追加 `completed/confirmed` 任务记录,避免队列长期显示等待确认;审计记录写 `agent.runtime.tool_confirmation.approved`。 - 2026-07-10 调整:Agent Runtime 工具箱新增 `task.create`,用于让 Agent 把目标拆成新的 manifest 任务,而不只能更新 seed task。该工具默认 `confirm` 权限,写入前要求 taskId 唯一、依赖指向已有任务、列表长度受限,并写 `agent.runtime.task.create` 审计;策略要求确认或拒绝时不修改 `.agent/manifest.json`。 - 2026-07-10 调整:Agent Runtime 新增 `agent.schedule_ready` 调度入口,默认 `confirm` 权限。命令会扫描 `.agent/manifest.json` 中依赖已完成且仍为 `pending` 的 ready task,先把任务标成 `running`,再用 taskId 作为 Agent id 投递到既有后台队列,source 记为 `agent-ready-task-scheduler`,并写 `agent.runtime.ready_task.scheduled` 审计;后续执行仍走原 per-agent 锁、任务 JSONL、LLM loop、工具策略和事件流,不新增独立 worker。默认确认策略下该命令不会静默调度。 -- 2026-07-10 调整:Agent Runtime state 新增 `recentToolCalls`,后台 loop 每次执行白名单工具后记录最近 20 条结构化动作,包含 tool、status、reason、summary、detail 和 updatedAt。状态面板展示最近动作时使用该字段,不解析 observation 文本;写入前继续过滤敏感上下文,不保存原始密钥或任意未过滤输入。 +- 2026-07-10 调整:Agent Runtime state 新增 `recentToolCalls`,后台 loop 每次执行白名单工具后记录最近 20 条结构化动作,包含 tool、status、actionFingerprint、inputSummary、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。 - 2026-07-10 调整:Agent Runtime state 新增 `planSteps / activePlanStepIndex`。Runtime 从 Agent 输出的 `plan` 派生结构化计划步骤,并在 action / observation / response / error 生命周期中更新 `pending / active / completed / failed` 和 detail;开发窗口 Runtime 面板、主窗口 Agent 状态列表、`agent.run_status` observation 和下一轮 planning prompt 都展示步骤进度,不再只依赖不可定位的 plan 字符串。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 2c600e56a..a5dfd66ec 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -40,9 +40,9 @@ Agent Runtime 负责: - 2026-07-10 补充:后台 Agent 任务的 `runId` 在同一 Agent 内必须唯一,因为任务快照按 runId 去重表示同一 run 的最新状态。`start_game_creator_agent_runtime_task`、`agent.delegate` 和 retry 入队前会读取该 Agent 全量 task JSONL 历史;如果调用方传入的 runId 已存在,Runtime 自动追加 `-dup--` 后缀生成实际 runId,并在任务队列、delegate observation 和 `agent.db` 审计中使用该实际值,避免两个独立任务互相折叠。 - 2026-07-10 补充:后台 Agent 任务的 `memory.write scope=agent` 只允许写当前 Agent 自己的私有记忆。若 action 指定其他 `agentId / targetAgentId`,Runtime 返回 `blocked` observation,不写目标 Agent 私有记忆,也不写 `agent.runtime.memory.write` 审计;跨 Agent 共享稳定结论必须使用 `blackboard.write`,给单个 Agent 留上下文必须使用 `agent.message`。 - 2026-07-10 补充:本地 append-only JSONL 追加写入按目标文件路径做进程内串行化。`.agent/agent.db`、项目 / Agent 对话、Runtime events、Runtime tasks、Agent activity 和 output 都通过共享 helper 写入完整 JSON 行,防止多个后台 Agent 并行运行时 record 内容与换行交错;该约束服务于当前单客户端进程内并行,不把跨进程同项目写入作为 v1 支持目标。 -- 2026-07-10 补充:后台 Agent 待确认工具动作支持由开发者确认后继续。确认命令会为新 run 写入一次性工具确认票据,权限 gate 在 deny 规则之后、confirm 规则处按 `agentId/runId/commandId` 消费该票据并只放行一次;原 waiting run 会追加 `completed/confirmed` 任务记录,新 run 继续走正常后台队列、LLM 规划和工具 observation 闭环。 +- 2026-07-10 补充:后台 Agent 待确认工具动作支持由开发者确认后继续。确认命令会为新 run 写入一次性工具确认票据,票据绑定 `agentId/runId/commandId/actionFingerprint`;`actionFingerprint` 是工具名和完整输入 JSON 的 SHA-256,权限 gate 在 deny 规则之后只放行完全一致的动作一次,同一工具若改了路径、checkpoint 或目标 Agent 会作废旧票据并重新等待确认。开发窗口的 Runtime 面板接入真实“确认继续”回调;显式确认命令允许 `agent.resume` 保持默认 confirm,只服从 deny,自动重启恢复仍要求 `agent.resume` 为 auto。原 waiting run 会追加 `completed/confirmed` 任务记录,新 run 继续走正常后台队列、LLM 规划和工具 observation 闭环。 - 2026-07-10 补充:后台任务工具箱已加入 `task.create`。Agent 可在 loop 中把拆解出的后续工作追加为 manifest 任务;Runtime 复用 `task.create` 策略和项目写锁,写入前校验 taskId 唯一、依赖指向已有任务、任务分组合法、标题 / 角色非空以及列表长度,并写入 `agent.runtime.task.create` 审计记录。策略要求确认或拒绝时不会修改 manifest。 -- 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 新增 `recentToolCalls`,每次后台工具执行后记录最近 20 条结构化工具动作,包含 tool、status、actionFingerprint、inputSummary、reason、summary、detail 和 updatedAt;开发窗口、项目内 Agent 对话弹窗和主窗口 Agent 状态列表可直接展示“最近动作”和安全目标摘要,不再只能从 observation 字符串里猜测 action / observation 对应关系。`inputSummary` 只保留相对路径、checkpoint id、目标 Agent、内容字符数等确认所需信息,不保存原始 API Key、待写正文、消息正文、素材 prompt 或任意未过滤输入。 - 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 的执行上限或工具权限。 - 2026-07-10 补充:Agent Runtime state 新增 `planSteps / activePlanStepIndex`,从 Agent 输出的 `plan` 派生结构化计划步骤,并在工具 action / observation / response / error 生命周期中更新 `pending / active / completed / failed` 和 detail;开发窗口 Runtime 面板、主窗口 Agent 状态列表、`agent.run_status` observation 和下一轮 planning prompt 都展示当前计划步骤与步骤进度,避免只能展示一串不可定位的 plan 文本。 @@ -285,7 +285,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 状态查看进度和结果,也可对当前 run 执行取消 / 重试,待确认 run 还可执行“确认继续”。后台任务会向 `.agent/runtime/tasks/.jsonl` 追加任务视角记录,任务状态使用 `pending / running / waiting-for-confirmation / cancelled / completed / failed`,读取时按 `runId` 去重返回最近任务;`runId` 在同一 Agent 内是单个 run 的身份,后台入队会自动把重复 runId 改写为唯一实际 runId,防止不同任务互相覆盖;runtime state 自身仍可在完成后显示 `idle / completed`,二者语义分开。同一 Agent 的 pending 任务由持有 `.agent/runtime/locks/.lock` 的后台 drain 串行执行,避免同一 Agent 并发抢上下文;不同 Agent 仍可并行;若某个工具动作命中确认策略,该 Agent 会停在 `waiting-for-confirmation` 并暂停继续消费队列,等待后续确认或策略调整;确认继续会为新 run 写入一次性 `.agent/runtime/confirmations///.json` 票据,权限 gate 在 deny 之后、confirm 阶段消费票据并只放行对应工具一次,原 run 追加 `completed/confirmed` 任务记录;若用户取消 pending 任务,drain 不再消费该 run,若取消 running 任务,则在当前 LLM 或工具调用返回后的检查点停止,不继续执行工具或保存最终 assistant 回复。客户端重开项目时会对当前项目路径自动尝试一次 Runtime 恢复;恢复命令必须通过 `agent.resume` 自动权限,默认确认策略下不会静默启动;同一 Agent 同时存在上一进程遗留 `running` 和 `pending` 时,先重接 `running`,再由 drain 继续 `pending`。开发构建和后台 Agent 工具箱都可通过 `agent.schedule_ready` 权限确认入口把 manifest ready task 投递进同一后台队列,命令会先把 ready task 标成 `running`,再用 taskId 作为 Agent id 入队,source 为 `agent-ready-task-scheduler`;该入口不新增独立 worker。后台任务的核心 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.ready_task.scheduled` / `agent.runtime.tool_observation` / `agent.runtime.tool_confirmation_required` / `agent.runtime.tool_confirmation.approved` / `agent.runtime.memory.write` / `agent.runtime.file.write` / `agent.runtime.task.create` / `agent.runtime.task.update` / `agent.runtime.command.run_limited` / `agent.runtime.blackboard.write` / `agent.runtime.agent.message` / `agent.runtime.agent.delegate` / `agent.runtime.background_task.cancelled` / `agent.runtime.background_task.retry` / `agent.runtime.background_task.completed` / `agent.runtime.background_task.failed` 审计记录。当前工具箱开放只读工具 `memory.read`、`conversation.read`、`asset.list`、`project.index`、`project.diff`、`file.list`、`file.read`、`task.list`、`agent.run_status`,以及受策略保护的写/运行工具 `memory.write`、`project.checkpoint`、`project.restore`、`file.write`、`task.create`、`task.update`、`command.run_limited`、`blackboard.write`、`agent.message`、`agent.delegate` 和 `agent.schedule_ready`;`memory.write scope=agent` 只允许写当前 Agent 自己的私有记忆,跨 Agent 共享必须改用 `blackboard.write` 或 `agent.message`;`project.checkpoint` 只创建本地 checkpoint,不返回本机绝对路径;`project.restore` 只按 checkpoint id 恢复当前项目,不返回本机绝对路径,默认确认策略下不会静默回滚;`task.create` 只追加新 manifest 任务,`task.update` 只更新已有任务状态;`agent.schedule_ready` 只调度 manifest ready task,不创建平行 runtime;若项目策略拒绝,对应工具不会执行,Runtime 会把策略结果作为 observation 回给 Agent 修正计划;若项目策略要求确认且没有一次性确认票据,Runtime 会保留待确认状态而不执行该工具。`toolPolicy` 保存当前工具级权限快照,供 planning prompt 和状态面板展示;`recentToolCalls` 保存最近 20 条结构化工具动作,供状态面板展示最近动作;append-only JSONL 写入按目标文件路径在当前进程内串行追加完整行,覆盖 `.agent/agent.db`、对话、Runtime events/tasks、activity 和 output,减少多个 Agent 同时完成时的行交错风险。 +- 开发窗口和项目内 Agent 对话弹窗的“后台运行”只启动或排队单 Agent 后台任务,不阻塞等待回复;用户可刷新同一 Agent 对话或 runtime 状态查看进度和结果,也可对当前 run 执行取消 / 重试,待确认 run 还可执行“确认继续”。后台任务会向 `.agent/runtime/tasks/.jsonl` 追加任务视角记录,任务状态使用 `pending / running / waiting-for-confirmation / cancelled / completed / failed`,读取时按 `runId` 去重返回最近任务;`runId` 在同一 Agent 内是单个 run 的身份,后台入队会自动把重复 runId 改写为唯一实际 runId,防止不同任务互相覆盖;runtime state 自身仍可在完成后显示 `idle / completed`,二者语义分开。同一 Agent 的 pending 任务由持有 `.agent/runtime/locks/.lock` 的后台 drain 串行执行,避免同一 Agent 并发抢上下文;不同 Agent 仍可并行;若某个工具动作命中确认策略,该 Agent 会停在 `waiting-for-confirmation` 并暂停继续消费队列,等待后续确认或策略调整;确认继续会为新 run 写入一次性 `.agent/runtime/confirmations///.json` 票据,票据绑定工具名与输入 JSON 的 SHA-256 `actionFingerprint`,权限 gate 在 deny 之后只放行同一 `agentId/runId/commandId/actionFingerprint` 一次,输入改变时旧票据失效并重新等待确认,原 run 追加 `completed/confirmed` 任务记录;若用户取消 pending 任务,drain 不再消费该 run,若取消 running 任务,则在当前 LLM 或工具调用返回后的检查点停止,不继续执行工具或保存最终 assistant 回复。客户端重开项目时会对当前项目路径自动尝试一次 Runtime 恢复;恢复命令必须通过 `agent.resume` 自动权限,默认确认策略下不会静默启动;同一 Agent 同时存在上一进程遗留 `running` 和 `pending` 时,先重接 `running`,再由 drain 继续 `pending`。开发构建和后台 Agent 工具箱都可通过 `agent.schedule_ready` 权限确认入口把 manifest ready task 投递进同一后台队列,命令会先把 ready task 标成 `running`,再用 taskId 作为 Agent id 入队,source 为 `agent-ready-task-scheduler`;该入口不新增独立 worker。后台任务的核心 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.ready_task.scheduled` / `agent.runtime.tool_observation` / `agent.runtime.tool_confirmation_required` / `agent.runtime.tool_confirmation.approved` / `agent.runtime.memory.write` / `agent.runtime.file.write` / `agent.runtime.task.create` / `agent.runtime.task.update` / `agent.runtime.command.run_limited` / `agent.runtime.blackboard.write` / `agent.runtime.agent.message` / `agent.runtime.agent.delegate` / `agent.runtime.background_task.cancelled` / `agent.runtime.background_task.retry` / `agent.runtime.background_task.completed` / `agent.runtime.background_task.failed` 审计记录。当前工具箱开放只读工具 `memory.read`、`conversation.read`、`asset.list`、`project.index`、`project.diff`、`file.list`、`file.read`、`task.list`、`agent.run_status`,以及受策略保护的写/运行工具 `memory.write`、`project.checkpoint`、`project.restore`、`file.write`、`task.create`、`task.update`、`command.run_limited`、`blackboard.write`、`agent.message`、`agent.delegate` 和 `agent.schedule_ready`;`memory.write scope=agent` 只允许写当前 Agent 自己的私有记忆,跨 Agent 共享必须改用 `blackboard.write` 或 `agent.message`;`project.checkpoint` 只创建本地 checkpoint,不返回本机绝对路径;`project.restore` 只按 checkpoint id 恢复当前项目,不返回本机绝对路径,默认确认策略下不会静默回滚;`task.create` 只追加新 manifest 任务,`task.update` 只更新已有任务状态;`agent.schedule_ready` 只调度 manifest ready task,不创建平行 runtime;若项目策略拒绝,对应工具不会执行,Runtime 会把策略结果作为 observation 回给 Agent 修正计划;若项目策略要求确认且没有匹配精确动作的一次性确认票据,Runtime 会保留待确认状态而不执行该工具。`toolPolicy` 保存当前工具级权限快照,供 planning prompt 和状态面板展示;`recentToolCalls` 保存最近 20 条结构化工具动作及安全 `inputSummary`,供状态面板展示最近动作和确认目标;append-only JSONL 写入按目标文件路径在当前进程内串行追加完整行,覆盖 `.agent/agent.db`、对话、Runtime events/tasks、activity 和 output,减少多个 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` 项目权限策略保护。