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 cbd668cfe..586088fe4 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent.rs @@ -1627,7 +1627,7 @@ fn build_game_creator_agent_background_tool_plan_request( let tool_policy_json = serde_json::to_string_pretty(&tool_policy) .map_err(|error| format!("序列化 Agent 工具策略失败:{error}"))?; let prompt = format!( - "当前工具策略:\n{tool_policy_json}\n\n项目上下文如下。你正在执行后台 Agent loop 第 {loop_index} 轮。请基于目标、已有工具观察和当前上下文修正计划,再决定是否调用最多 {AGENT_RUNTIME_BACKGROUND_TOOL_ACTION_LIMIT} 个白名单工具。只输出 JSON 对象,不要 markdown。\n\n{context}\n\n后台任务:\n{task}\n\n已有工具观察:\n{observations_json}\n\nJSON schema:{{\"thinkingSummary\":\"一句话理解\",\"plan\":[\"步骤\"],\"actions\":[{{\"tool\":\"memory.read|memory.write|conversation.read|asset.list|project.index|project.diff|file.list|file.read|file.write|task.list|task.create|task.update|command.run_limited|preview.start|canvas.asset_generate|blackboard.write|agent.message|agent.delegate|agent.run_status\",\"reason\":\"为什么需要\",\"input\":{{}}}}],\"response\":\"如果无需继续调用工具,可直接给最终回复\"}}\n\n工具输入约定:memory.read 使用 {{\"scope\":\"session|project|blackboard|agent\"}};memory.write 使用 {{\"scope\":\"agent|project|session|blackboard\",\"title\":\"标题\",\"content\":\"要沉淀的稳定结论\",\"mode\":\"append|overwrite\"}},其中 agent scope 只能写当前 Agent 自己的私有记忆,跨 Agent 共享请用 blackboard.write 或 agent.message;project.diff 使用 {{\"checkpointId\":\"checkpoint id\"}},用于读取当前项目相对路径 diff 摘要;file.list 使用 {{\"path\":\"可选项目内相对目录或文件\"}},path 为空时列出项目摘要;file.read 使用 {{\"path\":\"项目内相对路径\"}};file.write 使用 {{\"path\":\"项目内相对路径\",\"content\":\"文件内容\"}};task.list input 可为空,用于读取 manifest 任务图、状态和 readyTaskIds;task.create 使用 {{\"taskId\":\"可选自定义 taskId\",\"title\":\"任务标题\",\"group\":\"design|art|code|balance|audio|publishing\",\"role\":\"角色名\",\"dependencies\":[\"已有 taskId\"],\"artifacts\":[\"预期产物\"],\"acceptanceCriteria\":[\"验收标准\"],\"status\":\"pending|running|waiting-for-confirmation|completed|failed\"}},用于把 Agent 拆出的新任务追加到 manifest;task.update 使用 {{\"taskId\":\"manifest taskId\",\"status\":\"pending|running|waiting-for-confirmation|completed|failed\"}};command.run_limited 使用 {{\"commandId\":\"game.static_smoke\"}},只支持本地静态自检;preview.start input 可为空,用于启动当前项目的 127.0.0.1 本地 HTTP 预览;canvas.asset_generate 使用 {{\"prompt\":\"要生成的美术素材描述\"}},通过配置的 External Editor API 生成首版素材并登记到 assets;blackboard.write 使用 {{\"title\":\"标题\",\"content\":\"要共享给所有 Agent 的稳定结论\"}};agent.message 使用 {{\"agentId\":\"目标 taskId\",\"content\":\"给目标 Agent 的定向消息\"}};agent.delegate 使用 {{\"agentId\":\"目标 taskId\",\"task\":\"要委派的后台任务\",\"runId\":\"可选 run id\"}},用于把任务投递到另一个 Agent 的独立队列;agent.run_status 使用 {{\"agentId\":\"可选目标 taskId\",\"scope\":\"self|all\"}},用于读取自己或其他 Agent 的 Runtime 状态摘要;如果已有观察足够,请返回空 actions 并填写 response。其他工具 input 可为空。" + "当前工具策略:\n{tool_policy_json}\n\n项目上下文如下。你正在执行后台 Agent loop 第 {loop_index} 轮。请基于目标、已有工具观察和当前上下文修正计划,再决定是否调用最多 {AGENT_RUNTIME_BACKGROUND_TOOL_ACTION_LIMIT} 个白名单工具。只输出 JSON 对象,不要 markdown。\n\n{context}\n\n后台任务:\n{task}\n\n已有工具观察:\n{observations_json}\n\nJSON schema:{{\"thinkingSummary\":\"一句话理解\",\"plan\":[\"步骤\"],\"actions\":[{{\"tool\":\"memory.read|memory.write|conversation.read|asset.list|project.index|project.diff|file.list|file.read|file.write|task.list|task.create|task.update|command.run_limited|preview.start|canvas.asset_generate|blackboard.write|agent.message|agent.delegate|agent.schedule_ready|agent.run_status\",\"reason\":\"为什么需要\",\"input\":{{}}}}],\"response\":\"如果无需继续调用工具,可直接给最终回复\"}}\n\n工具输入约定:memory.read 使用 {{\"scope\":\"session|project|blackboard|agent\"}};memory.write 使用 {{\"scope\":\"agent|project|session|blackboard\",\"title\":\"标题\",\"content\":\"要沉淀的稳定结论\",\"mode\":\"append|overwrite\"}},其中 agent scope 只能写当前 Agent 自己的私有记忆,跨 Agent 共享请用 blackboard.write 或 agent.message;project.diff 使用 {{\"checkpointId\":\"checkpoint id\"}},用于读取当前项目相对路径 diff 摘要;file.list 使用 {{\"path\":\"可选项目内相对目录或文件\"}},path 为空时列出项目摘要;file.read 使用 {{\"path\":\"项目内相对路径\"}};file.write 使用 {{\"path\":\"项目内相对路径\",\"content\":\"文件内容\"}};task.list input 可为空,用于读取 manifest 任务图、状态和 readyTaskIds;task.create 使用 {{\"taskId\":\"可选自定义 taskId\",\"title\":\"任务标题\",\"group\":\"design|art|code|balance|audio|publishing\",\"role\":\"角色名\",\"dependencies\":[\"已有 taskId\"],\"artifacts\":[\"预期产物\"],\"acceptanceCriteria\":[\"验收标准\"],\"status\":\"pending|running|waiting-for-confirmation|completed|failed\"}},用于把 Agent 拆出的新任务追加到 manifest;task.update 使用 {{\"taskId\":\"manifest taskId\",\"status\":\"pending|running|waiting-for-confirmation|completed|failed\"}};command.run_limited 使用 {{\"commandId\":\"game.static_smoke\"}},只支持本地静态自检;preview.start input 可为空,用于启动当前项目的 127.0.0.1 本地 HTTP 预览;canvas.asset_generate 使用 {{\"prompt\":\"要生成的美术素材描述\"}},通过配置的 External Editor API 生成首版素材并登记到 assets;blackboard.write 使用 {{\"title\":\"标题\",\"content\":\"要共享给所有 Agent 的稳定结论\"}};agent.message 使用 {{\"agentId\":\"目标 taskId\",\"content\":\"给目标 Agent 的定向消息\"}};agent.delegate 使用 {{\"agentId\":\"目标 taskId\",\"task\":\"要委派的后台任务\",\"runId\":\"可选 run id\"}},用于把任务投递到另一个 Agent 的独立队列;agent.schedule_ready input 可为空或 {{\"limit\":1}},用于把 manifest 中依赖已完成的 ready task 投递到对应 Agent 后台队列;agent.run_status 使用 {{\"agentId\":\"可选目标 taskId\",\"scope\":\"self|all\"}},用于读取自己或其他 Agent 的 Runtime 状态摘要;如果已有观察足够,请返回空 actions 并填写 response。其他工具 input 可为空。" ); let request = LlmRunRequest::new(vec![ LlmMessage::system(game_creator_agent_runtime_tool_plan_system_prompt()), @@ -1751,6 +1751,7 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action( "blackboard.write" => observe_agent_runtime_blackboard_write(root, agent_id, &action.input), "agent.message" => observe_agent_runtime_agent_message(root, agent_id, &action.input), "agent.delegate" => observe_agent_runtime_agent_delegate(root, agent_id, &action.input), + "agent.schedule_ready" => observe_agent_runtime_schedule_ready_tasks(root, &action.input), "agent.run_status" => observe_agent_runtime_run_status(root, agent_id, &action.input), _ => AgentRuntimeToolObservation { tool: tool.to_string(), @@ -1781,6 +1782,7 @@ fn game_creator_agent_runtime_tool_command_id(tool: &str) -> Option<&'static str "blackboard.write" => Some("memory.write"), "agent.message" => Some("conversation.write"), "agent.delegate" => Some("agent.delegate"), + "agent.schedule_ready" => Some("agent.schedule_ready"), "agent.run_status" => Some("agent.run_status"), _ => None, } @@ -1901,6 +1903,7 @@ fn agent_runtime_executable_tools() -> Vec<&'static str> { "blackboard.write", "agent.message", "agent.delegate", + "agent.schedule_ready", "agent.run_status", ] } @@ -3199,6 +3202,51 @@ fn observe_agent_runtime_agent_delegate( } } +fn observe_agent_runtime_schedule_ready_tasks( + root: &Path, + input: &serde_json::Value, +) -> AgentRuntimeToolObservation { + let limit = agent_runtime_tool_input_usize(input, &["limit", "maxTasks"]) + .map(|value| value.clamp(1, 16)) + .unwrap_or(16); + match schedule_game_creator_agent_ready_tasks_at(root, limit) { + Ok(results) => { + let detail = results + .iter() + .map(|result| { + format!( + "{} · {} / {} · run {}", + result.state.agent_id, + result.state.status, + result.state.phase, + result.state.run_id + ) + }) + .collect::>(); + let detail = if detail.is_empty() { + "没有 ready task 被调度".to_string() + } else { + detail.join("\n") + }; + AgentRuntimeToolObservation { + tool: "agent.schedule_ready".to_string(), + status: "ok".to_string(), + summary: format!("已调度 {} 个 Ready 任务", results.len()), + detail: Some(truncate_agent_runtime_text( + sanitize_prompt_context(&detail).as_str(), + AGENT_RUNTIME_TOOL_OBSERVATION_MAX_CHARS, + )), + } + } + Err(error) => AgentRuntimeToolObservation { + tool: "agent.schedule_ready".to_string(), + status: "failed".to_string(), + summary: redact_agent_runtime_project_paths(root, &error, 240), + detail: None, + }, + } +} + fn observe_agent_runtime_run_status( root: &Path, agent_id: &str, @@ -3405,6 +3453,23 @@ fn agent_runtime_tool_input_text(input: &serde_json::Value, keys: &[&str]) -> St String::new() } +fn agent_runtime_tool_input_usize(input: &serde_json::Value, keys: &[&str]) -> Option { + for key in keys { + let Some(value) = input.get(*key) else { + continue; + }; + if let Some(number) = value.as_u64() { + return usize::try_from(number).ok(); + } + if let Some(text) = value.as_str() { + if let Ok(number) = text.trim().parse::() { + return Some(number); + } + } + } + None +} + fn agent_runtime_tool_input_string_list(input: &serde_json::Value, keys: &[&str]) -> Vec { for key in keys { let Some(value) = input.get(*key) else { @@ -3789,12 +3854,14 @@ fn default_game_creator_agent_runtime_allowed_tools() -> Vec { "blackboard.write", "agent.message", "agent.delegate", + "agent.schedule_ready", "asset.list", "project.diff", "file.list", "file.read", "file.write", "task.list", + "task.create", "task.update", "command.run_limited", "preview.start", @@ -5021,7 +5088,7 @@ pub(crate) fn game_creator_role_agent_chat_system_prompt() -> &'static str { } pub(crate) fn game_creator_agent_runtime_tool_plan_system_prompt() -> &'static str { - "你是 Genarrative AI 游戏创作多智能体 Runtime 中的专业 Agent。你必须在白名单工具内规划行动:先给一句 thinkingSummary,再给短计划,再决定是否请求工具。只能请求 memory.read、memory.write、conversation.read、asset.list、project.index、project.diff、file.list、file.read、file.write、task.list、task.create、task.update、command.run_limited、preview.start、canvas.asset_generate、blackboard.write、agent.message、agent.delegate、agent.run_status。Agent 私有记忆只能由本人写入,跨 Agent 共享稳定结论用 blackboard.write,给单个 Agent 留上下文用 agent.message。不要假装工具已执行;工具结果会由 Runtime 作为 observation 返回。只输出 JSON 对象,不要 markdown,不要泄露密钥。" + "你是 Genarrative AI 游戏创作多智能体 Runtime 中的专业 Agent。你必须在白名单工具内规划行动:先给一句 thinkingSummary,再给短计划,再决定是否请求工具。只能请求 memory.read、memory.write、conversation.read、asset.list、project.index、project.diff、file.list、file.read、file.write、task.list、task.create、task.update、command.run_limited、preview.start、canvas.asset_generate、blackboard.write、agent.message、agent.delegate、agent.schedule_ready、agent.run_status。Agent 私有记忆只能由本人写入,跨 Agent 共享稳定结论用 blackboard.write,给单个 Agent 留上下文用 agent.message。不要假装工具已执行;工具结果会由 Runtime 作为 observation 返回。只输出 JSON 对象,不要 markdown,不要泄露密钥。" } pub(crate) fn game_creator_agent_role_definition( 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 7e26afc62..7bc3064c4 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests.rs @@ -2970,6 +2970,237 @@ async fn background_agent_runtime_delegate_respects_project_policy() { fs::remove_dir_all(root).ok(); } +#[tokio::test] +async fn background_agent_runtime_can_schedule_ready_tasks_from_tool() { + let root = unique_project_path(); + init_local_game_project_at(&root, "project-1", "月光厨房").expect("project init"); + write_project_permission_policy_at( + &root, + ProjectPermissionPolicy { + denied_commands: Vec::new(), + confirm_commands: Vec::new(), + agent_policies: BTreeMap::new(), + }, + ) + .expect("allow ready scheduler tool"); + + let (design_sender, design_receiver) = mpsc::channel(); + let design_plan_json = serde_json::json!({ + "thinkingSummary": "完成当前任务后调度下一个 ready Agent", + "plan": ["标记当前任务完成", "调度 manifest ready task"], + "actions": [ + { + "tool": "task.update", + "reason": "当前设计拆解任务已经完成", + "input": { "taskId": "design-director", "status": "completed" } + }, + { + "tool": "agent.schedule_ready", + "reason": "依赖设计拆解的任务现在可以交给对应 Agent", + "input": { "limit": 1 } + } + ], + "response": "" + }) + .to_string(); + let design_base_url = spawn_mock_llm_server_responses_with_capture( + vec![ + design_plan_json, + "已完成设计拆解,并调度下一个 ready 任务。".to_string(), + ], + Some(design_sender), + ); + let (foundation_sender, foundation_receiver) = mpsc::channel(); + let foundation_plan_json = serde_json::json!({ + "thinkingSummary": "收到玩法规格 ready 任务", + "plan": ["标记玩法规格任务完成"], + "actions": [ + { + "tool": "task.update", + "reason": "玩法规格已整理完成", + "input": { "taskId": "design-foundation", "status": "completed" } + } + ], + "response": "" + }) + .to_string(); + let foundation_base_url = spawn_mock_llm_server_responses_with_capture( + vec![foundation_plan_json, "已完成玩法规格任务。".to_string()], + Some(foundation_sender), + ); + let _config_guard = write_test_local_config(format!( + r#"{{ + "agentLlm": {{ + "design-director": {{ + "apiKey": "design-key", + "baseUrl": {design_base_url:?}, + "model": "design-runtime-model", + "apiKind": "openai_responses" + }}, + "design-foundation": {{ + "apiKey": "foundation-key", + "baseUrl": {foundation_base_url:?}, + "model": "foundation-runtime-model", + "apiKind": "openai_responses" + }} + }} +}}"# + )); + + start_game_creator_agent_background_task_at( + &root, + "design-director", + "完成设计拆解并调度后续 ready 任务", + "design-schedule-ready-run", + ) + .expect("start scheduler agent task"); + + let design_plan_request = design_receiver + .recv_timeout(Duration::from_secs(2)) + .expect("design plan llm request"); + assert!(design_plan_request.contains("agent.schedule_ready")); + let foundation_plan_request = foundation_receiver + .recv_timeout(Duration::from_secs(2)) + .expect("foundation plan llm request"); + assert!(foundation_plan_request.contains("处理 manifest ready 任务:确定玩法规格")); + assert!(foundation_plan_request.contains("design-foundation")); + let design_final_request = design_receiver + .recv_timeout(Duration::from_secs(2)) + .expect("design final llm request"); + assert!(design_final_request.contains("agent.schedule_ready")); + assert!(design_final_request.contains("已调度 1 个 Ready 任务")); + assert!(design_final_request.contains("design-foundation")); + let foundation_final_request = foundation_receiver + .recv_timeout(Duration::from_secs(2)) + .expect("foundation final llm request"); + assert!(foundation_final_request.contains("任务 design-foundation 已更新为 completed")); + + let design_runtime = wait_for_agent_runtime_idle(&root, "design-director"); + assert_eq!(design_runtime.status, "idle"); + assert!(design_runtime + .tool_policy + .auto_tools + .contains(&"agent.schedule_ready".to_string())); + assert!(design_runtime.observations.iter().any(|item| { + item.contains("agent.schedule_ready:ok · 已调度 1 个 Ready 任务") + })); + assert!(design_runtime.recent_tool_calls.iter().any(|call| { + call.tool == "agent.schedule_ready" + && call.status == "ok" + && call + .detail + .as_deref() + .is_some_and(|detail| detail.contains("design-foundation")) + })); + + let foundation_runtime = wait_for_agent_runtime_idle(&root, "design-foundation"); + assert_eq!(foundation_runtime.status, "idle"); + assert_eq!(foundation_runtime.source, "agent-ready-task-scheduler"); + assert!(foundation_runtime + .observations + .iter() + .any(|item| item.contains("task.update:ok · 任务 design-foundation 已更新为 completed"))); + + let manifest_after: Value = + serde_json::from_str(&fs::read_to_string(root.join(".agent/manifest.json")).unwrap()) + .expect("manifest json"); + assert_task_status(&manifest_after, "design-director", "completed"); + assert_task_status(&manifest_after, "design-foundation", "completed"); + let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db"); + assert!(agent_db.contains("\"recordType\":\"agent.runtime.ready_task.scheduled\"")); + assert!(agent_db.contains("\"agentId\":\"design-foundation\"")); + + fs::remove_dir_all(root).ok(); +} + +#[tokio::test] +async fn background_agent_runtime_schedule_ready_respects_project_policy() { + let root = unique_project_path(); + init_local_game_project_at(&root, "project-1", "月光厨房").expect("project init"); + write_project_permission_policy_at( + &root, + ProjectPermissionPolicy { + denied_commands: Vec::new(), + confirm_commands: vec!["agent.schedule_ready".to_string()], + agent_policies: BTreeMap::new(), + }, + ) + .expect("write policy"); + + let (sender, receiver) = mpsc::channel(); + let plan_json = serde_json::json!({ + "thinkingSummary": "尝试调度 ready task", + "plan": ["请求调度 ready task", "等待权限结果"], + "actions": [ + { + "tool": "agent.schedule_ready", + "reason": "应该把 ready task 交给对应 Agent", + "input": { "limit": 1 } + } + ], + "response": "" + }) + .to_string(); + 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": {{ + "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", + "尝试调度 ready task", + "design-schedule-ready-policy-run", + ) + .expect("start scheduler agent task"); + + let plan_request = receiver + .recv_timeout(Duration::from_secs(2)) + .expect("design plan llm request"); + assert!(plan_request.contains("agent.schedule_ready")); + assert!(receiver.recv_timeout(Duration::from_millis(200)).is_err()); + + 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.schedule_ready".to_string())); + assert!(design_runtime.observations.iter().any(|item| { + item.contains( + "agent.schedule_ready:waiting-for-confirmation · 项目权限策略要求用户确认:agent.schedule_ready", + ) + })); + assert!(design_runtime.recent_tool_calls.iter().any(|call| { + call.tool == "agent.schedule_ready" && call.status == "waiting-for-confirmation" + })); + + let manifest_after: Value = + serde_json::from_str(&fs::read_to_string(root.join(".agent/manifest.json")).unwrap()) + .expect("manifest json"); + assert_task_status(&manifest_after, "design-director", "pending"); + assert_task_status(&manifest_after, "design-foundation", "pending"); + let foundation_runtime = + read_game_creator_agent_runtime_at(&root, "design-foundation").expect("runtime"); + assert!(foundation_runtime.recent_tasks.is_empty()); + let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db"); + assert!(!agent_db.contains("\"recordType\":\"agent.runtime.ready_task.scheduled\"")); + assert!(!agent_db.contains("\"agentId\":\"design-foundation\"")); + + fs::remove_dir_all(root).ok(); +} + #[tokio::test] async fn background_agent_runtime_can_write_memory_and_project_files() { let root = unique_project_path(); diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 7450719fb..b3378de5a 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -30,6 +30,7 @@ - 2026-07-10 补充:后台 Agent Runtime 的白名单工具继续扩到 `agent.run_status`,让 Agent 可在 loop 中读取自己、目标 Agent 或一组 Agent 的 Runtime 状态摘要,用于判断同伴是否正在运行、最近任务和最近工具动作。该工具复用 `agent.run_status` 项目权限策略,策略要求确认或拒绝时不会读取状态;observation 只包含 agentId、status、phase、runId、当前任务、当前动作、下一步、计划摘要、最近任务、最近工具和错误摘要,不返回 `.agent/runtime/*` 文件绝对路径。 - 2026-07-10 补充:后台 Agent Runtime 的白名单工具继续扩到 `agent.delegate`,让 Agent 可把明确子任务投递到另一个 Agent 的独立后台队列。该工具复用目标 Agent 既有锁和 pending drain 语义,不创建平行 runtime;同一目标 Agent 串行,不同 Agent 可并行。该工具使用独立 `agent.delegate` 权限策略,策略要求确认或拒绝时不会写目标对话、不会启动目标后台任务,也不会写 `agent.runtime.agent.delegate` 审计记录。 - 2026-07-10 补充:主窗口 Agent 状态栏在开发模式新增“调度 Ready”控制,用来显式调用 `schedule_game_creator_agent_ready_tasks`。该控制先读取项目策略,命中 `agent.schedule_ready` confirm 时走现有确认弹窗,确认后才把 manifest ready task 投递到对应 Agent Runtime;普通用户窗口不展示这个开发控制。 +- 2026-07-10 补充:后台 Agent Runtime 的白名单工具继续扩到 `agent.schedule_ready`,让 Agent 在完成或更新 manifest task 后可按项目权限策略自行调度新 ready task。该工具复用同一个 scheduler:扫描依赖已完成且仍为 pending 的 manifest task,先标为 running,再按 taskId 投递到对应 Agent 的既有后台队列;策略要求确认时当前 Agent 停在 `waiting-for-confirmation`,不会静默启动下游 Agent。 - 影响范围:`apps/ai-game-creator-shell` 的 Tauri command、Agent Runtime state/event、开发窗口单 Agent 聊天、项目内 Agent 对话弹窗、`appSurface.test.ts` 和 AI 游戏创作 App 实施计划。 - 验证方式:运行 Tauri Rust 后台 Agent 并行测试、壳前端 appSurface 测试、壳 typecheck、编码检查和 `git diff --check`。 - 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index dea9163f0..f5e1530b8 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -51,6 +51,7 @@ Agent Runtime 负责: - 2026-07-10 补充:Agent Runtime state / result 新增 `taskQueue`,从 `.agent/runtime/tasks/.jsonl` 中每个 `runId` 的最新记录汇总 `total / pending / running / completed / failed / latestRunId`;开发窗口 Runtime 面板、主窗口 Agent 状态列表、`agent.run_status` observation 和下一轮 planning prompt 都读取该摘要,用于判断同一 Agent 是否仍有排队任务。该字段是运行观测摘要,不新增调度器、SQLite 或独立 worker。 - 2026-07-10 补充:Runtime 新增 `agent.schedule_ready` 调度入口。开发构建可在权限确认后扫描 manifest ready task,把依赖已完成且仍为 `pending` 的任务标成 `running`,并按 taskId 投递到对应 Agent 的既有后台队列;source 固定为 `agent-ready-task-scheduler`,审计记录写 `agent.runtime.ready_task.scheduled`。该入口只把 manifest ready task 接入现有 per-agent 队列、锁、JSONL、LLM loop、工具策略和事件流,不新增独立 worker,也不会在默认确认策略下静默启动。 - 2026-07-10 补充:主窗口 Agent 状态栏的“调度 Ready”只在开发模式显示。点击后复用项目策略确认弹窗,确认通过才调用 `schedule_game_creator_agent_ready_tasks`,并把返回的 Runtime 合并回 Agent 状态卡;普通用户窗口继续只展示状态和单 Agent 对话入口,不直接暴露 ready-task 调度按钮。 +- 2026-07-10 补充:`agent.schedule_ready` 同时接入后台 Agent loop 白名单工具。Agent 可在 `task.update` 或 `task.create` 后自行请求调度新 ready task;执行仍走 `agent.schedule_ready` 项目权限策略,确认策略命中时当前 run 停在 `waiting-for-confirmation`,不会继续消费队列或启动下游 Agent。确认通过后复用同一 scheduler,把 ready task 投递到对应 Agent 的既有后台队列。 - 2026-07-10 补充:单 Agent 聊天和后台 planning prompt 会读取同一个 Agent 的 Runtime 连续上下文,把本 Agent 最近 status / phase / runId / 当前任务 / 下一步、最近回复、计划、观察、最近 3 条工具动作、最近事件、最近 3 条任务记录和工具策略摘要带入下一轮推理;上下文按规范 taskId 隔离,不读取其他 Agent 的 runtime 文件,并在进入 prompt 前过滤密钥和本机绝对路径。新后台 run 启动时会继承本 Agent 上次 `recentToolCalls` 和 `lastResponse`,让多轮任务不丢失结构化行动证据。 - 2026-07-10 补充:后台任务工具箱已加入 `preview.start`。Agent 可在 loop 中自行请求启动当前项目的本地 HTTP 预览;Runtime 会复用 `preview.start` 策略、项目写锁、共享 `PreviewRegistry`、manifest 预览状态、`.agent/logs/preview.log` 和 run trace 追加逻辑,并把 `agent.runtime.preview.start` 写入 `.agent/agent.db`。该 observation 只向 LLM 返回 localhost URL 与端口,不返回用户项目绝对路径。 - 2026-07-10 补充:后台任务工具箱已加入 `canvas.asset_generate`。Agent 可在 loop 中自行给出素材 prompt,通过 AppData / Tauri 配置里的 `editorApi` 调用 External Editor API 生成首版美术素材、下载到 `assets/canvas-generated/` 并登记 manifest;Runtime 复用 `canvas.asset_generate` 策略和项目写锁,并写入 `agent.runtime.canvas.asset_generate` 审计记录。API Key 不进入 observation、manifest、agent.db 或日志;策略要求确认或拒绝时不会调用外部 API。 @@ -282,7 +283,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.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`、`file.write`、`task.create`、`task.update`、`command.run_limited`、`blackboard.write`、`agent.message` 和 `agent.delegate`;`memory.write scope=agent` 只允许写当前 Agent 自己的私有记忆,跨 Agent 共享必须改用 `blackboard.write` 或 `agent.message`;`task.create` 只追加新 manifest 任务,`task.update` 只更新已有任务状态;若项目策略拒绝,对应工具不会执行,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` 票据,权限 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`、`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`;`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 同时完成时的行交错风险。 - 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` 项目权限策略保护。 @@ -292,7 +293,7 @@ game-project/ - 2026-07-10 补充:当前工具箱还开放 `project.diff`;该工具只读返回 checkpoint 对比后的新增、修改、删除计数和相对路径摘要,并受 `project.diff` 项目权限策略保护。 - 2026-07-10 补充:当前工具箱还开放 `agent.run_status`;该工具只读返回自己、目标 Agent 或一组 Agent 的 Runtime 状态摘要,并受 `agent.run_status` 项目权限策略保护。 - 2026-07-10 补充:当前工具箱还开放 `agent.delegate`,审计记录类型为 `agent.runtime.agent.delegate`;该工具只把任务投递到目标 Agent 既有后台队列,不创建平行 runtime,不绕过目标 Agent 的锁和队列,也受 `agent.delegate` 项目权限策略保护。 -- 2026-07-10 补充:当前 Runtime 命令还开放 `agent.schedule_ready`,审计记录类型为 `agent.runtime.ready_task.scheduled`;该命令把 manifest ready task 批量投递到对应 Agent 的既有后台队列,默认需要确认,不属于 Agent loop 内可直接调用的工具。 +- 2026-07-10 补充:当前 Runtime 命令和后台工具箱都开放 `agent.schedule_ready`,审计记录类型为 `agent.runtime.ready_task.scheduled`;该命令把 manifest ready task 批量投递到对应 Agent 的既有后台队列,默认需要确认,确认前不会启动下游 Agent。 - 普通用户可在聊天框输入 `/project /绝对路径` 生成待确认的 `project.create` 命令,用于授权并初始化本地项目目录;相对路径不会生成待确认命令;开发窗口仍可直接编辑项目路径。 - 单窗口首页和项目组页可选择、打开、新建或显示当前输入的项目绝对路径;最近项目行也可显示目录,非法或相对路径不会调用系统文件管理器。 - 普通用户侧的生成、上传、运行、自检、预览状态 / 启动 / 打开 / 停止、记忆写入和画板资产导入都必须先完成 `/project` 初始化;未初始化时只提示设置本地项目,不落到默认临时目录。