1use serde::Serialize;
4use serde_json::{json, Map, Value};
5
6use crate::error::{ErrorCode, ToolError};
7
8const MARKER_RESERVE: usize = 64;
10const MIN_SHORTEN_LEN: usize = 64;
12const SHORTEN_SLACK: usize = 40;
14
15#[derive(Debug, Clone)]
16pub struct Cap {
17 pub max_bytes: usize,
18 pub exempt_arrays: &'static [&'static str],
19 pub exempt_entry_max: usize,
20 pub exempt_len_max: usize,
21}
22
23fn scalar_len(v: &Value) -> usize {
24 serde_json::to_string(v).map(|s| s.len()).unwrap_or(0)
25}
26
27fn size(v: &Value, skip: &[&str]) -> usize {
29 match v {
30 Value::Array(a) => {
31 2 + a.iter().map(|e| size(e, skip)).sum::<usize>() + a.len().saturating_sub(1)
32 }
33 Value::Object(o) => {
34 2 + o
35 .iter()
36 .map(|(k, e)| {
37 let key = scalar_len(&Value::String(k.clone())) + 1;
38 if e.is_array() && skip.contains(&k.as_str()) {
39 key + 2
40 } else {
41 key + size(e, skip)
42 }
43 })
44 .sum::<usize>()
45 + o.len().saturating_sub(1)
46 }
47 other => scalar_len(other),
48 }
49}
50
51fn children<'a>(v: &'a Value, skip: &[&str], out: &mut Vec<&'a Value>) {
53 match v {
54 Value::Array(a) => out.extend(a.iter()),
55 Value::Object(o) => out.extend(
56 o.iter()
57 .filter(|(k, e)| !(e.is_array() && skip.contains(&k.as_str())))
58 .map(|(_, e)| e),
59 ),
60 _ => {}
61 }
62}
63
64fn largest(v: &Value, skip: &[&str], pick: &dyn Fn(&Value) -> Option<usize>, best: &mut usize) {
65 if let Some(n) = pick(v) {
66 *best = (*best).max(n);
67 }
68 let mut kids = Vec::new();
69 children(v, skip, &mut kids);
70 for k in kids {
71 largest(k, skip, pick, best);
72 }
73}
74
75fn for_first_mut(
76 v: &mut Value,
77 skip: &[&str],
78 pick: &dyn Fn(&Value) -> Option<usize>,
79 target: usize,
80 act: &mut dyn FnMut(&mut Value),
81) -> bool {
82 if pick(v) == Some(target) {
83 act(v);
84 return true;
85 }
86 match v {
87 Value::Array(a) => a
88 .iter_mut()
89 .any(|e| for_first_mut(e, skip, pick, target, act)),
90 Value::Object(o) => o
91 .iter_mut()
92 .filter(|(k, e)| !(e.is_array() && skip.contains(&k.as_str())))
93 .any(|(_, e)| for_first_mut(e, skip, pick, target, act)),
94 _ => false,
95 }
96}
97
98fn cut_array_tail(a: &mut Vec<Value>, excess: usize, skip: &[&str]) {
99 let mut removed = 0usize;
100 while removed < excess {
101 match a.pop() {
102 Some(e) => removed += size(&e, skip) + 1,
103 None => break,
104 }
105 }
106}
107
108fn shorten_string(s: &mut String, excess: usize) {
109 let removed = (excess + SHORTEN_SLACK).min(s.len());
110 let mut keep = s.len() - removed;
111 while !s.is_char_boundary(keep) {
112 keep -= 1;
113 }
114 let gone = s.len() - keep;
115 s.truncate(keep);
116 s.push_str(&format!("...[+{gone} bytes]"));
117}
118
119fn cut_object_tail(o: &mut Map<String, Value>, excess: usize, skip: &[&str]) {
120 let mut removed = 0usize;
121 while removed < excess {
122 match o.keys().next_back().cloned() {
123 Some(k) => {
124 if let Some(e) = o.remove(&k) {
125 removed += size(&e, skip) + scalar_len(&Value::String(k)) + 2;
126 }
127 }
128 None => break,
129 }
130 }
131}
132
133fn shrink(v: &mut Value, budget: usize, skip: &[&str]) {
136 let arr_len = |v: &Value| match v {
137 Value::Array(a) if !a.is_empty() => Some(size(v, skip)),
138 _ => None,
139 };
140 let str_len = |v: &Value| match v {
141 Value::String(s) if s.len() > MIN_SHORTEN_LEN => Some(s.len()),
142 _ => None,
143 };
144 let obj_len = |v: &Value| match v {
145 Value::Object(o) if !o.is_empty() => Some(size(v, skip)),
146 _ => None,
147 };
148 loop {
149 let cur = size(v, skip);
150 if cur <= budget {
151 return;
152 }
153 let excess = cur - budget;
154 let mut best = 0usize;
155 largest(v, skip, &arr_len, &mut best);
156 if best > 0 {
157 for_first_mut(v, skip, &arr_len, best, &mut |n| {
158 if let Value::Array(a) = n {
159 cut_array_tail(a, excess, skip)
160 }
161 });
162 continue;
163 }
164 largest(v, skip, &str_len, &mut best);
165 if best > 0 {
166 for_first_mut(v, skip, &str_len, best, &mut |n| {
167 if let Value::String(s) = n {
168 shorten_string(s, excess)
169 }
170 });
171 continue;
172 }
173 largest(v, skip, &obj_len, &mut best);
174 if best > 0 {
175 for_first_mut(v, skip, &obj_len, best, &mut |n| {
176 if let Value::Object(o) = n {
177 cut_object_tail(o, excess, skip)
178 }
179 });
180 continue;
181 }
182 *v = Value::Null;
184 return;
185 }
186}
187
188fn reduce_exempt(v: &mut Value, cap: &Cap) -> bool {
190 let mut changed = false;
191 match v {
192 Value::Object(o) => {
193 for (k, e) in o.iter_mut() {
194 if cap.exempt_arrays.contains(&k.as_str()) {
195 if let Value::Array(a) = e {
196 if a.len() > cap.exempt_len_max {
197 a.truncate(cap.exempt_len_max);
198 changed = true;
199 }
200 for entry in a.iter_mut() {
201 if scalar_or_full_len(entry) > cap.exempt_entry_max {
202 shrink(entry, cap.exempt_entry_max, &[]);
203 changed = true;
204 }
205 }
206 continue;
207 }
208 }
209 changed |= reduce_exempt(e, cap);
210 }
211 }
212 Value::Array(a) => {
213 for e in a.iter_mut() {
214 changed |= reduce_exempt(e, cap);
215 }
216 }
217 _ => {}
218 }
219 changed
220}
221
222fn scalar_or_full_len(v: &Value) -> usize {
223 size(v, &[])
224}
225
226pub fn cap_json(mut value: Value, cap: &Cap) -> Value {
229 let original = size(&value, &[]);
230 let reduced = reduce_exempt(&mut value, cap);
231 let over = size(&value, cap.exempt_arrays) > cap.max_bytes;
232 if !reduced && !over {
233 return value;
234 }
235 if over {
236 shrink(
237 &mut value,
238 cap.max_bytes.saturating_sub(MARKER_RESERVE),
239 cap.exempt_arrays,
240 );
241 }
242 let mut obj = match value {
243 Value::Object(o) => o,
244 other => {
245 let mut o = Map::new();
246 o.insert("data".to_string(), other);
247 o
248 }
249 };
250 obj.insert("truncated".to_string(), json!(true));
251 obj.insert("original_bytes".to_string(), json!(original));
252 Value::Object(obj)
253}
254
255pub fn depth_cap(value: Value, max_depth: usize) -> Value {
257 fn walk(v: Value, depth: usize, max: usize) -> Value {
258 match v {
259 Value::Array(_) | Value::Object(_) if depth > max => {
260 Value::String("<depth>".to_string())
261 }
262 Value::Array(a) => {
263 Value::Array(a.into_iter().map(|e| walk(e, depth + 1, max)).collect())
264 }
265 Value::Object(o) => Value::Object(
266 o.into_iter()
267 .map(|(k, e)| (k, walk(e, depth + 1, max)))
268 .collect(),
269 ),
270 other => other,
271 }
272 }
273 walk(value, 0, max_depth)
274}
275
276fn sensitive_key(key: &str) -> bool {
277 let k = key.to_lowercase().replace('-', "_");
278 k.replace("tokens", "").contains("token")
280 || [
281 "header",
282 "apikey",
283 "api_key",
284 "secret",
285 "password",
286 "auth",
287 "credential",
288 ]
289 .iter()
290 .any(|n| k.contains(n))
291}
292
293pub fn redact(value: &mut Value) {
295 match value {
296 Value::Object(o) => {
297 for (k, e) in o.iter_mut() {
298 if sensitive_key(k) {
299 *e = Value::String("<redacted>".to_string());
300 } else {
301 redact(e);
302 }
303 }
304 }
305 Value::Array(a) => a.iter_mut().for_each(redact),
306 Value::String(s) => {
307 let t = redact_text(s);
308 if t != *s {
309 *s = t;
310 }
311 }
312 _ => {}
313 }
314}
315
316pub fn redact_text(s: &str) -> String {
318 let mut out = String::with_capacity(s.len());
319 let mut rest = s;
320 while let Some(i) = rest.find("://") {
321 let (head, tail) = rest.split_at(i + 3);
322 out.push_str(head);
323 let end = tail
324 .find(|c: char| {
325 c.is_whitespace() || matches!(c, '/' | '?' | '#' | '"' | '\'' | '<' | '>')
326 })
327 .unwrap_or(tail.len());
328 let authority = &tail[..end];
329 match authority.rfind('@') {
330 Some(at)
331 if head[..head.len() - 3].ends_with(|c: char| {
332 c.is_ascii_alphanumeric() || matches!(c, '+' | '-' | '.')
333 }) =>
334 {
335 out.push_str(&authority[at + 1..]);
336 }
337 _ => out.push_str(authority),
338 }
339 rest = &tail[end..];
340 }
341 out.push_str(rest);
342 out
343}
344
345#[derive(Debug, Clone, Copy, PartialEq, Eq)]
346pub enum ToolsMode {
347 None,
348 Summary,
349 Full,
350}
351
352#[derive(Debug, Clone, PartialEq, Serialize)]
353pub struct Message {
354 pub id: String,
355 #[serde(rename = "type")]
356 pub r#type: String,
357 #[serde(skip_serializing_if = "Option::is_none")]
358 pub outcome: Option<String>,
359 #[serde(skip)]
360 pub created: i64,
361 pub text: String,
362 #[serde(skip)]
363 pub last_text: Option<String>,
364 pub tools: Vec<Value>,
365}
366
367fn str_of(v: &Value, key: &str) -> Option<String> {
368 v.get(key).and_then(Value::as_str).map(str::to_string)
369}
370
371pub fn compact_message(raw: &Value, reasoning: bool, tools: ToolsMode) -> Message {
372 let r#type = str_of(raw, "type").unwrap_or_default();
373 let mut m = Message {
374 id: str_of(raw, "id").unwrap_or_default(),
375 r#type: r#type.clone(),
376 outcome: None,
377 created: raw
378 .pointer("/time/created")
379 .and_then(Value::as_i64)
380 .unwrap_or(0),
381 text: String::new(),
382 last_text: None,
383 tools: Vec::new(),
384 };
385 match r#type.as_str() {
386 "user" | "synthetic" => m.text = str_of(raw, "text").unwrap_or_default(),
387 "idle" => m.outcome = str_of(raw, "outcome"),
388 "assistant" => {
389 for part in raw
390 .get("content")
391 .and_then(Value::as_array)
392 .into_iter()
393 .flatten()
394 {
395 let part_text = || str_of(part, "text").unwrap_or_default();
396 match part.get("type").and_then(Value::as_str) {
397 Some("text") => {
398 let t = part_text();
399 m.text.push_str(&t);
400 m.last_text = Some(t);
401 }
402 Some("reasoning") if reasoning => m.text.push_str(&part_text()),
403 Some("tool") => match tools {
404 ToolsMode::None => {}
405 ToolsMode::Summary => m.tools.push(json!({
406 "name": part.get("name").cloned().unwrap_or(Value::Null),
407 "status": part.pointer("/state/status").cloned().unwrap_or(Value::Null),
408 })),
409 ToolsMode::Full => m.tools.push(part.clone()),
410 },
411 _ => {}
412 }
413 }
414 }
415 _ => {}
416 }
417 m
418}
419
420fn pick(raw: &Value, keys: &[&str]) -> Map<String, Value> {
421 keys.iter()
422 .filter_map(|k| raw.get(*k).map(|v| ((*k).to_string(), v.clone())))
423 .collect()
424}
425
426pub fn compact_form_field(raw: &Value) -> Value {
428 let mut keys = vec![
429 "key",
430 "type",
431 "title",
432 "description",
433 "required",
434 "hidden",
435 "when",
436 ];
437 let extra: &[&str] = match raw.get("type").and_then(Value::as_str) {
438 Some("string") => &[
439 "format",
440 "minLength",
441 "maxLength",
442 "pattern",
443 "placeholder",
444 "default",
445 "options",
446 "custom",
447 ],
448 Some("number" | "integer") => &["minimum", "maximum", "default"],
449 Some("boolean") => &["default"],
450 Some("multiselect") => &["options", "minItems", "maxItems", "custom", "default"],
451 Some("external") => &["url"],
452 _ => &[],
453 };
454 keys.extend_from_slice(extra);
455 Value::Object(pick(raw, &keys))
456}
457
458pub fn compact_permission(raw: &Value) -> Value {
459 Value::Object(pick(
460 raw,
461 &["id", "action", "resources", "save", "message", "metadata"],
462 ))
463}
464
465pub fn unwrap_envelope(raw: Value) -> Result<(Value, Option<Value>, Option<Value>), ToolError> {
467 if let Some(tag) = raw.get("_tag").and_then(Value::as_str) {
468 let message = raw
469 .get("message")
470 .and_then(Value::as_str)
471 .unwrap_or(tag)
472 .to_string();
473 return Err(ToolError::new(ErrorCode::CliError, message).details(json!({ "tag": tag })));
474 }
475 match raw {
476 Value::Object(mut o) if o.contains_key("data") => {
477 let data = o.remove("data").unwrap_or(Value::Null);
478 let cursor = o.remove("cursor").filter(|c| !c.is_null());
479 let meta = if o.is_empty() {
480 None
481 } else {
482 Some(Value::Object(o))
483 };
484 Ok((data, cursor, meta))
485 }
486 other => Ok((other, None, None)),
487 }
488}