gemini_adk_rs/code_executors/
mod.rs

1//! Code execution infrastructure — sandboxed code execution for agents.
2
3/// Base trait and error types for code execution.
4pub mod base;
5/// Built-in code executor using Gemini's native code execution.
6pub mod built_in;
7/// Container-based code executor using Docker.
8pub mod container;
9/// Types used by code executors (input, output, files).
10pub mod types;
11/// Unsafe local code executor (no sandboxing).
12pub mod unsafe_local;
13/// Utility functions for extracting code blocks and building parts.
14pub mod utils;
15/// Vertex AI managed code executor (REST; requires `vertex-ai-code-executor`).
16#[cfg(feature = "vertex-ai-code-executor")]
17pub mod vertex_ai;
18
19pub use base::{CodeExecutor, CodeExecutorError};
20pub use built_in::BuiltInCodeExecutor;
21pub use container::{ContainerCodeExecutor, ContainerCodeExecutorConfig};
22pub use types::{CodeExecutionInput, CodeExecutionResult, CodeFile};
23pub use unsafe_local::UnsafeLocalCodeExecutor;
24#[cfg(feature = "vertex-ai-code-executor")]
25pub use vertex_ai::{VertexAiCodeExecutor, VertexAiCodeExecutorConfig};
26
27#[cfg(test)]
28mod tests {
29    use super::*;
30    use async_trait::async_trait;
31
32    // --- CodeFile tests ---
33
34    #[test]
35    fn code_file_construction() {
36        let file = CodeFile {
37            name: "main.py".into(),
38            content: "print('hello')".into(),
39            mime_type: "text/x-python".into(),
40        };
41        assert_eq!(file.name, "main.py");
42        assert_eq!(file.content, "print('hello')");
43        assert_eq!(file.mime_type, "text/x-python");
44    }
45
46    #[test]
47    fn code_file_serde_roundtrip() {
48        let file = CodeFile {
49            name: "data.csv".into(),
50            content: "a,b\n1,2".into(),
51            mime_type: "text/csv".into(),
52        };
53        let json = serde_json::to_string(&file).unwrap();
54        let deserialized: CodeFile = serde_json::from_str(&json).unwrap();
55        assert_eq!(deserialized.name, file.name);
56        assert_eq!(deserialized.content, file.content);
57        assert_eq!(deserialized.mime_type, file.mime_type);
58    }
59
60    // --- CodeExecutionResult tests ---
61
62    #[test]
63    fn code_execution_result_empty() {
64        let result = CodeExecutionResult::empty();
65        assert!(result.stdout.is_empty());
66        assert!(result.stderr.is_empty());
67        assert!(result.output_files.is_empty());
68    }
69
70    // --- CodeExecutionInput tests ---
71
72    #[test]
73    fn code_execution_input_construction() {
74        let input = CodeExecutionInput {
75            code: "x = 1 + 2".into(),
76            input_files: vec![CodeFile {
77                name: "input.txt".into(),
78                content: "hello".into(),
79                mime_type: "text/plain".into(),
80            }],
81            execution_id: Some("exec-123".into()),
82        };
83        assert_eq!(input.code, "x = 1 + 2");
84        assert_eq!(input.input_files.len(), 1);
85        assert_eq!(input.execution_id.as_deref(), Some("exec-123"));
86    }
87
88    #[test]
89    fn code_execution_input_no_execution_id() {
90        let input = CodeExecutionInput {
91            code: "pass".into(),
92            input_files: Vec::new(),
93            execution_id: None,
94        };
95        assert!(input.execution_id.is_none());
96        assert!(input.input_files.is_empty());
97    }
98
99    // --- CodeExecutor trait default methods ---
100
101    /// A minimal executor to test default trait method implementations.
102    struct StubExecutor;
103
104    #[async_trait]
105    impl CodeExecutor for StubExecutor {
106        async fn execute_code(
107            &self,
108            _input: CodeExecutionInput,
109        ) -> Result<CodeExecutionResult, CodeExecutorError> {
110            Ok(CodeExecutionResult::empty())
111        }
112    }
113
114    #[test]
115    fn default_code_block_delimiters() {
116        let exec = StubExecutor;
117        let delims = exec.code_block_delimiters();
118        assert_eq!(delims.len(), 2);
119        assert_eq!(
120            delims[0],
121            ("```tool_code\n".to_string(), "\n```".to_string())
122        );
123        assert_eq!(delims[1], ("```python\n".to_string(), "\n```".to_string()));
124    }
125
126    #[test]
127    fn default_execution_result_delimiters() {
128        let exec = StubExecutor;
129        let (open, close) = exec.execution_result_delimiters();
130        assert_eq!(open, "```tool_output\n");
131        assert_eq!(close, "\n```");
132    }
133
134    #[test]
135    fn default_error_retry_attempts() {
136        let exec = StubExecutor;
137        assert_eq!(exec.error_retry_attempts(), 2);
138    }
139
140    #[test]
141    fn default_stateful() {
142        let exec = StubExecutor;
143        assert!(!exec.stateful());
144    }
145
146    #[tokio::test]
147    async fn stub_executor_execute_code() {
148        let exec = StubExecutor;
149        let input = CodeExecutionInput {
150            code: "1 + 1".into(),
151            input_files: Vec::new(),
152            execution_id: None,
153        };
154        let result = exec.execute_code(input).await.unwrap();
155        assert!(result.stdout.is_empty());
156    }
157
158    // --- Object safety ---
159
160    fn _assert_object_safe(_: &dyn CodeExecutor) {}
161
162    #[test]
163    fn code_executor_is_object_safe() {
164        let exec = StubExecutor;
165        _assert_object_safe(&exec);
166    }
167
168    // --- BuiltInCodeExecutor tests ---
169
170    #[test]
171    fn built_in_process_llm_request_adds_code_execution_for_gemini2() {
172        let executor = built_in::BuiltInCodeExecutor;
173        let mut request = crate::llm::LlmRequest::from_text("hello");
174        executor
175            .process_llm_request(&mut request, "gemini-2.5-flash")
176            .unwrap();
177        assert_eq!(request.tools.len(), 1);
178        assert!(request.tools[0].code_execution.is_some());
179    }
180
181    #[test]
182    fn built_in_process_llm_request_rejects_non_gemini2() {
183        let executor = built_in::BuiltInCodeExecutor;
184        let mut request = crate::llm::LlmRequest::from_text("hello");
185        let err = executor
186            .process_llm_request(&mut request, "gemini-1.5-pro")
187            .unwrap_err();
188        assert!(
189            err.to_string().contains("Gemini 2.0+"),
190            "expected UnsupportedModel error, got: {}",
191            err
192        );
193        assert!(request.tools.is_empty());
194    }
195
196    #[tokio::test]
197    async fn built_in_execute_code_returns_empty() {
198        let executor = built_in::BuiltInCodeExecutor;
199        let input = CodeExecutionInput {
200            code: "print('hi')".into(),
201            input_files: Vec::new(),
202            execution_id: None,
203        };
204        let result = executor.execute_code(input).await.unwrap();
205        assert!(result.stdout.is_empty());
206        assert!(result.stderr.is_empty());
207        assert!(result.output_files.is_empty());
208    }
209
210    // --- utils tests ---
211
212    #[test]
213    fn extract_code_from_tool_code_block() {
214        let text = "Some text\n```tool_code\nprint('hello')\n```\nMore text";
215        let delimiters = vec![
216            ("```tool_code\n".to_string(), "\n```".to_string()),
217            ("```python\n".to_string(), "\n```".to_string()),
218        ];
219        let (code, remaining) = utils::extract_code_from_text(text, &delimiters).unwrap();
220        assert_eq!(code, "print('hello')");
221        assert_eq!(remaining, "Some text\n\nMore text");
222    }
223
224    #[test]
225    fn extract_code_from_python_block() {
226        let text = "Intro\n```python\nx = 42\n```\nDone";
227        let delimiters = vec![
228            ("```tool_code\n".to_string(), "\n```".to_string()),
229            ("```python\n".to_string(), "\n```".to_string()),
230        ];
231        let (code, remaining) = utils::extract_code_from_text(text, &delimiters).unwrap();
232        assert_eq!(code, "x = 42");
233        assert_eq!(remaining, "Intro\n\nDone");
234    }
235
236    #[test]
237    fn extract_code_returns_none_when_no_block() {
238        let text = "Just plain text, no code blocks here.";
239        let delimiters = vec![
240            ("```tool_code\n".to_string(), "\n```".to_string()),
241            ("```python\n".to_string(), "\n```".to_string()),
242        ];
243        assert!(utils::extract_code_from_text(text, &delimiters).is_none());
244    }
245
246    #[test]
247    fn build_executable_code_part_correct() {
248        let part = utils::build_executable_code_part("x = 1");
249        match part {
250            gemini_genai_rs::prelude::Part::ExecutableCode { executable_code } => {
251                assert_eq!(executable_code.language, "PYTHON");
252                assert_eq!(executable_code.code, "x = 1");
253            }
254            other => panic!("expected ExecutableCode, got: {:?}", other),
255        }
256    }
257
258    #[test]
259    fn build_code_execution_result_part_ok_outcome() {
260        let part = utils::build_code_execution_result_part("42\n", "");
261        match part {
262            gemini_genai_rs::prelude::Part::CodeExecutionResult {
263                code_execution_result,
264            } => {
265                assert_eq!(code_execution_result.outcome, "OK");
266                assert_eq!(code_execution_result.output.as_deref(), Some("42\n"));
267            }
268            other => panic!("expected CodeExecutionResult, got: {:?}", other),
269        }
270    }
271
272    #[test]
273    fn build_code_execution_result_part_failed_outcome() {
274        let part =
275            utils::build_code_execution_result_part("partial output", "NameError: x not defined");
276        match part {
277            gemini_genai_rs::prelude::Part::CodeExecutionResult {
278                code_execution_result,
279            } => {
280                assert_eq!(code_execution_result.outcome, "FAILED");
281                let output = code_execution_result.output.unwrap();
282                assert!(output.contains("partial output"));
283                assert!(output.contains("NameError: x not defined"));
284            }
285            other => panic!("expected CodeExecutionResult, got: {:?}", other),
286        }
287    }
288}