vtcode_core/tools/
builder.rs1use hashbrown::HashMap;
7use serde_json::{Value, json};
8use std::path::PathBuf;
9
10use crate::tools::result::{ToolMetadataBuilder, ToolResult};
11
12pub 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 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 pub fn success(mut self) -> Self {
49 self.success = true;
50 self
51 }
52
53 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 pub fn message(mut self, message: impl Into<String>) -> Self {
62 self.message = Some(message.into());
63 self
64 }
65
66 pub fn content(mut self, content: impl Into<String>) -> Self {
68 self.content = Some(content.into());
69 self
70 }
71
72 pub fn stdout(mut self, stdout: impl Into<String>) -> Self {
74 self.stdout = Some(stdout.into());
75 self
76 }
77
78 pub fn modified_file(mut self, path: impl Into<String>) -> Self {
80 self.modified_files.push(path.into());
81 self
82 }
83
84 pub fn modified_files(mut self, paths: Vec<String>) -> Self {
86 self.modified_files.extend(paths);
87 self
88 }
89
90 pub fn has_more(mut self, has_more: bool) -> Self {
92 self.has_more = has_more;
93 self
94 }
95
96 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 pub fn file(mut self, path: impl Into<PathBuf>) -> Self {
105 self.metadata = self.metadata.file(path.into());
106 self
107 }
108
109 pub fn files(mut self, paths: Vec<PathBuf>) -> Self {
111 self.metadata = self.metadata.files(paths);
112 self
113 }
114
115 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 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 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 for (k, v) in self.custom_fields {
168 obj.insert(k, v);
169 }
170
171 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 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 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}