Skip to main content

stynx_code_tools/infrastructure/
repl_tool.rs

1use stynx_code_errors::AppResult;
2use stynx_code_types::{InterruptBehavior, PermissionLevel, Tool};
3use serde_json::{Value, json};
4use tokio::process::Command;
5
6pub struct REPLTool;
7
8impl REPLTool {
9    pub fn new() -> Self {
10        Self
11    }
12}
13
14#[async_trait::async_trait]
15impl Tool for REPLTool {
16    fn name(&self) -> &str {
17        "repl"
18    }
19
20    fn description(&self) -> &str {
21        "Execute code in a Python or Node.js subprocess and return the output."
22    }
23
24    fn input_schema(&self) -> Value {
25        json!({
26            "type": "object",
27            "properties": {
28                "language": {
29                    "type": "string",
30                    "description": "Programming language: \"python\" or \"node\"",
31                    "enum": ["python", "node"]
32                },
33                "code": {
34                    "type": "string",
35                    "description": "The code to execute"
36                }
37            },
38            "required": ["language", "code"]
39        })
40    }
41
42    fn permission_level(&self) -> PermissionLevel {
43        PermissionLevel::Dangerous
44    }
45
46    fn interrupt_behavior(&self) -> InterruptBehavior {
47        InterruptBehavior::Cancel
48    }
49
50    async fn execute(&self, input: Value) -> AppResult<String> {
51        let language = input
52            .get("language")
53            .and_then(|v| v.as_str())
54            .ok_or_else(|| stynx_code_errors::AppError::Tool("missing 'language' field".into()))?;
55
56        let code = input
57            .get("code")
58            .and_then(|v| v.as_str())
59            .ok_or_else(|| stynx_code_errors::AppError::Tool("missing 'code' field".into()))?;
60
61        let (program, flag) = match language {
62            "python" => ("python3", "-c"),
63            "node" => ("node", "-e"),
64            other => return Err(stynx_code_errors::AppError::Tool(
65                format!("unsupported language: {other}. Use 'python' or 'node'.")
66            )),
67        };
68
69        tracing::info!(language, code_len = code.len(), "executing REPL");
70
71        let output = Command::new(program)
72            .arg(flag)
73            .arg(code)
74            .output()
75            .await
76            .map_err(|e| stynx_code_errors::AppError::Tool(
77                format!("failed to spawn {program}: {e}")
78            ))?;
79
80        let stdout = String::from_utf8_lossy(&output.stdout);
81        let stderr = String::from_utf8_lossy(&output.stderr);
82
83        let mut result = String::new();
84        if !stdout.is_empty() {
85            result.push_str(&stdout);
86        }
87        if !stderr.is_empty() {
88            if !result.is_empty() {
89                result.push('\n');
90            }
91            result.push_str("STDERR:\n");
92            result.push_str(&stderr);
93        }
94        if result.is_empty() {
95            result.push_str("(no output)");
96        }
97
98        if result.len() > 100_000 {
99            result.truncate(100_000);
100            result.push_str("\n... (truncated)");
101        }
102
103        Ok(result)
104    }
105}