Skip to main content

vtcode_core/tools/handlers/
adapter.rs

1//! Adapter layer connecting Codex-style ToolHandler to vtcode's Tool trait.
2//!
3//! This module bridges the new handler architecture with the existing tool system,
4//! enabling gradual migration while maintaining backward compatibility.
5
6use 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
22/// Adapter that wraps a ToolHandler to implement the Tool trait.
23///
24/// This allows Codex-style handlers to be used in the existing vtcode tool registry.
25pub 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), // Approval handled by existing system
58            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                // Create a summary for LLM (first ~500 bytes or key info)
129                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        // Map handler mutability to policy
165        ToolPolicy::Prompt
166    }
167}
168
169/// Adapter that wraps a Tool to implement ToolHandler trait.
170///
171/// This allows existing vtcode tools to be used in the new Codex-style router.
172/// Prefer native or borrowed tool paths when you still own the concrete tool;
173/// this adapter keeps an `Arc<dyn Tool>` only for already-shared tool handles.
174pub 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        // Conservative default: assume mutating unless we know otherwise
192        !matches!(self.tool.default_permission(), ToolPolicy::Allow)
193    }
194
195    async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutput, ToolCallError> {
196        // Extract arguments from payload
197        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        // Execute the underlying tool
204        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
219/// Default session implementation for adapters.
220pub 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
281/// Factory function to create a session for the current directory.
282pub 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}