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 self.checkpoint(false);
663 crate::state::kill_point("batch.k");
664 } else {
665 break;
666 }
667 }
668 let rate = progress["rate"].as_str().map(super::subagents::parse_rate);
670 let mut next_batch_at = progress["next_batch_at"].as_u64().unwrap_or(0);
671 while cursor < total {
672 let batches_in_flight = active
673 .iter()
674 .map(|i| i / size)
675 .collect::<std::collections::BTreeSet<_>>()
676 .len();
677 if batches_in_flight >= parallel {
678 break;
679 }
680 if rate.is_some() && now_ms() < next_batch_at {
681 break;
682 }
683 let end = (cursor + size).min(total);
684 for ix in cursor..end {
685 active.push(ix);
686 }
687 if let Some((_, per_sec)) = rate {
688 next_batch_at = now_ms() + (1000.0 / per_sec.max(0.001)) as u64;
689 }
690 cursor = end;
691 changed = true;
692 }
693 progress["active"] = json!(active);
695 progress["cursor"] = json!(cursor);
696 progress["done"] = json!(done);
697 progress["batches_done"] = json!(batches_done);
698 progress["results"] = Value::Object(results.clone());
699 progress["next_batch_at"] = json!(next_batch_at);
700 if let Some(st) = self
701 .runs
702 .get_mut(run_id)
703 .and_then(|r| r.steps.get_mut(parent_id))
704 {
705 st.wait = Some(progress.clone());
706 }
707 if changed {
708 if let Some(r) = self.runs.get_mut(run_id) {
709 r.touch();
710 }
711 self.checkpoint(false);
712 }
713 for ix in active.clone() {
715 if !self.parent_running(run_id, parent_id) {
716 return;
717 }
718 let scope_id = format!("{parent_id}[{ix}]");
719 let fresh = self.runs.get(run_id).is_some_and(|r| {
720 !body
721 .steps
722 .keys()
723 .any(|k| r.steps.contains_key(&scoped_id(&scope_id, k)))
724 });
725 if fresh {
726 let _ = items.get(ix); let _ = self.drive_body(run_id, &scope_id, &body, parent);
728 }
729 }
730 if !self.parent_running(run_id, parent_id) {
732 return;
733 }
734 if done >= total && cursor >= total {
735 let out = collect_results(&results, total);
736 self.apply_collect(run_id, &progress["collect"], &out);
737 self.finish_step_pub(run_id, parent_id, StepStatus::Done, Some(out), None, 0);
738 }
739 }
740
741 fn advance_iterate(
742 &mut self,
743 run_id: &str,
744 parent_id: &str,
745 parent: &Step,
746 mut progress: Value,
747 ) {
748 let Some(body) = parent.body.clone() else {
749 return;
750 };
751 let k = progress["iteration"].as_u64().unwrap_or(0) as usize;
752 let max = progress["max"].as_u64().unwrap_or(1) as usize;
753 let mut results: Vec<Value> = progress["results"].as_array().cloned().unwrap_or_default();
754 let scope_id = format!("{parent_id}[{k}]");
755 let started = self.runs.get(run_id).is_some_and(|r| {
756 body.steps
757 .keys()
758 .any(|s| r.steps.contains_key(&scoped_id(&scope_id, s)))
759 });
760 if !started {
761 if let Some(w) = parent.field_str("while") {
763 let scope = Scope {
764 parent: scope_id.clone(),
765 parent_step: Some(parent.clone()),
766 siblings: body.steps.clone(),
767 iteration: Some(k),
768 ..Default::default()
769 };
770 let mut data = self.scoped_data(run_id, &scope);
771 data.insert("results".into(), Value::Array(results.clone()));
772 data.insert(
773 "last".into(),
774 results.last().cloned().unwrap_or(Value::Null),
775 );
776 let vars: Vec<(&str, &Value)> = data.iter().map(|(k, v)| (k.as_str(), v)).collect();
777 match crate::cel::eval_bool(w.trim().trim_start_matches("CEL:").trim(), &vars) {
778 Ok(true) => {}
779 Ok(false) => {
780 let out = iterate_output(&progress, &results);
781 self.apply_collect(run_id, &progress["collect"], &out);
782 self.finish_step_pub(
783 run_id,
784 parent_id,
785 StepStatus::Done,
786 Some(out),
787 None,
788 0,
789 );
790 return;
791 }
792 Err(e) => {
793 self.finish_step_pub(
794 run_id,
795 parent_id,
796 StepStatus::Failed,
797 None,
798 Some(format!("iterate.while: {e}")),
799 0,
800 );
801 return;
802 }
803 }
804 }
805 if k >= max {
806 let out = iterate_output(&progress, &results);
807 self.apply_collect(run_id, &progress["collect"], &out);
808 self.finish_step_pub(run_id, parent_id, StepStatus::Done, Some(out), None, 0);
809 return;
810 }
811 }
812 match self.drive_body(run_id, &scope_id, &body, parent) {
813 BodyState::Waiting => {}
814 BodyState::Failed(e) => {
815 self.finish_step_pub(
816 run_id,
817 parent_id,
818 StepStatus::Failed,
819 Some(iterate_output(&progress, &results)),
820 Some(format!("iteration {k} failed: {e}")),
821 0,
822 );
823 }
824 BodyState::Done => {
825 let out = self.body_result(run_id, &scope_id, &body);
826 results.push(out.clone());
827 let mut stop = k + 1 >= max;
829 if let Some(u) = parent.field_str("until")
830 && !stop
831 {
832 let scope = Scope {
833 parent: scope_id.clone(),
834 parent_step: Some(parent.clone()),
835 siblings: body.steps.clone(),
836 iteration: Some(k),
837 ..Default::default()
838 };
839 let mut data = self.scoped_data(run_id, &scope);
840 data.insert("result".into(), out.clone());
841 data.insert("results".into(), Value::Array(results.clone()));
842 let vars: Vec<(&str, &Value)> =
843 data.iter().map(|(k, v)| (k.as_str(), v)).collect();
844 match crate::cel::eval_bool(u.trim().trim_start_matches("CEL:").trim(), &vars) {
845 Ok(b) => stop = b,
846 Err(e) => {
847 self.finish_step_pub(
848 run_id,
849 parent_id,
850 StepStatus::Failed,
851 None,
852 Some(format!("iterate.until: {e}")),
853 0,
854 );
855 return;
856 }
857 }
858 }
859 progress["results"] = Value::Array(results.clone());
860 progress["iteration"] = json!(k + 1);
861 if let Some(st) = self
862 .runs
863 .get_mut(run_id)
864 .and_then(|r| r.steps.get_mut(parent_id))
865 {
866 st.wait = Some(progress.clone());
867 }
868 if let Some(r) = self.runs.get_mut(run_id) {
869 r.touch();
870 }
871 self.checkpoint(false);
872 if stop {
873 let out = iterate_output(&progress, &results);
874 self.apply_collect(run_id, &progress["collect"], &out);
875 self.finish_step_pub(run_id, parent_id, StepStatus::Done, Some(out), None, 0);
876 } else {
877 self.nested_advance(run_id, parent_id);
879 }
880 }
881 }
882 }
883
884 fn advance_parallel(
885 &mut self,
886 run_id: &str,
887 parent_id: &str,
888 parent: &Step,
889 mut progress: Value,
890 race: bool,
891 ) {
892 let mut results = progress["results"].as_object().cloned().unwrap_or_default();
893 let mut errors = progress["errors"].as_object().cloned().unwrap_or_default();
894 let branches: Vec<String> = parent.branches.keys().cloned().collect();
895 let mut changed = false;
896 for b in &branches {
897 if results.contains_key(b) || errors.contains_key(b) {
898 continue;
899 }
900 if !self.parent_running(run_id, parent_id) {
901 return;
902 }
903 let Some(body) = parent.branches.get(b).cloned() else {
904 continue;
905 };
906 let scope_id = format!("{parent_id}{{{b}}}");
907 match self.drive_body(run_id, &scope_id, &body, parent) {
908 BodyState::Waiting => {}
909 BodyState::Done => {
910 results.insert(b.clone(), self.body_result(run_id, &scope_id, &body));
911 changed = true;
912 if race {
913 for other in &branches {
915 if other != b {
916 self.cancel_scoped_children(
917 run_id,
918 &format!("{parent_id}{{{other}}}"),
919 );
920 }
921 }
922 let winner = results.get(b).cloned().unwrap_or(Value::Null);
923 self.finish_step_pub(
924 run_id,
925 parent_id,
926 StepStatus::Done,
927 Some(json!({"winner": b, "output": winner})),
928 None,
929 0,
930 );
931 return;
932 }
933 }
934 BodyState::Failed(e) => {
935 errors.insert(b.clone(), Value::String(e.clone()));
936 changed = true;
937 if !race && parent.on_error == OnError::Fail {
938 self.cancel_scoped_children(run_id, parent_id);
939 self.finish_step_pub(
940 run_id,
941 parent_id,
942 StepStatus::Failed,
943 Some(Value::Object(results.clone())),
944 Some(format!("branch {b} failed: {e}")),
945 0,
946 );
947 return;
948 }
949 }
950 }
951 }
952 progress["results"] = Value::Object(results.clone());
953 progress["errors"] = Value::Object(errors.clone());
954 if let Some(st) = self
955 .runs
956 .get_mut(run_id)
957 .and_then(|r| r.steps.get_mut(parent_id))
958 {
959 st.wait = Some(progress.clone());
960 }
961 if changed {
962 if let Some(r) = self.runs.get_mut(run_id) {
963 r.touch();
964 }
965 self.checkpoint(false);
966 }
967 if race
969 && let Some(t) = progress["timeout_ms"].as_u64()
970 && now_ms() >= progress["started_ms"].as_u64().unwrap_or(0) + t
971 {
972 self.cancel_scoped_children(run_id, parent_id);
973 self.finish_step_pub(
974 run_id,
975 parent_id,
976 StepStatus::Timeout,
977 None,
978 Some("race: no branch finished in time".into()),
979 0,
980 );
981 return;
982 }
983 if results.len() + errors.len() >= branches.len() {
984 if race {
985 self.finish_step_pub(
987 run_id,
988 parent_id,
989 StepStatus::Failed,
990 Some(Value::Object(results)),
991 Some(format!(
992 "race: every branch failed: {}",
993 Value::Object(errors)
994 )),
995 0,
996 );
997 } else {
998 let min = progress["min_success"].as_u64().map(|m| m as usize);
999 let ok = min.is_none_or(|m| results.len() >= m)
1000 && (errors.is_empty() || parent.on_error != OnError::Fail);
1001 let mut out = Value::Object(results.clone());
1002 if !errors.is_empty() {
1003 out["_errors"] = Value::Object(errors.clone());
1004 }
1005 if ok {
1006 self.finish_step_pub(run_id, parent_id, StepStatus::Done, Some(out), None, 0);
1007 } else {
1008 self.finish_step_pub(
1009 run_id,
1010 parent_id,
1011 StepStatus::Failed,
1012 Some(out),
1013 Some("parallel: not enough branches succeeded".into()),
1014 0,
1015 );
1016 }
1017 }
1018 }
1019 }
1020
1021 fn advance_subgraph(&mut self, run_id: &str, parent_id: &str, parent: &Step) {
1022 let Some(body) = parent.body.clone() else {
1023 return;
1024 };
1025 match self.drive_body(run_id, parent_id, &body, parent) {
1026 BodyState::Waiting => {}
1027 BodyState::Done => {
1028 let out = self.body_result(run_id, parent_id, &body);
1029 self.finish_step_pub(run_id, parent_id, StepStatus::Done, Some(out), None, 0);
1030 }
1031 BodyState::Failed(e) => self.finish_step_pub(
1032 run_id,
1033 parent_id,
1034 StepStatus::Failed,
1035 None,
1036 Some(format!("subgraph failed: {e}")),
1037 0,
1038 ),
1039 }
1040 }
1041
1042 fn apply_collect(&mut self, run_id: &str, collect: &Value, out: &Value) {
1044 if let Some(into) = collect.get("into").and_then(Value::as_str) {
1045 let mode = collect
1046 .get("mode")
1047 .and_then(Value::as_str)
1048 .unwrap_or("overwrite")
1049 .to_string();
1050 if let Some(r) = self.runs.get_mut(run_id) {
1051 r.write_var(into, out.clone(), &mode);
1052 }
1053 }
1054 }
1055
1056 pub(crate) fn cancel_scoped_children(&mut self, run_id: &str, prefix: &str) {
1061 let nodes: Vec<_> = self
1062 .children
1063 .iter()
1064 .filter(|(_, c)| matches!(&c.kind, super::children::ChildKind::StepTurn { run, step, .. } if run == run_id && (under_scope(step, prefix) || step == prefix)))
1065 .map(|(n, _)| *n)
1066 .collect();
1067 for n in nodes {
1068 self.children.cancel(n, "nested step cancelled");
1069 }
1070 let timers = self.timers.owned_by(|o| {
1071 o["run"].as_str() == Some(run_id)
1072 && o["step"].as_str().is_some_and(|s| under_scope(s, prefix))
1073 });
1074 for t in timers {
1075 let _ = self.timers.disarm(&self.durable, &t);
1076 }
1077 self.pending.retain(|p| !matches!(&p.target, super::reactor::Target::Step(r, s) if r == run_id && under_scope(s, prefix)));
1080 if let Some(run) = self.runs.get_mut(run_id) {
1081 for (id, st) in run.steps.iter_mut() {
1082 if under_scope(id, prefix) && !st.status.is_terminal() {
1083 st.status = StepStatus::Cancelled;
1084 st.finished = Some(now_ms());
1085 }
1086 }
1087 run.touch();
1088 }
1089 }
1090
1091 pub(crate) fn on_scoped_step_done(&mut self, run_id: &str, scoped: &str) {
1095 if let Some(parent) = parent_of(scoped).map(str::to_string) {
1096 let parent_step_id = strip_scope_suffix(&parent);
1098 self.nested_advance(run_id, &parent_step_id);
1099 }
1100 }
1101}
1102
1103pub fn strip_scope_suffix(scoped_parent: &str) -> String {
1105 let (head, last) = match scoped_parent.rsplit_once('.') {
1106 Some((h, l)) => (Some(h), l),
1107 None => (None, scoped_parent),
1108 };
1109 let base = last.split(['[', '{']).next().unwrap_or(last);
1110 match head {
1111 Some(h) => format!("{h}.{base}"),
1112 None => base.to_string(),
1113 }
1114}
1115
1116fn under_scope(id: &str, prefix: &str) -> bool {
1125 id.strip_prefix(prefix)
1126 .is_some_and(|rest| rest.starts_with(['.', '[', '{']))
1127}
1128
1129fn seg_label(seg: &Segment) -> String {
1130 match (&seg.index, &seg.branch) {
1131 (Some(i), _) => format!("{}[{i}]", seg.name),
1132 (_, Some(b)) => format!("{}{{{b}}}", seg.name),
1133 _ => seg.name.clone(),
1134 }
1135}
1136
1137fn element_item(progress: &Value, ix: usize) -> Option<Value> {
1142 progress["items"]
1143 .as_array()
1144 .and_then(|a| a.get(ix).cloned())
1145}
1146
1147fn batch_of(progress: &Value, ix: usize) -> Option<usize> {
1148 let size = progress["size"].as_u64()? as usize;
1149 Some(ix / size.max(1))
1150}
1151
1152fn collect_results(results: &Map<String, Value>, total: usize) -> Value {
1153 Value::Array(
1154 (0..total)
1155 .map(|i| results.get(&i.to_string()).cloned().unwrap_or(Value::Null))
1156 .collect(),
1157 )
1158}
1159
1160fn iterate_output(progress: &Value, results: &[Value]) -> Value {
1161 if progress["collect"].is_null() && !results.is_empty() {
1162 results.last().cloned().unwrap_or(Value::Null)
1163 } else {
1164 Value::Array(results.to_vec())
1165 }
1166}
1167
1168fn unreachable_wf() -> std::sync::Arc<Workflow> {
1169 std::sync::Arc::new(Workflow {
1172 key: None,
1175 tool: None,
1176 state: Default::default(),
1177 name: String::new(),
1178 version: 3,
1179 priority: Default::default(),
1180 unload: Default::default(),
1181 durable: None,
1182 description: None,
1183 armed: false,
1184 inputs_schema: None,
1185 concurrency: Default::default(),
1186 limits: Default::default(),
1187 outputs_schema: None,
1188 steps: BTreeMap::new(),
1189 hash: String::new(),
1190 definition: Value::Null,
1191 })
1192}
1193
1194pub enum BodyState {
1196 Waiting,
1197 Done,
1198 Failed(String),
1199}
1200
1201pub(crate) trait StatusLabel {
1202 fn as_label(&self) -> &'static str;
1203}
1204impl StatusLabel for StepStatus {
1205 fn as_label(&self) -> &'static str {
1206 match self {
1207 StepStatus::Pending => "pending",
1208 StepStatus::Running => "running",
1209 StepStatus::Done => "done",
1210 StepStatus::Failed => "failed",
1211 StepStatus::Skipped => "skipped",
1212 StepStatus::Pruned => "pruned",
1213 StepStatus::Cancelled => "cancelled",
1214 StepStatus::Timeout => "timeout",
1215 StepStatus::Suspended => "suspended",
1216 }
1217 }
1218}
1219
1220#[cfg(test)]
1221mod tests {
1222 use super::*;
1223
1224 #[test]
1225 fn scoped_ids_parse_and_strip() {
1226 let segs = parse_scoped("each[3].classify");
1227 assert_eq!(segs.len(), 2);
1228 assert_eq!(
1229 segs[0],
1230 Segment {
1231 name: "each".into(),
1232 index: Some(3),
1233 branch: None
1234 }
1235 );
1236 assert_eq!(
1237 segs[1],
1238 Segment {
1239 name: "classify".into(),
1240 index: None,
1241 branch: None
1242 }
1243 );
1244 let segs = parse_scoped("par{a}.inner[2].leaf");
1245 assert_eq!(segs[0].branch.as_deref(), Some("a"));
1246 assert_eq!(segs[1].index, Some(2));
1247 assert_eq!(scoped_id("each[3]", "classify"), "each[3].classify");
1248 assert_eq!(parent_of("each[3].classify"), Some("each[3]"));
1249 assert_eq!(strip_scope_suffix("each[3]"), "each");
1250 assert_eq!(strip_scope_suffix("par{a}"), "par");
1251 assert_eq!(strip_scope_suffix("x[1].y[2]"), "x[1].y");
1252 assert!(is_scoped("a.b") && !is_scoped("a"));
1253 assert!(under_scope("each[0].work", "each"));
1257 assert!(under_scope("par{a}.work", "par"));
1258 assert!(under_scope("sub.work", "sub"));
1259 assert!(under_scope("each[0].inner{b}.leaf", "each[0].inner"));
1260 assert!(!under_scope("eachother.work", "each"));
1261 assert!(!under_scope("each", "each"));
1262 assert_eq!(
1263 collect_results(&[("0".to_string(), json!(1))].into_iter().collect(), 2),
1264 json!([1, null])
1265 );
1266 }
1267}