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