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 0edbfeb2c..256229d17 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent.rs @@ -853,22 +853,22 @@ struct AgentRuntimeToolPlan { #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] -struct AgentRuntimeToolAction { +pub(crate) struct AgentRuntimeToolAction { #[serde(default)] - tool: String, + pub(crate) tool: String, #[serde(default)] - reason: Option, + pub(crate) reason: Option, #[serde(default)] - input: serde_json::Value, + pub(crate) input: serde_json::Value, } #[derive(Clone, Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] -struct AgentRuntimeToolObservation { - tool: String, - status: String, - summary: String, - detail: Option, +pub(crate) struct AgentRuntimeToolObservation { + pub(crate) tool: String, + pub(crate) status: String, + pub(crate) summary: String, + pub(crate) detail: Option, } impl AgentRuntimeToolObservation { @@ -1119,7 +1119,7 @@ fn build_game_creator_agent_background_tool_plan_request( serde_json::to_string_pretty(observations) .map_err(|error| format!("序列化 Agent 工具观察失败:{error}"))? }; - let tool_policy = agent_runtime_tool_policy_snapshot_at(root)?; + let tool_policy = agent_runtime_tool_policy_snapshot_at(root, agent_id)?; let tool_policy_json = serde_json::to_string_pretty(&tool_policy) .map_err(|error| format!("序列化 Agent 工具策略失败:{error}"))?; let prompt = format!( @@ -1188,7 +1188,7 @@ fn parse_game_creator_agent_tool_plan_response( Ok(plan) } -async fn execute_game_creator_agent_runtime_tool_action( +pub(crate) async fn execute_game_creator_agent_runtime_tool_action( root: &Path, agent_id: &str, task: &str, @@ -1197,7 +1197,9 @@ async fn execute_game_creator_agent_runtime_tool_action( let tool = action.tool.trim(); let command_id = game_creator_agent_runtime_tool_command_id(tool); if let Some(command_id) = command_id { - if let Some(blocked) = game_creator_agent_runtime_tool_policy_block(root, command_id) { + if let Some(blocked) = + game_creator_agent_runtime_tool_policy_block(root, agent_id, command_id) + { return AgentRuntimeToolObservation { tool: tool.to_string(), status: "blocked".to_string(), @@ -1296,8 +1298,9 @@ fn agent_runtime_executable_tools() -> Vec<&'static str> { fn agent_runtime_tool_policy_snapshot_at( root: &Path, + agent_id: &str, ) -> Result { - let view = read_project_permission_policy_at(root)?; + let policy = agent_runtime_effective_tool_policy_at(root, agent_id)?; let mut auto_tools = Vec::new(); let mut confirm_tools = Vec::new(); let mut denied_tools = Vec::new(); @@ -1305,15 +1308,13 @@ fn agent_runtime_tool_policy_snapshot_at( let Some(command_id) = game_creator_agent_runtime_tool_command_id(tool) else { continue; }; - if view - .policy + if policy .denied_commands .iter() .any(|command| command == command_id) { denied_tools.push(tool.to_string()); - } else if view - .policy + } else if policy .confirm_commands .iter() .any(|command| command == command_id) @@ -1339,13 +1340,51 @@ fn refresh_game_creator_agent_runtime_tool_policy( root: &Path, state: &mut AgentRuntimeState, ) -> Result<(), String> { - state.tool_policy = agent_runtime_tool_policy_snapshot_at(root)?; + state.tool_policy = agent_runtime_tool_policy_snapshot_at(root, &state.agent_id)?; Ok(()) } -fn game_creator_agent_runtime_tool_policy_block(root: &Path, command_id: &str) -> Option { +fn agent_runtime_effective_tool_policy_at( + root: &Path, + agent_id: &str, +) -> Result { let view = match read_project_permission_policy_at(root) { Ok(view) => view, + Err(error) => return Err(error), + }; + let agent_id = normalize_game_creator_runtime_agent_id(agent_id)?; + let mut denied_commands = view.policy.denied_commands.clone(); + let mut confirm_commands = view.policy.confirm_commands.clone(); + if let Some(agent_policy) = view.policy.agent_policies.get(&agent_id) { + for command_id in &agent_policy.denied_commands { + if !denied_commands.iter().any(|command| command == command_id) { + denied_commands.push(command_id.clone()); + } + } + for command_id in &agent_policy.confirm_commands { + if !confirm_commands.iter().any(|command| command == command_id) { + confirm_commands.push(command_id.clone()); + } + } + } + confirm_commands.retain(|command| !denied_commands.contains(command)); + Ok(ProjectAgentPermissionPolicy { + denied_commands, + confirm_commands, + }) +} + +fn game_creator_agent_runtime_tool_policy_block( + root: &Path, + agent_id: &str, + command_id: &str, +) -> Option { + let view = match read_project_permission_policy_at(root) { + Ok(view) => view, + Err(error) => return Some(error), + }; + let agent_id = match normalize_game_creator_runtime_agent_id(agent_id) { + Ok(agent_id) => agent_id, Err(error) => return Some(error), }; if view @@ -1356,6 +1395,20 @@ fn game_creator_agent_runtime_tool_policy_block(root: &Path, command_id: &str) - { return Some(format!("项目权限策略拒绝执行:{command_id}")); } + if view + .policy + .agent_policies + .get(&agent_id) + .map(|policy| { + policy + .denied_commands + .iter() + .any(|command| command == command_id) + }) + .unwrap_or(false) + { + return Some(format!("Agent 权限策略拒绝执行:{agent_id} / {command_id}")); + } if view .policy .confirm_commands @@ -1364,6 +1417,22 @@ fn game_creator_agent_runtime_tool_policy_block(root: &Path, command_id: &str) - { return Some(format!("项目权限策略要求用户确认:{command_id}")); } + if view + .policy + .agent_policies + .get(&agent_id) + .map(|policy| { + policy + .confirm_commands + .iter() + .any(|command| command == command_id) + }) + .unwrap_or(false) + { + return Some(format!( + "Agent 权限策略要求用户确认:{agent_id} / {command_id}" + )); + } None } diff --git a/apps/ai-game-creator-shell/src-tauri/src/main.rs b/apps/ai-game-creator-shell/src-tauri/src/main.rs index 0fc345f3a..5e1117c93 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/main.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/main.rs @@ -644,6 +644,17 @@ struct LocalConversationResult { struct ProjectPermissionPolicy { denied_commands: Vec, confirm_commands: Vec, + #[serde(default)] + agent_policies: BTreeMap, +} + +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +struct ProjectAgentPermissionPolicy { + #[serde(default)] + denied_commands: Vec, + #[serde(default)] + confirm_commands: Vec, } #[derive(Debug, Eq, PartialEq, Serialize)] 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 52683fd3e..0a04da8d8 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project.rs @@ -487,6 +487,7 @@ impl Default for ProjectPermissionPolicy { .filter(|command| command.permission == GameCreationAppPermission::Confirm) .map(|command| command.id.to_string()) .collect(), + agent_policies: BTreeMap::new(), } } } @@ -533,6 +534,7 @@ pub(crate) fn write_project_permission_policy_at( "recordType": "project.policy_write", "deniedCommands": policy.denied_commands, "confirmCommands": policy.confirm_commands, + "agentPolicies": policy.agent_policies, }), )?; read_project_permission_policy_at(root) @@ -546,6 +548,18 @@ pub(crate) fn normalize_project_permission_policy( policy .confirm_commands .retain(|command| !policy.denied_commands.contains(command)); + let mut agent_policies = BTreeMap::new(); + for (agent_id, mut agent_policy) in policy.agent_policies { + let agent_id = normalize_game_creator_runtime_agent_id(&agent_id)?; + agent_policy.denied_commands = normalize_policy_command_ids(agent_policy.denied_commands)?; + agent_policy.confirm_commands = + normalize_policy_command_ids(agent_policy.confirm_commands)?; + agent_policy + .confirm_commands + .retain(|command| !agent_policy.denied_commands.contains(command)); + agent_policies.insert(agent_id, agent_policy); + } + policy.agent_policies = agent_policies; Ok(policy) } 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 34f297533..37e33647e 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests.rs @@ -1401,6 +1401,7 @@ fn agent_runtime_tool_policy_snapshot_reflects_project_policy() { ProjectPermissionPolicy { denied_commands: vec!["file.write".to_string()], confirm_commands: vec!["memory.write".to_string(), "task.update".to_string()], + agent_policies: BTreeMap::new(), }, ) .expect("write policy"); @@ -1452,6 +1453,116 @@ fn agent_runtime_tool_policy_snapshot_reflects_project_policy() { fs::remove_dir_all(root).ok(); } +#[test] +fn agent_runtime_tool_policy_snapshot_reflects_agent_policy() { + let root = unique_project_path(); + init_local_game_project_at(&root, "project-1", "月光厨房").expect("project init"); + let mut agent_policies = BTreeMap::new(); + agent_policies.insert( + "design-director".to_string(), + ProjectAgentPermissionPolicy { + denied_commands: vec!["file.read".to_string()], + confirm_commands: vec!["memory.write".to_string()], + }, + ); + write_project_permission_policy_at( + &root, + ProjectPermissionPolicy { + denied_commands: Vec::new(), + confirm_commands: Vec::new(), + agent_policies, + }, + ) + .expect("write agent policy"); + + let design_runtime = start_game_creator_agent_runtime_task_at( + &root, + "design-director", + "检查策划 Agent 工具策略", + "design-policy-snapshot-run", + "agent-chat", + "读取工具策略", + vec!["核对工具策略".to_string()], + ) + .expect("start design runtime"); + let art_runtime = start_game_creator_agent_runtime_task_at( + &root, + "art-director", + "检查美术 Agent 工具策略", + "art-policy-snapshot-run", + "agent-chat", + "读取工具策略", + vec!["核对工具策略".to_string()], + ) + .expect("start art runtime"); + + assert!(design_runtime + .tool_policy + .denied_tools + .contains(&"file.read".to_string())); + assert!(design_runtime + .tool_policy + .confirm_tools + .contains(&"memory.write".to_string())); + assert!(design_runtime + .tool_policy + .confirm_tools + .contains(&"blackboard.write".to_string())); + assert!(art_runtime + .tool_policy + .auto_tools + .contains(&"file.read".to_string())); + assert!(art_runtime + .tool_policy + .auto_tools + .contains(&"memory.write".to_string())); + + fs::remove_dir_all(root).ok(); +} + +#[tokio::test] +async fn background_agent_runtime_tool_action_respects_agent_policy() { + let root = unique_project_path(); + init_local_game_project_at(&root, "project-1", "月光厨房").expect("project init"); + let mut agent_policies = BTreeMap::new(); + agent_policies.insert( + "design-director".to_string(), + ProjectAgentPermissionPolicy { + denied_commands: vec!["file.read".to_string()], + confirm_commands: Vec::new(), + }, + ); + write_project_permission_policy_at( + &root, + ProjectPermissionPolicy { + denied_commands: Vec::new(), + confirm_commands: Vec::new(), + agent_policies, + }, + ) + .expect("write agent policy"); + + let observation = execute_game_creator_agent_runtime_tool_action( + &root, + "design-director", + "读取设计笔记", + &AgentRuntimeToolAction { + tool: "file.read".to_string(), + reason: Some("需要读取设计笔记".to_string()), + input: serde_json::json!({ "path": "game/notes.txt" }), + }, + ) + .await; + + assert_eq!(observation.status, "blocked"); + assert_eq!( + observation.summary, + "Agent 权限策略拒绝执行:design-director / file.read" + ); + + fs::remove_dir_all(root).ok(); +} + #[tokio::test] async fn role_agent_runtime_turn_persists_session_events_and_index() { let root = unique_project_path(); @@ -2422,6 +2533,7 @@ async fn background_agent_runtime_can_write_blackboard_and_message_other_agent() ProjectPermissionPolicy { denied_commands: Vec::new(), confirm_commands: Vec::new(), + agent_policies: BTreeMap::new(), }, ) .expect("allow runtime collaboration writes"); @@ -2543,6 +2655,7 @@ async fn background_agent_runtime_can_delegate_task_to_other_agent() { ProjectPermissionPolicy { denied_commands: Vec::new(), confirm_commands: Vec::new(), + agent_policies: BTreeMap::new(), }, ) .expect("allow runtime delegation"); @@ -2678,6 +2791,7 @@ async fn background_agent_runtime_delegate_respects_project_policy() { ProjectPermissionPolicy { denied_commands: Vec::new(), confirm_commands: vec!["agent.delegate".to_string()], + agent_policies: BTreeMap::new(), }, ) .expect("write policy"); @@ -2772,6 +2886,7 @@ async fn background_agent_runtime_can_write_memory_and_project_files() { ProjectPermissionPolicy { denied_commands: Vec::new(), confirm_commands: Vec::new(), + agent_policies: BTreeMap::new(), }, ) .expect("allow runtime writes"); @@ -2900,6 +3015,7 @@ async fn background_agent_runtime_write_tools_respect_project_policy() { ProjectPermissionPolicy { denied_commands: Vec::new(), confirm_commands: vec!["memory.write".to_string(), "conversation.write".to_string()], + agent_policies: BTreeMap::new(), }, ) .expect("write policy"); @@ -3000,6 +3116,7 @@ async fn background_agent_runtime_file_and_memory_writes_respect_project_policy( ProjectPermissionPolicy { denied_commands: Vec::new(), confirm_commands: vec!["memory.write".to_string(), "file.write".to_string()], + agent_policies: BTreeMap::new(), }, ) .expect("write policy"); @@ -3109,6 +3226,7 @@ async fn background_agent_runtime_can_list_manifest_tasks() { ProjectPermissionPolicy { denied_commands: Vec::new(), confirm_commands: Vec::new(), + agent_policies: BTreeMap::new(), }, ) .expect("allow task list"); @@ -3203,6 +3321,7 @@ async fn background_agent_runtime_task_list_respects_project_policy() { ProjectPermissionPolicy { denied_commands: Vec::new(), confirm_commands: vec!["task.list".to_string()], + agent_policies: BTreeMap::new(), }, ) .expect("write policy"); @@ -3285,6 +3404,7 @@ async fn background_agent_runtime_can_update_manifest_task_status() { ProjectPermissionPolicy { denied_commands: Vec::new(), confirm_commands: Vec::new(), + agent_policies: BTreeMap::new(), }, ) .expect("allow task update"); @@ -3382,6 +3502,7 @@ async fn background_agent_runtime_task_update_respects_project_policy() { ProjectPermissionPolicy { denied_commands: Vec::new(), confirm_commands: vec!["task.update".to_string()], + agent_policies: BTreeMap::new(), }, ) .expect("write policy"); @@ -3474,6 +3595,7 @@ async fn background_agent_runtime_can_run_limited_static_smoke() { ProjectPermissionPolicy { denied_commands: Vec::new(), confirm_commands: Vec::new(), + agent_policies: BTreeMap::new(), }, ) .expect("allow limited command"); @@ -3579,6 +3701,7 @@ async fn background_agent_runtime_limited_command_respects_project_policy() { ProjectPermissionPolicy { denied_commands: Vec::new(), confirm_commands: vec!["command.run_limited".to_string()], + agent_policies: BTreeMap::new(), }, ) .expect("write policy"); @@ -3664,6 +3787,7 @@ async fn background_agent_runtime_can_start_local_preview() { ProjectPermissionPolicy { denied_commands: Vec::new(), confirm_commands: Vec::new(), + agent_policies: BTreeMap::new(), }, ) .expect("allow preview start"); @@ -3768,6 +3892,7 @@ async fn background_agent_runtime_preview_start_respects_project_policy() { ProjectPermissionPolicy { denied_commands: Vec::new(), confirm_commands: vec!["preview.start".to_string()], + agent_policies: BTreeMap::new(), }, ) .expect("write policy"); @@ -3853,6 +3978,7 @@ async fn background_agent_runtime_can_generate_platform_art_asset() { ProjectPermissionPolicy { denied_commands: Vec::new(), confirm_commands: Vec::new(), + agent_policies: BTreeMap::new(), }, ) .expect("allow asset generation"); @@ -3972,6 +4098,7 @@ async fn background_agent_runtime_asset_generation_respects_project_policy() { ProjectPermissionPolicy { denied_commands: Vec::new(), confirm_commands: vec!["canvas.asset_generate".to_string()], + agent_policies: BTreeMap::new(), }, ) .expect("write policy"); @@ -4061,6 +4188,7 @@ async fn background_agent_runtime_tool_action_respects_confirm_policy() { ProjectPermissionPolicy { denied_commands: Vec::new(), confirm_commands: vec!["file.read".to_string()], + agent_policies: BTreeMap::new(), }, ) .expect("write policy"); @@ -4159,6 +4287,7 @@ async fn background_agent_runtime_can_list_project_files() { ProjectPermissionPolicy { denied_commands: Vec::new(), confirm_commands: Vec::new(), + agent_policies: BTreeMap::new(), }, ) .expect("allow file list"); @@ -4257,6 +4386,7 @@ async fn background_agent_runtime_file_list_respects_project_policy() { ProjectPermissionPolicy { denied_commands: Vec::new(), confirm_commands: vec!["file.list".to_string()], + agent_policies: BTreeMap::new(), }, ) .expect("write policy"); @@ -4345,6 +4475,7 @@ async fn background_agent_runtime_can_diff_checkpoint() { ProjectPermissionPolicy { denied_commands: Vec::new(), confirm_commands: Vec::new(), + agent_policies: BTreeMap::new(), }, ) .expect("allow project diff"); @@ -4441,6 +4572,7 @@ async fn background_agent_runtime_project_diff_respects_project_policy() { ProjectPermissionPolicy { denied_commands: Vec::new(), confirm_commands: vec!["project.diff".to_string()], + agent_policies: BTreeMap::new(), }, ) .expect("write policy"); @@ -4542,6 +4674,7 @@ async fn background_agent_runtime_can_read_other_agent_status() { ProjectPermissionPolicy { denied_commands: Vec::new(), confirm_commands: Vec::new(), + agent_policies: BTreeMap::new(), }, ) .expect("allow agent status"); @@ -4653,6 +4786,7 @@ async fn background_agent_runtime_run_status_respects_project_policy() { ProjectPermissionPolicy { denied_commands: Vec::new(), confirm_commands: vec!["agent.run_status".to_string()], + agent_policies: BTreeMap::new(), }, ) .expect("write policy"); @@ -7089,6 +7223,7 @@ async fn generate_platform_art_asset_respects_project_policy() { ProjectPermissionPolicy { denied_commands: vec!["canvas.asset_generate".to_string()], confirm_commands: Vec::new(), + agent_policies: BTreeMap::new(), }, ) .expect("write policy"); @@ -7234,6 +7369,7 @@ fn local_memory_reads_respect_project_policy() { ProjectPermissionPolicy { denied_commands: vec!["memory.read".to_string()], confirm_commands: Vec::new(), + agent_policies: BTreeMap::new(), }, ) .expect("write policy"); @@ -7259,6 +7395,7 @@ fn local_agent_memory_writes_respect_project_policy() { ProjectPermissionPolicy { denied_commands: vec!["memory.write".to_string()], confirm_commands: Vec::new(), + agent_policies: BTreeMap::new(), }, ) .expect("write policy"); @@ -7565,6 +7702,7 @@ fn local_conversation_write_respects_project_policy() { ProjectPermissionPolicy { denied_commands: vec!["conversation.write".to_string()], confirm_commands: Vec::new(), + agent_policies: BTreeMap::new(), }, ) .expect("write policy"); @@ -7603,6 +7741,7 @@ fn local_conversation_read_respects_project_policy() { ProjectPermissionPolicy { denied_commands: vec!["conversation.read".to_string()], confirm_commands: Vec::new(), + agent_policies: BTreeMap::new(), }, ) .expect("write policy"); @@ -7983,6 +8122,7 @@ fn project_permission_policy_can_deny_mutating_commands() { ProjectPermissionPolicy { denied_commands: vec!["file.write".to_string()], confirm_commands: Vec::new(), + agent_policies: BTreeMap::new(), }, ) .expect("write policy"); @@ -8003,6 +8143,7 @@ fn project_permission_policy_read_respects_project_policy() { ProjectPermissionPolicy { denied_commands: vec!["project.policy_read".to_string()], confirm_commands: Vec::new(), + agent_policies: BTreeMap::new(), }, ) .expect("write policy"); @@ -8023,6 +8164,7 @@ fn local_game_manifest_reads_respect_declared_command_policy() { ProjectPermissionPolicy { denied_commands: vec!["project.status".to_string(), "asset.list".to_string()], confirm_commands: Vec::new(), + agent_policies: BTreeMap::new(), }, ) .expect("write policy"); @@ -8052,6 +8194,7 @@ fn local_project_file_read_and_list_respect_project_policy() { ProjectPermissionPolicy { denied_commands: vec!["file.list".to_string(), "file.read".to_string()], confirm_commands: Vec::new(), + agent_policies: BTreeMap::new(), }, ) .expect("write policy"); @@ -8077,6 +8220,7 @@ fn local_project_file_read_can_enforce_agent_trace_read_policy() { ProjectPermissionPolicy { denied_commands: vec!["agent.trace_read".to_string()], confirm_commands: Vec::new(), + agent_policies: BTreeMap::new(), }, ) .expect("write policy"); @@ -8859,6 +9003,7 @@ fn preview_open_respects_project_policy_when_project_is_provided() { ProjectPermissionPolicy { denied_commands: vec!["preview.open".to_string()], confirm_commands: Vec::new(), + agent_policies: BTreeMap::new(), }, ) .expect("write policy"); @@ -8886,6 +9031,7 @@ fn preview_status_respects_project_policy_when_project_is_provided() { ProjectPermissionPolicy { denied_commands: vec!["preview.status".to_string()], confirm_commands: Vec::new(), + agent_policies: BTreeMap::new(), }, ) .expect("write policy"); diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index 73ea01953..e32f41fda 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -877,6 +877,12 @@ function AgentRuntimeStatusPanel({ interface ProjectPermissionPolicy { deniedCommands: string[]; confirmCommands: string[]; + agentPolicies?: Record; +} + +interface ProjectAgentPermissionPolicy { + deniedCommands: string[]; + confirmCommands: string[]; } interface ProjectPermissionPolicyView { @@ -4749,6 +4755,10 @@ const chatCommandHelp = [ '/policy-allow 命令:移除项目内某个命令拒绝项', '/policy-confirm 命令:执行前每次确认', '/policy-auto 命令:恢复自动执行', + '/agent-policy-deny Agent 命令:拒绝某个 Agent 调用工具', + '/agent-policy-allow Agent 命令:移除某个 Agent 的拒绝项', + '/agent-policy-confirm Agent 命令:某个 Agent 调用工具前要求确认', + '/agent-policy-auto Agent 命令:恢复某个 Agent 自动执行', '/tasks:查看任务拆分', '/agents:查看每个 Agent 的当前状态', '/agent-conversations:列出 Agent 对话读取命令', @@ -4812,6 +4822,14 @@ function missingChatCommandArgumentMessage(prompt: string) { return '格式:/policy-confirm project.index'; case '/policy-auto': return '格式:/policy-auto project.index'; + case '/agent-policy-deny': + return '格式:/agent-policy-deny design-director file.read'; + case '/agent-policy-allow': + return '格式:/agent-policy-allow design-director file.read'; + case '/agent-policy-confirm': + return '格式:/agent-policy-confirm design-director memory.write'; + case '/agent-policy-auto': + return '格式:/agent-policy-auto design-director memory.write'; case '/read': return '格式:/read game/index.html'; case '/asset-register': @@ -9841,10 +9859,25 @@ function summarizeProjectDiff(result: LocalProjectDiffResult) { } function summarizeProjectPolicy(view: ProjectPermissionPolicyView) { + const agentPolicies = view.policy.agentPolicies ?? {}; + const agentPolicyLines = Object.entries(agentPolicies) + .slice(0, 8) + .map( + ([agentId, policy]) => + `Agent ${agentId}:拒绝 ${formatProjectPolicyCommandList( + policy.deniedCommands, + )};确认 ${formatProjectPolicyCommandList(policy.confirmCommands)}`, + ); + if (Object.keys(agentPolicies).length > agentPolicyLines.length) { + agentPolicyLines.push( + `Agent 策略还有 ${Object.keys(agentPolicies).length - agentPolicyLines.length} 项`, + ); + } return [ `策略:${view.path}`, `拒绝:${formatProjectPolicyCommandList(view.policy.deniedCommands)}`, `确认:${formatProjectPolicyCommandList(view.policy.confirmCommands)}`, + ...agentPolicyLines, ].join('\n'); } @@ -11218,6 +11251,22 @@ function formatAgentCardRuntimeStatus(agent: AgentStatusCard) { .join(' · '); } +function formatAgentPolicySummary(policy: ProjectPermissionPolicy) { + const entries = Object.entries(policy.agentPolicies ?? {}); + if (entries.length === 0) { + return '无 Agent 独立策略'; + } + return entries + .slice(0, 4) + .map( + ([agentId, agentPolicy]) => + `${agentId} 拒绝:${formatProjectPolicyCommandList( + agentPolicy.deniedCommands, + )} 确认:${formatProjectPolicyCommandList(agentPolicy.confirmCommands)}`, + ) + .join(';'); +} + function formatAgentRecentRuntimeTask(task: AgentRuntimeTaskRecord) { return `${task.status} / ${task.phase} · ${ task.task || task.currentAction || task.runId @@ -11787,7 +11836,9 @@ export function pendingCommandDetail( if (command.id === 'project.policy_write') { return `写入 ${projectPath}/.agent/policy.json · 拒绝:${formatProjectPolicyCommandList( command.policy.deniedCommands, - )} · 确认:${formatProjectPolicyCommandList(command.policy.confirmCommands)}`; + )} · 确认:${formatProjectPolicyCommandList( + command.policy.confirmCommands, + )} · Agent:${formatAgentPolicySummary(command.policy)}`; } if (command.id === 'preview.start') { return `启动 ${projectPath}/game/ 并交给外部浏览器`; @@ -14873,7 +14924,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、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.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; @@ -14882,6 +14933,69 @@ export function App() { return; } + if ( + prompt.startsWith('/agent-policy-deny ') || + prompt.startsWith('/agent-policy-allow ') || + prompt.startsWith('/agent-policy-confirm ') || + prompt.startsWith('/agent-policy-auto ') + ) { + if (!requireChatProjectForUserAction()) { + return; + } + const mode = prompt.startsWith('/agent-policy-deny ') + ? 'deny' + : prompt.startsWith('/agent-policy-allow ') + ? 'allow' + : prompt.startsWith('/agent-policy-confirm ') + ? 'confirm' + : 'auto'; + const commandPrefix = + mode === 'deny' + ? '/agent-policy-deny ' + : mode === 'allow' + ? '/agent-policy-allow ' + : mode === 'confirm' + ? '/agent-policy-confirm ' + : '/agent-policy-auto '; + const args = prompt.slice(commandPrefix.length).trim(); + const [agentId = '', commandId = ''] = args.split(/\s+/, 2); + if (!agentId || !commandId) { + setMessages((current) => [ + ...current, + { + role: 'assistant', + text: + mode === 'deny' || mode === 'allow' + ? '格式:/agent-policy-deny design-director file.read' + : '格式:/agent-policy-confirm design-director memory.write', + }, + ]); + return; + } + if (!isRegisteredGameCreationCommandId(commandId)) { + setMessages((current) => [ + ...current, + { role: 'assistant', text: `未知内置命令:${commandId}` }, + ]); + return; + } + if ( + (mode === 'confirm' || mode === 'auto') && + !isProjectPolicyConfirmableCommandId(commandId) + ) { + setMessages((current) => [ + ...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。', + }, + ]); + return; + } + void queueAgentPolicyMutation(agentId, commandId, mode); + return; + } + if (prompt === '/llm-status') { void executeLlmConfigStatus(); return; @@ -16908,6 +17022,137 @@ export function App() { } } + async function queueAgentPolicyMutation( + agentId: string, + commandId: string, + mode: 'deny' | 'allow' | 'confirm' | 'auto', + ) { + const invoke = resolveTauriInvoke(); + if (!invoke) { + setMessages((current) => [ + ...current, + { role: 'assistant', text: '需要在 Tauri App 内运行。' }, + ]); + return; + } + const nextProjectPath = requireChatProjectForUserAction(); + if (!nextProjectPath) { + return; + } + + try { + const result = await invoke( + 'read_project_permission_policy', + { projectPath: nextProjectPath }, + ); + const agentPolicies = { ...(result.policy.agentPolicies ?? {}) }; + const agentPolicy = agentPolicies[agentId] ?? { + deniedCommands: [], + confirmCommands: [], + }; + const alreadyDenied = agentPolicy.deniedCommands.includes(commandId); + const alreadyConfirmed = agentPolicy.confirmCommands.includes(commandId); + if (mode === 'deny' && alreadyDenied) { + setMessages((current) => [ + ...current, + { + role: 'assistant', + text: `Agent ${agentId} 的命令已在拒绝列表中:${commandId}`, + }, + ]); + return; + } + if (mode === 'allow' && !alreadyDenied) { + setMessages((current) => [ + ...current, + { + role: 'assistant', + text: `Agent ${agentId} 的命令不在拒绝列表中:${commandId}`, + }, + ]); + return; + } + if (mode === 'confirm' && alreadyConfirmed) { + setMessages((current) => [ + ...current, + { + role: 'assistant', + text: `Agent ${agentId} 的命令已在确认列表中:${commandId}`, + }, + ]); + return; + } + if (mode === 'auto' && !alreadyConfirmed) { + setMessages((current) => [ + ...current, + { + role: 'assistant', + text: `Agent ${agentId} 的命令不在确认列表中:${commandId}`, + }, + ]); + return; + } + const deniedCommands = + mode === 'deny' + ? [...new Set([...agentPolicy.deniedCommands, commandId])] + : mode === 'allow' + ? agentPolicy.deniedCommands.filter( + (value) => value !== commandId, + ) + : mode === 'confirm' + ? agentPolicy.deniedCommands.filter( + (value) => value !== commandId, + ) + : agentPolicy.deniedCommands; + const confirmCommands = + mode === 'confirm' + ? [...new Set([...agentPolicy.confirmCommands, commandId])] + : mode === 'deny' + ? agentPolicy.confirmCommands.filter( + (value) => value !== commandId, + ) + : mode === 'auto' + ? agentPolicy.confirmCommands.filter( + (value) => value !== commandId, + ) + : agentPolicy.confirmCommands; + agentPolicies[agentId] = { + deniedCommands, + confirmCommands, + }; + queuePendingCommand({ + id: 'project.policy_write', + policy: { + ...result.policy, + agentPolicies, + }, + }); + const action = + mode === 'deny' + ? '拒绝' + : mode === 'allow' + ? '允许' + : mode === 'confirm' + ? '确认' + : '自动执行'; + setMessages((current) => [ + ...current, + { + role: 'assistant', + text: `准备${action} Agent ${agentId} 命令:${commandId}`, + }, + ]); + } catch (error) { + setMessages((current) => [ + ...current, + { + role: 'assistant', + text: error instanceof Error ? error.message : String(error), + }, + ]); + } + } + async function executeProjectPolicyWrite( policy: ProjectPermissionPolicy, announceToChat: boolean, diff --git a/apps/ai-game-creator-shell/tests/appSurface.test.ts b/apps/ai-game-creator-shell/tests/appSurface.test.ts index 7ff8c77ee..64a3d2860 100644 --- a/apps/ai-game-creator-shell/tests/appSurface.test.ts +++ b/apps/ai-game-creator-shell/tests/appSurface.test.ts @@ -17504,6 +17504,12 @@ describe('AI 游戏创作 App 界面边界', () => { policy: { deniedCommands, confirmCommands: ['game.static_smoke'], + agentPolicies: { + 'design-director': { + deniedCommands: ['file.read'], + confirmCommands: ['memory.write'], + }, + }, }, }; } @@ -17528,6 +17534,9 @@ describe('AI 游戏创作 App 界面边界', () => { expect(screen.queryByText(/file\.write\.13/)).toBeNull(); expect(screen.getByText(/拒绝:.*还有 2 项/)).not.toBeNull(); expect(screen.getByText(/确认:game\.static_smoke/)).not.toBeNull(); + expect( + screen.getByText(/Agent design-director:拒绝 file\.read;确认 memory\.write/), + ).not.toBeNull(); expect(invoke).toHaveBeenCalledWith('read_project_permission_policy', { projectPath: '/tmp/authorized-game', }); @@ -17541,6 +17550,12 @@ describe('AI 游戏创作 App 界面边界', () => { let policy = { deniedCommands: ['file.delete'], confirmCommands: ['game.static_smoke'], + agentPolicies: { + 'design-director': { + deniedCommands: ['file.read'], + confirmCommands: ['memory.write'], + }, + }, }; const invoke = vi.fn( async (command: string, args?: Record) => { @@ -17598,6 +17613,12 @@ describe('AI 游戏创作 App 界面边界', () => { policy: { deniedCommands: ['file.delete', 'file.write'], confirmCommands: ['game.static_smoke'], + agentPolicies: { + 'design-director': { + deniedCommands: ['file.read'], + confirmCommands: ['memory.write'], + }, + }, }, }); expect(invoke).toHaveBeenCalledWith('append_local_permission_log', { @@ -17607,6 +17628,92 @@ describe('AI 游戏创作 App 界面边界', () => { }); }); + it('updates per-agent project policy from chat after confirmation', async () => { + const manifest = createGameCreationAppManifest( + 'local-project-draft', + '未命名游戏原型', + ); + let policy = { + deniedCommands: ['project.index'], + confirmCommands: [], + agentPolicies: { + 'art-director': { + deniedCommands: [], + confirmCommands: ['memory.write'], + }, + }, + }; + const invoke = vi.fn( + async (command: string, args?: Record) => { + if (command === 'append_local_permission_log') { + return {}; + } + if (command === 'init_local_game_project') { + const projectPath = String(args?.projectPath ?? ''); + return { + projectPath, + manifestPath: `${projectPath}/.agent/manifest.json`, + manifest, + }; + } + if (command === 'read_project_permission_policy') { + return { + path: '.agent/policy.json', + policy, + }; + } + if (command === 'write_project_permission_policy') { + policy = args?.policy as typeof policy; + return { + path: '.agent/policy.json', + policy, + }; + } + throw new Error(`unexpected invoke ${command}`); + }, + ); + window.__TAURI__ = { core: { invoke } }; + renderAppAt('/'); + + submitChat('/project /tmp/authorized-game'); + fireEvent.click(screen.getByRole('button', { name: '确认' })); + expect( + await screen.findByText('已设置本地项目:/tmp/authorized-game'), + ).not.toBeNull(); + + submitChat('/agent-policy-deny design-director file.read'); + expect( + await screen.findByText('准备拒绝 Agent design-director 命令:file.read'), + ).not.toBeNull(); + expect( + screen.getByText( + /Agent:art-director 拒绝:无 确认:memory\.write;design-director 拒绝:file\.read 确认:无/, + ), + ).not.toBeNull(); + fireEvent.click(screen.getByRole('button', { name: '确认' })); + + expect( + await screen.findByText(/Agent design-director:拒绝 file\.read;确认 无/), + ).not.toBeNull(); + expect(invoke).toHaveBeenCalledWith('write_project_permission_policy', { + projectPath: '/tmp/authorized-game', + policy: { + deniedCommands: ['project.index'], + confirmCommands: [], + agentPolicies: { + 'art-director': { + deniedCommands: [], + confirmCommands: ['memory.write'], + }, + 'design-director': { + deniedCommands: ['file.read'], + confirmCommands: [], + }, + }, + }, + }); + }); + it('updates project policy confirm commands from chat after confirmation', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', @@ -18237,7 +18344,7 @@ describe('AI 游戏创作 App 界面边界', () => { expect( await screen.findByText( - '当前仅支持确认 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、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。', + '当前仅支持确认 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。', ), ).not.toBeNull(); expect(screen.queryByText('project.policy_write')).toBeNull(); diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 23cd295b9..fc6e8901c 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -4060,6 +4060,7 @@ - 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 state 新增 `toolPolicy`,从项目权限策略派生工具级 `allowedTools`、`autoTools`、`confirmTools` 和 `deniedTools`。后台 planning prompt 必须带入该快照,让 Agent 在规划阶段知道工具策略;执行阶段仍由 Runtime 白名单和项目权限 gate 决定。`blackboard.write` 继承 `memory.write` 策略,`agent.message` 继承 `conversation.write` 策略,`agent.delegate` 使用独立 `agent.delegate` 策略。 +- 2026-07-10 调整:`.agent/policy.json` 支持 `agentPolicies`,用规范 Agent id 保存单个 Agent 的 `deniedCommands / confirmCommands`。Runtime 计算有效工具策略时把项目级策略和 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 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 612805bd1..25844c759 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -34,6 +34,7 @@ Agent Runtime 负责: - 编排能力:任务拆分、任务图依赖、专业组调度、多智能体协作;Runtime V1 会为单 Agent 对话和生成 loop 中的角色 brief 写入独立 runtime state / event,先解决“每个 Agent 正在做什么、跑到哪一步、最近一次 task/run 是什么”的可观测性。 - 后台任务能力:开发窗口单 Agent 聊天和项目内 Agent 对话弹窗可把当前输入投递为单 Agent 后台任务,Tauri 命令 `start_game_creator_agent_runtime_task` 会立即写入该 Agent 的 `.agent/runtime/agents/.json`、`.agent/runtime/events/.jsonl`、`.agent/runtime/tasks/.jsonl` 和 `.agent/conversations/agents/.jsonl`,随后在 App 进程内启动 tokio task 执行最小 Agent loop:每轮让该 Agent 输出 `thinkingSummary / plan / actions / response`,Runtime 按白名单和项目权限策略执行工具动作,写入 `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。 - 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 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 的执行上限或工具权限。