1use 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, FlowSnapshot, Guard, Marking,
29 Step, StepAction,
30};
31use crate::state::State;
32
33pub fn escalate_flag(stage: &str) -> String {
35 format!("repair:{stage}:escalate")
36}
37
38pub fn reprompt_flag(stage: &str) -> String {
40 format!("repair:{stage}:reprompt")
41}
42
43pub fn correction_flag(slot: &str) -> String {
47 format!("correction:{slot}")
48}
49
50pub const TOOL_CALL_KEY: &str = "flow:tool_call";
56
57pub const TOOL_DENIED_KEY: &str = "flow:tool_denied";
60
61pub const TOOL_RESULT_KEY: &str = "flow:tool_result";
64
65pub const OVERLAY_STATE_KEY: &str = "flow:overlay";
68
69pub const TERMINATED_STATE_KEY: &str = "flow:terminated";
76
77#[derive(
84 Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, schemars::JsonSchema,
85)]
86#[serde(rename_all = "snake_case")]
87pub enum Resume {
88 #[default]
90 Previous,
91 Restart,
95 Terminate,
99}
100
101fn default_reprompt_after() -> u32 {
102 2
103}
104fn default_escalate_after() -> u32 {
105 4
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
115pub struct RepairPolicy {
116 #[serde(default = "default_reprompt_after")]
118 pub reprompt_after: u32,
119 #[serde(default = "default_escalate_after")]
121 pub escalate_after: u32,
122 #[serde(default, skip_serializing_if = "Option::is_none")]
124 pub escalate_to: Option<String>,
125 #[serde(default, skip_serializing_if = "Option::is_none")]
128 pub escalate_after_interruptions: Option<u32>,
129 #[serde(default, skip_serializing_if = "Option::is_none")]
132 pub escalate_after_tool_failures: Option<u32>,
133}
134
135impl Default for RepairPolicy {
136 fn default() -> Self {
137 Self {
138 reprompt_after: default_reprompt_after(),
139 escalate_after: default_escalate_after(),
140 escalate_to: None,
141 escalate_after_interruptions: None,
142 escalate_after_tool_failures: None,
143 }
144 }
145}
146
147impl RepairPolicy {
148 pub fn new(reprompt_after: u32, escalate_after: u32) -> Self {
150 Self {
151 reprompt_after,
152 escalate_after,
153 ..Self::default()
154 }
155 }
156
157 pub fn escalate_to(mut self, step: impl Into<String>) -> Self {
159 self.escalate_to = Some(step.into());
160 self
161 }
162
163 pub fn escalate_after_interruptions(mut self, n: u32) -> Self {
165 self.escalate_after_interruptions = Some(n);
166 self
167 }
168
169 pub fn escalate_after_tool_failures(mut self, n: u32) -> Self {
172 self.escalate_after_tool_failures = Some(n);
173 self
174 }
175}
176
177#[derive(Debug, Clone)]
180pub struct Overlay {
181 name: String,
182 trigger: Guard,
183 flow: Flow,
184 resume: Resume,
185}
186
187impl Overlay {
188 pub fn new(
190 name: impl Into<String>,
191 trigger: Guard,
192 flow: CompiledFlow,
193 resume: Resume,
194 ) -> Self {
195 Self {
196 name: name.into(),
197 trigger,
198 flow: flow.into_flow(),
199 resume,
200 }
201 }
202
203 pub fn name(&self) -> &str {
205 &self.name
206 }
207 pub fn trigger(&self) -> &Guard {
209 &self.trigger
210 }
211 pub fn flow(&self) -> &Flow {
213 &self.flow
214 }
215 pub fn flow_mut(&mut self) -> &mut Flow {
217 &mut self.flow
218 }
219 pub fn resume(&self) -> Resume {
221 self.resume
222 }
223}
224
225struct ActiveOverlay {
227 name: String,
228 monitor: FlowMonitor,
229 resume: Resume,
230}
231
232#[derive(Debug, Clone, Default, PartialEq, Eq)]
234pub struct DecisionScope {
235 pub questions: BTreeSet<String>,
238 pub reads: BTreeSet<String>,
240}
241
242pub type SharedFlowStack = Arc<parking_lot::Mutex<FlowStack>>;
248
249pub struct FlowStack {
263 main: FlowMonitor,
264 mode: Enforcement,
265 overlays: Vec<Overlay>,
266 active: Vec<ActiveOverlay>,
269 terminated: Option<String>,
271 repair: BTreeMap<String, RepairPolicy>,
273 active_turns: BTreeMap<String, u32>,
275 timing: BTreeMap<String, VoiceTiming>,
277 corrections: BTreeMap<String, Vec<String>>,
280 slot_values: BTreeMap<String, serde_json::Value>,
282 verbatim: BTreeMap<String, String>,
284 interruptions: BTreeMap<String, u32>,
286 tool_failures: BTreeMap<String, u32>,
288}
289
290impl std::fmt::Debug for FlowStack {
291 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
292 f.debug_struct("FlowStack")
293 .field("mode", &self.mode)
294 .field("overlays", &self.overlays.len())
295 .field(
296 "active",
297 &self.active.iter().map(|a| &a.name).collect::<Vec<_>>(),
298 )
299 .field("terminated", &self.terminated)
300 .field("repair", &self.repair)
301 .field("timing", &self.timing)
302 .field("corrections", &self.corrections)
303 .finish()
304 }
305}
306
307impl FlowStack {
308 pub fn new(main: CompiledFlow, mode: Enforcement) -> Self {
310 Self::from_monitor(FlowMonitor::compiled(main, mode))
311 }
312
313 pub fn from_monitor(main: FlowMonitor) -> Self {
317 Self {
318 mode: main.mode(),
319 main,
320 overlays: Vec::new(),
321 active: Vec::new(),
322 terminated: None,
323 repair: BTreeMap::new(),
324 active_turns: BTreeMap::new(),
325 timing: BTreeMap::new(),
326 corrections: BTreeMap::new(),
327 verbatim: BTreeMap::new(),
328 slot_values: BTreeMap::new(),
329 interruptions: BTreeMap::new(),
330 tool_failures: BTreeMap::new(),
331 }
332 }
333
334 pub fn with_overlay(mut self, overlay: Overlay) -> Self {
336 self.overlays.push(overlay);
337 self
338 }
339
340 pub fn with_overlays(mut self, overlays: impl IntoIterator<Item = Overlay>) -> Self {
342 self.overlays.extend(overlays);
343 self
344 }
345
346 pub fn with_ambient_tools(mut self, tools: impl IntoIterator<Item = String>) -> Self {
350 let tools: Vec<_> = tools.into_iter().collect();
351 let extend = |flow: &mut Flow| {
352 for name in &tools {
353 if !flow.ambient.contains(name) {
354 flow.ambient.push(name.clone());
355 }
356 }
357 };
358 extend(&mut self.main.flow);
359 for overlay in &mut self.overlays {
360 extend(&mut overlay.flow);
361 }
362 for active in &mut self.active {
363 extend(&mut active.monitor.flow);
364 }
365 self
366 }
367
368 pub fn with_repair(mut self, step: impl Into<String>, policy: RepairPolicy) -> Self {
370 self.repair.insert(step.into(), policy);
371 self
372 }
373
374 pub fn with_repairs(
376 mut self,
377 policies: impl IntoIterator<Item = (String, RepairPolicy)>,
378 ) -> Self {
379 self.repair.extend(policies);
380 self
381 }
382
383 pub fn with_correction(
394 mut self,
395 slot: impl Into<String>,
396 clear: impl IntoIterator<Item = String>,
397 ) -> Self {
398 self.corrections
399 .insert(slot.into(), clear.into_iter().collect());
400 self
401 }
402
403 pub fn with_corrections(
405 mut self,
406 corrections: impl IntoIterator<Item = (String, Vec<String>)>,
407 ) -> Self {
408 self.corrections.extend(corrections);
409 self
410 }
411
412 pub fn correction_policies(&self) -> &BTreeMap<String, Vec<String>> {
414 &self.corrections
415 }
416
417 pub fn with_verbatim(mut self, step: impl Into<String>, text: impl Into<String>) -> Self {
423 self.verbatim.insert(step.into(), text.into());
424 self
425 }
426
427 pub fn with_verbatims(mut self, texts: impl IntoIterator<Item = (String, String)>) -> Self {
429 self.verbatim.extend(texts);
430 self
431 }
432
433 pub fn verbatim_policies(&self) -> &BTreeMap<String, String> {
435 &self.verbatim
436 }
437
438 pub fn with_timing(mut self, step: impl Into<String>, timing: VoiceTiming) -> Self {
440 self.timing.insert(step.into(), timing);
441 self
442 }
443
444 pub fn with_timings(
446 mut self,
447 timings: impl IntoIterator<Item = (String, VoiceTiming)>,
448 ) -> Self {
449 self.timing.extend(timings);
450 self
451 }
452
453 pub fn timing_policies(&self) -> &BTreeMap<String, VoiceTiming> {
455 &self.timing
456 }
457
458 pub fn active_timing(&self, state: &State) -> VoiceTiming {
461 if self.timing.is_empty() || self.terminated.is_some() {
462 return VoiceTiming::default();
463 }
464 self.current()
465 .active_steps(state)
466 .iter()
467 .filter_map(|step| self.timing.get(&step.id))
468 .fold(VoiceTiming::default(), |acc, t| acc.merge(t))
469 }
470
471 pub fn publish_timing(&self, state: &State) {
477 self.publish_verbatim(state);
478 if self.timing.is_empty() {
479 return;
480 }
481 let timing = self.active_timing(state);
482 let current = state.get::<VoiceTiming>(VOICE_TIMING_KEY);
483 if timing.is_empty() {
484 if current.is_some() {
485 state.remove(VOICE_TIMING_KEY);
486 }
487 } else if current.as_ref() != Some(&timing) {
488 let _ = state.set(VOICE_TIMING_KEY, &timing);
489 }
490 }
491
492 fn publish_verbatim(&self, state: &State) {
496 if self.verbatim.is_empty() {
497 return;
498 }
499 let required = (self.terminated.is_none())
500 .then(|| {
501 self.current().active_steps(state).iter().find_map(|step| {
502 self.verbatim
503 .get(&step.id)
504 .map(|text| super::VerbatimRequirement {
505 step: step.id.clone(),
506 text: text.clone(),
507 })
508 })
509 })
510 .flatten();
511 let current = state.get::<super::VerbatimRequirement>(super::VERBATIM_KEY);
512 match required {
513 Some(req) if current.as_ref() != Some(&req) => {
514 let _ = state.set(super::VERBATIM_KEY, &req);
515 }
516 None if current.is_some() => {
517 state.remove(super::VERBATIM_KEY);
518 }
519 _ => {}
520 }
521 }
522
523 pub fn into_shared(self) -> SharedFlowStack {
526 Arc::new(parking_lot::Mutex::new(self))
527 }
528
529 pub fn mode(&self) -> Enforcement {
531 self.mode
532 }
533
534 pub fn overlays(&self) -> &[Overlay] {
536 &self.overlays
537 }
538
539 pub fn overlays_mut(&mut self) -> &mut [Overlay] {
541 &mut self.overlays
542 }
543
544 pub fn repair_policies(&self) -> &BTreeMap<String, RepairPolicy> {
546 &self.repair
547 }
548
549 pub fn main(&self) -> &FlowMonitor {
551 &self.main
552 }
553
554 pub fn current(&self) -> &FlowMonitor {
557 self.active.last().map_or(&self.main, |a| &a.monitor)
558 }
559
560 fn current_mut(&mut self) -> &mut FlowMonitor {
561 match self.active.last_mut() {
562 Some(a) => &mut a.monitor,
563 None => &mut self.main,
564 }
565 }
566
567 pub fn active_overlay(&self) -> Option<&str> {
569 self.active.last().map(|a| a.name.as_str())
570 }
571
572 pub fn overlay_path(&self) -> Vec<&str> {
575 self.active.iter().map(|a| a.name.as_str()).collect()
576 }
577
578 pub fn is_complete(&self) -> bool {
581 self.terminated.is_some() || (self.active.is_empty() && self.main.is_complete())
582 }
583
584 pub fn is_terminated(&self) -> bool {
589 self.terminated.is_some()
590 }
591
592 fn termination_denial(&self) -> Option<String> {
594 self.terminated
595 .as_ref()
596 .map(|by| format!("the conversation has ended: digression `{by}` terminated it"))
597 }
598
599 fn active_is_closing(&self) -> bool {
602 self.active.last().is_some_and(|a| a.monitor.is_complete())
603 }
604
605 fn triggered(&self, state: &State) -> Option<usize> {
609 self.overlays.iter().position(|ov| {
610 !self.active.iter().any(|a| a.name == ov.name) && self.main.eval(&ov.trigger, state)
611 })
612 }
613
614 fn enter(&mut self, idx: usize, state: &State) {
618 let ov = &self.overlays[idx];
619 let mut monitor = FlowMonitor::new(ov.flow.clone(), self.mode);
620 monitor.on_turn(state);
621 self.active.push(ActiveOverlay {
622 name: ov.name.clone(),
623 monitor,
624 resume: ov.resume,
625 });
626 }
627
628 fn detect_corrections(&mut self, state: &State) {
635 if self.corrections.is_empty() {
636 return;
637 }
638 for (slot, clear) in &self.corrections {
639 let current = state.get_raw(slot);
640 let corrected = matches!(
641 (self.slot_values.get(slot), ¤t),
642 (Some(before), Some(now)) if !crate::state::equivalent_values(before, now)
643 );
644 if corrected {
645 let _ = state.set(correction_flag(slot), true);
646 for key in clear {
647 state.remove(key);
648 }
649 }
650 match current {
651 Some(v) => {
652 self.slot_values.insert(slot.clone(), v);
653 }
654 None => {
655 self.slot_values.remove(slot);
656 }
657 }
658 }
659 }
660
661 pub fn on_interrupted(&mut self, state: &State) {
665 self.count_against_active(
666 state,
667 |p| p.escalate_after_interruptions,
668 |s| &mut s.interruptions,
669 );
670 }
671
672 pub fn on_tool_failed(&mut self, state: &State) {
676 self.count_against_active(
677 state,
678 |p| p.escalate_after_tool_failures,
679 |s| &mut s.tool_failures,
680 );
681 }
682
683 fn count_against_active(
684 &mut self,
685 state: &State,
686 threshold: impl Fn(&RepairPolicy) -> Option<u32>,
687 counters: impl Fn(&mut Self) -> &mut BTreeMap<String, u32>,
688 ) {
689 if self.terminated.is_some() || !self.active.is_empty() || self.repair.is_empty() {
690 return;
691 }
692 let steps: Vec<(String, u32)> = self
693 .main
694 .active_steps(state)
695 .iter()
696 .filter_map(|s| {
697 self.repair
698 .get(&s.id)
699 .and_then(&threshold)
700 .map(|limit| (s.id.clone(), limit))
701 })
702 .collect();
703 for (step, limit) in steps {
704 let count = counters(self).entry(step.clone()).or_insert(0);
705 *count += 1;
706 if *count >= limit {
707 let _ = state.set(escalate_flag(&step), true);
708 }
709 }
710 }
711
712 fn apply_repair(&mut self, state: &State, counted: bool) {
716 if self.repair.is_empty() {
717 return;
718 }
719 let active: BTreeSet<String> = self
720 .main
721 .active_steps(state)
722 .iter()
723 .map(|s| s.id.clone())
724 .collect();
725 let left: Vec<String> = self
726 .active_turns
727 .keys()
728 .filter(|k| !active.contains(*k))
729 .cloned()
730 .collect();
731 for step in left {
732 self.active_turns.remove(&step);
733 self.interruptions.remove(&step);
734 self.tool_failures.remove(&step);
735 let _ = state.set(reprompt_flag(&step), false);
736 if !self.main.marking().done.contains(&step) {
745 let _ = state.set(escalate_flag(&step), false);
746 }
747 }
748 for step in &active {
749 let count = self.active_turns.entry(step.clone()).or_insert(0);
750 if counted {
751 *count += 1;
752 }
753 if let Some(rp) = self.repair.get(step) {
754 if *count >= rp.reprompt_after {
755 let _ = state.set(reprompt_flag(step), true);
756 }
757 if *count >= rp.escalate_after {
758 let _ = state.set(escalate_flag(step), true);
759 }
760 }
761 }
762 }
763
764 fn clear_repair(&mut self, step: &str, state: &State) {
770 self.active_turns.remove(step);
771 self.interruptions.remove(step);
772 self.tool_failures.remove(step);
773 let _ = state.set(reprompt_flag(step), false);
774 let _ = state.set(escalate_flag(step), false);
775 }
776
777 fn apply_resume(&mut self, by: String, resume: Resume, state: &State) {
780 match resume {
781 Resume::Previous => {}
783 Resume::Restart => match self.active.last_mut() {
784 Some(beneath) => beneath.monitor.restart(),
785 None => {
786 self.main.restart();
787 let steps: Vec<String> = self.repair.keys().cloned().collect();
789 for step in steps {
790 self.clear_repair(&step, state);
791 }
792 }
793 },
794 Resume::Terminate => {
795 self.active.clear();
796 self.terminated = Some(by);
797 }
798 }
799 }
800
801 fn advance_main(&mut self, state: &State, counted: bool) {
805 for step in self.main.begin_turn(state) {
806 self.clear_repair(&step, state);
807 }
808 for slot in self.corrections.keys() {
812 let flag = correction_flag(slot);
813 if state.get::<bool>(&flag) == Some(true) {
814 let _ = state.set(&flag, false);
815 }
816 }
817 self.apply_repair(state, counted);
820 self.main.relatch(state);
821 }
822
823 pub fn on_turn(&mut self, state: &State) {
832 self.on_turn_counted(state, true);
833 }
834
835 pub fn on_turn_counted(&mut self, state: &State, counted: bool) {
842 self.advance_turn(state, counted);
843 self.publish_timing(state);
844 }
845
846 fn advance_turn(&mut self, state: &State, counted: bool) {
847 if self.terminated.is_some() {
848 return;
849 }
850 self.detect_corrections(state);
853 if self.active_is_closing() {
854 let closed = self.active.pop().expect("checked above");
855 self.apply_resume(closed.name, closed.resume, state);
856 if self.terminated.is_some() {
857 return;
858 }
859 }
860 if let Some(idx) = self.triggered(state) {
863 self.enter(idx, state);
864 return;
865 }
866 match self.active.last_mut() {
867 Some(active) => active.monitor.on_turn(state),
868 None => self.advance_main(state, counted),
869 }
870 }
871
872 pub fn on_tool_ok(&mut self, tool: &str, state: &State) {
882 self.advance_tool_ok(tool, state);
883 self.publish_timing(state);
884 }
885
886 fn advance_tool_ok(&mut self, tool: &str, state: &State) {
887 if self.terminated.is_some() {
888 return;
889 }
890 match self.active.last_mut() {
891 Some(active) => active.monitor.on_tool_ok(tool, state),
892 None => {
893 for step in self.main.begin_tool_ok(tool, state) {
894 self.clear_repair(&step, state);
895 }
896 self.main.relatch(state);
899 }
900 }
901 }
902
903 pub fn observe_tool(&mut self, tool: &str, ok: bool, state: &State) {
911 if let Some(denial) = self.termination_denial() {
912 if self.mode == Enforcement::Observe {
913 self.main.record_violation(tool, denial);
914 }
915 return;
916 }
917 if self.mode == Enforcement::Observe
921 && let Err(reason) = self.admits_tool(tool, state)
922 {
923 self.current_mut().record_violation(tool, reason);
924 }
925 if ok {
926 self.on_tool_ok(tool, state);
927 } else {
928 self.on_tool_failed(state);
929 }
930 }
931
932 pub fn admits_tool(&self, tool: &str, state: &State) -> Result<(), String> {
935 if let Some(denial) = self.termination_denial() {
936 return Err(denial);
937 }
938 self.current().admits_tool(tool, state)
939 }
940
941 pub fn offers_tool(&self, tool: &str, state: &State) -> bool {
945 self.termination_denial().is_none()
946 && (self.current().offers_tool(tool, state) || self.digression_offers(tool, state))
947 }
948
949 fn digression_offers(&self, tool: &str, state: &State) -> bool {
955 self.overlays.iter().any(|ov| {
956 !self.active.iter().any(|a| a.name == ov.name)
957 && self.main.possible(&ov.trigger, state).is_none()
958 && ov
959 .flow
960 .steps
961 .iter()
962 .filter(|s| s.after.is_empty())
963 .any(|s| s.allow.iter().any(|a| a == tool))
964 })
965 }
966
967 pub fn blocking_keys(&self, tool: &str, state: &State) -> std::collections::BTreeSet<String> {
970 if self.termination_denial().is_some() {
971 return std::collections::BTreeSet::new();
972 }
973 self.current().blocking_keys(tool, state)
974 }
975
976 pub fn explain(&self, state: &State) -> FlowExplanation {
982 let mut ex = self.current().explain(state);
983 if let Some(denial) = self.termination_denial() {
984 ex.active.clear();
985 ex.active_progress.clear();
986 ex.missing_requirements.clear();
987 for tool in std::mem::take(&mut ex.allowed_tools) {
988 ex.blocked_tools.insert(tool, denial.clone());
989 }
990 for reason in ex.blocked_tools.values_mut() {
991 *reason = denial.clone();
992 }
993 }
994 ex
995 }
996
997 pub fn snapshot(&self, state: &State) -> FlowSnapshot {
1003 FlowSnapshot {
1004 explanation: self.explain(state),
1005 done: self.marking().done.iter().cloned().collect(),
1006 complete: self.is_complete(),
1007 overlay_path: self.overlay_path().into_iter().map(str::to_owned).collect(),
1008 terminated: self.is_terminated(),
1009 }
1010 }
1011
1012 pub fn decision_scope(&self, state: &State) -> DecisionScope {
1017 if self.is_terminated() {
1018 return DecisionScope::default();
1019 }
1020 let current = self.current();
1021 let mut questions = current.flow().decisions();
1022 let mut reads = current.reads_now(state);
1023 for ov in &self.overlays {
1024 if !self.active.iter().any(|a| a.name == ov.name) {
1025 questions.extend(ov.trigger.decisions());
1026 reads.extend(ov.trigger.state_keys());
1027 }
1028 }
1029 DecisionScope { questions, reads }
1030 }
1031
1032 pub fn gate_scope(&self, tools: &[String], state: &State) -> DecisionScope {
1040 if self.is_terminated() {
1041 return DecisionScope::default();
1042 }
1043 let mut scope = DecisionScope::default();
1044 for c in &self.current().flow().constraints {
1045 if let crate::flow::Constraint::NeverUntil { tool, until } = c
1046 && tools.contains(tool)
1047 {
1048 scope.questions.extend(until.decisions());
1049 scope.reads.extend(until.state_keys());
1050 }
1051 }
1052 for tool in tools {
1053 let frontier = self.current().frontier_questions(tool, state);
1054 scope.questions.extend(frontier.questions);
1055 scope.reads.extend(frontier.reads);
1056 }
1057 for ov in &self.overlays {
1058 if !self.active.iter().any(|a| a.name == ov.name)
1059 && tools.iter().any(|t| ov.flow.tool_universe().contains(t))
1060 {
1061 scope.questions.extend(ov.trigger.decisions());
1062 scope.reads.extend(ov.trigger.state_keys());
1063 }
1064 }
1065 scope
1066 }
1067
1068 pub fn on_decisions(&mut self, state: &State) {
1074 if self.is_terminated() {
1075 return;
1076 }
1077 if let Some(idx) = self.triggered(state) {
1078 self.enter(idx, state);
1079 } else {
1080 self.relatch(state);
1081 }
1082 self.publish_timing(state);
1083 }
1084
1085 pub fn relatch(&mut self, state: &State) {
1090 if self.is_terminated() {
1091 return;
1092 }
1093 match self.active.last_mut() {
1094 Some(active) => active.monitor.relatch(state),
1095 None => {
1096 for step in self.main.apply_resets(state) {
1097 self.clear_repair(&step, state);
1098 }
1099 self.main.relatch(state);
1100 }
1101 }
1102 }
1103
1104 pub fn marking(&self) -> &Marking {
1107 self.current().marking()
1108 }
1109
1110 pub fn active_steps(&self, state: &State) -> Vec<&Step> {
1113 if self.terminated.is_some() {
1114 return Vec::new();
1115 }
1116 self.current().active_steps(state)
1117 }
1118
1119 pub fn active_postures(&self, state: &State) -> Vec<String> {
1123 if self.terminated.is_some() {
1124 return Vec::new();
1125 }
1126 if self.active_is_closing() {
1127 return self.current().closing_postures();
1128 }
1129 self.current().active_postures(state)
1130 }
1131
1132 pub fn active_grounds(&self, state: &State) -> Vec<String> {
1135 if self.terminated.is_some() {
1136 return Vec::new();
1137 }
1138 if self.active_is_closing() {
1139 return self.current().closing_grounds(state);
1140 }
1141 self.current().active_grounds(state)
1142 }
1143
1144 pub fn unmet_requirements(&self) -> Vec<String> {
1146 if self.terminated.is_some() {
1147 return Vec::new();
1148 }
1149 self.current().unmet_requirements()
1150 }
1151
1152 pub fn take_newly_active(&mut self, state: &State) -> Vec<String> {
1154 if self.terminated.is_some() {
1155 return Vec::new();
1156 }
1157 self.current_mut().take_newly_active(state)
1158 }
1159
1160 pub fn enter_action(&self, step: &str) -> Option<&StepAction> {
1162 if self.terminated.is_some() {
1163 return None;
1164 }
1165 self.current().enter_action(step)
1166 }
1167
1168 pub fn set_posture(&mut self, step_id: &str, posture: Option<String>) -> bool {
1170 self.main.set_posture(step_id, posture)
1171 }
1172
1173 pub fn set_ground(&mut self, step_id: &str, ground: Option<String>) -> bool {
1176 self.main.set_ground(step_id, ground)
1177 }
1178}
1179
1180#[cfg(test)]
1181mod tests {
1182 use super::*;
1183
1184 fn main_flow() -> CompiledFlow {
1185 Flow::new()
1186 .step("a")
1187 .done(Guard::is_true("a_done"))
1188 .step("b")
1189 .after("a")
1190 .terminal()
1191 .build()
1192 .expect("valid")
1193 .compile()
1194 .expect("compiles")
1195 }
1196
1197 fn faq_overlay() -> Overlay {
1198 let flow = Flow::new()
1199 .step("answer")
1200 .done(Guard::is_true("faq_answered"))
1201 .step("faq_end")
1202 .after("answer")
1203 .terminal()
1204 .require(["faq_end"])
1205 .build()
1206 .expect("valid")
1207 .compile()
1208 .expect("compiles");
1209 Overlay::new("faq", Guard::is_true("intent:faq"), flow, Resume::Previous)
1210 }
1211
1212 fn overlay(name: &str, trigger: &str, done_key: &str, resume: Resume) -> Overlay {
1213 let end = format!("{name}_end");
1214 let flow = Flow::new()
1215 .step(format!("{name}_step"))
1216 .done(Guard::is_true(done_key))
1217 .step(&end)
1218 .after(format!("{name}_step"))
1219 .terminal()
1220 .require([end.clone()])
1221 .build()
1222 .expect("valid")
1223 .compile()
1224 .expect("compiles");
1225 Overlay::new(name, Guard::is_true(trigger), flow, resume)
1226 }
1227
1228 #[test]
1229 fn snapshot_keeps_layer_progress_separate_from_stack_completion() {
1230 let state = State::new();
1231 let mut stack =
1232 FlowStack::new(main_flow(), Enforcement::Enforce).with_overlay(faq_overlay());
1233 let _ = state.set("a_done", true);
1234 stack.on_turn(&state);
1235 let _ = state.set("intent:faq", true);
1236 stack.on_turn(&state);
1237 let _ = state.set("faq_answered", true);
1238 stack.on_turn(&state);
1239
1240 let snapshot = stack.snapshot(&state);
1241 assert_eq!(snapshot.done, ["answer", "faq_end"]);
1242 assert_eq!(snapshot.overlay_path, ["faq"]);
1243 assert!(snapshot.explanation.missing_requirements.is_empty());
1244 assert!(!snapshot.complete, "a closing digression is still active");
1245 assert!(!snapshot.terminated);
1246 assert!(stack.main().marking().done.contains("a"));
1247
1248 let _ = state.set("intent:faq", false);
1249 stack.on_turn(&state);
1250 let snapshot = stack.snapshot(&state);
1251 assert_eq!(snapshot.done, ["a", "b"]);
1252 assert!(snapshot.overlay_path.is_empty());
1253 assert!(snapshot.complete);
1254 }
1255
1256 #[test]
1257 fn snapshot_reports_termination_without_inventing_main_progress() {
1258 let state = State::new();
1259 let mut stack = FlowStack::new(main_flow(), Enforcement::Enforce).with_overlay(overlay(
1260 "cancel",
1261 "cancel",
1262 "cancelled",
1263 Resume::Terminate,
1264 ));
1265 let _ = state.set("cancel", true);
1266 let _ = state.set("cancelled", true);
1267 stack.on_turn(&state);
1268 assert!(
1269 !stack.snapshot(&state).terminated,
1270 "closing turn comes first"
1271 );
1272 stack.on_turn(&state);
1273 let snapshot = stack.snapshot(&state);
1274 assert!(snapshot.terminated);
1275 assert!(snapshot.complete);
1276 assert!(snapshot.explanation.active.is_empty());
1277 assert!(snapshot.explanation.allowed_tools.is_empty());
1278 assert!(snapshot.done.is_empty(), "main task was never completed");
1279 let _ = state.set("a_done", true);
1280 stack.relatch(&state);
1281 assert!(stack.snapshot(&state).done.is_empty());
1282 }
1283
1284 #[test]
1285 fn a_digression_can_be_interrupted_by_another() {
1286 let state = State::new();
1287 let mut stack = FlowStack::new(main_flow(), Enforcement::Enforce)
1288 .with_overlay(overlay("faq", "intent:faq", "faq_done", Resume::Previous))
1289 .with_overlay(overlay(
1290 "clarify",
1291 "intent:clarify",
1292 "clarified",
1293 Resume::Previous,
1294 ));
1295
1296 let _ = state.set("intent:faq", true);
1297 stack.on_turn(&state);
1298 assert_eq!(stack.overlay_path(), ["faq"]);
1299
1300 let _ = state.set("intent:clarify", true);
1302 stack.on_turn(&state);
1303 assert_eq!(stack.overlay_path(), ["faq", "clarify"]);
1304 assert_eq!(stack.active_overlay(), Some("clarify"));
1305
1306 let _ = state.set("intent:clarify", false);
1308 let _ = state.set("clarified", true);
1309 stack.on_turn(&state);
1310 assert_eq!(stack.overlay_path(), ["faq", "clarify"], "closing turn");
1311 stack.on_turn(&state);
1312 assert_eq!(stack.overlay_path(), ["faq"]);
1313
1314 let _ = state.set("intent:faq", false);
1316 let _ = state.set("faq_done", true);
1317 stack.on_turn(&state);
1318 stack.on_turn(&state);
1319 assert!(stack.overlay_path().is_empty());
1320 assert_eq!(stack.explain(&state).active, ["a"]);
1321 }
1322
1323 #[test]
1324 fn a_nested_terminate_ends_the_whole_conversation() {
1325 let state = State::new();
1326 let mut stack = FlowStack::new(main_flow(), Enforcement::Enforce)
1327 .with_overlay(overlay("faq", "intent:faq", "faq_done", Resume::Previous))
1328 .with_overlay(overlay(
1329 "cancel",
1330 "intent:cancel",
1331 "cancelled",
1332 Resume::Terminate,
1333 ));
1334 let _ = state.set("intent:faq", true);
1335 stack.on_turn(&state);
1336 let _ = state.set("intent:cancel", true);
1337 let _ = state.set("cancelled", true);
1338 stack.on_turn(&state);
1339 assert_eq!(stack.overlay_path(), ["faq", "cancel"]);
1340 stack.on_turn(&state);
1341 assert!(stack.is_terminated());
1342 assert!(stack.overlay_path().is_empty());
1343 }
1344
1345 #[test]
1346 fn a_digression_does_not_re_enter_itself() {
1347 let state = State::new();
1348 let mut stack = FlowStack::new(main_flow(), Enforcement::Enforce).with_overlay(overlay(
1349 "faq",
1350 "intent:faq",
1351 "faq_done",
1352 Resume::Previous,
1353 ));
1354 let _ = state.set("intent:faq", true);
1355 stack.on_turn(&state);
1356 stack.on_turn(&state);
1357 assert_eq!(
1358 stack.overlay_path(),
1359 ["faq"],
1360 "the trigger still holding does not stack it twice"
1361 );
1362 }
1363
1364 #[test]
1365 fn barge_ins_and_tool_failures_escalate_the_active_step() {
1366 let state = State::new();
1367 let mut stack = FlowStack::new(main_flow(), Enforcement::Enforce).with_repair(
1368 "a",
1369 RepairPolicy::new(10, 10)
1370 .escalate_after_interruptions(2)
1371 .escalate_after_tool_failures(3),
1372 );
1373 stack.on_turn(&state);
1374 stack.on_interrupted(&state);
1375 assert_eq!(state.get::<bool>(&escalate_flag("a")), None);
1376 stack.on_interrupted(&state);
1377 assert_eq!(state.get::<bool>(&escalate_flag("a")), Some(true));
1378
1379 let state = State::new();
1380 let mut stack = FlowStack::new(main_flow(), Enforcement::Enforce).with_repair(
1381 "a",
1382 RepairPolicy::new(10, 10).escalate_after_tool_failures(2),
1383 );
1384 stack.on_turn(&state);
1385 stack.observe_tool("lookup", false, &state);
1386 stack.observe_tool("lookup", false, &state);
1387 assert_eq!(state.get::<bool>(&escalate_flag("a")), Some(true));
1388 }
1389
1390 #[test]
1391 fn a_corrected_slot_raises_its_flag_once_and_clears_its_keys() {
1392 let state = State::new();
1393 let mut stack = FlowStack::new(main_flow(), Enforcement::Enforce)
1394 .with_correction("party_size", ["confirmed".to_string()]);
1395 let _ = state.set("party_size", 4);
1396 let _ = state.set("confirmed", true);
1397 stack.on_turn(&state);
1398 assert_eq!(
1399 state.get::<bool>(&correction_flag("party_size")),
1400 None,
1401 "first value is not a correction"
1402 );
1403
1404 let _ = state.set("party_size", 5);
1405 stack.detect_corrections(&state);
1406 assert_eq!(
1407 state.get::<bool>(&correction_flag("party_size")),
1408 Some(true)
1409 );
1410 assert_eq!(
1411 state.get::<bool>("confirmed"),
1412 None,
1413 "the confirmation is cleared"
1414 );
1415
1416 stack.on_turn(&state);
1418 assert_eq!(
1419 state.get::<bool>(&correction_flag("party_size")),
1420 Some(false)
1421 );
1422 }
1423
1424 #[test]
1425 fn a_restated_slot_is_not_a_correction() {
1426 let state = State::new();
1429 let mut stack = FlowStack::new(main_flow(), Enforcement::Enforce)
1430 .with_correction("slot", ["confirmed".to_string()]);
1431 let _ = state.set("slot", "tomorrow at 7 pm");
1432 stack.on_turn(&state);
1433 let _ = state.set("confirmed", true);
1434 let _ = state.set("slot", "Tomorrow at 7 PM.");
1435 stack.detect_corrections(&state);
1436 assert_eq!(state.get::<bool>(&correction_flag("slot")), None);
1437 assert_eq!(state.get::<bool>("confirmed"), Some(true));
1438
1439 let _ = state.set("slot", "tomorrow at 8 pm");
1440 stack.detect_corrections(&state);
1441 assert_eq!(state.get::<bool>(&correction_flag("slot")), Some(true));
1442 assert_eq!(state.get::<bool>("confirmed"), None);
1443 }
1444
1445 #[test]
1446 fn timing_follows_the_active_step_and_digression() {
1447 use std::time::Duration;
1448 let state = State::new();
1449 let a = VoiceTiming::new().reprompt_after(Duration::from_secs(6));
1450 let answer = VoiceTiming::new().uninterruptible();
1451 let mut stack = FlowStack::new(main_flow(), Enforcement::Enforce)
1452 .with_overlay(faq_overlay())
1453 .with_timing("a", a.clone())
1454 .with_timing("answer", answer.clone());
1455
1456 stack.publish_timing(&state);
1457 assert_eq!(state.get::<VoiceTiming>(VOICE_TIMING_KEY), Some(a.clone()));
1458
1459 let _ = state.set("intent:faq", true);
1461 stack.on_turn(&state);
1462 assert_eq!(state.get::<VoiceTiming>(VOICE_TIMING_KEY), Some(answer));
1463
1464 let _ = state.set("intent:faq", false);
1466 let _ = state.set("faq_answered", true);
1467 stack.on_turn(&state);
1468 stack.on_turn(&state);
1469 assert_eq!(state.get::<VoiceTiming>(VOICE_TIMING_KEY), Some(a));
1470 let _ = state.set("a_done", true);
1471 stack.on_turn(&state);
1472 assert_eq!(state.get::<VoiceTiming>(VOICE_TIMING_KEY), None);
1473 }
1474
1475 #[test]
1476 fn bare_stack_behaves_like_its_monitor() {
1477 let state = State::new();
1478 let mut stack = FlowStack::new(main_flow(), Enforcement::Enforce);
1479 let mut mon = FlowMonitor::compiled(main_flow(), Enforcement::Enforce);
1480 for turn in 0..3 {
1481 if turn == 1 {
1482 let _ = state.set("a_done", true);
1483 }
1484 stack.on_turn(&state);
1485 mon.on_turn(&state);
1486 assert_eq!(stack.explain(&state).active, mon.explain(&state).active);
1487 assert_eq!(stack.marking().done, mon.marking().done);
1488 }
1489 assert!(stack.is_complete());
1490 }
1491
1492 #[test]
1493 fn overlay_suspends_main_then_resumes_previous() {
1494 let state = State::new();
1495 let mut stack =
1496 FlowStack::new(main_flow(), Enforcement::Enforce).with_overlay(faq_overlay());
1497
1498 assert!(stack.explain(&state).active.contains(&"a".to_string()));
1499 assert!(stack.active_overlay().is_none());
1500
1501 let _ = state.set("intent:faq", true);
1502 stack.on_turn(&state);
1503 assert_eq!(stack.active_overlay(), Some("faq"));
1504 assert!(stack.explain(&state).active.contains(&"answer".to_string()));
1505
1506 let _ = state.set("faq_answered", true);
1509 let _ = state.set("intent:faq", false);
1510 stack.on_turn(&state);
1511 assert_eq!(stack.active_overlay(), Some("faq"));
1512 assert!(stack.explain(&state).active.is_empty());
1513 assert!(stack.main().marking().done.is_empty());
1514
1515 let _ = state.set("a_done", true);
1518 stack.on_turn(&state);
1519 assert!(stack.active_overlay().is_none());
1520 assert!(stack.main().marking().done.contains("a"));
1521 assert!(stack.explain(&state).active.is_empty());
1522 assert!(stack.is_complete());
1523 }
1524
1525 #[test]
1530 fn entry_complete_digression_is_projected_before_it_resumes() {
1531 let state = State::new();
1532 let main = Flow::new()
1533 .step("a")
1534 .allow(["main_tool"])
1535 .posture("MAIN")
1536 .done(Guard::is_true("a_done"))
1537 .step("b")
1538 .after("a")
1539 .terminal()
1540 .build()
1541 .expect("valid")
1542 .compile()
1543 .expect("compiles");
1544 let handoff = Flow::new()
1547 .step("handoff")
1548 .posture("HAND OFF NOW")
1549 .terminal()
1550 .require(["handoff"])
1551 .never("main_tool")
1552 .until(Guard::is_true("human_joined"))
1553 .build()
1554 .expect("valid")
1555 .compile()
1556 .expect("compiles");
1557 let mut stack = FlowStack::new(main, Enforcement::Enforce).with_overlay(Overlay::new(
1558 "safety",
1559 Guard::is_true("intent:abuse"),
1560 handoff,
1561 Resume::Previous,
1562 ));
1563 assert_eq!(stack.active_postures(&state), ["MAIN"]);
1564
1565 let _ = state.set("intent:abuse", true);
1566 stack.on_turn(&state);
1567 assert_eq!(stack.active_overlay(), Some("safety"));
1568 assert_eq!(stack.active_postures(&state), ["HAND OFF NOW"]);
1569 assert!(stack.active_steps(&state).is_empty());
1571 assert!(stack.admits_tool("main_tool", &state).is_err());
1572 assert!(!stack.is_complete());
1573
1574 let _ = state.set("intent:abuse", false);
1575 stack.on_turn(&state);
1576 assert!(stack.active_overlay().is_none());
1577 assert_eq!(stack.active_postures(&state), ["MAIN"]);
1578 assert!(stack.admits_tool("main_tool", &state).is_ok());
1579 }
1580
1581 #[test]
1582 fn restart_resets_marking_but_not_state() {
1583 let state = State::new();
1584 let cancel = {
1585 let flow = Flow::new()
1586 .step("confirm")
1587 .done(Guard::is_true("confirmed"))
1588 .step("cancel_end")
1589 .after("confirm")
1590 .terminal()
1591 .require(["cancel_end"])
1592 .build()
1593 .expect("valid")
1594 .compile()
1595 .expect("compiles");
1596 Overlay::new(
1597 "cancel",
1598 Guard::is_true("intent:cancel"),
1599 flow,
1600 Resume::Restart,
1601 )
1602 };
1603 let mut stack = FlowStack::new(main_flow(), Enforcement::Enforce).with_overlay(cancel);
1604
1605 let _ = state.set("a_done", true);
1606 stack.on_turn(&state);
1607 assert!(stack.main().marking().done.contains("a"));
1608
1609 let _ = state.set("intent:cancel", true);
1610 stack.on_turn(&state);
1611 assert_eq!(stack.active_overlay(), Some("cancel"));
1612 let _ = state.set("confirmed", true);
1613 let _ = state.set("intent:cancel", false);
1614 stack.on_turn(&state); assert_eq!(stack.active_overlay(), Some("cancel"));
1616 assert!(stack.main().marking().done.contains("a"));
1617
1618 stack.on_turn(&state);
1621 assert!(stack.active_overlay().is_none());
1622 assert_eq!(state.get::<bool>("a_done"), Some(true));
1623 assert!(stack.main().marking().done.contains("a"));
1624 assert_eq!(stack.main().marking().turns, 1);
1625 }
1626
1627 #[test]
1631 fn restart_clears_repair_signals() {
1632 let state = State::new();
1633 let main = escalating_flow();
1634 let cancel = {
1635 let flow = Flow::new()
1636 .step("bye")
1637 .terminal()
1638 .require(["bye"])
1639 .build()
1640 .expect("valid")
1641 .compile()
1642 .expect("compiles");
1643 Overlay::new(
1644 "cancel",
1645 Guard::is_true("intent:cancel"),
1646 flow,
1647 Resume::Restart,
1648 )
1649 };
1650 let mut stack = FlowStack::new(main, Enforcement::Enforce)
1651 .with_overlay(cancel)
1652 .with_repair("collect", RepairPolicy::new(1, 2).escalate_to("handoff"));
1653 stack.on_turn(&state);
1654 stack.on_turn(&state); assert_eq!(stack.explain(&state).active, ["handoff"]);
1656
1657 let _ = state.set("intent:cancel", true);
1658 stack.on_turn(&state); let _ = state.set("intent:cancel", false);
1660 stack.on_turn(&state); assert!(stack.active_overlay().is_none());
1662 assert_eq!(stack.explain(&state).active, ["collect"]);
1663 assert_eq!(state.get::<bool>(&escalate_flag("collect")), Some(false));
1664 stack.on_turn(&state);
1666 assert_eq!(stack.explain(&state).active, ["handoff"]);
1667 }
1668
1669 #[test]
1673 fn terminate_ends_the_conversation() {
1674 let state = State::new();
1675 let main = Flow::new()
1676 .step("a")
1677 .allow(["main_tool"])
1678 .posture("MAIN")
1679 .done(Guard::is_true("a_done"))
1680 .step("b")
1681 .after("a")
1682 .terminal()
1683 .require(["b"])
1684 .build()
1685 .expect("valid")
1686 .compile()
1687 .expect("compiles");
1688 let bye = {
1689 let flow = Flow::new()
1690 .step("bye")
1691 .posture("SAY GOODBYE")
1692 .terminal()
1693 .require(["bye"])
1694 .build()
1695 .expect("valid")
1696 .compile()
1697 .expect("compiles");
1698 Overlay::new("bye", Guard::is_true("intent:bye"), flow, Resume::Terminate)
1699 };
1700 let mut stack = FlowStack::new(main, Enforcement::Enforce).with_overlay(bye);
1701 let _ = state.set("intent:bye", true);
1702 stack.on_turn(&state);
1703 assert_eq!(stack.active_overlay(), Some("bye"));
1704 assert_eq!(stack.active_postures(&state), ["SAY GOODBYE"]);
1705 assert!(!stack.is_terminated());
1706 assert!(!stack.is_complete());
1707
1708 stack.on_turn(&state);
1709 assert!(stack.is_terminated());
1710 assert!(stack.is_complete());
1711 assert!(stack.active_overlay().is_none());
1712 assert!(stack.active_steps(&state).is_empty());
1713 assert!(stack.active_postures(&state).is_empty());
1714 assert!(stack.unmet_requirements().is_empty());
1715 let denied = stack.admits_tool("main_tool", &state).unwrap_err();
1716 assert!(denied.contains("terminated"), "{denied}");
1717 assert!(denied.contains("bye"), "{denied}");
1718 let ex = stack.explain(&state);
1719 assert!(ex.active.is_empty());
1720 assert!(ex.allowed_tools.is_empty());
1721 assert_eq!(ex.blocked_tools.get("main_tool"), Some(&denied));
1722
1723 let _ = state.set("a_done", true);
1727 stack.on_turn(&state);
1728 assert!(stack.main().marking().done.is_empty());
1729 stack.on_tool_ok("main_tool", &state);
1730 stack.observe_tool("main_tool", true, &state);
1731 assert!(stack.main().marking().done.is_empty());
1732 assert!(stack.admits_tool("main_tool", &state).is_err());
1733 }
1734
1735 #[test]
1740 fn observe_mode_records_a_tool_used_after_termination() {
1741 let state = State::new();
1742 let bye = {
1743 let flow = Flow::new()
1744 .step("bye")
1745 .terminal()
1746 .require(["bye"])
1747 .build()
1748 .expect("valid")
1749 .compile()
1750 .expect("compiles");
1751 Overlay::new("bye", Guard::is_true("intent:bye"), flow, Resume::Terminate)
1752 };
1753 let mut stack = FlowStack::new(main_flow(), Enforcement::Observe).with_overlay(bye);
1754 let _ = state.set("intent:bye", true);
1755 stack.on_turn(&state); stack.on_turn(&state); assert!(stack.is_terminated());
1758
1759 let _ = state.set("a_done", true);
1760 stack.observe_tool("anything", true, &state);
1761 let violations = stack.main().violations();
1762 assert_eq!(violations.len(), 1, "{violations:?}");
1763 assert_eq!(violations[0].subject, "anything");
1764 assert!(
1765 violations[0].reason.contains("terminated"),
1766 "{violations:?}"
1767 );
1768 assert!(stack.main().marking().done.is_empty());
1770 }
1771
1772 #[test]
1773 fn repair_raises_then_clears_signals() {
1774 let state = State::new();
1775 let mut stack = FlowStack::new(main_flow(), Enforcement::Enforce)
1776 .with_repair("a", RepairPolicy::new(1, 3));
1777 stack.on_turn(&state);
1778 assert_eq!(state.get::<bool>("repair:a:reprompt"), Some(true));
1779 assert_eq!(state.get::<bool>("repair:a:escalate"), None);
1780 let _ = state.set("a_done", true);
1781 stack.on_turn(&state);
1782 stack.on_turn(&state);
1783 assert_eq!(state.get::<bool>("repair:a:reprompt"), Some(false));
1784 }
1785
1786 fn escalating_flow() -> CompiledFlow {
1790 Flow::new()
1791 .step("collect")
1792 .done(Guard::any(vec![
1793 Guard::is_true("info"),
1794 Guard::is_true(escalate_flag("collect")),
1795 ]))
1796 .step("handoff")
1797 .after("collect")
1798 .gate(Guard::is_true(escalate_flag("collect")))
1799 .done(Guard::is_true("handoff_complete"))
1800 .step("done")
1801 .after_when("collect", Guard::is_true("info"))
1802 .terminal()
1803 .build()
1804 .expect("valid")
1805 .compile()
1806 .expect("compiles")
1807 }
1808
1809 #[test]
1814 fn escalation_edge_stays_open_after_the_step_completes() {
1815 let state = State::new();
1816 let mut stack = FlowStack::new(escalating_flow(), Enforcement::Enforce)
1817 .with_repair("collect", RepairPolicy::new(1, 2).escalate_to("handoff"));
1818
1819 stack.on_turn(&state); stack.on_turn(&state); assert!(
1822 stack
1823 .explain(&state)
1824 .active
1825 .contains(&"handoff".to_string())
1826 );
1827 stack.on_turn(&state);
1829 assert!(
1830 stack
1831 .explain(&state)
1832 .active
1833 .contains(&"handoff".to_string())
1834 );
1835 assert_eq!(state.get::<bool>(&reprompt_flag("collect")), Some(false));
1836 assert_eq!(state.get::<bool>(&escalate_flag("collect")), Some(true));
1837 }
1838
1839 #[test]
1844 fn reset_clears_a_latched_escalation() {
1845 let state = State::new();
1846 let main = Flow::new()
1847 .step("collect")
1848 .done(Guard::any(vec![
1849 Guard::is_true("info"),
1850 Guard::is_true(escalate_flag("collect")),
1851 ]))
1852 .step("handoff")
1853 .after("collect")
1854 .gate(Guard::is_true(escalate_flag("collect")))
1855 .done(Guard::is_true("handoff_complete"))
1856 .step("done")
1857 .after_when("collect", Guard::is_true("info"))
1858 .terminal()
1859 .reset(["collect"])
1860 .when(Guard::is_true("retry"))
1861 .build()
1862 .expect("valid")
1863 .compile()
1864 .expect("compiles");
1865 let mut stack = FlowStack::new(main, Enforcement::Enforce)
1866 .with_repair("collect", RepairPolicy::new(1, 2).escalate_to("handoff"));
1867
1868 stack.on_turn(&state);
1869 stack.on_turn(&state); assert_eq!(stack.explain(&state).active, ["handoff"]);
1871
1872 let _ = state.set("retry", true);
1874 stack.on_turn(&state);
1875 assert_eq!(stack.explain(&state).active, ["collect"]);
1876 assert_eq!(state.get::<bool>(&escalate_flag("collect")), Some(false));
1877 assert_eq!(state.get::<bool>(&reprompt_flag("collect")), Some(true));
1878
1879 stack.on_turn(&state);
1881 assert_eq!(stack.explain(&state).active, ["handoff"]);
1882 assert_eq!(state.get::<bool>(&escalate_flag("collect")), Some(true));
1883 }
1884
1885 #[test]
1886 fn state_only_reset_clears_repair_without_counting_a_turn() {
1887 let state = State::new();
1888 let main = Flow::new()
1889 .step("collect")
1890 .done(Guard::is_true(escalate_flag("collect")))
1891 .reset(["collect"])
1892 .when(Guard::is_true("retry"))
1893 .build()
1894 .unwrap()
1895 .compile()
1896 .unwrap();
1897 let mut stack = FlowStack::new(main, Enforcement::Enforce)
1898 .with_repair("collect", RepairPolicy::new(1, 2));
1899 stack.on_turn(&state);
1900 stack.on_turn(&state);
1901 assert!(stack.marking().done.contains("collect"));
1902
1903 let _ = state.set("retry", true);
1904 stack.relatch(&state);
1905 assert_eq!(stack.marking().turns, 2);
1906 assert!(!stack.marking().done.contains("collect"));
1907 assert_eq!(state.get::<bool>(&escalate_flag("collect")), Some(false));
1908 assert_eq!(state.get::<bool>(&reprompt_flag("collect")), Some(false));
1909 stack.on_turn(&state);
1910 assert!(!stack.marking().done.contains("collect"));
1911 stack.on_turn(&state);
1912 assert!(stack.marking().done.contains("collect"));
1913 }
1914
1915 #[test]
1921 fn a_tool_triggered_reset_clears_a_latched_escalation() {
1922 let state = State::new();
1923 let main = Flow::new()
1924 .step("collect")
1925 .allow(["start_over"])
1926 .done(Guard::any(vec![
1927 Guard::is_true("info"),
1928 Guard::is_true(escalate_flag("collect")),
1929 ]))
1930 .step("handoff")
1931 .after("collect")
1932 .gate(Guard::is_true(escalate_flag("collect")))
1933 .done(Guard::is_true("handoff_complete"))
1934 .step("done")
1935 .after_when("collect", Guard::is_true("info"))
1936 .terminal()
1937 .reset(["collect"])
1938 .when(Guard::called_ok("start_over"))
1939 .build()
1940 .expect("valid")
1941 .compile()
1942 .expect("compiles");
1943 let mut stack = FlowStack::new(main, Enforcement::Enforce)
1944 .with_repair("collect", RepairPolicy::new(1, 2).escalate_to("handoff"));
1945
1946 stack.on_turn(&state);
1947 stack.on_turn(&state); assert_eq!(stack.explain(&state).active, ["handoff"]);
1949
1950 stack.on_tool_ok("start_over", &state);
1952 assert_eq!(state.get::<bool>(&escalate_flag("collect")), Some(false));
1953 assert_eq!(stack.explain(&state).active, ["collect"]);
1954
1955 stack.on_turn(&state);
1957 assert_eq!(stack.explain(&state).active, ["collect"]);
1958 }
1959
1960 #[test]
1961 fn tool_admission_follows_the_active_layer() {
1962 let state = State::new();
1963 let main = Flow::new()
1964 .step("a")
1965 .allow(["main_tool"])
1966 .done(Guard::is_true("a_done"))
1967 .step("b")
1968 .after("a")
1969 .terminal()
1970 .build()
1971 .expect("valid")
1972 .compile()
1973 .expect("compiles");
1974 let ov = {
1975 let flow = Flow::new()
1976 .step("answer")
1977 .allow(["faq_tool"])
1978 .done(Guard::is_true("faq_answered"))
1979 .step("faq_end")
1980 .after("answer")
1981 .terminal()
1982 .require(["faq_end"])
1983 .build()
1984 .expect("valid")
1985 .compile()
1986 .expect("compiles");
1987 Overlay::new("faq", Guard::is_true("intent:faq"), flow, Resume::Previous)
1988 };
1989 let mut stack = FlowStack::new(main, Enforcement::Enforce).with_overlay(ov);
1990 assert!(stack.admits_tool("main_tool", &state).is_ok());
1991 assert!(stack.admits_tool("faq_tool", &state).is_err());
1992 let _ = state.set("intent:faq", true);
1993 stack.on_turn(&state);
1994 assert!(stack.admits_tool("faq_tool", &state).is_ok());
1995 assert!(stack.admits_tool("main_tool", &state).is_err());
1996 }
1997
1998 fn answer(state: &State, question: &str, outcome: crate::flow::Outcome, turn: &str) {
1999 let _ = state.set(crate::flow::DECISION_TURN_KEY, turn);
2000 let _ = state.set(
2001 crate::flow::decision_key(question),
2002 serde_json::to_value(crate::flow::DecisionRecord {
2003 outcome,
2004 value: serde_json::Value::Null,
2005 confidence: None,
2006 turn: turn.into(),
2007 })
2008 .unwrap(),
2009 );
2010 }
2011
2012 fn booking_with_handoff() -> FlowStack {
2013 let main = Flow::new()
2014 .step("collect")
2015 .done(Guard::captured(["slot"]))
2016 .step("confirm")
2017 .after("collect")
2018 .allow(["book"])
2019 .commit("book", Guard::decided("confirmed"))
2020 .done(Guard::called_ok("book"))
2021 .step("end")
2022 .after("confirm")
2023 .terminal()
2024 .build()
2025 .expect("valid")
2026 .compile()
2027 .expect("compiles");
2028 let handoff = Flow::new()
2029 .step("transfer")
2030 .allow(["transfer"])
2031 .done(Guard::called_ok("transfer"))
2032 .build()
2033 .expect("valid")
2034 .compile()
2035 .expect("compiles");
2036 FlowStack::new(main, Enforcement::Enforce).with_overlay(Overlay::new(
2037 "handoff",
2038 Guard::decided("wants_person"),
2039 handoff,
2040 Resume::Terminate,
2041 ))
2042 }
2043
2044 fn routed() -> FlowStack {
2047 let main = Flow::new()
2048 .step("ask")
2049 .allow(["lookup"])
2050 .done(Guard::any([
2051 Guard::decided("wants_booking"),
2052 Guard::is_true("faq_flag"),
2053 ]))
2054 .step("book")
2055 .after_when("ask", Guard::decided("wants_booking"))
2056 .allow(["book_table"])
2057 .done(Guard::called_ok("book_table"))
2058 .step("faq")
2059 .after_when("ask", Guard::is_true("faq_flag"))
2060 .allow(["answer"])
2061 .done(Guard::called_ok("answer"))
2062 .build()
2063 .expect("valid")
2064 .compile()
2065 .expect("compiles");
2066 FlowStack::new(main, Enforcement::Enforce)
2067 }
2068
2069 #[test]
2070 fn a_step_one_decision_away_is_offered_and_decided_at_the_gate() {
2071 let mut stack = routed();
2072 let state = State::new();
2073 stack.on_turn(&state);
2074 let monitor = stack.current();
2075 let frontier: Vec<_> = monitor
2076 .frontier_steps(&state)
2077 .iter()
2078 .map(|s| s.id.clone())
2079 .collect();
2080 assert_eq!(frontier, ["book"], "faq waits on a flag, not a decision");
2081 assert!(stack.offers_tool("lookup", &state), "the active step's");
2082 assert!(stack.offers_tool("book_table", &state), "one decision away");
2083 assert!(!stack.offers_tool("answer", &state));
2084 assert!(
2085 stack.admits_tool("book_table", &state).is_err(),
2086 "offered is not admitted"
2087 );
2088 assert_eq!(
2089 stack
2090 .gate_scope(&["book_table".to_string()], &state)
2091 .questions,
2092 ["wants_booking".to_string()].into()
2093 );
2094
2095 answer(&state, "wants_booking", crate::flow::Outcome::Yes, "2:a");
2097 stack.on_decisions(&state);
2098 assert!(stack.admits_tool("book_table", &state).is_ok());
2099 assert!(
2100 !stack.offers_tool("lookup", &state),
2101 "ask is done, and its tool goes with it"
2102 );
2103 }
2104
2105 #[test]
2106 fn a_no_keeps_the_step_closed() {
2107 let mut stack = routed();
2108 let state = State::new();
2109 stack.on_turn(&state);
2110 answer(&state, "wants_booking", crate::flow::Outcome::No, "2:a");
2111 stack.on_decisions(&state);
2112 assert!(stack.admits_tool("book_table", &state).is_err());
2113 assert!(stack.offers_tool("book_table", &state));
2115 }
2116
2117 fn talk_with_handoff(trigger: Guard) -> FlowStack {
2120 let flow = |step: &str, tool: &str| {
2121 Flow::new()
2122 .step(step)
2123 .allow([tool])
2124 .done(Guard::called_ok(tool))
2125 .build()
2126 .expect("valid")
2127 .compile()
2128 .expect("compiles")
2129 };
2130 FlowStack::new(flow("talk", "lookup"), Enforcement::Enforce).with_overlay(Overlay::new(
2131 "handoff",
2132 trigger,
2133 flow("transfer", "transfer"),
2134 Resume::Terminate,
2135 ))
2136 }
2137
2138 #[test]
2139 fn a_digression_a_decision_opens_offers_its_entry_tools() {
2140 let state = State::new();
2141 let mut stack = talk_with_handoff(Guard::decided("wants_person"));
2142 stack.on_turn(&state);
2143 assert!(stack.offers_tool("transfer", &state));
2144 assert!(stack.admits_tool("transfer", &state).is_err());
2145 assert_eq!(
2146 stack
2147 .gate_scope(&["transfer".to_string()], &state)
2148 .questions,
2149 ["wants_person".to_string()].into()
2150 );
2151 answer(&state, "wants_person", crate::flow::Outcome::Yes, "2:a");
2152 stack.on_decisions(&state);
2153 assert!(stack.admits_tool("transfer", &state).is_ok());
2154
2155 let mut flagged = talk_with_handoff(Guard::is_true("intent:human_agent"));
2157 flagged.on_turn(&State::new());
2158 assert!(!flagged.offers_tool("transfer", &State::new()));
2159 }
2160
2161 #[test]
2162 fn the_decision_scope_names_guards_and_triggers_and_what_active_steps_read() {
2163 let stack = booking_with_handoff();
2164 let state = State::new();
2165 let scope = stack.decision_scope(&state);
2166 assert_eq!(
2167 scope.questions,
2168 ["confirmed", "wants_person"].map(String::from).into()
2169 );
2170 assert!(
2171 scope.reads.contains("slot"),
2172 "collect reads the slot: {scope:?}"
2173 );
2174 }
2175
2176 #[test]
2177 fn the_gate_scope_names_only_what_decides_these_calls() {
2178 let mut stack = booking_with_handoff();
2179 let state = State::new();
2180 let _ = state.set("slot", "19:00");
2181 stack.on_turn(&state);
2182 let scope = |tools: &[&str]| {
2183 stack
2184 .gate_scope(
2185 &tools.iter().map(ToString::to_string).collect::<Vec<_>>(),
2186 &state,
2187 )
2188 .questions
2189 };
2190 assert_eq!(scope(&["book"]), ["confirmed".to_string()].into());
2191 assert_eq!(scope(&["transfer"]), ["wants_person".to_string()].into());
2192 assert!(
2193 scope(&["check_availability"]).is_empty(),
2194 "a call no decision governs waits for none"
2195 );
2196 }
2197
2198 #[test]
2199 fn a_decided_commit_is_admitted_only_for_the_current_turn() {
2200 let mut stack = booking_with_handoff();
2201 let state = State::new();
2202 let _ = state.set("slot", "19:00");
2203 stack.on_turn(&state);
2204 assert!(
2205 stack.admits_tool("book", &state).is_err(),
2206 "not confirmed yet"
2207 );
2208 answer(&state, "confirmed", crate::flow::Outcome::Yes, "t1");
2209 assert!(stack.admits_tool("book", &state).is_ok());
2210 let _ = state.set(crate::flow::DECISION_TURN_KEY, "t2");
2212 let refusal = stack.admits_tool("book", &state).unwrap_err();
2213 assert!(refusal.contains("confirmed"), "{refusal}");
2214 }
2215
2216 #[test]
2217 fn fresh_decisions_open_a_digression_mid_turn() {
2218 let mut stack = booking_with_handoff();
2219 let state = State::new();
2220 stack.on_turn(&state);
2221 assert_eq!(stack.active_overlay(), None);
2222 answer(&state, "wants_person", crate::flow::Outcome::Yes, "t1");
2223 stack.on_decisions(&state);
2224 assert_eq!(stack.active_overlay(), Some("handoff"));
2225 assert!(stack.admits_tool("transfer", &state).is_ok());
2226 stack.on_tool_ok("transfer", &state);
2227 stack.on_turn(&state);
2228 stack.on_turn(&state);
2229 assert!(stack.is_terminated(), "one transfer completes the handoff");
2230 }
2231}