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 fe6c63c4b..10017dd9e 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent.rs @@ -1500,7 +1500,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.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.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.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 可为空。" ); let request = LlmRunRequest::new(vec![ LlmMessage::system(game_creator_agent_runtime_tool_plan_system_prompt()), @@ -1611,6 +1611,7 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action( "file.read" => observe_agent_runtime_file(root, &action.input), "file.write" => observe_agent_runtime_file_write(root, agent_id, &action.input), "task.list" => observe_agent_runtime_task_list(root), + "task.create" => observe_agent_runtime_task_create(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) @@ -1645,6 +1646,7 @@ fn game_creator_agent_runtime_tool_command_id(tool: &str) -> Option<&'static str "file.read" => Some("file.read"), "file.write" => Some("file.write"), "task.list" => Some("task.list"), + "task.create" => Some("task.create"), "task.update" => Some("task.update"), "command.run_limited" => Some("command.run_limited"), "preview.start" => Some("preview.start"), @@ -1764,6 +1766,7 @@ fn agent_runtime_executable_tools() -> Vec<&'static str> { "file.read", "file.write", "task.list", + "task.create", "task.update", "command.run_limited", "preview.start", @@ -2412,6 +2415,139 @@ fn observe_agent_runtime_file_write( } } +fn observe_agent_runtime_task_create( + root: &Path, + agent_id: &str, + input: &serde_json::Value, +) -> AgentRuntimeToolObservation { + let task_id = agent_runtime_tool_input_text(input, &["taskId", "task_id", "id"]); + let title = agent_runtime_tool_input_text(input, &["title", "name"]); + if title.trim().is_empty() { + return AgentRuntimeToolObservation { + tool: "task.create".to_string(), + status: "failed".to_string(), + summary: "缺少 title".to_string(), + detail: None, + }; + } + let group_input = agent_runtime_tool_input_text(input, &["group", "area"]); + let group = if group_input.trim().is_empty() { + game_creator_agent_role_definition(agent_id) + .map(|(group, _role)| group.id) + .and_then(agent_runtime_task_group_from_label) + .unwrap_or(GameCreationAppAgentGroup::Design) + } else { + match agent_runtime_task_group_from_label(&group_input) { + Some(group) => group, + None => { + return AgentRuntimeToolObservation { + tool: "task.create".to_string(), + status: "failed".to_string(), + summary: format!("不支持的任务分组:{group_input}"), + detail: None, + }; + } + } + }; + let role = { + let role_input = agent_runtime_tool_input_text(input, &["role", "ownerRole"]); + if role_input.trim().is_empty() { + game_creator_agent_role_definition(agent_id) + .map(|(_group, role)| role.role.to_string()) + .unwrap_or_else(|| "Agent".to_string()) + } else { + role_input + } + }; + let status_input = agent_runtime_tool_input_text(input, &["status", "state"]); + let status = if status_input.trim().is_empty() { + GameCreationAppTaskStatus::Pending + } else { + match parse_agent_runtime_task_status(status_input.as_str()) { + Ok(status) => status, + Err(error) => { + return AgentRuntimeToolObservation { + tool: "task.create".to_string(), + status: "failed".to_string(), + summary: error, + detail: None, + }; + } + } + }; + let dependencies = + agent_runtime_tool_input_string_list(input, &["dependencies", "deps", "dependsOn"]); + let artifacts = agent_runtime_tool_input_string_list(input, &["artifacts", "outputs"]); + let acceptance_criteria = agent_runtime_tool_input_string_list( + input, + &["acceptanceCriteria", "acceptance", "criteria"], + ); + let _lock = match acquire_project_write_lock(root, "task.create") { + Ok(lock) => lock, + Err(error) => { + return AgentRuntimeToolObservation { + tool: "task.create".to_string(), + status: "failed".to_string(), + summary: redact_agent_runtime_project_paths(root, &error, 240), + detail: None, + }; + } + }; + let status_label = agent_runtime_task_status_label(&status); + let result = create_manifest_task_at( + root, + task_id.as_str(), + title.as_str(), + group, + role.as_str(), + status, + dependencies, + artifacts, + acceptance_criteria, + ) + .and_then(|task| { + append_agent_db_record( + root, + serde_json::json!({ + "recordType": "agent.runtime.task.create", + "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(), + "dependencies": task.dependencies.clone(), + }), + ) + .map(|()| task) + }); + match result { + Ok(task) => AgentRuntimeToolObservation { + tool: "task.create".to_string(), + status: "ok".to_string(), + summary: format!("已创建任务 {}:{}", task.id, task.title), + detail: Some(format!( + "taskId={}, group={}, role={}, status={}, deps={}", + task.id, + agent_runtime_task_group_label(&task.group), + task.role, + status_label, + if task.dependencies.is_empty() { + "-".to_string() + } else { + task.dependencies.join(", ") + } + )), + }, + Err(error) => AgentRuntimeToolObservation { + tool: "task.create".to_string(), + status: "failed".to_string(), + summary: redact_agent_runtime_project_paths(root, &error, 240), + detail: None, + }, + } +} + fn observe_agent_runtime_task_update( root: &Path, agent_id: &str, @@ -2523,6 +2659,18 @@ fn agent_runtime_task_group_label(group: &GameCreationAppAgentGroup) -> &'static } } +fn agent_runtime_task_group_from_label(value: &str) -> Option { + match value.trim().to_ascii_lowercase().as_str() { + "design" => Some(GameCreationAppAgentGroup::Design), + "art" => Some(GameCreationAppAgentGroup::Art), + "code" => Some(GameCreationAppAgentGroup::Code), + "balance" => Some(GameCreationAppAgentGroup::Balance), + "audio" => Some(GameCreationAppAgentGroup::Audio), + "publishing" | "publish" => Some(GameCreationAppAgentGroup::Publishing), + _ => None, + } +} + fn observe_agent_runtime_limited_command( root: &Path, agent_id: &str, @@ -3130,6 +3278,32 @@ fn agent_runtime_tool_input_text(input: &serde_json::Value, keys: &[&str]) -> St String::new() } +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 { + continue; + }; + if let Some(items) = value.as_array() { + return items + .iter() + .filter_map(|item| item.as_str()) + .map(str::trim) + .filter(|item| !item.is_empty()) + .map(str::to_string) + .collect(); + } + if let Some(item) = value.as_str() { + return item + .split(',') + .map(str::trim) + .filter(|item| !item.is_empty()) + .map(str::to_string) + .collect(); + } + } + Vec::new() +} + fn agent_runtime_memory_write_entry(agent_id: &str, title: &str, content: &str) -> String { let title = if title.trim().is_empty() { "运行结论".to_string() @@ -4719,7 +4893,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.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.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/project.rs b/apps/ai-game-creator-shell/src-tauri/src/project.rs index c29e32712..195bea4a6 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project.rs @@ -1662,6 +1662,154 @@ pub(crate) fn update_manifest_task_status_at( Ok(updated) } +pub(crate) fn create_manifest_task_at( + root: &Path, + task_id: &str, + title: &str, + group: GameCreationAppAgentGroup, + role: &str, + status: GameCreationAppTaskStatus, + dependencies: Vec, + artifacts: Vec, + acceptance_criteria: Vec, +) -> Result { + let (manifest_path, mut manifest) = read_or_create_manifest(root)?; + ensure_manifest_seed_tasks(&mut manifest); + let fallback_id = format!( + "agent-task-{}-{}", + unix_timestamp(), + manifest.tasks.len() + 1 + ); + let task_id = normalize_manifest_task_id(task_id, &fallback_id)?; + if manifest.tasks.iter().any(|task| task.id == task_id) { + return Err(format!("项目任务已存在:{task_id}")); + } + let title = normalize_manifest_task_text(title, "任务标题", 120)?; + let role = normalize_manifest_task_text(role, "任务角色", 64)?; + let known_task_ids = manifest + .tasks + .iter() + .map(|task| task.id.clone()) + .collect::>(); + let dependencies = normalize_manifest_task_id_list(dependencies, "依赖任务", 8)?; + for dependency in &dependencies { + if dependency == &task_id { + return Err("任务不能依赖自己".to_string()); + } + if !known_task_ids.iter().any(|known| known == dependency) { + return Err(format!("依赖任务不存在:{dependency}")); + } + } + let artifacts = normalize_manifest_task_text_list(artifacts, "任务产物", 8, 160)?; + let acceptance_criteria = + normalize_manifest_task_text_list(acceptance_criteria, "验收标准", 8, 180)?; + let task = GameCreationAppTaskState { + id: task_id, + title, + group, + role, + status, + dependencies, + artifacts, + acceptance_criteria, + }; + manifest.tasks.push(task.clone()); + write_manifest(&manifest_path, &manifest)?; + Ok(task) +} + +fn normalize_manifest_task_id(value: &str, fallback: &str) -> Result { + let value = value.trim(); + let source = if value.is_empty() { + fallback.trim() + } else { + value + }; + let normalized = source + .to_ascii_lowercase() + .chars() + .map(|character| { + if character.is_ascii_alphanumeric() || character == '-' || character == '_' { + character + } else { + '-' + } + }) + .collect::(); + let normalized = normalized + .split('-') + .filter(|part| !part.is_empty()) + .collect::>() + .join("-"); + if normalized.is_empty() { + return Err("任务 ID 只能包含 ASCII 字母、数字、短横线和下划线".to_string()); + } + Ok(normalized.chars().take(96).collect()) +} + +fn normalize_manifest_task_text( + value: &str, + label: &str, + max_chars: usize, +) -> Result { + let value = value.trim(); + if value.is_empty() { + return Err(format!("{label}不能为空")); + } + if value.chars().any(char::is_control) { + return Err(format!("{label}不能包含控制字符")); + } + Ok(value.chars().take(max_chars).collect()) +} + +fn normalize_manifest_task_text_list( + values: Vec, + label: &str, + max_items: usize, + max_chars: usize, +) -> Result, String> { + let mut output = Vec::new(); + for value in values { + let value = value.trim(); + if value.is_empty() { + continue; + } + if value.chars().any(char::is_control) { + return Err(format!("{label}不能包含控制字符")); + } + let item = value.chars().take(max_chars).collect::(); + if !output.iter().any(|existing| existing == &item) { + output.push(item); + } + if output.len() > max_items { + return Err(format!("{label}最多支持 {max_items} 项")); + } + } + Ok(output) +} + +fn normalize_manifest_task_id_list( + values: Vec, + label: &str, + max_items: usize, +) -> Result, String> { + let mut output = Vec::new(); + for value in values { + let value = value.trim(); + if value.is_empty() { + continue; + } + let item = normalize_manifest_task_id(value, "")?; + if !output.iter().any(|existing| existing == &item) { + output.push(item); + } + if output.len() > max_items { + return Err(format!("{label}最多支持 {max_items} 项")); + } + } + Ok(output) +} + 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 67b21efe0..e82b82b85 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests.rs @@ -3555,6 +3555,201 @@ async fn background_agent_runtime_task_list_respects_project_policy() { fs::remove_dir_all(root).ok(); } +#[tokio::test] +async fn background_agent_runtime_can_create_manifest_task() { + 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 task create"); + let manifest_before: Value = + serde_json::from_str(&fs::read_to_string(root.join(".agent/manifest.json")).unwrap()) + .expect("manifest json"); + assert!(manifest_before["tasks"] + .as_array() + .expect("tasks") + .iter() + .all(|task| task["id"] != "design-extra-brief")); + + let (sender, receiver) = mpsc::channel(); + let plan_json = serde_json::json!({ + "thinkingSummary": "需要拆出补充设定任务", + "plan": ["创建补充设定任务", "回报开发者"], + "actions": [ + { + "tool": "task.create", + "reason": "让后续 Agent 有独立任务跟踪补充设定", + "input": { + "taskId": "design-extra-brief", + "title": "补充角色设定卡", + "group": "design", + "role": "Gameplay", + "dependencies": ["design-director"], + "artifacts": ["memory/character-sheet.md"], + "acceptanceCriteria": ["角色外观、性格、服装和世界观明确"], + "status": "pending" + } + } + ], + "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-task-create-run", + ) + .expect("start background task"); + + let plan_request = receiver + .recv_timeout(Duration::from_secs(2)) + .expect("plan llm request"); + assert!(plan_request.contains("task.create")); + let final_request = receiver + .recv_timeout(Duration::from_secs(2)) + .expect("final reply llm request"); + assert!(final_request.contains("task.create")); + assert!(final_request.contains("已创建任务 design-extra-brief")); + + let runtime = wait_for_agent_runtime_idle(&root, "design-director"); + assert_eq!(runtime.status, "idle"); + assert!(runtime + .observations + .iter() + .any(|item| item.contains("task.create:ok · 已创建任务 design-extra-brief"))); + let manifest_after: Value = + serde_json::from_str(&fs::read_to_string(root.join(".agent/manifest.json")).unwrap()) + .expect("manifest json"); + let created = manifest_after["tasks"] + .as_array() + .expect("tasks") + .iter() + .find(|task| task["id"] == "design-extra-brief") + .expect("created task"); + assert_eq!(created["title"], "补充角色设定卡"); + assert_eq!(created["group"], "design"); + assert_eq!(created["dependencies"][0], "design-director"); + assert_eq!(created["artifacts"][0], "memory/character-sheet.md"); + assert_eq!( + created["acceptanceCriteria"][0], + "角色外观、性格、服装和世界观明确" + ); + let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db"); + assert!(agent_db.contains("\"recordType\":\"agent.runtime.task.create\"")); + assert!(agent_db.contains("\"agentId\":\"design-director\"")); + assert!(agent_db.contains("\"taskId\":\"design-extra-brief\"")); + + fs::remove_dir_all(root).ok(); +} + +#[tokio::test] +async fn background_agent_runtime_task_create_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.create".to_string()], + agent_policies: BTreeMap::new(), + }, + ) + .expect("write policy"); + let (sender, receiver) = mpsc::channel(); + let plan_json = serde_json::json!({ + "thinkingSummary": "尝试创建任务", + "plan": ["创建补充设定任务"], + "actions": [ + { + "tool": "task.create", + "reason": "测试策略拦截任务创建", + "input": { + "taskId": "design-blocked-task", + "title": "不应创建的任务", + "group": "design", + "dependencies": ["design-director"] + } + } + ], + "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", + "后台尝试创建任务", + "design-task-create-policy-run", + ) + .expect("start background task"); + + receiver + .recv_timeout(Duration::from_secs(2)) + .expect("plan llm request"); + assert!(receiver.recv_timeout(Duration::from_millis(200)).is_err()); + + let runtime = wait_for_agent_runtime_confirmation(&root, "design-director"); + assert_eq!(runtime.status, "waiting-for-confirmation"); + assert_eq!(runtime.task_queue.waiting_for_confirmation, 1); + assert!(runtime.observations.iter().any(|item| item.contains( + "task.create:waiting-for-confirmation · 项目权限策略要求用户确认:task.create" + ))); + assert!(runtime + .recent_tool_calls + .iter() + .any(|item| item.tool == "task.create" && item.status == "waiting-for-confirmation")); + let manifest: Value = + serde_json::from_str(&fs::read_to_string(root.join(".agent/manifest.json")).unwrap()) + .expect("manifest json"); + assert!(manifest["tasks"] + .as_array() + .expect("tasks") + .iter() + .all(|task| task["id"] != "design-blocked-task")); + let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db"); + assert!(agent_db.contains("\"recordType\":\"agent.runtime.tool_confirmation_required\"")); + assert!(!agent_db.contains("\"recordType\":\"agent.runtime.task.create\"")); + + fs::remove_dir_all(root).ok(); +} + #[tokio::test] async fn background_agent_runtime_can_update_manifest_task_status() { 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 003e0be39..d38ee7f18 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -11079,6 +11079,7 @@ function isProjectPolicyConfirmableCommandId(value: string) { 'asset.register', 'asset.list', 'task.list', + 'task.create', 'task.update', 'agent.run_status', 'agent.kill', @@ -15308,7 +15309,7 @@ export function App() { ...current, { role: 'assistant', - text: '当前仅支持确认 project.index、project.status、project.checkpoint、project.diff、project.restore、project.export_package、project.export_list、file.list、file.read、memory.read、memory.write、memory.delete、asset.register、asset.list、task.list、task.update、agent.run_status、agent.kill、agent.retry、agent.resume、agent.delegate、agent.audit、agent.trace_read、preview.status、preview.start、preview.open、preview.stop、canvas.project_sync、canvas.asset_import、canvas.asset_generate、canvas.export_import、conversation.read 和 conversation.write。', + text: '当前仅支持确认 project.index、project.status、project.checkpoint、project.diff、project.restore、project.export_package、project.export_list、file.list、file.read、memory.read、memory.write、memory.delete、asset.register、asset.list、task.list、task.create、task.update、agent.run_status、agent.kill、agent.retry、agent.resume、agent.delegate、agent.audit、agent.trace_read、preview.status、preview.start、preview.open、preview.stop、canvas.project_sync、canvas.asset_import、canvas.asset_generate、canvas.export_import、conversation.read 和 conversation.write。', }, ]); return; @@ -15371,7 +15372,7 @@ export function App() { ...current, { role: 'assistant', - text: '当前仅支持确认 project.index、project.status、project.checkpoint、project.diff、project.restore、project.export_package、project.export_list、file.list、file.read、memory.read、memory.write、memory.delete、asset.register、asset.list、task.list、task.update、agent.run_status、agent.kill、agent.retry、agent.resume、agent.delegate、agent.audit、agent.trace_read、preview.status、preview.start、preview.open、preview.stop、canvas.project_sync、canvas.asset_import、canvas.asset_generate、canvas.export_import、conversation.read 和 conversation.write。', + text: '当前仅支持确认 project.index、project.status、project.checkpoint、project.diff、project.restore、project.export_package、project.export_list、file.list、file.read、memory.read、memory.write、memory.delete、asset.register、asset.list、task.list、task.create、task.update、agent.run_status、agent.kill、agent.retry、agent.resume、agent.delegate、agent.audit、agent.trace_read、preview.status、preview.start、preview.open、preview.stop、canvas.project_sync、canvas.asset_import、canvas.asset_generate、canvas.export_import、conversation.read 和 conversation.write。', }, ]); return; diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 6c29af80e..f769d5e8a 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -4067,6 +4067,7 @@ - 2026-07-10 调整:Agent Runtime 的 `memory.write scope=agent` 只能写当前 Agent 自己的私有记忆。若 action 指定其他 `agentId / targetAgentId`,Runtime 返回 `blocked` observation,不写目标 Agent 私有记忆、不写 `agent.runtime.memory.write` 审计;跨 Agent 共享稳定结论必须走 `blackboard.write`,给单个 Agent 留上下文必须走 `agent.message`。 - 2026-07-10 调整:Agent Runtime 和本地对话使用 append-only JSONL 作为事实源时,进程内必须按目标文件路径串行追加整行。`.agent/agent.db`、`.agent/conversations/**/*.jsonl`、`.agent/runtime/events/*.jsonl`、`.agent/runtime/tasks/*.jsonl`、`.agent/activity.jsonl` 和 `.agent/output.jsonl` 统一走共享追加 helper,避免多个后台 Agent 并行完成时 JSON record 与换行交错。 - 2026-07-10 调整:Agent Runtime 待确认工具动作支持确认后继续。开发者确认 `waiting-for-confirmation` run 时,Runtime 为新 run 写入一次性 `.agent/runtime/confirmations///.json` 票据并重新入队;工具权限 gate 在 deny 之后、confirm 阶段消费该票据,只放行对应 Agent/run/command 一次。原 waiting run 追加 `completed/confirmed` 任务记录,避免队列长期显示等待确认;审计记录写 `agent.runtime.tool_confirmation.approved`。 +- 2026-07-10 调整:Agent Runtime 工具箱新增 `task.create`,用于让 Agent 把目标拆成新的 manifest 任务,而不只能更新 seed task。该工具默认 `confirm` 权限,写入前要求 taskId 唯一、依赖指向已有任务、列表长度受限,并写 `agent.runtime.task.create` 审计;策略要求确认或拒绝时不修改 `.agent/manifest.json`。 - 2026-07-10 调整:Agent Runtime state 新增 `recentToolCalls`,后台 loop 每次执行白名单工具后记录最近 20 条结构化动作,包含 tool、status、reason、summary、detail 和 updatedAt。状态面板展示最近动作时使用该字段,不解析 observation 文本;写入前继续过滤敏感上下文,不保存原始密钥或任意未过滤输入。 - 2026-07-10 调整:Agent Runtime state 新增 `currentGoal` 和 `waitingOn`。`currentGoal` 固定表达本轮任务目标,`waitingOn` 表达当前等待 LLM、工具观察、开发者输入或失败处理;后台任务生命周期、`agent.run_status` observation、下一轮 planning prompt、开发单 Agent 对话页、项目内 Agent 对话弹窗和主窗口 Agent 状态列表都必须展示同一份目标 / 等待状态。 - 2026-07-10 调整:Agent Runtime state 新增 `loopIteration / maxLoopIterations / toolActionBudget`。后台 Agent loop 每轮规划前刷新当前轮次、最大轮次和每轮工具动作预算;开发窗口 Runtime 面板、主窗口 Agent 状态列表、`agent.run_status` observation 和下一轮 planning prompt 都展示该进度,字段只做运行观测,不改变 loop 上限或权限 gate。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index cac28c1c0..8953f6392 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 / response`,Runtime 按白名单和项目权限策略执行工具动作,写入 `thinking_summary / plan / action / observation / response / error` 事件,再把 observation 放入下一轮 prompt 让 Agent 修正计划、继续行动或用空 actions + response 收束;后台任务最多执行 3 轮 loop,仍未收束时再按最后计划和全部观察生成最终回复并追加回对话。工具箱包含只读工具 `memory.read`、`conversation.read`、`asset.list`、`project.index`、`project.diff`、`file.list`、`file.read`、`agent.run_status`,以及受策略保护的写/运行工具 `memory.write`、`file.write`、`command.run_limited`、`blackboard.write`、`agent.message` 和 `agent.delegate`;`memory.write` 可追加或覆盖本 Agent 私有记忆、项目长期/短期记忆或黑板,`file.write` 只能写项目内相对路径并记录审计,`command.run_limited` 只接受 `game.static_smoke` 并复用本地静态自检安全边界,`blackboard.write` 追加 `memory/blackboard.md`,`agent.message` 给目标 `.agent/conversations/agents/.jsonl` 写入 tool 留言,`agent.delegate` 把任务投递到目标 Agent 的独立后台队列,策略要求确认或拒绝时不执行写入、运行或委派,只把策略结果作为 observation 回给 Agent。`read_game_creator_agent_runtime` 会按 `runId` 去重返回最近任务和最近事件,`read_game_creator_agent_runtimes` 批量读取所有规范 taskId 的 runtime;开发窗口和项目内 Agent 对话弹窗的 Runtime 状态面板展示最近事件、最近任务、当前目标、当前任务、当前动作、等待对象、下一步和运行阶段,主窗口 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 / response`,Runtime 按白名单和项目权限策略执行工具动作,写入 `thinking_summary / plan / action / observation / response / error` 事件,再把 observation 放入下一轮 prompt 让 Agent 修正计划、继续行动或用空 actions + response 收束;后台任务最多执行 3 轮 loop,仍未收束时再按最后计划和全部观察生成最终回复并追加回对话。工具箱包含只读工具 `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` 可追加或覆盖本 Agent 私有记忆、项目长期/短期记忆或黑板,`file.write` 只能写项目内相对路径并记录审计,`task.create` 只能追加经过校验的新 manifest 任务,`task.update` 只能更新已有 manifest 任务状态,`command.run_limited` 只接受 `game.static_smoke` 并复用本地静态自检安全边界,`blackboard.write` 追加 `memory/blackboard.md`,`agent.message` 给目标 `.agent/conversations/agents/.jsonl` 写入 tool 留言,`agent.delegate` 把任务投递到目标 Agent 的独立后台队列,策略要求确认或拒绝时不执行写入、运行或委派,只把策略结果作为 observation 回给 Agent。`read_game_creator_agent_runtime` 会按 `runId` 去重返回最近任务和最近事件,`read_game_creator_agent_runtimes` 批量读取所有规范 taskId 的 runtime;开发窗口和项目内 Agent 对话弹窗的 Runtime 状态面板展示最近事件、最近任务、当前目标、当前任务、当前动作、等待对象、下一步和运行阶段,主窗口 Agent 状态列表展示当前目标、任务、动作、等待对象和运行阶段摘要。不同 Agent 使用各自 runtime 锁,可以并行运行;同一 Agent 已有运行任务时,新任务会先写成 `pending / queued`,由当前后台 drain 在完成后串行继续执行。该能力仍属于 Runtime V1 的进程内任务,不是独立 OS 进程或跨重启离线常驻 worker。 - 2026-07-10 补充:Agent Runtime state 新增 `toolPolicy`,按当前项目 `.agent/policy.json` 派生工具级 `allowedTools / autoTools / confirmTools / deniedTools` 快照;后台 planning prompt 会带入该快照,让 Agent 在规划时知道哪些工具会自动执行、需要确认或被拒绝。`blackboard.write` 继承 `memory.write` 策略,`agent.message` 继承 `conversation.write` 策略,`agent.delegate` 使用独立 `agent.delegate` 策略;实际执行仍以 Runtime 的白名单和项目权限 gate 为准。 - 2026-07-10 补充:`.agent/policy.json` 新增 `agentPolicies`,可按规范 Agent id 分别配置 `deniedCommands / confirmCommands`。有效策略为“项目级策略 + Agent 级策略”的保守叠加:项目级拒绝 / 确认仍对所有 Agent 生效,Agent 级策略只能进一步限制该 Agent,不能放宽项目级策略,拒绝优先于确认。主聊天新增 `/agent-policy-deny Agent 命令`、`/agent-policy-allow Agent 命令`、`/agent-policy-confirm Agent 命令` 和 `/agent-policy-auto Agent 命令`,写入前仍走 `project.policy_write` 确认卡。 - 2026-07-10 补充:后台 Agent 工具命中 `confirmCommands` 时不再继续整理最终回复,而是把本轮 Runtime 停在 `status/phase = waiting-for-confirmation`,`waitingOn` 指向“开发者确认 Agent 工具动作”,`recentToolCalls`、事件流、任务记录和 `taskQueue.waitingForConfirmation` 都保留该待确认事实;同一 Agent 的 drain 会暂停,不继续消费后续 pending 任务。命中拒绝策略仍作为 `blocked` observation 交回 Agent 继续修正计划。 @@ -41,6 +41,7 @@ Agent Runtime 负责: - 2026-07-10 补充:后台 Agent 任务的 `memory.write scope=agent` 只允许写当前 Agent 自己的私有记忆。若 action 指定其他 `agentId / targetAgentId`,Runtime 返回 `blocked` observation,不写目标 Agent 私有记忆,也不写 `agent.runtime.memory.write` 审计;跨 Agent 共享稳定结论必须使用 `blackboard.write`,给单个 Agent 留上下文必须使用 `agent.message`。 - 2026-07-10 补充:本地 append-only JSONL 追加写入按目标文件路径做进程内串行化。`.agent/agent.db`、项目 / Agent 对话、Runtime events、Runtime tasks、Agent activity 和 output 都通过共享 helper 写入完整 JSON 行,防止多个后台 Agent 并行运行时 record 内容与换行交错;该约束服务于当前单客户端进程内并行,不把跨进程同项目写入作为 v1 支持目标。 - 2026-07-10 补充:后台 Agent 待确认工具动作支持由开发者确认后继续。确认命令会为新 run 写入一次性工具确认票据,权限 gate 在 deny 规则之后、confirm 规则处按 `agentId/runId/commandId` 消费该票据并只放行一次;原 waiting run 会追加 `completed/confirmed` 任务记录,新 run 继续走正常后台队列、LLM 规划和工具 observation 闭环。 +- 2026-07-10 补充:后台任务工具箱已加入 `task.create`。Agent 可在 loop 中把拆解出的后续工作追加为 manifest 任务;Runtime 复用 `task.create` 策略和项目写锁,写入前校验 taskId 唯一、依赖指向已有任务、任务分组合法、标题 / 角色非空以及列表长度,并写入 `agent.runtime.task.create` 审计记录。策略要求确认或拒绝时不会修改 manifest。 - 2026-07-10 补充:Agent Runtime state 新增 `recentToolCalls`,每次后台工具执行后记录最近 20 条结构化工具动作,包含 tool、status、reason、summary、detail 和 updatedAt;开发窗口、项目内 Agent 对话弹窗和主窗口 Agent 状态列表可直接展示“最近动作”,不再只能从 observation 字符串里猜测 action / observation 对应关系。字段只保存过滤后的摘要和观察细节,不保存原始 API Key 或任意未过滤输入。 - 2026-07-10 补充:Agent Runtime state 新增 `currentGoal` 和 `waitingOn`,把本轮目标与当前等待对象从 `currentTask / currentAction / nextStep` 中显式拆出来;后台任务启动、工具 observation、完成和失败都会刷新该状态,开发窗口、项目内 Agent 对话弹窗、主窗口 Agent 状态列表、`agent.run_status` observation 和下一轮 planning prompt 都展示同一份目标 / 等待信息,避免开发者只能从动作文本里猜 Agent 卡在 LLM、工具、同伴还是人工输入。 - 2026-07-10 补充:Agent Runtime state 新增 `loopIteration / maxLoopIterations / toolActionBudget`,结构化记录后台 Agent loop 当前轮次、最大轮次和每轮工具动作预算;开发窗口 Runtime 面板、主窗口 Agent 状态列表、`agent.run_status` observation 和下一轮 planning prompt 都展示该进度,帮助判断 Agent 是刚开始规划、正在 replan,还是接近本轮 loop 上限。该字段只做运行观测,不改变后台 loop 的执行上限或工具权限。 @@ -279,10 +280,11 @@ 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`。后台任务的核心 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.tool_observation` / `agent.runtime.tool_confirmation_required` / `agent.runtime.tool_confirmation.approved` / `agent.runtime.memory.write` / `agent.runtime.file.write` / `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`、`agent.run_status`,以及受策略保护的写/运行工具 `memory.write`、`file.write`、`command.run_limited`、`blackboard.write`、`agent.message` 和 `agent.delegate`;`memory.write scope=agent` 只允许写当前 Agent 自己的私有记忆,跨 Agent 共享必须改用 `blackboard.write` 或 `agent.message`;若项目策略拒绝,对应工具不会执行,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`。后台任务的核心 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.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 同时完成时的行交错风险。 - 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` 项目权限策略保护。 +- 2026-07-10 补充:当前工具箱还开放 `task.create`,审计记录类型为 `agent.runtime.task.create`;该工具只追加新 manifest 任务,要求 taskId 唯一、依赖指向已有任务、分组合规且列表长度受限,并受 `task.create` 项目权限策略保护。 - 2026-07-10 补充:当前工具箱还开放 `task.update`,审计记录类型为 `agent.runtime.task.update`;该工具只允许更新 manifest 中已有任务的状态,并受 `task.update` 项目权限策略保护。 - 2026-07-10 补充:当前工具箱还开放 `file.list`;该工具只读列出项目文件摘要,可按相对路径过滤,并受 `file.list` 项目权限策略保护。 - 2026-07-10 补充:当前工具箱还开放 `project.diff`;该工具只读返回 checkpoint 对比后的新增、修改、删除计数和相对路径摘要,并受 `project.diff` 项目权限策略保护。 diff --git a/packages/shared/src/contracts/gameCreationApp.ts b/packages/shared/src/contracts/gameCreationApp.ts index f2329cc31..0487b588b 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.create', permission: 'confirm' }, { id: 'task.update', permission: 'confirm' }, { id: 'agent.trace_read', permission: 'auto' }, { id: 'agent.run_status', permission: 'auto' }, 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 86bbad9a8..f6ed4c9ec 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; 46] = [ +pub const GAME_CREATION_APP_COMMANDS: [GameCreationAppCommandDescriptor; 47] = [ 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; 46] = [ command("project.policy_read", GameCreationAppPermission::Auto), command("project.policy_write", GameCreationAppPermission::Confirm), command("task.list", GameCreationAppPermission::Auto), + command("task.create", GameCreationAppPermission::Confirm), command("task.update", GameCreationAppPermission::Confirm), command("agent.trace_read", GameCreationAppPermission::Auto), command("agent.run_status", GameCreationAppPermission::Auto), @@ -669,6 +670,12 @@ mod tests { .expect("command should exist"); assert_eq!(task_list.permission, GameCreationAppPermission::Auto); + let task_create = GAME_CREATION_APP_COMMANDS + .iter() + .find(|command| command.id == "task.create") + .expect("command should exist"); + assert_eq!(task_create.permission, GameCreationAppPermission::Confirm); + let trace_read = GAME_CREATION_APP_COMMANDS .iter() .find(|command| command.id == "agent.trace_read")