From 07ab58d2b96432088f0be24c61f956ddb790b826 Mon Sep 17 00:00:00 2001 From: Linghong Date: Sat, 22 Aug 2026 11:33:17 +0000 Subject: [PATCH] =?UTF-8?q?=E7=AB=8B=E9=A1=B9=E7=AD=96=E5=88=92=E9=93=BE?= =?UTF-8?q?=E8=B7=AF=E5=85=B3=E6=8E=89=E5=BE=AA=E7=8E=AF=E7=AA=97=E5=8F=A3?= =?UTF-8?q?=E8=AE=B0=E8=B4=A6=EF=BC=8C=E5=81=9A=E6=B8=B8=E6=88=8F=E9=93=BE?= =?UTF-8?q?=E8=B7=AF=E5=8E=9F=E6=A0=B7=E4=BF=9D=E7=95=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 窗口机制的用途是长 run 的进度 checkpoint 与停滞检测。立项策划链路两样都够不着: 策划子 Run 最多五轮左右,窗口是 AGENT_RUNTIME_BACKGROUND_LOOP_LIMIT,边界一次都 不会越过,checkpoint 和停滞判定永远不产出。它在这条链路上唯一还能产出的是失败—— windowCompletedLoops 跨暂停由 continuation 携带,nextLoopIndex 却从 pending 重读, 澄清应答恢复时两者分岔,context bundle 校验器把这个分岔变成整根 run 硬失败 (线上symptom:windowCompletedLoops=6 max=5)。一个在某条链路上无法产出收益的机制, 也不该有能力向它收费。 做法是给 tracker 加 window_disabled,构造时按 agent_runtime_context_window_applies 判定:source 为 project-supervisor-plan 的策划根、agentId 为 project-planning 的 策划子,两者关掉。complete_loop 命中标志即返回 Continue,计数恒为 0,于是持久化 bundle 在任何 nextLoopIndex 下都落在校验器的允许区间内,分岔不再有落点。 from_continuation 增加 &AgentRuntimeState 参数,让编译器强制 5 个生产构造点和 4 个 测试构造点都显式表态,而不是靠默认值静默选边。字段默认 false,未标记的 tracker 保持完整记账。 做游戏链路是这套机制真正的受益方(run 跨几十轮),一行不改:window_disabled 为 false 时 complete_loop 逐字未变。autonomous 相关 309 条测试全绿。 新增两条测试,都做过 A/B: - 窗口分档:做游戏链路仍在窗口边界产出 checkpoint;策划根与策划子跑满两个窗口 长度也只返回 Continue。 - 恢复偏移:continuation 逐轮接力,复现「计数携带、序号重读」的真实形状,断言每个 nextLoopIndex 下 bundle 都能通过窗口校验。撤掉修复后此测试报 「窗口轮次无效:windowCompletedLoops=1 max=0」,与线上同一条校验。 Co-Authored-By: Claude Opus 5 --- .../agent/runtime_actions/parallel_read.rs | 2 +- .../src/agent/runtime_driver/interaction.rs | 3 +- .../src/agent/runtime_driver/main_loop.rs | 3 +- .../agent/runtime_driver/main_loop_tests.rs | 6 +- .../agent/runtime_driver/pending_execution.rs | 3 +- .../src/agent/runtime_driver/recovery_scan.rs | 3 +- .../src-tauri/src/agent/runtime_protocol.rs | 3 +- .../agent/runtime_protocol/context_window.rs | 39 ++++- .../src/agent/runtime_protocol/models.rs | 4 + .../src-tauri/src/tests/project.rs | 141 +++++++++++++++++- 10 files changed, 196 insertions(+), 11 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/parallel_read.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/parallel_read.rs index d387eaa14..e11fcd9e9 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/parallel_read.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/parallel_read.rs @@ -595,7 +595,7 @@ pub(crate) fn project_game_creator_agent_runtime_parallel_read_batch_for_test_at }); let plan = continuation.plan.clone(); let mut observations = continuation.observations.clone(); - let mut tracker = AgentRuntimeContextWindowTracker::from_continuation(&continuation); + let mut tracker = AgentRuntimeContextWindowTracker::from_continuation(&continuation, &runtime); project_game_creator_agent_runtime_parallel_read_batch( root, &mut runtime, diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/interaction.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/interaction.rs index 92ea19715..9e9a692ea 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/interaction.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/interaction.rs @@ -786,7 +786,8 @@ pub(in crate::agent) async fn continue_game_creator_agent_parallel_read_batch( let mut observations = first_pending.observations.clone(); let mut plan = first_pending.tool_plan(); let next_loop_index = usize::try_from(first_pending.loop_iteration).unwrap_or(usize::MAX); - let mut context_tracker = AgentRuntimeContextWindowTracker::from_continuation(&continuation); + let mut context_tracker = + AgentRuntimeContextWindowTracker::from_continuation(&continuation, &runtime); if let Err(error) = project_game_creator_agent_runtime_parallel_read_batch( &root, &mut runtime, diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop.rs index 50228c21e..6856908c2 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop.rs @@ -1083,7 +1083,8 @@ async fn run_game_creator_agent_background_task_pass_without_deadline( let mut plan = continuation.plan.clone(); let mut observations = continuation.observations.clone(); let start_loop_index = continuation.next_loop_index; - let mut context_tracker = AgentRuntimeContextWindowTracker::from_continuation(&continuation); + let mut context_tracker = + AgentRuntimeContextWindowTracker::from_continuation(&continuation, &runtime); let mut final_reply = None; let mut final_reply_revision = None; let mut converged = false; diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop_tests.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop_tests.rs index de9092358..24fe547b7 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop_tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop_tests.rs @@ -2861,7 +2861,8 @@ fn autonomous_parent_waits_for_active_child_while_registered_derived_visuals_nee ); let mut observations = Vec::new(); let continuation = AgentRuntimeContinuationContext::default(); - let mut context_tracker = AgentRuntimeContextWindowTracker::from_continuation(&continuation); + let mut context_tracker = + AgentRuntimeContextWindowTracker::from_continuation(&continuation, &parent_state); let blocker = autonomous_game_build_completion_blocker_at_locked(&root, &parent_state) .expect("active manifest child must block parent completion"); persist_waiting_autonomous_manifest_parent_context_at( @@ -5698,7 +5699,8 @@ fn autonomous_manifest_waiting_context_persists_without_finishing_parent_run() { let mut runtime = agent_runtime_state_from_task_record(&task_record); let mut observations = Vec::new(); let continuation = AgentRuntimeContinuationContext::default(); - let mut context_tracker = AgentRuntimeContextWindowTracker::from_continuation(&continuation); + let mut context_tracker = + AgentRuntimeContextWindowTracker::from_continuation(&continuation, &runtime); let blocker = autonomous_game_build_completion_blocker_at_locked(&root, &runtime) .expect("running manifest must block completion"); diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/pending_execution.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/pending_execution.rs index 14c2fabdb..589287d58 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/pending_execution.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/pending_execution.rs @@ -979,7 +979,8 @@ async fn continue_game_creator_agent_pending_tool_action_within_stack_boundary( usize::try_from(pending.loop_iteration).unwrap_or(usize::MAX), ) }; - let mut context_tracker = AgentRuntimeContextWindowTracker::from_continuation(&continuation); + let mut context_tracker = + AgentRuntimeContextWindowTracker::from_continuation(&continuation, &runtime); if let Some(observation) = observations.last() { context_tracker.record(observation); } diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/recovery_scan.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/recovery_scan.rs index e8f816f9e..cda673935 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/recovery_scan.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/recovery_scan.rs @@ -324,7 +324,8 @@ pub(in crate::agent) fn ensure_waiting_provider_retry_projection_at( let bundle = read_game_creator_agent_runtime_context_bundle(root, &runtime)? .ok_or_else(|| "Provider retry 等待投影缺少 Runtime context bundle".to_string())?; let continuation = continuation_from_game_creator_agent_runtime_context_bundle(bundle); - let context_tracker = AgentRuntimeContextWindowTracker::from_continuation(&continuation); + let context_tracker = + AgentRuntimeContextWindowTracker::from_continuation(&continuation, &runtime); persist_waiting_provider_retry_context_at( root, &mut runtime, diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol.rs index a3e1b3b95..466d5d582 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol.rs @@ -53,7 +53,8 @@ pub(crate) use context_bundle::{ write_game_creator_agent_runtime_context_bundle, }; pub(crate) use context_window::{ - sanitize_agent_runtime_context_observation, AgentRuntimeContextCheckpoint, + agent_runtime_context_window_applies, sanitize_agent_runtime_context_observation, + AgentRuntimeContextCheckpoint, }; #[allow(unused_imports)] pub(crate) use finalization::{ diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/context_window.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/context_window.rs index 1bff16163..31bc81698 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/context_window.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/context_window.rs @@ -1,7 +1,30 @@ use super::*; +/// Whether the fixed loop window applies to this run. +/// +/// The window exists to checkpoint progress and detect a stalled Agent across +/// long runs. The 立项策划 lane cannot reach either: its child run tops out +/// around five loops against a window of +/// `AGENT_RUNTIME_BACKGROUND_LOOP_LIMIT`, so no boundary is ever crossed and no +/// checkpoint or stall verdict is ever produced. What the bookkeeping does +/// still produce there is a way to fail — `windowCompletedLoops` is carried +/// across a pause while `nextLoopIndex` is re-read from the pending, and the +/// two drift apart on the clarification resume, which the context-bundle +/// validator turns into a hard run failure. A mechanism that cannot pay out on +/// a lane should not be able to charge it either. +/// +/// The 做游戏 lane is the real beneficiary (runs there span dozens of loops) and +/// is deliberately left untouched. +pub(crate) fn agent_runtime_context_window_applies(runtime: &AgentRuntimeState) -> bool { + runtime.source != AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE + && runtime.agent_id != crate::GAME_CREATOR_PROJECT_PLANNING_AGENT_ID +} + impl AgentRuntimeContextWindowTracker { - pub(crate) fn from_continuation(continuation: &AgentRuntimeContinuationContext) -> Self { + pub(crate) fn from_continuation( + continuation: &AgentRuntimeContinuationContext, + runtime: &AgentRuntimeState, + ) -> Self { Self { completed_loops: continuation.window_completed_loops, observation_signatures: continuation @@ -12,6 +35,7 @@ impl AgentRuntimeContextWindowTracker { .collect(), last_window_fingerprint: continuation.last_window_fingerprint.clone(), stalled: continuation.context_stalled, + window_disabled: !agent_runtime_context_window_applies(runtime), } } @@ -45,6 +69,19 @@ impl AgentRuntimeContextWindowTracker { &mut self, next_loop_index: usize, ) -> AgentRuntimeContextCheckpoint { + // Stay at zero rather than counting into a window that will never + // close. A zero count also keeps the persisted bundle inside the + // context-bundle window validator for every `nextLoopIndex`, so a + // paused-and-resumed run on this lane can no longer be failed by a + // counter that drifted from the index it is validated against. + // Stay at zero rather than counting into a window that will never + // close. A zero count also keeps the persisted bundle inside the + // context-bundle window validator for every `nextLoopIndex`, so a + // paused-and-resumed run on this lane can no longer be failed by a + // counter that drifted from the index it is validated against. + if self.window_disabled { + return AgentRuntimeContextCheckpoint::Continue; + } if self.stalled { return AgentRuntimeContextCheckpoint::Stalled; } diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/models.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/models.rs index e7084d0f7..78bce914c 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/models.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/models.rs @@ -438,6 +438,10 @@ pub(crate) struct AgentRuntimeContextWindowTracker { pub(in crate::agent) observation_signatures: std::collections::BTreeSet, pub(in crate::agent) last_window_fingerprint: Option, pub(in crate::agent) stalled: bool, + /// Set for runs the loop window does not apply to (see + /// `agent_runtime_context_window_applies`). Defaults to `false` so an + /// unmarked tracker keeps the full bookkeeping. + pub(in crate::agent) window_disabled: bool, } #[derive(Clone, Debug, Default)] diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/project.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/project.rs index 671ac80ed..a5afc2c06 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/project.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/project.rs @@ -67,7 +67,7 @@ fn agent_runtime_context_window_restores_progress_and_counts_dynamic_detail_chan .expect("read mid-window context bundle") .expect("mid-window bundle exists"); let continuation = continuation_from_game_creator_agent_runtime_context_bundle(loaded); - let mut restored = AgentRuntimeContextWindowTracker::from_continuation(&continuation); + let mut restored = AgentRuntimeContextWindowTracker::from_continuation(&continuation, &state); for next_loop_index in 4..AGENT_RUNTIME_BACKGROUND_LOOP_LIMIT { restored.record(&AgentRuntimeToolObservation { tool: "project.search".to_string(), @@ -162,7 +162,7 @@ fn agent_runtime_context_window_persists_stall_across_revision_drift_and_restart .expect("stalled context bundle exists"); assert!(loaded.context_stalled); let continuation = continuation_from_game_creator_agent_runtime_context_bundle(loaded); - let mut restored = AgentRuntimeContextWindowTracker::from_continuation(&continuation); + let mut restored = AgentRuntimeContextWindowTracker::from_continuation(&continuation, &state); assert_eq!( restored.complete_loop(AGENT_RUNTIME_BACKGROUND_LOOP_LIMIT + 1), AgentRuntimeContextCheckpoint::Stalled, @@ -263,6 +263,143 @@ fn agent_runtime_context_window_counts_distinct_agent_message_bodies() { } } +#[test] +fn agent_runtime_context_window_is_off_for_the_planning_lane_and_on_elsewhere() { + let root = unique_project_path(); + init_local_game_project_at(&root, "project-window-lane", "窗口分档项目").expect("project init"); + + // 判据只读 agent_id / source 两个字段,所以起一次普通 run 再克隆覆盖即可; + // 直接以 project-planning 起 run 需要额外的 Run Profile 绑定,与本用例无关。 + let started = start_game_creator_agent_runtime_task_at( + &root, + "design-director", + "验证窗口分档", + "window-lane-run", + "agent-background-task", + "窗口分档测试", + vec!["跑满一个窗口".to_string()], + ) + .expect("start window lane runtime state"); + let lane_state = |agent_id: &str, source: &str| { + let mut state = started.clone(); + state.agent_id = agent_id.to_string(); + state.source = source.to_string(); + state + }; + + // 做游戏链路:窗口照常记账,跑满一轮窗口后给出 checkpoint。 + let game_state = lane_state("design-director", "agent-background-task"); + assert!(agent_runtime_context_window_applies(&game_state)); + let mut game_tracker = + AgentRuntimeContextWindowTracker::from_continuation(&Default::default(), &game_state); + for next_loop_index in 1..=AGENT_RUNTIME_BACKGROUND_LOOP_LIMIT { + game_tracker.record(&AgentRuntimeToolObservation { + tool: "project.search".to_string(), + status: "ok".to_string(), + summary: format!("第 {next_loop_index} 轮"), + detail: None, + }); + let checkpoint = game_tracker.complete_loop(next_loop_index); + if next_loop_index == AGENT_RUNTIME_BACKGROUND_LOOP_LIMIT { + assert_ne!( + checkpoint, + AgentRuntimeContextCheckpoint::Continue, + "做游戏链路必须保留窗口边界 checkpoint" + ); + } else { + assert_eq!(checkpoint, AgentRuntimeContextCheckpoint::Continue); + } + } + + // 立项策划链路(策划根 + 策划子):计数恒为 0,永远不越窗口边界。 + for state in [ + lane_state( + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE, + ), + lane_state( + crate::GAME_CREATOR_PROJECT_PLANNING_AGENT_ID, + "agent-delegate", + ), + ] { + assert!(!agent_runtime_context_window_applies(&state)); + let mut tracker = + AgentRuntimeContextWindowTracker::from_continuation(&Default::default(), &state); + for next_loop_index in 1..=(AGENT_RUNTIME_BACKGROUND_LOOP_LIMIT * 2 + 1) { + tracker.record(&AgentRuntimeToolObservation { + tool: "project.search".to_string(), + status: "ok".to_string(), + summary: "重复窗口标记".to_string(), + detail: None, + }); + assert_eq!( + tracker.complete_loop(next_loop_index), + AgentRuntimeContextCheckpoint::Continue, + "立项策划链路不产出 checkpoint,也不得被判停滞" + ); + } + } +} + +/// 计数恒为 0 的直接收益:暂停后恢复时,无论 `nextLoopIndex` 落在窗口的哪个位置, +/// 持久化的 bundle 都能通过窗口校验。这正是澄清应答后 `windowCompletedLoops=6 max=5` +/// 把整根 run 判失败的那条路径。 +#[test] +fn planning_lane_context_bundle_survives_every_resume_offset() { + let root = unique_project_path(); + init_local_game_project_at(&root, "project-window-resume", "窗口恢复项目") + .expect("project init"); + let mut state = start_game_creator_agent_runtime_task_at( + &root, + "design-director", + "验证澄清恢复", + "planning-window-resume-run", + AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE, + "窗口恢复测试", + vec!["提交 GDD".to_string()], + ) + .expect("start planning runtime state"); + assert!(!agent_runtime_context_window_applies(&state)); + + // continuation 逐轮接力,复现真实恢复路径上「计数跨暂停携带、序号从 pending 重读」 + // 的形状——这正是两个源分岔的地方。 + let mut continuation = AgentRuntimeContinuationContext::default(); + for next_loop_index in 0..=(AGENT_RUNTIME_BACKGROUND_LOOP_LIMIT * 2) { + let mut tracker = + AgentRuntimeContextWindowTracker::from_continuation(&continuation, &state); + tracker.record(&AgentRuntimeToolObservation { + tool: "project.search".to_string(), + status: "ok".to_string(), + summary: "窗口标记".to_string(), + detail: None, + }); + assert_eq!( + tracker.complete_loop(next_loop_index), + AgentRuntimeContextCheckpoint::Continue + ); + let bundle = build_game_creator_agent_runtime_context_bundle( + &root, + &state, + &state.current_task, + &AgentRuntimeToolPlan::default(), + &[], + next_loop_index, + &tracker, + ) + .expect("build planning context bundle"); + write_game_creator_agent_runtime_context_bundle(&root, &bundle) + .expect("write planning context bundle"); + state.loop_iteration = u32::try_from(next_loop_index).unwrap_or(u32::MAX); + let loaded = read_game_creator_agent_runtime_context_bundle(&root, &state) + .unwrap_or_else(|error| { + panic!("nextLoopIndex={next_loop_index} 的策划 bundle 必须通过窗口校验:{error}") + }) + .unwrap_or_else(|| panic!("nextLoopIndex={next_loop_index} 的策划 bundle 必须存在")); + assert_eq!(loaded.window_completed_loops, 0); + continuation = continuation_from_game_creator_agent_runtime_context_bundle(loaded); + } +} + #[test] fn platform_art_asset_output_path_rejects_escape_overwrite_and_symlink() { let root = unique_project_path();