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 stack;
33pub mod timing;
34pub mod verbatim;
35pub use stack::{
36    FlowStack, OVERLAY_STATE_KEY, Overlay, RepairPolicy, Resume, SharedFlowStack,
37    TERMINATED_STATE_KEY, TOOL_CALL_KEY, TOOL_DENIED_KEY, TOOL_RESULT_KEY, correction_flag,
38    escalate_flag, reprompt_flag,
39};
40pub use timing::{DEFAULT_REPROMPT, VOICE_TIMING_KEY, VoiceTiming};
41pub use verbatim::{VERBATIM_KEY, VerbatimRequirement, verbatim_flag};
42
43/// Evaluation context handed to a [`Guard`]: the session state plus the
44/// current flow marking.
45pub struct FlowCtx<'a> {
46    /// The session state.
47    pub state: &'a State,
48    /// The current flow marking (done steps + tool-call counts).
49    pub marking: &'a Marking,
50}
51
52/// A serializable predicate atom — the closed set of guard primitives.
53#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, schemars::JsonSchema)]
54#[serde(rename_all = "snake_case")]
55pub enum Pred {
56    /// Always true.
57    Always,
58    /// State key holds boolean `true`.
59    IsTrue(String),
60    /// State key is present.
61    IsSet(String),
62    /// State key equals the given JSON value.
63    Eq(String, Value),
64    /// All of the given state keys are present (e.g. extracted slots).
65    Captured(Vec<String>),
66    /// The named tool has completed successfully at least once.
67    CalledOk(String),
68    /// The named step is done.
69    Done(String),
70    /// Conjunction.
71    All(Vec<Pred>),
72    /// Disjunction.
73    Any(Vec<Pred>),
74    /// Negation.
75    Not(Box<Pred>),
76}
77
78impl Pred {
79    fn eval(&self, ctx: &FlowCtx) -> bool {
80        match self {
81            Pred::Always => true,
82            Pred::IsTrue(k) => ctx.state.get::<bool>(k) == Some(true),
83            Pred::IsSet(k) => ctx.state.contains(k),
84            Pred::Eq(k, v) => ctx.state.get::<Value>(k).as_ref() == Some(v),
85            Pred::Captured(fields) => fields.iter().all(|f| ctx.state.contains(f)),
86            Pred::CalledOk(t) => ctx.marking.tool_ok.contains_key(t),
87            Pred::Done(s) => ctx.marking.done.contains(s),
88            Pred::All(ps) => ps.iter().all(|p| p.eval(ctx)),
89            Pred::Any(ps) => ps.iter().any(|p| p.eval(ctx)),
90            Pred::Not(p) => !p.eval(ctx),
91        }
92    }
93
94    /// This predicate as a short prose clause. See [`Guard::describe`].
95    fn describe(&self) -> String {
96        fn join(ps: &[Pred], sep: &str) -> String {
97            ps.iter().map(Pred::describe).collect::<Vec<_>>().join(sep)
98        }
99        match self {
100            Pred::Always => "nothing".to_string(),
101            Pred::IsTrue(k) => format!("'{k}' must be true"),
102            Pred::IsSet(k) => format!("'{k}' must be known"),
103            Pred::Eq(k, v) => format!("'{k}' must be {v}"),
104            Pred::Captured(fields) => {
105                format!("these must be known: {}", fields.join(", "))
106            }
107            Pred::CalledOk(t) => format!("'{t}' must have run successfully"),
108            Pred::Done(s) => format!("step '{s}' must be complete"),
109            Pred::All(ps) => join(ps, " and "),
110            Pred::Any(ps) => join(ps, " or "),
111            Pred::Not(p) => format!("it must not be the case that {}", p.describe()),
112        }
113    }
114
115    /// Step ids referenced by `Done(..)` atoms (for validation).
116    fn referenced_steps(&self, out: &mut Vec<String>) {
117        match self {
118            Pred::Done(s) => out.push(s.clone()),
119            Pred::All(ps) | Pred::Any(ps) => ps.iter().for_each(|p| p.referenced_steps(out)),
120            Pred::Not(p) => p.referenced_steps(out),
121            _ => {}
122        }
123    }
124
125    /// Tool names referenced by `called_ok` atoms (for reset forgiveness).
126    fn referenced_tools(&self, out: &mut Vec<String>) {
127        match self {
128            Pred::CalledOk(t) => out.push(t.clone()),
129            Pred::All(ps) | Pred::Any(ps) => ps.iter().for_each(|p| p.referenced_tools(out)),
130            Pred::Not(p) => p.referenced_tools(out),
131            _ => {}
132        }
133    }
134
135    /// State keys this predicate reads (`is_true`/`is_set`/`eq`/`captured`
136    /// atoms, recursively). `called_ok`/`done` reference tools/steps, not
137    /// state, and are excluded.
138    fn referenced_state_keys(&self, out: &mut BTreeSet<String>) {
139        match self {
140            Pred::IsTrue(k) | Pred::IsSet(k) | Pred::Eq(k, _) => {
141                out.insert(k.clone());
142            }
143            Pred::Captured(fields) => out.extend(fields.iter().cloned()),
144            Pred::All(ps) | Pred::Any(ps) => {
145                ps.iter().for_each(|p| p.referenced_state_keys(out));
146            }
147            Pred::Not(p) => p.referenced_state_keys(out),
148            _ => {}
149        }
150    }
151
152    /// Evaluate to a [`GuardTrace`]: the predicate tree with each node's prose
153    /// description and its truth value under `ctx`. The atom-granular answer
154    /// to "why is this step not done?".
155    fn explain(&self, ctx: &FlowCtx) -> GuardTrace {
156        let children = match self {
157            Pred::All(ps) | Pred::Any(ps) => ps.iter().map(|p| p.explain(ctx)).collect(),
158            Pred::Not(p) => vec![p.explain(ctx)],
159            _ => Vec::new(),
160        };
161        GuardTrace {
162            desc: match self {
163                // Composite nodes describe only the connective; the operands
164                // carry their own descriptions as children.
165                Pred::All(_) => "all of".to_string(),
166                Pred::Any(_) => "any of".to_string(),
167                Pred::Not(_) => "not".to_string(),
168                atom => atom.describe(),
169            },
170            holds: self.eval(ctx),
171            children,
172        }
173    }
174}
175
176/// A [`Guard`] evaluated to a truth tree: each predicate node with its prose
177/// description and whether it currently holds. Serializable, so a devtool can
178/// render exactly which atom a stuck step is waiting on.
179#[derive(Clone, Debug, Serialize, schemars::JsonSchema)]
180pub struct GuardTrace {
181    /// Prose description of this node (from [`Guard::describe`] vocabulary).
182    pub desc: String,
183    /// Whether this node currently holds.
184    pub holds: bool,
185    /// Operand traces for `all`/`any`/`not`; empty for atoms.
186    #[serde(default, skip_serializing_if = "Vec::is_empty")]
187    pub children: Vec<GuardTrace>,
188}
189
190/// Render a grounding template against `state`.
191///
192/// - `{key}` interpolates the value at `key` (strings bare, other JSON compact);
193///   an absent key renders empty.
194/// - `{key?yes:no}` renders `yes` when `key` is *truthy* (present and not
195///   `false`/`null`/`0`/`""`), else `no`.
196///
197/// This is the realization of `Effect::ground`: a deterministic projection of
198/// known `State` into a steering line, so the model restates facts rather than
199/// inventing them.
200pub fn render_ground(template: &str, state: &State) -> String {
201    let mut out = String::with_capacity(template.len());
202    let mut rest = template;
203    while let Some(open) = rest.find('{') {
204        out.push_str(&rest[..open]);
205        let after = &rest[open + 1..];
206        let Some(close) = after.find('}') else {
207            // Unbalanced brace: emit the remainder verbatim.
208            out.push_str(&rest[open..]);
209            return out;
210        };
211        let expr = &after[..close];
212        out.push_str(&render_expr(expr, state));
213        rest = &after[close + 1..];
214    }
215    out.push_str(rest);
216    out
217}
218
219fn render_expr(expr: &str, state: &State) -> String {
220    if let Some((cond, arms)) = expr.split_once('?') {
221        let (yes, no) = arms.split_once(':').unwrap_or((arms, ""));
222        if is_truthy(state, cond.trim()) {
223            yes.to_string()
224        } else {
225            no.to_string()
226        }
227    } else {
228        match state.get::<Value>(expr.trim()) {
229            Some(Value::String(s)) => s,
230            Some(v) => v.to_string(),
231            None => String::new(),
232        }
233    }
234}
235
236fn is_truthy(state: &State, key: &str) -> bool {
237    match state.get::<Value>(key) {
238        None | Some(Value::Null) => false,
239        Some(Value::Bool(b)) => b,
240        Some(Value::Number(n)) => n.as_f64().map(|f| f != 0.0).unwrap_or(true),
241        Some(Value::String(s)) => !s.is_empty(),
242        Some(_) => true,
243    }
244}
245
246type CustomFn = Arc<dyn Fn(&FlowCtx) -> bool + Send + Sync>;
247
248/// A boolean predicate over `(state, marking)` — the *only* predicate type.
249///
250/// Use the constructors ([`Guard::is_true`], [`Guard::captured`],
251/// [`Guard::called_ok`], …) for the serializable closed atoms, or
252/// [`Guard::custom`] for a bespoke closure (not serializable).
253#[derive(Clone)]
254pub enum Guard {
255    /// A serializable predicate built from the closed atom set.
256    Spec(Pred),
257    /// A code-only escape hatch. Not serializable.
258    Custom(CustomFn),
259}
260
261impl Guard {
262    /// Always true.
263    pub fn always() -> Self {
264        Guard::Spec(Pred::Always)
265    }
266    /// State key holds boolean `true`.
267    pub fn is_true(key: impl Into<String>) -> Self {
268        Guard::Spec(Pred::IsTrue(key.into()))
269    }
270    /// State key is present.
271    pub fn is_set(key: impl Into<String>) -> Self {
272        Guard::Spec(Pred::IsSet(key.into()))
273    }
274    /// State key equals the given JSON value.
275    pub fn eq(key: impl Into<String>, value: impl Into<Value>) -> Self {
276        Guard::Spec(Pred::Eq(key.into(), value.into()))
277    }
278    /// All of the given state keys are present (extracted slots).
279    pub fn captured<I, S>(fields: I) -> Self
280    where
281        I: IntoIterator<Item = S>,
282        S: Into<String>,
283    {
284        Guard::Spec(Pred::Captured(fields.into_iter().map(Into::into).collect()))
285    }
286    /// The named tool has completed successfully.
287    pub fn called_ok(tool: impl Into<String>) -> Self {
288        Guard::Spec(Pred::CalledOk(tool.into()))
289    }
290    /// The named step is done.
291    pub fn done(step: impl Into<String>) -> Self {
292        Guard::Spec(Pred::Done(step.into()))
293    }
294    /// True once an orchestrated agent named `name` has produced a result
295    /// (its `{name}:result` state key is set). Pairs with the
296    /// [`orchestration`](crate::orchestration) `call`/`dispatch`/`background`.
297    pub fn resolved(name: impl AsRef<str>) -> Self {
298        Guard::Spec(Pred::IsSet(format!("{}:result", name.as_ref())))
299    }
300    /// Conjunction.
301    ///
302    /// If every input is a serializable atom, the result is a serializable
303    /// `Pred::All`. If any input is a [`Guard::custom`], the result is itself a
304    /// custom guard that evaluates the conjunction at runtime — the custom guard
305    /// is **never silently dropped** (it merely makes the combinator
306    /// non-serializable, which surfaces as an error only if you try to serialize
307    /// the flow).
308    pub fn all(guards: impl IntoIterator<Item = Guard>) -> Self {
309        let guards: Vec<Guard> = guards.into_iter().collect();
310        if guards.iter().all(|g| matches!(g, Guard::Spec(_))) {
311            Guard::Spec(Pred::All(specs_unchecked(guards)))
312        } else {
313            Guard::Custom(Arc::new(move |ctx| guards.iter().all(|g| g.eval(ctx))))
314        }
315    }
316    /// Disjunction.
317    ///
318    /// Mirrors [`Guard::all`]: custom inputs are preserved as a runtime closure
319    /// rather than erased.
320    pub fn any(guards: impl IntoIterator<Item = Guard>) -> Self {
321        let guards: Vec<Guard> = guards.into_iter().collect();
322        if guards.iter().all(|g| matches!(g, Guard::Spec(_))) {
323            Guard::Spec(Pred::Any(specs_unchecked(guards)))
324        } else {
325            Guard::Custom(Arc::new(move |ctx| guards.iter().any(|g| g.eval(ctx))))
326        }
327    }
328    /// Negation of a serializable atom.
329    #[allow(clippy::should_implement_trait)]
330    pub fn not(guard: Guard) -> Self {
331        match guard {
332            Guard::Spec(p) => Guard::Spec(Pred::Not(Box::new(p))),
333            // Negating a custom guard yields a custom guard.
334            Guard::Custom(f) => Guard::Custom(Arc::new(move |ctx| !f(ctx))),
335        }
336    }
337    /// This guard as a short prose clause, for telling a model what a refusal
338    /// is waiting on.
339    ///
340    /// A refusal that names only the tool leaves the model to guess the
341    /// precondition, and a wrong guess is indistinguishable from a wrong model:
342    /// in the governed collections evaluation, `record_promise_to_pay` was
343    /// refused with "not available in the current step", and the model — with
344    /// nothing else to go on — decided it must need to re-verify the caller and
345    /// asked for the card digits it had already checked.
346    pub fn describe(&self) -> String {
347        match self {
348            Guard::Spec(p) => p.describe(),
349            Guard::Custom(_) => "a condition set by the application".to_string(),
350        }
351    }
352
353    /// A bespoke closure over `(state, marking)`. Not serializable.
354    pub fn custom(f: impl Fn(&FlowCtx) -> bool + Send + Sync + 'static) -> Self {
355        Guard::Custom(Arc::new(f))
356    }
357
358    /// Evaluate the guard.
359    pub fn eval(&self, ctx: &FlowCtx) -> bool {
360        match self {
361            Guard::Spec(p) => p.eval(ctx),
362            Guard::Custom(f) => f(ctx),
363        }
364    }
365
366    /// Evaluate against state alone, with an empty [`Marking`].
367    ///
368    /// For contexts outside a governed flow (phase transitions, watcher
369    /// conditions) where no marking exists: `called_ok`/`done` atoms evaluate
370    /// `false` there — a validator should reject them in such positions.
371    pub fn eval_state(&self, state: &State) -> bool {
372        let marking = Marking::default();
373        self.eval(&FlowCtx {
374            state,
375            marking: &marking,
376        })
377    }
378
379    /// Evaluate to a [`GuardTrace`] — the predicate tree with per-node truth
380    /// values. A [`Guard::custom`] yields a single opaque node.
381    pub fn explain_trace(&self, ctx: &FlowCtx) -> GuardTrace {
382        match self {
383            Guard::Spec(p) => p.explain(ctx),
384            Guard::Custom(f) => GuardTrace {
385                desc: "a condition set by the application".to_string(),
386                holds: f(ctx),
387                children: Vec::new(),
388            },
389        }
390    }
391
392    fn referenced_steps(&self, out: &mut Vec<String>) {
393        if let Guard::Spec(p) = self {
394            p.referenced_steps(out);
395        }
396    }
397
398    fn referenced_state_keys(&self, out: &mut BTreeSet<String>) {
399        if let Guard::Spec(p) = self {
400            p.referenced_state_keys(out);
401        }
402    }
403
404    /// The state keys this guard reads (`is_true`/`is_set`/`eq`/`captured`
405    /// atoms). A custom guard reads nothing that can be named, so it
406    /// reports none.
407    pub fn state_keys(&self) -> BTreeSet<String> {
408        let mut keys = BTreeSet::new();
409        self.referenced_state_keys(&mut keys);
410        keys
411    }
412
413    fn referenced_tools(&self, out: &mut Vec<String>) {
414        if let Guard::Spec(p) = self {
415            p.referenced_tools(out);
416        }
417    }
418}
419
420/// Unwrap a list of guards known to be all `Spec` into their predicates.
421///
422/// The caller (`Guard::all`/`Guard::any`) only invokes this after verifying every
423/// guard is a `Spec`, so the `Custom` arm is unreachable.
424fn specs_unchecked(guards: Vec<Guard>) -> Vec<Pred> {
425    guards
426        .into_iter()
427        .map(|g| match g {
428            Guard::Spec(p) => p,
429            Guard::Custom(_) => unreachable!("specs_unchecked called with a custom guard"),
430        })
431        .collect()
432}
433
434impl Serialize for Guard {
435    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
436        match self {
437            Guard::Spec(p) => p.serialize(s),
438            Guard::Custom(_) => Err(serde::ser::Error::custom(
439                "custom guards are not serializable; use Guard atoms for data-driven flows",
440            )),
441        }
442    }
443}
444
445impl<'de> Deserialize<'de> for Guard {
446    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
447        Ok(Guard::Spec(Pred::deserialize(d)?))
448    }
449}
450
451// A serializable guard is exactly a `Pred` atom on the wire (the `Custom`
452// variant is code-only and rejected by `Serialize`), so its JSON Schema is
453// `Pred`'s. Inline the `Pred` subschema wherever a `Guard` appears.
454impl schemars::JsonSchema for Guard {
455    fn is_referenceable() -> bool {
456        false
457    }
458    fn schema_name() -> String {
459        "Guard".to_string()
460    }
461    fn json_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
462        generator.subschema_for::<Pred>()
463    }
464}
465
466impl std::fmt::Debug for Guard {
467    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
468        match self {
469            Guard::Spec(p) => write!(f, "{p:?}"),
470            Guard::Custom(_) => write!(f, "Custom(<fn>)"),
471        }
472    }
473}
474
475/// A dependency edge into a step.
476///
477/// In JSON, a plain string (`"verify"`) is an unconditional edge — satisfied
478/// once the source step is done. An object (`{"step": "triage", "when":
479/// {"is_true": "routine"}}`) is a **conditional edge**: satisfied only while
480/// the source is done *and* the guard holds. Conditional edges out of one
481/// source into sibling steps are how a flow branches; pair them with
482/// [`Join::Any`] on the merge step. Unconditional edges serialize back to the
483/// plain string form, so existing documents round-trip unchanged.
484#[derive(Clone, Debug)]
485pub struct Edge {
486    /// The source step this edge depends on.
487    pub step: String,
488    /// Optional condition; the edge is satisfied only while it holds.
489    pub when: Option<Guard>,
490}
491
492impl Edge {
493    /// An unconditional edge from `step`.
494    pub fn to(step: impl Into<String>) -> Self {
495        Edge {
496            step: step.into(),
497            when: None,
498        }
499    }
500    /// A conditional edge from `step`, satisfied only while `when` holds.
501    pub fn when(step: impl Into<String>, when: Guard) -> Self {
502        Edge {
503            step: step.into(),
504            when: Some(when),
505        }
506    }
507}
508
509impl<S: Into<String>> From<S> for Edge {
510    fn from(step: S) -> Self {
511        Edge::to(step)
512    }
513}
514
515impl Serialize for Edge {
516    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
517        match &self.when {
518            // Unconditional edges keep the original plain-string form.
519            None => self.step.serialize(s),
520            Some(when) => {
521                use serde::ser::SerializeStruct;
522                let mut out = s.serialize_struct("Edge", 2)?;
523                out.serialize_field("step", &self.step)?;
524                out.serialize_field("when", when)?;
525                out.end()
526            }
527        }
528    }
529}
530
531impl<'de> Deserialize<'de> for Edge {
532    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
533        #[derive(Deserialize)]
534        #[serde(untagged)]
535        enum Repr {
536            Simple(String),
537            Conditional {
538                step: String,
539                #[serde(default)]
540                when: Option<Guard>,
541            },
542        }
543        Ok(match Repr::deserialize(d)? {
544            Repr::Simple(step) => Edge { step, when: None },
545            Repr::Conditional { step, when } => Edge { step, when },
546        })
547    }
548}
549
550impl schemars::JsonSchema for Edge {
551    fn schema_name() -> String {
552        "Edge".to_string()
553    }
554    fn json_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
555        #[derive(schemars::JsonSchema)]
556        #[serde(untagged)]
557        #[allow(dead_code)]
558        enum EdgeSchema {
559            Simple(String),
560            Conditional { step: String, when: Option<Pred> },
561        }
562        EdgeSchema::json_schema(generator)
563    }
564}
565
566/// How a step's dependency edges combine.
567#[derive(
568    Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema,
569)]
570#[serde(rename_all = "snake_case")]
571pub enum Join {
572    /// Every edge must be satisfied (the default — a synchronizing join).
573    #[default]
574    All,
575    /// Any one satisfied edge suffices — the merge node after a branch.
576    Any,
577}
578
579impl Join {
580    fn is_all(&self) -> bool {
581        *self == Join::All
582    }
583}
584
585/// A node in the flow DAG — the only node type.
586#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)]
587pub struct Step {
588    /// Unique step id.
589    pub id: String,
590    /// Dependency edges; how they combine is set by `join`.
591    #[serde(default, skip_serializing_if = "Vec::is_empty")]
592    pub after: Vec<Edge>,
593    /// How the `after` edges combine: `all` (default) or `any`.
594    #[serde(default, skip_serializing_if = "Join::is_all")]
595    pub join: Join,
596    /// Extra eligibility predicate beyond dependencies.
597    #[serde(default, skip_serializing_if = "Option::is_none")]
598    pub gate: Option<Guard>,
599    /// Completion condition. Required for non-terminal steps.
600    #[serde(default, skip_serializing_if = "Option::is_none")]
601    pub done: Option<Guard>,
602    /// Instruction imposed while this step is active (projected as steering).
603    #[serde(default, skip_serializing_if = "Option::is_none")]
604    pub posture: Option<String>,
605    /// A grounding template projected while active: a curated, `State`-interpolated
606    /// fact line that pins the model to known values (anti-hallucination). See
607    /// [`render_ground`]. Serializable, like `posture`.
608    #[serde(default, skip_serializing_if = "Option::is_none")]
609    pub ground: Option<String>,
610    /// Tools available while this step is active (whitelist; empty = no restriction).
611    #[serde(default, skip_serializing_if = "Vec::is_empty")]
612    pub allow: Vec<String>,
613    /// Tools forbidden while this step is active.
614    #[serde(default, skip_serializing_if = "Vec::is_empty")]
615    pub deny: Vec<String>,
616    /// A terminal step — reaching it (deps + gate) marks it done with no milestone.
617    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
618    pub terminal: bool,
619}
620
621/// A cross-cutting flow constraint.
622#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)]
623#[serde(rename_all = "snake_case")]
624pub enum Constraint {
625    /// A tool may complete at most once.
626    Once(String),
627    /// Step `0` must be done before step `1` starts.
628    Before(String, String),
629    /// A tool is forbidden until the guard holds.
630    NeverUntil {
631        /// The gated tool.
632        tool: String,
633        /// The guard that must hold to permit it.
634        until: Guard,
635    },
636    /// These steps must be done for the flow to be complete.
637    Require(Vec<String>),
638    /// Un-latch the named steps when the guard *becomes* true (rising edge) —
639    /// the loop primitive. The DAG stays acyclic and statically checkable;
640    /// iteration is explicit marking surgery instead of back-edges.
641    ///
642    /// A reset also forgives the completion evidence the steps' `done` guards
643    /// reference: `called_ok` counts for those tools are cleared (so a `once`
644    /// on such a tool permits one run per latch-cycle). State keys the guards
645    /// read are the application's to clear — a reset never touches [`State`].
646    Reset {
647        /// The steps to un-latch.
648        steps: Vec<String>,
649        /// Fires on this guard's rising edge.
650        when: Guard,
651    },
652}
653
654/// A governed conversation/tool DAG.
655#[derive(Clone, Debug, Default, Serialize, Deserialize, schemars::JsonSchema)]
656pub struct Flow {
657    /// The steps (DAG nodes).
658    pub steps: Vec<Step>,
659    /// Cross-cutting constraints.
660    #[serde(default, skip_serializing_if = "Vec::is_empty")]
661    pub constraints: Vec<Constraint>,
662    /// Tools that require confirmation when reached (set by `commit`).
663    #[serde(default, skip_serializing_if = "Vec::is_empty")]
664    pub confirm_tools: Vec<String>,
665    /// Cross-cutting tools exempt from step `allow` whitelists.
666    ///
667    /// A step's `allow` list excludes by omission, which is correct for the
668    /// domain tools a step is *about* and wrong for infrastructure no step is
669    /// about — memory recall, escalation, logging. Naming a tool here says "this
670    /// is not part of any step's repertoire", not "this is ungovernable":
671    /// `deny`, `once` and `never(..).until(..)` all still bind, because each of
672    /// those *names* the tool and so is a decision about it.
673    #[serde(default, skip_serializing_if = "Vec::is_empty")]
674    pub ambient: Vec<String>,
675}
676
677impl Flow {
678    /// Start building a flow.
679    #[allow(
680        clippy::new_ret_no_self,
681        reason = "Flow::new() is the builder entry point; a Flow comes from FlowBuilder::build/compile"
682    )]
683    pub fn new() -> FlowBuilder {
684        FlowBuilder::default()
685    }
686
687    fn step(&self, id: &str) -> Option<&Step> {
688        self.steps.iter().find(|s| s.id == id)
689    }
690
691    /// Validate referential integrity and acyclicity. Every problem found is
692    /// reported in the returned [`ConfigError`], not just the first.
693    pub fn validate(&self) -> Result<(), ConfigError> {
694        let mut errs = Vec::new();
695        let ids: BTreeSet<&str> = self.steps.iter().map(|s| s.id.as_str()).collect();
696        if ids.len() != self.steps.len() {
697            errs.push("duplicate step ids".into());
698        }
699        for s in &self.steps {
700            for d in &s.after {
701                if !ids.contains(d.step.as_str()) {
702                    errs.push(format!(
703                        "step '{}' depends on unknown step '{}'",
704                        s.id, d.step
705                    ));
706                }
707                if let Some(when) = &d.when {
708                    let mut refs = Vec::new();
709                    when.referenced_steps(&mut refs);
710                    for r in refs {
711                        if !ids.contains(r.as_str()) {
712                            errs.push(format!(
713                                "step '{}' edge condition references unknown step '{r}'",
714                                s.id
715                            ));
716                        }
717                    }
718                }
719            }
720            if !s.terminal && s.done.is_none() {
721                errs.push(format!(
722                    "non-terminal step '{}' has no `done` condition (it can never complete)",
723                    s.id
724                ));
725            }
726            for g in s.gate.iter().chain(s.done.iter()) {
727                let mut refs = Vec::new();
728                g.referenced_steps(&mut refs);
729                for r in refs {
730                    if !ids.contains(r.as_str()) {
731                        errs.push(format!(
732                            "step '{}' guard references unknown step '{r}'",
733                            s.id
734                        ));
735                    }
736                }
737            }
738        }
739        for c in &self.constraints {
740            match c {
741                Constraint::Before(a, b) => {
742                    for x in [a, b] {
743                        if !ids.contains(x.as_str()) {
744                            errs.push(format!("constraint `before` references unknown step '{x}'"));
745                        }
746                    }
747                }
748                Constraint::Require(rs) => {
749                    for r in rs {
750                        if !ids.contains(r.as_str()) {
751                            errs.push(format!(
752                                "constraint `require` references unknown step '{r}'"
753                            ));
754                        }
755                    }
756                }
757                Constraint::Reset { steps, .. } => {
758                    for r in steps {
759                        if !ids.contains(r.as_str()) {
760                            errs.push(format!("constraint `reset` references unknown step '{r}'"));
761                        }
762                    }
763                }
764                _ => {}
765            }
766        }
767        if self.has_cycle() {
768            errs.push("flow dependency graph has a cycle (must be a DAG)".into());
769        }
770        ConfigError::from_issues(errs)
771    }
772
773    /// Every tool name referenced anywhere in the flow (allow/deny/once/
774    /// never_until/confirm). The universe over which [`ToolSurface`] reasons.
775    fn tool_universe(&self) -> BTreeSet<String> {
776        let mut tools = BTreeSet::new();
777        for s in &self.steps {
778            tools.extend(s.allow.iter().cloned());
779            tools.extend(s.deny.iter().cloned());
780        }
781        for c in &self.constraints {
782            match c {
783                Constraint::Once(t) => {
784                    tools.insert(t.clone());
785                }
786                Constraint::NeverUntil { tool, .. } => {
787                    tools.insert(tool.clone());
788                }
789                _ => {}
790            }
791        }
792        tools.extend(self.confirm_tools.iter().cloned());
793        tools.extend(self.ambient.iter().cloned());
794        tools
795    }
796
797    /// Every state key any guard in the flow reads (`is_true`/`is_set`/`eq`/
798    /// `captured` atoms in step gates, completion guards, and `never…until`
799    /// constraints).
800    ///
801    /// A key in this set that nothing in the session *writes* — no tool, no
802    /// extractor, no promotion — can never latch, which is the dominant silent
803    /// failure in data-authored flows. Validators diff this set against the
804    /// declared writers to catch it at load time, the way
805    /// [`compile_with_tools`](Self::compile_with_tools) catches tool typos.
806    pub fn state_keys_read(&self) -> BTreeSet<String> {
807        let mut keys = BTreeSet::new();
808        for s in &self.steps {
809            for g in s.gate.iter().chain(s.done.iter()) {
810                g.referenced_state_keys(&mut keys);
811            }
812            for e in &s.after {
813                if let Some(when) = &e.when {
814                    when.referenced_state_keys(&mut keys);
815                }
816            }
817        }
818        for c in &self.constraints {
819            match c {
820                Constraint::NeverUntil { until, .. } => until.referenced_state_keys(&mut keys),
821                Constraint::Reset { when, .. } => when.referenced_state_keys(&mut keys),
822                _ => {}
823            }
824        }
825        keys
826    }
827
828    /// Steps reachable from a root (a step with no `after` deps), following both
829    /// `after` edges and `Before(a, b)` ordering edges.
830    fn reachable_steps(&self) -> BTreeSet<String> {
831        let ids: BTreeSet<&str> = self.steps.iter().map(|s| s.id.as_str()).collect();
832        // Forward edges: a -> b when b.after contains a, or Before(a, b).
833        let mut succ: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
834        for s in &self.steps {
835            for d in &s.after {
836                if ids.contains(d.step.as_str()) {
837                    succ.entry(d.step.as_str()).or_default().push(s.id.as_str());
838                }
839            }
840        }
841        for c in &self.constraints {
842            if let Constraint::Before(a, b) = c
843                && ids.contains(a.as_str())
844                && ids.contains(b.as_str())
845            {
846                succ.entry(a.as_str()).or_default().push(b.as_str());
847            }
848        }
849        let roots: Vec<&str> = self
850            .steps
851            .iter()
852            .filter(|s| s.after.is_empty())
853            .map(|s| s.id.as_str())
854            .collect();
855        let mut seen = BTreeSet::new();
856        let mut stack = roots;
857        while let Some(id) = stack.pop() {
858            if seen.insert(id.to_string())
859                && let Some(next) = succ.get(id)
860            {
861                stack.extend(next.iter().copied());
862            }
863        }
864        seen
865    }
866
867    /// Compile and validate the flow into a [`CompiledFlow`], turning a class of
868    /// runtime surprises into load-time errors.
869    ///
870    /// On top of [`validate`](Self::validate)'s referential/acyclicity checks this
871    /// reports: unreachable steps, commit tools guarded by an always-true
872    /// condition (an effectively *unguarded* commit, which defeats the
873    /// confirm-before-commit contract), `never…until` guards whose `done(step)`
874    /// atoms reference unknown steps (unsatisfiable — the tool would be forbidden
875    /// forever), and ordering cycles across the combined `after` + `before` edges
876    /// (which deadlock every step on the cycle). Precomputes the [`ToolSurface`]
877    /// universe.
878    ///
879    /// To additionally validate tool names against a known registry, use
880    /// [`compile_with_tools`](Self::compile_with_tools).
881    pub fn compile(self) -> Result<CompiledFlow, FlowErrors> {
882        self.compile_internal(None)
883    }
884
885    /// Compile like [`compile`](Self::compile), additionally validating every
886    /// tool name the flow references (step `allow`/`deny`, `once`,
887    /// `never…until`, and commit/confirm tools) against the given registry of
888    /// known tool names.
889    ///
890    /// A referenced tool missing from `tools` is reported as
891    /// [`FlowError::UnknownTool`] — catching typos and drift between a flow
892    /// script and the tools actually registered on the session.
893    ///
894    /// ```ignore
895    /// let compiled = flow.compile_with_tools(&["lookup_account", "charge_card"])?;
896    /// ```
897    pub fn compile_with_tools(self, tools: &[&str]) -> Result<CompiledFlow, FlowErrors> {
898        self.compile_internal(Some(tools))
899    }
900
901    fn compile_internal(self, registry: Option<&[&str]>) -> Result<CompiledFlow, FlowErrors> {
902        let mut errors = Vec::new();
903        if let Err(err) = self.validate() {
904            errors.extend(err.issues.into_iter().map(FlowError::Invalid));
905        }
906
907        // Graph-shape checks (only meaningful once the graph is acyclic/valid).
908        if errors.is_empty() {
909            // Unreachable steps.
910            let reachable = self.reachable_steps();
911            for s in &self.steps {
912                if !reachable.contains(&s.id) {
913                    errors.push(FlowError::UnreachableStep(s.id.clone()));
914                }
915            }
916            // Ordering cycles across the combined `after` + `before(a, b)` edges.
917            // `validate()` only walks `after`; a cycle closed by a `Before`
918            // constraint deadlocks every step on it (none can become eligible).
919            if let Some(cycle) = self.ordering_cycle() {
920                errors.push(FlowError::OrderingCycle(cycle));
921            }
922        }
923
924        // A commit tool guarded by an always-true condition is effectively
925        // unguarded — the confirm-before-commit contract would never gate it.
926        for tool in &self.confirm_tools {
927            let guard = self.constraints.iter().find_map(|c| match c {
928                Constraint::NeverUntil { tool: t, until } if t == tool => Some(until),
929                _ => None,
930            });
931            let unguarded = matches!(guard, None | Some(Guard::Spec(Pred::Always)));
932            if unguarded {
933                errors.push(FlowError::UnguardedCommitTool(tool.clone()));
934            }
935        }
936
937        // An unsatisfiable `never(tool).until(guard)`: the guard's `done(step)`
938        // atom references a step that doesn't exist, so it can never latch and
939        // the tool is forbidden forever. (Step gate/done guards are already
940        // covered by `validate()`; constraints were not.)
941        let ids: BTreeSet<&str> = self.steps.iter().map(|s| s.id.as_str()).collect();
942        for c in &self.constraints {
943            if let Constraint::NeverUntil { tool, until } = c {
944                let mut refs = Vec::new();
945                until.referenced_steps(&mut refs);
946                for r in refs {
947                    if !ids.contains(r.as_str()) {
948                        errors.push(FlowError::UnsatisfiableGuard {
949                            tool: tool.clone(),
950                            step: r,
951                        });
952                    }
953                }
954            }
955        }
956
957        // Dangling tool names vs a known registry (opt-in).
958        if let Some(known) = registry {
959            for tool in self.tool_universe() {
960                if !known.contains(&tool.as_str()) {
961                    errors.push(FlowError::UnknownTool(tool));
962                }
963            }
964        }
965
966        if errors.is_empty() {
967            let surface = ToolSurface {
968                tools: self.tool_universe(),
969            };
970            Ok(CompiledFlow {
971                flow: self,
972                surface,
973            })
974        } else {
975            Err(FlowErrors(errors))
976        }
977    }
978
979    /// Find a cycle over the combined dependency edges (`after` plus
980    /// `before(a, b)` ordering constraints), if any. Returns the step ids on
981    /// the cycle path. `None` when the combined graph is acyclic.
982    fn ordering_cycle(&self) -> Option<Vec<String>> {
983        // Predecessor edges: step -> everything that must be done before it.
984        let mut deps: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
985        for s in &self.steps {
986            let entry = deps.entry(s.id.as_str()).or_default();
987            entry.extend(s.after.iter().map(|e| e.step.as_str()));
988        }
989        for c in &self.constraints {
990            if let Constraint::Before(a, b) = c {
991                deps.entry(b.as_str()).or_default().push(a.as_str());
992            }
993        }
994        // DFS with colors; on a back-edge, report the current path suffix.
995        fn dfs<'a>(
996            id: &'a str,
997            deps: &BTreeMap<&'a str, Vec<&'a str>>,
998            color: &mut BTreeMap<&'a str, u8>,
999            path: &mut Vec<&'a str>,
1000        ) -> Option<Vec<String>> {
1001            color.insert(id, 1);
1002            path.push(id);
1003            for d in deps.get(id).into_iter().flatten() {
1004                match color.get(d).copied() {
1005                    Some(1) => {
1006                        let start = path.iter().position(|p| p == d).unwrap_or(0);
1007                        return Some(
1008                            path[start..]
1009                                .iter()
1010                                .map(std::string::ToString::to_string)
1011                                .collect(),
1012                        );
1013                    }
1014                    Some(2) => {}
1015                    _ => {
1016                        if let Some(cycle) = dfs(d, deps, color, path) {
1017                            return Some(cycle);
1018                        }
1019                    }
1020                }
1021            }
1022            path.pop();
1023            color.insert(id, 2);
1024            None
1025        }
1026        let mut color: BTreeMap<&str, u8> = BTreeMap::new();
1027        for s in &self.steps {
1028            if color.get(s.id.as_str()).copied().unwrap_or(0) == 0 {
1029                let mut path = Vec::new();
1030                if let Some(cycle) = dfs(&s.id, &deps, &mut color, &mut path) {
1031                    return Some(cycle);
1032                }
1033            }
1034        }
1035        None
1036    }
1037
1038    fn has_cycle(&self) -> bool {
1039        // DFS with colors over the `after` dependency edges.
1040        let mut color: BTreeMap<&str, u8> = BTreeMap::new();
1041        fn dfs<'a>(flow: &'a Flow, id: &'a str, color: &mut BTreeMap<&'a str, u8>) -> bool {
1042            color.insert(id, 1);
1043            if let Some(step) = flow.step(id) {
1044                for d in &step.after {
1045                    match color.get(d.step.as_str()).copied() {
1046                        Some(1) => return true,
1047                        Some(2) => {}
1048                        _ => {
1049                            if dfs(flow, &d.step, color) {
1050                                return true;
1051                            }
1052                        }
1053                    }
1054                }
1055            }
1056            color.insert(id, 2);
1057            false
1058        }
1059        for s in &self.steps {
1060            if color.get(s.id.as_str()).copied().unwrap_or(0) == 0 && dfs(self, &s.id, &mut color) {
1061                return true;
1062            }
1063        }
1064        false
1065    }
1066
1067    /// Render the flow as a Mermaid `flowchart` — the spec *is* the diagram.
1068    pub fn to_mermaid(&self) -> String {
1069        let mut out = String::from("flowchart TD\n");
1070        for s in &self.steps {
1071            let shape = if s.terminal {
1072                format!("    {}([{}])\n", s.id, s.id)
1073            } else {
1074                format!("    {}[{}]\n", s.id, s.id)
1075            };
1076            out.push_str(&shape);
1077        }
1078        for s in &self.steps {
1079            for d in &s.after {
1080                match &d.when {
1081                    Some(when) => {
1082                        let label = when.describe().replace('|', "/");
1083                        out.push_str(&format!("    {} -->|{label}| {}\n", d.step, s.id));
1084                    }
1085                    None => out.push_str(&format!("    {} --> {}\n", d.step, s.id)),
1086                }
1087            }
1088        }
1089        out
1090    }
1091}
1092
1093/// The runtime position in a flow: which steps are done and how often each
1094/// tool has succeeded.
1095#[derive(Clone, Debug, Default)]
1096pub struct Marking {
1097    /// Steps that have latched done.
1098    pub done: BTreeSet<String>,
1099    /// Per-tool successful-completion counts.
1100    pub tool_ok: BTreeMap<String, u32>,
1101    /// Turns observed.
1102    pub turns: u32,
1103}
1104
1105/// The conformance status of a step.
1106#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1107#[serde(rename_all = "snake_case")]
1108pub enum Verdict {
1109    /// Not yet eligible.
1110    Pending,
1111    /// Eligible and awaiting completion.
1112    Active,
1113    /// Completed.
1114    Done,
1115    /// A successor completed while this never did (an out-of-order deviation).
1116    Skipped,
1117}
1118
1119/// A recorded conformance deviation (observe mode) or denial (enforce mode).
1120#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
1121pub struct Violation {
1122    /// What was attempted (e.g. a tool name).
1123    pub subject: String,
1124    /// Why it was a violation.
1125    pub reason: String,
1126}
1127
1128/// How a [`FlowMonitor`] treats off-path activity — enforcement vs observation.
1129///
1130/// Distinct from [`AgentMode`], which is the unrelated *resolver execution
1131/// discipline* (`Call`/`Dispatch`/`Background`).
1132#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
1133pub enum Enforcement {
1134    /// Block inadmissible tool calls and steer back on-path.
1135    #[default]
1136    Enforce,
1137    /// Allow everything, but record deviations for audit/analytics.
1138    Observe,
1139}
1140
1141/// An action fired the first time a step becomes active: run an agent in an
1142/// [`AgentMode`]. Built with [`on_enter`]. The result lands in `{name}:result` (the
1143/// name defaults to the step id), so a *downstream* step can complete on it via
1144/// [`Guard::resolved`] — this is how a flow drives orchestration in-session.
1145#[derive(Clone)]
1146pub struct StepAction {
1147    name: Option<String>,
1148    agent: Arc<dyn TextAgent>,
1149    mode: AgentMode,
1150}
1151
1152/// Build a step-enter action that runs `agent` in `mode` when the step first
1153/// activates. Pair with [`FlowMonitor::on_enter`].
1154///
1155/// ```ignore
1156/// let mon = FlowMonitor::new(flow, Enforcement::Enforce)
1157///     .on_enter("check", on_enter(availability_agent, AgentMode::Dispatch));
1158/// ```
1159pub fn on_enter(agent: Arc<dyn TextAgent>, mode: AgentMode) -> StepAction {
1160    StepAction {
1161        name: None,
1162        agent,
1163        mode,
1164    }
1165}
1166
1167impl StepAction {
1168    /// Override the result name (defaults to the step id it is attached to).
1169    pub fn named(mut self, name: impl Into<String>) -> Self {
1170        self.name = Some(name.into());
1171        self
1172    }
1173
1174    /// Run the action. `Call` awaits inline; `Dispatch`/`Background` spawn it
1175    /// detached so the turn is never blocked.
1176    pub(crate) async fn fire(&self, step_id: &str, state: &State) {
1177        let name = self.name.clone().unwrap_or_else(|| step_id.to_string());
1178        match self.mode {
1179            AgentMode::Call => {
1180                let _ = call_agent(&name, self.agent.clone(), state).await;
1181            }
1182            AgentMode::Dispatch | AgentMode::Background => {
1183                let agent = self.agent.clone();
1184                let state = state.clone();
1185                tokio::spawn(async move {
1186                    let _ = call_agent(&name, agent, &state).await;
1187                });
1188            }
1189        }
1190    }
1191}
1192
1193/// A shared, lock-protected [`FlowMonitor`] — the form in which the Live
1194/// control plane owns a governed flow, so runtime surfaces (e.g.
1195/// [`LiveHandle::explain`](crate::live::LiveHandle::explain)) can
1196/// snapshot it concurrently. All monitor methods are synchronous: lock
1197/// briefly and never hold the guard across an `await`.
1198pub type SharedFlowMonitor = Arc<parking_lot::Mutex<FlowMonitor>>;
1199
1200/// Observes the session trace, maintains the [`Marking`], answers tool
1201/// admissibility, and projects active postures.
1202pub struct FlowMonitor {
1203    flow: Flow,
1204    mode: Enforcement,
1205    marking: Marking,
1206    violations: Vec<Violation>,
1207    /// Per-step actions fired the first time the step becomes active.
1208    enter_actions: HashMap<String, StepAction>,
1209    /// Steps whose `on_enter` action has already fired (fire-once).
1210    announced: BTreeSet<String>,
1211    /// Previous guard values for `Constraint::Reset` rising-edge detection,
1212    /// in constraint order.
1213    reset_prev: Vec<bool>,
1214}
1215
1216impl FlowMonitor {
1217    /// Create a monitor for a (presumed-valid) flow.
1218    ///
1219    /// Prefer [`FlowMonitor::compiled`] or [`FlowMonitor::try_new`], which carry
1220    /// proof of compilation; this convenience skips compilation for flows already
1221    /// known valid (e.g. built in-process by trusted code).
1222    pub fn new(flow: Flow, mode: Enforcement) -> Self {
1223        Self {
1224            flow,
1225            mode,
1226            marking: Marking::default(),
1227            violations: Vec::new(),
1228            enter_actions: HashMap::new(),
1229            announced: BTreeSet::new(),
1230            reset_prev: Vec::new(),
1231        }
1232    }
1233
1234    /// Create a monitor from a [`CompiledFlow`] — the validated path.
1235    pub fn compiled(flow: CompiledFlow, mode: Enforcement) -> Self {
1236        Self::new(flow.into_flow(), mode)
1237    }
1238
1239    /// Compile `flow` and create a monitor, surfacing structural errors instead
1240    /// of trusting the caller.
1241    pub fn try_new(flow: Flow, mode: Enforcement) -> Result<Self, FlowErrors> {
1242        Ok(Self::compiled(flow.compile()?, mode))
1243    }
1244
1245    /// Wrap this monitor in a [`SharedFlowMonitor`] for shared ownership
1246    /// between the control lane (which advances it) and runtime accessors
1247    /// (which snapshot it, e.g.
1248    /// [`LiveHandle::explain`](crate::live::LiveHandle::explain)).
1249    pub fn into_shared(self) -> SharedFlowMonitor {
1250        Arc::new(parking_lot::Mutex::new(self))
1251    }
1252
1253    /// Wrap this monitor as the main layer of a [`FlowStack`] with no
1254    /// digressions — the form the Live control plane drives.
1255    pub fn into_stack(self) -> FlowStack {
1256        FlowStack::from_monitor(self)
1257    }
1258
1259    /// Re-enter the flow from its start: forget the marking, the fired
1260    /// `on_enter` actions and the reset edges. The flow, mode and registered
1261    /// actions are kept, and recorded violations stay for audit. `State` is not
1262    /// touched — the next re-latch runs against whatever facts it holds.
1263    pub fn restart(&mut self) {
1264        self.marking = Marking::default();
1265        self.announced.clear();
1266        self.reset_prev.clear();
1267    }
1268
1269    /// Explain the current control-plane state: active steps, which tools are
1270    /// admitted vs blocked (with reasons), and unmet requirements.
1271    ///
1272    /// This is the deterministic answer to "why did the assistant ask that?" —
1273    /// model-readable, without the model driving control flow.
1274    pub fn explain(&self, state: &State) -> FlowExplanation {
1275        let active_steps = self.active_steps(state);
1276        let active: Vec<String> = active_steps.iter().map(|s| s.id.clone()).collect();
1277        let ctx = self.ctx(state);
1278        let active_progress = active_steps
1279            .iter()
1280            .filter_map(|s| {
1281                s.done
1282                    .as_ref()
1283                    .map(|g| (s.id.clone(), g.explain_trace(&ctx)))
1284            })
1285            .collect();
1286        let mut allowed_tools = Vec::new();
1287        let mut blocked_tools = BTreeMap::new();
1288        for tool in self.flow.tool_universe() {
1289            match self.admits_tool(&tool, state) {
1290                Ok(()) => allowed_tools.push(tool),
1291                Err(reason) => {
1292                    blocked_tools.insert(tool, reason);
1293                }
1294            }
1295        }
1296        FlowExplanation {
1297            active,
1298            allowed_tools,
1299            blocked_tools,
1300            missing_requirements: self.unmet_requirements(),
1301            active_progress,
1302        }
1303    }
1304
1305    /// Attach an action fired the first time `step` becomes active (see
1306    /// [`on_enter`](on_enter())). Chainable at construction time.
1307    pub fn on_enter(mut self, step: impl Into<String>, action: StepAction) -> Self {
1308        self.enter_actions.insert(step.into(), action);
1309        self
1310    }
1311
1312    /// Steps that became active since the last call — each reported exactly once
1313    /// over the session. Drives [`on_enter`](Self::on_enter) firing.
1314    pub fn take_newly_active(&mut self, state: &State) -> Vec<String> {
1315        let mut fresh = Vec::new();
1316        for s in self.active_steps(state) {
1317            if !self.announced.contains(&s.id) {
1318                fresh.push(s.id.clone());
1319            }
1320        }
1321        for id in &fresh {
1322            self.announced.insert(id.clone());
1323        }
1324        fresh
1325    }
1326
1327    /// The enter-action registered for a step, if any.
1328    pub fn enter_action(&self, step: &str) -> Option<&StepAction> {
1329        self.enter_actions.get(step)
1330    }
1331
1332    /// Fire enter-actions for every step that just became active. Convenience
1333    /// over [`take_newly_active`](Self::take_newly_active) + [`enter_action`](Self::enter_action);
1334    /// call it right after [`on_turn`](Self::on_turn).
1335    pub async fn fire_enter_actions(&mut self, state: &State) {
1336        for id in self.take_newly_active(state) {
1337            if let Some(action) = self.enter_actions.get(&id) {
1338                action.fire(&id, state).await;
1339            }
1340        }
1341    }
1342
1343    /// The enforcement mode this monitor runs in.
1344    pub fn mode(&self) -> Enforcement {
1345        self.mode
1346    }
1347
1348    /// Replace a step's posture in place. Returns `false` if no such step.
1349    ///
1350    /// Postures are re-projected at every turn boundary, so an edit takes
1351    /// effect on the next turn — the safe subset of live spec editing. The
1352    /// DAG, guards, and tool gates are structural and stay fixed.
1353    pub fn set_posture(&mut self, step_id: &str, posture: Option<String>) -> bool {
1354        match self.flow.steps.iter_mut().find(|s| s.id == step_id) {
1355            Some(step) => {
1356                step.posture = posture;
1357                true
1358            }
1359            None => false,
1360        }
1361    }
1362
1363    /// Replace a step's grounding template in place. Returns `false` if no
1364    /// such step. Same next-turn semantics as [`set_posture`](Self::set_posture).
1365    pub fn set_ground(&mut self, step_id: &str, ground: Option<String>) -> bool {
1366        match self.flow.steps.iter_mut().find(|s| s.id == step_id) {
1367            Some(step) => {
1368                step.ground = ground;
1369                true
1370            }
1371            None => false,
1372        }
1373    }
1374
1375    /// Evaluate a [`Guard`] against this monitor's current context (the given
1376    /// `state` plus the monitor's marking). Used to test overlay/digression
1377    /// triggers without exposing the internal context.
1378    pub fn eval(&self, guard: &Guard, state: &State) -> bool {
1379        guard.eval(&self.ctx(state))
1380    }
1381    /// The current marking.
1382    pub fn marking(&self) -> &Marking {
1383        &self.marking
1384    }
1385    /// Recorded violations.
1386    pub fn violations(&self) -> &[Violation] {
1387        &self.violations
1388    }
1389
1390    /// Record a deviation the *caller* detected — one this monitor cannot see
1391    /// for itself. A [`FlowStack`] uses it for a tool called after a
1392    /// `Resume::Terminate` digression ended the conversation: the denial is the
1393    /// stack's, not this flow's, so `observe_tool` would find nothing wrong.
1394    pub fn record_violation(&mut self, subject: impl Into<String>, reason: impl Into<String>) {
1395        self.violations.push(Violation {
1396            subject: subject.into(),
1397            reason: reason.into(),
1398        });
1399    }
1400    /// The underlying flow.
1401    pub fn flow(&self) -> &Flow {
1402        &self.flow
1403    }
1404
1405    fn ctx<'a>(&'a self, state: &'a State) -> FlowCtx<'a> {
1406        FlowCtx {
1407            state,
1408            marking: &self.marking,
1409        }
1410    }
1411
1412    fn eligible(&self, step: &Step, state: &State) -> bool {
1413        let ctx = self.ctx(state);
1414        let edge_ok = |e: &Edge| {
1415            self.marking.done.contains(&e.step)
1416                && e.when.as_ref().map(|g| g.eval(&ctx)).unwrap_or(true)
1417        };
1418        let deps_done = if step.after.is_empty() {
1419            true
1420        } else {
1421            match step.join {
1422                Join::All => step.after.iter().all(edge_ok),
1423                Join::Any => step.after.iter().any(edge_ok),
1424            }
1425        };
1426        // Enforce `Constraint::Before(a, step)`: `a` must be done before this
1427        // step may start (an ordering constraint declared outside `after`).
1428        let before_ok = self.flow.constraints.iter().all(|c| match c {
1429            Constraint::Before(a, b) if *b == step.id => self.marking.done.contains(a),
1430            _ => true,
1431        });
1432        let gate_ok = step
1433            .gate
1434            .as_ref()
1435            .map(|g| g.eval(&self.ctx(state)))
1436            .unwrap_or(true);
1437        deps_done && before_ok && gate_ok
1438    }
1439
1440    /// Re-evaluate completion latches to a fixpoint. Call after any event that
1441    /// can change state or the marking (turn boundary, tool completion).
1442    ///
1443    /// [`Constraint::Reset`] constraints are applied first, on their guard's
1444    /// rising edge: the named steps un-latch, their `on_enter` re-arms, and
1445    /// the `called_ok` evidence their completion guards reference is forgiven
1446    /// — the loop primitive over an otherwise monotonic marking.
1447    pub fn relatch(&mut self, state: &State) {
1448        self.apply_resets(state);
1449        loop {
1450            let mut newly_done: Vec<String> = Vec::new();
1451            for s in &self.flow.steps {
1452                if self.marking.done.contains(&s.id) {
1453                    continue;
1454                }
1455                if !self.eligible(s, state) {
1456                    continue;
1457                }
1458                let complete = if s.terminal {
1459                    true
1460                } else {
1461                    s.done
1462                        .as_ref()
1463                        .map(|g| g.eval(&self.ctx(state)))
1464                        .unwrap_or(false)
1465                };
1466                if complete {
1467                    newly_done.push(s.id.clone());
1468                }
1469            }
1470            if newly_done.is_empty() {
1471                break;
1472            }
1473            for id in newly_done {
1474                self.marking.done.insert(id);
1475            }
1476        }
1477    }
1478
1479    /// Apply [`Constraint::Reset`] constraints on their guards' rising edges.
1480    /// Returns the steps that were un-latched.
1481    fn apply_resets(&mut self, state: &State) -> Vec<String> {
1482        let mut reset: Vec<String> = Vec::new();
1483        // Evaluate all guards first (immutable borrow), then mutate.
1484        let mut edges: Vec<(usize, bool)> = Vec::new();
1485        {
1486            let ctx = self.ctx(state);
1487            for (i, c) in self.flow.constraints.iter().enumerate() {
1488                if let Constraint::Reset { when, .. } = c {
1489                    edges.push((i, when.eval(&ctx)));
1490                }
1491            }
1492        }
1493        for (slot, (index, now)) in edges.into_iter().enumerate() {
1494            let prev = self.reset_prev.get(slot).copied().unwrap_or(false);
1495            if self.reset_prev.len() <= slot {
1496                self.reset_prev.resize(slot + 1, false);
1497            }
1498            self.reset_prev[slot] = now;
1499            if !now || prev {
1500                continue;
1501            }
1502            let Constraint::Reset { steps, .. } = &self.flow.constraints[index] else {
1503                continue;
1504            };
1505            let steps = steps.clone();
1506            for step_id in &steps {
1507                if !self.marking.done.remove(step_id) {
1508                    continue;
1509                }
1510                reset.push(step_id.clone());
1511                self.announced.remove(step_id);
1512                // Forgive the completion evidence this step's done guard
1513                // references, so `called_ok` (and any `once` on those tools)
1514                // count per latch-cycle. State keys stay the app's to clear.
1515                if let Some(step) = self.flow.steps.iter().find(|s| &s.id == step_id) {
1516                    let mut tools = Vec::new();
1517                    if let Some(done) = &step.done {
1518                        done.referenced_tools(&mut tools);
1519                    }
1520                    for tool in tools {
1521                        self.marking.tool_ok.remove(&tool);
1522                    }
1523                }
1524            }
1525        }
1526        reset
1527    }
1528
1529    /// Record a turn boundary, then re-latch.
1530    pub fn on_turn(&mut self, state: &State) {
1531        self.begin_turn(state);
1532        self.relatch(state);
1533    }
1534
1535    /// The first half of [`on_turn`](Self::on_turn): count the turn and apply
1536    /// [`Constraint::Reset`] edges, returning the steps that were un-latched.
1537    ///
1538    /// A caller that keeps evidence *about* steps outside the marking — the
1539    /// [`FlowStack`] and its repair signals — needs to see a reset before the
1540    /// re-latch runs, or a completion guard that references that evidence
1541    /// re-completes the step on the spot. Follow with
1542    /// [`relatch`](Self::relatch); the reset edges are consumed, so the
1543    /// re-latch does not apply them twice.
1544    pub fn begin_turn(&mut self, state: &State) -> Vec<String> {
1545        self.marking.turns += 1;
1546        self.apply_resets(state)
1547    }
1548
1549    /// Record a successful tool call, then re-latch.
1550    pub fn on_tool_ok(&mut self, tool: &str, state: &State) {
1551        self.begin_tool_ok(tool, state);
1552        self.relatch(state);
1553    }
1554
1555    /// The first half of [`on_tool_ok`](Self::on_tool_ok): count the call and
1556    /// apply [`Constraint::Reset`] edges, returning the steps that were
1557    /// un-latched.
1558    ///
1559    /// A reset can be gated on a tool — `reset(..).when(called_ok("start_over"))`
1560    /// — in which case its edge fires here rather than at a turn boundary. A
1561    /// caller holding evidence outside the marking needs the same chance to shed
1562    /// it that [`begin_turn`](Self::begin_turn) gives; follow with
1563    /// [`relatch`](Self::relatch).
1564    pub fn begin_tool_ok(&mut self, tool: &str, state: &State) -> Vec<String> {
1565        *self.marking.tool_ok.entry(tool.to_string()).or_insert(0) += 1;
1566        self.apply_resets(state)
1567    }
1568
1569    /// Steps that are eligible but not yet done.
1570    pub fn active_steps(&self, state: &State) -> Vec<&Step> {
1571        self.flow
1572            .steps
1573            .iter()
1574            .filter(|s| !self.marking.done.contains(&s.id) && self.eligible(s, state))
1575            .collect()
1576    }
1577
1578    /// Postures of the active steps — to inject as turn-boundary steering.
1579    pub fn active_postures(&self, state: &State) -> Vec<String> {
1580        self.active_steps(state)
1581            .into_iter()
1582            .filter_map(|s| s.posture.clone())
1583            .collect()
1584    }
1585
1586    /// Rendered grounding lines of the active steps — curated, `State`-
1587    /// interpolated facts to inject as turn-boundary steering (anti-hallucination).
1588    pub fn active_grounds(&self, state: &State) -> Vec<String> {
1589        self.active_steps(state)
1590            .into_iter()
1591            .filter_map(|s| s.ground.as_ref().map(|t| render_ground(t, state)))
1592            .filter(|s| !s.trim().is_empty())
1593            .collect()
1594    }
1595
1596    /// Terminal steps that are done — the flow's closing.
1597    ///
1598    /// A terminal step completes on eligibility, so it is never *active* and
1599    /// its posture is never among [`active_postures`](Self::active_postures).
1600    /// Its instruction is the flow's last word ("hand off to a human now"),
1601    /// which the [`FlowStack`] projects on the turn a digression completes.
1602    pub fn closing_steps(&self) -> Vec<&Step> {
1603        self.flow
1604            .steps
1605            .iter()
1606            .filter(|s| s.terminal && self.marking.done.contains(&s.id))
1607            .collect()
1608    }
1609
1610    /// Postures of the [`closing_steps`](Self::closing_steps).
1611    pub fn closing_postures(&self) -> Vec<String> {
1612        self.closing_steps()
1613            .into_iter()
1614            .filter_map(|s| s.posture.clone())
1615            .collect()
1616    }
1617
1618    /// Rendered grounding lines of the [`closing_steps`](Self::closing_steps).
1619    pub fn closing_grounds(&self, state: &State) -> Vec<String> {
1620        self.closing_steps()
1621            .into_iter()
1622            .filter_map(|s| s.ground.as_ref().map(|t| render_ground(t, state)))
1623            .filter(|s| !s.trim().is_empty())
1624            .collect()
1625    }
1626
1627    /// Required steps not yet done (drives repair).
1628    pub fn unmet_requirements(&self) -> Vec<String> {
1629        self.flow
1630            .constraints
1631            .iter()
1632            .flat_map(|c| match c {
1633                Constraint::Require(rs) => rs.clone(),
1634                _ => Vec::new(),
1635            })
1636            .filter(|r| !self.marking.done.contains(r))
1637            .collect()
1638    }
1639
1640    /// Whether all required steps are done.
1641    pub fn is_complete(&self) -> bool {
1642        self.unmet_requirements().is_empty()
1643    }
1644
1645    /// The conformance verdict for a step.
1646    pub fn verdict(&self, step_id: &str, state: &State) -> Verdict {
1647        if self.marking.done.contains(step_id) {
1648            return Verdict::Done;
1649        }
1650        if let Some(step) = self.flow.step(step_id)
1651            && self.eligible(step, state)
1652        {
1653            return Verdict::Active;
1654        }
1655        // Skipped: a successor is done but this step never completed.
1656        let bypassed = self.flow.steps.iter().any(|s| {
1657            s.after.iter().any(|d| d.step == step_id) && self.marking.done.contains(&s.id)
1658        });
1659        if bypassed {
1660            Verdict::Skipped
1661        } else {
1662            Verdict::Pending
1663        }
1664    }
1665
1666    /// Decide whether a tool call may proceed. `Ok(())` admits it; `Err(reason)`
1667    /// denies it (the caller blocks in Enforce mode, or records in Observe).
1668    pub fn admits_tool(&self, tool: &str, state: &State) -> Result<(), String> {
1669        match self.admissibility(tool, state) {
1670            Ok(()) => Ok(()),
1671            Err(denial) => Err(self.render_denial(tool, &denial, state)),
1672        }
1673    }
1674
1675    /// The admissibility decision, before it is put into words.
1676    ///
1677    /// Split from [`admits_tool`](Self::admits_tool) so that rendering a refusal
1678    /// can itself ask what *is* admissible without recursing: this function never
1679    /// renders, so `render_denial` may call it over the whole tool universe.
1680    fn admissibility(&self, tool: &str, state: &State) -> Result<(), Denial> {
1681        // 1. once(tool)
1682        for c in &self.flow.constraints {
1683            if let Constraint::Once(t) = c
1684                && t == tool
1685                && self.marking.tool_ok.contains_key(tool)
1686            {
1687                return Err(Denial::OnceExhausted);
1688            }
1689        }
1690        // 2. never(tool).until(guard)
1691        for c in &self.flow.constraints {
1692            if let Constraint::NeverUntil { tool: t, until } = c
1693                && t == tool
1694                && !until.eval(&self.ctx(state))
1695            {
1696                return Err(Denial::NotYet(until.describe()));
1697            }
1698        }
1699        // 3. active allow/deny (whitelist while any active step restricts).
1700        let active = self.active_steps(state);
1701        if let Some(step) = active.iter().find(|s| s.deny.iter().any(|d| d == tool)) {
1702            return Err(Denial::DeniedByStep(step.id.clone()));
1703        }
1704        // An ambient tool is exempt from the whitelist's exclusion-by-omission,
1705        // having already cleared every constraint that names it explicitly.
1706        if self.flow.ambient.iter().any(|a| a == tool) {
1707            return Ok(());
1708        }
1709        let restricting: Vec<&&Step> = active.iter().filter(|s| !s.allow.is_empty()).collect();
1710        if !restricting.is_empty()
1711            && !restricting
1712                .iter()
1713                .any(|s| s.allow.iter().any(|a| a == tool))
1714        {
1715            return Err(Denial::NotInStep(
1716                restricting.iter().map(|s| s.id.clone()).collect(),
1717            ));
1718        }
1719        Ok(())
1720    }
1721
1722    /// Put a refusal into words the model can act on.
1723    ///
1724    /// A refusal reaches the model as a tool error, and it is the only thing the
1725    /// model learns about the gate. "'X' is not available in the current step"
1726    /// names what failed and nothing about what would succeed, so the model is
1727    /// left to infer the precondition — and in the governed collections
1728    /// evaluation it inferred wrongly, re-asking a verified caller for their card
1729    /// digits after `record_promise_to_pay` was refused. The monitor knows the
1730    /// active step, what that step is waiting for, and which tools *are*
1731    /// admitted; every refusal now carries it.
1732    fn render_denial(&self, tool: &str, denial: &Denial, state: &State) -> String {
1733        let head = match denial {
1734            Denial::OnceExhausted => {
1735                // Nothing to redirect to: the answer is "you already did this".
1736                return format!(
1737                    "'{tool}' has already run and may run only once in this \
1738                     conversation. Do not call it again."
1739                );
1740            }
1741            Denial::NotYet(condition) => {
1742                format!("'{tool}' is not permitted yet — first, {condition}.")
1743            }
1744            Denial::DeniedByStep(step) => {
1745                format!("'{tool}' is not allowed during the current step ('{step}').")
1746            }
1747            Denial::NotInStep(steps) => format!(
1748                "'{tool}' is not part of the current step ({}).",
1749                steps
1750                    .iter()
1751                    .map(|s| format!("'{s}'"))
1752                    .collect::<Vec<_>>()
1753                    .join(", ")
1754            ),
1755        };
1756
1757        // What the model may do instead. Drawn from the flow's own tool
1758        // universe, so it never invents a tool the session has not declared.
1759        let available: Vec<String> = self
1760            .flow
1761            .tool_universe()
1762            .into_iter()
1763            .filter(|t| self.admissibility(t, state).is_ok())
1764            .collect();
1765
1766        let mut out = head;
1767        if available.is_empty() {
1768            out.push_str(" No tool is available right now — continue the conversation instead.");
1769        } else {
1770            out.push_str(" Available now: ");
1771            out.push_str(&available.join(", "));
1772            out.push('.');
1773        }
1774        // The posture is the step's own words for what it wants; repeating it
1775        // here means the redirection and the reason arrive together, rather than
1776        // the model having to reconcile an error with steering sent earlier.
1777        let postures = self.active_postures(state);
1778        if let Some(first) = postures.first() {
1779            out.push(' ');
1780            out.push_str(first);
1781        }
1782        out
1783    }
1784
1785    /// Observe a tool call for conformance. In Enforce mode the caller has
1786    /// already gated via [`admits_tool`](Self::admits_tool); this records the
1787    /// call and, in Observe mode, logs a deviation if it was inadmissible.
1788    pub fn observe_tool(&mut self, tool: &str, ok: bool, state: &State) {
1789        if self.mode == Enforcement::Observe
1790            && let Err(reason) = self.admits_tool(tool, state)
1791        {
1792            self.violations.push(Violation {
1793                subject: tool.to_string(),
1794                reason,
1795            });
1796        }
1797        if ok {
1798            self.on_tool_ok(tool, state);
1799        }
1800    }
1801}
1802
1803/// Why a tool call was refused, before it is rendered into prose.
1804///
1805/// Structured rather than a string so the renderer can add the redirection —
1806/// what *is* available, and what the active step is waiting for — which is the
1807/// half a model needs and the old message never carried.
1808enum Denial {
1809    /// A `once(tool)` constraint, already spent.
1810    OnceExhausted,
1811    /// A `never(tool).until(guard)` whose guard has not latched; carries the
1812    /// rendered guard.
1813    NotYet(String),
1814    /// The named active step lists the tool in `deny`.
1815    DeniedByStep(String),
1816    /// Active steps restrict by `allow` and none of them names the tool;
1817    /// carries the restricting step ids.
1818    NotInStep(Vec<String>),
1819}
1820
1821/// A single problem found while compiling a [`Flow`].
1822#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1823#[serde(tag = "kind", content = "detail", rename_all = "snake_case")]
1824pub enum FlowError {
1825    /// A referential-integrity or acyclicity error from [`Flow::validate`].
1826    Invalid(String),
1827    /// A step that no path from a root can ever reach.
1828    UnreachableStep(String),
1829    /// A commit (confirm) tool whose gate is always true — effectively
1830    /// unguarded, defeating confirm-before-commit.
1831    UnguardedCommitTool(String),
1832    /// A tool referenced by the flow (step `allow`/`deny`, `once`,
1833    /// `never…until`, confirm) that is not in the registry given to
1834    /// [`Flow::compile_with_tools`].
1835    UnknownTool(String),
1836    /// A `never(tool).until(guard)` whose guard references a step id that
1837    /// doesn't exist — the guard can never latch, so the tool would be
1838    /// forbidden forever.
1839    UnsatisfiableGuard {
1840        /// The tool the constraint gates.
1841        tool: String,
1842        /// The unknown step id the guard's `done(..)` atom references.
1843        step: String,
1844    },
1845    /// A cycle over the combined `after` + `before(a, b)` ordering edges —
1846    /// every step on the cycle waits on another, so none can ever become
1847    /// eligible. Contains the step ids on the cycle.
1848    OrderingCycle(Vec<String>),
1849}
1850
1851impl std::fmt::Display for FlowError {
1852    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1853        match self {
1854            FlowError::Invalid(m) => write!(f, "{m}"),
1855            FlowError::UnreachableStep(id) => {
1856                write!(f, "step '{id}' is unreachable from any root")
1857            }
1858            FlowError::UnguardedCommitTool(t) => write!(
1859                f,
1860                "commit tool '{t}' is guarded by an always-true condition (effectively unguarded)"
1861            ),
1862            FlowError::UnknownTool(t) => write!(
1863                f,
1864                "flow references tool '{t}' which is not in the provided tool registry"
1865            ),
1866            FlowError::UnsatisfiableGuard { tool, step } => write!(
1867                f,
1868                "`never('{tool}').until(..)` references unknown step '{step}' — the guard can \
1869                 never hold, so '{tool}' would be forbidden forever"
1870            ),
1871            FlowError::OrderingCycle(steps) => write!(
1872                f,
1873                "ordering cycle across `after`/`before` edges: {} (no step on it can ever start)",
1874                steps.join(" -> ")
1875            ),
1876        }
1877    }
1878}
1879
1880/// All problems found while compiling a [`Flow`]; non-empty on failure.
1881#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1882pub struct FlowErrors(pub Vec<FlowError>);
1883
1884impl std::fmt::Display for FlowErrors {
1885    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1886        writeln!(f, "flow failed to compile ({} error(s)):", self.0.len())?;
1887        for e in &self.0 {
1888            writeln!(f, "  - {e}")?;
1889        }
1890        Ok(())
1891    }
1892}
1893
1894impl std::error::Error for FlowErrors {}
1895
1896/// The precomputed tool surface of a compiled flow: every tool name the flow
1897/// reasons about (step `allow`/`deny`, `once`, `never…until`, confirm), so
1898/// introspection can enumerate and explain gating decisions. Distinct from
1899/// [`tool::ToolPolicy`](crate::tool::ToolPolicy), which is a per-tool runtime
1900/// policy (timeout/cache/confirm).
1901#[derive(Debug, Clone, Default)]
1902pub struct ToolSurface {
1903    /// Every tool referenced anywhere in the flow.
1904    pub tools: BTreeSet<String>,
1905}
1906
1907/// A validated [`Flow`] plus its precomputed [`ToolSurface`].
1908///
1909/// Produced by [`Flow::compile`]. Holding one is proof the flow passed
1910/// compilation, so the runtime never re-discovers structural errors. This is the
1911/// IR the conversation compiler targets and the type richer surfaces build on.
1912#[derive(Debug, Clone)]
1913pub struct CompiledFlow {
1914    flow: Flow,
1915    surface: ToolSurface,
1916}
1917
1918impl CompiledFlow {
1919    /// The underlying validated flow.
1920    pub fn flow(&self) -> &Flow {
1921        &self.flow
1922    }
1923    /// The precomputed tool surface.
1924    pub fn tool_surface(&self) -> &ToolSurface {
1925        &self.surface
1926    }
1927    /// Render the flow as a Mermaid diagram.
1928    pub fn to_mermaid(&self) -> String {
1929        self.flow.to_mermaid()
1930    }
1931    /// Consume into the inner flow.
1932    pub fn into_flow(self) -> Flow {
1933        self.flow
1934    }
1935}
1936
1937/// A model-readable explanation of the current control-plane state — the
1938/// foundation of `why did the assistant ask that?`.
1939///
1940/// Produced by [`FlowMonitor::explain`]. `Serialize` so it can be surfaced to a
1941/// model, a devtool, or a log without the model driving control flow.
1942#[derive(Debug, Clone, Serialize)]
1943pub struct FlowExplanation {
1944    /// Steps eligible-but-not-done right now.
1945    pub active: Vec<String>,
1946    /// Tools currently admitted.
1947    pub allowed_tools: Vec<String>,
1948    /// Tools currently blocked, mapped to the reason.
1949    pub blocked_tools: BTreeMap<String, String>,
1950    /// Required steps not yet done (drives repair).
1951    pub missing_requirements: Vec<String>,
1952    /// Per-active-step completion-guard truth trees: exactly which atom each
1953    /// stuck step is waiting on. Terminal steps (no `done` guard) are absent.
1954    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1955    pub active_progress: BTreeMap<String, GuardTrace>,
1956}
1957
1958/// Builder for a [`Flow`] using the cemented verbs.
1959#[derive(Default)]
1960pub struct FlowBuilder {
1961    steps: Vec<Step>,
1962    constraints: Vec<Constraint>,
1963    confirm_tools: Vec<String>,
1964    ambient: Vec<String>,
1965}
1966
1967impl FlowBuilder {
1968    fn current(&mut self) -> &mut Step {
1969        self.steps
1970            .last_mut()
1971            .expect("call `.step(id)` before configuring a step")
1972    }
1973
1974    /// Declare a new step.
1975    pub fn step(mut self, id: impl Into<String>) -> Self {
1976        self.steps.push(Step {
1977            id: id.into(),
1978            after: Vec::new(),
1979            join: Join::default(),
1980            gate: None,
1981            done: None,
1982            posture: None,
1983            ground: None,
1984            allow: Vec::new(),
1985            deny: Vec::new(),
1986            terminal: false,
1987        });
1988        self
1989    }
1990    /// Add a dependency (call multiple times for multiple deps).
1991    pub fn after(mut self, dep: impl Into<String>) -> Self {
1992        self.current().after.push(Edge::to(dep));
1993        self
1994    }
1995    /// Add a **conditional** dependency: satisfied only while `when` holds.
1996    /// Conditional edges out of one source are how a flow branches; pair with
1997    /// [`join_any`](Self::join_any) on the merge step.
1998    pub fn after_when(mut self, dep: impl Into<String>, when: Guard) -> Self {
1999        self.current().after.push(Edge::when(dep, when));
2000        self
2001    }
2002    /// Any one satisfied edge makes this step eligible (the merge node after
2003    /// a branch). Default is `all` — a synchronizing join.
2004    pub fn join_any(mut self) -> Self {
2005        self.current().join = Join::Any;
2006        self
2007    }
2008    /// Extra eligibility guard beyond dependencies.
2009    pub fn gate(mut self, g: Guard) -> Self {
2010        self.current().gate = Some(g);
2011        self
2012    }
2013    /// Completion condition.
2014    pub fn done(mut self, g: Guard) -> Self {
2015        self.current().done = Some(g);
2016        self
2017    }
2018    /// Instruction imposed while active.
2019    pub fn posture(mut self, text: impl Into<String>) -> Self {
2020        self.current().posture = Some(text.into());
2021        self
2022    }
2023    /// A grounding template projected while active — a curated, `State`-
2024    /// interpolated fact line that pins the model to known values. `{key}`
2025    /// interpolates a value; `{key?yes:no}` picks by truthiness. See
2026    /// [`render_ground`].
2027    pub fn ground(mut self, template: impl Into<String>) -> Self {
2028        self.current().ground = Some(template.into());
2029        self
2030    }
2031    /// Tools available while active (whitelist).
2032    pub fn allow<I, S>(mut self, tools: I) -> Self
2033    where
2034        I: IntoIterator<Item = S>,
2035        S: Into<String>,
2036    {
2037        self.current()
2038            .allow
2039            .extend(tools.into_iter().map(Into::into));
2040        self
2041    }
2042    /// Tools forbidden while active.
2043    pub fn deny<I, S>(mut self, tools: I) -> Self
2044    where
2045        I: IntoIterator<Item = S>,
2046        S: Into<String>,
2047    {
2048        self.current()
2049            .deny
2050            .extend(tools.into_iter().map(Into::into));
2051        self
2052    }
2053    /// Mark the current step terminal.
2054    pub fn terminal(mut self) -> Self {
2055        self.current().terminal = true;
2056        self
2057    }
2058
2059    /// A tool may run at most once.
2060    pub fn once(mut self, tool: impl Into<String>) -> Self {
2061        self.constraints.push(Constraint::Once(tool.into()));
2062        self
2063    }
2064    /// Ordering invariant: `a` before `b`.
2065    pub fn before(mut self, a: impl Into<String>, b: impl Into<String>) -> Self {
2066        self.constraints
2067            .push(Constraint::Before(a.into(), b.into()));
2068        self
2069    }
2070    /// Required terminal steps for completion.
2071    pub fn require<I, S>(mut self, steps: I) -> Self
2072    where
2073        I: IntoIterator<Item = S>,
2074        S: Into<String>,
2075    {
2076        self.constraints.push(Constraint::Require(
2077            steps.into_iter().map(Into::into).collect(),
2078        ));
2079        self
2080    }
2081    /// Exempt cross-cutting tools from every step's `allow` whitelist.
2082    ///
2083    /// Flow-level, and order-independent with respect to `.step(..)`. Use it for
2084    /// tools that serve the conversation rather than any one step of it —
2085    /// memory recall, escalation, logging. A tool named here still obeys
2086    /// `deny`, `once` and `never(..).until(..)`; see [`Flow::ambient`].
2087    ///
2088    /// ```
2089    /// # use gemini_adk_rs::flow::{Flow, Guard};
2090    /// let flow = Flow::new()
2091    ///     .ambient(["recall_context"])
2092    ///     .step("book")
2093    ///     .allow(["book_table"])
2094    ///     .done(Guard::called_ok("book_table"))
2095    ///     .build()
2096    ///     .unwrap();
2097    /// assert_eq!(flow.ambient, ["recall_context"]);
2098    /// ```
2099    pub fn ambient<I, S>(mut self, tools: I) -> Self
2100    where
2101        I: IntoIterator<Item = S>,
2102        S: Into<String>,
2103    {
2104        self.ambient.extend(tools.into_iter().map(Into::into));
2105        self
2106    }
2107
2108    /// Un-latch `steps` whenever the guard given to
2109    /// [`when`](ResetBuilder::when) *becomes* true — the loop primitive.
2110    /// See [`Constraint::Reset`] for the exact forgiveness semantics.
2111    pub fn reset<I, S>(self, steps: I) -> ResetBuilder
2112    where
2113        I: IntoIterator<Item = S>,
2114        S: Into<String>,
2115    {
2116        ResetBuilder {
2117            builder: self,
2118            steps: steps.into_iter().map(Into::into).collect(),
2119        }
2120    }
2121
2122    /// Forbid a tool until a guard holds (`never(tool).until(guard)`).
2123    pub fn never(self, tool: impl Into<String>) -> NeverBuilder {
2124        NeverBuilder {
2125            fb: self,
2126            tool: tool.into(),
2127        }
2128    }
2129    /// Commit-tool sugar: at most once, gated until `until`, and flagged for
2130    /// confirmation. Composes `once` + `never…until` + the confirmation seam.
2131    pub fn commit(mut self, tool: impl Into<String>, until: Guard) -> Self {
2132        let tool = tool.into();
2133        self.constraints.push(Constraint::Once(tool.clone()));
2134        self.constraints.push(Constraint::NeverUntil {
2135            tool: tool.clone(),
2136            until,
2137        });
2138        self.confirm_tools.push(tool);
2139        self
2140    }
2141
2142    /// Finalize and validate the flow.
2143    pub fn build(self) -> Result<Flow, ConfigError> {
2144        let flow = Flow {
2145            steps: self.steps,
2146            constraints: self.constraints,
2147            confirm_tools: self.confirm_tools,
2148            ambient: self.ambient,
2149        };
2150        flow.validate()?;
2151        Ok(flow)
2152    }
2153}
2154
2155/// Sub-builder for `never(tool).until(guard)`.
2156pub struct NeverBuilder {
2157    fb: FlowBuilder,
2158    tool: String,
2159}
2160
2161impl NeverBuilder {
2162    /// Permit the tool once the guard holds.
2163    pub fn until(mut self, guard: Guard) -> FlowBuilder {
2164        self.fb.constraints.push(Constraint::NeverUntil {
2165            tool: self.tool,
2166            until: guard,
2167        });
2168        self.fb
2169    }
2170}
2171
2172/// Intermediate for `reset(steps).when(guard)`.
2173pub struct ResetBuilder {
2174    builder: FlowBuilder,
2175    steps: Vec<String>,
2176}
2177
2178impl ResetBuilder {
2179    /// Fire the reset on this guard's rising edge.
2180    pub fn when(mut self, guard: Guard) -> FlowBuilder {
2181        self.builder.constraints.push(Constraint::Reset {
2182            steps: self.steps,
2183            when: guard,
2184        });
2185        self.builder
2186    }
2187}
2188
2189#[cfg(test)]
2190mod tests {
2191    use super::*;
2192    use serde_json::json;
2193
2194    /// A refusal must say what would succeed, not only what failed.
2195    ///
2196    /// The old message was "'X' is not available in the current step" — the
2197    /// tool's own name and nothing else. The model cannot act on that: it knows
2198    /// the call was rejected but not what the gate is holding out for, so it
2199    /// guesses. In the governed collections evaluation it guessed identity
2200    /// verification and re-asked a caller it had already verified for their card
2201    /// digits, which read as the model losing the tool result when in fact the
2202    /// gate had told it nothing.
2203    #[test]
2204    fn a_refusal_names_what_is_available_instead() {
2205        let state = State::new();
2206        let mon = FlowMonitor::new(debt_flow(), Enforcement::Enforce);
2207
2208        let reason = mon
2209            .admits_tool("charge_card", &state)
2210            .expect_err("charge_card is gated behind verification");
2211
2212        assert!(
2213            reason.contains("lookup_account"),
2214            "a refusal must point at the tool that would make progress: {reason}"
2215        );
2216        assert!(
2217            reason.contains("ptp_confirmed"),
2218            "a refusal must name the condition it is waiting on: {reason}"
2219        );
2220    }
2221
2222    /// The step's own posture rides along, so the reason and the redirection
2223    /// arrive in one payload rather than the model having to reconcile an error
2224    /// with steering sent on an earlier turn.
2225    #[test]
2226    fn a_refusal_carries_the_active_steps_posture() {
2227        let state = State::new();
2228        let mon = FlowMonitor::new(debt_flow(), Enforcement::Enforce);
2229
2230        let reason = mon
2231            .admits_tool("charge_card", &state)
2232            .expect_err("charge_card is gated behind verification");
2233
2234        assert!(
2235            reason.contains("Verify the caller's identity."),
2236            "the active step's posture belongs in the refusal: {reason}"
2237        );
2238    }
2239
2240    /// A spent `once` is the one refusal with nothing to redirect to: the answer
2241    /// is "you already did this", and offering alternatives would invite a
2242    /// retry under another name.
2243    #[test]
2244    fn a_spent_once_constraint_does_not_offer_alternatives() {
2245        let state = State::new();
2246        let flow = Flow::new()
2247            .step("pay")
2248            .allow(["charge_card"])
2249            .done(Guard::called_ok("charge_card"))
2250            .once("charge_card")
2251            .build()
2252            .expect("valid flow");
2253        let mut mon = FlowMonitor::new(flow, Enforcement::Enforce);
2254        mon.on_tool_ok("charge_card", &state);
2255
2256        let reason = mon
2257            .admits_tool("charge_card", &state)
2258            .expect_err("once is spent");
2259
2260        assert!(
2261            reason.contains("already run"),
2262            "a spent `once` must say so plainly: {reason}"
2263        );
2264        assert!(
2265            !reason.contains("Available now"),
2266            "nothing to redirect to — offering a menu invites a retry: {reason}"
2267        );
2268    }
2269
2270    /// The redirection is computed against live state, so it changes as the
2271    /// conversation progresses rather than describing the flow's opening move
2272    /// forever.
2273    #[test]
2274    fn the_redirection_tracks_the_conversation() {
2275        let state = State::new();
2276        let mon = FlowMonitor::new(debt_flow(), Enforcement::Enforce);
2277
2278        let before = mon
2279            .admits_tool("charge_card", &state)
2280            .expect_err("gated before the promise is confirmed");
2281        assert!(
2282            before.contains("ptp_confirmed") && before.contains("lookup_account"),
2283            "{before}"
2284        );
2285
2286        // Verification lands and the call moves on to the disclosure step. The
2287        // same constraint still blocks `charge_card`, but where the caller *is*
2288        // has changed, and the refusal has to change with it.
2289        let _ = state.set("identity_verified", true);
2290        let mut mon = mon;
2291        mon.on_turn(&state);
2292        let after = mon
2293            .admits_tool("charge_card", &state)
2294            .expect_err("the promise is still unconfirmed");
2295
2296        assert!(
2297            after.contains("Give the disclosure."),
2298            "the refusal must carry the posture of the step that is now active, \
2299             not the one that was active when the session opened: {after}"
2300        );
2301        assert!(
2302            !after.contains("Verify the caller's identity."),
2303            "a completed step's posture must not keep riding along on refusals — \
2304             that is how a verified caller gets asked to verify again: {after}"
2305        );
2306        assert_ne!(
2307            before, after,
2308            "the refusal did not change when the flow advanced"
2309        );
2310    }
2311
2312    /// Guard prose is what makes a refusal legible; pinned so the atoms do not
2313    /// silently start rendering as debug output.
2314    #[test]
2315    fn guards_describe_themselves_in_prose() {
2316        assert_eq!(
2317            Guard::is_true("verified").describe(),
2318            "'verified' must be true"
2319        );
2320        assert_eq!(
2321            Guard::captured(["amount", "date"]).describe(),
2322            "these must be known: amount, date"
2323        );
2324        assert_eq!(
2325            Guard::called_ok("disclose").describe(),
2326            "'disclose' must have run successfully"
2327        );
2328        assert_eq!(
2329            Guard::all([Guard::is_true("a"), Guard::done("b")]).describe(),
2330            "'a' must be true and step 'b' must be complete"
2331        );
2332        assert_eq!(
2333            Guard::custom(|_| true).describe(),
2334            "a condition set by the application",
2335            "a custom guard has no readable spec, and must not pretend otherwise"
2336        );
2337    }
2338
2339    fn debt_flow() -> Flow {
2340        Flow::new()
2341            .step("verify")
2342            .posture("Verify the caller's identity.")
2343            .allow(["lookup_account"])
2344            .done(Guard::is_true("identity_verified"))
2345            .step("disclose")
2346            .after("verify")
2347            .posture("Give the disclosure.")
2348            .done(Guard::is_true("disclosure_given"))
2349            .step("capture_ptp")
2350            .after("disclose")
2351            .done(Guard::captured(["ptp_amount", "ptp_date"]))
2352            .step("take_payment")
2353            .after("capture_ptp")
2354            .allow(["charge_card"])
2355            .done(Guard::called_ok("charge_card"))
2356            .step("close")
2357            .after("capture_ptp")
2358            .terminal()
2359            .never("charge_card")
2360            .until(Guard::is_true("ptp_confirmed"))
2361            .once("charge_card")
2362            .require(["close"])
2363            .build()
2364            .expect("valid flow")
2365    }
2366
2367    #[test]
2368    fn validates_and_detects_unknown_dep() {
2369        let bad = Flow::new()
2370            .step("a")
2371            .done(Guard::is_true("x"))
2372            .step("b")
2373            .after("missing")
2374            .terminal()
2375            .build();
2376        assert!(bad.is_err());
2377    }
2378
2379    #[test]
2380    fn detects_cycle() {
2381        // a after b, b after a — build() should reject.
2382        let steps = vec![
2383            Step {
2384                id: "a".into(),
2385                after: vec!["b".into()],
2386                join: Join::default(),
2387                gate: None,
2388                done: Some(Guard::always()),
2389                posture: None,
2390                ground: None,
2391                allow: vec![],
2392                deny: vec![],
2393                terminal: false,
2394            },
2395            Step {
2396                id: "b".into(),
2397                after: vec!["a".into()],
2398                join: Join::default(),
2399                gate: None,
2400                done: Some(Guard::always()),
2401                posture: None,
2402                ground: None,
2403                allow: vec![],
2404                deny: vec![],
2405                terminal: false,
2406            },
2407        ];
2408        let flow = Flow {
2409            steps,
2410            ..Flow::default()
2411        };
2412        assert!(flow.validate().is_err());
2413    }
2414
2415    #[test]
2416    fn marking_latches_in_order() {
2417        let flow = debt_flow();
2418        let mut mon = FlowMonitor::new(flow, Enforcement::Enforce);
2419        let state = State::new();
2420
2421        // Nothing done; only `verify` is active.
2422        assert_eq!(
2423            mon.active_steps(&state)
2424                .iter()
2425                .map(|s| s.id.as_str())
2426                .collect::<Vec<_>>(),
2427            vec!["verify"]
2428        );
2429
2430        let _ = state.set("identity_verified", true);
2431        mon.on_turn(&state);
2432        assert!(mon.marking().done.contains("verify"));
2433        assert_eq!(mon.verdict("verify", &state), Verdict::Done);
2434        assert_eq!(mon.verdict("disclose", &state), Verdict::Active);
2435
2436        let _ = state.set("disclosure_given", true);
2437        let _ = state.set("ptp_amount", 200);
2438        let _ = state.set("ptp_date", "2026-06-05");
2439        mon.on_turn(&state);
2440        // disclose + capture_ptp latch; close is terminal+eligible -> done.
2441        assert!(mon.marking().done.contains("capture_ptp"));
2442        assert!(mon.marking().done.contains("close"));
2443        assert!(mon.is_complete());
2444    }
2445
2446    #[test]
2447    fn enforces_never_until_and_once() {
2448        let flow = debt_flow();
2449        let mut mon = FlowMonitor::new(flow, Enforcement::Enforce);
2450        let state = State::new();
2451        // get to take_payment being active
2452        let _ = state.set("identity_verified", true);
2453        let _ = state.set("disclosure_given", true);
2454        let _ = state.set("ptp_amount", 200);
2455        let _ = state.set("ptp_date", "x");
2456        mon.on_turn(&state);
2457
2458        // charge_card blocked until ptp_confirmed.
2459        assert!(mon.admits_tool("charge_card", &state).is_err());
2460        let _ = state.set("ptp_confirmed", true);
2461        assert!(mon.admits_tool("charge_card", &state).is_ok());
2462
2463        // after it succeeds once, `once` blocks a second call.
2464        mon.on_tool_ok("charge_card", &state);
2465        assert!(mon.admits_tool("charge_card", &state).is_err());
2466    }
2467
2468    #[test]
2469    fn whitelist_scopes_tools_to_active_step() {
2470        let flow = debt_flow();
2471        let mon = FlowMonitor::new(flow, Enforcement::Enforce);
2472        let state = State::new();
2473        // In `verify`, only lookup_account is allowed.
2474        assert!(mon.admits_tool("lookup_account", &state).is_ok());
2475        assert!(mon.admits_tool("charge_card", &state).is_err());
2476    }
2477
2478    #[test]
2479    fn observe_mode_records_violations_not_blocks() {
2480        let flow = debt_flow();
2481        let mut mon = FlowMonitor::new(flow, Enforcement::Observe);
2482        let state = State::new();
2483        // charge_card out of order in observe mode -> recorded, still "runs".
2484        mon.observe_tool("charge_card", true, &state);
2485        assert_eq!(mon.violations().len(), 1);
2486        assert_eq!(mon.violations()[0].subject, "charge_card");
2487    }
2488
2489    #[test]
2490    fn compile_accepts_valid_flow_and_collects_tool_universe() {
2491        let compiled = debt_flow().compile().expect("valid flow compiles");
2492        // Tool universe spans allow/deny/once/never_until/confirm.
2493        assert!(compiled.tool_surface().tools.contains("charge_card"));
2494        assert!(compiled.tool_surface().tools.contains("lookup_account"));
2495        let _ = FlowMonitor::compiled(compiled, Enforcement::Enforce);
2496    }
2497
2498    #[test]
2499    fn compile_rejects_unreachable_step() {
2500        // `orphan` has no `after` and nothing leads to it — but it IS a root, so
2501        // to make it unreachable we give it an `after` on a step, then never make
2502        // that path lead anywhere. Simplest: a step depending on a missing root is
2503        // caught by validate; here we test a step unreachable via a broken chain.
2504        let flow = Flow::new()
2505            .step("a")
2506            .done(Guard::is_true("a_done"))
2507            .step("b")
2508            .after("a")
2509            .done(Guard::is_true("b_done"))
2510            .step("island")
2511            .after("b")
2512            .gate(Guard::is_true("never"))
2513            .terminal()
2514            .build()
2515            .expect("structurally valid");
2516        // island is reachable via a->b->island, so this compiles; assert it does.
2517        assert!(flow.compile().is_ok());
2518    }
2519
2520    #[test]
2521    fn compile_rejects_unguarded_commit_tool() {
2522        // commit tool gated by an always-true guard is effectively unguarded.
2523        let flow = Flow::new()
2524            .step("s")
2525            .allow(["pay"])
2526            .done(Guard::called_ok("pay"))
2527            .terminal()
2528            .commit("pay", Guard::always())
2529            .build()
2530            .expect("structurally valid");
2531        let err = flow
2532            .compile()
2533            .expect_err("unguarded commit must fail to compile");
2534        assert!(
2535            err.0
2536                .iter()
2537                .any(|e| matches!(e, FlowError::UnguardedCommitTool(t) if t == "pay"))
2538        );
2539    }
2540
2541    #[test]
2542    fn compile_with_tools_accepts_a_covering_registry() {
2543        let compiled = debt_flow()
2544            .compile_with_tools(&["lookup_account", "charge_card", "unrelated_extra"])
2545            .expect("registry covers the flow's tool universe");
2546        assert!(compiled.tool_surface().tools.contains("charge_card"));
2547    }
2548
2549    #[test]
2550    fn compile_with_tools_reports_dangling_tool_names() {
2551        // `charge_card` is referenced (allow/once/never_until) but missing from
2552        // the registry — a typo/drift the compiler must catch.
2553        let err = debt_flow()
2554            .compile_with_tools(&["lookup_account"])
2555            .expect_err("dangling tool must fail to compile");
2556        assert!(
2557            err.0
2558                .iter()
2559                .any(|e| matches!(e, FlowError::UnknownTool(t) if t == "charge_card"))
2560        );
2561        // Plain compile() stays registry-agnostic.
2562        assert!(debt_flow().compile().is_ok());
2563    }
2564
2565    #[test]
2566    fn compile_rejects_never_until_guard_on_unknown_step() {
2567        // `never(pay).until(done("missing"))` can never latch — `pay` would be
2568        // forbidden forever. validate() doesn't check constraint guards; compile must.
2569        let flow = Flow::new()
2570            .step("s")
2571            .allow(["pay"])
2572            .done(Guard::called_ok("pay"))
2573            .never("pay")
2574            .until(Guard::done("missing"))
2575            .build()
2576            .expect("structurally valid for build()");
2577        let err = flow.compile().expect_err("unsatisfiable guard must fail");
2578        assert!(err.0.iter().any(|e| matches!(
2579            e,
2580            FlowError::UnsatisfiableGuard { tool, step } if tool == "pay" && step == "missing"
2581        )));
2582    }
2583
2584    #[test]
2585    fn compile_rejects_before_cycle() {
2586        // `after` edges are acyclic, but before(a, b) + before(b, a) closes an
2587        // ordering cycle: neither step can ever become eligible. validate()'s
2588        // cycle check only walks `after`, so compile must catch this.
2589        let flow = Flow::new()
2590            .step("a")
2591            .done(Guard::is_true("a_done"))
2592            .step("b")
2593            .done(Guard::is_true("b_done"))
2594            .before("a", "b")
2595            .before("b", "a")
2596            .build()
2597            .expect("build() only checks `after` cycles");
2598        let err = flow
2599            .compile()
2600            .expect_err("before-cycle must fail to compile");
2601        assert!(err.0.iter().any(|e| matches!(
2602            e,
2603            FlowError::OrderingCycle(steps)
2604                if steps.contains(&"a".to_string()) && steps.contains(&"b".to_string())
2605        )));
2606    }
2607
2608    #[test]
2609    fn explain_reports_blocked_tools_and_reasons() {
2610        let flow = debt_flow();
2611        let mon = FlowMonitor::new(flow, Enforcement::Enforce);
2612        let state = State::new();
2613        let ex = mon.explain(&state);
2614        // In the initial `verify` step, charge_card is blocked; explain says so.
2615        assert!(ex.blocked_tools.contains_key("charge_card"));
2616        assert!(ex.active.contains(&"verify".to_string()));
2617    }
2618
2619    #[test]
2620    fn before_constraint_gates_step_eligibility() {
2621        // Regression: `before(a, b)` was validated but never enforced — `b` could
2622        // start before `a` was done. `a` and `b` have no `after` edge, so only the
2623        // Before constraint orders them.
2624        let flow = Flow::new()
2625            .step("a")
2626            .done(Guard::is_true("a_done"))
2627            .step("b")
2628            .done(Guard::is_true("b_done"))
2629            .before("a", "b")
2630            .build()
2631            .expect("valid flow");
2632        let mut mon = FlowMonitor::new(flow, Enforcement::Enforce);
2633        let state = State::new();
2634
2635        // `b` is NOT active until `a` is done, even though its own gate is open.
2636        let active: Vec<String> = mon
2637            .active_steps(&state)
2638            .iter()
2639            .map(|s| s.id.clone())
2640            .collect();
2641        assert!(active.contains(&"a".to_string()));
2642        assert!(
2643            !active.contains(&"b".to_string()),
2644            "b must wait for a (Before)"
2645        );
2646
2647        let _ = state.set("a_done", true);
2648        mon.on_turn(&state);
2649        let active: Vec<String> = mon
2650            .active_steps(&state)
2651            .iter()
2652            .map(|s| s.id.clone())
2653            .collect();
2654        assert!(active.contains(&"b".to_string()), "b active once a is done");
2655    }
2656
2657    #[test]
2658    fn custom_guard_in_combinator_is_not_erased() {
2659        // Regression: a custom guard nested in all()/any() was lowered to
2660        // Pred::Always, silently deleting it. It must still evaluate.
2661        let always_false = Guard::all([Guard::is_true("present"), Guard::custom(|_| false)]);
2662        // Mixed combinator is a Custom guard (non-serializable), not a Spec.
2663        assert!(matches!(always_false, Guard::Custom(_)));
2664
2665        let state = State::new();
2666        let _ = state.set("present", true);
2667        let marking = Marking::default();
2668        let ctx = FlowCtx {
2669            state: &state,
2670            marking: &marking,
2671        };
2672        // Would be `true` if the custom guard had been erased to Always.
2673        assert!(!always_false.eval(&ctx), "custom guard must still veto");
2674
2675        // all-spec combinator stays serializable.
2676        let serializable = Guard::all([Guard::is_true("a"), Guard::is_set("b")]);
2677        assert!(matches!(serializable, Guard::Spec(_)));
2678    }
2679
2680    #[test]
2681    fn serde_round_trips_data_driven_flow() {
2682        let flow = debt_flow();
2683        let jsonv = serde_json::to_value(&flow).expect("serialize");
2684        let back: Flow = serde_json::from_value(jsonv).expect("deserialize");
2685        back.validate().expect("round-tripped flow is valid");
2686        assert_eq!(back.steps.len(), flow.steps.len());
2687    }
2688
2689    #[test]
2690    fn custom_guard_is_not_serializable() {
2691        let flow = Flow::new()
2692            .step("a")
2693            .done(Guard::custom(|ctx| ctx.state.contains("ready")))
2694            .terminal()
2695            .build()
2696            .unwrap();
2697        assert!(serde_json::to_value(&flow).is_err());
2698    }
2699
2700    #[test]
2701    fn mermaid_export_has_nodes_and_edges() {
2702        let m = debt_flow().to_mermaid();
2703        assert!(m.contains("flowchart TD"));
2704        assert!(m.contains("verify --> disclose"));
2705        assert!(m.contains("close([close])")); // terminal shape
2706    }
2707
2708    struct WriteAgent;
2709    #[async_trait::async_trait]
2710    impl TextAgent for WriteAgent {
2711        fn name(&self) -> &str {
2712            "writer"
2713        }
2714        async fn run(&self, _state: &State) -> Result<String, crate::error::AgentError> {
2715            Ok("available".to_string())
2716        }
2717    }
2718
2719    #[tokio::test]
2720    async fn on_enter_fires_once_when_step_activates() {
2721        // collect -> check ; `check` runs an agent on enter whose result
2722        // (`check:result`) then completes a downstream `book` step.
2723        let flow = Flow::new()
2724            .step("collect")
2725            .done(Guard::is_true("collected"))
2726            .step("check")
2727            .after("collect")
2728            .done(Guard::resolved("check"))
2729            .step("book")
2730            .after("check")
2731            .terminal()
2732            .require(["book"])
2733            .build()
2734            .expect("valid flow");
2735
2736        let mut mon = FlowMonitor::new(flow, Enforcement::Enforce)
2737            .on_enter("check", on_enter(Arc::new(WriteAgent), AgentMode::Call));
2738        let state = State::new();
2739
2740        // Only `collect` is active at the start.
2741        assert_eq!(mon.take_newly_active(&state), vec!["collect".to_string()]);
2742        // Re-asking yields nothing — fire-once.
2743        assert!(mon.take_newly_active(&state).is_empty());
2744
2745        // Complete `collect`; `check` becomes active and its on_enter fires.
2746        let _ = state.set("collected", true);
2747        mon.on_turn(&state);
2748        mon.fire_enter_actions(&state).await;
2749        assert_eq!(
2750            state.get::<String>("check:result").as_deref(),
2751            Some("available")
2752        );
2753
2754        // The resolved result completes `check`, then terminal `book`.
2755        mon.on_turn(&state);
2756        assert!(mon.marking().done.contains("check"));
2757        assert!(mon.is_complete());
2758        // No further newly-active steps to announce.
2759        assert!(mon.take_newly_active(&state).is_empty());
2760    }
2761
2762    #[test]
2763    fn ground_template_interpolates_and_branches() {
2764        let state = State::new();
2765        let _ = state.set("when", "3pm");
2766        let _ = state.set("available", true);
2767        let _ = state.set("prior_visits", 2);
2768        assert_eq!(
2769            render_ground(
2770                "{when} is {available?open:taken}; {prior_visits} prior visits.",
2771                &state
2772            ),
2773            "3pm is open; 2 prior visits."
2774        );
2775        // Falsy branch + absent key renders empty.
2776        let _ = state.set("available", false);
2777        assert_eq!(
2778            render_ground("slot {missing}is {available?free:full}", &state),
2779            "slot is full"
2780        );
2781    }
2782
2783    #[test]
2784    fn active_grounds_projects_only_active_steps() {
2785        let flow = Flow::new()
2786            .step("collect")
2787            .ground("Known time: {when}.")
2788            .done(Guard::is_set("when"))
2789            .step("done")
2790            .after("collect")
2791            .terminal()
2792            .build()
2793            .expect("valid flow");
2794        let mut mon = FlowMonitor::new(flow, Enforcement::Enforce);
2795        let state = State::new();
2796        let _ = state.set("when", "3pm");
2797        assert_eq!(
2798            mon.active_grounds(&state),
2799            vec!["Known time: 3pm.".to_string()]
2800        );
2801        // Once collect completes, its ground no longer projects.
2802        mon.on_turn(&state);
2803        assert!(mon.active_grounds(&state).is_empty());
2804    }
2805
2806    #[test]
2807    fn eq_guard_matches_state_value() {
2808        let g = Guard::eq("status", json!("active"));
2809        let state = State::new();
2810        let marking = Marking::default();
2811        assert!(!g.eval(&FlowCtx {
2812            state: &state,
2813            marking: &marking
2814        }));
2815        let _ = state.set("status", "active");
2816        assert!(g.eval(&FlowCtx {
2817            state: &state,
2818            marking: &marking
2819        }));
2820    }
2821
2822    // ─── ambient tools ──────────────────────────────────────────────────────
2823    //
2824    // A step's `allow` list is a whitelist, so it excludes by omission: every
2825    // tool the author did not think to name is denied for the duration. That is
2826    // right for domain tools — naming `charge_card` is a statement that this is
2827    // the step where money moves — and wrong for cross-cutting infrastructure
2828    // that no step is *about*. Memory recall is the motivating case: a flow
2829    // author writing `.allow(["book_table"])` is saying "book here, don't search
2830    // the catalogue", not "stop remembering who the caller is".
2831    //
2832    // `ambient` names those tools once, at flow level. They are exempt from the
2833    // whitelist's implicit exclusion and from nothing else: a constraint that
2834    // *names* the tool still binds, because naming it is a deliberate act.
2835
2836    fn ambient_flow() -> Flow {
2837        Flow::new()
2838            .ambient(["recall_context"])
2839            .step("gather")
2840            .allow(["ask_diet"])
2841            .done(Guard::is_set("user:diet"))
2842            .step("book")
2843            .after("gather")
2844            .allow(["book_table"])
2845            .done(Guard::called_ok("book_table"))
2846            .build()
2847            .expect("flow is structurally valid")
2848    }
2849
2850    #[test]
2851    fn an_ambient_tool_survives_a_step_whitelist() {
2852        let mon = FlowMonitor::new(ambient_flow(), Enforcement::Enforce);
2853        let state = State::new();
2854        assert_eq!(
2855            mon.admits_tool("ask_diet", &state),
2856            Ok(()),
2857            "the step's own tool is admitted"
2858        );
2859        assert!(
2860            mon.admits_tool("book_table", &state).is_err(),
2861            "a later step's tool is still excluded by `gather`'s whitelist"
2862        );
2863        assert_eq!(
2864            mon.admits_tool("recall_context", &state),
2865            Ok(()),
2866            "an ambient tool must not be caught by a whitelist that never meant to exclude it"
2867        );
2868    }
2869
2870    #[test]
2871    fn an_ambient_tool_is_still_denied_when_a_step_names_it() {
2872        // `deny` names the tool, so it is a decision about that tool rather than
2873        // a side effect of listing others. Ambient must not override it.
2874        let flow = Flow::new()
2875            .ambient(["recall_context"])
2876            .step("sensitive")
2877            .deny(["recall_context"])
2878            .done(Guard::is_true("done"))
2879            .build()
2880            .expect("flow is structurally valid");
2881        let mon = FlowMonitor::new(flow, Enforcement::Enforce);
2882        assert!(
2883            mon.admits_tool("recall_context", &State::new()).is_err(),
2884            "an explicit `deny` outranks ambient"
2885        );
2886    }
2887
2888    #[test]
2889    fn an_ambient_tool_still_obeys_never_until() {
2890        let flow = Flow::new()
2891            .ambient(["manage_memory"])
2892            .step("verify")
2893            .done(Guard::is_true("verified"))
2894            .never("manage_memory")
2895            .until(Guard::is_true("verified"))
2896            .build()
2897            .expect("flow is structurally valid");
2898        let mon = FlowMonitor::new(flow, Enforcement::Enforce);
2899        let state = State::new();
2900        assert!(
2901            mon.admits_tool("manage_memory", &state).is_err(),
2902            "`never(..).until(..)` names the tool; ambient must not unlock it"
2903        );
2904        let _ = state.set("verified", true);
2905        assert_eq!(
2906            mon.admits_tool("manage_memory", &state),
2907            Ok(()),
2908            "once the guard holds the constraint stops binding"
2909        );
2910    }
2911
2912    #[test]
2913    fn an_ambient_tool_still_obeys_once() {
2914        let flow = Flow::new()
2915            .ambient(["summarize"])
2916            .step("work")
2917            .done(Guard::called_ok("summarize"))
2918            .once("summarize")
2919            .build()
2920            .expect("flow is structurally valid");
2921        let mut mon = FlowMonitor::new(flow, Enforcement::Enforce);
2922        let state = State::new();
2923        assert_eq!(mon.admits_tool("summarize", &state), Ok(()));
2924        mon.observe_tool("summarize", true, &state);
2925        assert!(
2926            mon.admits_tool("summarize", &state).is_err(),
2927            "`once` names the tool; ambient must not exempt it"
2928        );
2929    }
2930
2931    #[test]
2932    fn ambient_tools_join_the_tool_universe() {
2933        // Otherwise `compile_with_tools` would report a registry that covers the
2934        // flow as complete while an ambient tool it never heard of gets called.
2935        let flow = ambient_flow();
2936        assert!(
2937            flow.tool_universe().contains("recall_context"),
2938            "an ambient tool is part of the flow's tool universe"
2939        );
2940        assert!(
2941            ambient_flow()
2942                .compile_with_tools(&["ask_diet", "book_table"])
2943                .is_err(),
2944            "a registry missing the ambient tool must not compile clean"
2945        );
2946        assert!(
2947            ambient_flow()
2948                .compile_with_tools(&["ask_diet", "book_table", "recall_context"])
2949                .is_ok(),
2950            "a covering registry compiles"
2951        );
2952    }
2953
2954    #[test]
2955    fn a_flow_with_no_ambient_tools_is_unchanged() {
2956        // The whole point is that this is additive: a flow that never mentions
2957        // `ambient` must gate exactly as it did before the field existed.
2958        let mon = FlowMonitor::new(debt_flow(), Enforcement::Enforce);
2959        let state = State::new();
2960        assert_eq!(mon.admits_tool("lookup_account", &state), Ok(()));
2961        assert!(mon.admits_tool("charge_card", &state).is_err());
2962        assert!(mon.admits_tool("anything_else", &state).is_err());
2963    }
2964
2965    #[test]
2966    fn explain_trace_reports_per_atom_truth() {
2967        let state = State::new();
2968        let _ = state.set("ptp_amount", 200);
2969        let marking = Marking::default();
2970        let ctx = FlowCtx {
2971            state: &state,
2972            marking: &marking,
2973        };
2974        let guard = Guard::all([
2975            Guard::captured(["ptp_amount", "ptp_date"]),
2976            Guard::is_true("confirmed"),
2977        ]);
2978        let trace = guard.explain_trace(&ctx);
2979        assert!(!trace.holds);
2980        assert_eq!(trace.desc, "all of");
2981        assert_eq!(trace.children.len(), 2);
2982        // `captured` is a single atom (one node); `is_true` is false.
2983        assert!(!trace.children[0].holds, "ptp_date missing");
2984        assert!(!trace.children[1].holds);
2985        // JSON-serializable for devtools.
2986        assert!(serde_json::to_value(&trace).is_ok());
2987    }
2988
2989    #[test]
2990    fn explanation_carries_active_step_progress() {
2991        let mon = FlowMonitor::new(debt_flow(), Enforcement::Enforce);
2992        let state = State::new();
2993        let ex = mon.explain(&state);
2994        let verify = ex.active_progress.get("verify").expect("verify is active");
2995        assert!(!verify.holds);
2996        assert!(verify.desc.contains("identity_verified"));
2997    }
2998
2999    #[test]
3000    fn state_keys_read_covers_guards_and_constraints() {
3001        let keys = debt_flow().state_keys_read();
3002        for k in [
3003            "identity_verified",
3004            "disclosure_given",
3005            "ptp_amount",
3006            "ptp_date",
3007        ] {
3008            assert!(keys.contains(k), "missing read key {k}");
3009        }
3010        // Tool/step references are not state keys.
3011        assert!(!keys.contains("charge_card"));
3012    }
3013
3014    #[test]
3015    fn posture_updates_take_effect_in_place() {
3016        let mut mon = FlowMonitor::new(debt_flow(), Enforcement::Enforce);
3017        let state = State::new();
3018        assert!(mon.set_posture("verify", Some("New posture.".into())));
3019        assert!(!mon.set_posture("no_such_step", None));
3020        assert_eq!(
3021            mon.active_postures(&state),
3022            vec!["New posture.".to_string()]
3023        );
3024    }
3025
3026    #[test]
3027    fn eval_state_treats_marking_atoms_as_false() {
3028        let state = State::new();
3029        let _ = state.set("ready", true);
3030        assert!(Guard::is_true("ready").eval_state(&state));
3031        assert!(!Guard::called_ok("any_tool").eval_state(&state));
3032    }
3033
3034    #[test]
3035    fn conditional_edges_branch_and_any_join_merges() {
3036        // decide ─(routine)→ schedule ─┐
3037        //        ─(emergency)→ escalate ─┴→ close (join: any)
3038        let flow = Flow::new()
3039            .step("decide")
3040            .done(Guard::is_set("severity"))
3041            .step("schedule")
3042            .after_when("decide", Guard::eq("severity", "routine"))
3043            .done(Guard::called_ok("book"))
3044            .allow(["book"])
3045            .step("escalate")
3046            .after_when("decide", Guard::eq("severity", "emergency"))
3047            .done(Guard::called_ok("transfer"))
3048            .allow(["transfer"])
3049            .step("close")
3050            .after("schedule")
3051            .after("escalate")
3052            .join_any()
3053            .terminal()
3054            .build()
3055            .expect("valid");
3056        let mut mon = FlowMonitor::new(flow, Enforcement::Enforce);
3057        let state = State::new();
3058        let _ = state.set("severity", "routine");
3059        mon.relatch(&state);
3060        let active: Vec<String> = mon.explain(&state).active;
3061        assert!(
3062            active.contains(&"schedule".to_string()),
3063            "routine branch opens"
3064        );
3065        assert!(
3066            !active.contains(&"escalate".to_string()),
3067            "emergency branch stays closed"
3068        );
3069        // Merge via any-join: schedule alone completing closes the flow.
3070        mon.on_tool_ok("book", &state);
3071        assert!(mon.marking().done.contains("close"), "any-join merged");
3072    }
3073
3074    #[test]
3075    fn reset_unlatches_on_rising_edge_and_forgives_called_ok() {
3076        let flow = Flow::new()
3077            .step("pay")
3078            .allow(["charge"])
3079            .done(Guard::called_ok("charge"))
3080            .once("charge")
3081            .reset(["pay"])
3082            .when(Guard::is_true("declined"))
3083            .build()
3084            .expect("valid");
3085        let mut mon = FlowMonitor::new(flow, Enforcement::Enforce);
3086        let state = State::new();
3087        mon.relatch(&state);
3088        mon.on_tool_ok("charge", &state);
3089        assert!(mon.marking().done.contains("pay"));
3090        assert!(mon.admits_tool("charge", &state).is_err(), "once spent");
3091
3092        // Decline: rising edge un-latches pay and forgives the charge.
3093        let _ = state.set("declined", true);
3094        mon.relatch(&state);
3095        assert!(!mon.marking().done.contains("pay"), "un-latched");
3096        assert!(
3097            mon.admits_tool("charge", &state).is_ok(),
3098            "once counts per latch-cycle"
3099        );
3100
3101        // Still-true guard does not re-fire (edge, not level).
3102        mon.on_tool_ok("charge", &state);
3103        mon.relatch(&state);
3104        assert!(
3105            mon.marking().done.contains("pay"),
3106            "second charge latches again"
3107        );
3108    }
3109
3110    #[test]
3111    fn edge_serde_keeps_plain_strings_and_round_trips_conditions() {
3112        let flow = Flow::new()
3113            .step("a")
3114            .terminal()
3115            .step("b")
3116            .after("a")
3117            .after_when("a", Guard::is_true("x"))
3118            .join_any()
3119            .terminal()
3120            .build()
3121            .expect("valid");
3122        let json = serde_json::to_value(&flow).expect("serialize");
3123        // Unconditional edge stays the original plain string.
3124        assert_eq!(json["steps"][1]["after"][0], serde_json::json!("a"));
3125        assert_eq!(
3126            json["steps"][1]["after"][1],
3127            serde_json::json!({"step": "a", "when": {"is_true": "x"}})
3128        );
3129        assert_eq!(json["steps"][1]["join"], serde_json::json!("any"));
3130        let back: Flow = serde_json::from_value(json).expect("deserialize");
3131        assert_eq!(back.steps[1].after.len(), 2);
3132        assert!(back.steps[1].after[1].when.is_some());
3133    }
3134
3135    #[test]
3136    fn flow_json_schema_generates() {
3137        let schema = serde_json::to_value(schemars::schema_for!(Flow)).expect("schema");
3138        let text = schema.to_string();
3139        // The Guard schema must surface the Pred atoms.
3140        assert!(text.contains("is_true"));
3141        assert!(text.contains("never_until"));
3142    }
3143}