use super::{KernelObservation, LoopAction, LoopPhase, LoopStateMachine};
use crate::context::pressure::PressureAction;
use crate::mm::{page_in_requests_from_calls, tier_hint_for_compress};
use crate::runtime::kernel::KernelPressureAction;
use crate::types::message::{Message, ToolCall};
use crate::types::result::TerminationReason;
const MAX_RECOVERY_ATTEMPTS: u8 = 2;
pub(crate) fn is_prompt_too_long(message: &str) -> bool {
let msg = message.to_lowercase();
msg.contains("413")
|| msg.contains("too long")
|| msg.contains("prompt too long")
|| msg.contains("context length exceeded")
|| msg.contains("context_length_exceeded")
}
impl LoopStateMachine {
pub fn recover_from_provider_error(&mut self, message: &str) -> LoopAction {
self.observations.clear();
if !is_prompt_too_long(message) {
return self.terminate(TerminationReason::Error, None);
}
if self.recovery_attempts >= MAX_RECOVERY_ATTEMPTS {
return self.terminate(TerminationReason::ContextOverflow, None);
}
self.recovery_attempts += 1;
if self.force_compact() {
self.phase = LoopPhase::Reason;
self.emit_call_llm()
} else {
self.terminate(TerminationReason::ContextOverflow, None)
}
}
pub fn force_compact(&mut self) -> bool {
let action = PressureAction::AutoCompact;
let (saved, summary, archived, cache_at) = self.ctx.force_compress();
if saved > 0 {
self.push_compression_observations(action, summary, archived, cache_at);
true
} else {
false
}
}
pub(super) fn push_compression_observations(
&mut self,
action: PressureAction,
summary: Option<String>,
archived: Vec<Message>,
invalidates_prefix_at: Option<usize>,
) {
let rho_after = self.ctx.rho();
self.observations.push(KernelObservation::Compressed {
action: KernelPressureAction::from(action),
rho_after,
summary: summary.clone(),
archived: archived.clone(),
invalidates_prefix_at,
});
if archived.is_empty() {
return;
}
let tier_hint = tier_hint_for_compress(action).label().to_string();
self.observations.push(KernelObservation::PageOut {
turn: self.turn,
action: KernelPressureAction::from(action),
rho_after,
summary,
archived,
tier_hint,
});
}
pub(super) fn execute_eviction_op(&mut self, op: &crate::mm::EvictionOp) {
use crate::mm::EvictionOp;
match op {
EvictionOp::Spool(_) => {
}
EvictionOp::Snip { per_msg_ratio: _ } => {
let (saved, summary, archived, cache_at) =
self.ctx.compress_with_time(PressureAction::SnipCompact, self.last_now_ms);
if saved > 0 || summary.is_some() {
self.push_compression_observations(
PressureAction::SnipCompact,
summary,
archived,
cache_at,
);
}
}
EvictionOp::TimeDecayMicro => {
let (_, summary, archived, cache_at) = self.ctx.compress(PressureAction::MicroCompact);
self.push_compression_observations(
PressureAction::MicroCompact,
summary,
archived,
cache_at,
);
if let Some(now_ms) = self.last_now_ms {
self.ctx.last_compact_ms = Some(now_ms);
}
}
EvictionOp::Collapse { target_tokens } => {
let (saved, summary, archived, cache_at) = self.ctx.compress_with_target(
PressureAction::ContextCollapse,
*target_tokens,
self.last_now_ms,
);
if saved > 0 || summary.is_some() {
self.push_compression_observations(
PressureAction::ContextCollapse,
summary,
archived,
cache_at,
);
}
}
EvictionOp::AutoCompact { preserve_turns: _ } => {
let (saved, summary, archived, cache_at) = self.ctx.force_compress();
if saved > 0 || summary.is_some() {
self.push_compression_observations(
PressureAction::AutoCompact,
summary,
archived,
cache_at,
);
}
}
}
}
pub(super) fn emit_page_in_requested(&mut self, calls: &[ToolCall]) {
for req in page_in_requests_from_calls(calls) {
self.observations.push(KernelObservation::PageInRequested {
turn: self.turn,
call_id: req.call_id,
tool: req.tool,
query: req.query,
top_k: req.top_k,
});
}
}
pub fn apply_page_in(&mut self, entries: &[crate::mm::PageInEntry]) {
for entry in entries {
let tokens = entry
.tokens
.unwrap_or_else(|| self.ctx.engine.count(&entry.content).max(1));
self.ctx.push_knowledge(Message::system(entry.content.clone()), tokens);
}
}
}