vtcode_core/tools/handlers/
adapter.rs1use std::path::PathBuf;
13use std::sync::Arc;
14
15use anyhow::Result;
16use async_trait::async_trait;
17use serde_json::Value;
18use vtcode_commons::serde_helpers::json_to_string_pretty;
19
20use super::tool_handler::{
21 ApprovalPolicy, Constrained, ShellEnvironmentPolicy, ToolCallError, ToolHandler, ToolInvocation, ToolKind,
22 ToolOutput, ToolPayload, ToolSession, ToolSpec, TurnContext,
23};
24use crate::sandboxing::LinuxSandboxLauncher;
25use crate::tool_policy::ToolPolicy;
26use crate::tools::result::ToolResult as SplitToolResult;
27use crate::tools::traits::Tool;
28
29pub struct HandlerToToolAdapter<H: ToolHandler> {
33 handler: Arc<H>,
34 name: &'static str,
35 description: &'static str,
36 spec: ToolSpec,
37 session_factory: Arc<dyn Fn() -> Arc<dyn ToolSession> + Send + Sync>,
38}
39
40impl<H: ToolHandler + 'static> HandlerToToolAdapter<H> {
41 pub fn new(
42 handler: H,
43 name: &'static str,
44 description: &'static str,
45 spec: ToolSpec,
46 session_factory: impl Fn() -> Arc<dyn ToolSession> + Send + Sync + 'static,
47 ) -> Self {
48 Self {
49 handler: Arc::new(handler),
50 name,
51 description,
52 spec,
53 session_factory: Arc::new(session_factory),
54 }
55 }
56
57 fn create_invocation(&self, args: Value) -> ToolInvocation {
58 let session = (self.session_factory)();
59 let turn = Arc::new(TurnContext {
60 cwd: session.cwd().clone(),
61 turn_id: uuid::Uuid::new_v4().to_string(),
62 sub_id: None,
63 shell_environment_policy: ShellEnvironmentPolicy::Inherit,
64 approval_policy: Constrained::allow_any(ApprovalPolicy::Never), linux_sandbox_launcher: LinuxSandboxLauncher::resolve(),
66 sandbox_policy: Constrained::allow_any(Default::default()),
67 });
68
69 ToolInvocation {
70 session,
71 turn,
72 tracker: None,
73 call_id: uuid::Uuid::new_v4().to_string(),
74 tool_name: self.name.to_string(),
75 payload: ToolPayload::Function {
76 arguments: serde_json::to_string(&args).unwrap_or_default(),
77 },
78 }
79 }
80
81 fn output_to_value(&self, output: ToolOutput) -> Value {
82 let (text, is_success) = match &output {
83 ToolOutput::Function { content, .. } => (content.clone(), output.is_success()),
84 ToolOutput::Mcp { result } => {
85 let text = result
86 .content
87 .iter()
88 .filter_map(|c| c.as_text())
89 .map(|s| s.to_string())
90 .collect::<Vec<_>>()
91 .join("\n");
92 (text, output.is_success())
93 }
94 };
95
96 serde_json::json!({
97 "success": is_success,
98 "content": text,
99 })
100 }
101}
102
103#[async_trait]
104impl<H: ToolHandler + 'static> Tool for HandlerToToolAdapter<H> {
105 async fn execute(&self, args: Value) -> Result<Value> {
106 let invocation = self.create_invocation(args);
107
108 match self.handler.handle(invocation).await {
109 Ok(output) => Ok(self.output_to_value(output)),
110 Err(ToolCallError::RespondToModel(msg)) => Ok(serde_json::json!({
111 "success": false,
112 "error": msg,
113 })),
114 Err(ToolCallError::Rejected(msg)) => Ok(serde_json::json!({
115 "success": false,
116 "rejected": true,
117 "error": msg,
118 })),
119 Err(ToolCallError::Timeout(ms)) => Ok(serde_json::json!({
120 "success": false,
121 "timeout": true,
122 "timeout_ms": ms,
123 })),
124 Err(ToolCallError::Internal(e)) => Err(e),
125 }
126 }
127
128 async fn execute_dual(&self, args: Value) -> Result<SplitToolResult> {
129 let invocation = self.create_invocation(args);
130
131 match self.handler.handle(invocation).await {
132 Ok(output) => {
133 let ui_content = output.content().unwrap_or("").to_string();
134
135 let llm_content = if ui_content.len() > 500 {
137 let truncated = vtcode_commons::formatting::truncate_byte_budget(&ui_content, 500, "");
138 format!("{}...[truncated, {} chars total]", truncated, ui_content.len())
139 } else {
140 ui_content.clone()
141 };
142
143 Ok(SplitToolResult::new(self.name, &llm_content, &ui_content))
144 }
145 Err(e) => Err(e.into()),
146 }
147 }
148
149 fn name(&self) -> &str {
150 self.name
151 }
152
153 fn description(&self) -> &str {
154 self.description
155 }
156
157 fn parameter_schema(&self) -> Option<Value> {
158 match &self.spec {
159 ToolSpec::Function(tool) => serde_json::to_value(&tool.parameters).ok(),
160 ToolSpec::Freeform(tool) => serde_json::to_value(&tool.format).ok(),
161 _ => None,
162 }
163 }
164
165 fn default_permission(&self) -> ToolPolicy {
166 ToolPolicy::Prompt
168 }
169}
170
171pub struct ToolToHandlerAdapter {
177 tool: Arc<dyn Tool>,
178}
179
180impl ToolToHandlerAdapter {
181 pub fn new(tool: Arc<dyn Tool>) -> Self {
182 Self { tool }
183 }
184}
185
186#[async_trait]
187impl ToolHandler for ToolToHandlerAdapter {
188 fn kind(&self) -> ToolKind {
189 ToolKind::Function
190 }
191
192 async fn is_mutating(&self, _invocation: &ToolInvocation) -> bool {
193 !matches!(self.tool.default_permission(), ToolPolicy::Allow)
195 }
196
197 async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutput, ToolCallError> {
198 let args: Value = match &invocation.payload {
200 ToolPayload::Function { arguments } => serde_json::from_str(arguments)
201 .map_err(|e| ToolCallError::respond(format!("Invalid arguments: {e}")))?,
202 _ => return Err(ToolCallError::respond("Unsupported payload type")),
203 };
204
205 match self.tool.execute(args).await {
207 Ok(result) => {
208 let text = if result.is_string() {
209 result.as_str().unwrap_or("").to_string()
210 } else {
211 json_to_string_pretty(&result)
212 };
213
214 Ok(ToolOutput::simple(text))
215 }
216 Err(e) => Err(ToolCallError::Internal(e)),
217 }
218 }
219}
220
221pub struct DefaultToolSession {
223 monitor: Arc<crate::tools::edited_file_monitor::EditedFileMonitor>,
224 cwd: PathBuf,
225 workspace_root: PathBuf,
226 shell: String,
227}
228
229impl DefaultToolSession {
230 pub fn new(cwd: PathBuf) -> Self {
231 let workspace_root = cwd.clone();
232 let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".to_string());
233 Self {
234 cwd,
235 workspace_root,
236 shell,
237 monitor: Arc::new(crate::tools::edited_file_monitor::EditedFileMonitor::new()),
238 }
239 }
240
241 pub fn with_workspace(cwd: PathBuf, workspace_root: PathBuf) -> Self {
242 let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".to_string());
243 Self {
244 cwd,
245 workspace_root,
246 shell,
247 monitor: Arc::new(crate::tools::edited_file_monitor::EditedFileMonitor::new()),
248 }
249 }
250 pub fn with_file_monitor(mut self, monitor: Arc<crate::tools::edited_file_monitor::EditedFileMonitor>) -> Self {
252 self.monitor = monitor;
253 self
254 }
255}
256
257#[async_trait]
258impl ToolSession for DefaultToolSession {
259 fn edited_file_monitor(&self) -> Option<&crate::tools::edited_file_monitor::EditedFileMonitor> {
260 Some(&self.monitor)
261 }
262
263 fn cwd(&self) -> &PathBuf {
264 &self.cwd
265 }
266
267 fn workspace_root(&self) -> &PathBuf {
268 &self.workspace_root
269 }
270
271 async fn record_warning(&self, message: String) {
272 tracing::warn!("{}", message);
273 }
274
275 fn user_shell(&self) -> &str {
276 &self.shell
277 }
278}
279
280pub fn create_cwd_session() -> Arc<dyn ToolSession> {
282 Arc::new(DefaultToolSession::new(std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/"))))
283}
284
285#[cfg(test)]
286mod tests {
287 use super::super::tool_handler::ResponsesApiTool;
288 use super::*;
289 use serde_json::json;
290
291 struct TestHandler;
292 struct ErrorHandler;
293
294 #[async_trait]
295 impl ToolHandler for TestHandler {
296 fn kind(&self) -> ToolKind {
297 ToolKind::Function
298 }
299
300 async fn handle(&self, _invocation: ToolInvocation) -> Result<ToolOutput, ToolCallError> {
301 Ok(ToolOutput::simple("Test output"))
302 }
303 }
304
305 #[async_trait]
306 impl ToolHandler for ErrorHandler {
307 fn kind(&self) -> ToolKind {
308 ToolKind::Function
309 }
310
311 async fn handle(&self, _invocation: ToolInvocation) -> Result<ToolOutput, ToolCallError> {
312 Err(ToolCallError::respond("boom"))
313 }
314 }
315
316 #[tokio::test]
317 async fn test_handler_to_tool_adapter() {
318 let spec = ToolSpec::Function(ResponsesApiTool {
319 name: "test_tool".to_string(),
320 description: "A test tool".to_string(),
321 parameters: json!({"type": "object"}),
322 strict: false,
323 });
324
325 let adapter = HandlerToToolAdapter::new(TestHandler, "test_tool", "A test tool", spec, create_cwd_session);
326
327 assert_eq!(adapter.name(), "test_tool");
328 assert_eq!(adapter.description(), "A test tool");
329
330 let result = adapter.execute(serde_json::json!({})).await.unwrap();
331 assert!(result.get("success").and_then(|v| v.as_bool()).unwrap_or(false));
332 }
333
334 #[tokio::test]
335 async fn test_handler_to_tool_adapter_propagates_errors() {
336 let spec = ToolSpec::Function(ResponsesApiTool {
337 name: "test_tool".to_string(),
338 description: "A test tool".to_string(),
339 parameters: json!({"type": "object"}),
340 strict: false,
341 });
342
343 let adapter = HandlerToToolAdapter::new(ErrorHandler, "test_tool", "A test tool", spec, create_cwd_session);
344
345 let err = adapter.execute_dual(serde_json::json!({})).await.unwrap_err();
346 assert!(err.to_string().contains("boom"));
347 }
348
349 #[test]
350 fn test_default_tool_session() {
351 let session = DefaultToolSession::new(PathBuf::from("/tmp"));
352 assert_eq!(session.cwd(), &PathBuf::from("/tmp"));
353 assert_eq!(session.workspace_root(), &PathBuf::from("/tmp"));
354 }
355}