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};
14use crate::tools::tool_intent;
15
16use super::{SplitToolResult, ToolRegistry};
17
18impl ToolRegistry {
19    /// Execute tool with dual-channel output (Phase 4: Split Tool Results).
20    ///
21    /// This method enables significant token savings by separating:
22    /// - `llm_content`: Concise summary sent to LLM context (token-optimized)
23    /// - `ui_content`: Rich output displayed to user (full details)
24    ///
25    /// For tools with registered summarizers, this can achieve 90-97% token reduction
26    /// on tool outputs while preserving full details for the UI.
27    ///
28    /// # Example
29    /// This snippet is illustrative; registry setup and argument construction are
30    /// omitted because they depend on the caller's tool inventory.
31    /// ```rust,ignore
32    /// let result = registry.execute_tool_dual("grep_file", args).await?;
33    /// // result.llm_content: "Found 127 matches in 15 files. Key: src/tools/grep.rs (3)"
34    /// // result.ui_content: [Full formatted output with all 127 matches]
35    /// // Savings: ~98% token reduction
36    /// ```
37    pub async fn execute_tool_dual(&self, name: &str, args: Value) -> Result<SplitToolResult> {
38        // Execute the tool using existing infrastructure
39        let result = self.execute_tool_ref(name, &args).await?;
40
41        // Convert Value to string for UI content
42        let ui_content = if result.is_string() {
43            result.as_str().unwrap_or("").to_string()
44        } else {
45            json_to_string_pretty(&result)
46        };
47
48        // Get canonical tool name for summarizer lookup
49        // Resolve alias through registration lookup first
50        let tool_name = if let Some(registration) = self.inventory.registration_for(name) {
51            registration.name().to_string()
52        } else {
53            name.to_string() // Fallback to original name if not found
54        };
55
56        // Check if we have a summarizer for this tool
57        match tool_name.as_str() {
58            tools::CODE_SEARCH => Ok(SplitToolResult::simple(tool_name.as_str(), ui_content)),
59            tools::UNIFIED_FILE => match tool_intent::file_operation_action(&args).unwrap_or("read") {
60                "read" => {
61                    let mut metadata = args.clone();
62                    if let Value::Object(map) = &mut metadata
63                        && !map.contains_key("file_path")
64                    {
65                        let inferred_path = args
66                            .get("path")
67                            .or_else(|| args.get("file_path"))
68                            .or_else(|| args.get("filepath"))
69                            .or_else(|| args.get("target_path"))
70                            .and_then(Value::as_str)
71                            .map(str::to_string);
72                        if let Some(path) = inferred_path {
73                            map.insert("file_path".to_string(), Value::String(path));
74                        }
75                    }
76
77                    let summarizer = ReadSummarizer::default();
78                    match summarizer.summarize(&ui_content, Some(&metadata)) {
79                        Ok(llm_content) => {
80                            let savings = summarizer.estimate_savings(&ui_content, &llm_content);
81                            debug!(
82                                tool = tools::UNIFIED_FILE,
83                                action = "read",
84                                ui_tokens = %savings.ui_tokens,
85                                llm_tokens = %savings.llm_tokens,
86                                savings_pct = %savings.savings_percent,
87                                "Applied file_operation read summarization"
88                            );
89                            Ok(SplitToolResult::new(tool_name.as_str(), llm_content, ui_content))
90                        }
91                        Err(e) => {
92                            warn!(
93                                tool = tools::UNIFIED_FILE,
94                                action = "read",
95                                error = %e,
96                                "Failed to summarize file_operation read output, using simple result"
97                            );
98                            Ok(SplitToolResult::simple(tool_name.as_str(), ui_content))
99                        }
100                    }
101                }
102                "write" | "edit" | "patch" | "move" | "copy" | "delete" => {
103                    let summarizer = EditSummarizer::default();
104                    match summarizer.summarize(&ui_content, None) {
105                        Ok(llm_content) => {
106                            let savings = summarizer.estimate_savings(&ui_content, &llm_content);
107                            debug!(
108                                tool = tools::UNIFIED_FILE,
109                                action = "mutate",
110                                ui_tokens = %savings.ui_tokens,
111                                llm_tokens = %savings.llm_tokens,
112                                savings_pct = %savings.savings_percent,
113                                "Applied file_operation mutation summarization"
114                            );
115                            Ok(SplitToolResult::new(tool_name.as_str(), llm_content, ui_content))
116                        }
117                        Err(e) => {
118                            warn!(
119                                tool = tools::UNIFIED_FILE,
120                                action = "mutate",
121                                error = %e,
122                                "Failed to summarize file_operation mutation output, using simple result"
123                            );
124                            Ok(SplitToolResult::simple(tool_name.as_str(), ui_content))
125                        }
126                    }
127                }
128                _ => Ok(SplitToolResult::simple(tool_name.as_str(), ui_content)),
129            },
130            tools::UNIFIED_EXEC | tools::EXEC_COMMAND => {
131                match tool_intent::command_session_action(&args).unwrap_or("run") {
132                    "run" | "code" => {
133                        let summarizer = BashSummarizer::default();
134                        let metadata = args.as_object().map(|_| args.clone());
135                        match summarizer.summarize(&ui_content, metadata.as_ref()) {
136                            Ok(llm_content) => {
137                                let savings = summarizer.estimate_savings(&ui_content, &llm_content);
138                                debug!(
139                                    tool = tools::UNIFIED_EXEC,
140                                    action = "run",
141                                    ui_tokens = %savings.ui_tokens,
142                                    llm_tokens = %savings.llm_tokens,
143                                    savings_pct = %savings.savings_percent,
144                                    "Applied command_session summarization"
145                                );
146                                Ok(SplitToolResult::new(tool_name.as_str(), llm_content, ui_content))
147                            }
148                            Err(e) => {
149                                warn!(
150                                    tool = tools::UNIFIED_EXEC,
151                                    action = "run",
152                                    error = %e,
153                                    "Failed to summarize command_session output, using simple result"
154                                );
155                                Ok(SplitToolResult::simple(tool_name.as_str(), ui_content))
156                            }
157                        }
158                    }
159                    _ => Ok(SplitToolResult::simple(tool_name.as_str(), ui_content)),
160                }
161            }
162            tools::READ_FILE => {
163                // Apply read file summarization
164                let summarizer = ReadSummarizer::default();
165                // Extract file path from args if available for better summary
166                let metadata = args.as_object().map(|_| args.clone());
167                match summarizer.summarize(&ui_content, metadata.as_ref()) {
168                    Ok(llm_content) => {
169                        let savings = summarizer.estimate_savings(&ui_content, &llm_content);
170                        debug!(
171                            tool = tools::READ_FILE,
172                            ui_tokens = %savings.ui_tokens,
173                            llm_tokens = %savings.llm_tokens,
174                            savings_pct = %savings.savings_percent,
175                            "Applied read file summarization"
176                        );
177                        Ok(SplitToolResult::new(tool_name.as_str(), llm_content, ui_content))
178                    }
179                    Err(e) => {
180                        warn!(
181                            tool = tools::READ_FILE,
182                            error = %e,
183                            "Failed to summarize read output, using simple result"
184                        );
185                        Ok(SplitToolResult::simple(tool_name.as_str(), ui_content))
186                    }
187                }
188            }
189            tools::RUN_PTY_CMD => {
190                // Apply bash execution summarization
191                let summarizer = BashSummarizer::default();
192                // Pass command info from args if available
193                let metadata = args.as_object().map(|_| args.clone());
194                match summarizer.summarize(&ui_content, metadata.as_ref()) {
195                    Ok(llm_content) => {
196                        let savings = summarizer.estimate_savings(&ui_content, &llm_content);
197                        debug!(
198                            tool = tools::RUN_PTY_CMD,
199                            ui_tokens = %savings.ui_tokens,
200                            llm_tokens = %savings.llm_tokens,
201                            savings_pct = %savings.savings_percent,
202                            "Applied bash summarization"
203                        );
204                        Ok(SplitToolResult::new(tool_name.as_str(), llm_content, ui_content))
205                    }
206                    Err(e) => {
207                        warn!(
208                            tool = tools::RUN_PTY_CMD,
209                            error = %e,
210                            "Failed to summarize bash output, using simple result"
211                        );
212                        Ok(SplitToolResult::simple(tool_name.as_str(), ui_content))
213                    }
214                }
215            }
216            tools::WRITE_FILE | tools::EDIT_FILE | tools::APPLY_PATCH => {
217                // Apply edit/write file summarization
218                let summarizer = EditSummarizer::default();
219                match summarizer.summarize(&ui_content, None) {
220                    Ok(llm_content) => {
221                        let savings = summarizer.estimate_savings(&ui_content, &llm_content);
222                        debug!(
223                            tool = tool_name.as_str(),
224                            ui_tokens = %savings.ui_tokens,
225                            llm_tokens = %savings.llm_tokens,
226                            savings_pct = %savings.savings_percent,
227                            "Applied edit summarization"
228                        );
229                        Ok(SplitToolResult::new(tool_name.as_str(), llm_content, ui_content))
230                    }
231                    Err(e) => {
232                        warn!(
233                            tool = tool_name.as_str(),
234                            error = %e,
235                            "Failed to summarize edit output, using simple result"
236                        );
237                        Ok(SplitToolResult::simple(tool_name.as_str(), ui_content))
238                    }
239                }
240            }
241            _ => {
242                // No summarizer registered, use same content for both channels
243                Ok(SplitToolResult::simple(tool_name.as_str(), ui_content))
244            }
245        }
246    }
247}