1use std::collections::{BTreeMap, BTreeSet, HashMap};
22use std::sync::Arc;
23
24use serde::{Deserialize, Deserializer, Serialize, Serializer};
25use serde_json::Value;
26
27use crate::error::ConfigError;
28use crate::orchestration::{AgentMode, call_agent};
29use crate::state::State;
30use crate::text::TextAgent;
31
32pub struct FlowCtx<'a> {
35 pub state: &'a State,
37 pub marking: &'a Marking,
39}
40
41#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, schemars::JsonSchema)]
43#[serde(rename_all = "snake_case")]
44pub enum Pred {
45 Always,
47 IsTrue(String),
49 IsSet(String),
51 Eq(String, Value),
53 Captured(Vec<String>),
55 CalledOk(String),
57 Done(String),
59 All(Vec<Pred>),
61 Any(Vec<Pred>),
63 Not(Box<Pred>),
65}
66
67impl Pred {
68 fn eval(&self, ctx: &FlowCtx) -> bool {
69 match self {
70 Pred::Always => true,
71 Pred::IsTrue(k) => ctx.state.get::<bool>(k) == Some(true),
72 Pred::IsSet(k) => ctx.state.contains(k),
73 Pred::Eq(k, v) => ctx.state.get::<Value>(k).as_ref() == Some(v),
74 Pred::Captured(fields) => fields.iter().all(|f| ctx.state.contains(f)),
75 Pred::CalledOk(t) => ctx.marking.tool_ok.contains_key(t),
76 Pred::Done(s) => ctx.marking.done.contains(s),
77 Pred::All(ps) => ps.iter().all(|p| p.eval(ctx)),
78 Pred::Any(ps) => ps.iter().any(|p| p.eval(ctx)),
79 Pred::Not(p) => !p.eval(ctx),
80 }
81 }
82
83 fn describe(&self) -> String {
85 fn join(ps: &[Pred], sep: &str) -> String {
86 ps.iter().map(Pred::describe).collect::<Vec<_>>().join(sep)
87 }
88 match self {
89 Pred::Always => "nothing".to_string(),
90 Pred::IsTrue(k) => format!("'{k}' must be true"),
91 Pred::IsSet(k) => format!("'{k}' must be known"),
92 Pred::Eq(k, v) => format!("'{k}' must be {v}"),
93 Pred::Captured(fields) => {
94 format!("these must be known: {}", fields.join(", "))
95 }
96 Pred::CalledOk(t) => format!("'{t}' must have run successfully"),
97 Pred::Done(s) => format!("step '{s}' must be complete"),
98 Pred::All(ps) => join(ps, " and "),
99 Pred::Any(ps) => join(ps, " or "),
100 Pred::Not(p) => format!("it must not be the case that {}", p.describe()),
101 }
102 }
103
104 fn referenced_steps(&self, out: &mut Vec<String>) {
106 match self {
107 Pred::Done(s) => out.push(s.clone()),
108 Pred::All(ps) | Pred::Any(ps) => ps.iter().for_each(|p| p.referenced_steps(out)),
109 Pred::Not(p) => p.referenced_steps(out),
110 _ => {}
111 }
112 }
113
114 fn referenced_tools(&self, out: &mut Vec<String>) {
116 match self {
117 Pred::CalledOk(t) => out.push(t.clone()),
118 Pred::All(ps) | Pred::Any(ps) => ps.iter().for_each(|p| p.referenced_tools(out)),
119 Pred::Not(p) => p.referenced_tools(out),
120 _ => {}
121 }
122 }
123
124 fn referenced_state_keys(&self, out: &mut BTreeSet<String>) {
128 match self {
129 Pred::IsTrue(k) | Pred::IsSet(k) | Pred::Eq(k, _) => {
130 out.insert(k.clone());
131 }
132 Pred::Captured(fields) => out.extend(fields.iter().cloned()),
133 Pred::All(ps) | Pred::Any(ps) => {
134 ps.iter().for_each(|p| p.referenced_state_keys(out));
135 }
136 Pred::Not(p) => p.referenced_state_keys(out),
137 _ => {}
138 }
139 }
140
141 fn explain(&self, ctx: &FlowCtx) -> GuardTrace {
145 let children = match self {
146 Pred::All(ps) | Pred::Any(ps) => ps.iter().map(|p| p.explain(ctx)).collect(),
147 Pred::Not(p) => vec![p.explain(ctx)],
148 _ => Vec::new(),
149 };
150 GuardTrace {
151 desc: match self {
152 Pred::All(_) => "all of".to_string(),
155 Pred::Any(_) => "any of".to_string(),
156 Pred::Not(_) => "not".to_string(),
157 atom => atom.describe(),
158 },
159 holds: self.eval(ctx),
160 children,
161 }
162 }
163}
164
165#[derive(Clone, Debug, Serialize, schemars::JsonSchema)]
169pub struct GuardTrace {
170 pub desc: String,
172 pub holds: bool,
174 #[serde(default, skip_serializing_if = "Vec::is_empty")]
176 pub children: Vec<GuardTrace>,
177}
178
179pub fn render_ground(template: &str, state: &State) -> String {
190 let mut out = String::with_capacity(template.len());
191 let mut rest = template;
192 while let Some(open) = rest.find('{') {
193 out.push_str(&rest[..open]);
194 let after = &rest[open + 1..];
195 let Some(close) = after.find('}') else {
196 out.push_str(&rest[open..]);
198 return out;
199 };
200 let expr = &after[..close];
201 out.push_str(&render_expr(expr, state));
202 rest = &after[close + 1..];
203 }
204 out.push_str(rest);
205 out
206}
207
208fn render_expr(expr: &str, state: &State) -> String {
209 if let Some((cond, arms)) = expr.split_once('?') {
210 let (yes, no) = arms.split_once(':').unwrap_or((arms, ""));
211 if is_truthy(state, cond.trim()) {
212 yes.to_string()
213 } else {
214 no.to_string()
215 }
216 } else {
217 match state.get::<Value>(expr.trim()) {
218 Some(Value::String(s)) => s,
219 Some(v) => v.to_string(),
220 None => String::new(),
221 }
222 }
223}
224
225fn is_truthy(state: &State, key: &str) -> bool {
226 match state.get::<Value>(key) {
227 None | Some(Value::Null) => false,
228 Some(Value::Bool(b)) => b,
229 Some(Value::Number(n)) => n.as_f64().map(|f| f != 0.0).unwrap_or(true),
230 Some(Value::String(s)) => !s.is_empty(),
231 Some(_) => true,
232 }
233}
234
235type CustomFn = Arc<dyn Fn(&FlowCtx) -> bool + Send + Sync>;
236
237#[derive(Clone)]
243pub enum Guard {
244 Spec(Pred),
246 Custom(CustomFn),
248}
249
250impl Guard {
251 pub fn always() -> Self {
253 Guard::Spec(Pred::Always)
254 }
255 pub fn is_true(key: impl Into<String>) -> Self {
257 Guard::Spec(Pred::IsTrue(key.into()))
258 }
259 pub fn is_set(key: impl Into<String>) -> Self {
261 Guard::Spec(Pred::IsSet(key.into()))
262 }
263 pub fn eq(key: impl Into<String>, value: impl Into<Value>) -> Self {
265 Guard::Spec(Pred::Eq(key.into(), value.into()))
266 }
267 pub fn captured<I, S>(fields: I) -> Self
269 where
270 I: IntoIterator<Item = S>,
271 S: Into<String>,
272 {
273 Guard::Spec(Pred::Captured(fields.into_iter().map(Into::into).collect()))
274 }
275 pub fn called_ok(tool: impl Into<String>) -> Self {
277 Guard::Spec(Pred::CalledOk(tool.into()))
278 }
279 pub fn done(step: impl Into<String>) -> Self {
281 Guard::Spec(Pred::Done(step.into()))
282 }
283 pub fn resolved(name: impl AsRef<str>) -> Self {
287 Guard::Spec(Pred::IsSet(format!("{}:result", name.as_ref())))
288 }
289 pub fn all(guards: impl IntoIterator<Item = Guard>) -> Self {
298 let guards: Vec<Guard> = guards.into_iter().collect();
299 if guards.iter().all(|g| matches!(g, Guard::Spec(_))) {
300 Guard::Spec(Pred::All(specs_unchecked(guards)))
301 } else {
302 Guard::Custom(Arc::new(move |ctx| guards.iter().all(|g| g.eval(ctx))))
303 }
304 }
305 pub fn any(guards: impl IntoIterator<Item = Guard>) -> Self {
310 let guards: Vec<Guard> = guards.into_iter().collect();
311 if guards.iter().all(|g| matches!(g, Guard::Spec(_))) {
312 Guard::Spec(Pred::Any(specs_unchecked(guards)))
313 } else {
314 Guard::Custom(Arc::new(move |ctx| guards.iter().any(|g| g.eval(ctx))))
315 }
316 }
317 #[allow(clippy::should_implement_trait)]
319 pub fn not(guard: Guard) -> Self {
320 match guard {
321 Guard::Spec(p) => Guard::Spec(Pred::Not(Box::new(p))),
322 Guard::Custom(f) => Guard::Custom(Arc::new(move |ctx| !f(ctx))),
324 }
325 }
326 pub fn describe(&self) -> String {
336 match self {
337 Guard::Spec(p) => p.describe(),
338 Guard::Custom(_) => "a condition set by the application".to_string(),
339 }
340 }
341
342 pub fn custom(f: impl Fn(&FlowCtx) -> bool + Send + Sync + 'static) -> Self {
344 Guard::Custom(Arc::new(f))
345 }
346
347 pub fn eval(&self, ctx: &FlowCtx) -> bool {
349 match self {
350 Guard::Spec(p) => p.eval(ctx),
351 Guard::Custom(f) => f(ctx),
352 }
353 }
354
355 pub fn eval_state(&self, state: &State) -> bool {
361 let marking = Marking::default();
362 self.eval(&FlowCtx {
363 state,
364 marking: &marking,
365 })
366 }
367
368 pub fn explain_trace(&self, ctx: &FlowCtx) -> GuardTrace {
371 match self {
372 Guard::Spec(p) => p.explain(ctx),
373 Guard::Custom(f) => GuardTrace {
374 desc: "a condition set by the application".to_string(),
375 holds: f(ctx),
376 children: Vec::new(),
377 },
378 }
379 }
380
381 fn referenced_steps(&self, out: &mut Vec<String>) {
382 if let Guard::Spec(p) = self {
383 p.referenced_steps(out);
384 }
385 }
386
387 fn referenced_state_keys(&self, out: &mut BTreeSet<String>) {
388 if let Guard::Spec(p) = self {
389 p.referenced_state_keys(out);
390 }
391 }
392
393 fn referenced_tools(&self, out: &mut Vec<String>) {
394 if let Guard::Spec(p) = self {
395 p.referenced_tools(out);
396 }
397 }
398}
399
400fn specs_unchecked(guards: Vec<Guard>) -> Vec<Pred> {
405 guards
406 .into_iter()
407 .map(|g| match g {
408 Guard::Spec(p) => p,
409 Guard::Custom(_) => unreachable!("specs_unchecked called with a custom guard"),
410 })
411 .collect()
412}
413
414impl Serialize for Guard {
415 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
416 match self {
417 Guard::Spec(p) => p.serialize(s),
418 Guard::Custom(_) => Err(serde::ser::Error::custom(
419 "custom guards are not serializable; use Guard atoms for data-driven flows",
420 )),
421 }
422 }
423}
424
425impl<'de> Deserialize<'de> for Guard {
426 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
427 Ok(Guard::Spec(Pred::deserialize(d)?))
428 }
429}
430
431impl schemars::JsonSchema for Guard {
435 fn is_referenceable() -> bool {
436 false
437 }
438 fn schema_name() -> String {
439 "Guard".to_string()
440 }
441 fn json_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
442 generator.subschema_for::<Pred>()
443 }
444}
445
446impl std::fmt::Debug for Guard {
447 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
448 match self {
449 Guard::Spec(p) => write!(f, "{p:?}"),
450 Guard::Custom(_) => write!(f, "Custom(<fn>)"),
451 }
452 }
453}
454
455#[derive(Clone, Debug)]
465pub struct Edge {
466 pub step: String,
468 pub when: Option<Guard>,
470}
471
472impl Edge {
473 pub fn to(step: impl Into<String>) -> Self {
475 Edge {
476 step: step.into(),
477 when: None,
478 }
479 }
480 pub fn when(step: impl Into<String>, when: Guard) -> Self {
482 Edge {
483 step: step.into(),
484 when: Some(when),
485 }
486 }
487}
488
489impl<S: Into<String>> From<S> for Edge {
490 fn from(step: S) -> Self {
491 Edge::to(step)
492 }
493}
494
495impl Serialize for Edge {
496 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
497 match &self.when {
498 None => self.step.serialize(s),
500 Some(when) => {
501 use serde::ser::SerializeStruct;
502 let mut out = s.serialize_struct("Edge", 2)?;
503 out.serialize_field("step", &self.step)?;
504 out.serialize_field("when", when)?;
505 out.end()
506 }
507 }
508 }
509}
510
511impl<'de> Deserialize<'de> for Edge {
512 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
513 #[derive(Deserialize)]
514 #[serde(untagged)]
515 enum Repr {
516 Simple(String),
517 Conditional {
518 step: String,
519 #[serde(default)]
520 when: Option<Guard>,
521 },
522 }
523 Ok(match Repr::deserialize(d)? {
524 Repr::Simple(step) => Edge { step, when: None },
525 Repr::Conditional { step, when } => Edge { step, when },
526 })
527 }
528}
529
530impl schemars::JsonSchema for Edge {
531 fn schema_name() -> String {
532 "Edge".to_string()
533 }
534 fn json_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
535 #[derive(schemars::JsonSchema)]
536 #[serde(untagged)]
537 #[allow(dead_code)]
538 enum EdgeSchema {
539 Simple(String),
540 Conditional { step: String, when: Option<Pred> },
541 }
542 EdgeSchema::json_schema(generator)
543 }
544}
545
546#[derive(
548 Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema,
549)]
550#[serde(rename_all = "snake_case")]
551pub enum Join {
552 #[default]
554 All,
555 Any,
557}
558
559impl Join {
560 fn is_all(&self) -> bool {
561 *self == Join::All
562 }
563}
564
565#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)]
567pub struct Step {
568 pub id: String,
570 #[serde(default, skip_serializing_if = "Vec::is_empty")]
572 pub after: Vec<Edge>,
573 #[serde(default, skip_serializing_if = "Join::is_all")]
575 pub join: Join,
576 #[serde(default, skip_serializing_if = "Option::is_none")]
578 pub gate: Option<Guard>,
579 #[serde(default, skip_serializing_if = "Option::is_none")]
581 pub done: Option<Guard>,
582 #[serde(default, skip_serializing_if = "Option::is_none")]
584 pub posture: Option<String>,
585 #[serde(default, skip_serializing_if = "Option::is_none")]
589 pub ground: Option<String>,
590 #[serde(default, skip_serializing_if = "Vec::is_empty")]
592 pub allow: Vec<String>,
593 #[serde(default, skip_serializing_if = "Vec::is_empty")]
595 pub deny: Vec<String>,
596 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
598 pub terminal: bool,
599}
600
601#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)]
603#[serde(rename_all = "snake_case")]
604pub enum Constraint {
605 Once(String),
607 Before(String, String),
609 NeverUntil {
611 tool: String,
613 until: Guard,
615 },
616 Require(Vec<String>),
618 Reset {
627 steps: Vec<String>,
629 when: Guard,
631 },
632}
633
634#[derive(Clone, Debug, Default, Serialize, Deserialize, schemars::JsonSchema)]
636pub struct Flow {
637 pub steps: Vec<Step>,
639 #[serde(default, skip_serializing_if = "Vec::is_empty")]
641 pub constraints: Vec<Constraint>,
642 #[serde(default, skip_serializing_if = "Vec::is_empty")]
644 pub confirm_tools: Vec<String>,
645 #[serde(default, skip_serializing_if = "Vec::is_empty")]
654 pub ambient: Vec<String>,
655}
656
657impl Flow {
658 #[allow(
660 clippy::new_ret_no_self,
661 reason = "Flow::new() is the builder entry point; a Flow comes from FlowBuilder::build/compile"
662 )]
663 pub fn new() -> FlowBuilder {
664 FlowBuilder::default()
665 }
666
667 fn step(&self, id: &str) -> Option<&Step> {
668 self.steps.iter().find(|s| s.id == id)
669 }
670
671 pub fn validate(&self) -> Result<(), ConfigError> {
674 let mut errs = Vec::new();
675 let ids: BTreeSet<&str> = self.steps.iter().map(|s| s.id.as_str()).collect();
676 if ids.len() != self.steps.len() {
677 errs.push("duplicate step ids".into());
678 }
679 for s in &self.steps {
680 for d in &s.after {
681 if !ids.contains(d.step.as_str()) {
682 errs.push(format!(
683 "step '{}' depends on unknown step '{}'",
684 s.id, d.step
685 ));
686 }
687 if let Some(when) = &d.when {
688 let mut refs = Vec::new();
689 when.referenced_steps(&mut refs);
690 for r in refs {
691 if !ids.contains(r.as_str()) {
692 errs.push(format!(
693 "step '{}' edge condition references unknown step '{r}'",
694 s.id
695 ));
696 }
697 }
698 }
699 }
700 if !s.terminal && s.done.is_none() {
701 errs.push(format!(
702 "non-terminal step '{}' has no `done` condition (it can never complete)",
703 s.id
704 ));
705 }
706 for g in s.gate.iter().chain(s.done.iter()) {
707 let mut refs = Vec::new();
708 g.referenced_steps(&mut refs);
709 for r in refs {
710 if !ids.contains(r.as_str()) {
711 errs.push(format!(
712 "step '{}' guard references unknown step '{r}'",
713 s.id
714 ));
715 }
716 }
717 }
718 }
719 for c in &self.constraints {
720 match c {
721 Constraint::Before(a, b) => {
722 for x in [a, b] {
723 if !ids.contains(x.as_str()) {
724 errs.push(format!("constraint `before` references unknown step '{x}'"));
725 }
726 }
727 }
728 Constraint::Require(rs) => {
729 for r in rs {
730 if !ids.contains(r.as_str()) {
731 errs.push(format!(
732 "constraint `require` references unknown step '{r}'"
733 ));
734 }
735 }
736 }
737 Constraint::Reset { steps, .. } => {
738 for r in steps {
739 if !ids.contains(r.as_str()) {
740 errs.push(format!("constraint `reset` references unknown step '{r}'"));
741 }
742 }
743 }
744 _ => {}
745 }
746 }
747 if self.has_cycle() {
748 errs.push("flow dependency graph has a cycle (must be a DAG)".into());
749 }
750 ConfigError::from_issues(errs)
751 }
752
753 fn tool_universe(&self) -> BTreeSet<String> {
756 let mut tools = BTreeSet::new();
757 for s in &self.steps {
758 tools.extend(s.allow.iter().cloned());
759 tools.extend(s.deny.iter().cloned());
760 }
761 for c in &self.constraints {
762 match c {
763 Constraint::Once(t) => {
764 tools.insert(t.clone());
765 }
766 Constraint::NeverUntil { tool, .. } => {
767 tools.insert(tool.clone());
768 }
769 _ => {}
770 }
771 }
772 tools.extend(self.confirm_tools.iter().cloned());
773 tools.extend(self.ambient.iter().cloned());
774 tools
775 }
776
777 pub fn state_keys_read(&self) -> BTreeSet<String> {
787 let mut keys = BTreeSet::new();
788 for s in &self.steps {
789 for g in s.gate.iter().chain(s.done.iter()) {
790 g.referenced_state_keys(&mut keys);
791 }
792 for e in &s.after {
793 if let Some(when) = &e.when {
794 when.referenced_state_keys(&mut keys);
795 }
796 }
797 }
798 for c in &self.constraints {
799 match c {
800 Constraint::NeverUntil { until, .. } => until.referenced_state_keys(&mut keys),
801 Constraint::Reset { when, .. } => when.referenced_state_keys(&mut keys),
802 _ => {}
803 }
804 }
805 keys
806 }
807
808 fn reachable_steps(&self) -> BTreeSet<String> {
811 let ids: BTreeSet<&str> = self.steps.iter().map(|s| s.id.as_str()).collect();
812 let mut succ: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
814 for s in &self.steps {
815 for d in &s.after {
816 if ids.contains(d.step.as_str()) {
817 succ.entry(d.step.as_str()).or_default().push(s.id.as_str());
818 }
819 }
820 }
821 for c in &self.constraints {
822 if let Constraint::Before(a, b) = c
823 && ids.contains(a.as_str())
824 && ids.contains(b.as_str())
825 {
826 succ.entry(a.as_str()).or_default().push(b.as_str());
827 }
828 }
829 let roots: Vec<&str> = self
830 .steps
831 .iter()
832 .filter(|s| s.after.is_empty())
833 .map(|s| s.id.as_str())
834 .collect();
835 let mut seen = BTreeSet::new();
836 let mut stack = roots;
837 while let Some(id) = stack.pop() {
838 if seen.insert(id.to_string())
839 && let Some(next) = succ.get(id)
840 {
841 stack.extend(next.iter().copied());
842 }
843 }
844 seen
845 }
846
847 pub fn compile(self) -> Result<CompiledFlow, FlowErrors> {
862 self.compile_internal(None)
863 }
864
865 pub fn compile_with_tools(self, tools: &[&str]) -> Result<CompiledFlow, FlowErrors> {
878 self.compile_internal(Some(tools))
879 }
880
881 fn compile_internal(self, registry: Option<&[&str]>) -> Result<CompiledFlow, FlowErrors> {
882 let mut errors = Vec::new();
883 if let Err(err) = self.validate() {
884 errors.extend(err.issues.into_iter().map(FlowError::Invalid));
885 }
886
887 if errors.is_empty() {
889 let reachable = self.reachable_steps();
891 for s in &self.steps {
892 if !reachable.contains(&s.id) {
893 errors.push(FlowError::UnreachableStep(s.id.clone()));
894 }
895 }
896 if let Some(cycle) = self.ordering_cycle() {
900 errors.push(FlowError::OrderingCycle(cycle));
901 }
902 }
903
904 for tool in &self.confirm_tools {
907 let guard = self.constraints.iter().find_map(|c| match c {
908 Constraint::NeverUntil { tool: t, until } if t == tool => Some(until),
909 _ => None,
910 });
911 let unguarded = matches!(guard, None | Some(Guard::Spec(Pred::Always)));
912 if unguarded {
913 errors.push(FlowError::UnguardedCommitTool(tool.clone()));
914 }
915 }
916
917 let ids: BTreeSet<&str> = self.steps.iter().map(|s| s.id.as_str()).collect();
922 for c in &self.constraints {
923 if let Constraint::NeverUntil { tool, until } = c {
924 let mut refs = Vec::new();
925 until.referenced_steps(&mut refs);
926 for r in refs {
927 if !ids.contains(r.as_str()) {
928 errors.push(FlowError::UnsatisfiableGuard {
929 tool: tool.clone(),
930 step: r,
931 });
932 }
933 }
934 }
935 }
936
937 if let Some(known) = registry {
939 for tool in self.tool_universe() {
940 if !known.contains(&tool.as_str()) {
941 errors.push(FlowError::UnknownTool(tool));
942 }
943 }
944 }
945
946 if errors.is_empty() {
947 let surface = ToolSurface {
948 tools: self.tool_universe(),
949 };
950 Ok(CompiledFlow {
951 flow: self,
952 surface,
953 })
954 } else {
955 Err(FlowErrors(errors))
956 }
957 }
958
959 fn ordering_cycle(&self) -> Option<Vec<String>> {
963 let mut deps: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
965 for s in &self.steps {
966 let entry = deps.entry(s.id.as_str()).or_default();
967 entry.extend(s.after.iter().map(|e| e.step.as_str()));
968 }
969 for c in &self.constraints {
970 if let Constraint::Before(a, b) = c {
971 deps.entry(b.as_str()).or_default().push(a.as_str());
972 }
973 }
974 fn dfs<'a>(
976 id: &'a str,
977 deps: &BTreeMap<&'a str, Vec<&'a str>>,
978 color: &mut BTreeMap<&'a str, u8>,
979 path: &mut Vec<&'a str>,
980 ) -> Option<Vec<String>> {
981 color.insert(id, 1);
982 path.push(id);
983 for d in deps.get(id).into_iter().flatten() {
984 match color.get(d).copied() {
985 Some(1) => {
986 let start = path.iter().position(|p| p == d).unwrap_or(0);
987 return Some(
988 path[start..]
989 .iter()
990 .map(std::string::ToString::to_string)
991 .collect(),
992 );
993 }
994 Some(2) => {}
995 _ => {
996 if let Some(cycle) = dfs(d, deps, color, path) {
997 return Some(cycle);
998 }
999 }
1000 }
1001 }
1002 path.pop();
1003 color.insert(id, 2);
1004 None
1005 }
1006 let mut color: BTreeMap<&str, u8> = BTreeMap::new();
1007 for s in &self.steps {
1008 if color.get(s.id.as_str()).copied().unwrap_or(0) == 0 {
1009 let mut path = Vec::new();
1010 if let Some(cycle) = dfs(&s.id, &deps, &mut color, &mut path) {
1011 return Some(cycle);
1012 }
1013 }
1014 }
1015 None
1016 }
1017
1018 fn has_cycle(&self) -> bool {
1019 let mut color: BTreeMap<&str, u8> = BTreeMap::new();
1021 fn dfs<'a>(flow: &'a Flow, id: &'a str, color: &mut BTreeMap<&'a str, u8>) -> bool {
1022 color.insert(id, 1);
1023 if let Some(step) = flow.step(id) {
1024 for d in &step.after {
1025 match color.get(d.step.as_str()).copied() {
1026 Some(1) => return true,
1027 Some(2) => {}
1028 _ => {
1029 if dfs(flow, &d.step, color) {
1030 return true;
1031 }
1032 }
1033 }
1034 }
1035 }
1036 color.insert(id, 2);
1037 false
1038 }
1039 for s in &self.steps {
1040 if color.get(s.id.as_str()).copied().unwrap_or(0) == 0 && dfs(self, &s.id, &mut color) {
1041 return true;
1042 }
1043 }
1044 false
1045 }
1046
1047 pub fn to_mermaid(&self) -> String {
1049 let mut out = String::from("flowchart TD\n");
1050 for s in &self.steps {
1051 let shape = if s.terminal {
1052 format!(" {}([{}])\n", s.id, s.id)
1053 } else {
1054 format!(" {}[{}]\n", s.id, s.id)
1055 };
1056 out.push_str(&shape);
1057 }
1058 for s in &self.steps {
1059 for d in &s.after {
1060 match &d.when {
1061 Some(when) => {
1062 let label = when.describe().replace('|', "/");
1063 out.push_str(&format!(" {} -->|{label}| {}\n", d.step, s.id));
1064 }
1065 None => out.push_str(&format!(" {} --> {}\n", d.step, s.id)),
1066 }
1067 }
1068 }
1069 out
1070 }
1071}
1072
1073#[derive(Clone, Debug, Default)]
1076pub struct Marking {
1077 pub done: BTreeSet<String>,
1079 pub tool_ok: BTreeMap<String, u32>,
1081 pub turns: u32,
1083}
1084
1085#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1087#[serde(rename_all = "snake_case")]
1088pub enum Verdict {
1089 Pending,
1091 Active,
1093 Done,
1095 Skipped,
1097}
1098
1099#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
1101pub struct Violation {
1102 pub subject: String,
1104 pub reason: String,
1106}
1107
1108#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
1113pub enum Enforcement {
1114 #[default]
1116 Enforce,
1117 Observe,
1119}
1120
1121#[derive(Clone)]
1126pub struct StepAction {
1127 name: Option<String>,
1128 agent: Arc<dyn TextAgent>,
1129 mode: AgentMode,
1130}
1131
1132pub fn on_enter(agent: Arc<dyn TextAgent>, mode: AgentMode) -> StepAction {
1140 StepAction {
1141 name: None,
1142 agent,
1143 mode,
1144 }
1145}
1146
1147impl StepAction {
1148 pub fn named(mut self, name: impl Into<String>) -> Self {
1150 self.name = Some(name.into());
1151 self
1152 }
1153
1154 pub(crate) async fn fire(&self, step_id: &str, state: &State) {
1157 let name = self.name.clone().unwrap_or_else(|| step_id.to_string());
1158 match self.mode {
1159 AgentMode::Call => {
1160 let _ = call_agent(&name, self.agent.clone(), state).await;
1161 }
1162 AgentMode::Dispatch | AgentMode::Background => {
1163 let agent = self.agent.clone();
1164 let state = state.clone();
1165 tokio::spawn(async move {
1166 let _ = call_agent(&name, agent, &state).await;
1167 });
1168 }
1169 }
1170 }
1171}
1172
1173pub type SharedFlowMonitor = Arc<parking_lot::Mutex<FlowMonitor>>;
1179
1180pub struct FlowMonitor {
1183 flow: Flow,
1184 mode: Enforcement,
1185 marking: Marking,
1186 violations: Vec<Violation>,
1187 enter_actions: HashMap<String, StepAction>,
1189 announced: BTreeSet<String>,
1191 reset_prev: Vec<bool>,
1194}
1195
1196impl FlowMonitor {
1197 pub fn new(flow: Flow, mode: Enforcement) -> Self {
1203 Self {
1204 flow,
1205 mode,
1206 marking: Marking::default(),
1207 violations: Vec::new(),
1208 enter_actions: HashMap::new(),
1209 announced: BTreeSet::new(),
1210 reset_prev: Vec::new(),
1211 }
1212 }
1213
1214 pub fn compiled(flow: CompiledFlow, mode: Enforcement) -> Self {
1216 Self::new(flow.into_flow(), mode)
1217 }
1218
1219 pub fn try_new(flow: Flow, mode: Enforcement) -> Result<Self, FlowErrors> {
1222 Ok(Self::compiled(flow.compile()?, mode))
1223 }
1224
1225 pub fn into_shared(self) -> SharedFlowMonitor {
1230 Arc::new(parking_lot::Mutex::new(self))
1231 }
1232
1233 pub fn explain(&self, state: &State) -> FlowExplanation {
1239 let active_steps = self.active_steps(state);
1240 let active: Vec<String> = active_steps.iter().map(|s| s.id.clone()).collect();
1241 let ctx = self.ctx(state);
1242 let active_progress = active_steps
1243 .iter()
1244 .filter_map(|s| {
1245 s.done
1246 .as_ref()
1247 .map(|g| (s.id.clone(), g.explain_trace(&ctx)))
1248 })
1249 .collect();
1250 let mut allowed_tools = Vec::new();
1251 let mut blocked_tools = BTreeMap::new();
1252 for tool in self.flow.tool_universe() {
1253 match self.admits_tool(&tool, state) {
1254 Ok(()) => allowed_tools.push(tool),
1255 Err(reason) => {
1256 blocked_tools.insert(tool, reason);
1257 }
1258 }
1259 }
1260 FlowExplanation {
1261 active,
1262 allowed_tools,
1263 blocked_tools,
1264 missing_requirements: self.unmet_requirements(),
1265 active_progress,
1266 }
1267 }
1268
1269 pub fn on_enter(mut self, step: impl Into<String>, action: StepAction) -> Self {
1272 self.enter_actions.insert(step.into(), action);
1273 self
1274 }
1275
1276 pub fn take_newly_active(&mut self, state: &State) -> Vec<String> {
1279 let mut fresh = Vec::new();
1280 for s in self.active_steps(state) {
1281 if !self.announced.contains(&s.id) {
1282 fresh.push(s.id.clone());
1283 }
1284 }
1285 for id in &fresh {
1286 self.announced.insert(id.clone());
1287 }
1288 fresh
1289 }
1290
1291 pub fn enter_action(&self, step: &str) -> Option<&StepAction> {
1293 self.enter_actions.get(step)
1294 }
1295
1296 pub async fn fire_enter_actions(&mut self, state: &State) {
1300 for id in self.take_newly_active(state) {
1301 if let Some(action) = self.enter_actions.get(&id) {
1302 action.fire(&id, state).await;
1303 }
1304 }
1305 }
1306
1307 pub fn mode(&self) -> Enforcement {
1309 self.mode
1310 }
1311
1312 pub fn set_posture(&mut self, step_id: &str, posture: Option<String>) -> bool {
1318 match self.flow.steps.iter_mut().find(|s| s.id == step_id) {
1319 Some(step) => {
1320 step.posture = posture;
1321 true
1322 }
1323 None => false,
1324 }
1325 }
1326
1327 pub fn set_ground(&mut self, step_id: &str, ground: Option<String>) -> bool {
1330 match self.flow.steps.iter_mut().find(|s| s.id == step_id) {
1331 Some(step) => {
1332 step.ground = ground;
1333 true
1334 }
1335 None => false,
1336 }
1337 }
1338
1339 pub fn eval(&self, guard: &Guard, state: &State) -> bool {
1343 guard.eval(&self.ctx(state))
1344 }
1345 pub fn marking(&self) -> &Marking {
1347 &self.marking
1348 }
1349 pub fn violations(&self) -> &[Violation] {
1351 &self.violations
1352 }
1353 pub fn flow(&self) -> &Flow {
1355 &self.flow
1356 }
1357
1358 fn ctx<'a>(&'a self, state: &'a State) -> FlowCtx<'a> {
1359 FlowCtx {
1360 state,
1361 marking: &self.marking,
1362 }
1363 }
1364
1365 fn eligible(&self, step: &Step, state: &State) -> bool {
1366 let ctx = self.ctx(state);
1367 let edge_ok = |e: &Edge| {
1368 self.marking.done.contains(&e.step)
1369 && e.when.as_ref().map(|g| g.eval(&ctx)).unwrap_or(true)
1370 };
1371 let deps_done = if step.after.is_empty() {
1372 true
1373 } else {
1374 match step.join {
1375 Join::All => step.after.iter().all(edge_ok),
1376 Join::Any => step.after.iter().any(edge_ok),
1377 }
1378 };
1379 let before_ok = self.flow.constraints.iter().all(|c| match c {
1382 Constraint::Before(a, b) if *b == step.id => self.marking.done.contains(a),
1383 _ => true,
1384 });
1385 let gate_ok = step
1386 .gate
1387 .as_ref()
1388 .map(|g| g.eval(&self.ctx(state)))
1389 .unwrap_or(true);
1390 deps_done && before_ok && gate_ok
1391 }
1392
1393 pub fn relatch(&mut self, state: &State) {
1401 self.apply_resets(state);
1402 loop {
1403 let mut newly_done: Vec<String> = Vec::new();
1404 for s in &self.flow.steps {
1405 if self.marking.done.contains(&s.id) {
1406 continue;
1407 }
1408 if !self.eligible(s, state) {
1409 continue;
1410 }
1411 let complete = if s.terminal {
1412 true
1413 } else {
1414 s.done
1415 .as_ref()
1416 .map(|g| g.eval(&self.ctx(state)))
1417 .unwrap_or(false)
1418 };
1419 if complete {
1420 newly_done.push(s.id.clone());
1421 }
1422 }
1423 if newly_done.is_empty() {
1424 break;
1425 }
1426 for id in newly_done {
1427 self.marking.done.insert(id);
1428 }
1429 }
1430 }
1431
1432 fn apply_resets(&mut self, state: &State) {
1434 let mut edges: Vec<(usize, bool)> = Vec::new();
1436 {
1437 let ctx = self.ctx(state);
1438 for (i, c) in self.flow.constraints.iter().enumerate() {
1439 if let Constraint::Reset { when, .. } = c {
1440 edges.push((i, when.eval(&ctx)));
1441 }
1442 }
1443 }
1444 for (slot, (index, now)) in edges.into_iter().enumerate() {
1445 let prev = self.reset_prev.get(slot).copied().unwrap_or(false);
1446 if self.reset_prev.len() <= slot {
1447 self.reset_prev.resize(slot + 1, false);
1448 }
1449 self.reset_prev[slot] = now;
1450 if !now || prev {
1451 continue;
1452 }
1453 let Constraint::Reset { steps, .. } = &self.flow.constraints[index] else {
1454 continue;
1455 };
1456 let steps = steps.clone();
1457 for step_id in &steps {
1458 if !self.marking.done.remove(step_id) {
1459 continue;
1460 }
1461 self.announced.remove(step_id);
1462 if let Some(step) = self.flow.steps.iter().find(|s| &s.id == step_id) {
1466 let mut tools = Vec::new();
1467 if let Some(done) = &step.done {
1468 done.referenced_tools(&mut tools);
1469 }
1470 for tool in tools {
1471 self.marking.tool_ok.remove(&tool);
1472 }
1473 }
1474 }
1475 }
1476 }
1477
1478 pub fn on_turn(&mut self, state: &State) {
1480 self.marking.turns += 1;
1481 self.relatch(state);
1482 }
1483
1484 pub fn on_tool_ok(&mut self, tool: &str, state: &State) {
1486 *self.marking.tool_ok.entry(tool.to_string()).or_insert(0) += 1;
1487 self.relatch(state);
1488 }
1489
1490 pub fn active_steps(&self, state: &State) -> Vec<&Step> {
1492 self.flow
1493 .steps
1494 .iter()
1495 .filter(|s| !self.marking.done.contains(&s.id) && self.eligible(s, state))
1496 .collect()
1497 }
1498
1499 pub fn active_postures(&self, state: &State) -> Vec<String> {
1501 self.active_steps(state)
1502 .into_iter()
1503 .filter_map(|s| s.posture.clone())
1504 .collect()
1505 }
1506
1507 pub fn active_grounds(&self, state: &State) -> Vec<String> {
1510 self.active_steps(state)
1511 .into_iter()
1512 .filter_map(|s| s.ground.as_ref().map(|t| render_ground(t, state)))
1513 .filter(|s| !s.trim().is_empty())
1514 .collect()
1515 }
1516
1517 pub fn unmet_requirements(&self) -> Vec<String> {
1519 self.flow
1520 .constraints
1521 .iter()
1522 .flat_map(|c| match c {
1523 Constraint::Require(rs) => rs.clone(),
1524 _ => Vec::new(),
1525 })
1526 .filter(|r| !self.marking.done.contains(r))
1527 .collect()
1528 }
1529
1530 pub fn is_complete(&self) -> bool {
1532 self.unmet_requirements().is_empty()
1533 }
1534
1535 pub fn verdict(&self, step_id: &str, state: &State) -> Verdict {
1537 if self.marking.done.contains(step_id) {
1538 return Verdict::Done;
1539 }
1540 if let Some(step) = self.flow.step(step_id)
1541 && self.eligible(step, state)
1542 {
1543 return Verdict::Active;
1544 }
1545 let bypassed = self.flow.steps.iter().any(|s| {
1547 s.after.iter().any(|d| d.step == step_id) && self.marking.done.contains(&s.id)
1548 });
1549 if bypassed {
1550 Verdict::Skipped
1551 } else {
1552 Verdict::Pending
1553 }
1554 }
1555
1556 pub fn admits_tool(&self, tool: &str, state: &State) -> Result<(), String> {
1559 match self.admissibility(tool, state) {
1560 Ok(()) => Ok(()),
1561 Err(denial) => Err(self.render_denial(tool, &denial, state)),
1562 }
1563 }
1564
1565 fn admissibility(&self, tool: &str, state: &State) -> Result<(), Denial> {
1571 for c in &self.flow.constraints {
1573 if let Constraint::Once(t) = c
1574 && t == tool
1575 && self.marking.tool_ok.contains_key(tool)
1576 {
1577 return Err(Denial::OnceExhausted);
1578 }
1579 }
1580 for c in &self.flow.constraints {
1582 if let Constraint::NeverUntil { tool: t, until } = c
1583 && t == tool
1584 && !until.eval(&self.ctx(state))
1585 {
1586 return Err(Denial::NotYet(until.describe()));
1587 }
1588 }
1589 let active = self.active_steps(state);
1591 if let Some(step) = active.iter().find(|s| s.deny.iter().any(|d| d == tool)) {
1592 return Err(Denial::DeniedByStep(step.id.clone()));
1593 }
1594 if self.flow.ambient.iter().any(|a| a == tool) {
1597 return Ok(());
1598 }
1599 let restricting: Vec<&&Step> = active.iter().filter(|s| !s.allow.is_empty()).collect();
1600 if !restricting.is_empty()
1601 && !restricting
1602 .iter()
1603 .any(|s| s.allow.iter().any(|a| a == tool))
1604 {
1605 return Err(Denial::NotInStep(
1606 restricting.iter().map(|s| s.id.clone()).collect(),
1607 ));
1608 }
1609 Ok(())
1610 }
1611
1612 fn render_denial(&self, tool: &str, denial: &Denial, state: &State) -> String {
1623 let head = match denial {
1624 Denial::OnceExhausted => {
1625 return format!(
1627 "'{tool}' has already run and may run only once in this \
1628 conversation. Do not call it again."
1629 );
1630 }
1631 Denial::NotYet(condition) => {
1632 format!("'{tool}' is not permitted yet — first, {condition}.")
1633 }
1634 Denial::DeniedByStep(step) => {
1635 format!("'{tool}' is not allowed during the current step ('{step}').")
1636 }
1637 Denial::NotInStep(steps) => format!(
1638 "'{tool}' is not part of the current step ({}).",
1639 steps
1640 .iter()
1641 .map(|s| format!("'{s}'"))
1642 .collect::<Vec<_>>()
1643 .join(", ")
1644 ),
1645 };
1646
1647 let available: Vec<String> = self
1650 .flow
1651 .tool_universe()
1652 .into_iter()
1653 .filter(|t| self.admissibility(t, state).is_ok())
1654 .collect();
1655
1656 let mut out = head;
1657 if available.is_empty() {
1658 out.push_str(" No tool is available right now — continue the conversation instead.");
1659 } else {
1660 out.push_str(" Available now: ");
1661 out.push_str(&available.join(", "));
1662 out.push('.');
1663 }
1664 let postures = self.active_postures(state);
1668 if let Some(first) = postures.first() {
1669 out.push(' ');
1670 out.push_str(first);
1671 }
1672 out
1673 }
1674
1675 pub fn observe_tool(&mut self, tool: &str, ok: bool, state: &State) {
1679 if self.mode == Enforcement::Observe
1680 && let Err(reason) = self.admits_tool(tool, state)
1681 {
1682 self.violations.push(Violation {
1683 subject: tool.to_string(),
1684 reason,
1685 });
1686 }
1687 if ok {
1688 self.on_tool_ok(tool, state);
1689 }
1690 }
1691}
1692
1693enum Denial {
1699 OnceExhausted,
1701 NotYet(String),
1704 DeniedByStep(String),
1706 NotInStep(Vec<String>),
1709}
1710
1711#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1713#[serde(tag = "kind", content = "detail", rename_all = "snake_case")]
1714pub enum FlowError {
1715 Invalid(String),
1717 UnreachableStep(String),
1719 UnguardedCommitTool(String),
1722 UnknownTool(String),
1726 UnsatisfiableGuard {
1730 tool: String,
1732 step: String,
1734 },
1735 OrderingCycle(Vec<String>),
1739}
1740
1741impl std::fmt::Display for FlowError {
1742 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1743 match self {
1744 FlowError::Invalid(m) => write!(f, "{m}"),
1745 FlowError::UnreachableStep(id) => {
1746 write!(f, "step '{id}' is unreachable from any root")
1747 }
1748 FlowError::UnguardedCommitTool(t) => write!(
1749 f,
1750 "commit tool '{t}' is guarded by an always-true condition (effectively unguarded)"
1751 ),
1752 FlowError::UnknownTool(t) => write!(
1753 f,
1754 "flow references tool '{t}' which is not in the provided tool registry"
1755 ),
1756 FlowError::UnsatisfiableGuard { tool, step } => write!(
1757 f,
1758 "`never('{tool}').until(..)` references unknown step '{step}' — the guard can \
1759 never hold, so '{tool}' would be forbidden forever"
1760 ),
1761 FlowError::OrderingCycle(steps) => write!(
1762 f,
1763 "ordering cycle across `after`/`before` edges: {} (no step on it can ever start)",
1764 steps.join(" -> ")
1765 ),
1766 }
1767 }
1768}
1769
1770#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1772pub struct FlowErrors(pub Vec<FlowError>);
1773
1774impl std::fmt::Display for FlowErrors {
1775 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1776 writeln!(f, "flow failed to compile ({} error(s)):", self.0.len())?;
1777 for e in &self.0 {
1778 writeln!(f, " - {e}")?;
1779 }
1780 Ok(())
1781 }
1782}
1783
1784impl std::error::Error for FlowErrors {}
1785
1786#[derive(Debug, Clone, Default)]
1792pub struct ToolSurface {
1793 pub tools: BTreeSet<String>,
1795}
1796
1797#[derive(Debug, Clone)]
1803pub struct CompiledFlow {
1804 flow: Flow,
1805 surface: ToolSurface,
1806}
1807
1808impl CompiledFlow {
1809 pub fn flow(&self) -> &Flow {
1811 &self.flow
1812 }
1813 pub fn tool_surface(&self) -> &ToolSurface {
1815 &self.surface
1816 }
1817 pub fn to_mermaid(&self) -> String {
1819 self.flow.to_mermaid()
1820 }
1821 pub fn into_flow(self) -> Flow {
1823 self.flow
1824 }
1825}
1826
1827#[derive(Debug, Clone, Serialize)]
1833pub struct FlowExplanation {
1834 pub active: Vec<String>,
1836 pub allowed_tools: Vec<String>,
1838 pub blocked_tools: BTreeMap<String, String>,
1840 pub missing_requirements: Vec<String>,
1842 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1845 pub active_progress: BTreeMap<String, GuardTrace>,
1846}
1847
1848#[derive(Default)]
1850pub struct FlowBuilder {
1851 steps: Vec<Step>,
1852 constraints: Vec<Constraint>,
1853 confirm_tools: Vec<String>,
1854 ambient: Vec<String>,
1855}
1856
1857impl FlowBuilder {
1858 fn current(&mut self) -> &mut Step {
1859 self.steps
1860 .last_mut()
1861 .expect("call `.step(id)` before configuring a step")
1862 }
1863
1864 pub fn step(mut self, id: impl Into<String>) -> Self {
1866 self.steps.push(Step {
1867 id: id.into(),
1868 after: Vec::new(),
1869 join: Join::default(),
1870 gate: None,
1871 done: None,
1872 posture: None,
1873 ground: None,
1874 allow: Vec::new(),
1875 deny: Vec::new(),
1876 terminal: false,
1877 });
1878 self
1879 }
1880 pub fn after(mut self, dep: impl Into<String>) -> Self {
1882 self.current().after.push(Edge::to(dep));
1883 self
1884 }
1885 pub fn after_when(mut self, dep: impl Into<String>, when: Guard) -> Self {
1889 self.current().after.push(Edge::when(dep, when));
1890 self
1891 }
1892 pub fn join_any(mut self) -> Self {
1895 self.current().join = Join::Any;
1896 self
1897 }
1898 pub fn gate(mut self, g: Guard) -> Self {
1900 self.current().gate = Some(g);
1901 self
1902 }
1903 pub fn done(mut self, g: Guard) -> Self {
1905 self.current().done = Some(g);
1906 self
1907 }
1908 pub fn posture(mut self, text: impl Into<String>) -> Self {
1910 self.current().posture = Some(text.into());
1911 self
1912 }
1913 pub fn ground(mut self, template: impl Into<String>) -> Self {
1918 self.current().ground = Some(template.into());
1919 self
1920 }
1921 pub fn allow<I, S>(mut self, tools: I) -> Self
1923 where
1924 I: IntoIterator<Item = S>,
1925 S: Into<String>,
1926 {
1927 self.current()
1928 .allow
1929 .extend(tools.into_iter().map(Into::into));
1930 self
1931 }
1932 pub fn deny<I, S>(mut self, tools: I) -> Self
1934 where
1935 I: IntoIterator<Item = S>,
1936 S: Into<String>,
1937 {
1938 self.current()
1939 .deny
1940 .extend(tools.into_iter().map(Into::into));
1941 self
1942 }
1943 pub fn terminal(mut self) -> Self {
1945 self.current().terminal = true;
1946 self
1947 }
1948
1949 pub fn once(mut self, tool: impl Into<String>) -> Self {
1951 self.constraints.push(Constraint::Once(tool.into()));
1952 self
1953 }
1954 pub fn before(mut self, a: impl Into<String>, b: impl Into<String>) -> Self {
1956 self.constraints
1957 .push(Constraint::Before(a.into(), b.into()));
1958 self
1959 }
1960 pub fn require<I, S>(mut self, steps: I) -> Self
1962 where
1963 I: IntoIterator<Item = S>,
1964 S: Into<String>,
1965 {
1966 self.constraints.push(Constraint::Require(
1967 steps.into_iter().map(Into::into).collect(),
1968 ));
1969 self
1970 }
1971 pub fn ambient<I, S>(mut self, tools: I) -> Self
1990 where
1991 I: IntoIterator<Item = S>,
1992 S: Into<String>,
1993 {
1994 self.ambient.extend(tools.into_iter().map(Into::into));
1995 self
1996 }
1997
1998 pub fn reset<I, S>(self, steps: I) -> ResetBuilder
2002 where
2003 I: IntoIterator<Item = S>,
2004 S: Into<String>,
2005 {
2006 ResetBuilder {
2007 builder: self,
2008 steps: steps.into_iter().map(Into::into).collect(),
2009 }
2010 }
2011
2012 pub fn never(self, tool: impl Into<String>) -> NeverBuilder {
2014 NeverBuilder {
2015 fb: self,
2016 tool: tool.into(),
2017 }
2018 }
2019 pub fn commit(mut self, tool: impl Into<String>, until: Guard) -> Self {
2022 let tool = tool.into();
2023 self.constraints.push(Constraint::Once(tool.clone()));
2024 self.constraints.push(Constraint::NeverUntil {
2025 tool: tool.clone(),
2026 until,
2027 });
2028 self.confirm_tools.push(tool);
2029 self
2030 }
2031
2032 pub fn build(self) -> Result<Flow, ConfigError> {
2034 let flow = Flow {
2035 steps: self.steps,
2036 constraints: self.constraints,
2037 confirm_tools: self.confirm_tools,
2038 ambient: self.ambient,
2039 };
2040 flow.validate()?;
2041 Ok(flow)
2042 }
2043}
2044
2045pub struct NeverBuilder {
2047 fb: FlowBuilder,
2048 tool: String,
2049}
2050
2051impl NeverBuilder {
2052 pub fn until(mut self, guard: Guard) -> FlowBuilder {
2054 self.fb.constraints.push(Constraint::NeverUntil {
2055 tool: self.tool,
2056 until: guard,
2057 });
2058 self.fb
2059 }
2060}
2061
2062pub struct ResetBuilder {
2064 builder: FlowBuilder,
2065 steps: Vec<String>,
2066}
2067
2068impl ResetBuilder {
2069 pub fn when(mut self, guard: Guard) -> FlowBuilder {
2071 self.builder.constraints.push(Constraint::Reset {
2072 steps: self.steps,
2073 when: guard,
2074 });
2075 self.builder
2076 }
2077}
2078
2079#[cfg(test)]
2080mod tests {
2081 use super::*;
2082 use serde_json::json;
2083
2084 #[test]
2094 fn a_refusal_names_what_is_available_instead() {
2095 let state = State::new();
2096 let mon = FlowMonitor::new(debt_flow(), Enforcement::Enforce);
2097
2098 let reason = mon
2099 .admits_tool("charge_card", &state)
2100 .expect_err("charge_card is gated behind verification");
2101
2102 assert!(
2103 reason.contains("lookup_account"),
2104 "a refusal must point at the tool that would make progress: {reason}"
2105 );
2106 assert!(
2107 reason.contains("ptp_confirmed"),
2108 "a refusal must name the condition it is waiting on: {reason}"
2109 );
2110 }
2111
2112 #[test]
2116 fn a_refusal_carries_the_active_steps_posture() {
2117 let state = State::new();
2118 let mon = FlowMonitor::new(debt_flow(), Enforcement::Enforce);
2119
2120 let reason = mon
2121 .admits_tool("charge_card", &state)
2122 .expect_err("charge_card is gated behind verification");
2123
2124 assert!(
2125 reason.contains("Verify the caller's identity."),
2126 "the active step's posture belongs in the refusal: {reason}"
2127 );
2128 }
2129
2130 #[test]
2134 fn a_spent_once_constraint_does_not_offer_alternatives() {
2135 let state = State::new();
2136 let flow = Flow::new()
2137 .step("pay")
2138 .allow(["charge_card"])
2139 .done(Guard::called_ok("charge_card"))
2140 .once("charge_card")
2141 .build()
2142 .expect("valid flow");
2143 let mut mon = FlowMonitor::new(flow, Enforcement::Enforce);
2144 mon.on_tool_ok("charge_card", &state);
2145
2146 let reason = mon
2147 .admits_tool("charge_card", &state)
2148 .expect_err("once is spent");
2149
2150 assert!(
2151 reason.contains("already run"),
2152 "a spent `once` must say so plainly: {reason}"
2153 );
2154 assert!(
2155 !reason.contains("Available now"),
2156 "nothing to redirect to — offering a menu invites a retry: {reason}"
2157 );
2158 }
2159
2160 #[test]
2164 fn the_redirection_tracks_the_conversation() {
2165 let state = State::new();
2166 let mon = FlowMonitor::new(debt_flow(), Enforcement::Enforce);
2167
2168 let before = mon
2169 .admits_tool("charge_card", &state)
2170 .expect_err("gated before the promise is confirmed");
2171 assert!(
2172 before.contains("ptp_confirmed") && before.contains("lookup_account"),
2173 "{before}"
2174 );
2175
2176 let _ = state.set("identity_verified", true);
2180 let mut mon = mon;
2181 mon.on_turn(&state);
2182 let after = mon
2183 .admits_tool("charge_card", &state)
2184 .expect_err("the promise is still unconfirmed");
2185
2186 assert!(
2187 after.contains("Give the disclosure."),
2188 "the refusal must carry the posture of the step that is now active, \
2189 not the one that was active when the session opened: {after}"
2190 );
2191 assert!(
2192 !after.contains("Verify the caller's identity."),
2193 "a completed step's posture must not keep riding along on refusals — \
2194 that is how a verified caller gets asked to verify again: {after}"
2195 );
2196 assert_ne!(
2197 before, after,
2198 "the refusal did not change when the flow advanced"
2199 );
2200 }
2201
2202 #[test]
2205 fn guards_describe_themselves_in_prose() {
2206 assert_eq!(
2207 Guard::is_true("verified").describe(),
2208 "'verified' must be true"
2209 );
2210 assert_eq!(
2211 Guard::captured(["amount", "date"]).describe(),
2212 "these must be known: amount, date"
2213 );
2214 assert_eq!(
2215 Guard::called_ok("disclose").describe(),
2216 "'disclose' must have run successfully"
2217 );
2218 assert_eq!(
2219 Guard::all([Guard::is_true("a"), Guard::done("b")]).describe(),
2220 "'a' must be true and step 'b' must be complete"
2221 );
2222 assert_eq!(
2223 Guard::custom(|_| true).describe(),
2224 "a condition set by the application",
2225 "a custom guard has no readable spec, and must not pretend otherwise"
2226 );
2227 }
2228
2229 fn debt_flow() -> Flow {
2230 Flow::new()
2231 .step("verify")
2232 .posture("Verify the caller's identity.")
2233 .allow(["lookup_account"])
2234 .done(Guard::is_true("identity_verified"))
2235 .step("disclose")
2236 .after("verify")
2237 .posture("Give the disclosure.")
2238 .done(Guard::is_true("disclosure_given"))
2239 .step("capture_ptp")
2240 .after("disclose")
2241 .done(Guard::captured(["ptp_amount", "ptp_date"]))
2242 .step("take_payment")
2243 .after("capture_ptp")
2244 .allow(["charge_card"])
2245 .done(Guard::called_ok("charge_card"))
2246 .step("close")
2247 .after("capture_ptp")
2248 .terminal()
2249 .never("charge_card")
2250 .until(Guard::is_true("ptp_confirmed"))
2251 .once("charge_card")
2252 .require(["close"])
2253 .build()
2254 .expect("valid flow")
2255 }
2256
2257 #[test]
2258 fn validates_and_detects_unknown_dep() {
2259 let bad = Flow::new()
2260 .step("a")
2261 .done(Guard::is_true("x"))
2262 .step("b")
2263 .after("missing")
2264 .terminal()
2265 .build();
2266 assert!(bad.is_err());
2267 }
2268
2269 #[test]
2270 fn detects_cycle() {
2271 let steps = vec![
2273 Step {
2274 id: "a".into(),
2275 after: vec!["b".into()],
2276 join: Join::default(),
2277 gate: None,
2278 done: Some(Guard::always()),
2279 posture: None,
2280 ground: None,
2281 allow: vec![],
2282 deny: vec![],
2283 terminal: false,
2284 },
2285 Step {
2286 id: "b".into(),
2287 after: vec!["a".into()],
2288 join: Join::default(),
2289 gate: None,
2290 done: Some(Guard::always()),
2291 posture: None,
2292 ground: None,
2293 allow: vec![],
2294 deny: vec![],
2295 terminal: false,
2296 },
2297 ];
2298 let flow = Flow {
2299 steps,
2300 ..Flow::default()
2301 };
2302 assert!(flow.validate().is_err());
2303 }
2304
2305 #[test]
2306 fn marking_latches_in_order() {
2307 let flow = debt_flow();
2308 let mut mon = FlowMonitor::new(flow, Enforcement::Enforce);
2309 let state = State::new();
2310
2311 assert_eq!(
2313 mon.active_steps(&state)
2314 .iter()
2315 .map(|s| s.id.as_str())
2316 .collect::<Vec<_>>(),
2317 vec!["verify"]
2318 );
2319
2320 let _ = state.set("identity_verified", true);
2321 mon.on_turn(&state);
2322 assert!(mon.marking().done.contains("verify"));
2323 assert_eq!(mon.verdict("verify", &state), Verdict::Done);
2324 assert_eq!(mon.verdict("disclose", &state), Verdict::Active);
2325
2326 let _ = state.set("disclosure_given", true);
2327 let _ = state.set("ptp_amount", 200);
2328 let _ = state.set("ptp_date", "2026-06-05");
2329 mon.on_turn(&state);
2330 assert!(mon.marking().done.contains("capture_ptp"));
2332 assert!(mon.marking().done.contains("close"));
2333 assert!(mon.is_complete());
2334 }
2335
2336 #[test]
2337 fn enforces_never_until_and_once() {
2338 let flow = debt_flow();
2339 let mut mon = FlowMonitor::new(flow, Enforcement::Enforce);
2340 let state = State::new();
2341 let _ = state.set("identity_verified", true);
2343 let _ = state.set("disclosure_given", true);
2344 let _ = state.set("ptp_amount", 200);
2345 let _ = state.set("ptp_date", "x");
2346 mon.on_turn(&state);
2347
2348 assert!(mon.admits_tool("charge_card", &state).is_err());
2350 let _ = state.set("ptp_confirmed", true);
2351 assert!(mon.admits_tool("charge_card", &state).is_ok());
2352
2353 mon.on_tool_ok("charge_card", &state);
2355 assert!(mon.admits_tool("charge_card", &state).is_err());
2356 }
2357
2358 #[test]
2359 fn whitelist_scopes_tools_to_active_step() {
2360 let flow = debt_flow();
2361 let mon = FlowMonitor::new(flow, Enforcement::Enforce);
2362 let state = State::new();
2363 assert!(mon.admits_tool("lookup_account", &state).is_ok());
2365 assert!(mon.admits_tool("charge_card", &state).is_err());
2366 }
2367
2368 #[test]
2369 fn observe_mode_records_violations_not_blocks() {
2370 let flow = debt_flow();
2371 let mut mon = FlowMonitor::new(flow, Enforcement::Observe);
2372 let state = State::new();
2373 mon.observe_tool("charge_card", true, &state);
2375 assert_eq!(mon.violations().len(), 1);
2376 assert_eq!(mon.violations()[0].subject, "charge_card");
2377 }
2378
2379 #[test]
2380 fn compile_accepts_valid_flow_and_collects_tool_universe() {
2381 let compiled = debt_flow().compile().expect("valid flow compiles");
2382 assert!(compiled.tool_surface().tools.contains("charge_card"));
2384 assert!(compiled.tool_surface().tools.contains("lookup_account"));
2385 let _ = FlowMonitor::compiled(compiled, Enforcement::Enforce);
2386 }
2387
2388 #[test]
2389 fn compile_rejects_unreachable_step() {
2390 let flow = Flow::new()
2395 .step("a")
2396 .done(Guard::is_true("a_done"))
2397 .step("b")
2398 .after("a")
2399 .done(Guard::is_true("b_done"))
2400 .step("island")
2401 .after("b")
2402 .gate(Guard::is_true("never"))
2403 .terminal()
2404 .build()
2405 .expect("structurally valid");
2406 assert!(flow.compile().is_ok());
2408 }
2409
2410 #[test]
2411 fn compile_rejects_unguarded_commit_tool() {
2412 let flow = Flow::new()
2414 .step("s")
2415 .allow(["pay"])
2416 .done(Guard::called_ok("pay"))
2417 .terminal()
2418 .commit("pay", Guard::always())
2419 .build()
2420 .expect("structurally valid");
2421 let err = flow
2422 .compile()
2423 .expect_err("unguarded commit must fail to compile");
2424 assert!(
2425 err.0
2426 .iter()
2427 .any(|e| matches!(e, FlowError::UnguardedCommitTool(t) if t == "pay"))
2428 );
2429 }
2430
2431 #[test]
2432 fn compile_with_tools_accepts_a_covering_registry() {
2433 let compiled = debt_flow()
2434 .compile_with_tools(&["lookup_account", "charge_card", "unrelated_extra"])
2435 .expect("registry covers the flow's tool universe");
2436 assert!(compiled.tool_surface().tools.contains("charge_card"));
2437 }
2438
2439 #[test]
2440 fn compile_with_tools_reports_dangling_tool_names() {
2441 let err = debt_flow()
2444 .compile_with_tools(&["lookup_account"])
2445 .expect_err("dangling tool must fail to compile");
2446 assert!(
2447 err.0
2448 .iter()
2449 .any(|e| matches!(e, FlowError::UnknownTool(t) if t == "charge_card"))
2450 );
2451 assert!(debt_flow().compile().is_ok());
2453 }
2454
2455 #[test]
2456 fn compile_rejects_never_until_guard_on_unknown_step() {
2457 let flow = Flow::new()
2460 .step("s")
2461 .allow(["pay"])
2462 .done(Guard::called_ok("pay"))
2463 .never("pay")
2464 .until(Guard::done("missing"))
2465 .build()
2466 .expect("structurally valid for build()");
2467 let err = flow.compile().expect_err("unsatisfiable guard must fail");
2468 assert!(err.0.iter().any(|e| matches!(
2469 e,
2470 FlowError::UnsatisfiableGuard { tool, step } if tool == "pay" && step == "missing"
2471 )));
2472 }
2473
2474 #[test]
2475 fn compile_rejects_before_cycle() {
2476 let flow = Flow::new()
2480 .step("a")
2481 .done(Guard::is_true("a_done"))
2482 .step("b")
2483 .done(Guard::is_true("b_done"))
2484 .before("a", "b")
2485 .before("b", "a")
2486 .build()
2487 .expect("build() only checks `after` cycles");
2488 let err = flow
2489 .compile()
2490 .expect_err("before-cycle must fail to compile");
2491 assert!(err.0.iter().any(|e| matches!(
2492 e,
2493 FlowError::OrderingCycle(steps)
2494 if steps.contains(&"a".to_string()) && steps.contains(&"b".to_string())
2495 )));
2496 }
2497
2498 #[test]
2499 fn explain_reports_blocked_tools_and_reasons() {
2500 let flow = debt_flow();
2501 let mon = FlowMonitor::new(flow, Enforcement::Enforce);
2502 let state = State::new();
2503 let ex = mon.explain(&state);
2504 assert!(ex.blocked_tools.contains_key("charge_card"));
2506 assert!(ex.active.contains(&"verify".to_string()));
2507 }
2508
2509 #[test]
2510 fn before_constraint_gates_step_eligibility() {
2511 let flow = Flow::new()
2515 .step("a")
2516 .done(Guard::is_true("a_done"))
2517 .step("b")
2518 .done(Guard::is_true("b_done"))
2519 .before("a", "b")
2520 .build()
2521 .expect("valid flow");
2522 let mut mon = FlowMonitor::new(flow, Enforcement::Enforce);
2523 let state = State::new();
2524
2525 let active: Vec<String> = mon
2527 .active_steps(&state)
2528 .iter()
2529 .map(|s| s.id.clone())
2530 .collect();
2531 assert!(active.contains(&"a".to_string()));
2532 assert!(
2533 !active.contains(&"b".to_string()),
2534 "b must wait for a (Before)"
2535 );
2536
2537 let _ = state.set("a_done", true);
2538 mon.on_turn(&state);
2539 let active: Vec<String> = mon
2540 .active_steps(&state)
2541 .iter()
2542 .map(|s| s.id.clone())
2543 .collect();
2544 assert!(active.contains(&"b".to_string()), "b active once a is done");
2545 }
2546
2547 #[test]
2548 fn custom_guard_in_combinator_is_not_erased() {
2549 let always_false = Guard::all([Guard::is_true("present"), Guard::custom(|_| false)]);
2552 assert!(matches!(always_false, Guard::Custom(_)));
2554
2555 let state = State::new();
2556 let _ = state.set("present", true);
2557 let marking = Marking::default();
2558 let ctx = FlowCtx {
2559 state: &state,
2560 marking: &marking,
2561 };
2562 assert!(!always_false.eval(&ctx), "custom guard must still veto");
2564
2565 let serializable = Guard::all([Guard::is_true("a"), Guard::is_set("b")]);
2567 assert!(matches!(serializable, Guard::Spec(_)));
2568 }
2569
2570 #[test]
2571 fn serde_round_trips_data_driven_flow() {
2572 let flow = debt_flow();
2573 let jsonv = serde_json::to_value(&flow).expect("serialize");
2574 let back: Flow = serde_json::from_value(jsonv).expect("deserialize");
2575 back.validate().expect("round-tripped flow is valid");
2576 assert_eq!(back.steps.len(), flow.steps.len());
2577 }
2578
2579 #[test]
2580 fn custom_guard_is_not_serializable() {
2581 let flow = Flow::new()
2582 .step("a")
2583 .done(Guard::custom(|ctx| ctx.state.contains("ready")))
2584 .terminal()
2585 .build()
2586 .unwrap();
2587 assert!(serde_json::to_value(&flow).is_err());
2588 }
2589
2590 #[test]
2591 fn mermaid_export_has_nodes_and_edges() {
2592 let m = debt_flow().to_mermaid();
2593 assert!(m.contains("flowchart TD"));
2594 assert!(m.contains("verify --> disclose"));
2595 assert!(m.contains("close([close])")); }
2597
2598 struct WriteAgent;
2599 #[async_trait::async_trait]
2600 impl TextAgent for WriteAgent {
2601 fn name(&self) -> &str {
2602 "writer"
2603 }
2604 async fn run(&self, _state: &State) -> Result<String, crate::error::AgentError> {
2605 Ok("available".to_string())
2606 }
2607 }
2608
2609 #[tokio::test]
2610 async fn on_enter_fires_once_when_step_activates() {
2611 let flow = Flow::new()
2614 .step("collect")
2615 .done(Guard::is_true("collected"))
2616 .step("check")
2617 .after("collect")
2618 .done(Guard::resolved("check"))
2619 .step("book")
2620 .after("check")
2621 .terminal()
2622 .require(["book"])
2623 .build()
2624 .expect("valid flow");
2625
2626 let mut mon = FlowMonitor::new(flow, Enforcement::Enforce)
2627 .on_enter("check", on_enter(Arc::new(WriteAgent), AgentMode::Call));
2628 let state = State::new();
2629
2630 assert_eq!(mon.take_newly_active(&state), vec!["collect".to_string()]);
2632 assert!(mon.take_newly_active(&state).is_empty());
2634
2635 let _ = state.set("collected", true);
2637 mon.on_turn(&state);
2638 mon.fire_enter_actions(&state).await;
2639 assert_eq!(
2640 state.get::<String>("check:result").as_deref(),
2641 Some("available")
2642 );
2643
2644 mon.on_turn(&state);
2646 assert!(mon.marking().done.contains("check"));
2647 assert!(mon.is_complete());
2648 assert!(mon.take_newly_active(&state).is_empty());
2650 }
2651
2652 #[test]
2653 fn ground_template_interpolates_and_branches() {
2654 let state = State::new();
2655 let _ = state.set("when", "3pm");
2656 let _ = state.set("available", true);
2657 let _ = state.set("prior_visits", 2);
2658 assert_eq!(
2659 render_ground(
2660 "{when} is {available?open:taken}; {prior_visits} prior visits.",
2661 &state
2662 ),
2663 "3pm is open; 2 prior visits."
2664 );
2665 let _ = state.set("available", false);
2667 assert_eq!(
2668 render_ground("slot {missing}is {available?free:full}", &state),
2669 "slot is full"
2670 );
2671 }
2672
2673 #[test]
2674 fn active_grounds_projects_only_active_steps() {
2675 let flow = Flow::new()
2676 .step("collect")
2677 .ground("Known time: {when}.")
2678 .done(Guard::is_set("when"))
2679 .step("done")
2680 .after("collect")
2681 .terminal()
2682 .build()
2683 .expect("valid flow");
2684 let mut mon = FlowMonitor::new(flow, Enforcement::Enforce);
2685 let state = State::new();
2686 let _ = state.set("when", "3pm");
2687 assert_eq!(
2688 mon.active_grounds(&state),
2689 vec!["Known time: 3pm.".to_string()]
2690 );
2691 mon.on_turn(&state);
2693 assert!(mon.active_grounds(&state).is_empty());
2694 }
2695
2696 #[test]
2697 fn eq_guard_matches_state_value() {
2698 let g = Guard::eq("status", json!("active"));
2699 let state = State::new();
2700 let marking = Marking::default();
2701 assert!(!g.eval(&FlowCtx {
2702 state: &state,
2703 marking: &marking
2704 }));
2705 let _ = state.set("status", "active");
2706 assert!(g.eval(&FlowCtx {
2707 state: &state,
2708 marking: &marking
2709 }));
2710 }
2711
2712 fn ambient_flow() -> Flow {
2727 Flow::new()
2728 .ambient(["recall_context"])
2729 .step("gather")
2730 .allow(["ask_diet"])
2731 .done(Guard::is_set("user:diet"))
2732 .step("book")
2733 .after("gather")
2734 .allow(["book_table"])
2735 .done(Guard::called_ok("book_table"))
2736 .build()
2737 .expect("flow is structurally valid")
2738 }
2739
2740 #[test]
2741 fn an_ambient_tool_survives_a_step_whitelist() {
2742 let mon = FlowMonitor::new(ambient_flow(), Enforcement::Enforce);
2743 let state = State::new();
2744 assert_eq!(
2745 mon.admits_tool("ask_diet", &state),
2746 Ok(()),
2747 "the step's own tool is admitted"
2748 );
2749 assert!(
2750 mon.admits_tool("book_table", &state).is_err(),
2751 "a later step's tool is still excluded by `gather`'s whitelist"
2752 );
2753 assert_eq!(
2754 mon.admits_tool("recall_context", &state),
2755 Ok(()),
2756 "an ambient tool must not be caught by a whitelist that never meant to exclude it"
2757 );
2758 }
2759
2760 #[test]
2761 fn an_ambient_tool_is_still_denied_when_a_step_names_it() {
2762 let flow = Flow::new()
2765 .ambient(["recall_context"])
2766 .step("sensitive")
2767 .deny(["recall_context"])
2768 .done(Guard::is_true("done"))
2769 .build()
2770 .expect("flow is structurally valid");
2771 let mon = FlowMonitor::new(flow, Enforcement::Enforce);
2772 assert!(
2773 mon.admits_tool("recall_context", &State::new()).is_err(),
2774 "an explicit `deny` outranks ambient"
2775 );
2776 }
2777
2778 #[test]
2779 fn an_ambient_tool_still_obeys_never_until() {
2780 let flow = Flow::new()
2781 .ambient(["manage_memory"])
2782 .step("verify")
2783 .done(Guard::is_true("verified"))
2784 .never("manage_memory")
2785 .until(Guard::is_true("verified"))
2786 .build()
2787 .expect("flow is structurally valid");
2788 let mon = FlowMonitor::new(flow, Enforcement::Enforce);
2789 let state = State::new();
2790 assert!(
2791 mon.admits_tool("manage_memory", &state).is_err(),
2792 "`never(..).until(..)` names the tool; ambient must not unlock it"
2793 );
2794 let _ = state.set("verified", true);
2795 assert_eq!(
2796 mon.admits_tool("manage_memory", &state),
2797 Ok(()),
2798 "once the guard holds the constraint stops binding"
2799 );
2800 }
2801
2802 #[test]
2803 fn an_ambient_tool_still_obeys_once() {
2804 let flow = Flow::new()
2805 .ambient(["summarize"])
2806 .step("work")
2807 .done(Guard::called_ok("summarize"))
2808 .once("summarize")
2809 .build()
2810 .expect("flow is structurally valid");
2811 let mut mon = FlowMonitor::new(flow, Enforcement::Enforce);
2812 let state = State::new();
2813 assert_eq!(mon.admits_tool("summarize", &state), Ok(()));
2814 mon.observe_tool("summarize", true, &state);
2815 assert!(
2816 mon.admits_tool("summarize", &state).is_err(),
2817 "`once` names the tool; ambient must not exempt it"
2818 );
2819 }
2820
2821 #[test]
2822 fn ambient_tools_join_the_tool_universe() {
2823 let flow = ambient_flow();
2826 assert!(
2827 flow.tool_universe().contains("recall_context"),
2828 "an ambient tool is part of the flow's tool universe"
2829 );
2830 assert!(
2831 ambient_flow()
2832 .compile_with_tools(&["ask_diet", "book_table"])
2833 .is_err(),
2834 "a registry missing the ambient tool must not compile clean"
2835 );
2836 assert!(
2837 ambient_flow()
2838 .compile_with_tools(&["ask_diet", "book_table", "recall_context"])
2839 .is_ok(),
2840 "a covering registry compiles"
2841 );
2842 }
2843
2844 #[test]
2845 fn a_flow_with_no_ambient_tools_is_unchanged() {
2846 let mon = FlowMonitor::new(debt_flow(), Enforcement::Enforce);
2849 let state = State::new();
2850 assert_eq!(mon.admits_tool("lookup_account", &state), Ok(()));
2851 assert!(mon.admits_tool("charge_card", &state).is_err());
2852 assert!(mon.admits_tool("anything_else", &state).is_err());
2853 }
2854
2855 #[test]
2856 fn explain_trace_reports_per_atom_truth() {
2857 let state = State::new();
2858 let _ = state.set("ptp_amount", 200);
2859 let marking = Marking::default();
2860 let ctx = FlowCtx {
2861 state: &state,
2862 marking: &marking,
2863 };
2864 let guard = Guard::all([
2865 Guard::captured(["ptp_amount", "ptp_date"]),
2866 Guard::is_true("confirmed"),
2867 ]);
2868 let trace = guard.explain_trace(&ctx);
2869 assert!(!trace.holds);
2870 assert_eq!(trace.desc, "all of");
2871 assert_eq!(trace.children.len(), 2);
2872 assert!(!trace.children[0].holds, "ptp_date missing");
2874 assert!(!trace.children[1].holds);
2875 assert!(serde_json::to_value(&trace).is_ok());
2877 }
2878
2879 #[test]
2880 fn explanation_carries_active_step_progress() {
2881 let mon = FlowMonitor::new(debt_flow(), Enforcement::Enforce);
2882 let state = State::new();
2883 let ex = mon.explain(&state);
2884 let verify = ex.active_progress.get("verify").expect("verify is active");
2885 assert!(!verify.holds);
2886 assert!(verify.desc.contains("identity_verified"));
2887 }
2888
2889 #[test]
2890 fn state_keys_read_covers_guards_and_constraints() {
2891 let keys = debt_flow().state_keys_read();
2892 for k in [
2893 "identity_verified",
2894 "disclosure_given",
2895 "ptp_amount",
2896 "ptp_date",
2897 ] {
2898 assert!(keys.contains(k), "missing read key {k}");
2899 }
2900 assert!(!keys.contains("charge_card"));
2902 }
2903
2904 #[test]
2905 fn posture_updates_take_effect_in_place() {
2906 let mut mon = FlowMonitor::new(debt_flow(), Enforcement::Enforce);
2907 let state = State::new();
2908 assert!(mon.set_posture("verify", Some("New posture.".into())));
2909 assert!(!mon.set_posture("no_such_step", None));
2910 assert_eq!(
2911 mon.active_postures(&state),
2912 vec!["New posture.".to_string()]
2913 );
2914 }
2915
2916 #[test]
2917 fn eval_state_treats_marking_atoms_as_false() {
2918 let state = State::new();
2919 let _ = state.set("ready", true);
2920 assert!(Guard::is_true("ready").eval_state(&state));
2921 assert!(!Guard::called_ok("any_tool").eval_state(&state));
2922 }
2923
2924 #[test]
2925 fn conditional_edges_branch_and_any_join_merges() {
2926 let flow = Flow::new()
2929 .step("decide")
2930 .done(Guard::is_set("severity"))
2931 .step("schedule")
2932 .after_when("decide", Guard::eq("severity", "routine"))
2933 .done(Guard::called_ok("book"))
2934 .allow(["book"])
2935 .step("escalate")
2936 .after_when("decide", Guard::eq("severity", "emergency"))
2937 .done(Guard::called_ok("transfer"))
2938 .allow(["transfer"])
2939 .step("close")
2940 .after("schedule")
2941 .after("escalate")
2942 .join_any()
2943 .terminal()
2944 .build()
2945 .expect("valid");
2946 let mut mon = FlowMonitor::new(flow, Enforcement::Enforce);
2947 let state = State::new();
2948 let _ = state.set("severity", "routine");
2949 mon.relatch(&state);
2950 let active: Vec<String> = mon.explain(&state).active;
2951 assert!(
2952 active.contains(&"schedule".to_string()),
2953 "routine branch opens"
2954 );
2955 assert!(
2956 !active.contains(&"escalate".to_string()),
2957 "emergency branch stays closed"
2958 );
2959 mon.on_tool_ok("book", &state);
2961 assert!(mon.marking().done.contains("close"), "any-join merged");
2962 }
2963
2964 #[test]
2965 fn reset_unlatches_on_rising_edge_and_forgives_called_ok() {
2966 let flow = Flow::new()
2967 .step("pay")
2968 .allow(["charge"])
2969 .done(Guard::called_ok("charge"))
2970 .once("charge")
2971 .reset(["pay"])
2972 .when(Guard::is_true("declined"))
2973 .build()
2974 .expect("valid");
2975 let mut mon = FlowMonitor::new(flow, Enforcement::Enforce);
2976 let state = State::new();
2977 mon.relatch(&state);
2978 mon.on_tool_ok("charge", &state);
2979 assert!(mon.marking().done.contains("pay"));
2980 assert!(mon.admits_tool("charge", &state).is_err(), "once spent");
2981
2982 let _ = state.set("declined", true);
2984 mon.relatch(&state);
2985 assert!(!mon.marking().done.contains("pay"), "un-latched");
2986 assert!(
2987 mon.admits_tool("charge", &state).is_ok(),
2988 "once counts per latch-cycle"
2989 );
2990
2991 mon.on_tool_ok("charge", &state);
2993 mon.relatch(&state);
2994 assert!(
2995 mon.marking().done.contains("pay"),
2996 "second charge latches again"
2997 );
2998 }
2999
3000 #[test]
3001 fn edge_serde_keeps_plain_strings_and_round_trips_conditions() {
3002 let flow = Flow::new()
3003 .step("a")
3004 .terminal()
3005 .step("b")
3006 .after("a")
3007 .after_when("a", Guard::is_true("x"))
3008 .join_any()
3009 .terminal()
3010 .build()
3011 .expect("valid");
3012 let json = serde_json::to_value(&flow).expect("serialize");
3013 assert_eq!(json["steps"][1]["after"][0], serde_json::json!("a"));
3015 assert_eq!(
3016 json["steps"][1]["after"][1],
3017 serde_json::json!({"step": "a", "when": {"is_true": "x"}})
3018 );
3019 assert_eq!(json["steps"][1]["join"], serde_json::json!("any"));
3020 let back: Flow = serde_json::from_value(json).expect("deserialize");
3021 assert_eq!(back.steps[1].after.len(), 2);
3022 assert!(back.steps[1].after[1].when.is_some());
3023 }
3024
3025 #[test]
3026 fn flow_json_schema_generates() {
3027 let schema = serde_json::to_value(schemars::schema_for!(Flow)).expect("schema");
3028 let text = schema.to_string();
3029 assert!(text.contains("is_true"));
3031 assert!(text.contains("never_until"));
3032 }
3033}