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 stack;
33pub mod timing;
34pub mod verbatim;
35pub use stack::{
36 FlowStack, OVERLAY_STATE_KEY, Overlay, RepairPolicy, Resume, SharedFlowStack,
37 TERMINATED_STATE_KEY, TOOL_CALL_KEY, TOOL_DENIED_KEY, TOOL_RESULT_KEY, correction_flag,
38 escalate_flag, reprompt_flag,
39};
40pub use timing::{DEFAULT_REPROMPT, VOICE_TIMING_KEY, VoiceTiming};
41pub use verbatim::{VERBATIM_KEY, VerbatimRequirement, verbatim_flag};
42
43pub struct FlowCtx<'a> {
46 pub state: &'a State,
48 pub marking: &'a Marking,
50}
51
52#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, schemars::JsonSchema)]
54#[serde(rename_all = "snake_case")]
55pub enum Pred {
56 Always,
58 IsTrue(String),
60 IsSet(String),
62 Eq(String, Value),
64 Captured(Vec<String>),
66 CalledOk(String),
68 Done(String),
70 All(Vec<Pred>),
72 Any(Vec<Pred>),
74 Not(Box<Pred>),
76}
77
78impl Pred {
79 fn eval(&self, ctx: &FlowCtx) -> bool {
80 match self {
81 Pred::Always => true,
82 Pred::IsTrue(k) => ctx.state.get::<bool>(k) == Some(true),
83 Pred::IsSet(k) => ctx.state.contains(k),
84 Pred::Eq(k, v) => ctx.state.get::<Value>(k).as_ref() == Some(v),
85 Pred::Captured(fields) => fields.iter().all(|f| ctx.state.contains(f)),
86 Pred::CalledOk(t) => ctx.marking.tool_ok.contains_key(t),
87 Pred::Done(s) => ctx.marking.done.contains(s),
88 Pred::All(ps) => ps.iter().all(|p| p.eval(ctx)),
89 Pred::Any(ps) => ps.iter().any(|p| p.eval(ctx)),
90 Pred::Not(p) => !p.eval(ctx),
91 }
92 }
93
94 fn describe(&self) -> String {
96 fn join(ps: &[Pred], sep: &str) -> String {
97 ps.iter().map(Pred::describe).collect::<Vec<_>>().join(sep)
98 }
99 match self {
100 Pred::Always => "nothing".to_string(),
101 Pred::IsTrue(k) => format!("'{k}' must be true"),
102 Pred::IsSet(k) => format!("'{k}' must be known"),
103 Pred::Eq(k, v) => format!("'{k}' must be {v}"),
104 Pred::Captured(fields) => {
105 format!("these must be known: {}", fields.join(", "))
106 }
107 Pred::CalledOk(t) => format!("'{t}' must have run successfully"),
108 Pred::Done(s) => format!("step '{s}' must be complete"),
109 Pred::All(ps) => join(ps, " and "),
110 Pred::Any(ps) => join(ps, " or "),
111 Pred::Not(p) => format!("it must not be the case that {}", p.describe()),
112 }
113 }
114
115 fn referenced_steps(&self, out: &mut Vec<String>) {
117 match self {
118 Pred::Done(s) => out.push(s.clone()),
119 Pred::All(ps) | Pred::Any(ps) => ps.iter().for_each(|p| p.referenced_steps(out)),
120 Pred::Not(p) => p.referenced_steps(out),
121 _ => {}
122 }
123 }
124
125 fn referenced_tools(&self, out: &mut Vec<String>) {
127 match self {
128 Pred::CalledOk(t) => out.push(t.clone()),
129 Pred::All(ps) | Pred::Any(ps) => ps.iter().for_each(|p| p.referenced_tools(out)),
130 Pred::Not(p) => p.referenced_tools(out),
131 _ => {}
132 }
133 }
134
135 fn referenced_state_keys(&self, out: &mut BTreeSet<String>) {
139 match self {
140 Pred::IsTrue(k) | Pred::IsSet(k) | Pred::Eq(k, _) => {
141 out.insert(k.clone());
142 }
143 Pred::Captured(fields) => out.extend(fields.iter().cloned()),
144 Pred::All(ps) | Pred::Any(ps) => {
145 ps.iter().for_each(|p| p.referenced_state_keys(out));
146 }
147 Pred::Not(p) => p.referenced_state_keys(out),
148 _ => {}
149 }
150 }
151
152 fn explain(&self, ctx: &FlowCtx) -> GuardTrace {
156 let children = match self {
157 Pred::All(ps) | Pred::Any(ps) => ps.iter().map(|p| p.explain(ctx)).collect(),
158 Pred::Not(p) => vec![p.explain(ctx)],
159 _ => Vec::new(),
160 };
161 GuardTrace {
162 desc: match self {
163 Pred::All(_) => "all of".to_string(),
166 Pred::Any(_) => "any of".to_string(),
167 Pred::Not(_) => "not".to_string(),
168 atom => atom.describe(),
169 },
170 holds: self.eval(ctx),
171 children,
172 }
173 }
174}
175
176#[derive(Clone, Debug, Serialize, schemars::JsonSchema)]
180pub struct GuardTrace {
181 pub desc: String,
183 pub holds: bool,
185 #[serde(default, skip_serializing_if = "Vec::is_empty")]
187 pub children: Vec<GuardTrace>,
188}
189
190pub fn render_ground(template: &str, state: &State) -> String {
201 let mut out = String::with_capacity(template.len());
202 let mut rest = template;
203 while let Some(open) = rest.find('{') {
204 out.push_str(&rest[..open]);
205 let after = &rest[open + 1..];
206 let Some(close) = after.find('}') else {
207 out.push_str(&rest[open..]);
209 return out;
210 };
211 let expr = &after[..close];
212 out.push_str(&render_expr(expr, state));
213 rest = &after[close + 1..];
214 }
215 out.push_str(rest);
216 out
217}
218
219fn render_expr(expr: &str, state: &State) -> String {
220 if let Some((cond, arms)) = expr.split_once('?') {
221 let (yes, no) = arms.split_once(':').unwrap_or((arms, ""));
222 if is_truthy(state, cond.trim()) {
223 yes.to_string()
224 } else {
225 no.to_string()
226 }
227 } else {
228 match state.get::<Value>(expr.trim()) {
229 Some(Value::String(s)) => s,
230 Some(v) => v.to_string(),
231 None => String::new(),
232 }
233 }
234}
235
236fn is_truthy(state: &State, key: &str) -> bool {
237 match state.get::<Value>(key) {
238 None | Some(Value::Null) => false,
239 Some(Value::Bool(b)) => b,
240 Some(Value::Number(n)) => n.as_f64().map(|f| f != 0.0).unwrap_or(true),
241 Some(Value::String(s)) => !s.is_empty(),
242 Some(_) => true,
243 }
244}
245
246type CustomFn = Arc<dyn Fn(&FlowCtx) -> bool + Send + Sync>;
247
248#[derive(Clone)]
254pub enum Guard {
255 Spec(Pred),
257 Custom(CustomFn),
259}
260
261impl Guard {
262 pub fn always() -> Self {
264 Guard::Spec(Pred::Always)
265 }
266 pub fn is_true(key: impl Into<String>) -> Self {
268 Guard::Spec(Pred::IsTrue(key.into()))
269 }
270 pub fn is_set(key: impl Into<String>) -> Self {
272 Guard::Spec(Pred::IsSet(key.into()))
273 }
274 pub fn eq(key: impl Into<String>, value: impl Into<Value>) -> Self {
276 Guard::Spec(Pred::Eq(key.into(), value.into()))
277 }
278 pub fn captured<I, S>(fields: I) -> Self
280 where
281 I: IntoIterator<Item = S>,
282 S: Into<String>,
283 {
284 Guard::Spec(Pred::Captured(fields.into_iter().map(Into::into).collect()))
285 }
286 pub fn called_ok(tool: impl Into<String>) -> Self {
288 Guard::Spec(Pred::CalledOk(tool.into()))
289 }
290 pub fn done(step: impl Into<String>) -> Self {
292 Guard::Spec(Pred::Done(step.into()))
293 }
294 pub fn resolved(name: impl AsRef<str>) -> Self {
298 Guard::Spec(Pred::IsSet(format!("{}:result", name.as_ref())))
299 }
300 pub fn all(guards: impl IntoIterator<Item = Guard>) -> Self {
309 let guards: Vec<Guard> = guards.into_iter().collect();
310 if guards.iter().all(|g| matches!(g, Guard::Spec(_))) {
311 Guard::Spec(Pred::All(specs_unchecked(guards)))
312 } else {
313 Guard::Custom(Arc::new(move |ctx| guards.iter().all(|g| g.eval(ctx))))
314 }
315 }
316 pub fn any(guards: impl IntoIterator<Item = Guard>) -> Self {
321 let guards: Vec<Guard> = guards.into_iter().collect();
322 if guards.iter().all(|g| matches!(g, Guard::Spec(_))) {
323 Guard::Spec(Pred::Any(specs_unchecked(guards)))
324 } else {
325 Guard::Custom(Arc::new(move |ctx| guards.iter().any(|g| g.eval(ctx))))
326 }
327 }
328 #[allow(clippy::should_implement_trait)]
330 pub fn not(guard: Guard) -> Self {
331 match guard {
332 Guard::Spec(p) => Guard::Spec(Pred::Not(Box::new(p))),
333 Guard::Custom(f) => Guard::Custom(Arc::new(move |ctx| !f(ctx))),
335 }
336 }
337 pub fn describe(&self) -> String {
347 match self {
348 Guard::Spec(p) => p.describe(),
349 Guard::Custom(_) => "a condition set by the application".to_string(),
350 }
351 }
352
353 pub fn custom(f: impl Fn(&FlowCtx) -> bool + Send + Sync + 'static) -> Self {
355 Guard::Custom(Arc::new(f))
356 }
357
358 pub fn eval(&self, ctx: &FlowCtx) -> bool {
360 match self {
361 Guard::Spec(p) => p.eval(ctx),
362 Guard::Custom(f) => f(ctx),
363 }
364 }
365
366 pub fn eval_state(&self, state: &State) -> bool {
372 let marking = Marking::default();
373 self.eval(&FlowCtx {
374 state,
375 marking: &marking,
376 })
377 }
378
379 pub fn explain_trace(&self, ctx: &FlowCtx) -> GuardTrace {
382 match self {
383 Guard::Spec(p) => p.explain(ctx),
384 Guard::Custom(f) => GuardTrace {
385 desc: "a condition set by the application".to_string(),
386 holds: f(ctx),
387 children: Vec::new(),
388 },
389 }
390 }
391
392 fn referenced_steps(&self, out: &mut Vec<String>) {
393 if let Guard::Spec(p) = self {
394 p.referenced_steps(out);
395 }
396 }
397
398 fn referenced_state_keys(&self, out: &mut BTreeSet<String>) {
399 if let Guard::Spec(p) = self {
400 p.referenced_state_keys(out);
401 }
402 }
403
404 pub fn state_keys(&self) -> BTreeSet<String> {
408 let mut keys = BTreeSet::new();
409 self.referenced_state_keys(&mut keys);
410 keys
411 }
412
413 fn referenced_tools(&self, out: &mut Vec<String>) {
414 if let Guard::Spec(p) = self {
415 p.referenced_tools(out);
416 }
417 }
418}
419
420fn specs_unchecked(guards: Vec<Guard>) -> Vec<Pred> {
425 guards
426 .into_iter()
427 .map(|g| match g {
428 Guard::Spec(p) => p,
429 Guard::Custom(_) => unreachable!("specs_unchecked called with a custom guard"),
430 })
431 .collect()
432}
433
434impl Serialize for Guard {
435 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
436 match self {
437 Guard::Spec(p) => p.serialize(s),
438 Guard::Custom(_) => Err(serde::ser::Error::custom(
439 "custom guards are not serializable; use Guard atoms for data-driven flows",
440 )),
441 }
442 }
443}
444
445impl<'de> Deserialize<'de> for Guard {
446 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
447 Ok(Guard::Spec(Pred::deserialize(d)?))
448 }
449}
450
451impl schemars::JsonSchema for Guard {
455 fn is_referenceable() -> bool {
456 false
457 }
458 fn schema_name() -> String {
459 "Guard".to_string()
460 }
461 fn json_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
462 generator.subschema_for::<Pred>()
463 }
464}
465
466impl std::fmt::Debug for Guard {
467 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
468 match self {
469 Guard::Spec(p) => write!(f, "{p:?}"),
470 Guard::Custom(_) => write!(f, "Custom(<fn>)"),
471 }
472 }
473}
474
475#[derive(Clone, Debug)]
485pub struct Edge {
486 pub step: String,
488 pub when: Option<Guard>,
490}
491
492impl Edge {
493 pub fn to(step: impl Into<String>) -> Self {
495 Edge {
496 step: step.into(),
497 when: None,
498 }
499 }
500 pub fn when(step: impl Into<String>, when: Guard) -> Self {
502 Edge {
503 step: step.into(),
504 when: Some(when),
505 }
506 }
507}
508
509impl<S: Into<String>> From<S> for Edge {
510 fn from(step: S) -> Self {
511 Edge::to(step)
512 }
513}
514
515impl Serialize for Edge {
516 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
517 match &self.when {
518 None => self.step.serialize(s),
520 Some(when) => {
521 use serde::ser::SerializeStruct;
522 let mut out = s.serialize_struct("Edge", 2)?;
523 out.serialize_field("step", &self.step)?;
524 out.serialize_field("when", when)?;
525 out.end()
526 }
527 }
528 }
529}
530
531impl<'de> Deserialize<'de> for Edge {
532 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
533 #[derive(Deserialize)]
534 #[serde(untagged)]
535 enum Repr {
536 Simple(String),
537 Conditional {
538 step: String,
539 #[serde(default)]
540 when: Option<Guard>,
541 },
542 }
543 Ok(match Repr::deserialize(d)? {
544 Repr::Simple(step) => Edge { step, when: None },
545 Repr::Conditional { step, when } => Edge { step, when },
546 })
547 }
548}
549
550impl schemars::JsonSchema for Edge {
551 fn schema_name() -> String {
552 "Edge".to_string()
553 }
554 fn json_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
555 #[derive(schemars::JsonSchema)]
556 #[serde(untagged)]
557 #[allow(dead_code)]
558 enum EdgeSchema {
559 Simple(String),
560 Conditional { step: String, when: Option<Pred> },
561 }
562 EdgeSchema::json_schema(generator)
563 }
564}
565
566#[derive(
568 Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema,
569)]
570#[serde(rename_all = "snake_case")]
571pub enum Join {
572 #[default]
574 All,
575 Any,
577}
578
579impl Join {
580 fn is_all(&self) -> bool {
581 *self == Join::All
582 }
583}
584
585#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)]
587pub struct Step {
588 pub id: String,
590 #[serde(default, skip_serializing_if = "Vec::is_empty")]
592 pub after: Vec<Edge>,
593 #[serde(default, skip_serializing_if = "Join::is_all")]
595 pub join: Join,
596 #[serde(default, skip_serializing_if = "Option::is_none")]
598 pub gate: Option<Guard>,
599 #[serde(default, skip_serializing_if = "Option::is_none")]
601 pub done: Option<Guard>,
602 #[serde(default, skip_serializing_if = "Option::is_none")]
604 pub posture: Option<String>,
605 #[serde(default, skip_serializing_if = "Option::is_none")]
609 pub ground: Option<String>,
610 #[serde(default, skip_serializing_if = "Vec::is_empty")]
612 pub allow: Vec<String>,
613 #[serde(default, skip_serializing_if = "Vec::is_empty")]
615 pub deny: Vec<String>,
616 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
618 pub terminal: bool,
619}
620
621#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)]
623#[serde(rename_all = "snake_case")]
624pub enum Constraint {
625 Once(String),
627 Before(String, String),
629 NeverUntil {
631 tool: String,
633 until: Guard,
635 },
636 Require(Vec<String>),
638 Reset {
647 steps: Vec<String>,
649 when: Guard,
651 },
652}
653
654#[derive(Clone, Debug, Default, Serialize, Deserialize, schemars::JsonSchema)]
656pub struct Flow {
657 pub steps: Vec<Step>,
659 #[serde(default, skip_serializing_if = "Vec::is_empty")]
661 pub constraints: Vec<Constraint>,
662 #[serde(default, skip_serializing_if = "Vec::is_empty")]
664 pub confirm_tools: Vec<String>,
665 #[serde(default, skip_serializing_if = "Vec::is_empty")]
674 pub ambient: Vec<String>,
675}
676
677impl Flow {
678 #[allow(
680 clippy::new_ret_no_self,
681 reason = "Flow::new() is the builder entry point; a Flow comes from FlowBuilder::build/compile"
682 )]
683 pub fn new() -> FlowBuilder {
684 FlowBuilder::default()
685 }
686
687 fn step(&self, id: &str) -> Option<&Step> {
688 self.steps.iter().find(|s| s.id == id)
689 }
690
691 pub fn validate(&self) -> Result<(), ConfigError> {
694 let mut errs = Vec::new();
695 let ids: BTreeSet<&str> = self.steps.iter().map(|s| s.id.as_str()).collect();
696 if ids.len() != self.steps.len() {
697 errs.push("duplicate step ids".into());
698 }
699 for s in &self.steps {
700 for d in &s.after {
701 if !ids.contains(d.step.as_str()) {
702 errs.push(format!(
703 "step '{}' depends on unknown step '{}'",
704 s.id, d.step
705 ));
706 }
707 if let Some(when) = &d.when {
708 let mut refs = Vec::new();
709 when.referenced_steps(&mut refs);
710 for r in refs {
711 if !ids.contains(r.as_str()) {
712 errs.push(format!(
713 "step '{}' edge condition references unknown step '{r}'",
714 s.id
715 ));
716 }
717 }
718 }
719 }
720 if !s.terminal && s.done.is_none() {
721 errs.push(format!(
722 "non-terminal step '{}' has no `done` condition (it can never complete)",
723 s.id
724 ));
725 }
726 for g in s.gate.iter().chain(s.done.iter()) {
727 let mut refs = Vec::new();
728 g.referenced_steps(&mut refs);
729 for r in refs {
730 if !ids.contains(r.as_str()) {
731 errs.push(format!(
732 "step '{}' guard references unknown step '{r}'",
733 s.id
734 ));
735 }
736 }
737 }
738 }
739 for c in &self.constraints {
740 match c {
741 Constraint::Before(a, b) => {
742 for x in [a, b] {
743 if !ids.contains(x.as_str()) {
744 errs.push(format!("constraint `before` references unknown step '{x}'"));
745 }
746 }
747 }
748 Constraint::Require(rs) => {
749 for r in rs {
750 if !ids.contains(r.as_str()) {
751 errs.push(format!(
752 "constraint `require` references unknown step '{r}'"
753 ));
754 }
755 }
756 }
757 Constraint::Reset { steps, .. } => {
758 for r in steps {
759 if !ids.contains(r.as_str()) {
760 errs.push(format!("constraint `reset` references unknown step '{r}'"));
761 }
762 }
763 }
764 _ => {}
765 }
766 }
767 if self.has_cycle() {
768 errs.push("flow dependency graph has a cycle (must be a DAG)".into());
769 }
770 ConfigError::from_issues(errs)
771 }
772
773 fn tool_universe(&self) -> BTreeSet<String> {
776 let mut tools = BTreeSet::new();
777 for s in &self.steps {
778 tools.extend(s.allow.iter().cloned());
779 tools.extend(s.deny.iter().cloned());
780 }
781 for c in &self.constraints {
782 match c {
783 Constraint::Once(t) => {
784 tools.insert(t.clone());
785 }
786 Constraint::NeverUntil { tool, .. } => {
787 tools.insert(tool.clone());
788 }
789 _ => {}
790 }
791 }
792 tools.extend(self.confirm_tools.iter().cloned());
793 tools.extend(self.ambient.iter().cloned());
794 tools
795 }
796
797 pub fn state_keys_read(&self) -> BTreeSet<String> {
807 let mut keys = BTreeSet::new();
808 for s in &self.steps {
809 for g in s.gate.iter().chain(s.done.iter()) {
810 g.referenced_state_keys(&mut keys);
811 }
812 for e in &s.after {
813 if let Some(when) = &e.when {
814 when.referenced_state_keys(&mut keys);
815 }
816 }
817 }
818 for c in &self.constraints {
819 match c {
820 Constraint::NeverUntil { until, .. } => until.referenced_state_keys(&mut keys),
821 Constraint::Reset { when, .. } => when.referenced_state_keys(&mut keys),
822 _ => {}
823 }
824 }
825 keys
826 }
827
828 fn reachable_steps(&self) -> BTreeSet<String> {
831 let ids: BTreeSet<&str> = self.steps.iter().map(|s| s.id.as_str()).collect();
832 let mut succ: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
834 for s in &self.steps {
835 for d in &s.after {
836 if ids.contains(d.step.as_str()) {
837 succ.entry(d.step.as_str()).or_default().push(s.id.as_str());
838 }
839 }
840 }
841 for c in &self.constraints {
842 if let Constraint::Before(a, b) = c
843 && ids.contains(a.as_str())
844 && ids.contains(b.as_str())
845 {
846 succ.entry(a.as_str()).or_default().push(b.as_str());
847 }
848 }
849 let roots: Vec<&str> = self
850 .steps
851 .iter()
852 .filter(|s| s.after.is_empty())
853 .map(|s| s.id.as_str())
854 .collect();
855 let mut seen = BTreeSet::new();
856 let mut stack = roots;
857 while let Some(id) = stack.pop() {
858 if seen.insert(id.to_string())
859 && let Some(next) = succ.get(id)
860 {
861 stack.extend(next.iter().copied());
862 }
863 }
864 seen
865 }
866
867 pub fn compile(self) -> Result<CompiledFlow, FlowErrors> {
882 self.compile_internal(None)
883 }
884
885 pub fn compile_with_tools(self, tools: &[&str]) -> Result<CompiledFlow, FlowErrors> {
898 self.compile_internal(Some(tools))
899 }
900
901 fn compile_internal(self, registry: Option<&[&str]>) -> Result<CompiledFlow, FlowErrors> {
902 let mut errors = Vec::new();
903 if let Err(err) = self.validate() {
904 errors.extend(err.issues.into_iter().map(FlowError::Invalid));
905 }
906
907 if errors.is_empty() {
909 let reachable = self.reachable_steps();
911 for s in &self.steps {
912 if !reachable.contains(&s.id) {
913 errors.push(FlowError::UnreachableStep(s.id.clone()));
914 }
915 }
916 if let Some(cycle) = self.ordering_cycle() {
920 errors.push(FlowError::OrderingCycle(cycle));
921 }
922 }
923
924 for tool in &self.confirm_tools {
927 let guard = self.constraints.iter().find_map(|c| match c {
928 Constraint::NeverUntil { tool: t, until } if t == tool => Some(until),
929 _ => None,
930 });
931 let unguarded = matches!(guard, None | Some(Guard::Spec(Pred::Always)));
932 if unguarded {
933 errors.push(FlowError::UnguardedCommitTool(tool.clone()));
934 }
935 }
936
937 let ids: BTreeSet<&str> = self.steps.iter().map(|s| s.id.as_str()).collect();
942 for c in &self.constraints {
943 if let Constraint::NeverUntil { tool, until } = c {
944 let mut refs = Vec::new();
945 until.referenced_steps(&mut refs);
946 for r in refs {
947 if !ids.contains(r.as_str()) {
948 errors.push(FlowError::UnsatisfiableGuard {
949 tool: tool.clone(),
950 step: r,
951 });
952 }
953 }
954 }
955 }
956
957 if let Some(known) = registry {
959 for tool in self.tool_universe() {
960 if !known.contains(&tool.as_str()) {
961 errors.push(FlowError::UnknownTool(tool));
962 }
963 }
964 }
965
966 if errors.is_empty() {
967 let surface = ToolSurface {
968 tools: self.tool_universe(),
969 };
970 Ok(CompiledFlow {
971 flow: self,
972 surface,
973 })
974 } else {
975 Err(FlowErrors(errors))
976 }
977 }
978
979 fn ordering_cycle(&self) -> Option<Vec<String>> {
983 let mut deps: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
985 for s in &self.steps {
986 let entry = deps.entry(s.id.as_str()).or_default();
987 entry.extend(s.after.iter().map(|e| e.step.as_str()));
988 }
989 for c in &self.constraints {
990 if let Constraint::Before(a, b) = c {
991 deps.entry(b.as_str()).or_default().push(a.as_str());
992 }
993 }
994 fn dfs<'a>(
996 id: &'a str,
997 deps: &BTreeMap<&'a str, Vec<&'a str>>,
998 color: &mut BTreeMap<&'a str, u8>,
999 path: &mut Vec<&'a str>,
1000 ) -> Option<Vec<String>> {
1001 color.insert(id, 1);
1002 path.push(id);
1003 for d in deps.get(id).into_iter().flatten() {
1004 match color.get(d).copied() {
1005 Some(1) => {
1006 let start = path.iter().position(|p| p == d).unwrap_or(0);
1007 return Some(
1008 path[start..]
1009 .iter()
1010 .map(std::string::ToString::to_string)
1011 .collect(),
1012 );
1013 }
1014 Some(2) => {}
1015 _ => {
1016 if let Some(cycle) = dfs(d, deps, color, path) {
1017 return Some(cycle);
1018 }
1019 }
1020 }
1021 }
1022 path.pop();
1023 color.insert(id, 2);
1024 None
1025 }
1026 let mut color: BTreeMap<&str, u8> = BTreeMap::new();
1027 for s in &self.steps {
1028 if color.get(s.id.as_str()).copied().unwrap_or(0) == 0 {
1029 let mut path = Vec::new();
1030 if let Some(cycle) = dfs(&s.id, &deps, &mut color, &mut path) {
1031 return Some(cycle);
1032 }
1033 }
1034 }
1035 None
1036 }
1037
1038 fn has_cycle(&self) -> bool {
1039 let mut color: BTreeMap<&str, u8> = BTreeMap::new();
1041 fn dfs<'a>(flow: &'a Flow, id: &'a str, color: &mut BTreeMap<&'a str, u8>) -> bool {
1042 color.insert(id, 1);
1043 if let Some(step) = flow.step(id) {
1044 for d in &step.after {
1045 match color.get(d.step.as_str()).copied() {
1046 Some(1) => return true,
1047 Some(2) => {}
1048 _ => {
1049 if dfs(flow, &d.step, color) {
1050 return true;
1051 }
1052 }
1053 }
1054 }
1055 }
1056 color.insert(id, 2);
1057 false
1058 }
1059 for s in &self.steps {
1060 if color.get(s.id.as_str()).copied().unwrap_or(0) == 0 && dfs(self, &s.id, &mut color) {
1061 return true;
1062 }
1063 }
1064 false
1065 }
1066
1067 pub fn to_mermaid(&self) -> String {
1069 let mut out = String::from("flowchart TD\n");
1070 for s in &self.steps {
1071 let shape = if s.terminal {
1072 format!(" {}([{}])\n", s.id, s.id)
1073 } else {
1074 format!(" {}[{}]\n", s.id, s.id)
1075 };
1076 out.push_str(&shape);
1077 }
1078 for s in &self.steps {
1079 for d in &s.after {
1080 match &d.when {
1081 Some(when) => {
1082 let label = when.describe().replace('|', "/");
1083 out.push_str(&format!(" {} -->|{label}| {}\n", d.step, s.id));
1084 }
1085 None => out.push_str(&format!(" {} --> {}\n", d.step, s.id)),
1086 }
1087 }
1088 }
1089 out
1090 }
1091}
1092
1093#[derive(Clone, Debug, Default)]
1096pub struct Marking {
1097 pub done: BTreeSet<String>,
1099 pub tool_ok: BTreeMap<String, u32>,
1101 pub turns: u32,
1103}
1104
1105#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1107#[serde(rename_all = "snake_case")]
1108pub enum Verdict {
1109 Pending,
1111 Active,
1113 Done,
1115 Skipped,
1117}
1118
1119#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
1121pub struct Violation {
1122 pub subject: String,
1124 pub reason: String,
1126}
1127
1128#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
1133pub enum Enforcement {
1134 #[default]
1136 Enforce,
1137 Observe,
1139}
1140
1141#[derive(Clone)]
1146pub struct StepAction {
1147 name: Option<String>,
1148 agent: Arc<dyn TextAgent>,
1149 mode: AgentMode,
1150}
1151
1152pub fn on_enter(agent: Arc<dyn TextAgent>, mode: AgentMode) -> StepAction {
1160 StepAction {
1161 name: None,
1162 agent,
1163 mode,
1164 }
1165}
1166
1167impl StepAction {
1168 pub fn named(mut self, name: impl Into<String>) -> Self {
1170 self.name = Some(name.into());
1171 self
1172 }
1173
1174 pub(crate) async fn fire(&self, step_id: &str, state: &State) {
1177 let name = self.name.clone().unwrap_or_else(|| step_id.to_string());
1178 match self.mode {
1179 AgentMode::Call => {
1180 let _ = call_agent(&name, self.agent.clone(), state).await;
1181 }
1182 AgentMode::Dispatch | AgentMode::Background => {
1183 let agent = self.agent.clone();
1184 let state = state.clone();
1185 tokio::spawn(async move {
1186 let _ = call_agent(&name, agent, &state).await;
1187 });
1188 }
1189 }
1190 }
1191}
1192
1193pub type SharedFlowMonitor = Arc<parking_lot::Mutex<FlowMonitor>>;
1199
1200pub struct FlowMonitor {
1203 flow: Flow,
1204 mode: Enforcement,
1205 marking: Marking,
1206 violations: Vec<Violation>,
1207 enter_actions: HashMap<String, StepAction>,
1209 announced: BTreeSet<String>,
1211 reset_prev: Vec<bool>,
1214}
1215
1216impl FlowMonitor {
1217 pub fn new(flow: Flow, mode: Enforcement) -> Self {
1223 Self {
1224 flow,
1225 mode,
1226 marking: Marking::default(),
1227 violations: Vec::new(),
1228 enter_actions: HashMap::new(),
1229 announced: BTreeSet::new(),
1230 reset_prev: Vec::new(),
1231 }
1232 }
1233
1234 pub fn compiled(flow: CompiledFlow, mode: Enforcement) -> Self {
1236 Self::new(flow.into_flow(), mode)
1237 }
1238
1239 pub fn try_new(flow: Flow, mode: Enforcement) -> Result<Self, FlowErrors> {
1242 Ok(Self::compiled(flow.compile()?, mode))
1243 }
1244
1245 pub fn into_shared(self) -> SharedFlowMonitor {
1250 Arc::new(parking_lot::Mutex::new(self))
1251 }
1252
1253 pub fn into_stack(self) -> FlowStack {
1256 FlowStack::from_monitor(self)
1257 }
1258
1259 pub fn restart(&mut self) {
1264 self.marking = Marking::default();
1265 self.announced.clear();
1266 self.reset_prev.clear();
1267 }
1268
1269 pub fn explain(&self, state: &State) -> FlowExplanation {
1275 let active_steps = self.active_steps(state);
1276 let active: Vec<String> = active_steps.iter().map(|s| s.id.clone()).collect();
1277 let ctx = self.ctx(state);
1278 let active_progress = active_steps
1279 .iter()
1280 .filter_map(|s| {
1281 s.done
1282 .as_ref()
1283 .map(|g| (s.id.clone(), g.explain_trace(&ctx)))
1284 })
1285 .collect();
1286 let mut allowed_tools = Vec::new();
1287 let mut blocked_tools = BTreeMap::new();
1288 for tool in self.flow.tool_universe() {
1289 match self.admits_tool(&tool, state) {
1290 Ok(()) => allowed_tools.push(tool),
1291 Err(reason) => {
1292 blocked_tools.insert(tool, reason);
1293 }
1294 }
1295 }
1296 FlowExplanation {
1297 active,
1298 allowed_tools,
1299 blocked_tools,
1300 missing_requirements: self.unmet_requirements(),
1301 active_progress,
1302 }
1303 }
1304
1305 pub fn on_enter(mut self, step: impl Into<String>, action: StepAction) -> Self {
1308 self.enter_actions.insert(step.into(), action);
1309 self
1310 }
1311
1312 pub fn take_newly_active(&mut self, state: &State) -> Vec<String> {
1315 let mut fresh = Vec::new();
1316 for s in self.active_steps(state) {
1317 if !self.announced.contains(&s.id) {
1318 fresh.push(s.id.clone());
1319 }
1320 }
1321 for id in &fresh {
1322 self.announced.insert(id.clone());
1323 }
1324 fresh
1325 }
1326
1327 pub fn enter_action(&self, step: &str) -> Option<&StepAction> {
1329 self.enter_actions.get(step)
1330 }
1331
1332 pub async fn fire_enter_actions(&mut self, state: &State) {
1336 for id in self.take_newly_active(state) {
1337 if let Some(action) = self.enter_actions.get(&id) {
1338 action.fire(&id, state).await;
1339 }
1340 }
1341 }
1342
1343 pub fn mode(&self) -> Enforcement {
1345 self.mode
1346 }
1347
1348 pub fn set_posture(&mut self, step_id: &str, posture: Option<String>) -> bool {
1354 match self.flow.steps.iter_mut().find(|s| s.id == step_id) {
1355 Some(step) => {
1356 step.posture = posture;
1357 true
1358 }
1359 None => false,
1360 }
1361 }
1362
1363 pub fn set_ground(&mut self, step_id: &str, ground: Option<String>) -> bool {
1366 match self.flow.steps.iter_mut().find(|s| s.id == step_id) {
1367 Some(step) => {
1368 step.ground = ground;
1369 true
1370 }
1371 None => false,
1372 }
1373 }
1374
1375 pub fn eval(&self, guard: &Guard, state: &State) -> bool {
1379 guard.eval(&self.ctx(state))
1380 }
1381 pub fn marking(&self) -> &Marking {
1383 &self.marking
1384 }
1385 pub fn violations(&self) -> &[Violation] {
1387 &self.violations
1388 }
1389
1390 pub fn record_violation(&mut self, subject: impl Into<String>, reason: impl Into<String>) {
1395 self.violations.push(Violation {
1396 subject: subject.into(),
1397 reason: reason.into(),
1398 });
1399 }
1400 pub fn flow(&self) -> &Flow {
1402 &self.flow
1403 }
1404
1405 fn ctx<'a>(&'a self, state: &'a State) -> FlowCtx<'a> {
1406 FlowCtx {
1407 state,
1408 marking: &self.marking,
1409 }
1410 }
1411
1412 fn eligible(&self, step: &Step, state: &State) -> bool {
1413 let ctx = self.ctx(state);
1414 let edge_ok = |e: &Edge| {
1415 self.marking.done.contains(&e.step)
1416 && e.when.as_ref().map(|g| g.eval(&ctx)).unwrap_or(true)
1417 };
1418 let deps_done = if step.after.is_empty() {
1419 true
1420 } else {
1421 match step.join {
1422 Join::All => step.after.iter().all(edge_ok),
1423 Join::Any => step.after.iter().any(edge_ok),
1424 }
1425 };
1426 let before_ok = self.flow.constraints.iter().all(|c| match c {
1429 Constraint::Before(a, b) if *b == step.id => self.marking.done.contains(a),
1430 _ => true,
1431 });
1432 let gate_ok = step
1433 .gate
1434 .as_ref()
1435 .map(|g| g.eval(&self.ctx(state)))
1436 .unwrap_or(true);
1437 deps_done && before_ok && gate_ok
1438 }
1439
1440 pub fn relatch(&mut self, state: &State) {
1448 self.apply_resets(state);
1449 loop {
1450 let mut newly_done: Vec<String> = Vec::new();
1451 for s in &self.flow.steps {
1452 if self.marking.done.contains(&s.id) {
1453 continue;
1454 }
1455 if !self.eligible(s, state) {
1456 continue;
1457 }
1458 let complete = if s.terminal {
1459 true
1460 } else {
1461 s.done
1462 .as_ref()
1463 .map(|g| g.eval(&self.ctx(state)))
1464 .unwrap_or(false)
1465 };
1466 if complete {
1467 newly_done.push(s.id.clone());
1468 }
1469 }
1470 if newly_done.is_empty() {
1471 break;
1472 }
1473 for id in newly_done {
1474 self.marking.done.insert(id);
1475 }
1476 }
1477 }
1478
1479 fn apply_resets(&mut self, state: &State) -> Vec<String> {
1482 let mut reset: Vec<String> = Vec::new();
1483 let mut edges: Vec<(usize, bool)> = Vec::new();
1485 {
1486 let ctx = self.ctx(state);
1487 for (i, c) in self.flow.constraints.iter().enumerate() {
1488 if let Constraint::Reset { when, .. } = c {
1489 edges.push((i, when.eval(&ctx)));
1490 }
1491 }
1492 }
1493 for (slot, (index, now)) in edges.into_iter().enumerate() {
1494 let prev = self.reset_prev.get(slot).copied().unwrap_or(false);
1495 if self.reset_prev.len() <= slot {
1496 self.reset_prev.resize(slot + 1, false);
1497 }
1498 self.reset_prev[slot] = now;
1499 if !now || prev {
1500 continue;
1501 }
1502 let Constraint::Reset { steps, .. } = &self.flow.constraints[index] else {
1503 continue;
1504 };
1505 let steps = steps.clone();
1506 for step_id in &steps {
1507 if !self.marking.done.remove(step_id) {
1508 continue;
1509 }
1510 reset.push(step_id.clone());
1511 self.announced.remove(step_id);
1512 if let Some(step) = self.flow.steps.iter().find(|s| &s.id == step_id) {
1516 let mut tools = Vec::new();
1517 if let Some(done) = &step.done {
1518 done.referenced_tools(&mut tools);
1519 }
1520 for tool in tools {
1521 self.marking.tool_ok.remove(&tool);
1522 }
1523 }
1524 }
1525 }
1526 reset
1527 }
1528
1529 pub fn on_turn(&mut self, state: &State) {
1531 self.begin_turn(state);
1532 self.relatch(state);
1533 }
1534
1535 pub fn begin_turn(&mut self, state: &State) -> Vec<String> {
1545 self.marking.turns += 1;
1546 self.apply_resets(state)
1547 }
1548
1549 pub fn on_tool_ok(&mut self, tool: &str, state: &State) {
1551 self.begin_tool_ok(tool, state);
1552 self.relatch(state);
1553 }
1554
1555 pub fn begin_tool_ok(&mut self, tool: &str, state: &State) -> Vec<String> {
1565 *self.marking.tool_ok.entry(tool.to_string()).or_insert(0) += 1;
1566 self.apply_resets(state)
1567 }
1568
1569 pub fn active_steps(&self, state: &State) -> Vec<&Step> {
1571 self.flow
1572 .steps
1573 .iter()
1574 .filter(|s| !self.marking.done.contains(&s.id) && self.eligible(s, state))
1575 .collect()
1576 }
1577
1578 pub fn active_postures(&self, state: &State) -> Vec<String> {
1580 self.active_steps(state)
1581 .into_iter()
1582 .filter_map(|s| s.posture.clone())
1583 .collect()
1584 }
1585
1586 pub fn active_grounds(&self, state: &State) -> Vec<String> {
1589 self.active_steps(state)
1590 .into_iter()
1591 .filter_map(|s| s.ground.as_ref().map(|t| render_ground(t, state)))
1592 .filter(|s| !s.trim().is_empty())
1593 .collect()
1594 }
1595
1596 pub fn closing_steps(&self) -> Vec<&Step> {
1603 self.flow
1604 .steps
1605 .iter()
1606 .filter(|s| s.terminal && self.marking.done.contains(&s.id))
1607 .collect()
1608 }
1609
1610 pub fn closing_postures(&self) -> Vec<String> {
1612 self.closing_steps()
1613 .into_iter()
1614 .filter_map(|s| s.posture.clone())
1615 .collect()
1616 }
1617
1618 pub fn closing_grounds(&self, state: &State) -> Vec<String> {
1620 self.closing_steps()
1621 .into_iter()
1622 .filter_map(|s| s.ground.as_ref().map(|t| render_ground(t, state)))
1623 .filter(|s| !s.trim().is_empty())
1624 .collect()
1625 }
1626
1627 pub fn unmet_requirements(&self) -> Vec<String> {
1629 self.flow
1630 .constraints
1631 .iter()
1632 .flat_map(|c| match c {
1633 Constraint::Require(rs) => rs.clone(),
1634 _ => Vec::new(),
1635 })
1636 .filter(|r| !self.marking.done.contains(r))
1637 .collect()
1638 }
1639
1640 pub fn is_complete(&self) -> bool {
1642 self.unmet_requirements().is_empty()
1643 }
1644
1645 pub fn verdict(&self, step_id: &str, state: &State) -> Verdict {
1647 if self.marking.done.contains(step_id) {
1648 return Verdict::Done;
1649 }
1650 if let Some(step) = self.flow.step(step_id)
1651 && self.eligible(step, state)
1652 {
1653 return Verdict::Active;
1654 }
1655 let bypassed = self.flow.steps.iter().any(|s| {
1657 s.after.iter().any(|d| d.step == step_id) && self.marking.done.contains(&s.id)
1658 });
1659 if bypassed {
1660 Verdict::Skipped
1661 } else {
1662 Verdict::Pending
1663 }
1664 }
1665
1666 pub fn admits_tool(&self, tool: &str, state: &State) -> Result<(), String> {
1669 match self.admissibility(tool, state) {
1670 Ok(()) => Ok(()),
1671 Err(denial) => Err(self.render_denial(tool, &denial, state)),
1672 }
1673 }
1674
1675 fn admissibility(&self, tool: &str, state: &State) -> Result<(), Denial> {
1681 for c in &self.flow.constraints {
1683 if let Constraint::Once(t) = c
1684 && t == tool
1685 && self.marking.tool_ok.contains_key(tool)
1686 {
1687 return Err(Denial::OnceExhausted);
1688 }
1689 }
1690 for c in &self.flow.constraints {
1692 if let Constraint::NeverUntil { tool: t, until } = c
1693 && t == tool
1694 && !until.eval(&self.ctx(state))
1695 {
1696 return Err(Denial::NotYet(until.describe()));
1697 }
1698 }
1699 let active = self.active_steps(state);
1701 if let Some(step) = active.iter().find(|s| s.deny.iter().any(|d| d == tool)) {
1702 return Err(Denial::DeniedByStep(step.id.clone()));
1703 }
1704 if self.flow.ambient.iter().any(|a| a == tool) {
1707 return Ok(());
1708 }
1709 let restricting: Vec<&&Step> = active.iter().filter(|s| !s.allow.is_empty()).collect();
1710 if !restricting.is_empty()
1711 && !restricting
1712 .iter()
1713 .any(|s| s.allow.iter().any(|a| a == tool))
1714 {
1715 return Err(Denial::NotInStep(
1716 restricting.iter().map(|s| s.id.clone()).collect(),
1717 ));
1718 }
1719 Ok(())
1720 }
1721
1722 fn render_denial(&self, tool: &str, denial: &Denial, state: &State) -> String {
1733 let head = match denial {
1734 Denial::OnceExhausted => {
1735 return format!(
1737 "'{tool}' has already run and may run only once in this \
1738 conversation. Do not call it again."
1739 );
1740 }
1741 Denial::NotYet(condition) => {
1742 format!("'{tool}' is not permitted yet — first, {condition}.")
1743 }
1744 Denial::DeniedByStep(step) => {
1745 format!("'{tool}' is not allowed during the current step ('{step}').")
1746 }
1747 Denial::NotInStep(steps) => format!(
1748 "'{tool}' is not part of the current step ({}).",
1749 steps
1750 .iter()
1751 .map(|s| format!("'{s}'"))
1752 .collect::<Vec<_>>()
1753 .join(", ")
1754 ),
1755 };
1756
1757 let available: Vec<String> = self
1760 .flow
1761 .tool_universe()
1762 .into_iter()
1763 .filter(|t| self.admissibility(t, state).is_ok())
1764 .collect();
1765
1766 let mut out = head;
1767 if available.is_empty() {
1768 out.push_str(" No tool is available right now — continue the conversation instead.");
1769 } else {
1770 out.push_str(" Available now: ");
1771 out.push_str(&available.join(", "));
1772 out.push('.');
1773 }
1774 let postures = self.active_postures(state);
1778 if let Some(first) = postures.first() {
1779 out.push(' ');
1780 out.push_str(first);
1781 }
1782 out
1783 }
1784
1785 pub fn observe_tool(&mut self, tool: &str, ok: bool, state: &State) {
1789 if self.mode == Enforcement::Observe
1790 && let Err(reason) = self.admits_tool(tool, state)
1791 {
1792 self.violations.push(Violation {
1793 subject: tool.to_string(),
1794 reason,
1795 });
1796 }
1797 if ok {
1798 self.on_tool_ok(tool, state);
1799 }
1800 }
1801}
1802
1803enum Denial {
1809 OnceExhausted,
1811 NotYet(String),
1814 DeniedByStep(String),
1816 NotInStep(Vec<String>),
1819}
1820
1821#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1823#[serde(tag = "kind", content = "detail", rename_all = "snake_case")]
1824pub enum FlowError {
1825 Invalid(String),
1827 UnreachableStep(String),
1829 UnguardedCommitTool(String),
1832 UnknownTool(String),
1836 UnsatisfiableGuard {
1840 tool: String,
1842 step: String,
1844 },
1845 OrderingCycle(Vec<String>),
1849}
1850
1851impl std::fmt::Display for FlowError {
1852 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1853 match self {
1854 FlowError::Invalid(m) => write!(f, "{m}"),
1855 FlowError::UnreachableStep(id) => {
1856 write!(f, "step '{id}' is unreachable from any root")
1857 }
1858 FlowError::UnguardedCommitTool(t) => write!(
1859 f,
1860 "commit tool '{t}' is guarded by an always-true condition (effectively unguarded)"
1861 ),
1862 FlowError::UnknownTool(t) => write!(
1863 f,
1864 "flow references tool '{t}' which is not in the provided tool registry"
1865 ),
1866 FlowError::UnsatisfiableGuard { tool, step } => write!(
1867 f,
1868 "`never('{tool}').until(..)` references unknown step '{step}' — the guard can \
1869 never hold, so '{tool}' would be forbidden forever"
1870 ),
1871 FlowError::OrderingCycle(steps) => write!(
1872 f,
1873 "ordering cycle across `after`/`before` edges: {} (no step on it can ever start)",
1874 steps.join(" -> ")
1875 ),
1876 }
1877 }
1878}
1879
1880#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1882pub struct FlowErrors(pub Vec<FlowError>);
1883
1884impl std::fmt::Display for FlowErrors {
1885 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1886 writeln!(f, "flow failed to compile ({} error(s)):", self.0.len())?;
1887 for e in &self.0 {
1888 writeln!(f, " - {e}")?;
1889 }
1890 Ok(())
1891 }
1892}
1893
1894impl std::error::Error for FlowErrors {}
1895
1896#[derive(Debug, Clone, Default)]
1902pub struct ToolSurface {
1903 pub tools: BTreeSet<String>,
1905}
1906
1907#[derive(Debug, Clone)]
1913pub struct CompiledFlow {
1914 flow: Flow,
1915 surface: ToolSurface,
1916}
1917
1918impl CompiledFlow {
1919 pub fn flow(&self) -> &Flow {
1921 &self.flow
1922 }
1923 pub fn tool_surface(&self) -> &ToolSurface {
1925 &self.surface
1926 }
1927 pub fn to_mermaid(&self) -> String {
1929 self.flow.to_mermaid()
1930 }
1931 pub fn into_flow(self) -> Flow {
1933 self.flow
1934 }
1935}
1936
1937#[derive(Debug, Clone, Serialize)]
1943pub struct FlowExplanation {
1944 pub active: Vec<String>,
1946 pub allowed_tools: Vec<String>,
1948 pub blocked_tools: BTreeMap<String, String>,
1950 pub missing_requirements: Vec<String>,
1952 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1955 pub active_progress: BTreeMap<String, GuardTrace>,
1956}
1957
1958#[derive(Default)]
1960pub struct FlowBuilder {
1961 steps: Vec<Step>,
1962 constraints: Vec<Constraint>,
1963 confirm_tools: Vec<String>,
1964 ambient: Vec<String>,
1965}
1966
1967impl FlowBuilder {
1968 fn current(&mut self) -> &mut Step {
1969 self.steps
1970 .last_mut()
1971 .expect("call `.step(id)` before configuring a step")
1972 }
1973
1974 pub fn step(mut self, id: impl Into<String>) -> Self {
1976 self.steps.push(Step {
1977 id: id.into(),
1978 after: Vec::new(),
1979 join: Join::default(),
1980 gate: None,
1981 done: None,
1982 posture: None,
1983 ground: None,
1984 allow: Vec::new(),
1985 deny: Vec::new(),
1986 terminal: false,
1987 });
1988 self
1989 }
1990 pub fn after(mut self, dep: impl Into<String>) -> Self {
1992 self.current().after.push(Edge::to(dep));
1993 self
1994 }
1995 pub fn after_when(mut self, dep: impl Into<String>, when: Guard) -> Self {
1999 self.current().after.push(Edge::when(dep, when));
2000 self
2001 }
2002 pub fn join_any(mut self) -> Self {
2005 self.current().join = Join::Any;
2006 self
2007 }
2008 pub fn gate(mut self, g: Guard) -> Self {
2010 self.current().gate = Some(g);
2011 self
2012 }
2013 pub fn done(mut self, g: Guard) -> Self {
2015 self.current().done = Some(g);
2016 self
2017 }
2018 pub fn posture(mut self, text: impl Into<String>) -> Self {
2020 self.current().posture = Some(text.into());
2021 self
2022 }
2023 pub fn ground(mut self, template: impl Into<String>) -> Self {
2028 self.current().ground = Some(template.into());
2029 self
2030 }
2031 pub fn allow<I, S>(mut self, tools: I) -> Self
2033 where
2034 I: IntoIterator<Item = S>,
2035 S: Into<String>,
2036 {
2037 self.current()
2038 .allow
2039 .extend(tools.into_iter().map(Into::into));
2040 self
2041 }
2042 pub fn deny<I, S>(mut self, tools: I) -> Self
2044 where
2045 I: IntoIterator<Item = S>,
2046 S: Into<String>,
2047 {
2048 self.current()
2049 .deny
2050 .extend(tools.into_iter().map(Into::into));
2051 self
2052 }
2053 pub fn terminal(mut self) -> Self {
2055 self.current().terminal = true;
2056 self
2057 }
2058
2059 pub fn once(mut self, tool: impl Into<String>) -> Self {
2061 self.constraints.push(Constraint::Once(tool.into()));
2062 self
2063 }
2064 pub fn before(mut self, a: impl Into<String>, b: impl Into<String>) -> Self {
2066 self.constraints
2067 .push(Constraint::Before(a.into(), b.into()));
2068 self
2069 }
2070 pub fn require<I, S>(mut self, steps: I) -> Self
2072 where
2073 I: IntoIterator<Item = S>,
2074 S: Into<String>,
2075 {
2076 self.constraints.push(Constraint::Require(
2077 steps.into_iter().map(Into::into).collect(),
2078 ));
2079 self
2080 }
2081 pub fn ambient<I, S>(mut self, tools: I) -> Self
2100 where
2101 I: IntoIterator<Item = S>,
2102 S: Into<String>,
2103 {
2104 self.ambient.extend(tools.into_iter().map(Into::into));
2105 self
2106 }
2107
2108 pub fn reset<I, S>(self, steps: I) -> ResetBuilder
2112 where
2113 I: IntoIterator<Item = S>,
2114 S: Into<String>,
2115 {
2116 ResetBuilder {
2117 builder: self,
2118 steps: steps.into_iter().map(Into::into).collect(),
2119 }
2120 }
2121
2122 pub fn never(self, tool: impl Into<String>) -> NeverBuilder {
2124 NeverBuilder {
2125 fb: self,
2126 tool: tool.into(),
2127 }
2128 }
2129 pub fn commit(mut self, tool: impl Into<String>, until: Guard) -> Self {
2132 let tool = tool.into();
2133 self.constraints.push(Constraint::Once(tool.clone()));
2134 self.constraints.push(Constraint::NeverUntil {
2135 tool: tool.clone(),
2136 until,
2137 });
2138 self.confirm_tools.push(tool);
2139 self
2140 }
2141
2142 pub fn build(self) -> Result<Flow, ConfigError> {
2144 let flow = Flow {
2145 steps: self.steps,
2146 constraints: self.constraints,
2147 confirm_tools: self.confirm_tools,
2148 ambient: self.ambient,
2149 };
2150 flow.validate()?;
2151 Ok(flow)
2152 }
2153}
2154
2155pub struct NeverBuilder {
2157 fb: FlowBuilder,
2158 tool: String,
2159}
2160
2161impl NeverBuilder {
2162 pub fn until(mut self, guard: Guard) -> FlowBuilder {
2164 self.fb.constraints.push(Constraint::NeverUntil {
2165 tool: self.tool,
2166 until: guard,
2167 });
2168 self.fb
2169 }
2170}
2171
2172pub struct ResetBuilder {
2174 builder: FlowBuilder,
2175 steps: Vec<String>,
2176}
2177
2178impl ResetBuilder {
2179 pub fn when(mut self, guard: Guard) -> FlowBuilder {
2181 self.builder.constraints.push(Constraint::Reset {
2182 steps: self.steps,
2183 when: guard,
2184 });
2185 self.builder
2186 }
2187}
2188
2189#[cfg(test)]
2190mod tests {
2191 use super::*;
2192 use serde_json::json;
2193
2194 #[test]
2204 fn a_refusal_names_what_is_available_instead() {
2205 let state = State::new();
2206 let mon = FlowMonitor::new(debt_flow(), Enforcement::Enforce);
2207
2208 let reason = mon
2209 .admits_tool("charge_card", &state)
2210 .expect_err("charge_card is gated behind verification");
2211
2212 assert!(
2213 reason.contains("lookup_account"),
2214 "a refusal must point at the tool that would make progress: {reason}"
2215 );
2216 assert!(
2217 reason.contains("ptp_confirmed"),
2218 "a refusal must name the condition it is waiting on: {reason}"
2219 );
2220 }
2221
2222 #[test]
2226 fn a_refusal_carries_the_active_steps_posture() {
2227 let state = State::new();
2228 let mon = FlowMonitor::new(debt_flow(), Enforcement::Enforce);
2229
2230 let reason = mon
2231 .admits_tool("charge_card", &state)
2232 .expect_err("charge_card is gated behind verification");
2233
2234 assert!(
2235 reason.contains("Verify the caller's identity."),
2236 "the active step's posture belongs in the refusal: {reason}"
2237 );
2238 }
2239
2240 #[test]
2244 fn a_spent_once_constraint_does_not_offer_alternatives() {
2245 let state = State::new();
2246 let flow = Flow::new()
2247 .step("pay")
2248 .allow(["charge_card"])
2249 .done(Guard::called_ok("charge_card"))
2250 .once("charge_card")
2251 .build()
2252 .expect("valid flow");
2253 let mut mon = FlowMonitor::new(flow, Enforcement::Enforce);
2254 mon.on_tool_ok("charge_card", &state);
2255
2256 let reason = mon
2257 .admits_tool("charge_card", &state)
2258 .expect_err("once is spent");
2259
2260 assert!(
2261 reason.contains("already run"),
2262 "a spent `once` must say so plainly: {reason}"
2263 );
2264 assert!(
2265 !reason.contains("Available now"),
2266 "nothing to redirect to — offering a menu invites a retry: {reason}"
2267 );
2268 }
2269
2270 #[test]
2274 fn the_redirection_tracks_the_conversation() {
2275 let state = State::new();
2276 let mon = FlowMonitor::new(debt_flow(), Enforcement::Enforce);
2277
2278 let before = mon
2279 .admits_tool("charge_card", &state)
2280 .expect_err("gated before the promise is confirmed");
2281 assert!(
2282 before.contains("ptp_confirmed") && before.contains("lookup_account"),
2283 "{before}"
2284 );
2285
2286 let _ = state.set("identity_verified", true);
2290 let mut mon = mon;
2291 mon.on_turn(&state);
2292 let after = mon
2293 .admits_tool("charge_card", &state)
2294 .expect_err("the promise is still unconfirmed");
2295
2296 assert!(
2297 after.contains("Give the disclosure."),
2298 "the refusal must carry the posture of the step that is now active, \
2299 not the one that was active when the session opened: {after}"
2300 );
2301 assert!(
2302 !after.contains("Verify the caller's identity."),
2303 "a completed step's posture must not keep riding along on refusals — \
2304 that is how a verified caller gets asked to verify again: {after}"
2305 );
2306 assert_ne!(
2307 before, after,
2308 "the refusal did not change when the flow advanced"
2309 );
2310 }
2311
2312 #[test]
2315 fn guards_describe_themselves_in_prose() {
2316 assert_eq!(
2317 Guard::is_true("verified").describe(),
2318 "'verified' must be true"
2319 );
2320 assert_eq!(
2321 Guard::captured(["amount", "date"]).describe(),
2322 "these must be known: amount, date"
2323 );
2324 assert_eq!(
2325 Guard::called_ok("disclose").describe(),
2326 "'disclose' must have run successfully"
2327 );
2328 assert_eq!(
2329 Guard::all([Guard::is_true("a"), Guard::done("b")]).describe(),
2330 "'a' must be true and step 'b' must be complete"
2331 );
2332 assert_eq!(
2333 Guard::custom(|_| true).describe(),
2334 "a condition set by the application",
2335 "a custom guard has no readable spec, and must not pretend otherwise"
2336 );
2337 }
2338
2339 fn debt_flow() -> Flow {
2340 Flow::new()
2341 .step("verify")
2342 .posture("Verify the caller's identity.")
2343 .allow(["lookup_account"])
2344 .done(Guard::is_true("identity_verified"))
2345 .step("disclose")
2346 .after("verify")
2347 .posture("Give the disclosure.")
2348 .done(Guard::is_true("disclosure_given"))
2349 .step("capture_ptp")
2350 .after("disclose")
2351 .done(Guard::captured(["ptp_amount", "ptp_date"]))
2352 .step("take_payment")
2353 .after("capture_ptp")
2354 .allow(["charge_card"])
2355 .done(Guard::called_ok("charge_card"))
2356 .step("close")
2357 .after("capture_ptp")
2358 .terminal()
2359 .never("charge_card")
2360 .until(Guard::is_true("ptp_confirmed"))
2361 .once("charge_card")
2362 .require(["close"])
2363 .build()
2364 .expect("valid flow")
2365 }
2366
2367 #[test]
2368 fn validates_and_detects_unknown_dep() {
2369 let bad = Flow::new()
2370 .step("a")
2371 .done(Guard::is_true("x"))
2372 .step("b")
2373 .after("missing")
2374 .terminal()
2375 .build();
2376 assert!(bad.is_err());
2377 }
2378
2379 #[test]
2380 fn detects_cycle() {
2381 let steps = vec![
2383 Step {
2384 id: "a".into(),
2385 after: vec!["b".into()],
2386 join: Join::default(),
2387 gate: None,
2388 done: Some(Guard::always()),
2389 posture: None,
2390 ground: None,
2391 allow: vec![],
2392 deny: vec![],
2393 terminal: false,
2394 },
2395 Step {
2396 id: "b".into(),
2397 after: vec!["a".into()],
2398 join: Join::default(),
2399 gate: None,
2400 done: Some(Guard::always()),
2401 posture: None,
2402 ground: None,
2403 allow: vec![],
2404 deny: vec![],
2405 terminal: false,
2406 },
2407 ];
2408 let flow = Flow {
2409 steps,
2410 ..Flow::default()
2411 };
2412 assert!(flow.validate().is_err());
2413 }
2414
2415 #[test]
2416 fn marking_latches_in_order() {
2417 let flow = debt_flow();
2418 let mut mon = FlowMonitor::new(flow, Enforcement::Enforce);
2419 let state = State::new();
2420
2421 assert_eq!(
2423 mon.active_steps(&state)
2424 .iter()
2425 .map(|s| s.id.as_str())
2426 .collect::<Vec<_>>(),
2427 vec!["verify"]
2428 );
2429
2430 let _ = state.set("identity_verified", true);
2431 mon.on_turn(&state);
2432 assert!(mon.marking().done.contains("verify"));
2433 assert_eq!(mon.verdict("verify", &state), Verdict::Done);
2434 assert_eq!(mon.verdict("disclose", &state), Verdict::Active);
2435
2436 let _ = state.set("disclosure_given", true);
2437 let _ = state.set("ptp_amount", 200);
2438 let _ = state.set("ptp_date", "2026-06-05");
2439 mon.on_turn(&state);
2440 assert!(mon.marking().done.contains("capture_ptp"));
2442 assert!(mon.marking().done.contains("close"));
2443 assert!(mon.is_complete());
2444 }
2445
2446 #[test]
2447 fn enforces_never_until_and_once() {
2448 let flow = debt_flow();
2449 let mut mon = FlowMonitor::new(flow, Enforcement::Enforce);
2450 let state = State::new();
2451 let _ = state.set("identity_verified", true);
2453 let _ = state.set("disclosure_given", true);
2454 let _ = state.set("ptp_amount", 200);
2455 let _ = state.set("ptp_date", "x");
2456 mon.on_turn(&state);
2457
2458 assert!(mon.admits_tool("charge_card", &state).is_err());
2460 let _ = state.set("ptp_confirmed", true);
2461 assert!(mon.admits_tool("charge_card", &state).is_ok());
2462
2463 mon.on_tool_ok("charge_card", &state);
2465 assert!(mon.admits_tool("charge_card", &state).is_err());
2466 }
2467
2468 #[test]
2469 fn whitelist_scopes_tools_to_active_step() {
2470 let flow = debt_flow();
2471 let mon = FlowMonitor::new(flow, Enforcement::Enforce);
2472 let state = State::new();
2473 assert!(mon.admits_tool("lookup_account", &state).is_ok());
2475 assert!(mon.admits_tool("charge_card", &state).is_err());
2476 }
2477
2478 #[test]
2479 fn observe_mode_records_violations_not_blocks() {
2480 let flow = debt_flow();
2481 let mut mon = FlowMonitor::new(flow, Enforcement::Observe);
2482 let state = State::new();
2483 mon.observe_tool("charge_card", true, &state);
2485 assert_eq!(mon.violations().len(), 1);
2486 assert_eq!(mon.violations()[0].subject, "charge_card");
2487 }
2488
2489 #[test]
2490 fn compile_accepts_valid_flow_and_collects_tool_universe() {
2491 let compiled = debt_flow().compile().expect("valid flow compiles");
2492 assert!(compiled.tool_surface().tools.contains("charge_card"));
2494 assert!(compiled.tool_surface().tools.contains("lookup_account"));
2495 let _ = FlowMonitor::compiled(compiled, Enforcement::Enforce);
2496 }
2497
2498 #[test]
2499 fn compile_rejects_unreachable_step() {
2500 let flow = Flow::new()
2505 .step("a")
2506 .done(Guard::is_true("a_done"))
2507 .step("b")
2508 .after("a")
2509 .done(Guard::is_true("b_done"))
2510 .step("island")
2511 .after("b")
2512 .gate(Guard::is_true("never"))
2513 .terminal()
2514 .build()
2515 .expect("structurally valid");
2516 assert!(flow.compile().is_ok());
2518 }
2519
2520 #[test]
2521 fn compile_rejects_unguarded_commit_tool() {
2522 let flow = Flow::new()
2524 .step("s")
2525 .allow(["pay"])
2526 .done(Guard::called_ok("pay"))
2527 .terminal()
2528 .commit("pay", Guard::always())
2529 .build()
2530 .expect("structurally valid");
2531 let err = flow
2532 .compile()
2533 .expect_err("unguarded commit must fail to compile");
2534 assert!(
2535 err.0
2536 .iter()
2537 .any(|e| matches!(e, FlowError::UnguardedCommitTool(t) if t == "pay"))
2538 );
2539 }
2540
2541 #[test]
2542 fn compile_with_tools_accepts_a_covering_registry() {
2543 let compiled = debt_flow()
2544 .compile_with_tools(&["lookup_account", "charge_card", "unrelated_extra"])
2545 .expect("registry covers the flow's tool universe");
2546 assert!(compiled.tool_surface().tools.contains("charge_card"));
2547 }
2548
2549 #[test]
2550 fn compile_with_tools_reports_dangling_tool_names() {
2551 let err = debt_flow()
2554 .compile_with_tools(&["lookup_account"])
2555 .expect_err("dangling tool must fail to compile");
2556 assert!(
2557 err.0
2558 .iter()
2559 .any(|e| matches!(e, FlowError::UnknownTool(t) if t == "charge_card"))
2560 );
2561 assert!(debt_flow().compile().is_ok());
2563 }
2564
2565 #[test]
2566 fn compile_rejects_never_until_guard_on_unknown_step() {
2567 let flow = Flow::new()
2570 .step("s")
2571 .allow(["pay"])
2572 .done(Guard::called_ok("pay"))
2573 .never("pay")
2574 .until(Guard::done("missing"))
2575 .build()
2576 .expect("structurally valid for build()");
2577 let err = flow.compile().expect_err("unsatisfiable guard must fail");
2578 assert!(err.0.iter().any(|e| matches!(
2579 e,
2580 FlowError::UnsatisfiableGuard { tool, step } if tool == "pay" && step == "missing"
2581 )));
2582 }
2583
2584 #[test]
2585 fn compile_rejects_before_cycle() {
2586 let flow = Flow::new()
2590 .step("a")
2591 .done(Guard::is_true("a_done"))
2592 .step("b")
2593 .done(Guard::is_true("b_done"))
2594 .before("a", "b")
2595 .before("b", "a")
2596 .build()
2597 .expect("build() only checks `after` cycles");
2598 let err = flow
2599 .compile()
2600 .expect_err("before-cycle must fail to compile");
2601 assert!(err.0.iter().any(|e| matches!(
2602 e,
2603 FlowError::OrderingCycle(steps)
2604 if steps.contains(&"a".to_string()) && steps.contains(&"b".to_string())
2605 )));
2606 }
2607
2608 #[test]
2609 fn explain_reports_blocked_tools_and_reasons() {
2610 let flow = debt_flow();
2611 let mon = FlowMonitor::new(flow, Enforcement::Enforce);
2612 let state = State::new();
2613 let ex = mon.explain(&state);
2614 assert!(ex.blocked_tools.contains_key("charge_card"));
2616 assert!(ex.active.contains(&"verify".to_string()));
2617 }
2618
2619 #[test]
2620 fn before_constraint_gates_step_eligibility() {
2621 let flow = Flow::new()
2625 .step("a")
2626 .done(Guard::is_true("a_done"))
2627 .step("b")
2628 .done(Guard::is_true("b_done"))
2629 .before("a", "b")
2630 .build()
2631 .expect("valid flow");
2632 let mut mon = FlowMonitor::new(flow, Enforcement::Enforce);
2633 let state = State::new();
2634
2635 let active: Vec<String> = mon
2637 .active_steps(&state)
2638 .iter()
2639 .map(|s| s.id.clone())
2640 .collect();
2641 assert!(active.contains(&"a".to_string()));
2642 assert!(
2643 !active.contains(&"b".to_string()),
2644 "b must wait for a (Before)"
2645 );
2646
2647 let _ = state.set("a_done", true);
2648 mon.on_turn(&state);
2649 let active: Vec<String> = mon
2650 .active_steps(&state)
2651 .iter()
2652 .map(|s| s.id.clone())
2653 .collect();
2654 assert!(active.contains(&"b".to_string()), "b active once a is done");
2655 }
2656
2657 #[test]
2658 fn custom_guard_in_combinator_is_not_erased() {
2659 let always_false = Guard::all([Guard::is_true("present"), Guard::custom(|_| false)]);
2662 assert!(matches!(always_false, Guard::Custom(_)));
2664
2665 let state = State::new();
2666 let _ = state.set("present", true);
2667 let marking = Marking::default();
2668 let ctx = FlowCtx {
2669 state: &state,
2670 marking: &marking,
2671 };
2672 assert!(!always_false.eval(&ctx), "custom guard must still veto");
2674
2675 let serializable = Guard::all([Guard::is_true("a"), Guard::is_set("b")]);
2677 assert!(matches!(serializable, Guard::Spec(_)));
2678 }
2679
2680 #[test]
2681 fn serde_round_trips_data_driven_flow() {
2682 let flow = debt_flow();
2683 let jsonv = serde_json::to_value(&flow).expect("serialize");
2684 let back: Flow = serde_json::from_value(jsonv).expect("deserialize");
2685 back.validate().expect("round-tripped flow is valid");
2686 assert_eq!(back.steps.len(), flow.steps.len());
2687 }
2688
2689 #[test]
2690 fn custom_guard_is_not_serializable() {
2691 let flow = Flow::new()
2692 .step("a")
2693 .done(Guard::custom(|ctx| ctx.state.contains("ready")))
2694 .terminal()
2695 .build()
2696 .unwrap();
2697 assert!(serde_json::to_value(&flow).is_err());
2698 }
2699
2700 #[test]
2701 fn mermaid_export_has_nodes_and_edges() {
2702 let m = debt_flow().to_mermaid();
2703 assert!(m.contains("flowchart TD"));
2704 assert!(m.contains("verify --> disclose"));
2705 assert!(m.contains("close([close])")); }
2707
2708 struct WriteAgent;
2709 #[async_trait::async_trait]
2710 impl TextAgent for WriteAgent {
2711 fn name(&self) -> &str {
2712 "writer"
2713 }
2714 async fn run(&self, _state: &State) -> Result<String, crate::error::AgentError> {
2715 Ok("available".to_string())
2716 }
2717 }
2718
2719 #[tokio::test]
2720 async fn on_enter_fires_once_when_step_activates() {
2721 let flow = Flow::new()
2724 .step("collect")
2725 .done(Guard::is_true("collected"))
2726 .step("check")
2727 .after("collect")
2728 .done(Guard::resolved("check"))
2729 .step("book")
2730 .after("check")
2731 .terminal()
2732 .require(["book"])
2733 .build()
2734 .expect("valid flow");
2735
2736 let mut mon = FlowMonitor::new(flow, Enforcement::Enforce)
2737 .on_enter("check", on_enter(Arc::new(WriteAgent), AgentMode::Call));
2738 let state = State::new();
2739
2740 assert_eq!(mon.take_newly_active(&state), vec!["collect".to_string()]);
2742 assert!(mon.take_newly_active(&state).is_empty());
2744
2745 let _ = state.set("collected", true);
2747 mon.on_turn(&state);
2748 mon.fire_enter_actions(&state).await;
2749 assert_eq!(
2750 state.get::<String>("check:result").as_deref(),
2751 Some("available")
2752 );
2753
2754 mon.on_turn(&state);
2756 assert!(mon.marking().done.contains("check"));
2757 assert!(mon.is_complete());
2758 assert!(mon.take_newly_active(&state).is_empty());
2760 }
2761
2762 #[test]
2763 fn ground_template_interpolates_and_branches() {
2764 let state = State::new();
2765 let _ = state.set("when", "3pm");
2766 let _ = state.set("available", true);
2767 let _ = state.set("prior_visits", 2);
2768 assert_eq!(
2769 render_ground(
2770 "{when} is {available?open:taken}; {prior_visits} prior visits.",
2771 &state
2772 ),
2773 "3pm is open; 2 prior visits."
2774 );
2775 let _ = state.set("available", false);
2777 assert_eq!(
2778 render_ground("slot {missing}is {available?free:full}", &state),
2779 "slot is full"
2780 );
2781 }
2782
2783 #[test]
2784 fn active_grounds_projects_only_active_steps() {
2785 let flow = Flow::new()
2786 .step("collect")
2787 .ground("Known time: {when}.")
2788 .done(Guard::is_set("when"))
2789 .step("done")
2790 .after("collect")
2791 .terminal()
2792 .build()
2793 .expect("valid flow");
2794 let mut mon = FlowMonitor::new(flow, Enforcement::Enforce);
2795 let state = State::new();
2796 let _ = state.set("when", "3pm");
2797 assert_eq!(
2798 mon.active_grounds(&state),
2799 vec!["Known time: 3pm.".to_string()]
2800 );
2801 mon.on_turn(&state);
2803 assert!(mon.active_grounds(&state).is_empty());
2804 }
2805
2806 #[test]
2807 fn eq_guard_matches_state_value() {
2808 let g = Guard::eq("status", json!("active"));
2809 let state = State::new();
2810 let marking = Marking::default();
2811 assert!(!g.eval(&FlowCtx {
2812 state: &state,
2813 marking: &marking
2814 }));
2815 let _ = state.set("status", "active");
2816 assert!(g.eval(&FlowCtx {
2817 state: &state,
2818 marking: &marking
2819 }));
2820 }
2821
2822 fn ambient_flow() -> Flow {
2837 Flow::new()
2838 .ambient(["recall_context"])
2839 .step("gather")
2840 .allow(["ask_diet"])
2841 .done(Guard::is_set("user:diet"))
2842 .step("book")
2843 .after("gather")
2844 .allow(["book_table"])
2845 .done(Guard::called_ok("book_table"))
2846 .build()
2847 .expect("flow is structurally valid")
2848 }
2849
2850 #[test]
2851 fn an_ambient_tool_survives_a_step_whitelist() {
2852 let mon = FlowMonitor::new(ambient_flow(), Enforcement::Enforce);
2853 let state = State::new();
2854 assert_eq!(
2855 mon.admits_tool("ask_diet", &state),
2856 Ok(()),
2857 "the step's own tool is admitted"
2858 );
2859 assert!(
2860 mon.admits_tool("book_table", &state).is_err(),
2861 "a later step's tool is still excluded by `gather`'s whitelist"
2862 );
2863 assert_eq!(
2864 mon.admits_tool("recall_context", &state),
2865 Ok(()),
2866 "an ambient tool must not be caught by a whitelist that never meant to exclude it"
2867 );
2868 }
2869
2870 #[test]
2871 fn an_ambient_tool_is_still_denied_when_a_step_names_it() {
2872 let flow = Flow::new()
2875 .ambient(["recall_context"])
2876 .step("sensitive")
2877 .deny(["recall_context"])
2878 .done(Guard::is_true("done"))
2879 .build()
2880 .expect("flow is structurally valid");
2881 let mon = FlowMonitor::new(flow, Enforcement::Enforce);
2882 assert!(
2883 mon.admits_tool("recall_context", &State::new()).is_err(),
2884 "an explicit `deny` outranks ambient"
2885 );
2886 }
2887
2888 #[test]
2889 fn an_ambient_tool_still_obeys_never_until() {
2890 let flow = Flow::new()
2891 .ambient(["manage_memory"])
2892 .step("verify")
2893 .done(Guard::is_true("verified"))
2894 .never("manage_memory")
2895 .until(Guard::is_true("verified"))
2896 .build()
2897 .expect("flow is structurally valid");
2898 let mon = FlowMonitor::new(flow, Enforcement::Enforce);
2899 let state = State::new();
2900 assert!(
2901 mon.admits_tool("manage_memory", &state).is_err(),
2902 "`never(..).until(..)` names the tool; ambient must not unlock it"
2903 );
2904 let _ = state.set("verified", true);
2905 assert_eq!(
2906 mon.admits_tool("manage_memory", &state),
2907 Ok(()),
2908 "once the guard holds the constraint stops binding"
2909 );
2910 }
2911
2912 #[test]
2913 fn an_ambient_tool_still_obeys_once() {
2914 let flow = Flow::new()
2915 .ambient(["summarize"])
2916 .step("work")
2917 .done(Guard::called_ok("summarize"))
2918 .once("summarize")
2919 .build()
2920 .expect("flow is structurally valid");
2921 let mut mon = FlowMonitor::new(flow, Enforcement::Enforce);
2922 let state = State::new();
2923 assert_eq!(mon.admits_tool("summarize", &state), Ok(()));
2924 mon.observe_tool("summarize", true, &state);
2925 assert!(
2926 mon.admits_tool("summarize", &state).is_err(),
2927 "`once` names the tool; ambient must not exempt it"
2928 );
2929 }
2930
2931 #[test]
2932 fn ambient_tools_join_the_tool_universe() {
2933 let flow = ambient_flow();
2936 assert!(
2937 flow.tool_universe().contains("recall_context"),
2938 "an ambient tool is part of the flow's tool universe"
2939 );
2940 assert!(
2941 ambient_flow()
2942 .compile_with_tools(&["ask_diet", "book_table"])
2943 .is_err(),
2944 "a registry missing the ambient tool must not compile clean"
2945 );
2946 assert!(
2947 ambient_flow()
2948 .compile_with_tools(&["ask_diet", "book_table", "recall_context"])
2949 .is_ok(),
2950 "a covering registry compiles"
2951 );
2952 }
2953
2954 #[test]
2955 fn a_flow_with_no_ambient_tools_is_unchanged() {
2956 let mon = FlowMonitor::new(debt_flow(), Enforcement::Enforce);
2959 let state = State::new();
2960 assert_eq!(mon.admits_tool("lookup_account", &state), Ok(()));
2961 assert!(mon.admits_tool("charge_card", &state).is_err());
2962 assert!(mon.admits_tool("anything_else", &state).is_err());
2963 }
2964
2965 #[test]
2966 fn explain_trace_reports_per_atom_truth() {
2967 let state = State::new();
2968 let _ = state.set("ptp_amount", 200);
2969 let marking = Marking::default();
2970 let ctx = FlowCtx {
2971 state: &state,
2972 marking: &marking,
2973 };
2974 let guard = Guard::all([
2975 Guard::captured(["ptp_amount", "ptp_date"]),
2976 Guard::is_true("confirmed"),
2977 ]);
2978 let trace = guard.explain_trace(&ctx);
2979 assert!(!trace.holds);
2980 assert_eq!(trace.desc, "all of");
2981 assert_eq!(trace.children.len(), 2);
2982 assert!(!trace.children[0].holds, "ptp_date missing");
2984 assert!(!trace.children[1].holds);
2985 assert!(serde_json::to_value(&trace).is_ok());
2987 }
2988
2989 #[test]
2990 fn explanation_carries_active_step_progress() {
2991 let mon = FlowMonitor::new(debt_flow(), Enforcement::Enforce);
2992 let state = State::new();
2993 let ex = mon.explain(&state);
2994 let verify = ex.active_progress.get("verify").expect("verify is active");
2995 assert!(!verify.holds);
2996 assert!(verify.desc.contains("identity_verified"));
2997 }
2998
2999 #[test]
3000 fn state_keys_read_covers_guards_and_constraints() {
3001 let keys = debt_flow().state_keys_read();
3002 for k in [
3003 "identity_verified",
3004 "disclosure_given",
3005 "ptp_amount",
3006 "ptp_date",
3007 ] {
3008 assert!(keys.contains(k), "missing read key {k}");
3009 }
3010 assert!(!keys.contains("charge_card"));
3012 }
3013
3014 #[test]
3015 fn posture_updates_take_effect_in_place() {
3016 let mut mon = FlowMonitor::new(debt_flow(), Enforcement::Enforce);
3017 let state = State::new();
3018 assert!(mon.set_posture("verify", Some("New posture.".into())));
3019 assert!(!mon.set_posture("no_such_step", None));
3020 assert_eq!(
3021 mon.active_postures(&state),
3022 vec!["New posture.".to_string()]
3023 );
3024 }
3025
3026 #[test]
3027 fn eval_state_treats_marking_atoms_as_false() {
3028 let state = State::new();
3029 let _ = state.set("ready", true);
3030 assert!(Guard::is_true("ready").eval_state(&state));
3031 assert!(!Guard::called_ok("any_tool").eval_state(&state));
3032 }
3033
3034 #[test]
3035 fn conditional_edges_branch_and_any_join_merges() {
3036 let flow = Flow::new()
3039 .step("decide")
3040 .done(Guard::is_set("severity"))
3041 .step("schedule")
3042 .after_when("decide", Guard::eq("severity", "routine"))
3043 .done(Guard::called_ok("book"))
3044 .allow(["book"])
3045 .step("escalate")
3046 .after_when("decide", Guard::eq("severity", "emergency"))
3047 .done(Guard::called_ok("transfer"))
3048 .allow(["transfer"])
3049 .step("close")
3050 .after("schedule")
3051 .after("escalate")
3052 .join_any()
3053 .terminal()
3054 .build()
3055 .expect("valid");
3056 let mut mon = FlowMonitor::new(flow, Enforcement::Enforce);
3057 let state = State::new();
3058 let _ = state.set("severity", "routine");
3059 mon.relatch(&state);
3060 let active: Vec<String> = mon.explain(&state).active;
3061 assert!(
3062 active.contains(&"schedule".to_string()),
3063 "routine branch opens"
3064 );
3065 assert!(
3066 !active.contains(&"escalate".to_string()),
3067 "emergency branch stays closed"
3068 );
3069 mon.on_tool_ok("book", &state);
3071 assert!(mon.marking().done.contains("close"), "any-join merged");
3072 }
3073
3074 #[test]
3075 fn reset_unlatches_on_rising_edge_and_forgives_called_ok() {
3076 let flow = Flow::new()
3077 .step("pay")
3078 .allow(["charge"])
3079 .done(Guard::called_ok("charge"))
3080 .once("charge")
3081 .reset(["pay"])
3082 .when(Guard::is_true("declined"))
3083 .build()
3084 .expect("valid");
3085 let mut mon = FlowMonitor::new(flow, Enforcement::Enforce);
3086 let state = State::new();
3087 mon.relatch(&state);
3088 mon.on_tool_ok("charge", &state);
3089 assert!(mon.marking().done.contains("pay"));
3090 assert!(mon.admits_tool("charge", &state).is_err(), "once spent");
3091
3092 let _ = state.set("declined", true);
3094 mon.relatch(&state);
3095 assert!(!mon.marking().done.contains("pay"), "un-latched");
3096 assert!(
3097 mon.admits_tool("charge", &state).is_ok(),
3098 "once counts per latch-cycle"
3099 );
3100
3101 mon.on_tool_ok("charge", &state);
3103 mon.relatch(&state);
3104 assert!(
3105 mon.marking().done.contains("pay"),
3106 "second charge latches again"
3107 );
3108 }
3109
3110 #[test]
3111 fn edge_serde_keeps_plain_strings_and_round_trips_conditions() {
3112 let flow = Flow::new()
3113 .step("a")
3114 .terminal()
3115 .step("b")
3116 .after("a")
3117 .after_when("a", Guard::is_true("x"))
3118 .join_any()
3119 .terminal()
3120 .build()
3121 .expect("valid");
3122 let json = serde_json::to_value(&flow).expect("serialize");
3123 assert_eq!(json["steps"][1]["after"][0], serde_json::json!("a"));
3125 assert_eq!(
3126 json["steps"][1]["after"][1],
3127 serde_json::json!({"step": "a", "when": {"is_true": "x"}})
3128 );
3129 assert_eq!(json["steps"][1]["join"], serde_json::json!("any"));
3130 let back: Flow = serde_json::from_value(json).expect("deserialize");
3131 assert_eq!(back.steps[1].after.len(), 2);
3132 assert!(back.steps[1].after[1].when.is_some());
3133 }
3134
3135 #[test]
3136 fn flow_json_schema_generates() {
3137 let schema = serde_json::to_value(schemars::schema_for!(Flow)).expect("schema");
3138 let text = schema.to_string();
3139 assert!(text.contains("is_true"));
3141 assert!(text.contains("never_until"));
3142 }
3143}