1use machi_compaction::max_messages::compact_max_messages;
4use machi_protocol::{MESSAGE_FRAME_TOKENS, estimate_image_tokens, estimate_text_tokens};
5use machi_types::{ContentPart, Message};
6
7pub trait ConversationState: Send {
9 fn messages(&self) -> &[Message];
11 fn append(&mut self, message: Message);
13 fn replace(&mut self, messages: Vec<Message>);
15 fn token_estimate(&self) -> u64 {
19 u64::from(estimate_messages_tokens(self.messages()))
20 }
21}
22
23#[must_use]
25pub fn estimate_messages_tokens(messages: &[Message]) -> u32 {
26 messages
27 .iter()
28 .fold(0u32, |acc, m| acc.saturating_add(estimate_one_message(m)))
29}
30
31fn estimate_one_message(m: &Message) -> u32 {
32 let mut n = MESSAGE_FRAME_TOKENS;
33 if m.parts.is_empty() {
34 n = n.saturating_add(estimate_text_tokens(&m.text()));
35 } else {
36 for part in &m.parts {
37 n = n.saturating_add(match part {
38 ContentPart::Text { text } => estimate_text_tokens(text),
39 ContentPart::Image { .. } => estimate_image_tokens(),
40 _ => 0,
41 });
42 }
43 }
44 for call in &m.tool_calls {
45 n = n.saturating_add(estimate_text_tokens(&call.name));
46 n = n.saturating_add(estimate_text_tokens(&call.arguments.to_string()));
47 }
48 n
49}
50
51#[derive(Debug, Clone, Default)]
53pub struct VecConversationState {
54 messages: Vec<Message>,
55}
56
57impl VecConversationState {
58 #[must_use]
60 pub fn new() -> Self {
61 Self::default()
62 }
63
64 #[must_use]
66 pub fn from_messages(messages: Vec<Message>) -> Self {
67 Self { messages }
68 }
69
70 pub fn compact_max_messages(&mut self, max_messages: usize) {
74 self.messages = compact_max_messages(std::mem::take(&mut self.messages), max_messages);
75 }
76}
77
78impl ConversationState for VecConversationState {
79 fn messages(&self) -> &[Message] {
80 &self.messages
81 }
82
83 fn append(&mut self, message: Message) {
84 self.messages.push(message);
85 }
86
87 fn replace(&mut self, messages: Vec<Message>) {
88 self.messages = messages;
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use machi_types::Message;
95
96 use super::*;
97
98 #[test]
99 fn max_messages_keeps_system_and_tail() {
100 let mut state = VecConversationState::from_messages(vec![
101 Message::system("sys"),
102 Message::user("1"),
103 Message::user("2"),
104 Message::user("3"),
105 Message::user("4"),
106 ]);
107 state.compact_max_messages(3);
108 let msgs = state.messages();
109 assert_eq!(msgs.len(), 3);
110 assert_eq!(msgs.first().map(Message::text).as_deref(), Some("sys"));
111 assert_eq!(msgs.get(1).map(Message::text).as_deref(), Some("3"));
112 assert_eq!(msgs.get(2).map(Message::text).as_deref(), Some("4"));
113 }
114}