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 let mut meta = self.metadata.build();
173 meta.data.retain(|key, _| !self.custom_fields.contains_key(key));
174 if !meta.data.is_empty() || !meta.files.is_empty() || !meta.lines.is_empty() {
175 obj.insert("metadata".to_string(), json!(meta));
176 }
177
178 for (k, v) in self.custom_fields {
180 obj.insert(k, v);
181 }
182
183 res
184 }
185
186 pub fn build_result(self) -> ToolResult {
188 if !self.success {
189 return ToolResult::error(self.tool_name, self.error.unwrap_or_else(|| "Unknown error".to_string()));
190 }
191
192 let llm = self.llm_content.or_else(|| self.content.clone()).unwrap_or_default();
193 let ui = self.ui_content.or_else(|| self.content.clone()).unwrap_or_default();
194
195 let mut res = ToolResult::new(self.tool_name, llm, ui);
196 res.metadata = self.metadata.build();
197
198 for (k, v) in self.custom_fields {
200 res.metadata.data.insert(k, v);
201 }
202
203 res
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::ToolResponseBuilder;
210 use serde_json::json;
211
212 #[test]
213 fn build_json_omits_stdout_when_same_as_content() {
214 let value = ToolResponseBuilder::new("test").content("same").stdout("same").build_json();
215
216 assert_eq!(value.get("content").and_then(|v| v.as_str()), Some("same"));
217 assert!(value.get("stdout").is_none());
218 }
219
220 #[test]
221 fn build_json_keeps_metadata_only_keys_and_deduplicates_field_keys() {
222 let value = ToolResponseBuilder::new("read_file")
223 .field("content_kind", json!("text"))
224 .field("encoding", json!("utf8"))
225 .data("content_kind", json!("text"))
226 .data("encoding", json!("utf8"))
227 .data("size_bytes", json!(1024))
228 .build_json();
229
230 assert_eq!(value["content_kind"], json!("text"));
232 assert_eq!(value["encoding"], json!("utf8"));
233 assert_eq!(value["metadata"]["data"]["size_bytes"], json!(1024));
234 assert!(value["metadata"]["data"].get("content_kind").is_none());
235 assert!(value["metadata"]["data"].get("encoding").is_none());
236 }
237
238 #[test]
239 fn build_json_omits_metadata_object_when_all_data_keys_are_duplicates() {
240 let value = ToolResponseBuilder::new("read_file")
241 .field("content_kind", json!("text"))
242 .data("content_kind", json!("text"))
243 .build_json();
244
245 assert_eq!(value["content_kind"], json!("text"));
246 assert!(value.get("metadata").is_none());
247 }
248
249 #[test]
250 fn build_json_keeps_data_keys_colliding_with_structural_names() {
251 let value = ToolResponseBuilder::new("read_file")
254 .message("done")
255 .field("kind", json!("top"))
256 .data("message", json!("from data"))
257 .data("kind", json!("from data"))
258 .build_json();
259
260 assert_eq!(value["message"], json!("done"));
261 assert_eq!(value["kind"], json!("top"));
262 assert_eq!(value["metadata"]["data"]["message"], json!("from data"));
263 assert!(value["metadata"]["data"].get("kind").is_none());
264 }
265}