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