立项策划链路关掉循环窗口记账,做游戏链路原样保留
窗口机制的用途是长 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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::{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -438,6 +438,10 @@ pub(crate) struct AgentRuntimeContextWindowTracker {
|
||||
pub(in crate::agent) observation_signatures: std::collections::BTreeSet<String>,
|
||||
pub(in crate::agent) last_window_fingerprint: Option<String>,
|
||||
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)]
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user