gemini_adk_rs/flow/
stack.rs

1//! `FlowStack` — the runtime above the DAG: a main flow plus its digressions.
2//!
3//! A [`FlowMonitor`] governs one [`Flow`]. Real conversations
4//! leave the main task temporarily — a side question, a cancel, a hand-off —
5//! and expect to come back to it. A [`FlowStack`] holds the **main** monitor
6//! plus a set of [`Overlay`]s (digressions): when an overlay's trigger holds,
7//! the main flow is suspended untouched, the overlay's own monitor drives the
8//! session until it completes, and the main flow then continues per the
9//! overlay's [`Resume`] policy.
10//!
11//! This is the *only* governance object the Live control plane drives. A
12//! session governed by a bare flow is a stack with no overlays, so every
13//! execution path — the simulator, a live session, a replay — advances the same
14//! type with the same semantics. The authoring layer lowers a conversation into
15//! a stack; it does not implement one.
16//!
17//! Digressions nest: a digression can itself be interrupted by another (one
18//! not already on the active path), which drives until it completes and then
19//! resumes the one beneath it per its own [`Resume`] policy.
20
21use std::collections::{BTreeMap, BTreeSet};
22use std::sync::Arc;
23
24use serde::{Deserialize, Serialize};
25
26use super::timing::{VOICE_TIMING_KEY, VoiceTiming};
27use super::{
28    CompiledFlow, Enforcement, Flow, FlowExplanation, FlowMonitor, Guard, Marking, Step, StepAction,
29};
30use crate::state::State;
31
32/// The state key raised when a stage's repair policy escalates.
33pub fn escalate_flag(stage: &str) -> String {
34    format!("repair:{stage}:escalate")
35}
36
37/// The state key raised when a stage's repair policy asks for a reprompt.
38pub fn reprompt_flag(stage: &str) -> String {
39    format!("repair:{stage}:reprompt")
40}
41
42/// The state key raised for one turn when the user corrects `slot`: its
43/// value changed from one captured value to another. See
44/// [`FlowStack::with_correction`].
45pub fn correction_flag(slot: &str) -> String {
46    format!("correction:{slot}")
47}
48
49/// Written when the governed flow admits a tool call, before it runs:
50/// `{"tool": name, "id": call id}`. With [`TOOL_DENIED_KEY`] and
51/// [`TOOL_RESULT_KEY`] it puts every governance decision about a tool in the
52/// mutation journal, in order, which is what makes a recorded session
53/// replayable as a scenario.
54pub const TOOL_CALL_KEY: &str = "flow:tool_call";
55
56/// Written when the governed flow refuses a tool call:
57/// `{"tool": name, "id": call id, "reason": why}`.
58pub const TOOL_DENIED_KEY: &str = "flow:tool_denied";
59
60/// Written when an admitted tool call completes:
61/// `{"tool": name, "id": call id, "ok": succeeded}`.
62pub const TOOL_RESULT_KEY: &str = "flow:tool_result";
63
64/// The state key that names the active digression (`null` when the main flow
65/// is driving). Published by the control plane at every turn boundary.
66pub const OVERLAY_STATE_KEY: &str = "flow:overlay";
67
68/// The state key raised (`true`) once a [`Resume::Terminate`] digression has
69/// ended the conversation. Governance is inert from then on: no postures, no
70/// admitted tools. The runtime does not hang up by itself — the application
71/// decides how a call ends — so watch this key (or
72/// [`FlowStack::is_terminated`]) and close the session. Published by the
73/// control plane at every turn boundary.
74pub const TERMINATED_STATE_KEY: &str = "flow:terminated";
75
76/// How the main flow continues after a digression (overlay) completes.
77///
78/// `Restart` resets the main flow's *monitor* — its marking, fired `on_enter`
79/// actions and reset edges — against the session's existing `State`. It does
80/// not clear state, so slots the user already filled stay filled. It is a
81/// fresh pass over the same conversation, not a fresh business task.
82#[derive(
83    Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, schemars::JsonSchema,
84)]
85#[serde(rename_all = "snake_case")]
86pub enum Resume {
87    /// Resume the main flow exactly where it was suspended (history state).
88    #[default]
89    Previous,
90    /// Re-enter the main flow from its start (see the type docs for what that
91    /// does and does not reset). Repair signals and counters are cleared with
92    /// the marking: a fresh pass starts with no step already escalated.
93    Restart,
94    /// End the conversation (e.g. a cancel/handoff digression). From the next
95    /// turn boundary the stack governs nothing: see
96    /// [`FlowStack::is_terminated`] and [`TERMINATED_STATE_KEY`].
97    Terminate,
98}
99
100fn default_reprompt_after() -> u32 {
101    2
102}
103fn default_escalate_after() -> u32 {
104    4
105}
106
107/// A step's repair policy for the weird paths (silence, no-match, the user
108/// stalling). The stack sets `repair:{step}:reprompt` once the step has been
109/// active `reprompt_after` turns without completing, and `repair:{step}:escalate`
110/// after `escalate_after`. When `escalate_to` is set, the authoring layer lowers
111/// an extra edge gated on the escalate signal — a deterministic "give up and
112/// hand off".
113#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
114pub struct RepairPolicy {
115    /// Turns the step may be active before a reprompt signal is raised.
116    #[serde(default = "default_reprompt_after")]
117    pub reprompt_after: u32,
118    /// Turns the step may be active before an escalation signal is raised.
119    #[serde(default = "default_escalate_after")]
120    pub escalate_after: u32,
121    /// Step to route to on escalation (also completes the current step).
122    #[serde(default, skip_serializing_if = "Option::is_none")]
123    pub escalate_to: Option<String>,
124    /// Escalate once the user has barged in this many times while the step
125    /// is active (they keep cutting the model off: it is not working).
126    #[serde(default, skip_serializing_if = "Option::is_none")]
127    pub escalate_after_interruptions: Option<u32>,
128    /// Escalate once this many tool calls have failed or timed out while
129    /// the step is active.
130    #[serde(default, skip_serializing_if = "Option::is_none")]
131    pub escalate_after_tool_failures: Option<u32>,
132}
133
134impl Default for RepairPolicy {
135    fn default() -> Self {
136        Self {
137            reprompt_after: default_reprompt_after(),
138            escalate_after: default_escalate_after(),
139            escalate_to: None,
140            escalate_after_interruptions: None,
141            escalate_after_tool_failures: None,
142        }
143    }
144}
145
146impl RepairPolicy {
147    /// A policy with the given reprompt/escalate turn thresholds.
148    pub fn new(reprompt_after: u32, escalate_after: u32) -> Self {
149        Self {
150            reprompt_after,
151            escalate_after,
152            ..Self::default()
153        }
154    }
155
156    /// Route to `step` on escalation (also completes the current step).
157    pub fn escalate_to(mut self, step: impl Into<String>) -> Self {
158        self.escalate_to = Some(step.into());
159        self
160    }
161
162    /// Also escalate after `n` barge-ins while the step is active.
163    pub fn escalate_after_interruptions(mut self, n: u32) -> Self {
164        self.escalate_after_interruptions = Some(n);
165        self
166    }
167
168    /// Also escalate after `n` failed or timed-out tool calls while the step
169    /// is active.
170    pub fn escalate_after_tool_failures(mut self, n: u32) -> Self {
171        self.escalate_after_tool_failures = Some(n);
172        self
173    }
174}
175
176/// A digression the runtime can enter: its trigger, governed flow and resume
177/// policy. Built from a compiled flow so it carries proof of compilation.
178#[derive(Debug, Clone)]
179pub struct Overlay {
180    name: String,
181    trigger: Guard,
182    flow: Flow,
183    resume: Resume,
184}
185
186impl Overlay {
187    /// A digression over a compiled flow.
188    pub fn new(
189        name: impl Into<String>,
190        trigger: Guard,
191        flow: CompiledFlow,
192        resume: Resume,
193    ) -> Self {
194        Self {
195            name: name.into(),
196            trigger,
197            flow: flow.into_flow(),
198            resume,
199        }
200    }
201
202    /// The digression's name.
203    pub fn name(&self) -> &str {
204        &self.name
205    }
206    /// The guard that activates it, evaluated against the main flow's context.
207    pub fn trigger(&self) -> &Guard {
208        &self.trigger
209    }
210    /// The digression's governed flow.
211    pub fn flow(&self) -> &Flow {
212        &self.flow
213    }
214    /// Mutable access to the flow, for connect-time merges (ambient tools).
215    pub fn flow_mut(&mut self) -> &mut Flow {
216        &mut self.flow
217    }
218    /// What the main flow does once this digression completes.
219    pub fn resume(&self) -> Resume {
220        self.resume
221    }
222}
223
224/// A digression currently suspending the main flow.
225struct ActiveOverlay {
226    name: String,
227    monitor: FlowMonitor,
228    resume: Resume,
229}
230
231/// A shared, lock-protected [`FlowStack`] — the form in which the Live
232/// control plane owns governance, so runtime surfaces (e.g.
233/// [`LiveHandle::explain`](crate::live::LiveHandle::explain)) can snapshot it
234/// concurrently. All methods are synchronous: lock briefly and never hold the
235/// guard across an `await`.
236pub type SharedFlowStack = Arc<parking_lot::Mutex<FlowStack>>;
237
238/// The main flow plus its digressions, with push-on-trigger and
239/// resume-on-completion.
240///
241/// While a digression is active, governance — tool admission, postures and
242/// grounds, `explain()` — delegates to the **active** layer, and the main
243/// flow's marking is untouched, so [`Resume::Previous`] resumes exactly where
244/// it left off. Driven by `State` and guards: model-free and deterministic.
245///
246/// A digression stays the active layer through the turn on which it
247/// completes, so that turn's projection carries its closing instruction (the
248/// terminal stage's posture — "hand off to a human now"), and its
249/// [`Resume`] policy applies at the *next* turn boundary. Without that, a
250/// digression that completes on its entry turn would never be seen at all.
251pub struct FlowStack {
252    main: FlowMonitor,
253    mode: Enforcement,
254    overlays: Vec<Overlay>,
255    /// The digressions suspending the main flow, outermost first. The last
256    /// one drives; each suspends the one beneath it.
257    active: Vec<ActiveOverlay>,
258    /// The digression that ended the conversation, once one has.
259    terminated: Option<String>,
260    /// Per-main-step repair policies.
261    repair: BTreeMap<String, RepairPolicy>,
262    /// Consecutive turns each main step has been active without completing.
263    active_turns: BTreeMap<String, u32>,
264    /// Per-step voice timing, keyed by step id in whichever layer is active.
265    timing: BTreeMap<String, VoiceTiming>,
266    /// Slots whose correction re-opens later stages, with the state keys to
267    /// clear when that happens (e.g. the confirmation of a commit stage).
268    corrections: BTreeMap<String, Vec<String>>,
269    /// The last value seen for each watched slot.
270    slot_values: BTreeMap<String, serde_json::Value>,
271    /// Text each step must have said word for word.
272    verbatim: BTreeMap<String, String>,
273    /// Barge-ins each main step has seen while active.
274    interruptions: BTreeMap<String, u32>,
275    /// Failed tool calls each main step has seen while active.
276    tool_failures: BTreeMap<String, u32>,
277}
278
279impl std::fmt::Debug for FlowStack {
280    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
281        f.debug_struct("FlowStack")
282            .field("mode", &self.mode)
283            .field("overlays", &self.overlays.len())
284            .field(
285                "active",
286                &self.active.iter().map(|a| &a.name).collect::<Vec<_>>(),
287            )
288            .field("terminated", &self.terminated)
289            .field("repair", &self.repair)
290            .field("timing", &self.timing)
291            .field("corrections", &self.corrections)
292            .finish()
293    }
294}
295
296impl FlowStack {
297    /// A stack over a compiled main flow with no digressions yet.
298    pub fn new(main: CompiledFlow, mode: Enforcement) -> Self {
299        Self::from_monitor(FlowMonitor::compiled(main, mode))
300    }
301
302    /// A stack whose main layer is an existing monitor (keeps its `on_enter`
303    /// actions and mode). This is how a bare governed flow becomes the one
304    /// governance object the control plane drives.
305    pub fn from_monitor(main: FlowMonitor) -> Self {
306        Self {
307            mode: main.mode(),
308            main,
309            overlays: Vec::new(),
310            active: Vec::new(),
311            terminated: None,
312            repair: BTreeMap::new(),
313            active_turns: BTreeMap::new(),
314            timing: BTreeMap::new(),
315            corrections: BTreeMap::new(),
316            verbatim: BTreeMap::new(),
317            slot_values: BTreeMap::new(),
318            interruptions: BTreeMap::new(),
319            tool_failures: BTreeMap::new(),
320        }
321    }
322
323    /// Add a digression.
324    pub fn with_overlay(mut self, overlay: Overlay) -> Self {
325        self.overlays.push(overlay);
326        self
327    }
328
329    /// Add several digressions, in trigger-priority order.
330    pub fn with_overlays(mut self, overlays: impl IntoIterator<Item = Overlay>) -> Self {
331        self.overlays.extend(overlays);
332        self
333    }
334
335    /// Attach a repair policy to a main-flow step.
336    pub fn with_repair(mut self, step: impl Into<String>, policy: RepairPolicy) -> Self {
337        self.repair.insert(step.into(), policy);
338        self
339    }
340
341    /// Attach repair policies keyed by main-flow step.
342    pub fn with_repairs(
343        mut self,
344        policies: impl IntoIterator<Item = (String, RepairPolicy)>,
345    ) -> Self {
346        self.repair.extend(policies);
347        self
348    }
349
350    /// Watch `slot` for corrections: when its value changes from one captured
351    /// value to another, [`correction_flag`]`(slot)` is raised for that turn
352    /// and `clear` keys are removed from state.
353    ///
354    /// Raising the flag does nothing by itself. The main flow reacts through
355    /// a [`Constraint::Reset`](super::Constraint::Reset) gated on it, which
356    /// un-latches the stages downstream of the slot so they run again with
357    /// the corrected value. A `Conversation` lowers exactly that for every
358    /// collected slot, clearing the confirmation of any commit stage it
359    /// re-opens.
360    pub fn with_correction(
361        mut self,
362        slot: impl Into<String>,
363        clear: impl IntoIterator<Item = String>,
364    ) -> Self {
365        self.corrections
366            .insert(slot.into(), clear.into_iter().collect());
367        self
368    }
369
370    /// Watch several slots; see [`with_correction`](Self::with_correction).
371    pub fn with_corrections(
372        mut self,
373        corrections: impl IntoIterator<Item = (String, Vec<String>)>,
374    ) -> Self {
375        self.corrections.extend(corrections);
376        self
377    }
378
379    /// The watched slots and the keys each correction clears.
380    pub fn correction_policies(&self) -> &BTreeMap<String, Vec<String>> {
381        &self.corrections
382    }
383
384    /// Require `step` to say `text` word for word; see
385    /// [`verbatim`](super::verbatim). The stack publishes the requirement
386    /// while the step is active. Its completion guard must also require
387    /// [`verbatim_flag`](super::verbatim_flag)`(step)`, which a
388    /// `Conversation` lowers for you.
389    pub fn with_verbatim(mut self, step: impl Into<String>, text: impl Into<String>) -> Self {
390        self.verbatim.insert(step.into(), text.into());
391        self
392    }
393
394    /// Verbatim requirements keyed by step.
395    pub fn with_verbatims(mut self, texts: impl IntoIterator<Item = (String, String)>) -> Self {
396        self.verbatim.extend(texts);
397        self
398    }
399
400    /// The verbatim requirements keyed by step.
401    pub fn verbatim_policies(&self) -> &BTreeMap<String, String> {
402        &self.verbatim
403    }
404
405    /// Attach voice timing to a step (main flow or digression).
406    pub fn with_timing(mut self, step: impl Into<String>, timing: VoiceTiming) -> Self {
407        self.timing.insert(step.into(), timing);
408        self
409    }
410
411    /// Attach voice timing keyed by step.
412    pub fn with_timings(
413        mut self,
414        timings: impl IntoIterator<Item = (String, VoiceTiming)>,
415    ) -> Self {
416        self.timing.extend(timings);
417        self
418    }
419
420    /// The voice timing keyed by step.
421    pub fn timing_policies(&self) -> &BTreeMap<String, VoiceTiming> {
422        &self.timing
423    }
424
425    /// The merged timing of the steps active right now in the driving layer
426    /// (empty when none of them has timing, or the conversation is over).
427    pub fn active_timing(&self, state: &State) -> VoiceTiming {
428        if self.timing.is_empty() || self.terminated.is_some() {
429            return VoiceTiming::default();
430        }
431        self.current()
432            .active_steps(state)
433            .iter()
434            .filter_map(|step| self.timing.get(&step.id))
435            .fold(VoiceTiming::default(), |acc, t| acc.merge(t))
436    }
437
438    /// Publish [`active_timing`](Self::active_timing) to
439    /// [`VOICE_TIMING_KEY`] in state, where the runtime's audio path, timers
440    /// and turn lifecycle read it. Writes only on change; removes the key
441    /// when no timing applies. The stack calls this itself after every turn
442    /// and tool call; call it once when installing the stack.
443    pub fn publish_timing(&self, state: &State) {
444        self.publish_verbatim(state);
445        if self.timing.is_empty() {
446            return;
447        }
448        let timing = self.active_timing(state);
449        let current = state.get::<VoiceTiming>(VOICE_TIMING_KEY);
450        if timing.is_empty() {
451            if current.is_some() {
452                state.remove(VOICE_TIMING_KEY);
453            }
454        } else if current.as_ref() != Some(&timing) {
455            let _ = state.set(VOICE_TIMING_KEY, &timing);
456        }
457    }
458
459    /// Publish the active step's verbatim requirement to
460    /// [`VERBATIM_KEY`](super::VERBATIM_KEY), or remove it when no active step
461    /// has one. Called with [`publish_timing`](Self::publish_timing).
462    fn publish_verbatim(&self, state: &State) {
463        if self.verbatim.is_empty() {
464            return;
465        }
466        let required = (self.terminated.is_none())
467            .then(|| {
468                self.current().active_steps(state).iter().find_map(|step| {
469                    self.verbatim
470                        .get(&step.id)
471                        .map(|text| super::VerbatimRequirement {
472                            step: step.id.clone(),
473                            text: text.clone(),
474                        })
475                })
476            })
477            .flatten();
478        let current = state.get::<super::VerbatimRequirement>(super::VERBATIM_KEY);
479        match required {
480            Some(req) if current.as_ref() != Some(&req) => {
481                let _ = state.set(super::VERBATIM_KEY, &req);
482            }
483            None if current.is_some() => {
484                state.remove(super::VERBATIM_KEY);
485            }
486            _ => {}
487        }
488    }
489
490    /// Wrap in a [`SharedFlowStack`] for shared ownership between the control
491    /// lane (which advances it) and runtime accessors (which snapshot it).
492    pub fn into_shared(self) -> SharedFlowStack {
493        Arc::new(parking_lot::Mutex::new(self))
494    }
495
496    /// The enforcement mode (shared by every layer).
497    pub fn mode(&self) -> Enforcement {
498        self.mode
499    }
500
501    /// The digressions this stack can enter, in trigger-priority order.
502    pub fn overlays(&self) -> &[Overlay] {
503        &self.overlays
504    }
505
506    /// Mutable access to the digressions, for connect-time merges.
507    pub fn overlays_mut(&mut self) -> &mut [Overlay] {
508        &mut self.overlays
509    }
510
511    /// The repair policies keyed by main-flow step.
512    pub fn repair_policies(&self) -> &BTreeMap<String, RepairPolicy> {
513        &self.repair
514    }
515
516    /// The main flow's monitor, whether or not it is currently driving.
517    pub fn main(&self) -> &FlowMonitor {
518        &self.main
519    }
520
521    /// The monitor currently driving — the active digression if any, else the
522    /// main flow.
523    pub fn current(&self) -> &FlowMonitor {
524        self.active.last().map_or(&self.main, |a| &a.monitor)
525    }
526
527    fn current_mut(&mut self) -> &mut FlowMonitor {
528        match self.active.last_mut() {
529            Some(a) => &mut a.monitor,
530            None => &mut self.main,
531        }
532    }
533
534    /// The name of the driving digression, if one is suspending the main flow.
535    pub fn active_overlay(&self) -> Option<&str> {
536        self.active.last().map(|a| a.name.as_str())
537    }
538
539    /// Every active digression, outermost first: a digression can itself be
540    /// interrupted by another, which then drives until it completes.
541    pub fn overlay_path(&self) -> Vec<&str> {
542        self.active.iter().map(|a| a.name.as_str()).collect()
543    }
544
545    /// Whether the conversation is finished (main complete, or a `Terminate`
546    /// digression ran).
547    pub fn is_complete(&self) -> bool {
548        self.terminated.is_some() || (self.active.is_empty() && self.main.is_complete())
549    }
550
551    /// Whether a `Terminate` digression ended the conversation. From then on
552    /// the stack governs nothing: no active steps or postures, every tool
553    /// denied. The runtime does not close the session by itself; the
554    /// application reads this (or [`TERMINATED_STATE_KEY`]) and hangs up.
555    pub fn is_terminated(&self) -> bool {
556        self.terminated.is_some()
557    }
558
559    /// Why a tool is denied once the conversation has ended.
560    fn termination_denial(&self) -> Option<String> {
561        self.terminated
562            .as_ref()
563            .map(|by| format!("the conversation has ended: digression `{by}` terminated it"))
564    }
565
566    /// Whether the active digression has run to completion and is being
567    /// projected for its closing turn.
568    fn active_is_closing(&self) -> bool {
569        self.active.last().is_some_and(|a| a.monitor.is_complete())
570    }
571
572    /// Index of the first overlay whose trigger holds against the main
573    /// context and that is not already on the active path (a digression
574    /// cannot interrupt itself).
575    fn triggered(&self, state: &State) -> Option<usize> {
576        self.overlays.iter().position(|ov| {
577            !self.active.iter().any(|a| a.name == ov.name) && self.main.eval(&ov.trigger, state)
578        })
579    }
580
581    /// Enter overlay `idx` on top of the active path, driving its first turn
582    /// so single-step overlays latch. If that completes it, it still stays
583    /// active for this turn's projection (see the type docs).
584    fn enter(&mut self, idx: usize, state: &State) {
585        let ov = &self.overlays[idx];
586        let mut monitor = FlowMonitor::new(ov.flow.clone(), self.mode);
587        monitor.on_turn(state);
588        self.active.push(ActiveOverlay {
589            name: ov.name.clone(),
590            monitor,
591            resume: ov.resume,
592        });
593    }
594
595    /// Raise the correction flag of every watched slot whose value changed
596    /// from one captured value to another since the last turn, clearing the
597    /// keys its rule names. The main flow lowers the flags once it has
598    /// advanced past them.
599    fn detect_corrections(&mut self, state: &State) {
600        if self.corrections.is_empty() {
601            return;
602        }
603        for (slot, clear) in &self.corrections {
604            let current = state.get_raw(slot);
605            let corrected = matches!(
606                (self.slot_values.get(slot), &current),
607                (Some(before), Some(now)) if before != now
608            );
609            if corrected {
610                let _ = state.set(correction_flag(slot), true);
611                for key in clear {
612                    state.remove(key);
613                }
614            }
615            match current {
616                Some(v) => {
617                    self.slot_values.insert(slot.clone(), v);
618                }
619                None => {
620                    self.slot_values.remove(slot);
621                }
622            }
623        }
624    }
625
626    /// Count a barge-in against every active main step and escalate those
627    /// whose policy's `escalate_after_interruptions` is reached. Called by
628    /// the control plane when the user interrupts the model.
629    pub fn on_interrupted(&mut self, state: &State) {
630        self.count_against_active(
631            state,
632            |p| p.escalate_after_interruptions,
633            |s| &mut s.interruptions,
634        );
635    }
636
637    /// Count a failed (or timed-out) tool call against every active main step
638    /// and escalate those whose policy's `escalate_after_tool_failures` is
639    /// reached.
640    pub fn on_tool_failed(&mut self, state: &State) {
641        self.count_against_active(
642            state,
643            |p| p.escalate_after_tool_failures,
644            |s| &mut s.tool_failures,
645        );
646    }
647
648    fn count_against_active(
649        &mut self,
650        state: &State,
651        threshold: impl Fn(&RepairPolicy) -> Option<u32>,
652        counters: impl Fn(&mut Self) -> &mut BTreeMap<String, u32>,
653    ) {
654        if self.terminated.is_some() || !self.active.is_empty() || self.repair.is_empty() {
655            return;
656        }
657        let steps: Vec<(String, u32)> = self
658            .main
659            .active_steps(state)
660            .iter()
661            .filter_map(|s| {
662                self.repair
663                    .get(&s.id)
664                    .and_then(&threshold)
665                    .map(|limit| (s.id.clone(), limit))
666            })
667            .collect();
668        for (step, limit) in steps {
669            let count = counters(self).entry(step.clone()).or_insert(0);
670            *count += 1;
671            if *count >= limit {
672                let _ = state.set(escalate_flag(&step), true);
673            }
674        }
675    }
676
677    /// Bump per-step active-turn counters for the main flow and raise repair
678    /// signals when thresholds are hit. Clears signals for steps that are no
679    /// longer active.
680    fn apply_repair(&mut self, state: &State) {
681        if self.repair.is_empty() {
682            return;
683        }
684        let active: BTreeSet<String> = self
685            .main
686            .active_steps(state)
687            .iter()
688            .map(|s| s.id.clone())
689            .collect();
690        let left: Vec<String> = self
691            .active_turns
692            .keys()
693            .filter(|k| !active.contains(*k))
694            .cloned()
695            .collect();
696        for step in left {
697            self.active_turns.remove(&step);
698            self.interruptions.remove(&step);
699            self.tool_failures.remove(&step);
700            let _ = state.set(reprompt_flag(&step), false);
701            // A step that completed *by escalating* must keep its escalate
702            // signal: the authoring layer lowers `escalate_to` into an edge
703            // gated on it, and gates are evaluated every turn, so clearing the
704            // flag here would drop the hand-off target out of the active set
705            // one turn after it was entered. Clear it only when the step left
706            // the active set without completing (its gate closed). Steps a
707            // `Constraint::Reset` un-latches are cleared in `advance_main`,
708            // before the re-latch can see the stale signal.
709            if !self.main.marking().done.contains(&step) {
710                let _ = state.set(escalate_flag(&step), false);
711            }
712        }
713        for step in &active {
714            let count = self.active_turns.entry(step.clone()).or_insert(0);
715            *count += 1;
716            if let Some(rp) = self.repair.get(step) {
717                if *count >= rp.reprompt_after {
718                    let _ = state.set(reprompt_flag(step), true);
719                }
720                if *count >= rp.escalate_after {
721                    let _ = state.set(escalate_flag(step), true);
722                }
723            }
724        }
725    }
726
727    /// Forget everything the repair bookkeeping knows about `step`: its
728    /// active-turn count and both signals. Used when a step is un-latched (a
729    /// reset, or a restart of the whole main flow), because the lowered
730    /// `escalate_to` completion guard references the escalate signal and would
731    /// otherwise re-complete the step the moment it is re-latched.
732    fn clear_repair(&mut self, step: &str, state: &State) {
733        self.active_turns.remove(step);
734        self.interruptions.remove(step);
735        self.tool_failures.remove(step);
736        let _ = state.set(reprompt_flag(step), false);
737        let _ = state.set(escalate_flag(step), false);
738    }
739
740    /// Apply a closed digression's resume policy to the layer beneath it:
741    /// the next digression on the path, or the main flow.
742    fn apply_resume(&mut self, by: String, resume: Resume, state: &State) {
743        match resume {
744            // The suspended layer's marking was untouched — nothing to do.
745            Resume::Previous => {}
746            Resume::Restart => match self.active.last_mut() {
747                Some(beneath) => beneath.monitor.restart(),
748                None => {
749                    self.main.restart();
750                    // A fresh pass starts with no step already escalated.
751                    let steps: Vec<String> = self.repair.keys().cloned().collect();
752                    for step in steps {
753                        self.clear_repair(&step, state);
754                    }
755                }
756            },
757            Resume::Terminate => {
758                self.active.clear();
759                self.terminated = Some(by);
760            }
761        }
762    }
763
764    /// Advance the main flow one turn: resets first (shedding the repair
765    /// signals of any step they un-latch), then repair bookkeeping over the
766    /// pre-turn active set, then the re-latch.
767    fn advance_main(&mut self, state: &State) {
768        for step in self.main.begin_turn(state) {
769            self.clear_repair(&step, state);
770        }
771        // The main flow has now seen any correction raised since it last
772        // advanced (its reset edges fired above): lower the flags so the
773        // next correction is a fresh rising edge.
774        for slot in self.corrections.keys() {
775            let flag = correction_flag(slot);
776            if state.get::<bool>(&flag) == Some(true) {
777                let _ = state.set(&flag, false);
778            }
779        }
780        // Repair bookkeeping is based on the pre-turn active set so an
781        // escalation signal can take effect this turn.
782        self.apply_repair(state);
783        self.main.relatch(state);
784    }
785
786    /// Advance one turn.
787    ///
788    /// A digression that completed on the previous turn has had its closing
789    /// turn projected; its resume policy applies now, and the turn then
790    /// proceeds as if the main flow had been driving all along (a new
791    /// digression may trigger, or the main flow advances). Otherwise: advance
792    /// the active digression, enter a triggered one (suspending the main
793    /// flow), or advance the main flow.
794    pub fn on_turn(&mut self, state: &State) {
795        self.advance_turn(state);
796        self.publish_timing(state);
797    }
798
799    fn advance_turn(&mut self, state: &State) {
800        if self.terminated.is_some() {
801            return;
802        }
803        // A correction can come while a digression drives; its flag stays
804        // raised until the main flow advances and sees it.
805        self.detect_corrections(state);
806        if self.active_is_closing() {
807            let closed = self.active.pop().expect("checked above");
808            self.apply_resume(closed.name, closed.resume, state);
809            if self.terminated.is_some() {
810                return;
811            }
812        }
813        // A triggered digression suspends whichever layer is driving, the
814        // main flow or another digression.
815        if let Some(idx) = self.triggered(state) {
816            self.enter(idx, state);
817            return;
818        }
819        match self.active.last_mut() {
820            Some(active) => active.monitor.on_turn(state),
821            None => self.advance_main(state),
822        }
823    }
824
825    /// Record a successful tool call against the active layer. A no-op once the
826    /// conversation has been terminated: nothing is governing, so there is no
827    /// marking for the call to advance. (In `Enforce` the call is denied before
828    /// it runs; in `Observe` it runs but must not move a flow that has ended.)
829    ///
830    /// A tool can itself fire a reset (`reset(..).when(called_ok(..))`), so the
831    /// main layer sheds the repair signals of whatever that un-latches, exactly
832    /// as the main layer does at a turn boundary. Repair
833    /// is tracked for the main flow only, so a digression just delegates.
834    pub fn on_tool_ok(&mut self, tool: &str, state: &State) {
835        self.advance_tool_ok(tool, state);
836        self.publish_timing(state);
837    }
838
839    fn advance_tool_ok(&mut self, tool: &str, state: &State) {
840        if self.terminated.is_some() {
841            return;
842        }
843        match self.active.last_mut() {
844            Some(active) => active.monitor.on_tool_ok(tool, state),
845            None => {
846                for step in self.main.begin_tool_ok(tool, state) {
847                    self.clear_repair(&step, state);
848                }
849                // No `apply_repair` here: repair counters advance per turn, not
850                // per tool call.
851                self.main.relatch(state);
852            }
853        }
854    }
855
856    /// Observe a tool call for conformance against the active layer (see
857    /// [`FlowMonitor::observe_tool`]).
858    ///
859    /// After termination nothing is advanced, but in `Observe` the call is
860    /// still recorded as a deviation: a tool used after the conversation ended
861    /// is exactly what that mode exists to catch, and the monitor cannot see it
862    /// for itself because the denial is the stack's, not the flow's.
863    pub fn observe_tool(&mut self, tool: &str, ok: bool, state: &State) {
864        if let Some(denial) = self.termination_denial() {
865            if self.mode == Enforcement::Observe {
866                self.main.record_violation(tool, denial);
867            }
868            return;
869        }
870        // The conformance check is the *stack's*, not the active monitor's, and
871        // the call is recorded through `Self::on_tool_ok` so a tool-fired reset
872        // still sheds its repair signals.
873        if self.mode == Enforcement::Observe
874            && let Err(reason) = self.admits_tool(tool, state)
875        {
876            self.current_mut().record_violation(tool, reason);
877        }
878        if ok {
879            self.on_tool_ok(tool, state);
880        } else {
881            self.on_tool_failed(state);
882        }
883    }
884
885    /// Whether `tool` is admitted right now (delegates to the active layer).
886    /// Every tool is denied once the conversation has been terminated.
887    pub fn admits_tool(&self, tool: &str, state: &State) -> Result<(), String> {
888        if let Some(denial) = self.termination_denial() {
889            return Err(denial);
890        }
891        self.current().admits_tool(tool, state)
892    }
893
894    /// Explain the active layer's control-plane state. After termination:
895    /// nothing active, nothing admitted, every tool blocked with the reason —
896    /// the conversation is over, so it is waiting for nothing. To ask instead
897    /// what it *never finished*, read [`main()`](Self::main): its marking and
898    /// `unmet_requirements()` are kept intact for exactly that audit.
899    pub fn explain(&self, state: &State) -> FlowExplanation {
900        let mut ex = self.current().explain(state);
901        if let Some(denial) = self.termination_denial() {
902            ex.active.clear();
903            ex.active_progress.clear();
904            ex.missing_requirements.clear();
905            for tool in std::mem::take(&mut ex.allowed_tools) {
906                ex.blocked_tools.insert(tool, denial.clone());
907            }
908            for reason in ex.blocked_tools.values_mut() {
909                *reason = denial.clone();
910            }
911        }
912        ex
913    }
914
915    /// The active layer's marking (the last driving layer's, after
916    /// termination — kept for audit).
917    pub fn marking(&self) -> &Marking {
918        self.current().marking()
919    }
920
921    /// Steps of the active layer that are eligible but not yet done. Empty
922    /// after termination.
923    pub fn active_steps(&self, state: &State) -> Vec<&Step> {
924        if self.terminated.is_some() {
925            return Vec::new();
926        }
927        self.current().active_steps(state)
928    }
929
930    /// Postures to project this turn: the active layer's active steps', or —
931    /// on the turn a digression completes — its closing steps' (see
932    /// [`FlowMonitor::closing_postures`]). Empty after termination.
933    pub fn active_postures(&self, state: &State) -> Vec<String> {
934        if self.terminated.is_some() {
935            return Vec::new();
936        }
937        if self.active_is_closing() {
938            return self.current().closing_postures();
939        }
940        self.current().active_postures(state)
941    }
942
943    /// Grounding lines to project this turn, chosen like
944    /// [`active_postures`](Self::active_postures).
945    pub fn active_grounds(&self, state: &State) -> Vec<String> {
946        if self.terminated.is_some() {
947            return Vec::new();
948        }
949        if self.active_is_closing() {
950            return self.current().closing_grounds(state);
951        }
952        self.current().active_grounds(state)
953    }
954
955    /// The active layer's unmet requirements. Empty after termination.
956    pub fn unmet_requirements(&self) -> Vec<String> {
957        if self.terminated.is_some() {
958            return Vec::new();
959        }
960        self.current().unmet_requirements()
961    }
962
963    /// Steps of the active layer that became active since the last call.
964    pub fn take_newly_active(&mut self, state: &State) -> Vec<String> {
965        if self.terminated.is_some() {
966            return Vec::new();
967        }
968        self.current_mut().take_newly_active(state)
969    }
970
971    /// The `on_enter` action registered for a step of the active layer.
972    pub fn enter_action(&self, step: &str) -> Option<&StepAction> {
973        if self.terminated.is_some() {
974            return None;
975        }
976        self.current().enter_action(step)
977    }
978
979    /// Replace a main-flow step's posture. Returns `true` when the step exists.
980    pub fn set_posture(&mut self, step_id: &str, posture: Option<String>) -> bool {
981        self.main.set_posture(step_id, posture)
982    }
983
984    /// Replace a main-flow step's grounding template. Returns `true` when the
985    /// step exists.
986    pub fn set_ground(&mut self, step_id: &str, ground: Option<String>) -> bool {
987        self.main.set_ground(step_id, ground)
988    }
989}
990
991#[cfg(test)]
992mod tests {
993    use super::*;
994
995    fn main_flow() -> CompiledFlow {
996        Flow::new()
997            .step("a")
998            .done(Guard::is_true("a_done"))
999            .step("b")
1000            .after("a")
1001            .terminal()
1002            .build()
1003            .expect("valid")
1004            .compile()
1005            .expect("compiles")
1006    }
1007
1008    fn faq_overlay() -> Overlay {
1009        let flow = Flow::new()
1010            .step("answer")
1011            .done(Guard::is_true("faq_answered"))
1012            .step("faq_end")
1013            .after("answer")
1014            .terminal()
1015            .require(["faq_end"])
1016            .build()
1017            .expect("valid")
1018            .compile()
1019            .expect("compiles");
1020        Overlay::new("faq", Guard::is_true("intent:faq"), flow, Resume::Previous)
1021    }
1022
1023    fn overlay(name: &str, trigger: &str, done_key: &str, resume: Resume) -> Overlay {
1024        let end = format!("{name}_end");
1025        let flow = Flow::new()
1026            .step(format!("{name}_step"))
1027            .done(Guard::is_true(done_key))
1028            .step(&end)
1029            .after(format!("{name}_step"))
1030            .terminal()
1031            .require([end.clone()])
1032            .build()
1033            .expect("valid")
1034            .compile()
1035            .expect("compiles");
1036        Overlay::new(name, Guard::is_true(trigger), flow, resume)
1037    }
1038
1039    #[test]
1040    fn a_digression_can_be_interrupted_by_another() {
1041        let state = State::new();
1042        let mut stack = FlowStack::new(main_flow(), Enforcement::Enforce)
1043            .with_overlay(overlay("faq", "intent:faq", "faq_done", Resume::Previous))
1044            .with_overlay(overlay(
1045                "clarify",
1046                "intent:clarify",
1047                "clarified",
1048                Resume::Previous,
1049            ));
1050
1051        let _ = state.set("intent:faq", true);
1052        stack.on_turn(&state);
1053        assert_eq!(stack.overlay_path(), ["faq"]);
1054
1055        // Mid-FAQ the user needs a clarification: it nests on top.
1056        let _ = state.set("intent:clarify", true);
1057        stack.on_turn(&state);
1058        assert_eq!(stack.overlay_path(), ["faq", "clarify"]);
1059        assert_eq!(stack.active_overlay(), Some("clarify"));
1060
1061        // The clarification completes (closing turn), then FAQ drives again.
1062        let _ = state.set("intent:clarify", false);
1063        let _ = state.set("clarified", true);
1064        stack.on_turn(&state);
1065        assert_eq!(stack.overlay_path(), ["faq", "clarify"], "closing turn");
1066        stack.on_turn(&state);
1067        assert_eq!(stack.overlay_path(), ["faq"]);
1068
1069        // FAQ completes; the main flow resumes where it was.
1070        let _ = state.set("intent:faq", false);
1071        let _ = state.set("faq_done", true);
1072        stack.on_turn(&state);
1073        stack.on_turn(&state);
1074        assert!(stack.overlay_path().is_empty());
1075        assert_eq!(stack.explain(&state).active, ["a"]);
1076    }
1077
1078    #[test]
1079    fn a_nested_terminate_ends_the_whole_conversation() {
1080        let state = State::new();
1081        let mut stack = FlowStack::new(main_flow(), Enforcement::Enforce)
1082            .with_overlay(overlay("faq", "intent:faq", "faq_done", Resume::Previous))
1083            .with_overlay(overlay(
1084                "cancel",
1085                "intent:cancel",
1086                "cancelled",
1087                Resume::Terminate,
1088            ));
1089        let _ = state.set("intent:faq", true);
1090        stack.on_turn(&state);
1091        let _ = state.set("intent:cancel", true);
1092        let _ = state.set("cancelled", true);
1093        stack.on_turn(&state);
1094        assert_eq!(stack.overlay_path(), ["faq", "cancel"]);
1095        stack.on_turn(&state);
1096        assert!(stack.is_terminated());
1097        assert!(stack.overlay_path().is_empty());
1098    }
1099
1100    #[test]
1101    fn a_digression_does_not_re_enter_itself() {
1102        let state = State::new();
1103        let mut stack = FlowStack::new(main_flow(), Enforcement::Enforce).with_overlay(overlay(
1104            "faq",
1105            "intent:faq",
1106            "faq_done",
1107            Resume::Previous,
1108        ));
1109        let _ = state.set("intent:faq", true);
1110        stack.on_turn(&state);
1111        stack.on_turn(&state);
1112        assert_eq!(
1113            stack.overlay_path(),
1114            ["faq"],
1115            "the trigger still holding does not stack it twice"
1116        );
1117    }
1118
1119    #[test]
1120    fn barge_ins_and_tool_failures_escalate_the_active_step() {
1121        let state = State::new();
1122        let mut stack = FlowStack::new(main_flow(), Enforcement::Enforce).with_repair(
1123            "a",
1124            RepairPolicy::new(10, 10)
1125                .escalate_after_interruptions(2)
1126                .escalate_after_tool_failures(3),
1127        );
1128        stack.on_turn(&state);
1129        stack.on_interrupted(&state);
1130        assert_eq!(state.get::<bool>(&escalate_flag("a")), None);
1131        stack.on_interrupted(&state);
1132        assert_eq!(state.get::<bool>(&escalate_flag("a")), Some(true));
1133
1134        let state = State::new();
1135        let mut stack = FlowStack::new(main_flow(), Enforcement::Enforce).with_repair(
1136            "a",
1137            RepairPolicy::new(10, 10).escalate_after_tool_failures(2),
1138        );
1139        stack.on_turn(&state);
1140        stack.observe_tool("lookup", false, &state);
1141        stack.observe_tool("lookup", false, &state);
1142        assert_eq!(state.get::<bool>(&escalate_flag("a")), Some(true));
1143    }
1144
1145    #[test]
1146    fn a_corrected_slot_raises_its_flag_once_and_clears_its_keys() {
1147        let state = State::new();
1148        let mut stack = FlowStack::new(main_flow(), Enforcement::Enforce)
1149            .with_correction("party_size", ["confirmed".to_string()]);
1150        let _ = state.set("party_size", 4);
1151        let _ = state.set("confirmed", true);
1152        stack.on_turn(&state);
1153        assert_eq!(
1154            state.get::<bool>(&correction_flag("party_size")),
1155            None,
1156            "first value is not a correction"
1157        );
1158
1159        let _ = state.set("party_size", 5);
1160        stack.detect_corrections(&state);
1161        assert_eq!(
1162            state.get::<bool>(&correction_flag("party_size")),
1163            Some(true)
1164        );
1165        assert_eq!(
1166            state.get::<bool>("confirmed"),
1167            None,
1168            "the confirmation is cleared"
1169        );
1170
1171        // The main flow advances past it: the flag drops for the next edge.
1172        stack.on_turn(&state);
1173        assert_eq!(
1174            state.get::<bool>(&correction_flag("party_size")),
1175            Some(false)
1176        );
1177    }
1178
1179    #[test]
1180    fn timing_follows_the_active_step_and_digression() {
1181        use std::time::Duration;
1182        let state = State::new();
1183        let a = VoiceTiming::new().reprompt_after(Duration::from_secs(6));
1184        let answer = VoiceTiming::new().uninterruptible();
1185        let mut stack = FlowStack::new(main_flow(), Enforcement::Enforce)
1186            .with_overlay(faq_overlay())
1187            .with_timing("a", a.clone())
1188            .with_timing("answer", answer.clone());
1189
1190        stack.publish_timing(&state);
1191        assert_eq!(state.get::<VoiceTiming>(VOICE_TIMING_KEY), Some(a.clone()));
1192
1193        // A digression takes over: its step's timing applies.
1194        let _ = state.set("intent:faq", true);
1195        stack.on_turn(&state);
1196        assert_eq!(state.get::<VoiceTiming>(VOICE_TIMING_KEY), Some(answer));
1197
1198        // Back in the main flow, past `a`: `b` has no timing, so none applies.
1199        let _ = state.set("intent:faq", false);
1200        let _ = state.set("faq_answered", true);
1201        stack.on_turn(&state);
1202        stack.on_turn(&state);
1203        assert_eq!(state.get::<VoiceTiming>(VOICE_TIMING_KEY), Some(a));
1204        let _ = state.set("a_done", true);
1205        stack.on_turn(&state);
1206        assert_eq!(state.get::<VoiceTiming>(VOICE_TIMING_KEY), None);
1207    }
1208
1209    #[test]
1210    fn bare_stack_behaves_like_its_monitor() {
1211        let state = State::new();
1212        let mut stack = FlowStack::new(main_flow(), Enforcement::Enforce);
1213        let mut mon = FlowMonitor::compiled(main_flow(), Enforcement::Enforce);
1214        for turn in 0..3 {
1215            if turn == 1 {
1216                let _ = state.set("a_done", true);
1217            }
1218            stack.on_turn(&state);
1219            mon.on_turn(&state);
1220            assert_eq!(stack.explain(&state).active, mon.explain(&state).active);
1221            assert_eq!(stack.marking().done, mon.marking().done);
1222        }
1223        assert!(stack.is_complete());
1224    }
1225
1226    #[test]
1227    fn overlay_suspends_main_then_resumes_previous() {
1228        let state = State::new();
1229        let mut stack =
1230            FlowStack::new(main_flow(), Enforcement::Enforce).with_overlay(faq_overlay());
1231
1232        assert!(stack.explain(&state).active.contains(&"a".to_string()));
1233        assert!(stack.active_overlay().is_none());
1234
1235        let _ = state.set("intent:faq", true);
1236        stack.on_turn(&state);
1237        assert_eq!(stack.active_overlay(), Some("faq"));
1238        assert!(stack.explain(&state).active.contains(&"answer".to_string()));
1239
1240        // The turn the digression completes it is still the projected layer
1241        // (its closing turn); the main flow is untouched underneath.
1242        let _ = state.set("faq_answered", true);
1243        let _ = state.set("intent:faq", false);
1244        stack.on_turn(&state);
1245        assert_eq!(stack.active_overlay(), Some("faq"));
1246        assert!(stack.explain(&state).active.is_empty());
1247        assert!(stack.main().marking().done.is_empty());
1248
1249        // Next boundary: resumed exactly where it was, and that same turn
1250        // advances the main flow.
1251        let _ = state.set("a_done", true);
1252        stack.on_turn(&state);
1253        assert!(stack.active_overlay().is_none());
1254        assert!(stack.main().marking().done.contains("a"));
1255        assert!(stack.explain(&state).active.is_empty());
1256        assert!(stack.is_complete());
1257    }
1258
1259    /// A digression that completes on its entry turn (the shape the safety
1260    /// hand-off policy lowers to: one terminal stage) must still be seen: it
1261    /// is the projected layer for that turn, its closing instruction is what
1262    /// the model is told, and the main flow's tools are not admitted.
1263    #[test]
1264    fn entry_complete_digression_is_projected_before_it_resumes() {
1265        let state = State::new();
1266        let main = Flow::new()
1267            .step("a")
1268            .allow(["main_tool"])
1269            .posture("MAIN")
1270            .done(Guard::is_true("a_done"))
1271            .step("b")
1272            .after("a")
1273            .terminal()
1274            .build()
1275            .expect("valid")
1276            .compile()
1277            .expect("compiles");
1278        // The digression governs its closing turn, so what it says about tools
1279        // is what holds: this one locks the main flow's tool outright.
1280        let handoff = Flow::new()
1281            .step("handoff")
1282            .posture("HAND OFF NOW")
1283            .terminal()
1284            .require(["handoff"])
1285            .never("main_tool")
1286            .until(Guard::is_true("human_joined"))
1287            .build()
1288            .expect("valid")
1289            .compile()
1290            .expect("compiles");
1291        let mut stack = FlowStack::new(main, Enforcement::Enforce).with_overlay(Overlay::new(
1292            "safety",
1293            Guard::is_true("intent:abuse"),
1294            handoff,
1295            Resume::Previous,
1296        ));
1297        assert_eq!(stack.active_postures(&state), ["MAIN"]);
1298
1299        let _ = state.set("intent:abuse", true);
1300        stack.on_turn(&state);
1301        assert_eq!(stack.active_overlay(), Some("safety"));
1302        assert_eq!(stack.active_postures(&state), ["HAND OFF NOW"]);
1303        // Complete, so nothing is *active* — but it is still the governing layer.
1304        assert!(stack.active_steps(&state).is_empty());
1305        assert!(stack.admits_tool("main_tool", &state).is_err());
1306        assert!(!stack.is_complete());
1307
1308        let _ = state.set("intent:abuse", false);
1309        stack.on_turn(&state);
1310        assert!(stack.active_overlay().is_none());
1311        assert_eq!(stack.active_postures(&state), ["MAIN"]);
1312        assert!(stack.admits_tool("main_tool", &state).is_ok());
1313    }
1314
1315    #[test]
1316    fn restart_resets_marking_but_not_state() {
1317        let state = State::new();
1318        let cancel = {
1319            let flow = Flow::new()
1320                .step("confirm")
1321                .done(Guard::is_true("confirmed"))
1322                .step("cancel_end")
1323                .after("confirm")
1324                .terminal()
1325                .require(["cancel_end"])
1326                .build()
1327                .expect("valid")
1328                .compile()
1329                .expect("compiles");
1330            Overlay::new(
1331                "cancel",
1332                Guard::is_true("intent:cancel"),
1333                flow,
1334                Resume::Restart,
1335            )
1336        };
1337        let mut stack = FlowStack::new(main_flow(), Enforcement::Enforce).with_overlay(cancel);
1338
1339        let _ = state.set("a_done", true);
1340        stack.on_turn(&state);
1341        assert!(stack.main().marking().done.contains("a"));
1342
1343        let _ = state.set("intent:cancel", true);
1344        stack.on_turn(&state);
1345        assert_eq!(stack.active_overlay(), Some("cancel"));
1346        let _ = state.set("confirmed", true);
1347        let _ = state.set("intent:cancel", false);
1348        stack.on_turn(&state); // closing turn
1349        assert_eq!(stack.active_overlay(), Some("cancel"));
1350        assert!(stack.main().marking().done.contains("a"));
1351
1352        // Next boundary: the monitor restarts and re-latches in the same turn.
1353        // State is untouched, so `a` completes again from the same facts.
1354        stack.on_turn(&state);
1355        assert!(stack.active_overlay().is_none());
1356        assert_eq!(state.get::<bool>("a_done"), Some(true));
1357        assert!(stack.main().marking().done.contains("a"));
1358        assert_eq!(stack.main().marking().turns, 1);
1359    }
1360
1361    /// A restart is a fresh pass: a step that had escalated must not start
1362    /// the new pass already escalated. The lowered completion guard references
1363    /// the escalate signal, so a stale signal would complete it on re-latch.
1364    #[test]
1365    fn restart_clears_repair_signals() {
1366        let state = State::new();
1367        let main = escalating_flow();
1368        let cancel = {
1369            let flow = Flow::new()
1370                .step("bye")
1371                .terminal()
1372                .require(["bye"])
1373                .build()
1374                .expect("valid")
1375                .compile()
1376                .expect("compiles");
1377            Overlay::new(
1378                "cancel",
1379                Guard::is_true("intent:cancel"),
1380                flow,
1381                Resume::Restart,
1382            )
1383        };
1384        let mut stack = FlowStack::new(main, Enforcement::Enforce)
1385            .with_overlay(cancel)
1386            .with_repair("collect", RepairPolicy::new(1, 2).escalate_to("handoff"));
1387        stack.on_turn(&state);
1388        stack.on_turn(&state); // escalated: collect done, handoff active
1389        assert_eq!(stack.explain(&state).active, ["handoff"]);
1390
1391        let _ = state.set("intent:cancel", true);
1392        stack.on_turn(&state); // enters and completes the digression
1393        let _ = state.set("intent:cancel", false);
1394        stack.on_turn(&state); // restart + re-latch
1395        assert!(stack.active_overlay().is_none());
1396        assert_eq!(stack.explain(&state).active, ["collect"]);
1397        assert_eq!(state.get::<bool>(&escalate_flag("collect")), Some(false));
1398        // And it can escalate again, from a fresh count.
1399        stack.on_turn(&state);
1400        assert_eq!(stack.explain(&state).active, ["handoff"]);
1401    }
1402
1403    /// A `Terminate` digression is projected for its closing turn (the model is
1404    /// told to say goodbye), and from the next boundary the stack governs
1405    /// nothing: no steps, no postures, every tool denied, further turns no-ops.
1406    #[test]
1407    fn terminate_ends_the_conversation() {
1408        let state = State::new();
1409        let main = Flow::new()
1410            .step("a")
1411            .allow(["main_tool"])
1412            .posture("MAIN")
1413            .done(Guard::is_true("a_done"))
1414            .step("b")
1415            .after("a")
1416            .terminal()
1417            .require(["b"])
1418            .build()
1419            .expect("valid")
1420            .compile()
1421            .expect("compiles");
1422        let bye = {
1423            let flow = Flow::new()
1424                .step("bye")
1425                .posture("SAY GOODBYE")
1426                .terminal()
1427                .require(["bye"])
1428                .build()
1429                .expect("valid")
1430                .compile()
1431                .expect("compiles");
1432            Overlay::new("bye", Guard::is_true("intent:bye"), flow, Resume::Terminate)
1433        };
1434        let mut stack = FlowStack::new(main, Enforcement::Enforce).with_overlay(bye);
1435        let _ = state.set("intent:bye", true);
1436        stack.on_turn(&state);
1437        assert_eq!(stack.active_overlay(), Some("bye"));
1438        assert_eq!(stack.active_postures(&state), ["SAY GOODBYE"]);
1439        assert!(!stack.is_terminated());
1440        assert!(!stack.is_complete());
1441
1442        stack.on_turn(&state);
1443        assert!(stack.is_terminated());
1444        assert!(stack.is_complete());
1445        assert!(stack.active_overlay().is_none());
1446        assert!(stack.active_steps(&state).is_empty());
1447        assert!(stack.active_postures(&state).is_empty());
1448        assert!(stack.unmet_requirements().is_empty());
1449        let denied = stack.admits_tool("main_tool", &state).unwrap_err();
1450        assert!(denied.contains("terminated"), "{denied}");
1451        assert!(denied.contains("bye"), "{denied}");
1452        let ex = stack.explain(&state);
1453        assert!(ex.active.is_empty());
1454        assert!(ex.allowed_tools.is_empty());
1455        assert_eq!(ex.blocked_tools.get("main_tool"), Some(&denied));
1456
1457        // Later turns change nothing, whatever state says — and neither does a
1458        // tool that slipped through (only possible in `Observe`): a flow that
1459        // has ended must not keep advancing.
1460        let _ = state.set("a_done", true);
1461        stack.on_turn(&state);
1462        assert!(stack.main().marking().done.is_empty());
1463        stack.on_tool_ok("main_tool", &state);
1464        stack.observe_tool("main_tool", true, &state);
1465        assert!(stack.main().marking().done.is_empty());
1466        assert!(stack.admits_tool("main_tool", &state).is_err());
1467    }
1468
1469    /// `Observe` exists to record what a session did wrong without blocking it.
1470    /// A tool called after the conversation ended is exactly that, and the
1471    /// monitor cannot see it — the denial belongs to the stack — so the stack
1472    /// records it. The marking still does not move.
1473    #[test]
1474    fn observe_mode_records_a_tool_used_after_termination() {
1475        let state = State::new();
1476        let bye = {
1477            let flow = Flow::new()
1478                .step("bye")
1479                .terminal()
1480                .require(["bye"])
1481                .build()
1482                .expect("valid")
1483                .compile()
1484                .expect("compiles");
1485            Overlay::new("bye", Guard::is_true("intent:bye"), flow, Resume::Terminate)
1486        };
1487        let mut stack = FlowStack::new(main_flow(), Enforcement::Observe).with_overlay(bye);
1488        let _ = state.set("intent:bye", true);
1489        stack.on_turn(&state); // closing turn
1490        stack.on_turn(&state); // terminated
1491        assert!(stack.is_terminated());
1492
1493        let _ = state.set("a_done", true);
1494        stack.observe_tool("anything", true, &state);
1495        let violations = stack.main().violations();
1496        assert_eq!(violations.len(), 1, "{violations:?}");
1497        assert_eq!(violations[0].subject, "anything");
1498        assert!(
1499            violations[0].reason.contains("terminated"),
1500            "{violations:?}"
1501        );
1502        // Recorded, not acted on: the ended flow did not advance.
1503        assert!(stack.main().marking().done.is_empty());
1504    }
1505
1506    #[test]
1507    fn repair_raises_then_clears_signals() {
1508        let state = State::new();
1509        let mut stack = FlowStack::new(main_flow(), Enforcement::Enforce)
1510            .with_repair("a", RepairPolicy::new(1, 3));
1511        stack.on_turn(&state);
1512        assert_eq!(state.get::<bool>("repair:a:reprompt"), Some(true));
1513        assert_eq!(state.get::<bool>("repair:a:escalate"), None);
1514        let _ = state.set("a_done", true);
1515        stack.on_turn(&state);
1516        stack.on_turn(&state);
1517        assert_eq!(state.get::<bool>("repair:a:reprompt"), Some(false));
1518    }
1519
1520    /// The shape the conversation compiler lowers for `escalate_to`: `collect`
1521    /// completes on `info` or on its escalate flag; `handoff` follows, gated on
1522    /// the flag; `done` follows `collect` only when it completed properly.
1523    fn escalating_flow() -> CompiledFlow {
1524        Flow::new()
1525            .step("collect")
1526            .done(Guard::any(vec![
1527                Guard::is_true("info"),
1528                Guard::is_true(escalate_flag("collect")),
1529            ]))
1530            .step("handoff")
1531            .after("collect")
1532            .gate(Guard::is_true(escalate_flag("collect")))
1533            .done(Guard::is_true("handoff_complete"))
1534            .step("done")
1535            .after_when("collect", Guard::is_true("info"))
1536            .terminal()
1537            .build()
1538            .expect("valid")
1539            .compile()
1540            .expect("compiles")
1541    }
1542
1543    /// `escalate_to` lowers to an edge gated on the escalate signal. Gates are
1544    /// evaluated every turn, so the signal must stay latched once the step
1545    /// has completed by escalating, or the hand-off target is active for one
1546    /// turn and then vanishes.
1547    #[test]
1548    fn escalation_edge_stays_open_after_the_step_completes() {
1549        let state = State::new();
1550        let mut stack = FlowStack::new(escalating_flow(), Enforcement::Enforce)
1551            .with_repair("collect", RepairPolicy::new(1, 2).escalate_to("handoff"));
1552
1553        stack.on_turn(&state); // active 1 turn: reprompt
1554        stack.on_turn(&state); // active 2 turns: escalate -> collect done -> handoff
1555        assert!(
1556            stack
1557                .explain(&state)
1558                .active
1559                .contains(&"handoff".to_string())
1560        );
1561        // The turn after, collect has left the active set; handoff must stay.
1562        stack.on_turn(&state);
1563        assert!(
1564            stack
1565                .explain(&state)
1566                .active
1567                .contains(&"handoff".to_string())
1568        );
1569        assert_eq!(state.get::<bool>(&reprompt_flag("collect")), Some(false));
1570        assert_eq!(state.get::<bool>(&escalate_flag("collect")), Some(true));
1571    }
1572
1573    /// A `Constraint::Reset` that un-latches an escalated step must also shed
1574    /// its latched escalate signal, before the re-latch: the lowered
1575    /// completion guard references that signal and would complete the step
1576    /// again on the spot, routing straight back to the hand-off target.
1577    #[test]
1578    fn reset_clears_a_latched_escalation() {
1579        let state = State::new();
1580        let main = Flow::new()
1581            .step("collect")
1582            .done(Guard::any(vec![
1583                Guard::is_true("info"),
1584                Guard::is_true(escalate_flag("collect")),
1585            ]))
1586            .step("handoff")
1587            .after("collect")
1588            .gate(Guard::is_true(escalate_flag("collect")))
1589            .done(Guard::is_true("handoff_complete"))
1590            .step("done")
1591            .after_when("collect", Guard::is_true("info"))
1592            .terminal()
1593            .reset(["collect"])
1594            .when(Guard::is_true("retry"))
1595            .build()
1596            .expect("valid")
1597            .compile()
1598            .expect("compiles");
1599        let mut stack = FlowStack::new(main, Enforcement::Enforce)
1600            .with_repair("collect", RepairPolicy::new(1, 2).escalate_to("handoff"));
1601
1602        stack.on_turn(&state);
1603        stack.on_turn(&state); // escalated
1604        assert_eq!(stack.explain(&state).active, ["handoff"]);
1605
1606        // The reset fires: collect is back, handoff is gone, the count restarts.
1607        let _ = state.set("retry", true);
1608        stack.on_turn(&state);
1609        assert_eq!(stack.explain(&state).active, ["collect"]);
1610        assert_eq!(state.get::<bool>(&escalate_flag("collect")), Some(false));
1611        assert_eq!(state.get::<bool>(&reprompt_flag("collect")), Some(true));
1612
1613        // A second stall escalates again from that fresh count.
1614        stack.on_turn(&state);
1615        assert_eq!(stack.explain(&state).active, ["handoff"]);
1616        assert_eq!(state.get::<bool>(&escalate_flag("collect")), Some(true));
1617    }
1618
1619    /// A reset can be gated on a *tool*, not just a state flag — `reset(..)
1620    /// .when(called_ok("start_over"))` is the natural "start over" button. That
1621    /// edge fires inside `on_tool_ok`, not at a turn boundary, so the repair
1622    /// signals must be shed there too, or the escalated step re-completes on its
1623    /// own stale flag exactly as it would at a turn boundary.
1624    #[test]
1625    fn a_tool_triggered_reset_clears_a_latched_escalation() {
1626        let state = State::new();
1627        let main = Flow::new()
1628            .step("collect")
1629            .allow(["start_over"])
1630            .done(Guard::any(vec![
1631                Guard::is_true("info"),
1632                Guard::is_true(escalate_flag("collect")),
1633            ]))
1634            .step("handoff")
1635            .after("collect")
1636            .gate(Guard::is_true(escalate_flag("collect")))
1637            .done(Guard::is_true("handoff_complete"))
1638            .step("done")
1639            .after_when("collect", Guard::is_true("info"))
1640            .terminal()
1641            .reset(["collect"])
1642            .when(Guard::called_ok("start_over"))
1643            .build()
1644            .expect("valid")
1645            .compile()
1646            .expect("compiles");
1647        let mut stack = FlowStack::new(main, Enforcement::Enforce)
1648            .with_repair("collect", RepairPolicy::new(1, 2).escalate_to("handoff"));
1649
1650        stack.on_turn(&state);
1651        stack.on_turn(&state); // escalated
1652        assert_eq!(stack.explain(&state).active, ["handoff"]);
1653
1654        // The caller hits "start over" mid-turn.
1655        stack.on_tool_ok("start_over", &state);
1656        assert_eq!(state.get::<bool>(&escalate_flag("collect")), Some(false));
1657        assert_eq!(stack.explain(&state).active, ["collect"]);
1658
1659        // And it stays reset across the next boundary rather than snapping back.
1660        stack.on_turn(&state);
1661        assert_eq!(stack.explain(&state).active, ["collect"]);
1662    }
1663
1664    #[test]
1665    fn tool_admission_follows_the_active_layer() {
1666        let state = State::new();
1667        let main = Flow::new()
1668            .step("a")
1669            .allow(["main_tool"])
1670            .done(Guard::is_true("a_done"))
1671            .step("b")
1672            .after("a")
1673            .terminal()
1674            .build()
1675            .expect("valid")
1676            .compile()
1677            .expect("compiles");
1678        let ov = {
1679            let flow = Flow::new()
1680                .step("answer")
1681                .allow(["faq_tool"])
1682                .done(Guard::is_true("faq_answered"))
1683                .step("faq_end")
1684                .after("answer")
1685                .terminal()
1686                .require(["faq_end"])
1687                .build()
1688                .expect("valid")
1689                .compile()
1690                .expect("compiles");
1691            Overlay::new("faq", Guard::is_true("intent:faq"), flow, Resume::Previous)
1692        };
1693        let mut stack = FlowStack::new(main, Enforcement::Enforce).with_overlay(ov);
1694        assert!(stack.admits_tool("main_tool", &state).is_ok());
1695        assert!(stack.admits_tool("faq_tool", &state).is_err());
1696        let _ = state.set("intent:faq", true);
1697        stack.on_turn(&state);
1698        assert!(stack.admits_tool("faq_tool", &state).is_ok());
1699        assert!(stack.admits_tool("main_tool", &state).is_err());
1700    }
1701}