Skip to main content

vtcode_core/tools/summarizers/
mod.rs

1//! Tool result summarization strategies
2//!
3//! Implements Phase 4 summarization: Converting full tool outputs into
4//! concise LLM-friendly summaries while preserving rich UI content.
5//!
6//! ## Summarization Strategies
7//!
8//! Different tool types need different summarization approaches:
9//! - **Search tools** (grep, list): Count-based summaries
10//! - **File operations** (read, edit): Content previews and statistics
11//! - **Edit tools** (edit, patch): Diff statistics
12//! - **Execution tools** (bash, code): Output summaries
13
14use anyhow::Result;
15
16use crate::utils::tokens::{estimate_tokens, truncate_to_tokens};
17
18pub mod execution;
19pub mod file_ops;
20pub mod search;
21
22use std::borrow::Cow;
23
24/// Truncate a line to max length with ellipsis (shared by execution + file_ops summarizers).
25///
26/// Returns `Cow::Borrowed` when no truncation is needed (zero allocation).
27pub(super) fn truncate_line<'a>(line: &'a str, max_len: usize) -> Cow<'a, str> {
28    if line.len() <= max_len {
29        Cow::Borrowed(line)
30    } else {
31        let target = max_len.saturating_sub(3);
32        let end = line
33            .char_indices()
34            .map(|(i, _)| i)
35            .rfind(|&i| i <= target)
36            .unwrap_or(0);
37        Cow::Owned(format!("{}...", &line[..end]))
38    }
39}
40
41/// Trait for tool result summarization strategies
42///
43/// Each tool type implements its own summarization logic
44/// to convert full output into concise LLM context
45pub trait Summarizer {
46    /// Summarize full output into concise LLM content
47    ///
48    /// # Arguments
49    /// * `full_output` - The complete tool output (for UI)
50    /// * `metadata` - Optional metadata about the operation
51    ///
52    /// # Returns
53    /// Concise summary optimized for LLM context (target: <100 tokens)
54    fn summarize(&self, full_output: &str, metadata: Option<&serde_json::Value>) -> Result<String>;
55
56    /// Estimate token savings from summarization
57    ///
58    /// Returns (llm_tokens, ui_tokens, savings_percent)
59    fn estimate_savings(&self, full_output: &str, summary: &str) -> (usize, usize, f32) {
60        let ui_tokens = estimate_tokens(full_output);
61        let llm_tokens = estimate_tokens(summary);
62        let savings = ui_tokens.saturating_sub(llm_tokens);
63        let savings_pct = if ui_tokens > 0 {
64            (savings as f32 / ui_tokens as f32) * 100.0
65        } else {
66            0.0
67        };
68        (llm_tokens, ui_tokens, savings_pct)
69    }
70}
71
72/// Extract key information from text (first N lines, keywords, etc.)
73///
74/// Useful for command output, file content, etc.
75pub fn extract_key_info(text: &str, max_lines: usize) -> String {
76    let mut lines: Vec<&str> = Vec::with_capacity(max_lines.min(32));
77    let mut total_lines = 0usize;
78    for line in text.lines() {
79        total_lines += 1;
80        if lines.len() < max_lines {
81            lines.push(line);
82        }
83    }
84
85    if total_lines > max_lines {
86        format!(
87            "{}\n[...{} more lines]",
88            lines.join("\n"),
89            total_lines - max_lines
90        )
91    } else {
92        lines.join("\n")
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn test_estimate_tokens() {
102        assert_eq!(estimate_tokens("Hello world"), 3); // 11 chars / 4 ≈ 3
103        assert_eq!(estimate_tokens(""), 0);
104        assert_eq!(estimate_tokens("a".repeat(1000).as_str()), 250); // 1000 / 4 = 250
105    }
106
107    #[test]
108    fn test_truncate_to_tokens() {
109        let text = "a".repeat(1000);
110        let truncated = truncate_to_tokens(&text, 50); // 50 tokens = 200 chars
111        assert!(truncated.len() <= 203); // 200 + "..."
112        assert!(truncated.ends_with("..."));
113    }
114
115    #[test]
116    fn test_truncate_short_text() {
117        let text = "Short text";
118        let truncated = truncate_to_tokens(text, 100);
119        assert_eq!(truncated, text);
120    }
121
122    #[test]
123    fn test_extract_key_info() {
124        let text = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5";
125        let extracted = extract_key_info(text, 3);
126        assert!(extracted.contains("Line 1"));
127        assert!(extracted.contains("Line 3"));
128        assert!(extracted.contains("[...2 more lines]"));
129    }
130
131    #[test]
132    fn test_extract_key_info_exact() {
133        let text = "Line 1\nLine 2\nLine 3";
134        let extracted = extract_key_info(text, 3);
135        assert_eq!(extracted, text);
136        assert!(!extracted.contains("more lines"));
137    }
138}