From 65c3d172b21d995e79c253777476d44a573ad7cd Mon Sep 17 00:00:00 2001 From: Linghong Date: Sat, 22 Aug 2026 07:32:25 +0000 Subject: [PATCH] =?UTF-8?q?=E6=BE=84=E6=B8=85=E8=BD=AC=E8=BF=B0=E9=82=A3?= =?UTF-8?q?=E4=B8=80=E8=BD=AE=E7=9A=84=20context=20bundle=20=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E8=A2=AB=E5=88=A4=E8=BA=AB=E4=BB=BD=E4=B8=8D=E5=8C=B9?= =?UTF-8?q?=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 走过一轮澄清的立项策划必挂:策划子 Agent 完成、父 run 醒来认领委派回执时,整轮报 「恢复 Agent Runtime context bundle 失败:身份与当前任务不匹配」,用户答案已落盘却 无法继续。 bundle.task 存的是写出这份 bundle 的那一轮的任务,跟 run 的持久 task 是两个独立参数 (build_game_creator_agent_runtime_context_bundle 的 task 形参)。用户答完澄清后整条 run 改跑在转述任务上(run_recovered_game_creator_context_on_fresh_task 传的就是 pending.task),那一轮写出的 bundle 因此带着转述文本,而 task record 与 current_task 始终是用户原始请求。实测项目里两侧身份字段全等,只有 task 一项分叉。 validate_agent_runtime_pending_context 早就为 pending 入口豁免过同一条文本相等断言, 并留了注释说明转述是唯一一处 task 不等于本 run 任务的场景;bundle 这个入口漏了。这里 补上同一个豁免,只放开文本相等一条——project/agent/task_id/session/run/source/profile/ 绑定指纹与 verification_gate 三项照旧全等,转述文本也只能由 Runtime 在本 run 内生成, 不放开任何跨 run 或跨身份的重放面。 新增用例同时断言两侧:转述 bundle 必须能读回,其它文本分叉照旧 fail closed。 Co-Authored-By: Claude Opus 5 --- .../agent/runtime_protocol/context_bundle.rs | 18 ++++++- .../src-tauri/src/tests/runtime_state.rs | 53 +++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) 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 6ca737782..60f436634 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 @@ -486,6 +486,21 @@ pub(in crate::agent) fn read_game_creator_agent_runtime_context_bundle_with_supe bundle.run_profile_binding_fingerprint = runtime.run_profile_binding_fingerprint.clone(); bundle = sanitize_game_creator_agent_runtime_context_bundle(root, &bundle); } + // `bundle.task` 存的是**写出这份 bundle 的那一轮**的任务,和 run 的持久 task 是两个 + // 独立参数(build_game_creator_agent_runtime_context_bundle 的 `task` 形参)。澄清转述 + // 是唯一一处二者必然不同的场景:用户答完后整条 run 改跑在转述任务上 + // (run_recovered_game_creator_context_on_fresh_task 传的就是 pending.task),那一轮 + // 写出的 bundle 因此带着转述文本,而 task record 与 current_task 始终是用户原始请求。 + // + // validate_agent_runtime_pending_context 已经为 pending 入口豁免过同一条文本相等断言, + // 但 bundle 这个入口漏了:D11 实测走过一轮澄清后,策划子 Agent 完成、父 run 醒来认领 + // 委派回执时必定在这里判失败,用户答案已落盘却整轮报「身份与当前任务不匹配」。 + // + // 豁免同样只放开文本相等这一条。project/agent/task_id/session/run/source/profile/绑定 + // 指纹和 verification_gate 三项在上下都照旧全等,转述文本本身也只能由 Runtime 在本 run + // 内生成,所以不放开任何跨 run 或跨身份的重放面。 + let bundle_task_is_clarification_relay = + agent_runtime_task_is_delegate_clarification_relay(&bundle.task); if bundle.project_id != game_creator_agent_runtime_context_project_id(root)? || bundle.agent_id != redact_agent_runtime_project_paths(root, &runtime.agent_id, 160) || bundle.task_id != redact_agent_runtime_project_paths(root, &runtime.task_id, 160) @@ -494,7 +509,8 @@ pub(in crate::agent) fn read_game_creator_agent_runtime_context_bundle_with_supe || bundle.source != redact_agent_runtime_project_paths(root, &runtime.source, 120) || bundle.run_profile != runtime.run_profile || bundle.run_profile_binding_fingerprint != runtime.run_profile_binding_fingerprint - || validate_agent_runtime_context_task_parameter(root, runtime, &bundle.task).is_err() + || (!bundle_task_is_clarification_relay + && validate_agent_runtime_context_task_parameter(root, runtime, &bundle.task).is_err()) || bundle.verification_gate.project_id != bundle.project_id || bundle.verification_gate.agent_id != bundle.agent_id || bundle.verification_gate.run_id != bundle.run_id diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_state.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_state.rs index ced24c71c..647f2f8a6 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_state.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_state.rs @@ -1971,6 +1971,59 @@ fn legacy_context_and_pending_records_fail_closed() { fs::remove_dir_all(root).ok(); } +/// 澄清转述那一轮写出的 bundle 必须能被父 run 唤醒时读回来。 +/// +/// 用户答完澄清后整条 run 改跑在转述任务上,那一轮的 bundle 因此带着转述文本,而 run 的 +/// 持久 task 始终是用户原始请求。validate_agent_runtime_pending_context 早就为 pending 入口 +/// 豁免过同一条文本相等断言,bundle 入口漏了——D11 走过一轮澄清后,策划子 Agent 完成、父 +/// run 认领委派回执时必定在这里失败。豁免只放开转述这一种 task,别的文本照旧 fail closed。 +#[test] +fn agent_runtime_context_bundle_accepts_only_the_clarification_relay_task_divergence() { + let root = unique_project_path(); + init_local_game_project_at(&root, "project-1", "澄清转述上下文项目").expect("project init"); + let task = "贪吃蛇"; + let state = start_game_creator_agent_runtime_task_at( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + task, + "plan-clarification-relay-run", + AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE, + "等待澄清回答", + vec!["回答澄清后续跑".to_string()], + ) + .expect("start clarification runtime state"); + let context_tracker = AgentRuntimeContextWindowTracker::default(); + let relay_task = format!( + "{AGENT_RUNTIME_DELEGATE_CLARIFICATION_TASK_PREFIX}delegation-96db734b07f794a576ac5c99;questionCount=1 · questionsSha256=ef5b1201。请回答以下问题。" + ); + let relay_bundle = build_game_creator_agent_runtime_context_bundle( + &root, + &state, + &relay_task, + &AgentRuntimeToolPlan::default(), + &[], + 0, + &context_tracker, + ) + .expect("build clarification relay context bundle"); + write_game_creator_agent_runtime_context_bundle(&root, &relay_bundle) + .expect("write clarification relay bundle"); + let restored = read_game_creator_agent_runtime_context_bundle(&root, &state) + .expect("clarification relay bundle must restore") + .expect("clarification relay bundle present"); + assert_eq!(restored.task, relay_task); + + let mut unrelated_task = relay_bundle; + unrelated_task.task = "另一个不相干的任务".to_string(); + write_game_creator_agent_runtime_context_bundle(&root, &unrelated_task) + .expect("write unrelated-task bundle"); + let error = read_game_creator_agent_runtime_context_bundle(&root, &state) + .expect_err("非转述任务的文本分叉必须照旧 fail closed"); + assert!(error.contains("身份与当前任务不匹配"), "{error}"); + + fs::remove_dir_all(root).ok(); +} + #[test] fn agent_runtime_context_bundle_rejects_task_source_and_loop_identity_mismatches() { let root = unique_project_path();