algocline_engine/
executor.rs1use std::path::PathBuf;
21
22use algocline_core::{Budget, ExecutionMetrics, ExecutionSpec};
23use mlua::LuaSerdeExt;
24use mlua_isle::{AsyncIsle, AsyncIsleDriver, IsleError};
25use mlua_pkg::{resolvers::FsResolver, Registry};
26
27use crate::bridge;
28use crate::llm_bridge::LlmRequest;
29use crate::session::Session;
30
31const PRELUDE: &str = include_str!("prelude.lua");
34
35pub struct Executor {
44 isle: AsyncIsle,
46 _driver: AsyncIsleDriver,
47 lib_paths: Vec<PathBuf>,
49}
50
51impl Executor {
52 pub async fn new(lib_paths: Vec<PathBuf>) -> anyhow::Result<Self> {
53 let paths_for_shared = lib_paths.clone();
54 let (isle, driver) = AsyncIsle::spawn(move |lua| {
55 let mut reg = Registry::new();
56 for path in &paths_for_shared {
57 if let Ok(resolver) = FsResolver::new(path) {
58 reg.add(resolver);
59 }
60 }
61 reg.install(lua)?;
62 Ok(())
63 })
64 .await?;
65
66 Ok(Self {
67 isle,
68 _driver: driver,
69 lib_paths,
70 })
71 }
72
73 pub async fn eval_simple(&self, code: String) -> Result<serde_json::Value, String> {
79 self.eval_simple_with_paths(code, vec![]).await
80 }
81
82 pub async fn eval_simple_with_paths(
89 &self,
90 code: String,
91 extra_lib_paths: Vec<PathBuf>,
92 ) -> Result<serde_json::Value, String> {
93 if extra_lib_paths.is_empty() {
94 let task = self.isle.spawn_exec(move |lua| {
96 let result: mlua::Value = lua
97 .load(&code)
98 .eval()
99 .map_err(|e| IsleError::Lua(e.to_string()))?;
100 let json: serde_json::Value = lua
101 .from_value(result)
102 .map_err(|e| IsleError::Lua(e.to_string()))?;
103 serde_json::to_string(&json)
104 .map_err(|e| IsleError::Lua(format!("JSON serialize: {e}")))
105 });
106 let json_str = task.await.map_err(|e| e.to_string())?;
107 return serde_json::from_str(&json_str).map_err(|e| format!("JSON parse: {e}"));
108 }
109
110 let mut effective = extra_lib_paths;
112 effective.extend(self.lib_paths.iter().cloned());
113
114 let (tmp_isle, _tmp_driver) = AsyncIsle::spawn(move |lua| {
115 let mut reg = Registry::new();
116 for path in &effective {
117 if let Ok(resolver) = FsResolver::new(path) {
118 reg.add(resolver);
119 }
120 }
121 reg.install(lua)?;
122 Ok(())
123 })
124 .await
125 .map_err(|e| format!("eval_simple VM spawn failed: {e}"))?;
126
127 let task = tmp_isle.spawn_exec(move |lua| {
128 let result: mlua::Value = lua
129 .load(&code)
130 .eval()
131 .map_err(|e| IsleError::Lua(e.to_string()))?;
132 let json: serde_json::Value = lua
133 .from_value(result)
134 .map_err(|e| IsleError::Lua(e.to_string()))?;
135 serde_json::to_string(&json).map_err(|e| IsleError::Lua(format!("JSON serialize: {e}")))
136 });
137
138 let json_str = task.await.map_err(|e| e.to_string())?;
139 serde_json::from_str(&json_str).map_err(|e| format!("JSON parse: {e}"))
140 }
141
142 pub async fn start_session(
152 &self,
153 code: String,
154 ctx: serde_json::Value,
155 extra_lib_paths: Vec<PathBuf>,
156 ) -> Result<Session, String> {
157 let spec = ExecutionSpec::new(code, ctx);
158 let metrics = ExecutionMetrics::new();
159
160 if let Some(budget) = Budget::from_ctx(&spec.ctx) {
162 metrics.set_budget(budget);
163 }
164
165 let (llm_tx, llm_rx) = tokio::sync::mpsc::channel::<LlmRequest>(16);
166
167 let mut effective = extra_lib_paths;
170 effective.extend(self.lib_paths.iter().cloned());
171
172 let bridge_config = bridge::BridgeConfig {
173 llm_tx: Some(llm_tx),
174 ns: spec.namespace.clone(),
175 custom_metrics: metrics.custom_metrics_handle(),
176 budget: metrics.budget_handle(),
177 progress: metrics.progress_handle(),
178 lib_paths: effective.clone(), };
180 let lua_ctx = spec.ctx.clone();
181 let lua_code = spec.code.clone();
182
183 let (session_isle, session_driver) = AsyncIsle::spawn(move |lua| {
185 let mut reg = Registry::new();
186 for path in &effective {
187 if let Ok(resolver) = FsResolver::new(path) {
188 reg.add(resolver);
189 }
190 }
191 reg.install(lua)?;
192 Ok(())
193 })
194 .await
195 .map_err(|e| format!("Session VM spawn failed: {e}"))?;
196
197 session_isle
200 .exec(move |lua| {
201 let alc_table = lua.create_table()?;
202 bridge::register(lua, &alc_table, bridge_config)?;
203 lua.globals().set("alc", alc_table)?;
204
205 let ctx_value = lua.to_value(&lua_ctx)?;
206 lua.globals().set("ctx", ctx_value)?;
207
208 lua.load(PRELUDE)
209 .exec()
210 .map_err(|e| IsleError::Lua(format!("Prelude load failed: {e}")))?;
211
212 Ok("ok".to_string())
215 })
216 .await
217 .map_err(|e| format!("Session setup failed: {e}"))?;
218
219 let wrapped_code = format!("return alc.json_encode((function()\n{lua_code}\nend)())");
221 let exec_task = session_isle.spawn_coroutine_eval(&wrapped_code);
222
223 drop(session_isle);
226
227 Ok(Session::new(llm_rx, exec_task, metrics, session_driver))
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234 use std::fs;
235
236 fn make_pkg_dir(parent: &std::path::Path, pkg_name: &str, init_lua: &str) -> PathBuf {
238 let pkg_dir = parent.join(pkg_name);
239 fs::create_dir_all(&pkg_dir).unwrap();
240 fs::write(pkg_dir.join("init.lua"), init_lua).unwrap();
241 parent.to_path_buf()
242 }
243
244 #[tokio::test]
246 async fn no_extra_lib_paths_eval_simple() {
247 let executor = Executor::new(vec![]).await.unwrap();
248 let result = executor.eval_simple("return 42".to_string()).await.unwrap();
249 assert_eq!(result, serde_json::json!(42));
250 }
251
252 #[tokio::test]
257 async fn extra_lib_paths_reachable_via_eval_simple_with_paths() {
258 let tmp = tempfile::tempdir().unwrap();
259 let pkg_root = make_pkg_dir(tmp.path(), "test_pkg", "return { value = 99 }");
260
261 let executor = Executor::new(vec![]).await.unwrap();
262 let code = r#"
263 local pkg = require("test_pkg")
264 return pkg.value
265 "#
266 .to_string();
267
268 let result = executor
269 .eval_simple_with_paths(code, vec![pkg_root])
270 .await
271 .unwrap();
272
273 assert_eq!(result, serde_json::json!(99));
274 }
275
276 #[tokio::test]
279 async fn extra_lib_paths_priority_over_default() {
280 let global_tmp = tempfile::tempdir().unwrap();
281 let extra_tmp = tempfile::tempdir().unwrap();
282
283 make_pkg_dir(global_tmp.path(), "test_pkg", "return { value = 1 }");
285 let extra_root = make_pkg_dir(extra_tmp.path(), "test_pkg", "return { value = 2 }");
287
288 let executor = Executor::new(vec![global_tmp.path().to_path_buf()])
290 .await
291 .unwrap();
292
293 let code = r#"
294 local pkg = require("test_pkg")
295 return pkg.value
296 "#
297 .to_string();
298
299 let result = executor
300 .eval_simple_with_paths(code, vec![extra_root])
301 .await
302 .unwrap();
303
304 assert_eq!(result, serde_json::json!(2));
306 }
307}