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