From 2a44169ddb7e0c7865504fb67ebb762d17fe5a03 Mon Sep 17 00:00:00 2001 From: Linghong Date: Sat, 22 Aug 2026 07:09:50 +0000 Subject: [PATCH] =?UTF-8?q?=E6=BE=84=E6=B8=85=E5=8D=A1=E4=B8=8D=E5=86=8D?= =?UTF-8?q?=E5=8F=A0=E4=B8=80=E5=BC=A0=E9=80=9A=E7=94=A8=E7=A1=AE=E8=AE=A4?= =?UTF-8?q?=E5=8D=A1=EF=BC=8C=E7=AD=96=E5=88=92=E6=A0=B9=E5=A7=94=E6=B4=BE?= =?UTF-8?q?=E5=85=8D=E7=A1=AE=E8=AE=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 子 Agent 要提问时,Runtime 在 parent-wake 屏障处把问题包成 user.input_request pending(ensure_static_delegate_user_input_wait_at_locked),同一份问题再投影成 userInputRequest。这个 pending 只是问题的载体,user.input_request 也从来不在 GAME_CREATION_APP_COMMANDS 的 confirm 集合里,没有任何确认语义。但三个渲染面都 无条件把 pendingToolAction 也画成通用待确认卡,于是同一个请求出现两遍:上面是问答 卡,下面是拿 tool 名当标题、拿 questionCount/questionsSha256 这种取证摘要当副标题 的确认卡,还和问答卡在聊天视图里糊在一起。 加 agentRuntimePendingActionIsUserInputCarrier,在开发者 Runtime 面板、项目总控 概览卡和聊天视图三处统一把通用确认面判掉,只留问答卡。 立项策划根 Run 的 agent.delegate 同时改为免确认。plan 根的工具面按阶段收窄到「当前 唯一能推进链路的动作」,Delegate 阶段就只广告 agent.delegate 一个工具,让用户确认 「要不要执行唯一能做的那件事」没有决策含量,返工那一轮同理;这条链上真正由人把关的 关口是 §13.0 的 Fast GDD 审批卡,不动。source 只作弱判据,命中后必须过 validate_project_supervisor_plan_root_binding_at 强判据,否则做游戏那条根的委派确认 会被漂移或伪造的 binding 悄悄放开;绑定读盘只发生在本来就要确认的 agent.delegate 这一个组合上。 新增两条用例:Rust 侧一起断言 plan 根 delegate 免确认、plan 根上 agent.spawn_isolated 照旧确认、gui 根 delegate 照旧确认;前端侧断言载体 pending 只渲染问答卡,且 user.input_request 与 questionsSha256 都不出现在界面上。 Co-Authored-By: Claude Opus 5 --- .../src/agent/runtime_tools/policy.rs | 29 ++++++ .../src/tests/runtime_actions/policy.rs | 89 +++++++++++++++++++ .../src/features/agent-runtime/model.ts | 17 ++++ .../src/features/agent-runtime/panels.tsx | 23 +++-- .../appSurface/supervisor-runtime.suite.ts | 46 ++++++++++ 5 files changed, 199 insertions(+), 5 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/policy.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/policy.rs index 194db6402..37c8c3561 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/policy.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/policy.rs @@ -106,6 +106,35 @@ pub(crate) fn game_creator_agent_runtime_tool_policy_rule_for_run( if matches!(blocked, Some(AgentRuntimeToolPolicyBlock::Denied(_))) { return blocked; } + // 立项策划根 Run 的委派不再要人工确认。plan 根的工具面本身按阶段收窄 + // (`agent_runtime_plan_root_supervisor_tools_for_stage`):Delegate 阶段只广告 + // `agent.delegate` 这一个工具,它就是当前唯一能推进链路的动作;让用户确认「要不要 + // 执行唯一能做的那件事」没有决策含量,返工那一轮同理。这条链上真正由人把关的关口 + // 是 §13.0 的 Fast GDD 审批卡,那个不动。 + // + // source 只是弱判据,一旦它说 plan 就必须过 `validate_project_supervisor_plan_root_binding_at` + // 这个强判据——否则做游戏那条链的委派确认会被漂移或伪造的 binding 悄悄放开。绑定 + // 读盘只发生在「本来就要确认的 agent.delegate」这一个组合上,不给其它工具的每次 + // 策略判定加磁盘读。 + if command_id == "agent.delegate" + && matches!( + blocked, + Some(AgentRuntimeToolPolicyBlock::RequiresConfirmation(_)) + ) + { + match read_game_creator_agent_runtime_run_profile_binding(root, agent_id, run_id) { + Ok(Some(binding)) if binding.source == AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE => { + if let Err(error) = + validate_project_supervisor_plan_root_binding_at(root, agent_id, run_id) + { + return Some(AgentRuntimeToolPolicyBlock::Denied(error)); + } + return None; + } + Ok(_) => {} + Err(error) => return Some(AgentRuntimeToolPolicyBlock::Denied(error)), + } + } let (run_profile, _) = match agent_runtime_run_profile_identity_at( root, agent_id, diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_actions/policy.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_actions/policy.rs index 62e5c5e86..7fea36247 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_actions/policy.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_actions/policy.rs @@ -714,6 +714,95 @@ fn gui_root_delegate_and_spawn_isolated_are_unaffected_by_plan_root_symmetry() { fs::remove_dir_all(root).ok(); } +/// 立项策划根 Run 的委派免确认,并且这条豁免既不外溢到做游戏那条根,也不外溢到别的工具。 +/// +/// plan 根的工具面按阶段收窄到「当前唯一能推进链路的动作」,Delegate 阶段就只有 +/// `agent.delegate`;再要用户点一次确认没有决策含量,人的关口留在 Fast GDD 审批卡。 +/// 判据取自持久 binding 的强判据,所以这里连同 gui 根一起断言:同一份策略下,做游戏的 +/// 委派必须照旧停在待确认。 +#[test] +fn plan_root_delegate_is_auto_while_other_roots_and_tools_still_confirm() { + let root = unique_project_path(); + init_local_game_project_at(&root, "plan-root-delegate-auto", "立项策划根委派免确认") + .expect("project init"); + write_project_permission_policy_at( + &root, + ProjectPermissionPolicy { + denied_commands: Vec::new(), + confirm_commands: vec![ + "agent.delegate".to_string(), + "agent.spawn_isolated".to_string(), + ], + agent_policies: BTreeMap::new(), + }, + ) + .expect("write policy"); + + let plan_run_id = "plan-root-delegate-run"; + bind_game_creator_agent_runtime_run_profile_at( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + plan_run_id, + AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE, + Some(AGENT_RUNTIME_RUN_PROFILE_STANDARD), + None, + ) + .expect("bind plan root"); + assert!( + game_creator_agent_runtime_tool_policy_rule_for_run( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + plan_run_id, + None, + None, + "agent.delegate", + ) + .is_none(), + "立项策划根的 agent.delegate 不应再要人工确认" + ); + assert!( + matches!( + game_creator_agent_runtime_tool_policy_rule_for_run( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + plan_run_id, + None, + None, + "agent.spawn_isolated", + ), + Some(AgentRuntimeToolPolicyBlock::RequiresConfirmation(_)) + ), + "免确认只给 agent.delegate,plan 根上其它 confirm 工具必须照旧" + ); + + let gui_run_id = "gui-root-delegate-run"; + bind_game_creator_agent_runtime_run_profile_at( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + gui_run_id, + AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE, + Some(AGENT_RUNTIME_RUN_PROFILE_STANDARD), + None, + ) + .expect("bind gui root"); + assert!( + matches!( + game_creator_agent_runtime_tool_policy_rule_for_run( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + gui_run_id, + None, + None, + "agent.delegate", + ), + Some(AgentRuntimeToolPolicyBlock::RequiresConfirmation(_)) + ), + "做游戏那条根的委派确认不能被 plan 根的豁免带走" + ); + + fs::remove_dir_all(root).ok(); +} + #[tokio::test] async fn background_agent_runtime_asset_generation_respects_project_policy() { let root = unique_project_path(); diff --git a/apps/ai-game-creator-shell/src/features/agent-runtime/model.ts b/apps/ai-game-creator-shell/src/features/agent-runtime/model.ts index bf7b20546..157788f74 100644 --- a/apps/ai-game-creator-shell/src/features/agent-runtime/model.ts +++ b/apps/ai-game-creator-shell/src/features/agent-runtime/model.ts @@ -857,6 +857,23 @@ export function agentRuntimeNeedsUserInput( ); } +export const AGENT_RUNTIME_USER_INPUT_REQUEST_TOOL = 'user.input_request'; + +// `user.input_request` 的 pending 不是等待批准的动作,而是问题本身的载体:子 Agent 以 +// needs-user-input 终态退出后,Runtime 在 parent-wake 屏障处按信封原文构造这个 pending +// (`ensure_static_delegate_user_input_wait_at_locked`),同一份问题再投影成 +// `userInputRequest`。它也从来不在 `GAME_CREATION_APP_COMMANDS` 的 confirm 集合里, +// 没有任何「确认/拒绝」语义。 +// +// 通用待确认卡按 `tool` + `inputSummary` 渲染,套到它身上就是把同一个请求画两遍——标题 +// 是原始工具名,副标题是 `questionCount=… · questionsSha256=…` 这种给日志看的取证摘要。 +// 真正的交互面是问答卡,所以这里统一把通用确认面判掉。 +export function agentRuntimePendingActionIsUserInputCarrier( + action: AgentRuntimePendingToolActionSummary | null | undefined, +) { + return action?.tool === AGENT_RUNTIME_USER_INPUT_REQUEST_TOOL; +} + export function matchingAgentRuntimeForSteer( runtimes: Array, agentId: string, diff --git a/apps/ai-game-creator-shell/src/features/agent-runtime/panels.tsx b/apps/ai-game-creator-shell/src/features/agent-runtime/panels.tsx index cd30bb208..3c9ac8944 100644 --- a/apps/ai-game-creator-shell/src/features/agent-runtime/panels.tsx +++ b/apps/ai-game-creator-shell/src/features/agent-runtime/panels.tsx @@ -29,6 +29,7 @@ import { agentRuntimeCanRetry, agentRuntimeNeedsUserInput, agentRuntimeNextStepFromPhase, + agentRuntimePendingActionIsUserInputCarrier, agentRuntimePlanStepText, agentRuntimeWaitingOnFromPhase, createAgentRuntimeUserInputResponseId, @@ -398,14 +399,18 @@ export function AgentRuntimeStatusPanel({ !agentGoalStatusIsPaused(runtime.phase) && !pendingToolAction && Boolean(onRetryRuntimeTask); + const pendingActionIsUserInputCarrier = + agentRuntimePendingActionIsUserInputCarrier(pendingToolAction); const canConfirm = Boolean(runtime.runId) && Boolean(pendingToolAction?.actionId) && + !pendingActionIsUserInputCarrier && agentRuntimeCanConfirm(runtime.status) && Boolean(onConfirmRuntimeTask); const canReject = Boolean(runtime.runId) && Boolean(pendingToolAction?.actionId) && + !pendingActionIsUserInputCarrier && agentRuntimeCanConfirm(runtime.status) && Boolean(onRejectRuntimeTask); const canCompact = @@ -776,6 +781,8 @@ export function ProjectSupervisorRuntimePanel({ const pendingActionPresentation = pendingToolAction ? projectSupervisorPendingActionPresentation(pendingToolAction) : null; + const pendingActionIsUserInputCarrier = + agentRuntimePendingActionIsUserInputCarrier(pendingToolAction); const userInputRequest = runtime?.userInputRequest ?? null; const needsUserInput = agentRuntimeNeedsUserInput(runtime); const needsSupervisorReconciliation = Boolean( @@ -953,6 +960,7 @@ export function ProjectSupervisorRuntimePanel({ ) : null} {pendingToolAction && pendingActionPresentation && + !pendingActionIsUserInputCarrier && !readOnly && !needsSupervisorReconciliation ? (
void | Promise; }) { const pendingToolAction = runtime?.pendingToolAction ?? null; + const confirmableToolAction = agentRuntimePendingActionIsUserInputCarrier( + pendingToolAction, + ) + ? null + : pendingToolAction; const userInputRequest = runtime?.userInputRequest ?? null; const needsUserInput = agentRuntimeNeedsUserInput(runtime); - if (!pendingToolAction && !userInputRequest && !needsUserInput) { + if (!confirmableToolAction && !userInputRequest && !needsUserInput) { return null; } @@ -1307,12 +1320,12 @@ export function ProjectSupervisorRuntimeControls({ 待回答问题未能读取,请稍后重试。

) : null} - {pendingToolAction ? ( + {confirmableToolAction ? (
- {pendingToolAction.tool} - {pendingToolAction.inputSummary ? ( - {pendingToolAction.inputSummary} + {confirmableToolAction.tool} + {confirmableToolAction.inputSummary ? ( + {confirmableToolAction.inputSummary} ) : null}