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 fc5ab42c2..490e309d3 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent.rs @@ -797,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|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 可为空。" + "项目上下文如下。你正在执行后台 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|task.update|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\":\"文件内容\"}};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 的定向消息\"}};如果已有观察足够,请返回空 actions 并填写 response。其他工具 input 可为空。" ); let request = LlmRunRequest::new(vec![ LlmMessage::system(game_creator_agent_runtime_tool_plan_system_prompt()), @@ -896,6 +896,7 @@ async fn execute_game_creator_agent_runtime_tool_action( "project.index" => observe_agent_runtime_project_index(root), "file.read" => observe_agent_runtime_file(root, &action.input), "file.write" => observe_agent_runtime_file_write(root, agent_id, &action.input), + "task.update" => observe_agent_runtime_task_update(root, agent_id, &action.input), "command.run_limited" => { observe_agent_runtime_limited_command(root, agent_id, &action.input) } @@ -924,6 +925,7 @@ fn game_creator_agent_runtime_tool_command_id(tool: &str) -> Option<&'static str "project.index" => Some("project.index"), "file.read" => Some("file.read"), "file.write" => Some("file.write"), + "task.update" => Some("task.update"), "command.run_limited" => Some("command.run_limited"), "preview.start" => Some("preview.start"), "canvas.asset_generate" => Some("canvas.asset_generate"), @@ -1224,6 +1226,117 @@ fn observe_agent_runtime_file_write( } } +fn observe_agent_runtime_task_update( + root: &Path, + agent_id: &str, + input: &serde_json::Value, +) -> AgentRuntimeToolObservation { + let task_id = agent_runtime_tool_input_text(input, &["taskId", "task_id", "id"]); + if task_id.trim().is_empty() { + return AgentRuntimeToolObservation { + tool: "task.update".to_string(), + status: "failed".to_string(), + summary: "缺少 taskId".to_string(), + detail: None, + }; + } + let status_input = agent_runtime_tool_input_text(input, &["status", "state"]); + let status = match parse_agent_runtime_task_status(status_input.as_str()) { + Ok(status) => status, + Err(error) => { + return AgentRuntimeToolObservation { + tool: "task.update".to_string(), + status: "failed".to_string(), + summary: error, + detail: None, + }; + } + }; + let status_label = agent_runtime_task_status_label(&status); + let _lock = match acquire_project_write_lock(root, "task.update") { + Ok(lock) => lock, + Err(error) => { + return AgentRuntimeToolObservation { + tool: "task.update".to_string(), + status: "failed".to_string(), + summary: redact_agent_runtime_project_paths(root, &error, 240), + detail: None, + }; + } + }; + let result = update_manifest_task_status_at(root, task_id.as_str(), status).and_then(|task| { + append_agent_db_record( + root, + serde_json::json!({ + "recordType": "agent.runtime.task.update", + "agentId": agent_id, + "taskId": task.id.clone(), + "status": status_label, + "title": task.title.clone(), + "group": agent_runtime_task_group_label(&task.group), + "role": task.role.clone(), + }), + ) + .map(|()| task) + }); + match result { + Ok(task) => AgentRuntimeToolObservation { + tool: "task.update".to_string(), + status: "ok".to_string(), + summary: format!("任务 {} 已更新为 {}", task.id, status_label), + detail: Some(format!( + "taskId={}, title={}, group={}, role={}, status={}", + task.id, + task.title, + agent_runtime_task_group_label(&task.group), + task.role, + status_label + )), + }, + Err(error) => AgentRuntimeToolObservation { + tool: "task.update".to_string(), + status: "failed".to_string(), + summary: redact_agent_runtime_project_paths(root, &error, 240), + detail: None, + }, + } +} + +fn parse_agent_runtime_task_status(value: &str) -> Result { + match value.trim() { + "pending" => Ok(GameCreationAppTaskStatus::Pending), + "running" => Ok(GameCreationAppTaskStatus::Running), + "waiting-for-confirmation" | "waiting_for_confirmation" => { + Ok(GameCreationAppTaskStatus::WaitingForConfirmation) + } + "completed" => Ok(GameCreationAppTaskStatus::Completed), + "failed" => Ok(GameCreationAppTaskStatus::Failed), + "" => Err("缺少 status".to_string()), + status => Err(format!("不支持的任务状态:{status}")), + } +} + +fn agent_runtime_task_status_label(status: &GameCreationAppTaskStatus) -> &'static str { + match status { + GameCreationAppTaskStatus::Pending => "pending", + GameCreationAppTaskStatus::Running => "running", + GameCreationAppTaskStatus::WaitingForConfirmation => "waiting-for-confirmation", + GameCreationAppTaskStatus::Completed => "completed", + GameCreationAppTaskStatus::Failed => "failed", + } +} + +fn agent_runtime_task_group_label(group: &GameCreationAppAgentGroup) -> &'static str { + match group { + GameCreationAppAgentGroup::Design => "design", + GameCreationAppAgentGroup::Art => "art", + GameCreationAppAgentGroup::Code => "code", + GameCreationAppAgentGroup::Balance => "balance", + GameCreationAppAgentGroup::Audio => "audio", + GameCreationAppAgentGroup::Publishing => "publishing", + } +} + fn observe_agent_runtime_limited_command( root: &Path, agent_id: &str, @@ -1831,27 +1944,35 @@ fn default_game_creator_agent_runtime_state(agent_id: &str, run_id: &str) -> Age "回复并记录 runtime 事件".to_string(), ], observations: Vec::new(), - allowed_tools: vec![ - "conversation.read".to_string(), - "conversation.write".to_string(), - "memory.read".to_string(), - "memory.write".to_string(), - "blackboard.write".to_string(), - "agent.message".to_string(), - "asset.list".to_string(), - "file.read".to_string(), - "file.write".to_string(), - "command.run_limited".to_string(), - "preview.start".to_string(), - "canvas.asset_generate".to_string(), - "agent.run_status".to_string(), - ], + allowed_tools: default_game_creator_agent_runtime_allowed_tools(), last_response: None, error: None, updated_at: unix_timestamp(), } } +fn default_game_creator_agent_runtime_allowed_tools() -> Vec { + [ + "conversation.read", + "conversation.write", + "memory.read", + "memory.write", + "blackboard.write", + "agent.message", + "asset.list", + "file.read", + "file.write", + "task.update", + "command.run_limited", + "preview.start", + "canvas.asset_generate", + "agent.run_status", + ] + .into_iter() + .map(str::to_string) + .collect() +} + fn normalize_game_creator_agent_runtime_state(state: &mut AgentRuntimeState, agent_id: &str) { if state.schema_version.trim().is_empty() { state.schema_version = AGENT_RUNTIME_SCHEMA_VERSION.to_string(); @@ -1888,21 +2009,13 @@ fn normalize_game_creator_agent_runtime_state(state: &mut AgentRuntimeState, age ]; } if state.allowed_tools.is_empty() { - state.allowed_tools = vec![ - "conversation.read".to_string(), - "conversation.write".to_string(), - "memory.read".to_string(), - "memory.write".to_string(), - "blackboard.write".to_string(), - "agent.message".to_string(), - "asset.list".to_string(), - "file.read".to_string(), - "file.write".to_string(), - "command.run_limited".to_string(), - "preview.start".to_string(), - "canvas.asset_generate".to_string(), - "agent.run_status".to_string(), - ]; + state.allowed_tools = default_game_creator_agent_runtime_allowed_tools(); + } else { + for tool in default_game_creator_agent_runtime_allowed_tools() { + if !state.allowed_tools.iter().any(|existing| existing == &tool) { + state.allowed_tools.push(tool); + } + } } if state.updated_at == 0 { state.updated_at = unix_timestamp(); @@ -2513,7 +2626,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、canvas.asset_generate、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、task.update、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/project.rs b/apps/ai-game-creator-shell/src-tauri/src/project.rs index c3ff69d3d..47327794e 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project.rs @@ -1581,6 +1581,26 @@ pub(crate) fn set_task_status( } } +pub(crate) fn update_manifest_task_status_at( + root: &Path, + task_id: &str, + status: GameCreationAppTaskStatus, +) -> Result { + let task_id = task_id.trim(); + if task_id.is_empty() { + return Err("任务 ID 不能为空".to_string()); + } + let (manifest_path, mut manifest) = read_or_create_manifest(root)?; + ensure_manifest_seed_tasks(&mut manifest); + let Some(task) = manifest.tasks.iter_mut().find(|task| task.id == task_id) else { + return Err(format!("项目任务不存在:{task_id}")); + }; + task.status = status; + let updated = task.clone(); + write_manifest(&manifest_path, &manifest)?; + Ok(updated) +} + pub(crate) fn read_or_create_manifest( root: &Path, ) -> Result<(PathBuf, GameCreationAppManifest), 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 bddb36db6..d16b2169e 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests.rs @@ -2164,6 +2164,190 @@ async fn background_agent_runtime_file_and_memory_writes_respect_project_policy( fs::remove_dir_all(root).ok(); } +#[tokio::test] +async fn background_agent_runtime_can_update_manifest_task_status() { + 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 task update"); + let manifest_before: Value = + serde_json::from_str(&fs::read_to_string(root.join(".agent/manifest.json")).unwrap()) + .expect("manifest json"); + assert_task_status(&manifest_before, "art-asset-plan", "pending"); + + let (sender, receiver) = mpsc::channel(); + let plan_json = serde_json::json!({ + "thinkingSummary": "美术资产计划已经完成,需要更新任务状态", + "plan": ["标记美术资产计划完成", "回报开发者"], + "actions": [ + { + "tool": "task.update", + "reason": "让 manifest 任务图反映当前 Agent 进度", + "input": { "taskId": "art-asset-plan", "status": "completed" } + } + ], + "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": {{ + "art-director": {{ + "apiKey": "art-key", + "baseUrl": {base_url:?}, + "model": "art-runtime-model", + "apiKind": "openai_responses" + }} + }} +}}"# + )); + + start_game_creator_agent_background_task_at( + &root, + "art-director", + "后台整理美术任务状态", + "art-task-update-run", + ) + .expect("start background task"); + + let plan_request = receiver + .recv_timeout(Duration::from_secs(2)) + .expect("plan llm request"); + assert!(plan_request.contains("task.update")); + assert!(plan_request.contains("waiting-for-confirmation")); + let final_request = receiver + .recv_timeout(Duration::from_secs(2)) + .expect("final reply llm request"); + assert!(final_request.contains("task.update")); + assert!(final_request.contains("任务 art-asset-plan 已更新为 completed")); + + let mut runtime = read_game_creator_agent_runtime_at(&root, "art-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, "art-director") + .expect("read runtime") + .state; + } + assert_eq!(runtime.status, "idle"); + assert!(runtime + .observations + .iter() + .any(|item| item.contains("task.update:ok · 任务 art-asset-plan 已更新为 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, "art-asset-plan", "completed"); + let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db"); + assert!(agent_db.contains("\"recordType\":\"agent.runtime.task.update\"")); + assert!(agent_db.contains("\"agentId\":\"art-director\"")); + assert!(agent_db.contains("\"taskId\":\"art-asset-plan\"")); + assert!(agent_db.contains("\"status\":\"completed\"")); + + fs::remove_dir_all(root).ok(); +} + +#[tokio::test] +async fn background_agent_runtime_task_update_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!["task.update".to_string()], + }, + ) + .expect("write policy"); + let (sender, receiver) = mpsc::channel(); + let plan_json = serde_json::json!({ + "thinkingSummary": "尝试更新任务状态", + "plan": ["标记任务完成"], + "actions": [ + { + "tool": "task.update", + "reason": "测试策略拦截任务状态更新", + "input": { "taskId": "art-asset-plan", "status": "completed" } + } + ], + "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": {{ + "art-director": {{ + "apiKey": "art-key", + "baseUrl": {base_url:?}, + "model": "art-runtime-model", + "apiKind": "openai_responses" + }} + }} +}}"# + )); + + start_game_creator_agent_background_task_at( + &root, + "art-director", + "后台尝试更新美术任务状态", + "art-task-update-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("项目权限策略要求用户确认:task.update")); + assert!(!final_request.contains("任务 art-asset-plan 已更新为 completed")); + + let mut runtime = read_game_creator_agent_runtime_at(&root, "art-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, "art-director") + .expect("read runtime") + .state; + } + assert_eq!(runtime.status, "idle"); + assert!(runtime + .observations + .iter() + .any(|item| item.contains("task.update:blocked · 项目权限策略要求用户确认:task.update"))); + 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, "art-asset-plan", "pending"); + let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db"); + assert!(!agent_db.contains("\"recordType\":\"agent.runtime.task.update\"")); + + fs::remove_dir_all(root).ok(); +} + #[tokio::test] async fn background_agent_runtime_can_run_limited_static_smoke() { let root = unique_project_path(); diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index 11d63db2a..292d004c7 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -10499,6 +10499,7 @@ function isProjectPolicyConfirmableCommandId(value: string) { 'asset.register', 'asset.list', 'task.list', + 'task.update', 'agent.run_status', 'agent.kill', 'agent.retry', diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 5b7d706f3..f37b6f2ba 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -4053,6 +4053,7 @@ - 2026-07-01 调整:AI 游戏创作 App 在 `memory/session.md` 与 `memory/project.md` 之外新增项目级黑板 `memory/blackboard.md`,只记录重要跨 agent 决策、依赖和风险摘要;每个角色 agent 拥有私有记忆 `memory/agents//.md`。角色 brief 必须读取自己的私有记忆和项目黑板;`game.generate_draft` 通过 Evaluator 与 `game.static_smoke` 后,追加项目黑板摘要和各角色成功产出摘要,不得覆盖既有记忆。失败 run 仍只保留 trace 和 pass 快照,不写最终记忆摘要。 - 2026-07-06 调整:AI 游戏创作 App 主聊天普通文本改为进入主聊天 Agent,而不是直接排队 `game.generate_draft`;主聊天 Agent 读取短期记忆、长期记忆、项目黑板、最近项目对话和本地资产摘要作为背景,支持 `agentLlm.chat` 单独 provider 配置,但只做自然语言交互、澄清和 slash 命令建议,不写项目、不运行工具、不伪装生成结果。显式 `/generate <创作想法>` 或 `/draft <创作想法>` 才进入 `game.generate_draft` 待确认流。 - 2026-07-09 调整:AI 游戏创作 App 新增 Agent Runtime V1 最小可观测状态。单 Agent 对话和生成 loop 中的角色 brief 必须写 `.agent/runtime/agents/.json` 与 `.agent/runtime/events/.jsonl`,记录 `agentId`、`taskId`、`sessionId`、`runId`、`source`、`status`、`phase`、当前任务 / 动作、计划、观测、允许工具、最近回复和错误;单 Agent 流式聊天事件要回传最新 `runtimeState`,开发单 Agent 聊天页和项目内单 Agent 对话弹窗只读展示该状态,读取失败必须可见提示,不得静默伪装为空状态。`source=agent-chat` 表示开发者单 Agent 对话,`source=generate-draft` 表示生成 loop 角色 brief;carry-over brief 只记录继承和完成,不伪装成重新调用 LLM。Runtime state 写入使用临时文件替换,event JSONL 读取跳过坏行,用户 prompt / 回复摘要进入 runtime 与 `agent.db` 前复用敏感上下文过滤;`.agent/runtime/` 是运行观测状态,不进入项目索引、checkpoint diff 或 restore 删除范围。该层仍是本地 JSONL 状态与事件,不引入 SQLite、常驻独立进程、远程 runner 或可中断上游 LLM 的承诺。 +- 2026-07-10 调整:Agent Runtime V1 后台工具箱新增受策略保护的 `task.update`。Agent 只能把 `.agent/manifest.json` 中已有 seed task 的状态更新为 `pending`、`running`、`waiting-for-confirmation`、`completed` 或 `failed`,Runtime 必须复用项目写锁、`task.update` 权限策略和 `.agent/agent.db` 审计记录;策略要求确认或拒绝时不得修改 manifest,不得创建新任务。 - 2026-07-01 调整:AI 游戏创作 App 借鉴 Godcoder 的本地工程护栏,但只收敛到五项本地机制:`ArtifactWriter` 写入前 checkpoint、写入后 diff、用户确认 restore;进入 LLM 前过滤密钥和本机配置痕迹;`.agent/agent.db` 继续作为轻量 JSONL 项目索引,`/index` 额外刷新 `.agent/project.index.json`;同一项目写入通过 `.agent/project.lock` 串行化;`.agent/policy.json` 记录项目级命令拒绝 / 确认策略。v1 不引入通用 IDE 插件、云工作区、SQLite 或任意 shell 代理。 - 2026-07-03 调整:主窗口最近 checkpoint 列表必须直接展示 checkpoint id、文件数、大小和创建时间,并提供直接对比、填入 `/diff`、确认回滚和填入 `/restore` 的轻量操作;回滚仍走 `project.restore` 确认卡,不在列表按钮中直接写项目文件。 - 2026-06-24 调整:普通用户通过聊天输入 `/help` 发现可用内置命令;命令发现必须留在聊天消息里,不得因此暴露开发面板。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index c67549af5..456576954 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -35,6 +35,7 @@ Agent Runtime 负责: - 后台任务能力:开发窗口单 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。 +- 2026-07-10 补充:后台任务工具箱已加入 `task.update`。Agent 可在 loop 中把 manifest 种子任务状态更新为 `pending / running / waiting-for-confirmation / completed / failed`,用于表达长期后台任务的当前进度;Runtime 复用 `task.update` 策略和项目写锁,实际只修改 `.agent/manifest.json` 中已有 taskId 的 `status`,并写入 `agent.runtime.task.update` 审计记录。策略要求确认或拒绝时不会修改 manifest,也不会创建新任务。 - 任务图能力:每轮 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`,不把原始对话混进项目黑板或角色私有记忆。 @@ -257,6 +258,7 @@ game-project/ - 开发窗口和项目内 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 生成并回流素材,不暴露任意上传 / 任意网络请求能力。 +- 2026-07-10 补充:当前工具箱还开放 `task.update`,审计记录类型为 `agent.runtime.task.update`;该工具只允许更新 manifest 中已有任务的状态,并受 `task.update` 项目权限策略保护。 - 普通用户可在聊天框输入 `/project /绝对路径` 生成待确认的 `project.create` 命令,用于授权并初始化本地项目目录;相对路径不会生成待确认命令;开发窗口仍可直接编辑项目路径。 - 单窗口首页和项目组页可选择、打开、新建或显示当前输入的项目绝对路径;最近项目行也可显示目录,非法或相对路径不会调用系统文件管理器。 - 普通用户侧的生成、上传、运行、自检、预览状态 / 启动 / 打开 / 停止、记忆写入和画板资产导入都必须先完成 `/project` 初始化;未初始化时只提示设置本地项目,不落到默认临时目录。 diff --git a/packages/shared/src/contracts/gameCreationApp.ts b/packages/shared/src/contracts/gameCreationApp.ts index 21297948f..6ebf9dad1 100644 --- a/packages/shared/src/contracts/gameCreationApp.ts +++ b/packages/shared/src/contracts/gameCreationApp.ts @@ -25,6 +25,7 @@ export const GAME_CREATION_APP_COMMANDS = [ { id: 'project.policy_read', permission: 'auto' }, { id: 'project.policy_write', permission: 'confirm' }, { id: 'task.list', permission: 'auto' }, + { id: 'task.update', permission: 'confirm' }, { id: 'agent.trace_read', permission: 'auto' }, { id: 'agent.run_status', permission: 'auto' }, { id: 'agent.kill', permission: 'confirm' }, diff --git a/server-rs/crates/shared-contracts/src/game_creation_app.rs b/server-rs/crates/shared-contracts/src/game_creation_app.rs index ea31c7f5b..1aba7814d 100644 --- a/server-rs/crates/shared-contracts/src/game_creation_app.rs +++ b/server-rs/crates/shared-contracts/src/game_creation_app.rs @@ -21,7 +21,7 @@ pub struct GameCreationAppCommandDescriptor { pub permission: GameCreationAppPermission, } -pub const GAME_CREATION_APP_COMMANDS: [GameCreationAppCommandDescriptor; 44] = [ +pub const GAME_CREATION_APP_COMMANDS: [GameCreationAppCommandDescriptor; 45] = [ command("help.show", GameCreationAppPermission::Auto), command("project.create", GameCreationAppPermission::Confirm), command("project.status", GameCreationAppPermission::Auto), @@ -34,6 +34,7 @@ pub const GAME_CREATION_APP_COMMANDS: [GameCreationAppCommandDescriptor; 44] = [ command("project.policy_read", GameCreationAppPermission::Auto), command("project.policy_write", GameCreationAppPermission::Confirm), command("task.list", GameCreationAppPermission::Auto), + command("task.update", GameCreationAppPermission::Confirm), command("agent.trace_read", GameCreationAppPermission::Auto), command("agent.run_status", GameCreationAppPermission::Auto), command("agent.kill", GameCreationAppPermission::Confirm),