1use serde::{Deserialize, Serialize};
48use serde_json::Value;
49use std::collections::{BTreeMap, BTreeSet};
50use std::future::Future;
51use std::pin::Pin;
52use std::sync::Arc;
53use std::time::Duration;
54
55use gemini_adk_rs::extract::Extract;
56use gemini_adk_rs::flow::{CompiledFlow, Enforcement, Flow, FlowErrors, FlowMonitor, Guard, Pred};
57use gemini_adk_rs::frame::{Frame, FrameSpec};
58
59type SlotFetch =
61 Arc<dyn Fn(Value) -> Pin<Box<dyn Future<Output = Result<Value, String>> + Send>> + Send + Sync>;
62
63#[derive(Clone, Default)]
85pub struct ResolverRegistry {
86 fetchers: BTreeMap<String, SlotFetch>,
87}
88
89impl ResolverRegistry {
90 pub fn new() -> Self {
92 Self::default()
93 }
94
95 pub fn with<F, Fut>(mut self, name: impl Into<String>, fetch: F) -> Self
97 where
98 F: Fn(Value) -> Fut + Send + Sync + 'static,
99 Fut: Future<Output = Result<Value, String>> + Send + 'static,
100 {
101 self.add(name, fetch);
102 self
103 }
104
105 pub fn add<F, Fut>(&mut self, name: impl Into<String>, fetch: F)
107 where
108 F: Fn(Value) -> Fut + Send + Sync + 'static,
109 Fut: Future<Output = Result<Value, String>> + Send + 'static,
110 {
111 let fetch = Arc::new(fetch);
112 self.fetchers.insert(
113 name.into(),
114 Arc::new(move |v| {
115 let fetch = fetch.clone();
116 Box::pin(async move { fetch(v).await })
117 }),
118 );
119 }
120
121 pub fn contains(&self, name: &str) -> bool {
123 self.fetchers.contains_key(name)
124 }
125
126 pub fn stubbing(spec: &ConversationSpec) -> Self {
136 let mut reg = Self::new();
137 let stages = spec
138 .stages
139 .iter()
140 .chain(spec.overlays.iter().flat_map(|o| o.stages.iter()));
141 for stage in stages {
142 for r in &stage.resolve {
143 let name = r.resolver_name().to_string();
144 if !reg.contains(&name) {
145 reg.add(name, |_args| async { Ok(serde_json::Value::Null) });
146 }
147 }
148 }
149 reg
150 }
151
152 fn get(&self, name: &str) -> Option<SlotFetch> {
153 self.fetchers.get(name).cloned()
154 }
155}
156
157#[derive(Clone)]
161struct StageResolver {
162 stage: String,
163 name: String,
164 args: Vec<String>,
165 ttl: Option<Duration>,
166 fetch: SlotFetch,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
176pub struct ResolveSpec {
177 pub slot: String,
179 #[serde(default, skip_serializing_if = "Option::is_none")]
181 pub resolver: Option<String>,
182 #[serde(default, skip_serializing_if = "Vec::is_empty")]
184 pub args: Vec<String>,
185 #[serde(default, skip_serializing_if = "Option::is_none")]
187 pub ttl_secs: Option<u64>,
188}
189
190impl ResolveSpec {
191 fn resolver_name(&self) -> &str {
194 self.resolver.as_deref().unwrap_or(&self.slot)
195 }
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
200pub struct TransitionSpec {
201 pub to: String,
203 pub when: Guard,
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
209pub struct CommitSpec {
210 pub tool: String,
212 pub when: Guard,
214}
215
216#[derive(Debug, Clone, Default, Serialize, Deserialize, schemars::JsonSchema)]
218pub struct StageSpec {
219 pub id: String,
221 #[serde(
224 default,
225 alias = "instruction",
226 skip_serializing_if = "Option::is_none"
227 )]
228 pub say: Option<String>,
229 #[serde(default, skip_serializing_if = "Option::is_none")]
231 pub ground: Option<String>,
232 #[serde(default, skip_serializing_if = "Vec::is_empty")]
234 pub collect: Vec<String>,
235 #[serde(default, skip_serializing_if = "Option::is_none")]
238 pub frame: Option<FrameSpec>,
239 #[serde(default, skip_serializing_if = "Vec::is_empty")]
241 pub allow: Vec<String>,
242 #[serde(default, skip_serializing_if = "Option::is_none")]
244 pub done: Option<Guard>,
245 #[serde(default, skip_serializing_if = "Option::is_none")]
247 pub commit: Option<CommitSpec>,
248 #[serde(default, skip_serializing_if = "Vec::is_empty")]
250 pub next: Vec<TransitionSpec>,
251 #[serde(default, skip_serializing_if = "Vec::is_empty")]
253 pub after: Vec<String>,
254 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
256 pub terminal: bool,
257 #[serde(default, skip_serializing_if = "Option::is_none")]
259 pub repair: Option<RepairPolicy>,
260 #[serde(default, skip_serializing_if = "Option::is_none")]
263 pub timing: Option<VoiceTiming>,
264 #[serde(default, skip_serializing_if = "Option::is_none")]
269 pub verbatim: Option<String>,
270 #[serde(default, skip_serializing_if = "Vec::is_empty")]
273 pub resolve: Vec<ResolveSpec>,
274}
275
276pub use gemini_adk_rs::flow::{FlowStack, RepairPolicy, Resume, VoiceTiming};
280use gemini_adk_rs::flow::{Overlay, correction_flag, escalate_flag, verbatim_flag};
281
282#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
285pub struct OverlaySpec {
286 pub name: String,
288 pub trigger: Guard,
290 #[serde(default)]
292 pub stages: Vec<StageSpec>,
293 #[serde(default, skip_serializing_if = "Vec::is_empty")]
295 pub require: Vec<String>,
296 #[serde(default)]
298 pub resume: Resume,
299}
300
301#[derive(Debug, Clone, Default, Serialize, Deserialize, schemars::JsonSchema)]
304pub struct ConversationSpec {
305 pub name: String,
307 #[serde(default)]
309 pub stages: Vec<StageSpec>,
310 #[serde(default, skip_serializing_if = "Vec::is_empty")]
312 pub require: Vec<String>,
313 #[serde(default, skip_serializing_if = "Vec::is_empty")]
315 pub overlays: Vec<OverlaySpec>,
316 #[serde(default, skip_serializing_if = "Vec::is_empty")]
318 pub policies: Vec<crate::policy::Policy>,
319}
320
321#[derive(Debug)]
323pub enum ConversationError {
324 Empty,
326 Spec(String),
328 Flow(Vec<String>),
330 Compile(FlowErrors),
332}
333
334impl std::fmt::Display for ConversationError {
335 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
336 match self {
337 ConversationError::Empty => write!(f, "conversation has no stages"),
338 ConversationError::Spec(m) => write!(f, "conversation spec error: {m}"),
339 ConversationError::Flow(errs) => {
340 write!(f, "lowered flow is invalid: {}", errs.join("; "))
341 }
342 ConversationError::Compile(e) => write!(f, "lowered flow failed to compile: {e}"),
343 }
344 }
345}
346
347impl std::error::Error for ConversationError {}
348
349pub fn conversation_spec_schema() -> String {
355 let schema = schemars::schema_for!(ConversationSpec);
356 serde_json::to_string_pretty(&schema).expect("schema serialization is infallible")
357}
358
359impl serde::Serialize for ConversationError {
360 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
364 use serde::ser::SerializeMap;
365 let mut m = s.serialize_map(None)?;
366 m.serialize_entry("message", &self.to_string())?;
367 match self {
368 ConversationError::Empty => {
369 m.serialize_entry("kind", "empty")?;
370 }
371 ConversationError::Spec(msg) => {
372 m.serialize_entry("kind", "spec")?;
373 m.serialize_entry("detail", msg)?;
374 }
375 ConversationError::Flow(errs) => {
376 m.serialize_entry("kind", "flow")?;
377 m.serialize_entry("errors", errs)?;
378 }
379 ConversationError::Compile(e) => {
380 m.serialize_entry("kind", "compile")?;
381 m.serialize_entry("errors", &e.0)?;
382 }
383 }
384 m.end()
385 }
386}
387
388#[derive(Clone)]
390pub struct CompiledOverlay {
391 pub name: String,
393 pub trigger: Guard,
395 pub flow: CompiledFlow,
397 pub extractors: Vec<Extract>,
399 pub resume: Resume,
401}
402
403impl CompiledOverlay {
404 pub fn to_runtime(&self) -> Overlay {
408 Overlay::new(
409 self.name.clone(),
410 self.trigger.clone(),
411 self.flow.clone(),
412 self.resume,
413 )
414 }
415}
416
417#[derive(Clone)]
420pub struct CompiledConversation {
421 flow: CompiledFlow,
422 extractors: Vec<Extract>,
423 overlays: Vec<CompiledOverlay>,
424 repair: BTreeMap<String, RepairPolicy>,
425 timing: BTreeMap<String, VoiceTiming>,
426 corrections: BTreeMap<String, Vec<String>>,
427 verbatim: BTreeMap<String, String>,
428 policies: Vec<crate::policy::Policy>,
429 spec: ConversationSpec,
430}
431
432impl std::fmt::Debug for CompiledConversation {
435 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
436 f.debug_struct("CompiledConversation")
437 .field("flow", &self.flow)
438 .field("extractors", &self.extractors.len())
439 .field("overlays", &self.overlays.len())
440 .field("policies", &self.policies)
441 .field("spec", &self.spec)
442 .finish()
443 }
444}
445
446impl CompiledConversation {
447 pub fn flow(&self) -> &CompiledFlow {
449 &self.flow
450 }
451 pub fn extractors(&self) -> &[Extract] {
454 &self.extractors
455 }
456 pub fn overlays(&self) -> &[CompiledOverlay] {
458 &self.overlays
459 }
460 pub fn policies(&self) -> &[crate::policy::Policy] {
462 &self.policies
463 }
464 pub fn redacted_fields(&self) -> BTreeSet<String> {
466 self.policies
467 .iter()
468 .flat_map(|p| p.redacted_keys().iter().cloned())
469 .collect()
470 }
471 pub fn all_extractors(&self) -> Vec<Extract> {
474 let mut all = self.extractors.clone();
475 for ov in &self.overlays {
476 all.extend(ov.extractors.iter().cloned());
477 }
478 all
479 }
480 pub fn stack(&self, mode: Enforcement) -> FlowStack {
488 FlowStack::new(self.flow.clone(), mode)
489 .with_overlays(self.overlays.iter().map(CompiledOverlay::to_runtime))
490 .with_repairs(self.repair.clone())
491 .with_timings(self.timing.clone())
492 .with_corrections(self.corrections.clone())
493 .with_verbatims(self.verbatim.clone())
494 }
495 pub fn repair_policies(&self) -> &BTreeMap<String, RepairPolicy> {
497 &self.repair
498 }
499 pub fn verbatim_policies(&self) -> &BTreeMap<String, String> {
501 &self.verbatim
502 }
503 pub fn correction_policies(&self) -> &BTreeMap<String, Vec<String>> {
507 &self.corrections
508 }
509 pub fn timing_policies(&self) -> &BTreeMap<String, VoiceTiming> {
512 &self.timing
513 }
514 pub fn spec(&self) -> &ConversationSpec {
516 &self.spec
517 }
518 pub fn to_mermaid(&self) -> String {
520 self.flow.to_mermaid()
521 }
522 pub fn monitor(&self, mode: Enforcement) -> FlowMonitor {
524 FlowMonitor::compiled(self.flow.clone(), mode)
525 }
526}
527
528#[derive(Clone, Default)]
533pub struct Conversation {
534 spec: ConversationSpec,
535 resolvers: Vec<StageResolver>,
536 current_overlay: Option<usize>,
539}
540
541impl Conversation {
542 pub fn new(name: impl Into<String>) -> Self {
544 Self {
545 spec: ConversationSpec {
546 name: name.into(),
547 ..Default::default()
548 },
549 resolvers: Vec::new(),
550 current_overlay: None,
551 }
552 }
553
554 pub fn stage(mut self, id: impl Into<String>) -> Self {
557 let stage = StageSpec {
558 id: id.into(),
559 ..Default::default()
560 };
561 match self.current_overlay {
562 Some(i) => self.spec.overlays[i].stages.push(stage),
563 None => self.spec.stages.push(stage),
564 }
565 self
566 }
567
568 pub fn overlay(mut self, name: impl Into<String>) -> Self {
572 self.spec.overlays.push(OverlaySpec {
573 name: name.into(),
574 trigger: Guard::is_true("__overlay_never_triggers__"),
576 stages: Vec::new(),
577 require: Vec::new(),
578 resume: Resume::Previous,
579 });
580 self.current_overlay = Some(self.spec.overlays.len() - 1);
581 self
582 }
583
584 pub fn trigger(mut self, guard: Guard) -> Self {
586 if let Some(i) = self.current_overlay {
587 self.spec.overlays[i].trigger = guard;
588 }
589 self
590 }
591
592 pub fn resume(mut self, resume: Resume) -> Self {
594 if let Some(i) = self.current_overlay {
595 self.spec.overlays[i].resume = resume;
596 }
597 self
598 }
599
600 pub fn end_overlay(mut self) -> Self {
604 self.current_overlay = None;
605 self
606 }
607
608 pub fn add_stage(mut self, stage: StageSpec) -> Self {
612 match self.current_overlay {
613 Some(i) => self.spec.overlays[i].stages.push(stage),
614 None => self.spec.stages.push(stage),
615 }
616 self
617 }
618
619 pub fn add_overlay(mut self, overlay: OverlaySpec) -> Self {
622 self.spec.overlays.push(overlay);
623 self.current_overlay = None;
624 self
625 }
626
627 pub fn policy(mut self, policy: impl Into<crate::policy::Policy>) -> Self {
629 self.spec.policies.push(policy.into());
630 self
631 }
632
633 fn current(&mut self) -> &mut StageSpec {
634 let stages = match self.current_overlay {
635 Some(i) => &mut self.spec.overlays[i].stages,
636 None => &mut self.spec.stages,
637 };
638 stages
639 .last_mut()
640 .expect("call .stage(..) before configuring a stage")
641 }
642
643 pub fn instruction(mut self, text: impl Into<String>) -> Self {
647 self.current().say = Some(text.into());
648 self
649 }
650
651 pub fn say(self, text: impl Into<String>) -> Self {
654 self.instruction(text)
655 }
656
657 pub fn ground(mut self, template: impl Into<String>) -> Self {
659 self.current().ground = Some(template.into());
660 self
661 }
662
663 pub fn collect<I, S>(mut self, fields: I) -> Self
665 where
666 I: IntoIterator<Item = S>,
667 S: Into<String>,
668 {
669 self.current().collect = fields.into_iter().map(Into::into).collect();
670 self
671 }
672
673 pub fn collect_frame<F: Frame>(mut self) -> Self {
678 let spec = F::frame();
679 let stage = self.current();
680 stage.collect = spec.slot_keys();
681 stage.frame = Some(spec);
682 self
683 }
684
685 pub fn allow<I, S>(mut self, tools: I) -> Self
687 where
688 I: IntoIterator<Item = S>,
689 S: Into<String>,
690 {
691 self.current().allow = tools.into_iter().map(Into::into).collect();
692 self
693 }
694
695 pub fn complete_when(mut self, guard: Guard) -> Self {
698 self.current().done = Some(guard);
699 self
700 }
701
702 pub fn commit(mut self, tool: impl Into<String>, when: Guard) -> Self {
704 self.current().commit = Some(CommitSpec {
705 tool: tool.into(),
706 when,
707 });
708 self
709 }
710
711 pub fn next(mut self, to: impl Into<String>, when: Guard) -> Self {
713 self.current().next.push(TransitionSpec {
714 to: to.into(),
715 when,
716 });
717 self
718 }
719
720 pub fn resolve_slot<I, S, F, Fut>(
729 mut self,
730 name: impl Into<String>,
731 args: I,
732 ttl: Option<Duration>,
733 fetch: F,
734 ) -> Self
735 where
736 I: IntoIterator<Item = S>,
737 S: Into<String>,
738 F: Fn(Value) -> Fut + Send + Sync + 'static,
739 Fut: Future<Output = Result<Value, String>> + Send + 'static,
740 {
741 let name = name.into();
742 let stage = self.current().id.clone();
743 let args: Vec<String> = args.into_iter().map(Into::into).collect();
744 if !self.current().collect.contains(&name) {
745 self.current().collect.push(name.clone());
746 }
747 self.current().resolve.push(ResolveSpec {
750 slot: name.clone(),
751 resolver: None,
752 args: args.clone(),
753 ttl_secs: ttl.map(|d| d.as_secs()),
754 });
755 let fetch = Arc::new(fetch);
756 self.resolvers.push(StageResolver {
757 stage,
758 name,
759 args,
760 ttl,
761 fetch: Arc::new(move |v| {
762 let fetch = fetch.clone();
763 Box::pin(async move { fetch(v).await })
764 }),
765 });
766 self
767 }
768
769 pub fn after(mut self, dep: impl Into<String>) -> Self {
771 self.current().after.push(dep.into());
772 self
773 }
774
775 pub fn terminal(mut self) -> Self {
777 self.current().terminal = true;
778 self
779 }
780
781 pub fn repair(mut self, policy: RepairPolicy) -> Self {
783 self.current().repair = Some(policy);
784 self
785 }
786
787 pub fn verbatim(mut self, text: impl Into<String>) -> Self {
792 self.current().verbatim = Some(text.into());
793 self
794 }
795
796 pub fn timing(mut self, timing: VoiceTiming) -> Self {
810 self.current().timing = Some(timing);
811 self
812 }
813
814 pub fn require<I, S>(mut self, steps: I) -> Self
817 where
818 I: IntoIterator<Item = S>,
819 S: Into<String>,
820 {
821 let req: Vec<String> = steps.into_iter().map(Into::into).collect();
822 match self.current_overlay {
823 Some(i) => self.spec.overlays[i].require = req,
824 None => self.spec.require = req,
825 }
826 self
827 }
828
829 pub fn spec(&self) -> &ConversationSpec {
831 &self.spec
832 }
833
834 pub fn into_spec(self) -> ConversationSpec {
836 self.spec
837 }
838
839 pub fn from_spec(spec: ConversationSpec) -> Result<CompiledConversation, ConversationError> {
845 Self::from_spec_with_resolvers(spec, &ResolverRegistry::new())
846 }
847
848 pub fn from_spec_stubbing_resolvers(
858 spec: ConversationSpec,
859 ) -> Result<CompiledConversation, ConversationError> {
860 let registry = ResolverRegistry::stubbing(&spec);
861 Self::from_spec_with_resolvers(spec, ®istry)
862 }
863
864 pub fn from_spec_with_resolvers(
870 spec: ConversationSpec,
871 registry: &ResolverRegistry,
872 ) -> Result<CompiledConversation, ConversationError> {
873 let mut resolvers = Vec::new();
874 let stages = spec
875 .stages
876 .iter()
877 .chain(spec.overlays.iter().flat_map(|o| o.stages.iter()));
878 for stage in stages {
879 for r in &stage.resolve {
880 let fetch = registry.get(r.resolver_name()).ok_or_else(|| {
881 ConversationError::Spec(format!(
882 "stage '{}' slot '{}' needs resolver '{}', which is not in the registry",
883 stage.id,
884 r.slot,
885 r.resolver_name()
886 ))
887 })?;
888 resolvers.push(StageResolver {
889 stage: stage.id.clone(),
890 name: r.slot.clone(),
891 args: r.args.clone(),
892 ttl: r.ttl_secs.map(Duration::from_secs),
893 fetch,
894 });
895 }
896 }
897 compile_spec(spec, resolvers)
898 }
899
900 pub fn compile(self) -> Result<CompiledConversation, ConversationError> {
902 compile_spec(self.spec, self.resolvers)
903 }
904}
905
906impl crate::live::Live {
907 pub fn converse(self, convo: &CompiledConversation) -> Self {
930 self.govern_compiled(convo.flow().clone())
931 .install_conversation(convo)
932 }
933
934 pub fn converse_observe(self, convo: &CompiledConversation) -> Self {
937 self.observe_compiled(convo.flow().clone())
938 .install_conversation(convo)
939 }
940
941 fn install_conversation(mut self, convo: &CompiledConversation) -> Self {
946 self.digressions = convo
947 .overlays()
948 .iter()
949 .map(CompiledOverlay::to_runtime)
950 .collect();
951 self.repair_policies = convo.repair_policies().clone();
952 for (step, timing) in convo.timing_policies() {
955 self.stage_timings
956 .entry(step.clone())
957 .or_insert_with(|| timing.clone());
958 }
959 self.corrections = convo.correction_policies().clone();
960 self.verbatims = convo.verbatim_policies().clone();
961 let mut policies: Vec<_> = convo
965 .policies()
966 .iter()
967 .filter(|p| !matches!(p, crate::policy::Policy::SafetyHandoff { .. }))
968 .cloned()
969 .collect();
970 policies.append(&mut self.policies);
971 self.policies = policies;
972 for extract in convo.all_extractors() {
973 self = self.extract_record(extract);
974 }
975 self
976 }
977}
978
979fn is_always(g: &Guard) -> bool {
980 matches!(g, Guard::Spec(Pred::Always))
981}
982
983fn any_of(guards: Vec<Guard>) -> Option<Guard> {
985 if guards.is_empty() {
986 return None;
987 }
988 if guards.iter().any(is_always) {
989 return Some(Guard::always());
990 }
991 if guards.len() == 1 {
992 return guards.into_iter().next();
993 }
994 Some(Guard::any(guards))
995}
996
997fn lower_flow(stages: &[StageSpec], require: &[String]) -> Result<CompiledFlow, ConversationError> {
1000 if stages.is_empty() {
1001 return Err(ConversationError::Empty);
1002 }
1003 let ids: BTreeSet<&str> = stages.iter().map(|s| s.id.as_str()).collect();
1004 if ids.len() != stages.len() {
1005 return Err(ConversationError::Spec("duplicate stage ids".into()));
1006 }
1007 for s in stages {
1008 for t in &s.next {
1009 if !ids.contains(t.to.as_str()) {
1010 return Err(ConversationError::Spec(format!(
1011 "stage '{}' transitions to unknown stage '{}'",
1012 s.id, t.to
1013 )));
1014 }
1015 }
1016 for d in &s.after {
1017 if !ids.contains(d.as_str()) {
1018 return Err(ConversationError::Spec(format!(
1019 "stage '{}' depends on unknown stage '{}'",
1020 s.id, d
1021 )));
1022 }
1023 }
1024 if let Some(target) = s.repair.as_ref().and_then(|r| r.escalate_to.as_ref())
1025 && !ids.contains(target.as_str())
1026 {
1027 return Err(ConversationError::Spec(format!(
1028 "stage '{}' escalates to unknown stage '{}'",
1029 s.id, target
1030 )));
1031 }
1032 }
1033 for r in require {
1034 if !ids.contains(r.as_str()) {
1035 return Err(ConversationError::Spec(format!(
1036 "require references unknown stage '{r}'"
1037 )));
1038 }
1039 }
1040
1041 let mut incoming: BTreeMap<&str, Vec<(&str, Guard)>> = BTreeMap::new();
1043 for s in stages {
1044 for t in &s.next {
1045 incoming
1046 .entry(t.to.as_str())
1047 .or_default()
1048 .push((s.id.as_str(), t.when.clone()));
1049 }
1050 if let Some(target) = s.repair.as_ref().and_then(|r| r.escalate_to.as_ref()) {
1052 incoming
1053 .entry(target.as_str())
1054 .or_default()
1055 .push((s.id.as_str(), Guard::is_true(escalate_flag(&s.id))));
1056 }
1057 }
1058
1059 let mut fb = Flow::new();
1060 for s in stages {
1061 fb = fb.step(&s.id);
1062
1063 let mut deps: BTreeSet<&str> = s.after.iter().map(String::as_str).collect();
1064 if let Some(inc) = incoming.get(s.id.as_str()) {
1065 for (src, _) in inc {
1066 deps.insert(src);
1067 }
1068 }
1069 for d in deps {
1070 fb = fb.after(d);
1071 }
1072
1073 if let Some(inc) = incoming.get(s.id.as_str())
1074 && let Some(gate) = any_of(inc.iter().map(|(_, w)| w.clone()).collect())
1075 {
1076 fb = fb.gate(gate);
1077 }
1078
1079 if let Some(posture) = stage_posture(s) {
1080 fb = fb.posture(posture);
1081 }
1082 if let Some(ground) = &s.ground {
1083 fb = fb.ground(ground.clone());
1084 }
1085
1086 let mut allow: Vec<String> = s.allow.clone();
1087 if let Some(c) = &s.commit
1088 && !allow.contains(&c.tool)
1089 {
1090 allow.push(c.tool.clone());
1091 }
1092 if !allow.is_empty() {
1093 fb = fb.allow(allow);
1094 }
1095 if let Some(c) = &s.commit {
1096 fb = fb.commit(&c.tool, c.when.clone());
1097 }
1098
1099 if s.terminal {
1100 fb = fb.terminal();
1101 } else {
1102 let done = stage_completion(s).ok_or_else(|| {
1103 ConversationError::Spec(format!(
1104 "non-terminal stage '{}' has no completion (add collect, next, or done)",
1105 s.id
1106 ))
1107 })?;
1108 fb = fb.done(done);
1109 }
1110 }
1111
1112 for rule in reopen_rules(stages) {
1114 if let Some(when) = any_of(
1115 rule.slots
1116 .iter()
1117 .map(|slot| Guard::is_true(correction_flag(slot)))
1118 .collect(),
1119 ) {
1120 fb = fb.reset(rule.steps).when(when);
1121 }
1122 }
1123
1124 if !require.is_empty() {
1125 fb = fb.require(require.to_vec());
1126 }
1127
1128 let flow = fb.build().map_err(|e| ConversationError::Flow(e.issues))?;
1129 flow.compile().map_err(ConversationError::Compile)
1130}
1131
1132fn frame_extractors(stages: &[StageSpec]) -> Vec<Extract> {
1134 stages
1135 .iter()
1136 .filter_map(|s| s.frame.as_ref().and_then(FrameSpec::to_extract))
1137 .collect()
1138}
1139
1140fn compile_spec(
1141 mut spec: ConversationSpec,
1142 resolvers: Vec<StageResolver>,
1143) -> Result<CompiledConversation, ConversationError> {
1144 for policy in spec.policies.clone() {
1147 if let crate::policy::Policy::SafetyHandoff { intents } = policy
1148 && let Some(trigger) = any_of(
1149 intents
1150 .iter()
1151 .map(|i| Guard::is_true(format!("intent:{i}")))
1152 .collect(),
1153 )
1154 {
1155 spec.overlays.push(OverlaySpec {
1156 name: "safety".into(),
1157 trigger,
1158 stages: vec![StageSpec {
1159 id: "safety_handoff".into(),
1160 say: Some("Safety concern detected — hand off to a human now.".into()),
1161 terminal: true,
1162 ..Default::default()
1163 }],
1164 require: Vec::new(),
1165 resume: Resume::Terminate,
1166 });
1167 }
1168 }
1169
1170 for stage in spec
1175 .stages
1176 .iter_mut()
1177 .chain(spec.overlays.iter_mut().flat_map(|o| o.stages.iter_mut()))
1178 {
1179 for slot in stage
1180 .resolve
1181 .iter()
1182 .map(|r| r.slot.clone())
1183 .collect::<Vec<_>>()
1184 {
1185 if !stage.collect.contains(&slot) {
1186 stage.collect.push(slot);
1187 }
1188 }
1189 }
1190
1191 let flow = lower_flow(&spec.stages, &spec.require)?;
1193
1194 let ids: BTreeSet<&str> = spec.stages.iter().map(|s| s.id.as_str()).collect();
1198 let mut overlay_of: BTreeMap<&str, usize> = BTreeMap::new();
1199 for (i, ov) in spec.overlays.iter().enumerate() {
1200 for s in &ov.stages {
1201 overlay_of.entry(s.id.as_str()).or_insert(i);
1202 }
1203 }
1204 for r in &resolvers {
1205 if !ids.contains(r.stage.as_str()) && !overlay_of.contains_key(r.stage.as_str()) {
1206 return Err(ConversationError::Spec(format!(
1207 "resolver for slot '{}' references unknown stage '{}'",
1208 r.name, r.stage
1209 )));
1210 }
1211 }
1212
1213 let build_resolver_extractor = |stage: &str, binds: &[&StageResolver]| {
1216 let mut builder = Extract::record(format!("{}__{}_resolve", spec.name, stage));
1217 for r in binds {
1218 let fetch = r.fetch.clone();
1219 builder = builder.field_resolve(r.name.clone(), r.args.clone(), r.ttl, move |args| {
1220 let fetch = fetch.clone();
1221 async move { fetch(args).await }
1222 });
1223 }
1224 builder.build()
1225 };
1226 let mut by_stage: BTreeMap<&str, Vec<&StageResolver>> = BTreeMap::new();
1227 for r in &resolvers {
1228 by_stage.entry(r.stage.as_str()).or_default().push(r);
1229 }
1230 let mut overlay_extractors: BTreeMap<usize, Vec<Extract>> = BTreeMap::new();
1231
1232 let mut extractors = frame_extractors(&spec.stages);
1234 for (stage, binds) in &by_stage {
1235 let extractor = build_resolver_extractor(stage, binds);
1236 if ids.contains(stage) {
1237 extractors.push(extractor);
1238 } else if let Some(&i) = overlay_of.get(stage) {
1239 overlay_extractors.entry(i).or_default().push(extractor);
1240 }
1241 }
1242
1243 let mut overlays = Vec::with_capacity(spec.overlays.len());
1247 for ov in &spec.overlays {
1248 let require = if ov.require.is_empty() {
1249 ov.stages
1250 .iter()
1251 .filter(|s| s.terminal)
1252 .map(|s| s.id.clone())
1253 .collect()
1254 } else {
1255 ov.require.clone()
1256 };
1257 let ov_flow = lower_flow(&ov.stages, &require)?;
1258 let mut ov_extractors = frame_extractors(&ov.stages);
1259 if let Some(bound) = overlay_extractors.remove(&overlays.len()) {
1260 ov_extractors.extend(bound);
1261 }
1262 overlays.push(CompiledOverlay {
1263 name: ov.name.clone(),
1264 trigger: ov.trigger.clone(),
1265 flow: ov_flow,
1266 extractors: ov_extractors,
1267 resume: ov.resume,
1268 });
1269 }
1270
1271 let repair = spec
1273 .stages
1274 .iter()
1275 .filter_map(|s| s.repair.clone().map(|p| (s.id.clone(), p)))
1276 .collect();
1277
1278 let timing = spec
1280 .stages
1281 .iter()
1282 .chain(spec.overlays.iter().flat_map(|ov| ov.stages.iter()))
1283 .filter_map(|s| {
1284 let timing = s.timing.clone().or_else(|| {
1285 s.verbatim
1286 .as_ref()
1287 .map(|_| VoiceTiming::new().uninterruptible())
1288 });
1289 timing.map(|t| (s.id.clone(), t))
1290 })
1291 .collect();
1292
1293 let verbatim = spec
1295 .stages
1296 .iter()
1297 .chain(spec.overlays.iter().flat_map(|ov| ov.stages.iter()))
1298 .filter_map(|s| s.verbatim.clone().map(|t| (s.id.clone(), t)))
1299 .collect();
1300
1301 let mut corrections: BTreeMap<String, Vec<String>> = BTreeMap::new();
1304 for rule in reopen_rules(&spec.stages) {
1305 for slot in rule.slots {
1306 let clear = corrections.entry(slot).or_default();
1307 for key in &rule.clear {
1308 if !clear.contains(key) {
1309 clear.push(key.clone());
1310 }
1311 }
1312 }
1313 }
1314
1315 let policies = spec.policies.clone();
1316
1317 Ok(CompiledConversation {
1318 flow,
1319 extractors,
1320 overlays,
1321 repair,
1322 timing,
1323 corrections,
1324 verbatim,
1325 policies,
1326 spec,
1327 })
1328}
1329
1330struct Reopen {
1338 slots: Vec<String>,
1339 steps: Vec<String>,
1340 clear: Vec<String>,
1341}
1342
1343fn reopen_rules(stages: &[StageSpec]) -> Vec<Reopen> {
1347 let mut dependents: BTreeMap<&str, BTreeSet<&str>> = BTreeMap::new();
1349 for s in stages {
1350 for d in &s.after {
1351 dependents
1352 .entry(d.as_str())
1353 .or_default()
1354 .insert(s.id.as_str());
1355 }
1356 for t in &s.next {
1357 dependents
1358 .entry(s.id.as_str())
1359 .or_default()
1360 .insert(t.to.as_str());
1361 }
1362 if let Some(target) = s.repair.as_ref().and_then(|r| r.escalate_to.as_ref()) {
1363 dependents
1364 .entry(s.id.as_str())
1365 .or_default()
1366 .insert(target.as_str());
1367 }
1368 }
1369 let collected: BTreeSet<&str> = stages
1370 .iter()
1371 .flat_map(|s| s.collect.iter().map(String::as_str))
1372 .collect();
1373 stages
1374 .iter()
1375 .filter(|s| !s.collect.is_empty())
1376 .filter_map(|s| {
1377 let mut downstream: BTreeSet<&str> = BTreeSet::new();
1378 let mut queue = vec![s.id.as_str()];
1379 while let Some(id) = queue.pop() {
1380 for next in dependents.get(id).into_iter().flatten() {
1381 if *next != s.id && downstream.insert(next) {
1382 queue.push(next);
1383 }
1384 }
1385 }
1386 if downstream.is_empty() {
1387 return None;
1388 }
1389 let clear = stages
1390 .iter()
1391 .filter(|st| downstream.contains(st.id.as_str()))
1392 .filter_map(|st| st.commit.as_ref())
1393 .flat_map(|c| c.when.state_keys())
1394 .filter(|k| !collected.contains(k.as_str()))
1395 .collect::<BTreeSet<_>>()
1396 .into_iter()
1397 .collect();
1398 Some(Reopen {
1399 slots: s.collect.clone(),
1400 steps: downstream.into_iter().map(str::to_string).collect(),
1401 clear,
1402 })
1403 })
1404 .collect()
1405}
1406
1407fn stage_posture(s: &StageSpec) -> Option<String> {
1410 let read = s.verbatim.as_ref().map(|text| {
1411 format!(
1412 "Say the following exactly as written, word for word. Do not paraphrase, \
1413 add to or leave out any part of it:\n\n{text}"
1414 )
1415 });
1416 match (&s.say, read) {
1417 (Some(say), Some(read)) => Some(format!("{say}\n\n{read}")),
1418 (say, read) => say.clone().or(read),
1419 }
1420}
1421
1422fn stage_completion(s: &StageSpec) -> Option<Guard> {
1423 let base = stage_base_completion(s);
1424 match (base, s.verbatim.is_some()) {
1426 (base, false) => base,
1427 (None, true) => Some(Guard::is_true(verbatim_flag(&s.id))),
1428 (Some(base), true) => Some(Guard::all(vec![base, Guard::is_true(verbatim_flag(&s.id))])),
1429 }
1430}
1431
1432fn stage_base_completion(s: &StageSpec) -> Option<Guard> {
1433 let base = if let Some(g) = &s.done {
1434 Some(g.clone())
1435 } else if !s.collect.is_empty() {
1436 Some(Guard::captured(s.collect.clone()))
1437 } else {
1438 any_of(s.next.iter().map(|t| t.when.clone()).collect())
1439 };
1440 if s.repair
1441 .as_ref()
1442 .and_then(|r| r.escalate_to.as_ref())
1443 .is_some()
1444 {
1445 let esc = Guard::is_true(escalate_flag(&s.id));
1446 return Some(match base {
1447 Some(b) => Guard::any(vec![b, esc]),
1448 None => esc,
1449 });
1450 }
1451 base
1452}
1453
1454#[cfg(test)]
1455mod tests {
1456 use super::*;
1457 use gemini_adk_rs::flow::Enforcement;
1458 use gemini_adk_rs::state::State;
1459
1460 fn booking() -> CompiledConversation {
1461 Conversation::new("booking")
1462 .stage("collect")
1463 .instruction("Help the user book a table.")
1464 .collect(["party_size", "slot"])
1465 .next("check", Guard::captured(["party_size", "slot"]))
1466 .stage("check")
1467 .ground("Party of {party_size} at {slot}.")
1468 .next("confirm", Guard::is_true("availability_ok"))
1469 .stage("confirm")
1470 .commit("book", Guard::is_true("user_confirmed"))
1471 .next("done", Guard::called_ok("book"))
1472 .stage("done")
1473 .terminal()
1474 .require(["done"])
1475 .compile()
1476 .expect("booking compiles")
1477 }
1478
1479 #[test]
1480 fn compiles_to_a_governed_flow() {
1481 let convo = booking();
1482 assert!(convo.flow().tool_surface().tools.contains("book"));
1484 assert_eq!(convo.flow().flow().steps.len(), 4);
1485 }
1486
1487 #[test]
1488 fn lowered_flow_enforces_stage_order_and_commit() {
1489 let convo = booking();
1490 let mut mon = convo.monitor(Enforcement::Enforce);
1491 let state = State::new();
1492
1493 let ex = mon.explain(&state);
1495 assert!(ex.active.contains(&"collect".to_string()));
1496 assert!(ex.blocked_tools.contains_key("book"));
1497
1498 let _ = state.set("party_size", 4u8);
1500 let _ = state.set("slot", "tomorrow 7pm");
1501 mon.on_turn(&state);
1502 assert!(mon.explain(&state).active.contains(&"check".to_string()));
1503
1504 let _ = state.set("availability_ok", true);
1506 mon.on_turn(&state);
1507 assert!(mon.admits_tool("book", &state).is_err());
1508
1509 let _ = state.set("user_confirmed", true);
1511 assert!(mon.admits_tool("book", &state).is_ok());
1512 mon.on_tool_ok("book", &state);
1513 mon.on_turn(&state);
1514 assert!(mon.is_complete());
1515 }
1516
1517 #[test]
1518 fn spec_round_trips_through_json() {
1519 let spec = booking().spec().clone();
1520 let json = serde_json::to_string(&spec).expect("serialize spec");
1521 let back: ConversationSpec = serde_json::from_str(&json).expect("deserialize spec");
1522 let recompiled = Conversation::from_spec(back).expect("recompile from spec");
1523 assert_eq!(recompiled.flow().flow().steps.len(), 4);
1524 }
1525
1526 #[tokio::test]
1527 async fn named_resolver_spec_round_trips_and_binds_from_registry() {
1528 let json = r#"
1530 {
1531 "name": "booking",
1532 "stages": [
1533 { "id": "check",
1534 "resolve": [{ "slot": "availability", "resolver": "avail", "args": ["party_size"] }],
1535 "next": [{ "to": "done", "when": { "captured": ["availability"] } }] },
1536 { "id": "done", "terminal": true }
1537 ],
1538 "require": ["done"]
1539 }
1540 "#;
1541 let spec: ConversationSpec = serde_json::from_str(json).expect("parse");
1542 let reser = serde_json::to_string(&spec).unwrap();
1544 let back: ConversationSpec = serde_json::from_str(&reser).unwrap();
1545 assert_eq!(back.stages[0].resolve[0].resolver_name(), "avail");
1546
1547 let err = Conversation::from_spec(back.clone()).expect_err("unbound resolver errors");
1550 assert!(matches!(err, ConversationError::Spec(m) if m.contains("avail")));
1551
1552 let registry = ResolverRegistry::new().with("avail", |_args| async move {
1554 Ok(serde_json::json!({ "open": true }))
1555 });
1556 let convo =
1557 Conversation::from_spec_with_resolvers(back, ®istry).expect("binds and compiles");
1558 assert!(
1560 convo.extractors().iter().any(|e| e
1561 .field_state_keys()
1562 .iter()
1563 .any(|(_, k)| k == "availability")),
1564 "a resolver extractor filling 'availability' was compiled in"
1565 );
1566 }
1567
1568 #[tokio::test]
1569 async fn resolver_spec_is_validatable_and_simulatable_without_a_registry() {
1570 let json = r#"{ "name": "r", "stages": [
1573 { "id": "check",
1574 "resolve": [{ "slot": "avail", "resolver": "lookup", "args": ["x"] }],
1575 "next": [{ "to": "done", "when": { "captured": ["avail"] } }] },
1576 { "id": "done", "terminal": true } ], "require": ["done"] }"#;
1577 let spec: ConversationSpec = serde_json::from_str(json).unwrap();
1578
1579 assert!(Conversation::from_spec(spec.clone()).is_err());
1581
1582 let convo = Conversation::from_spec_stubbing_resolvers(spec)
1584 .expect("stubbed resolvers compile for testing");
1585
1586 use crate::simulation::Sim;
1589 let mut sim = Sim::new(&convo, gemini_adk_rs::flow::Enforcement::Enforce);
1590 sim.set("avail", serde_json::json!({ "open": true }));
1591 sim.turn();
1592 assert!(sim.is_complete(), "resolver slot supplied by set completes");
1593 }
1594
1595 #[tokio::test]
1596 async fn overlay_resolver_binds_validates_and_fills_the_overlay() {
1597 let json = r#"{ "name": "ov", "stages": [
1601 { "id": "main", "terminal": true } ],
1602 "require": ["main"],
1603 "overlays": [ { "name": "lookup", "trigger": { "is_true": "intent:lookup" },
1604 "stages": [
1605 { "id": "fetch", "resolve": [{ "slot": "balance", "resolver": "bal" }] },
1606 { "id": "ov_done", "terminal": true, "after": ["fetch"] } ] } ] }"#;
1607 let spec: ConversationSpec = serde_json::from_str(json).unwrap();
1608
1609 let err = Conversation::from_spec(spec.clone()).expect_err("unbound overlay resolver");
1611 assert!(matches!(err, ConversationError::Spec(m) if m.contains("bal")));
1612
1613 let registry =
1615 ResolverRegistry::new().with("bal", |_args| async move { Ok(serde_json::json!(42)) });
1616 let convo = Conversation::from_spec_with_resolvers(spec, ®istry).expect("compiles");
1617 assert!(
1618 convo.overlays()[0]
1619 .extractors
1620 .iter()
1621 .any(|e| e.field_state_keys().iter().any(|(_, k)| k == "balance")),
1622 "the overlay carries the resolver extractor for 'balance'"
1623 );
1624 }
1625
1626 #[tokio::test]
1627 async fn resolver_slot_counts_toward_stage_completion_from_json() {
1628 let json = r#"{ "name": "r2", "stages": [
1632 { "id": "check", "resolve": [{ "slot": "avail", "resolver": "lookup" }] },
1633 { "id": "done", "terminal": true, "after": ["check"] } ],
1634 "require": ["done"] }"#;
1635 let spec: ConversationSpec = serde_json::from_str(json).unwrap();
1636 let convo =
1637 Conversation::from_spec_stubbing_resolvers(spec).expect("resolver-only stage compiles");
1638 assert!(
1639 convo.spec().stages[0]
1640 .collect
1641 .contains(&"avail".to_string()),
1642 "declared resolver slot normalized into collect"
1643 );
1644 }
1645
1646 #[test]
1647 fn conversation_spec_schema_is_valid_json_with_expected_shape() {
1648 let schema_str = conversation_spec_schema();
1649 let schema: serde_json::Value =
1650 serde_json::from_str(&schema_str).expect("schema is valid JSON");
1651 assert_eq!(schema["title"], "ConversationSpec");
1654 assert!(
1655 schema["properties"]["stages"].is_object(),
1656 "schema exposes the stages property: {schema}"
1657 );
1658 assert!(
1660 schema["definitions"]["Pred"].is_object() || schema["$defs"]["Pred"].is_object(),
1661 "Pred atom schema is present"
1662 );
1663 }
1664
1665 #[test]
1666 fn conversation_error_serializes_machine_readable() {
1667 let err = Conversation::new("x")
1669 .stage("s")
1670 .commit("pay", Guard::always())
1671 .complete_when(Guard::called_ok("pay"))
1672 .next("done", Guard::called_ok("pay"))
1673 .stage("done")
1674 .terminal()
1675 .compile()
1676 .expect_err("unguarded commit must fail");
1677 let json = serde_json::to_value(&err).expect("error serializes");
1678 assert_eq!(json["kind"], "compile");
1679 assert!(json["message"].is_string());
1680 let errors = json["errors"].as_array().expect("errors array");
1682 assert!(
1683 errors.iter().any(|e| e["kind"] == "unguarded_commit_tool"),
1684 "structured FlowError kinds present: {json}"
1685 );
1686 }
1687
1688 #[test]
1689 fn collect_frame_uses_frame_slot_keys() {
1690 use gemini_adk_rs::frame::{Frame, FrameSpec, SlotSpec};
1691
1692 struct Booking;
1693 impl Frame for Booking {
1694 fn frame() -> FrameSpec {
1695 FrameSpec {
1696 name: "booking".into(),
1697 slots: vec![SlotSpec::new("party_size"), SlotSpec::new("slot")],
1698 }
1699 }
1700 }
1701
1702 let convo = Conversation::new("b")
1703 .stage("collect")
1704 .collect_frame::<Booking>()
1705 .next("done", Guard::captured(["party_size", "slot"]))
1706 .stage("done")
1707 .terminal()
1708 .compile()
1709 .expect("compiles");
1710
1711 let mut mon = convo.monitor(Enforcement::Enforce);
1713 let state = State::new();
1714 assert!(mon.explain(&state).active.contains(&"collect".to_string()));
1715 let _ = state.set("party_size", 2u8);
1716 let _ = state.set("slot", "noon");
1717 mon.on_turn(&state);
1718 assert!(mon.marking().done.contains("collect"));
1720 assert!(mon.marking().done.contains("done"));
1721 }
1722
1723 #[tokio::test]
1724 async fn collect_frame_extractor_fills_and_scores_slots() {
1725 use gemini_adk_rs::frame::{Frame, FrameSpec, SlotRecognizer, SlotSpec};
1726 use gemini_adk_rs::live::TranscriptTurn;
1727
1728 struct Order;
1729 impl Frame for Order {
1730 fn frame() -> FrameSpec {
1731 FrameSpec {
1732 name: "order".into(),
1733 slots: vec![SlotSpec {
1734 recognizer: Some(SlotRecognizer::OneOf(vec![
1735 "pizza".into(),
1736 "salad".into(),
1737 ])),
1738 ..SlotSpec::new("item")
1739 }],
1740 }
1741 }
1742 }
1743
1744 let convo = Conversation::new("o")
1745 .stage("collect")
1746 .collect_frame::<Order>()
1747 .next("done", Guard::captured(["item"]))
1748 .stage("done")
1749 .terminal()
1750 .compile()
1751 .expect("compiles");
1752
1753 assert_eq!(convo.extractors().len(), 1);
1755 let extractor = convo.extractors()[0].clone().into_extractor();
1756
1757 let state = State::new();
1760 let window = vec![TranscriptTurn {
1761 turn_number: 0,
1762 user: "I'd like a large PIZZA".into(),
1763 model: String::new(),
1764 tool_calls: Vec::new(),
1765 timestamp: std::time::Instant::now(),
1766 }];
1767 let out = extractor.extract_with_state(&window, &state).await.unwrap();
1768 assert_eq!(out.get("item").and_then(|v| v.as_str()), Some("pizza"));
1769
1770 let ev = state.evidence("item");
1771 assert_eq!(ev.source.as_deref(), Some("extraction"));
1772 assert!(ev.confidence.unwrap() > 0.0);
1773 }
1774
1775 #[tokio::test]
1776 async fn validate_rejects_out_of_range_recognized_values() {
1777 use gemini_adk_rs::frame::{Frame, FrameSpec, SlotRecognizer, SlotSpec, SlotValidator};
1778 use gemini_adk_rs::live::TranscriptTurn;
1779
1780 struct Party;
1781 impl Frame for Party {
1782 fn frame() -> FrameSpec {
1783 FrameSpec {
1784 name: "party".into(),
1785 slots: vec![SlotSpec {
1786 recognizer: Some(SlotRecognizer::Integer),
1787 validate: Some(SlotValidator::Range {
1788 min: Some(1.0),
1789 max: Some(12.0),
1790 }),
1791 ..SlotSpec::new("party_size")
1792 }],
1793 }
1794 }
1795 }
1796
1797 let convo = Conversation::new("p")
1798 .stage("collect")
1799 .collect_frame::<Party>()
1800 .next("done", Guard::captured(["party_size"]))
1801 .stage("done")
1802 .terminal()
1803 .compile()
1804 .expect("compiles");
1805 let extractor = convo.extractors()[0].clone().into_extractor();
1806
1807 let run = |text: &str| {
1808 let extractor = extractor.clone();
1809 let text = text.to_string();
1810 async move {
1811 let state = State::new();
1812 let window = vec![TranscriptTurn {
1813 turn_number: 0,
1814 user: text,
1815 model: String::new(),
1816 tool_calls: Vec::new(),
1817 timestamp: std::time::Instant::now(),
1818 }];
1819 let out = extractor.extract_with_state(&window, &state).await.unwrap();
1820 out.get("party_size").cloned()
1821 }
1822 };
1823
1824 assert_eq!(run("a table for 4").await, Some(serde_json::json!(4)));
1826 assert_eq!(run("a table for 40").await, None);
1827 }
1828
1829 #[tokio::test]
1830 async fn resolve_slot_fills_from_async_fetch() {
1831 use gemini_adk_rs::live::TranscriptTurn;
1832
1833 let convo = Conversation::new("c")
1834 .stage("check")
1835 .resolve_slot("availability", ["party_size"], None, |args| async move {
1836 let n = args
1838 .get("party_size")
1839 .and_then(serde_json::Value::as_i64)
1840 .unwrap_or(0);
1841 Ok(serde_json::json!(n <= 8))
1842 })
1843 .next("done", Guard::is_set("availability"))
1844 .stage("done")
1845 .terminal()
1846 .compile()
1847 .expect("compiles");
1848
1849 assert_eq!(convo.extractors().len(), 1);
1851 let extractor = convo.extractors()[0].clone().into_extractor();
1852
1853 let state = State::new();
1854 let _ = state.set("party_size", 4i64);
1855 let window = vec![TranscriptTurn {
1856 turn_number: 0,
1857 user: "any".into(),
1858 model: String::new(),
1859 tool_calls: Vec::new(),
1860 timestamp: std::time::Instant::now(),
1861 }];
1862 let out = extractor.extract_with_state(&window, &state).await.unwrap();
1863 assert_eq!(out.get("availability"), Some(&serde_json::json!(true)));
1864 }
1865
1866 #[test]
1867 fn converse_registers_flow_and_extractors() {
1868 use gemini_adk_rs::frame::{Frame, FrameSpec, SlotRecognizer, SlotSpec};
1870
1871 struct Order;
1872 impl Frame for Order {
1873 fn frame() -> FrameSpec {
1874 FrameSpec {
1875 name: "order".into(),
1876 slots: vec![SlotSpec {
1877 recognizer: Some(SlotRecognizer::OneOf(vec!["pizza".into()])),
1878 ..SlotSpec::new("item")
1879 }],
1880 }
1881 }
1882 }
1883 let convo = Conversation::new("o")
1884 .stage("collect")
1885 .collect_frame::<Order>()
1886 .next("done", Guard::captured(["item"]))
1887 .stage("done")
1888 .terminal()
1889 .compile()
1890 .expect("compiles");
1891
1892 let _live = crate::live::Live::builder().converse(&convo);
1894 }
1895
1896 #[test]
1897 fn overlay_suspends_main_then_resumes_previous() {
1898 let convo = Conversation::new("support")
1901 .stage("a")
1902 .next("b", Guard::is_true("a_done"))
1903 .stage("b")
1904 .terminal()
1905 .overlay("faq")
1906 .trigger(Guard::is_true("intent:faq"))
1907 .stage("answer")
1909 .complete_when(Guard::is_true("faq_answered"))
1910 .next("faq_end", Guard::is_true("faq_answered"))
1911 .stage("faq_end")
1912 .terminal()
1913 .resume(Resume::Previous)
1914 .end_overlay()
1915 .compile()
1916 .expect("compiles");
1917
1918 assert_eq!(convo.overlays().len(), 1);
1919 let mut stack = convo.stack(Enforcement::Enforce);
1920 let state = State::new();
1921
1922 assert!(stack.explain(&state).active.contains(&"a".to_string()));
1924 assert!(stack.active_overlay().is_none());
1925
1926 let _ = state.set("intent:faq", true);
1928 stack.on_turn(&state);
1929 assert_eq!(stack.active_overlay(), Some("faq"));
1930
1931 let _ = state.set("faq_answered", true);
1934 let _ = state.set("intent:faq", false);
1935 stack.on_turn(&state);
1936 assert_eq!(stack.active_overlay(), Some("faq"));
1937
1938 stack.on_turn(&state);
1940 assert!(stack.active_overlay().is_none());
1941 assert!(stack.explain(&state).active.contains(&"a".to_string()));
1942
1943 let _ = state.set("a_done", true);
1945 stack.on_turn(&state);
1946 assert!(stack.current().marking().done.contains("a"));
1947 }
1948
1949 #[test]
1955 fn live_and_simulator_drive_the_same_stack() {
1956 use crate::simulation::Sim;
1957
1958 let convo = Conversation::new("support")
1959 .stage("collect")
1960 .allow(["lookup"])
1961 .complete_when(Guard::is_true("info"))
1962 .next("done", Guard::is_true("info"))
1963 .repair(RepairPolicy::new(2, 3).escalate_to("handoff"))
1964 .stage("done")
1965 .terminal()
1966 .stage("handoff")
1967 .complete_when(Guard::is_true("handoff_complete"))
1968 .overlay("faq")
1969 .trigger(Guard::is_true("intent:faq"))
1970 .stage("answer")
1971 .allow(["search_faq"])
1972 .complete_when(Guard::is_true("faq_answered"))
1973 .next("faq_end", Guard::is_true("faq_answered"))
1974 .stage("faq_end")
1975 .terminal()
1976 .resume(Resume::Previous)
1977 .end_overlay()
1978 .compile()
1979 .expect("compiles");
1980
1981 let live = crate::live::Live::builder().converse(&convo);
1983 assert_eq!(
1984 live.digressions()
1985 .iter()
1986 .map(gemini_adk_rs::flow::Overlay::name)
1987 .collect::<Vec<_>>(),
1988 ["faq"]
1989 );
1990 assert!(live.repair_policies().contains_key("collect"));
1991 let monitor = FlowMonitor::compiled(convo.flow().clone(), Enforcement::Enforce);
1992 let mut installed = crate::live::connect::assemble_stack(
1993 monitor,
1994 live.digressions().to_vec(),
1995 live.repair_policies().clone(),
1996 std::collections::BTreeMap::new(),
1997 std::collections::BTreeMap::new(),
1998 std::collections::BTreeMap::new(),
1999 &[],
2000 );
2001
2002 let mut sim = Sim::new(&convo, Enforcement::Enforce);
2004 let state = sim.state().clone();
2006
2007 let script: [&[(&str, bool)]; 6] = [
2008 &[],
2009 &[("intent:faq", true)],
2010 &[("faq_answered", true), ("intent:faq", false)],
2011 &[],
2012 &[],
2013 &[],
2014 ];
2015 for facts in script {
2016 for (k, v) in facts {
2017 let _ = state.set(*k, *v);
2018 }
2019 sim.turn();
2020 installed.on_turn(&state);
2021 let a = sim.explain();
2022 let b = installed.explain(&state);
2023 assert_eq!(a.active, b.active);
2024 assert_eq!(a.allowed_tools, b.allowed_tools);
2025 assert_eq!(a.blocked_tools, b.blocked_tools);
2026 assert_eq!(sim.active_overlay(), installed.active_overlay());
2027 assert_eq!(sim.is_complete(), installed.is_complete());
2028 }
2029 assert!(
2031 installed
2032 .explain(&state)
2033 .active
2034 .contains(&"handoff".to_string())
2035 );
2036 assert_eq!(state.get::<bool>("repair:collect:escalate"), Some(true));
2037 }
2038
2039 #[test]
2042 fn regoverning_drops_installed_digressions() {
2043 let convo = Conversation::new("s")
2044 .stage("a")
2045 .terminal()
2046 .overlay("faq")
2047 .trigger(Guard::is_true("intent:faq"))
2048 .stage("x")
2049 .terminal()
2050 .end_overlay()
2051 .compile()
2052 .expect("compiles");
2053 let live = crate::live::Live::builder().converse(&convo);
2054 assert_eq!(live.digressions().len(), 1);
2055 let live = live.govern(Flow::new().step("only").terminal().build().expect("valid"));
2056 assert!(live.digressions().is_empty());
2057 }
2058
2059 #[test]
2060 fn overlay_spec_round_trips_through_json() {
2061 let spec = Conversation::new("s")
2062 .stage("main")
2063 .terminal()
2064 .overlay("cancel")
2065 .trigger(Guard::is_true("intent:cancel"))
2066 .stage("confirm")
2067 .terminal()
2068 .resume(Resume::Terminate)
2069 .end_overlay()
2070 .into_spec();
2071 let json = serde_json::to_string(&spec).unwrap();
2072 let back: ConversationSpec = serde_json::from_str(&json).unwrap();
2073 assert_eq!(back.overlays.len(), 1);
2074 assert_eq!(back.overlays[0].resume, Resume::Terminate);
2075 assert!(Conversation::from_spec(back).is_ok());
2077 }
2078
2079 #[tokio::test]
2080 async fn safety_policy_terminates_on_intent() {
2081 use crate::policy::Policy;
2082 use crate::simulation::Sim;
2083
2084 let convo = Conversation::new("support")
2085 .policy(Policy::safety_handoff(["self_harm", "abuse"]))
2086 .policy(Policy::redact(["card_number"]))
2087 .stage("triage")
2088 .next("resolve", Guard::is_true("triaged"))
2089 .stage("resolve")
2090 .terminal()
2091 .require(["resolve"])
2092 .compile()
2093 .expect("compiles");
2094
2095 assert!(convo.redacted_fields().contains("card_number"));
2097 assert!(convo.overlays().iter().any(|o| o.name == "safety"));
2099
2100 let mut sim = Sim::new(&convo, Enforcement::Enforce);
2101 assert!(sim.active().contains(&"triage".to_string()));
2102 assert!(!sim.is_complete());
2103
2104 sim.set("intent:abuse", true);
2107 sim.turn();
2108 assert_eq!(sim.active_overlay(), Some("safety"));
2109 assert!(!sim.is_complete());
2110 assert!(
2111 sim.postures()
2112 .iter()
2113 .any(|p| p.contains("hand off to a human")),
2114 "{:?}",
2115 sim.postures()
2116 );
2117
2118 sim.turn();
2120 assert!(sim.is_complete());
2121 assert!(sim.active().is_empty());
2122 }
2123
2124 #[tokio::test]
2127 async fn a_corrected_slot_reopens_the_confirmation() {
2128 use crate::simulation::{Scenario, SimStep};
2129 use serde_json::json;
2130 let convo = Conversation::new("booking")
2131 .stage("collect")
2132 .collect(["party_size"])
2133 .stage("book")
2134 .after("collect")
2135 .commit("book_table", Guard::is_true("confirmed"))
2136 .complete_when(Guard::called_ok("book_table"))
2137 .stage("end")
2138 .after("book")
2139 .terminal()
2140 .compile()
2141 .unwrap();
2142 assert_eq!(
2143 convo.correction_policies()["party_size"],
2144 ["confirmed"],
2145 "a correction to party_size clears the booking confirmation"
2146 );
2147
2148 let set = |key: &str, value: serde_json::Value| SimStep::Set {
2149 key: key.into(),
2150 value,
2151 };
2152 let scenario = Scenario {
2153 name: "correction".into(),
2154 steps: vec![
2155 set("party_size", json!(4)),
2156 SimStep::Turn,
2157 SimStep::ExpectActive(vec!["book".into()]),
2158 set("confirmed", json!(true)),
2159 SimStep::Turn,
2160 SimStep::ExpectAllowed("book_table".into()),
2161 set("party_size", json!(5)),
2163 SimStep::Turn,
2164 SimStep::ExpectDenied("book_table".into()),
2165 SimStep::ExpectActive(vec!["book".into()]),
2166 set("confirmed", json!(true)),
2167 SimStep::Turn,
2168 SimStep::ExpectAllowed("book_table".into()),
2169 SimStep::ToolOk("book_table".into()),
2170 SimStep::ExpectComplete,
2171 ],
2172 };
2173 scenario.run(&convo, Enforcement::Enforce).await.unwrap();
2174 }
2175
2176 #[tokio::test]
2178 async fn repeated_barge_ins_escalate_to_the_handoff() {
2179 use crate::simulation::{Scenario, SimStep};
2180 let convo = Conversation::new("support")
2181 .stage("collect")
2182 .collect(["issue"])
2183 .repair(
2184 RepairPolicy::new(10, 10)
2185 .escalate_after_interruptions(2)
2186 .escalate_to("handoff"),
2187 )
2188 .stage("handoff")
2189 .terminal()
2190 .compile()
2191 .unwrap();
2192 let scenario = Scenario {
2193 name: "barge-ins".into(),
2194 steps: vec![
2195 SimStep::Turn,
2196 SimStep::ExpectActive(vec!["collect".into()]),
2197 SimStep::Interrupt,
2198 SimStep::Turn,
2199 SimStep::ExpectActive(vec!["collect".into()]),
2200 SimStep::Interrupt,
2201 SimStep::ExpectSlot {
2202 key: escalate_flag("collect"),
2203 value: serde_json::json!(true),
2204 },
2205 SimStep::Turn,
2206 SimStep::ExpectComplete,
2208 ],
2209 };
2210 scenario.run(&convo, Enforcement::Enforce).await.unwrap();
2211 }
2212
2213 #[test]
2214 fn stage_timing_round_trips_and_reaches_the_stack() {
2215 use std::time::Duration;
2216 let convo = Conversation::new("pacing")
2217 .stage("ask")
2218 .collect(["name"])
2219 .timing(VoiceTiming::new().reprompt_after(Duration::from_secs(6)))
2220 .add_stage(crate::motifs::Motif::disclosure("terms", "terms_ack"))
2221 .compile()
2222 .unwrap();
2223
2224 let json = serde_json::to_value(convo.spec()).unwrap();
2225 assert_eq!(json["stages"][0]["timing"]["reprompt_after_ms"], 6000);
2226 assert_eq!(json["stages"][1]["timing"]["interruptible"], false);
2227 let back: ConversationSpec = serde_json::from_value(json).unwrap();
2228 assert_eq!(
2229 back.stages[0].timing,
2230 Some(VoiceTiming::new().reprompt_after(Duration::from_secs(6)))
2231 );
2232
2233 assert!(convo.timing_policies()["terms"].holds_floor());
2234 let stack = convo.stack(Enforcement::Enforce);
2235 assert_eq!(stack.timing_policies().len(), 2);
2236 }
2237
2238 #[test]
2239 fn converse_keeps_timings_and_policies_set_on_the_builder() {
2240 use std::time::Duration;
2241 let convo = Conversation::new("pacing")
2242 .stage("ask")
2243 .collect(["name"])
2244 .timing(VoiceTiming::new().reprompt_after(Duration::from_secs(6)))
2245 .stage("done")
2246 .after("ask")
2247 .terminal()
2248 .policy(crate::policy::Policy::redact(["card"]))
2249 .compile()
2250 .unwrap();
2251 let mine = VoiceTiming::new().reprompt_after(Duration::from_secs(3));
2252
2253 for live in [
2255 crate::live::Live::builder()
2256 .stage_timing("ask", mine.clone())
2257 .policy(crate::policy::Policy::redact(["ssn"]))
2258 .converse(&convo),
2259 crate::live::Live::builder()
2260 .converse(&convo)
2261 .stage_timing("ask", mine.clone())
2262 .policy(crate::policy::Policy::redact(["ssn"])),
2263 ] {
2264 assert_eq!(live.stage_timings["ask"], mine);
2265 assert_eq!(live.policies.len(), 2, "{:?}", live.policies);
2266 }
2267 }
2268
2269 #[tokio::test]
2270 async fn repair_reprompts_then_escalates_to_handoff() {
2271 use crate::simulation::Sim;
2272
2273 let convo = Conversation::new("support")
2276 .stage("collect")
2277 .complete_when(Guard::is_true("info"))
2278 .next("done", Guard::is_true("info"))
2279 .repair(RepairPolicy::new(2, 3).escalate_to("handoff"))
2280 .stage("done")
2281 .terminal()
2282 .stage("handoff")
2284 .complete_when(Guard::is_true("handoff_complete"))
2285 .compile()
2286 .expect("compiles");
2287
2288 let mut sim = Sim::new(&convo, Enforcement::Enforce);
2289 assert!(sim.active().contains(&"collect".to_string()));
2290
2291 sim.turn();
2293 assert_eq!(sim.slot::<bool>("repair:collect:reprompt"), None);
2294 sim.turn();
2296 assert_eq!(sim.slot::<bool>("repair:collect:reprompt"), Some(true));
2297 assert!(sim.active().contains(&"collect".to_string()));
2298 sim.turn();
2300 assert_eq!(sim.slot::<bool>("repair:collect:escalate"), Some(true));
2301 assert!(sim.active().contains(&"handoff".to_string()));
2302 assert!(!sim.active().contains(&"collect".to_string()));
2303 }
2304
2305 #[tokio::test]
2306 async fn repair_signal_clears_when_stage_satisfied() {
2307 use crate::simulation::Sim;
2308
2309 let convo = Conversation::new("s")
2310 .stage("collect")
2311 .complete_when(Guard::is_true("info"))
2312 .next("done", Guard::is_true("info"))
2313 .repair(RepairPolicy::new(1, 9))
2314 .stage("done")
2315 .terminal()
2316 .require(["done"])
2317 .compile()
2318 .expect("compiles");
2319
2320 let mut sim = Sim::new(&convo, Enforcement::Enforce);
2321 sim.turn(); assert_eq!(sim.slot::<bool>("repair:collect:reprompt"), Some(true));
2323
2324 sim.set("info", true);
2327 sim.turn();
2328 sim.turn();
2329 assert_eq!(sim.slot::<bool>("repair:collect:reprompt"), Some(false));
2330 assert!(sim.is_complete());
2331 }
2332
2333 #[test]
2334 fn rejects_transition_to_unknown_stage() {
2335 let err = Conversation::new("x")
2336 .stage("a")
2337 .next("ghost", Guard::always())
2338 .stage("b")
2339 .terminal()
2340 .compile()
2341 .expect_err("unknown target must fail");
2342 assert!(matches!(err, ConversationError::Spec(_)));
2343 }
2344
2345 #[test]
2346 fn rejects_unguarded_commit_via_flow_compile() {
2347 let err = Conversation::new("x")
2349 .stage("s")
2350 .commit("pay", Guard::always())
2351 .complete_when(Guard::called_ok("pay"))
2352 .next("done", Guard::called_ok("pay"))
2353 .stage("done")
2354 .terminal()
2355 .compile()
2356 .expect_err("unguarded commit must fail");
2357 assert!(matches!(err, ConversationError::Compile(_)));
2358 }
2359}