use super::journal::{record, slot_from_cache, slot_from_result};
use crate::sdk::task::{build_result_table, build_task};
use crate::sdk::{PendingTask, SdkContext};
use luft_core::contract::event::{AgentEvent, LogLevel};
use mlua::{Lua, Table};
use std::sync::atomic::Ordering;
pub(super) fn register(lua: &Lua, cx: &SdkContext) -> mlua::Result<()> {
let globals = lua.globals();
let run_id = cx.run_id();
let sched = cx.scheduler.clone();
let journal = cx.journal.clone();
let handle = cx.handle.clone();
let events = cx.events();
let phase_counter = cx.phase_counter.clone();
let agent_seq_counter = cx.agent_seq_counter.clone();
let bridge = cx.coroutine_bridge.clone();
let agent_fn = lua.create_function(move |lua, opts: Table| {
let phase_id = phase_counter.load(Ordering::Relaxed);
let (task, cache_key, backend) = build_task(&opts, phase_id, &agent_seq_counter)?;
if let Some(ref j) = journal {
if let Some(cached) = j.get_cached(&cache_key) {
let _ = events.send(AgentEvent::Log {
run_id,
agent_id: None,
level: LogLevel::Info,
msg: format!(
"resume: skip cached agent ({}…)",
&cache_key.hash[..8.min(cache_key.hash.len())]
),
});
let (status, output, tokens, findings) = slot_from_cache(cached);
return build_result_table(lua, &status, output, tokens, &findings);
}
}
let agent_id = task.agent_id;
let in_pmap = bridge.is_in_pmap();
if in_pmap {
let pending = PendingTask {
task,
backend,
cache_key,
agent_id,
phase_id,
};
let request_id = bridge.deposit(pending);
tracing::debug!(%agent_id, request_id, "agent() returning pmap yield sentinel");
let sentinel = lua.create_table()?;
sentinel.set("__yield", request_id as i64)?;
Ok(sentinel)
} else {
tracing::debug!(%agent_id, "agent() submitting to scheduler (block_on)");
let result = handle
.block_on(sched.run_agent(run_id, task, backend.as_deref()))
.map_err(|e| {
tracing::error!(%agent_id, error = %e, "agent() scheduler error");
mlua::Error::RuntimeError(format!("agent error: {}", e))
})?;
tracing::debug!(%agent_id, "agent() completed");
record(&journal, &cache_key, agent_id, phase_id, &result);
let (status, output, tokens, findings) = slot_from_result(result);
build_result_table(lua, &status, output, tokens, &findings)
}
})?;
globals.set("agent", agent_fn)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sdk::test_support::make_sdk_context;
use mlua::Lua;
#[tokio::test]
async fn register_sets_agent_global_as_callable() {
let lua = Lua::new();
let cx = make_sdk_context();
register(&lua, &cx).expect("register must succeed");
let globals = lua.globals();
let agent: mlua::Function = globals
.get("agent")
.expect("agent global must be registered as a function");
let _ = agent;
}
#[tokio::test]
async fn register_does_not_set_parallel_or_pmap_globals() {
let lua = Lua::new();
let cx = make_sdk_context();
register(&lua, &cx).unwrap();
let globals = lua.globals();
assert!(globals.get::<mlua::Function>("agent").is_ok());
assert!(
globals.get::<mlua::Function>("parallel").is_err(),
"parallel must NOT be set by single::register"
);
assert!(
globals.get::<mlua::Function>("pmap").is_err(),
"pmap must NOT be set by single::register"
);
}
#[tokio::test]
async fn register_can_be_called_multiple_times_without_panic() {
let lua = Lua::new();
let cx = make_sdk_context();
register(&lua, &cx).unwrap();
register(&lua, &cx).unwrap();
register(&lua, &cx).unwrap();
assert!(lua.globals().get::<mlua::Function>("agent").is_ok());
}
#[tokio::test]
async fn agent_without_prompt_returns_runtime_error() {
let lua = Lua::new();
let cx = make_sdk_context();
register(&lua, &cx).unwrap();
let script = r#"local ok, err = pcall(agent, {}) return { ok = ok, err = tostring(err) }"#;
let result: mlua::Table = lua.load(script).eval().unwrap();
assert_eq!(
result.get::<bool>("ok").unwrap(),
false,
"agent(table) without prompt must fail"
);
let err = result.get::<String>("err").unwrap();
assert!(
err.contains("prompt"),
"error message should mention 'prompt'; got: {err}"
);
}
#[tokio::test]
async fn register_preserves_existing_globals_by_overwriting() {
let lua = Lua::new();
let cx = make_sdk_context();
lua.globals()
.set("agent", lua.create_function(|_, ()| Ok(42i64)).unwrap())
.unwrap();
register(&lua, &cx).unwrap();
let _: mlua::Function = lua.globals().get("agent").unwrap();
}
#[test]
fn single_registrar_is_in_super_module() {
let _f: fn(&Lua, &SdkContext) -> mlua::Result<()> = register;
}
}