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, Guard, Marking, Step, StepAction,
29};
30use crate::state::State;
31
32pub fn escalate_flag(stage: &str) -> String {
34 format!("repair:{stage}:escalate")
35}
36
37pub fn reprompt_flag(stage: &str) -> String {
39 format!("repair:{stage}:reprompt")
40}
41
42pub fn correction_flag(slot: &str) -> String {
46 format!("correction:{slot}")
47}
48
49pub const TOOL_CALL_KEY: &str = "flow:tool_call";
55
56pub const TOOL_DENIED_KEY: &str = "flow:tool_denied";
59
60pub const TOOL_RESULT_KEY: &str = "flow:tool_result";
63
64pub const OVERLAY_STATE_KEY: &str = "flow:overlay";
67
68pub const TERMINATED_STATE_KEY: &str = "flow:terminated";
75
76#[derive(
83 Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, schemars::JsonSchema,
84)]
85#[serde(rename_all = "snake_case")]
86pub enum Resume {
87 #[default]
89 Previous,
90 Restart,
94 Terminate,
98}
99
100fn default_reprompt_after() -> u32 {
101 2
102}
103fn default_escalate_after() -> u32 {
104 4
105}
106
107#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
114pub struct RepairPolicy {
115 #[serde(default = "default_reprompt_after")]
117 pub reprompt_after: u32,
118 #[serde(default = "default_escalate_after")]
120 pub escalate_after: u32,
121 #[serde(default, skip_serializing_if = "Option::is_none")]
123 pub escalate_to: Option<String>,
124 #[serde(default, skip_serializing_if = "Option::is_none")]
127 pub escalate_after_interruptions: Option<u32>,
128 #[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 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 pub fn escalate_to(mut self, step: impl Into<String>) -> Self {
158 self.escalate_to = Some(step.into());
159 self
160 }
161
162 pub fn escalate_after_interruptions(mut self, n: u32) -> Self {
164 self.escalate_after_interruptions = Some(n);
165 self
166 }
167
168 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#[derive(Debug, Clone)]
179pub struct Overlay {
180 name: String,
181 trigger: Guard,
182 flow: Flow,
183 resume: Resume,
184}
185
186impl Overlay {
187 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 pub fn name(&self) -> &str {
204 &self.name
205 }
206 pub fn trigger(&self) -> &Guard {
208 &self.trigger
209 }
210 pub fn flow(&self) -> &Flow {
212 &self.flow
213 }
214 pub fn flow_mut(&mut self) -> &mut Flow {
216 &mut self.flow
217 }
218 pub fn resume(&self) -> Resume {
220 self.resume
221 }
222}
223
224struct ActiveOverlay {
226 name: String,
227 monitor: FlowMonitor,
228 resume: Resume,
229}
230
231pub type SharedFlowStack = Arc<parking_lot::Mutex<FlowStack>>;
237
238pub struct FlowStack {
252 main: FlowMonitor,
253 mode: Enforcement,
254 overlays: Vec<Overlay>,
255 active: Vec<ActiveOverlay>,
258 terminated: Option<String>,
260 repair: BTreeMap<String, RepairPolicy>,
262 active_turns: BTreeMap<String, u32>,
264 timing: BTreeMap<String, VoiceTiming>,
266 corrections: BTreeMap<String, Vec<String>>,
269 slot_values: BTreeMap<String, serde_json::Value>,
271 verbatim: BTreeMap<String, String>,
273 interruptions: BTreeMap<String, u32>,
275 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 pub fn new(main: CompiledFlow, mode: Enforcement) -> Self {
299 Self::from_monitor(FlowMonitor::compiled(main, mode))
300 }
301
302 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 pub fn with_overlay(mut self, overlay: Overlay) -> Self {
325 self.overlays.push(overlay);
326 self
327 }
328
329 pub fn with_overlays(mut self, overlays: impl IntoIterator<Item = Overlay>) -> Self {
331 self.overlays.extend(overlays);
332 self
333 }
334
335 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 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 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 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 pub fn correction_policies(&self) -> &BTreeMap<String, Vec<String>> {
381 &self.corrections
382 }
383
384 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 pub fn with_verbatims(mut self, texts: impl IntoIterator<Item = (String, String)>) -> Self {
396 self.verbatim.extend(texts);
397 self
398 }
399
400 pub fn verbatim_policies(&self) -> &BTreeMap<String, String> {
402 &self.verbatim
403 }
404
405 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 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 pub fn timing_policies(&self) -> &BTreeMap<String, VoiceTiming> {
422 &self.timing
423 }
424
425 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 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 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 pub fn into_shared(self) -> SharedFlowStack {
493 Arc::new(parking_lot::Mutex::new(self))
494 }
495
496 pub fn mode(&self) -> Enforcement {
498 self.mode
499 }
500
501 pub fn overlays(&self) -> &[Overlay] {
503 &self.overlays
504 }
505
506 pub fn overlays_mut(&mut self) -> &mut [Overlay] {
508 &mut self.overlays
509 }
510
511 pub fn repair_policies(&self) -> &BTreeMap<String, RepairPolicy> {
513 &self.repair
514 }
515
516 pub fn main(&self) -> &FlowMonitor {
518 &self.main
519 }
520
521 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 pub fn active_overlay(&self) -> Option<&str> {
536 self.active.last().map(|a| a.name.as_str())
537 }
538
539 pub fn overlay_path(&self) -> Vec<&str> {
542 self.active.iter().map(|a| a.name.as_str()).collect()
543 }
544
545 pub fn is_complete(&self) -> bool {
548 self.terminated.is_some() || (self.active.is_empty() && self.main.is_complete())
549 }
550
551 pub fn is_terminated(&self) -> bool {
556 self.terminated.is_some()
557 }
558
559 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 fn active_is_closing(&self) -> bool {
569 self.active.last().is_some_and(|a| a.monitor.is_complete())
570 }
571
572 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 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 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), ¤t),
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 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 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 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 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 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 fn apply_resume(&mut self, by: String, resume: Resume, state: &State) {
743 match resume {
744 Resume::Previous => {}
746 Resume::Restart => match self.active.last_mut() {
747 Some(beneath) => beneath.monitor.restart(),
748 None => {
749 self.main.restart();
750 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 fn advance_main(&mut self, state: &State) {
768 for step in self.main.begin_turn(state) {
769 self.clear_repair(&step, state);
770 }
771 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 self.apply_repair(state);
783 self.main.relatch(state);
784 }
785
786 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 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 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 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 self.main.relatch(state);
852 }
853 }
854 }
855
856 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 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 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 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 pub fn marking(&self) -> &Marking {
918 self.current().marking()
919 }
920
921 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 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 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 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 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 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 pub fn set_posture(&mut self, step_id: &str, posture: Option<String>) -> bool {
981 self.main.set_posture(step_id, posture)
982 }
983
984 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 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 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 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 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 let _ = state.set("intent:faq", true);
1195 stack.on_turn(&state);
1196 assert_eq!(state.get::<VoiceTiming>(VOICE_TIMING_KEY), Some(answer));
1197
1198 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 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 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 #[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 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 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); assert_eq!(stack.active_overlay(), Some("cancel"));
1350 assert!(stack.main().marking().done.contains("a"));
1351
1352 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 #[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); assert_eq!(stack.explain(&state).active, ["handoff"]);
1390
1391 let _ = state.set("intent:cancel", true);
1392 stack.on_turn(&state); let _ = state.set("intent:cancel", false);
1394 stack.on_turn(&state); 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 stack.on_turn(&state);
1400 assert_eq!(stack.explain(&state).active, ["handoff"]);
1401 }
1402
1403 #[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 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 #[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); stack.on_turn(&state); 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 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 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 #[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); stack.on_turn(&state); assert!(
1556 stack
1557 .explain(&state)
1558 .active
1559 .contains(&"handoff".to_string())
1560 );
1561 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 #[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); assert_eq!(stack.explain(&state).active, ["handoff"]);
1605
1606 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 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 #[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); assert_eq!(stack.explain(&state).active, ["handoff"]);
1653
1654 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 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}