agent_code_lib/tools/
repl_tool.rs1use async_trait::async_trait;
8use serde_json::json;
9use std::process::Stdio;
10use std::time::Duration;
11use tokio::io::AsyncReadExt;
12use tokio::process::Command;
13
14use super::{Tool, ToolContext, ToolResult};
15use crate::error::ToolError;
16
17pub struct ReplTool;
18
19#[async_trait]
20impl Tool for ReplTool {
21 fn name(&self) -> &'static str {
22 "REPL"
23 }
24
25 fn description(&self) -> &'static str {
26 "Execute code in a Python or Node.js interpreter and return the output."
27 }
28
29 fn input_schema(&self) -> serde_json::Value {
30 json!({
31 "type": "object",
32 "required": ["language", "code"],
33 "properties": {
34 "language": {
35 "type": "string",
36 "enum": ["python", "node"],
37 "description": "Interpreter to use"
38 },
39 "code": {
40 "type": "string",
41 "description": "Code to execute"
42 },
43 "timeout": {
44 "type": "integer",
45 "description": "Timeout in milliseconds (default 30000)",
46 "default": 30000
47 }
48 }
49 })
50 }
51
52 fn is_read_only(&self) -> bool {
53 false
54 }
55
56 fn is_concurrency_safe(&self) -> bool {
57 false
58 }
59
60 async fn call(
61 &self,
62 input: serde_json::Value,
63 ctx: &ToolContext,
64 ) -> Result<ToolResult, ToolError> {
65 let language = input
66 .get("language")
67 .and_then(|v| v.as_str())
68 .ok_or_else(|| ToolError::InvalidInput("'language' is required".into()))?;
69
70 let code = input
71 .get("code")
72 .and_then(|v| v.as_str())
73 .ok_or_else(|| ToolError::InvalidInput("'code' is required".into()))?;
74
75 let timeout_ms = input
76 .get("timeout")
77 .and_then(|v| v.as_u64())
78 .unwrap_or(30_000)
79 .min(120_000);
80
81 let (cmd, flag) = match language {
82 "python" => ("python3", "-c"),
83 "node" => ("node", "-e"),
84 other => {
85 return Err(ToolError::InvalidInput(format!(
86 "Unsupported language '{other}'. Use 'python' or 'node'."
87 )));
88 }
89 };
90
91 let mut child = Command::new(cmd)
92 .arg(flag)
93 .arg(code)
94 .current_dir(&ctx.cwd)
95 .stdout(Stdio::piped())
96 .stderr(Stdio::piped())
97 .spawn()
98 .map_err(|e| {
99 ToolError::ExecutionFailed(format!(
100 "Failed to start {language} interpreter: {e}. \
101 Make sure '{cmd}' is installed and in PATH."
102 ))
103 })?;
104
105 let mut stdout_handle = child.stdout.take().unwrap();
106 let mut stderr_handle = child.stderr.take().unwrap();
107 let mut stdout_buf = Vec::new();
108 let mut stderr_buf = Vec::new();
109
110 let timeout = Duration::from_millis(timeout_ms);
111
112 let result = tokio::select! {
113 r = async {
114 let (so, se) = tokio::join!(
115 async { stdout_handle.read_to_end(&mut stdout_buf).await },
116 async { stderr_handle.read_to_end(&mut stderr_buf).await },
117 );
118 so?;
119 se?;
120 child.wait().await
121 } => {
122 match r {
123 Ok(status) => {
124 let stdout = String::from_utf8_lossy(&stdout_buf).to_string();
125 let stderr = String::from_utf8_lossy(&stderr_buf).to_string();
126 let exit_code = status.code().unwrap_or(-1);
127
128 let mut content = String::new();
129 if !stdout.is_empty() {
130 content.push_str(&stdout);
131 }
132 if !stderr.is_empty() {
133 if !content.is_empty() {
134 content.push('\n');
135 }
136 content.push_str(&stderr);
137 }
138 if content.is_empty() {
139 content = "(no output)".to_string();
140 }
141
142 Ok(ToolResult {
143 content,
144 is_error: exit_code != 0,
145 })
146 }
147 Err(e) => Err(ToolError::ExecutionFailed(e.to_string())),
148 }
149 }
150 _ = tokio::time::sleep(timeout) => {
151 let _ = child.kill().await;
152 Err(ToolError::Timeout(timeout_ms))
153 }
154 _ = ctx.cancel.cancelled() => {
155 let _ = child.kill().await;
156 Err(ToolError::Cancelled)
157 }
158 };
159
160 result
161 }
162}