Skip to main content

vtcode_core/tools/registry/
dual_output.rs

1//! Dual-channel tool execution helpers.
2
3use anyhow::Result;
4use serde_json::Value;
5use tracing::{debug, warn};
6use vtcode_commons::serde_helpers::json_to_string_pretty;
7
8use crate::config::constants::tools;
9use crate::tools::summarizers::{
10    Summarizer,
11    execution::BashSummarizer,
12    file_ops::{EditSummarizer, ReadSummarizer},
13    search::{GrepSummarizer, ListSummarizer},
14};
15use crate::tools::tool_intent;
16
17use super::{SplitToolResult, ToolRegistry};
18
19impl ToolRegistry {
20    /// Execute tool with dual-channel output (Phase 4: Split Tool Results).
21    ///
22    /// This method enables significant token savings by separating:
23    /// - `llm_content`: Concise summary sent to LLM context (token-optimized)
24    /// - `ui_content`: Rich output displayed to user (full details)
25    ///
26    /// For tools with registered summarizers, this can achieve 90-97% token reduction
27    /// on tool outputs while preserving full details for the UI.
28    ///
29    /// # Example
30    /// ```rust,no_run
31    /// let result = registry.execute_tool_dual("grep_file", args).await?;
32    /// // result.llm_content: "Found 127 matches in 15 files. Key: src/tools/grep.rs (3)"
33    /// // result.ui_content: [Full formatted output with all 127 matches]
34    /// // Savings: ~98% token reduction
35    /// ```
36    pub async fn execute_tool_dual(&self, name: &str, args: Value) -> Result<SplitToolResult> {
37        // Execute the tool using existing infrastructure
38        let result = self.execute_tool_ref(name, &args).await?;
39
40        // Convert Value to string for UI content
41        let ui_content = if result.is_string() {
42            result.as_str().unwrap_or("").to_string()
43        } else {
44            json_to_string_pretty(&result)
45        };
46
47        // Get canonical tool name for summarizer lookup
48        // Resolve alias through registration lookup first
49        let tool_name = if let Some(registration) = self.inventory.registration_for(name) {
50            registration.name().to_string()
51        } else {
52            name.to_string() // Fallback to original name if not found
53        };
54
55        // Check if we have a summarizer for this tool
56        match tool_name.as_str() {
57            tools::UNIFIED_SEARCH => {
58                match tool_intent::unified_search_action(&args).unwrap_or("grep") {
59                    "grep" => {
60                        let summarizer = GrepSummarizer::default();
61                        match summarizer.summarize(&ui_content, None) {
62                            Ok(llm_content) => {
63                                debug!(
64                                    tool = tools::UNIFIED_SEARCH,
65                                    action = "grep",
66                                    ui_tokens = %summarizer.estimate_savings(&ui_content, &llm_content).1,
67                                    llm_tokens = %summarizer.estimate_savings(&ui_content, &llm_content).0,
68                                    savings_pct = %summarizer.estimate_savings(&ui_content, &llm_content).2,
69                                    "Applied grep summarization"
70                                );
71                                Ok(SplitToolResult::new(
72                                    tool_name.as_str(),
73                                    llm_content,
74                                    ui_content,
75                                ))
76                            }
77                            Err(e) => {
78                                warn!(
79                                    tool = tools::UNIFIED_SEARCH,
80                                    action = "grep",
81                                    error = %e,
82                                    "Failed to summarize grep output, using simple result"
83                                );
84                                Ok(SplitToolResult::simple(tool_name.as_str(), ui_content))
85                            }
86                        }
87                    }
88                    "list" => {
89                        let summarizer = ListSummarizer::default();
90                        match summarizer.summarize(&ui_content, None) {
91                            Ok(llm_content) => {
92                                debug!(
93                                    tool = tools::UNIFIED_SEARCH,
94                                    action = "list",
95                                    ui_tokens = %summarizer.estimate_savings(&ui_content, &llm_content).1,
96                                    llm_tokens = %summarizer.estimate_savings(&ui_content, &llm_content).0,
97                                    savings_pct = %summarizer.estimate_savings(&ui_content, &llm_content).2,
98                                    "Applied list summarization"
99                                );
100                                Ok(SplitToolResult::new(
101                                    tool_name.as_str(),
102                                    llm_content,
103                                    ui_content,
104                                ))
105                            }
106                            Err(e) => {
107                                warn!(
108                                    tool = tools::UNIFIED_SEARCH,
109                                    action = "list",
110                                    error = %e,
111                                    "Failed to summarize list output, using simple result"
112                                );
113                                Ok(SplitToolResult::simple(tool_name.as_str(), ui_content))
114                            }
115                        }
116                    }
117                    _ => Ok(SplitToolResult::simple(tool_name.as_str(), ui_content)),
118                }
119            }
120            tools::UNIFIED_FILE => {
121                match tool_intent::unified_file_action(&args).unwrap_or("read") {
122                    "read" => {
123                        let mut metadata = args.clone();
124                        if let Value::Object(map) = &mut metadata
125                            && !map.contains_key("file_path")
126                        {
127                            let inferred_path = args
128                                .get("path")
129                                .or_else(|| args.get("file_path"))
130                                .or_else(|| args.get("filepath"))
131                                .or_else(|| args.get("target_path"))
132                                .and_then(Value::as_str)
133                                .map(str::to_string);
134                            if let Some(path) = inferred_path {
135                                map.insert("file_path".to_string(), Value::String(path));
136                            }
137                        }
138
139                        let summarizer = ReadSummarizer::default();
140                        match summarizer.summarize(&ui_content, Some(&metadata)) {
141                            Ok(llm_content) => {
142                                debug!(
143                                    tool = tools::UNIFIED_FILE,
144                                    action = "read",
145                                    ui_tokens = %summarizer.estimate_savings(&ui_content, &llm_content).1,
146                                    llm_tokens = %summarizer.estimate_savings(&ui_content, &llm_content).0,
147                                    savings_pct = %summarizer.estimate_savings(&ui_content, &llm_content).2,
148                                    "Applied unified_file read summarization"
149                                );
150                                Ok(SplitToolResult::new(
151                                    tool_name.as_str(),
152                                    llm_content,
153                                    ui_content,
154                                ))
155                            }
156                            Err(e) => {
157                                warn!(
158                                    tool = tools::UNIFIED_FILE,
159                                    action = "read",
160                                    error = %e,
161                                    "Failed to summarize unified_file read output, using simple result"
162                                );
163                                Ok(SplitToolResult::simple(tool_name.as_str(), ui_content))
164                            }
165                        }
166                    }
167                    "write" | "edit" | "patch" | "move" | "copy" | "delete" => {
168                        let summarizer = EditSummarizer::default();
169                        match summarizer.summarize(&ui_content, None) {
170                            Ok(llm_content) => {
171                                debug!(
172                                    tool = tools::UNIFIED_FILE,
173                                    action = "mutate",
174                                    ui_tokens = %summarizer.estimate_savings(&ui_content, &llm_content).1,
175                                    llm_tokens = %summarizer.estimate_savings(&ui_content, &llm_content).0,
176                                    savings_pct = %summarizer.estimate_savings(&ui_content, &llm_content).2,
177                                    "Applied unified_file mutation summarization"
178                                );
179                                Ok(SplitToolResult::new(
180                                    tool_name.as_str(),
181                                    llm_content,
182                                    ui_content,
183                                ))
184                            }
185                            Err(e) => {
186                                warn!(
187                                    tool = tools::UNIFIED_FILE,
188                                    action = "mutate",
189                                    error = %e,
190                                    "Failed to summarize unified_file mutation output, using simple result"
191                                );
192                                Ok(SplitToolResult::simple(tool_name.as_str(), ui_content))
193                            }
194                        }
195                    }
196                    _ => Ok(SplitToolResult::simple(tool_name.as_str(), ui_content)),
197                }
198            }
199            tools::UNIFIED_EXEC => match tool_intent::unified_exec_action(&args).unwrap_or("run") {
200                "run" | "code" => {
201                    let summarizer = BashSummarizer::default();
202                    let metadata = args.as_object().map(|_| args.clone());
203                    match summarizer.summarize(&ui_content, metadata.as_ref()) {
204                        Ok(llm_content) => {
205                            debug!(
206                                tool = tools::UNIFIED_EXEC,
207                                action = "run",
208                                ui_tokens = %summarizer.estimate_savings(&ui_content, &llm_content).1,
209                                llm_tokens = %summarizer.estimate_savings(&ui_content, &llm_content).0,
210                                savings_pct = %summarizer.estimate_savings(&ui_content, &llm_content).2,
211                                "Applied unified_exec summarization"
212                            );
213                            Ok(SplitToolResult::new(
214                                tool_name.as_str(),
215                                llm_content,
216                                ui_content,
217                            ))
218                        }
219                        Err(e) => {
220                            warn!(
221                                tool = tools::UNIFIED_EXEC,
222                                action = "run",
223                                error = %e,
224                                "Failed to summarize unified_exec output, using simple result"
225                            );
226                            Ok(SplitToolResult::simple(tool_name.as_str(), ui_content))
227                        }
228                    }
229                }
230                _ => Ok(SplitToolResult::simple(tool_name.as_str(), ui_content)),
231            },
232            tools::READ_FILE => {
233                // Apply read file summarization
234                let summarizer = ReadSummarizer::default();
235                // Extract file path from args if available for better summary
236                let metadata = args.as_object().map(|_| args.clone());
237                match summarizer.summarize(&ui_content, metadata.as_ref()) {
238                    Ok(llm_content) => {
239                        debug!(
240                            tool = tools::READ_FILE,
241                            ui_tokens = %summarizer.estimate_savings(&ui_content, &llm_content).1,
242                            llm_tokens = %summarizer.estimate_savings(&ui_content, &llm_content).0,
243                            savings_pct = %summarizer.estimate_savings(&ui_content, &llm_content).2,
244                            "Applied read file summarization"
245                        );
246                        Ok(SplitToolResult::new(
247                            tool_name.as_str(),
248                            llm_content,
249                            ui_content,
250                        ))
251                    }
252                    Err(e) => {
253                        warn!(
254                            tool = tools::READ_FILE,
255                            error = %e,
256                            "Failed to summarize read output, using simple result"
257                        );
258                        Ok(SplitToolResult::simple(tool_name.as_str(), ui_content))
259                    }
260                }
261            }
262            tools::RUN_PTY_CMD => {
263                // Apply bash execution summarization
264                let summarizer = BashSummarizer::default();
265                // Pass command info from args if available
266                let metadata = args.as_object().map(|_| args.clone());
267                match summarizer.summarize(&ui_content, metadata.as_ref()) {
268                    Ok(llm_content) => {
269                        debug!(
270                            tool = tools::RUN_PTY_CMD,
271                            ui_tokens = %summarizer.estimate_savings(&ui_content, &llm_content).1,
272                            llm_tokens = %summarizer.estimate_savings(&ui_content, &llm_content).0,
273                            savings_pct = %summarizer.estimate_savings(&ui_content, &llm_content).2,
274                            "Applied bash summarization"
275                        );
276                        Ok(SplitToolResult::new(
277                            tool_name.as_str(),
278                            llm_content,
279                            ui_content,
280                        ))
281                    }
282                    Err(e) => {
283                        warn!(
284                            tool = tools::RUN_PTY_CMD,
285                            error = %e,
286                            "Failed to summarize bash output, using simple result"
287                        );
288                        Ok(SplitToolResult::simple(tool_name.as_str(), ui_content))
289                    }
290                }
291            }
292            tools::WRITE_FILE | tools::EDIT_FILE | tools::APPLY_PATCH => {
293                // Apply edit/write file summarization
294                let summarizer = EditSummarizer::default();
295                match summarizer.summarize(&ui_content, None) {
296                    Ok(llm_content) => {
297                        debug!(
298                            tool = tool_name.as_str(),
299                            ui_tokens = %summarizer.estimate_savings(&ui_content, &llm_content).1,
300                            llm_tokens = %summarizer.estimate_savings(&ui_content, &llm_content).0,
301                            savings_pct = %summarizer.estimate_savings(&ui_content, &llm_content).2,
302                            "Applied edit summarization"
303                        );
304                        Ok(SplitToolResult::new(
305                            tool_name.as_str(),
306                            llm_content,
307                            ui_content,
308                        ))
309                    }
310                    Err(e) => {
311                        warn!(
312                            tool = tool_name.as_str(),
313                            error = %e,
314                            "Failed to summarize edit output, using simple result"
315                        );
316                        Ok(SplitToolResult::simple(tool_name.as_str(), ui_content))
317                    }
318                }
319            }
320            _ => {
321                // No summarizer registered, use same content for both channels
322                Ok(SplitToolResult::simple(tool_name.as_str(), ui_content))
323            }
324        }
325    }
326}