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
89 .as_deref()
90 .map(|c| Self::estimate_tokens(c))
91 .unwrap_or(0);
92 if let Some(rc) = reasoning_content {
93 tokens += Self::estimate_tokens(rc);
94 }
95 if let Some(tc) = tool_calls {
96 for t in tc {
97 tokens += Self::estimate_tokens(&t.name);
98 tokens += Self::estimate_tokens(&t.arguments);
99 tokens += Self::estimate_tokens(&t.id);
100 }
101 }
102 tokens
103 }
104 ChatMessage::Tool {
105 tool_call_id,
106 content,
107 } => Self::estimate_tokens(tool_call_id) + Self::estimate_tokens(content),
108 }
109 }
110
111 pub fn trim(&self, messages: &mut Vec<ChatMessage>) {
118 if messages.is_empty() || self.max_tokens == 0 {
119 return;
120 }
121
122 let total_tokens: usize = messages.iter().map(|m| Self::message_tokens(m)).sum();
123 if total_tokens <= self.max_tokens {
124 return;
125 }
126
127 let keep_first = self.keep_first_n.min(messages.len());
128 let keep_last = self
129 .keep_last_n
130 .min(messages.len().saturating_sub(keep_first));
131
132 let trim_start = keep_first;
134 let trim_end = messages.len().saturating_sub(keep_last);
135 if trim_start >= trim_end {
136 return;
137 }
138
139 let mut current_tokens: usize = total_tokens;
140 let remove_idx = trim_start;
141 let mut trim_end = trim_end;
142
143 while current_tokens > self.max_tokens && remove_idx < trim_end {
144 let removed = Self::message_tokens(&messages[remove_idx]);
145 messages.remove(remove_idx);
146 current_tokens = current_tokens.saturating_sub(removed);
147 trim_end = messages.len().saturating_sub(keep_last);
148 }
149 }
150}
151
152fn is_cjk(c: char) -> bool {
153 matches!(
154 c,
155 '\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}' )
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[test]
170 fn test_estimate_tokens_empty() {
171 assert_eq!(ContextWindowManager::estimate_tokens(""), 0);
172 }
173
174 #[test]
175 fn test_estimate_tokens_english() {
176 let text = "Hello world this is a test";
177 let tokens = ContextWindowManager::estimate_tokens(text);
178 assert!(tokens > 0 && tokens <= 15);
180 }
181
182 #[test]
183 fn test_trim_no_trim_needed() {
184 let mgr = ContextWindowManager::new(1000);
185 let mut msgs = vec![
186 ChatMessage::system("You are a helpful assistant."),
187 ChatMessage::user("Hello"),
188 ChatMessage::assistant("Hi there!"),
189 ];
190 let original_len = msgs.len();
191 mgr.trim(&mut msgs);
192 assert_eq!(msgs.len(), original_len);
193 }
194
195 #[test]
196 fn test_trim_keeps_first_and_last() {
197 let mgr = ContextWindowManager::new(8)
198 .with_keep_first_n(1)
199 .with_keep_last_n(2);
200 let mut msgs = vec![
201 ChatMessage::system("system"),
202 ChatMessage::user("message number one"),
203 ChatMessage::assistant("message number two"),
204 ChatMessage::user("message number three"),
205 ChatMessage::assistant("message number four"),
206 ChatMessage::user("message number five"),
207 ChatMessage::assistant("message number six"),
208 ];
209 mgr.trim(&mut msgs);
210 assert_eq!(msgs.len(), 3);
211 assert!(matches!(msgs[0], ChatMessage::System { .. }));
212 }
213}