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//!
6//! Based on [openai/codex] handler architecture patterns (Apache-2.0).
7//! Copyright 2025 OpenAI. See the repository `THIRD-PARTY-NOTICES` file for
8//! full attribution.
9//!
10//! [openai/codex]: https://github.com/openai/codex
11
12use 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
29/// Adapter that wraps a ToolHandler to implement the Tool trait.
30///
31/// This allows Codex-style handlers to be used in the existing vtcode tool registry.
32pub 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), // Approval handled by existing system
65            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                // Create a summary for LLM (first ~500 bytes or key info)
136                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        // Map handler mutability to policy
167        ToolPolicy::Prompt
168    }
169}
170
171/// Adapter that wraps a Tool to implement ToolHandler trait.
172///
173/// This allows existing vtcode tools to be used in the new Codex-style router.
174/// Prefer native or borrowed tool paths when you still own the concrete tool;
175/// this adapter keeps an `Arc<dyn Tool>` only for already-shared tool handles.
176pub 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        // Conservative default: assume mutating unless we know otherwise
194        !matches!(self.tool.default_permission(), ToolPolicy::Allow)
195    }
196
197    async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutput, ToolCallError> {
198        // Extract arguments from payload
199        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        // Execute the underlying tool
206        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
221/// Default session implementation for adapters.
222pub 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    /// Share the same mutation leases and source ledger with registry handlers.
251    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
280/// Factory function to create a session for the current directory.
281pub 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}