Skip to main content

gemini_adk_rs/flow/
mod.rs

1//! `Flow` — a governed conversation/tool DAG.
2//!
3//! A [`Flow`] is a directed acyclic graph of [`Step`]s. A `Step` is the *only*
4//! node type; it unifies "conversation stage" and "tool-call milestone" by
5//! differing only in attributes, not in kind. A step is *done* when its
6//! completion [`Guard`] latches true; edges (`after`) are dependencies. The
7//! [`FlowMonitor`] maintains a [`Marking`] (the set of done steps) by observing
8//! the session trace, **projects** active steps' postures into turn-boundary
9//! steering, and **enforces** ordering by admitting/denying tool calls.
10//!
11//! The vocabulary is deliberately closed: the only
12//! nouns are `Flow`, `Step`, `Guard`, `Posture`, `Marking`, `Verdict`. Words
13//! like *phase*, *transition*, *watch*, *needs* are lowering details and never
14//! appear here.
15//!
16//! Because every [`Guard`] atom is a named, parameterized predicate, a `Flow`
17//! is fully serializable — enabling data-driven scripts edited without a
18//! recompile. The `custom` closure escape hatch is available in code but is not
19//! serializable.
20
21use 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
47/// Evaluation context handed to a [`Guard`]: the session state plus the
48/// current flow marking.
49pub struct FlowCtx<'a> {
50    /// The session state.
51    pub state: &'a State,
52    /// The current flow marking (done steps + tool-call counts).
53    pub marking: &'a Marking,
54}
55
56/// A serializable predicate atom — the closed set of guard primitives.
57#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, schemars::JsonSchema)]
58#[serde(rename_all = "snake_case")]
59pub enum Pred {
60    /// Always true.
61    Always,
62    /// State key holds boolean `true`.
63    IsTrue(String),
64    /// State key is present.
65    IsSet(String),
66    /// State key equals the given JSON value.
67    Eq(String, Value),
68    /// All of the given state keys are present (e.g. extracted slots).
69    Captured(Vec<String>),
70    /// The named tool has completed successfully at least once.
71    CalledOk(String),
72    /// The named step is done.
73    Done(String),
74    /// A decision model's answer to a declared question, about the caller's
75    /// current turn (see [`decided`]).
76    Decided(Decided),
77    /// Conjunction.
78    All(Vec<Pred>),
79    /// Disjunction.
80    Any(Vec<Pred>),
81    /// Negation.
82    Not(Box<Pred>),
83}
84
85/// Three-valued conjunction: false if any is false, unknown if any is unknown.
86fn 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
98/// Three-valued disjunction: true if any is true, unknown if any is unknown.
99fn 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    /// [`eval`](Self::eval) with every `decided` atom unknown. See
113    /// [`Guard::possible`].
114    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    /// This predicate as a short prose clause. See [`Guard::describe`].
141    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    /// Step ids referenced by `Done(..)` atoms (for validation).
163    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    /// Tool names referenced by `called_ok` atoms (for reset forgiveness).
173    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    /// Questions referenced by `decided` atoms.
183    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    /// State keys this predicate reads (`is_true`/`is_set`/`eq`/`captured`
195    /// atoms, recursively). `called_ok`/`done` reference tools/steps, not
196    /// state, and are excluded.
197    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    /// Evaluate to a [`GuardTrace`]: the predicate tree with each node's prose
212    /// description and its truth value under `ctx`. The atom-granular answer
213    /// to "why is this step not done?".
214    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                // Composite nodes describe only the connective; the operands
223                // carry their own descriptions as children.
224                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/// A [`Guard`] evaluated to a truth tree: each predicate node with its prose
236/// description and whether it currently holds. Serializable, so a devtool can
237/// render exactly which atom a stuck step is waiting on.
238#[derive(Clone, Debug, Serialize, schemars::JsonSchema)]
239pub struct GuardTrace {
240    /// Prose description of this node (from [`Guard::describe`] vocabulary).
241    pub desc: String,
242    /// Whether this node currently holds.
243    pub holds: bool,
244    /// Operand traces for `all`/`any`/`not`; empty for atoms.
245    #[serde(default, skip_serializing_if = "Vec::is_empty")]
246    pub children: Vec<GuardTrace>,
247}
248
249/// Render a grounding template against `state`.
250///
251/// - `{key}` interpolates the value at `key` (strings bare, other JSON compact);
252///   an absent key renders empty.
253/// - `{key?yes:no}` renders `yes` when `key` is *truthy* (present and not
254///   `false`/`null`/`0`/`""`), else `no`.
255///
256/// This is the realization of `Effect::ground`: a deterministic projection of
257/// known `State` into a steering line, so the model restates facts rather than
258/// inventing them.
259pub 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            // Unbalanced brace: emit the remainder verbatim.
267            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/// A boolean predicate over `(state, marking)` — the *only* predicate type.
308///
309/// Use the constructors ([`Guard::is_true`], [`Guard::captured`],
310/// [`Guard::called_ok`], …) for the serializable closed atoms, or
311/// [`Guard::custom`] for a bespoke closure (not serializable).
312#[derive(Clone)]
313pub enum Guard {
314    /// A serializable predicate built from the closed atom set.
315    Spec(Pred),
316    /// A code-only escape hatch. Not serializable.
317    Custom(CustomFn),
318}
319
320impl Guard {
321    /// Always true.
322    pub fn always() -> Self {
323        Guard::Spec(Pred::Always)
324    }
325    /// State key holds boolean `true`.
326    pub fn is_true(key: impl Into<String>) -> Self {
327        Guard::Spec(Pred::IsTrue(key.into()))
328    }
329    /// State key is present.
330    pub fn is_set(key: impl Into<String>) -> Self {
331        Guard::Spec(Pred::IsSet(key.into()))
332    }
333    /// State key equals the given JSON value.
334    pub fn eq(key: impl Into<String>, value: impl Into<Value>) -> Self {
335        Guard::Spec(Pred::Eq(key.into(), value.into()))
336    }
337    /// All of the given state keys are present (extracted slots).
338    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    /// The named tool has completed successfully.
346    pub fn called_ok(tool: impl Into<String>) -> Self {
347        Guard::Spec(Pred::CalledOk(tool.into()))
348    }
349    /// The named step is done.
350    pub fn done(step: impl Into<String>) -> Self {
351        Guard::Spec(Pred::Done(step.into()))
352    }
353    /// The caller's current turn settles the declared question `question`: a
354    /// boolean answered yes, a choice that picked an option, or a score. See
355    /// [`decided`].
356    pub fn decided(question: impl Into<String>) -> Self {
357        Self::decided_as(question, Expect::Yes)
358    }
359    /// The caller's current turn answers the boolean `question` no (or a
360    /// choice picks "none of these").
361    pub fn decided_no(question: impl Into<String>) -> Self {
362        Self::decided_as(question, Expect::No)
363    }
364    /// The caller's current turn picks `option` for the choice `question`.
365    pub fn decided_is(question: impl Into<String>, option: impl Into<String>) -> Self {
366        Self::decided_as(question, Expect::Is(option.into()))
367    }
368    /// The score `question` is at least `bound`.
369    pub fn decided_at_least(question: impl Into<String>, bound: f64) -> Self {
370        Self::decided_as(question, Expect::AtLeast(bound))
371    }
372    /// The score `question` is at most `bound`.
373    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    /// True once an orchestrated agent named `name` has produced a result
383    /// (its `{name}:result` state key is set). Pairs with the
384    /// [`orchestration`](crate::orchestration) `call`/`dispatch`/`background`.
385    pub fn resolved(name: impl AsRef<str>) -> Self {
386        Guard::Spec(Pred::IsSet(format!("{}:result", name.as_ref())))
387    }
388    /// Conjunction.
389    ///
390    /// If every input is a serializable atom, the result is a serializable
391    /// `Pred::All`. If any input is a [`Guard::custom`], the result is itself a
392    /// custom guard that evaluates the conjunction at runtime — the custom guard
393    /// is **never silently dropped** (it merely makes the combinator
394    /// non-serializable, which surfaces as an error only if you try to serialize
395    /// the flow).
396    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    /// Disjunction.
405    ///
406    /// Mirrors [`Guard::all`]: custom inputs are preserved as a runtime closure
407    /// rather than erased.
408    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    /// Negation of a serializable atom.
417    #[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            // Negating a custom guard yields a custom guard.
422            Guard::Custom(f) => Guard::Custom(Arc::new(move |ctx| !f(ctx))),
423        }
424    }
425    /// This guard as a short prose clause, for telling a model what a refusal
426    /// is waiting on.
427    ///
428    /// A refusal that names only the tool leaves the model to guess the
429    /// precondition, and a wrong guess is indistinguishable from a wrong model:
430    /// in the governed collections evaluation, `record_promise_to_pay` was
431    /// refused with "not available in the current step", and the model — with
432    /// nothing else to go on — decided it must need to re-verify the caller and
433    /// asked for the card digits it had already checked.
434    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    /// A bespoke closure over `(state, marking)`. Not serializable.
442    pub fn custom(f: impl Fn(&FlowCtx) -> bool + Send + Sync + 'static) -> Self {
443        Guard::Custom(Arc::new(f))
444    }
445
446    /// Evaluate the guard as the caller's next words could leave it: every
447    /// `decided` atom unknown, since its question is asked again about each
448    /// turn. `Some(value)` when no decision can change the outcome, `None`
449    /// when one could make the guard hold or fail. A custom guard is
450    /// evaluated as it stands.
451    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    /// Evaluate the guard.
459    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    /// Evaluate against state alone, with an empty [`Marking`].
467    ///
468    /// For contexts outside a governed flow (phase transitions, watcher
469    /// conditions) where no marking exists: `called_ok`/`done` atoms evaluate
470    /// `false` there — a validator should reject them in such positions.
471    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    /// Evaluate to a [`GuardTrace`] — the predicate tree with per-node truth
480    /// values. A [`Guard::custom`] yields a single opaque node.
481    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    /// The questions this guard's `decided` atoms name.
505    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    /// The state keys this guard reads (`is_true`/`is_set`/`eq`/`captured`
514    /// atoms). A custom guard reads nothing that can be named, so it
515    /// reports none.
516    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
529/// Unwrap a list of guards known to be all `Spec` into their predicates.
530///
531/// The caller (`Guard::all`/`Guard::any`) only invokes this after verifying every
532/// guard is a `Spec`, so the `Custom` arm is unreachable.
533fn 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
560// A serializable guard is exactly a `Pred` atom on the wire (the `Custom`
561// variant is code-only and rejected by `Serialize`), so its JSON Schema is
562// `Pred`'s. Inline the `Pred` subschema wherever a `Guard` appears.
563impl 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/// A dependency edge into a step.
585///
586/// In JSON, a plain string (`"verify"`) is an unconditional edge — satisfied
587/// once the source step is done. An object (`{"step": "triage", "when":
588/// {"is_true": "routine"}}`) is a **conditional edge**: satisfied only while
589/// the source is done *and* the guard holds. Conditional edges out of one
590/// source into sibling steps are how a flow branches; pair them with
591/// [`Join::Any`] on the merge step. Unconditional edges serialize back to the
592/// plain string form, so existing documents round-trip unchanged.
593#[derive(Clone, Debug)]
594pub struct Edge {
595    /// The source step this edge depends on.
596    pub step: String,
597    /// Optional condition; the edge is satisfied only while it holds.
598    pub when: Option<Guard>,
599}
600
601impl Edge {
602    /// An unconditional edge from `step`.
603    pub fn to(step: impl Into<String>) -> Self {
604        Edge {
605            step: step.into(),
606            when: None,
607        }
608    }
609    /// A conditional edge from `step`, satisfied only while `when` holds.
610    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            // Unconditional edges keep the original plain-string form.
628            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/// How a step's dependency edges combine.
676#[derive(
677    Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema,
678)]
679#[serde(rename_all = "snake_case")]
680pub enum Join {
681    /// Every edge must be satisfied (the default — a synchronizing join).
682    #[default]
683    All,
684    /// Any one satisfied edge suffices — the merge node after a branch.
685    Any,
686}
687
688impl Join {
689    fn is_all(&self) -> bool {
690        *self == Join::All
691    }
692}
693
694/// A node in the flow DAG — the only node type.
695#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)]
696pub struct Step {
697    /// Unique step id.
698    pub id: String,
699    /// Dependency edges; how they combine is set by `join`.
700    #[serde(default, skip_serializing_if = "Vec::is_empty")]
701    pub after: Vec<Edge>,
702    /// How the `after` edges combine: `all` (default) or `any`.
703    #[serde(default, skip_serializing_if = "Join::is_all")]
704    pub join: Join,
705    /// Extra eligibility predicate beyond dependencies.
706    #[serde(default, skip_serializing_if = "Option::is_none")]
707    pub gate: Option<Guard>,
708    /// Completion condition. Required for non-terminal steps.
709    #[serde(default, skip_serializing_if = "Option::is_none")]
710    pub done: Option<Guard>,
711    /// Instruction imposed while this step is active (projected as steering).
712    #[serde(default, skip_serializing_if = "Option::is_none")]
713    pub posture: Option<String>,
714    /// A grounding template projected while active: a curated, `State`-interpolated
715    /// fact line that pins the model to known values (anti-hallucination). See
716    /// [`render_ground`]. Serializable, like `posture`.
717    #[serde(default, skip_serializing_if = "Option::is_none")]
718    pub ground: Option<String>,
719    /// Tools available while this step is active (whitelist; empty = no restriction).
720    #[serde(default, skip_serializing_if = "Vec::is_empty")]
721    pub allow: Vec<String>,
722    /// Tools forbidden while this step is active.
723    #[serde(default, skip_serializing_if = "Vec::is_empty")]
724    pub deny: Vec<String>,
725    /// A terminal step — reaching it (deps + gate) marks it done with no milestone.
726    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
727    pub terminal: bool,
728}
729
730/// A cross-cutting flow constraint.
731#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)]
732#[serde(rename_all = "snake_case")]
733pub enum Constraint {
734    /// A tool may complete at most once.
735    Once(String),
736    /// Step `0` must be done before step `1` starts.
737    Before(String, String),
738    /// A tool is forbidden until the guard holds.
739    NeverUntil {
740        /// The gated tool.
741        tool: String,
742        /// The guard that must hold to permit it.
743        until: Guard,
744    },
745    /// These steps must be done for the flow to be complete.
746    Require(Vec<String>),
747    /// Un-latch the named steps when the guard *becomes* true (rising edge) —
748    /// the loop primitive. The DAG stays acyclic and statically checkable;
749    /// iteration is explicit marking surgery instead of back-edges.
750    ///
751    /// A reset also forgives the completion evidence the steps' `done` guards
752    /// reference: `called_ok` counts for those tools are cleared (so a `once`
753    /// on such a tool permits one run per latch-cycle). State keys the guards
754    /// read are the application's to clear — a reset never touches [`State`].
755    Reset {
756        /// The steps to un-latch.
757        steps: Vec<String>,
758        /// Fires on this guard's rising edge.
759        when: Guard,
760    },
761}
762
763/// A governed conversation/tool DAG.
764#[derive(Clone, Debug, Default, Serialize, Deserialize, schemars::JsonSchema)]
765pub struct Flow {
766    /// The steps (DAG nodes).
767    pub steps: Vec<Step>,
768    /// Cross-cutting constraints.
769    #[serde(default, skip_serializing_if = "Vec::is_empty")]
770    pub constraints: Vec<Constraint>,
771    /// Tools that require confirmation when reached (set by `commit`).
772    #[serde(default, skip_serializing_if = "Vec::is_empty")]
773    pub confirm_tools: Vec<String>,
774    /// Cross-cutting tools exempt from step `allow` whitelists.
775    ///
776    /// A step's `allow` list excludes by omission, which is correct for the
777    /// domain tools a step is *about* and wrong for infrastructure no step is
778    /// about — memory recall, escalation, logging. Naming a tool here says "this
779    /// is not part of any step's repertoire", not "this is ungovernable":
780    /// `deny`, `once` and `never(..).until(..)` all still bind, because each of
781    /// those *names* the tool and so is a decision about it.
782    #[serde(default, skip_serializing_if = "Vec::is_empty")]
783    pub ambient: Vec<String>,
784}
785
786impl Flow {
787    /// Every question a `decided` atom names in this flow: step gates and
788    /// `done` guards, edge guards, and constraints.
789    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    /// Start building a flow.
810    #[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    /// Validate referential integrity and acyclicity. Every problem found is
823    /// reported in the returned [`ConfigError`], not just the first.
824    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    /// Every tool name referenced anywhere in the flow (allow/deny/once/
905    /// never_until/confirm). The universe over which [`ToolSurface`] reasons.
906    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    /// Every state key any guard in the flow reads (`is_true`/`is_set`/`eq`/
929    /// `captured` atoms in step gates, completion guards, and `never…until`
930    /// constraints).
931    ///
932    /// A key in this set that nothing in the session *writes* — no tool, no
933    /// extractor, no promotion — can never latch, which is the dominant silent
934    /// failure in data-authored flows. Validators diff this set against the
935    /// declared writers to catch it at load time, the way
936    /// [`compile_with_tools`](Self::compile_with_tools) catches tool typos.
937    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    /// Steps reachable from a root (a step with no `after` deps), following both
960    /// `after` edges and `Before(a, b)` ordering edges.
961    fn reachable_steps(&self) -> BTreeSet<String> {
962        let ids: BTreeSet<&str> = self.steps.iter().map(|s| s.id.as_str()).collect();
963        // Forward edges: a -> b when b.after contains a, or Before(a, b).
964        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    /// Compile and validate the flow into a [`CompiledFlow`], turning a class of
999    /// runtime surprises into load-time errors.
1000    ///
1001    /// On top of [`validate`](Self::validate)'s referential/acyclicity checks this
1002    /// reports: unreachable steps, commit tools guarded by an always-true
1003    /// condition (an effectively *unguarded* commit, which defeats the
1004    /// confirm-before-commit contract), `never…until` guards whose `done(step)`
1005    /// atoms reference unknown steps (unsatisfiable — the tool would be forbidden
1006    /// forever), and ordering cycles across the combined `after` + `before` edges
1007    /// (which deadlock every step on the cycle). Precomputes the [`ToolSurface`]
1008    /// universe.
1009    ///
1010    /// To additionally validate tool names against a known registry, use
1011    /// [`compile_with_tools`](Self::compile_with_tools).
1012    pub fn compile(self) -> Result<CompiledFlow, FlowErrors> {
1013        self.compile_internal(None)
1014    }
1015
1016    /// Compile like [`compile`](Self::compile), additionally validating every
1017    /// tool name the flow references (step `allow`/`deny`, `once`,
1018    /// `never…until`, and commit/confirm tools) against the given registry of
1019    /// known tool names.
1020    ///
1021    /// A referenced tool missing from `tools` is reported as
1022    /// [`FlowError::UnknownTool`] — catching typos and drift between a flow
1023    /// script and the tools actually registered on the session.
1024    ///
1025    /// ```ignore
1026    /// let compiled = flow.compile_with_tools(&["lookup_account", "charge_card"])?;
1027    /// ```
1028    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        // Graph-shape checks (only meaningful once the graph is acyclic/valid).
1039        if errors.is_empty() {
1040            // Unreachable steps.
1041            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            // Ordering cycles across the combined `after` + `before(a, b)` edges.
1048            // `validate()` only walks `after`; a cycle closed by a `Before`
1049            // constraint deadlocks every step on it (none can become eligible).
1050            if let Some(cycle) = self.ordering_cycle() {
1051                errors.push(FlowError::OrderingCycle(cycle));
1052            }
1053        }
1054
1055        // A commit tool guarded by an always-true condition is effectively
1056        // unguarded — the confirm-before-commit contract would never gate it.
1057        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        // An unsatisfiable `never(tool).until(guard)`: the guard's `done(step)`
1069        // atom references a step that doesn't exist, so it can never latch and
1070        // the tool is forbidden forever. (Step gate/done guards are already
1071        // covered by `validate()`; constraints were not.)
1072        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        // Dangling tool names vs a known registry (opt-in).
1089        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    /// Find a cycle over the combined dependency edges (`after` plus
1111    /// `before(a, b)` ordering constraints), if any. Returns the step ids on
1112    /// the cycle path. `None` when the combined graph is acyclic.
1113    fn ordering_cycle(&self) -> Option<Vec<String>> {
1114        // Predecessor edges: step -> everything that must be done before it.
1115        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        // DFS with colors; on a back-edge, report the current path suffix.
1126        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        // DFS with colors over the `after` dependency edges.
1171        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    /// Render the flow as a Mermaid `flowchart` — the spec *is* the diagram.
1199    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/// The runtime position in a flow: which steps are done and how often each
1225/// tool has succeeded.
1226#[derive(Clone, Debug, Default)]
1227pub struct Marking {
1228    /// Steps that have latched done.
1229    pub done: BTreeSet<String>,
1230    /// Per-tool successful-completion counts.
1231    pub tool_ok: BTreeMap<String, u32>,
1232    /// Turns observed.
1233    pub turns: u32,
1234}
1235
1236/// The conformance status of a step.
1237#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1238#[serde(rename_all = "snake_case")]
1239pub enum Verdict {
1240    /// Not yet eligible.
1241    Pending,
1242    /// Eligible and awaiting completion.
1243    Active,
1244    /// Completed.
1245    Done,
1246    /// A successor completed while this never did (an out-of-order deviation).
1247    Skipped,
1248}
1249
1250/// A recorded conformance deviation (observe mode) or denial (enforce mode).
1251#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
1252pub struct Violation {
1253    /// What was attempted (e.g. a tool name).
1254    pub subject: String,
1255    /// Why it was a violation.
1256    pub reason: String,
1257}
1258
1259/// How a [`FlowMonitor`] treats off-path activity — enforcement vs observation.
1260///
1261/// Distinct from [`AgentMode`], which is the unrelated *resolver execution
1262/// discipline* (`Call`/`Dispatch`/`Background`).
1263#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
1264pub enum Enforcement {
1265    /// Block inadmissible tool calls and steer back on-path.
1266    #[default]
1267    Enforce,
1268    /// Allow everything, but record deviations for audit/analytics.
1269    Observe,
1270}
1271
1272/// An action fired the first time a step becomes active: run an agent in an
1273/// [`AgentMode`]. Built with [`on_enter`]. The result lands in `{name}:result` (the
1274/// name defaults to the step id), so a *downstream* step can complete on it via
1275/// [`Guard::resolved`] — this is how a flow drives orchestration in-session.
1276#[derive(Clone)]
1277pub struct StepAction {
1278    name: Option<String>,
1279    agent: Arc<dyn TextAgent>,
1280    mode: AgentMode,
1281}
1282
1283/// Build a step-enter action that runs `agent` in `mode` when the step first
1284/// activates. Pair with [`FlowMonitor::on_enter`].
1285///
1286/// ```ignore
1287/// let mon = FlowMonitor::new(flow, Enforcement::Enforce)
1288///     .on_enter("check", on_enter(availability_agent, AgentMode::Dispatch));
1289/// ```
1290pub 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    /// Override the result name (defaults to the step id it is attached to).
1300    pub fn named(mut self, name: impl Into<String>) -> Self {
1301        self.name = Some(name.into());
1302        self
1303    }
1304
1305    /// Run the action. `Call` awaits inline; `Dispatch`/`Background` spawn it
1306    /// detached so the turn is never blocked.
1307    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
1324/// A shared, lock-protected [`FlowMonitor`] — the form in which the Live
1325/// control plane owns a governed flow, so runtime surfaces (e.g.
1326/// [`LiveHandle::explain`](crate::live::LiveHandle::explain)) can
1327/// snapshot it concurrently. All monitor methods are synchronous: lock
1328/// briefly and never hold the guard across an `await`.
1329pub type SharedFlowMonitor = Arc<parking_lot::Mutex<FlowMonitor>>;
1330
1331/// Observes the session trace, maintains the [`Marking`], answers tool
1332/// admissibility, and projects active postures.
1333pub struct FlowMonitor {
1334    flow: Flow,
1335    mode: Enforcement,
1336    marking: Marking,
1337    violations: Vec<Violation>,
1338    /// Per-step actions fired the first time the step becomes active.
1339    enter_actions: HashMap<String, StepAction>,
1340    /// Steps whose `on_enter` action has already fired (fire-once).
1341    announced: BTreeSet<String>,
1342    /// Previous guard values for `Constraint::Reset` rising-edge detection,
1343    /// in constraint order.
1344    reset_prev: Vec<bool>,
1345}
1346
1347impl FlowMonitor {
1348    /// Create a monitor for a (presumed-valid) flow.
1349    ///
1350    /// Prefer [`FlowMonitor::compiled`] or [`FlowMonitor::try_new`], which carry
1351    /// proof of compilation; this convenience skips compilation for flows already
1352    /// known valid (e.g. built in-process by trusted code).
1353    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    /// Create a monitor from a [`CompiledFlow`] — the validated path.
1366    pub fn compiled(flow: CompiledFlow, mode: Enforcement) -> Self {
1367        Self::new(flow.into_flow(), mode)
1368    }
1369
1370    /// Compile `flow` and create a monitor, surfacing structural errors instead
1371    /// of trusting the caller.
1372    pub fn try_new(flow: Flow, mode: Enforcement) -> Result<Self, FlowErrors> {
1373        Ok(Self::compiled(flow.compile()?, mode))
1374    }
1375
1376    /// Wrap this monitor in a [`SharedFlowMonitor`] for shared ownership
1377    /// between the control lane (which advances it) and runtime accessors
1378    /// (which snapshot it, e.g.
1379    /// [`LiveHandle::explain`](crate::live::LiveHandle::explain)).
1380    pub fn into_shared(self) -> SharedFlowMonitor {
1381        Arc::new(parking_lot::Mutex::new(self))
1382    }
1383
1384    /// Wrap this monitor as the main layer of a [`FlowStack`] with no
1385    /// digressions — the form the Live control plane drives.
1386    pub fn into_stack(self) -> FlowStack {
1387        FlowStack::from_monitor(self)
1388    }
1389
1390    /// Re-enter the flow from its start: forget the marking, the fired
1391    /// `on_enter` actions and the reset edges. The flow, mode and registered
1392    /// actions are kept, and recorded violations stay for audit. `State` is not
1393    /// touched — the next re-latch runs against whatever facts it holds.
1394    pub fn restart(&mut self) {
1395        self.marking = Marking::default();
1396        self.announced.clear();
1397        self.reset_prev.clear();
1398    }
1399
1400    /// Explain the current control-plane state: active steps, which tools are
1401    /// admitted vs blocked (with reasons), and unmet requirements.
1402    ///
1403    /// This is the deterministic answer to "why did the assistant ask that?" —
1404    /// model-readable, without the model driving control flow.
1405    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    /// Attach an action fired the first time `step` becomes active (see
1437    /// [`on_enter`](on_enter())). Chainable at construction time.
1438    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    /// Steps that became active since the last call — each reported exactly once
1444    /// over the session. Drives [`on_enter`](Self::on_enter) firing.
1445    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    /// The enter-action registered for a step, if any.
1459    pub fn enter_action(&self, step: &str) -> Option<&StepAction> {
1460        self.enter_actions.get(step)
1461    }
1462
1463    /// Fire enter-actions for every step that just became active. Convenience
1464    /// over [`take_newly_active`](Self::take_newly_active) + [`enter_action`](Self::enter_action);
1465    /// call it right after [`on_turn`](Self::on_turn).
1466    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    /// The enforcement mode this monitor runs in.
1475    pub fn mode(&self) -> Enforcement {
1476        self.mode
1477    }
1478
1479    /// Replace a step's posture in place. Returns `false` if no such step.
1480    ///
1481    /// Postures are re-projected at every turn boundary, so an edit takes
1482    /// effect on the next turn — the safe subset of live spec editing. The
1483    /// DAG, guards, and tool gates are structural and stay fixed.
1484    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    /// Replace a step's grounding template in place. Returns `false` if no
1495    /// such step. Same next-turn semantics as [`set_posture`](Self::set_posture).
1496    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    /// Evaluate a [`Guard`] against this monitor's current context (the given
1507    /// `state` plus the monitor's marking). Used to test overlay/digression
1508    /// triggers without exposing the internal context.
1509    pub fn eval(&self, guard: &Guard, state: &State) -> bool {
1510        guard.eval(&self.ctx(state))
1511    }
1512    /// The current marking.
1513    pub fn marking(&self) -> &Marking {
1514        &self.marking
1515    }
1516    /// Recorded violations.
1517    pub fn violations(&self) -> &[Violation] {
1518        &self.violations
1519    }
1520
1521    /// Record a deviation the *caller* detected — one this monitor cannot see
1522    /// for itself. A [`FlowStack`] uses it for a tool called after a
1523    /// `Resume::Terminate` digression ended the conversation: the denial is the
1524    /// stack's, not this flow's, so `observe_tool` would find nothing wrong.
1525    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    /// The underlying flow.
1532    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        // Enforce `Constraint::Before(a, step)`: `a` must be done before this
1558        // step may start (an ordering constraint declared outside `after`).
1559        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    /// Re-evaluate completion latches to a fixpoint. Call after any event that
1572    /// can change state or the marking (turn boundary, tool completion).
1573    ///
1574    /// [`Constraint::Reset`] constraints are applied first, on their guard's
1575    /// rising edge: the named steps un-latch, their `on_enter` re-arms, and
1576    /// the `called_ok` evidence their completion guards reference is forgiven
1577    /// — the loop primitive over an otherwise monotonic marking.
1578    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    /// Apply [`Constraint::Reset`] constraints on their guards' rising edges.
1611    /// Returns the steps that were un-latched.
1612    fn apply_resets(&mut self, state: &State) -> Vec<String> {
1613        let mut reset: Vec<String> = Vec::new();
1614        // Evaluate all guards first (immutable borrow), then mutate.
1615        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                // Forgive the completion evidence this step's done guard
1644                // references, so `called_ok` (and any `once` on those tools)
1645                // count per latch-cycle. State keys stay the app's to clear.
1646                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    /// Record a turn boundary, then re-latch.
1661    pub fn on_turn(&mut self, state: &State) {
1662        self.begin_turn(state);
1663        self.relatch(state);
1664    }
1665
1666    /// The first half of [`on_turn`](Self::on_turn): count the turn and apply
1667    /// [`Constraint::Reset`] edges, returning the steps that were un-latched.
1668    ///
1669    /// A caller that keeps evidence *about* steps outside the marking — the
1670    /// [`FlowStack`] and its repair signals — needs to see a reset before the
1671    /// re-latch runs, or a completion guard that references that evidence
1672    /// re-completes the step on the spot. Follow with
1673    /// [`relatch`](Self::relatch); the reset edges are consumed, so the
1674    /// re-latch does not apply them twice.
1675    pub fn begin_turn(&mut self, state: &State) -> Vec<String> {
1676        self.marking.turns += 1;
1677        self.apply_resets(state)
1678    }
1679
1680    /// Record a successful tool call, then re-latch.
1681    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    /// The first half of [`on_tool_ok`](Self::on_tool_ok): count the call and
1687    /// apply [`Constraint::Reset`] edges, returning the steps that were
1688    /// un-latched.
1689    ///
1690    /// A reset can be gated on a tool — `reset(..).when(called_ok("start_over"))`
1691    /// — in which case its edge fires here rather than at a turn boundary. A
1692    /// caller holding evidence outside the marking needs the same chance to shed
1693    /// it that [`begin_turn`](Self::begin_turn) gives; follow with
1694    /// [`relatch`](Self::relatch).
1695    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    /// The state keys the flow could act on now: those read by the active
1701    /// steps' gates and `done` guards, by the edges leaving them, and by the
1702    /// constraints. A decision that `writes` one of these is worth asking.
1703    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    /// Steps that are eligible but not yet done.
1733    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    /// Postures of the active steps — to inject as turn-boundary steering.
1742    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    /// Rendered grounding lines of the active steps — curated, `State`-
1750    /// interpolated facts to inject as turn-boundary steering (anti-hallucination).
1751    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    /// Terminal steps that are done — the flow's closing.
1760    ///
1761    /// A terminal step completes on eligibility, so it is never *active* and
1762    /// its posture is never among [`active_postures`](Self::active_postures).
1763    /// Its instruction is the flow's last word ("hand off to a human now"),
1764    /// which the [`FlowStack`] projects on the turn a digression completes.
1765    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    /// Postures of the [`closing_steps`](Self::closing_steps).
1774    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    /// Rendered grounding lines of the [`closing_steps`](Self::closing_steps).
1782    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    /// Required steps not yet done (drives repair).
1791    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    /// Whether all required steps are done.
1804    pub fn is_complete(&self) -> bool {
1805        self.unmet_requirements().is_empty()
1806    }
1807
1808    /// The conformance verdict for a step.
1809    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        // Skipped: a successor is done but this step never completed.
1819        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    /// Decide whether a tool call may proceed. `Ok(())` admits it; `Err(reason)`
1830    /// denies it (the caller blocks in Enforce mode, or records in Observe).
1831    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    /// Whether `tool` would be admitted once every `never(tool).until(..)`
1839    /// guard on it holds: the active steps allow it and it has not used up a
1840    /// `once`, or a [frontier](Self::frontier_steps) step allows it. Under
1841    /// `ContextUpdate` steering the model is offered these tools, so that it
1842    /// can call one in the turn its guard comes to hold or its step opens;
1843    /// the call itself is still decided by [`admits_tool`](Self::admits_tool).
1844    pub fn offers_tool(&self, tool: &str, state: &State) -> bool {
1845        match self.admissibility_with(tool, state, false) {
1846            Ok(()) => true,
1847            // An active step's `deny` and a spent `once` bind any step.
1848            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    /// Evaluate `guard` as the caller's next words could leave it, against
1857    /// this flow's marking. See [`Guard::possible`].
1858    pub fn possible(&self, guard: &Guard, state: &State) -> Option<bool> {
1859        guard.possible(&self.ctx(state))
1860    }
1861
1862    /// The steps one decision away: neither done nor active, but able to
1863    /// become active in the caller's next turn because what stands in their
1864    /// way is a `decided` atom, in their gate, an incoming edge, or the
1865    /// `done` guard of the active step they follow.
1866    ///
1867    /// A decision made after the caller's turn ends cannot change the tools
1868    /// the model has for that turn: measured on Gemini 3.8 Live, a tool
1869    /// declared even 0 ms after the turn ended was not used in that turn's
1870    /// reply (0 of 15), while one declared before it was (5 of 5). So under
1871    /// `ContextUpdate` steering these steps' tools are offered ahead, and a
1872    /// call to one is decided at the gate: the questions in its way
1873    /// ([`frontier_questions`](Self::frontier_questions)) are asked about the
1874    /// turn in progress, the flow relatches, and the call is admitted only if
1875    /// the step opened.
1876    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                // An active step completes when its `done` guard holds.
1889                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                // Not eligible now, so only an unknown can open it.
1924                all_of([deps, could(step.gate.as_ref())]).is_none()
1925            })
1926            .collect()
1927    }
1928
1929    /// The questions that decide whether a [frontier](Self::frontier_steps)
1930    /// step allowing `tool` opens, with the state keys their guards read:
1931    /// the step's gate, its incoming edges, and the `done` guards of the
1932    /// active steps it follows. Empty when no frontier step allows `tool`.
1933    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    /// The state keys read by the `never(tool).until(..)` guards that refuse
1962    /// `tool` right now. Empty when no such guard is refusing it.
1963    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    /// The admissibility decision, before it is put into words.
1979    ///
1980    /// Split from [`admits_tool`](Self::admits_tool) so that rendering a refusal
1981    /// can itself ask what *is* admissible without recursing: this function never
1982    /// renders, so `render_denial` may call it over the whole tool universe.
1983    fn admissibility(&self, tool: &str, state: &State) -> Result<(), Denial> {
1984        self.admissibility_with(tool, state, true)
1985    }
1986
1987    /// [`admissibility`](Self::admissibility), optionally ignoring
1988    /// `never(tool).until(..)` guards.
1989    fn admissibility_with(&self, tool: &str, state: &State, guards: bool) -> Result<(), Denial> {
1990        // 1. once(tool)
1991        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        // 2. never(tool).until(guard)
2000        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        // 3. active allow/deny (whitelist while any active step restricts).
2010        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        // An ambient tool is exempt from the whitelist's exclusion-by-omission,
2015        // having already cleared every constraint that names it explicitly.
2016        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    /// Put a refusal into words the model can act on.
2033    ///
2034    /// A refusal reaches the model as a tool error, and it is the only thing the
2035    /// model learns about the gate. "'X' is not available in the current step"
2036    /// names what failed and nothing about what would succeed, so the model is
2037    /// left to infer the precondition — and in the governed collections
2038    /// evaluation it inferred wrongly, re-asking a verified caller for their card
2039    /// digits after `record_promise_to_pay` was refused. The monitor knows the
2040    /// active step, what that step is waiting for, and which tools *are*
2041    /// admitted; every refusal now carries it.
2042    fn render_denial(&self, tool: &str, denial: &Denial, state: &State) -> String {
2043        let head = match denial {
2044            Denial::OnceExhausted => {
2045                // Nothing to redirect to: the answer is "you already did this".
2046                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        // What the model may do instead. Drawn from the flow's own tool
2068        // universe, so it never invents a tool the session has not declared.
2069        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        // In live runs the model told the caller "your table is booked" right
2077        // after its booking call was refused. Say plainly that nothing happened.
2078        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        // The posture is the step's own words for what it wants; repeating it
2088        // here means the redirection and the reason arrive together, rather than
2089        // the model having to reconcile an error with steering sent earlier.
2090        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    /// Observe a tool call for conformance. In Enforce mode the caller has
2099    /// already gated via [`admits_tool`](Self::admits_tool); this records the
2100    /// call and, in Observe mode, logs a deviation if it was inadmissible.
2101    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
2116/// Why a tool call was refused, before it is rendered into prose.
2117///
2118/// Structured rather than a string so the renderer can add the redirection —
2119/// what *is* available, and what the active step is waiting for — which is the
2120/// half a model needs and the old message never carried.
2121enum Denial {
2122    /// A `once(tool)` constraint, already spent.
2123    OnceExhausted,
2124    /// A `never(tool).until(guard)` whose guard has not latched; carries the
2125    /// rendered guard.
2126    NotYet(String),
2127    /// The named active step lists the tool in `deny`.
2128    DeniedByStep(String),
2129    /// Active steps restrict by `allow` and none of them names the tool;
2130    /// carries the restricting step ids.
2131    NotInStep(Vec<String>),
2132}
2133
2134/// A single problem found while compiling a [`Flow`].
2135#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2136#[serde(tag = "kind", content = "detail", rename_all = "snake_case")]
2137pub enum FlowError {
2138    /// A referential-integrity or acyclicity error from [`Flow::validate`].
2139    Invalid(String),
2140    /// A step that no path from a root can ever reach.
2141    UnreachableStep(String),
2142    /// A commit (confirm) tool whose gate is always true — effectively
2143    /// unguarded, defeating confirm-before-commit.
2144    UnguardedCommitTool(String),
2145    /// A tool referenced by the flow (step `allow`/`deny`, `once`,
2146    /// `never…until`, confirm) that is not in the registry given to
2147    /// [`Flow::compile_with_tools`].
2148    UnknownTool(String),
2149    /// A `never(tool).until(guard)` whose guard references a step id that
2150    /// doesn't exist — the guard can never latch, so the tool would be
2151    /// forbidden forever.
2152    UnsatisfiableGuard {
2153        /// The tool the constraint gates.
2154        tool: String,
2155        /// The unknown step id the guard's `done(..)` atom references.
2156        step: String,
2157    },
2158    /// A cycle over the combined `after` + `before(a, b)` ordering edges —
2159    /// every step on the cycle waits on another, so none can ever become
2160    /// eligible. Contains the step ids on the cycle.
2161    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/// All problems found while compiling a [`Flow`]; non-empty on failure.
2194#[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/// The precomputed tool surface of a compiled flow: every tool name the flow
2210/// reasons about (step `allow`/`deny`, `once`, `never…until`, confirm), so
2211/// introspection can enumerate and explain gating decisions. Distinct from
2212/// [`tool::ToolPolicy`](crate::tool::ToolPolicy), which is a per-tool runtime
2213/// policy (timeout/cache/confirm).
2214#[derive(Debug, Clone, Default)]
2215pub struct ToolSurface {
2216    /// Every tool referenced anywhere in the flow.
2217    pub tools: BTreeSet<String>,
2218}
2219
2220/// A validated [`Flow`] plus its precomputed [`ToolSurface`].
2221///
2222/// Produced by [`Flow::compile`]. Holding one is proof the flow passed
2223/// compilation, so the runtime never re-discovers structural errors. This is the
2224/// IR the conversation compiler targets and the type richer surfaces build on.
2225#[derive(Debug, Clone)]
2226pub struct CompiledFlow {
2227    flow: Flow,
2228    surface: ToolSurface,
2229}
2230
2231impl CompiledFlow {
2232    /// The underlying validated flow.
2233    pub fn flow(&self) -> &Flow {
2234        &self.flow
2235    }
2236    /// The precomputed tool surface.
2237    pub fn tool_surface(&self) -> &ToolSurface {
2238        &self.surface
2239    }
2240    /// Render the flow as a Mermaid diagram.
2241    pub fn to_mermaid(&self) -> String {
2242        self.flow.to_mermaid()
2243    }
2244    /// Consume into the inner flow.
2245    pub fn into_flow(self) -> Flow {
2246        self.flow
2247    }
2248}
2249
2250/// A model-readable explanation of the current control-plane state — the
2251/// foundation of `why did the assistant ask that?`.
2252///
2253/// Produced by [`FlowMonitor::explain`]. `Serialize` so it can be surfaced to a
2254/// model, a devtool, or a log without the model driving control flow.
2255#[derive(Debug, Clone, Serialize)]
2256pub struct FlowExplanation {
2257    /// Steps eligible-but-not-done right now.
2258    pub active: Vec<String>,
2259    /// Tools currently admitted.
2260    pub allowed_tools: Vec<String>,
2261    /// Tools currently blocked, mapped to the reason.
2262    pub blocked_tools: BTreeMap<String, String>,
2263    /// Required steps not yet done (drives repair).
2264    pub missing_requirements: Vec<String>,
2265    /// Per-active-step completion-guard truth trees: exactly which atom each
2266    /// stuck step is waiting on. Terminal steps (no `done` guard) are absent.
2267    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
2268    pub active_progress: BTreeMap<String, GuardTrace>,
2269}
2270
2271/// Observed governance state for a simulator, UI, or handoff.
2272///
2273/// Step lists describe the currently driving layer. `complete` describes the
2274/// entire stack, so a digression's closing turn does not finish the main task.
2275/// This is an observation, not a checkpoint for restoring a conversation.
2276#[derive(Debug, Clone, Serialize)]
2277pub struct FlowSnapshot {
2278    /// Active-layer admission and completion guards.
2279    #[serde(flatten)]
2280    pub explanation: FlowExplanation,
2281    /// Completed steps in the same layer as `explanation`.
2282    pub done: Vec<String>,
2283    /// Whether the whole stack is complete, including explicit termination.
2284    pub complete: bool,
2285    /// Active digressions, outermost first. Empty when the main flow drives.
2286    pub overlay_path: Vec<String>,
2287    /// Whether a terminating digression ended the conversation.
2288    pub terminated: bool,
2289}
2290
2291/// Builder for a [`Flow`] using the cemented verbs.
2292#[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    /// Declare a new step.
2308    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    /// Add a dependency (call multiple times for multiple deps).
2324    pub fn after(mut self, dep: impl Into<String>) -> Self {
2325        self.current().after.push(Edge::to(dep));
2326        self
2327    }
2328    /// Add a **conditional** dependency: satisfied only while `when` holds.
2329    /// Conditional edges out of one source are how a flow branches; pair with
2330    /// [`join_any`](Self::join_any) on the merge step.
2331    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    /// Any one satisfied edge makes this step eligible (the merge node after
2336    /// a branch). Default is `all` — a synchronizing join.
2337    pub fn join_any(mut self) -> Self {
2338        self.current().join = Join::Any;
2339        self
2340    }
2341    /// Extra eligibility guard beyond dependencies.
2342    pub fn gate(mut self, g: Guard) -> Self {
2343        self.current().gate = Some(g);
2344        self
2345    }
2346    /// Completion condition.
2347    pub fn done(mut self, g: Guard) -> Self {
2348        self.current().done = Some(g);
2349        self
2350    }
2351    /// Instruction imposed while active.
2352    pub fn posture(mut self, text: impl Into<String>) -> Self {
2353        self.current().posture = Some(text.into());
2354        self
2355    }
2356    /// A grounding template projected while active — a curated, `State`-
2357    /// interpolated fact line that pins the model to known values. `{key}`
2358    /// interpolates a value; `{key?yes:no}` picks by truthiness. See
2359    /// [`render_ground`].
2360    pub fn ground(mut self, template: impl Into<String>) -> Self {
2361        self.current().ground = Some(template.into());
2362        self
2363    }
2364    /// Tools available while active (whitelist).
2365    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    /// Tools forbidden while active.
2376    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    /// Mark the current step terminal.
2387    pub fn terminal(mut self) -> Self {
2388        self.current().terminal = true;
2389        self
2390    }
2391
2392    /// A tool may run at most once.
2393    pub fn once(mut self, tool: impl Into<String>) -> Self {
2394        self.constraints.push(Constraint::Once(tool.into()));
2395        self
2396    }
2397    /// Ordering invariant: `a` before `b`.
2398    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    /// Required terminal steps for completion.
2404    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    /// Exempt cross-cutting tools from every step's `allow` whitelist.
2415    ///
2416    /// Flow-level, and order-independent with respect to `.step(..)`. Use it for
2417    /// tools that serve the conversation rather than any one step of it —
2418    /// memory recall, escalation, logging. A tool named here still obeys
2419    /// `deny`, `once` and `never(..).until(..)`; see [`Flow::ambient`].
2420    ///
2421    /// ```
2422    /// # use gemini_adk_rs::flow::{Flow, Guard};
2423    /// let flow = Flow::new()
2424    ///     .ambient(["recall_context"])
2425    ///     .step("book")
2426    ///     .allow(["book_table"])
2427    ///     .done(Guard::called_ok("book_table"))
2428    ///     .build()
2429    ///     .unwrap();
2430    /// assert_eq!(flow.ambient, ["recall_context"]);
2431    /// ```
2432    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    /// Un-latch `steps` whenever the guard given to
2442    /// [`when`](ResetBuilder::when) *becomes* true — the loop primitive.
2443    /// See [`Constraint::Reset`] for the exact forgiveness semantics.
2444    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    /// Forbid a tool until a guard holds (`never(tool).until(guard)`).
2456    pub fn never(self, tool: impl Into<String>) -> NeverBuilder {
2457        NeverBuilder {
2458            fb: self,
2459            tool: tool.into(),
2460        }
2461    }
2462    /// Commit-tool sugar: at most once, gated until `until`, and flagged for
2463    /// confirmation. Composes `once` + `never…until` + the confirmation seam.
2464    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    /// Finalize and validate the flow.
2476    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
2488/// Sub-builder for `never(tool).until(guard)`.
2489pub struct NeverBuilder {
2490    fb: FlowBuilder,
2491    tool: String,
2492}
2493
2494impl NeverBuilder {
2495    /// Permit the tool once the guard holds.
2496    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
2505/// Intermediate for `reset(steps).when(guard)`.
2506pub struct ResetBuilder {
2507    builder: FlowBuilder,
2508    steps: Vec<String>,
2509}
2510
2511impl ResetBuilder {
2512    /// Fire the reset on this guard's rising edge.
2513    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    /// A refusal must say what would succeed, not only what failed.
2528    ///
2529    /// The old message was "'X' is not available in the current step" — the
2530    /// tool's own name and nothing else. The model cannot act on that: it knows
2531    /// the call was rejected but not what the gate is holding out for, so it
2532    /// guesses. In the governed collections evaluation it guessed identity
2533    /// verification and re-asked a caller it had already verified for their card
2534    /// digits, which read as the model losing the tool result when in fact the
2535    /// gate had told it nothing.
2536    #[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    /// The step's own posture rides along, so the reason and the redirection
2556    /// arrive in one payload rather than the model having to reconcile an error
2557    /// with steering sent on an earlier turn.
2558    #[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    /// In live runs the model announced a booking right after its booking call
2574    /// was refused; the refusal says the tool did not run.
2575    #[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    /// A spent `once` is the one refusal with nothing to redirect to: the answer
2589    /// is "you already did this", and offering alternatives would invite a
2590    /// retry under another name.
2591    #[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    /// The redirection is computed against live state, so it changes as the
2619    /// conversation progresses rather than describing the flow's opening move
2620    /// forever.
2621    #[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        // Verification lands and the call moves on to the disclosure step. The
2635        // same constraint still blocks `charge_card`, but where the caller *is*
2636        // has changed, and the refusal has to change with it.
2637        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    /// Guard prose is what makes a refusal legible; pinned so the atoms do not
2661    /// silently start rendering as debug output.
2662    #[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        // a after b, b after a — build() should reject.
2730        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        // Nothing done; only `verify` is active.
2770        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        // disclose + capture_ptp latch; close is terminal+eligible -> done.
2789        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        // get to take_payment being active
2800        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        // charge_card blocked until ptp_confirmed.
2807        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        // after it succeeds once, `once` blocks a second call.
2812        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        // In `verify`, only lookup_account is allowed.
2822        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        // charge_card out of order in observe mode -> recorded, still "runs".
2832        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        // Tool universe spans allow/deny/once/never_until/confirm.
2841        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        // `orphan` has no `after` and nothing leads to it — but it IS a root, so
2849        // to make it unreachable we give it an `after` on a step, then never make
2850        // that path lead anywhere. Simplest: a step depending on a missing root is
2851        // caught by validate; here we test a step unreachable via a broken chain.
2852        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        // island is reachable via a->b->island, so this compiles; assert it does.
2865        assert!(flow.compile().is_ok());
2866    }
2867
2868    #[test]
2869    fn compile_rejects_unguarded_commit_tool() {
2870        // commit tool gated by an always-true guard is effectively unguarded.
2871        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        // `charge_card` is referenced (allow/once/never_until) but missing from
2900        // the registry — a typo/drift the compiler must catch.
2901        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        // Plain compile() stays registry-agnostic.
2910        assert!(debt_flow().compile().is_ok());
2911    }
2912
2913    #[test]
2914    fn compile_rejects_never_until_guard_on_unknown_step() {
2915        // `never(pay).until(done("missing"))` can never latch — `pay` would be
2916        // forbidden forever. validate() doesn't check constraint guards; compile must.
2917        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        // `after` edges are acyclic, but before(a, b) + before(b, a) closes an
2935        // ordering cycle: neither step can ever become eligible. validate()'s
2936        // cycle check only walks `after`, so compile must catch this.
2937        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        // In the initial `verify` step, charge_card is blocked; explain says so.
2963        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        // Regression: `before(a, b)` was validated but never enforced — `b` could
2970        // start before `a` was done. `a` and `b` have no `after` edge, so only the
2971        // Before constraint orders them.
2972        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        // `b` is NOT active until `a` is done, even though its own gate is open.
2984        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        // Regression: a custom guard nested in all()/any() was lowered to
3008        // Pred::Always, silently deleting it. It must still evaluate.
3009        let always_false = Guard::all([Guard::is_true("present"), Guard::custom(|_| false)]);
3010        // Mixed combinator is a Custom guard (non-serializable), not a Spec.
3011        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        // Would be `true` if the custom guard had been erased to Always.
3021        assert!(!always_false.eval(&ctx), "custom guard must still veto");
3022
3023        // all-spec combinator stays serializable.
3024        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])")); // terminal shape
3054    }
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        // collect -> check ; `check` runs an agent on enter whose result
3070        // (`check:result`) then completes a downstream `book` step.
3071        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        // Only `collect` is active at the start.
3089        assert_eq!(mon.take_newly_active(&state), vec!["collect".to_string()]);
3090        // Re-asking yields nothing — fire-once.
3091        assert!(mon.take_newly_active(&state).is_empty());
3092
3093        // Complete `collect`; `check` becomes active and its on_enter fires.
3094        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        // The resolved result completes `check`, then terminal `book`.
3103        mon.on_turn(&state);
3104        assert!(mon.marking().done.contains("check"));
3105        assert!(mon.is_complete());
3106        // No further newly-active steps to announce.
3107        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        // Falsy branch + absent key renders empty.
3124        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        // Once collect completes, its ground no longer projects.
3150        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    // ─── ambient tools ──────────────────────────────────────────────────────
3171    //
3172    // A step's `allow` list is a whitelist, so it excludes by omission: every
3173    // tool the author did not think to name is denied for the duration. That is
3174    // right for domain tools — naming `charge_card` is a statement that this is
3175    // the step where money moves — and wrong for cross-cutting infrastructure
3176    // that no step is *about*. Memory recall is the motivating case: a flow
3177    // author writing `.allow(["book_table"])` is saying "book here, don't search
3178    // the catalogue", not "stop remembering who the caller is".
3179    //
3180    // `ambient` names those tools once, at flow level. They are exempt from the
3181    // whitelist's implicit exclusion and from nothing else: a constraint that
3182    // *names* the tool still binds, because naming it is a deliberate act.
3183
3184    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        // `deny` names the tool, so it is a decision about that tool rather than
3221        // a side effect of listing others. Ambient must not override it.
3222        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        // Otherwise `compile_with_tools` would report a registry that covers the
3282        // flow as complete while an ambient tool it never heard of gets called.
3283        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        // The whole point is that this is additive: a flow that never mentions
3305        // `ambient` must gate exactly as it did before the field existed.
3306        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        // `captured` is a single atom (one node); `is_true` is false.
3331        assert!(!trace.children[0].holds, "ptp_date missing");
3332        assert!(!trace.children[1].holds);
3333        // JSON-serializable for devtools.
3334        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        // Tool/step references are not state keys.
3359        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        // decide ─(routine)→ schedule ─┐
3385        //        ─(emergency)→ escalate ─┴→ close (join: any)
3386        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        // Merge via any-join: schedule alone completing closes the flow.
3418        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        // Decline: rising edge un-latches pay and forgives the charge.
3441        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        // Still-true guard does not re-fire (edge, not level).
3450        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        // Unconditional edge stays the original plain string.
3472        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        // The Guard schema must surface the Pred atoms.
3488        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        // A false conjunct settles a conjunction; a true disjunct a disjunction.
3506        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}