diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/context_bundle.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/context_bundle.rs index 542620fbd..6ca737782 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/context_bundle.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/context_bundle.rs @@ -173,18 +173,22 @@ pub(in crate::agent) fn validate_agent_runtime_pending_context( { return Err("Agent Runtime 待确认动作身份与当前状态不匹配".to_string()); } - // 转述 pending 是唯一一处 `task` 字段存的不是本 run 任务的动作:Runtime 代 + // 澄清转述是唯一一处 `task` 字段存的不是本 run 任务的场景:Runtime 代 // Supervisor 汇总子 Agent 澄清问题时,把「回答后创建唯一 continuation 委派」 // 那段指令连同 delegationId 写在这里(provider_recovery.rs),而 run 的 // current_task 始终是用户原始请求。其余五个 pending 创建点传的都是 run 的真实 // task,所以这条相等断言对它们成立,对转述则永不可能成立——D11 澄清路径实测 // 每一次都会在 resume 时被判成 needs-reconciliation,用户答案已落盘却无法继续。 // + // 豁免按 task 判、不按工具判:用户答完后整条 run 都改跑在转述任务上 + // (run_recovered_game_creator_context_on_fresh_task 传的就是 pending.task), + // 所以续跑里的 agent.run_status、agent.delegate 同样带着它。 + // // 这里只豁免文本相等这一条。pending 与 runtime 的 agent/task_id/session/run/ // source 五项身份检查在上面已经全部通过,轮次检查在下面继续执行,转述 pending // 本身也只能由 Runtime 在本 run 内生成,所以豁免不放开任何跨 run 或跨身份的 // 重放面。 - if !agent_runtime_pending_is_delegate_clarification_relay(pending) { + if !agent_runtime_task_is_delegate_clarification_relay(&pending.task) { validate_agent_runtime_context_task_parameter(root, runtime, &pending.task)?; } if pending.loop_iteration != runtime.loop_iteration { @@ -894,20 +898,36 @@ mod tests { }; let relay = build(&relay_task, &action); - assert!(agent_runtime_pending_is_delegate_clarification_relay( - &relay + assert!(agent_runtime_task_is_delegate_clarification_relay( + &relay.task )); assert_ne!(relay.task, runtime.current_task); validate_agent_runtime_pending_context(&root, &runtime, &relay) .expect("转述 pending 必须能通过身份校验,否则用户答案落盘后无法续跑"); - // 豁免只覆盖转述本身。同一工具、同一 run,但 task 不是 Runtime 生成的 - // 转述指令时,原有的相等断言必须照旧拒绝——否则这就是个洞而不是修复。 + // 用户答完后整条 run 都改跑在转述任务上,后续动作同样带着它。按工具收窄 + // 豁免会让续跑第一步 agent.run_status 就被判身份变化——run 15 实测如此, + // 所以这里显式覆盖非 user.input_request 的后续动作。 + let follow_up = AgentRuntimeToolAction { + tool: "agent.run_status".to_string(), + reason: Some("读取原委派的权威合同,准备创建唯一一次续跑委派".to_string()), + input: serde_json::json!({}), + }; + let resumed = build(&relay_task, &follow_up); + validate_agent_runtime_pending_context(&root, &runtime, &resumed) + .expect("续跑动作同样跑在转述任务上,必须通过身份校验"); + + // 豁免只覆盖 Runtime 生成的转述任务。task 不是转述指令时,原有的相等断言 + // 必须照旧拒绝——否则这就是个洞而不是修复。 let forged = build("被改写的任务", &action); - assert!(!agent_runtime_pending_is_delegate_clarification_relay( - &forged + assert!(!agent_runtime_task_is_delegate_clarification_relay( + &forged.task )); assert!(validate_agent_runtime_pending_context(&root, &runtime, &forged).is_err()); + let forged_follow_up = build("被改写的任务", &follow_up); + assert!( + validate_agent_runtime_pending_context(&root, &runtime, &forged_follow_up).is_err() + ); // 身份五项仍然照查:换一个 run_id 的转述 pending 不得被放行。 let mut cross_run = relay.clone(); diff --git a/apps/ai-game-creator-shell/src-tauri/src/user_input.rs b/apps/ai-game-creator-shell/src-tauri/src/user_input.rs index 9efa09623..017142cfb 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/user_input.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/user_input.rs @@ -17,12 +17,15 @@ pub(crate) fn agent_runtime_delegate_clarification_delegation_id(task: &str) -> .filter(|value| !value.is_empty()) } -/// 该 pending 是否由 Runtime 代 Supervisor 生成的子 Agent 澄清转述。 -pub(crate) fn agent_runtime_pending_is_delegate_clarification_relay( - pending: &AgentRuntimePendingToolAction, -) -> bool { - pending.action.tool == GAME_CREATOR_USER_INPUT_REQUEST_TOOL - && agent_runtime_delegate_clarification_delegation_id(&pending.task).is_some() +/// 该任务是否是 Runtime 代 Supervisor 生成的子 Agent 澄清转述任务。 +/// +/// 判据只看 task,不看工具:用户答完后 +/// `run_recovered_game_creator_context_on_fresh_task` 会把 `pending.task` 当作同一 +/// run 后续每一轮的任务,所以转述任务会一路传播到 `agent.run_status`、 +/// `agent.delegate` 等动作上,而不是只停在那一次 `user.input_request`。按工具收窄 +/// 会让续跑第一步就被判身份变化——run 15 实测如此。 +pub(crate) fn agent_runtime_task_is_delegate_clarification_relay(task: &str) -> bool { + agent_runtime_delegate_clarification_delegation_id(task).is_some() } pub(crate) const AGENT_RUNTIME_USER_INPUT_SCHEMA_VERSION: &str =