1use super::reactor::Runtime;
16use crate::engine::model::{Body, OnError, Step, Workflow};
17use crate::engine::run::{RunState, StepStatus};
18use crate::engine::template::Data;
19use crate::state::now_ms;
20use serde_json::{Map, Value, json};
21use std::collections::BTreeMap;
22
23#[derive(Debug, Clone, Default)]
25pub struct Scope {
26 pub parent: String,
28 pub parent_step: Option<Step>,
30 pub siblings: BTreeMap<String, Step>,
31 pub item: Option<Value>,
32 pub index: Option<usize>,
33 pub batch: Option<usize>,
34 pub iteration: Option<usize>,
35 pub branch: Option<String>,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct Segment {
41 pub name: String,
42 pub index: Option<usize>,
43 pub branch: Option<String>,
44}
45
46pub fn parse_scoped(id: &str) -> Vec<Segment> {
48 id.split('.')
49 .map(|seg| {
50 if let Some((name, rest)) = seg.split_once('[') {
51 Segment {
52 name: name.to_string(),
53 index: rest.trim_end_matches(']').parse().ok(),
54 branch: None,
55 }
56 } else if let Some((name, rest)) = seg.split_once('{') {
57 Segment {
58 name: name.to_string(),
59 branch: Some(rest.trim_end_matches('}').to_string()),
60 index: None,
61 }
62 } else {
63 Segment {
64 name: seg.to_string(),
65 index: None,
66 branch: None,
67 }
68 }
69 })
70 .collect()
71}
72
73pub fn scoped_id(parent: &str, step: &str) -> String {
75 format!("{parent}.{step}")
76}
77
78pub fn is_scoped(id: &str) -> bool {
80 id.contains('.')
81}
82
83pub fn parent_of(id: &str) -> Option<&str> {
85 id.rsplit_once('.').map(|(p, _)| p)
86}
87
88impl Runtime {
89 pub(crate) fn resolve_step(
92 &self,
93 wf: &Workflow,
94 run: &RunState,
95 id: &str,
96 ) -> Option<(Step, Option<Scope>)> {
97 let segs = parse_scoped(id);
98 if segs.len() == 1 && segs[0].index.is_none() && segs[0].branch.is_none() {
99 return wf.step(id).cloned().map(|s| (s, None));
100 }
101 let mut current: Step = wf.step(&segs[0].name)?.clone();
104 let mut scope = Scope::default();
105 let mut parent_path = String::new();
106 for (i, seg) in segs.iter().enumerate() {
107 let is_last = i + 1 == segs.len();
108 let this_path = if parent_path.is_empty() {
109 seg_label(seg)
110 } else {
111 format!("{parent_path}.{}", seg_label(seg))
112 };
113 if is_last {
114 break;
115 }
116 let body: &Body = match &seg.branch {
118 Some(b) => current.branches.get(b)?,
119 None => current.body.as_ref()?,
120 };
121 let next = &segs[i + 1];
122 let child = body.steps.get(&next.name)?.clone();
123 let progress = run
125 .steps
126 .get(&strip_scope_suffix(&this_path))
127 .and_then(|s| s.wait.clone())
128 .unwrap_or(Value::Null);
129 let item = seg.index.and_then(|ix| {
130 element_item(&progress, ix).or_else(|| self.items_of(&progress).get(ix).cloned())
131 });
132 scope = Scope {
133 parent: this_path.clone(),
134 parent_step: Some(current.clone()),
135 siblings: body.steps.clone(),
136 item,
137 index: seg.index,
138 batch: seg.index.and_then(|ix| batch_of(&progress, ix)),
139 iteration: if current.kind == "iterate" {
140 seg.index
141 } else {
142 None
143 },
144 branch: seg.branch.clone(),
145 };
146 current = child;
147 parent_path = this_path;
148 }
149 Some((current, Some(scope)))
150 }
151
152 pub(crate) fn scoped_data(&mut self, run_id: &str, scope: &Scope) -> Data {
155 let mut data = self.run_data(run_id);
156 if let Some(it) = &scope.item {
157 data.insert("item".into(), it.clone());
158 if let Some(alias) = scope.parent_step.as_ref().and_then(|p| p.field_str("as"))
159 && alias != "item"
160 {
161 data.insert(alias.to_string(), it.clone());
162 }
163 }
164 if let Some(ix) = scope.index {
165 data.insert("index".into(), json!(ix));
166 }
167 if let Some(b) = scope.batch {
168 data.insert("batch".into(), json!(b));
169 }
170 if let Some(k) = scope.iteration {
171 data.insert("iteration".into(), json!(k));
172 }
173 if let Some(b) = &scope.branch {
174 data.insert("branch".into(), json!(b));
175 }
176 if let Some(run) = self.runs.get(run_id) {
177 let mut steps = data
178 .get("steps")
179 .and_then(Value::as_object)
180 .cloned()
181 .unwrap_or_default();
182 for sib in scope.siblings.keys() {
183 let sid = scoped_id(&scope.parent, sib);
184 if let Some(st) = run.steps.get(&sid) {
185 steps.insert(sib.clone(), json!({"status": st.status, "output": st.output, "error": st.error, "attempt": st.attempt}));
186 }
187 }
188 data.insert("steps".into(), Value::Object(steps));
189 }
190 data
191 }
192
193 pub(crate) fn nested_start(
197 &mut self,
198 run_id: &str,
199 step_id: &str,
200 step: &Step,
201 spec: &Map<String, Value>,
202 ) {
203 let progress = match step.kind.as_str() {
204 "foreach" | "batch" => {
205 let over = spec.get("over").cloned().unwrap_or(Value::Null);
206 let items = match over {
207 Value::Array(a) => a,
208 Value::Null => Vec::new(),
209 Value::Object(o) => o
210 .into_iter()
211 .map(|(k, v)| json!({"key": k, "value": v}))
212 .collect(),
213 other => {
214 self.finish_step_pub(
215 run_id,
216 step_id,
217 StepStatus::Failed,
218 None,
219 Some(format!(
220 "{}: over must be an array (got {})",
221 step.kind, other
222 )),
223 0,
224 );
225 return;
226 }
227 };
228 let batch = spec.get("batch").cloned().unwrap_or(json!({}));
229 let size = spec
230 .get("size")
231 .and_then(Value::as_u64)
232 .or_else(|| batch.get("size").and_then(Value::as_u64))
233 .unwrap_or(if step.kind == "batch" { 10 } else { 1 })
234 .max(1) as usize;
235 let parallel = spec
236 .get("parallel")
237 .and_then(Value::as_u64)
238 .or_else(|| batch.get("parallel").and_then(Value::as_u64))
239 .unwrap_or(1)
240 .clamp(1, crate::engine::model::MAX_BATCH_PARALLEL)
241 as usize;
242 let rate = spec
243 .get("rate")
244 .and_then(Value::as_str)
245 .or_else(|| batch.get("rate").and_then(Value::as_str))
246 .map(str::to_string);
247 let group_by = spec.get("by").and_then(Value::as_str).map(str::to_string);
248 let items = match (&group_by, step.kind.as_str()) {
250 (Some(key), "batch") => {
251 let mut groups: Vec<(Value, Vec<Value>)> = Vec::new();
252 for it in items {
253 let k = crate::engine::data::path_of(&it, key).unwrap_or(Value::Null);
254 match groups.iter_mut().find(|(gk, _)| *gk == k) {
255 Some((_, g)) => g.push(it),
256 None => groups.push((k, vec![it])),
257 }
258 }
259 groups.into_iter().map(|(_, g)| Value::Array(g)).collect()
260 }
261 _ => items,
262 };
263 let total = items.len();
264 let items_ref = self.store_items(run_id, step_id, items);
265 json!({
266 "kind": step.kind, "total": total, "size": size, "parallel": parallel, "rate": rate,
267 "cursor": 0, "active": [], "results": {}, "done": 0, "batches_done": 0, "next_batch_at": 0,
268 "items": items_ref, "started_ms": now_ms(),
269 "collect": spec.get("collect").cloned().unwrap_or(Value::Null),
270 "as": spec.get("as").and_then(Value::as_str).unwrap_or("item"),
271 })
272 }
273 "iterate" => {
274 let max = spec
275 .get("max_iterations")
276 .and_then(Value::as_u64)
277 .unwrap_or(crate::engine::model::MAX_ITERATIONS)
278 .min(crate::engine::model::MAX_ITERATIONS);
279 json!({"kind": "iterate", "iteration": 0, "max": max, "results": [], "collect": spec.get("collect").cloned().unwrap_or(Value::Null), "started_ms": now_ms()})
280 }
281 "parallel" | "race" => {
282 let branches: Vec<String> = step.branches.keys().cloned().collect();
283 json!({"kind": step.kind, "branches": branches, "results": {}, "errors": {}, "started_ms": now_ms(), "min_success": spec.get("min_success").and_then(Value::as_u64), "timeout_ms": step.timeout_ms})
288 }
289 "subgraph" => json!({"kind": "subgraph", "started_ms": now_ms()}),
290 other => {
291 self.finish_step_pub(
292 run_id,
293 step_id,
294 StepStatus::Failed,
295 None,
296 Some(format!("{other} is not a nested kind")),
297 0,
298 );
299 return;
300 }
301 };
302 if let Some(st) = self
303 .runs
304 .get_mut(run_id)
305 .and_then(|r| r.steps.get_mut(step_id))
306 {
307 st.status = StepStatus::Running;
308 st.wait = Some(progress);
309 }
310 if let Some(r) = self.runs.get_mut(run_id) {
311 r.touch();
312 }
313 self.checkpoint(false);
314 self.nested_advance(run_id, step_id);
315 }
316
317 fn store_items(&mut self, run_id: &str, step_id: &str, items: Vec<Value>) -> Value {
319 let v = Value::Array(items);
320 let cap = self.settings.limits.inline_max_bytes.unwrap_or(65_536) as usize;
321 if v.to_string().len() > cap {
322 match self.artifacts.create(
323 &self.durable,
324 super::artifacts::NewArtifact {
325 name: format!("{run_id}/{step_id}/items.json").as_str(),
326 mime: Some("application/json"),
327 content: v.clone(),
328 created_by: Some("engine"),
329 sensitive: false,
330 owner: Some(run_id),
331 },
332 ) {
333 Ok(meta) => return json!({"$artifact": meta["id"]}),
334 Err(e) => self.log.warn(
335 "nested.items.artifact_fail",
336 json!({"run": run_id, "step": step_id, "err": e}),
337 ),
338 }
339 }
340 v
341 }
342
343 fn items_of(&self, progress: &Value) -> Vec<Value> {
345 match &progress["items"] {
346 Value::Array(a) => a.clone(),
347 Value::Object(o) if o.get("$artifact").is_some() => o
348 .get("$artifact")
349 .and_then(Value::as_str)
350 .and_then(|id| self.artifacts.get(id))
351 .and_then(|a| a.content.as_array().cloned())
352 .unwrap_or_default(),
353 _ => Vec::new(),
354 }
355 }
356
357 pub(crate) fn nested_advance(&mut self, run_id: &str, parent_id: &str) {
363 let Some(wf) = self.definition_for_run(run_id) else {
364 return;
365 };
366 let Some(run) = self.runs.get(run_id) else {
367 return;
368 };
369 if run.status.is_terminal() {
370 return;
371 }
372 let Some((parent, _)) = self.resolve_step(&wf, run, parent_id) else {
373 return;
374 };
375 let Some(progress) = run.steps.get(parent_id).and_then(|s| s.wait.clone()) else {
376 return;
377 };
378 if run.steps.get(parent_id).map(|s| s.status) != Some(StepStatus::Running) {
379 return;
380 }
381 match progress["kind"].as_str() {
382 Some("foreach") | Some("batch") => {
383 self.advance_foreach(run_id, parent_id, &parent, progress)
384 }
385 Some("iterate") => self.advance_iterate(run_id, parent_id, &parent, progress),
386 Some("parallel") => self.advance_parallel(run_id, parent_id, &parent, progress, false),
387 Some("race") => self.advance_parallel(run_id, parent_id, &parent, progress, true),
388 Some("subgraph") => self.advance_subgraph(run_id, parent_id, &parent),
389 _ => {}
390 }
391 }
392
393 fn drive_body(
396 &mut self,
397 run_id: &str,
398 scope_id: &str,
399 body: &Body,
400 parent: &Step,
401 ) -> BodyState {
402 let ids: Vec<String> = body.topo_order();
403 if let Some(run) = self.runs.get_mut(run_id) {
405 for id in &ids {
406 run.steps.entry(scoped_id(scope_id, id)).or_default();
407 }
408 }
409 let mut all_terminal = true;
410 let mut failed: Option<String> = None;
411 let mut ready: Vec<String> = Vec::new();
412 let mut in_flight = false;
413 let mut changed = true;
415 while changed {
416 changed = false;
417 let Some(snapshot) = self.runs.get(run_id).map(|r| r.steps.clone()) else {
418 return BodyState::Waiting;
419 };
420 for id in &ids {
421 let sid = scoped_id(scope_id, id);
422 let st = snapshot.get(&sid).cloned().unwrap_or_default();
423 match st.status {
424 StepStatus::Running | StepStatus::Suspended => {
425 in_flight = true;
426 all_terminal = false;
427 continue;
428 }
429 StepStatus::Failed | StepStatus::Timeout | StepStatus::Cancelled => {
430 if failed.is_none() {
431 failed = Some(
432 st.error
433 .clone()
434 .unwrap_or_else(|| format!("{id} {}", st.status.as_label())),
435 );
436 }
437 continue;
438 }
439 StepStatus::Done | StepStatus::Skipped => continue,
440 StepStatus::Pending => {}
441 }
442 all_terminal = false;
443 if ready.contains(&sid) {
444 continue;
445 }
446 let step = &body.steps[id];
447 if st.forced {
448 ready.push(sid);
449 continue;
450 }
451 let deps_ok = step.depends_on.iter().all(|d| {
452 snapshot
453 .get(&scoped_id(scope_id, d))
454 .is_some_and(|s| s.status.is_satisfied())
455 });
456 let deps_failed = step.depends_on.iter().any(|d| {
457 snapshot.get(&scoped_id(scope_id, d)).is_some_and(|s| {
458 matches!(
459 s.status,
460 StepStatus::Failed | StepStatus::Cancelled | StepStatus::Timeout
461 )
462 })
463 });
464 if deps_failed {
465 continue;
467 }
468 if !deps_ok {
469 continue;
470 }
471 if let Some(w) = &step.when {
472 let scope = {
473 let wf = self
474 .definition_for_run(run_id)
475 .unwrap_or_else(unreachable_wf);
476 self.runs
477 .get(run_id)
478 .and_then(|r| self.resolve_step(&wf, r, &sid))
479 .and_then(|(_, s)| s)
480 .unwrap_or_default()
481 };
482 let data = self.scoped_data_view(run_id, &scope);
483 let expr = w.trim().trim_start_matches("CEL:").trim();
484 let vars: Vec<(&str, &Value)> =
485 data.iter().map(|(k, v)| (k.as_str(), v)).collect();
486 match crate::cel::eval_bool(expr, &vars) {
487 Ok(true) => {}
488 Ok(false) => {
489 if let Some(r) = self.runs.get_mut(run_id) {
490 r.end_step(&sid, StepStatus::Skipped, None, None);
491 }
492 changed = true;
493 continue;
494 }
495 Err(e) => {
496 failed = Some(format!("{id}: when: {e}"));
497 continue;
498 }
499 }
500 }
501 ready.push(sid);
502 }
503 }
504 if let Some(e) = failed {
505 if !in_flight {
508 return BodyState::Failed(e);
509 }
510 return BodyState::Waiting;
511 }
512 if all_terminal {
513 return BodyState::Done;
514 }
515 for sid in ready {
516 if !self.parent_running(run_id, &strip_scope_suffix(scope_id)) {
519 break;
520 }
521 self.execute_step_pub(run_id, &sid);
522 }
523 let _ = parent;
524 BodyState::Waiting
525 }
526
527 fn parent_running(&self, run_id: &str, parent_id: &str) -> bool {
529 self.runs.get(run_id).is_some_and(|r| {
530 !r.status.is_terminal()
531 && r.steps
532 .get(parent_id)
533 .is_some_and(|st| st.status == StepStatus::Running)
534 })
535 }
536
537 fn scoped_data_view(&mut self, run_id: &str, scope: &Scope) -> Data {
539 self.scoped_data(run_id, scope)
540 }
541
542 fn body_result(&self, run_id: &str, scope_id: &str, body: &Body) -> Value {
545 let Some(run) = self.runs.get(run_id) else {
546 return Value::Null;
547 };
548 let sinks = body.sinks();
549 if sinks.len() == 1 {
550 return run
551 .steps
552 .get(&scoped_id(scope_id, &sinks[0]))
553 .and_then(|s| s.output.clone())
554 .unwrap_or(Value::Null);
555 }
556 let mut o = Map::new();
557 for s in sinks {
558 o.insert(
559 s.clone(),
560 run.steps
561 .get(&scoped_id(scope_id, &s))
562 .and_then(|st| st.output.clone())
563 .unwrap_or(Value::Null),
564 );
565 }
566 Value::Object(o)
567 }
568
569 fn advance_foreach(
570 &mut self,
571 run_id: &str,
572 parent_id: &str,
573 parent: &Step,
574 mut progress: Value,
575 ) {
576 let Some(body) = parent.body.clone() else {
577 return;
578 };
579 let total = progress["total"].as_u64().unwrap_or(0) as usize;
580 let size = progress["size"].as_u64().unwrap_or(1).max(1) as usize;
581 let parallel = progress["parallel"].as_u64().unwrap_or(1).max(1) as usize;
582 let items = self.items_of(&progress);
583 let mut active: Vec<usize> = progress["active"]
584 .as_array()
585 .map(|a| {
586 a.iter()
587 .filter_map(Value::as_u64)
588 .map(|x| x as usize)
589 .collect()
590 })
591 .unwrap_or_default();
592 let mut cursor = progress["cursor"].as_u64().unwrap_or(0) as usize;
593 let mut done = progress["done"].as_u64().unwrap_or(0) as usize;
594 let mut batches_done = progress["batches_done"].as_u64().unwrap_or(0) as usize;
595 let mut results = progress["results"].as_object().cloned().unwrap_or_default();
596 let mut changed = false;
597 for ix in active.clone() {
599 if !self.parent_running(run_id, parent_id) {
600 return;
601 }
602 let scope_id = format!("{parent_id}[{ix}]");
603 match self.drive_body(run_id, &scope_id, &body, parent) {
604 BodyState::Waiting => {}
605 BodyState::Done => {
606 let out = self.body_result(run_id, &scope_id, &body);
607 results.insert(ix.to_string(), out);
608 active.retain(|a| *a != ix);
609 done += 1;
610 changed = true;
611 }
612 BodyState::Failed(e) => {
613 active.retain(|a| *a != ix);
614 done += 1;
615 changed = true;
616 match parent.on_error {
617 OnError::Continue | OnError::Goto(_) => {
618 results.insert(ix.to_string(), json!({"index": ix, "error": e}));
619 }
620 OnError::Fail => {
621 self.cancel_scoped_children(run_id, parent_id);
623 self.finish_step_pub(
624 run_id,
625 parent_id,
626 StepStatus::Failed,
627 Some(collect_results(&results, total)),
628 Some(format!("element {ix} failed: {e}")),
629 0,
630 );
631 return;
632 }
633 }
634 }
635 }
636 }
637 let batches_total = total.div_ceil(size).max(if total == 0 { 0 } else { 1 });
639 while batches_done < batches_total {
640 let (from, to) = (batches_done * size, ((batches_done + 1) * size).min(total));
641 if (from..to).all(|i| results.contains_key(&i.to_string())) {
642 batches_done += 1;
643 changed = true;
644 crate::state::kill_point("batch.k");
645 } else {
646 break;
647 }
648 }
649 let rate = progress["rate"].as_str().map(super::subagents::parse_rate);
651 let mut next_batch_at = progress["next_batch_at"].as_u64().unwrap_or(0);
652 while cursor < total {
653 let batches_in_flight = active
654 .iter()
655 .map(|i| i / size)
656 .collect::<std::collections::BTreeSet<_>>()
657 .len();
658 if batches_in_flight >= parallel {
659 break;
660 }
661 if rate.is_some() && now_ms() < next_batch_at {
662 break;
663 }
664 let end = (cursor + size).min(total);
665 for ix in cursor..end {
666 active.push(ix);
667 }
668 if let Some((_, per_sec)) = rate {
669 next_batch_at = now_ms() + (1000.0 / per_sec.max(0.001)) as u64;
670 }
671 cursor = end;
672 changed = true;
673 }
674 progress["active"] = json!(active);
676 progress["cursor"] = json!(cursor);
677 progress["done"] = json!(done);
678 progress["batches_done"] = json!(batches_done);
679 progress["results"] = Value::Object(results.clone());
680 progress["next_batch_at"] = json!(next_batch_at);
681 if let Some(st) = self
682 .runs
683 .get_mut(run_id)
684 .and_then(|r| r.steps.get_mut(parent_id))
685 {
686 st.wait = Some(progress.clone());
687 }
688 if changed {
689 if let Some(r) = self.runs.get_mut(run_id) {
690 r.touch();
691 }
692 self.checkpoint(false);
693 }
694 for ix in active.clone() {
696 if !self.parent_running(run_id, parent_id) {
697 return;
698 }
699 let scope_id = format!("{parent_id}[{ix}]");
700 let fresh = self.runs.get(run_id).is_some_and(|r| {
701 !body
702 .steps
703 .keys()
704 .any(|k| r.steps.contains_key(&scoped_id(&scope_id, k)))
705 });
706 if fresh {
707 let _ = items.get(ix); let _ = self.drive_body(run_id, &scope_id, &body, parent);
709 }
710 }
711 if !self.parent_running(run_id, parent_id) {
713 return;
714 }
715 if done >= total && cursor >= total {
716 let out = collect_results(&results, total);
717 self.apply_collect(run_id, &progress["collect"], &out);
718 self.finish_step_pub(run_id, parent_id, StepStatus::Done, Some(out), None, 0);
719 }
720 }
721
722 fn advance_iterate(
723 &mut self,
724 run_id: &str,
725 parent_id: &str,
726 parent: &Step,
727 mut progress: Value,
728 ) {
729 let Some(body) = parent.body.clone() else {
730 return;
731 };
732 let k = progress["iteration"].as_u64().unwrap_or(0) as usize;
733 let max = progress["max"].as_u64().unwrap_or(1) as usize;
734 let mut results: Vec<Value> = progress["results"].as_array().cloned().unwrap_or_default();
735 let scope_id = format!("{parent_id}[{k}]");
736 let started = self.runs.get(run_id).is_some_and(|r| {
737 body.steps
738 .keys()
739 .any(|s| r.steps.contains_key(&scoped_id(&scope_id, s)))
740 });
741 if !started {
742 if let Some(w) = parent.field_str("while") {
744 let scope = Scope {
745 parent: scope_id.clone(),
746 parent_step: Some(parent.clone()),
747 siblings: body.steps.clone(),
748 iteration: Some(k),
749 ..Default::default()
750 };
751 let mut data = self.scoped_data(run_id, &scope);
752 data.insert("results".into(), Value::Array(results.clone()));
753 data.insert(
754 "last".into(),
755 results.last().cloned().unwrap_or(Value::Null),
756 );
757 let vars: Vec<(&str, &Value)> = data.iter().map(|(k, v)| (k.as_str(), v)).collect();
758 match crate::cel::eval_bool(w.trim().trim_start_matches("CEL:").trim(), &vars) {
759 Ok(true) => {}
760 Ok(false) => {
761 let out = iterate_output(&progress, &results);
762 self.apply_collect(run_id, &progress["collect"], &out);
763 self.finish_step_pub(
764 run_id,
765 parent_id,
766 StepStatus::Done,
767 Some(out),
768 None,
769 0,
770 );
771 return;
772 }
773 Err(e) => {
774 self.finish_step_pub(
775 run_id,
776 parent_id,
777 StepStatus::Failed,
778 None,
779 Some(format!("iterate.while: {e}")),
780 0,
781 );
782 return;
783 }
784 }
785 }
786 if k >= max {
787 let out = iterate_output(&progress, &results);
788 self.apply_collect(run_id, &progress["collect"], &out);
789 self.finish_step_pub(run_id, parent_id, StepStatus::Done, Some(out), None, 0);
790 return;
791 }
792 }
793 match self.drive_body(run_id, &scope_id, &body, parent) {
794 BodyState::Waiting => {}
795 BodyState::Failed(e) => {
796 self.finish_step_pub(
797 run_id,
798 parent_id,
799 StepStatus::Failed,
800 Some(iterate_output(&progress, &results)),
801 Some(format!("iteration {k} failed: {e}")),
802 0,
803 );
804 }
805 BodyState::Done => {
806 let out = self.body_result(run_id, &scope_id, &body);
807 results.push(out.clone());
808 let mut stop = k + 1 >= max;
810 if let Some(u) = parent.field_str("until")
811 && !stop
812 {
813 let scope = Scope {
814 parent: scope_id.clone(),
815 parent_step: Some(parent.clone()),
816 siblings: body.steps.clone(),
817 iteration: Some(k),
818 ..Default::default()
819 };
820 let mut data = self.scoped_data(run_id, &scope);
821 data.insert("result".into(), out.clone());
822 data.insert("results".into(), Value::Array(results.clone()));
823 let vars: Vec<(&str, &Value)> =
824 data.iter().map(|(k, v)| (k.as_str(), v)).collect();
825 match crate::cel::eval_bool(u.trim().trim_start_matches("CEL:").trim(), &vars) {
826 Ok(b) => stop = b,
827 Err(e) => {
828 self.finish_step_pub(
829 run_id,
830 parent_id,
831 StepStatus::Failed,
832 None,
833 Some(format!("iterate.until: {e}")),
834 0,
835 );
836 return;
837 }
838 }
839 }
840 progress["results"] = Value::Array(results.clone());
841 progress["iteration"] = json!(k + 1);
842 if let Some(st) = self
843 .runs
844 .get_mut(run_id)
845 .and_then(|r| r.steps.get_mut(parent_id))
846 {
847 st.wait = Some(progress.clone());
848 }
849 if let Some(r) = self.runs.get_mut(run_id) {
850 r.touch();
851 }
852 self.checkpoint(false);
853 if stop {
854 let out = iterate_output(&progress, &results);
855 self.apply_collect(run_id, &progress["collect"], &out);
856 self.finish_step_pub(run_id, parent_id, StepStatus::Done, Some(out), None, 0);
857 } else {
858 self.nested_advance(run_id, parent_id);
860 }
861 }
862 }
863 }
864
865 fn advance_parallel(
866 &mut self,
867 run_id: &str,
868 parent_id: &str,
869 parent: &Step,
870 mut progress: Value,
871 race: bool,
872 ) {
873 let mut results = progress["results"].as_object().cloned().unwrap_or_default();
874 let mut errors = progress["errors"].as_object().cloned().unwrap_or_default();
875 let branches: Vec<String> = parent.branches.keys().cloned().collect();
876 let mut changed = false;
877 for b in &branches {
878 if results.contains_key(b) || errors.contains_key(b) {
879 continue;
880 }
881 if !self.parent_running(run_id, parent_id) {
882 return;
883 }
884 let Some(body) = parent.branches.get(b).cloned() else {
885 continue;
886 };
887 let scope_id = format!("{parent_id}{{{b}}}");
888 match self.drive_body(run_id, &scope_id, &body, parent) {
889 BodyState::Waiting => {}
890 BodyState::Done => {
891 results.insert(b.clone(), self.body_result(run_id, &scope_id, &body));
892 changed = true;
893 if race {
894 for other in &branches {
896 if other != b {
897 self.cancel_scoped_children(
898 run_id,
899 &format!("{parent_id}{{{other}}}"),
900 );
901 }
902 }
903 let winner = results.get(b).cloned().unwrap_or(Value::Null);
904 self.finish_step_pub(
905 run_id,
906 parent_id,
907 StepStatus::Done,
908 Some(json!({"winner": b, "output": winner})),
909 None,
910 0,
911 );
912 return;
913 }
914 }
915 BodyState::Failed(e) => {
916 errors.insert(b.clone(), Value::String(e.clone()));
917 changed = true;
918 if !race && parent.on_error == OnError::Fail {
919 self.cancel_scoped_children(run_id, parent_id);
920 self.finish_step_pub(
921 run_id,
922 parent_id,
923 StepStatus::Failed,
924 Some(Value::Object(results.clone())),
925 Some(format!("branch {b} failed: {e}")),
926 0,
927 );
928 return;
929 }
930 }
931 }
932 }
933 progress["results"] = Value::Object(results.clone());
934 progress["errors"] = Value::Object(errors.clone());
935 if let Some(st) = self
936 .runs
937 .get_mut(run_id)
938 .and_then(|r| r.steps.get_mut(parent_id))
939 {
940 st.wait = Some(progress.clone());
941 }
942 if changed {
943 if let Some(r) = self.runs.get_mut(run_id) {
944 r.touch();
945 }
946 self.checkpoint(false);
947 }
948 if race
950 && let Some(t) = progress["timeout_ms"].as_u64()
951 && now_ms() >= progress["started_ms"].as_u64().unwrap_or(0) + t
952 {
953 self.cancel_scoped_children(run_id, parent_id);
954 self.finish_step_pub(
955 run_id,
956 parent_id,
957 StepStatus::Timeout,
958 None,
959 Some("race: no branch finished in time".into()),
960 0,
961 );
962 return;
963 }
964 if results.len() + errors.len() >= branches.len() {
965 if race {
966 self.finish_step_pub(
968 run_id,
969 parent_id,
970 StepStatus::Failed,
971 Some(Value::Object(results)),
972 Some(format!(
973 "race: every branch failed: {}",
974 Value::Object(errors)
975 )),
976 0,
977 );
978 } else {
979 let min = progress["min_success"].as_u64().map(|m| m as usize);
980 let ok = min.is_none_or(|m| results.len() >= m)
981 && (errors.is_empty() || parent.on_error != OnError::Fail);
982 let mut out = Value::Object(results.clone());
983 if !errors.is_empty() {
984 out["_errors"] = Value::Object(errors.clone());
985 }
986 if ok {
987 self.finish_step_pub(run_id, parent_id, StepStatus::Done, Some(out), None, 0);
988 } else {
989 self.finish_step_pub(
990 run_id,
991 parent_id,
992 StepStatus::Failed,
993 Some(out),
994 Some("parallel: not enough branches succeeded".into()),
995 0,
996 );
997 }
998 }
999 }
1000 }
1001
1002 fn advance_subgraph(&mut self, run_id: &str, parent_id: &str, parent: &Step) {
1003 let Some(body) = parent.body.clone() else {
1004 return;
1005 };
1006 match self.drive_body(run_id, parent_id, &body, parent) {
1007 BodyState::Waiting => {}
1008 BodyState::Done => {
1009 let out = self.body_result(run_id, parent_id, &body);
1010 self.finish_step_pub(run_id, parent_id, StepStatus::Done, Some(out), None, 0);
1011 }
1012 BodyState::Failed(e) => self.finish_step_pub(
1013 run_id,
1014 parent_id,
1015 StepStatus::Failed,
1016 None,
1017 Some(format!("subgraph failed: {e}")),
1018 0,
1019 ),
1020 }
1021 }
1022
1023 fn apply_collect(&mut self, run_id: &str, collect: &Value, out: &Value) {
1025 if let Some(into) = collect.get("into").and_then(Value::as_str) {
1026 let mode = collect
1027 .get("mode")
1028 .and_then(Value::as_str)
1029 .unwrap_or("overwrite")
1030 .to_string();
1031 if let Some(r) = self.runs.get_mut(run_id) {
1032 r.write_var(into, out.clone(), &mode);
1033 }
1034 }
1035 }
1036
1037 pub(crate) fn cancel_scoped_children(&mut self, run_id: &str, prefix: &str) {
1042 let nodes: Vec<_> = self
1043 .children
1044 .iter()
1045 .filter(|(_, c)| matches!(&c.kind, super::children::ChildKind::StepTurn { run, step, .. } if run == run_id && (under_scope(step, prefix) || step == prefix)))
1046 .map(|(n, _)| *n)
1047 .collect();
1048 for n in nodes {
1049 self.children.cancel(n, "nested step cancelled");
1050 }
1051 let timers = self.timers.owned_by(|o| {
1052 o["run"].as_str() == Some(run_id)
1053 && o["step"].as_str().is_some_and(|s| under_scope(s, prefix))
1054 });
1055 for t in timers {
1056 let _ = self.timers.disarm(&self.durable, &t);
1057 }
1058 self.pending.retain(|p| !matches!(&p.target, super::reactor::Target::Step(r, s) if r == run_id && under_scope(s, prefix)));
1061 if let Some(run) = self.runs.get_mut(run_id) {
1062 for (id, st) in run.steps.iter_mut() {
1063 if under_scope(id, prefix) && !st.status.is_terminal() {
1064 st.status = StepStatus::Cancelled;
1065 st.finished = Some(now_ms());
1066 }
1067 }
1068 run.touch();
1069 }
1070 }
1071
1072 pub(crate) fn on_scoped_step_done(&mut self, run_id: &str, scoped: &str) {
1076 if let Some(parent) = parent_of(scoped).map(str::to_string) {
1077 let parent_step_id = strip_scope_suffix(&parent);
1079 self.nested_advance(run_id, &parent_step_id);
1080 }
1081 }
1082}
1083
1084pub fn strip_scope_suffix(scoped_parent: &str) -> String {
1086 let (head, last) = match scoped_parent.rsplit_once('.') {
1087 Some((h, l)) => (Some(h), l),
1088 None => (None, scoped_parent),
1089 };
1090 let base = last.split(['[', '{']).next().unwrap_or(last);
1091 match head {
1092 Some(h) => format!("{h}.{base}"),
1093 None => base.to_string(),
1094 }
1095}
1096
1097fn under_scope(id: &str, prefix: &str) -> bool {
1106 id.strip_prefix(prefix)
1107 .is_some_and(|rest| rest.starts_with(['.', '[', '{']))
1108}
1109
1110fn seg_label(seg: &Segment) -> String {
1111 match (&seg.index, &seg.branch) {
1112 (Some(i), _) => format!("{}[{i}]", seg.name),
1113 (_, Some(b)) => format!("{}{{{b}}}", seg.name),
1114 _ => seg.name.clone(),
1115 }
1116}
1117
1118fn element_item(progress: &Value, ix: usize) -> Option<Value> {
1123 progress["items"]
1124 .as_array()
1125 .and_then(|a| a.get(ix).cloned())
1126}
1127
1128fn batch_of(progress: &Value, ix: usize) -> Option<usize> {
1129 let size = progress["size"].as_u64()? as usize;
1130 Some(ix / size.max(1))
1131}
1132
1133fn collect_results(results: &Map<String, Value>, total: usize) -> Value {
1134 Value::Array(
1135 (0..total)
1136 .map(|i| results.get(&i.to_string()).cloned().unwrap_or(Value::Null))
1137 .collect(),
1138 )
1139}
1140
1141fn iterate_output(progress: &Value, results: &[Value]) -> Value {
1142 if progress["collect"].is_null() && !results.is_empty() {
1143 results.last().cloned().unwrap_or(Value::Null)
1144 } else {
1145 Value::Array(results.to_vec())
1146 }
1147}
1148
1149fn unreachable_wf() -> Workflow {
1150 Workflow {
1153 name: String::new(),
1154 version: 3,
1155 description: None,
1156 armed: false,
1157 inputs_schema: None,
1158 concurrency: Default::default(),
1159 limits: Default::default(),
1160 outputs_schema: None,
1161 steps: BTreeMap::new(),
1162 hash: String::new(),
1163 definition: Value::Null,
1164 }
1165}
1166
1167pub enum BodyState {
1169 Waiting,
1170 Done,
1171 Failed(String),
1172}
1173
1174trait StatusLabel {
1175 fn as_label(&self) -> &'static str;
1176}
1177impl StatusLabel for StepStatus {
1178 fn as_label(&self) -> &'static str {
1179 match self {
1180 StepStatus::Pending => "pending",
1181 StepStatus::Running => "running",
1182 StepStatus::Done => "done",
1183 StepStatus::Failed => "failed",
1184 StepStatus::Skipped => "skipped",
1185 StepStatus::Cancelled => "cancelled",
1186 StepStatus::Timeout => "timeout",
1187 StepStatus::Suspended => "suspended",
1188 }
1189 }
1190}
1191
1192#[cfg(test)]
1193mod tests {
1194 use super::*;
1195
1196 #[test]
1197 fn scoped_ids_parse_and_strip() {
1198 let segs = parse_scoped("each[3].classify");
1199 assert_eq!(segs.len(), 2);
1200 assert_eq!(
1201 segs[0],
1202 Segment {
1203 name: "each".into(),
1204 index: Some(3),
1205 branch: None
1206 }
1207 );
1208 assert_eq!(
1209 segs[1],
1210 Segment {
1211 name: "classify".into(),
1212 index: None,
1213 branch: None
1214 }
1215 );
1216 let segs = parse_scoped("par{a}.inner[2].leaf");
1217 assert_eq!(segs[0].branch.as_deref(), Some("a"));
1218 assert_eq!(segs[1].index, Some(2));
1219 assert_eq!(scoped_id("each[3]", "classify"), "each[3].classify");
1220 assert_eq!(parent_of("each[3].classify"), Some("each[3]"));
1221 assert_eq!(strip_scope_suffix("each[3]"), "each");
1222 assert_eq!(strip_scope_suffix("par{a}"), "par");
1223 assert_eq!(strip_scope_suffix("x[1].y[2]"), "x[1].y");
1224 assert!(is_scoped("a.b") && !is_scoped("a"));
1225 assert!(under_scope("each[0].work", "each"));
1229 assert!(under_scope("par{a}.work", "par"));
1230 assert!(under_scope("sub.work", "sub"));
1231 assert!(under_scope("each[0].inner{b}.leaf", "each[0].inner"));
1232 assert!(!under_scope("eachother.work", "each"));
1233 assert!(!under_scope("each", "each"));
1234 assert_eq!(
1235 collect_results(&[("0".to_string(), json!(1))].into_iter().collect(), 2),
1236 json!([1, null])
1237 );
1238 }
1239}