mlua_swarm/middleware/
worker_binding.rs1use crate::core::ctx::Ctx;
23use crate::core::engine::Engine;
24use crate::middleware::SpawnerLayer;
25use crate::operator::WorkerBinding;
26use crate::types::{CapToken, TaskId};
27use crate::worker::adapter::{SpawnError, SpawnerAdapter};
28use crate::worker::Worker;
29use async_trait::async_trait;
30use std::collections::HashMap;
31use std::sync::Arc;
32
33pub const WORKER_BINDING_KEY: &str = "worker_binding";
36
37pub struct WorkerBindingMiddleware {
40 bindings: Arc<HashMap<String, WorkerBinding>>,
41}
42
43impl WorkerBindingMiddleware {
44 pub fn new(bindings: HashMap<String, WorkerBinding>) -> Self {
47 Self {
48 bindings: Arc::new(bindings),
49 }
50 }
51}
52
53impl SpawnerLayer for WorkerBindingMiddleware {
54 fn wrap(&self, inner: Arc<dyn SpawnerAdapter>) -> Arc<dyn SpawnerAdapter> {
55 Arc::new(WorkerBindingWrapped {
56 inner,
57 bindings: self.bindings.clone(),
58 })
59 }
60}
61
62struct WorkerBindingWrapped {
63 inner: Arc<dyn SpawnerAdapter>,
64 bindings: Arc<HashMap<String, WorkerBinding>>,
65}
66
67#[async_trait]
68impl SpawnerAdapter for WorkerBindingWrapped {
69 async fn spawn(
70 &self,
71 engine: &Engine,
72 ctx: &Ctx,
73 task_id: TaskId,
74 attempt: u32,
75 token: CapToken,
76 ) -> Result<Box<dyn Worker>, SpawnError> {
77 let Some(binding) = self.bindings.get(&ctx.agent) else {
78 return self.inner.spawn(engine, ctx, task_id, attempt, token).await;
81 };
82 let value = serde_json::to_value(binding).map_err(|e| {
83 SpawnError::Internal(format!(
84 "worker_binding for agent '{}' failed to serialize: {e}",
85 ctx.agent
86 ))
87 })?;
88 let mut new_ctx = ctx.clone();
89 new_ctx
90 .meta
91 .runtime
92 .insert(WORKER_BINDING_KEY.to_string(), value);
93 self.inner
94 .spawn(engine, &new_ctx, task_id, attempt, token)
95 .await
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102 use crate::core::config::EngineCfg;
103 use crate::types::Role;
104 use std::sync::Mutex;
105 use std::time::Duration;
106
107 struct CtxProbe {
110 seen: Arc<Mutex<Option<Ctx>>>,
111 }
112
113 #[async_trait]
114 impl SpawnerAdapter for CtxProbe {
115 async fn spawn(
116 &self,
117 _engine: &Engine,
118 ctx: &Ctx,
119 _task_id: TaskId,
120 _attempt: u32,
121 _token: CapToken,
122 ) -> Result<Box<dyn Worker>, SpawnError> {
123 *self.seen.lock().unwrap() = Some(ctx.clone());
124 Err(SpawnError::Internal("probe stop".into()))
125 }
126 }
127
128 fn probe_stack(
129 bindings: HashMap<String, WorkerBinding>,
130 ) -> (Arc<dyn SpawnerAdapter>, Arc<Mutex<Option<Ctx>>>) {
131 let seen = Arc::new(Mutex::new(None));
132 let inner = Arc::new(CtxProbe { seen: seen.clone() });
133 let wrapped = WorkerBindingMiddleware::new(bindings).wrap(inner);
134 (wrapped, seen)
135 }
136
137 #[tokio::test]
138 async fn injects_binding_into_ctx_meta_runtime_on_hit() {
139 let mut map = HashMap::new();
140 map.insert(
141 "planner".to_string(),
142 WorkerBinding {
143 variant: "mse-worker-knowledge".to_string(),
144 tools: vec!["Read".to_string()],
145 },
146 );
147 let (stack, seen) = probe_stack(map);
148 let engine = Engine::new(EngineCfg::default());
149 let task_id = TaskId("t-1".to_string());
150 let ctx = Ctx::new(task_id.clone(), 1, "planner");
151 let token = engine
152 .attach("ut-op", Role::Operator, Duration::from_secs(30))
153 .await
154 .expect("attach");
155 let _ = stack.spawn(&engine, &ctx, task_id, 1, token).await;
156
157 let observed = seen.lock().unwrap().clone().expect("inner ctx captured");
158 let v = observed
159 .meta
160 .runtime
161 .get(WORKER_BINDING_KEY)
162 .expect("worker_binding key present");
163 let wb: WorkerBinding = serde_json::from_value(v.clone()).expect("round-trip");
164 assert_eq!(wb.variant, "mse-worker-knowledge");
165 assert_eq!(wb.tools, vec!["Read".to_string()]);
166 }
167
168 #[tokio::test]
169 async fn passes_through_untouched_on_miss() {
170 let (stack, seen) = probe_stack(HashMap::new());
171 let engine = Engine::new(EngineCfg::default());
172 let task_id = TaskId("t-2".to_string());
173 let ctx = Ctx::new(task_id.clone(), 1, "unbound-agent");
174 let token = engine
175 .attach("ut-op", Role::Operator, Duration::from_secs(30))
176 .await
177 .expect("attach");
178 let _ = stack.spawn(&engine, &ctx, task_id, 1, token).await;
179
180 let observed = seen.lock().unwrap().clone().expect("inner ctx captured");
181 assert!(
182 !observed.meta.runtime.contains_key(WORKER_BINDING_KEY),
183 "no binding entry must be injected on miss"
184 );
185 }
186}