1use crate::core::ctx::Ctx;
24use crate::core::engine::Engine;
25use crate::middleware::SpawnerLayer;
26use crate::types::{CapToken, StepId};
27use crate::worker::adapter::{SpawnError, SpawnerAdapter, WorkerError};
28use crate::worker::{wrap_join, Worker};
29use async_trait::async_trait;
30use mlua::LuaSerdeExt;
31use mlua_isle::{AsyncIslePool, IsleError, LuaErrorKind, LuaFailure, PoolConfig, PoolStrategy};
32use serde_json::Value;
33use std::sync::{Arc, OnceLock};
34
35fn default_pool_config() -> PoolConfig {
38 PoolConfig {
39 max_size: 4,
40 strategy: PoolStrategy::Warm,
41 }
42}
43
44fn build_default_pool() -> Arc<AsyncIslePool> {
45 Arc::new(
46 AsyncIslePool::new(|_lua| Ok(()), default_pool_config())
47 .expect("AsyncIslePool::new (no-op factory) must succeed"),
48 )
49}
50
51#[derive(Clone, Default)]
56pub struct LuaMiddleware {
57 before_src: Option<String>,
58 after_src: Option<String>,
59 pool: Option<Arc<AsyncIslePool>>,
60}
61
62impl LuaMiddleware {
63 pub fn new() -> Self {
65 Self::default()
66 }
67 pub fn before(mut self, src: impl Into<String>) -> Self {
70 self.before_src = Some(src.into());
71 self
72 }
73 pub fn after(mut self, src: impl Into<String>) -> Self {
77 self.after_src = Some(src.into());
78 self
79 }
80 pub fn with_pool(mut self, pool: Arc<AsyncIslePool>) -> Self {
84 self.pool = Some(pool);
85 self
86 }
87}
88
89impl SpawnerLayer for LuaMiddleware {
90 fn wrap(&self, inner: Arc<dyn SpawnerAdapter>) -> Arc<dyn SpawnerAdapter> {
91 static DEFAULT_POOL: OnceLock<Arc<AsyncIslePool>> = OnceLock::new();
93 let pool = self
94 .pool
95 .clone()
96 .unwrap_or_else(|| DEFAULT_POOL.get_or_init(build_default_pool).clone());
97 Arc::new(LuaWrapped {
98 inner,
99 before_src: self.before_src.clone(),
100 after_src: self.after_src.clone(),
101 pool,
102 })
103 }
104}
105
106struct LuaWrapped {
107 inner: Arc<dyn SpawnerAdapter>,
108 before_src: Option<String>,
109 after_src: Option<String>,
110 pool: Arc<AsyncIslePool>,
111}
112
113fn ctx_to_serializable(ctx: &Ctx) -> Value {
115 serde_json::json!({
116 "task_id": ctx.task_id.as_str(),
117 "attempt": ctx.attempt,
118 "agent": ctx.agent,
119 "operator": {
120 "kind": format!("{:?}", ctx.operator.kind),
121 "id": ctx.operator.id,
122 },
123 })
124}
125
126fn json_failure(e: serde_json::Error) -> IsleError {
129 IsleError::Lua(LuaFailure::new(LuaErrorKind::Conversion, e.to_string()))
130}
131
132fn make_before_exec(
136 src: String,
137 ctx_json: String,
138) -> impl FnOnce(&mlua::Lua) -> Result<String, IsleError> + Send + 'static {
139 move |lua| {
140 let ctx_val: Value = serde_json::from_str(&ctx_json).map_err(json_failure)?;
141 let ctx_lua: mlua::Value = lua.to_value(&ctx_val)?;
142 let f: mlua::Function = lua.load(&src).eval()?;
143 let _: mlua::Value = f.call(ctx_lua)?;
144 Ok("ok".to_string())
145 }
146}
147
148fn make_after_exec(
151 src: String,
152 ctx_json: String,
153 result_json: String,
154) -> impl FnOnce(&mlua::Lua) -> Result<String, IsleError> + Send + 'static {
155 move |lua| {
156 let ctx_val: Value = serde_json::from_str(&ctx_json).map_err(json_failure)?;
157 let result_val: Value = serde_json::from_str(&result_json).map_err(json_failure)?;
158 let ctx_lua: mlua::Value = lua.to_value(&ctx_val)?;
159 let result_lua: mlua::Value = lua.to_value(&result_val)?;
160 let f: mlua::Function = lua.load(&src).eval()?;
161 let returned: mlua::Value = f.call((ctx_lua, result_lua))?;
162 let new_result: Value = lua.from_value(returned)?;
163 serde_json::to_string(&new_result).map_err(json_failure)
164 }
165}
166
167#[async_trait]
168impl SpawnerAdapter for LuaWrapped {
169 async fn spawn(
170 &self,
171 engine: &Engine,
172 ctx: &Ctx,
173 task_id: StepId,
174 attempt: u32,
175 token: CapToken,
176 ) -> Result<Box<dyn Worker>, SpawnError> {
177 if let Some(src) = &self.before_src {
179 let ctx_json = serde_json::to_string(&ctx_to_serializable(ctx))
180 .map_err(|e| SpawnError::Internal(format!("ctx serialize: {e}")))?;
181 let isle = self
182 .pool
183 .checkout()
184 .await
185 .map_err(|e| SpawnError::Internal(format!("isle pool checkout: {e}")))?;
186 let f = make_before_exec(src.clone(), ctx_json);
187 isle.exec(f)
188 .await
189 .map_err(|e| SpawnError::RejectedByMiddleware(format!("lua before: {e}")))?;
190 }
192
193 let engine_clone = engine.clone();
194 let token_clone = token.clone();
195 let task_id_clone = task_id.clone();
196 let handle = self
197 .inner
198 .spawn(engine, ctx, task_id, attempt, token)
199 .await?;
200
201 let Some(after_src) = self.after_src.clone() else {
203 return Ok(handle);
204 };
205 let ctx_val = ctx_to_serializable(ctx);
206 Ok(wrap_completion_with_lua_pool(
207 handle,
208 after_src,
209 ctx_val,
210 self.pool.clone(),
211 engine_clone,
212 token_clone,
213 task_id_clone,
214 attempt,
215 ))
216 }
217}
218
219#[allow(clippy::too_many_arguments)]
224fn wrap_completion_with_lua_pool(
225 handle: Box<dyn Worker>,
226 after_src: String,
227 ctx_val: Value,
228 pool: Arc<AsyncIslePool>,
229 engine: Engine,
230 token: crate::types::CapToken,
231 task_id: StepId,
232 attempt: u32,
233) -> Box<dyn Worker> {
234 wrap_join(handle, move |signal| async move {
235 match signal {
236 Ok(()) => {
237 let _ = apply_lua_after_pool(
238 &after_src, &ctx_val, &pool, &engine, &token, &task_id, attempt,
239 )
240 .await;
241 Ok(())
242 }
243 Err(e) => Err(e),
244 }
245 })
246}
247
248async fn apply_lua_after_pool(
249 after_src: &str,
250 ctx_val: &Value,
251 pool: &AsyncIslePool,
252 engine: &Engine,
253 token: &crate::types::CapToken,
254 task_id: &StepId,
255 attempt: u32,
256) -> Result<(), WorkerError> {
257 let tail = engine.output_tail(task_id, attempt).await;
259 let (value, ok) = match tail.iter().rev().find_map(|ev| match ev {
260 crate::worker::output::OutputEvent::Final {
261 content: crate::worker::output::ContentRef::Inline { value },
262 ok,
263 } => Some((value.clone(), *ok)),
264 _ => None,
265 }) {
266 Some(v) => v,
267 None => return Ok(()), };
269
270 let ctx_json = serde_json::to_string(ctx_val)
271 .map_err(|e| WorkerError::Failed(format!("ctx serialize: {e}")))?;
272 let result_json = serde_json::to_string(&serde_json::json!({"value": value, "ok": ok}))
273 .map_err(|e| WorkerError::Failed(format!("result serialize: {e}")))?;
274
275 let isle = pool
276 .checkout()
277 .await
278 .map_err(|e| WorkerError::Failed(format!("isle pool checkout: {e}")))?;
279 let f = make_after_exec(after_src.to_string(), ctx_json, result_json);
280 let new_json = isle
281 .exec(f)
282 .await
283 .map_err(|e| WorkerError::Failed(format!("lua after: {e}")))?;
284 let new_result: Value = serde_json::from_str(&new_json)
285 .map_err(|e| WorkerError::Failed(format!("lua after decode: {e}")))?;
286 let new_value = new_result.get("value").cloned().unwrap_or(Value::Null);
287 let new_ok = new_result.get("ok").and_then(|v| v.as_bool()).unwrap_or(ok);
288
289 let _ = engine
291 .submit_output(
292 token,
293 task_id,
294 attempt,
295 crate::worker::output::OutputEvent::Final {
296 content: crate::worker::output::ContentRef::Inline { value: new_value },
297 ok: new_ok,
298 },
299 )
300 .await;
301 Ok(())
302}