mod detect;
mod diff;
mod generated;
mod grep;
mod log;
mod normalize;
mod plaintext;
pub(crate) mod signals;
mod template;
use std::collections::HashSet;
use anyhow::Result;
use serde_json::Value;
use crate::gate::{GateKind, PlanEntry, Scope, Transform};
use crate::ir::Request;
use crate::provider::Provider;
use crate::stages::tools::lex_words;
use detect::OutKind;
const HEAD: usize = 2;
const TAIL: usize = 2;
const FORCE_PRIORITY: f64 = 1.0;
const MIN_KEEP: usize = 3;
const AUTO_MIN_LINES: usize = 60;
const AUTO_SIGNAL_PCT: usize = 25;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ModeSetting {
Adaptive,
Aggressive,
Auto,
}
impl ModeSetting {
pub fn parse(s: &str) -> Self {
match s.trim().to_ascii_lowercase().as_str() {
"adaptive" => Self::Adaptive,
"aggressive" => Self::Aggressive,
_ => Self::Auto,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum Mode {
Adaptive,
Aggressive,
}
pub(crate) fn pick_mode(setting: ModeSetting, total: usize, signal: usize) -> Mode {
match setting {
ModeSetting::Adaptive => Mode::Adaptive,
ModeSetting::Aggressive => Mode::Aggressive,
ModeSetting::Auto => {
if signal >= 1 && total >= AUTO_MIN_LINES && signal * 100 <= total * AUTO_SIGNAL_PCT {
Mode::Aggressive
} else {
Mode::Adaptive
}
}
}
}
pub struct ToolOutputStage {
pub max_lines: usize,
pub min_lines: usize,
pub template: bool,
pub mode: ModeSetting,
}
pub(crate) struct Ctx {
pub max_lines: usize,
pub template: bool,
pub mode: ModeSetting,
}
impl Transform for ToolOutputStage {
fn name(&self) -> &str {
"toolout"
}
fn gate_kind(&self) -> GateKind {
GateKind::InputTokens
}
fn scope(&self) -> Scope {
Scope::Content
}
fn apply(
&self,
req: &mut Request,
provider: &dyn Provider,
_plan: &mut Vec<PlanEntry>,
) -> Result<()> {
let pointers = crate::cache_zone::compressible_pointers(req, provider);
let texts: Vec<Option<(String, bool)>> = pointers
.iter()
.map(|p| req.get_str(p).map(normalize::normalize))
.collect();
let kinds: Vec<Option<OutKind>> = texts
.iter()
.map(|t| t.as_ref().and_then(|(t, _)| detect::detect(t)))
.collect();
let query: HashSet<String> = texts
.iter()
.filter_map(|t| t.as_ref().map(|(t, _)| t.as_str()))
.filter(|t| t.lines().count() < self.min_lines)
.flat_map(lex_words)
.collect();
let ctx = Ctx {
max_lines: self.max_lines,
template: self.template,
mode: self.mode,
};
let repeats: HashSet<String> = {
let candidates: HashSet<&str> = pointers.iter().map(String::as_str).collect();
let mut earlier: HashSet<u64> = HashSet::new();
let mut repeats = HashSet::new();
for p in provider.content_text_pointers(req) {
let Some(t) = req.get_str(&p) else { continue };
let fp = template::fingerprint(t);
if candidates.contains(p.as_str()) && earlier.contains(&fp) {
repeats.insert(p);
} else {
earlier.insert(fp);
}
}
repeats
};
for ((ptr, text), kind) in pointers.iter().zip(&texts).zip(&kinds) {
let Some((text, normalized)) = text else {
continue;
};
if text.lines().count() < self.min_lines || repeats.contains(ptr) {
if *normalized {
req.set(ptr, Value::String(text.clone()));
}
continue;
}
let compressed = match kind {
Some(OutKind::Log) => log::compress(text, &ctx, &query),
Some(OutKind::Grep) => grep::compress(text, &ctx, &query),
Some(OutKind::Diff) => diff::compress(text, &ctx, &query),
None => {
generated::compress(text).or_else(|| plaintext::compress(text, &ctx, &query))
}
};
if let Some(compressed) = compressed {
req.set(ptr, Value::String(compressed));
} else if *normalized {
req.set(ptr, Value::String(text.clone()));
}
}
Ok(())
}
}
pub(crate) const WARN_PRIORITY: f64 = 0.6;
pub(crate) fn priority(line: &str) -> f64 {
if signals::STRONG.is_match(line) {
FORCE_PRIORITY
} else if signals::WARN.is_match(line) {
WARN_PRIORITY
} else if line.starts_with(' ') || line.starts_with('\t') {
0.5 } else {
0.3
}
}
pub(crate) fn query_bonus(line: &str, query: &HashSet<String>) -> f64 {
if query.is_empty() {
return 0.0;
}
let hits: HashSet<String> = lex_words(line)
.into_iter()
.filter(|w| query.contains(w))
.collect();
(hits.len() as f64 * 0.2).min(0.6)
}
pub(crate) fn fill_by_score(keep: &mut [bool], scores: &[f64], budget: usize) {
let n = keep.len();
let mut order: Vec<usize> = (0..n).collect();
order.sort_by(|&a, &b| {
scores[b]
.partial_cmp(&scores[a])
.unwrap_or(std::cmp::Ordering::Equal)
.then(a.cmp(&b))
});
let mut count = keep.iter().filter(|&&x| x).count();
for &i in &order {
if count >= budget {
break;
}
if !keep[i] {
keep[i] = true;
count += 1;
}
}
}
pub(crate) fn select_keep(scores: &[f64], k: usize, force: f64) -> Vec<bool> {
let n = scores.len();
let mut keep = vec![false; n];
for slot in keep.iter_mut().take(HEAD.min(n)) {
*slot = true;
}
for slot in keep.iter_mut().skip(n.saturating_sub(TAIL)) {
*slot = true;
}
for (i, &s) in scores.iter().enumerate() {
if s >= force {
keep[i] = true;
}
}
fill_by_score(&mut keep, scores, k);
keep
}
pub(crate) fn elide(dropped: &[&str]) -> String {
if dropped.len() == 1 {
"[… 1 line omitted …]".to_string()
} else {
format!("[… {} lines omitted …]", dropped.len())
}
}
pub(crate) fn elide_into(dropped: &[&str], out: &mut Vec<String>) -> bool {
let marker = elide(dropped);
let dropped_len: usize = dropped.iter().map(|l| l.len() + 1).sum();
if marker.len() + 1 >= dropped_len {
out.extend(dropped.iter().map(|l| (*l).to_string()));
false
} else {
out.push(marker);
true
}
}
fn is_elision_marker(line: &str) -> bool {
line.starts_with("[… ") && line.ends_with(" omitted …]")
}
pub(crate) fn attributed(body: Vec<String>, total: usize) -> String {
if !body.iter().any(|l| is_elision_marker(l)) {
return body.join("\n");
}
let shown = body.iter().filter(|l| !is_elision_marker(l)).count();
format!(
"[llmtrim: showing {shown} of {total} lines — re-run the tool for the full output]\n{}",
body.join("\n")
)
}
pub(crate) fn rebuild(lines: &[&str], keep: &[bool]) -> String {
let mut out: Vec<String> = Vec::new();
let mut i = 0;
while i < lines.len() {
if keep[i] {
out.push(lines[i].to_string());
i += 1;
} else {
let start = i;
while i < lines.len() && !keep[i] {
i += 1;
}
elide_into(&lines[start..i], &mut out);
}
}
attributed(out, lines.len())
}
#[cfg(test)]
pub(crate) fn test_ctx() -> Ctx {
Ctx {
max_lines: 30,
template: true,
mode: ModeSetting::Adaptive,
}
}
#[cfg(test)]
mod tests {
use super::{Mode, ModeSetting, attributed, elide, elide_into, pick_mode, rebuild};
#[test]
fn auto_goes_aggressive_only_when_big_and_sparse() {
assert_eq!(
pick_mode(ModeSetting::Auto, 100, 5),
Mode::Aggressive,
"big + sparse"
);
assert_eq!(
pick_mode(ModeSetting::Auto, 100, 40),
Mode::Adaptive,
"big but dense"
);
assert_eq!(
pick_mode(ModeSetting::Auto, 30, 1),
Mode::Adaptive,
"too small"
);
assert_eq!(
pick_mode(ModeSetting::Auto, 1000, 0),
Mode::Adaptive,
"zero signal stays adaptive"
);
}
#[test]
fn elide_grammar_is_number_aware() {
assert_eq!(elide(&["x"]), "[… 1 line omitted …]");
assert_eq!(elide(&["x", "y"]), "[… 2 lines omitted …]");
}
#[test]
fn elide_never_inflates() {
let mut out = Vec::new();
assert!(!elide_into(&["--"], &mut out), "no marker for a tiny run");
assert_eq!(out, vec!["--".to_string()]);
let big = vec!["this line is long enough to be worth eliding away"; 4];
let mut out = Vec::new();
assert!(elide_into(&big, &mut out), "marker for a real run");
assert_eq!(out, vec!["[… 4 lines omitted …]".to_string()]);
}
#[test]
fn rebuild_attributes_the_elision_and_states_recovery() {
let lines: Vec<&str> = (0..10)
.map(|_| "payload line with real content here")
.collect();
let mut keep = vec![false; 10];
keep[0] = true;
keep[9] = true;
let out = rebuild(&lines, &keep);
assert!(
out.starts_with("[llmtrim: showing 2 of 10 lines — re-run the tool"),
"self-identifying recovery header first: {out}"
);
assert!(
out.contains("[… 8 lines omitted …]"),
"positional marker kept"
);
}
#[test]
fn no_header_when_nothing_was_actually_elided() {
let body = vec!["--".to_string(), "ok".to_string()];
let out = attributed(body, 2);
assert_eq!(out, "--\nok");
}
#[test]
fn forced_modes_ignore_the_signal() {
assert_eq!(pick_mode(ModeSetting::Aggressive, 10, 9), Mode::Aggressive);
assert_eq!(pick_mode(ModeSetting::Adaptive, 1000, 1), Mode::Adaptive);
}
#[test]
fn mode_setting_parses_with_auto_fallback() {
assert_eq!(ModeSetting::parse("aggressive"), ModeSetting::Aggressive);
assert_eq!(ModeSetting::parse("ADAPTIVE"), ModeSetting::Adaptive);
assert_eq!(ModeSetting::parse("auto"), ModeSetting::Auto);
assert_eq!(ModeSetting::parse("nonsense"), ModeSetting::Auto);
}
}