2026-07-12 11:28:39 +07:00
|
|
|
//! Short-send / message shaping: compacts long conversation histories so
|
|
|
|
|
//! they fit within the provider's context window before being sent to the
|
|
|
|
|
//! LLM API.
|
2026-07-11 18:23:01 +07:00
|
|
|
use crate::dto::chat::message::ChatMessage;
|
|
|
|
|
|
2026-07-12 04:01:10 +07:00
|
|
|
const MAX_WIRE_TOKENS: usize = 2_000_000;
|
2026-07-11 18:23:01 +07:00
|
|
|
const MIN_MESSAGES_BEFORE_SHAPE: usize = 20;
|
|
|
|
|
const ENGAGE_HYSTERESIS: usize = 5;
|
|
|
|
|
|
2026-07-12 11:28:39 +07:00
|
|
|
/// Decide whether the message list should be shaped (compacted) before
|
|
|
|
|
/// sending to the LLM.
|
|
|
|
|
///
|
|
|
|
|
/// Flow: skip shaping if fewer than `MIN_MESSAGES_BEFORE_SHAPE` messages
|
|
|
|
|
/// → once past that threshold, use hysteresis (require 5 more messages
|
|
|
|
|
/// before re-engaging if shaping is currently active) to avoid oscillation.
|
|
|
|
|
///
|
|
|
|
|
/// Return: `true` if shaping should be applied.
|
2026-07-11 18:23:01 +07:00
|
|
|
pub fn should_shape(total_messages: usize, prev_shaped: bool) -> bool {
|
|
|
|
|
if total_messages < MIN_MESSAGES_BEFORE_SHAPE {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
let threshold = if prev_shaped {
|
|
|
|
|
MIN_MESSAGES_BEFORE_SHAPE + ENGAGE_HYSTERESIS
|
|
|
|
|
} else {
|
|
|
|
|
MIN_MESSAGES_BEFORE_SHAPE
|
|
|
|
|
};
|
|
|
|
|
total_messages >= threshold
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 11:28:39 +07:00
|
|
|
/// Compact a long message list by dropping middle messages and inserting
|
|
|
|
|
/// a summary placeholder.
|
|
|
|
|
///
|
|
|
|
|
/// Flow: if the estimated token count is within budget, return messages
|
|
|
|
|
/// unchanged → otherwise keep the system message and the most recent
|
|
|
|
|
/// messages (up to `MAX_WIRE_TOKENS / 200` of them) with a `[prior
|
|
|
|
|
/// conversation compacted]` system message in between.
|
|
|
|
|
///
|
|
|
|
|
/// Why: keeps context-size overhead roughly constant regardless of
|
|
|
|
|
/// session length.
|
|
|
|
|
///
|
|
|
|
|
/// Return: a new Vec<ChatMessage> that preserves the first message and
|
|
|
|
|
/// the tail.
|
2026-07-11 18:23:01 +07:00
|
|
|
pub fn shape_messages(messages: &[ChatMessage], token_count: usize) -> Vec<ChatMessage> {
|
|
|
|
|
if token_count <= MAX_WIRE_TOKENS || messages.len() < 10 {
|
|
|
|
|
return messages.to_vec();
|
|
|
|
|
}
|
|
|
|
|
let keep_recent = messages
|
|
|
|
|
.iter()
|
|
|
|
|
.rev()
|
|
|
|
|
.take(MAX_WIRE_TOKENS / 200)
|
|
|
|
|
.cloned()
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
let mut result = Vec::new();
|
|
|
|
|
if let Some(first) = messages.first() {
|
|
|
|
|
result.push(first.clone());
|
|
|
|
|
}
|
|
|
|
|
result.push(ChatMessage::system(
|
|
|
|
|
"[prior conversation compacted]".to_string(),
|
|
|
|
|
));
|
|
|
|
|
result.extend(keep_recent.into_iter().rev());
|
|
|
|
|
result
|
|
|
|
|
}
|