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::Registry;
26
27use crate::bridge;
28use crate::llm_bridge::LlmRequest;
29use crate::resolver_factory::make_resolver;
30use crate::session::Session;
31use crate::variant_pkg::{register_variant_pkgs, VariantPkg};
32
33const PRELUDE: &str = include_str!("prelude.lua");
36
37pub struct Executor {
46 isle: AsyncIsle,
48 _driver: AsyncIsleDriver,
49 lib_paths: Vec<PathBuf>,
51}
52
53impl Executor {
54 pub async fn new(lib_paths: Vec<PathBuf>) -> anyhow::Result<Self> {
55 let paths_for_shared = lib_paths.clone();
56 let (isle, driver) = AsyncIsle::spawn(move |lua| {
57 let mut reg = Registry::new();
58 for path in &paths_for_shared {
59 if let Some(resolver) = make_resolver(path) {
60 reg.add(resolver);
61 }
62 }
63 reg.install(lua)?;
64 Ok(())
65 })
66 .await?;
67
68 Ok(Self {
69 isle,
70 _driver: driver,
71 lib_paths,
72 })
73 }
74
75 pub async fn eval_simple(&self, code: String) -> Result<serde_json::Value, String> {
81 self.eval_simple_with_paths(code, vec![], vec![]).await
82 }
83
84 pub async fn eval_simple_with_paths(
92 &self,
93 code: String,
94 extra_lib_paths: Vec<PathBuf>,
95 variant_pkgs: Vec<VariantPkg>,
96 ) -> Result<serde_json::Value, String> {
97 if extra_lib_paths.is_empty() && variant_pkgs.is_empty() {
98 let task = self.isle.spawn_exec(move |lua| {
100 let result: mlua::Value = lua
101 .load(&code)
102 .eval()
103 .map_err(|e| IsleError::Lua(e.to_string()))?;
104 let json: serde_json::Value = lua
105 .from_value(result)
106 .map_err(|e| IsleError::Lua(e.to_string()))?;
107 serde_json::to_string(&json)
108 .map_err(|e| IsleError::Lua(format!("JSON serialize: {e}")))
109 });
110 let json_str = task.await.map_err(|e| e.to_string())?;
111 return serde_json::from_str(&json_str).map_err(|e| format!("JSON parse: {e}"));
112 }
113
114 let mut effective = extra_lib_paths;
116 effective.extend(self.lib_paths.iter().cloned());
117
118 let (tmp_isle, _tmp_driver) = AsyncIsle::spawn(move |lua| {
119 let mut reg = Registry::new();
120 register_variant_pkgs(&mut reg, &variant_pkgs);
122 for path in &effective {
123 if let Some(resolver) = make_resolver(path) {
124 reg.add(resolver);
125 }
126 }
127 reg.install(lua)?;
128 Ok(())
129 })
130 .await
131 .map_err(|e| format!("eval_simple VM spawn failed: {e}"))?;
132
133 let task = tmp_isle.spawn_exec(move |lua| {
134 let result: mlua::Value = lua
135 .load(&code)
136 .eval()
137 .map_err(|e| IsleError::Lua(e.to_string()))?;
138 let json: serde_json::Value = lua
139 .from_value(result)
140 .map_err(|e| IsleError::Lua(e.to_string()))?;
141 serde_json::to_string(&json).map_err(|e| IsleError::Lua(format!("JSON serialize: {e}")))
142 });
143
144 let json_str = task.await.map_err(|e| e.to_string())?;
145 serde_json::from_str(&json_str).map_err(|e| format!("JSON parse: {e}"))
146 }
147
148 pub async fn start_session(
160 &self,
161 code: String,
162 ctx: serde_json::Value,
163 extra_lib_paths: Vec<PathBuf>,
164 variant_pkgs: Vec<VariantPkg>,
165 ) -> Result<Session, String> {
166 let spec = ExecutionSpec::new(code, ctx);
167 let metrics = ExecutionMetrics::new();
168
169 if let Some(budget) = Budget::from_ctx(&spec.ctx) {
171 metrics.set_budget(budget);
172 }
173
174 let (llm_tx, llm_rx) = tokio::sync::mpsc::channel::<LlmRequest>(16);
175
176 let mut effective = extra_lib_paths;
180 effective.extend(self.lib_paths.iter().cloned());
181
182 let bridge_config = bridge::BridgeConfig {
183 llm_tx: Some(llm_tx),
184 ns: spec.namespace.clone(),
185 custom_metrics: metrics.custom_metrics_handle(),
186 budget: metrics.budget_handle(),
187 progress: metrics.progress_handle(),
188 lib_paths: effective.clone(), variant_pkgs: variant_pkgs.clone(), };
191 let lua_ctx = spec.ctx.clone();
192 let lua_code = spec.code.clone();
193
194 let (session_isle, session_driver) = AsyncIsle::spawn(move |lua| {
196 let mut reg = Registry::new();
197 register_variant_pkgs(&mut reg, &variant_pkgs);
199 for path in &effective {
200 if let Some(resolver) = make_resolver(path) {
201 reg.add(resolver);
202 }
203 }
204 reg.install(lua)?;
205 Ok(())
206 })
207 .await
208 .map_err(|e| format!("Session VM spawn failed: {e}"))?;
209
210 session_isle
213 .exec(move |lua| {
214 let alc_table = lua.create_table()?;
215 bridge::register(lua, &alc_table, bridge_config)?;
216 lua.globals().set("alc", alc_table)?;
217
218 let ctx_value = lua.to_value(&lua_ctx)?;
219 lua.globals().set("ctx", ctx_value)?;
220
221 lua.load(PRELUDE)
222 .exec()
223 .map_err(|e| IsleError::Lua(format!("Prelude load failed: {e}")))?;
224
225 Ok("ok".to_string())
228 })
229 .await
230 .map_err(|e| format!("Session setup failed: {e}"))?;
231
232 let wrapped_code = format!("return alc.json_encode((function()\n{lua_code}\nend)())");
234 let exec_task = session_isle.spawn_coroutine_eval(&wrapped_code);
235
236 drop(session_isle);
239
240 Ok(Session::new(llm_rx, exec_task, metrics, session_driver))
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247 use std::fs;
248
249 fn make_pkg_dir(parent: &std::path::Path, pkg_name: &str, init_lua: &str) -> PathBuf {
251 let pkg_dir = parent.join(pkg_name);
252 fs::create_dir_all(&pkg_dir).unwrap();
253 fs::write(pkg_dir.join("init.lua"), init_lua).unwrap();
254 parent.to_path_buf()
255 }
256
257 #[tokio::test]
259 async fn no_extra_lib_paths_eval_simple() {
260 let executor = Executor::new(vec![]).await.unwrap();
261 let result = executor.eval_simple("return 42".to_string()).await.unwrap();
262 assert_eq!(result, serde_json::json!(42));
263 }
264
265 #[tokio::test]
270 async fn extra_lib_paths_reachable_via_eval_simple_with_paths() {
271 let tmp = tempfile::tempdir().unwrap();
272 let pkg_root = make_pkg_dir(tmp.path(), "test_pkg", "return { value = 99 }");
273
274 let executor = Executor::new(vec![]).await.unwrap();
275 let code = r#"
276 local pkg = require("test_pkg")
277 return pkg.value
278 "#
279 .to_string();
280
281 let result = executor
282 .eval_simple_with_paths(code, vec![pkg_root], vec![])
283 .await
284 .unwrap();
285
286 assert_eq!(result, serde_json::json!(99));
287 }
288
289 #[tokio::test]
292 async fn variant_pkg_resolves_root_and_submodule() {
293 let tmp = tempfile::tempdir().unwrap();
294 let pkg_dir = tmp.path().join("physical-dir");
297 fs::create_dir_all(&pkg_dir).unwrap();
298 fs::write(
299 pkg_dir.join("init.lua"),
300 "return { greet = function(n) return 'hi-' .. n end, sub = require('logical_name.sub') }",
301 )
302 .unwrap();
303 fs::write(pkg_dir.join("sub.lua"), "return { value = 7 }").unwrap();
304
305 let executor = Executor::new(vec![]).await.unwrap();
306 let code = r#"
307 local pkg = require("logical_name")
308 return { msg = pkg.greet("there"), sub_value = pkg.sub.value }
309 "#
310 .to_string();
311
312 let result = executor
313 .eval_simple_with_paths(code, vec![], vec![VariantPkg::new("logical_name", pkg_dir)])
314 .await
315 .unwrap();
316
317 assert_eq!(result["msg"], serde_json::json!("hi-there"));
318 assert_eq!(result["sub_value"], serde_json::json!(7));
319 }
320
321 #[tokio::test]
323 async fn variant_pkg_overrides_global_same_name() {
324 let global_tmp = tempfile::tempdir().unwrap();
325 let variant_tmp = tempfile::tempdir().unwrap();
326
327 make_pkg_dir(global_tmp.path(), "my_pkg", "return { value = 1 }");
329 let variant_dir = variant_tmp.path().join("my_pkg");
331 fs::create_dir_all(&variant_dir).unwrap();
332 fs::write(variant_dir.join("init.lua"), "return { value = 2 }").unwrap();
333
334 let executor = Executor::new(vec![global_tmp.path().to_path_buf()])
335 .await
336 .unwrap();
337
338 let code = r#"
339 local pkg = require("my_pkg")
340 return pkg.value
341 "#
342 .to_string();
343
344 let result = executor
345 .eval_simple_with_paths(code, vec![], vec![VariantPkg::new("my_pkg", variant_dir)])
346 .await
347 .unwrap();
348
349 assert_eq!(result, serde_json::json!(2));
350 }
351
352 #[tokio::test]
355 async fn extra_lib_paths_priority_over_default() {
356 let global_tmp = tempfile::tempdir().unwrap();
357 let extra_tmp = tempfile::tempdir().unwrap();
358
359 make_pkg_dir(global_tmp.path(), "test_pkg", "return { value = 1 }");
361 let extra_root = make_pkg_dir(extra_tmp.path(), "test_pkg", "return { value = 2 }");
363
364 let executor = Executor::new(vec![global_tmp.path().to_path_buf()])
366 .await
367 .unwrap();
368
369 let code = r#"
370 local pkg = require("test_pkg")
371 return pkg.value
372 "#
373 .to_string();
374
375 let result = executor
376 .eval_simple_with_paths(code, vec![extra_root], vec![])
377 .await
378 .unwrap();
379
380 assert_eq!(result, serde_json::json!(2));
382 }
383}