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