1use super::model::{OnError, Step, Workflow};
12use super::template::{self, Data};
13use crate::state::now_ms;
14use serde::{Deserialize, Serialize};
15use serde_json::{Map, Value, json};
16use std::collections::BTreeMap;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
19#[serde(rename_all = "snake_case")]
20pub enum StepStatus {
21 #[default]
22 Pending,
23 Running,
24 Done,
25 Failed,
26 Skipped,
27 Cancelled,
28 Timeout,
29 Suspended,
31 Pruned,
41}
42
43impl StepStatus {
44 pub fn is_terminal(self) -> bool {
45 matches!(
46 self,
47 StepStatus::Done
48 | StepStatus::Failed
49 | StepStatus::Skipped
50 | StepStatus::Pruned
51 | StepStatus::Cancelled
52 | StepStatus::Timeout
53 )
54 }
55 pub fn is_satisfied(self) -> bool {
57 matches!(self, StepStatus::Done | StepStatus::Skipped)
58 }
59}
60
61#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
63pub struct StepState {
64 #[serde(default)]
65 pub status: StepStatus,
66 #[serde(default)]
67 pub attempt: u32,
68 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub started: Option<u64>,
70 #[serde(default, skip_serializing_if = "Option::is_none")]
71 pub finished: Option<u64>,
72 #[serde(default, skip_serializing_if = "Option::is_none")]
73 pub output: Option<Value>,
74 #[serde(default, skip_serializing_if = "Option::is_none")]
75 pub error: Option<String>,
76 #[serde(default, skip_serializing_if = "Option::is_none")]
78 pub wait: Option<Value>,
79 #[serde(default, skip_serializing_if = "Option::is_none")]
81 pub worker: Option<String>,
82 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
85 pub forced: bool,
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
89#[serde(rename_all = "snake_case")]
90pub enum RunStatus {
91 #[default]
92 Pending,
93 Running,
94 Suspended,
96 Paused,
97 Completed,
98 Failed,
99 Refused,
100 Cancelled,
101 Stalled,
102}
103
104impl RunStatus {
105 pub fn is_terminal(self) -> bool {
106 matches!(
107 self,
108 RunStatus::Completed
109 | RunStatus::Failed
110 | RunStatus::Refused
111 | RunStatus::Cancelled
112 | RunStatus::Stalled
113 )
114 }
115 pub fn as_str(self) -> &'static str {
116 match self {
117 RunStatus::Pending => "pending",
118 RunStatus::Running => "running",
119 RunStatus::Suspended => "suspended",
120 RunStatus::Paused => "paused",
121 RunStatus::Completed => "completed",
122 RunStatus::Failed => "failed",
123 RunStatus::Refused => "refused",
124 RunStatus::Cancelled => "cancelled",
125 RunStatus::Stalled => "stalled",
126 }
127 }
128}
129
130#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
132pub struct Start {
133 pub node: String,
134 #[serde(default)]
135 pub payload: Value,
136 #[serde(default)]
137 pub ts: u64,
138}
139
140#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
142pub struct RunState {
143 #[serde(default, skip_serializing_if = "Option::is_none")]
147 pub break_before: Option<String>,
148 pub id: String,
149 pub workflow: String,
150 pub workflow_hash: String,
151 #[serde(default)]
152 pub inputs: Value,
153 #[serde(default)]
154 pub status: RunStatus,
155 #[serde(default)]
156 pub start: Start,
157 #[serde(default)]
158 pub steps: BTreeMap<String, StepState>,
159 #[serde(default)]
160 pub vars: Map<String, Value>,
161 #[serde(default)]
162 pub tokens: u64,
163 #[serde(default)]
164 pub steps_run: u32,
165 #[serde(default, skip_serializing_if = "Option::is_none")]
166 pub output: Option<Value>,
167 #[serde(default, skip_serializing_if = "Option::is_none")]
168 pub error: Option<String>,
169 #[serde(default, skip_serializing_if = "Option::is_none")]
170 pub task: Option<String>,
171 #[serde(default, skip_serializing_if = "Option::is_none")]
172 pub principal: Option<String>,
173 #[serde(default, skip_serializing_if = "Option::is_none")]
174 pub conversation: Option<String>,
175 #[serde(default, skip_serializing_if = "Vec::is_empty")]
176 pub children: Vec<String>,
177 #[serde(default, skip_serializing_if = "Option::is_none")]
178 pub parent: Option<Value>,
179 #[serde(default)]
180 pub attempt: u32,
181 #[serde(default)]
182 pub created: u64,
183 #[serde(default)]
184 pub updated: u64,
185 #[serde(default, skip_serializing_if = "Option::is_none")]
186 pub finished: Option<u64>,
187 #[serde(default, skip_serializing_if = "Option::is_none")]
188 pub deadline_ms: Option<u64>,
189 #[serde(skip)]
190 pub dirty: bool,
191}
192
193impl RunState {
194 pub fn new(id: &str, wf: &Workflow, start: Start, inputs: Value) -> RunState {
195 let now = now_ms();
196 let mut steps = BTreeMap::new();
197 for s in wf.steps.keys() {
198 steps.insert(s.clone(), StepState::default());
199 }
200 for s in wf.start_steps() {
203 let st = steps.get_mut(&s.id).expect("present");
204 if s.id == start.node {
205 st.status = StepStatus::Done;
206 st.output = Some(start.payload.clone());
207 st.started = Some(now);
208 st.finished = Some(now);
209 st.attempt = 1;
210 } else {
211 st.status = StepStatus::Skipped;
212 }
213 }
214 RunState {
215 break_before: None,
216 id: id.to_string(),
217 workflow: wf.name.clone(),
218 workflow_hash: wf.hash.clone(),
219 inputs,
220 status: RunStatus::Running,
221 start,
222 steps,
223 vars: Map::new(),
224 tokens: 0,
225 steps_run: 0,
226 output: None,
227 error: None,
228 task: None,
229 principal: None,
230 conversation: None,
231 children: Vec::new(),
232 parent: None,
233 attempt: 1,
234 created: now,
235 updated: now,
236 finished: None,
237 deadline_ms: wf.limits.deadline_ms.map(|d| now + d),
238 dirty: true,
239 }
240 }
241
242 pub fn touch(&mut self) {
243 self.updated = now_ms();
244 self.dirty = true;
245 }
246
247 pub fn step(&self, id: &str) -> Option<&StepState> {
248 self.steps.get(id)
249 }
250
251 pub fn data(&self, env: Value, memory: Value) -> Data {
254 let mut d = Data::new();
255 d.insert("inputs".into(), self.inputs.clone());
256 d.insert(
257 "run".into(),
258 json!({"id": self.id, "workflow": self.workflow, "start": self.start, "principal": self.principal, "task": self.task, "attempt": self.attempt, "status": self.status}),
259 );
260 d.insert(
261 "steps".into(),
262 Value::Object(
263 self.steps
264 .iter()
265 .map(|(k, s)| (k.clone(), json!({"status": s.status, "output": s.output, "error": s.error, "attempt": s.attempt})))
266 .collect(),
267 ),
268 );
269 d.insert("vars".into(), Value::Object(self.vars.clone()));
270 d.insert("env".into(), env);
271 d.insert("memory".into(), memory);
272 d
273 }
274
275 pub fn begin_step(&mut self, id: &str) -> u32 {
277 let attempt = {
278 let st = self.steps.entry(id.to_string()).or_default();
279 st.status = StepStatus::Running;
280 st.attempt += 1;
281 st.started = Some(now_ms());
282 st.finished = None;
283 st.error = None;
284 st.wait = None;
285 st.forced = false;
286 st.attempt
287 };
288 if !self.status.is_terminal() {
289 self.status = RunStatus::Running;
290 }
291 self.touch();
292 attempt
293 }
294
295 pub fn end_step(
297 &mut self,
298 id: &str,
299 status: StepStatus,
300 output: Option<Value>,
301 error: Option<String>,
302 ) {
303 let st = self.steps.entry(id.to_string()).or_default();
304 st.status = status;
305 st.finished = Some(now_ms());
306 st.output = output;
307 st.error = error;
308 st.wait = None;
309 st.worker = None;
310 self.steps_run += 1;
311 self.touch();
312 }
313
314 pub fn suspend_step(&mut self, id: &str, wait: Value) {
316 let st = self.steps.entry(id.to_string()).or_default();
317 st.status = StepStatus::Suspended;
318 st.wait = Some(wait);
319 self.touch();
320 }
321
322 pub fn finish(&mut self, status: RunStatus, output: Option<Value>, error: Option<String>) {
324 self.status = status;
325 self.output = output;
326 self.error = error;
327 self.finished = Some(now_ms());
328 for st in self.steps.values_mut() {
330 if !st.status.is_terminal() {
331 st.status = StepStatus::Cancelled;
332 st.finished = Some(now_ms());
333 }
334 }
335 self.touch();
336 }
337
338 pub fn write_var(&mut self, key: &str, value: Value, mode: &str) {
340 let cur = self.vars.remove(key);
341 let next = match (mode, cur) {
342 ("append", Some(Value::Array(mut a))) => {
343 match value {
344 Value::Array(more) => a.extend(more),
345 other => a.push(other),
346 }
347 Value::Array(a)
348 }
349 ("append", Some(other)) => json!([other, value]),
350 ("append", None) => match value {
351 Value::Array(a) => Value::Array(a),
352 other => json!([other]),
353 },
354 ("merge", Some(Value::Object(mut o))) => {
355 if let Value::Object(more) = value {
356 for (k, v) in more {
357 o.insert(k, v);
358 }
359 }
360 Value::Object(o)
361 }
362 ("union", Some(Value::Array(mut a))) => {
363 if let Value::Array(more) = value {
364 for v in more {
365 if !a.contains(&v) {
366 a.push(v);
367 }
368 }
369 } else if !a.contains(&value) {
370 a.push(value);
371 }
372 Value::Array(a)
373 }
374 (_, _) => value,
375 };
376 self.vars.insert(key.to_string(), next);
377 self.touch();
378 }
379
380 pub fn progress(&self) -> Value {
382 let mut counts: BTreeMap<&str, u32> = BTreeMap::new();
383 for s in self.steps.values() {
384 *counts
385 .entry(match s.status {
386 StepStatus::Pending => "pending",
387 StepStatus::Running => "running",
388 StepStatus::Done => "done",
389 StepStatus::Failed => "failed",
390 StepStatus::Skipped => "skipped",
391 StepStatus::Pruned => "pruned",
392 StepStatus::Cancelled => "cancelled",
393 StepStatus::Timeout => "timeout",
394 StepStatus::Suspended => "suspended",
395 })
396 .or_default() += 1;
397 }
398 json!(counts)
399 }
400
401 pub fn summary(&self) -> Value {
402 json!({
403 "id": self.id, "workflow": self.workflow, "status": self.status, "start": self.start.node,
404 "steps": self.progress(), "tokens": self.tokens, "created": self.created, "updated": self.updated,
405 "finished": self.finished, "output": self.output, "error": self.error, "task": self.task, "principal": self.principal,
406 })
407 }
408}
409
410#[derive(Debug, Clone, PartialEq)]
412pub enum Next {
413 Ready(Vec<String>),
415 Waiting,
417 Stalled,
419 Terminal,
421}
422
423pub fn schedule(wf: &Workflow, run: &mut RunState, data: &Data) -> Result<Next, String> {
426 if run.status.is_terminal() {
427 return Ok(Next::Terminal);
428 }
429 let mut ready = Vec::new();
430 let mut in_flight = false;
431 let mut changed = true;
432 while changed {
434 changed = false;
435 for id in wf.topo_order() {
436 let step = &wf.steps[&id];
437 let st = run.steps.get(&id).cloned().unwrap_or_default();
438 match st.status {
439 StepStatus::Running => {
440 in_flight = true;
441 continue;
442 }
443 StepStatus::Suspended => {
444 in_flight = true;
445 continue;
446 }
447 s if s.is_terminal() => continue,
448 _ => {}
449 }
450 if ready.contains(&id) {
451 continue;
452 }
453 if st.forced {
454 ready.push(id.clone());
455 continue;
456 }
457 let pruned_deps = step
463 .depends_on
464 .iter()
465 .filter(|d| {
466 run.steps
467 .get(*d)
468 .is_some_and(|s| s.status == StepStatus::Pruned)
469 })
470 .count();
471 if !step.depends_on.is_empty() && pruned_deps == step.depends_on.len() {
472 run.end_step(&id, StepStatus::Pruned, None, None);
473 changed = true;
474 continue;
475 }
476 let deps_ok = step.depends_on.iter().all(|d| {
478 run.steps
479 .get(d)
480 .is_some_and(|s| s.status.is_satisfied() || s.status == StepStatus::Pruned)
481 });
482 let deps_failed = step.depends_on.iter().any(|d| {
483 run.steps.get(d).is_some_and(|s| {
484 matches!(
485 s.status,
486 StepStatus::Failed | StepStatus::Cancelled | StepStatus::Timeout
487 )
488 })
489 });
490 if deps_failed {
491 continue;
494 }
495 if !deps_ok {
496 continue;
497 }
498 if let Some(w) = &step.when {
499 let expr = w.trim().trim_start_matches("CEL:").trim();
500 let vars: Vec<(&str, &Value)> = data.iter().map(|(k, v)| (k.as_str(), v)).collect();
501 match crate::cel::eval_bool(expr, &vars) {
502 Ok(true) => {}
503 Ok(false) => {
504 run.end_step(&id, StepStatus::Pruned, None, None);
506 changed = true;
507 continue;
508 }
509 Err(e) => return Err(format!("step {id:?}: when: {e}")),
510 }
511 }
512 ready.push(id.clone());
513 }
514 }
515 if !ready.is_empty() {
516 return Ok(Next::Ready(ready));
517 }
518 if in_flight {
519 return Ok(Next::Waiting);
520 }
521 Ok(Next::Stalled)
522}
523
524pub fn route_failure(
527 wf: &Workflow,
528 run: &mut RunState,
529 step: &Step,
530 error: &str,
531) -> Result<Vec<String>, String> {
532 match &step.on_error {
533 OnError::Fail => Err(format!("step {:?} failed: {error}", step.id)),
534 OnError::Continue => {
535 let st = run.steps.entry(step.id.clone()).or_default();
538 st.status = StepStatus::Done;
539 st.error = Some(error.to_string());
540 if st.output.is_none() {
541 st.output = Some(json!({"error": error}));
542 }
543 run.touch();
544 Ok(Vec::new())
545 }
546 OnError::Goto(target) => {
547 if !wf.steps.contains_key(target) {
548 return Err(format!(
549 "step {:?}: on_error goto {target:?} does not exist",
550 step.id
551 ));
552 }
553 let st = run.steps.entry(target.clone()).or_default();
555 st.status = StepStatus::Pending;
556 st.forced = true;
557 run.touch();
558 Ok(vec![target.clone()])
559 }
560 }
561}
562
563pub fn deadline_passed(run: &RunState) -> bool {
565 run.deadline_ms.is_some_and(|d| now_ms() >= d)
566}
567
568pub fn env_view(
570 instance: &str,
571 run_id: &str,
572 instruction: Option<&str>,
573 prompt: Option<&str>,
574) -> Value {
575 json!({
576 "instance": instance,
577 "run": run_id,
578 "ts": now_ms(),
579 "instruction": instruction,
580 "prompt": prompt,
582 })
583}
584
585pub fn render_spec(step: &Step, data: &Data) -> Result<Map<String, Value>, String> {
587 let mut out = Map::new();
588 for (k, v) in &step.spec {
589 if super::model::is_raw_field(&step.kind, k) {
590 out.insert(k.clone(), v.clone());
591 continue;
592 }
593 out.insert(
594 k.clone(),
595 template::render(v, data).map_err(|e| format!("step {:?}: {k}: {e}", step.id))?,
596 );
597 }
598 Ok(out)
599}
600
601#[cfg(all(test, feature = "cel"))]
604mod tests {
605 use super::*;
606 use crate::engine::model::parse_workflow;
607
608 fn start_at(node: &str) -> Start {
609 Start {
610 node: node.into(),
611 payload: json!({}),
612 ts: 0,
613 }
614 }
615
616 #[test]
621 fn an_untaken_branch_prunes_its_tail_but_not_a_live_join() {
622 let w = parse_workflow(&json!({
623 "name": "w", "steps": {
624 "go": {"kind": "once"},
625 "la": {"kind": "noop", "depends_on": ["go"]},
626 "ra": {"kind": "noop", "depends_on": ["go"]},
627 "la2": {"kind": "noop", "depends_on": ["la"]},
628 "ra2": {"kind": "noop", "depends_on": ["ra"]},
629 "fin": {"kind": "finish", "depends_on": ["la2", "ra2"], "status": "completed"}
630 }
631 }))
632 .unwrap();
633 let mut run = RunState::new("r", &w, start_at("go"), json!({}));
634 run.end_step("ra", StepStatus::Pruned, None, None);
635 run.end_step("la", StepStatus::Done, None, None);
636 let data = run.data(env_view("i", "r", None, None), json!({}));
637 let _ = schedule(&w, &mut run, &data).unwrap();
638 assert_eq!(
639 run.steps["ra2"].status,
640 StepStatus::Pruned,
641 "the dead branch's tail must be pruned, not run"
642 );
643
644 run.end_step("la2", StepStatus::Done, None, None);
645 let data = run.data(env_view("i", "r", None, None), json!({}));
646 match schedule(&w, &mut run, &data).unwrap() {
647 Next::Ready(r) => assert!(
648 r.iter().any(|s| s == "fin"),
649 "a join with one pruned and one live parent must run, got {r:?}"
650 ),
651 other => panic!("expected fin ready, got {other:?}"),
652 }
653 }
654
655 #[test]
659 fn sibling_start_nodes_still_satisfy_their_dependents() {
660 let w = parse_workflow(&json!({
661 "name": "w", "steps": {
662 "a": {"kind": "once"},
663 "b": {"kind": "manual"},
664 "work": {"kind": "noop", "depends_on": ["a", "b"]},
665 "fin": {"kind": "finish", "depends_on": ["work"], "status": "completed"}
666 }
667 }))
668 .unwrap();
669 let mut run = RunState::new("r", &w, start_at("a"), json!({}));
670 assert_eq!(run.steps["b"].status, StepStatus::Skipped);
671 let data = run.data(env_view("i", "r", None, None), json!({}));
672 match schedule(&w, &mut run, &data).unwrap() {
673 Next::Ready(r) => assert!(
674 r.iter().any(|s| s == "work"),
675 "a step below several start nodes must run when one fired, got {r:?}"
676 ),
677 other => panic!("expected work ready, got {other:?}"),
678 }
679 }
680
681 fn wf() -> Workflow {
682 parse_workflow(&json!({
683 "name": "w", "steps": {
684 "s": {"kind": "once"},
685 "a": {"kind": "noop", "depends_on": ["s"]},
686 "b": {"kind": "noop", "depends_on": ["s"], "when": "CEL: inputs.go == true"},
687 "c": {"kind": "noop", "depends_on": ["a", "b"], "on_error": "goto:fix"},
688 "fix": {"kind": "noop", "depends_on": ["c"]},
689 "f": {"kind": "finish", "depends_on": ["c"], "status": "completed", "output": "{{vars.x | none}}"}
690 }
691 }))
692 .unwrap()
693 }
694
695 #[cfg(feature = "cel")]
696 #[test]
697 fn scheduling_guards_failures_and_terminal_states() {
698 let w = wf();
699 let mut run = RunState::new(
700 "r1",
701 &w,
702 Start {
703 node: "s".into(),
704 payload: json!({"p": 1}),
705 ts: 0,
706 },
707 json!({"go": false}),
708 );
709 assert_eq!(run.steps["s"].status, StepStatus::Done);
710 assert_eq!(run.steps["s"].output, Some(json!({"p": 1})));
711 let data = run.data(env_view("i", "r1", None, None), json!({}));
712 assert_eq!(
714 schedule(&w, &mut run, &data).unwrap(),
715 Next::Ready(vec!["a".to_string()])
716 );
717 assert_eq!(run.steps["b"].status, StepStatus::Pruned);
721 run.begin_step("a");
722 let data = run.data(env_view("i", "r1", None, None), json!({}));
723 assert_eq!(schedule(&w, &mut run, &data).unwrap(), Next::Waiting);
724 run.end_step("a", StepStatus::Done, Some(json!("A")), None);
725 let data = run.data(env_view("i", "r1", None, None), json!({}));
726 assert_eq!(
727 schedule(&w, &mut run, &data).unwrap(),
728 Next::Ready(vec!["c".to_string()])
729 );
730 run.begin_step("c");
732 run.end_step("c", StepStatus::Failed, None, Some("boom".into()));
733 let next = route_failure(&w, &mut run, w.step("c").unwrap(), "boom").unwrap();
734 assert_eq!(next, vec!["fix".to_string()]);
735 run.begin_step("fix");
736 run.end_step("fix", StepStatus::Done, None, None);
737 let data = run.data(env_view("i", "r1", None, None), json!({}));
739 assert_eq!(schedule(&w, &mut run, &data).unwrap(), Next::Stalled);
740 run.finish(RunStatus::Stalled, None, Some("stalled".into()));
741 assert!(run.status.is_terminal());
742 let data = run.data(env_view("i", "r1", None, None), json!({}));
743 assert_eq!(schedule(&w, &mut run, &data).unwrap(), Next::Terminal);
744 let mut run2 = RunState::new(
746 "r2",
747 &w,
748 Start {
749 node: "s".into(),
750 payload: json!({}),
751 ts: 0,
752 },
753 json!({"go": true}),
754 );
755 let mut c = w.step("c").unwrap().clone();
756 c.on_error = OnError::Continue;
757 run2.begin_step("c");
758 run2.end_step("c", StepStatus::Failed, None, Some("e".into()));
759 assert!(route_failure(&w, &mut run2, &c, "e").unwrap().is_empty());
760 assert_eq!(run2.steps["c"].status, StepStatus::Done);
761 assert_eq!(run2.steps["c"].error.as_deref(), Some("e"));
762 let mut a = w.step("a").unwrap().clone();
764 a.on_error = OnError::Fail;
765 assert!(route_failure(&w, &mut run2, &a, "e").is_err());
766 }
767
768 #[test]
769 fn vars_reducers_and_serialization() {
770 let w = wf();
771 let mut run = RunState::new("r", &w, Start::default(), json!({}));
772 run.write_var("l", json!([1]), "overwrite");
773 run.write_var("l", json!(2), "append");
774 run.write_var("l", json!([3, 4]), "append");
775 assert_eq!(run.vars["l"], json!([1, 2, 3, 4]));
776 run.write_var("l", json!([4, 5]), "union");
777 assert_eq!(run.vars["l"], json!([1, 2, 3, 4, 5]));
778 run.write_var("o", json!({"a": 1}), "overwrite");
779 run.write_var("o", json!({"b": 2}), "merge");
780 assert_eq!(run.vars["o"], json!({"a": 1, "b": 2}));
781 run.write_var("o", json!(7), "overwrite");
782 assert_eq!(run.vars["o"], json!(7));
783 let v = serde_json::to_value(&run).unwrap();
784 let back: RunState = serde_json::from_value(v).unwrap();
785 assert_eq!(back.vars, run.vars);
786 assert!(!back.dirty);
787 assert_eq!(back.summary()["workflow"], json!("w"));
788 let data = run.data(
790 env_view("inst", "r", Some("brief"), None),
791 json!({"k": "v"}),
792 );
793 let mut s = w.step("f").unwrap().clone();
794 s.spec
795 .insert("extra".into(), json!("{{env.instruction}}/{{memory.k}}"));
796 let rendered = render_spec(&s, &data).unwrap();
797 assert_eq!(rendered["output"], json!("none"));
798 assert_eq!(rendered["extra"], json!("brief/v"));
799 assert!(!deadline_passed(&run));
800 }
801}