agent_base/engine/
context.rs1use crate::types::ChatMessage;
2
3#[derive(Clone, Debug)]
4pub struct ContextWindowManager {
5 pub max_tokens: usize,
6 pub keep_first_n: usize,
8 pub keep_last_n: usize,
10}
11
12impl Default for ContextWindowManager {
13 fn default() -> Self {
14 Self {
15 max_tokens: 128_000,
16 keep_first_n: 1,
17 keep_last_n: 20,
18 }
19 }
20}
21
22impl ContextWindowManager {
23 const IMAGE_OVERHEAD_TOKENS: usize = 85;
25
26 pub fn new(max_tokens: usize) -> Self {
27 Self {
28 max_tokens,
29 ..Default::default()
30 }
31 }
32
33 pub fn with_keep_first_n(mut self, n: usize) -> Self {
34 self.keep_first_n = n;
35 self
36 }
37
38 pub fn with_keep_last_n(mut self, n: usize) -> Self {
39 self.keep_last_n = n;
40 self
41 }
42
43 pub fn estimate_tokens(text: &str) -> usize {
46 if text.is_empty() {
47 return 0;
48 }
49 let chars = text.chars().count();
50 let cjk_count = text.chars().filter(|c| is_cjk(*c)).count();
51 let latin_count = chars - cjk_count;
52 (cjk_count as f64 / 1.5 + latin_count as f64 / 4.0).ceil() as usize
54 }
55
56 pub(crate) fn message_tokens(msg: &ChatMessage) -> usize {
57 match msg {
58 ChatMessage::System { content, .. } => Self::estimate_tokens(content),
59 ChatMessage::User {
60 content, images, ..
61 } => {
62 let mut tokens = Self::estimate_tokens(content);
63 for img in images {
64 match img {
65 crate::types::ImageAttachment::Url { url, detail: _ } => {
66 tokens += Self::estimate_tokens(url);
67 }
68 crate::types::ImageAttachment::Base64 {
69 data,
70 media_type,
71 detail: _,
72 } => {
73 tokens += data.len() / 4;
74 if let Some(mt) = media_type {
75 tokens += Self::estimate_tokens(mt);
76 }
77 }
78 }
79 tokens += Self::IMAGE_OVERHEAD_TOKENS;
80 }
81 tokens
82 }
83 ChatMessage::Assistant {
84 content,
85 reasoning_content,
86 tool_calls,
87 } => {
88 let mut tokens = content.as_deref().map(Self::estimate_tokens).unwrap_or(0);
89 if let Some(rc) = reasoning_content {
90 tokens += Self::estimate_tokens(rc);
91 }
92 if let Some(tc) = tool_calls {
93 for t in tc {
94 tokens += Self::estimate_tokens(&t.name);
95 tokens += Self::estimate_tokens(&t.arguments);
96 tokens += Self::estimate_tokens(&t.id);
97 }
98 }
99 tokens
100 }
101 ChatMessage::Tool {
102 tool_call_id,
103 content,
104 } => Self::estimate_tokens(tool_call_id) + Self::estimate_tokens(content),
105 }
106 }
107
108 pub fn trim(&self, messages: &mut Vec<ChatMessage>) {
115 if messages.is_empty() || self.max_tokens == 0 {
116 return;
117 }
118
119 let total_tokens: usize = messages.iter().map(Self::message_tokens).sum();
120 if total_tokens <= self.max_tokens {
121 return;
122 }
123
124 let keep_first = self.keep_first_n.min(messages.len());
125 let keep_last = self
126 .keep_last_n
127 .min(messages.len().saturating_sub(keep_first));
128
129 let trim_start = keep_first;
131 let trim_end = messages.len().saturating_sub(keep_last);
132 if trim_start >= trim_end {
133 return;
134 }
135
136 let mut current_tokens: usize = total_tokens;
137 let remove_idx = trim_start;
138 let mut trim_end = trim_end;
139
140 while current_tokens > self.max_tokens && remove_idx < trim_end {
141 let removed = Self::message_tokens(&messages[remove_idx]);
142 messages.remove(remove_idx);
143 current_tokens = current_tokens.saturating_sub(removed);
144 trim_end = messages.len().saturating_sub(keep_last);
145 }
146 }
147}
148
149fn is_cjk(c: char) -> bool {
150 matches!(
151 c,
152 '\u{4E00}'..='\u{9FFF}' | '\u{3400}'..='\u{4DBF}' | '\u{3000}'..='\u{303F}' | '\u{FF00}'..='\u{FFEF}' | '\u{3040}'..='\u{309F}' | '\u{30A0}'..='\u{30FF}' | '\u{AC00}'..='\u{D7AF}' )
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn test_estimate_tokens_empty() {
168 assert_eq!(ContextWindowManager::estimate_tokens(""), 0);
169 }
170
171 #[test]
172 fn test_estimate_tokens_english() {
173 let text = "Hello world this is a test";
174 let tokens = ContextWindowManager::estimate_tokens(text);
175 assert!(tokens > 0 && tokens <= 15);
177 }
178
179 #[test]
180 fn test_trim_no_trim_needed() {
181 let mgr = ContextWindowManager::new(1000);
182 let mut msgs = vec![
183 ChatMessage::system("You are a helpful assistant."),
184 ChatMessage::user("Hello"),
185 ChatMessage::assistant("Hi there!"),
186 ];
187 let original_len = msgs.len();
188 mgr.trim(&mut msgs);
189 assert_eq!(msgs.len(), original_len);
190 }
191
192 #[test]
193 fn test_trim_keeps_first_and_last() {
194 let mgr = ContextWindowManager::new(8)
195 .with_keep_first_n(1)
196 .with_keep_last_n(2);
197 let mut msgs = vec![
198 ChatMessage::system("system"),
199 ChatMessage::user("message number one"),
200 ChatMessage::assistant("message number two"),
201 ChatMessage::user("message number three"),
202 ChatMessage::assistant("message number four"),
203 ChatMessage::user("message number five"),
204 ChatMessage::assistant("message number six"),
205 ];
206 mgr.trim(&mut msgs);
207 assert_eq!(msgs.len(), 3);
208 assert!(matches!(msgs[0], ChatMessage::System { .. }));
209 }
210}