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 mod decided;
33pub mod stack;
34pub mod timing;
35pub mod verbatim;
36pub use decided::{
37 DECISION_PREFIX, DECISION_TURN_KEY, Decided, DecisionRecord, Expect, Outcome, decision_key,
38};
39pub use stack::{
40 DecisionScope, FlowStack, OVERLAY_STATE_KEY, Overlay, RepairPolicy, Resume, SharedFlowStack,
41 TERMINATED_STATE_KEY, TOOL_CALL_KEY, TOOL_DENIED_KEY, TOOL_RESULT_KEY, correction_flag,
42 escalate_flag, reprompt_flag,
43};
44pub use timing::{DEFAULT_REPROMPT, VOICE_TIMING_KEY, VoiceTiming};
45pub use verbatim::{VERBATIM_KEY, VerbatimRequirement, verbatim_flag};
46
47pub struct FlowCtx<'a> {
50 pub state: &'a State,
52 pub marking: &'a Marking,
54}
55
56#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, schemars::JsonSchema)]
58#[serde(rename_all = "snake_case")]
59pub enum Pred {
60 Always,
62 IsTrue(String),
64 IsSet(String),
66 Eq(String, Value),
68 Captured(Vec<String>),
70 CalledOk(String),
72 Done(String),
74 Decided(Decided),
77 All(Vec<Pred>),
79 Any(Vec<Pred>),
81 Not(Box<Pred>),
83}
84
85fn all_of(values: impl IntoIterator<Item = Option<bool>>) -> Option<bool> {
87 let mut unknown = false;
88 for value in values {
89 match value {
90 Some(false) => return Some(false),
91 None => unknown = true,
92 Some(true) => {}
93 }
94 }
95 if unknown { None } else { Some(true) }
96}
97
98fn any_of(values: impl IntoIterator<Item = Option<bool>>) -> Option<bool> {
100 let mut unknown = false;
101 for value in values {
102 match value {
103 Some(true) => return Some(true),
104 None => unknown = true,
105 Some(false) => {}
106 }
107 }
108 if unknown { None } else { Some(false) }
109}
110
111impl Pred {
112 fn possible(&self, ctx: &FlowCtx) -> Option<bool> {
115 match self {
116 Pred::Decided(_) => None,
117 Pred::All(ps) => all_of(ps.iter().map(|p| p.possible(ctx))),
118 Pred::Any(ps) => any_of(ps.iter().map(|p| p.possible(ctx))),
119 Pred::Not(p) => p.possible(ctx).map(|b| !b),
120 other => Some(other.eval(ctx)),
121 }
122 }
123
124 fn eval(&self, ctx: &FlowCtx) -> bool {
125 match self {
126 Pred::Always => true,
127 Pred::IsTrue(k) => ctx.state.get::<bool>(k) == Some(true),
128 Pred::IsSet(k) => ctx.state.contains(k),
129 Pred::Eq(k, v) => ctx.state.get::<Value>(k).as_ref() == Some(v),
130 Pred::Captured(fields) => fields.iter().all(|f| ctx.state.contains(f)),
131 Pred::CalledOk(t) => ctx.marking.tool_ok.contains_key(t),
132 Pred::Done(s) => ctx.marking.done.contains(s),
133 Pred::Decided(d) => d.holds(ctx.state),
134 Pred::All(ps) => ps.iter().all(|p| p.eval(ctx)),
135 Pred::Any(ps) => ps.iter().any(|p| p.eval(ctx)),
136 Pred::Not(p) => !p.eval(ctx),
137 }
138 }
139
140 fn describe(&self) -> String {
142 fn join(ps: &[Pred], sep: &str) -> String {
143 ps.iter().map(Pred::describe).collect::<Vec<_>>().join(sep)
144 }
145 match self {
146 Pred::Always => "nothing".to_string(),
147 Pred::IsTrue(k) => format!("'{k}' must be true"),
148 Pred::IsSet(k) => format!("'{k}' must be known"),
149 Pred::Eq(k, v) => format!("'{k}' must be {v}"),
150 Pred::Captured(fields) => {
151 format!("these must be known: {}", fields.join(", "))
152 }
153 Pred::CalledOk(t) => format!("'{t}' must have run successfully"),
154 Pred::Done(s) => format!("step '{s}' must be complete"),
155 Pred::Decided(d) => d.describe(),
156 Pred::All(ps) => join(ps, " and "),
157 Pred::Any(ps) => join(ps, " or "),
158 Pred::Not(p) => format!("it must not be the case that {}", p.describe()),
159 }
160 }
161
162 fn referenced_steps(&self, out: &mut Vec<String>) {
164 match self {
165 Pred::Done(s) => out.push(s.clone()),
166 Pred::All(ps) | Pred::Any(ps) => ps.iter().for_each(|p| p.referenced_steps(out)),
167 Pred::Not(p) => p.referenced_steps(out),
168 _ => {}
169 }
170 }
171
172 fn referenced_tools(&self, out: &mut Vec<String>) {
174 match self {
175 Pred::CalledOk(t) => out.push(t.clone()),
176 Pred::All(ps) | Pred::Any(ps) => ps.iter().for_each(|p| p.referenced_tools(out)),
177 Pred::Not(p) => p.referenced_tools(out),
178 _ => {}
179 }
180 }
181
182 fn referenced_decisions(&self, out: &mut BTreeSet<String>) {
184 match self {
185 Pred::Decided(d) => {
186 out.insert(d.question.clone());
187 }
188 Pred::All(ps) | Pred::Any(ps) => ps.iter().for_each(|p| p.referenced_decisions(out)),
189 Pred::Not(p) => p.referenced_decisions(out),
190 _ => {}
191 }
192 }
193
194 fn referenced_state_keys(&self, out: &mut BTreeSet<String>) {
198 match self {
199 Pred::IsTrue(k) | Pred::IsSet(k) | Pred::Eq(k, _) => {
200 out.insert(k.clone());
201 }
202 Pred::Captured(fields) => out.extend(fields.iter().cloned()),
203 Pred::All(ps) | Pred::Any(ps) => {
204 ps.iter().for_each(|p| p.referenced_state_keys(out));
205 }
206 Pred::Not(p) => p.referenced_state_keys(out),
207 _ => {}
208 }
209 }
210
211 fn explain(&self, ctx: &FlowCtx) -> GuardTrace {
215 let children = match self {
216 Pred::All(ps) | Pred::Any(ps) => ps.iter().map(|p| p.explain(ctx)).collect(),
217 Pred::Not(p) => vec![p.explain(ctx)],
218 _ => Vec::new(),
219 };
220 GuardTrace {
221 desc: match self {
222 Pred::All(_) => "all of".to_string(),
225 Pred::Any(_) => "any of".to_string(),
226 Pred::Not(_) => "not".to_string(),
227 atom => atom.describe(),
228 },
229 holds: self.eval(ctx),
230 children,
231 }
232 }
233}
234
235#[derive(Clone, Debug, Serialize, schemars::JsonSchema)]
239pub struct GuardTrace {
240 pub desc: String,
242 pub holds: bool,
244 #[serde(default, skip_serializing_if = "Vec::is_empty")]
246 pub children: Vec<GuardTrace>,
247}
248
249pub fn render_ground(template: &str, state: &State) -> String {
260 let mut out = String::with_capacity(template.len());
261 let mut rest = template;
262 while let Some(open) = rest.find('{') {
263 out.push_str(&rest[..open]);
264 let after = &rest[open + 1..];
265 let Some(close) = after.find('}') else {
266 out.push_str(&rest[open..]);
268 return out;
269 };
270 let expr = &after[..close];
271 out.push_str(&render_expr(expr, state));
272 rest = &after[close + 1..];
273 }
274 out.push_str(rest);
275 out
276}
277
278fn render_expr(expr: &str, state: &State) -> String {
279 if let Some((cond, arms)) = expr.split_once('?') {
280 let (yes, no) = arms.split_once(':').unwrap_or((arms, ""));
281 if is_truthy(state, cond.trim()) {
282 yes.to_string()
283 } else {
284 no.to_string()
285 }
286 } else {
287 match state.get::<Value>(expr.trim()) {
288 Some(Value::String(s)) => s,
289 Some(v) => v.to_string(),
290 None => String::new(),
291 }
292 }
293}
294
295fn is_truthy(state: &State, key: &str) -> bool {
296 match state.get::<Value>(key) {
297 None | Some(Value::Null) => false,
298 Some(Value::Bool(b)) => b,
299 Some(Value::Number(n)) => n.as_f64().map(|f| f != 0.0).unwrap_or(true),
300 Some(Value::String(s)) => !s.is_empty(),
301 Some(_) => true,
302 }
303}
304
305type CustomFn = Arc<dyn Fn(&FlowCtx) -> bool + Send + Sync>;
306
307#[derive(Clone)]
313pub enum Guard {
314 Spec(Pred),
316 Custom(CustomFn),
318}
319
320impl Guard {
321 pub fn always() -> Self {
323 Guard::Spec(Pred::Always)
324 }
325 pub fn is_true(key: impl Into<String>) -> Self {
327 Guard::Spec(Pred::IsTrue(key.into()))
328 }
329 pub fn is_set(key: impl Into<String>) -> Self {
331 Guard::Spec(Pred::IsSet(key.into()))
332 }
333 pub fn eq(key: impl Into<String>, value: impl Into<Value>) -> Self {
335 Guard::Spec(Pred::Eq(key.into(), value.into()))
336 }
337 pub fn captured<I, S>(fields: I) -> Self
339 where
340 I: IntoIterator<Item = S>,
341 S: Into<String>,
342 {
343 Guard::Spec(Pred::Captured(fields.into_iter().map(Into::into).collect()))
344 }
345 pub fn called_ok(tool: impl Into<String>) -> Self {
347 Guard::Spec(Pred::CalledOk(tool.into()))
348 }
349 pub fn done(step: impl Into<String>) -> Self {
351 Guard::Spec(Pred::Done(step.into()))
352 }
353 pub fn decided(question: impl Into<String>) -> Self {
357 Self::decided_as(question, Expect::Yes)
358 }
359 pub fn decided_no(question: impl Into<String>) -> Self {
362 Self::decided_as(question, Expect::No)
363 }
364 pub fn decided_is(question: impl Into<String>, option: impl Into<String>) -> Self {
366 Self::decided_as(question, Expect::Is(option.into()))
367 }
368 pub fn decided_at_least(question: impl Into<String>, bound: f64) -> Self {
370 Self::decided_as(question, Expect::AtLeast(bound))
371 }
372 pub fn decided_at_most(question: impl Into<String>, bound: f64) -> Self {
374 Self::decided_as(question, Expect::AtMost(bound))
375 }
376 fn decided_as(question: impl Into<String>, expect: Expect) -> Self {
377 Guard::Spec(Pred::Decided(Decided {
378 question: question.into(),
379 expect,
380 }))
381 }
382 pub fn resolved(name: impl AsRef<str>) -> Self {
386 Guard::Spec(Pred::IsSet(format!("{}:result", name.as_ref())))
387 }
388 pub fn all(guards: impl IntoIterator<Item = Guard>) -> Self {
397 let guards: Vec<Guard> = guards.into_iter().collect();
398 if guards.iter().all(|g| matches!(g, Guard::Spec(_))) {
399 Guard::Spec(Pred::All(specs_unchecked(guards)))
400 } else {
401 Guard::Custom(Arc::new(move |ctx| guards.iter().all(|g| g.eval(ctx))))
402 }
403 }
404 pub fn any(guards: impl IntoIterator<Item = Guard>) -> Self {
409 let guards: Vec<Guard> = guards.into_iter().collect();
410 if guards.iter().all(|g| matches!(g, Guard::Spec(_))) {
411 Guard::Spec(Pred::Any(specs_unchecked(guards)))
412 } else {
413 Guard::Custom(Arc::new(move |ctx| guards.iter().any(|g| g.eval(ctx))))
414 }
415 }
416 #[allow(clippy::should_implement_trait)]
418 pub fn not(guard: Guard) -> Self {
419 match guard {
420 Guard::Spec(p) => Guard::Spec(Pred::Not(Box::new(p))),
421 Guard::Custom(f) => Guard::Custom(Arc::new(move |ctx| !f(ctx))),
423 }
424 }
425 pub fn describe(&self) -> String {
435 match self {
436 Guard::Spec(p) => p.describe(),
437 Guard::Custom(_) => "a condition set by the application".to_string(),
438 }
439 }
440
441 pub fn custom(f: impl Fn(&FlowCtx) -> bool + Send + Sync + 'static) -> Self {
443 Guard::Custom(Arc::new(f))
444 }
445
446 pub fn possible(&self, ctx: &FlowCtx) -> Option<bool> {
452 match self {
453 Guard::Spec(p) => p.possible(ctx),
454 Guard::Custom(f) => Some(f(ctx)),
455 }
456 }
457
458 pub fn eval(&self, ctx: &FlowCtx) -> bool {
460 match self {
461 Guard::Spec(p) => p.eval(ctx),
462 Guard::Custom(f) => f(ctx),
463 }
464 }
465
466 pub fn eval_state(&self, state: &State) -> bool {
472 let marking = Marking::default();
473 self.eval(&FlowCtx {
474 state,
475 marking: &marking,
476 })
477 }
478
479 pub fn explain_trace(&self, ctx: &FlowCtx) -> GuardTrace {
482 match self {
483 Guard::Spec(p) => p.explain(ctx),
484 Guard::Custom(f) => GuardTrace {
485 desc: "a condition set by the application".to_string(),
486 holds: f(ctx),
487 children: Vec::new(),
488 },
489 }
490 }
491
492 fn referenced_steps(&self, out: &mut Vec<String>) {
493 if let Guard::Spec(p) = self {
494 p.referenced_steps(out);
495 }
496 }
497
498 fn referenced_state_keys(&self, out: &mut BTreeSet<String>) {
499 if let Guard::Spec(p) = self {
500 p.referenced_state_keys(out);
501 }
502 }
503
504 pub fn decisions(&self) -> BTreeSet<String> {
506 let mut out = BTreeSet::new();
507 if let Guard::Spec(p) = self {
508 p.referenced_decisions(&mut out);
509 }
510 out
511 }
512
513 pub fn state_keys(&self) -> BTreeSet<String> {
517 let mut keys = BTreeSet::new();
518 self.referenced_state_keys(&mut keys);
519 keys
520 }
521
522 fn referenced_tools(&self, out: &mut Vec<String>) {
523 if let Guard::Spec(p) = self {
524 p.referenced_tools(out);
525 }
526 }
527}
528
529fn specs_unchecked(guards: Vec<Guard>) -> Vec<Pred> {
534 guards
535 .into_iter()
536 .map(|g| match g {
537 Guard::Spec(p) => p,
538 Guard::Custom(_) => unreachable!("specs_unchecked called with a custom guard"),
539 })
540 .collect()
541}
542
543impl Serialize for Guard {
544 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
545 match self {
546 Guard::Spec(p) => p.serialize(s),
547 Guard::Custom(_) => Err(serde::ser::Error::custom(
548 "custom guards are not serializable; use Guard atoms for data-driven flows",
549 )),
550 }
551 }
552}
553
554impl<'de> Deserialize<'de> for Guard {
555 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
556 Ok(Guard::Spec(Pred::deserialize(d)?))
557 }
558}
559
560impl schemars::JsonSchema for Guard {
564 fn is_referenceable() -> bool {
565 false
566 }
567 fn schema_name() -> String {
568 "Guard".to_string()
569 }
570 fn json_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
571 generator.subschema_for::<Pred>()
572 }
573}
574
575impl std::fmt::Debug for Guard {
576 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
577 match self {
578 Guard::Spec(p) => write!(f, "{p:?}"),
579 Guard::Custom(_) => write!(f, "Custom(<fn>)"),
580 }
581 }
582}
583
584#[derive(Clone, Debug)]
594pub struct Edge {
595 pub step: String,
597 pub when: Option<Guard>,
599}
600
601impl Edge {
602 pub fn to(step: impl Into<String>) -> Self {
604 Edge {
605 step: step.into(),
606 when: None,
607 }
608 }
609 pub fn when(step: impl Into<String>, when: Guard) -> Self {
611 Edge {
612 step: step.into(),
613 when: Some(when),
614 }
615 }
616}
617
618impl<S: Into<String>> From<S> for Edge {
619 fn from(step: S) -> Self {
620 Edge::to(step)
621 }
622}
623
624impl Serialize for Edge {
625 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
626 match &self.when {
627 None => self.step.serialize(s),
629 Some(when) => {
630 use serde::ser::SerializeStruct;
631 let mut out = s.serialize_struct("Edge", 2)?;
632 out.serialize_field("step", &self.step)?;
633 out.serialize_field("when", when)?;
634 out.end()
635 }
636 }
637 }
638}
639
640impl<'de> Deserialize<'de> for Edge {
641 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
642 #[derive(Deserialize)]
643 #[serde(untagged)]
644 enum Repr {
645 Simple(String),
646 Conditional {
647 step: String,
648 #[serde(default)]
649 when: Option<Guard>,
650 },
651 }
652 Ok(match Repr::deserialize(d)? {
653 Repr::Simple(step) => Edge { step, when: None },
654 Repr::Conditional { step, when } => Edge { step, when },
655 })
656 }
657}
658
659impl schemars::JsonSchema for Edge {
660 fn schema_name() -> String {
661 "Edge".to_string()
662 }
663 fn json_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
664 #[derive(schemars::JsonSchema)]
665 #[serde(untagged)]
666 #[allow(dead_code)]
667 enum EdgeSchema {
668 Simple(String),
669 Conditional { step: String, when: Option<Pred> },
670 }
671 EdgeSchema::json_schema(generator)
672 }
673}
674
675#[derive(
677 Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema,
678)]
679#[serde(rename_all = "snake_case")]
680pub enum Join {
681 #[default]
683 All,
684 Any,
686}
687
688impl Join {
689 fn is_all(&self) -> bool {
690 *self == Join::All
691 }
692}
693
694#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)]
696pub struct Step {
697 pub id: String,
699 #[serde(default, skip_serializing_if = "Vec::is_empty")]
701 pub after: Vec<Edge>,
702 #[serde(default, skip_serializing_if = "Join::is_all")]
704 pub join: Join,
705 #[serde(default, skip_serializing_if = "Option::is_none")]
707 pub gate: Option<Guard>,
708 #[serde(default, skip_serializing_if = "Option::is_none")]
710 pub done: Option<Guard>,
711 #[serde(default, skip_serializing_if = "Option::is_none")]
713 pub posture: Option<String>,
714 #[serde(default, skip_serializing_if = "Option::is_none")]
718 pub ground: Option<String>,
719 #[serde(default, skip_serializing_if = "Vec::is_empty")]
721 pub allow: Vec<String>,
722 #[serde(default, skip_serializing_if = "Vec::is_empty")]
724 pub deny: Vec<String>,
725 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
727 pub terminal: bool,
728}
729
730#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)]
732#[serde(rename_all = "snake_case")]
733pub enum Constraint {
734 Once(String),
736 Before(String, String),
738 NeverUntil {
740 tool: String,
742 until: Guard,
744 },
745 Require(Vec<String>),
747 Reset {
756 steps: Vec<String>,
758 when: Guard,
760 },
761}
762
763#[derive(Clone, Debug, Default, Serialize, Deserialize, schemars::JsonSchema)]
765pub struct Flow {
766 pub steps: Vec<Step>,
768 #[serde(default, skip_serializing_if = "Vec::is_empty")]
770 pub constraints: Vec<Constraint>,
771 #[serde(default, skip_serializing_if = "Vec::is_empty")]
773 pub confirm_tools: Vec<String>,
774 #[serde(default, skip_serializing_if = "Vec::is_empty")]
783 pub ambient: Vec<String>,
784}
785
786impl Flow {
787 pub fn decisions(&self) -> BTreeSet<String> {
790 let mut out = BTreeSet::new();
791 let mut add = |g: &Guard| out.extend(g.decisions());
792 for step in &self.steps {
793 step.gate.iter().chain(step.done.iter()).for_each(&mut add);
794 step.after
795 .iter()
796 .filter_map(|e| e.when.as_ref())
797 .for_each(&mut add);
798 }
799 for c in &self.constraints {
800 match c {
801 Constraint::NeverUntil { until, .. } => add(until),
802 Constraint::Reset { when, .. } => add(when),
803 _ => {}
804 }
805 }
806 out
807 }
808
809 #[allow(
811 clippy::new_ret_no_self,
812 reason = "Flow::new() is the builder entry point; a Flow comes from FlowBuilder::build/compile"
813 )]
814 pub fn new() -> FlowBuilder {
815 FlowBuilder::default()
816 }
817
818 fn step(&self, id: &str) -> Option<&Step> {
819 self.steps.iter().find(|s| s.id == id)
820 }
821
822 pub fn validate(&self) -> Result<(), ConfigError> {
825 let mut errs = Vec::new();
826 let ids: BTreeSet<&str> = self.steps.iter().map(|s| s.id.as_str()).collect();
827 if ids.len() != self.steps.len() {
828 errs.push("duplicate step ids".into());
829 }
830 for s in &self.steps {
831 for d in &s.after {
832 if !ids.contains(d.step.as_str()) {
833 errs.push(format!(
834 "step '{}' depends on unknown step '{}'",
835 s.id, d.step
836 ));
837 }
838 if let Some(when) = &d.when {
839 let mut refs = Vec::new();
840 when.referenced_steps(&mut refs);
841 for r in refs {
842 if !ids.contains(r.as_str()) {
843 errs.push(format!(
844 "step '{}' edge condition references unknown step '{r}'",
845 s.id
846 ));
847 }
848 }
849 }
850 }
851 if !s.terminal && s.done.is_none() {
852 errs.push(format!(
853 "non-terminal step '{}' has no `done` condition (it can never complete)",
854 s.id
855 ));
856 }
857 for g in s.gate.iter().chain(s.done.iter()) {
858 let mut refs = Vec::new();
859 g.referenced_steps(&mut refs);
860 for r in refs {
861 if !ids.contains(r.as_str()) {
862 errs.push(format!(
863 "step '{}' guard references unknown step '{r}'",
864 s.id
865 ));
866 }
867 }
868 }
869 }
870 for c in &self.constraints {
871 match c {
872 Constraint::Before(a, b) => {
873 for x in [a, b] {
874 if !ids.contains(x.as_str()) {
875 errs.push(format!("constraint `before` references unknown step '{x}'"));
876 }
877 }
878 }
879 Constraint::Require(rs) => {
880 for r in rs {
881 if !ids.contains(r.as_str()) {
882 errs.push(format!(
883 "constraint `require` references unknown step '{r}'"
884 ));
885 }
886 }
887 }
888 Constraint::Reset { steps, .. } => {
889 for r in steps {
890 if !ids.contains(r.as_str()) {
891 errs.push(format!("constraint `reset` references unknown step '{r}'"));
892 }
893 }
894 }
895 _ => {}
896 }
897 }
898 if self.has_cycle() {
899 errs.push("flow dependency graph has a cycle (must be a DAG)".into());
900 }
901 ConfigError::from_issues(errs)
902 }
903
904 fn tool_universe(&self) -> BTreeSet<String> {
907 let mut tools = BTreeSet::new();
908 for s in &self.steps {
909 tools.extend(s.allow.iter().cloned());
910 tools.extend(s.deny.iter().cloned());
911 }
912 for c in &self.constraints {
913 match c {
914 Constraint::Once(t) => {
915 tools.insert(t.clone());
916 }
917 Constraint::NeverUntil { tool, .. } => {
918 tools.insert(tool.clone());
919 }
920 _ => {}
921 }
922 }
923 tools.extend(self.confirm_tools.iter().cloned());
924 tools.extend(self.ambient.iter().cloned());
925 tools
926 }
927
928 pub fn state_keys_read(&self) -> BTreeSet<String> {
938 let mut keys = BTreeSet::new();
939 for s in &self.steps {
940 for g in s.gate.iter().chain(s.done.iter()) {
941 g.referenced_state_keys(&mut keys);
942 }
943 for e in &s.after {
944 if let Some(when) = &e.when {
945 when.referenced_state_keys(&mut keys);
946 }
947 }
948 }
949 for c in &self.constraints {
950 match c {
951 Constraint::NeverUntil { until, .. } => until.referenced_state_keys(&mut keys),
952 Constraint::Reset { when, .. } => when.referenced_state_keys(&mut keys),
953 _ => {}
954 }
955 }
956 keys
957 }
958
959 fn reachable_steps(&self) -> BTreeSet<String> {
962 let ids: BTreeSet<&str> = self.steps.iter().map(|s| s.id.as_str()).collect();
963 let mut succ: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
965 for s in &self.steps {
966 for d in &s.after {
967 if ids.contains(d.step.as_str()) {
968 succ.entry(d.step.as_str()).or_default().push(s.id.as_str());
969 }
970 }
971 }
972 for c in &self.constraints {
973 if let Constraint::Before(a, b) = c
974 && ids.contains(a.as_str())
975 && ids.contains(b.as_str())
976 {
977 succ.entry(a.as_str()).or_default().push(b.as_str());
978 }
979 }
980 let roots: Vec<&str> = self
981 .steps
982 .iter()
983 .filter(|s| s.after.is_empty())
984 .map(|s| s.id.as_str())
985 .collect();
986 let mut seen = BTreeSet::new();
987 let mut stack = roots;
988 while let Some(id) = stack.pop() {
989 if seen.insert(id.to_string())
990 && let Some(next) = succ.get(id)
991 {
992 stack.extend(next.iter().copied());
993 }
994 }
995 seen
996 }
997
998 pub fn compile(self) -> Result<CompiledFlow, FlowErrors> {
1013 self.compile_internal(None)
1014 }
1015
1016 pub fn compile_with_tools(self, tools: &[&str]) -> Result<CompiledFlow, FlowErrors> {
1029 self.compile_internal(Some(tools))
1030 }
1031
1032 fn compile_internal(self, registry: Option<&[&str]>) -> Result<CompiledFlow, FlowErrors> {
1033 let mut errors = Vec::new();
1034 if let Err(err) = self.validate() {
1035 errors.extend(err.issues.into_iter().map(FlowError::Invalid));
1036 }
1037
1038 if errors.is_empty() {
1040 let reachable = self.reachable_steps();
1042 for s in &self.steps {
1043 if !reachable.contains(&s.id) {
1044 errors.push(FlowError::UnreachableStep(s.id.clone()));
1045 }
1046 }
1047 if let Some(cycle) = self.ordering_cycle() {
1051 errors.push(FlowError::OrderingCycle(cycle));
1052 }
1053 }
1054
1055 for tool in &self.confirm_tools {
1058 let guard = self.constraints.iter().find_map(|c| match c {
1059 Constraint::NeverUntil { tool: t, until } if t == tool => Some(until),
1060 _ => None,
1061 });
1062 let unguarded = matches!(guard, None | Some(Guard::Spec(Pred::Always)));
1063 if unguarded {
1064 errors.push(FlowError::UnguardedCommitTool(tool.clone()));
1065 }
1066 }
1067
1068 let ids: BTreeSet<&str> = self.steps.iter().map(|s| s.id.as_str()).collect();
1073 for c in &self.constraints {
1074 if let Constraint::NeverUntil { tool, until } = c {
1075 let mut refs = Vec::new();
1076 until.referenced_steps(&mut refs);
1077 for r in refs {
1078 if !ids.contains(r.as_str()) {
1079 errors.push(FlowError::UnsatisfiableGuard {
1080 tool: tool.clone(),
1081 step: r,
1082 });
1083 }
1084 }
1085 }
1086 }
1087
1088 if let Some(known) = registry {
1090 for tool in self.tool_universe() {
1091 if !known.contains(&tool.as_str()) {
1092 errors.push(FlowError::UnknownTool(tool));
1093 }
1094 }
1095 }
1096
1097 if errors.is_empty() {
1098 let surface = ToolSurface {
1099 tools: self.tool_universe(),
1100 };
1101 Ok(CompiledFlow {
1102 flow: self,
1103 surface,
1104 })
1105 } else {
1106 Err(FlowErrors(errors))
1107 }
1108 }
1109
1110 fn ordering_cycle(&self) -> Option<Vec<String>> {
1114 let mut deps: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
1116 for s in &self.steps {
1117 let entry = deps.entry(s.id.as_str()).or_default();
1118 entry.extend(s.after.iter().map(|e| e.step.as_str()));
1119 }
1120 for c in &self.constraints {
1121 if let Constraint::Before(a, b) = c {
1122 deps.entry(b.as_str()).or_default().push(a.as_str());
1123 }
1124 }
1125 fn dfs<'a>(
1127 id: &'a str,
1128 deps: &BTreeMap<&'a str, Vec<&'a str>>,
1129 color: &mut BTreeMap<&'a str, u8>,
1130 path: &mut Vec<&'a str>,
1131 ) -> Option<Vec<String>> {
1132 color.insert(id, 1);
1133 path.push(id);
1134 for d in deps.get(id).into_iter().flatten() {
1135 match color.get(d).copied() {
1136 Some(1) => {
1137 let start = path.iter().position(|p| p == d).unwrap_or(0);
1138 return Some(
1139 path[start..]
1140 .iter()
1141 .map(std::string::ToString::to_string)
1142 .collect(),
1143 );
1144 }
1145 Some(2) => {}
1146 _ => {
1147 if let Some(cycle) = dfs(d, deps, color, path) {
1148 return Some(cycle);
1149 }
1150 }
1151 }
1152 }
1153 path.pop();
1154 color.insert(id, 2);
1155 None
1156 }
1157 let mut color: BTreeMap<&str, u8> = BTreeMap::new();
1158 for s in &self.steps {
1159 if color.get(s.id.as_str()).copied().unwrap_or(0) == 0 {
1160 let mut path = Vec::new();
1161 if let Some(cycle) = dfs(&s.id, &deps, &mut color, &mut path) {
1162 return Some(cycle);
1163 }
1164 }
1165 }
1166 None
1167 }
1168
1169 fn has_cycle(&self) -> bool {
1170 let mut color: BTreeMap<&str, u8> = BTreeMap::new();
1172 fn dfs<'a>(flow: &'a Flow, id: &'a str, color: &mut BTreeMap<&'a str, u8>) -> bool {
1173 color.insert(id, 1);
1174 if let Some(step) = flow.step(id) {
1175 for d in &step.after {
1176 match color.get(d.step.as_str()).copied() {
1177 Some(1) => return true,
1178 Some(2) => {}
1179 _ => {
1180 if dfs(flow, &d.step, color) {
1181 return true;
1182 }
1183 }
1184 }
1185 }
1186 }
1187 color.insert(id, 2);
1188 false
1189 }
1190 for s in &self.steps {
1191 if color.get(s.id.as_str()).copied().unwrap_or(0) == 0 && dfs(self, &s.id, &mut color) {
1192 return true;
1193 }
1194 }
1195 false
1196 }
1197
1198 pub fn to_mermaid(&self) -> String {
1200 let mut out = String::from("flowchart TD\n");
1201 for s in &self.steps {
1202 let shape = if s.terminal {
1203 format!(" {}([{}])\n", s.id, s.id)
1204 } else {
1205 format!(" {}[{}]\n", s.id, s.id)
1206 };
1207 out.push_str(&shape);
1208 }
1209 for s in &self.steps {
1210 for d in &s.after {
1211 match &d.when {
1212 Some(when) => {
1213 let label = when.describe().replace('|', "/");
1214 out.push_str(&format!(" {} -->|{label}| {}\n", d.step, s.id));
1215 }
1216 None => out.push_str(&format!(" {} --> {}\n", d.step, s.id)),
1217 }
1218 }
1219 }
1220 out
1221 }
1222}
1223
1224#[derive(Clone, Debug, Default)]
1227pub struct Marking {
1228 pub done: BTreeSet<String>,
1230 pub tool_ok: BTreeMap<String, u32>,
1232 pub turns: u32,
1234}
1235
1236#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1238#[serde(rename_all = "snake_case")]
1239pub enum Verdict {
1240 Pending,
1242 Active,
1244 Done,
1246 Skipped,
1248}
1249
1250#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
1252pub struct Violation {
1253 pub subject: String,
1255 pub reason: String,
1257}
1258
1259#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
1264pub enum Enforcement {
1265 #[default]
1267 Enforce,
1268 Observe,
1270}
1271
1272#[derive(Clone)]
1277pub struct StepAction {
1278 name: Option<String>,
1279 agent: Arc<dyn TextAgent>,
1280 mode: AgentMode,
1281}
1282
1283pub fn on_enter(agent: Arc<dyn TextAgent>, mode: AgentMode) -> StepAction {
1291 StepAction {
1292 name: None,
1293 agent,
1294 mode,
1295 }
1296}
1297
1298impl StepAction {
1299 pub fn named(mut self, name: impl Into<String>) -> Self {
1301 self.name = Some(name.into());
1302 self
1303 }
1304
1305 pub(crate) async fn fire(&self, step_id: &str, state: &State) {
1308 let name = self.name.clone().unwrap_or_else(|| step_id.to_string());
1309 match self.mode {
1310 AgentMode::Call => {
1311 let _ = call_agent(&name, self.agent.clone(), state).await;
1312 }
1313 AgentMode::Dispatch | AgentMode::Background => {
1314 let agent = self.agent.clone();
1315 let state = state.clone();
1316 tokio::spawn(async move {
1317 let _ = call_agent(&name, agent, &state).await;
1318 });
1319 }
1320 }
1321 }
1322}
1323
1324pub type SharedFlowMonitor = Arc<parking_lot::Mutex<FlowMonitor>>;
1330
1331pub struct FlowMonitor {
1334 flow: Flow,
1335 mode: Enforcement,
1336 marking: Marking,
1337 violations: Vec<Violation>,
1338 enter_actions: HashMap<String, StepAction>,
1340 announced: BTreeSet<String>,
1342 reset_prev: Vec<bool>,
1345}
1346
1347impl FlowMonitor {
1348 pub fn new(flow: Flow, mode: Enforcement) -> Self {
1354 Self {
1355 flow,
1356 mode,
1357 marking: Marking::default(),
1358 violations: Vec::new(),
1359 enter_actions: HashMap::new(),
1360 announced: BTreeSet::new(),
1361 reset_prev: Vec::new(),
1362 }
1363 }
1364
1365 pub fn compiled(flow: CompiledFlow, mode: Enforcement) -> Self {
1367 Self::new(flow.into_flow(), mode)
1368 }
1369
1370 pub fn try_new(flow: Flow, mode: Enforcement) -> Result<Self, FlowErrors> {
1373 Ok(Self::compiled(flow.compile()?, mode))
1374 }
1375
1376 pub fn into_shared(self) -> SharedFlowMonitor {
1381 Arc::new(parking_lot::Mutex::new(self))
1382 }
1383
1384 pub fn into_stack(self) -> FlowStack {
1387 FlowStack::from_monitor(self)
1388 }
1389
1390 pub fn restart(&mut self) {
1395 self.marking = Marking::default();
1396 self.announced.clear();
1397 self.reset_prev.clear();
1398 }
1399
1400 pub fn explain(&self, state: &State) -> FlowExplanation {
1406 let active_steps = self.active_steps(state);
1407 let active: Vec<String> = active_steps.iter().map(|s| s.id.clone()).collect();
1408 let ctx = self.ctx(state);
1409 let active_progress = active_steps
1410 .iter()
1411 .filter_map(|s| {
1412 s.done
1413 .as_ref()
1414 .map(|g| (s.id.clone(), g.explain_trace(&ctx)))
1415 })
1416 .collect();
1417 let mut allowed_tools = Vec::new();
1418 let mut blocked_tools = BTreeMap::new();
1419 for tool in self.flow.tool_universe() {
1420 match self.admits_tool(&tool, state) {
1421 Ok(()) => allowed_tools.push(tool),
1422 Err(reason) => {
1423 blocked_tools.insert(tool, reason);
1424 }
1425 }
1426 }
1427 FlowExplanation {
1428 active,
1429 allowed_tools,
1430 blocked_tools,
1431 missing_requirements: self.unmet_requirements(),
1432 active_progress,
1433 }
1434 }
1435
1436 pub fn on_enter(mut self, step: impl Into<String>, action: StepAction) -> Self {
1439 self.enter_actions.insert(step.into(), action);
1440 self
1441 }
1442
1443 pub fn take_newly_active(&mut self, state: &State) -> Vec<String> {
1446 let mut fresh = Vec::new();
1447 for s in self.active_steps(state) {
1448 if !self.announced.contains(&s.id) {
1449 fresh.push(s.id.clone());
1450 }
1451 }
1452 for id in &fresh {
1453 self.announced.insert(id.clone());
1454 }
1455 fresh
1456 }
1457
1458 pub fn enter_action(&self, step: &str) -> Option<&StepAction> {
1460 self.enter_actions.get(step)
1461 }
1462
1463 pub async fn fire_enter_actions(&mut self, state: &State) {
1467 for id in self.take_newly_active(state) {
1468 if let Some(action) = self.enter_actions.get(&id) {
1469 action.fire(&id, state).await;
1470 }
1471 }
1472 }
1473
1474 pub fn mode(&self) -> Enforcement {
1476 self.mode
1477 }
1478
1479 pub fn set_posture(&mut self, step_id: &str, posture: Option<String>) -> bool {
1485 match self.flow.steps.iter_mut().find(|s| s.id == step_id) {
1486 Some(step) => {
1487 step.posture = posture;
1488 true
1489 }
1490 None => false,
1491 }
1492 }
1493
1494 pub fn set_ground(&mut self, step_id: &str, ground: Option<String>) -> bool {
1497 match self.flow.steps.iter_mut().find(|s| s.id == step_id) {
1498 Some(step) => {
1499 step.ground = ground;
1500 true
1501 }
1502 None => false,
1503 }
1504 }
1505
1506 pub fn eval(&self, guard: &Guard, state: &State) -> bool {
1510 guard.eval(&self.ctx(state))
1511 }
1512 pub fn marking(&self) -> &Marking {
1514 &self.marking
1515 }
1516 pub fn violations(&self) -> &[Violation] {
1518 &self.violations
1519 }
1520
1521 pub fn record_violation(&mut self, subject: impl Into<String>, reason: impl Into<String>) {
1526 self.violations.push(Violation {
1527 subject: subject.into(),
1528 reason: reason.into(),
1529 });
1530 }
1531 pub fn flow(&self) -> &Flow {
1533 &self.flow
1534 }
1535
1536 fn ctx<'a>(&'a self, state: &'a State) -> FlowCtx<'a> {
1537 FlowCtx {
1538 state,
1539 marking: &self.marking,
1540 }
1541 }
1542
1543 fn eligible(&self, step: &Step, state: &State) -> bool {
1544 let ctx = self.ctx(state);
1545 let edge_ok = |e: &Edge| {
1546 self.marking.done.contains(&e.step)
1547 && e.when.as_ref().map(|g| g.eval(&ctx)).unwrap_or(true)
1548 };
1549 let deps_done = if step.after.is_empty() {
1550 true
1551 } else {
1552 match step.join {
1553 Join::All => step.after.iter().all(edge_ok),
1554 Join::Any => step.after.iter().any(edge_ok),
1555 }
1556 };
1557 let before_ok = self.flow.constraints.iter().all(|c| match c {
1560 Constraint::Before(a, b) if *b == step.id => self.marking.done.contains(a),
1561 _ => true,
1562 });
1563 let gate_ok = step
1564 .gate
1565 .as_ref()
1566 .map(|g| g.eval(&self.ctx(state)))
1567 .unwrap_or(true);
1568 deps_done && before_ok && gate_ok
1569 }
1570
1571 pub fn relatch(&mut self, state: &State) {
1579 self.apply_resets(state);
1580 loop {
1581 let mut newly_done: Vec<String> = Vec::new();
1582 for s in &self.flow.steps {
1583 if self.marking.done.contains(&s.id) {
1584 continue;
1585 }
1586 if !self.eligible(s, state) {
1587 continue;
1588 }
1589 let complete = if s.terminal {
1590 true
1591 } else {
1592 s.done
1593 .as_ref()
1594 .map(|g| g.eval(&self.ctx(state)))
1595 .unwrap_or(false)
1596 };
1597 if complete {
1598 newly_done.push(s.id.clone());
1599 }
1600 }
1601 if newly_done.is_empty() {
1602 break;
1603 }
1604 for id in newly_done {
1605 self.marking.done.insert(id);
1606 }
1607 }
1608 }
1609
1610 fn apply_resets(&mut self, state: &State) -> Vec<String> {
1613 let mut reset: Vec<String> = Vec::new();
1614 let mut edges: Vec<(usize, bool)> = Vec::new();
1616 {
1617 let ctx = self.ctx(state);
1618 for (i, c) in self.flow.constraints.iter().enumerate() {
1619 if let Constraint::Reset { when, .. } = c {
1620 edges.push((i, when.eval(&ctx)));
1621 }
1622 }
1623 }
1624 for (slot, (index, now)) in edges.into_iter().enumerate() {
1625 let prev = self.reset_prev.get(slot).copied().unwrap_or(false);
1626 if self.reset_prev.len() <= slot {
1627 self.reset_prev.resize(slot + 1, false);
1628 }
1629 self.reset_prev[slot] = now;
1630 if !now || prev {
1631 continue;
1632 }
1633 let Constraint::Reset { steps, .. } = &self.flow.constraints[index] else {
1634 continue;
1635 };
1636 let steps = steps.clone();
1637 for step_id in &steps {
1638 if !self.marking.done.remove(step_id) {
1639 continue;
1640 }
1641 reset.push(step_id.clone());
1642 self.announced.remove(step_id);
1643 if let Some(step) = self.flow.steps.iter().find(|s| &s.id == step_id) {
1647 let mut tools = Vec::new();
1648 if let Some(done) = &step.done {
1649 done.referenced_tools(&mut tools);
1650 }
1651 for tool in tools {
1652 self.marking.tool_ok.remove(&tool);
1653 }
1654 }
1655 }
1656 }
1657 reset
1658 }
1659
1660 pub fn on_turn(&mut self, state: &State) {
1662 self.begin_turn(state);
1663 self.relatch(state);
1664 }
1665
1666 pub fn begin_turn(&mut self, state: &State) -> Vec<String> {
1676 self.marking.turns += 1;
1677 self.apply_resets(state)
1678 }
1679
1680 pub fn on_tool_ok(&mut self, tool: &str, state: &State) {
1682 self.begin_tool_ok(tool, state);
1683 self.relatch(state);
1684 }
1685
1686 pub fn begin_tool_ok(&mut self, tool: &str, state: &State) -> Vec<String> {
1696 *self.marking.tool_ok.entry(tool.to_string()).or_insert(0) += 1;
1697 self.apply_resets(state)
1698 }
1699
1700 pub fn reads_now(&self, state: &State) -> BTreeSet<String> {
1704 let active: BTreeSet<&str> = self
1705 .active_steps(state)
1706 .into_iter()
1707 .map(|s| s.id.as_str())
1708 .collect();
1709 let mut out = BTreeSet::new();
1710 for step in &self.flow.steps {
1711 if active.contains(step.id.as_str()) {
1712 for g in step.gate.iter().chain(step.done.iter()) {
1713 out.extend(g.state_keys());
1714 }
1715 }
1716 for edge in &step.after {
1717 if active.contains(edge.step.as_str())
1718 && let Some(when) = &edge.when
1719 {
1720 out.extend(when.state_keys());
1721 }
1722 }
1723 }
1724 for c in &self.flow.constraints {
1725 if let Constraint::NeverUntil { until: g, .. } | Constraint::Reset { when: g, .. } = c {
1726 out.extend(g.state_keys());
1727 }
1728 }
1729 out
1730 }
1731
1732 pub fn active_steps(&self, state: &State) -> Vec<&Step> {
1734 self.flow
1735 .steps
1736 .iter()
1737 .filter(|s| !self.marking.done.contains(&s.id) && self.eligible(s, state))
1738 .collect()
1739 }
1740
1741 pub fn active_postures(&self, state: &State) -> Vec<String> {
1743 self.active_steps(state)
1744 .into_iter()
1745 .filter_map(|s| s.posture.clone())
1746 .collect()
1747 }
1748
1749 pub fn active_grounds(&self, state: &State) -> Vec<String> {
1752 self.active_steps(state)
1753 .into_iter()
1754 .filter_map(|s| s.ground.as_ref().map(|t| render_ground(t, state)))
1755 .filter(|s| !s.trim().is_empty())
1756 .collect()
1757 }
1758
1759 pub fn closing_steps(&self) -> Vec<&Step> {
1766 self.flow
1767 .steps
1768 .iter()
1769 .filter(|s| s.terminal && self.marking.done.contains(&s.id))
1770 .collect()
1771 }
1772
1773 pub fn closing_postures(&self) -> Vec<String> {
1775 self.closing_steps()
1776 .into_iter()
1777 .filter_map(|s| s.posture.clone())
1778 .collect()
1779 }
1780
1781 pub fn closing_grounds(&self, state: &State) -> Vec<String> {
1783 self.closing_steps()
1784 .into_iter()
1785 .filter_map(|s| s.ground.as_ref().map(|t| render_ground(t, state)))
1786 .filter(|s| !s.trim().is_empty())
1787 .collect()
1788 }
1789
1790 pub fn unmet_requirements(&self) -> Vec<String> {
1792 self.flow
1793 .constraints
1794 .iter()
1795 .flat_map(|c| match c {
1796 Constraint::Require(rs) => rs.clone(),
1797 _ => Vec::new(),
1798 })
1799 .filter(|r| !self.marking.done.contains(r))
1800 .collect()
1801 }
1802
1803 pub fn is_complete(&self) -> bool {
1805 self.unmet_requirements().is_empty()
1806 }
1807
1808 pub fn verdict(&self, step_id: &str, state: &State) -> Verdict {
1810 if self.marking.done.contains(step_id) {
1811 return Verdict::Done;
1812 }
1813 if let Some(step) = self.flow.step(step_id)
1814 && self.eligible(step, state)
1815 {
1816 return Verdict::Active;
1817 }
1818 let bypassed = self.flow.steps.iter().any(|s| {
1820 s.after.iter().any(|d| d.step == step_id) && self.marking.done.contains(&s.id)
1821 });
1822 if bypassed {
1823 Verdict::Skipped
1824 } else {
1825 Verdict::Pending
1826 }
1827 }
1828
1829 pub fn admits_tool(&self, tool: &str, state: &State) -> Result<(), String> {
1832 match self.admissibility(tool, state) {
1833 Ok(()) => Ok(()),
1834 Err(denial) => Err(self.render_denial(tool, &denial, state)),
1835 }
1836 }
1837
1838 pub fn offers_tool(&self, tool: &str, state: &State) -> bool {
1845 match self.admissibility_with(tool, state, false) {
1846 Ok(()) => true,
1847 Err(Denial::OnceExhausted | Denial::DeniedByStep(_)) => false,
1849 Err(_) => self
1850 .frontier_steps(state)
1851 .iter()
1852 .any(|s| s.allow.iter().any(|a| a == tool)),
1853 }
1854 }
1855
1856 pub fn possible(&self, guard: &Guard, state: &State) -> Option<bool> {
1859 guard.possible(&self.ctx(state))
1860 }
1861
1862 pub fn frontier_steps(&self, state: &State) -> Vec<&Step> {
1877 let ctx = self.ctx(state);
1878 let active: BTreeSet<&str> = self
1879 .active_steps(state)
1880 .into_iter()
1881 .map(|s| s.id.as_str())
1882 .collect();
1883 let could = |g: Option<&Guard>| g.map_or(Some(true), |g| g.possible(&ctx));
1884 let source = |id: &str| -> Option<bool> {
1885 if self.marking.done.contains(id) {
1886 Some(true)
1887 } else if active.contains(id) {
1888 self.flow
1890 .step(id)
1891 .and_then(|s| s.done.as_ref())
1892 .map_or(Some(false), |done| done.possible(&ctx))
1893 } else {
1894 Some(false)
1895 }
1896 };
1897 self.flow
1898 .steps
1899 .iter()
1900 .filter(|step| {
1901 if self.marking.done.contains(&step.id) || active.contains(step.id.as_str()) {
1902 return false;
1903 }
1904 let before_ok = self.flow.constraints.iter().all(|c| match c {
1905 Constraint::Before(a, b) if *b == step.id => self.marking.done.contains(a),
1906 _ => true,
1907 });
1908 if !before_ok {
1909 return false;
1910 }
1911 let edges = step
1912 .after
1913 .iter()
1914 .map(|e| all_of([source(&e.step), could(e.when.as_ref())]));
1915 let deps = if step.after.is_empty() {
1916 Some(true)
1917 } else {
1918 match step.join {
1919 Join::All => all_of(edges),
1920 Join::Any => any_of(edges),
1921 }
1922 };
1923 all_of([deps, could(step.gate.as_ref())]).is_none()
1925 })
1926 .collect()
1927 }
1928
1929 pub fn frontier_questions(&self, tool: &str, state: &State) -> DecisionScope {
1934 let active: BTreeSet<String> = self
1935 .active_steps(state)
1936 .into_iter()
1937 .map(|s| s.id.clone())
1938 .collect();
1939 let mut scope = DecisionScope::default();
1940 for step in self.frontier_steps(state) {
1941 if !step.allow.iter().any(|a| a == tool) {
1942 continue;
1943 }
1944 let mut add = |g: &Guard| {
1945 scope.questions.extend(g.decisions());
1946 scope.reads.extend(g.state_keys());
1947 };
1948 step.gate.iter().for_each(&mut add);
1949 for edge in &step.after {
1950 edge.when.iter().for_each(&mut add);
1951 if active.contains(&edge.step)
1952 && let Some(done) = self.flow.step(&edge.step).and_then(|s| s.done.as_ref())
1953 {
1954 add(done);
1955 }
1956 }
1957 }
1958 scope
1959 }
1960
1961 pub fn blocking_keys(&self, tool: &str, state: &State) -> BTreeSet<String> {
1964 let ctx = self.ctx(state);
1965 self.flow
1966 .constraints
1967 .iter()
1968 .filter_map(|c| match c {
1969 Constraint::NeverUntil { tool: t, until } if t == tool && !until.eval(&ctx) => {
1970 Some(until.state_keys())
1971 }
1972 _ => None,
1973 })
1974 .flatten()
1975 .collect()
1976 }
1977
1978 fn admissibility(&self, tool: &str, state: &State) -> Result<(), Denial> {
1984 self.admissibility_with(tool, state, true)
1985 }
1986
1987 fn admissibility_with(&self, tool: &str, state: &State, guards: bool) -> Result<(), Denial> {
1990 for c in &self.flow.constraints {
1992 if let Constraint::Once(t) = c
1993 && t == tool
1994 && self.marking.tool_ok.contains_key(tool)
1995 {
1996 return Err(Denial::OnceExhausted);
1997 }
1998 }
1999 for c in &self.flow.constraints {
2001 if let Constraint::NeverUntil { tool: t, until } = c
2002 && guards
2003 && t == tool
2004 && !until.eval(&self.ctx(state))
2005 {
2006 return Err(Denial::NotYet(until.describe()));
2007 }
2008 }
2009 let active = self.active_steps(state);
2011 if let Some(step) = active.iter().find(|s| s.deny.iter().any(|d| d == tool)) {
2012 return Err(Denial::DeniedByStep(step.id.clone()));
2013 }
2014 if self.flow.ambient.iter().any(|a| a == tool) {
2017 return Ok(());
2018 }
2019 let restricting: Vec<&&Step> = active.iter().filter(|s| !s.allow.is_empty()).collect();
2020 if !restricting.is_empty()
2021 && !restricting
2022 .iter()
2023 .any(|s| s.allow.iter().any(|a| a == tool))
2024 {
2025 return Err(Denial::NotInStep(
2026 restricting.iter().map(|s| s.id.clone()).collect(),
2027 ));
2028 }
2029 Ok(())
2030 }
2031
2032 fn render_denial(&self, tool: &str, denial: &Denial, state: &State) -> String {
2043 let head = match denial {
2044 Denial::OnceExhausted => {
2045 return format!(
2047 "'{tool}' has already run and may run only once in this \
2048 conversation. Do not call it again."
2049 );
2050 }
2051 Denial::NotYet(condition) => {
2052 format!("'{tool}' is not permitted yet — first, {condition}.")
2053 }
2054 Denial::DeniedByStep(step) => {
2055 format!("'{tool}' is not allowed during the current step ('{step}').")
2056 }
2057 Denial::NotInStep(steps) => format!(
2058 "'{tool}' is not part of the current step ({}).",
2059 steps
2060 .iter()
2061 .map(|s| format!("'{s}'"))
2062 .collect::<Vec<_>>()
2063 .join(", ")
2064 ),
2065 };
2066
2067 let available: Vec<String> = self
2070 .flow
2071 .tool_universe()
2072 .into_iter()
2073 .filter(|t| self.admissibility(t, state).is_ok())
2074 .collect();
2075
2076 let mut out = head;
2079 out.push_str(" It did not run, so do not tell the caller it is done.");
2080 if available.is_empty() {
2081 out.push_str(" No tool is available right now — continue the conversation instead.");
2082 } else {
2083 out.push_str(" Available now: ");
2084 out.push_str(&available.join(", "));
2085 out.push('.');
2086 }
2087 let postures = self.active_postures(state);
2091 if let Some(first) = postures.first() {
2092 out.push(' ');
2093 out.push_str(first);
2094 }
2095 out
2096 }
2097
2098 pub fn observe_tool(&mut self, tool: &str, ok: bool, state: &State) {
2102 if self.mode == Enforcement::Observe
2103 && let Err(reason) = self.admits_tool(tool, state)
2104 {
2105 self.violations.push(Violation {
2106 subject: tool.to_string(),
2107 reason,
2108 });
2109 }
2110 if ok {
2111 self.on_tool_ok(tool, state);
2112 }
2113 }
2114}
2115
2116enum Denial {
2122 OnceExhausted,
2124 NotYet(String),
2127 DeniedByStep(String),
2129 NotInStep(Vec<String>),
2132}
2133
2134#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2136#[serde(tag = "kind", content = "detail", rename_all = "snake_case")]
2137pub enum FlowError {
2138 Invalid(String),
2140 UnreachableStep(String),
2142 UnguardedCommitTool(String),
2145 UnknownTool(String),
2149 UnsatisfiableGuard {
2153 tool: String,
2155 step: String,
2157 },
2158 OrderingCycle(Vec<String>),
2162}
2163
2164impl std::fmt::Display for FlowError {
2165 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2166 match self {
2167 FlowError::Invalid(m) => write!(f, "{m}"),
2168 FlowError::UnreachableStep(id) => {
2169 write!(f, "step '{id}' is unreachable from any root")
2170 }
2171 FlowError::UnguardedCommitTool(t) => write!(
2172 f,
2173 "commit tool '{t}' is guarded by an always-true condition (effectively unguarded)"
2174 ),
2175 FlowError::UnknownTool(t) => write!(
2176 f,
2177 "flow references tool '{t}' which is not in the provided tool registry"
2178 ),
2179 FlowError::UnsatisfiableGuard { tool, step } => write!(
2180 f,
2181 "`never('{tool}').until(..)` references unknown step '{step}' — the guard can \
2182 never hold, so '{tool}' would be forbidden forever"
2183 ),
2184 FlowError::OrderingCycle(steps) => write!(
2185 f,
2186 "ordering cycle across `after`/`before` edges: {} (no step on it can ever start)",
2187 steps.join(" -> ")
2188 ),
2189 }
2190 }
2191}
2192
2193#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2195pub struct FlowErrors(pub Vec<FlowError>);
2196
2197impl std::fmt::Display for FlowErrors {
2198 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2199 writeln!(f, "flow failed to compile ({} error(s)):", self.0.len())?;
2200 for e in &self.0 {
2201 writeln!(f, " - {e}")?;
2202 }
2203 Ok(())
2204 }
2205}
2206
2207impl std::error::Error for FlowErrors {}
2208
2209#[derive(Debug, Clone, Default)]
2215pub struct ToolSurface {
2216 pub tools: BTreeSet<String>,
2218}
2219
2220#[derive(Debug, Clone)]
2226pub struct CompiledFlow {
2227 flow: Flow,
2228 surface: ToolSurface,
2229}
2230
2231impl CompiledFlow {
2232 pub fn flow(&self) -> &Flow {
2234 &self.flow
2235 }
2236 pub fn tool_surface(&self) -> &ToolSurface {
2238 &self.surface
2239 }
2240 pub fn to_mermaid(&self) -> String {
2242 self.flow.to_mermaid()
2243 }
2244 pub fn into_flow(self) -> Flow {
2246 self.flow
2247 }
2248}
2249
2250#[derive(Debug, Clone, Serialize)]
2256pub struct FlowExplanation {
2257 pub active: Vec<String>,
2259 pub allowed_tools: Vec<String>,
2261 pub blocked_tools: BTreeMap<String, String>,
2263 pub missing_requirements: Vec<String>,
2265 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
2268 pub active_progress: BTreeMap<String, GuardTrace>,
2269}
2270
2271#[derive(Debug, Clone, Serialize)]
2277pub struct FlowSnapshot {
2278 #[serde(flatten)]
2280 pub explanation: FlowExplanation,
2281 pub done: Vec<String>,
2283 pub complete: bool,
2285 pub overlay_path: Vec<String>,
2287 pub terminated: bool,
2289}
2290
2291#[derive(Default)]
2293pub struct FlowBuilder {
2294 steps: Vec<Step>,
2295 constraints: Vec<Constraint>,
2296 confirm_tools: Vec<String>,
2297 ambient: Vec<String>,
2298}
2299
2300impl FlowBuilder {
2301 fn current(&mut self) -> &mut Step {
2302 self.steps
2303 .last_mut()
2304 .expect("call `.step(id)` before configuring a step")
2305 }
2306
2307 pub fn step(mut self, id: impl Into<String>) -> Self {
2309 self.steps.push(Step {
2310 id: id.into(),
2311 after: Vec::new(),
2312 join: Join::default(),
2313 gate: None,
2314 done: None,
2315 posture: None,
2316 ground: None,
2317 allow: Vec::new(),
2318 deny: Vec::new(),
2319 terminal: false,
2320 });
2321 self
2322 }
2323 pub fn after(mut self, dep: impl Into<String>) -> Self {
2325 self.current().after.push(Edge::to(dep));
2326 self
2327 }
2328 pub fn after_when(mut self, dep: impl Into<String>, when: Guard) -> Self {
2332 self.current().after.push(Edge::when(dep, when));
2333 self
2334 }
2335 pub fn join_any(mut self) -> Self {
2338 self.current().join = Join::Any;
2339 self
2340 }
2341 pub fn gate(mut self, g: Guard) -> Self {
2343 self.current().gate = Some(g);
2344 self
2345 }
2346 pub fn done(mut self, g: Guard) -> Self {
2348 self.current().done = Some(g);
2349 self
2350 }
2351 pub fn posture(mut self, text: impl Into<String>) -> Self {
2353 self.current().posture = Some(text.into());
2354 self
2355 }
2356 pub fn ground(mut self, template: impl Into<String>) -> Self {
2361 self.current().ground = Some(template.into());
2362 self
2363 }
2364 pub fn allow<I, S>(mut self, tools: I) -> Self
2366 where
2367 I: IntoIterator<Item = S>,
2368 S: Into<String>,
2369 {
2370 self.current()
2371 .allow
2372 .extend(tools.into_iter().map(Into::into));
2373 self
2374 }
2375 pub fn deny<I, S>(mut self, tools: I) -> Self
2377 where
2378 I: IntoIterator<Item = S>,
2379 S: Into<String>,
2380 {
2381 self.current()
2382 .deny
2383 .extend(tools.into_iter().map(Into::into));
2384 self
2385 }
2386 pub fn terminal(mut self) -> Self {
2388 self.current().terminal = true;
2389 self
2390 }
2391
2392 pub fn once(mut self, tool: impl Into<String>) -> Self {
2394 self.constraints.push(Constraint::Once(tool.into()));
2395 self
2396 }
2397 pub fn before(mut self, a: impl Into<String>, b: impl Into<String>) -> Self {
2399 self.constraints
2400 .push(Constraint::Before(a.into(), b.into()));
2401 self
2402 }
2403 pub fn require<I, S>(mut self, steps: I) -> Self
2405 where
2406 I: IntoIterator<Item = S>,
2407 S: Into<String>,
2408 {
2409 self.constraints.push(Constraint::Require(
2410 steps.into_iter().map(Into::into).collect(),
2411 ));
2412 self
2413 }
2414 pub fn ambient<I, S>(mut self, tools: I) -> Self
2433 where
2434 I: IntoIterator<Item = S>,
2435 S: Into<String>,
2436 {
2437 self.ambient.extend(tools.into_iter().map(Into::into));
2438 self
2439 }
2440
2441 pub fn reset<I, S>(self, steps: I) -> ResetBuilder
2445 where
2446 I: IntoIterator<Item = S>,
2447 S: Into<String>,
2448 {
2449 ResetBuilder {
2450 builder: self,
2451 steps: steps.into_iter().map(Into::into).collect(),
2452 }
2453 }
2454
2455 pub fn never(self, tool: impl Into<String>) -> NeverBuilder {
2457 NeverBuilder {
2458 fb: self,
2459 tool: tool.into(),
2460 }
2461 }
2462 pub fn commit(mut self, tool: impl Into<String>, until: Guard) -> Self {
2465 let tool = tool.into();
2466 self.constraints.push(Constraint::Once(tool.clone()));
2467 self.constraints.push(Constraint::NeverUntil {
2468 tool: tool.clone(),
2469 until,
2470 });
2471 self.confirm_tools.push(tool);
2472 self
2473 }
2474
2475 pub fn build(self) -> Result<Flow, ConfigError> {
2477 let flow = Flow {
2478 steps: self.steps,
2479 constraints: self.constraints,
2480 confirm_tools: self.confirm_tools,
2481 ambient: self.ambient,
2482 };
2483 flow.validate()?;
2484 Ok(flow)
2485 }
2486}
2487
2488pub struct NeverBuilder {
2490 fb: FlowBuilder,
2491 tool: String,
2492}
2493
2494impl NeverBuilder {
2495 pub fn until(mut self, guard: Guard) -> FlowBuilder {
2497 self.fb.constraints.push(Constraint::NeverUntil {
2498 tool: self.tool,
2499 until: guard,
2500 });
2501 self.fb
2502 }
2503}
2504
2505pub struct ResetBuilder {
2507 builder: FlowBuilder,
2508 steps: Vec<String>,
2509}
2510
2511impl ResetBuilder {
2512 pub fn when(mut self, guard: Guard) -> FlowBuilder {
2514 self.builder.constraints.push(Constraint::Reset {
2515 steps: self.steps,
2516 when: guard,
2517 });
2518 self.builder
2519 }
2520}
2521
2522#[cfg(test)]
2523mod tests {
2524 use super::*;
2525 use serde_json::json;
2526
2527 #[test]
2537 fn a_refusal_names_what_is_available_instead() {
2538 let state = State::new();
2539 let mon = FlowMonitor::new(debt_flow(), Enforcement::Enforce);
2540
2541 let reason = mon
2542 .admits_tool("charge_card", &state)
2543 .expect_err("charge_card is gated behind verification");
2544
2545 assert!(
2546 reason.contains("lookup_account"),
2547 "a refusal must point at the tool that would make progress: {reason}"
2548 );
2549 assert!(
2550 reason.contains("ptp_confirmed"),
2551 "a refusal must name the condition it is waiting on: {reason}"
2552 );
2553 }
2554
2555 #[test]
2559 fn a_refusal_carries_the_active_steps_posture() {
2560 let state = State::new();
2561 let mon = FlowMonitor::new(debt_flow(), Enforcement::Enforce);
2562
2563 let reason = mon
2564 .admits_tool("charge_card", &state)
2565 .expect_err("charge_card is gated behind verification");
2566
2567 assert!(
2568 reason.contains("Verify the caller's identity."),
2569 "the active step's posture belongs in the refusal: {reason}"
2570 );
2571 }
2572
2573 #[test]
2576 fn a_refusal_says_nothing_was_done() {
2577 let state = State::new();
2578 let mon = FlowMonitor::new(debt_flow(), Enforcement::Enforce);
2579 let reason = mon
2580 .admits_tool("charge_card", &state)
2581 .expect_err("charge_card is gated behind verification");
2582 assert!(
2583 reason.contains("It did not run, so do not tell the caller it is done."),
2584 "{reason}"
2585 );
2586 }
2587
2588 #[test]
2592 fn a_spent_once_constraint_does_not_offer_alternatives() {
2593 let state = State::new();
2594 let flow = Flow::new()
2595 .step("pay")
2596 .allow(["charge_card"])
2597 .done(Guard::called_ok("charge_card"))
2598 .once("charge_card")
2599 .build()
2600 .expect("valid flow");
2601 let mut mon = FlowMonitor::new(flow, Enforcement::Enforce);
2602 mon.on_tool_ok("charge_card", &state);
2603
2604 let reason = mon
2605 .admits_tool("charge_card", &state)
2606 .expect_err("once is spent");
2607
2608 assert!(
2609 reason.contains("already run"),
2610 "a spent `once` must say so plainly: {reason}"
2611 );
2612 assert!(
2613 !reason.contains("Available now"),
2614 "nothing to redirect to — offering a menu invites a retry: {reason}"
2615 );
2616 }
2617
2618 #[test]
2622 fn the_redirection_tracks_the_conversation() {
2623 let state = State::new();
2624 let mon = FlowMonitor::new(debt_flow(), Enforcement::Enforce);
2625
2626 let before = mon
2627 .admits_tool("charge_card", &state)
2628 .expect_err("gated before the promise is confirmed");
2629 assert!(
2630 before.contains("ptp_confirmed") && before.contains("lookup_account"),
2631 "{before}"
2632 );
2633
2634 let _ = state.set("identity_verified", true);
2638 let mut mon = mon;
2639 mon.on_turn(&state);
2640 let after = mon
2641 .admits_tool("charge_card", &state)
2642 .expect_err("the promise is still unconfirmed");
2643
2644 assert!(
2645 after.contains("Give the disclosure."),
2646 "the refusal must carry the posture of the step that is now active, \
2647 not the one that was active when the session opened: {after}"
2648 );
2649 assert!(
2650 !after.contains("Verify the caller's identity."),
2651 "a completed step's posture must not keep riding along on refusals — \
2652 that is how a verified caller gets asked to verify again: {after}"
2653 );
2654 assert_ne!(
2655 before, after,
2656 "the refusal did not change when the flow advanced"
2657 );
2658 }
2659
2660 #[test]
2663 fn guards_describe_themselves_in_prose() {
2664 assert_eq!(
2665 Guard::is_true("verified").describe(),
2666 "'verified' must be true"
2667 );
2668 assert_eq!(
2669 Guard::captured(["amount", "date"]).describe(),
2670 "these must be known: amount, date"
2671 );
2672 assert_eq!(
2673 Guard::called_ok("disclose").describe(),
2674 "'disclose' must have run successfully"
2675 );
2676 assert_eq!(
2677 Guard::all([Guard::is_true("a"), Guard::done("b")]).describe(),
2678 "'a' must be true and step 'b' must be complete"
2679 );
2680 assert_eq!(
2681 Guard::custom(|_| true).describe(),
2682 "a condition set by the application",
2683 "a custom guard has no readable spec, and must not pretend otherwise"
2684 );
2685 }
2686
2687 fn debt_flow() -> Flow {
2688 Flow::new()
2689 .step("verify")
2690 .posture("Verify the caller's identity.")
2691 .allow(["lookup_account"])
2692 .done(Guard::is_true("identity_verified"))
2693 .step("disclose")
2694 .after("verify")
2695 .posture("Give the disclosure.")
2696 .done(Guard::is_true("disclosure_given"))
2697 .step("capture_ptp")
2698 .after("disclose")
2699 .done(Guard::captured(["ptp_amount", "ptp_date"]))
2700 .step("take_payment")
2701 .after("capture_ptp")
2702 .allow(["charge_card"])
2703 .done(Guard::called_ok("charge_card"))
2704 .step("close")
2705 .after("capture_ptp")
2706 .terminal()
2707 .never("charge_card")
2708 .until(Guard::is_true("ptp_confirmed"))
2709 .once("charge_card")
2710 .require(["close"])
2711 .build()
2712 .expect("valid flow")
2713 }
2714
2715 #[test]
2716 fn validates_and_detects_unknown_dep() {
2717 let bad = Flow::new()
2718 .step("a")
2719 .done(Guard::is_true("x"))
2720 .step("b")
2721 .after("missing")
2722 .terminal()
2723 .build();
2724 assert!(bad.is_err());
2725 }
2726
2727 #[test]
2728 fn detects_cycle() {
2729 let steps = vec![
2731 Step {
2732 id: "a".into(),
2733 after: vec!["b".into()],
2734 join: Join::default(),
2735 gate: None,
2736 done: Some(Guard::always()),
2737 posture: None,
2738 ground: None,
2739 allow: vec![],
2740 deny: vec![],
2741 terminal: false,
2742 },
2743 Step {
2744 id: "b".into(),
2745 after: vec!["a".into()],
2746 join: Join::default(),
2747 gate: None,
2748 done: Some(Guard::always()),
2749 posture: None,
2750 ground: None,
2751 allow: vec![],
2752 deny: vec![],
2753 terminal: false,
2754 },
2755 ];
2756 let flow = Flow {
2757 steps,
2758 ..Flow::default()
2759 };
2760 assert!(flow.validate().is_err());
2761 }
2762
2763 #[test]
2764 fn marking_latches_in_order() {
2765 let flow = debt_flow();
2766 let mut mon = FlowMonitor::new(flow, Enforcement::Enforce);
2767 let state = State::new();
2768
2769 assert_eq!(
2771 mon.active_steps(&state)
2772 .iter()
2773 .map(|s| s.id.as_str())
2774 .collect::<Vec<_>>(),
2775 vec!["verify"]
2776 );
2777
2778 let _ = state.set("identity_verified", true);
2779 mon.on_turn(&state);
2780 assert!(mon.marking().done.contains("verify"));
2781 assert_eq!(mon.verdict("verify", &state), Verdict::Done);
2782 assert_eq!(mon.verdict("disclose", &state), Verdict::Active);
2783
2784 let _ = state.set("disclosure_given", true);
2785 let _ = state.set("ptp_amount", 200);
2786 let _ = state.set("ptp_date", "2026-06-05");
2787 mon.on_turn(&state);
2788 assert!(mon.marking().done.contains("capture_ptp"));
2790 assert!(mon.marking().done.contains("close"));
2791 assert!(mon.is_complete());
2792 }
2793
2794 #[test]
2795 fn enforces_never_until_and_once() {
2796 let flow = debt_flow();
2797 let mut mon = FlowMonitor::new(flow, Enforcement::Enforce);
2798 let state = State::new();
2799 let _ = state.set("identity_verified", true);
2801 let _ = state.set("disclosure_given", true);
2802 let _ = state.set("ptp_amount", 200);
2803 let _ = state.set("ptp_date", "x");
2804 mon.on_turn(&state);
2805
2806 assert!(mon.admits_tool("charge_card", &state).is_err());
2808 let _ = state.set("ptp_confirmed", true);
2809 assert!(mon.admits_tool("charge_card", &state).is_ok());
2810
2811 mon.on_tool_ok("charge_card", &state);
2813 assert!(mon.admits_tool("charge_card", &state).is_err());
2814 }
2815
2816 #[test]
2817 fn whitelist_scopes_tools_to_active_step() {
2818 let flow = debt_flow();
2819 let mon = FlowMonitor::new(flow, Enforcement::Enforce);
2820 let state = State::new();
2821 assert!(mon.admits_tool("lookup_account", &state).is_ok());
2823 assert!(mon.admits_tool("charge_card", &state).is_err());
2824 }
2825
2826 #[test]
2827 fn observe_mode_records_violations_not_blocks() {
2828 let flow = debt_flow();
2829 let mut mon = FlowMonitor::new(flow, Enforcement::Observe);
2830 let state = State::new();
2831 mon.observe_tool("charge_card", true, &state);
2833 assert_eq!(mon.violations().len(), 1);
2834 assert_eq!(mon.violations()[0].subject, "charge_card");
2835 }
2836
2837 #[test]
2838 fn compile_accepts_valid_flow_and_collects_tool_universe() {
2839 let compiled = debt_flow().compile().expect("valid flow compiles");
2840 assert!(compiled.tool_surface().tools.contains("charge_card"));
2842 assert!(compiled.tool_surface().tools.contains("lookup_account"));
2843 let _ = FlowMonitor::compiled(compiled, Enforcement::Enforce);
2844 }
2845
2846 #[test]
2847 fn compile_rejects_unreachable_step() {
2848 let flow = Flow::new()
2853 .step("a")
2854 .done(Guard::is_true("a_done"))
2855 .step("b")
2856 .after("a")
2857 .done(Guard::is_true("b_done"))
2858 .step("island")
2859 .after("b")
2860 .gate(Guard::is_true("never"))
2861 .terminal()
2862 .build()
2863 .expect("structurally valid");
2864 assert!(flow.compile().is_ok());
2866 }
2867
2868 #[test]
2869 fn compile_rejects_unguarded_commit_tool() {
2870 let flow = Flow::new()
2872 .step("s")
2873 .allow(["pay"])
2874 .done(Guard::called_ok("pay"))
2875 .terminal()
2876 .commit("pay", Guard::always())
2877 .build()
2878 .expect("structurally valid");
2879 let err = flow
2880 .compile()
2881 .expect_err("unguarded commit must fail to compile");
2882 assert!(
2883 err.0
2884 .iter()
2885 .any(|e| matches!(e, FlowError::UnguardedCommitTool(t) if t == "pay"))
2886 );
2887 }
2888
2889 #[test]
2890 fn compile_with_tools_accepts_a_covering_registry() {
2891 let compiled = debt_flow()
2892 .compile_with_tools(&["lookup_account", "charge_card", "unrelated_extra"])
2893 .expect("registry covers the flow's tool universe");
2894 assert!(compiled.tool_surface().tools.contains("charge_card"));
2895 }
2896
2897 #[test]
2898 fn compile_with_tools_reports_dangling_tool_names() {
2899 let err = debt_flow()
2902 .compile_with_tools(&["lookup_account"])
2903 .expect_err("dangling tool must fail to compile");
2904 assert!(
2905 err.0
2906 .iter()
2907 .any(|e| matches!(e, FlowError::UnknownTool(t) if t == "charge_card"))
2908 );
2909 assert!(debt_flow().compile().is_ok());
2911 }
2912
2913 #[test]
2914 fn compile_rejects_never_until_guard_on_unknown_step() {
2915 let flow = Flow::new()
2918 .step("s")
2919 .allow(["pay"])
2920 .done(Guard::called_ok("pay"))
2921 .never("pay")
2922 .until(Guard::done("missing"))
2923 .build()
2924 .expect("structurally valid for build()");
2925 let err = flow.compile().expect_err("unsatisfiable guard must fail");
2926 assert!(err.0.iter().any(|e| matches!(
2927 e,
2928 FlowError::UnsatisfiableGuard { tool, step } if tool == "pay" && step == "missing"
2929 )));
2930 }
2931
2932 #[test]
2933 fn compile_rejects_before_cycle() {
2934 let flow = Flow::new()
2938 .step("a")
2939 .done(Guard::is_true("a_done"))
2940 .step("b")
2941 .done(Guard::is_true("b_done"))
2942 .before("a", "b")
2943 .before("b", "a")
2944 .build()
2945 .expect("build() only checks `after` cycles");
2946 let err = flow
2947 .compile()
2948 .expect_err("before-cycle must fail to compile");
2949 assert!(err.0.iter().any(|e| matches!(
2950 e,
2951 FlowError::OrderingCycle(steps)
2952 if steps.contains(&"a".to_string()) && steps.contains(&"b".to_string())
2953 )));
2954 }
2955
2956 #[test]
2957 fn explain_reports_blocked_tools_and_reasons() {
2958 let flow = debt_flow();
2959 let mon = FlowMonitor::new(flow, Enforcement::Enforce);
2960 let state = State::new();
2961 let ex = mon.explain(&state);
2962 assert!(ex.blocked_tools.contains_key("charge_card"));
2964 assert!(ex.active.contains(&"verify".to_string()));
2965 }
2966
2967 #[test]
2968 fn before_constraint_gates_step_eligibility() {
2969 let flow = Flow::new()
2973 .step("a")
2974 .done(Guard::is_true("a_done"))
2975 .step("b")
2976 .done(Guard::is_true("b_done"))
2977 .before("a", "b")
2978 .build()
2979 .expect("valid flow");
2980 let mut mon = FlowMonitor::new(flow, Enforcement::Enforce);
2981 let state = State::new();
2982
2983 let active: Vec<String> = mon
2985 .active_steps(&state)
2986 .iter()
2987 .map(|s| s.id.clone())
2988 .collect();
2989 assert!(active.contains(&"a".to_string()));
2990 assert!(
2991 !active.contains(&"b".to_string()),
2992 "b must wait for a (Before)"
2993 );
2994
2995 let _ = state.set("a_done", true);
2996 mon.on_turn(&state);
2997 let active: Vec<String> = mon
2998 .active_steps(&state)
2999 .iter()
3000 .map(|s| s.id.clone())
3001 .collect();
3002 assert!(active.contains(&"b".to_string()), "b active once a is done");
3003 }
3004
3005 #[test]
3006 fn custom_guard_in_combinator_is_not_erased() {
3007 let always_false = Guard::all([Guard::is_true("present"), Guard::custom(|_| false)]);
3010 assert!(matches!(always_false, Guard::Custom(_)));
3012
3013 let state = State::new();
3014 let _ = state.set("present", true);
3015 let marking = Marking::default();
3016 let ctx = FlowCtx {
3017 state: &state,
3018 marking: &marking,
3019 };
3020 assert!(!always_false.eval(&ctx), "custom guard must still veto");
3022
3023 let serializable = Guard::all([Guard::is_true("a"), Guard::is_set("b")]);
3025 assert!(matches!(serializable, Guard::Spec(_)));
3026 }
3027
3028 #[test]
3029 fn serde_round_trips_data_driven_flow() {
3030 let flow = debt_flow();
3031 let jsonv = serde_json::to_value(&flow).expect("serialize");
3032 let back: Flow = serde_json::from_value(jsonv).expect("deserialize");
3033 back.validate().expect("round-tripped flow is valid");
3034 assert_eq!(back.steps.len(), flow.steps.len());
3035 }
3036
3037 #[test]
3038 fn custom_guard_is_not_serializable() {
3039 let flow = Flow::new()
3040 .step("a")
3041 .done(Guard::custom(|ctx| ctx.state.contains("ready")))
3042 .terminal()
3043 .build()
3044 .unwrap();
3045 assert!(serde_json::to_value(&flow).is_err());
3046 }
3047
3048 #[test]
3049 fn mermaid_export_has_nodes_and_edges() {
3050 let m = debt_flow().to_mermaid();
3051 assert!(m.contains("flowchart TD"));
3052 assert!(m.contains("verify --> disclose"));
3053 assert!(m.contains("close([close])")); }
3055
3056 struct WriteAgent;
3057 #[async_trait::async_trait]
3058 impl TextAgent for WriteAgent {
3059 fn name(&self) -> &str {
3060 "writer"
3061 }
3062 async fn run(&self, _state: &State) -> Result<String, crate::error::AgentError> {
3063 Ok("available".to_string())
3064 }
3065 }
3066
3067 #[tokio::test]
3068 async fn on_enter_fires_once_when_step_activates() {
3069 let flow = Flow::new()
3072 .step("collect")
3073 .done(Guard::is_true("collected"))
3074 .step("check")
3075 .after("collect")
3076 .done(Guard::resolved("check"))
3077 .step("book")
3078 .after("check")
3079 .terminal()
3080 .require(["book"])
3081 .build()
3082 .expect("valid flow");
3083
3084 let mut mon = FlowMonitor::new(flow, Enforcement::Enforce)
3085 .on_enter("check", on_enter(Arc::new(WriteAgent), AgentMode::Call));
3086 let state = State::new();
3087
3088 assert_eq!(mon.take_newly_active(&state), vec!["collect".to_string()]);
3090 assert!(mon.take_newly_active(&state).is_empty());
3092
3093 let _ = state.set("collected", true);
3095 mon.on_turn(&state);
3096 mon.fire_enter_actions(&state).await;
3097 assert_eq!(
3098 state.get::<String>("check:result").as_deref(),
3099 Some("available")
3100 );
3101
3102 mon.on_turn(&state);
3104 assert!(mon.marking().done.contains("check"));
3105 assert!(mon.is_complete());
3106 assert!(mon.take_newly_active(&state).is_empty());
3108 }
3109
3110 #[test]
3111 fn ground_template_interpolates_and_branches() {
3112 let state = State::new();
3113 let _ = state.set("when", "3pm");
3114 let _ = state.set("available", true);
3115 let _ = state.set("prior_visits", 2);
3116 assert_eq!(
3117 render_ground(
3118 "{when} is {available?open:taken}; {prior_visits} prior visits.",
3119 &state
3120 ),
3121 "3pm is open; 2 prior visits."
3122 );
3123 let _ = state.set("available", false);
3125 assert_eq!(
3126 render_ground("slot {missing}is {available?free:full}", &state),
3127 "slot is full"
3128 );
3129 }
3130
3131 #[test]
3132 fn active_grounds_projects_only_active_steps() {
3133 let flow = Flow::new()
3134 .step("collect")
3135 .ground("Known time: {when}.")
3136 .done(Guard::is_set("when"))
3137 .step("done")
3138 .after("collect")
3139 .terminal()
3140 .build()
3141 .expect("valid flow");
3142 let mut mon = FlowMonitor::new(flow, Enforcement::Enforce);
3143 let state = State::new();
3144 let _ = state.set("when", "3pm");
3145 assert_eq!(
3146 mon.active_grounds(&state),
3147 vec!["Known time: 3pm.".to_string()]
3148 );
3149 mon.on_turn(&state);
3151 assert!(mon.active_grounds(&state).is_empty());
3152 }
3153
3154 #[test]
3155 fn eq_guard_matches_state_value() {
3156 let g = Guard::eq("status", json!("active"));
3157 let state = State::new();
3158 let marking = Marking::default();
3159 assert!(!g.eval(&FlowCtx {
3160 state: &state,
3161 marking: &marking
3162 }));
3163 let _ = state.set("status", "active");
3164 assert!(g.eval(&FlowCtx {
3165 state: &state,
3166 marking: &marking
3167 }));
3168 }
3169
3170 fn ambient_flow() -> Flow {
3185 Flow::new()
3186 .ambient(["recall_context"])
3187 .step("gather")
3188 .allow(["ask_diet"])
3189 .done(Guard::is_set("user:diet"))
3190 .step("book")
3191 .after("gather")
3192 .allow(["book_table"])
3193 .done(Guard::called_ok("book_table"))
3194 .build()
3195 .expect("flow is structurally valid")
3196 }
3197
3198 #[test]
3199 fn an_ambient_tool_survives_a_step_whitelist() {
3200 let mon = FlowMonitor::new(ambient_flow(), Enforcement::Enforce);
3201 let state = State::new();
3202 assert_eq!(
3203 mon.admits_tool("ask_diet", &state),
3204 Ok(()),
3205 "the step's own tool is admitted"
3206 );
3207 assert!(
3208 mon.admits_tool("book_table", &state).is_err(),
3209 "a later step's tool is still excluded by `gather`'s whitelist"
3210 );
3211 assert_eq!(
3212 mon.admits_tool("recall_context", &state),
3213 Ok(()),
3214 "an ambient tool must not be caught by a whitelist that never meant to exclude it"
3215 );
3216 }
3217
3218 #[test]
3219 fn an_ambient_tool_is_still_denied_when_a_step_names_it() {
3220 let flow = Flow::new()
3223 .ambient(["recall_context"])
3224 .step("sensitive")
3225 .deny(["recall_context"])
3226 .done(Guard::is_true("done"))
3227 .build()
3228 .expect("flow is structurally valid");
3229 let mon = FlowMonitor::new(flow, Enforcement::Enforce);
3230 assert!(
3231 mon.admits_tool("recall_context", &State::new()).is_err(),
3232 "an explicit `deny` outranks ambient"
3233 );
3234 }
3235
3236 #[test]
3237 fn an_ambient_tool_still_obeys_never_until() {
3238 let flow = Flow::new()
3239 .ambient(["manage_memory"])
3240 .step("verify")
3241 .done(Guard::is_true("verified"))
3242 .never("manage_memory")
3243 .until(Guard::is_true("verified"))
3244 .build()
3245 .expect("flow is structurally valid");
3246 let mon = FlowMonitor::new(flow, Enforcement::Enforce);
3247 let state = State::new();
3248 assert!(
3249 mon.admits_tool("manage_memory", &state).is_err(),
3250 "`never(..).until(..)` names the tool; ambient must not unlock it"
3251 );
3252 let _ = state.set("verified", true);
3253 assert_eq!(
3254 mon.admits_tool("manage_memory", &state),
3255 Ok(()),
3256 "once the guard holds the constraint stops binding"
3257 );
3258 }
3259
3260 #[test]
3261 fn an_ambient_tool_still_obeys_once() {
3262 let flow = Flow::new()
3263 .ambient(["summarize"])
3264 .step("work")
3265 .done(Guard::called_ok("summarize"))
3266 .once("summarize")
3267 .build()
3268 .expect("flow is structurally valid");
3269 let mut mon = FlowMonitor::new(flow, Enforcement::Enforce);
3270 let state = State::new();
3271 assert_eq!(mon.admits_tool("summarize", &state), Ok(()));
3272 mon.observe_tool("summarize", true, &state);
3273 assert!(
3274 mon.admits_tool("summarize", &state).is_err(),
3275 "`once` names the tool; ambient must not exempt it"
3276 );
3277 }
3278
3279 #[test]
3280 fn ambient_tools_join_the_tool_universe() {
3281 let flow = ambient_flow();
3284 assert!(
3285 flow.tool_universe().contains("recall_context"),
3286 "an ambient tool is part of the flow's tool universe"
3287 );
3288 assert!(
3289 ambient_flow()
3290 .compile_with_tools(&["ask_diet", "book_table"])
3291 .is_err(),
3292 "a registry missing the ambient tool must not compile clean"
3293 );
3294 assert!(
3295 ambient_flow()
3296 .compile_with_tools(&["ask_diet", "book_table", "recall_context"])
3297 .is_ok(),
3298 "a covering registry compiles"
3299 );
3300 }
3301
3302 #[test]
3303 fn a_flow_with_no_ambient_tools_is_unchanged() {
3304 let mon = FlowMonitor::new(debt_flow(), Enforcement::Enforce);
3307 let state = State::new();
3308 assert_eq!(mon.admits_tool("lookup_account", &state), Ok(()));
3309 assert!(mon.admits_tool("charge_card", &state).is_err());
3310 assert!(mon.admits_tool("anything_else", &state).is_err());
3311 }
3312
3313 #[test]
3314 fn explain_trace_reports_per_atom_truth() {
3315 let state = State::new();
3316 let _ = state.set("ptp_amount", 200);
3317 let marking = Marking::default();
3318 let ctx = FlowCtx {
3319 state: &state,
3320 marking: &marking,
3321 };
3322 let guard = Guard::all([
3323 Guard::captured(["ptp_amount", "ptp_date"]),
3324 Guard::is_true("confirmed"),
3325 ]);
3326 let trace = guard.explain_trace(&ctx);
3327 assert!(!trace.holds);
3328 assert_eq!(trace.desc, "all of");
3329 assert_eq!(trace.children.len(), 2);
3330 assert!(!trace.children[0].holds, "ptp_date missing");
3332 assert!(!trace.children[1].holds);
3333 assert!(serde_json::to_value(&trace).is_ok());
3335 }
3336
3337 #[test]
3338 fn explanation_carries_active_step_progress() {
3339 let mon = FlowMonitor::new(debt_flow(), Enforcement::Enforce);
3340 let state = State::new();
3341 let ex = mon.explain(&state);
3342 let verify = ex.active_progress.get("verify").expect("verify is active");
3343 assert!(!verify.holds);
3344 assert!(verify.desc.contains("identity_verified"));
3345 }
3346
3347 #[test]
3348 fn state_keys_read_covers_guards_and_constraints() {
3349 let keys = debt_flow().state_keys_read();
3350 for k in [
3351 "identity_verified",
3352 "disclosure_given",
3353 "ptp_amount",
3354 "ptp_date",
3355 ] {
3356 assert!(keys.contains(k), "missing read key {k}");
3357 }
3358 assert!(!keys.contains("charge_card"));
3360 }
3361
3362 #[test]
3363 fn posture_updates_take_effect_in_place() {
3364 let mut mon = FlowMonitor::new(debt_flow(), Enforcement::Enforce);
3365 let state = State::new();
3366 assert!(mon.set_posture("verify", Some("New posture.".into())));
3367 assert!(!mon.set_posture("no_such_step", None));
3368 assert_eq!(
3369 mon.active_postures(&state),
3370 vec!["New posture.".to_string()]
3371 );
3372 }
3373
3374 #[test]
3375 fn eval_state_treats_marking_atoms_as_false() {
3376 let state = State::new();
3377 let _ = state.set("ready", true);
3378 assert!(Guard::is_true("ready").eval_state(&state));
3379 assert!(!Guard::called_ok("any_tool").eval_state(&state));
3380 }
3381
3382 #[test]
3383 fn conditional_edges_branch_and_any_join_merges() {
3384 let flow = Flow::new()
3387 .step("decide")
3388 .done(Guard::is_set("severity"))
3389 .step("schedule")
3390 .after_when("decide", Guard::eq("severity", "routine"))
3391 .done(Guard::called_ok("book"))
3392 .allow(["book"])
3393 .step("escalate")
3394 .after_when("decide", Guard::eq("severity", "emergency"))
3395 .done(Guard::called_ok("transfer"))
3396 .allow(["transfer"])
3397 .step("close")
3398 .after("schedule")
3399 .after("escalate")
3400 .join_any()
3401 .terminal()
3402 .build()
3403 .expect("valid");
3404 let mut mon = FlowMonitor::new(flow, Enforcement::Enforce);
3405 let state = State::new();
3406 let _ = state.set("severity", "routine");
3407 mon.relatch(&state);
3408 let active: Vec<String> = mon.explain(&state).active;
3409 assert!(
3410 active.contains(&"schedule".to_string()),
3411 "routine branch opens"
3412 );
3413 assert!(
3414 !active.contains(&"escalate".to_string()),
3415 "emergency branch stays closed"
3416 );
3417 mon.on_tool_ok("book", &state);
3419 assert!(mon.marking().done.contains("close"), "any-join merged");
3420 }
3421
3422 #[test]
3423 fn reset_unlatches_on_rising_edge_and_forgives_called_ok() {
3424 let flow = Flow::new()
3425 .step("pay")
3426 .allow(["charge"])
3427 .done(Guard::called_ok("charge"))
3428 .once("charge")
3429 .reset(["pay"])
3430 .when(Guard::is_true("declined"))
3431 .build()
3432 .expect("valid");
3433 let mut mon = FlowMonitor::new(flow, Enforcement::Enforce);
3434 let state = State::new();
3435 mon.relatch(&state);
3436 mon.on_tool_ok("charge", &state);
3437 assert!(mon.marking().done.contains("pay"));
3438 assert!(mon.admits_tool("charge", &state).is_err(), "once spent");
3439
3440 let _ = state.set("declined", true);
3442 mon.relatch(&state);
3443 assert!(!mon.marking().done.contains("pay"), "un-latched");
3444 assert!(
3445 mon.admits_tool("charge", &state).is_ok(),
3446 "once counts per latch-cycle"
3447 );
3448
3449 mon.on_tool_ok("charge", &state);
3451 mon.relatch(&state);
3452 assert!(
3453 mon.marking().done.contains("pay"),
3454 "second charge latches again"
3455 );
3456 }
3457
3458 #[test]
3459 fn edge_serde_keeps_plain_strings_and_round_trips_conditions() {
3460 let flow = Flow::new()
3461 .step("a")
3462 .terminal()
3463 .step("b")
3464 .after("a")
3465 .after_when("a", Guard::is_true("x"))
3466 .join_any()
3467 .terminal()
3468 .build()
3469 .expect("valid");
3470 let json = serde_json::to_value(&flow).expect("serialize");
3471 assert_eq!(json["steps"][1]["after"][0], serde_json::json!("a"));
3473 assert_eq!(
3474 json["steps"][1]["after"][1],
3475 serde_json::json!({"step": "a", "when": {"is_true": "x"}})
3476 );
3477 assert_eq!(json["steps"][1]["join"], serde_json::json!("any"));
3478 let back: Flow = serde_json::from_value(json).expect("deserialize");
3479 assert_eq!(back.steps[1].after.len(), 2);
3480 assert!(back.steps[1].after[1].when.is_some());
3481 }
3482
3483 #[test]
3484 fn flow_json_schema_generates() {
3485 let schema = serde_json::to_value(schemars::schema_for!(Flow)).expect("schema");
3486 let text = schema.to_string();
3487 assert!(text.contains("is_true"));
3489 assert!(text.contains("never_until"));
3490 }
3491
3492 #[test]
3493 fn possible_leaves_a_decision_unknown_and_settles_the_rest() {
3494 let state = State::new();
3495 let _ = state.set("yes", true);
3496 let marking = Marking::default();
3497 let ctx = FlowCtx {
3498 state: &state,
3499 marking: &marking,
3500 };
3501 let decided = Guard::decided("q");
3502 assert_eq!(decided.possible(&ctx), None);
3503 assert_eq!(Guard::not(decided.clone()).possible(&ctx), None);
3504 assert_eq!(Guard::is_true("yes").possible(&ctx), Some(true));
3505 assert_eq!(
3507 Guard::all([decided.clone(), Guard::is_true("no")]).possible(&ctx),
3508 Some(false)
3509 );
3510 assert_eq!(
3511 Guard::any([decided.clone(), Guard::is_true("yes")]).possible(&ctx),
3512 Some(true)
3513 );
3514 assert_eq!(
3515 Guard::all([decided.clone(), Guard::is_true("yes")]).possible(&ctx),
3516 None
3517 );
3518 assert_eq!(
3519 Guard::any([decided, Guard::is_true("no")]).possible(&ctx),
3520 None
3521 );
3522 }
3523}