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    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
260/// Factory function to create a session for the current directory.
261pub 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}