1use axum::{
45 extract::{Query, State},
46 http::{header::AUTHORIZATION, HeaderMap, StatusCode},
47 Json,
48};
49use mlua_swarm::core::agent_context::StepPointer;
50use mlua_swarm::core::step_naming::StepNaming;
51use mlua_swarm::{CapToken, ContentRef, OutputEvent, RunId, StepId, WorkerPayload};
52use mlua_swarm_schema::ContextPolicy;
53use serde::Deserialize;
54use serde_json::Value;
55
56use crate::projection::McpQueryAdapter;
57use crate::{ApiError, AppState};
58
59#[derive(Debug, Deserialize)]
61pub struct PromptQuery {
62 pub task_id: StepId,
66}
67
68pub async fn worker_prompt(
74 State(state): State<AppState>,
75 headers: HeaderMap,
76 Query(q): Query<PromptQuery>,
77) -> Result<Json<WorkerPayload>, ApiError> {
78 let task_id = q.task_id;
79 let bearer = extract_bearer_raw(&headers)?;
80 let mut payload = if let Some(handle) = parse_worker_handle(&bearer) {
81 let resolved = state
83 .engine
84 .task_id_from_handle(handle)
85 .await
86 .map_err(|e| ApiError::engine(format!("task_id_from_handle: {e}")))?;
87 if resolved != task_id {
88 return Err(ApiError::bad_request(format!(
89 "handle {handle} is bound to task {resolved}, not {task_id}"
90 )));
91 }
92 state
93 .engine
94 .fetch_worker_payload_trusted(&task_id)
95 .await
96 .map_err(|e| ApiError::engine(format!("fetch_worker_payload_trusted: {e}")))?
97 } else {
98 let token = CapToken::decode(bearer.trim())
100 .map_err(|e| ApiError::bad_request(format!("invalid token: {e}")))?;
101 state
102 .engine
103 .fetch_worker_payload(&token, &task_id)
104 .await
105 .map_err(|e| ApiError::engine(format!("fetch_worker_payload: {e}")))?
106 };
107 assemble_step_pointers(&state, &mut payload).await;
108 Ok(Json(payload))
109}
110
111async fn assemble_step_pointers(state: &AppState, payload: &mut WorkerPayload) {
139 let Some(context) = payload.context.as_mut() else {
140 return;
141 };
142 let Some(run_id_str) = context.run_id.clone() else {
143 return;
144 };
145 let Ok(run_id) = RunId::parse(run_id_str) else {
146 return;
147 };
148
149 let adapter = McpQueryAdapter::new(
150 state.data_store.clone(),
151 state.run_store.clone(),
152 state.engine.clone(),
153 );
154 let Ok((run, resolved_steps)) = adapter.list_steps_by_run_id(&run_id).await else {
155 return;
156 };
157
158 let naming = state.engine.step_naming_for(&payload.task_id).await;
159 let policy = state
160 .engine
161 .context_policy_for(&payload.task_id, payload.attempt)
162 .await;
163 let self_canonical = naming
164 .as_deref()
165 .and_then(|n| n.canonical_of_producer(&payload.agent))
166 .map(str::to_string)
167 .unwrap_or_else(|| payload.agent.clone());
168
169 let mut pointers = Vec::new();
170 for step in &resolved_steps {
171 if step.name == self_canonical
172 || !allows_step_canonical(&policy, naming.as_deref(), &step.name)
173 {
174 continue;
175 }
176 if let Some((size_bytes, file_path, content_url, sha256)) =
177 crate::projection::resolve_step_pointer_fields(state, &run, step).await
178 {
179 pointers.push(StepPointer {
180 name: step.name.clone(),
181 size_bytes,
182 file_path,
183 content_url,
184 sha256,
185 });
186 }
187 }
188 context.steps = pointers;
189}
190
191fn allows_step_canonical(
206 policy: &ContextPolicy,
207 naming: Option<&StepNaming>,
208 canonical_name: &str,
209) -> bool {
210 let resolves_to = |raw: &str| -> bool {
211 match naming {
212 Some(n) => n
213 .resolve(raw)
214 .map(|c| c == canonical_name)
215 .unwrap_or(raw == canonical_name),
216 None => raw == canonical_name,
217 }
218 };
219 if policy
220 .steps_exclude
221 .iter()
222 .any(|excluded| resolves_to(excluded))
223 {
224 return false;
225 }
226 match &policy.steps {
227 None => true,
228 Some(list) => list.iter().any(|included| resolves_to(included)),
229 }
230}
231
232#[derive(Debug, Deserialize)]
234pub struct WorkerResultReq {
235 pub task_id: StepId,
238 pub value: Value,
240 #[serde(default = "default_ok_true")]
244 pub ok: bool,
245 #[serde(default)]
248 pub attempt: Option<u32>,
249}
250
251fn default_ok_true() -> bool {
252 true
253}
254
255pub async fn worker_result(
258 State(state): State<AppState>,
259 headers: HeaderMap,
260 Json(req): Json<WorkerResultReq>,
261) -> Result<StatusCode, ApiError> {
262 let token = decode_worker_bearer(&headers)?;
263 let task_id = req.task_id.clone();
264
265 let attempt = match req.attempt {
267 Some(n) => n,
268 None => state
269 .engine
270 .task_attempt(&task_id)
271 .await
272 .map_err(|e| ApiError::engine(format!("task_attempt: {e}")))?,
273 };
274
275 let event = OutputEvent::Final {
276 content: ContentRef::Inline {
277 value: req.value.clone(),
278 },
279 ok: req.ok,
280 };
281 state
282 .engine
283 .submit_output(&token, &task_id, attempt, event)
284 .await
285 .map_err(|e| ApiError::engine(format!("submit_output: {e}")))?;
286 state
287 .engine
288 .post_result(&token, &task_id, req.value)
289 .await
290 .map_err(|e| ApiError::engine(format!("post_result: {e}")))?;
291 Ok(StatusCode::NO_CONTENT)
292}
293
294#[derive(Debug, Deserialize, Default)]
308pub struct SubmitQuery {
309 #[serde(default)]
313 pub ok: Option<bool>,
314}
315
316pub async fn worker_submit(
322 State(state): State<AppState>,
323 headers: HeaderMap,
324 Query(q): Query<SubmitQuery>,
325 body: axum::body::Bytes,
326) -> Result<StatusCode, ApiError> {
327 let bearer = extract_bearer_raw(&headers)?;
330 let task_id = if let Some(handle) = parse_worker_handle(&bearer) {
331 state
332 .engine
333 .task_id_from_handle(handle)
334 .await
335 .map_err(|e| ApiError::engine(format!("task_id_from_handle: {e}")))?
336 } else {
337 let token = CapToken::decode(bearer.trim())
338 .map_err(|e| ApiError::bad_request(format!("invalid token: {e}")))?;
339 state
340 .engine
341 .task_id_from_token(&token)
342 .await
343 .map_err(|e| ApiError::engine(format!("task_id_from_token: {e}")))?
344 };
345 let attempt = state
346 .engine
347 .task_attempt(&task_id)
348 .await
349 .map_err(|e| ApiError::engine(format!("task_attempt: {e}")))?;
350 let body_str = String::from_utf8_lossy(&body).trim_end().to_string();
355 let value = Value::String(body_str);
356
357 let ok = q.ok.unwrap_or(true);
363 state
364 .engine
365 .submit_worker_result_trusted(&task_id, attempt, value, ok)
366 .await
367 .map_err(|e| ApiError::engine(format!("submit_worker_result_trusted: {e}")))?;
368 Ok(StatusCode::NO_CONTENT)
369}
370
371fn extract_bearer_raw(headers: &HeaderMap) -> Result<String, ApiError> {
375 let v = headers
376 .get(AUTHORIZATION)
377 .ok_or_else(|| ApiError::bad_request("missing Authorization header".into()))?
378 .to_str()
379 .map_err(|_| ApiError::bad_request("invalid Authorization header encoding".into()))?;
380 let s = v
381 .strip_prefix("Bearer ")
382 .ok_or_else(|| ApiError::bad_request("Authorization must be 'Bearer <token>'".into()))?
383 .trim();
384 if s.is_empty() {
385 return Err(ApiError::bad_request("Bearer is empty".into()));
386 }
387 Ok(s.to_string())
388}
389
390fn parse_worker_handle(s: &str) -> Option<&str> {
394 let s = s.trim();
395 if s.starts_with("wh-")
396 && s.len() >= 5
397 && s.len() <= 64
398 && s[3..].chars().all(|c| c.is_ascii_alphanumeric())
399 {
400 Some(s)
401 } else {
402 None
403 }
404}
405
406fn decode_worker_bearer(headers: &HeaderMap) -> Result<CapToken, ApiError> {
410 let v = headers
411 .get(AUTHORIZATION)
412 .ok_or_else(|| ApiError::bad_request("missing Authorization header".into()))?
413 .to_str()
414 .map_err(|_| ApiError::bad_request("invalid Authorization header encoding".into()))?;
415 let encoded = v
416 .strip_prefix("Bearer ")
417 .ok_or_else(|| ApiError::bad_request("Authorization must be 'Bearer <token>'".into()))?
418 .trim();
419 if encoded.is_empty() {
420 return Err(ApiError::bad_request("Bearer token is empty".into()));
421 }
422 CapToken::decode(encoded).map_err(|e| ApiError::bad_request(format!("invalid token: {e}")))
423}
424
425#[cfg(test)]
430mod tests {
431 use super::*;
432 use mlua_swarm::core::agent_context::AgentContextView;
433 use mlua_swarm::core::config::EngineCfg;
434 use mlua_swarm::core::engine::Engine;
435 use mlua_swarm::store::output::{InMemoryOutputStore, OutputStore};
436 use mlua_swarm::store::run::{InMemoryRunStore, RunRecord, RunStatus, RunStore, StepEntry};
437 use mlua_swarm::store::task::InMemoryTaskStore;
438 use mlua_swarm::{RunId, StepId, TaskId};
439 use serde_json::json;
440 use std::collections::HashMap;
441 use std::sync::Arc;
442 use tokio::sync::Mutex;
443
444 fn test_state(data_store: Arc<dyn OutputStore>, run_store: Arc<dyn RunStore>) -> AppState {
450 let engine = Engine::new(EngineCfg::default());
451 let compiler = mlua_swarm::Compiler::new(crate::default_registry());
452 let launch = Arc::new(mlua_swarm::TaskLaunchService::new(engine.clone(), compiler));
453 AppState {
454 engine,
455 sessions: Arc::new(Mutex::new(crate::SessionStore::default())),
456 task_app: Arc::new(mlua_swarm::TaskApplication::new_inline_only(launch)),
457 ws_operator_factory: None,
458 data_store,
459 operator_sessions: Arc::new(Mutex::new(HashMap::new())),
460 roles_to_sid: Arc::new(Mutex::new(HashMap::new())),
461 task_store: Arc::new(InMemoryTaskStore::new()),
462 run_store,
463 base_url: None,
464 }
465 }
466
467 async fn append_final(
468 data_store: &Arc<dyn OutputStore>,
469 task_id: &str,
470 producer: &str,
471 value: Value,
472 ) {
473 data_store
474 .append(
475 task_id,
476 1,
477 producer,
478 OutputEvent::Final {
479 content: ContentRef::Inline { value },
480 ok: true,
481 },
482 vec![],
483 )
484 .await
485 .expect("append final");
486 }
487
488 fn step_entry(step_id: &StepId, step_ref: &str) -> StepEntry {
489 StepEntry {
490 step_id: step_id.clone(),
491 step_ref: Some(step_ref.to_string()),
492 status: Some("passed".to_string()),
493 at: 0,
494 }
495 }
496
497 fn run_record(task_id: &TaskId, run_id: &RunId, step_entries: Vec<StepEntry>) -> RunRecord {
498 RunRecord {
499 id: run_id.clone(),
500 task_id: task_id.clone(),
501 status: RunStatus::Running,
502 step_entries,
503 operator_sid: None,
504 result_ref: None,
505 created_at: 0,
506 updated_at: 0,
507 }
508 }
509
510 fn consumer_payload(consumer_step_id: &StepId, run_id: &RunId) -> WorkerPayload {
511 WorkerPayload {
512 task_id: consumer_step_id.clone(),
513 attempt: 1,
514 agent: "consumer".to_string(),
515 system: None,
516 prompt: String::new(),
517 context: Some(AgentContextView {
518 task_id: consumer_step_id.to_string(),
519 agent: "consumer".to_string(),
520 attempt: 1,
521 run_id: Some(run_id.to_string()),
522 ..Default::default()
523 }),
524 }
525 }
526
527 #[tokio::test]
532 async fn context_policy_unspecified_yields_every_submitted_step() {
533 let data_store: Arc<dyn OutputStore> = Arc::new(InMemoryOutputStore::new());
534 let run_store: Arc<dyn RunStore> = Arc::new(InMemoryRunStore::new());
535 let task_id = TaskId::new();
536 let run_id = RunId::new();
537 let planner_id = StepId::new();
538 let coder_id = StepId::new();
539
540 append_final(
541 &data_store,
542 planner_id.as_str(),
543 "planner",
544 json!({"plan": "x"}),
545 )
546 .await;
547 append_final(
548 &data_store,
549 coder_id.as_str(),
550 "coder",
551 json!({"code": "y"}),
552 )
553 .await;
554 run_store
555 .create(run_record(
556 &task_id,
557 &run_id,
558 vec![
559 step_entry(&planner_id, "planner"),
560 step_entry(&coder_id, "coder"),
561 ],
562 ))
563 .await
564 .expect("create run");
565
566 let state = test_state(data_store, run_store);
567 let consumer_id = StepId::new();
568 let mut payload = consumer_payload(&consumer_id, &run_id);
569 assemble_step_pointers(&state, &mut payload).await;
570
571 let names: Vec<&str> = payload
572 .context
573 .as_ref()
574 .expect("context")
575 .steps
576 .iter()
577 .map(|p| p.name.as_str())
578 .collect();
579 assert!(names.contains(&"planner"), "names: {names:?}");
580 assert!(names.contains(&"coder"), "names: {names:?}");
581 }
582
583 #[tokio::test]
585 async fn context_policy_steps_include_list_filters_to_named_steps() {
586 let data_store: Arc<dyn OutputStore> = Arc::new(InMemoryOutputStore::new());
587 let run_store: Arc<dyn RunStore> = Arc::new(InMemoryRunStore::new());
588 let task_id = TaskId::new();
589 let run_id = RunId::new();
590 let planner_id = StepId::new();
591 let coder_id = StepId::new();
592 append_final(&data_store, planner_id.as_str(), "planner", json!("x")).await;
593 append_final(&data_store, coder_id.as_str(), "coder", json!("y")).await;
594 run_store
595 .create(run_record(
596 &task_id,
597 &run_id,
598 vec![
599 step_entry(&planner_id, "planner"),
600 step_entry(&coder_id, "coder"),
601 ],
602 ))
603 .await
604 .expect("create run");
605
606 let state = test_state(data_store, run_store);
607 let consumer_id = StepId::new();
608 state
609 .engine
610 .with_state("test.seed_policy", {
611 let consumer_id = consumer_id.clone();
612 move |s| {
613 s.agent_ctx.insert(
614 (consumer_id, 1),
615 mlua_swarm::core::state::AgentCtxEntry {
616 policy: mlua_swarm_schema::ContextPolicy {
617 steps: Some(vec!["planner".to_string()]),
618 ..Default::default()
619 },
620 ..Default::default()
621 },
622 );
623 }
624 })
625 .await
626 .expect("seed policy");
627
628 let mut payload = consumer_payload(&consumer_id, &run_id);
629 assemble_step_pointers(&state, &mut payload).await;
630
631 let names: Vec<&str> = payload
632 .context
633 .as_ref()
634 .expect("context")
635 .steps
636 .iter()
637 .map(|p| p.name.as_str())
638 .collect();
639 assert_eq!(names, vec!["planner"], "names: {names:?}");
640 }
641
642 #[tokio::test]
644 async fn context_policy_steps_empty_list_yields_no_pointers() {
645 let data_store: Arc<dyn OutputStore> = Arc::new(InMemoryOutputStore::new());
646 let run_store: Arc<dyn RunStore> = Arc::new(InMemoryRunStore::new());
647 let task_id = TaskId::new();
648 let run_id = RunId::new();
649 let planner_id = StepId::new();
650 append_final(&data_store, planner_id.as_str(), "planner", json!("x")).await;
651 run_store
652 .create(run_record(
653 &task_id,
654 &run_id,
655 vec![step_entry(&planner_id, "planner")],
656 ))
657 .await
658 .expect("create run");
659
660 let state = test_state(data_store, run_store);
661 let consumer_id = StepId::new();
662 state
663 .engine
664 .with_state("test.seed_policy", {
665 let consumer_id = consumer_id.clone();
666 move |s| {
667 s.agent_ctx.insert(
668 (consumer_id, 1),
669 mlua_swarm::core::state::AgentCtxEntry {
670 policy: mlua_swarm_schema::ContextPolicy {
671 steps: Some(vec![]),
672 ..Default::default()
673 },
674 ..Default::default()
675 },
676 );
677 }
678 })
679 .await
680 .expect("seed policy");
681
682 let mut payload = consumer_payload(&consumer_id, &run_id);
683 assemble_step_pointers(&state, &mut payload).await;
684
685 assert!(payload.context.expect("context").steps.is_empty());
686 }
687
688 #[tokio::test]
690 async fn context_policy_steps_exclude_wins_over_steps() {
691 let data_store: Arc<dyn OutputStore> = Arc::new(InMemoryOutputStore::new());
692 let run_store: Arc<dyn RunStore> = Arc::new(InMemoryRunStore::new());
693 let task_id = TaskId::new();
694 let run_id = RunId::new();
695 let planner_id = StepId::new();
696 let coder_id = StepId::new();
697 append_final(&data_store, planner_id.as_str(), "planner", json!("x")).await;
698 append_final(&data_store, coder_id.as_str(), "coder", json!("y")).await;
699 run_store
700 .create(run_record(
701 &task_id,
702 &run_id,
703 vec![
704 step_entry(&planner_id, "planner"),
705 step_entry(&coder_id, "coder"),
706 ],
707 ))
708 .await
709 .expect("create run");
710
711 let state = test_state(data_store, run_store);
712 let consumer_id = StepId::new();
713 state
714 .engine
715 .with_state("test.seed_policy", {
716 let consumer_id = consumer_id.clone();
717 move |s| {
718 s.agent_ctx.insert(
719 (consumer_id, 1),
720 mlua_swarm::core::state::AgentCtxEntry {
721 policy: mlua_swarm_schema::ContextPolicy {
722 steps: Some(vec!["planner".to_string(), "coder".to_string()]),
723 steps_exclude: vec!["planner".to_string()],
724 ..Default::default()
725 },
726 ..Default::default()
727 },
728 );
729 }
730 })
731 .await
732 .expect("seed policy");
733
734 let mut payload = consumer_payload(&consumer_id, &run_id);
735 assemble_step_pointers(&state, &mut payload).await;
736
737 let names: Vec<&str> = payload
738 .context
739 .as_ref()
740 .expect("context")
741 .steps
742 .iter()
743 .map(|p| p.name.as_str())
744 .collect();
745 assert_eq!(names, vec!["coder"], "names: {names:?}");
746 }
747
748 #[tokio::test]
757 async fn in_flight_step_output_is_visible_before_run_finalizes() {
758 let data_store: Arc<dyn OutputStore> = Arc::new(InMemoryOutputStore::new());
759 let run_store: Arc<dyn RunStore> = Arc::new(InMemoryRunStore::new());
760 let task_id = TaskId::new();
761 let run_id = RunId::new();
762 let step1_id = StepId::new();
763 append_final(
764 &data_store,
765 step1_id.as_str(),
766 "step1",
767 json!({"step1_out": "hi"}),
768 )
769 .await;
770 let mut run = run_record(&task_id, &run_id, vec![step_entry(&step1_id, "step1")]);
771 run.status = RunStatus::Running;
772 run.result_ref = None; run_store.create(run).await.expect("create run");
774
775 let state = test_state(data_store, run_store);
776 let consumer_id = StepId::new();
777 let mut payload = consumer_payload(&consumer_id, &run_id);
778 assemble_step_pointers(&state, &mut payload).await;
779
780 let steps = &payload.context.expect("context").steps;
781 assert_eq!(steps.len(), 1);
782 assert_eq!(steps[0].name, "step1");
783 }
784
785 #[tokio::test]
789 async fn self_agent_name_is_always_excluded() {
790 let data_store: Arc<dyn OutputStore> = Arc::new(InMemoryOutputStore::new());
791 let run_store: Arc<dyn RunStore> = Arc::new(InMemoryRunStore::new());
792 let task_id = TaskId::new();
793 let run_id = RunId::new();
794 let planner_id = StepId::new();
795 let consumer_prior_id = StepId::new();
796 append_final(&data_store, planner_id.as_str(), "planner", json!("x")).await;
797 append_final(
798 &data_store,
799 consumer_prior_id.as_str(),
800 "consumer",
801 json!("self"),
802 )
803 .await;
804 run_store
805 .create(run_record(
806 &task_id,
807 &run_id,
808 vec![
809 step_entry(&planner_id, "planner"),
810 step_entry(&consumer_prior_id, "consumer"),
811 ],
812 ))
813 .await
814 .expect("create run");
815
816 let state = test_state(data_store, run_store);
817 let consumer_id = StepId::new();
818 let mut payload = consumer_payload(&consumer_id, &run_id);
819 assemble_step_pointers(&state, &mut payload).await;
820
821 let names: Vec<&str> = payload
822 .context
823 .as_ref()
824 .expect("context")
825 .steps
826 .iter()
827 .map(|p| p.name.as_str())
828 .collect();
829 assert!(!names.contains(&"consumer"), "names: {names:?}");
830 assert!(names.contains(&"planner"), "names: {names:?}");
831 }
832
833 #[tokio::test]
837 async fn step_pointer_serializes_with_no_preview_or_content_bytes() {
838 let data_store: Arc<dyn OutputStore> = Arc::new(InMemoryOutputStore::new());
839 let run_store: Arc<dyn RunStore> = Arc::new(InMemoryRunStore::new());
840 let task_id = TaskId::new();
841 let run_id = RunId::new();
842 let planner_id = StepId::new();
843 append_final(
844 &data_store,
845 planner_id.as_str(),
846 "planner",
847 json!({"plan": "do the thing, at length".repeat(50)}),
848 )
849 .await;
850 run_store
851 .create(run_record(
852 &task_id,
853 &run_id,
854 vec![step_entry(&planner_id, "planner")],
855 ))
856 .await
857 .expect("create run");
858
859 let state = test_state(data_store, run_store);
860 let consumer_id = StepId::new();
861 let mut payload = consumer_payload(&consumer_id, &run_id);
862 assemble_step_pointers(&state, &mut payload).await;
863
864 let steps = &payload.context.expect("context").steps;
865 assert_eq!(steps.len(), 1);
866 let json_value = serde_json::to_value(&steps[0]).expect("serialize StepPointer");
867 let obj = json_value.as_object().expect("object");
868 for forbidden in ["preview", "content", "value", "bytes"] {
869 assert!(
870 !obj.contains_key(forbidden),
871 "StepPointer must not carry a {forbidden:?} field: {obj:?}"
872 );
873 }
874 assert!(obj.contains_key("name"));
875 assert!(obj.contains_key("size_bytes"));
876 assert!(obj.contains_key("content_url"));
877 assert!(obj.contains_key("sha256"));
878 }
879
880 fn declared_name_bp() -> mlua_swarm::blueprint::Blueprint {
888 use mlua_flow_ir::{Expr, Node};
889 use mlua_swarm::blueprint::{
890 current_schema_version, AgentDef, AgentKind, AgentMeta, Blueprint, BlueprintMetadata,
891 CompilerHints, CompilerStrategy,
892 };
893 Blueprint {
894 schema_version: current_schema_version(),
895 id: "worker-test-declared-name-bp".into(),
896 flow: Node::Step {
897 ref_: "planner".to_string(),
898 in_: Expr::Path {
899 at: "$.in".to_string(),
900 },
901 out: Expr::Path {
902 at: "$.plan".to_string(),
903 },
904 },
905 agents: vec![AgentDef {
906 name: "planner".to_string(),
907 kind: AgentKind::RustFn,
908 spec: json!({"fn_id": "planner"}),
909 profile: None,
910 meta: Some(AgentMeta {
911 projection_name: Some("plan-out".to_string()),
912 ..Default::default()
913 }),
914 }],
915 operators: vec![],
916 metas: vec![],
917 hints: CompilerHints::default(),
918 strategy: CompilerStrategy::default(),
919 metadata: BlueprintMetadata::default(),
920 spawner_hints: Default::default(),
921 default_agent_kind: AgentKind::Operator,
922 default_operator_kind: None,
923 default_init_ctx: None,
924 default_agent_ctx: None,
925 default_context_policy: None,
926 projection_placement: None,
927 }
928 }
929
930 #[tokio::test]
936 async fn declared_projection_name_pointer_name_is_canonical_and_policy_matches_it() {
937 let data_store: Arc<dyn OutputStore> = Arc::new(InMemoryOutputStore::new());
938 let run_store: Arc<dyn RunStore> = Arc::new(InMemoryRunStore::new());
939 let task_id = TaskId::new();
940 let run_id = RunId::new();
941 let planner_id = StepId::new();
942
943 append_final(
946 &data_store,
947 planner_id.as_str(),
948 "plan-out",
949 json!({"plan": "x"}),
950 )
951 .await;
952 run_store
953 .create(run_record(
954 &task_id,
955 &run_id,
956 vec![step_entry(&planner_id, "planner")],
957 ))
958 .await
959 .expect("create run");
960
961 let state = test_state(data_store, run_store);
962
963 let (naming, _warnings) =
969 mlua_swarm::core::step_naming::StepNaming::from_blueprint(&declared_name_bp())
970 .expect("no collision");
971 let naming = Arc::new(naming);
972 let consumer_id = StepId::new();
973 state
974 .engine
975 .with_state("test.seed_step_naming", {
976 let naming = naming.clone();
977 let planner_id = planner_id.clone();
978 let consumer_id = consumer_id.clone();
979 move |s| {
980 s.step_namings.insert(planner_id, naming.clone());
981 s.step_namings.insert(consumer_id, naming);
982 }
983 })
984 .await
985 .expect("seed step naming");
986 state
987 .engine
988 .with_state("test.seed_policy", {
989 let consumer_id = consumer_id.clone();
990 move |s| {
991 s.agent_ctx.insert(
992 (consumer_id, 1),
993 mlua_swarm::core::state::AgentCtxEntry {
994 policy: mlua_swarm_schema::ContextPolicy {
995 steps: Some(vec!["plan-out".to_string()]),
996 ..Default::default()
997 },
998 ..Default::default()
999 },
1000 );
1001 }
1002 })
1003 .await
1004 .expect("seed policy");
1005
1006 let mut payload = consumer_payload(&consumer_id, &run_id);
1007 assemble_step_pointers(&state, &mut payload).await;
1008
1009 let steps = &payload.context.expect("context").steps;
1010 assert_eq!(steps.len(), 1, "steps: {steps:?}");
1011 assert_eq!(
1012 steps[0].name, "plan-out",
1013 "StepPointer.name must be the canonical name"
1014 );
1015 }
1016}