use chrono::{DateTime, Utc};
use entelix_core::ir::Message;
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct ConsolidationContext<'a> {
pub buffer: &'a [Message],
pub last_consolidated_at: Option<DateTime<Utc>>,
pub context_tokens_used: Option<usize>,
pub context_tokens_available: Option<usize>,
}
impl<'a> ConsolidationContext<'a> {
#[must_use]
pub const fn new(buffer: &'a [Message]) -> Self {
Self {
buffer,
last_consolidated_at: None,
context_tokens_used: None,
context_tokens_available: None,
}
}
#[must_use]
pub const fn with_last_consolidated_at(mut self, at: DateTime<Utc>) -> Self {
self.last_consolidated_at = Some(at);
self
}
#[must_use]
pub const fn with_context_tokens(mut self, used: usize, available: usize) -> Self {
self.context_tokens_used = Some(used);
self.context_tokens_available = Some(available);
self
}
}
pub trait ConsolidationPolicy: Send + Sync + 'static {
fn should_consolidate(&self, ctx: &ConsolidationContext<'_>) -> bool;
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub struct OnMessageCount {
pub max_messages: usize,
}
impl OnMessageCount {
#[must_use]
pub const fn new(max_messages: usize) -> Self {
Self { max_messages }
}
}
impl ConsolidationPolicy for OnMessageCount {
fn should_consolidate(&self, ctx: &ConsolidationContext<'_>) -> bool {
ctx.buffer.len() >= self.max_messages
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub struct OnTokenBudget {
pub max_bytes: usize,
}
impl OnTokenBudget {
#[must_use]
pub const fn new(max_bytes: usize) -> Self {
Self { max_bytes }
}
}
impl ConsolidationPolicy for OnTokenBudget {
fn should_consolidate(&self, ctx: &ConsolidationContext<'_>) -> bool {
let mut total: usize = 0;
for msg in ctx.buffer {
for part in &msg.content {
if let entelix_core::ir::ContentPart::Text { text, .. } = part {
total = total.saturating_add(text.len());
if total >= self.max_bytes {
return true;
}
}
}
}
false
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash)]
pub struct NeverConsolidate;
impl ConsolidationPolicy for NeverConsolidate {
fn should_consolidate(&self, _ctx: &ConsolidationContext<'_>) -> bool {
false
}
}
#[cfg(test)]
#[allow(clippy::indexing_slicing)]
mod tests {
use super::*;
use entelix_core::ir::Message;
#[test]
fn on_message_count_fires_at_threshold() {
let policy = OnMessageCount::new(3);
let one = vec![Message::user("a")];
let three = vec![Message::user("a"), Message::user("b"), Message::user("c")];
assert!(!policy.should_consolidate(&ConsolidationContext::new(&one)));
assert!(policy.should_consolidate(&ConsolidationContext::new(&three)));
}
#[test]
fn on_token_budget_fires_when_text_exceeds_limit() {
let policy = OnTokenBudget::new(10);
let small = vec![Message::user("hi")];
let large = vec![Message::user("hello there friend")];
assert!(!policy.should_consolidate(&ConsolidationContext::new(&small)));
assert!(policy.should_consolidate(&ConsolidationContext::new(&large)));
}
#[test]
fn never_consolidate_is_always_false() {
let policy = NeverConsolidate;
let buf = vec![Message::user("anything"); 1000];
assert!(!policy.should_consolidate(&ConsolidationContext::new(&buf)));
}
}