Skip to main content

vtcode_core/tools/
builder.rs

1//! Unified builder for tool responses
2//!
3//! Provides a consistent way to construct tool execution results with
4//! support for dual-channel output, structured metadata, and standardized error reporting.
5
6use hashbrown::HashMap;
7use serde_json::{Value, json};
8use std::path::PathBuf;
9
10use crate::tools::result::{ToolMetadataBuilder, ToolResult};
11
12/// Builder for standardized tool responses
13pub struct ToolResponseBuilder {
14    tool_name: String,
15    success: bool,
16    message: Option<String>,
17    content: Option<String>,
18    stdout: Option<String>,
19    modified_files: Vec<String>,
20    has_more: bool,
21    llm_content: Option<String>,
22    ui_content: Option<String>,
23    error: Option<String>,
24    metadata: ToolMetadataBuilder,
25    custom_fields: HashMap<String, Value>,
26}
27
28impl ToolResponseBuilder {
29    /// Create a new builder for the given tool
30    pub fn new(tool_name: impl Into<String>) -> Self {
31        Self {
32            tool_name: tool_name.into(),
33            success: true,
34            message: None,
35            content: None,
36            stdout: None,
37            modified_files: Vec::new(),
38            has_more: false,
39            llm_content: None,
40            ui_content: None,
41            error: None,
42            metadata: ToolMetadataBuilder::new(),
43            custom_fields: HashMap::new(),
44        }
45    }
46
47    /// Mark the execution as successful
48    pub fn success(mut self) -> Self {
49        self.success = true;
50        self
51    }
52
53    /// Mark the execution as failed with an error message
54    pub fn failure(mut self, error: impl Into<String>) -> Self {
55        self.success = false;
56        self.error = Some(error.into());
57        self
58    }
59
60    /// Set a user-friendly status message
61    pub fn message(mut self, message: impl Into<String>) -> Self {
62        self.message = Some(message.into());
63        self
64    }
65
66    /// Set the main content (used for both LLM and UI if not overridden)
67    pub fn content(mut self, content: impl Into<String>) -> Self {
68        self.content = Some(content.into());
69        self
70    }
71
72    /// Set standard output from a process
73    pub fn stdout(mut self, stdout: impl Into<String>) -> Self {
74        self.stdout = Some(stdout.into());
75        self
76    }
77
78    /// Add a modified file to the list
79    pub fn modified_file(mut self, path: impl Into<String>) -> Self {
80        self.modified_files.push(path.into());
81        self
82    }
83
84    /// Add multiple modified files
85    pub fn modified_files(mut self, paths: Vec<String>) -> Self {
86        self.modified_files.extend(paths);
87        self
88    }
89
90    /// Set whether there are more results available
91    pub fn has_more(mut self, has_more: bool) -> Self {
92        self.has_more = has_more;
93        self
94    }
95
96    /// Set explicit dual-channel content
97    pub fn dual_content(mut self, llm: impl Into<String>, ui: impl Into<String>) -> Self {
98        self.llm_content = Some(llm.into());
99        self.ui_content = Some(ui.into());
100        self
101    }
102
103    /// Add a file reference to the metadata (for UI linking)
104    pub fn file(mut self, path: impl Into<PathBuf>) -> Self {
105        self.metadata = self.metadata.file(path.into());
106        self
107    }
108
109    /// Add multiple file references
110    pub fn files(mut self, paths: Vec<PathBuf>) -> Self {
111        self.metadata = self.metadata.files(paths);
112        self
113    }
114
115    /// Add structured data to the metadata
116    pub fn data(mut self, key: impl Into<String>, value: Value) -> Self {
117        self.metadata = self.metadata.data(key, value);
118        self
119    }
120
121    /// Add a custom top-level field to the final JSON response
122    pub fn field(mut self, key: impl Into<String>, value: Value) -> Self {
123        self.custom_fields.insert(key.into(), value);
124        self
125    }
126
127    /// Build the legacy JSON Value response
128    pub fn build_json(self) -> Value {
129        let mut res = json!({
130            "success": self.success,
131            "status": if self.success { "success" } else { "error" },
132        });
133
134        let Some(obj) = res.as_object_mut() else {
135            return res;
136        };
137
138        if let Some(msg) = self.message {
139            obj.insert("message".to_string(), json!(msg));
140        }
141
142        if let Some(err) = self.error {
143            obj.insert("error".to_string(), json!(err));
144        }
145
146        let content_value = self.content;
147        if let Some(c) = content_value.as_ref() {
148            obj.insert("content".to_string(), json!(c));
149        }
150
151        if let Some(s) = self.stdout {
152            let duplicates_content = content_value.as_deref() == Some(s.as_str());
153            if !duplicates_content {
154                obj.insert("stdout".to_string(), json!(s));
155            }
156        }
157
158        if !self.modified_files.is_empty() {
159            obj.insert("modified_files".to_string(), json!(self.modified_files));
160        }
161
162        if self.has_more {
163            obj.insert("has_more".to_string(), json!(true));
164        }
165
166        // Add custom top-level fields
167        for (k, v) in self.custom_fields {
168            obj.insert(k, v);
169        }
170
171        // Build and merge metadata
172        let meta = self.metadata.build();
173        if !meta.data.is_empty() || !meta.files.is_empty() || !meta.lines.is_empty() {
174            obj.insert("metadata".to_string(), json!(meta));
175        }
176
177        res
178    }
179
180    /// Build the modern dual-channel ToolResult
181    pub fn build_result(self) -> ToolResult {
182        if !self.success {
183            return ToolResult::error(
184                self.tool_name,
185                self.error.unwrap_or_else(|| "Unknown error".to_string()),
186            );
187        }
188
189        let llm = self
190            .llm_content
191            .or_else(|| self.content.clone())
192            .unwrap_or_default();
193        let ui = self
194            .ui_content
195            .or_else(|| self.content.clone())
196            .unwrap_or_default();
197
198        let mut res = ToolResult::new(self.tool_name, llm, ui);
199        res.metadata = self.metadata.build();
200
201        // Add custom fields to metadata data map
202        for (k, v) in self.custom_fields {
203            res.metadata.data.insert(k, v);
204        }
205
206        res
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::ToolResponseBuilder;
213
214    #[test]
215    fn build_json_omits_stdout_when_same_as_content() {
216        let value = ToolResponseBuilder::new("test")
217            .content("same")
218            .stdout("same")
219            .build_json();
220
221        assert_eq!(value.get("content").and_then(|v| v.as_str()), Some("same"));
222        assert!(value.get("stdout").is_none());
223    }
224}