1use super::events::kinds;
17use super::reactor::Runtime;
18use crate::engine::model::Step;
19use crate::engine::run::RunStatus;
20use crate::state::now_ms;
21use serde_json::{Map, Value, json};
22
23type StartSpec = (String, String, String, Map<String, Value>);
25
26impl Runtime {
27 fn start_key(workflow: &str, node: &str) -> String {
29 format!("{workflow}.{node}")
30 }
31
32 pub(crate) fn start_state_pub(&self, workflow: &str, node: &str) -> Value {
34 self.start_state(workflow, node)
35 }
36 pub(crate) fn set_start_state_pub(&mut self, workflow: &str, node: &str, state: Value) {
37 self.set_start_state(workflow, node, state)
38 }
39
40 fn start_state(&self, workflow: &str, node: &str) -> Value {
41 self.durable
42 .manifest()
43 .starts
44 .get(&Self::start_key(workflow, node))
45 .cloned()
46 .unwrap_or(json!({}))
47 }
48
49 fn set_start_state(&mut self, workflow: &str, node: &str, state: Value) {
51 let key = Self::start_key(workflow, node);
52 self.durable.manifest_update(|m| {
53 m.starts.insert(key, state);
54 });
55 }
56
57 pub(crate) fn arm_long_lived_starts(&mut self) {
61 self.repair_orphaned_timer_waits();
66 let specs: Vec<StartSpec> = self
67 .workflows
68 .values()
69 .filter(|w| w.armed)
70 .flat_map(|w| {
71 w.start_steps()
72 .into_iter()
73 .map(|s| (w.name.clone(), s.id.clone(), s.kind.clone(), s.spec.clone()))
74 .collect::<Vec<_>>()
75 })
76 .collect();
77 for (workflow, node, kind, spec) in specs {
78 match kind.as_str() {
79 "schedule" => self.arm_schedule(&workflow, &node, &spec),
80 "loop" => {
81 let live = self
83 .runs
84 .values()
85 .any(|r| r.workflow == workflow && !r.status.is_terminal());
86 let iteration = self.start_state(&workflow, &node)["iteration"]
87 .as_u64()
88 .unwrap_or(0);
89 let max = spec.get("max_iterations").and_then(Value::as_u64);
90 if !live && max.is_none_or(|m| iteration < m) {
91 let delay = spec
92 .get("delay")
93 .and_then(Value::as_str)
94 .and_then(|d| crate::config::parse_duration(d).ok());
95 match delay {
96 Some(d) if !d.is_zero() => self.set_start_state(&workflow, &node, json!({"iteration": iteration, "next_ms": now_ms() + d.as_millis() as u64})),
97 _ => self.fire_start(&workflow, &node, &spec, json!({"iteration": iteration}), "loop"),
98 }
99 }
100 }
101 "subscribe" => self.arm_subscribe(&workflow, &node, &spec),
102 _ => {}
103 }
104 }
105 }
106
107 fn arm_schedule(&mut self, workflow: &str, node: &str, spec: &Map<String, Value>) {
108 let st = self.start_state(workflow, node);
109 let at_fired = st["at_fired"].as_bool().unwrap_or(false);
110 let next = self.next_schedule_ms(spec, now_ms(), at_fired);
111 if let Some(next) = next {
112 let mut st = st;
113 st["next_ms"] = json!(next);
114 self.set_start_state(workflow, node, st);
115 self.log.info(
116 "start.schedule.armed",
117 json!({"workflow": workflow, "node": node, "next_ms": next}),
118 );
119 } else if at_fired {
120 self.log.info(
123 "start.schedule.done",
124 json!({"workflow": workflow, "node": node, "note": "one-shot `at` already fired"}),
125 );
126 } else {
127 self.log.warn(
128 "start.schedule.invalid",
129 json!({"workflow": workflow, "node": node, "note": "no cron/every"}),
130 );
131 }
132 }
133
134 fn next_schedule_ms(
138 &self,
139 spec: &Map<String, Value>,
140 after_ms: u64,
141 at_fired: bool,
142 ) -> Option<u64> {
143 if let Some(every) = spec
144 .get("every")
145 .and_then(Value::as_str)
146 .and_then(|e| crate::config::parse_duration(e).ok())
147 {
148 return Some(after_ms + every.as_millis() as u64);
149 }
150 if let Some(at) = spec
151 .get("at")
152 .and_then(Value::as_str)
153 .filter(|_| !at_fired)
154 .and_then(|a| crate::config::parse_duration(a).ok())
155 {
156 return Some(now_ms() + at.as_millis() as u64);
159 }
160 #[cfg(feature = "cron")]
161 if let Some(cron) = spec.get("cron").and_then(Value::as_str) {
162 return crate::triggers::timer::CronExpr::parse(cron)
163 .ok()
164 .and_then(|c| c.next_after(after_ms / 1000))
165 .map(|s| s * 1000);
166 }
167 None
168 }
169
170 fn arm_subscribe(&mut self, workflow: &str, node: &str, spec: &Map<String, Value>) {
171 let server = spec.get("server").and_then(Value::as_str).unwrap_or("");
172 let uri = spec.get("uri").and_then(Value::as_str).unwrap_or("");
173 match self.mcp.get(server) {
174 Some(c) => match c.subscribe(uri) {
175 Ok(()) => self.log.info(
176 "start.subscribe.armed",
177 json!({"workflow": workflow, "node": node, "server": server, "uri": uri}),
178 ),
179 Err(e) => self.log.warn(
180 "start.subscribe.fail",
181 json!({"workflow": workflow, "node": node, "err": e.to_string()}),
182 ),
183 },
184 None => self.log.warn(
185 "start.subscribe.no_server",
186 json!({"workflow": workflow, "node": node, "server": server}),
187 ),
188 }
189 }
190
191 pub(crate) fn poll_starts(&mut self) {
194 let now = now_ms();
195 let due: Vec<StartSpec> = self
196 .workflows
197 .values()
198 .filter(|w| w.armed)
199 .flat_map(|w| {
200 w.start_steps()
201 .into_iter()
202 .filter(|s| matches!(s.kind.as_str(), "schedule" | "loop" | "subscribe"))
203 .map(|s| (w.name.clone(), s.id.clone(), s.kind.clone(), s.spec.clone()))
204 .collect::<Vec<_>>()
205 })
206 .collect();
207 for (workflow, node, kind, spec) in due {
208 let st = self.start_state(&workflow, &node);
209 match kind.as_str() {
210 "schedule" => {
211 if let Some(next) = st["next_ms"].as_u64()
212 && now >= next
213 {
214 let at_fired =
223 st["at_fired"].as_bool().unwrap_or(false) || spec.contains_key("at");
224 self.fire_start(
225 &workflow,
226 &node,
227 &spec,
228 json!({"scheduled_for": next}),
229 "schedule",
230 );
231 let following = self.next_schedule_ms(&spec, now, at_fired);
233 let mut st = self.start_state(&workflow, &node);
234 match following {
235 Some(n) => st["next_ms"] = json!(n),
236 None => {
237 st.as_object_mut().map(|o| o.remove("next_ms"));
238 }
239 }
240 if at_fired {
241 st["at_fired"] = json!(true);
242 }
243 st["last_fired"] = json!(now);
244 self.set_start_state(&workflow, &node, st);
245 }
246 }
247 "loop" => {
248 let live = self
249 .runs
250 .values()
251 .any(|r| r.workflow == workflow && !r.status.is_terminal());
252 if !live
253 && let Some(next) = st["next_ms"].as_u64()
254 && now >= next
255 {
256 let mut st2 = self.start_state(&workflow, &node);
259 st2.as_object_mut().map(|o| o.remove("next_ms"));
260 self.set_start_state(&workflow, &node, st2);
261 let iteration = st["iteration"].as_u64().unwrap_or(0);
262 let max = spec.get("max_iterations").and_then(Value::as_u64);
263 if max.is_none_or(|m| iteration < m) {
264 self.fire_start(
265 &workflow,
266 &node,
267 &spec,
268 json!({"iteration": iteration}),
269 "loop",
270 );
271 }
272 }
273 }
274 "subscribe" => {
275 if let Some(fire_at) = st["debounce_until"].as_u64()
277 && now >= fire_at
278 {
279 let mut payload = st["pending_payload"].clone();
280 if payload.get("window").is_some() {
284 payload["window"] = st["window"].clone();
285 }
286 let mut st2 = self.start_state(&workflow, &node);
287 st2.as_object_mut().map(|o| {
288 o.remove("debounce_until");
289 o.remove("pending_payload")
290 });
291 self.set_start_state(&workflow, &node, st2);
292 self.fire_start(&workflow, &node, &spec, payload, "subscribe");
293 }
294 }
295 _ => {}
296 }
297 }
298 }
299
300 pub(crate) fn fire_start(
304 &mut self,
305 workflow: &str,
306 node: &str,
307 spec: &Map<String, Value>,
308 payload: Value,
309 kind: &str,
310 ) {
311 self.fire_start_run(workflow, node, spec, payload, kind, None);
312 }
313
314 pub(crate) fn fire_start_run(
317 &mut self,
318 workflow: &str,
319 node: &str,
320 spec: &Map<String, Value>,
321 payload: Value,
322 kind: &str,
323 run_id: Option<&str>,
324 ) {
325 let low = self
331 .workflows
332 .get(workflow)
333 .is_some_and(|w| w.priority == crate::engine::model::Priority::Low);
334 if let Some(cause) = self.pressure.refusal(low) {
335 self.log.warn(
336 "start.shed",
337 json!({"workflow": workflow, "node": node, "kind": kind, "cause": cause}),
338 );
339 return;
340 }
341 let inputs = match spec.get("inputs") {
342 Some(mapping) => {
343 let mut data = crate::engine::template::Data::new();
344 data.insert("payload".into(), payload.clone());
345 data.insert(
346 "env".into(),
347 json!({"instance": self.instance, "ts": now_ms()}),
348 );
349 match crate::engine::template::render(mapping, &data) {
350 Ok(v) => v,
351 Err(e) => {
352 self.log.warn(
358 "start.inputs.invalid",
359 json!({"workflow": workflow, "node": node, "kind": kind, "err": e}),
360 );
361 return;
362 }
363 }
364 }
365 None => json!({}),
366 };
367 self.log.info(
368 "start.fired",
369 json!({"workflow": workflow, "node": node, "kind": kind}),
370 );
371 let mut st = self.start_state(workflow, node);
372 st["last_fired"] = json!(now_ms());
373 if kind == "loop" {
374 st["iteration"] = json!(st["iteration"].as_u64().unwrap_or(0) + 1);
375 }
376 self.set_start_state(workflow, node, st);
377 let mut ev =
378 json!({"workflow": workflow, "node": node, "payload": payload, "inputs": inputs});
379 if let Some(tpl) = self.workflows.get(workflow).and_then(|w| w.key.clone()) {
385 let mut data = crate::engine::template::Data::new();
386 data.insert("payload".into(), ev["payload"].clone());
387 data.insert("inputs".into(), ev["inputs"].clone());
388 match crate::engine::template::render_str(&tpl, &data) {
389 Ok(Value::String(k)) if !k.trim().is_empty() => ev["key"] = json!(k),
390 Ok(other) if !other.is_null() => ev["key"] = json!(other.to_string()),
391 Ok(_) => self.log.warn(
392 "start.key.empty",
393 json!({"workflow": workflow, "node": node, "template": tpl}),
394 ),
395 Err(e) => self.log.warn(
396 "start.key.invalid",
397 json!({"workflow": workflow, "node": node, "err": e}),
398 ),
399 }
400 }
401 for k in ["conversation", "task", "msg_depth"] {
408 if let Some(v) = ev["payload"].get(k).filter(|v| !v.is_null()).cloned() {
409 ev[k] = v;
410 }
411 }
412 if let Some(rid) = run_id {
413 ev["run_id"] = json!(rid);
414 }
415 let acting = ev
426 .get("payload")
427 .and_then(|p| p.get("principal"))
428 .and_then(Value::as_str)
429 .map(str::to_string)
430 .unwrap_or_else(|| self.settings.identity.autonomous_id().to_string());
431 let _ = self.accept_event(kinds::START_FIRED, Some(acting), ev);
432 }
433
434 pub(crate) fn on_loop_run_finished(
437 &mut self,
438 workflow: &str,
439 node: &str,
440 spec: &Map<String, Value>,
441 ok: bool,
442 last_output: &Value,
443 ) {
444 if let Some(until) = spec.get("until").and_then(Value::as_str) {
446 let mut data = crate::engine::template::Data::new();
447 data.insert("outcome".into(), json!({"ok": ok, "output": last_output}));
448 data.insert("last".into(), last_output.clone());
449 let vars: Vec<(&str, &Value)> = data.iter().map(|(k, v)| (k.as_str(), v)).collect();
450 if crate::cel::eval_bool(until.trim().trim_start_matches("CEL:").trim(), &vars)
451 == Ok(true)
452 {
453 self.log.info(
454 "start.loop.stopped",
455 json!({"workflow": workflow, "node": node, "reason": "until"}),
456 );
457 let mut st = self.start_state(workflow, node);
458 st.as_object_mut().map(|o| o.remove("next_ms"));
459 self.set_start_state(workflow, node, st);
460 return;
461 }
462 }
463 let st = self.start_state(workflow, node);
464 let iteration = st["iteration"].as_u64().unwrap_or(0);
465 if let Some(max) = spec.get("max_iterations").and_then(Value::as_u64)
466 && iteration >= max
467 {
468 self.log.info(
469 "start.loop.stopped",
470 json!({"workflow": workflow, "node": node, "reason": "max_iterations"}),
471 );
472 let mut st = self.start_state(workflow, node);
473 st.as_object_mut().map(|o| o.remove("next_ms"));
474 self.set_start_state(workflow, node, st);
475 return;
476 }
477 let interval = spec
479 .get("interval")
480 .and_then(Value::as_str)
481 .and_then(|i| crate::config::parse_duration(i).ok())
482 .map(|d| d.as_millis() as u64)
483 .unwrap_or(0);
484 let delay = if !ok {
485 spec.get("backoff")
486 .and_then(|b| b.get("initial"))
487 .and_then(Value::as_str)
488 .and_then(|i| crate::config::parse_duration(i).ok())
489 .map(|d| d.as_millis() as u64)
490 .unwrap_or(interval)
491 } else {
492 interval
493 };
494 let mut st = self.start_state(workflow, node);
495 st["next_ms"] = json!(now_ms() + delay);
496 self.set_start_state(workflow, node, st);
497 }
498
499 pub(crate) fn on_subscribe_resource(&mut self, server: &str, uri: &str) {
501 let matches: Vec<(String, String, Map<String, Value>)> = self
502 .workflows
503 .values()
504 .filter(|w| w.armed)
505 .flat_map(|w| {
506 w.start_steps()
507 .into_iter()
508 .filter(|s| {
509 s.kind == "subscribe"
510 && s.field_str("server") == Some(server)
511 && s.field_str("uri") == Some(uri)
512 })
513 .map(|s| (w.name.clone(), s.id.clone(), s.spec.clone()))
514 .collect::<Vec<_>>()
515 })
516 .collect();
517 if matches.is_empty() {
518 return;
519 }
520 let Some(client) = self.mcp.get(server).cloned() else {
527 return; };
529 let tx = self.events_tx.clone();
530 let (srv, u) = (server.to_string(), uri.to_string());
531 std::thread::Builder::new()
532 .name(format!("mcp.subscribe:{server}"))
533 .spawn(move || {
534 let content = client.read_resource(&u).ok().map(|r| {
535 let t = r.text();
536 serde_json::from_str::<Value>(&t).unwrap_or(Value::String(t))
537 });
538 let _ = tx.send(super::events::Event::SubscribeRead {
539 server: srv,
540 uri: u,
541 content,
542 });
543 })
544 .ok();
545 }
546
547 pub(crate) fn on_subscribe_read(&mut self, server: &str, uri: &str, content: Option<Value>) {
550 let matches: Vec<(String, String, Map<String, Value>)> = self
551 .workflows
552 .values()
553 .filter(|w| w.armed)
554 .flat_map(|w| {
555 w.start_steps()
556 .into_iter()
557 .filter(|s| {
558 s.kind == "subscribe"
559 && s.field_str("server") == Some(server)
560 && s.field_str("uri") == Some(uri)
561 })
562 .map(|s| (w.name.clone(), s.id.clone(), s.spec.clone()))
563 .collect::<Vec<_>>()
564 })
565 .collect();
566 for (workflow, node, spec) in matches {
567 if let Some(filter) = spec.get("filter").and_then(Value::as_str) {
569 let mut data = crate::engine::template::Data::new();
570 data.insert("content".into(), content.clone().unwrap_or(Value::Null));
571 let vars: Vec<(&str, &Value)> = data.iter().map(|(k, v)| (k.as_str(), v)).collect();
572 if crate::cel::eval_bool(filter.trim().trim_start_matches("CEL:").trim(), &vars)
573 != Ok(true)
574 {
575 continue;
576 }
577 }
578 let window_n = spec
585 .get("window")
586 .and_then(|w| w.get("samples"))
587 .and_then(Value::as_u64)
588 .map(|n| n as usize);
589 if let Some(n) = window_n {
590 let mut st = self.start_state(&workflow, &node);
591 let mut ring: Vec<Value> = st["window"].as_array().cloned().unwrap_or_default();
592 ring.push(content.clone().unwrap_or(Value::Null));
593 if ring.len() > n {
594 let drop = ring.len() - n;
595 ring.drain(..drop);
596 }
597 st["window"] = Value::Array(ring);
598 self.set_start_state(&workflow, &node, st);
599 }
600 let mut payload = json!({"server": server, "uri": uri, "content": content});
601 if window_n.is_some() {
602 payload["window"] = self.start_state(&workflow, &node)["window"]
603 .as_array()
604 .cloned()
605 .map(Value::Array)
606 .unwrap_or_else(|| json!([]));
607 }
608 let debounce = spec.get("debounce_ms").and_then(Value::as_u64).unwrap_or(0);
609 if debounce > 0 {
610 let mut st = self.start_state(&workflow, &node);
612 st["debounce_until"] = json!(now_ms() + debounce);
613 st["pending_payload"] = payload;
614 self.set_start_state(&workflow, &node, st);
615 } else {
616 self.fire_start(&workflow, &node, &spec, payload, "subscribe");
617 }
618 }
619 }
620
621 pub(crate) fn fire_signal_starts(
623 &mut self,
624 name: &str,
625 payload: &Value,
626 _broadcast: bool,
627 ) -> u64 {
628 let matches: Vec<(String, String, Map<String, Value>)> = self
629 .workflows
630 .values()
631 .filter(|w| w.armed)
632 .flat_map(|w| {
633 w.start_steps()
634 .into_iter()
635 .filter(|s| s.kind == "signal" && s.field_str("name") == Some(name))
636 .map(|s| (w.name.clone(), s.id.clone(), s.spec.clone()))
637 .collect::<Vec<_>>()
638 })
639 .collect();
640 let mut fired = 0;
641 for (workflow, node, spec) in matches {
642 if let Some(filter) = spec.get("filter").and_then(Value::as_str) {
643 let mut data = crate::engine::template::Data::new();
644 data.insert("payload".into(), payload.clone());
645 let vars: Vec<(&str, &Value)> = data.iter().map(|(k, v)| (k.as_str(), v)).collect();
646 if crate::cel::eval_bool(filter.trim().trim_start_matches("CEL:").trim(), &vars)
647 != Ok(true)
648 {
649 continue;
650 }
651 }
652 self.fire_start(
653 &workflow,
654 &node,
655 &spec,
656 json!({"signal": name, "payload": payload}),
657 "signal",
658 );
659 fired += 1;
660 }
661 fired
662 }
663
664 pub(crate) fn push_pending(&mut self, p: super::reactor::PendingTool) {
671 if let super::reactor::PendingKind::Human {
672 task,
673 question,
674 deadline_ms,
675 ..
676 } = &p.kind
677 {
678 let payload = serde_json::json!({
679 "task": task, "question": question, "deadline_ms": deadline_ms,
680 });
681 self.fire_event_starts("human.asked", &payload);
682 }
683 self.pending.push(p);
684 }
685
686 pub(crate) fn fire_event_starts(&mut self, event: &str, payload: &Value) {
687 let matches: Vec<(String, String, Map<String, Value>)> = self
688 .workflows
689 .values()
690 .filter(|w| w.armed)
691 .flat_map(|w| {
692 w.start_steps()
693 .into_iter()
694 .filter(|s| s.kind == "event" && s.field_str("on") == Some(event))
695 .map(|s| (w.name.clone(), s.id.clone(), s.spec.clone()))
696 .collect::<Vec<_>>()
697 })
698 .collect();
699 for (workflow, node, spec) in matches {
700 if payload.get("workflow").and_then(Value::as_str) == Some(workflow.as_str()) {
705 continue;
706 }
707 if let Some(filter) = spec.get("filter").and_then(Value::as_str) {
708 let mut data = crate::engine::template::Data::new();
709 data.insert("payload".into(), payload.clone());
710 let vars: Vec<(&str, &Value)> = data.iter().map(|(k, v)| (k.as_str(), v)).collect();
711 if crate::cel::eval_bool(filter.trim().trim_start_matches("CEL:").trim(), &vars)
712 != Ok(true)
713 {
714 continue;
715 }
716 }
717 self.fire_start(
718 &workflow,
719 &node,
720 &spec,
721 json!({"event": event, "payload": payload}),
722 "event",
723 );
724 }
725 }
726
727 pub(crate) fn run_start_spec(
729 &self,
730 run_id: &str,
731 ) -> Option<(String, String, Map<String, Value>, String)> {
732 let run = self.runs.get(run_id)?;
733 let w = self.workflows.get(&run.workflow)?;
734 let s: &Step = w.step(&run.start.node)?;
735 Some((
736 run.workflow.clone(),
737 run.start.node.clone(),
738 s.spec.clone(),
739 s.kind.clone(),
740 ))
741 }
742}
743
744pub fn run_event(status: RunStatus) -> Option<&'static str> {
746 match status {
747 RunStatus::Completed => Some("workflow.finished"),
748 RunStatus::Failed | RunStatus::Stalled | RunStatus::Cancelled | RunStatus::Refused => {
749 Some("workflow.failed")
750 }
751 _ => None,
752 }
753}