use super::ExperimentalStats;
use crate::provider::{ContentPart, Message, Role};
pub const SINK_MESSAGES: usize = 2;
pub const RECENT_MESSAGES: usize = 16;
pub const STREAM_MIN_MESSAGES: usize = SINK_MESSAGES + RECENT_MESSAGES + 4;
pub fn trim_middle(messages: &mut Vec<Message>) -> ExperimentalStats {
let mut stats = ExperimentalStats::default();
if messages.len() < STREAM_MIN_MESSAGES {
return stats;
}
let total = messages.len();
let mut cut_start = SINK_MESSAGES;
let mut cut_end = total - RECENT_MESSAGES;
while cut_end < total && has_open_tool_call(&messages[cut_end - 1]) {
cut_end += 1;
}
while cut_end < total && messages[cut_end].role == Role::Tool {
cut_end += 1;
}
while cut_start < cut_end && messages[cut_start].role == Role::Tool {
cut_start += 1;
}
if cut_end <= cut_start + 1 {
return stats;
}
let dropped = cut_end - cut_start;
let bytes_dropped: usize = messages[cut_start..cut_end].iter().map(approx_bytes).sum();
let marker = Message {
role: Role::User,
content: vec![ContentPart::Text {
text: format!(
"[...streaming-llm trim: {dropped} messages ({bytes_dropped} bytes) elided between sinks and recent window...]"
),
}],
};
let marker_bytes = approx_bytes(&marker);
messages.splice(cut_start..cut_end, std::iter::once(marker));
stats.snippet_hits += 1;
stats.total_bytes_saved = bytes_dropped.saturating_sub(marker_bytes);
stats
}
fn has_open_tool_call(msg: &Message) -> bool {
msg.role == Role::Assistant
&& msg
.content
.iter()
.any(|p| matches!(p, ContentPart::ToolCall { .. }))
}
fn approx_bytes(msg: &Message) -> usize {
msg.content
.iter()
.map(|p| match p {
ContentPart::Text { text } => text.len(),
ContentPart::ToolResult { content, .. } => content.len(),
ContentPart::ToolCall {
arguments, name, ..
} => arguments.len() + name.len(),
ContentPart::Thinking { .. } => 0,
ContentPart::Image { .. } | ContentPart::File { .. } => 1024,
})
.sum()
}
#[cfg(test)]
mod tests {
use super::*;
fn u(t: &str) -> Message {
Message {
role: Role::User,
content: vec![ContentPart::Text { text: t.into() }],
}
}
fn a(t: &str) -> Message {
Message {
role: Role::Assistant,
content: vec![ContentPart::Text { text: t.into() }],
}
}
#[test]
fn short_history_is_noop() {
let mut msgs = vec![u("a"), a("b"), u("c"), a("d")];
let before = msgs.clone();
let stats = trim_middle(&mut msgs);
assert_eq!(stats.total_bytes_saved, 0);
assert_eq!(msgs.len(), before.len());
}
#[test]
fn long_history_preserves_sinks_and_recency() {
let mut msgs = vec![u("SINK0"), a("SINK1")];
for i in 0..60 {
msgs.push(u(&format!("mid-user-{i}")));
msgs.push(a(&"X".repeat(300)));
}
for i in 0..RECENT_MESSAGES {
msgs.push(a(&format!("recent-{i}")));
}
let stats = trim_middle(&mut msgs);
assert!(stats.total_bytes_saved > 1000);
let ContentPart::Text { text } = &msgs[0].content[0] else {
panic!();
};
assert_eq!(text, "SINK0");
assert!(msgs.iter().any(|m| m.content.iter().any(|p| matches!(
p,
ContentPart::Text { text } if text.contains("streaming-llm trim")
))));
}
#[test]
fn does_not_split_tool_call_pair() {
let mut msgs = vec![u("sink0"), a("sink1")];
for _ in 0..40 {
msgs.push(u("filler"));
msgs.push(a("filler"));
}
let boundary = msgs.len() - RECENT_MESSAGES - 1;
msgs[boundary] = Message {
role: Role::Assistant,
content: vec![ContentPart::ToolCall {
id: "call_1".into(),
name: "read_file".into(),
arguments: "{}".into(),
thought_signature: None,
}],
};
let _ = trim_middle(&mut msgs);
for i in 0..msgs.len().saturating_sub(1) {
if has_open_tool_call(&msgs[i]) {
let next = &msgs[i + 1];
assert!(
next.role == Role::Tool || next.role == Role::Assistant,
"tool call at {i} orphaned; next role {:?}",
next.role
);
}
}
}
}