use super::{ApiClient, BareLoop, LoopError};
#[cfg(feature = "hooks")]
use super::{CompactTrigger, Instant, PostCompactContext, PreCompactContext};
use crate::compact::EnsureContextResult;
use crate::capabilities::Compactable;
#[cfg(feature = "hooks")]
use crate::capabilities::Hookable;
use crate::message::Message;
use crate::observer::CompactedContext;
pub(super) struct CompactStepOutcome {
pub(super) tokens_before: u64,
pub(super) tokens_after: u64,
pub(super) compacted: Option<Vec<Message>>,
}
impl<C: ApiClient> BareLoop<C> {
pub(super) async fn run_compaction(
&mut self,
turn: usize,
reason: crate::compact::types::CompactReason,
) -> Result<CompactStepOutcome, LoopError> {
let history = self.machine.full_history();
let tokens_before = self
.count_context(&history)
.saturating_add(self.overhead_tokens());
let Some(ctx_manager) = self.managers.context_manager() else {
return Ok(CompactStepOutcome {
tokens_before,
tokens_after: tokens_before,
compacted: None,
});
};
#[cfg(feature = "hooks")]
let hook = self.pre_compact_hook(&history, reason);
#[cfg(feature = "hooks")]
if hook.abort {
return Ok(CompactStepOutcome {
tokens_before,
tokens_after: tokens_before,
compacted: None,
});
}
#[cfg(not(feature = "hooks"))]
let (instructions, additional_context) = (None, Vec::new());
#[cfg(feature = "hooks")]
let messages_before = history.len();
#[cfg(feature = "hooks")]
let compact_start = Instant::now();
#[cfg(feature = "hooks")]
let (instructions, additional_context) = (hook.new_instructions, hook.additional_context);
let reserved = self
.overhead_tokens()
.saturating_add(std::mem::take(&mut self.deferred_transient_tokens));
let result = ctx_manager
.compact_with_reason(
history,
turn,
reason,
instructions,
additional_context,
reserved,
)
.await;
match result {
Ok(EnsureContextResult::Compacted(outcome)) => {
let tokens_after = outcome.tokens_after.saturating_add(self.overhead_tokens());
let tokens_saved = tokens_before.saturating_sub(tokens_after);
#[cfg(feature = "hooks")]
let messages_after = outcome.messages.len();
self.managers.observers().on_compaction(&CompactedContext {
tokens_before,
tokens_after,
tokens_saved,
});
#[cfg(feature = "hooks")]
self.notify_post_compact_hook(
messages_before,
messages_after,
tokens_after,
tokens_saved,
compact_start.elapsed(),
);
Ok(CompactStepOutcome {
tokens_before,
tokens_after,
compacted: Some(outcome.messages),
})
}
Ok(EnsureContextResult::NoAction(messages)) => {
let tokens_after = self
.count_context(&messages)
.saturating_add(self.overhead_tokens());
Ok(CompactStepOutcome {
tokens_before,
tokens_after,
compacted: None,
})
}
Err(overflow) => {
if let Some(error) = &overflow.compactor_error {
tracing::warn!(
error = %error,
"compaction failed; reporting the context overflow it could not fix"
);
}
Err(LoopError::ContextExceeded {
used: overflow.tokens_used,
limit: overflow.context_window,
})
}
}
}
#[cfg(feature = "hooks")]
fn pre_compact_hook(
&self,
history: &[Message],
reason: crate::compact::types::CompactReason,
) -> crate::hooks::context::CompactResult {
let Some(executor) = self.managers.hook_executor() else {
return crate::hooks::context::CompactResult::allow();
};
let tokens_before = self
.count_context(history)
.saturating_add(self.overhead_tokens());
let ctx = PreCompactContext {
trigger: CompactTrigger::from(reason),
custom_instructions: None,
message_count: history.len(),
tokens_before,
context_window: self.session.config.context_window,
session_id: self.session.id,
};
executor.check_pre_compact(&ctx)
}
#[cfg(feature = "hooks")]
fn notify_post_compact_hook(
&self,
messages_before: usize,
messages_after: usize,
tokens_after: u64,
tokens_saved: u64,
duration: std::time::Duration,
) {
let Some(executor) = self.managers.hook_executor() else {
return;
};
let ctx = PostCompactContext {
trigger: CompactTrigger::Auto,
messages_compacted: messages_before.saturating_sub(messages_after),
tokens_saved,
tokens_after,
duration_ms: Self::millis_u64(duration),
session_id: self.session.id,
};
executor.notify_post_compact(&ctx);
}
}