1use super::children::ChildKind;
10use super::events::kinds;
11use super::reactor::{PendingKind, Runtime, Target};
12use super::tools::{ToolCaller, ToolOutcome};
13use crate::context::Msg;
14use crate::engine::model::{OnError, Step, Workflow, parse_workflow};
15use crate::engine::run::{
16 self, Next, RunState, RunStatus, Start, StepStatus, env_view, render_spec,
17};
18use crate::engine::template;
19use crate::governor::Admission;
20use crate::registry::Caller;
21use crate::state::{InboxEvent, Kind, now_ms, ulid};
22use crate::subagent::protocol::{TurnKind, TurnResult, TurnSpec};
23use serde_json::{Map, Value, json};
24use std::collections::BTreeMap;
25
26const WORKFLOW_DEF_PREFIX: &str = "_workflows/";
28
29impl Runtime {
30 pub(crate) fn load_workflows(&mut self) -> Result<(), Vec<String>> {
35 let mut errs = Vec::new();
36 let docs = self.settings.workflows.clone();
37 for doc in docs {
38 let resolved = match (
39 doc.get("file").and_then(Value::as_str),
40 doc.get("uri").and_then(Value::as_str),
41 ) {
42 (Some(path), _) => match std::fs::read_to_string(path)
43 .map_err(|e| e.to_string())
44 .and_then(|t| {
45 crate::config::file::parse_document(
46 &t,
47 crate::config::file::Format::detect(
48 Some(std::path::Path::new(path)),
49 &t,
50 ),
51 )
52 }) {
53 Ok(mut d) => {
54 if d.get("name").is_none()
55 && let Some(n) = doc.get("name")
56 {
57 d["name"] = n.clone();
58 }
59 d
60 }
61 Err(e) => {
62 errs.push(format!("workflow file {path}: {e}"));
63 continue;
64 }
65 },
66 (None, Some(uri)) => match self.read_resource_any(uri) {
67 Ok(text) => match crate::config::file::parse_document(
68 &text,
69 crate::config::file::Format::detect(Some(std::path::Path::new(uri)), &text),
70 ) {
71 Ok(mut d) => {
72 if d.get("name").is_none()
73 && let Some(n) = doc.get("name")
74 {
75 d["name"] = n.clone();
76 }
77 d
78 }
79 Err(e) => {
80 errs.push(format!("workflow uri {uri}: {e}"));
81 continue;
82 }
83 },
84 Err(e) => {
85 errs.push(format!("workflow uri {uri}: {e}"));
86 continue;
87 }
88 },
89 _ => doc.clone(),
90 };
91 match parse_workflow(&resolved) {
92 Ok(w) => {
93 self.log.info("workflow.loaded", json!({"name": w.name, "hash": &w.hash[..12], "steps": w.steps.len(), "starts": w.start_steps().iter().map(|s| s.kind.clone()).collect::<Vec<_>>()}));
94 self.workflows.insert(w.name.clone(), w);
95 }
96 Err(e) => errs.extend(e),
97 }
98 }
99 if let Ok(list) = self.durable.list(Kind::Memory) {
101 for ks in list {
102 let Some((_, id)) = crate::store::parse_key(
103 self.durable.prefix(),
104 self.durable.instance(),
105 &ks.key,
106 ) else {
107 continue;
108 };
109 if let Some(name) = id.strip_prefix(WORKFLOW_DEF_PREFIX)
110 && !self.workflows.contains_key(name)
111 && let Ok(Some(env)) = self.durable.get(Kind::Memory, id)
112 && let Some(def) = env.state.get("value")
113 {
114 match parse_workflow(def) {
115 Ok(w) => {
116 self.log.info(
117 "workflow.loaded",
118 json!({"name": w.name, "source": "store"}),
119 );
120 self.workflows.insert(w.name.clone(), w);
121 }
122 Err(e) => self.log.warn(
123 "workflow.stored.invalid",
124 json!({"name": name, "errors": e}),
125 ),
126 }
127 }
128 }
129 }
130 for w in self.workflows.values() {
132 for s in w.steps.values() {
133 match s.kind.as_str() {
134 "tool" => {
135 if let Some(n) = s.field_str("name")
136 && !self.registry.allowed(&Caller::Workflow, n)
137 {
138 errs.push(format!("workflow {:?} step {:?}: tool {n:?} is unknown, disabled or not granted to workflows", w.name, s.id));
139 }
140 }
141 "mcp.tool" => {
142 if let Some(srv) = s.field_str("server")
143 && !self.mcp.contains_key(srv)
144 {
145 errs.push(format!(
146 "workflow {:?} step {:?}: mcp server {srv:?} is not connected",
147 w.name, s.id
148 ));
149 }
150 }
151 k if (k.starts_with("memory.")
152 || k.starts_with("artifact.")
153 || k.starts_with("knowledge.")
154 || k.starts_with("search."))
155 && !self.registry.allowed(&Caller::Workflow, k) =>
156 {
157 errs.push(format!("workflow {:?} step {:?}: {k} is unavailable (map it with tools.overrides or configure its server)", w.name, s.id));
158 }
159 _ => {}
160 }
161 }
162 }
163 if errs.is_empty() { Ok(()) } else { Err(errs) }
164 }
165
166 pub(crate) fn read_resource_any(&self, uri: &str) -> Result<String, String> {
168 if let Some(rest) = uri.strip_prefix("mcp://") {
169 let (server, res) = rest
170 .split_once('/')
171 .ok_or("mcp:// uri needs <server>/<resource-uri>")?;
172 let c = self
173 .mcp
174 .get(server)
175 .ok_or_else(|| format!("mcp server {server:?} is not connected"))?;
176 return c
177 .read_resource(res)
178 .map(|r| r.text())
179 .map_err(|e| e.to_string());
180 }
181 let mut last = String::from("no connected server serves it");
182 for c in self.mcp.values() {
183 match c.read_resource(uri) {
184 Ok(r) => return Ok(r.text()),
185 Err(e) => last = e.to_string(),
186 }
187 }
188 Err(last)
189 }
190
191 pub(crate) fn arm_workflows(&mut self) {
194 let names: Vec<String> = self.workflows.keys().cloned().collect();
195 for name in names {
196 let Some(w) = self.workflows.get(&name) else {
197 continue;
198 };
199 if !w.armed {
200 continue;
201 }
202 let starts: Vec<(String, String, Map<String, Value>)> = w
203 .start_steps()
204 .iter()
205 .map(|s| (s.id.clone(), s.kind.clone(), s.spec.clone()))
206 .collect();
207 for (id, kind, spec) in starts {
208 match kind.as_str() {
209 "once" => {
210 let policy = spec
211 .get("policy")
212 .and_then(Value::as_str)
213 .unwrap_or("ensure");
214 let live = self
215 .runs
216 .values()
217 .any(|r| r.workflow == name && !r.status.is_terminal());
218 let ever = self
219 .runs
220 .values()
221 .any(|r| r.workflow == name && r.start.node == id);
222 let pending = self.inbox_queue.iter().any(|e| {
224 e.kind == kinds::START_FIRED
225 && e.payload["workflow"] == name.as_str()
226 && e.payload["node"] == id.as_str()
227 });
228 if policy == "ensure" && (live || ever || pending) {
229 self.log.info("start.once.skipped", json!({"workflow": name, "node": id, "live": live, "pending": pending}));
230 continue;
231 }
232 let inputs = spec.get("inputs").cloned().unwrap_or(json!({}));
233 let _ = self.accept_event(kinds::START_FIRED, None, json!({"workflow": name, "node": id, "payload": {"fired_at": now_ms()}, "inputs": inputs}));
234 }
235 "manual" => {}
236 _ => {}
238 }
239 }
240 }
241 }
242
243 pub(crate) fn on_start_event(&mut self, ev: &InboxEvent) -> bool {
245 let name = ev.payload["workflow"].as_str().unwrap_or("").to_string();
246 let Some(w) = self.workflows.get(&name).cloned() else {
247 self.log.warn(
248 "start.unknown_workflow",
249 json!({"inbox_event": ev.id, "workflow": name}),
250 );
251 return true;
252 };
253 let node = ev.payload["node"]
254 .as_str()
255 .map(str::to_string)
256 .unwrap_or_else(|| default_start(&w).unwrap_or_default());
257 let live = self
259 .runs
260 .values()
261 .filter(|r| r.workflow == name && !r.status.is_terminal())
262 .count() as u32;
263 let global_live = self
264 .runs
265 .values()
266 .filter(|r| !r.status.is_terminal())
267 .count() as u32;
268 if live >= w.concurrency.max_runs
269 || global_live >= self.settings.limits.max_runs.unwrap_or(8)
270 {
271 match w.concurrency.on_overflow {
272 crate::engine::model::OnOverflow::Queue => {
273 self.inbox_queue.push_back(ev.clone());
280 return false;
281 }
282 crate::engine::model::OnOverflow::Drop => {
283 self.log.warn(
284 "run.dropped",
285 json!({"workflow": name, "reason": "concurrency"}),
286 );
287 return true;
288 }
289 crate::engine::model::OnOverflow::Replace => {
290 if let Some(oldest) = self
291 .runs
292 .values()
293 .filter(|r| r.workflow == name && !r.status.is_terminal())
294 .min_by_key(|r| r.created)
295 .map(|r| r.id.clone())
296 {
297 self.cancel_run(&oldest, "replaced by a newer run");
298 }
299 }
300 }
301 }
302 let inputs = ev.payload.get("inputs").cloned().unwrap_or(json!({}));
304 if let Some(schema) = &w.inputs_schema
305 && let Err(e) = crate::jsonschema::validate(schema, &inputs)
306 {
307 self.log
308 .warn("run.inputs.invalid", json!({"workflow": name, "errors": e}));
309 return true;
310 }
311 let run_id = ev
313 .payload
314 .get("run_id")
315 .and_then(Value::as_str)
316 .map(str::to_string)
317 .unwrap_or_else(|| format!("{}-{}", name, ulid::new()));
318 let mut run = RunState::new(
319 &run_id,
320 &w,
321 Start {
322 node: node.clone(),
323 payload: ev.payload.get("payload").cloned().unwrap_or(Value::Null),
324 ts: now_ms(),
325 },
326 inputs,
327 );
328 run.principal = ev.principal.clone();
329 run.parent = ev.payload.get("parent").cloned().filter(|p| !p.is_null());
330 run.conversation = ev
331 .payload
332 .get("conversation")
333 .and_then(Value::as_str)
334 .map(str::to_string);
335 run.task = ev
336 .payload
337 .get("task")
338 .and_then(Value::as_str)
339 .map(str::to_string);
340 if let Err(e) = self.durable.put(
342 Kind::Run,
343 &run_id,
344 serde_json::to_value(&run).unwrap_or(Value::Null),
345 Some(w.hash.clone()),
346 ) {
347 self.log.error(
348 "run.create.fail",
349 json!({"workflow": name, "err": e.to_string()}),
350 );
351 self.inbox_queue.push_back(ev.clone());
356 return false;
357 }
358 run.dirty = false;
359 self.log.info(
360 "run.start",
361 json!({"run": run_id, "workflow": name, "node": node, "inbox_event": ev.id}),
362 );
363 self.counters.runs_started += 1;
364 crate::obs::metrics::record_run_started();
365 if node_kind(&w, &node) == Some("once") && self.job_shape {
366 self.job_runs.push(run_id.clone());
367 }
368 if ev.kind == kinds::WORKFLOW_RUN
370 && let Some(req) = ev.payload.get("request").and_then(Value::as_object)
371 {
372 let target = match (
373 req.get("node").and_then(Value::as_u64),
374 req.get("req").and_then(Value::as_u64),
375 req.get("run").and_then(Value::as_str),
376 req.get("step").and_then(Value::as_str),
377 ) {
378 (Some(n), Some(r), _, _) => {
379 Some(Target::Child(crate::supervisor::tree::NodeId(n), r))
380 }
381 (None, None, Some(r), Some(s)) => Some(Target::Step(r.to_string(), s.to_string())),
382 _ => None,
383 };
384 if let Some(t) = target {
385 if req.get("wait").and_then(Value::as_bool).unwrap_or(false) {
386 let deadline = now_ms()
387 + req
388 .get("timeout_ms")
389 .and_then(Value::as_u64)
390 .unwrap_or(3_600_000);
391 self.pending.push(super::reactor::PendingTool {
392 target: t,
393 name: "workflow.run".into(),
394 kind: PendingKind::Run {
395 run: run_id.clone(),
396 deadline_ms: deadline,
397 },
398 started_ms: now_ms(),
399 });
400 } else {
401 self.reply(
402 &t,
403 json!({"run": run_id, "status": "running", "workflow": name}),
404 false,
405 );
406 }
407 }
408 }
409 self.runs.insert(run_id, run);
410 true
411 }
412
413 pub(crate) fn schedule_runs(&mut self) {
417 if self.paused {
418 return; }
420 let ids: Vec<String> = self
421 .runs
422 .iter()
423 .filter(|(_, r)| !r.status.is_terminal() && r.status != RunStatus::Paused)
424 .map(|(id, _)| id.clone())
425 .collect();
426 for id in ids {
427 self.schedule_run(&id);
428 }
429 }
430
431 pub(crate) fn definition_for_run(&self, run_id: &str) -> Option<Workflow> {
436 let run = self.runs.get(run_id)?;
437 if let Some(w) = self.workflows.get(&run.workflow)
438 && w.hash == run.workflow_hash
439 {
440 return Some(w.clone());
441 }
442 self.pinned.get(&run.workflow_hash).cloned()
443 }
444
445 fn schedule_run(&mut self, run_id: &str) {
446 let Some(wf) = self.definition_for_run(run_id) else {
447 let (name, hash) = self
450 .runs
451 .get(run_id)
452 .map(|r| (r.workflow.clone(), r.workflow_hash.clone()))
453 .unwrap_or_default();
454 let reason = if self.workflows.contains_key(&name) {
455 format!(
456 "workflow {name:?} definition changed (run pinned to hash {}); resume_policy refuse",
457 &hash[..hash.len().min(12)]
458 )
459 } else {
460 format!("workflow {name:?} definition is gone")
461 };
462 self.log
463 .warn("run.refused", json!({"run": run_id, "reason": reason}));
464 if let Some(r) = self.runs.get_mut(run_id) {
465 r.finish(RunStatus::Refused, None, Some(reason));
466 }
467 self.on_run_terminal(run_id);
468 return;
469 };
470 if let Some(r) = self.runs.get(run_id)
471 && run::deadline_passed(r)
472 {
473 self.log.warn("run.deadline", json!({"run": run_id}));
474 self.cancel_children_of_run(run_id, "run deadline");
475 self.runs.get_mut(run_id).expect("present").finish(
476 RunStatus::Failed,
477 None,
478 Some("deadline exceeded".into()),
479 );
480 self.on_run_terminal(run_id);
481 return;
482 }
483 if let Some(cap) = wf.limits.steps
484 && self.runs.get(run_id).is_some_and(|r| r.steps_run >= cap)
485 {
486 self.runs.get_mut(run_id).expect("present").finish(
487 RunStatus::Failed,
488 None,
489 Some(format!("exhausted steps: limits.steps = {cap}")),
490 );
491 self.on_run_terminal(run_id);
492 return;
493 }
494 if let Some(cap) = wf.limits.tokens
495 && self.runs.get(run_id).is_some_and(|r| r.tokens >= cap)
496 {
497 self.runs.get_mut(run_id).expect("present").finish(
498 RunStatus::Failed,
499 None,
500 Some(format!("exhausted tokens: limits.tokens = {cap}")),
501 );
502 self.on_run_terminal(run_id);
503 return;
504 }
505 let data = self.run_data(run_id);
506 let next = {
507 let run = self.runs.get_mut(run_id).expect("present");
508 run::schedule(&wf, run, &data)
509 };
510 let nested: Vec<String> = self
513 .runs
514 .get(run_id)
515 .map(|r| {
516 r.steps
517 .iter()
518 .filter(|(_, st)| {
519 st.status == StepStatus::Running
520 && st.wait.as_ref().is_some_and(|w| {
521 matches!(
522 w["kind"].as_str(),
523 Some("foreach")
524 | Some("batch")
525 | Some("iterate")
526 | Some("parallel")
527 | Some("race")
528 | Some("subgraph")
529 )
530 })
531 })
532 .map(|(id, _)| id.clone())
533 .collect()
534 })
535 .unwrap_or_default();
536 for id in nested {
537 self.nested_advance(run_id, &id);
538 }
539 match next {
540 Ok(Next::Ready(steps)) => {
541 for s in steps {
542 if self.draining {
543 return;
544 }
545 self.execute_step(run_id, &s);
546 }
547 }
548 Ok(Next::Waiting) | Ok(Next::Terminal) => {}
549 Ok(Next::Stalled) => {
550 self.log.warn("run.stalled", json!({"run": run_id}));
551 self.runs.get_mut(run_id).expect("present").finish(
552 RunStatus::Stalled,
553 None,
554 Some("no ready step and no finish reached".into()),
555 );
556 self.on_run_terminal(run_id);
557 }
558 Err(e) => {
559 self.runs.get_mut(run_id).expect("present").finish(
560 RunStatus::Failed,
561 None,
562 Some(e),
563 );
564 self.on_run_terminal(run_id);
565 }
566 }
567 }
568
569 pub(crate) fn run_data(&mut self, run_id: &str) -> template::Data {
573 let env = env_view(
574 &self.instance,
575 run_id,
576 Some(&self.instruction.text),
577 self.settings.agent.prompt.as_deref(),
578 );
579 let mut memory = Map::new();
581 if let Some(wf) = self.definition_for_run(run_id) {
582 let mut keys: Vec<String> = Vec::new();
583 for s in wf.steps.values() {
584 for (_, v) in &s.spec {
585 collect_memory_keys(v, &mut keys);
586 }
587 if let Some(w) = &s.when {
588 collect_memory_keys(&Value::String(w.clone()), &mut keys);
589 }
590 }
591 for k in keys {
592 if let Ok(v) = self.memory.get(&self.durable, &k)
593 && v["found"] == json!(true)
594 {
595 memory.insert(k, v["value"].clone());
596 }
597 }
598 }
599 let mut data = self
600 .runs
601 .get(run_id)
602 .map(|r| r.data(env, Value::Object(memory)))
603 .unwrap_or_default();
604 for key in ["steps", "vars", "inputs"] {
607 if let Some(v) = data.get_mut(key) {
608 self.deref_artifacts(v);
609 }
610 }
611 data
612 }
613
614 pub(crate) fn deref_artifacts(&self, v: &mut Value) {
616 match v {
617 Value::Object(o) => {
618 if let Some(id) = o.get("$artifact").and_then(Value::as_str) {
619 if let Some(a) = self.artifacts.get(id) {
620 *v = a.content.clone();
621 }
622 return;
623 }
624 for x in o.values_mut() {
625 self.deref_artifacts(x);
626 }
627 }
628 Value::Array(a) => {
629 for x in a.iter_mut() {
630 self.deref_artifacts(x);
631 }
632 }
633 _ => {}
634 }
635 }
636
637 pub(crate) fn execute_step_pub(&mut self, run_id: &str, step_id: &str) {
641 self.execute_step(run_id, step_id)
642 }
643
644 fn execute_step(&mut self, run_id: &str, step_id: &str) {
645 if self.runs.get(run_id).is_none_or(|r| r.status.is_terminal()) {
646 return;
647 }
648 let Some(wf) = self
649 .runs
650 .get(run_id)
651 .and_then(|_| self.definition_for_run(run_id))
652 else {
653 return;
654 };
655 let Some((step, scope)) = self
656 .runs
657 .get(run_id)
658 .and_then(|r| self.resolve_step(&wf, r, step_id))
659 else {
660 return;
661 };
662 let attempt = self
663 .runs
664 .get_mut(run_id)
665 .expect("present")
666 .begin_step(step_id);
667 crate::state::kill_point("step.running");
669 self.checkpoint(false);
670 self.log.info(
671 "step.start",
672 json!({"run": run_id, "step": step_id, "kind": step.kind, "attempt": attempt}),
673 );
674 let data = match &scope {
675 Some(sc) => self.scoped_data(run_id, sc),
676 None => self.run_data(run_id),
677 };
678 let spec = match render_spec(&step, &data) {
679 Ok(s) => s,
680 Err(e) => {
681 self.finish_step(run_id, step_id, StepStatus::Failed, None, Some(e), 0);
682 return;
683 }
684 };
685 let step_caller = ToolCaller {
686 run: Some(run_id.to_string()),
687 step: Some(step_id.to_string()),
688 req: attempt as u64,
689 principal: self.runs.get(run_id).and_then(|r| r.principal.clone()),
690 ctx: self.runs.get(run_id).and_then(|r| r.conversation.clone()),
691 ..Default::default()
692 };
693 let cache_key = match self.cache_lookup(&step, &spec, &data) {
695 Some((_key, Some(hit))) => {
696 self.log
697 .info("step.cache_hit", json!({"run": run_id, "step": step_id}));
698 self.finish_step(run_id, step_id, StepStatus::Done, Some(hit), None, 0);
699 return;
700 }
701 Some((key, None)) => Some(key),
702 None => None,
703 };
704 if let Some(k) = cache_key
705 && let Some(st) = self
706 .runs
707 .get_mut(run_id)
708 .and_then(|r| r.steps.get_mut(step_id))
709 {
710 st.wait = Some(json!({"cache_key": k}));
711 }
712 match step.kind.as_str() {
713 "noop" | "checkpoint" => self.finish_step(
714 run_id,
715 step_id,
716 StepStatus::Done,
717 Some(Value::Null),
718 None,
719 0,
720 ),
721 "assign" | "transform" => {
722 let value = spec.get("value").cloned().unwrap_or(Value::Null);
723 let key = spec
724 .get("writes")
725 .and_then(Value::as_str)
726 .unwrap_or(step_id)
727 .to_string();
728 let mode = spec
729 .get("mode")
730 .and_then(Value::as_str)
731 .unwrap_or("overwrite")
732 .to_string();
733 self.runs
734 .get_mut(run_id)
735 .expect("present")
736 .write_var(&key, value.clone(), &mode);
737 self.finish_step(run_id, step_id, StepStatus::Done, Some(value), None, 0);
738 }
739 "template" => {
740 let out = spec
741 .get("text")
742 .cloned()
743 .or_else(|| spec.get("value").cloned())
744 .unwrap_or(Value::String(String::new()));
745 self.finish_step(run_id, step_id, StepStatus::Done, Some(out), None, 0);
746 }
747 "validate" => {
748 let value = spec.get("value").cloned().unwrap_or(Value::Null);
749 let schema = spec.get("schema").cloned().unwrap_or(json!({}));
750 match crate::jsonschema::validate(&schema, &value) {
751 Ok(()) => {
752 self.finish_step(run_id, step_id, StepStatus::Done, Some(value), None, 0)
753 }
754 Err(e) => self.finish_step(
755 run_id,
756 step_id,
757 StepStatus::Failed,
758 Some(value),
759 Some(format!(
760 "validation failed: {}",
761 crate::jsonschema::explain(&e)
762 )),
763 0,
764 ),
765 }
766 }
767 "assert" => {
768 let cond = step
769 .field_str("condition")
770 .unwrap_or("false")
771 .trim()
772 .trim_start_matches("CEL:")
773 .trim()
774 .to_string();
775 let vars: Vec<(&str, &Value)> = data.iter().map(|(k, v)| (k.as_str(), v)).collect();
776 match crate::cel::eval_bool(&cond, &vars) {
777 Ok(true) => self.finish_step(
778 run_id,
779 step_id,
780 StepStatus::Done,
781 Some(json!(true)),
782 None,
783 0,
784 ),
785 Ok(false) => self.finish_step(
786 run_id,
787 step_id,
788 StepStatus::Failed,
789 Some(json!(false)),
790 Some(
791 spec.get("message")
792 .and_then(Value::as_str)
793 .map(str::to_string)
794 .unwrap_or_else(|| format!("assertion failed: {cond}")),
795 ),
796 0,
797 ),
798 Err(e) => self.finish_step(
799 run_id,
800 step_id,
801 StepStatus::Failed,
802 None,
803 Some(format!("assert: {e}")),
804 0,
805 ),
806 }
807 }
808 "fail" => {
809 let msg = spec
810 .get("message")
811 .and_then(Value::as_str)
812 .unwrap_or("deliberate failure")
813 .to_string();
814 self.finish_step(
815 run_id,
816 step_id,
817 StepStatus::Failed,
818 spec.get("code").cloned(),
819 Some(msg),
820 0,
821 );
822 }
823 "emit" => {
824 if let Some(n) = spec.get("note").and_then(Value::as_str) {
825 let text = format!("run {run_id}: {n}");
826 self.note_root(text);
827 }
828 if let Some(a) = spec.get("audit") {
829 self.log.info(
830 "audit.emit",
831 json!({"run": run_id, "step": step_id, "audit": a}),
832 );
833 }
834 self.finish_step(
835 run_id,
836 step_id,
837 StepStatus::Done,
838 spec.get("value").cloned().or(Some(Value::Null)),
839 None,
840 0,
841 );
842 }
843 "finish" => {
844 let status = match spec
845 .get("status")
846 .and_then(Value::as_str)
847 .unwrap_or("completed")
848 {
849 "completed" => RunStatus::Completed,
850 "refused" => RunStatus::Refused,
851 "cancelled" => RunStatus::Cancelled,
852 _ => RunStatus::Failed,
853 };
854 let output = spec.get("output").cloned();
855 let reason = spec
856 .get("reason")
857 .and_then(Value::as_str)
858 .map(str::to_string);
859 self.runs.get_mut(run_id).expect("present").end_step(
860 step_id,
861 StepStatus::Done,
862 output.clone(),
863 None,
864 );
865 self.runs
866 .get_mut(run_id)
867 .expect("present")
868 .finish(status, output, reason);
869 self.on_run_terminal(run_id);
870 }
871 "sleep" => {
872 let ms = spec
873 .get("duration")
874 .map(crate::engine::model::duration_ms)
875 .unwrap_or(Ok(0))
876 .unwrap_or(0);
877 match self.timers.arm(
878 &self.durable,
879 now_ms() + ms,
880 json!({"kind": "step", "run": run_id, "step": step_id}),
881 json!({"slept_ms": ms}),
882 ) {
883 Ok(id) => {
884 self.runs.get_mut(run_id).expect("present").suspend_step(
885 step_id,
886 json!({"kind": "sleep", "timer": id, "deadline_ms": now_ms() + ms}),
887 );
888 self.checkpoint(false);
889 }
890 Err(e) => self.finish_step(
891 run_id,
892 step_id,
893 StepStatus::Failed,
894 None,
895 Some(format!("sleep: {e}")),
896 0,
897 ),
898 }
899 }
900 "tool" => {
901 let name = spec
902 .get("name")
903 .and_then(Value::as_str)
904 .unwrap_or("")
905 .to_string();
906 let args = spec.get("args").cloned().unwrap_or(json!({}));
907 self.step_tool_call(run_id, step_id, &step_caller, &name, args);
908 }
909 "http" => self.step_http(run_id, step_id, &spec),
910 k if k.starts_with("memory.")
911 || k.starts_with("artifact.")
912 || k.starts_with("knowledge.")
913 || k.starts_with("search.") =>
914 {
915 let mut args = spec.clone();
916 args.retain(|_, v| !v.is_null());
918 self.step_tool_call(run_id, step_id, &step_caller, k, Value::Object(args));
919 }
920 "mcp.tool" => {
921 let server = spec
922 .get("server")
923 .and_then(Value::as_str)
924 .unwrap_or("")
925 .to_string();
926 let tool = spec
927 .get("tool")
928 .and_then(Value::as_str)
929 .unwrap_or("")
930 .to_string();
931 let args = spec.get("args").cloned().unwrap_or(json!({}));
932 let Some(client) = self.mcp.get(&server).cloned() else {
933 self.finish_step(
934 run_id,
935 step_id,
936 StepStatus::Failed,
937 None,
938 Some(format!("mcp server {server:?} is not connected")),
939 0,
940 );
941 return;
942 };
943 let meta = json!({"agent/idempotency_key": format!("{}/{run_id}/{step_id}#{attempt}", self.instance), "agent/instance": self.instance, "agent/run": run_id});
944 let timeout = step
945 .timeout_ms
946 .map(std::time::Duration::from_millis)
947 .unwrap_or(
948 self.settings
949 .limits
950 .step_timeout
951 .map(|d| d.0)
952 .unwrap_or(std::time::Duration::from_secs(600)),
953 );
954 let tx = self.events_tx.clone();
955 let (r, s) = (run_id.to_string(), step_id.to_string());
956 self.executing
957 .insert(format!("{run_id}/{step_id}"), std::time::Instant::now());
958 std::thread::Builder::new()
959 .name(format!("step:{server}.{tool}"))
960 .spawn(move || {
961 let (output, is_error, error) = match client.call_tool_with_meta_within(
962 &tool,
963 Some(args),
964 meta,
965 timeout,
966 ) {
967 Ok(res) => {
968 let v = super::worker::tool_result_value(&res);
969 if res.is_error() {
970 (v.clone(), true, Some(res.text()))
971 } else {
972 (v, false, None)
973 }
974 }
975 Err(e) => (Value::Null, true, Some(format!("transport error: {e}"))),
976 };
977 let _ = tx.send(super::events::Event::StepDone {
978 run: r,
979 step: s,
980 output,
981 is_error,
982 error,
983 tokens: 0,
984 });
985 })
986 .ok();
987 }
988 "agent" | "think" => self.step_turn(run_id, step_id, &step, &spec, &data),
989 "foreach" | "batch" | "iterate" | "parallel" | "race" | "subgraph" => {
990 self.nested_start(run_id, step_id, &step, &spec)
991 }
992 "wait" | "join" | "workflow" | "workflow.signal" | "workflow.wait"
993 | "workflow.cancel" | "subagent" | "human" | "mcp.resource" | "a2a.delegate"
994 | "a2a.send" | "a2a.wait" | "classify" | "extract" | "summarize" | "judge"
995 | "route" => {
996 self.execute_orchestration_step(run_id, step_id, &step, &spec, &data, &step_caller)
997 }
998 "switch" => {
999 let on = spec.get("on").cloned().unwrap_or(Value::Null);
1000 let key = match &on {
1001 Value::String(x) => x.clone(),
1002 other => other.to_string(),
1003 };
1004 let cases = step
1005 .field("cases")
1006 .and_then(Value::as_object)
1007 .cloned()
1008 .unwrap_or_default();
1009 let target = cases
1010 .get(&key)
1011 .and_then(Value::as_str)
1012 .map(str::to_string)
1013 .or_else(|| step.field_str("default").map(str::to_string));
1014 match target {
1015 Some(t) => {
1016 let scope_prefix = step_id
1019 .rsplit_once('.')
1020 .map(|(p, _)| format!("{p}."))
1021 .unwrap_or_default();
1022 let mut skipped = Vec::new();
1023 let mut others: Vec<String> = cases
1026 .values()
1027 .filter_map(Value::as_str)
1028 .map(str::to_string)
1029 .collect();
1030 if let Some(d) = step.field_str("default") {
1031 others.push(d.to_string());
1032 }
1033 if let Some(run) = self.runs.get_mut(run_id) {
1034 for tid in others {
1035 if tid == t {
1036 continue;
1037 }
1038 let sid = format!("{scope_prefix}{tid}");
1039 if let Some(st) = run.steps.get_mut(&sid)
1040 && st.status == StepStatus::Pending
1041 {
1042 st.status = StepStatus::Skipped;
1043 skipped.push(sid);
1044 }
1045 }
1046 let sid = format!("{scope_prefix}{t}");
1047 if let Some(st) = run.steps.get_mut(&sid) {
1048 st.status = StepStatus::Pending;
1049 st.forced = true;
1050 }
1051 }
1052 self.finish_step(
1053 run_id,
1054 step_id,
1055 StepStatus::Done,
1056 Some(json!({"case": key, "goto": t, "skipped": skipped})),
1057 None,
1058 0,
1059 );
1060 }
1061 None => self.finish_step(
1062 run_id,
1063 step_id,
1064 StepStatus::Failed,
1065 Some(json!({"case": key})),
1066 Some(format!("switch: no case for {key:?} and no default")),
1067 0,
1068 ),
1069 }
1070 }
1071 "map" | "filter" | "reduce" | "sort" | "dedupe" | "chunk" | "parse" => {
1072 let out = match step.kind.as_str() {
1073 "map" => crate::engine::data::map(
1074 spec.get("over").unwrap_or(&Value::Null),
1075 step.field_str("expr").unwrap_or(""),
1076 step.field_str("as").unwrap_or("item"),
1077 &data,
1078 ),
1079 "filter" => crate::engine::data::filter(
1080 spec.get("over").unwrap_or(&Value::Null),
1081 step.field_str("expr").unwrap_or(""),
1082 step.field_str("as").unwrap_or("item"),
1083 &data,
1084 ),
1085 "reduce" => crate::engine::data::reduce(
1086 spec.get("over").unwrap_or(&Value::Null),
1087 step.field_str("expr").unwrap_or(""),
1088 spec.get("initial").cloned().unwrap_or(Value::Null),
1089 step.field_str("as").unwrap_or("item"),
1090 step.field_str("acc").unwrap_or("acc"),
1091 &data,
1092 ),
1093 "sort" => crate::engine::data::sort(
1094 spec.get("over").unwrap_or(&Value::Null),
1095 spec.get("by").and_then(Value::as_str),
1096 spec.get("order").and_then(Value::as_str),
1097 ),
1098 "dedupe" => crate::engine::data::dedupe(
1099 spec.get("over").unwrap_or(&Value::Null),
1100 spec.get("by").and_then(Value::as_str),
1101 ),
1102 "chunk" => crate::engine::data::chunk(
1103 spec.get("value").unwrap_or(&Value::Null),
1104 spec.get("by").and_then(Value::as_str),
1105 spec.get("size").and_then(Value::as_u64).unwrap_or(0) as usize,
1106 spec.get("overlap").and_then(Value::as_u64).unwrap_or(0) as usize,
1107 ),
1108 _ => crate::engine::data::parse(
1109 spec.get("text").and_then(Value::as_str).unwrap_or(""),
1110 spec.get("format").and_then(Value::as_str),
1111 ),
1112 };
1113 match out {
1114 Ok(v) => self.finish_step(run_id, step_id, StepStatus::Done, Some(v), None, 0),
1115 Err(e) => {
1116 self.finish_step(run_id, step_id, StepStatus::Failed, None, Some(e), 0)
1117 }
1118 }
1119 }
1120 other => self.finish_step(
1121 run_id,
1122 step_id,
1123 StepStatus::Failed,
1124 None,
1125 Some(format!(
1126 "step kind {other:?} is not executable in this build (P4)"
1127 )),
1128 0,
1129 ),
1130 }
1131 }
1132
1133 fn step_tool_call(
1135 &mut self,
1136 run_id: &str,
1137 step_id: &str,
1138 caller: &ToolCaller,
1139 name: &str,
1140 args: Value,
1141 ) {
1142 match self.execute_tool(caller, name, args) {
1143 ToolOutcome::Ready(v, is_error) => {
1144 let err = is_error.then(|| match &v {
1145 Value::String(s) => s.clone(),
1146 o => o.to_string(),
1147 });
1148 self.finish_step(
1149 run_id,
1150 step_id,
1151 if is_error {
1152 StepStatus::Failed
1153 } else {
1154 StepStatus::Done
1155 },
1156 Some(v),
1157 err,
1158 0,
1159 );
1160 }
1161 ToolOutcome::Deferred(kind) => {
1162 let wait = match &kind {
1163 PendingKind::Timer { id } => json!({"kind": "timer", "timer": id}),
1164 PendingKind::Subagent { handle } => {
1165 json!({"kind": "subagent", "handle": handle})
1166 }
1167 PendingKind::Think { .. } => json!({"kind": "think"}),
1168 PendingKind::Run { run, .. } => json!({"kind": "run", "run": run}),
1169 PendingKind::Await {
1170 condition,
1171 deadline_ms,
1172 } => {
1173 json!({"kind": "await", "condition": condition, "deadline_ms": deadline_ms})
1174 }
1175 PendingKind::Human {
1176 task, deadline_ms, ..
1177 } => {
1178 json!({"kind": "human", "task": task, "deadline_ms": deadline_ms})
1179 }
1180 };
1181 self.runs
1182 .get_mut(run_id)
1183 .expect("present")
1184 .suspend_step(step_id, wait);
1185 if !matches!(kind, PendingKind::Timer { .. }) {
1186 self.pending.push(super::reactor::PendingTool {
1187 target: Target::Step(run_id.to_string(), step_id.to_string()),
1188 name: name.to_string(),
1189 kind,
1190 started_ms: now_ms(),
1191 });
1192 }
1193 self.checkpoint(false);
1194 }
1195 ToolOutcome::Executing => {
1196 self.executing
1197 .insert(format!("{run_id}/{step_id}"), std::time::Instant::now());
1198 }
1199 }
1200 }
1201
1202 pub(crate) fn step_turn_pub(
1203 &mut self,
1204 run_id: &str,
1205 step_id: &str,
1206 step: &Step,
1207 spec: &Map<String, Value>,
1208 data: &template::Data,
1209 ) {
1210 self.step_turn(run_id, step_id, step, spec, data)
1211 }
1212
1213 fn step_turn(
1215 &mut self,
1216 run_id: &str,
1217 step_id: &str,
1218 step: &Step,
1219 spec: &Map<String, Value>,
1220 data: &template::Data,
1221 ) {
1222 let is_think = step.kind == "think";
1223 let prompt = if is_think {
1224 spec.get("prompt").and_then(Value::as_str).unwrap_or("")
1225 } else {
1226 spec.get("instruction")
1227 .and_then(Value::as_str)
1228 .unwrap_or("")
1229 }
1230 .to_string();
1231 let output_schema = spec.get("output_schema").cloned();
1232 let mut messages = Vec::new();
1233 if let Some(reads) = spec.get("reads").and_then(Value::as_array) {
1235 for path in reads.iter().filter_map(Value::as_str) {
1236 if let Some(v) = template::lookup(path, data) {
1237 messages.push(Msg::system(format!("{path} = {v}")));
1238 }
1239 }
1240 }
1241 if let Some(seed) = spec.get("context").and_then(Value::as_array) {
1243 for m in seed {
1244 match (m["role"].as_str(), m["content"].as_str()) {
1245 (Some("system"), Some(c)) => messages.push(Msg::system(c)),
1246 (Some("assistant"), Some(c)) => {
1247 messages.push(Msg::assistant(Some(c.to_string()), vec![]))
1248 }
1249 (_, Some(c)) => messages.push(Msg::user(c, None)),
1250 _ => {}
1251 }
1252 }
1253 }
1254 let mut user = prompt.clone();
1255 if let Some(c) = spec.get("output_contract").and_then(Value::as_str) {
1256 user.push_str(&format!("\n\nOutput contract:\n{c}"));
1257 }
1258 if let Some(s) = &output_schema {
1259 user.push_str(&format!(
1260 "\n\nReply with ONLY one JSON object matching this JSON Schema:\n{s}"
1261 ));
1262 }
1263 messages.push(Msg::user(user, None));
1264 let skill_bodies: Vec<String> = step
1266 .skills
1267 .iter()
1268 .chain(
1269 spec.get("skills")
1270 .and_then(Value::as_array)
1271 .map(|a| {
1272 a.iter()
1273 .filter_map(Value::as_str)
1274 .map(str::to_string)
1275 .collect::<Vec<_>>()
1276 })
1277 .unwrap_or_default()
1278 .iter(),
1279 )
1280 .filter_map(|name| {
1281 let mcp = self.mcp.clone();
1282 let resolver = move |server: &str| -> Option<
1283 std::sync::Arc<dyn crate::context::skills::SkillServer>,
1284 > {
1285 mcp.get(server).map(|c| {
1286 c.clone() as std::sync::Arc<dyn crate::context::skills::SkillServer>
1287 })
1288 };
1289 self.skills
1290 .load(name, None, &resolver)
1291 .ok()
1292 .map(|b| format!("### Skill: {}\n{}", b.name, b.body))
1293 })
1294 .collect();
1295 let extra = if skill_bodies.is_empty() {
1296 None
1297 } else {
1298 Some(format!(
1299 "Loaded skills — follow these instructions when relevant:\n{}",
1300 skill_bodies.join("\n\n")
1301 ))
1302 };
1303 let system = match spec.get("system").and_then(Value::as_str) {
1304 Some(s) => s.to_string(),
1305 None if is_think => format!(
1306 "You are the reasoning module of {}. Reply with {}. No tools are available.",
1307 self.instance,
1308 if output_schema.is_some() {
1309 "ONLY one JSON object matching the schema"
1310 } else {
1311 "your conclusion"
1312 }
1313 ),
1314 None => self.system_prompt(None, extra.as_deref()),
1315 };
1316 let (tools, internal, routes) = if is_think {
1317 (Vec::new(), Vec::new(), BTreeMap::new())
1318 } else {
1319 let allow: Option<Vec<String>> = spec.get("tools").and_then(Value::as_array).map(|a| {
1320 a.iter()
1321 .filter_map(Value::as_str)
1322 .map(str::to_string)
1323 .collect()
1324 });
1325 self.tool_plan(&Caller::Workflow, allow.as_deref())
1326 };
1327 let servers: Vec<String> = match spec.get("servers").and_then(Value::as_array) {
1328 Some(a) => a
1329 .iter()
1330 .filter_map(Value::as_str)
1331 .map(str::to_string)
1332 .collect(),
1333 None => routes
1334 .values()
1335 .map(|(s, _)| s.clone())
1336 .collect::<std::collections::BTreeSet<_>>()
1337 .into_iter()
1338 .collect(),
1339 };
1340 let est: u64 = messages.iter().map(Msg::est_tokens).sum::<u64>()
1342 + crate::context::tokens::estimate(&system)
1343 + 4096;
1344 let scopes = self.run_scopes(run_id);
1345 let reservation = match self.governor.admit(est, &scopes, now_ms()) {
1346 Admission::Ok { reservation, model } => {
1347 if let Some(m) = model {
1348 self.log.info(
1349 "budget.degraded",
1350 json!({"run": run_id, "step": step_id, "model": m}),
1351 );
1352 }
1353 Some(reservation)
1354 }
1355 Admission::Wait { until_ms, reason } => {
1356 self.log.info(
1357 "budget.wait",
1358 json!({"run": run_id, "step": step_id, "until_ms": until_ms, "reason": reason}),
1359 );
1360 crate::state::kill_point("budget.waiting");
1361 match self.timers.arm(
1362 &self.durable,
1363 until_ms,
1364 json!({"kind": "step_budget", "run": run_id, "step": step_id}),
1365 Value::Null,
1366 ) {
1367 Ok(id) => {
1368 self.runs.get_mut(run_id).expect("present").suspend_step(step_id, json!({"kind": "waiting_budget", "timer": id, "until_ms": until_ms, "reason": reason}));
1369 self.checkpoint(false);
1370 }
1371 Err(e) => self.finish_step(
1372 run_id,
1373 step_id,
1374 StepStatus::Failed,
1375 None,
1376 Some(format!("budget wait: {e}")),
1377 0,
1378 ),
1379 }
1380 return;
1381 }
1382 Admission::Refuse { reason } | Admission::Fail { reason } => {
1383 self.finish_step(run_id, step_id, StepStatus::Failed, None, Some(reason), 0);
1384 return;
1385 }
1386 };
1387 let limits = spec.get("limits").cloned().unwrap_or(json!({}));
1388 let max_steps = limits
1389 .get("steps")
1390 .and_then(Value::as_u64)
1391 .map(|s| s as u32)
1392 .unwrap_or(self.settings.limits.run.steps());
1393 let max_tokens = step
1394 .budget
1395 .or_else(|| limits.get("tokens").and_then(Value::as_u64))
1396 .unwrap_or(self.settings.limits.run.tokens());
1397 let deadline_ms = step.timeout_ms.unwrap_or(
1398 self.settings
1399 .limits
1400 .step_timeout
1401 .map(|d| d.0.as_millis() as u64)
1402 .unwrap_or(600_000),
1403 );
1404 let spec = TurnSpec {
1405 kind: if is_think {
1406 TurnKind::Think
1407 } else {
1408 TurnKind::Agent
1409 },
1410 system,
1411 messages,
1412 tools,
1413 internal,
1414 mcp_routes: routes,
1415 output_schema,
1416 max_rounds: if is_think { 3 } else { 0 },
1417 budget_admission: self.governor.is_active(),
1418 idempotency_prefix: format!("{}/{run_id}/{step_id}", self.instance),
1419 tool_meta: Some(
1420 json!({"agent/run": run_id, "agent/step": step_id, "agent/instance": self.instance}),
1421 ),
1422 temperature: None,
1423 max_tokens_per_call: 0,
1424 turn_id: format!(
1425 "{run_id}/{step_id}#{}",
1426 self.runs
1427 .get(run_id)
1428 .and_then(|r| r.step(step_id))
1429 .map(|s| s.attempt)
1430 .unwrap_or(1)
1431 ),
1432 };
1433 let launch = super::turns::TurnLaunch {
1434 spec,
1435 kind: ChildKind::StepTurn {
1436 run: run_id.to_string(),
1437 step: step_id.to_string(),
1438 reservation,
1439 },
1440 servers,
1441 max_steps,
1442 max_tokens,
1443 deadline_ms,
1444 agent_path: format!("run/{run_id}/{step_id}"),
1445 };
1446 match self.spawn_turn(launch) {
1447 Ok(node) => {
1448 if let Some(st) = self
1449 .runs
1450 .get_mut(run_id)
1451 .and_then(|r| r.steps.get_mut(step_id))
1452 {
1453 st.worker = Some(node.0.to_string());
1454 }
1455 self.log.info(
1456 "step.turn.spawn",
1457 json!({"run": run_id, "step": step_id, "node": node.0}),
1458 );
1459 }
1460 Err(e) => {
1461 if let Some(r) = reservation {
1462 self.governor.release(r);
1463 }
1464 self.finish_step(
1465 run_id,
1466 step_id,
1467 StepStatus::Failed,
1468 None,
1469 Some(format!("spawn: {e}")),
1470 0,
1471 );
1472 }
1473 }
1474 }
1475
1476 fn run_scopes(&mut self, run_id: &str) -> Vec<String> {
1478 let Some(wf) = self.definition_for_run(run_id) else {
1479 return Vec::new();
1480 };
1481 match wf
1482 .limits
1483 .budget
1484 .as_ref()
1485 .and_then(|b| serde_json::from_value::<crate::config::v2::Budget>(b.clone()).ok())
1486 {
1487 Some(b) => {
1488 let key = format!("run:{run_id}");
1489 self.governor.ensure_scope(&key, &b);
1490 vec![key]
1491 }
1492 None => Vec::new(),
1493 }
1494 }
1495
1496 pub(crate) fn on_step_done(
1500 &mut self,
1501 run_id: &str,
1502 step_id: &str,
1503 output: Value,
1504 is_error: bool,
1505 error: Option<String>,
1506 tokens: u64,
1507 ) {
1508 self.executing.remove(&format!("{run_id}/{step_id}"));
1509 self.finish_step(
1510 run_id,
1511 step_id,
1512 if is_error {
1513 StepStatus::Failed
1514 } else {
1515 StepStatus::Done
1516 },
1517 Some(output),
1518 error,
1519 tokens,
1520 );
1521 }
1522
1523 pub(crate) fn on_step_turn_done(&mut self, run_id: &str, step_id: &str, turn: TurnResult) {
1525 let tokens = turn.usage.total();
1526 if turn.status == "completed" {
1527 let output = turn
1528 .value
1529 .clone()
1530 .or_else(|| turn.finish.as_ref().and_then(|f| f.get("output").cloned()))
1531 .or_else(|| turn.text.clone().map(Value::String))
1532 .unwrap_or(Value::Null);
1533 let failed = turn
1535 .finish
1536 .as_ref()
1537 .and_then(|f| f.get("status"))
1538 .and_then(Value::as_str)
1539 .is_some_and(|s| s != "completed");
1540 if failed {
1541 let reason = turn
1542 .finish
1543 .as_ref()
1544 .and_then(|f| f.get("reason"))
1545 .and_then(Value::as_str)
1546 .unwrap_or("agent finished with a non-completed status")
1547 .to_string();
1548 self.finish_step(
1549 run_id,
1550 step_id,
1551 StepStatus::Failed,
1552 Some(output),
1553 Some(reason),
1554 tokens,
1555 );
1556 } else {
1557 self.finish_step(
1558 run_id,
1559 step_id,
1560 StepStatus::Done,
1561 Some(output),
1562 None,
1563 tokens,
1564 );
1565 }
1566 } else {
1567 let status = if turn.status == "deadline" {
1568 StepStatus::Timeout
1569 } else {
1570 StepStatus::Failed
1571 };
1572 self.finish_step(
1573 run_id,
1574 step_id,
1575 status,
1576 turn.value
1577 .clone()
1578 .or_else(|| turn.text.clone().map(Value::String)),
1579 Some(format!(
1580 "turn {}{}",
1581 turn.status,
1582 turn.error
1583 .as_deref()
1584 .map(|e| format!(": {e}"))
1585 .unwrap_or_default()
1586 )),
1587 tokens,
1588 );
1589 }
1590 }
1591
1592 pub(crate) fn on_step_timer(
1594 &mut self,
1595 run_id: &str,
1596 step_id: &str,
1597 budget: bool,
1598 payload: &Value,
1599 ) {
1600 if budget {
1601 if let Some(st) = self
1602 .runs
1603 .get_mut(run_id)
1604 .and_then(|r| r.steps.get_mut(step_id))
1605 {
1606 st.status = StepStatus::Pending;
1607 st.wait = None;
1608 }
1609 if let Some(r) = self.runs.get_mut(run_id) {
1610 r.touch();
1611 }
1612 return;
1613 }
1614 self.finish_step(
1615 run_id,
1616 step_id,
1617 StepStatus::Done,
1618 Some(payload.clone()),
1619 None,
1620 0,
1621 );
1622 }
1623
1624 pub(crate) fn finish_step_pub(
1626 &mut self,
1627 run_id: &str,
1628 step_id: &str,
1629 status: StepStatus,
1630 output: Option<Value>,
1631 error: Option<String>,
1632 tokens: u64,
1633 ) {
1634 self.finish_step(run_id, step_id, status, output, error, tokens)
1635 }
1636
1637 fn finish_step(
1638 &mut self,
1639 run_id: &str,
1640 step_id: &str,
1641 status: StepStatus,
1642 output: Option<Value>,
1643 error: Option<String>,
1644 tokens: u64,
1645 ) {
1646 let Some(wf) = self
1647 .runs
1648 .get(run_id)
1649 .and_then(|_| self.definition_for_run(run_id))
1650 else {
1651 return;
1652 };
1653 let Some((step, scope)) = self
1654 .runs
1655 .get(run_id)
1656 .and_then(|r| self.resolve_step(&wf, r, step_id))
1657 else {
1658 return;
1659 };
1660 {
1661 let run = self.runs.get_mut(run_id).expect("present");
1662 if run.status.is_terminal() {
1663 return; }
1665 run.tokens += tokens;
1666 }
1667 crate::obs::metrics::record_step(match status {
1668 StepStatus::Done => "done",
1669 StepStatus::Failed => "failed",
1670 _ => "other",
1671 });
1672 let output = match output {
1674 Some(v) if !v.is_null() => {
1675 let cap = self.settings.limits.inline_max_bytes.unwrap_or(65_536) as usize;
1676 if v.to_string().len() > cap {
1677 match self.artifacts.create(
1678 &self.durable,
1679 super::artifacts::NewArtifact {
1680 name: &format!("{run_id}/{step_id}/output.json"),
1681 mime: Some("application/json"),
1682 content: v.clone(),
1683 created_by: Some("engine"),
1684 sensitive: false,
1685 owner: Some(run_id),
1686 },
1687 ) {
1688 Ok(meta) => {
1689 self.log.info("step.output.artifact", json!({"run": run_id, "step": step_id, "artifact": meta["id"], "size": meta["size"]}));
1690 Some(json!({"$artifact": meta["id"], "size": meta["size"]}))
1691 }
1692 Err(e) => {
1693 self.log.warn(
1694 "step.output.artifact_fail",
1695 json!({"run": run_id, "step": step_id, "err": e}),
1696 );
1697 Some(v)
1698 }
1699 }
1700 } else {
1701 Some(v)
1702 }
1703 }
1704 other => other,
1705 };
1706 let (status, error) = match (&status, &step.output_schema, &output) {
1708 (StepStatus::Done, Some(schema), Some(out)) => {
1709 match crate::jsonschema::validate(schema, out) {
1710 Ok(()) => (status, error),
1711 Err(e) => (
1712 StepStatus::Failed,
1713 Some(format!(
1714 "output does not match output_schema: {}",
1715 crate::jsonschema::explain(&e)
1716 )),
1717 ),
1718 }
1719 }
1720 _ => (status, error),
1721 };
1722 let attempt = self
1723 .runs
1724 .get(run_id)
1725 .and_then(|r| r.step(step_id))
1726 .map(|s| s.attempt)
1727 .unwrap_or(1);
1728 self.log.info("step.done", json!({"run": run_id, "step": step_id, "status": status, "attempt": attempt, "tokens": tokens, "err": error}));
1729 if matches!(status, StepStatus::Failed | StepStatus::Timeout) {
1730 if let Some(retry) = &step.retry
1732 && attempt <= retry.max
1733 {
1734 let backoff = retry
1735 .backoff_ms
1736 .saturating_mul(1u64 << (attempt.saturating_sub(1)).min(10));
1737 self.log.info("step.retry", json!({"run": run_id, "step": step_id, "attempt": attempt, "backoff_ms": backoff}));
1738 if backoff == 0 {
1739 if let Some(st) = self
1740 .runs
1741 .get_mut(run_id)
1742 .and_then(|r| r.steps.get_mut(step_id))
1743 {
1744 st.status = StepStatus::Pending;
1745 st.error = error;
1746 }
1747 } else {
1748 match self.timers.arm(
1749 &self.durable,
1750 now_ms() + backoff,
1751 json!({"kind": "step_budget", "run": run_id, "step": step_id}),
1752 Value::Null,
1753 ) {
1754 Ok(id) => {
1755 self.runs.get_mut(run_id).expect("present").suspend_step(
1756 step_id,
1757 json!({"kind": "retry_backoff", "timer": id, "error": error}),
1758 );
1759 }
1760 Err(_) => {
1761 if let Some(st) = self
1762 .runs
1763 .get_mut(run_id)
1764 .and_then(|r| r.steps.get_mut(step_id))
1765 {
1766 st.status = StepStatus::Pending;
1767 }
1768 }
1769 }
1770 }
1771 self.checkpoint(false);
1772 return;
1773 }
1774 let err_text = error.clone().unwrap_or_else(|| "failed".into());
1775 self.runs
1776 .get_mut(run_id)
1777 .expect("present")
1778 .end_step(step_id, status, output, error);
1779 if let Some(sc) = &scope {
1780 match &step.on_error {
1783 OnError::Continue => {
1784 if let Some(st) = self
1785 .runs
1786 .get_mut(run_id)
1787 .and_then(|r| r.steps.get_mut(step_id))
1788 {
1789 st.status = StepStatus::Done;
1790 st.error = Some(err_text.clone());
1791 if st.output.is_none() {
1792 st.output = Some(json!({"error": err_text}));
1793 }
1794 }
1795 }
1796 OnError::Goto(t) => {
1797 let sid = super::nested::scoped_id(&sc.parent, t);
1798 if let Some(st) = self
1799 .runs
1800 .get_mut(run_id)
1801 .and_then(|r| r.steps.get_mut(&sid))
1802 {
1803 st.status = StepStatus::Pending;
1804 st.forced = true;
1805 }
1806 }
1807 OnError::Fail => {}
1808 }
1809 crate::state::kill_point("step.before_done");
1810 self.checkpoint(false);
1811 self.on_scoped_step_done(run_id, step_id);
1812 return;
1813 }
1814 let routed = run::route_failure(
1815 &wf,
1816 self.runs.get_mut(run_id).expect("present"),
1817 &step,
1818 &err_text,
1819 );
1820 match routed {
1821 Ok(next) => {
1822 if !next.is_empty() {
1823 self.log.info(
1824 "step.goto",
1825 json!({"run": run_id, "from": step_id, "to": next}),
1826 );
1827 }
1828 }
1829 Err(reason) => {
1830 self.cancel_children_of_run(run_id, "run failed");
1831 self.runs.get_mut(run_id).expect("present").finish(
1832 RunStatus::Failed,
1833 None,
1834 Some(reason),
1835 );
1836 self.on_run_terminal(run_id);
1837 return;
1838 }
1839 }
1840 if let OnError::Continue = step.on_error {
1841 }
1843 } else {
1844 if step.cache.is_some()
1846 && status == StepStatus::Done
1847 && let Some(key) = self
1848 .runs
1849 .get(run_id)
1850 .and_then(|r| r.steps.get(step_id))
1851 .and_then(|st| st.wait.as_ref())
1852 .and_then(|w| w.get("cache_key"))
1853 .and_then(Value::as_str)
1854 .map(str::to_string)
1855 && let Some(out) = &output
1856 {
1857 self.cache_store(&key, out);
1858 }
1859 self.runs
1860 .get_mut(run_id)
1861 .expect("present")
1862 .end_step(step_id, status, output, error);
1863 }
1864 crate::state::kill_point("step.before_done");
1865 self.checkpoint(false);
1866 if scope.is_some() {
1867 self.on_scoped_step_done(run_id, step_id);
1868 }
1869 }
1870
1871 pub(crate) fn on_run_terminal(&mut self, run_id: &str) {
1873 let Some(run) = self.runs.get(run_id) else {
1874 return;
1875 };
1876 let (status, output, error, workflow) = (
1877 run.status,
1878 run.output.clone(),
1879 run.error.clone(),
1880 run.workflow.clone(),
1881 );
1882 #[cfg(feature = "a2a")]
1883 let a2a_task = run.task.clone();
1884 #[cfg(feature = "a2a")]
1886 self.webhook_sync_reply(run_id);
1887 if let Some(parent) = self.runs.get(run_id).and_then(|r| r.parent.clone())
1889 && let (Some(pr), Some(ps)) = (
1890 parent["run"].as_str().map(str::to_string),
1891 parent["step"].as_str().map(str::to_string),
1892 )
1893 && self
1894 .runs
1895 .get(&pr)
1896 .and_then(|r| r.steps.get(&ps))
1897 .is_some_and(|st| {
1898 st.status == StepStatus::Suspended
1899 && st.wait.as_ref().is_some_and(|w| w["kind"] == "child_run")
1900 })
1901 {
1902 self.finish_step_pub(
1903 &pr,
1904 &ps,
1905 if status == RunStatus::Completed {
1906 StepStatus::Done
1907 } else {
1908 StepStatus::Failed
1909 },
1910 Some(json!({"run": run_id, "status": status, "output": output, "error": error})),
1911 (status != RunStatus::Completed).then(|| {
1912 error
1913 .clone()
1914 .unwrap_or_else(|| format!("child run {}", status.as_str()))
1915 }),
1916 0,
1917 );
1918 }
1919 self.counters.runs_finished += 1;
1920 crate::obs::metrics::record_run(match status {
1921 RunStatus::Completed => crate::obs::metrics::RunOutcome::Completed,
1922 RunStatus::Cancelled => crate::obs::metrics::RunOutcome::Killed,
1923 _ => crate::obs::metrics::RunOutcome::Failed,
1924 });
1925 crate::obs::metrics::record_run_status(status.as_str());
1926 self.log.info("run.done", json!({"run": run_id, "workflow": workflow, "status": status, "err": error, "output": if self.log.content_capture() { output.clone().unwrap_or(Value::Null) } else { Value::Null }}));
1927 self.governor.drop_scope(&format!("run:{run_id}"));
1928 let waiting: Vec<Target> = self
1930 .pending
1931 .iter()
1932 .filter(|p| matches!(&p.kind, PendingKind::Run { run, .. } if run == run_id))
1933 .map(|p| p.target.clone())
1934 .collect();
1935 self.pending
1936 .retain(|p| !matches!(&p.kind, PendingKind::Run { run, .. } if run == run_id));
1937 for t in waiting {
1938 self.reply(
1939 &t,
1940 json!({"run": run_id, "status": status, "output": output, "error": error}),
1941 false,
1942 );
1943 }
1944 let ok = status == RunStatus::Completed;
1946 let note = error
1947 .clone()
1948 .or_else(|| output.as_ref().map(|o| o.to_string()))
1949 .unwrap_or_default();
1950 self.settle_plan_bindings(
1951 &crate::context::plan::Binding::Run {
1952 id: run_id.to_string(),
1953 },
1954 ok,
1955 ¬e,
1956 );
1957 let wake = self.settings.agent.wake_on();
1958 let notify = match self.settings.agent.on_workflow_finished {
1959 crate::config::v2::OnWorkflowFinished::Ignore => false,
1960 _ => {
1961 ok && wake.contains(&crate::config::v2::WakeEvent::WorkflowFinished)
1962 || !ok && wake.contains(&crate::config::v2::WakeEvent::WorkflowFailed)
1963 }
1964 };
1965 if notify && !self.job_shape {
1966 let short = if note.chars().count() > 400 {
1967 format!("{}…", note.chars().take(400).collect::<String>())
1968 } else {
1969 note.clone()
1970 };
1971 self.note_root(format!(
1972 "workflow {workflow} run {run_id} {}: {short}",
1973 status.as_str()
1974 ));
1975 }
1976 if let Some((wf, node, spec, kind)) = self.run_start_spec(run_id)
1979 && kind == "loop"
1980 {
1981 self.on_loop_run_finished(
1982 &wf,
1983 &node,
1984 &spec,
1985 ok,
1986 &output.clone().unwrap_or(Value::Null),
1987 );
1988 }
1989 if let Some(ev) = super::starts::run_event(status) {
1990 self.fire_event_starts(
1991 ev,
1992 &json!({"run": run_id, "workflow": workflow, "status": status.as_str()}),
1993 );
1994 }
1995 #[cfg(feature = "a2a")]
1997 if let Some(tid) = &a2a_task {
1998 self.a2a_task_for_run(tid, status.as_str(), output.as_ref(), error.as_deref());
1999 }
2000 self.checkpoint(false);
2001 }
2002
2003 pub(crate) fn cancel_run(&mut self, run_id: &str, reason: &str) {
2005 let kids: Vec<String> = self
2007 .runs
2008 .values()
2009 .filter(|r| {
2010 !r.status.is_terminal()
2011 && r.parent.as_ref().is_some_and(|p| {
2012 p["run"].as_str() == Some(run_id) && p["cascade"].as_bool().unwrap_or(true)
2013 })
2014 })
2015 .map(|r| r.id.clone())
2016 .collect();
2017 for k in kids {
2018 self.cancel_run(&k, "parent run cancelled");
2019 }
2020 self.cancel_children_of_run(run_id, reason);
2021 let timers = self.timers.owned_by(|o| o["run"].as_str() == Some(run_id));
2022 for t in timers {
2023 let _ = self.timers.disarm(&self.durable, &t);
2024 }
2025 self.pending
2026 .retain(|p| !matches!(&p.target, Target::Step(r, _) if r == run_id));
2027 if let Some(r) = self.runs.get_mut(run_id)
2028 && !r.status.is_terminal()
2029 {
2030 r.finish(RunStatus::Cancelled, None, Some(reason.to_string()));
2031 self.on_run_terminal(run_id);
2032 }
2033 }
2034
2035 fn cancel_children_of_run(&mut self, run_id: &str, reason: &str) {
2036 let nodes: Vec<_> = self
2037 .children
2038 .iter()
2039 .filter(|(_, c)| matches!(&c.kind, ChildKind::StepTurn { run, .. } if run == run_id))
2040 .map(|(n, _)| *n)
2041 .collect();
2042 for n in nodes {
2043 self.children.cancel(n, reason);
2044 }
2045 }
2046
2047 pub(crate) fn workflow_tool(
2050 &mut self,
2051 caller: &ToolCaller,
2052 name: &str,
2053 args: Value,
2054 ) -> ToolOutcome {
2055 let err = |e: String| ToolOutcome::Ready(Value::String(e), true);
2056 match name {
2057 "workflow.run" => {
2058 let wname = args["name"].as_str().unwrap_or("").to_string();
2059 let Some(w) = self.workflows.get(&wname) else {
2060 return err(format!("no such workflow {wname:?}"));
2061 };
2062 let start = match args.get("start").and_then(Value::as_str) {
2063 Some(s) => match w.step(s) {
2064 Some(st) if st.is_start() => s.to_string(),
2065 _ => return err(format!("workflow {wname:?} has no start node {s:?}")),
2066 },
2067 None => match default_start(w) {
2068 Some(s) => s,
2069 None => return err(format!("workflow {wname:?} has no start node")),
2070 },
2071 };
2072 let wait = args.get("wait").and_then(Value::as_bool).unwrap_or(false);
2073 let timeout_ms = args
2074 .get("timeout")
2075 .and_then(Value::as_str)
2076 .and_then(|t| crate::config::parse_duration(t).ok())
2077 .map(|d| d.as_millis() as u64)
2078 .unwrap_or(3_600_000);
2079 let request = match (caller.node, &caller.run, &caller.step) {
2080 (Some(n), _, _) => {
2081 json!({"node": n.0, "req": caller.req, "wait": wait, "timeout_ms": timeout_ms})
2082 }
2083 (None, Some(r), Some(s)) => {
2084 json!({"run": r, "step": s, "wait": wait, "timeout_ms": timeout_ms})
2085 }
2086 _ => Value::Null,
2087 };
2088 let payload = json!({"workflow": wname, "node": start, "payload": {"requested_by": caller.label_pub()}, "inputs": args.get("inputs").cloned().unwrap_or(json!({})), "request": request, "conversation": caller.ctx});
2089 match self.accept_event(kinds::WORKFLOW_RUN, caller.principal.clone(), payload) {
2090 Ok(_) => {
2091 if let Some(ev) = self.inbox_queue.pop_back() {
2093 let done = self.on_start_event(&ev);
2094 if done {
2095 self.inbox_done(&ev.id);
2096 }
2097 }
2098 ToolOutcome::Executing
2102 }
2103 Err(e) => err(e),
2104 }
2105 }
2106 "workflow.list" => ToolOutcome::Ready(
2107 json!({"workflows": self.workflows.values().map(|w| json!({
2108 "name": w.name, "description": w.description, "armed": w.armed, "hash": w.hash,
2109 "starts": w.start_steps().iter().map(|s| json!({"node": s.id, "kind": s.kind})).collect::<Vec<_>>(),
2110 "runs": self.runs.values().filter(|r| r.workflow == w.name).map(|r| json!({"id": r.id, "status": r.status})).collect::<Vec<_>>(),
2111 })).collect::<Vec<_>>()}),
2112 false,
2113 ),
2114 "workflow.status" => {
2115 let runs: Vec<Value> = match (
2116 args.get("run").and_then(Value::as_str),
2117 args.get("name").and_then(Value::as_str),
2118 ) {
2119 (Some(id), _) => self
2120 .runs
2121 .get(id)
2122 .map(|r| vec![run_detail(r)])
2123 .unwrap_or_default(),
2124 (None, Some(n)) => self
2125 .runs
2126 .values()
2127 .filter(|r| r.workflow == n)
2128 .map(RunState::summary)
2129 .collect(),
2130 _ => self.runs.values().map(RunState::summary).collect(),
2131 };
2132 ToolOutcome::Ready(json!({"runs": runs}), false)
2133 }
2134 "workflow.cancel" => {
2135 let id = args["run"].as_str().unwrap_or("").to_string();
2136 if !self.runs.contains_key(&id) {
2137 return err(format!("no such run {id:?}"));
2138 }
2139 let reason = args
2140 .get("reason")
2141 .and_then(Value::as_str)
2142 .unwrap_or("cancelled by request")
2143 .to_string();
2144 self.cancel_run(&id, &reason);
2145 ToolOutcome::Ready(
2146 json!({"ok": true, "status": self.runs.get(&id).map(|r| r.status.as_str()).unwrap_or("cancelled")}),
2147 false,
2148 )
2149 }
2150 "workflow.wait" => {
2151 let id = args["run"].as_str().unwrap_or("").to_string();
2152 let timeout_ms = args
2153 .get("timeout")
2154 .and_then(Value::as_str)
2155 .and_then(|t| crate::config::parse_duration(t).ok())
2156 .map(|d| d.as_millis() as u64)
2157 .unwrap_or(3_600_000);
2158 match self.runs.get(&id) {
2159 None => err(format!("no such run {id:?}")),
2160 Some(r) if r.status.is_terminal() => ToolOutcome::Ready(
2161 json!({"run": id, "status": r.status, "output": r.output, "error": r.error}),
2162 false,
2163 ),
2164 Some(_) => ToolOutcome::Deferred(PendingKind::Run {
2165 run: id,
2166 deadline_ms: now_ms() + timeout_ms,
2167 }),
2168 }
2169 }
2170 "workflow.pause" | "workflow.resume" => {
2171 let pause = name == "workflow.pause";
2172 if let Some(id) = args.get("run").and_then(Value::as_str) {
2173 match self.runs.get_mut(id) {
2174 None => return err(format!("no such run {id:?}")),
2175 Some(r) if r.status.is_terminal() => {
2176 return err(format!("run {id:?} is already {}", r.status.as_str()));
2177 }
2178 Some(r) => {
2179 r.status = if pause {
2180 RunStatus::Paused
2181 } else {
2182 RunStatus::Running
2183 };
2184 r.touch();
2185 }
2186 }
2187 return ToolOutcome::Ready(json!({"ok": true}), false);
2188 }
2189 if let Some(n) = args.get("name").and_then(Value::as_str) {
2190 match self.workflows.get_mut(n) {
2191 None => return err(format!("no such workflow {n:?}")),
2192 Some(w) => w.armed = !pause,
2193 }
2194 if !pause {
2195 self.arm_workflows();
2196 }
2197 return ToolOutcome::Ready(json!({"ok": true}), false);
2198 }
2199 err(format!("{name}: give run or name"))
2200 }
2201 "workflow.create" | "workflow.update" => {
2202 let def = args["definition"].clone();
2203 match parse_workflow(&def) {
2204 Err(e) => err(format!("{name}: {}", e.join("; "))),
2205 Ok(w) => {
2206 if name == "workflow.create" && self.workflows.contains_key(&w.name) {
2207 return err(format!(
2208 "workflow {:?} exists (use workflow.update)",
2209 w.name
2210 ));
2211 }
2212 if name == "workflow.update" && !self.workflows.contains_key(&w.name) {
2213 return err(format!(
2214 "workflow {:?} does not exist (use workflow.create)",
2215 w.name
2216 ));
2217 }
2218 let (wname, hash) = (w.name.clone(), w.hash.clone());
2219 let rec = crate::context::memory::Record {
2221 value: def,
2222 ts: now_ms(),
2223 ttl_ms: None,
2224 by: Some(caller.label_pub()),
2225 };
2226 if let Err(e) = self.durable.put(
2227 Kind::Memory,
2228 &format!("{WORKFLOW_DEF_PREFIX}{wname}"),
2229 serde_json::to_value(&rec).unwrap_or(Value::Null),
2230 None,
2231 ) {
2232 return err(format!("{name}: store: {e}"));
2233 }
2234 let arm = args.get("arm").and_then(Value::as_bool).unwrap_or(true);
2235 let mut w = w;
2236 w.armed = arm;
2237 self.workflows.insert(wname.clone(), w);
2238 self.log.info(
2239 "workflow.defined",
2240 json!({"name": wname, "hash": &hash[..12], "op": name}),
2241 );
2242 if arm {
2243 self.arm_workflows();
2244 }
2245 ToolOutcome::Ready(
2246 json!({"name": wname, "hash": hash, "armed": arm}),
2247 false,
2248 )
2249 }
2250 }
2251 }
2252 "workflow.delete" => {
2253 let wname = args["name"].as_str().unwrap_or("").to_string();
2254 if self.workflows.remove(&wname).is_none() {
2255 return err(format!("no such workflow {wname:?}"));
2256 }
2257 let _ = self
2258 .durable
2259 .delete(Kind::Memory, &format!("{WORKFLOW_DEF_PREFIX}{wname}"));
2260 self.log.info("workflow.deleted", json!({"name": wname}));
2261 ToolOutcome::Ready(json!({"ok": true}), false)
2262 }
2263 "workflow.signal" => {
2264 let sname = args["name"].as_str().unwrap_or("").to_string();
2265 let _ = self.accept_event(kinds::SIGNAL, caller.principal.clone(), json!({"name": sname, "payload": args.get("payload").cloned().unwrap_or(Value::Null), "run": args.get("run"), "from": caller.label_pub()}));
2266 ToolOutcome::Ready(
2268 json!({"delivered": 0, "note": "signal recorded; signal start nodes and waits land with the P4 engine"}),
2269 false,
2270 )
2271 }
2272 _ => err(format!("unknown workflow tool {name}")),
2273 }
2274 }
2275}
2276
2277impl ToolCaller {
2278 pub(crate) fn label_pub(&self) -> String {
2279 if let Some(s) = &self.subagent {
2280 return format!("subagent:{s}");
2281 }
2282 if let (Some(r), Some(s)) = (&self.run, &self.step) {
2283 return format!("step:{r}/{s}");
2284 }
2285 format!(
2286 "ctx:{}",
2287 self.ctx.as_deref().unwrap_or(crate::context::ROOT)
2288 )
2289 }
2290}
2291
2292fn default_start(w: &Workflow) -> Option<String> {
2294 let starts = w.start_steps();
2295 starts
2296 .iter()
2297 .find(|s| s.kind == "manual")
2298 .or_else(|| starts.first())
2299 .map(|s| s.id.clone())
2300}
2301
2302fn node_kind<'a>(w: &'a Workflow, node: &str) -> Option<&'a str> {
2303 w.step(node).map(|s| s.kind.as_str())
2304}
2305
2306fn run_detail(r: &RunState) -> Value {
2307 let mut v = r.summary();
2308 v["step_states"] = json!(r.steps);
2309 v["vars"] = Value::Object(r.vars.clone());
2310 v
2311}
2312
2313fn collect_memory_keys(v: &Value, out: &mut Vec<String>) {
2315 match v {
2316 Value::String(s) => {
2317 let mut rest = s.as_str();
2318 while let Some(i) = rest.find("memory.") {
2319 let after = &rest[i + "memory.".len()..];
2320 let key: String = after
2321 .chars()
2322 .take_while(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '/' | ':'))
2323 .collect();
2324 if !key.is_empty() && !out.contains(&key) {
2325 out.push(key.clone());
2326 }
2327 rest = &after[key.len().min(after.len())..];
2328 }
2329 }
2330 Value::Array(a) => a.iter().for_each(|x| collect_memory_keys(x, out)),
2331 Value::Object(o) => o.values().for_each(|x| collect_memory_keys(x, out)),
2332 _ => {}
2333 }
2334}