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 fan_out_cap = self
233 .settings
234 .limits
235 .workflow
236 .fan_out
237 .map(u64::from)
238 .unwrap_or(crate::engine::model::MAX_BATCH_PARALLEL);
239 let size = spec
240 .get("size")
241 .and_then(Value::as_u64)
242 .or_else(|| batch.get("size").and_then(Value::as_u64))
243 .unwrap_or(if step.kind == "batch" { 10 } else { 1 })
244 .max(1) as usize;
245 let parallel = spec
246 .get("parallel")
247 .and_then(Value::as_u64)
248 .or_else(|| batch.get("parallel").and_then(Value::as_u64))
249 .unwrap_or(crate::engine::model::DEFAULT_FAN_OUT)
255 .clamp(1, fan_out_cap) as usize;
256 let rate = spec
257 .get("rate")
258 .and_then(Value::as_str)
259 .or_else(|| batch.get("rate").and_then(Value::as_str))
260 .map(str::to_string);
261 let group_by = spec.get("by").and_then(Value::as_str).map(str::to_string);
262 let items = match (&group_by, step.kind.as_str()) {
264 (Some(key), "batch") => {
265 let mut groups: Vec<(Value, Vec<Value>)> = Vec::new();
266 for it in items {
267 let k = crate::engine::data::path_of(&it, key).unwrap_or(Value::Null);
268 match groups.iter_mut().find(|(gk, _)| *gk == k) {
269 Some((_, g)) => g.push(it),
270 None => groups.push((k, vec![it])),
271 }
272 }
273 groups.into_iter().map(|(_, g)| Value::Array(g)).collect()
274 }
275 _ => items,
276 };
277 let total = items.len();
278 let items_ref = self.store_items(run_id, step_id, items);
279 json!({
280 "kind": step.kind, "total": total, "size": size, "parallel": parallel, "rate": rate,
281 "cursor": 0, "active": [], "results": {}, "done": 0, "batches_done": 0, "next_batch_at": 0,
282 "items": items_ref, "started_ms": now_ms(),
283 "collect": spec.get("collect").cloned().unwrap_or(Value::Null),
284 "as": spec.get("as").and_then(Value::as_str).unwrap_or("item"),
285 })
286 }
287 "iterate" => {
288 let max = spec
289 .get("max_iterations")
290 .and_then(Value::as_u64)
291 .unwrap_or(crate::engine::model::MAX_ITERATIONS)
292 .min(crate::engine::model::MAX_ITERATIONS);
293 json!({"kind": "iterate", "iteration": 0, "max": max, "results": [], "collect": spec.get("collect").cloned().unwrap_or(Value::Null), "started_ms": now_ms()})
294 }
295 "parallel" | "race" => {
296 let branches: Vec<String> = step.branches.keys().cloned().collect();
297 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})
302 }
303 "subgraph" => json!({"kind": "subgraph", "started_ms": now_ms()}),
304 other => {
305 self.finish_step_pub(
306 run_id,
307 step_id,
308 StepStatus::Failed,
309 None,
310 Some(format!("{other} is not a nested kind")),
311 0,
312 );
313 return;
314 }
315 };
316 if let Some(st) = self
317 .runs
318 .get_mut(run_id)
319 .and_then(|r| r.steps.get_mut(step_id))
320 {
321 st.status = StepStatus::Running;
322 st.wait = Some(progress);
323 }
324 if let Some(r) = self.runs.get_mut(run_id) {
325 r.touch();
326 }
327 self.checkpoint(false);
328 self.nested_advance(run_id, step_id);
329 }
330
331 fn store_items(&mut self, run_id: &str, step_id: &str, items: Vec<Value>) -> Value {
333 let v = Value::Array(items);
334 let cap = self.settings.limits.inline_max_bytes.unwrap_or(65_536) as usize;
335 if v.to_string().len() > cap {
336 match self.artifacts.create(
337 &self.durable,
338 super::artifacts::NewArtifact {
339 name: format!("{run_id}/{step_id}/items.json").as_str(),
340 mime: Some("application/json"),
341 content: v.clone(),
342 created_by: Some("engine"),
343 sensitive: false,
344 owner: Some(run_id),
345 },
346 ) {
347 Ok(meta) => return json!({"$artifact": meta["id"]}),
348 Err(e) => self.log.warn(
349 "nested.items.artifact_fail",
350 json!({"run": run_id, "step": step_id, "err": e}),
351 ),
352 }
353 }
354 v
355 }
356
357 fn items_of(&self, progress: &Value) -> Vec<Value> {
359 match &progress["items"] {
360 Value::Array(a) => a.clone(),
361 Value::Object(o) if o.get("$artifact").is_some() => o
362 .get("$artifact")
363 .and_then(Value::as_str)
364 .and_then(|id| self.artifacts.get(id))
365 .and_then(|a| a.content.as_array().cloned())
366 .unwrap_or_default(),
367 _ => Vec::new(),
368 }
369 }
370
371 pub(crate) fn nested_advance(&mut self, run_id: &str, parent_id: &str) {
377 let Some(wf) = self.definition_for_run(run_id) else {
378 return;
379 };
380 let Some(run) = self.runs.get(run_id) else {
381 return;
382 };
383 if run.status.is_terminal() {
384 return;
385 }
386 let Some((parent, _)) = self.resolve_step(&wf, run, parent_id) else {
387 return;
388 };
389 let Some(progress) = run.steps.get(parent_id).and_then(|s| s.wait.clone()) else {
390 return;
391 };
392 if run.steps.get(parent_id).map(|s| s.status) != Some(StepStatus::Running) {
393 return;
394 }
395 match progress["kind"].as_str() {
396 Some("foreach") | Some("batch") => {
397 self.advance_foreach(run_id, parent_id, &parent, progress)
398 }
399 Some("iterate") => self.advance_iterate(run_id, parent_id, &parent, progress),
400 Some("parallel") => self.advance_parallel(run_id, parent_id, &parent, progress, false),
401 Some("race") => self.advance_parallel(run_id, parent_id, &parent, progress, true),
402 Some("subgraph") => self.advance_subgraph(run_id, parent_id, &parent),
403 _ => {}
404 }
405 }
406
407 fn drive_body(
410 &mut self,
411 run_id: &str,
412 scope_id: &str,
413 body: &Body,
414 parent: &Step,
415 ) -> BodyState {
416 let ids: Vec<String> = body.topo_order();
417 if let Some(run) = self.runs.get_mut(run_id) {
419 for id in &ids {
420 run.steps.entry(scoped_id(scope_id, id)).or_default();
421 }
422 }
423 let mut all_terminal = true;
424 let mut failed: Option<String> = None;
425 let mut ready: Vec<String> = Vec::new();
426 let mut in_flight = false;
427 let mut changed = true;
429 while changed {
430 changed = false;
431 let Some(snapshot) = self.runs.get(run_id).map(|r| r.steps.clone()) else {
432 return BodyState::Waiting;
433 };
434 for id in &ids {
435 let sid = scoped_id(scope_id, id);
436 let st = snapshot.get(&sid).cloned().unwrap_or_default();
437 match st.status {
438 StepStatus::Running | StepStatus::Suspended => {
439 in_flight = true;
440 all_terminal = false;
441 continue;
442 }
443 StepStatus::Failed | StepStatus::Timeout | StepStatus::Cancelled => {
444 if failed.is_none() {
445 failed = Some(
446 st.error
447 .clone()
448 .unwrap_or_else(|| format!("{id} {}", st.status.as_label())),
449 );
450 }
451 continue;
452 }
453 StepStatus::Done | StepStatus::Skipped | StepStatus::Pruned => continue,
454 StepStatus::Pending => {}
455 }
456 all_terminal = false;
457 if ready.contains(&sid) {
458 continue;
459 }
460 let step = &body.steps[id];
461 if st.forced {
462 ready.push(sid);
463 continue;
464 }
465 let deps_ok = step.depends_on.iter().all(|d| {
466 snapshot
467 .get(&scoped_id(scope_id, d))
468 .is_some_and(|s| s.status.is_satisfied())
469 });
470 let deps_failed = step.depends_on.iter().any(|d| {
471 snapshot.get(&scoped_id(scope_id, d)).is_some_and(|s| {
472 matches!(
473 s.status,
474 StepStatus::Failed | StepStatus::Cancelled | StepStatus::Timeout
475 )
476 })
477 });
478 if deps_failed {
479 continue;
481 }
482 if !deps_ok {
483 continue;
484 }
485 if let Some(w) = &step.when {
486 let scope = {
487 let wf = self
488 .definition_for_run(run_id)
489 .unwrap_or_else(unreachable_wf);
490 self.runs
491 .get(run_id)
492 .and_then(|r| self.resolve_step(&wf, r, &sid))
493 .and_then(|(_, s)| s)
494 .unwrap_or_default()
495 };
496 let data = self.scoped_data_view(run_id, &scope);
497 let expr = w.trim().trim_start_matches("CEL:").trim();
498 let vars: Vec<(&str, &Value)> =
499 data.iter().map(|(k, v)| (k.as_str(), v)).collect();
500 match crate::cel::eval_bool(expr, &vars) {
501 Ok(true) => {}
502 Ok(false) => {
503 if let Some(r) = self.runs.get_mut(run_id) {
504 r.end_step(&sid, StepStatus::Skipped, None, None);
505 }
506 changed = true;
507 continue;
508 }
509 Err(e) => {
510 failed = Some(format!("{id}: when: {e}"));
511 continue;
512 }
513 }
514 }
515 ready.push(sid);
516 }
517 }
518 if let Some(e) = failed {
519 if !in_flight {
522 return BodyState::Failed(e);
523 }
524 return BodyState::Waiting;
525 }
526 if all_terminal {
527 return BodyState::Done;
528 }
529 for sid in ready {
530 if !self.parent_running(run_id, &strip_scope_suffix(scope_id)) {
533 break;
534 }
535 self.execute_step_pub(run_id, &sid);
536 }
537 let _ = parent;
538 BodyState::Waiting
539 }
540
541 fn parent_running(&self, run_id: &str, parent_id: &str) -> bool {
543 self.runs.get(run_id).is_some_and(|r| {
544 !r.status.is_terminal()
545 && r.steps
546 .get(parent_id)
547 .is_some_and(|st| st.status == StepStatus::Running)
548 })
549 }
550
551 fn scoped_data_view(&mut self, run_id: &str, scope: &Scope) -> Data {
553 self.scoped_data(run_id, scope)
554 }
555
556 fn body_result(&self, run_id: &str, scope_id: &str, body: &Body) -> Value {
559 let Some(run) = self.runs.get(run_id) else {
560 return Value::Null;
561 };
562 let sinks = body.sinks();
563 if sinks.len() == 1 {
564 return run
565 .steps
566 .get(&scoped_id(scope_id, &sinks[0]))
567 .and_then(|s| s.output.clone())
568 .unwrap_or(Value::Null);
569 }
570 let mut o = Map::new();
571 for s in sinks {
572 o.insert(
573 s.clone(),
574 run.steps
575 .get(&scoped_id(scope_id, &s))
576 .and_then(|st| st.output.clone())
577 .unwrap_or(Value::Null),
578 );
579 }
580 Value::Object(o)
581 }
582
583 fn advance_foreach(
584 &mut self,
585 run_id: &str,
586 parent_id: &str,
587 parent: &Step,
588 mut progress: Value,
589 ) {
590 let Some(body) = parent.body.clone() else {
591 return;
592 };
593 let total = progress["total"].as_u64().unwrap_or(0) as usize;
594 let size = progress["size"].as_u64().unwrap_or(1).max(1) as usize;
595 let parallel = progress["parallel"].as_u64().unwrap_or(1).max(1) as usize;
596 let items = self.items_of(&progress);
597 let mut active: Vec<usize> = progress["active"]
598 .as_array()
599 .map(|a| {
600 a.iter()
601 .filter_map(Value::as_u64)
602 .map(|x| x as usize)
603 .collect()
604 })
605 .unwrap_or_default();
606 let mut cursor = progress["cursor"].as_u64().unwrap_or(0) as usize;
607 let mut done = progress["done"].as_u64().unwrap_or(0) as usize;
608 let mut batches_done = progress["batches_done"].as_u64().unwrap_or(0) as usize;
609 let mut results = progress["results"].as_object().cloned().unwrap_or_default();
610 let mut changed = false;
611 for ix in active.clone() {
613 if !self.parent_running(run_id, parent_id) {
614 return;
615 }
616 let scope_id = format!("{parent_id}[{ix}]");
617 match self.drive_body(run_id, &scope_id, &body, parent) {
618 BodyState::Waiting => {}
619 BodyState::Done => {
620 let out = self.body_result(run_id, &scope_id, &body);
621 results.insert(ix.to_string(), out);
622 active.retain(|a| *a != ix);
623 done += 1;
624 changed = true;
625 }
626 BodyState::Failed(e) => {
627 active.retain(|a| *a != ix);
628 done += 1;
629 changed = true;
630 match parent.on_error {
631 OnError::Continue | OnError::Goto(_) => {
632 results.insert(ix.to_string(), json!({"index": ix, "error": e}));
633 }
634 OnError::Fail => {
635 self.cancel_scoped_children(run_id, parent_id);
637 self.finish_step_pub(
638 run_id,
639 parent_id,
640 StepStatus::Failed,
641 Some(collect_results(&results, total)),
642 Some(format!("element {ix} failed: {e}")),
643 0,
644 );
645 return;
646 }
647 }
648 }
649 }
650 }
651 let batches_total = total.div_ceil(size).max(if total == 0 { 0 } else { 1 });
653 while batches_done < batches_total {
654 let (from, to) = (batches_done * size, ((batches_done + 1) * size).min(total));
655 if (from..to).all(|i| results.contains_key(&i.to_string())) {
656 batches_done += 1;
657 changed = true;
658 crate::state::kill_point("batch.k");
659 } else {
660 break;
661 }
662 }
663 let rate = progress["rate"].as_str().map(super::subagents::parse_rate);
665 let mut next_batch_at = progress["next_batch_at"].as_u64().unwrap_or(0);
666 while cursor < total {
667 let batches_in_flight = active
668 .iter()
669 .map(|i| i / size)
670 .collect::<std::collections::BTreeSet<_>>()
671 .len();
672 if batches_in_flight >= parallel {
673 break;
674 }
675 if rate.is_some() && now_ms() < next_batch_at {
676 break;
677 }
678 let end = (cursor + size).min(total);
679 for ix in cursor..end {
680 active.push(ix);
681 }
682 if let Some((_, per_sec)) = rate {
683 next_batch_at = now_ms() + (1000.0 / per_sec.max(0.001)) as u64;
684 }
685 cursor = end;
686 changed = true;
687 }
688 progress["active"] = json!(active);
690 progress["cursor"] = json!(cursor);
691 progress["done"] = json!(done);
692 progress["batches_done"] = json!(batches_done);
693 progress["results"] = Value::Object(results.clone());
694 progress["next_batch_at"] = json!(next_batch_at);
695 if let Some(st) = self
696 .runs
697 .get_mut(run_id)
698 .and_then(|r| r.steps.get_mut(parent_id))
699 {
700 st.wait = Some(progress.clone());
701 }
702 if changed {
703 if let Some(r) = self.runs.get_mut(run_id) {
704 r.touch();
705 }
706 self.checkpoint(false);
707 }
708 for ix in active.clone() {
710 if !self.parent_running(run_id, parent_id) {
711 return;
712 }
713 let scope_id = format!("{parent_id}[{ix}]");
714 let fresh = self.runs.get(run_id).is_some_and(|r| {
715 !body
716 .steps
717 .keys()
718 .any(|k| r.steps.contains_key(&scoped_id(&scope_id, k)))
719 });
720 if fresh {
721 let _ = items.get(ix); let _ = self.drive_body(run_id, &scope_id, &body, parent);
723 }
724 }
725 if !self.parent_running(run_id, parent_id) {
727 return;
728 }
729 if done >= total && cursor >= total {
730 let out = collect_results(&results, total);
731 self.apply_collect(run_id, &progress["collect"], &out);
732 self.finish_step_pub(run_id, parent_id, StepStatus::Done, Some(out), None, 0);
733 }
734 }
735
736 fn advance_iterate(
737 &mut self,
738 run_id: &str,
739 parent_id: &str,
740 parent: &Step,
741 mut progress: Value,
742 ) {
743 let Some(body) = parent.body.clone() else {
744 return;
745 };
746 let k = progress["iteration"].as_u64().unwrap_or(0) as usize;
747 let max = progress["max"].as_u64().unwrap_or(1) as usize;
748 let mut results: Vec<Value> = progress["results"].as_array().cloned().unwrap_or_default();
749 let scope_id = format!("{parent_id}[{k}]");
750 let started = self.runs.get(run_id).is_some_and(|r| {
751 body.steps
752 .keys()
753 .any(|s| r.steps.contains_key(&scoped_id(&scope_id, s)))
754 });
755 if !started {
756 if let Some(w) = parent.field_str("while") {
758 let scope = Scope {
759 parent: scope_id.clone(),
760 parent_step: Some(parent.clone()),
761 siblings: body.steps.clone(),
762 iteration: Some(k),
763 ..Default::default()
764 };
765 let mut data = self.scoped_data(run_id, &scope);
766 data.insert("results".into(), Value::Array(results.clone()));
767 data.insert(
768 "last".into(),
769 results.last().cloned().unwrap_or(Value::Null),
770 );
771 let vars: Vec<(&str, &Value)> = data.iter().map(|(k, v)| (k.as_str(), v)).collect();
772 match crate::cel::eval_bool(w.trim().trim_start_matches("CEL:").trim(), &vars) {
773 Ok(true) => {}
774 Ok(false) => {
775 let out = iterate_output(&progress, &results);
776 self.apply_collect(run_id, &progress["collect"], &out);
777 self.finish_step_pub(
778 run_id,
779 parent_id,
780 StepStatus::Done,
781 Some(out),
782 None,
783 0,
784 );
785 return;
786 }
787 Err(e) => {
788 self.finish_step_pub(
789 run_id,
790 parent_id,
791 StepStatus::Failed,
792 None,
793 Some(format!("iterate.while: {e}")),
794 0,
795 );
796 return;
797 }
798 }
799 }
800 if k >= max {
801 let out = iterate_output(&progress, &results);
802 self.apply_collect(run_id, &progress["collect"], &out);
803 self.finish_step_pub(run_id, parent_id, StepStatus::Done, Some(out), None, 0);
804 return;
805 }
806 }
807 match self.drive_body(run_id, &scope_id, &body, parent) {
808 BodyState::Waiting => {}
809 BodyState::Failed(e) => {
810 self.finish_step_pub(
811 run_id,
812 parent_id,
813 StepStatus::Failed,
814 Some(iterate_output(&progress, &results)),
815 Some(format!("iteration {k} failed: {e}")),
816 0,
817 );
818 }
819 BodyState::Done => {
820 let out = self.body_result(run_id, &scope_id, &body);
821 results.push(out.clone());
822 let mut stop = k + 1 >= max;
824 if let Some(u) = parent.field_str("until")
825 && !stop
826 {
827 let scope = Scope {
828 parent: scope_id.clone(),
829 parent_step: Some(parent.clone()),
830 siblings: body.steps.clone(),
831 iteration: Some(k),
832 ..Default::default()
833 };
834 let mut data = self.scoped_data(run_id, &scope);
835 data.insert("result".into(), out.clone());
836 data.insert("results".into(), Value::Array(results.clone()));
837 let vars: Vec<(&str, &Value)> =
838 data.iter().map(|(k, v)| (k.as_str(), v)).collect();
839 match crate::cel::eval_bool(u.trim().trim_start_matches("CEL:").trim(), &vars) {
840 Ok(b) => stop = b,
841 Err(e) => {
842 self.finish_step_pub(
843 run_id,
844 parent_id,
845 StepStatus::Failed,
846 None,
847 Some(format!("iterate.until: {e}")),
848 0,
849 );
850 return;
851 }
852 }
853 }
854 progress["results"] = Value::Array(results.clone());
855 progress["iteration"] = json!(k + 1);
856 if let Some(st) = self
857 .runs
858 .get_mut(run_id)
859 .and_then(|r| r.steps.get_mut(parent_id))
860 {
861 st.wait = Some(progress.clone());
862 }
863 if let Some(r) = self.runs.get_mut(run_id) {
864 r.touch();
865 }
866 self.checkpoint(false);
867 if stop {
868 let out = iterate_output(&progress, &results);
869 self.apply_collect(run_id, &progress["collect"], &out);
870 self.finish_step_pub(run_id, parent_id, StepStatus::Done, Some(out), None, 0);
871 } else {
872 self.nested_advance(run_id, parent_id);
874 }
875 }
876 }
877 }
878
879 fn advance_parallel(
880 &mut self,
881 run_id: &str,
882 parent_id: &str,
883 parent: &Step,
884 mut progress: Value,
885 race: bool,
886 ) {
887 let mut results = progress["results"].as_object().cloned().unwrap_or_default();
888 let mut errors = progress["errors"].as_object().cloned().unwrap_or_default();
889 let branches: Vec<String> = parent.branches.keys().cloned().collect();
890 let mut changed = false;
891 for b in &branches {
892 if results.contains_key(b) || errors.contains_key(b) {
893 continue;
894 }
895 if !self.parent_running(run_id, parent_id) {
896 return;
897 }
898 let Some(body) = parent.branches.get(b).cloned() else {
899 continue;
900 };
901 let scope_id = format!("{parent_id}{{{b}}}");
902 match self.drive_body(run_id, &scope_id, &body, parent) {
903 BodyState::Waiting => {}
904 BodyState::Done => {
905 results.insert(b.clone(), self.body_result(run_id, &scope_id, &body));
906 changed = true;
907 if race {
908 for other in &branches {
910 if other != b {
911 self.cancel_scoped_children(
912 run_id,
913 &format!("{parent_id}{{{other}}}"),
914 );
915 }
916 }
917 let winner = results.get(b).cloned().unwrap_or(Value::Null);
918 self.finish_step_pub(
919 run_id,
920 parent_id,
921 StepStatus::Done,
922 Some(json!({"winner": b, "output": winner})),
923 None,
924 0,
925 );
926 return;
927 }
928 }
929 BodyState::Failed(e) => {
930 errors.insert(b.clone(), Value::String(e.clone()));
931 changed = true;
932 if !race && parent.on_error == OnError::Fail {
933 self.cancel_scoped_children(run_id, parent_id);
934 self.finish_step_pub(
935 run_id,
936 parent_id,
937 StepStatus::Failed,
938 Some(Value::Object(results.clone())),
939 Some(format!("branch {b} failed: {e}")),
940 0,
941 );
942 return;
943 }
944 }
945 }
946 }
947 progress["results"] = Value::Object(results.clone());
948 progress["errors"] = Value::Object(errors.clone());
949 if let Some(st) = self
950 .runs
951 .get_mut(run_id)
952 .and_then(|r| r.steps.get_mut(parent_id))
953 {
954 st.wait = Some(progress.clone());
955 }
956 if changed {
957 if let Some(r) = self.runs.get_mut(run_id) {
958 r.touch();
959 }
960 self.checkpoint(false);
961 }
962 if race
964 && let Some(t) = progress["timeout_ms"].as_u64()
965 && now_ms() >= progress["started_ms"].as_u64().unwrap_or(0) + t
966 {
967 self.cancel_scoped_children(run_id, parent_id);
968 self.finish_step_pub(
969 run_id,
970 parent_id,
971 StepStatus::Timeout,
972 None,
973 Some("race: no branch finished in time".into()),
974 0,
975 );
976 return;
977 }
978 if results.len() + errors.len() >= branches.len() {
979 if race {
980 self.finish_step_pub(
982 run_id,
983 parent_id,
984 StepStatus::Failed,
985 Some(Value::Object(results)),
986 Some(format!(
987 "race: every branch failed: {}",
988 Value::Object(errors)
989 )),
990 0,
991 );
992 } else {
993 let min = progress["min_success"].as_u64().map(|m| m as usize);
994 let ok = min.is_none_or(|m| results.len() >= m)
995 && (errors.is_empty() || parent.on_error != OnError::Fail);
996 let mut out = Value::Object(results.clone());
997 if !errors.is_empty() {
998 out["_errors"] = Value::Object(errors.clone());
999 }
1000 if ok {
1001 self.finish_step_pub(run_id, parent_id, StepStatus::Done, Some(out), None, 0);
1002 } else {
1003 self.finish_step_pub(
1004 run_id,
1005 parent_id,
1006 StepStatus::Failed,
1007 Some(out),
1008 Some("parallel: not enough branches succeeded".into()),
1009 0,
1010 );
1011 }
1012 }
1013 }
1014 }
1015
1016 fn advance_subgraph(&mut self, run_id: &str, parent_id: &str, parent: &Step) {
1017 let Some(body) = parent.body.clone() else {
1018 return;
1019 };
1020 match self.drive_body(run_id, parent_id, &body, parent) {
1021 BodyState::Waiting => {}
1022 BodyState::Done => {
1023 let out = self.body_result(run_id, parent_id, &body);
1024 self.finish_step_pub(run_id, parent_id, StepStatus::Done, Some(out), None, 0);
1025 }
1026 BodyState::Failed(e) => self.finish_step_pub(
1027 run_id,
1028 parent_id,
1029 StepStatus::Failed,
1030 None,
1031 Some(format!("subgraph failed: {e}")),
1032 0,
1033 ),
1034 }
1035 }
1036
1037 fn apply_collect(&mut self, run_id: &str, collect: &Value, out: &Value) {
1039 if let Some(into) = collect.get("into").and_then(Value::as_str) {
1040 let mode = collect
1041 .get("mode")
1042 .and_then(Value::as_str)
1043 .unwrap_or("overwrite")
1044 .to_string();
1045 if let Some(r) = self.runs.get_mut(run_id) {
1046 r.write_var(into, out.clone(), &mode);
1047 }
1048 }
1049 }
1050
1051 pub(crate) fn cancel_scoped_children(&mut self, run_id: &str, prefix: &str) {
1056 let nodes: Vec<_> = self
1057 .children
1058 .iter()
1059 .filter(|(_, c)| matches!(&c.kind, super::children::ChildKind::StepTurn { run, step, .. } if run == run_id && (under_scope(step, prefix) || step == prefix)))
1060 .map(|(n, _)| *n)
1061 .collect();
1062 for n in nodes {
1063 self.children.cancel(n, "nested step cancelled");
1064 }
1065 let timers = self.timers.owned_by(|o| {
1066 o["run"].as_str() == Some(run_id)
1067 && o["step"].as_str().is_some_and(|s| under_scope(s, prefix))
1068 });
1069 for t in timers {
1070 let _ = self.timers.disarm(&self.durable, &t);
1071 }
1072 self.pending.retain(|p| !matches!(&p.target, super::reactor::Target::Step(r, s) if r == run_id && under_scope(s, prefix)));
1075 if let Some(run) = self.runs.get_mut(run_id) {
1076 for (id, st) in run.steps.iter_mut() {
1077 if under_scope(id, prefix) && !st.status.is_terminal() {
1078 st.status = StepStatus::Cancelled;
1079 st.finished = Some(now_ms());
1080 }
1081 }
1082 run.touch();
1083 }
1084 }
1085
1086 pub(crate) fn on_scoped_step_done(&mut self, run_id: &str, scoped: &str) {
1090 if let Some(parent) = parent_of(scoped).map(str::to_string) {
1091 let parent_step_id = strip_scope_suffix(&parent);
1093 self.nested_advance(run_id, &parent_step_id);
1094 }
1095 }
1096}
1097
1098pub fn strip_scope_suffix(scoped_parent: &str) -> String {
1100 let (head, last) = match scoped_parent.rsplit_once('.') {
1101 Some((h, l)) => (Some(h), l),
1102 None => (None, scoped_parent),
1103 };
1104 let base = last.split(['[', '{']).next().unwrap_or(last);
1105 match head {
1106 Some(h) => format!("{h}.{base}"),
1107 None => base.to_string(),
1108 }
1109}
1110
1111fn under_scope(id: &str, prefix: &str) -> bool {
1120 id.strip_prefix(prefix)
1121 .is_some_and(|rest| rest.starts_with(['.', '[', '{']))
1122}
1123
1124fn seg_label(seg: &Segment) -> String {
1125 match (&seg.index, &seg.branch) {
1126 (Some(i), _) => format!("{}[{i}]", seg.name),
1127 (_, Some(b)) => format!("{}{{{b}}}", seg.name),
1128 _ => seg.name.clone(),
1129 }
1130}
1131
1132fn element_item(progress: &Value, ix: usize) -> Option<Value> {
1137 progress["items"]
1138 .as_array()
1139 .and_then(|a| a.get(ix).cloned())
1140}
1141
1142fn batch_of(progress: &Value, ix: usize) -> Option<usize> {
1143 let size = progress["size"].as_u64()? as usize;
1144 Some(ix / size.max(1))
1145}
1146
1147fn collect_results(results: &Map<String, Value>, total: usize) -> Value {
1148 Value::Array(
1149 (0..total)
1150 .map(|i| results.get(&i.to_string()).cloned().unwrap_or(Value::Null))
1151 .collect(),
1152 )
1153}
1154
1155fn iterate_output(progress: &Value, results: &[Value]) -> Value {
1156 if progress["collect"].is_null() && !results.is_empty() {
1157 results.last().cloned().unwrap_or(Value::Null)
1158 } else {
1159 Value::Array(results.to_vec())
1160 }
1161}
1162
1163fn unreachable_wf() -> Workflow {
1164 Workflow {
1167 state: Default::default(),
1168 name: String::new(),
1169 version: 3,
1170 description: None,
1171 armed: false,
1172 inputs_schema: None,
1173 concurrency: Default::default(),
1174 limits: Default::default(),
1175 outputs_schema: None,
1176 steps: BTreeMap::new(),
1177 hash: String::new(),
1178 definition: Value::Null,
1179 }
1180}
1181
1182pub enum BodyState {
1184 Waiting,
1185 Done,
1186 Failed(String),
1187}
1188
1189pub(crate) trait StatusLabel {
1190 fn as_label(&self) -> &'static str;
1191}
1192impl StatusLabel for StepStatus {
1193 fn as_label(&self) -> &'static str {
1194 match self {
1195 StepStatus::Pending => "pending",
1196 StepStatus::Running => "running",
1197 StepStatus::Done => "done",
1198 StepStatus::Failed => "failed",
1199 StepStatus::Skipped => "skipped",
1200 StepStatus::Pruned => "pruned",
1201 StepStatus::Cancelled => "cancelled",
1202 StepStatus::Timeout => "timeout",
1203 StepStatus::Suspended => "suspended",
1204 }
1205 }
1206}
1207
1208#[cfg(test)]
1209mod tests {
1210 use super::*;
1211
1212 #[test]
1213 fn scoped_ids_parse_and_strip() {
1214 let segs = parse_scoped("each[3].classify");
1215 assert_eq!(segs.len(), 2);
1216 assert_eq!(
1217 segs[0],
1218 Segment {
1219 name: "each".into(),
1220 index: Some(3),
1221 branch: None
1222 }
1223 );
1224 assert_eq!(
1225 segs[1],
1226 Segment {
1227 name: "classify".into(),
1228 index: None,
1229 branch: None
1230 }
1231 );
1232 let segs = parse_scoped("par{a}.inner[2].leaf");
1233 assert_eq!(segs[0].branch.as_deref(), Some("a"));
1234 assert_eq!(segs[1].index, Some(2));
1235 assert_eq!(scoped_id("each[3]", "classify"), "each[3].classify");
1236 assert_eq!(parent_of("each[3].classify"), Some("each[3]"));
1237 assert_eq!(strip_scope_suffix("each[3]"), "each");
1238 assert_eq!(strip_scope_suffix("par{a}"), "par");
1239 assert_eq!(strip_scope_suffix("x[1].y[2]"), "x[1].y");
1240 assert!(is_scoped("a.b") && !is_scoped("a"));
1241 assert!(under_scope("each[0].work", "each"));
1245 assert!(under_scope("par{a}.work", "par"));
1246 assert!(under_scope("sub.work", "sub"));
1247 assert!(under_scope("each[0].inner{b}.leaf", "each[0].inner"));
1248 assert!(!under_scope("eachother.work", "each"));
1249 assert!(!under_scope("each", "each"));
1250 assert_eq!(
1251 collect_results(&[("0".to_string(), json!(1))].into_iter().collect(), 2),
1252 json!([1, null])
1253 );
1254 }
1255}