1use serde_json::Value;
15
16use crate::core::aggressiveness::AggressivenessProfile;
17use crate::core::tokens::count_tokens;
18use crate::core::web::distill::squeeze_prose;
19
20const MIN_PROSE_CHARS: usize = 400;
23
24const CODE_SYMBOLS: &str = "{}<>;=|\\$`[]";
27
28#[must_use]
35pub fn compress_prose(text: &str, aggressiveness: f64) -> Option<String> {
36 if text.len() < MIN_PROSE_CHARS || !looks_like_prose(text) {
37 return None;
38 }
39 let profile = AggressivenessProfile::from_level(aggressiveness);
40 let budget = ((text.len() as f64) * profile.density_target).ceil() as usize;
45 let squeezed = squeeze_prose(text, budget);
46 let before = count_tokens(text);
47 let after = count_tokens(&squeezed);
48 (after < before).then_some(squeezed)
49}
50
51fn looks_like_prose(text: &str) -> bool {
55 let sample: String = text.chars().take(4000).collect();
56 let total = sample.chars().count();
57 if total < 200 {
58 return false;
59 }
60 let total_f = total as f32;
61 let alpha = sample.chars().filter(|c| c.is_alphabetic()).count() as f32;
62 let spaces = sample.chars().filter(|c| c.is_whitespace()).count() as f32;
63 let symbols = sample.chars().filter(|c| CODE_SYMBOLS.contains(*c)).count() as f32;
64 let sentences = sample.matches(['.', '!', '?']).count();
68 alpha / total_f >= 0.5
69 && spaces / total_f >= 0.08
70 && symbols / total_f <= 0.06
71 && sentences >= 3
72}
73
74fn block_has_cache_control(block: &Value) -> bool {
77 block.get("cache_control").is_some()
78}
79
80#[must_use]
85pub fn value_has_cache_control(v: &Value) -> bool {
86 match v {
87 Value::Array(blocks) => blocks.iter().any(block_has_cache_control),
88 _ => false,
89 }
90}
91
92pub fn compress_string_field(obj: &mut Value, field: &str, aggressiveness: f64) -> bool {
95 let Some(text) = obj.get(field).and_then(Value::as_str) else {
96 return false;
97 };
98 if let Some(compressed) = compress_prose(text, aggressiveness) {
99 obj[field] = Value::String(compressed);
100 return true;
101 }
102 false
103}
104
105pub fn compress_text_blocks(blocks: &mut [Value], aggressiveness: f64) -> u32 {
110 let mut count = 0;
111 for block in blocks.iter_mut() {
112 if block.get("type").and_then(Value::as_str) != Some("text")
113 || block_has_cache_control(block)
114 {
115 continue;
116 }
117 let Some(text) = block.get("text").and_then(Value::as_str) else {
118 continue;
119 };
120 if let Some(compressed) = compress_prose(text, aggressiveness) {
121 block["text"] = Value::String(compressed);
122 count += 1;
123 }
124 }
125 count
126}
127
128pub fn compress_system_value(system: &mut Value, aggressiveness: f64) -> u32 {
131 match system {
132 Value::String(s) => {
133 if let Some(compressed) = compress_prose(s, aggressiveness) {
134 *s = compressed;
135 return 1;
136 }
137 0
138 }
139 Value::Array(blocks) => compress_text_blocks(blocks, aggressiveness),
140 _ => 0,
141 }
142}
143
144pub fn compress_message_content(msg: &mut Value, aggressiveness: f64) -> u32 {
148 match msg.get_mut("content") {
149 Some(Value::String(s)) => {
150 if let Some(compressed) = compress_prose(s, aggressiveness) {
151 *s = compressed;
152 return 1;
153 }
154 0
155 }
156 Some(Value::Array(parts)) => compress_text_blocks(parts, aggressiveness),
157 _ => 0,
158 }
159}
160
161pub fn compress_gemini_text_parts(parts: &mut [Value], aggressiveness: f64) -> u32 {
166 let mut count = 0;
167 for part in parts.iter_mut() {
168 if part.get("functionCall").is_some()
169 || part.get("functionResponse").is_some()
170 || part.get("inlineData").is_some()
171 {
172 continue;
173 }
174 let Some(text) = part.get("text").and_then(Value::as_str) else {
175 continue;
176 };
177 if let Some(compressed) = compress_prose(text, aggressiveness) {
178 part["text"] = Value::String(compressed);
179 count += 1;
180 }
181 }
182 count
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 fn long_prose() -> String {
190 let p = "You are a meticulous senior engineer who values clarity and \
193 correctness above all. Always explain your reasoning before \
194 acting, and prefer small, reviewable changes over large ones. ";
195 format!("{p}\n\n{p}\n\n{p}")
196 }
197
198 #[test]
199 fn compresses_long_prose_deterministically() {
200 let text = long_prose();
201 let a = compress_prose(&text, 0.5);
202 let b = compress_prose(&text, 0.5);
203 assert_eq!(a, b, "compress_prose must be a pure function of its inputs");
204 assert!(a.is_some(), "long, duplicate-rich prose must compress");
205 assert!(count_tokens(&a.unwrap()) < count_tokens(&text));
206 }
207
208 #[test]
209 fn anti_inflation_leaves_short_text() {
210 assert_eq!(compress_prose("Be concise.", 1.0), None);
212 }
213
214 #[test]
215 fn anti_inflation_when_no_saving_possible() {
216 let unique = (0..40)
219 .map(|i| {
220 format!("Distinct instruction number {i} about handling edge case {i} carefully.")
221 })
222 .collect::<Vec<_>>()
223 .join("\n");
224 assert_eq!(compress_prose(&unique, 0.0), None);
225 }
226
227 #[test]
228 fn rejects_code_like_input() {
229 let code = (0..40)
230 .map(|i| format!(" let value_{i} = compute_{i}(ctx, opts);"))
231 .collect::<Vec<_>>()
232 .join("\n");
233 assert!(!looks_like_prose(&code));
234 assert_eq!(compress_prose(&code, 1.0), None);
235 }
236
237 #[test]
238 fn rejects_json_like_input() {
239 let json = r#"{"a": 1, "b": {"c": [1,2,3], "d": "x"}, "e": true, "f": null}"#.repeat(20);
240 assert!(!looks_like_prose(&json));
241 }
242
243 #[test]
244 fn text_blocks_skip_cache_control() {
245 let big = long_prose();
246 let mut blocks = vec![
247 serde_json::json!({"type": "text", "text": big, "cache_control": {"type": "ephemeral"}}),
248 serde_json::json!({"type": "text", "text": big}),
249 ];
250 let n = compress_text_blocks(&mut blocks, 0.5);
251 assert_eq!(n, 1, "only the non-cache_control block may be rewritten");
252 assert!(
254 blocks[0]["text"]
255 .as_str()
256 .unwrap()
257 .contains("meticulous senior engineer"),
258 "cache_control block must survive verbatim"
259 );
260 }
261
262 #[test]
263 fn system_value_handles_string_and_array() {
264 let big = long_prose();
265 let mut as_string = Value::String(big.clone());
266 assert_eq!(compress_system_value(&mut as_string, 0.5), 1);
267
268 let mut as_array = serde_json::json!([{"type": "text", "text": big}]);
269 let arr = as_array.as_array_mut().unwrap();
270 assert_eq!(compress_text_blocks(arr, 0.5), 1);
271 }
272}