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
10 changes: 9 additions & 1 deletion crates/buzz-agent/src/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,14 @@ impl RunCtx<'_> {
*self.turn_output_tokens = None;
*self.turn_cached_input_tokens = None;
*self.turn_total_state = TurnTotalState::Unseen;
// Per-turn handoff-attempt counter. Scoped here (not persisted in the
// session) so `BUZZ_AGENT_MAX_HANDOFFS` bounds compactions per
// `session/prompt` turn rather than per session lifetime. A
// long-lived session legitimately needs unbounded handoffs across
// prompts; the cap only exists to stop runaway within a single turn.
// The session-cumulative `handoff_count` (used in log lines) is not
// reset: it reflects total compactions since session start.
let mut handoff_attempts: usize = 0;

let mut round = 0u32;
// Per-prompt `_Stop` objection count. Bounded per prompt (not per
Expand All @@ -227,7 +235,7 @@ impl RunCtx<'_> {
// its next request — the turn continues, it is not restarted. Drain
// non-blocking; an empty queue is the common case.
self.drain_steers();
match self.maybe_handoff().await {
match self.maybe_handoff(&mut handoff_attempts).await {
HandoffOutcome::Cancelled => return Ok(StopReason::Cancelled),
// Context was just reset — the prior request's token count no
// longer describes the (now much smaller) history. Clear both
Expand Down
5 changes: 5 additions & 0 deletions crates/buzz-agent/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,11 @@ pub struct Config {
/// operators lower/raise it for other models. Set via
/// `BUZZ_AGENT_MAX_CONTEXT_TOKENS`.
pub max_context_tokens: u64,
/// Maximum context-handoff attempts permitted within a single
/// `session/prompt` turn. Caps runaway compaction loops inside one turn;
/// does NOT limit handoffs across a session's lifetime — a long-lived
/// session can compact on every successive turn without hitting this bound.
/// Set via `BUZZ_AGENT_MAX_HANDOFFS`. Default 10.
pub max_handoffs: usize,
pub max_parallel_tools: usize,
pub hook_timeout: Duration,
Expand Down
23 changes: 18 additions & 5 deletions crates/buzz-agent/src/handoff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,30 @@ you accomplished, key decisions made, what remains, and one concrete next step.
text only — no tool calls, no JSON. Stay under 8192 tokens.";

impl RunCtx<'_> {
pub(crate) async fn maybe_handoff(&mut self) -> HandoffOutcome {
pub(crate) async fn maybe_handoff(&mut self, handoff_attempts: &mut usize) -> HandoffOutcome {
if !self.should_handoff() {
return HandoffOutcome::Skipped;
}
if *self.handoff_count >= self.cfg.max_handoffs {
tracing::info!(
"handoff cap reached ({}); using truncation",
self.cfg.max_handoffs
if *handoff_attempts >= self.cfg.max_handoffs {
let projected = self.projected_handoff_input_tokens();
let threshold =
token_threshold(self.cfg.max_context_tokens, self.cfg.max_output_tokens);
tracing::warn!(
session_id = self.session_id,
reason = "preflight",
handoff_attempts = *handoff_attempts,
max_handoffs = self.cfg.max_handoffs,
projected_tokens = projected,
threshold_tokens = threshold,
"handoff cap reached; using truncation",
);
return HandoffOutcome::Skipped;
}
// Consume one attempt slot before calling summarize(). This ensures
// that empty-summary, summarize-error, and cancellation outcomes all
// burn budget — not just successful compactions — so the cap cannot
// be bypassed by a flaky summarizer.
*handoff_attempts += 1;
let prompt = self.build_handoff_prompt();
let tokens_before = self.projected_handoff_input_tokens();
let summary = tokio::select! {
Expand Down
Loading
Loading