use crate::cm_types::{Message, message_content_byte_len_for_estimate};
fn estimate_chars_from_bytes(s: &str) -> usize {
s.len().div_ceil(2)
}
pub fn estimate_message_chars(m: &Message) -> usize {
let mut n = message_content_byte_len_for_estimate(&m.content).div_ceil(2);
n = n.saturating_add(
m.reasoning_content
.as_deref()
.map(estimate_chars_from_bytes)
.unwrap_or(0),
);
if let Some(ref tcs) = m.tool_calls {
for tc in tcs {
n = n.saturating_add(tc.function.name.len());
n = n.saturating_add(tc.function.arguments.len());
n = n.saturating_add(tc.id.len());
}
}
n
}
pub fn estimate_non_system_chars(messages: &[Message]) -> usize {
messages
.iter()
.filter(|m| m.role != "system")
.map(estimate_message_chars)
.sum()
}
pub fn compress_tool_message_contents(messages: &mut [Message], max_chars: usize) -> usize {
let max_chars = max_chars.max(256);
let mut n = 0usize;
for m in messages.iter_mut() {
if m.role != "tool" {
continue;
}
let Some(c) = &mut m.content else {
continue;
};
if let crate::cm_types::MessageContent::Text(s) = c
&& let Some(compressed) =
crate::cm_tools::tool_result::maybe_compress_tool_message_content(s, max_chars)
{
*s = compressed;
n += 1;
}
}
n
}
pub fn trim_messages_by_count(messages: &mut Vec<Message>, max_after_system: usize) -> bool {
if messages.is_empty() || max_after_system == 0 {
return false;
}
let before = messages.len();
if messages[0].role == "system" {
if messages.len() <= 1 + max_after_system {
return false;
}
let sys = messages[0].clone();
let after: Vec<Message> = messages[1..].to_vec();
let tail_keep = max_after_system;
let skip = after.len().saturating_sub(tail_keep);
let mut tail: Vec<Message> = after.iter().skip(skip).cloned().collect();
let tail_opens_with_assistant_run = tail.len() >= 2
&& tail[0].role.trim().eq_ignore_ascii_case("assistant")
&& tail[1].role.trim().eq_ignore_ascii_case("assistant");
if tail_opens_with_assistant_run
&& let Some(ui) = after[..skip]
.iter()
.rposition(|m| m.role.trim().eq_ignore_ascii_case("user"))
{
tail.insert(0, after[ui].clone());
while tail.len() > tail_keep {
if tail.len() <= 1 {
break;
}
tail.remove(1);
}
}
let mut out = vec![sys];
out.extend(tail);
*messages = out;
} else if messages.len() > max_after_system {
let skip = messages.len() - max_after_system;
*messages = messages.iter().skip(skip).cloned().collect();
}
messages.len() < before
}
pub fn trim_messages_by_char_budget(
messages: &mut Vec<Message>,
budget: usize,
min_messages_after_system: usize,
) -> bool {
if budget == 0 || messages.len() <= 1 {
return false;
}
let min_total = 1 + min_messages_after_system;
if messages.len() <= min_total {
return false;
}
let current_chars = estimate_non_system_chars(messages);
if current_chars <= budget {
return false;
}
let has_system_head = messages[0].role == "system";
let start_idx = if has_system_head { 1 } else { 0 };
let removable = messages.len().saturating_sub(min_total);
if removable == 0 {
return false;
}
let mut remaining_chars = current_chars;
let mut remove_count = 0usize;
for msg in messages.iter().skip(start_idx).take(removable) {
if remaining_chars <= budget {
break;
}
remaining_chars = remaining_chars.saturating_sub(estimate_message_chars(msg));
remove_count += 1;
}
if remove_count == 0 {
return false;
}
messages.drain(start_idx..start_idx + remove_count);
true
}
pub fn drop_orphan_tool_messages(messages: &mut Vec<Message>) -> usize {
let before_len = messages.len();
let mut keep = vec![true; messages.len()];
for i in 0..messages.len() {
if messages[i].role != "tool" {
continue;
}
let has_valid_predecessor = i > 0 && {
let mut prev = i - 1;
while prev > 0 && !keep[prev] {
prev -= 1;
}
keep[prev]
&& (messages[prev].role == "tool"
|| (messages[prev].role == "assistant"
&& messages[prev]
.tool_calls
.as_ref()
.is_some_and(|c| !c.is_empty())))
};
if !has_valid_predecessor {
keep[i] = false;
}
}
let mut idx = 0;
messages.retain(|_| {
let k = keep[idx];
idx += 1;
k
});
before_len.saturating_sub(messages.len())
}