1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
// SPDX-License-Identifier: Apache-2.0
//! Frozen-region prose compression for the proxy (#710).
//!
//! The proxy already prunes OLD tool-result content at a frozen, cache-aware
//! boundary (see [`super::history_prune`]). This module adds the *prose*
//! counterpart: when an operator opts in via `[proxy.role_aggressiveness]`,
//! system and user free-text is squeezed with a deterministic, anti-inflation
//! pass. Because the output is a pure function of `(text, aggressiveness)`, a
//! frozen-region rewrite is byte-identical on every later turn, so the provider
//! prompt-cache prefix stays valid (#498).
//!
//! Assistant turns are never passed to this module — the passthrough guarantee
//! lives at the call sites, which only invoke it for system/user roles.
use serde_json::Value;
use lean_ctx::core::aggressiveness::AggressivenessProfile;
use lean_ctx::core::tokens::count_tokens;
use lean_ctx::core::web::distill::squeeze_prose;
/// Below this many chars a string is never worth a prose pass — the squeeze can
/// only add risk, not save meaningful tokens. Keeps short instructions intact.
const MIN_PROSE_CHARS: usize = 400;
/// Code/structured symbols whose density cleanly separates source, JSON, logs
/// and tables from natural-language prose.
const CODE_SYMBOLS: &str = "{}<>;=|\\$`[]";
/// Compress a single prose string at `aggressiveness` (`0.0–1.0`).
///
/// Returns `Some(compressed)` only when the text is long enough, *looks like*
/// prose, and the squeeze actually saves tokens; otherwise `None` (leave the
/// original intact — the anti-inflation guard). Deterministic: the result is a
/// pure function of `(text, aggressiveness)`.
#[must_use]
pub fn compress_prose(text: &str, aggressiveness: f64) -> Option<String> {
if text.len() < MIN_PROSE_CHARS || !looks_like_prose(text) {
return None;
}
let profile = AggressivenessProfile::from_level(aggressiveness);
// `density_target` is the fraction of content to keep; map it to a char
// budget. Below the budget the squeeze is a near-lossless dedup pass; when it
// must actually shrink (budget < len) we use cache-safe extractive ranking
// (#895) — keeping the most central sentences instead of just the prefix —
// which falls back to truncation when the embedding engine is unavailable.
let budget = ((text.len() as f64) * profile.density_target).ceil() as usize;
let squeezed = if budget < text.len() {
crate::prose_ranker::squeeze(text, budget)
} else {
squeeze_prose(text, budget)
};
let before = count_tokens(text);
let after = count_tokens(&squeezed);
if after >= before {
return None;
}
// The squeeze drops and ranks sentences: it loses text. Under protection
// it is applied only with a recovery copy bound to the lineage of `text`
// (the shared gate); otherwise the prose stays whole and is counted.
super::compress::gate::shortened(text, squeezed)
}
/// Conservative prose gate: substantial, letter-dense, low on code symbols.
/// Excludes source code, JSON, logs and tables while accepting natural-language
/// system prompts and user turns (including bulleted instructions).
fn looks_like_prose(text: &str) -> bool {
let sample: String = text.chars().take(4000).collect();
let total = sample.chars().count();
if total < 200 {
return false;
}
let total_f = total as f32;
let alpha = sample.chars().filter(|c| c.is_alphabetic()).count() as f32;
let spaces = sample.chars().filter(|c| c.is_whitespace()).count() as f32;
let symbols = sample.chars().filter(|c| CODE_SYMBOLS.contains(*c)).count() as f32;
// Real prose has sentences; source code, JSON, logs and tables largely do
// not. This is the signal that separates `let x = f(a);`-dense code (which
// slips under a pure symbol-ratio gate) from natural-language instructions.
let sentences = sample.matches(['.', '!', '?']).count();
alpha / total_f >= 0.5
&& spaces / total_f >= 0.08
&& symbols / total_f <= 0.06
&& sentences >= 3
}
/// `true` if a content block carries a `cache_control` breakpoint — such a
/// block anchors the client's prompt cache and must never be rewritten.
fn block_has_cache_control(block: &Value) -> bool {
block.get("cache_control").is_some()
}
/// `true` if a `system` value (string or array of blocks) carries any
/// `cache_control` breakpoint. Then it anchors the client's prompt cache and
/// the whole field must be left verbatim. A plain string system prompt never
/// carries one (Anthropic places `cache_control` on blocks), so it is safe.
#[must_use]
pub fn value_has_cache_control(v: &Value) -> bool {
match v {
Value::Array(blocks) => blocks.iter().any(block_has_cache_control),
_ => false,
}
}
/// Compress a JSON *string* field in place (e.g. OpenAI message `content`).
/// Returns `true` if it was rewritten.
pub fn compress_string_field(obj: &mut Value, field: &str, aggressiveness: f64) -> bool {
let Some(text) = obj.get(field).and_then(Value::as_str) else {
return false;
};
if let Some(compressed) = compress_prose(text, aggressiveness) {
obj[field] = Value::String(compressed);
return true;
}
false
}
/// Compress every `{ "type": "text", "text": … }` block in a content array
/// (Anthropic message content / system blocks). Blocks carrying a
/// `cache_control` breakpoint are skipped so client cache anchors survive.
/// Returns the number of blocks rewritten.
pub fn compress_text_blocks(blocks: &mut [Value], aggressiveness: f64) -> u32 {
let mut count = 0;
for block in blocks.iter_mut() {
if block.get("type").and_then(Value::as_str) != Some("text")
|| block_has_cache_control(block)
{
continue;
}
let Some(text) = block.get("text").and_then(Value::as_str) else {
continue;
};
if let Some(compressed) = compress_prose(text, aggressiveness) {
block["text"] = Value::String(compressed);
count += 1;
}
}
count
}
/// Compress an Anthropic top-level `system` field, which may be a plain string
/// or an array of text blocks. Returns the number of segments rewritten.
pub fn compress_system_value(system: &mut Value, aggressiveness: f64) -> u32 {
match system {
Value::String(s) => {
if let Some(compressed) = compress_prose(s, aggressiveness) {
*s = compressed;
return 1;
}
0
}
Value::Array(blocks) => compress_text_blocks(blocks, aggressiveness),
_ => 0,
}
}
/// Compress an OpenAI chat message's `content`, which is either a plain string
/// or an array of `{type:"text", text}` parts (multimodal). Returns the number
/// of segments rewritten.
pub fn compress_message_content(msg: &mut Value, aggressiveness: f64) -> u32 {
match msg.get_mut("content") {
Some(Value::String(s)) => {
if let Some(compressed) = compress_prose(s, aggressiveness) {
*s = compressed;
return 1;
}
0
}
Some(Value::Array(parts)) => compress_text_blocks(parts, aggressiveness),
_ => 0,
}
}
/// Compress the plain-`text` parts of a Gemini `parts` array. `functionCall`,
/// `functionResponse` and `inlineData` parts are never touched (tool I/O and
/// binary), so only natural-language turns are squeezed. Returns segments
/// rewritten.
pub fn compress_gemini_text_parts(parts: &mut [Value], aggressiveness: f64) -> u32 {
let mut count = 0;
for part in parts.iter_mut() {
if part.get("functionCall").is_some()
|| part.get("functionResponse").is_some()
|| part.get("inlineData").is_some()
{
continue;
}
let Some(text) = part.get("text").and_then(Value::as_str) else {
continue;
};
if let Some(compressed) = compress_prose(text, aggressiveness) {
part["text"] = Value::String(compressed);
count += 1;
}
}
count
}
/// Rough token estimate for a JSON value (chars / 4).
#[must_use]
pub fn estimate_tokens(val: &serde_json::Value) -> usize {
let s = serde_json::to_string(val).unwrap_or_default();
s.len() / 4
}
#[cfg(test)]
mod tests {
use super::*;
fn long_prose() -> String {
// Natural-language paragraphs with a repeated sentence the squeeze can
// dedup, comfortably over the prose floor.
let p = "You are a meticulous senior engineer who values clarity and \
correctness above all. Always explain your reasoning before \
acting, and prefer small, reviewable changes over large ones. ";
format!("{p}\n\n{p}\n\n{p}")
}
#[test]
fn compresses_long_prose_deterministically() {
let _data = lean_ctx::core::data_dir::isolated_data_dir();
let _community = lean_ctx::core::policy::runtime::TestPolicyOverride::set(None);
let text = long_prose();
let a = compress_prose(&text, 0.5);
let b = compress_prose(&text, 0.5);
assert_eq!(a, b, "compress_prose must be a pure function of its inputs");
assert!(a.is_some(), "long, duplicate-rich prose must compress");
assert!(count_tokens(&a.unwrap()) < count_tokens(&text));
}
#[test]
fn anti_inflation_leaves_short_text() {
// Below the prose floor → never touched.
assert_eq!(compress_prose("Be concise.", 1.0), None);
}
#[test]
fn anti_inflation_when_no_saving_possible() {
// Long but already-unique, high-entropy prose at a=0.0 (keep everything)
// cannot get smaller → None, never a same-or-bigger rewrite.
let unique = (0..40)
.map(|i| {
format!("Distinct instruction number {i} about handling edge case {i} carefully.")
})
.collect::<Vec<_>>()
.join("\n");
assert_eq!(compress_prose(&unique, 0.0), None);
}
#[test]
fn rejects_code_like_input() {
let code = (0..40)
.map(|i| format!(" let value_{i} = compute_{i}(ctx, opts);"))
.collect::<Vec<_>>()
.join("\n");
assert!(!looks_like_prose(&code));
assert_eq!(compress_prose(&code, 1.0), None);
}
#[test]
fn rejects_json_like_input() {
let json = r#"{"a": 1, "b": {"c": [1,2,3], "d": "x"}, "e": true, "f": null}"#.repeat(20);
assert!(!looks_like_prose(&json));
}
#[test]
fn text_blocks_skip_cache_control() {
let _data = lean_ctx::core::data_dir::isolated_data_dir();
let _community = lean_ctx::core::policy::runtime::TestPolicyOverride::set(None);
let big = long_prose();
let mut blocks = vec![
serde_json::json!({"type": "text", "text": big, "cache_control": {"type": "ephemeral"}}),
serde_json::json!({"type": "text", "text": big}),
];
let n = compress_text_blocks(&mut blocks, 0.5);
assert_eq!(n, 1, "only the non-cache_control block may be rewritten");
// The cached block is byte-identical to its original.
assert!(
blocks[0]["text"]
.as_str()
.unwrap()
.contains("meticulous senior engineer"),
"cache_control block must survive verbatim"
);
}
#[test]
fn system_value_handles_string_and_array() {
let _data = lean_ctx::core::data_dir::isolated_data_dir();
let _community = lean_ctx::core::policy::runtime::TestPolicyOverride::set(None);
let big = long_prose();
let mut as_string = Value::String(big.clone());
assert_eq!(compress_system_value(&mut as_string, 0.5), 1);
let mut as_array = serde_json::json!([{"type": "text", "text": big}]);
let arr = as_array.as_array_mut().unwrap();
assert_eq!(compress_text_blocks(arr, 0.5), 1);
}
}