1use std::collections::{BTreeMap, BTreeSet, HashMap};
22use std::sync::Arc;
23
24use serde::{Deserialize, Deserializer, Serialize, Serializer};
25use serde_json::Value;
26
27use crate::orchestration::{call, Mode as AgentMode};
28use crate::state::State;
29use crate::text::TextAgent;
30
31pub struct FlowCtx<'a> {
34 pub state: &'a State,
36 pub marking: &'a Marking,
38}
39
40#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
42#[serde(rename_all = "snake_case")]
43pub enum Pred {
44 Always,
46 IsTrue(String),
48 IsSet(String),
50 Eq(String, Value),
52 Captured(Vec<String>),
54 CalledOk(String),
56 Done(String),
58 All(Vec<Pred>),
60 Any(Vec<Pred>),
62 Not(Box<Pred>),
64}
65
66impl Pred {
67 fn eval(&self, ctx: &FlowCtx) -> bool {
68 match self {
69 Pred::Always => true,
70 Pred::IsTrue(k) => ctx.state.get::<bool>(k) == Some(true),
71 Pred::IsSet(k) => ctx.state.contains(k),
72 Pred::Eq(k, v) => ctx.state.get::<Value>(k).as_ref() == Some(v),
73 Pred::Captured(fields) => fields.iter().all(|f| ctx.state.contains(f)),
74 Pred::CalledOk(t) => ctx.marking.tool_ok.contains_key(t),
75 Pred::Done(s) => ctx.marking.done.contains(s),
76 Pred::All(ps) => ps.iter().all(|p| p.eval(ctx)),
77 Pred::Any(ps) => ps.iter().any(|p| p.eval(ctx)),
78 Pred::Not(p) => !p.eval(ctx),
79 }
80 }
81
82 fn describe(&self) -> String {
84 fn join(ps: &[Pred], sep: &str) -> String {
85 ps.iter().map(Pred::describe).collect::<Vec<_>>().join(sep)
86 }
87 match self {
88 Pred::Always => "nothing".to_string(),
89 Pred::IsTrue(k) => format!("'{k}' must be true"),
90 Pred::IsSet(k) => format!("'{k}' must be known"),
91 Pred::Eq(k, v) => format!("'{k}' must be {v}"),
92 Pred::Captured(fields) => {
93 format!("these must be known: {}", fields.join(", "))
94 }
95 Pred::CalledOk(t) => format!("'{t}' must have run successfully"),
96 Pred::Done(s) => format!("step '{s}' must be complete"),
97 Pred::All(ps) => join(ps, " and "),
98 Pred::Any(ps) => join(ps, " or "),
99 Pred::Not(p) => format!("it must not be the case that {}", p.describe()),
100 }
101 }
102
103 fn referenced_steps(&self, out: &mut Vec<String>) {
105 match self {
106 Pred::Done(s) => out.push(s.clone()),
107 Pred::All(ps) | Pred::Any(ps) => ps.iter().for_each(|p| p.referenced_steps(out)),
108 Pred::Not(p) => p.referenced_steps(out),
109 _ => {}
110 }
111 }
112}
113
114pub fn render_ground(template: &str, state: &State) -> String {
125 let mut out = String::with_capacity(template.len());
126 let mut rest = template;
127 while let Some(open) = rest.find('{') {
128 out.push_str(&rest[..open]);
129 let after = &rest[open + 1..];
130 let Some(close) = after.find('}') else {
131 out.push_str(&rest[open..]);
133 return out;
134 };
135 let expr = &after[..close];
136 out.push_str(&render_expr(expr, state));
137 rest = &after[close + 1..];
138 }
139 out.push_str(rest);
140 out
141}
142
143fn render_expr(expr: &str, state: &State) -> String {
144 if let Some((cond, arms)) = expr.split_once('?') {
145 let (yes, no) = arms.split_once(':').unwrap_or((arms, ""));
146 if is_truthy(state, cond.trim()) {
147 yes.to_string()
148 } else {
149 no.to_string()
150 }
151 } else {
152 match state.get::<Value>(expr.trim()) {
153 Some(Value::String(s)) => s,
154 Some(v) => v.to_string(),
155 None => String::new(),
156 }
157 }
158}
159
160fn is_truthy(state: &State, key: &str) -> bool {
161 match state.get::<Value>(key) {
162 None | Some(Value::Null) => false,
163 Some(Value::Bool(b)) => b,
164 Some(Value::Number(n)) => n.as_f64().map(|f| f != 0.0).unwrap_or(true),
165 Some(Value::String(s)) => !s.is_empty(),
166 Some(_) => true,
167 }
168}
169
170type CustomFn = Arc<dyn Fn(&FlowCtx) -> bool + Send + Sync>;
171
172#[derive(Clone)]
178pub enum Guard {
179 Spec(Pred),
181 Custom(CustomFn),
183}
184
185impl Guard {
186 pub fn always() -> Self {
188 Guard::Spec(Pred::Always)
189 }
190 pub fn is_true(key: impl Into<String>) -> Self {
192 Guard::Spec(Pred::IsTrue(key.into()))
193 }
194 pub fn is_set(key: impl Into<String>) -> Self {
196 Guard::Spec(Pred::IsSet(key.into()))
197 }
198 pub fn eq(key: impl Into<String>, value: impl Into<Value>) -> Self {
200 Guard::Spec(Pred::Eq(key.into(), value.into()))
201 }
202 pub fn captured<I, S>(fields: I) -> Self
204 where
205 I: IntoIterator<Item = S>,
206 S: Into<String>,
207 {
208 Guard::Spec(Pred::Captured(fields.into_iter().map(Into::into).collect()))
209 }
210 pub fn called_ok(tool: impl Into<String>) -> Self {
212 Guard::Spec(Pred::CalledOk(tool.into()))
213 }
214 pub fn done(step: impl Into<String>) -> Self {
216 Guard::Spec(Pred::Done(step.into()))
217 }
218 pub fn resolved(name: impl AsRef<str>) -> Self {
222 Guard::Spec(Pred::IsSet(format!("{}:result", name.as_ref())))
223 }
224 pub fn all(guards: impl IntoIterator<Item = Guard>) -> Self {
233 let guards: Vec<Guard> = guards.into_iter().collect();
234 if guards.iter().all(|g| matches!(g, Guard::Spec(_))) {
235 Guard::Spec(Pred::All(specs_unchecked(guards)))
236 } else {
237 Guard::Custom(Arc::new(move |ctx| guards.iter().all(|g| g.eval(ctx))))
238 }
239 }
240 pub fn any(guards: impl IntoIterator<Item = Guard>) -> Self {
245 let guards: Vec<Guard> = guards.into_iter().collect();
246 if guards.iter().all(|g| matches!(g, Guard::Spec(_))) {
247 Guard::Spec(Pred::Any(specs_unchecked(guards)))
248 } else {
249 Guard::Custom(Arc::new(move |ctx| guards.iter().any(|g| g.eval(ctx))))
250 }
251 }
252 #[allow(clippy::should_implement_trait)]
254 pub fn not(guard: Guard) -> Self {
255 match guard {
256 Guard::Spec(p) => Guard::Spec(Pred::Not(Box::new(p))),
257 Guard::Custom(f) => Guard::Custom(Arc::new(move |ctx| !f(ctx))),
259 }
260 }
261 pub fn describe(&self) -> String {
271 match self {
272 Guard::Spec(p) => p.describe(),
273 Guard::Custom(_) => "a condition set by the application".to_string(),
274 }
275 }
276
277 pub fn custom(f: impl Fn(&FlowCtx) -> bool + Send + Sync + 'static) -> Self {
279 Guard::Custom(Arc::new(f))
280 }
281
282 pub fn eval(&self, ctx: &FlowCtx) -> bool {
284 match self {
285 Guard::Spec(p) => p.eval(ctx),
286 Guard::Custom(f) => f(ctx),
287 }
288 }
289
290 fn referenced_steps(&self, out: &mut Vec<String>) {
291 if let Guard::Spec(p) = self {
292 p.referenced_steps(out);
293 }
294 }
295}
296
297fn specs_unchecked(guards: Vec<Guard>) -> Vec<Pred> {
302 guards
303 .into_iter()
304 .map(|g| match g {
305 Guard::Spec(p) => p,
306 Guard::Custom(_) => unreachable!("specs_unchecked called with a custom guard"),
307 })
308 .collect()
309}
310
311impl Serialize for Guard {
312 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
313 match self {
314 Guard::Spec(p) => p.serialize(s),
315 Guard::Custom(_) => Err(serde::ser::Error::custom(
316 "custom guards are not serializable; use Guard atoms for data-driven flows",
317 )),
318 }
319 }
320}
321
322impl<'de> Deserialize<'de> for Guard {
323 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
324 Ok(Guard::Spec(Pred::deserialize(d)?))
325 }
326}
327
328impl std::fmt::Debug for Guard {
329 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
330 match self {
331 Guard::Spec(p) => write!(f, "{p:?}"),
332 Guard::Custom(_) => write!(f, "Custom(<fn>)"),
333 }
334 }
335}
336
337#[derive(Clone, Debug, Serialize, Deserialize)]
339pub struct Step {
340 pub id: String,
342 #[serde(default, skip_serializing_if = "Vec::is_empty")]
344 pub after: Vec<String>,
345 #[serde(default, skip_serializing_if = "Option::is_none")]
347 pub gate: Option<Guard>,
348 #[serde(default, skip_serializing_if = "Option::is_none")]
350 pub done: Option<Guard>,
351 #[serde(default, skip_serializing_if = "Option::is_none")]
353 pub posture: Option<String>,
354 #[serde(default, skip_serializing_if = "Option::is_none")]
358 pub ground: Option<String>,
359 #[serde(default, skip_serializing_if = "Vec::is_empty")]
361 pub allow: Vec<String>,
362 #[serde(default, skip_serializing_if = "Vec::is_empty")]
364 pub deny: Vec<String>,
365 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
367 pub terminal: bool,
368}
369
370#[derive(Clone, Debug, Serialize, Deserialize)]
372#[serde(rename_all = "snake_case")]
373pub enum Constraint {
374 Once(String),
376 Before(String, String),
378 NeverUntil {
380 tool: String,
382 until: Guard,
384 },
385 Require(Vec<String>),
387}
388
389#[derive(Clone, Debug, Default, Serialize, Deserialize)]
391pub struct Flow {
392 pub steps: Vec<Step>,
394 #[serde(default, skip_serializing_if = "Vec::is_empty")]
396 pub constraints: Vec<Constraint>,
397 #[serde(default, skip_serializing_if = "Vec::is_empty")]
399 pub confirm_tools: Vec<String>,
400 #[serde(default, skip_serializing_if = "Vec::is_empty")]
409 pub ambient: Vec<String>,
410}
411
412impl Flow {
413 #[allow(
415 clippy::new_ret_no_self,
416 reason = "Flow::new() is the builder entry point; a Flow comes from FlowBuilder::build/compile"
417 )]
418 pub fn new() -> FlowBuilder {
419 FlowBuilder::default()
420 }
421
422 fn step(&self, id: &str) -> Option<&Step> {
423 self.steps.iter().find(|s| s.id == id)
424 }
425
426 pub fn validate(&self) -> Result<(), Vec<String>> {
428 let mut errs = Vec::new();
429 let ids: BTreeSet<&str> = self.steps.iter().map(|s| s.id.as_str()).collect();
430 if ids.len() != self.steps.len() {
431 errs.push("duplicate step ids".into());
432 }
433 for s in &self.steps {
434 for d in &s.after {
435 if !ids.contains(d.as_str()) {
436 errs.push(format!("step '{}' depends on unknown step '{}'", s.id, d));
437 }
438 }
439 if !s.terminal && s.done.is_none() {
440 errs.push(format!(
441 "non-terminal step '{}' has no `done` condition (it can never complete)",
442 s.id
443 ));
444 }
445 for g in s.gate.iter().chain(s.done.iter()) {
446 let mut refs = Vec::new();
447 g.referenced_steps(&mut refs);
448 for r in refs {
449 if !ids.contains(r.as_str()) {
450 errs.push(format!(
451 "step '{}' guard references unknown step '{r}'",
452 s.id
453 ));
454 }
455 }
456 }
457 }
458 for c in &self.constraints {
459 match c {
460 Constraint::Before(a, b) => {
461 for x in [a, b] {
462 if !ids.contains(x.as_str()) {
463 errs.push(format!("constraint `before` references unknown step '{x}'"));
464 }
465 }
466 }
467 Constraint::Require(rs) => {
468 for r in rs {
469 if !ids.contains(r.as_str()) {
470 errs.push(format!(
471 "constraint `require` references unknown step '{r}'"
472 ));
473 }
474 }
475 }
476 _ => {}
477 }
478 }
479 if self.has_cycle() {
480 errs.push("flow dependency graph has a cycle (must be a DAG)".into());
481 }
482 if errs.is_empty() {
483 Ok(())
484 } else {
485 Err(errs)
486 }
487 }
488
489 fn tool_universe(&self) -> BTreeSet<String> {
492 let mut tools = BTreeSet::new();
493 for s in &self.steps {
494 tools.extend(s.allow.iter().cloned());
495 tools.extend(s.deny.iter().cloned());
496 }
497 for c in &self.constraints {
498 match c {
499 Constraint::Once(t) => {
500 tools.insert(t.clone());
501 }
502 Constraint::NeverUntil { tool, .. } => {
503 tools.insert(tool.clone());
504 }
505 _ => {}
506 }
507 }
508 tools.extend(self.confirm_tools.iter().cloned());
509 tools.extend(self.ambient.iter().cloned());
510 tools
511 }
512
513 fn reachable_steps(&self) -> BTreeSet<String> {
516 let ids: BTreeSet<&str> = self.steps.iter().map(|s| s.id.as_str()).collect();
517 let mut succ: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
519 for s in &self.steps {
520 for d in &s.after {
521 if ids.contains(d.as_str()) {
522 succ.entry(d.as_str()).or_default().push(s.id.as_str());
523 }
524 }
525 }
526 for c in &self.constraints {
527 if let Constraint::Before(a, b) = c {
528 if ids.contains(a.as_str()) && ids.contains(b.as_str()) {
529 succ.entry(a.as_str()).or_default().push(b.as_str());
530 }
531 }
532 }
533 let roots: Vec<&str> = self
534 .steps
535 .iter()
536 .filter(|s| s.after.is_empty())
537 .map(|s| s.id.as_str())
538 .collect();
539 let mut seen = BTreeSet::new();
540 let mut stack = roots;
541 while let Some(id) = stack.pop() {
542 if seen.insert(id.to_string()) {
543 if let Some(next) = succ.get(id) {
544 stack.extend(next.iter().copied());
545 }
546 }
547 }
548 seen
549 }
550
551 pub fn compile(self) -> Result<CompiledFlow, FlowErrors> {
566 self.compile_internal(None)
567 }
568
569 pub fn compile_with_tools(self, tools: &[&str]) -> Result<CompiledFlow, FlowErrors> {
582 self.compile_internal(Some(tools))
583 }
584
585 fn compile_internal(self, registry: Option<&[&str]>) -> Result<CompiledFlow, FlowErrors> {
586 let mut errors = Vec::new();
587 if let Err(errs) = self.validate() {
588 errors.extend(errs.into_iter().map(FlowError::Invalid));
589 }
590
591 if errors.is_empty() {
593 let reachable = self.reachable_steps();
595 for s in &self.steps {
596 if !reachable.contains(&s.id) {
597 errors.push(FlowError::UnreachableStep(s.id.clone()));
598 }
599 }
600 if let Some(cycle) = self.ordering_cycle() {
604 errors.push(FlowError::OrderingCycle(cycle));
605 }
606 }
607
608 for tool in &self.confirm_tools {
611 let guard = self.constraints.iter().find_map(|c| match c {
612 Constraint::NeverUntil { tool: t, until } if t == tool => Some(until),
613 _ => None,
614 });
615 let unguarded = matches!(guard, None | Some(Guard::Spec(Pred::Always)));
616 if unguarded {
617 errors.push(FlowError::UnguardedCommitTool(tool.clone()));
618 }
619 }
620
621 let ids: BTreeSet<&str> = self.steps.iter().map(|s| s.id.as_str()).collect();
626 for c in &self.constraints {
627 if let Constraint::NeverUntil { tool, until } = c {
628 let mut refs = Vec::new();
629 until.referenced_steps(&mut refs);
630 for r in refs {
631 if !ids.contains(r.as_str()) {
632 errors.push(FlowError::UnsatisfiableGuard {
633 tool: tool.clone(),
634 step: r,
635 });
636 }
637 }
638 }
639 }
640
641 if let Some(known) = registry {
643 for tool in self.tool_universe() {
644 if !known.contains(&tool.as_str()) {
645 errors.push(FlowError::UnknownTool(tool));
646 }
647 }
648 }
649
650 if errors.is_empty() {
651 let policy = ToolPolicy {
652 tools: self.tool_universe(),
653 };
654 Ok(CompiledFlow { flow: self, policy })
655 } else {
656 Err(FlowErrors(errors))
657 }
658 }
659
660 fn ordering_cycle(&self) -> Option<Vec<String>> {
664 let mut deps: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
666 for s in &self.steps {
667 let entry = deps.entry(s.id.as_str()).or_default();
668 entry.extend(s.after.iter().map(String::as_str));
669 }
670 for c in &self.constraints {
671 if let Constraint::Before(a, b) = c {
672 deps.entry(b.as_str()).or_default().push(a.as_str());
673 }
674 }
675 fn dfs<'a>(
677 id: &'a str,
678 deps: &BTreeMap<&'a str, Vec<&'a str>>,
679 color: &mut BTreeMap<&'a str, u8>,
680 path: &mut Vec<&'a str>,
681 ) -> Option<Vec<String>> {
682 color.insert(id, 1);
683 path.push(id);
684 for d in deps.get(id).into_iter().flatten() {
685 match color.get(d).copied() {
686 Some(1) => {
687 let start = path.iter().position(|p| p == d).unwrap_or(0);
688 return Some(path[start..].iter().map(|s| s.to_string()).collect());
689 }
690 Some(2) => {}
691 _ => {
692 if let Some(cycle) = dfs(d, deps, color, path) {
693 return Some(cycle);
694 }
695 }
696 }
697 }
698 path.pop();
699 color.insert(id, 2);
700 None
701 }
702 let mut color: BTreeMap<&str, u8> = BTreeMap::new();
703 for s in &self.steps {
704 if color.get(s.id.as_str()).copied().unwrap_or(0) == 0 {
705 let mut path = Vec::new();
706 if let Some(cycle) = dfs(&s.id, &deps, &mut color, &mut path) {
707 return Some(cycle);
708 }
709 }
710 }
711 None
712 }
713
714 fn has_cycle(&self) -> bool {
715 let mut color: BTreeMap<&str, u8> = BTreeMap::new();
717 fn dfs<'a>(flow: &'a Flow, id: &'a str, color: &mut BTreeMap<&'a str, u8>) -> bool {
718 color.insert(id, 1);
719 if let Some(step) = flow.step(id) {
720 for d in &step.after {
721 match color.get(d.as_str()).copied() {
722 Some(1) => return true,
723 Some(2) => {}
724 _ => {
725 if dfs(flow, d, color) {
726 return true;
727 }
728 }
729 }
730 }
731 }
732 color.insert(id, 2);
733 false
734 }
735 for s in &self.steps {
736 if color.get(s.id.as_str()).copied().unwrap_or(0) == 0 && dfs(self, &s.id, &mut color) {
737 return true;
738 }
739 }
740 false
741 }
742
743 pub fn to_mermaid(&self) -> String {
745 let mut out = String::from("flowchart TD\n");
746 for s in &self.steps {
747 let shape = if s.terminal {
748 format!(" {}([{}])\n", s.id, s.id)
749 } else {
750 format!(" {}[{}]\n", s.id, s.id)
751 };
752 out.push_str(&shape);
753 }
754 for s in &self.steps {
755 for d in &s.after {
756 out.push_str(&format!(" {d} --> {}\n", s.id));
757 }
758 }
759 out
760 }
761}
762
763#[derive(Clone, Debug, Default)]
766pub struct Marking {
767 pub done: BTreeSet<String>,
769 pub tool_ok: BTreeMap<String, u32>,
771 pub turns: u32,
773}
774
775#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
777#[serde(rename_all = "snake_case")]
778pub enum Verdict {
779 Pending,
781 Active,
783 Done,
785 Skipped,
787}
788
789#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
791pub struct Violation {
792 pub subject: String,
794 pub reason: String,
796}
797
798#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
804pub enum Enforcement {
805 #[default]
807 Enforce,
808 Observe,
810}
811
812#[deprecated(note = "renamed to `Enforcement` to avoid colliding with orchestration::Mode")]
814pub type Mode = Enforcement;
815
816#[derive(Clone)]
821pub struct StepAction {
822 name: Option<String>,
823 agent: Arc<dyn TextAgent>,
824 mode: AgentMode,
825}
826
827pub fn run(agent: Arc<dyn TextAgent>, mode: AgentMode) -> StepAction {
835 StepAction {
836 name: None,
837 agent,
838 mode,
839 }
840}
841
842impl StepAction {
843 pub fn named(mut self, name: impl Into<String>) -> Self {
845 self.name = Some(name.into());
846 self
847 }
848
849 pub(crate) async fn fire(&self, step_id: &str, state: &State) {
852 let name = self.name.clone().unwrap_or_else(|| step_id.to_string());
853 match self.mode {
854 AgentMode::Call => {
855 let _ = call(&name, self.agent.clone(), state).await;
856 }
857 AgentMode::Dispatch | AgentMode::Background => {
858 let agent = self.agent.clone();
859 let state = state.clone();
860 tokio::spawn(async move {
861 let _ = call(&name, agent, &state).await;
862 });
863 }
864 }
865 }
866}
867
868pub type SharedFlowMonitor = Arc<parking_lot::Mutex<FlowMonitor>>;
874
875pub struct FlowMonitor {
878 flow: Flow,
879 mode: Enforcement,
880 marking: Marking,
881 violations: Vec<Violation>,
882 enter_actions: HashMap<String, StepAction>,
884 announced: BTreeSet<String>,
886}
887
888impl FlowMonitor {
889 pub fn new(flow: Flow, mode: Enforcement) -> Self {
895 Self {
896 flow,
897 mode,
898 marking: Marking::default(),
899 violations: Vec::new(),
900 enter_actions: HashMap::new(),
901 announced: BTreeSet::new(),
902 }
903 }
904
905 pub fn compiled(flow: CompiledFlow, mode: Enforcement) -> Self {
907 Self::new(flow.into_flow(), mode)
908 }
909
910 pub fn try_new(flow: Flow, mode: Enforcement) -> Result<Self, FlowErrors> {
913 Ok(Self::compiled(flow.compile()?, mode))
914 }
915
916 pub fn into_shared(self) -> SharedFlowMonitor {
921 Arc::new(parking_lot::Mutex::new(self))
922 }
923
924 pub fn explain(&self, state: &State) -> FlowExplanation {
930 let active = self
931 .active_steps(state)
932 .iter()
933 .map(|s| s.id.clone())
934 .collect();
935 let mut allowed_tools = Vec::new();
936 let mut blocked_tools = BTreeMap::new();
937 for tool in self.flow.tool_universe() {
938 match self.admits_tool(&tool, state) {
939 Ok(()) => allowed_tools.push(tool),
940 Err(reason) => {
941 blocked_tools.insert(tool, reason);
942 }
943 }
944 }
945 FlowExplanation {
946 active,
947 allowed_tools,
948 blocked_tools,
949 missing_requirements: self.unmet_requirements(),
950 }
951 }
952
953 pub fn why_blocked(&self, state: &State) -> FlowExplanation {
956 self.explain(state)
957 }
958
959 pub fn on_enter(mut self, step: impl Into<String>, action: StepAction) -> Self {
962 self.enter_actions.insert(step.into(), action);
963 self
964 }
965
966 pub fn take_newly_active(&mut self, state: &State) -> Vec<String> {
969 let mut fresh = Vec::new();
970 for s in self.active_steps(state) {
971 if !self.announced.contains(&s.id) {
972 fresh.push(s.id.clone());
973 }
974 }
975 for id in &fresh {
976 self.announced.insert(id.clone());
977 }
978 fresh
979 }
980
981 pub fn enter_action(&self, step: &str) -> Option<&StepAction> {
983 self.enter_actions.get(step)
984 }
985
986 pub async fn fire_enter_actions(&mut self, state: &State) {
990 for id in self.take_newly_active(state) {
991 if let Some(action) = self.enter_actions.get(&id) {
992 action.fire(&id, state).await;
993 }
994 }
995 }
996
997 pub fn mode(&self) -> Enforcement {
999 self.mode
1000 }
1001
1002 pub fn eval(&self, guard: &Guard, state: &State) -> bool {
1006 guard.eval(&self.ctx(state))
1007 }
1008 pub fn marking(&self) -> &Marking {
1010 &self.marking
1011 }
1012 pub fn violations(&self) -> &[Violation] {
1014 &self.violations
1015 }
1016 pub fn flow(&self) -> &Flow {
1018 &self.flow
1019 }
1020
1021 fn ctx<'a>(&'a self, state: &'a State) -> FlowCtx<'a> {
1022 FlowCtx {
1023 state,
1024 marking: &self.marking,
1025 }
1026 }
1027
1028 fn eligible(&self, step: &Step, state: &State) -> bool {
1029 let deps_done = step.after.iter().all(|d| self.marking.done.contains(d));
1030 let before_ok = self.flow.constraints.iter().all(|c| match c {
1033 Constraint::Before(a, b) if *b == step.id => self.marking.done.contains(a),
1034 _ => true,
1035 });
1036 let gate_ok = step
1037 .gate
1038 .as_ref()
1039 .map(|g| g.eval(&self.ctx(state)))
1040 .unwrap_or(true);
1041 deps_done && before_ok && gate_ok
1042 }
1043
1044 pub fn relatch(&mut self, state: &State) {
1047 loop {
1048 let mut newly_done: Vec<String> = Vec::new();
1049 for s in &self.flow.steps {
1050 if self.marking.done.contains(&s.id) {
1051 continue;
1052 }
1053 if !self.eligible(s, state) {
1054 continue;
1055 }
1056 let complete = if s.terminal {
1057 true
1058 } else {
1059 s.done
1060 .as_ref()
1061 .map(|g| g.eval(&self.ctx(state)))
1062 .unwrap_or(false)
1063 };
1064 if complete {
1065 newly_done.push(s.id.clone());
1066 }
1067 }
1068 if newly_done.is_empty() {
1069 break;
1070 }
1071 for id in newly_done {
1072 self.marking.done.insert(id);
1073 }
1074 }
1075 }
1076
1077 pub fn on_turn(&mut self, state: &State) {
1079 self.marking.turns += 1;
1080 self.relatch(state);
1081 }
1082
1083 pub fn on_tool_ok(&mut self, tool: &str, state: &State) {
1085 *self.marking.tool_ok.entry(tool.to_string()).or_insert(0) += 1;
1086 self.relatch(state);
1087 }
1088
1089 pub fn active_steps(&self, state: &State) -> Vec<&Step> {
1091 self.flow
1092 .steps
1093 .iter()
1094 .filter(|s| !self.marking.done.contains(&s.id) && self.eligible(s, state))
1095 .collect()
1096 }
1097
1098 pub fn active_postures(&self, state: &State) -> Vec<String> {
1100 self.active_steps(state)
1101 .into_iter()
1102 .filter_map(|s| s.posture.clone())
1103 .collect()
1104 }
1105
1106 pub fn active_grounds(&self, state: &State) -> Vec<String> {
1109 self.active_steps(state)
1110 .into_iter()
1111 .filter_map(|s| s.ground.as_ref().map(|t| render_ground(t, state)))
1112 .filter(|s| !s.trim().is_empty())
1113 .collect()
1114 }
1115
1116 pub fn unmet_requirements(&self) -> Vec<String> {
1118 self.flow
1119 .constraints
1120 .iter()
1121 .flat_map(|c| match c {
1122 Constraint::Require(rs) => rs.clone(),
1123 _ => Vec::new(),
1124 })
1125 .filter(|r| !self.marking.done.contains(r))
1126 .collect()
1127 }
1128
1129 pub fn is_complete(&self) -> bool {
1131 self.unmet_requirements().is_empty()
1132 }
1133
1134 pub fn verdict(&self, step_id: &str, state: &State) -> Verdict {
1136 if self.marking.done.contains(step_id) {
1137 return Verdict::Done;
1138 }
1139 if let Some(step) = self.flow.step(step_id) {
1140 if self.eligible(step, state) {
1141 return Verdict::Active;
1142 }
1143 }
1144 let bypassed = self
1146 .flow
1147 .steps
1148 .iter()
1149 .any(|s| s.after.iter().any(|d| d == step_id) && self.marking.done.contains(&s.id));
1150 if bypassed {
1151 Verdict::Skipped
1152 } else {
1153 Verdict::Pending
1154 }
1155 }
1156
1157 pub fn admits_tool(&self, tool: &str, state: &State) -> Result<(), String> {
1160 match self.admissibility(tool, state) {
1161 Ok(()) => Ok(()),
1162 Err(denial) => Err(self.render_denial(tool, &denial, state)),
1163 }
1164 }
1165
1166 fn admissibility(&self, tool: &str, state: &State) -> Result<(), Denial> {
1172 for c in &self.flow.constraints {
1174 if let Constraint::Once(t) = c {
1175 if t == tool && self.marking.tool_ok.contains_key(tool) {
1176 return Err(Denial::OnceExhausted);
1177 }
1178 }
1179 }
1180 for c in &self.flow.constraints {
1182 if let Constraint::NeverUntil { tool: t, until } = c {
1183 if t == tool && !until.eval(&self.ctx(state)) {
1184 return Err(Denial::NotYet(until.describe()));
1185 }
1186 }
1187 }
1188 let active = self.active_steps(state);
1190 if let Some(step) = active.iter().find(|s| s.deny.iter().any(|d| d == tool)) {
1191 return Err(Denial::DeniedByStep(step.id.clone()));
1192 }
1193 if self.flow.ambient.iter().any(|a| a == tool) {
1196 return Ok(());
1197 }
1198 let restricting: Vec<&&Step> = active.iter().filter(|s| !s.allow.is_empty()).collect();
1199 if !restricting.is_empty()
1200 && !restricting
1201 .iter()
1202 .any(|s| s.allow.iter().any(|a| a == tool))
1203 {
1204 return Err(Denial::NotInStep(
1205 restricting.iter().map(|s| s.id.clone()).collect(),
1206 ));
1207 }
1208 Ok(())
1209 }
1210
1211 fn render_denial(&self, tool: &str, denial: &Denial, state: &State) -> String {
1222 let head = match denial {
1223 Denial::OnceExhausted => {
1224 return format!(
1226 "'{tool}' has already run and may run only once in this \
1227 conversation. Do not call it again."
1228 );
1229 }
1230 Denial::NotYet(condition) => {
1231 format!("'{tool}' is not permitted yet — first, {condition}.")
1232 }
1233 Denial::DeniedByStep(step) => {
1234 format!("'{tool}' is not allowed during the current step ('{step}').")
1235 }
1236 Denial::NotInStep(steps) => format!(
1237 "'{tool}' is not part of the current step ({}).",
1238 steps
1239 .iter()
1240 .map(|s| format!("'{s}'"))
1241 .collect::<Vec<_>>()
1242 .join(", ")
1243 ),
1244 };
1245
1246 let available: Vec<String> = self
1249 .flow
1250 .tool_universe()
1251 .into_iter()
1252 .filter(|t| self.admissibility(t, state).is_ok())
1253 .collect();
1254
1255 let mut out = head;
1256 if available.is_empty() {
1257 out.push_str(" No tool is available right now — continue the conversation instead.");
1258 } else {
1259 out.push_str(" Available now: ");
1260 out.push_str(&available.join(", "));
1261 out.push('.');
1262 }
1263 let postures = self.active_postures(state);
1267 if let Some(first) = postures.first() {
1268 out.push(' ');
1269 out.push_str(first);
1270 }
1271 out
1272 }
1273
1274 pub fn observe_tool(&mut self, tool: &str, ok: bool, state: &State) {
1278 if self.mode == Enforcement::Observe {
1279 if let Err(reason) = self.admits_tool(tool, state) {
1280 self.violations.push(Violation {
1281 subject: tool.to_string(),
1282 reason,
1283 });
1284 }
1285 }
1286 if ok {
1287 self.on_tool_ok(tool, state);
1288 }
1289 }
1290}
1291
1292enum Denial {
1298 OnceExhausted,
1300 NotYet(String),
1303 DeniedByStep(String),
1305 NotInStep(Vec<String>),
1308}
1309
1310#[derive(Debug, Clone, PartialEq, Eq)]
1312pub enum FlowError {
1313 Invalid(String),
1315 UnreachableStep(String),
1317 UnguardedCommitTool(String),
1320 UnknownTool(String),
1324 UnsatisfiableGuard {
1328 tool: String,
1330 step: String,
1332 },
1333 OrderingCycle(Vec<String>),
1337}
1338
1339impl std::fmt::Display for FlowError {
1340 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1341 match self {
1342 FlowError::Invalid(m) => write!(f, "{m}"),
1343 FlowError::UnreachableStep(id) => {
1344 write!(f, "step '{id}' is unreachable from any root")
1345 }
1346 FlowError::UnguardedCommitTool(t) => write!(
1347 f,
1348 "commit tool '{t}' is guarded by an always-true condition (effectively unguarded)"
1349 ),
1350 FlowError::UnknownTool(t) => write!(
1351 f,
1352 "flow references tool '{t}' which is not in the provided tool registry"
1353 ),
1354 FlowError::UnsatisfiableGuard { tool, step } => write!(
1355 f,
1356 "`never('{tool}').until(..)` references unknown step '{step}' — the guard can \
1357 never hold, so '{tool}' would be forbidden forever"
1358 ),
1359 FlowError::OrderingCycle(steps) => write!(
1360 f,
1361 "ordering cycle across `after`/`before` edges: {} (no step on it can ever start)",
1362 steps.join(" -> ")
1363 ),
1364 }
1365 }
1366}
1367
1368#[derive(Debug, Clone, PartialEq, Eq)]
1370pub struct FlowErrors(pub Vec<FlowError>);
1371
1372impl std::fmt::Display for FlowErrors {
1373 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1374 writeln!(f, "flow failed to compile ({} error(s)):", self.0.len())?;
1375 for e in &self.0 {
1376 writeln!(f, " - {e}")?;
1377 }
1378 Ok(())
1379 }
1380}
1381
1382impl std::error::Error for FlowErrors {}
1383
1384#[derive(Debug, Clone, Default)]
1387pub struct ToolPolicy {
1388 pub tools: BTreeSet<String>,
1390}
1391
1392#[derive(Debug, Clone)]
1398pub struct CompiledFlow {
1399 flow: Flow,
1400 policy: ToolPolicy,
1401}
1402
1403impl CompiledFlow {
1404 pub fn flow(&self) -> &Flow {
1406 &self.flow
1407 }
1408 pub fn tool_policy(&self) -> &ToolPolicy {
1410 &self.policy
1411 }
1412 pub fn to_mermaid(&self) -> String {
1414 self.flow.to_mermaid()
1415 }
1416 pub fn into_flow(self) -> Flow {
1418 self.flow
1419 }
1420}
1421
1422#[derive(Debug, Clone, Serialize)]
1428pub struct FlowExplanation {
1429 pub active: Vec<String>,
1431 pub allowed_tools: Vec<String>,
1433 pub blocked_tools: BTreeMap<String, String>,
1435 pub missing_requirements: Vec<String>,
1437}
1438
1439#[derive(Default)]
1441pub struct FlowBuilder {
1442 steps: Vec<Step>,
1443 constraints: Vec<Constraint>,
1444 confirm_tools: Vec<String>,
1445 ambient: Vec<String>,
1446}
1447
1448impl FlowBuilder {
1449 fn current(&mut self) -> &mut Step {
1450 self.steps
1451 .last_mut()
1452 .expect("call `.step(id)` before configuring a step")
1453 }
1454
1455 pub fn step(mut self, id: impl Into<String>) -> Self {
1457 self.steps.push(Step {
1458 id: id.into(),
1459 after: Vec::new(),
1460 gate: None,
1461 done: None,
1462 posture: None,
1463 ground: None,
1464 allow: Vec::new(),
1465 deny: Vec::new(),
1466 terminal: false,
1467 });
1468 self
1469 }
1470 pub fn after(mut self, dep: impl Into<String>) -> Self {
1472 self.current().after.push(dep.into());
1473 self
1474 }
1475 pub fn gate(mut self, g: Guard) -> Self {
1477 self.current().gate = Some(g);
1478 self
1479 }
1480 pub fn done(mut self, g: Guard) -> Self {
1482 self.current().done = Some(g);
1483 self
1484 }
1485 pub fn posture(mut self, text: impl Into<String>) -> Self {
1487 self.current().posture = Some(text.into());
1488 self
1489 }
1490 pub fn ground(mut self, template: impl Into<String>) -> Self {
1495 self.current().ground = Some(template.into());
1496 self
1497 }
1498 pub fn allow<I, S>(mut self, tools: I) -> Self
1500 where
1501 I: IntoIterator<Item = S>,
1502 S: Into<String>,
1503 {
1504 self.current()
1505 .allow
1506 .extend(tools.into_iter().map(Into::into));
1507 self
1508 }
1509 pub fn deny<I, S>(mut self, tools: I) -> Self
1511 where
1512 I: IntoIterator<Item = S>,
1513 S: Into<String>,
1514 {
1515 self.current()
1516 .deny
1517 .extend(tools.into_iter().map(Into::into));
1518 self
1519 }
1520 pub fn terminal(mut self) -> Self {
1522 self.current().terminal = true;
1523 self
1524 }
1525
1526 pub fn once(mut self, tool: impl Into<String>) -> Self {
1528 self.constraints.push(Constraint::Once(tool.into()));
1529 self
1530 }
1531 pub fn before(mut self, a: impl Into<String>, b: impl Into<String>) -> Self {
1533 self.constraints
1534 .push(Constraint::Before(a.into(), b.into()));
1535 self
1536 }
1537 pub fn require<I, S>(mut self, steps: I) -> Self
1539 where
1540 I: IntoIterator<Item = S>,
1541 S: Into<String>,
1542 {
1543 self.constraints.push(Constraint::Require(
1544 steps.into_iter().map(Into::into).collect(),
1545 ));
1546 self
1547 }
1548 pub fn ambient<I, S>(mut self, tools: I) -> Self
1567 where
1568 I: IntoIterator<Item = S>,
1569 S: Into<String>,
1570 {
1571 self.ambient.extend(tools.into_iter().map(Into::into));
1572 self
1573 }
1574
1575 pub fn never(self, tool: impl Into<String>) -> NeverBuilder {
1577 NeverBuilder {
1578 fb: self,
1579 tool: tool.into(),
1580 }
1581 }
1582 pub fn commit(mut self, tool: impl Into<String>, until: Guard) -> Self {
1585 let tool = tool.into();
1586 self.constraints.push(Constraint::Once(tool.clone()));
1587 self.constraints.push(Constraint::NeverUntil {
1588 tool: tool.clone(),
1589 until,
1590 });
1591 self.confirm_tools.push(tool);
1592 self
1593 }
1594
1595 pub fn build(self) -> Result<Flow, Vec<String>> {
1597 let flow = Flow {
1598 steps: self.steps,
1599 constraints: self.constraints,
1600 confirm_tools: self.confirm_tools,
1601 ambient: self.ambient,
1602 };
1603 flow.validate()?;
1604 Ok(flow)
1605 }
1606}
1607
1608pub struct NeverBuilder {
1610 fb: FlowBuilder,
1611 tool: String,
1612}
1613
1614impl NeverBuilder {
1615 pub fn until(mut self, guard: Guard) -> FlowBuilder {
1617 self.fb.constraints.push(Constraint::NeverUntil {
1618 tool: self.tool,
1619 until: guard,
1620 });
1621 self.fb
1622 }
1623}
1624
1625#[cfg(test)]
1626mod tests {
1627 use super::*;
1628 use serde_json::json;
1629
1630 #[test]
1640 fn a_refusal_names_what_is_available_instead() {
1641 let state = State::new();
1642 let mon = FlowMonitor::new(debt_flow(), Enforcement::Enforce);
1643
1644 let reason = mon
1645 .admits_tool("charge_card", &state)
1646 .expect_err("charge_card is gated behind verification");
1647
1648 assert!(
1649 reason.contains("lookup_account"),
1650 "a refusal must point at the tool that would make progress: {reason}"
1651 );
1652 assert!(
1653 reason.contains("ptp_confirmed"),
1654 "a refusal must name the condition it is waiting on: {reason}"
1655 );
1656 }
1657
1658 #[test]
1662 fn a_refusal_carries_the_active_steps_posture() {
1663 let state = State::new();
1664 let mon = FlowMonitor::new(debt_flow(), Enforcement::Enforce);
1665
1666 let reason = mon
1667 .admits_tool("charge_card", &state)
1668 .expect_err("charge_card is gated behind verification");
1669
1670 assert!(
1671 reason.contains("Verify the caller's identity."),
1672 "the active step's posture belongs in the refusal: {reason}"
1673 );
1674 }
1675
1676 #[test]
1680 fn a_spent_once_constraint_does_not_offer_alternatives() {
1681 let state = State::new();
1682 let flow = Flow::new()
1683 .step("pay")
1684 .allow(["charge_card"])
1685 .done(Guard::called_ok("charge_card"))
1686 .once("charge_card")
1687 .build()
1688 .expect("valid flow");
1689 let mut mon = FlowMonitor::new(flow, Enforcement::Enforce);
1690 mon.on_tool_ok("charge_card", &state);
1691
1692 let reason = mon
1693 .admits_tool("charge_card", &state)
1694 .expect_err("once is spent");
1695
1696 assert!(
1697 reason.contains("already run"),
1698 "a spent `once` must say so plainly: {reason}"
1699 );
1700 assert!(
1701 !reason.contains("Available now"),
1702 "nothing to redirect to — offering a menu invites a retry: {reason}"
1703 );
1704 }
1705
1706 #[test]
1710 fn the_redirection_tracks_the_conversation() {
1711 let state = State::new();
1712 let mon = FlowMonitor::new(debt_flow(), Enforcement::Enforce);
1713
1714 let before = mon
1715 .admits_tool("charge_card", &state)
1716 .expect_err("gated before the promise is confirmed");
1717 assert!(
1718 before.contains("ptp_confirmed") && before.contains("lookup_account"),
1719 "{before}"
1720 );
1721
1722 let _ = state.set("identity_verified", true);
1726 let mut mon = mon;
1727 mon.on_turn(&state);
1728 let after = mon
1729 .admits_tool("charge_card", &state)
1730 .expect_err("the promise is still unconfirmed");
1731
1732 assert!(
1733 after.contains("Give the disclosure."),
1734 "the refusal must carry the posture of the step that is now active, \
1735 not the one that was active when the session opened: {after}"
1736 );
1737 assert!(
1738 !after.contains("Verify the caller's identity."),
1739 "a completed step's posture must not keep riding along on refusals — \
1740 that is how a verified caller gets asked to verify again: {after}"
1741 );
1742 assert_ne!(
1743 before, after,
1744 "the refusal did not change when the flow advanced"
1745 );
1746 }
1747
1748 #[test]
1751 fn guards_describe_themselves_in_prose() {
1752 assert_eq!(
1753 Guard::is_true("verified").describe(),
1754 "'verified' must be true"
1755 );
1756 assert_eq!(
1757 Guard::captured(["amount", "date"]).describe(),
1758 "these must be known: amount, date"
1759 );
1760 assert_eq!(
1761 Guard::called_ok("disclose").describe(),
1762 "'disclose' must have run successfully"
1763 );
1764 assert_eq!(
1765 Guard::all([Guard::is_true("a"), Guard::done("b")]).describe(),
1766 "'a' must be true and step 'b' must be complete"
1767 );
1768 assert_eq!(
1769 Guard::custom(|_| true).describe(),
1770 "a condition set by the application",
1771 "a custom guard has no readable spec, and must not pretend otherwise"
1772 );
1773 }
1774
1775 fn debt_flow() -> Flow {
1776 Flow::new()
1777 .step("verify")
1778 .posture("Verify the caller's identity.")
1779 .allow(["lookup_account"])
1780 .done(Guard::is_true("identity_verified"))
1781 .step("disclose")
1782 .after("verify")
1783 .posture("Give the disclosure.")
1784 .done(Guard::is_true("disclosure_given"))
1785 .step("capture_ptp")
1786 .after("disclose")
1787 .done(Guard::captured(["ptp_amount", "ptp_date"]))
1788 .step("take_payment")
1789 .after("capture_ptp")
1790 .allow(["charge_card"])
1791 .done(Guard::called_ok("charge_card"))
1792 .step("close")
1793 .after("capture_ptp")
1794 .terminal()
1795 .never("charge_card")
1796 .until(Guard::is_true("ptp_confirmed"))
1797 .once("charge_card")
1798 .require(["close"])
1799 .build()
1800 .expect("valid flow")
1801 }
1802
1803 #[test]
1804 fn validates_and_detects_unknown_dep() {
1805 let bad = Flow::new()
1806 .step("a")
1807 .done(Guard::is_true("x"))
1808 .step("b")
1809 .after("missing")
1810 .terminal()
1811 .build();
1812 assert!(bad.is_err());
1813 }
1814
1815 #[test]
1816 fn detects_cycle() {
1817 let steps = vec![
1819 Step {
1820 id: "a".into(),
1821 after: vec!["b".into()],
1822 gate: None,
1823 done: Some(Guard::always()),
1824 posture: None,
1825 ground: None,
1826 allow: vec![],
1827 deny: vec![],
1828 terminal: false,
1829 },
1830 Step {
1831 id: "b".into(),
1832 after: vec!["a".into()],
1833 gate: None,
1834 done: Some(Guard::always()),
1835 posture: None,
1836 ground: None,
1837 allow: vec![],
1838 deny: vec![],
1839 terminal: false,
1840 },
1841 ];
1842 let flow = Flow {
1843 steps,
1844 ..Flow::default()
1845 };
1846 assert!(flow.validate().is_err());
1847 }
1848
1849 #[test]
1850 fn marking_latches_in_order() {
1851 let flow = debt_flow();
1852 let mut mon = FlowMonitor::new(flow, Enforcement::Enforce);
1853 let state = State::new();
1854
1855 assert_eq!(
1857 mon.active_steps(&state)
1858 .iter()
1859 .map(|s| s.id.as_str())
1860 .collect::<Vec<_>>(),
1861 vec!["verify"]
1862 );
1863
1864 let _ = state.set("identity_verified", true);
1865 mon.on_turn(&state);
1866 assert!(mon.marking().done.contains("verify"));
1867 assert_eq!(mon.verdict("verify", &state), Verdict::Done);
1868 assert_eq!(mon.verdict("disclose", &state), Verdict::Active);
1869
1870 let _ = state.set("disclosure_given", true);
1871 let _ = state.set("ptp_amount", 200);
1872 let _ = state.set("ptp_date", "2026-06-05");
1873 mon.on_turn(&state);
1874 assert!(mon.marking().done.contains("capture_ptp"));
1876 assert!(mon.marking().done.contains("close"));
1877 assert!(mon.is_complete());
1878 }
1879
1880 #[test]
1881 fn enforces_never_until_and_once() {
1882 let flow = debt_flow();
1883 let mut mon = FlowMonitor::new(flow, Enforcement::Enforce);
1884 let state = State::new();
1885 let _ = state.set("identity_verified", true);
1887 let _ = state.set("disclosure_given", true);
1888 let _ = state.set("ptp_amount", 200);
1889 let _ = state.set("ptp_date", "x");
1890 mon.on_turn(&state);
1891
1892 assert!(mon.admits_tool("charge_card", &state).is_err());
1894 let _ = state.set("ptp_confirmed", true);
1895 assert!(mon.admits_tool("charge_card", &state).is_ok());
1896
1897 mon.on_tool_ok("charge_card", &state);
1899 assert!(mon.admits_tool("charge_card", &state).is_err());
1900 }
1901
1902 #[test]
1903 fn whitelist_scopes_tools_to_active_step() {
1904 let flow = debt_flow();
1905 let mon = FlowMonitor::new(flow, Enforcement::Enforce);
1906 let state = State::new();
1907 assert!(mon.admits_tool("lookup_account", &state).is_ok());
1909 assert!(mon.admits_tool("charge_card", &state).is_err());
1910 }
1911
1912 #[test]
1913 fn observe_mode_records_violations_not_blocks() {
1914 let flow = debt_flow();
1915 let mut mon = FlowMonitor::new(flow, Enforcement::Observe);
1916 let state = State::new();
1917 mon.observe_tool("charge_card", true, &state);
1919 assert_eq!(mon.violations().len(), 1);
1920 assert_eq!(mon.violations()[0].subject, "charge_card");
1921 }
1922
1923 #[test]
1924 fn compile_accepts_valid_flow_and_collects_tool_universe() {
1925 let compiled = debt_flow().compile().expect("valid flow compiles");
1926 assert!(compiled.tool_policy().tools.contains("charge_card"));
1928 assert!(compiled.tool_policy().tools.contains("lookup_account"));
1929 let _ = FlowMonitor::compiled(compiled, Enforcement::Enforce);
1930 }
1931
1932 #[test]
1933 fn compile_rejects_unreachable_step() {
1934 let flow = Flow::new()
1939 .step("a")
1940 .done(Guard::is_true("a_done"))
1941 .step("b")
1942 .after("a")
1943 .done(Guard::is_true("b_done"))
1944 .step("island")
1945 .after("b")
1946 .gate(Guard::is_true("never"))
1947 .terminal()
1948 .build()
1949 .expect("structurally valid");
1950 assert!(flow.compile().is_ok());
1952 }
1953
1954 #[test]
1955 fn compile_rejects_unguarded_commit_tool() {
1956 let flow = Flow::new()
1958 .step("s")
1959 .allow(["pay"])
1960 .done(Guard::called_ok("pay"))
1961 .terminal()
1962 .commit("pay", Guard::always())
1963 .build()
1964 .expect("structurally valid");
1965 let err = flow
1966 .compile()
1967 .expect_err("unguarded commit must fail to compile");
1968 assert!(err
1969 .0
1970 .iter()
1971 .any(|e| matches!(e, FlowError::UnguardedCommitTool(t) if t == "pay")));
1972 }
1973
1974 #[test]
1975 fn compile_with_tools_accepts_a_covering_registry() {
1976 let compiled = debt_flow()
1977 .compile_with_tools(&["lookup_account", "charge_card", "unrelated_extra"])
1978 .expect("registry covers the flow's tool universe");
1979 assert!(compiled.tool_policy().tools.contains("charge_card"));
1980 }
1981
1982 #[test]
1983 fn compile_with_tools_reports_dangling_tool_names() {
1984 let err = debt_flow()
1987 .compile_with_tools(&["lookup_account"])
1988 .expect_err("dangling tool must fail to compile");
1989 assert!(err
1990 .0
1991 .iter()
1992 .any(|e| matches!(e, FlowError::UnknownTool(t) if t == "charge_card")));
1993 assert!(debt_flow().compile().is_ok());
1995 }
1996
1997 #[test]
1998 fn compile_rejects_never_until_guard_on_unknown_step() {
1999 let flow = Flow::new()
2002 .step("s")
2003 .allow(["pay"])
2004 .done(Guard::called_ok("pay"))
2005 .never("pay")
2006 .until(Guard::done("missing"))
2007 .build()
2008 .expect("structurally valid for build()");
2009 let err = flow.compile().expect_err("unsatisfiable guard must fail");
2010 assert!(err.0.iter().any(|e| matches!(
2011 e,
2012 FlowError::UnsatisfiableGuard { tool, step } if tool == "pay" && step == "missing"
2013 )));
2014 }
2015
2016 #[test]
2017 fn compile_rejects_before_cycle() {
2018 let flow = Flow::new()
2022 .step("a")
2023 .done(Guard::is_true("a_done"))
2024 .step("b")
2025 .done(Guard::is_true("b_done"))
2026 .before("a", "b")
2027 .before("b", "a")
2028 .build()
2029 .expect("build() only checks `after` cycles");
2030 let err = flow
2031 .compile()
2032 .expect_err("before-cycle must fail to compile");
2033 assert!(err.0.iter().any(|e| matches!(
2034 e,
2035 FlowError::OrderingCycle(steps)
2036 if steps.contains(&"a".to_string()) && steps.contains(&"b".to_string())
2037 )));
2038 }
2039
2040 #[test]
2041 fn explain_reports_blocked_tools_and_reasons() {
2042 let flow = debt_flow();
2043 let mon = FlowMonitor::new(flow, Enforcement::Enforce);
2044 let state = State::new();
2045 let ex = mon.explain(&state);
2046 assert!(ex.blocked_tools.contains_key("charge_card"));
2048 assert!(ex.active.contains(&"verify".to_string()));
2049 assert_eq!(mon.why_blocked(&state).blocked_tools, ex.blocked_tools);
2051 }
2052
2053 #[test]
2054 fn before_constraint_gates_step_eligibility() {
2055 let flow = Flow::new()
2059 .step("a")
2060 .done(Guard::is_true("a_done"))
2061 .step("b")
2062 .done(Guard::is_true("b_done"))
2063 .before("a", "b")
2064 .build()
2065 .expect("valid flow");
2066 let mut mon = FlowMonitor::new(flow, Enforcement::Enforce);
2067 let state = State::new();
2068
2069 let active: Vec<String> = mon
2071 .active_steps(&state)
2072 .iter()
2073 .map(|s| s.id.clone())
2074 .collect();
2075 assert!(active.contains(&"a".to_string()));
2076 assert!(
2077 !active.contains(&"b".to_string()),
2078 "b must wait for a (Before)"
2079 );
2080
2081 let _ = state.set("a_done", true);
2082 mon.on_turn(&state);
2083 let active: Vec<String> = mon
2084 .active_steps(&state)
2085 .iter()
2086 .map(|s| s.id.clone())
2087 .collect();
2088 assert!(active.contains(&"b".to_string()), "b active once a is done");
2089 }
2090
2091 #[test]
2092 fn custom_guard_in_combinator_is_not_erased() {
2093 let always_false = Guard::all([Guard::is_true("present"), Guard::custom(|_| false)]);
2096 assert!(matches!(always_false, Guard::Custom(_)));
2098
2099 let state = State::new();
2100 let _ = state.set("present", true);
2101 let marking = Marking::default();
2102 let ctx = FlowCtx {
2103 state: &state,
2104 marking: &marking,
2105 };
2106 assert!(!always_false.eval(&ctx), "custom guard must still veto");
2108
2109 let serializable = Guard::all([Guard::is_true("a"), Guard::is_set("b")]);
2111 assert!(matches!(serializable, Guard::Spec(_)));
2112 }
2113
2114 #[test]
2115 fn serde_round_trips_data_driven_flow() {
2116 let flow = debt_flow();
2117 let jsonv = serde_json::to_value(&flow).expect("serialize");
2118 let back: Flow = serde_json::from_value(jsonv).expect("deserialize");
2119 back.validate().expect("round-tripped flow is valid");
2120 assert_eq!(back.steps.len(), flow.steps.len());
2121 }
2122
2123 #[test]
2124 fn custom_guard_is_not_serializable() {
2125 let flow = Flow::new()
2126 .step("a")
2127 .done(Guard::custom(|ctx| ctx.state.contains("ready")))
2128 .terminal()
2129 .build()
2130 .unwrap();
2131 assert!(serde_json::to_value(&flow).is_err());
2132 }
2133
2134 #[test]
2135 fn mermaid_export_has_nodes_and_edges() {
2136 let m = debt_flow().to_mermaid();
2137 assert!(m.contains("flowchart TD"));
2138 assert!(m.contains("verify --> disclose"));
2139 assert!(m.contains("close([close])")); }
2141
2142 struct WriteAgent;
2143 #[async_trait::async_trait]
2144 impl TextAgent for WriteAgent {
2145 fn name(&self) -> &str {
2146 "writer"
2147 }
2148 async fn run(&self, _state: &State) -> Result<String, crate::error::AgentError> {
2149 Ok("available".to_string())
2150 }
2151 }
2152
2153 #[tokio::test]
2154 async fn on_enter_fires_once_when_step_activates() {
2155 let flow = Flow::new()
2158 .step("collect")
2159 .done(Guard::is_true("collected"))
2160 .step("check")
2161 .after("collect")
2162 .done(Guard::resolved("check"))
2163 .step("book")
2164 .after("check")
2165 .terminal()
2166 .require(["book"])
2167 .build()
2168 .expect("valid flow");
2169
2170 let mut mon = FlowMonitor::new(flow, Enforcement::Enforce)
2171 .on_enter("check", run(Arc::new(WriteAgent), AgentMode::Call));
2172 let state = State::new();
2173
2174 assert_eq!(mon.take_newly_active(&state), vec!["collect".to_string()]);
2176 assert!(mon.take_newly_active(&state).is_empty());
2178
2179 let _ = state.set("collected", true);
2181 mon.on_turn(&state);
2182 mon.fire_enter_actions(&state).await;
2183 assert_eq!(
2184 state.get::<String>("check:result").as_deref(),
2185 Some("available")
2186 );
2187
2188 mon.on_turn(&state);
2190 assert!(mon.marking().done.contains("check"));
2191 assert!(mon.is_complete());
2192 assert!(mon.take_newly_active(&state).is_empty());
2194 }
2195
2196 #[test]
2197 fn ground_template_interpolates_and_branches() {
2198 let state = State::new();
2199 let _ = state.set("when", "3pm");
2200 let _ = state.set("available", true);
2201 let _ = state.set("prior_visits", 2);
2202 assert_eq!(
2203 render_ground(
2204 "{when} is {available?open:taken}; {prior_visits} prior visits.",
2205 &state
2206 ),
2207 "3pm is open; 2 prior visits."
2208 );
2209 let _ = state.set("available", false);
2211 assert_eq!(
2212 render_ground("slot {missing}is {available?free:full}", &state),
2213 "slot is full"
2214 );
2215 }
2216
2217 #[test]
2218 fn active_grounds_projects_only_active_steps() {
2219 let flow = Flow::new()
2220 .step("collect")
2221 .ground("Known time: {when}.")
2222 .done(Guard::is_set("when"))
2223 .step("done")
2224 .after("collect")
2225 .terminal()
2226 .build()
2227 .expect("valid flow");
2228 let mut mon = FlowMonitor::new(flow, Enforcement::Enforce);
2229 let state = State::new();
2230 let _ = state.set("when", "3pm");
2231 assert_eq!(
2232 mon.active_grounds(&state),
2233 vec!["Known time: 3pm.".to_string()]
2234 );
2235 mon.on_turn(&state);
2237 assert!(mon.active_grounds(&state).is_empty());
2238 }
2239
2240 #[test]
2241 fn eq_guard_matches_state_value() {
2242 let g = Guard::eq("status", json!("active"));
2243 let state = State::new();
2244 let marking = Marking::default();
2245 assert!(!g.eval(&FlowCtx {
2246 state: &state,
2247 marking: &marking
2248 }));
2249 let _ = state.set("status", "active");
2250 assert!(g.eval(&FlowCtx {
2251 state: &state,
2252 marking: &marking
2253 }));
2254 }
2255
2256 fn ambient_flow() -> Flow {
2271 Flow::new()
2272 .ambient(["recall_context"])
2273 .step("gather")
2274 .allow(["ask_diet"])
2275 .done(Guard::is_set("user:diet"))
2276 .step("book")
2277 .after("gather")
2278 .allow(["book_table"])
2279 .done(Guard::called_ok("book_table"))
2280 .build()
2281 .expect("flow is structurally valid")
2282 }
2283
2284 #[test]
2285 fn an_ambient_tool_survives_a_step_whitelist() {
2286 let mon = FlowMonitor::new(ambient_flow(), Enforcement::Enforce);
2287 let state = State::new();
2288 assert_eq!(
2289 mon.admits_tool("ask_diet", &state),
2290 Ok(()),
2291 "the step's own tool is admitted"
2292 );
2293 assert!(
2294 mon.admits_tool("book_table", &state).is_err(),
2295 "a later step's tool is still excluded by `gather`'s whitelist"
2296 );
2297 assert_eq!(
2298 mon.admits_tool("recall_context", &state),
2299 Ok(()),
2300 "an ambient tool must not be caught by a whitelist that never meant to exclude it"
2301 );
2302 }
2303
2304 #[test]
2305 fn an_ambient_tool_is_still_denied_when_a_step_names_it() {
2306 let flow = Flow::new()
2309 .ambient(["recall_context"])
2310 .step("sensitive")
2311 .deny(["recall_context"])
2312 .done(Guard::is_true("done"))
2313 .build()
2314 .expect("flow is structurally valid");
2315 let mon = FlowMonitor::new(flow, Enforcement::Enforce);
2316 assert!(
2317 mon.admits_tool("recall_context", &State::new()).is_err(),
2318 "an explicit `deny` outranks ambient"
2319 );
2320 }
2321
2322 #[test]
2323 fn an_ambient_tool_still_obeys_never_until() {
2324 let flow = Flow::new()
2325 .ambient(["manage_memory"])
2326 .step("verify")
2327 .done(Guard::is_true("verified"))
2328 .never("manage_memory")
2329 .until(Guard::is_true("verified"))
2330 .build()
2331 .expect("flow is structurally valid");
2332 let mon = FlowMonitor::new(flow, Enforcement::Enforce);
2333 let state = State::new();
2334 assert!(
2335 mon.admits_tool("manage_memory", &state).is_err(),
2336 "`never(..).until(..)` names the tool; ambient must not unlock it"
2337 );
2338 let _ = state.set("verified", true);
2339 assert_eq!(
2340 mon.admits_tool("manage_memory", &state),
2341 Ok(()),
2342 "once the guard holds the constraint stops binding"
2343 );
2344 }
2345
2346 #[test]
2347 fn an_ambient_tool_still_obeys_once() {
2348 let flow = Flow::new()
2349 .ambient(["summarize"])
2350 .step("work")
2351 .done(Guard::called_ok("summarize"))
2352 .once("summarize")
2353 .build()
2354 .expect("flow is structurally valid");
2355 let mut mon = FlowMonitor::new(flow, Enforcement::Enforce);
2356 let state = State::new();
2357 assert_eq!(mon.admits_tool("summarize", &state), Ok(()));
2358 mon.observe_tool("summarize", true, &state);
2359 assert!(
2360 mon.admits_tool("summarize", &state).is_err(),
2361 "`once` names the tool; ambient must not exempt it"
2362 );
2363 }
2364
2365 #[test]
2366 fn ambient_tools_join_the_tool_universe() {
2367 let flow = ambient_flow();
2370 assert!(
2371 flow.tool_universe().contains("recall_context"),
2372 "an ambient tool is part of the flow's tool universe"
2373 );
2374 assert!(
2375 ambient_flow()
2376 .compile_with_tools(&["ask_diet", "book_table"])
2377 .is_err(),
2378 "a registry missing the ambient tool must not compile clean"
2379 );
2380 assert!(
2381 ambient_flow()
2382 .compile_with_tools(&["ask_diet", "book_table", "recall_context"])
2383 .is_ok(),
2384 "a covering registry compiles"
2385 );
2386 }
2387
2388 #[test]
2389 fn a_flow_with_no_ambient_tools_is_unchanged() {
2390 let mon = FlowMonitor::new(debt_flow(), Enforcement::Enforce);
2393 let state = State::new();
2394 assert_eq!(mon.admits_tool("lookup_account", &state), Ok(()));
2395 assert!(mon.admits_tool("charge_card", &state).is_err());
2396 assert!(mon.admits_tool("anything_else", &state).is_err());
2397 }
2398}