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}