use crate::engine::context::MAX_CONTEXT_RECOVERY_ATTEMPTS;
use crate::engine::kernel_event::OverflowStrategy;
use crate::engine::streaming::{
MAX_CONTEXT_CYCLE_HANDOFFS, MAX_IN_TURN_CYCLE_ADVANCES, MAX_LOOP_GUARD_CONTINUATIONS,
MAX_STEP_LIMIT_CONTINUATIONS,
};
use crate::turn::{TurnContext, TurnLoopMode};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OuterBoundaryKind {
StepLimit,
LoopGuard,
ContextOverflowCycleHandoff,
InTurnCycleAdvance,
PreRequestCapacityHold,
ErrorEscalationCapacityHold,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct OuterBoundaryCounters {
pub step_limit_continuations: u32,
pub loop_guard_continuations: u32,
pub cycle_handoff_attempts: u32,
pub in_turn_cycle_advances: u32,
}
pub type ContinuationBoundaryCounters = OuterBoundaryCounters;
pub type ContinuationBoundaryKind = OuterBoundaryKind;
#[must_use]
pub fn step_limit_boundary_eligible(mode: TurnLoopMode, counters: OuterBoundaryCounters) -> bool {
!mode.is_plan() && counters.step_limit_continuations < MAX_STEP_LIMIT_CONTINUATIONS
}
#[must_use]
pub fn loop_guard_boundary_eligible(
mode: TurnLoopMode,
loop_guard_halted: bool,
counters: OuterBoundaryCounters,
) -> bool {
loop_guard_halted
&& !mode.is_plan()
&& counters.loop_guard_continuations < MAX_LOOP_GUARD_CONTINUATIONS
}
#[must_use]
pub fn context_recovery_attempts_exhausted(context_recovery_attempts: u8) -> bool {
context_recovery_attempts >= MAX_CONTEXT_RECOVERY_ATTEMPTS
}
#[must_use]
pub fn cycle_handoff_boundary_eligible(
mode: TurnLoopMode,
counters: OuterBoundaryCounters,
) -> bool {
!mode.is_plan() && counters.cycle_handoff_attempts < MAX_CONTEXT_CYCLE_HANDOFFS
}
#[must_use]
pub fn in_turn_cycle_advance_boundary_eligible(
mode: TurnLoopMode,
counters: OuterBoundaryCounters,
) -> bool {
!mode.is_plan() && counters.in_turn_cycle_advances < MAX_IN_TURN_CYCLE_ADVANCES
}
#[must_use]
pub const fn context_recovery_budget_after_cycle_handoff() -> u8 {
0
}
#[must_use]
pub fn context_overflow_hard_fail_message(estimated_input: usize, input_budget: usize) -> String {
format!(
"Context remains above model limit after {} recovery attempts \
(~{} token estimate, ~{} budget). Please run /compact or /clear.",
MAX_CONTEXT_RECOVERY_ATTEMPTS, estimated_input, input_budget
)
}
#[must_use]
pub const fn context_overflow_cycle_handoff_strategy() -> OverflowStrategy {
OverflowStrategy::CycleHandoff
}
#[must_use]
pub const fn context_overflow_budget_recompile_strategy() -> OverflowStrategy {
OverflowStrategy::BudgetRecompile
}
#[must_use]
pub fn step_limit_budget_after_grant(turn: &TurnContext, step_budget_increment: u32) -> u32 {
turn.max_steps.saturating_add(step_budget_increment)
}
#[must_use]
pub fn max_step_limit_grants() -> u32 {
MAX_STEP_LIMIT_CONTINUATIONS
}
#[must_use]
pub fn max_loop_guard_grants() -> u32 {
MAX_LOOP_GUARD_CONTINUATIONS
}
#[must_use]
pub fn max_context_cycle_handoffs() -> u32 {
MAX_CONTEXT_CYCLE_HANDOFFS
}
#[must_use]
pub fn max_in_turn_cycle_advances() -> u32 {
MAX_IN_TURN_CYCLE_ADVANCES
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn step_limit_boundary_respects_plan_mode_and_cap() {
let counters = OuterBoundaryCounters {
step_limit_continuations: MAX_STEP_LIMIT_CONTINUATIONS,
..Default::default()
};
assert!(!step_limit_boundary_eligible(TurnLoopMode::Agent, counters));
assert!(!step_limit_boundary_eligible(
TurnLoopMode::Plan,
Default::default()
));
assert!(step_limit_boundary_eligible(
TurnLoopMode::Agent,
Default::default()
));
}
#[test]
fn loop_guard_boundary_requires_halt_and_cap() {
let at_cap = OuterBoundaryCounters {
loop_guard_continuations: MAX_LOOP_GUARD_CONTINUATIONS,
..Default::default()
};
assert!(!loop_guard_boundary_eligible(
TurnLoopMode::Agent,
true,
at_cap
));
assert!(!loop_guard_boundary_eligible(
TurnLoopMode::Agent,
false,
Default::default()
));
assert!(loop_guard_boundary_eligible(
TurnLoopMode::Agent,
true,
Default::default()
));
}
#[test]
fn step_limit_budget_after_grant_adds_increment() {
let turn = TurnContext::new(100);
assert_eq!(step_limit_budget_after_grant(&turn, 100), 200);
}
#[test]
fn context_recovery_exhaustion_matches_max() {
assert!(!context_recovery_attempts_exhausted(0));
assert!(!context_recovery_attempts_exhausted(
MAX_CONTEXT_RECOVERY_ATTEMPTS.saturating_sub(1)
));
assert!(context_recovery_attempts_exhausted(
MAX_CONTEXT_RECOVERY_ATTEMPTS
));
}
#[test]
fn cycle_handoff_boundary_respects_plan_and_cap() {
let at_cap = OuterBoundaryCounters {
cycle_handoff_attempts: MAX_CONTEXT_CYCLE_HANDOFFS,
..Default::default()
};
assert!(!cycle_handoff_boundary_eligible(
TurnLoopMode::Plan,
Default::default()
));
assert!(!cycle_handoff_boundary_eligible(
TurnLoopMode::Agent,
at_cap
));
assert!(cycle_handoff_boundary_eligible(
TurnLoopMode::Agent,
Default::default()
));
}
#[test]
fn in_turn_cycle_advance_boundary_respects_cap() {
let at_cap = OuterBoundaryCounters {
in_turn_cycle_advances: MAX_IN_TURN_CYCLE_ADVANCES,
..Default::default()
};
assert!(!in_turn_cycle_advance_boundary_eligible(
TurnLoopMode::Plan,
Default::default()
));
assert!(!in_turn_cycle_advance_boundary_eligible(
TurnLoopMode::Agent,
at_cap
));
assert!(in_turn_cycle_advance_boundary_eligible(
TurnLoopMode::Agent,
Default::default()
));
}
}