1use std::collections::HashMap;
21use std::path::PathBuf;
22use std::sync::Arc;
23
24use algocline_core::{Budget, ExecutionMetrics, ExecutionSpec};
25use mlua::LuaSerdeExt;
26use mlua_isle::{AsyncIsle, AsyncIsleDriver, IsleError};
27use mlua_pkg::Registry;
28
29use crate::bridge;
30use crate::card::FileCardStore;
31use crate::llm_bridge::LlmRequest;
32use crate::resolver_factory::make_resolver;
33use crate::session::Session;
34use crate::state::JsonFileStore;
35use crate::variant_pkg::{register_variant_pkgs, VariantPkg};
36
37const PRELUDE: &str = include_str!("prelude.lua");
40
41#[derive(Clone)]
52pub struct SessionDirs {
53 pub state_store: Arc<JsonFileStore>,
55 pub card_store: Arc<FileCardStore>,
57 pub scenarios_dir: PathBuf,
59 pub nn_dir: PathBuf,
63}
64
65pub struct Executor {
74 isle: AsyncIsle,
76 _driver: AsyncIsleDriver,
77 lib_paths: Vec<PathBuf>,
79}
80
81impl Executor {
82 pub async fn new(lib_paths: Vec<PathBuf>) -> anyhow::Result<Self> {
83 let paths_for_shared = lib_paths.clone();
84 let (isle, driver) = AsyncIsle::spawn(move |lua| {
85 let mut reg = Registry::new();
86 for path in &paths_for_shared {
87 if let Some(resolver) = make_resolver(path) {
88 reg.add(resolver);
89 }
90 }
91 reg.install(lua)?;
92 Ok(())
93 })
94 .await?;
95
96 Ok(Self {
97 isle,
98 _driver: driver,
99 lib_paths,
100 })
101 }
102
103 pub async fn eval_simple(&self, code: String) -> Result<serde_json::Value, String> {
109 self.eval_simple_with_paths(code, vec![], vec![]).await
110 }
111
112 pub async fn eval_simple_with_paths(
125 &self,
126 code: String,
127 extra_lib_paths: Vec<PathBuf>,
128 variant_pkgs: Vec<VariantPkg>,
129 ) -> Result<serde_json::Value, String> {
130 if extra_lib_paths.is_empty() && variant_pkgs.is_empty() {
131 let task = self.isle.spawn_exec(move |lua| {
133 let result: mlua::Value = lua
134 .load(&code)
135 .eval()
136 .map_err(|e| IsleError::Lua(e.to_string()))?;
137 let json: serde_json::Value = lua
138 .from_value(result)
139 .map_err(|e| IsleError::Lua(e.to_string()))?;
140 serde_json::to_string(&json)
141 .map_err(|e| IsleError::Lua(format!("JSON serialize: {e}")))
142 });
143 let json_str = task.await.map_err(|e| e.to_string())?;
144 return serde_json::from_str(&json_str).map_err(|e| format!("JSON parse: {e}"));
145 }
146
147 let mut effective = extra_lib_paths;
149 effective.extend(self.lib_paths.iter().cloned());
150
151 let (tmp_isle, _tmp_driver) = AsyncIsle::spawn(move |lua| {
152 let mut reg = Registry::new();
153 register_variant_pkgs(&mut reg, &variant_pkgs);
155 for path in &effective {
156 if let Some(resolver) = make_resolver(path) {
157 reg.add(resolver);
158 }
159 }
160 reg.install(lua)?;
161 Ok(())
162 })
163 .await
164 .map_err(|e| format!("eval_simple VM spawn failed: {e}"))?;
165
166 let task = tmp_isle.spawn_exec(move |lua| {
167 let result: mlua::Value = lua
168 .load(&code)
169 .eval()
170 .map_err(|e| IsleError::Lua(e.to_string()))?;
171 let json: serde_json::Value = lua
172 .from_value(result)
173 .map_err(|e| IsleError::Lua(e.to_string()))?;
174 serde_json::to_string(&json).map_err(|e| IsleError::Lua(format!("JSON serialize: {e}")))
175 });
176
177 let json_str = task.await.map_err(|e| e.to_string())?;
178 serde_json::from_str(&json_str).map_err(|e| format!("JSON parse: {e}"))
179 }
180
181 #[allow(clippy::too_many_arguments)]
200 pub async fn start_session(
201 &self,
202 code: String,
203 ctx: serde_json::Value,
204 extra_lib_paths: Vec<PathBuf>,
205 variant_pkgs: Vec<VariantPkg>,
206 dirs: SessionDirs,
207 card_run_enabled: bool,
208 ) -> Result<Session, String> {
209 let SessionDirs {
210 state_store,
211 card_store,
212 scenarios_dir,
213 nn_dir,
214 } = dirs;
215 let spec = ExecutionSpec::new(code, ctx);
216 let metrics = ExecutionMetrics::new();
217
218 if let Some(budget) = Budget::from_ctx(&spec.ctx) {
220 metrics.set_budget(budget);
221 }
222
223 let (llm_tx, llm_rx) = tokio::sync::mpsc::channel::<LlmRequest>(16);
224
225 let mut effective = extra_lib_paths;
229 effective.extend(self.lib_paths.iter().cloned());
230
231 let log_sink = metrics.log_sink_handle();
234
235 let log_sink_registration = crate::card::register_log_sink(log_sink.clone());
240
241 let bridge_config = bridge::BridgeConfig {
242 llm_tx: Some(llm_tx),
243 ns: spec.namespace.clone(),
244 custom_metrics: metrics.custom_metrics_handle(),
245 stats: metrics.stats_handle(),
246 budget: metrics.budget_handle(),
247 progress: metrics.progress_handle(),
248 lib_paths: effective.clone(), variant_pkgs: variant_pkgs.clone(), state_store,
251 card_store,
252 card_run_enabled,
253 scenarios_dir,
254 nn_dir,
255 log_sink: Some(log_sink.clone()),
256 };
257 let lua_ctx = spec.ctx.clone();
258 let lua_code = spec.code.clone();
259
260 let (session_isle, session_driver) = AsyncIsle::spawn(move |lua| {
262 let mut reg = Registry::new();
263 register_variant_pkgs(&mut reg, &variant_pkgs);
265 for path in &effective {
266 if let Some(resolver) = make_resolver(path) {
267 reg.add(resolver);
268 }
269 }
270 reg.install(lua)?;
271 Ok(())
272 })
273 .await
274 .map_err(|e| format!("Session VM spawn failed: {e}"))?;
275
276 session_isle
279 .exec(move |lua| {
280 let alc_table = lua.create_table()?;
281 bridge::register(lua, &alc_table, bridge_config)?;
282 lua.globals().set("alc", alc_table)?;
283
284 bridge::register_evalframe(lua)
288 .map_err(|e| IsleError::Lua(format!("register_evalframe failed: {e}")))?;
289
290 let ctx_value = lua.to_value(&lua_ctx)?;
291 lua.globals().set("ctx", ctx_value)?;
292
293 lua.load(PRELUDE)
294 .exec()
295 .map_err(|e| IsleError::Lua(format!("Prelude load failed: {e}")))?;
296
297 Ok("ok".to_string())
309 })
310 .await
311 .map_err(|e| format!("Session setup failed: {e}"))?;
312
313 let wrapped_code = format!("return alc.json_encode((function()\n{lua_code}\nend)())");
315 let exec_task = session_isle.spawn_coroutine_eval(&wrapped_code);
316
317 drop(session_isle);
320
321 let mut session = Session::new(llm_rx, exec_task, metrics, session_driver);
322 session.attach_log_sink_registration(Some(log_sink_registration));
323 Ok(session)
324 }
325
326 #[allow(clippy::too_many_arguments)]
337 pub async fn start_session_with_env(
338 &self,
339 env_map: Arc<HashMap<String, String>>,
340 code: String,
341 ctx: serde_json::Value,
342 extra_lib_paths: Vec<PathBuf>,
343 variant_pkgs: Vec<VariantPkg>,
344 dirs: SessionDirs,
345 card_run_enabled: bool,
346 ) -> Result<Session, String> {
347 let SessionDirs {
348 state_store,
349 card_store,
350 scenarios_dir,
351 nn_dir,
352 } = dirs;
353 let spec = ExecutionSpec::new(code, ctx);
354 let metrics = ExecutionMetrics::new();
355
356 if let Some(budget) = Budget::from_ctx(&spec.ctx) {
357 metrics.set_budget(budget);
358 }
359
360 let (llm_tx, llm_rx) = tokio::sync::mpsc::channel::<LlmRequest>(16);
361
362 let mut effective = extra_lib_paths;
363 effective.extend(self.lib_paths.iter().cloned());
364
365 let log_sink = metrics.log_sink_handle();
366
367 let log_sink_registration = crate::card::register_log_sink(log_sink.clone());
370
371 let bridge_config = bridge::BridgeConfig {
372 llm_tx: Some(llm_tx),
373 ns: spec.namespace.clone(),
374 custom_metrics: metrics.custom_metrics_handle(),
375 stats: metrics.stats_handle(),
376 budget: metrics.budget_handle(),
377 progress: metrics.progress_handle(),
378 lib_paths: effective.clone(),
379 variant_pkgs: variant_pkgs.clone(),
380 state_store,
381 card_store,
382 card_run_enabled,
383 scenarios_dir,
384 nn_dir,
385 log_sink: Some(log_sink.clone()),
386 };
387 let lua_ctx = spec.ctx.clone();
388 let lua_code = spec.code.clone();
389
390 let (session_isle, session_driver) = AsyncIsle::spawn(move |lua| {
391 let mut reg = Registry::new();
392 register_variant_pkgs(&mut reg, &variant_pkgs);
393 for path in &effective {
394 if let Some(resolver) = make_resolver(path) {
395 reg.add(resolver);
396 }
397 }
398 reg.install(lua)?;
399 Ok(())
400 })
401 .await
402 .map_err(|e| format!("Session VM spawn failed: {e}"))?;
403
404 session_isle
405 .exec(move |lua| {
406 let alc_table = lua.create_table()?;
407 bridge::register(lua, &alc_table, bridge_config)?;
408 bridge::register_env(lua, &alc_table, env_map)
411 .map_err(|e| IsleError::Lua(e.to_string()))?;
412 lua.globals().set("alc", alc_table)?;
413
414 bridge::register_evalframe(lua)
416 .map_err(|e| IsleError::Lua(format!("register_evalframe failed: {e}")))?;
417
418 let ctx_value = lua.to_value(&lua_ctx)?;
419 lua.globals().set("ctx", ctx_value)?;
420
421 lua.load(PRELUDE)
422 .exec()
423 .map_err(|e| IsleError::Lua(format!("Prelude load failed: {e}")))?;
424
425 Ok("ok".to_string())
426 })
427 .await
428 .map_err(|e| format!("Session setup failed: {e}"))?;
429
430 let wrapped_code = format!("return alc.json_encode((function()\n{lua_code}\nend)())");
431 let exec_task = session_isle.spawn_coroutine_eval(&wrapped_code);
432
433 drop(session_isle);
434
435 let mut session = Session::new(llm_rx, exec_task, metrics, session_driver);
436 session.attach_log_sink_registration(Some(log_sink_registration));
437 Ok(session)
438 }
439}
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444 use std::fs;
445
446 fn make_pkg_dir(parent: &std::path::Path, pkg_name: &str, init_lua: &str) -> PathBuf {
448 let pkg_dir = parent.join(pkg_name);
449 fs::create_dir_all(&pkg_dir).unwrap();
450 fs::write(pkg_dir.join("init.lua"), init_lua).unwrap();
451 parent.to_path_buf()
452 }
453
454 #[tokio::test]
456 async fn no_extra_lib_paths_eval_simple() {
457 let executor = Executor::new(vec![]).await.unwrap();
458 let result = executor.eval_simple("return 42".to_string()).await.unwrap();
459 assert_eq!(result, serde_json::json!(42));
460 }
461
462 #[tokio::test]
467 async fn extra_lib_paths_reachable_via_eval_simple_with_paths() {
468 let tmp = tempfile::tempdir().unwrap();
469 let pkg_root = make_pkg_dir(tmp.path(), "test_pkg", "return { value = 99 }");
470
471 let executor = Executor::new(vec![]).await.unwrap();
472 let code = r#"
473 local pkg = require("test_pkg")
474 return pkg.value
475 "#
476 .to_string();
477
478 let result = executor
479 .eval_simple_with_paths(code, vec![pkg_root], vec![])
480 .await
481 .unwrap();
482
483 assert_eq!(result, serde_json::json!(99));
484 }
485
486 #[tokio::test]
489 async fn variant_pkg_resolves_root_and_submodule() {
490 let tmp = tempfile::tempdir().unwrap();
491 let pkg_dir = tmp.path().join("physical-dir");
494 fs::create_dir_all(&pkg_dir).unwrap();
495 fs::write(
496 pkg_dir.join("init.lua"),
497 "return { greet = function(n) return 'hi-' .. n end, sub = require('logical_name.sub') }",
498 )
499 .unwrap();
500 fs::write(pkg_dir.join("sub.lua"), "return { value = 7 }").unwrap();
501
502 let executor = Executor::new(vec![]).await.unwrap();
503 let code = r#"
504 local pkg = require("logical_name")
505 return { msg = pkg.greet("there"), sub_value = pkg.sub.value }
506 "#
507 .to_string();
508
509 let result = executor
510 .eval_simple_with_paths(code, vec![], vec![VariantPkg::new("logical_name", pkg_dir)])
511 .await
512 .unwrap();
513
514 assert_eq!(result["msg"], serde_json::json!("hi-there"));
515 assert_eq!(result["sub_value"], serde_json::json!(7));
516 }
517
518 #[tokio::test]
520 async fn variant_pkg_overrides_global_same_name() {
521 let global_tmp = tempfile::tempdir().unwrap();
522 let variant_tmp = tempfile::tempdir().unwrap();
523
524 make_pkg_dir(global_tmp.path(), "my_pkg", "return { value = 1 }");
526 let variant_dir = variant_tmp.path().join("my_pkg");
528 fs::create_dir_all(&variant_dir).unwrap();
529 fs::write(variant_dir.join("init.lua"), "return { value = 2 }").unwrap();
530
531 let executor = Executor::new(vec![global_tmp.path().to_path_buf()])
532 .await
533 .unwrap();
534
535 let code = r#"
536 local pkg = require("my_pkg")
537 return pkg.value
538 "#
539 .to_string();
540
541 let result = executor
542 .eval_simple_with_paths(code, vec![], vec![VariantPkg::new("my_pkg", variant_dir)])
543 .await
544 .unwrap();
545
546 assert_eq!(result, serde_json::json!(2));
547 }
548
549 #[tokio::test]
552 async fn extra_lib_paths_priority_over_default() {
553 let global_tmp = tempfile::tempdir().unwrap();
554 let extra_tmp = tempfile::tempdir().unwrap();
555
556 make_pkg_dir(global_tmp.path(), "test_pkg", "return { value = 1 }");
558 let extra_root = make_pkg_dir(extra_tmp.path(), "test_pkg", "return { value = 2 }");
560
561 let executor = Executor::new(vec![global_tmp.path().to_path_buf()])
563 .await
564 .unwrap();
565
566 let code = r#"
567 local pkg = require("test_pkg")
568 return pkg.value
569 "#
570 .to_string();
571
572 let result = executor
573 .eval_simple_with_paths(code, vec![extra_root], vec![])
574 .await
575 .unwrap();
576
577 assert_eq!(result, serde_json::json!(2));
579 }
580}