diff --git a/src/agent/agent_loop/run.rs b/src/agent/agent_loop/run.rs index 7bd6d85a..da924126 100644 --- a/src/agent/agent_loop/run.rs +++ b/src/agent/agent_loop/run.rs @@ -633,6 +633,17 @@ pub async fn run_loop( // the intended 3-round budget (review fix). First record wins. let mut compaction_recorded_this_iter = false; + // The model's context window is constant within one inner-loop + // iteration — the model can only change at a turn boundary + // (prepareNextTurn), after the post-usage decision. Look it up + // once and reuse at all three sites that need it: the turn-start + // fold, the per-result snip cap, and the post-usage decision. + let ctx_max = config + .model_name + .as_deref() + .and_then(crate::config::context_window_for_model) + .unwrap_or(128_000); + // Pi lines 175-179: turn_start (skipped on very first // iteration — the outer wrapper already emitted it). if !first_turn { @@ -660,11 +671,6 @@ pub async fn run_loop( // count (otherwise array content was 0 and the // estimate stayed at 0% forever). if !folded_this_turn { - let ctx_max = config - .model_name - .as_deref() - .and_then(crate::config::context_window_for_model) - .unwrap_or(128_000); let rough_estimate = crate::agent::compression::estimate_messages_tokens(¤t_context.messages); let estimate = context_manager::estimate_turn_start(rough_estimate, ctx_max); @@ -743,15 +749,9 @@ pub async fn run_loop( // the cap tightens (3000 → 1000 tokens) so a single oversized // result can't push the NEXT request over the limit before // the (reactive) post-response fold fires. - let cap_ctx_max = config - .model_name - .as_deref() - .and_then(crate::config::context_window_for_model) - .unwrap_or(128_000); let cap_estimate = crate::agent::compression::estimate_messages_tokens(¤t_context.messages); - let result_cap = - crate::agent::compression::tiered_result_cap(cap_estimate, cap_ctx_max); + let result_cap = crate::agent::compression::tiered_result_cap(cap_estimate, ctx_max); // Counted variant (IMPROVEMENTS_PLAN #4): track how much the // snip freed so the post-response fold can be skipped if it // bought enough headroom. @@ -997,11 +997,6 @@ pub async fn run_loop( // into the stream pipeline (future phase). With None, // decision defaults to None (carry on). { - let ctx_max = config - .model_name - .as_deref() - .and_then(crate::config::context_window_for_model) - .unwrap_or(128_000); let decision = context_manager::decide_after_usage( token_usage.map(|u| u.input_tokens), ctx_max, diff --git a/src/agent/agent_loop/run_tests.rs b/src/agent/agent_loop/run_tests.rs index 14649747..dc5fb623 100644 --- a/src/agent/agent_loop/run_tests.rs +++ b/src/agent/agent_loop/run_tests.rs @@ -2238,6 +2238,7 @@ async fn context_compacted_reports_compaction_kind() { async fn kind_for( summarize_fn: Option, + failures: u32, ) -> CompactionKind { let mut ctx = empty_context(); ctx.messages @@ -2253,7 +2254,7 @@ async fn context_compacted_reports_compaction_kind() { ctx.messages .push(serde_json::json!({"role":"user","content":"latest"})); let (tx, mut rx) = mpsc::channel::(8); - super::run_compaction_pass(&mut ctx, &summarize_fn, 5, 0, &None, None, &tx).await; + super::run_compaction_pass(&mut ctx, &summarize_fn, 5, failures, &None, None, &tx).await; drop(tx); while let Some(ev) = rx.recv().await { if let LoopEvent::ContextCompacted { @@ -2275,15 +2276,37 @@ async fn context_compacted_reports_compaction_kind() { }) }, )); - assert_eq!(kind_for(good).await, CompactionKind::PruneAndSummary); + assert_eq!(kind_for(good, 0).await, CompactionKind::PruneAndSummary); // Failing summary → PruneAndFailedSummary. let bad: Option = Some(std::sync::Arc::new(|_p: String| { Box::pin(async move { Err(anyhow::anyhow!("boom")) }) })); - assert_eq!(kind_for(bad).await, CompactionKind::PruneAndFailedSummary); + assert_eq!( + kind_for(bad, 0).await, + CompactionKind::PruneAndFailedSummary + ); // No summarizer wired → PruneOnly. - assert_eq!(kind_for(None).await, CompactionKind::PruneOnly); + assert_eq!(kind_for(None, 0).await, CompactionKind::PruneOnly); + + // Summarizer wired but the circuit breaker is OPEN (failures at the + // cap) → PruneSummarizerDisabled, NOT PruneOnly. The distinct kind + // keeps the ongoing-failure signal visible after the breaker latches + // instead of masquerading as a healthy no-summarizer pass. Use a + // summarizer that would SUCCEED if called, to prove the kind comes + // from the breaker being open and not from the summarizer's outcome. + let would_succeed: Option = Some(std::sync::Arc::new( + |_p: String| { + Box::pin(async move { + Ok("## Active Task\nx\n\n## Goal\ny\n\n## Completed Actions\n1. z\n\n## Remaining Work\nw" + .to_string()) + }) + }, + )); + assert_eq!( + kind_for(would_succeed, super::MAX_CONSECUTIVE_COMPACTION_FAILURES).await, + CompactionKind::PruneSummarizerDisabled + ); }