From 31ec13c52ce68e62c314f95c5b766a2db1e99df4 Mon Sep 17 00:00:00 2001 From: AIGameCreator App Date: Fri, 10 Jul 2026 01:20:11 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E9=BD=90Agent=20Runtime=E7=B4=A0?= =?UTF-8?q?=E6=9D=90=E7=94=9F=E6=88=90=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后台 Agent 工具箱新增 canvas.asset_generate,可由 Agent 自主生成首版美术素材。 素材生成复用 External Editor API、项目权限策略、项目写锁和本地 manifest 资产登记。 补充生成成功与策略拦截测试,同步 Runtime V1 文档。 --- .../src-tauri/src/agent.rs | 87 +++++++- .../src-tauri/src/tests.rs | 209 ++++++++++++++++++ .../shared-memory/decision-log.md | 1 + ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 2 + 4 files changed, 295 insertions(+), 4 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 c35fbadea..fc5ab42c2 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent.rs @@ -536,7 +536,8 @@ async fn run_game_creator_agent_background_task( action.reason.as_deref(), ); let observation = - execute_game_creator_agent_runtime_tool_action(&root, &agent_id, action); + execute_game_creator_agent_runtime_tool_action(&root, &agent_id, &task, action) + .await; let observation_summary = observation.summary(); runtime.observations.push(observation_summary.clone()); runtime.next_step = "把工具观察交给 Agent 修正计划".to_string(); @@ -796,7 +797,7 @@ fn build_game_creator_agent_background_tool_plan_request( .map_err(|error| format!("序列化 Agent 工具观察失败:{error}"))? }; let prompt = format!( - "项目上下文如下。你正在执行后台 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|file.read|file.write|command.run_limited|preview.start|blackboard.write|agent.message\",\"reason\":\"为什么需要\",\"input\":{{}}}}],\"response\":\"如果无需继续调用工具,可直接给最终回复\"}}\n\n工具输入约定:memory.read 使用 {{\"scope\":\"session|project|blackboard|agent\"}};memory.write 使用 {{\"scope\":\"agent|project|session|blackboard\",\"title\":\"标题\",\"content\":\"要沉淀的稳定结论\",\"mode\":\"append|overwrite\"}};file.read 使用 {{\"path\":\"项目内相对路径\"}};file.write 使用 {{\"path\":\"项目内相对路径\",\"content\":\"文件内容\"}};command.run_limited 使用 {{\"commandId\":\"game.static_smoke\"}},只支持本地静态自检;preview.start input 可为空,用于启动当前项目的 127.0.0.1 本地 HTTP 预览;blackboard.write 使用 {{\"title\":\"标题\",\"content\":\"要共享给所有 Agent 的稳定结论\"}};agent.message 使用 {{\"agentId\":\"目标 taskId\",\"content\":\"给目标 Agent 的定向消息\"}};如果已有观察足够,请返回空 actions 并填写 response。其他工具 input 可为空。" + "项目上下文如下。你正在执行后台 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|file.read|file.write|command.run_limited|preview.start|canvas.asset_generate|blackboard.write|agent.message\",\"reason\":\"为什么需要\",\"input\":{{}}}}],\"response\":\"如果无需继续调用工具,可直接给最终回复\"}}\n\n工具输入约定:memory.read 使用 {{\"scope\":\"session|project|blackboard|agent\"}};memory.write 使用 {{\"scope\":\"agent|project|session|blackboard\",\"title\":\"标题\",\"content\":\"要沉淀的稳定结论\",\"mode\":\"append|overwrite\"}};file.read 使用 {{\"path\":\"项目内相对路径\"}};file.write 使用 {{\"path\":\"项目内相对路径\",\"content\":\"文件内容\"}};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 的定向消息\"}};如果已有观察足够,请返回空 actions 并填写 response。其他工具 input 可为空。" ); let request = LlmRunRequest::new(vec![ LlmMessage::system(game_creator_agent_runtime_tool_plan_system_prompt()), @@ -861,9 +862,10 @@ fn parse_game_creator_agent_tool_plan_response( Ok(plan) } -fn execute_game_creator_agent_runtime_tool_action( +async fn execute_game_creator_agent_runtime_tool_action( root: &Path, agent_id: &str, + task: &str, action: &AgentRuntimeToolAction, ) -> AgentRuntimeToolObservation { let tool = action.tool.trim(); @@ -898,6 +900,10 @@ fn execute_game_creator_agent_runtime_tool_action( observe_agent_runtime_limited_command(root, agent_id, &action.input) } "preview.start" => observe_agent_runtime_preview_start(root, agent_id), + "canvas.asset_generate" => { + observe_agent_runtime_platform_art_asset_generation(root, agent_id, task, &action.input) + .await + } "blackboard.write" => observe_agent_runtime_blackboard_write(root, agent_id, &action.input), "agent.message" => observe_agent_runtime_agent_message(root, agent_id, &action.input), _ => AgentRuntimeToolObservation { @@ -920,6 +926,7 @@ fn game_creator_agent_runtime_tool_command_id(tool: &str) -> Option<&'static str "file.write" => Some("file.write"), "command.run_limited" => Some("command.run_limited"), "preview.start" => Some("preview.start"), + "canvas.asset_generate" => Some("canvas.asset_generate"), "blackboard.write" => Some("memory.write"), "agent.message" => Some("conversation.write"), _ => None, @@ -1318,6 +1325,76 @@ fn observe_agent_runtime_preview_start(root: &Path, agent_id: &str) -> AgentRunt } } +async fn observe_agent_runtime_platform_art_asset_generation( + root: &Path, + agent_id: &str, + task: &str, + input: &serde_json::Value, +) -> AgentRuntimeToolObservation { + let prompt = agent_runtime_tool_input_text(input, &["prompt", "assetPrompt", "description"]); + let prompt = if prompt.trim().is_empty() { + task.trim().to_string() + } else { + prompt + }; + if prompt.trim().is_empty() { + return AgentRuntimeToolObservation { + tool: "canvas.asset_generate".to_string(), + status: "failed".to_string(), + summary: "缺少素材生成提示词".to_string(), + detail: None, + }; + } + let _lock = match acquire_project_write_lock(root, "canvas.asset_generate") { + Ok(lock) => lock, + Err(error) => { + return AgentRuntimeToolObservation { + tool: "canvas.asset_generate".to_string(), + status: "failed".to_string(), + summary: redact_agent_runtime_project_paths(root, &error, 240), + detail: None, + }; + } + }; + match generate_platform_art_asset_at(root, prompt.trim(), &[]).await { + Ok(generated) => { + let _ = append_agent_db_record( + root, + serde_json::json!({ + "recordType": "agent.runtime.canvas.asset_generate", + "agentId": agent_id, + "assetId": generated.asset.id.clone(), + "localPath": generated.asset.local_path.clone(), + "resourceId": generated.resource_id.clone(), + "assetObjectId": generated.asset_object_id.clone(), + "taskId": generated.task_id.clone(), + "model": generated.model.clone(), + }), + ); + AgentRuntimeToolObservation { + tool: "canvas.asset_generate".to_string(), + status: "ok".to_string(), + summary: format!("已生成美术素材:{}", generated.asset.local_path), + detail: Some(format!( + "assetId={}, localPath={}, resourceId={}, assetObjectId={}, taskId={}, model={}", + generated.asset.id, + generated.asset.local_path, + generated.resource_id.as_deref().unwrap_or(""), + generated.asset_object_id.as_deref().unwrap_or(""), + generated.task_id.as_deref().unwrap_or(""), + generated.model.as_deref().unwrap_or("") + )), + } + } + Err(error) => AgentRuntimeToolObservation { + tool: "canvas.asset_generate".to_string(), + status: "failed".to_string(), + summary: redact_agent_runtime_project_paths(root, &error, 240), + detail: None, + }, + } +} + fn observe_agent_runtime_blackboard_write( root: &Path, agent_id: &str, @@ -1766,6 +1843,7 @@ fn default_game_creator_agent_runtime_state(agent_id: &str, run_id: &str) -> Age "file.write".to_string(), "command.run_limited".to_string(), "preview.start".to_string(), + "canvas.asset_generate".to_string(), "agent.run_status".to_string(), ], last_response: None, @@ -1822,6 +1900,7 @@ fn normalize_game_creator_agent_runtime_state(state: &mut AgentRuntimeState, age "file.write".to_string(), "command.run_limited".to_string(), "preview.start".to_string(), + "canvas.asset_generate".to_string(), "agent.run_status".to_string(), ]; } @@ -2434,7 +2513,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、file.read、file.write、command.run_limited、preview.start、blackboard.write、agent.message。不要假装工具已执行;工具结果会由 Runtime 作为 observation 返回。只输出 JSON 对象,不要 markdown,不要泄露密钥。" + "你是 Genarrative AI 游戏创作多智能体 Runtime 中的专业 Agent。你必须在白名单工具内规划行动:先给一句 thinkingSummary,再给短计划,再决定是否请求工具。只能请求 memory.read、memory.write、conversation.read、asset.list、project.index、file.read、file.write、command.run_limited、preview.start、canvas.asset_generate、blackboard.write、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 88135b091..bddb36db6 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests.rs @@ -2546,6 +2546,215 @@ async fn background_agent_runtime_preview_start_respects_project_policy() { fs::remove_dir_all(root).ok(); } +#[tokio::test] +async fn background_agent_runtime_can_generate_platform_art_asset() { + let root = unique_project_path(); + let config_dir = unique_project_path(); + let canvas_base_url = spawn_mock_external_canvas_api_server(); + 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(), + }, + ) + .expect("allow asset generation"); + let (sender, receiver) = mpsc::channel(); + let plan_json = serde_json::json!({ + "thinkingSummary": "当前项目缺少首版角色素材,需要通过平台生成一张核心美术图", + "plan": ["生成主角素材", "登记到本地资产清单", "回报素材路径"], + "actions": [ + { + "tool": "canvas.asset_generate", + "reason": "生成可用于首版原型的主角素材", + "input": { "prompt": "透明 PNG 像素月光主角,适合厨房弹幕游戏" } + } + ], + "response": "" + }) + .to_string(); + let llm_base_url = spawn_mock_llm_server_responses_with_capture( + vec![ + plan_json, + "首版主角素材已经生成并登记到本地项目。".to_string(), + ], + Some(sender), + ); + fs::create_dir_all(&config_dir).expect("runtime config dir"); + fs::write( + config_dir.join(GAME_CREATOR_CONFIG_FILE_NAME), + serde_json::json!({ + "editorApi": { + "baseUrl": canvas_base_url, + "apiKey": "editor-runtime-key" + }, + "agentLlm": { + "art-asset-plan": { + "apiKey": "art-key", + "baseUrl": llm_base_url, + "model": "art-runtime-model", + "apiKind": "openai_responses" + } + } + }) + .to_string(), + ) + .expect("write runtime config"); + let _config_guard = use_test_runtime_config_dir(config_dir.clone()); + + start_game_creator_agent_background_task_at( + &root, + "art-asset-plan", + "为月光厨房生成首版主角素材", + "art-generate-run", + ) + .expect("start background task"); + + let plan_request = receiver + .recv_timeout(Duration::from_secs(2)) + .expect("plan llm request"); + assert!(plan_request.contains("canvas.asset_generate")); + let final_request = receiver + .recv_timeout(Duration::from_secs(4)) + .expect("final reply llm request"); + assert!(final_request.contains("canvas.asset_generate")); + assert!(final_request.contains("\\\"status\\\": \\\"ok\\\"")); + assert!(final_request.contains("assets/canvas-generated/")); + assert!(!final_request.contains("editor-runtime-key")); + let root_display = root.to_string_lossy(); + assert!(!final_request.contains(root_display.as_ref())); + + let mut runtime = read_game_creator_agent_runtime_at(&root, "art-asset-plan") + .expect("read runtime") + .state; + for _ in 0..50 { + if runtime.status == "idle" { + break; + } + std::thread::sleep(Duration::from_millis(20)); + runtime = read_game_creator_agent_runtime_at(&root, "art-asset-plan") + .expect("read runtime") + .state; + } + assert_eq!(runtime.status, "idle"); + assert!(runtime + .observations + .iter() + .any(|item| item.contains("canvas.asset_generate:ok · 已生成美术素材:"))); + let manifest: Value = + serde_json::from_str(&fs::read_to_string(root.join(".agent/manifest.json")).unwrap()) + .expect("manifest json"); + let asset = &manifest["assets"][0]; + assert_eq!(asset["source"]["kind"], "canvas"); + assert_eq!(asset["source"]["resourceId"], "resource-1"); + assert!(asset["localPath"] + .as_str() + .unwrap() + .starts_with("assets/canvas-generated/")); + assert_eq!( + fs::read(root.join(asset["localPath"].as_str().unwrap())).unwrap(), + b"fake-png" + ); + let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db"); + assert!(agent_db.contains("\"recordType\":\"canvas.asset_generate\"")); + assert!(agent_db.contains("\"recordType\":\"agent.runtime.canvas.asset_generate\"")); + assert!(agent_db.contains("\"agentId\":\"art-asset-plan\"")); + assert!(!agent_db.contains("editor-runtime-key")); + + fs::remove_dir_all(root).ok(); + fs::remove_dir_all(config_dir).ok(); +} + +#[tokio::test] +async fn background_agent_runtime_asset_generation_respects_project_policy() { + let root = unique_project_path(); + let config_dir = 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!["canvas.asset_generate".to_string()], + }, + ) + .expect("write policy"); + let (sender, receiver) = mpsc::channel(); + let plan_json = serde_json::json!({ + "thinkingSummary": "尝试生成首版素材", + "plan": ["生成主角素材"], + "actions": [ + { + "tool": "canvas.asset_generate", + "reason": "测试策略拦截平台素材生成", + "input": { "prompt": "像素月光主角" } + } + ], + "response": "" + }) + .to_string(); + let llm_base_url = spawn_mock_llm_server_responses_with_capture( + vec![plan_json, "生成素材需要用户确认。".to_string()], + Some(sender), + ); + fs::create_dir_all(&config_dir).expect("runtime config dir"); + fs::write( + config_dir.join(GAME_CREATOR_CONFIG_FILE_NAME), + serde_json::json!({ + "agentLlm": { + "art-asset-plan": { + "apiKey": "art-key", + "baseUrl": llm_base_url, + "model": "art-runtime-model", + "apiKind": "openai_responses" + } + } + }) + .to_string(), + ) + .expect("write runtime config"); + let _config_guard = use_test_runtime_config_dir(config_dir.clone()); + + start_game_creator_agent_background_task_at( + &root, + "art-asset-plan", + "后台尝试生成首版素材", + "art-generate-policy-run", + ) + .expect("start background task"); + + let _plan_request = receiver + .recv_timeout(Duration::from_secs(2)) + .expect("plan llm request"); + let final_request = receiver + .recv_timeout(Duration::from_secs(2)) + .expect("final reply llm request"); + assert!(final_request.contains("项目权限策略要求用户确认:canvas.asset_generate")); + assert!(!final_request.contains("已生成美术素材")); + + let mut runtime = read_game_creator_agent_runtime_at(&root, "art-asset-plan") + .expect("read runtime") + .state; + for _ in 0..50 { + if runtime.status == "idle" { + break; + } + std::thread::sleep(Duration::from_millis(20)); + runtime = read_game_creator_agent_runtime_at(&root, "art-asset-plan") + .expect("read runtime") + .state; + } + assert_eq!(runtime.status, "idle"); + assert!(runtime.observations.iter().any(|item| item.contains( + "canvas.asset_generate:blocked · 项目权限策略要求用户确认:canvas.asset_generate" + ))); + let manifest = read_manifest_for_project(&root).expect("manifest"); + assert!(manifest.assets.is_empty()); + + fs::remove_dir_all(root).ok(); + fs::remove_dir_all(config_dir).ok(); +} + #[tokio::test] async fn background_agent_runtime_tool_action_respects_confirm_policy() { 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 275e4890f..5b7d706f3 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -21,6 +21,7 @@ - 背景:开发用单 Agent 聊天已经能真实调用各 Agent 的 LLM 路由并持久化对话,但 Agent 仍主要表现为同步问答,用户无法明确投递一个任务让某个 Agent 独立运行,也无法同时启动多个 Agent 的工作。 - 决策:在现有 `.agent/runtime` 和 `.agent/conversations` 基础上新增单 Agent 后台任务入口。Tauri 命令 `start_game_creator_agent_runtime_task` 立即写入该 Agent 的 runtime state/event/task history,追加用户任务到 `.agent/conversations/agents/.jsonl`,随后在 App 进程内启动 tokio task 执行最小 Agent loop:Agent 按轮输出 `thinkingSummary / plan / actions / response`,Runtime 按白名单和项目权限策略执行工具并记录 `action / observation` 事件,再把已有 observation 放回下一轮 prompt,让 Agent 修正计划、继续行动或用空 actions + response 收束;当前后台任务最多执行 3 轮 loop,仍未收束时再按最后计划和全部观察生成最终回复。完成或失败后把 assistant 回复或错误追加回对话,并写入 `.agent/agent.db` 审计记录。工具箱包含只读工具 `memory.read`、`conversation.read`、`asset.list`、`project.index`、`file.read`,以及受策略保护的写/运行工具 `memory.write`、`file.write`、`command.run_limited`、`blackboard.write` 和 `agent.message`;`memory.write` 可追加或覆盖本 Agent 私有记忆、项目长期/短期记忆或黑板,`file.write` 只能写项目内相对路径,`command.run_limited` 只接受 `game.static_smoke` 并复用本地静态自检安全边界,`blackboard.write` 追加共享黑板,`agent.message` 写目标 Agent 对话;策略要求确认或拒绝时不执行写入或运行,只把策略结果作为 observation 回给 Agent。每个 Agent 的任务历史落在 `.agent/runtime/tasks/.jsonl`,读 runtime 时按 `runId` 去重返回最近任务,任务视角状态使用 `pending / running / completed / failed`,Runtime state 增加 `nextStep`,UI 在 Runtime 面板和主 Agent 状态卡展示当前任务、动作、下一步与最近任务。不同 Agent 使用独立 `.agent/runtime/locks/.lock`,允许并行运行;同一 Agent 已有运行任务时,新任务会先进入该 Agent 的 pending 队列,当前 drain 持锁完成后串行继续下一条 pending。该能力仍不是独立 OS 进程或跨重启离线常驻 worker。 - 2026-07-10 补充:后台 Agent Runtime 的白名单工具继续扩到 `preview.start`,让 Agent 在完成写盘或静态自检后能按策略自行启动当前项目的 `127.0.0.1` 本地 HTTP 预览。该工具复用 `preview.start` 权限策略、项目写锁、共享 `PreviewRegistry`、manifest 预览状态、`.agent/logs/preview.log` 和 run trace 追加逻辑;写入 `.agent/agent.db` 的审计类型为 `agent.runtime.preview.start`。发给 LLM 的 observation 只包含 localhost URL 和端口,不包含用户项目绝对路径。 +- 2026-07-10 补充:后台 Agent Runtime 的白名单工具继续扩到 `canvas.asset_generate`,让美术类 Agent 可在 loop 中自行请求生成首版美术素材。该工具读取 AppData / Tauri 配置中的 `editorApi`,复用 `canvas.asset_generate` 权限策略、项目写锁、External Editor API 生成和下载链路、manifest 资产登记以及 `canvas.asset_generate` 本地索引记录;另写 `agent.runtime.canvas.asset_generate` 记录到 `.agent/agent.db`,标明触发的 agent 与本地素材路径。API Key 不进入 prompt observation、manifest、agent.db 或日志;策略要求确认或拒绝时不会调用外部 API。 - 补充:规范 Agent ID 统一使用 manifest taskId,例如 `art-asset-plan` 和 `code-prototype`;历史前端曾使用的 `group-role` 别名只在 Tauri command 层兼容并映射到规范 taskId。主窗口 Agent 状态列表通过 `read_game_creator_agent_runtimes` 批量读取 `.agent/runtime/agents/.json` 和最近任务,把每个 Agent 的 Runtime 状态、当前动作和最近 task 直接显示在状态卡片和 `/agents` 汇总里。 - 影响范围:`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`。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index a733b1d26..c67549af5 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -34,6 +34,7 @@ Agent Runtime 负责: - 编排能力:任务拆分、任务图依赖、专业组调度、多智能体协作;Runtime V1 会为单 Agent 对话和生成 loop 中的角色 brief 写入独立 runtime state / event,先解决“每个 Agent 正在做什么、跑到哪一步、最近一次 task/run 是什么”的可观测性。 - 后台任务能力:开发窗口单 Agent 聊天和项目内 Agent 对话弹窗可把当前输入投递为单 Agent 后台任务,Tauri 命令 `start_game_creator_agent_runtime_task` 会立即写入该 Agent 的 `.agent/runtime/agents/.json`、`.agent/runtime/events/.jsonl`、`.agent/runtime/tasks/.jsonl` 和 `.agent/conversations/agents/.jsonl`,随后在 App 进程内启动 tokio task 执行最小 Agent loop:每轮让该 Agent 输出 `thinkingSummary / plan / actions / response`,Runtime 按白名单和项目权限策略执行工具动作,写入 `action / observation` 事件,再把 observation 放入下一轮 prompt 让 Agent 修正计划、继续行动或用空 actions + response 收束;后台任务最多执行 3 轮 loop,仍未收束时再按最后计划和全部观察生成最终回复并追加回对话。工具箱包含只读工具 `memory.read`、`conversation.read`、`asset.list`、`project.index`、`file.read`,以及受策略保护的写/运行工具 `memory.write`、`file.write`、`command.run_limited`、`blackboard.write` 和 `agent.message`;`memory.write` 可追加或覆盖本 Agent 私有记忆、项目长期/短期记忆或黑板,`file.write` 只能写项目内相对路径并记录审计,`command.run_limited` 只接受 `game.static_smoke` 并复用本地静态自检安全边界,`blackboard.write` 追加 `memory/blackboard.md`,`agent.message` 给目标 `.agent/conversations/agents/.jsonl` 写入 tool 留言,策略要求确认或拒绝时不执行写入或运行,只把策略结果作为 observation 回给 Agent。`read_game_creator_agent_runtime` 会按 `runId` 去重返回最近任务,`read_game_creator_agent_runtimes` 批量读取所有规范 taskId 的 runtime;开发窗口、项目内 Agent 对话弹窗和主窗口 Agent 状态列表展示最近任务、当前任务、当前动作、下一步和运行阶段。不同 Agent 使用各自 runtime 锁,可以并行运行;同一 Agent 已有运行任务时,新任务会先写成 `pending / queued`,由当前后台 drain 在完成后串行继续执行。该能力仍属于 Runtime V1 的进程内任务,不是独立 OS 进程或跨重启离线常驻 worker。 - 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。 - 任务图能力:每轮 Orchestrator agenda、ready / active task 选择、Evaluator 结构化返工路由、返工轮 carry-over。 - 记忆能力:短期记忆 `memory/session.md`、长期记忆 `memory/project.md`、项目级黑板 `memory/blackboard.md` 和角色私有记忆 `memory/agents//.md`;黑板用于共享重要跨 agent 记忆,角色私有记忆只给对应角色 brief 读取和追加。最近 project / agent conversation 会作为短期 prompt 上下文读取,不替代正式 memory 文件。 - 对话能力:结构化对话记录统一落在 `.agent/conversations/` 的 append-only JSONL;普通聊天写 `.agent/conversations/project.jsonl`,进入单个 agent 后只写对应 `.agent/conversations/agents/.jsonl`,不把原始对话混进项目黑板或角色私有记忆。 @@ -255,6 +256,7 @@ game-project/ - v1 conversation 记录使用 append-only JSONL,每行带 `schemaVersion`、`role`、`content`、`agentId` 和 `updatedAt`,作为聊天历史和单 agent 对话历史的事实源;目录在首次写入时创建。 - 开发窗口和项目内 Agent 对话弹窗的“后台运行”只启动或排队单 Agent 后台任务,不阻塞等待回复;用户可刷新同一 Agent 对话或 runtime 状态查看进度和结果。后台任务会向 `.agent/runtime/tasks/.jsonl` 追加任务视角记录,任务状态使用 `pending / running / completed / failed`,读取时按 `runId` 去重返回最近任务;runtime state 自身仍可在完成后显示 `idle / completed`,二者语义分开。同一 Agent 的 pending 任务由持有 `.agent/runtime/locks/.lock` 的后台 drain 串行执行,避免同一 Agent 并发抢上下文;不同 Agent 仍可并行。后台任务的核心 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.tool_observation` / `agent.runtime.memory.write` / `agent.runtime.file.write` / `agent.runtime.command.run_limited` / `agent.runtime.blackboard.write` / `agent.runtime.agent.message` / `agent.runtime.background_task.completed` / `agent.runtime.background_task.failed` 审计记录。当前工具箱开放只读工具 `memory.read`、`conversation.read`、`asset.list`、`project.index`、`file.read`,以及受策略保护的写/运行工具 `memory.write`、`file.write`、`command.run_limited`、`blackboard.write` 和 `agent.message`;若项目策略要求确认或拒绝,对应工具不会执行,Runtime 会把策略结果作为 observation 回给 Agent。`.agent/agent.db` 追加写入按整行 JSONL 写入,减少多个 Agent 同时完成时的行交错风险。 - 2026-07-10 补充:当前工具箱还开放 `preview.start`,审计记录类型为 `agent.runtime.preview.start`;该工具不会打开任意 URL,只启动当前授权项目的 `127.0.0.1` 本地预览,并和 Tauri 用户命令共用同一个 `PreviewRegistry`。 +- 2026-07-10 补充:当前工具箱还开放 `canvas.asset_generate`,审计记录类型为 `agent.runtime.canvas.asset_generate`;该工具只通过配置好的 External Editor API 生成并回流素材,不暴露任意上传 / 任意网络请求能力。 - 普通用户可在聊天框输入 `/project /绝对路径` 生成待确认的 `project.create` 命令,用于授权并初始化本地项目目录;相对路径不会生成待确认命令;开发窗口仍可直接编辑项目路径。 - 单窗口首页和项目组页可选择、打开、新建或显示当前输入的项目绝对路径;最近项目行也可显示目录,非法或相对路径不会调用系统文件管理器。 - 普通用户侧的生成、上传、运行、自检、预览状态 / 启动 / 打开 / 停止、记忆写入和画板资产导入都必须先完成 `/project` 初始化;未初始化时只提示设置本地项目,不落到默认临时目录。