adk_core/
intra_compaction.rs1use crate::Event;
14use crate::Part;
15
16#[derive(Debug, Clone)]
34pub struct IntraCompactionConfig {
35 pub token_threshold: u64,
37 pub overlap_event_count: usize,
39 pub chars_per_token: u32,
41}
42
43impl Default for IntraCompactionConfig {
44 fn default() -> Self {
45 Self { token_threshold: 100_000, overlap_event_count: 10, chars_per_token: 4 }
46 }
47}
48
49pub fn estimate_tokens(events: &[Event], chars_per_token: u32) -> u64 {
75 if chars_per_token == 0 {
76 return 0;
77 }
78 let total_chars: u64 = events.iter().map(|e| estimate_event_chars(e) as u64).sum();
79 total_chars / chars_per_token as u64
80}
81
82fn estimate_event_chars(event: &Event) -> usize {
91 let mut chars = 0;
92
93 if let Some(content) = &event.llm_response.content {
94 for part in &content.parts {
95 chars += estimate_part_chars(part);
96 }
97 }
98
99 if !event.actions.state_delta.is_empty() {
101 if let Ok(json) = serde_json::to_string(&event.actions.state_delta) {
102 chars += json.len();
103 }
104 }
105
106 chars
107}
108
109fn estimate_part_chars(part: &Part) -> usize {
111 match part {
112 Part::Text { text } => text.len(),
113 Part::Thinking { thinking, .. } => thinking.len(),
114 Part::FunctionCall { name, args, .. } => {
115 name.len() + serde_json::to_string(args).map_or(0, |s| s.len())
116 }
117 Part::FunctionResponse { function_response, .. } => {
118 function_response.name.len()
119 + serde_json::to_string(&function_response.response).map_or(0, |s| s.len())
120 }
121 Part::InlineData { .. } | Part::FileData { .. } => 0,
123 Part::ServerToolCall { server_tool_call } => {
124 serde_json::to_string(server_tool_call).map_or(0, |s| s.len())
125 }
126 Part::ServerToolResponse { server_tool_response } => {
127 serde_json::to_string(server_tool_response).map_or(0, |s| s.len())
128 }
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use crate::{Content, FunctionResponseData};
136
137 #[test]
138 fn test_default_config() {
139 let config = IntraCompactionConfig::default();
140 assert_eq!(config.token_threshold, 100_000);
141 assert_eq!(config.overlap_event_count, 10);
142 assert_eq!(config.chars_per_token, 4);
143 }
144
145 #[test]
146 fn test_estimate_tokens_empty() {
147 assert_eq!(estimate_tokens(&[], 4), 0);
148 }
149
150 #[test]
151 fn test_estimate_tokens_zero_ratio() {
152 let mut event = Event::new("inv-1");
153 event.set_content(Content::new("user").with_text("Hello"));
154 assert_eq!(estimate_tokens(&[event], 0), 0);
155 }
156
157 #[test]
158 fn test_estimate_tokens_text_only() {
159 let mut event = Event::new("inv-1");
160 event.set_content(Content::new("user").with_text("Hello"));
162 assert_eq!(estimate_tokens(&[event], 4), 1);
163 }
164
165 #[test]
166 fn test_estimate_tokens_multiple_events() {
167 let mut e1 = Event::new("inv-1");
168 e1.set_content(Content::new("user").with_text("Hello")); let mut e2 = Event::new("inv-1");
170 e2.set_content(Content::new("model").with_text("World!")); assert_eq!(estimate_tokens(&[e1, e2], 4), 2);
173 }
174
175 #[test]
176 fn test_estimate_tokens_with_function_call() {
177 let mut event = Event::new("inv-1");
178 event.llm_response.content = Some(Content {
179 role: "model".to_string(),
180 parts: vec![Part::FunctionCall {
181 name: "get_weather".to_string(),
182 args: serde_json::json!({"city": "NYC"}),
183 id: None,
184 thought_signature: None,
185 }],
186 });
187 let tokens = estimate_tokens(&[event], 4);
188 assert!(tokens > 0);
190 }
191
192 #[test]
193 fn test_estimate_tokens_with_function_response() {
194 let mut event = Event::new("inv-1");
195 event.llm_response.content = Some(Content {
196 role: "function".to_string(),
197 parts: vec![Part::FunctionResponse {
198 function_response: FunctionResponseData::new(
199 "get_weather",
200 serde_json::json!({"temp": 72}),
201 ),
202 id: None,
203 }],
204 });
205 let tokens = estimate_tokens(&[event], 4);
206 assert!(tokens > 0);
207 }
208}