From abb174965bedf9270c1ec8d81363507e40ef79db Mon Sep 17 00:00:00 2001 From: AIGameCreator App Date: Fri, 10 Jul 2026 00:23:37 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E9=BD=90Agent=20Runtime=E5=8D=8F?= =?UTF-8?q?=E4=BD=9C=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent 后台任务工具箱新增项目黑板写入和 Agent 定向留言。 协作写工具复用 memory.write 和 conversation.write 项目策略。 补充 Runtime 写入成功与策略拦截测试,更新实施计划和决策记录。 --- .../src-tauri/src/agent.rs | 163 ++++++++++++- .../src-tauri/src/tests.rs | 221 ++++++++++++++++++ .../shared-memory/decision-log.md | 2 +- ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 4 +- 4 files changed, 385 insertions(+), 5 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 9be94b00e..920668e0f 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent.rs @@ -753,7 +753,7 @@ fn build_game_creator_agent_background_tool_plan_request( ) -> Result<(GameCreatorLlmConfig, String, LlmRunRequest), String> { let (llm, config_path, context) = build_game_creator_role_agent_context(root, agent_id)?; let prompt = format!( - "项目上下文如下。请先制定短计划,再决定是否调用最多 {AGENT_RUNTIME_BACKGROUND_TOOL_ACTION_LIMIT} 个白名单工具。只输出 JSON 对象,不要 markdown。\n\n{context}\n\n后台任务:\n{task}\n\nJSON schema:{{\"thinkingSummary\":\"一句话理解\",\"plan\":[\"步骤\"],\"actions\":[{{\"tool\":\"memory.read|conversation.read|asset.list|project.index|file.read\",\"reason\":\"为什么需要\",\"input\":{{}}}}],\"response\":\"如果无需工具,可直接给最终回复\"}}\n\n工具输入约定:memory.read 使用 {{\"scope\":\"session|project|blackboard|agent\"}};file.read 使用 {{\"path\":\"项目内相对路径\"}};其他工具 input 可为空。" + "项目上下文如下。请先制定短计划,再决定是否调用最多 {AGENT_RUNTIME_BACKGROUND_TOOL_ACTION_LIMIT} 个白名单工具。只输出 JSON 对象,不要 markdown。\n\n{context}\n\n后台任务:\n{task}\n\nJSON schema:{{\"thinkingSummary\":\"一句话理解\",\"plan\":[\"步骤\"],\"actions\":[{{\"tool\":\"memory.read|conversation.read|asset.list|project.index|file.read|blackboard.write|agent.message\",\"reason\":\"为什么需要\",\"input\":{{}}}}],\"response\":\"如果无需工具,可直接给最终回复\"}}\n\n工具输入约定:memory.read 使用 {{\"scope\":\"session|project|blackboard|agent\"}};file.read 使用 {{\"path\":\"项目内相对路径\"}};blackboard.write 使用 {{\"title\":\"标题\",\"content\":\"要共享给所有 Agent 的稳定结论\"}};agent.message 使用 {{\"agentId\":\"目标 taskId\",\"content\":\"给目标 Agent 的定向消息\"}};其他工具 input 可为空。" ); let request = LlmRunRequest::new(vec![ LlmMessage::system(game_creator_agent_runtime_tool_plan_system_prompt()), @@ -849,6 +849,8 @@ fn execute_game_creator_agent_runtime_tool_action( "asset.list" => observe_agent_runtime_assets(root), "project.index" => observe_agent_runtime_project_index(root), "file.read" => observe_agent_runtime_file(root, &action.input), + "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 { tool: tool.to_string(), status: "rejected".to_string(), @@ -865,6 +867,8 @@ fn game_creator_agent_runtime_tool_command_id(tool: &str) -> Option<&'static str "asset.list" => Some("asset.list"), "project.index" => Some("project.index"), "file.read" => Some("file.read"), + "blackboard.write" => Some("memory.write"), + "agent.message" => Some("conversation.write"), _ => None, } } @@ -970,6 +974,157 @@ fn observe_agent_runtime_file( observation_from_text_result("file.read", result, &format!("已读取 {path}")) } +fn observe_agent_runtime_blackboard_write( + root: &Path, + agent_id: &str, + input: &serde_json::Value, +) -> AgentRuntimeToolObservation { + let content = agent_runtime_tool_input_text(input, &["content", "summary", "message"]); + if content.trim().is_empty() { + return AgentRuntimeToolObservation { + tool: "blackboard.write".to_string(), + status: "failed".to_string(), + summary: "缺少 content".to_string(), + detail: None, + }; + } + let _lock = match acquire_project_write_lock(root, "memory.write") { + Ok(lock) => lock, + Err(error) => { + return AgentRuntimeToolObservation { + tool: "blackboard.write".to_string(), + status: "failed".to_string(), + summary: sanitize_agent_runtime_text(&error, 240), + detail: None, + }; + } + }; + let title_input = agent_runtime_tool_input_text(input, &["title", "topic"]); + let title_text = if title_input.trim().is_empty() { + "共享结论" + } else { + title_input.trim() + }; + let title = sanitize_agent_runtime_text(title_text, 80); + let content = truncate_agent_runtime_text(sanitize_prompt_context(&content).as_str(), 1_200); + let entry = format!("\n\n## Agent {agent_id} - {title}\n\n{content}\n"); + let result = append_markdown_entry( + &root.join(PROJECT_BLACKBOARD_MEMORY_PATH), + "# 项目黑板\n", + &entry, + "写入项目黑板失败", + ) + .and_then(|()| { + append_agent_db_record( + root, + serde_json::json!({ + "recordType": "agent.runtime.blackboard.write", + "agentId": agent_id, + "path": PROJECT_BLACKBOARD_MEMORY_PATH, + "title": title, + }), + ) + }); + match result { + Ok(()) => AgentRuntimeToolObservation { + tool: "blackboard.write".to_string(), + status: "ok".to_string(), + summary: "已追加项目黑板".to_string(), + detail: Some(content), + }, + Err(error) => AgentRuntimeToolObservation { + tool: "blackboard.write".to_string(), + status: "failed".to_string(), + summary: sanitize_agent_runtime_text(&error, 240), + detail: None, + }, + } +} + +fn observe_agent_runtime_agent_message( + root: &Path, + agent_id: &str, + input: &serde_json::Value, +) -> AgentRuntimeToolObservation { + let target_agent_id = agent_runtime_tool_input_text(input, &["agentId", "targetAgentId"]); + let target_agent_id = match normalize_game_creator_runtime_agent_id(target_agent_id.as_str()) { + Ok(target_agent_id) => target_agent_id, + Err(error) => { + return AgentRuntimeToolObservation { + tool: "agent.message".to_string(), + status: "failed".to_string(), + summary: sanitize_agent_runtime_text(&error, 240), + detail: None, + }; + } + }; + let content = agent_runtime_tool_input_text(input, &["content", "message", "summary"]); + if content.trim().is_empty() { + return AgentRuntimeToolObservation { + tool: "agent.message".to_string(), + status: "failed".to_string(), + summary: "缺少 content".to_string(), + detail: None, + }; + } + let _lock = match acquire_project_write_lock(root, "conversation.write") { + Ok(lock) => lock, + Err(error) => { + return AgentRuntimeToolObservation { + tool: "agent.message".to_string(), + status: "failed".to_string(), + summary: sanitize_agent_runtime_text(&error, 240), + detail: None, + }; + } + }; + let content = truncate_agent_runtime_text(sanitize_prompt_context(&content).as_str(), 1_200); + let message = format!("来自 {agent_id} 的定向消息:{content}"); + let result = append_local_conversation_message_at( + root, + Some(&target_agent_id), + LocalConversationMessage { + role: "tool".to_string(), + content: message, + agent_id: None, + }, + ) + .and_then(|conversation| { + append_agent_db_record( + root, + serde_json::json!({ + "recordType": "agent.runtime.agent.message", + "agentId": agent_id, + "targetAgentId": target_agent_id.clone(), + "path": conversation.path, + }), + ) + }); + match result { + Ok(()) => AgentRuntimeToolObservation { + tool: "agent.message".to_string(), + status: "ok".to_string(), + summary: format!("已给 {target_agent_id} 留消息"), + detail: Some(content), + }, + Err(error) => AgentRuntimeToolObservation { + tool: "agent.message".to_string(), + status: "failed".to_string(), + summary: sanitize_agent_runtime_text(&error, 240), + detail: None, + }, + } +} + +fn agent_runtime_tool_input_text(input: &serde_json::Value, keys: &[&str]) -> String { + for key in keys { + if let Some(value) = input.get(*key).and_then(|value| value.as_str()) { + return value.trim().to_string(); + } + } + String::new() +} + fn observation_from_text_result( tool: &str, result: Result, @@ -1234,6 +1389,8 @@ fn default_game_creator_agent_runtime_state(agent_id: &str, run_id: &str) -> Age "conversation.read".to_string(), "conversation.write".to_string(), "memory.read".to_string(), + "blackboard.write".to_string(), + "agent.message".to_string(), "asset.list".to_string(), "file.read".to_string(), "agent.run_status".to_string(), @@ -1281,6 +1438,8 @@ fn normalize_game_creator_agent_runtime_state(state: &mut AgentRuntimeState, age "conversation.read".to_string(), "conversation.write".to_string(), "memory.read".to_string(), + "blackboard.write".to_string(), + "agent.message".to_string(), "asset.list".to_string(), "file.read".to_string(), "agent.run_status".to_string(), @@ -1869,7 +2028,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、conversation.read、asset.list、project.index、file.read。不要假装工具已执行;工具结果会由 Runtime 作为 observation 返回。只输出 JSON 对象,不要 markdown,不要泄露密钥。" + "你是 Genarrative AI 游戏创作多智能体 Runtime 中的专业 Agent。你必须在白名单工具内规划行动:先给一句 thinkingSummary,再给短计划,再决定是否请求工具。只能请求 memory.read、conversation.read、asset.list、project.index、file.read、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 cbcf3de09..14c1b19fc 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests.rs @@ -1576,6 +1576,227 @@ async fn background_agent_runtime_task_executes_plan_tool_observation_loop() { fs::remove_dir_all(root).ok(); } +#[tokio::test] +async fn background_agent_runtime_can_write_blackboard_and_message_other_agent() { + 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(), + }, + ) + .expect("allow runtime collaboration writes"); + let (sender, receiver) = mpsc::channel(); + let plan_json = serde_json::json!({ + "thinkingSummary": "需要把策划结论同步给项目和美术", + "plan": ["沉淀共享结论", "通知美术资产 Agent", "回复开发者"], + "actions": [ + { + "tool": "blackboard.write", + "reason": "这条核心循环会影响所有 Agent", + "input": { + "title": "核心循环确认", + "content": "核心循环:玩家收集月光食材,躲避暗影厨具。" + } + }, + { + "tool": "agent.message", + "reason": "美术资产需要根据核心循环规划角色和道具", + "input": { + "agentId": "art-asset-plan", + "content": "请优先规划月光食材、暗影厨具和主角剪影。" + } + } + ], + "response": "" + }) + .to_string(); + let base_url = spawn_mock_llm_server_responses_with_capture( + vec![ + plan_json, + "已把核心循环写入项目黑板,并给美术资产 Agent 留了后续消息。".to_string(), + ], + 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", + "后台同步核心循环给其他 Agent", + "design-collab-run", + ) + .expect("start background task"); + + let plan_request = receiver + .recv_timeout(Duration::from_secs(2)) + .expect("plan llm request"); + assert!(plan_request.contains("blackboard.write")); + assert!(plan_request.contains("agent.message")); + let final_request = receiver + .recv_timeout(Duration::from_secs(2)) + .expect("final reply llm request"); + assert!(final_request.contains("blackboard.write")); + assert!(final_request.contains("agent.message")); + assert!(final_request.contains("已追加项目黑板")); + assert!(final_request.contains("已给 art-asset-plan 留消息")); + + let mut runtime = read_game_creator_agent_runtime_at(&root, "design-director") + .expect("read runtime") + .state; + for _ in 0..50 { + if runtime.status == "idle" { + break; + } + std::thread::sleep(Duration::from_millis(20)); + runtime = read_game_creator_agent_runtime_at(&root, "design-director") + .expect("read runtime") + .state; + } + + assert_eq!(runtime.status, "idle"); + assert!(runtime + .observations + .iter() + .any(|item| item.contains("blackboard.write:ok · 已追加项目黑板"))); + assert!(runtime + .observations + .iter() + .any(|item| item.contains("agent.message:ok · 已给 art-asset-plan 留消息"))); + let blackboard = + fs::read_to_string(root.join(PROJECT_BLACKBOARD_MEMORY_PATH)).expect("blackboard"); + assert!(blackboard.contains("## Agent design-director - 核心循环确认")); + assert!(blackboard.contains("核心循环:玩家收集月光食材,躲避暗影厨具。")); + let art_conversation = + read_local_conversation_at(&root, Some("art-asset-plan")).expect("art conversation"); + assert!(art_conversation.messages.iter().any(|message| { + message.role == "tool" + && message.content.contains("来自 design-director 的定向消息") + && message + .content + .contains("请优先规划月光食材、暗影厨具和主角剪影。") + })); + let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db"); + assert!(agent_db.contains("\"recordType\":\"agent.runtime.blackboard.write\"")); + assert!(agent_db.contains("\"recordType\":\"agent.runtime.agent.message\"")); + assert!(agent_db.contains("\"targetAgentId\":\"art-asset-plan\"")); + + fs::remove_dir_all(root).ok(); +} + +#[tokio::test] +async fn background_agent_runtime_write_tools_respect_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!["memory.write".to_string(), "conversation.write".to_string()], + }, + ) + .expect("write policy"); + let (sender, receiver) = mpsc::channel(); + let plan_json = serde_json::json!({ + "thinkingSummary": "尝试写协作结论", + "plan": ["写黑板", "通知美术"], + "actions": [ + { + "tool": "blackboard.write", + "reason": "需要同步共享结论", + "input": { + "title": "不应写入", + "content": "这段黑板内容不应该绕过确认策略。" + } + }, + { + "tool": "agent.message", + "reason": "需要通知目标 Agent", + "input": { + "agentId": "art-asset-plan", + "content": "这条定向消息不应该绕过确认策略。" + } + } + ], + "response": "" + }) + .to_string(); + let base_url = spawn_mock_llm_server_responses_with_capture( + vec![plan_json, "写入需要用户确认。".to_string()], + 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-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("项目权限策略要求用户确认:memory.write")); + assert!(final_request.contains("项目权限策略要求用户确认:conversation.write")); + + let mut runtime = read_game_creator_agent_runtime_at(&root, "design-director") + .expect("read runtime") + .state; + for _ in 0..50 { + if runtime.status == "idle" { + break; + } + std::thread::sleep(Duration::from_millis(20)); + runtime = read_game_creator_agent_runtime_at(&root, "design-director") + .expect("read runtime") + .state; + } + assert_eq!(runtime.status, "idle"); + assert!(runtime.observations.iter().any(|item| { + item.contains("blackboard.write:blocked · 项目权限策略要求用户确认:memory.write") + })); + assert!(runtime.observations.iter().any(|item| { + item.contains("agent.message:blocked · 项目权限策略要求用户确认:conversation.write") + })); + let blackboard = fs::read_to_string(root.join(PROJECT_BLACKBOARD_MEMORY_PATH)) + .unwrap_or_else(|_| String::new()); + assert!(!blackboard.contains("这段黑板内容不应该绕过确认策略")); + let art_conversation = + read_local_conversation_at(&root, Some("art-asset-plan")).expect("art conversation"); + assert!(art_conversation.messages.is_empty()); + + fs::remove_dir_all(root).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 cc30d0a9b..2c4f30bd9 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -19,7 +19,7 @@ ## 2026-07-09 AI 游戏创作 App Runtime V1 增加单 Agent 后台任务 - 背景:开发用单 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`,Runtime 按白名单和项目权限策略执行只读工具并记录 `action / observation` 事件,再把观察结果交给 Agent 生成最终回复;完成或失败后把 assistant 回复或错误追加回对话,并写入 `.agent/agent.db` 审计记录。每个 Agent 的任务历史落在 `.agent/runtime/tasks/.jsonl`,读 runtime 时按 `runId` 去重返回最近任务,任务视角状态使用 `pending / running / completed / failed`,UI 在 Runtime 面板展示最近任务。不同 Agent 使用独立 `.agent/runtime/locks/.lock`,允许并行运行;同一 Agent 已有运行任务时,新任务会先进入该 Agent 的 pending 队列,当前 drain 持锁完成后串行继续下一条 pending。该能力仍不是独立 OS 进程或跨重启离线常驻 worker。 +- 决策:在现有 `.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`,Runtime 按白名单和项目权限策略执行工具并记录 `action / observation` 事件,再把观察结果交给 Agent 生成最终回复;完成或失败后把 assistant 回复或错误追加回对话,并写入 `.agent/agent.db` 审计记录。工具箱包含只读工具 `memory.read`、`conversation.read`、`asset.list`、`project.index`、`file.read`,以及受 `memory.write` / `conversation.write` 策略保护的协作写动作 `blackboard.write` 和 `agent.message`;策略要求确认或拒绝时不执行写入,只把策略结果作为 observation 回给 Agent。每个 Agent 的任务历史落在 `.agent/runtime/tasks/.jsonl`,读 runtime 时按 `runId` 去重返回最近任务,任务视角状态使用 `pending / running / completed / failed`,UI 在 Runtime 面板展示最近任务。不同 Agent 使用独立 `.agent/runtime/locks/.lock`,允许并行运行;同一 Agent 已有运行任务时,新任务会先进入该 Agent 的 pending 队列,当前 drain 持锁完成后串行继续下一条 pending。该能力仍不是独立 OS 进程或跨重启离线常驻 worker。 - 补充:规范 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 1a596428a..7d6d88342 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -32,7 +32,7 @@ Agent Runtime 负责: - 开发窗口能力:debug 构建额外打开 `developer` 窗口,走 `index.html?agent-chat`;开发者可选择 Agent、授权本地项目路径,并通过 `read_local_conversation` / `append_local_conversation_message` 读写 `.agent/conversations/agents/.jsonl`,通过 `agentLlm.` 调用该 Agent 的独立 LLM 路由做真实对话,用于单独调试某个 Agent 的长期对话上下文。这里的 `` 以 manifest taskId 为规范值,旧 `group-role` 别名只作为兼容输入映射到 taskId。 - 命令能力:内置命令调用、权限 gate、执行日志;v1 只允许白名单受限命令,不执行任意 shell。 - 编排能力:任务拆分、任务图依赖、专业组调度、多智能体协作;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`,Runtime 按白名单和项目权限策略执行工具动作,写入 `action / 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。 +- 后台任务能力:开发窗口单 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`,Runtime 按白名单和项目权限策略执行工具动作,写入 `action / observation` 事件,再把观察结果交给 Agent 生成最终回复并追加回对话。工具箱包含只读工具 `memory.read`、`conversation.read`、`asset.list`、`project.index`、`file.read`,以及受 `memory.write` / `conversation.write` 策略保护的协作写动作 `blackboard.write` 和 `agent.message`;`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。 - 任务图能力:每轮 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`,不把原始对话混进项目黑板或角色私有记忆。 @@ -252,7 +252,7 @@ game-project/ - Tauri Rust 入口保持薄壳:`src-tauri/src/main.rs` 只保留共享类型 / 常量、模块声明、CLI preflight、`tauri::Builder`、运行时配置初始化和 `invoke_handler` 清单;命令行入口放在 `cli.rs`,Tauri command 包装放在 `commands.rs`,运行时配置与 LLM 配置检查放在 `config.rs`,Agent loop 与生成编排放在 `agent.rs`,上传 / 画板 / 平台美术生成接入放在 `assets.rs`,本地项目文件、记忆、对话、权限、checkpoint、manifest 和通用路径工具放在 `project.rs`,本地 HTTP 预览与 preview 命令放在 `preview.rs`,旧窗口兼容命令放在 `windows.rs`,Rust 单测放在 `tests.rs`。后续继续拆分时保持 Tauri command 名、JSON 字段、`.agent/*` 路径和错误语义不变。 - 本地项目初始化会创建 `game/`、`assets/`、`memory/`、`memory/agents/`、`exports/`、`.agent/logs/`,写入 `.agent/manifest.json`,生成 append-only JSONL 本地项目索引 `.agent/agent.db`,并生成默认 `game/index.html`。 - v1 conversation 记录使用 append-only JSONL,每行带 `schemaVersion`、`role`、`content`、`agentId` 和 `updatedAt`,作为聊天历史和单 agent 对话历史的事实源;目录在首次写入时创建。 -- 开发窗口和项目内 Agent 对话弹窗的“后台运行”只启动或排队单 Agent 后台任务,不阻塞等待回复;用户可刷新同一 Agent 对话或 runtime 状态查看进度和结果。后台任务会向 `.agent/runtime/tasks/.jsonl` 追加任务视角记录,任务状态使用 `pending / running / completed / failed`,读取时按 `runId` 去重返回最近任务;runtime state 自身仍可在完成后显示 `idle / completed`,二者语义分开。同一 Agent 的 pending 任务由持有 `.agent/runtime/locks/.lock` 的后台 drain 串行执行,避免同一 Agent 并发抢上下文;不同 Agent 仍可并行。后台任务完成后会把 assistant 回复追加到对应 `.agent/conversations/agents/.jsonl`,并向 `.agent/agent.db` 写入 `agent.runtime.background_task.queued` / `agent.runtime.background_task` / `agent.runtime.tool_observation` / `agent.runtime.background_task.completed` / `agent.runtime.background_task.failed` 审计记录。当前工具箱只开放只读工具 `memory.read`、`conversation.read`、`asset.list`、`project.index` 和 `file.read`;若项目策略要求确认或拒绝,对应工具不会执行,Runtime 会把策略结果作为 observation 回给 Agent。`.agent/agent.db` 追加写入按整行 JSONL 写入,减少多个 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 仍可并行。后台任务完成后会把 assistant 回复追加到对应 `.agent/conversations/agents/.jsonl`,并向 `.agent/agent.db` 写入 `agent.runtime.background_task.queued` / `agent.runtime.background_task` / `agent.runtime.tool_observation` / `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`,以及受策略保护的协作写工具 `blackboard.write` 和 `agent.message`;若项目策略要求确认或拒绝,对应工具不会执行,Runtime 会把策略结果作为 observation 回给 Agent。`.agent/agent.db` 追加写入按整行 JSONL 写入,减少多个 Agent 同时完成时的行交错风险。 - 普通用户可在聊天框输入 `/project /绝对路径` 生成待确认的 `project.create` 命令,用于授权并初始化本地项目目录;相对路径不会生成待确认命令;开发窗口仍可直接编辑项目路径。 - 单窗口首页和项目组页可选择、打开、新建或显示当前输入的项目绝对路径;最近项目行也可显示目录,非法或相对路径不会调用系统文件管理器。 - 普通用户侧的生成、上传、运行、自检、预览状态 / 启动 / 打开 / 停止、记忆写入和画板资产导入都必须先完成 `/project` 初始化;未初始化时只提示设置本地项目,不落到默认临时目录。