Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 12 additions & 17 deletions src/agent/agent_loop/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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(&current_context.messages);
let estimate = context_manager::estimate_turn_start(rough_estimate, ctx_max);
Expand Down Expand Up @@ -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(&current_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.
Expand Down Expand Up @@ -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,
Expand Down
31 changes: 27 additions & 4 deletions src/agent/agent_loop/run_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2238,6 +2238,7 @@ async fn context_compacted_reports_compaction_kind() {

async fn kind_for(
summarize_fn: Option<crate::agent::compression::SummarizeFn>,
failures: u32,
) -> CompactionKind {
let mut ctx = empty_context();
ctx.messages
Expand All @@ -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::<LoopEvent>(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 {
Expand All @@ -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<crate::agent::compression::SummarizeFn> =
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<crate::agent::compression::SummarizeFn> = 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
);
}
Loading