oxicode_agent/tools/
eval_tool.rs1use async_trait::async_trait;
13use serde_json::{Value, json};
14use tokio::sync::oneshot;
15
16use super::{AgentTool, AgentToolResult, ToolContext, ToolError, ToolExecutionMode};
17
18pub struct EvalTool;
20
21#[async_trait]
22impl AgentTool for EvalTool {
23 fn name(&self) -> &str {
24 "eval"
25 }
26
27 fn label(&self) -> &str {
28 "Eval"
29 }
30
31 fn description(&self) -> &str {
32 "Execute code in Python (`py`) or JavaScript (`js`) and capture \
33 stdout, stderr, and the return value. Each call runs in a fresh \
34 process — state does NOT persist across calls. Use `reset: true` \
35 to discard previous state explicitly.\n\n\
36 For interactive or multi-step sessions, prefer the `bash` tool \
37 with `python3 -i` or `bun -i` for persistent state. Use `eval` \
38 for quick one-shot computations where only the output matters."
39 }
40
41 fn essential(&self) -> bool {
42 false
43 }
44
45 fn parameters_schema(&self) -> Value {
46 json!({
47 "type": "object",
48 "properties": {
49 "language": {
50 "type": "string",
51 "enum": ["py", "js"],
52 "description": "Language runtime: `py` (Python 3) or `js` (JavaScript / Bun)",
53 "default": "py"
54 },
55 "code": {
56 "type": "string",
57 "description": "Code to execute. Imports and variable definitions persist across calls."
58 },
59 "title": {
60 "type": "string",
61 "description": "Optional cell label for readability in the transcript"
62 },
63 "reset": {
64 "type": "boolean",
65 "description": "Reset the kernel/session before executing this cell",
66 "default": false
67 }
68 },
69 "required": ["code"]
70 })
71 }
72
73 fn intent(&self) -> Option<&str> {
74 Some("Execute code and capture output")
75 }
76
77 fn execution_mode(&self) -> ToolExecutionMode {
78 ToolExecutionMode::SequentialOnly
79 }
80
81 async fn execute(
82 &self,
83 _tool_call_id: &str,
84 params: Value,
85 _signal: Option<oneshot::Receiver<()>>,
86 _ctx: &ToolContext,
87 ) -> Result<AgentToolResult, ToolError> {
88 let code = params
89 .get("code")
90 .and_then(|v| v.as_str())
91 .ok_or_else(|| "Missing required parameter: code".to_string())?;
92
93 if code.trim().is_empty() {
94 return Err("Parameter `code` must be a non-empty string".to_string());
95 }
96
97 let language = params
98 .get("language")
99 .and_then(|v| v.as_str())
100 .unwrap_or("py");
101
102 let _title = params.get("title").and_then(|v| v.as_str());
103
104 let _reset = params
105 .get("reset")
106 .and_then(|v| v.as_bool())
107 .unwrap_or(false);
108
109 let tmp = std::env::temp_dir().join(format!(
111 "oxicode_eval_{}.{}",
112 std::time::SystemTime::now()
113 .duration_since(std::time::UNIX_EPOCH)
114 .unwrap_or_default()
115 .as_nanos(),
116 match language {
117 "py" => "py",
118 "js" => "mjs",
119 other => return Err(format!("Unsupported language: {}", other)),
120 }
121 ));
122
123 if let Err(e) = tokio::fs::write(&tmp, code).await {
124 return Ok(AgentToolResult::error(format!(
125 "Failed to write temp file: {}",
126 e
127 )));
128 }
129
130 let runner = match language {
131 "py" => "python3",
132 "js" => {
133 let has_bun = tokio::process::Command::new("which")
135 .arg("bun")
136 .output()
137 .await
138 .map(|o| o.status.success())
139 .unwrap_or(false);
140 if has_bun { "bun" } else { "node" }
141 }
142 other => {
143 return Ok(AgentToolResult::error(format!(
144 "Unsupported language: '{}'. Supported: py, js",
145 other
146 )));
147 }
148 };
149
150 let output = match tokio::process::Command::new(runner)
151 .arg(tmp.to_str().unwrap_or(""))
152 .output()
153 .await
154 {
155 Ok(o) => o,
156 Err(e) => {
157 let _ = tokio::fs::remove_file(&tmp).await;
159 return Ok(AgentToolResult::error(format!(
160 "Failed to execute code via {}: {}",
161 runner, e
162 )));
163 }
164 };
165
166 let _ = tokio::fs::remove_file(&tmp).await;
168
169 let mut result_parts = Vec::new();
170
171 if !output.stdout.is_empty() {
172 let stdout = String::from_utf8_lossy(&output.stdout);
173 if !stdout.trim().is_empty() {
174 result_parts.push(format!("── stdout ──\n{}", stdout.trim()));
175 }
176 }
177
178 if !output.stderr.is_empty() {
179 let stderr = String::from_utf8_lossy(&output.stderr);
180 if !stderr.trim().is_empty() {
181 result_parts.push(format!("── stderr ──\n{}", stderr.trim()));
182 }
183 }
184
185 let exit_code = output.status.code().unwrap_or(-1);
186
187 if exit_code != 0 {
188 result_parts.push(format!("── exit code: {} ──", exit_code));
189 }
190
191 let result_text = if result_parts.is_empty() {
192 "Code executed successfully (exit code 0, no output)".to_string()
193 } else {
194 result_parts.join("\n")
195 };
196
197 Ok(AgentToolResult::success(result_text))
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204 use serde_json::json;
205
206 fn ctx() -> ToolContext {
207 ToolContext::default()
208 }
209
210 #[tokio::test]
211 async fn rejects_missing_code() {
212 let result = EvalTool
213 .execute("c1", json!({"language": "py"}), None, &ctx())
214 .await;
215 assert!(result.is_err());
216 assert!(result.unwrap_err().contains("code"));
217 }
218
219 #[tokio::test]
220 async fn rejects_empty_code() {
221 let result = EvalTool
222 .execute("c2", json!({"code": " \n\t "}), None, &ctx())
223 .await;
224 assert!(result.is_err());
225 }
226
227 #[tokio::test]
228 async fn rejects_unknown_language() {
229 let result = EvalTool
230 .execute("c3", json!({"code": "x", "language": "ruby"}), None, &ctx())
231 .await;
232 assert!(result.is_err());
233 assert!(result.unwrap_err().contains("ruby"));
234 }
235
236 #[tokio::test]
237 async fn executes_python_code() {
238 let result = EvalTool
239 .execute(
240 "c4",
241 json!({"code": "print(1+1)", "language": "py"}),
242 None,
243 &ctx(),
244 )
245 .await
246 .expect("py execution should succeed");
247 assert!(result.success);
248 assert!(result.output.contains("2"), "output: {}", result.output);
249 }
250
251 #[tokio::test]
252 async fn executes_js_code() {
253 let result = EvalTool
254 .execute(
255 "c5",
256 json!({"code": "console.log(2+2)", "language": "js"}),
257 None,
258 &ctx(),
259 )
260 .await;
261 if let Ok(result) = result {
263 assert!(result.success);
264 assert!(result.output.contains("4"), "output: {}", result.output);
265 }
266 }
267
268 #[tokio::test]
269 async fn captures_stderr() {
270 let result = EvalTool
271 .execute(
272 "c6",
273 json!({"code": "import sys; print('ok', file=sys.stderr); print('stdout')", "language": "py"}),
274 None,
275 &ctx(),
276 )
277 .await
278 .expect("py execution should succeed");
279 assert!(result.success);
280 assert!(
281 result.output.contains("stdout"),
282 "output: {}",
283 result.output
284 );
285 }
286}