Skip to main content

harn_vm/orchestration/compaction/
tool_output.rs

1use super::{is_failure_signal_line, snap_to_line_end, snap_to_line_start};
2
3#[derive(Debug, Clone, PartialEq, Eq)]
4pub struct MicrocompactedToolOutput {
5    pub text: String,
6    /// Bytes from the source output that are no longer represented.
7    pub dropped_bytes: usize,
8}
9
10/// Microcompact a tool result and retain exact source-loss metadata.
11pub fn microcompact_tool_output_result(output: &str, max_chars: usize) -> MicrocompactedToolOutput {
12    if output.len() <= max_chars || max_chars < 200 {
13        return MicrocompactedToolOutput {
14            text: output.to_string(),
15            dropped_bytes: 0,
16        };
17    }
18    let mut offset = 0;
19    let diagnostic_lines = output
20        .split_inclusive('\n')
21        .filter_map(|segment| {
22            let start = offset;
23            offset += segment.len();
24            let line = segment.strip_suffix('\n').unwrap_or(segment);
25            is_failure_signal_line(line).then_some((line, start..start + line.len()))
26        })
27        .take(32)
28        .collect::<Vec<_>>();
29    if !diagnostic_lines.is_empty() {
30        let diagnostics = diagnostic_lines
31            .iter()
32            .map(|(line, _)| *line)
33            .collect::<Vec<_>>()
34            .join("\n");
35        let budget = max_chars.saturating_sub(diagnostics.len() + 64);
36        let keep = budget / 2;
37        if keep >= 80 && output.len() > keep * 2 {
38            let head = snap_to_line_end(output, keep);
39            let tail = snap_to_line_start(output, output.len().saturating_sub(keep));
40            let retained = retained_interval_bytes(
41                output.len(),
42                std::iter::once(0..head.len())
43                    .chain(diagnostic_lines.iter().map(|(_, range)| range.clone()))
44                    .chain(std::iter::once(output.len() - tail.len()..output.len())),
45            );
46            return MicrocompactedToolOutput {
47                text: format!(
48                    "{head}\n\n[diagnostic lines preserved]\n{diagnostics}\n\n[... output compacted ...]\n\n{tail}"
49                ),
50                dropped_bytes: output.len().saturating_sub(retained),
51            };
52        }
53    }
54    let keep = max_chars / 2;
55    let head = snap_to_line_end(output, keep);
56    let tail = snap_to_line_start(output, output.len().saturating_sub(keep));
57    let snipped = output.len().saturating_sub(head.len() + tail.len());
58    MicrocompactedToolOutput {
59        text: format!("{head}\n\n[... {snipped} characters snipped ...]\n\n{tail}"),
60        dropped_bytes: snipped,
61    }
62}
63
64/// Microcompact a tool result: if it exceeds `max_chars`, keep the first and
65/// last portions with a snip marker in between.
66pub fn microcompact_tool_output(output: &str, max_chars: usize) -> String {
67    microcompact_tool_output_result(output, max_chars).text
68}
69
70fn retained_interval_bytes(
71    source_len: usize,
72    intervals: impl IntoIterator<Item = std::ops::Range<usize>>,
73) -> usize {
74    let mut intervals = intervals
75        .into_iter()
76        .filter(|range| range.start < range.end)
77        .collect::<Vec<_>>();
78    intervals.sort_by_key(|range| range.start);
79    let mut retained = 0;
80    let mut covered_end = 0;
81    for range in intervals {
82        let start = range.start.min(source_len).max(covered_end);
83        let end = range.end.min(source_len);
84        if start < end {
85            retained += end - start;
86            covered_end = end;
87        }
88    }
89    retained
90}