Skip to main content

mlua_swarm/lua/
bridge.rs

1//! Lua bridge — parses Lua-authored BluePrints (a Lua-table
2//! representation of `mlua_flow_ir::Node`) into a Rust `Node`, plus the
3//! initial `ctx` when the Lua source also builds one.
4//!
5//! This module is **parse-only**. Execution goes through
6//! `crate::service::TaskLaunchService` (compile, `service::linker::link`,
7//! `Engine::dispatch_attempt_with`), not through this bridge — the
8//! one-shot glue that used to live here (build a throwaway `mlua::Lua`,
9//! parse, then drive the result straight through an `EngineDispatcher`)
10//! was retired once `TaskLaunchService` became the sole production entry
11//! point.
12//!
13//! `mlua-flow-ir` also ships a sync path (`flow.eval`) that lets a
14//! dispatcher be written on the Lua side, but engine-async ↔ sync
15//! impedance forces a `block_on` there. That would violate our
16//! discipline, so we do not use it.
17
18use crate::core::errors::EngineError;
19use mlua::LuaSerdeExt;
20use mlua_flow_ir::Node;
21use serde_json::Value;
22
23/// Convert a Lua-side flow table to a [`Node`], reading a legacy `assign`
24/// node as `let` the same way a JSON Blueprint is read
25/// (`mlua_swarm_schema::legacy_flow`).
26fn lua_to_node(lua: &mlua::Lua, bp_val: mlua::Value) -> Result<Node, EngineError> {
27    let raw: Value = lua
28        .from_value(bp_val)
29        .map_err(|e| EngineError::Internal(format!("lua → Node parse: {e}")))?;
30    mlua_swarm_schema::legacy_flow::flow_node_from_value(raw)
31        .map_err(|e| EngineError::Internal(format!("lua → Node parse: {e}")))
32}
33
34/// Load a Lua source, evaluate it, and pull out a BluePrint `Node`
35/// (`mlua_flow_ir::Node`). The Lua source must ultimately
36/// **`return` a table** — one that follows the flow.ir schema.
37///
38/// Example:
39/// ```lua
40/// return {
41///   kind = "seq",
42///   children = {
43///     { kind = "step", ref = "agent-a",
44///       in_  = {op="path", at="$.input"},
45///       out  = {op="path", at="$.mid"} },
46///     ...
47///   }
48/// }
49/// ```
50///
51/// Note: the serde tag is `kind` for `Node` and `op` for `Expr`;
52/// field names are the same snake_case as the Rust struct. `ref` is a
53/// reserved word, so the Lua key stays `ref` and serde renames
54/// `ref` ↔ `ref_`; the same holds for `in`.
55pub fn parse_lua_blueprint(lua_src: &str) -> Result<Node, EngineError> {
56    let lua = mlua::Lua::new();
57    let bp_val: mlua::Value = lua
58        .load(lua_src)
59        .eval()
60        .map_err(|e| EngineError::Internal(format!("lua eval: {e}")))?;
61    let bp = lua_to_node(&lua, bp_val)?;
62    Ok(bp)
63}
64
65/// Load a Lua source, and also build the initial `ctx` (a Lua table)
66/// on the Lua side and convert it to a JSON `Value`. Returns
67/// `(BluePrint, initial ctx)`. The Lua source is expected to return a
68/// table of the form `return { bp = ..., ctx = ... }`.
69pub fn parse_lua_blueprint_with_ctx(lua_src: &str) -> Result<(Node, Value), EngineError> {
70    let lua = mlua::Lua::new();
71    let outer: mlua::Table = lua
72        .load(lua_src)
73        .eval()
74        .map_err(|e| EngineError::Internal(format!("lua eval: {e}")))?;
75
76    let bp_val: mlua::Value = outer
77        .get("bp")
78        .map_err(|e| EngineError::Internal(format!("table missing `bp`: {e}")))?;
79    let ctx_val: mlua::Value = outer
80        .get("ctx")
81        .map_err(|e| EngineError::Internal(format!("table missing `ctx`: {e}")))?;
82
83    let bp = lua_to_node(&lua, bp_val)?;
84    let ctx: Value = lua
85        .from_value(ctx_val)
86        .map_err(|e| EngineError::Internal(format!("lua → ctx parse: {e}")))?;
87    Ok((bp, ctx))
88}