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 cwd: PathBuf,
224 workspace_root: PathBuf,
225 shell: String,
226}
227
228impl DefaultToolSession {
229 pub fn new(cwd: PathBuf) -> Self {
230 let workspace_root = cwd.clone();
231 let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".to_string());
232 Self { cwd, workspace_root, shell }
233 }
234
235 pub fn with_workspace(cwd: PathBuf, workspace_root: PathBuf) -> Self {
236 let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".to_string());
237 Self { cwd, workspace_root, shell }
238 }
239}
240
241#[async_trait]
242impl ToolSession for DefaultToolSession {
243 fn cwd(&self) -> &PathBuf {
244 &self.cwd
245 }
246
247 fn workspace_root(&self) -> &PathBuf {
248 &self.workspace_root
249 }
250
251 async fn record_warning(&self, message: String) {
252 tracing::warn!("{}", message);
253 }
254
255 fn user_shell(&self) -> &str {
256 &self.shell
257 }
258}
259
260pub fn create_cwd_session() -> Arc<dyn ToolSession> {
262 Arc::new(DefaultToolSession::new(std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/"))))
263}
264
265#[cfg(test)]
266mod tests {
267 use super::super::tool_handler::ResponsesApiTool;
268 use super::*;
269 use serde_json::json;
270
271 struct TestHandler;
272 struct ErrorHandler;
273
274 #[async_trait]
275 impl ToolHandler for TestHandler {
276 fn kind(&self) -> ToolKind {
277 ToolKind::Function
278 }
279
280 async fn handle(&self, _invocation: ToolInvocation) -> Result<ToolOutput, ToolCallError> {
281 Ok(ToolOutput::simple("Test output"))
282 }
283 }
284
285 #[async_trait]
286 impl ToolHandler for ErrorHandler {
287 fn kind(&self) -> ToolKind {
288 ToolKind::Function
289 }
290
291 async fn handle(&self, _invocation: ToolInvocation) -> Result<ToolOutput, ToolCallError> {
292 Err(ToolCallError::respond("boom"))
293 }
294 }
295
296 #[tokio::test]
297 async fn test_handler_to_tool_adapter() {
298 let spec = ToolSpec::Function(ResponsesApiTool {
299 name: "test_tool".to_string(),
300 description: "A test tool".to_string(),
301 parameters: json!({"type": "object"}),
302 strict: false,
303 });
304
305 let adapter = HandlerToToolAdapter::new(TestHandler, "test_tool", "A test tool", spec, create_cwd_session);
306
307 assert_eq!(adapter.name(), "test_tool");
308 assert_eq!(adapter.description(), "A test tool");
309
310 let result = adapter.execute(serde_json::json!({})).await.unwrap();
311 assert!(result.get("success").and_then(|v| v.as_bool()).unwrap_or(false));
312 }
313
314 #[tokio::test]
315 async fn test_handler_to_tool_adapter_propagates_errors() {
316 let spec = ToolSpec::Function(ResponsesApiTool {
317 name: "test_tool".to_string(),
318 description: "A test tool".to_string(),
319 parameters: json!({"type": "object"}),
320 strict: false,
321 });
322
323 let adapter = HandlerToToolAdapter::new(ErrorHandler, "test_tool", "A test tool", spec, create_cwd_session);
324
325 let err = adapter.execute_dual(serde_json::json!({})).await.unwrap_err();
326 assert!(err.to_string().contains("boom"));
327 }
328
329 #[test]
330 fn test_default_tool_session() {
331 let session = DefaultToolSession::new(PathBuf::from("/tmp"));
332 assert_eq!(session.cwd(), &PathBuf::from("/tmp"));
333 assert_eq!(session.workspace_root(), &PathBuf::from("/tmp"));
334 }
335}