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::{
57 CompiledFlow, Enforcement, Flow, FlowErrors, FlowExplanation, FlowMonitor, Guard, Pred,
58};
59use gemini_adk_rs::frame::{Frame, FrameSpec};
60use gemini_adk_rs::state::State;
61
62type SlotFetch =
64 Arc<dyn Fn(Value) -> Pin<Box<dyn Future<Output = Result<Value, String>> + Send>> + Send + Sync>;
65
66#[derive(Clone, Default)]
88pub struct ResolverRegistry {
89 fetchers: BTreeMap<String, SlotFetch>,
90}
91
92impl ResolverRegistry {
93 pub fn new() -> Self {
95 Self::default()
96 }
97
98 pub fn with<F, Fut>(mut self, name: impl Into<String>, fetch: F) -> Self
100 where
101 F: Fn(Value) -> Fut + Send + Sync + 'static,
102 Fut: Future<Output = Result<Value, String>> + Send + 'static,
103 {
104 self.add(name, fetch);
105 self
106 }
107
108 pub fn add<F, Fut>(&mut self, name: impl Into<String>, fetch: F)
110 where
111 F: Fn(Value) -> Fut + Send + Sync + 'static,
112 Fut: Future<Output = Result<Value, String>> + Send + 'static,
113 {
114 let fetch = Arc::new(fetch);
115 self.fetchers.insert(
116 name.into(),
117 Arc::new(move |v| {
118 let fetch = fetch.clone();
119 Box::pin(async move { fetch(v).await })
120 }),
121 );
122 }
123
124 pub fn contains(&self, name: &str) -> bool {
126 self.fetchers.contains_key(name)
127 }
128
129 pub fn stubbing(spec: &ConversationSpec) -> Self {
139 let mut reg = Self::new();
140 let stages = spec
141 .stages
142 .iter()
143 .chain(spec.overlays.iter().flat_map(|o| o.stages.iter()));
144 for stage in stages {
145 for r in &stage.resolve {
146 let name = r.resolver_name().to_string();
147 if !reg.contains(&name) {
148 reg.add(name, |_args| async { Ok(serde_json::Value::Null) });
149 }
150 }
151 }
152 reg
153 }
154
155 fn get(&self, name: &str) -> Option<SlotFetch> {
156 self.fetchers.get(name).cloned()
157 }
158}
159
160#[derive(Clone)]
164struct StageResolver {
165 stage: String,
166 name: String,
167 args: Vec<String>,
168 ttl: Option<Duration>,
169 fetch: SlotFetch,
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
179pub struct ResolveSpec {
180 pub slot: String,
182 #[serde(default, skip_serializing_if = "Option::is_none")]
184 pub resolver: Option<String>,
185 #[serde(default, skip_serializing_if = "Vec::is_empty")]
187 pub args: Vec<String>,
188 #[serde(default, skip_serializing_if = "Option::is_none")]
190 pub ttl_secs: Option<u64>,
191}
192
193impl ResolveSpec {
194 fn resolver_name(&self) -> &str {
197 self.resolver.as_deref().unwrap_or(&self.slot)
198 }
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
203pub struct TransitionSpec {
204 pub to: String,
206 pub when: Guard,
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
212pub struct CommitSpec {
213 pub tool: String,
215 pub when: Guard,
217}
218
219#[derive(Debug, Clone, Default, Serialize, Deserialize, schemars::JsonSchema)]
221pub struct StageSpec {
222 pub id: String,
224 #[serde(default, skip_serializing_if = "Option::is_none")]
226 pub say: Option<String>,
227 #[serde(default, skip_serializing_if = "Option::is_none")]
229 pub ground: Option<String>,
230 #[serde(default, skip_serializing_if = "Vec::is_empty")]
232 pub collect: Vec<String>,
233 #[serde(default, skip_serializing_if = "Option::is_none")]
236 pub frame: Option<FrameSpec>,
237 #[serde(default, skip_serializing_if = "Vec::is_empty")]
239 pub allow: Vec<String>,
240 #[serde(default, skip_serializing_if = "Option::is_none")]
242 pub done: Option<Guard>,
243 #[serde(default, skip_serializing_if = "Option::is_none")]
245 pub commit: Option<CommitSpec>,
246 #[serde(default, skip_serializing_if = "Vec::is_empty")]
248 pub next: Vec<TransitionSpec>,
249 #[serde(default, skip_serializing_if = "Vec::is_empty")]
251 pub after: Vec<String>,
252 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
254 pub terminal: bool,
255 #[serde(default, skip_serializing_if = "Option::is_none")]
257 pub repair: Option<RepairPolicy>,
258 #[serde(default, skip_serializing_if = "Vec::is_empty")]
261 pub resolve: Vec<ResolveSpec>,
262}
263
264fn escalate_flag(stage: &str) -> String {
266 format!("repair:{stage}:escalate")
267}
268
269fn reprompt_flag(stage: &str) -> String {
271 format!("repair:{stage}:reprompt")
272}
273
274#[derive(
276 Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, schemars::JsonSchema,
277)]
278#[serde(rename_all = "snake_case")]
279pub enum Resume {
280 #[default]
282 Previous,
283 Restart,
285 Terminate,
287}
288
289fn default_reprompt_after() -> u32 {
290 2
291}
292fn default_escalate_after() -> u32 {
293 4
294}
295
296#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
302pub struct RepairPolicy {
303 #[serde(default = "default_reprompt_after")]
305 pub reprompt_after: u32,
306 #[serde(default = "default_escalate_after")]
308 pub escalate_after: u32,
309 #[serde(default, skip_serializing_if = "Option::is_none")]
311 pub escalate_to: Option<String>,
312}
313
314impl Default for RepairPolicy {
315 fn default() -> Self {
316 Self {
317 reprompt_after: default_reprompt_after(),
318 escalate_after: default_escalate_after(),
319 escalate_to: None,
320 }
321 }
322}
323
324impl RepairPolicy {
325 pub fn new(reprompt_after: u32, escalate_after: u32) -> Self {
327 Self {
328 reprompt_after,
329 escalate_after,
330 escalate_to: None,
331 }
332 }
333
334 pub fn escalate_to(mut self, stage: impl Into<String>) -> Self {
336 self.escalate_to = Some(stage.into());
337 self
338 }
339}
340
341#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
344pub struct OverlaySpec {
345 pub name: String,
347 pub trigger: Guard,
349 #[serde(default)]
351 pub stages: Vec<StageSpec>,
352 #[serde(default, skip_serializing_if = "Vec::is_empty")]
354 pub require: Vec<String>,
355 #[serde(default)]
357 pub resume: Resume,
358}
359
360#[derive(Debug, Clone, Default, Serialize, Deserialize, schemars::JsonSchema)]
363pub struct ConversationSpec {
364 pub name: String,
366 #[serde(default)]
368 pub stages: Vec<StageSpec>,
369 #[serde(default, skip_serializing_if = "Vec::is_empty")]
371 pub require: Vec<String>,
372 #[serde(default, skip_serializing_if = "Vec::is_empty")]
374 pub overlays: Vec<OverlaySpec>,
375 #[serde(default, skip_serializing_if = "Vec::is_empty")]
377 pub policies: Vec<crate::policy::Policy>,
378}
379
380#[derive(Debug)]
382pub enum ConversationError {
383 Empty,
385 Spec(String),
387 Flow(Vec<String>),
389 Compile(FlowErrors),
391}
392
393impl std::fmt::Display for ConversationError {
394 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
395 match self {
396 ConversationError::Empty => write!(f, "conversation has no stages"),
397 ConversationError::Spec(m) => write!(f, "conversation spec error: {m}"),
398 ConversationError::Flow(errs) => {
399 write!(f, "lowered flow is invalid: {}", errs.join("; "))
400 }
401 ConversationError::Compile(e) => write!(f, "lowered flow failed to compile: {e}"),
402 }
403 }
404}
405
406impl std::error::Error for ConversationError {}
407
408pub fn conversation_spec_schema() -> String {
414 let schema = schemars::schema_for!(ConversationSpec);
415 serde_json::to_string_pretty(&schema).expect("schema serialization is infallible")
416}
417
418impl serde::Serialize for ConversationError {
419 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
423 use serde::ser::SerializeMap;
424 let mut m = s.serialize_map(None)?;
425 m.serialize_entry("message", &self.to_string())?;
426 match self {
427 ConversationError::Empty => {
428 m.serialize_entry("kind", "empty")?;
429 }
430 ConversationError::Spec(msg) => {
431 m.serialize_entry("kind", "spec")?;
432 m.serialize_entry("detail", msg)?;
433 }
434 ConversationError::Flow(errs) => {
435 m.serialize_entry("kind", "flow")?;
436 m.serialize_entry("errors", errs)?;
437 }
438 ConversationError::Compile(e) => {
439 m.serialize_entry("kind", "compile")?;
440 m.serialize_entry("errors", &e.0)?;
441 }
442 }
443 m.end()
444 }
445}
446
447#[derive(Clone)]
449pub struct CompiledOverlay {
450 pub name: String,
452 pub trigger: Guard,
454 pub flow: CompiledFlow,
456 pub extractors: Vec<Extract>,
458 pub resume: Resume,
460}
461
462#[derive(Clone)]
465pub struct CompiledConversation {
466 flow: CompiledFlow,
467 extractors: Vec<Extract>,
468 overlays: Vec<CompiledOverlay>,
469 repair: BTreeMap<String, RepairPolicy>,
470 policies: Vec<crate::policy::Policy>,
471 spec: ConversationSpec,
472}
473
474impl std::fmt::Debug for CompiledConversation {
477 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
478 f.debug_struct("CompiledConversation")
479 .field("flow", &self.flow)
480 .field("extractors", &self.extractors.len())
481 .field("overlays", &self.overlays.len())
482 .field("policies", &self.policies)
483 .field("spec", &self.spec)
484 .finish()
485 }
486}
487
488impl CompiledConversation {
489 pub fn flow(&self) -> &CompiledFlow {
491 &self.flow
492 }
493 pub fn extractors(&self) -> &[Extract] {
496 &self.extractors
497 }
498 pub fn overlays(&self) -> &[CompiledOverlay] {
500 &self.overlays
501 }
502 pub fn policies(&self) -> &[crate::policy::Policy] {
504 &self.policies
505 }
506 pub fn redacted_fields(&self) -> BTreeSet<String> {
508 self.policies
509 .iter()
510 .flat_map(|p| p.redacted_keys().iter().cloned())
511 .collect()
512 }
513 pub fn all_extractors(&self) -> Vec<Extract> {
516 let mut all = self.extractors.clone();
517 for ov in &self.overlays {
518 all.extend(ov.extractors.iter().cloned());
519 }
520 all
521 }
522 pub fn stack(&self, mode: Enforcement) -> FlowStack {
525 FlowStack::new(self, mode)
526 }
527 pub fn spec(&self) -> &ConversationSpec {
529 &self.spec
530 }
531 pub fn to_mermaid(&self) -> String {
533 self.flow.to_mermaid()
534 }
535 pub fn monitor(&self, mode: Enforcement) -> FlowMonitor {
537 FlowMonitor::compiled(self.flow.clone(), mode)
538 }
539}
540
541#[derive(Clone, Default)]
546pub struct Conversation {
547 spec: ConversationSpec,
548 resolvers: Vec<StageResolver>,
549 current_overlay: Option<usize>,
552}
553
554impl Conversation {
555 pub fn new(name: impl Into<String>) -> Self {
557 Self {
558 spec: ConversationSpec {
559 name: name.into(),
560 ..Default::default()
561 },
562 resolvers: Vec::new(),
563 current_overlay: None,
564 }
565 }
566
567 pub fn stage(mut self, id: impl Into<String>) -> Self {
570 let stage = StageSpec {
571 id: id.into(),
572 ..Default::default()
573 };
574 match self.current_overlay {
575 Some(i) => self.spec.overlays[i].stages.push(stage),
576 None => self.spec.stages.push(stage),
577 }
578 self
579 }
580
581 pub fn overlay(mut self, name: impl Into<String>) -> Self {
585 self.spec.overlays.push(OverlaySpec {
586 name: name.into(),
587 trigger: Guard::is_true("__overlay_never_triggers__"),
589 stages: Vec::new(),
590 require: Vec::new(),
591 resume: Resume::Previous,
592 });
593 self.current_overlay = Some(self.spec.overlays.len() - 1);
594 self
595 }
596
597 pub fn trigger(mut self, guard: Guard) -> Self {
599 if let Some(i) = self.current_overlay {
600 self.spec.overlays[i].trigger = guard;
601 }
602 self
603 }
604
605 pub fn resume(mut self, resume: Resume) -> Self {
607 if let Some(i) = self.current_overlay {
608 self.spec.overlays[i].resume = resume;
609 }
610 self
611 }
612
613 pub fn end_overlay(mut self) -> Self {
617 self.current_overlay = None;
618 self
619 }
620
621 pub fn add_stage(mut self, stage: StageSpec) -> Self {
625 match self.current_overlay {
626 Some(i) => self.spec.overlays[i].stages.push(stage),
627 None => self.spec.stages.push(stage),
628 }
629 self
630 }
631
632 pub fn add_overlay(mut self, overlay: OverlaySpec) -> Self {
635 self.spec.overlays.push(overlay);
636 self.current_overlay = None;
637 self
638 }
639
640 pub fn policy(mut self, policy: impl Into<crate::policy::Policy>) -> Self {
642 self.spec.policies.push(policy.into());
643 self
644 }
645
646 fn current(&mut self) -> &mut StageSpec {
647 let stages = match self.current_overlay {
648 Some(i) => &mut self.spec.overlays[i].stages,
649 None => &mut self.spec.stages,
650 };
651 stages
652 .last_mut()
653 .expect("call .stage(..) before configuring a stage")
654 }
655
656 pub fn say(mut self, text: impl Into<String>) -> Self {
658 self.current().say = Some(text.into());
659 self
660 }
661
662 pub fn ground(mut self, template: impl Into<String>) -> Self {
664 self.current().ground = Some(template.into());
665 self
666 }
667
668 pub fn collect<I, S>(mut self, fields: I) -> Self
670 where
671 I: IntoIterator<Item = S>,
672 S: Into<String>,
673 {
674 self.current().collect = fields.into_iter().map(Into::into).collect();
675 self
676 }
677
678 pub fn collect_frame<F: Frame>(mut self) -> Self {
683 let spec = F::frame();
684 let stage = self.current();
685 stage.collect = spec.slot_keys();
686 stage.frame = Some(spec);
687 self
688 }
689
690 pub fn allow<I, S>(mut self, tools: I) -> Self
692 where
693 I: IntoIterator<Item = S>,
694 S: Into<String>,
695 {
696 self.current().allow = tools.into_iter().map(Into::into).collect();
697 self
698 }
699
700 pub fn complete_when(mut self, guard: Guard) -> Self {
703 self.current().done = Some(guard);
704 self
705 }
706
707 pub fn commit(mut self, tool: impl Into<String>, when: Guard) -> Self {
709 self.current().commit = Some(CommitSpec {
710 tool: tool.into(),
711 when,
712 });
713 self
714 }
715
716 pub fn next(mut self, to: impl Into<String>, when: Guard) -> Self {
718 self.current().next.push(TransitionSpec {
719 to: to.into(),
720 when,
721 });
722 self
723 }
724
725 pub fn resolve_slot<I, S, F, Fut>(
734 mut self,
735 name: impl Into<String>,
736 args: I,
737 ttl: Option<Duration>,
738 fetch: F,
739 ) -> Self
740 where
741 I: IntoIterator<Item = S>,
742 S: Into<String>,
743 F: Fn(Value) -> Fut + Send + Sync + 'static,
744 Fut: Future<Output = Result<Value, String>> + Send + 'static,
745 {
746 let name = name.into();
747 let stage = self.current().id.clone();
748 let args: Vec<String> = args.into_iter().map(Into::into).collect();
749 if !self.current().collect.contains(&name) {
750 self.current().collect.push(name.clone());
751 }
752 self.current().resolve.push(ResolveSpec {
755 slot: name.clone(),
756 resolver: None,
757 args: args.clone(),
758 ttl_secs: ttl.map(|d| d.as_secs()),
759 });
760 let fetch = Arc::new(fetch);
761 self.resolvers.push(StageResolver {
762 stage,
763 name,
764 args,
765 ttl,
766 fetch: Arc::new(move |v| {
767 let fetch = fetch.clone();
768 Box::pin(async move { fetch(v).await })
769 }),
770 });
771 self
772 }
773
774 pub fn after(mut self, dep: impl Into<String>) -> Self {
776 self.current().after.push(dep.into());
777 self
778 }
779
780 pub fn terminal(mut self) -> Self {
782 self.current().terminal = true;
783 self
784 }
785
786 pub fn repair(mut self, policy: RepairPolicy) -> Self {
788 self.current().repair = Some(policy);
789 self
790 }
791
792 pub fn require<I, S>(mut self, steps: I) -> Self
795 where
796 I: IntoIterator<Item = S>,
797 S: Into<String>,
798 {
799 let req: Vec<String> = steps.into_iter().map(Into::into).collect();
800 match self.current_overlay {
801 Some(i) => self.spec.overlays[i].require = req,
802 None => self.spec.require = req,
803 }
804 self
805 }
806
807 pub fn spec(&self) -> &ConversationSpec {
809 &self.spec
810 }
811
812 pub fn into_spec(self) -> ConversationSpec {
814 self.spec
815 }
816
817 pub fn from_spec(spec: ConversationSpec) -> Result<CompiledConversation, ConversationError> {
823 Self::from_spec_with_resolvers(spec, &ResolverRegistry::new())
824 }
825
826 pub fn from_spec_stubbing_resolvers(
836 spec: ConversationSpec,
837 ) -> Result<CompiledConversation, ConversationError> {
838 let registry = ResolverRegistry::stubbing(&spec);
839 Self::from_spec_with_resolvers(spec, ®istry)
840 }
841
842 pub fn from_spec_with_resolvers(
848 spec: ConversationSpec,
849 registry: &ResolverRegistry,
850 ) -> Result<CompiledConversation, ConversationError> {
851 let mut resolvers = Vec::new();
852 let stages = spec
853 .stages
854 .iter()
855 .chain(spec.overlays.iter().flat_map(|o| o.stages.iter()));
856 for stage in stages {
857 for r in &stage.resolve {
858 let fetch = registry.get(r.resolver_name()).ok_or_else(|| {
859 ConversationError::Spec(format!(
860 "stage '{}' slot '{}' needs resolver '{}', which is not in the registry",
861 stage.id,
862 r.slot,
863 r.resolver_name()
864 ))
865 })?;
866 resolvers.push(StageResolver {
867 stage: stage.id.clone(),
868 name: r.slot.clone(),
869 args: r.args.clone(),
870 ttl: r.ttl_secs.map(Duration::from_secs),
871 fetch,
872 });
873 }
874 }
875 compile_spec(spec, resolvers)
876 }
877
878 pub fn compile(self) -> Result<CompiledConversation, ConversationError> {
880 compile_spec(self.spec, self.resolvers)
881 }
882}
883
884impl crate::live::Live {
885 pub fn converse(self, convo: &CompiledConversation) -> Self {
906 let mut live = self.govern_compiled(convo.flow().clone());
907 for extract in convo.all_extractors() {
908 live = live.extract_record(extract);
909 }
910 live
911 }
912
913 pub fn converse_observe(self, convo: &CompiledConversation) -> Self {
916 let mut live = self.observe_compiled(convo.flow().clone());
917 for extract in convo.all_extractors() {
918 live = live.extract_record(extract);
919 }
920 live
921 }
922}
923
924struct ActiveOverlay {
926 name: String,
927 monitor: FlowMonitor,
928 resume: Resume,
929}
930
931pub struct FlowStack {
939 main_flow: CompiledFlow,
940 main: FlowMonitor,
941 mode: Enforcement,
942 overlays: Vec<CompiledOverlay>,
943 active: Option<ActiveOverlay>,
944 terminated: bool,
945 repair: BTreeMap<String, RepairPolicy>,
947 active_turns: BTreeMap<String, u32>,
949}
950
951impl FlowStack {
952 fn new(convo: &CompiledConversation, mode: Enforcement) -> Self {
953 Self {
954 main_flow: convo.flow.clone(),
955 main: FlowMonitor::compiled(convo.flow.clone(), mode),
956 mode,
957 overlays: convo.overlays.clone(),
958 active: None,
959 terminated: false,
960 repair: convo.repair.clone(),
961 active_turns: BTreeMap::new(),
962 }
963 }
964
965 fn apply_repair(&mut self, state: &State) {
969 if self.repair.is_empty() {
970 return;
971 }
972 let active: BTreeSet<String> = self.main.explain(state).active.into_iter().collect();
973 let left: Vec<String> = self
975 .active_turns
976 .keys()
977 .filter(|k| !active.contains(*k))
978 .cloned()
979 .collect();
980 for stage in left {
981 self.active_turns.remove(&stage);
982 let _ = state.set(reprompt_flag(&stage), false);
983 let _ = state.set(escalate_flag(&stage), false);
984 }
985 for stage in &active {
986 let count = self.active_turns.entry(stage.clone()).or_insert(0);
987 *count += 1;
988 if let Some(rp) = self.repair.get(stage) {
989 if *count >= rp.reprompt_after {
990 let _ = state.set(reprompt_flag(stage), true);
991 }
992 if *count >= rp.escalate_after {
993 let _ = state.set(escalate_flag(stage), true);
994 }
995 }
996 }
997 }
998
999 pub fn current(&self) -> &FlowMonitor {
1001 self.active.as_ref().map_or(&self.main, |a| &a.monitor)
1002 }
1003
1004 pub fn active_overlay(&self) -> Option<&str> {
1006 self.active.as_ref().map(|a| a.name.as_str())
1007 }
1008
1009 pub fn is_complete(&self) -> bool {
1012 self.terminated || (self.active.is_none() && self.main.is_complete())
1013 }
1014
1015 fn triggered(&self, state: &State) -> Option<usize> {
1017 self.overlays
1018 .iter()
1019 .position(|ov| self.main.eval(&ov.trigger, state))
1020 }
1021
1022 pub fn on_turn(&mut self, state: &State) {
1026 if self.terminated {
1027 return;
1028 }
1029 match &mut self.active {
1030 Some(active) => {
1031 active.monitor.on_turn(state);
1032 if active.monitor.is_complete() {
1033 let resume = active.resume;
1034 self.active = None;
1035 match resume {
1036 Resume::Previous => {}
1038 Resume::Restart => {
1039 self.main = FlowMonitor::compiled(self.main_flow.clone(), self.mode);
1040 }
1041 Resume::Terminate => self.terminated = true,
1042 }
1043 }
1044 }
1045 None => {
1046 if let Some(idx) = self.triggered(state) {
1047 let ov = &self.overlays[idx];
1048 let mut monitor = FlowMonitor::compiled(ov.flow.clone(), self.mode);
1049 monitor.on_turn(state);
1051 if monitor.is_complete() {
1052 match ov.resume {
1053 Resume::Previous => {}
1054 Resume::Restart => {
1055 self.main =
1056 FlowMonitor::compiled(self.main_flow.clone(), self.mode);
1057 }
1058 Resume::Terminate => self.terminated = true,
1059 }
1060 } else {
1061 self.active = Some(ActiveOverlay {
1062 name: ov.name.clone(),
1063 monitor,
1064 resume: ov.resume,
1065 });
1066 }
1067 } else {
1068 self.apply_repair(state);
1071 self.main.on_turn(state);
1072 }
1073 }
1074 }
1075 }
1076
1077 pub fn on_tool_ok(&mut self, tool: &str, state: &State) {
1079 match &mut self.active {
1080 Some(active) => active.monitor.on_tool_ok(tool, state),
1081 None => self.main.on_tool_ok(tool, state),
1082 }
1083 }
1084
1085 pub fn admits_tool(&self, tool: &str, state: &State) -> Result<(), String> {
1087 self.current().admits_tool(tool, state)
1088 }
1089
1090 pub fn explain(&self, state: &State) -> FlowExplanation {
1092 self.current().explain(state)
1093 }
1094}
1095
1096fn is_always(g: &Guard) -> bool {
1097 matches!(g, Guard::Spec(Pred::Always))
1098}
1099
1100fn any_of(guards: Vec<Guard>) -> Option<Guard> {
1102 if guards.is_empty() {
1103 return None;
1104 }
1105 if guards.iter().any(is_always) {
1106 return Some(Guard::always());
1107 }
1108 if guards.len() == 1 {
1109 return guards.into_iter().next();
1110 }
1111 Some(Guard::any(guards))
1112}
1113
1114fn lower_flow(stages: &[StageSpec], require: &[String]) -> Result<CompiledFlow, ConversationError> {
1117 if stages.is_empty() {
1118 return Err(ConversationError::Empty);
1119 }
1120 let ids: BTreeSet<&str> = stages.iter().map(|s| s.id.as_str()).collect();
1121 if ids.len() != stages.len() {
1122 return Err(ConversationError::Spec("duplicate stage ids".into()));
1123 }
1124 for s in stages {
1125 for t in &s.next {
1126 if !ids.contains(t.to.as_str()) {
1127 return Err(ConversationError::Spec(format!(
1128 "stage '{}' transitions to unknown stage '{}'",
1129 s.id, t.to
1130 )));
1131 }
1132 }
1133 for d in &s.after {
1134 if !ids.contains(d.as_str()) {
1135 return Err(ConversationError::Spec(format!(
1136 "stage '{}' depends on unknown stage '{}'",
1137 s.id, d
1138 )));
1139 }
1140 }
1141 if let Some(target) = s.repair.as_ref().and_then(|r| r.escalate_to.as_ref())
1142 && !ids.contains(target.as_str())
1143 {
1144 return Err(ConversationError::Spec(format!(
1145 "stage '{}' escalates to unknown stage '{}'",
1146 s.id, target
1147 )));
1148 }
1149 }
1150 for r in require {
1151 if !ids.contains(r.as_str()) {
1152 return Err(ConversationError::Spec(format!(
1153 "require references unknown stage '{r}'"
1154 )));
1155 }
1156 }
1157
1158 let mut incoming: BTreeMap<&str, Vec<(&str, Guard)>> = BTreeMap::new();
1160 for s in stages {
1161 for t in &s.next {
1162 incoming
1163 .entry(t.to.as_str())
1164 .or_default()
1165 .push((s.id.as_str(), t.when.clone()));
1166 }
1167 if let Some(target) = s.repair.as_ref().and_then(|r| r.escalate_to.as_ref()) {
1169 incoming
1170 .entry(target.as_str())
1171 .or_default()
1172 .push((s.id.as_str(), Guard::is_true(escalate_flag(&s.id))));
1173 }
1174 }
1175
1176 let mut fb = Flow::new();
1177 for s in stages {
1178 fb = fb.step(&s.id);
1179
1180 let mut deps: BTreeSet<&str> = s.after.iter().map(String::as_str).collect();
1181 if let Some(inc) = incoming.get(s.id.as_str()) {
1182 for (src, _) in inc {
1183 deps.insert(src);
1184 }
1185 }
1186 for d in deps {
1187 fb = fb.after(d);
1188 }
1189
1190 if let Some(inc) = incoming.get(s.id.as_str())
1191 && let Some(gate) = any_of(inc.iter().map(|(_, w)| w.clone()).collect())
1192 {
1193 fb = fb.gate(gate);
1194 }
1195
1196 if let Some(say) = &s.say {
1197 fb = fb.posture(say.clone());
1198 }
1199 if let Some(ground) = &s.ground {
1200 fb = fb.ground(ground.clone());
1201 }
1202
1203 let mut allow: Vec<String> = s.allow.clone();
1204 if let Some(c) = &s.commit
1205 && !allow.contains(&c.tool)
1206 {
1207 allow.push(c.tool.clone());
1208 }
1209 if !allow.is_empty() {
1210 fb = fb.allow(allow);
1211 }
1212 if let Some(c) = &s.commit {
1213 fb = fb.commit(&c.tool, c.when.clone());
1214 }
1215
1216 if s.terminal {
1217 fb = fb.terminal();
1218 } else {
1219 let done = stage_completion(s).ok_or_else(|| {
1220 ConversationError::Spec(format!(
1221 "non-terminal stage '{}' has no completion (add collect, next, or done)",
1222 s.id
1223 ))
1224 })?;
1225 fb = fb.done(done);
1226 }
1227 }
1228
1229 if !require.is_empty() {
1230 fb = fb.require(require.to_vec());
1231 }
1232
1233 let flow = fb.build().map_err(|e| ConversationError::Flow(e.issues))?;
1234 flow.compile().map_err(ConversationError::Compile)
1235}
1236
1237fn frame_extractors(stages: &[StageSpec]) -> Vec<Extract> {
1239 stages
1240 .iter()
1241 .filter_map(|s| s.frame.as_ref().and_then(FrameSpec::to_extract))
1242 .collect()
1243}
1244
1245fn compile_spec(
1246 mut spec: ConversationSpec,
1247 resolvers: Vec<StageResolver>,
1248) -> Result<CompiledConversation, ConversationError> {
1249 for policy in spec.policies.clone() {
1252 if let crate::policy::Policy::SafetyHandoff { intents } = policy
1253 && let Some(trigger) = any_of(
1254 intents
1255 .iter()
1256 .map(|i| Guard::is_true(format!("intent:{i}")))
1257 .collect(),
1258 )
1259 {
1260 spec.overlays.push(OverlaySpec {
1261 name: "safety".into(),
1262 trigger,
1263 stages: vec![StageSpec {
1264 id: "safety_handoff".into(),
1265 say: Some("Safety concern detected — hand off to a human now.".into()),
1266 terminal: true,
1267 ..Default::default()
1268 }],
1269 require: Vec::new(),
1270 resume: Resume::Terminate,
1271 });
1272 }
1273 }
1274
1275 for stage in spec
1280 .stages
1281 .iter_mut()
1282 .chain(spec.overlays.iter_mut().flat_map(|o| o.stages.iter_mut()))
1283 {
1284 for slot in stage
1285 .resolve
1286 .iter()
1287 .map(|r| r.slot.clone())
1288 .collect::<Vec<_>>()
1289 {
1290 if !stage.collect.contains(&slot) {
1291 stage.collect.push(slot);
1292 }
1293 }
1294 }
1295
1296 let flow = lower_flow(&spec.stages, &spec.require)?;
1298
1299 let ids: BTreeSet<&str> = spec.stages.iter().map(|s| s.id.as_str()).collect();
1303 let mut overlay_of: BTreeMap<&str, usize> = BTreeMap::new();
1304 for (i, ov) in spec.overlays.iter().enumerate() {
1305 for s in &ov.stages {
1306 overlay_of.entry(s.id.as_str()).or_insert(i);
1307 }
1308 }
1309 for r in &resolvers {
1310 if !ids.contains(r.stage.as_str()) && !overlay_of.contains_key(r.stage.as_str()) {
1311 return Err(ConversationError::Spec(format!(
1312 "resolver for slot '{}' references unknown stage '{}'",
1313 r.name, r.stage
1314 )));
1315 }
1316 }
1317
1318 let build_resolver_extractor = |stage: &str, binds: &[&StageResolver]| {
1321 let mut builder = Extract::record(format!("{}__{}_resolve", spec.name, stage));
1322 for r in binds {
1323 let fetch = r.fetch.clone();
1324 builder = builder.field_resolve(r.name.clone(), r.args.clone(), r.ttl, move |args| {
1325 let fetch = fetch.clone();
1326 async move { fetch(args).await }
1327 });
1328 }
1329 builder.build()
1330 };
1331 let mut by_stage: BTreeMap<&str, Vec<&StageResolver>> = BTreeMap::new();
1332 for r in &resolvers {
1333 by_stage.entry(r.stage.as_str()).or_default().push(r);
1334 }
1335 let mut overlay_extractors: BTreeMap<usize, Vec<Extract>> = BTreeMap::new();
1336
1337 let mut extractors = frame_extractors(&spec.stages);
1339 for (stage, binds) in &by_stage {
1340 let extractor = build_resolver_extractor(stage, binds);
1341 if ids.contains(stage) {
1342 extractors.push(extractor);
1343 } else if let Some(&i) = overlay_of.get(stage) {
1344 overlay_extractors.entry(i).or_default().push(extractor);
1345 }
1346 }
1347
1348 let mut overlays = Vec::with_capacity(spec.overlays.len());
1352 for ov in &spec.overlays {
1353 let require = if ov.require.is_empty() {
1354 ov.stages
1355 .iter()
1356 .filter(|s| s.terminal)
1357 .map(|s| s.id.clone())
1358 .collect()
1359 } else {
1360 ov.require.clone()
1361 };
1362 let ov_flow = lower_flow(&ov.stages, &require)?;
1363 let mut ov_extractors = frame_extractors(&ov.stages);
1364 if let Some(bound) = overlay_extractors.remove(&overlays.len()) {
1365 ov_extractors.extend(bound);
1366 }
1367 overlays.push(CompiledOverlay {
1368 name: ov.name.clone(),
1369 trigger: ov.trigger.clone(),
1370 flow: ov_flow,
1371 extractors: ov_extractors,
1372 resume: ov.resume,
1373 });
1374 }
1375
1376 let repair = spec
1378 .stages
1379 .iter()
1380 .filter_map(|s| s.repair.clone().map(|p| (s.id.clone(), p)))
1381 .collect();
1382
1383 let policies = spec.policies.clone();
1384
1385 Ok(CompiledConversation {
1386 flow,
1387 extractors,
1388 overlays,
1389 repair,
1390 policies,
1391 spec,
1392 })
1393}
1394
1395fn stage_completion(s: &StageSpec) -> Option<Guard> {
1400 let base = if let Some(g) = &s.done {
1401 Some(g.clone())
1402 } else if !s.collect.is_empty() {
1403 Some(Guard::captured(s.collect.clone()))
1404 } else {
1405 any_of(s.next.iter().map(|t| t.when.clone()).collect())
1406 };
1407 if s.repair
1408 .as_ref()
1409 .and_then(|r| r.escalate_to.as_ref())
1410 .is_some()
1411 {
1412 let esc = Guard::is_true(escalate_flag(&s.id));
1413 return Some(match base {
1414 Some(b) => Guard::any(vec![b, esc]),
1415 None => esc,
1416 });
1417 }
1418 base
1419}
1420
1421#[cfg(test)]
1422mod tests {
1423 use super::*;
1424 use gemini_adk_rs::flow::Enforcement;
1425 use gemini_adk_rs::state::State;
1426
1427 fn booking() -> CompiledConversation {
1428 Conversation::new("booking")
1429 .stage("collect")
1430 .say("Help the user book a table.")
1431 .collect(["party_size", "slot"])
1432 .next("check", Guard::captured(["party_size", "slot"]))
1433 .stage("check")
1434 .ground("Party of {party_size} at {slot}.")
1435 .next("confirm", Guard::is_true("availability_ok"))
1436 .stage("confirm")
1437 .commit("book", Guard::is_true("user_confirmed"))
1438 .next("done", Guard::called_ok("book"))
1439 .stage("done")
1440 .terminal()
1441 .require(["done"])
1442 .compile()
1443 .expect("booking compiles")
1444 }
1445
1446 #[test]
1447 fn compiles_to_a_governed_flow() {
1448 let convo = booking();
1449 assert!(convo.flow().tool_surface().tools.contains("book"));
1451 assert_eq!(convo.flow().flow().steps.len(), 4);
1452 }
1453
1454 #[test]
1455 fn lowered_flow_enforces_stage_order_and_commit() {
1456 let convo = booking();
1457 let mut mon = convo.monitor(Enforcement::Enforce);
1458 let state = State::new();
1459
1460 let ex = mon.explain(&state);
1462 assert!(ex.active.contains(&"collect".to_string()));
1463 assert!(ex.blocked_tools.contains_key("book"));
1464
1465 let _ = state.set("party_size", 4u8);
1467 let _ = state.set("slot", "tomorrow 7pm");
1468 mon.on_turn(&state);
1469 assert!(mon.explain(&state).active.contains(&"check".to_string()));
1470
1471 let _ = state.set("availability_ok", true);
1473 mon.on_turn(&state);
1474 assert!(mon.admits_tool("book", &state).is_err());
1475
1476 let _ = state.set("user_confirmed", true);
1478 assert!(mon.admits_tool("book", &state).is_ok());
1479 mon.on_tool_ok("book", &state);
1480 mon.on_turn(&state);
1481 assert!(mon.is_complete());
1482 }
1483
1484 #[test]
1485 fn spec_round_trips_through_json() {
1486 let spec = booking().spec().clone();
1487 let json = serde_json::to_string(&spec).expect("serialize spec");
1488 let back: ConversationSpec = serde_json::from_str(&json).expect("deserialize spec");
1489 let recompiled = Conversation::from_spec(back).expect("recompile from spec");
1490 assert_eq!(recompiled.flow().flow().steps.len(), 4);
1491 }
1492
1493 #[tokio::test]
1494 async fn named_resolver_spec_round_trips_and_binds_from_registry() {
1495 let json = r#"
1497 {
1498 "name": "booking",
1499 "stages": [
1500 { "id": "check",
1501 "resolve": [{ "slot": "availability", "resolver": "avail", "args": ["party_size"] }],
1502 "next": [{ "to": "done", "when": { "captured": ["availability"] } }] },
1503 { "id": "done", "terminal": true }
1504 ],
1505 "require": ["done"]
1506 }
1507 "#;
1508 let spec: ConversationSpec = serde_json::from_str(json).expect("parse");
1509 let reser = serde_json::to_string(&spec).unwrap();
1511 let back: ConversationSpec = serde_json::from_str(&reser).unwrap();
1512 assert_eq!(back.stages[0].resolve[0].resolver_name(), "avail");
1513
1514 let err = Conversation::from_spec(back.clone()).expect_err("unbound resolver errors");
1517 assert!(matches!(err, ConversationError::Spec(m) if m.contains("avail")));
1518
1519 let registry = ResolverRegistry::new().with("avail", |_args| async move {
1521 Ok(serde_json::json!({ "open": true }))
1522 });
1523 let convo =
1524 Conversation::from_spec_with_resolvers(back, ®istry).expect("binds and compiles");
1525 assert!(
1527 convo.extractors().iter().any(|e| e
1528 .field_state_keys()
1529 .iter()
1530 .any(|(_, k)| k == "availability")),
1531 "a resolver extractor filling 'availability' was compiled in"
1532 );
1533 }
1534
1535 #[tokio::test]
1536 async fn resolver_spec_is_validatable_and_simulatable_without_a_registry() {
1537 let json = r#"{ "name": "r", "stages": [
1540 { "id": "check",
1541 "resolve": [{ "slot": "avail", "resolver": "lookup", "args": ["x"] }],
1542 "next": [{ "to": "done", "when": { "captured": ["avail"] } }] },
1543 { "id": "done", "terminal": true } ], "require": ["done"] }"#;
1544 let spec: ConversationSpec = serde_json::from_str(json).unwrap();
1545
1546 assert!(Conversation::from_spec(spec.clone()).is_err());
1548
1549 let convo = Conversation::from_spec_stubbing_resolvers(spec)
1551 .expect("stubbed resolvers compile for testing");
1552
1553 use crate::simulation::Sim;
1556 let mut sim = Sim::new(&convo, gemini_adk_rs::flow::Enforcement::Enforce);
1557 sim.set("avail", serde_json::json!({ "open": true }));
1558 sim.turn();
1559 assert!(sim.is_complete(), "resolver slot supplied by set completes");
1560 }
1561
1562 #[tokio::test]
1563 async fn overlay_resolver_binds_validates_and_fills_the_overlay() {
1564 let json = r#"{ "name": "ov", "stages": [
1568 { "id": "main", "terminal": true } ],
1569 "require": ["main"],
1570 "overlays": [ { "name": "lookup", "trigger": { "is_true": "intent:lookup" },
1571 "stages": [
1572 { "id": "fetch", "resolve": [{ "slot": "balance", "resolver": "bal" }] },
1573 { "id": "ov_done", "terminal": true, "after": ["fetch"] } ] } ] }"#;
1574 let spec: ConversationSpec = serde_json::from_str(json).unwrap();
1575
1576 let err = Conversation::from_spec(spec.clone()).expect_err("unbound overlay resolver");
1578 assert!(matches!(err, ConversationError::Spec(m) if m.contains("bal")));
1579
1580 let registry =
1582 ResolverRegistry::new().with("bal", |_args| async move { Ok(serde_json::json!(42)) });
1583 let convo = Conversation::from_spec_with_resolvers(spec, ®istry).expect("compiles");
1584 assert!(
1585 convo.overlays()[0]
1586 .extractors
1587 .iter()
1588 .any(|e| e.field_state_keys().iter().any(|(_, k)| k == "balance")),
1589 "the overlay carries the resolver extractor for 'balance'"
1590 );
1591 }
1592
1593 #[tokio::test]
1594 async fn resolver_slot_counts_toward_stage_completion_from_json() {
1595 let json = r#"{ "name": "r2", "stages": [
1599 { "id": "check", "resolve": [{ "slot": "avail", "resolver": "lookup" }] },
1600 { "id": "done", "terminal": true, "after": ["check"] } ],
1601 "require": ["done"] }"#;
1602 let spec: ConversationSpec = serde_json::from_str(json).unwrap();
1603 let convo =
1604 Conversation::from_spec_stubbing_resolvers(spec).expect("resolver-only stage compiles");
1605 assert!(
1606 convo.spec().stages[0]
1607 .collect
1608 .contains(&"avail".to_string()),
1609 "declared resolver slot normalized into collect"
1610 );
1611 }
1612
1613 #[test]
1614 fn conversation_spec_schema_is_valid_json_with_expected_shape() {
1615 let schema_str = conversation_spec_schema();
1616 let schema: serde_json::Value =
1617 serde_json::from_str(&schema_str).expect("schema is valid JSON");
1618 assert_eq!(schema["title"], "ConversationSpec");
1621 assert!(
1622 schema["properties"]["stages"].is_object(),
1623 "schema exposes the stages property: {schema}"
1624 );
1625 assert!(
1627 schema["definitions"]["Pred"].is_object() || schema["$defs"]["Pred"].is_object(),
1628 "Pred atom schema is present"
1629 );
1630 }
1631
1632 #[test]
1633 fn conversation_error_serializes_machine_readable() {
1634 let err = Conversation::new("x")
1636 .stage("s")
1637 .commit("pay", Guard::always())
1638 .complete_when(Guard::called_ok("pay"))
1639 .next("done", Guard::called_ok("pay"))
1640 .stage("done")
1641 .terminal()
1642 .compile()
1643 .expect_err("unguarded commit must fail");
1644 let json = serde_json::to_value(&err).expect("error serializes");
1645 assert_eq!(json["kind"], "compile");
1646 assert!(json["message"].is_string());
1647 let errors = json["errors"].as_array().expect("errors array");
1649 assert!(
1650 errors.iter().any(|e| e["kind"] == "unguarded_commit_tool"),
1651 "structured FlowError kinds present: {json}"
1652 );
1653 }
1654
1655 #[test]
1656 fn collect_frame_uses_frame_slot_keys() {
1657 use gemini_adk_rs::frame::{Frame, FrameSpec, SlotSpec};
1658
1659 struct Booking;
1660 impl Frame for Booking {
1661 fn frame() -> FrameSpec {
1662 FrameSpec {
1663 name: "booking".into(),
1664 slots: vec![SlotSpec::new("party_size"), SlotSpec::new("slot")],
1665 }
1666 }
1667 }
1668
1669 let convo = Conversation::new("b")
1670 .stage("collect")
1671 .collect_frame::<Booking>()
1672 .next("done", Guard::captured(["party_size", "slot"]))
1673 .stage("done")
1674 .terminal()
1675 .compile()
1676 .expect("compiles");
1677
1678 let mut mon = convo.monitor(Enforcement::Enforce);
1680 let state = State::new();
1681 assert!(mon.explain(&state).active.contains(&"collect".to_string()));
1682 let _ = state.set("party_size", 2u8);
1683 let _ = state.set("slot", "noon");
1684 mon.on_turn(&state);
1685 assert!(mon.marking().done.contains("collect"));
1687 assert!(mon.marking().done.contains("done"));
1688 }
1689
1690 #[tokio::test]
1691 async fn collect_frame_extractor_fills_and_scores_slots() {
1692 use gemini_adk_rs::frame::{Frame, FrameSpec, SlotRecognizer, SlotSpec};
1693 use gemini_adk_rs::live::TranscriptTurn;
1694
1695 struct Order;
1696 impl Frame for Order {
1697 fn frame() -> FrameSpec {
1698 FrameSpec {
1699 name: "order".into(),
1700 slots: vec![SlotSpec {
1701 recognizer: Some(SlotRecognizer::OneOf(vec![
1702 "pizza".into(),
1703 "salad".into(),
1704 ])),
1705 ..SlotSpec::new("item")
1706 }],
1707 }
1708 }
1709 }
1710
1711 let convo = Conversation::new("o")
1712 .stage("collect")
1713 .collect_frame::<Order>()
1714 .next("done", Guard::captured(["item"]))
1715 .stage("done")
1716 .terminal()
1717 .compile()
1718 .expect("compiles");
1719
1720 assert_eq!(convo.extractors().len(), 1);
1722 let extractor = convo.extractors()[0].clone().into_extractor();
1723
1724 let state = State::new();
1727 let window = vec![TranscriptTurn {
1728 turn_number: 0,
1729 user: "I'd like a large PIZZA".into(),
1730 model: String::new(),
1731 tool_calls: Vec::new(),
1732 timestamp: std::time::Instant::now(),
1733 }];
1734 let out = extractor.extract_with_state(&window, &state).await.unwrap();
1735 assert_eq!(out.get("item").and_then(|v| v.as_str()), Some("pizza"));
1736
1737 let ev = state.evidence("item");
1738 assert_eq!(ev.source.as_deref(), Some("extraction"));
1739 assert!(ev.confidence.unwrap() > 0.0);
1740 }
1741
1742 #[tokio::test]
1743 async fn validate_rejects_out_of_range_recognized_values() {
1744 use gemini_adk_rs::frame::{Frame, FrameSpec, SlotRecognizer, SlotSpec, SlotValidator};
1745 use gemini_adk_rs::live::TranscriptTurn;
1746
1747 struct Party;
1748 impl Frame for Party {
1749 fn frame() -> FrameSpec {
1750 FrameSpec {
1751 name: "party".into(),
1752 slots: vec![SlotSpec {
1753 recognizer: Some(SlotRecognizer::Integer),
1754 validate: Some(SlotValidator::Range {
1755 min: Some(1.0),
1756 max: Some(12.0),
1757 }),
1758 ..SlotSpec::new("party_size")
1759 }],
1760 }
1761 }
1762 }
1763
1764 let convo = Conversation::new("p")
1765 .stage("collect")
1766 .collect_frame::<Party>()
1767 .next("done", Guard::captured(["party_size"]))
1768 .stage("done")
1769 .terminal()
1770 .compile()
1771 .expect("compiles");
1772 let extractor = convo.extractors()[0].clone().into_extractor();
1773
1774 let run = |text: &str| {
1775 let extractor = extractor.clone();
1776 let text = text.to_string();
1777 async move {
1778 let state = State::new();
1779 let window = vec![TranscriptTurn {
1780 turn_number: 0,
1781 user: text,
1782 model: String::new(),
1783 tool_calls: Vec::new(),
1784 timestamp: std::time::Instant::now(),
1785 }];
1786 let out = extractor.extract_with_state(&window, &state).await.unwrap();
1787 out.get("party_size").cloned()
1788 }
1789 };
1790
1791 assert_eq!(run("a table for 4").await, Some(serde_json::json!(4)));
1793 assert_eq!(run("a table for 40").await, None);
1794 }
1795
1796 #[tokio::test]
1797 async fn resolve_slot_fills_from_async_fetch() {
1798 use gemini_adk_rs::live::TranscriptTurn;
1799
1800 let convo = Conversation::new("c")
1801 .stage("check")
1802 .resolve_slot("availability", ["party_size"], None, |args| async move {
1803 let n = args
1805 .get("party_size")
1806 .and_then(serde_json::Value::as_i64)
1807 .unwrap_or(0);
1808 Ok(serde_json::json!(n <= 8))
1809 })
1810 .next("done", Guard::is_set("availability"))
1811 .stage("done")
1812 .terminal()
1813 .compile()
1814 .expect("compiles");
1815
1816 assert_eq!(convo.extractors().len(), 1);
1818 let extractor = convo.extractors()[0].clone().into_extractor();
1819
1820 let state = State::new();
1821 let _ = state.set("party_size", 4i64);
1822 let window = vec![TranscriptTurn {
1823 turn_number: 0,
1824 user: "any".into(),
1825 model: String::new(),
1826 tool_calls: Vec::new(),
1827 timestamp: std::time::Instant::now(),
1828 }];
1829 let out = extractor.extract_with_state(&window, &state).await.unwrap();
1830 assert_eq!(out.get("availability"), Some(&serde_json::json!(true)));
1831 }
1832
1833 #[test]
1834 fn converse_registers_flow_and_extractors() {
1835 use gemini_adk_rs::frame::{Frame, FrameSpec, SlotRecognizer, SlotSpec};
1837
1838 struct Order;
1839 impl Frame for Order {
1840 fn frame() -> FrameSpec {
1841 FrameSpec {
1842 name: "order".into(),
1843 slots: vec![SlotSpec {
1844 recognizer: Some(SlotRecognizer::OneOf(vec!["pizza".into()])),
1845 ..SlotSpec::new("item")
1846 }],
1847 }
1848 }
1849 }
1850 let convo = Conversation::new("o")
1851 .stage("collect")
1852 .collect_frame::<Order>()
1853 .next("done", Guard::captured(["item"]))
1854 .stage("done")
1855 .terminal()
1856 .compile()
1857 .expect("compiles");
1858
1859 let _live = crate::live::Live::builder().converse(&convo);
1861 }
1862
1863 #[test]
1864 fn overlay_suspends_main_then_resumes_previous() {
1865 let convo = Conversation::new("support")
1868 .stage("a")
1869 .next("b", Guard::is_true("a_done"))
1870 .stage("b")
1871 .terminal()
1872 .overlay("faq")
1873 .trigger(Guard::is_true("intent:faq"))
1874 .stage("answer")
1876 .complete_when(Guard::is_true("faq_answered"))
1877 .next("faq_end", Guard::is_true("faq_answered"))
1878 .stage("faq_end")
1879 .terminal()
1880 .resume(Resume::Previous)
1881 .end_overlay()
1882 .compile()
1883 .expect("compiles");
1884
1885 assert_eq!(convo.overlays().len(), 1);
1886 let mut stack = convo.stack(Enforcement::Enforce);
1887 let state = State::new();
1888
1889 assert!(stack.explain(&state).active.contains(&"a".to_string()));
1891 assert!(stack.active_overlay().is_none());
1892
1893 let _ = state.set("intent:faq", true);
1895 stack.on_turn(&state);
1896 assert_eq!(stack.active_overlay(), Some("faq"));
1897
1898 let _ = state.set("faq_answered", true);
1900 let _ = state.set("intent:faq", false);
1901 stack.on_turn(&state);
1902 assert!(stack.active_overlay().is_none());
1903
1904 assert!(stack.explain(&state).active.contains(&"a".to_string()));
1906
1907 let _ = state.set("a_done", true);
1909 stack.on_turn(&state);
1910 assert!(stack.current().marking().done.contains("a"));
1911 }
1912
1913 #[test]
1914 fn overlay_spec_round_trips_through_json() {
1915 let spec = Conversation::new("s")
1916 .stage("main")
1917 .terminal()
1918 .overlay("cancel")
1919 .trigger(Guard::is_true("intent:cancel"))
1920 .stage("confirm")
1921 .terminal()
1922 .resume(Resume::Terminate)
1923 .end_overlay()
1924 .into_spec();
1925 let json = serde_json::to_string(&spec).unwrap();
1926 let back: ConversationSpec = serde_json::from_str(&json).unwrap();
1927 assert_eq!(back.overlays.len(), 1);
1928 assert_eq!(back.overlays[0].resume, Resume::Terminate);
1929 assert!(Conversation::from_spec(back).is_ok());
1931 }
1932
1933 #[tokio::test]
1934 async fn safety_policy_terminates_on_intent() {
1935 use crate::policy::Policy;
1936 use crate::simulation::Sim;
1937
1938 let convo = Conversation::new("support")
1939 .policy(Policy::safety_handoff(["self_harm", "abuse"]))
1940 .policy(Policy::redact(["card_number"]))
1941 .stage("triage")
1942 .next("resolve", Guard::is_true("triaged"))
1943 .stage("resolve")
1944 .terminal()
1945 .require(["resolve"])
1946 .compile()
1947 .expect("compiles");
1948
1949 assert!(convo.redacted_fields().contains("card_number"));
1951 assert!(convo.overlays().iter().any(|o| o.name == "safety"));
1953
1954 let mut sim = Sim::new(&convo, Enforcement::Enforce);
1955 assert!(sim.active().contains(&"triage".to_string()));
1956 assert!(!sim.is_complete());
1957
1958 sim.set("intent:abuse", true);
1960 sim.turn();
1961 assert!(sim.is_complete());
1962 }
1963
1964 #[tokio::test]
1965 async fn repair_reprompts_then_escalates_to_handoff() {
1966 use crate::simulation::Sim;
1967
1968 let convo = Conversation::new("support")
1971 .stage("collect")
1972 .complete_when(Guard::is_true("info"))
1973 .next("done", Guard::is_true("info"))
1974 .repair(RepairPolicy::new(2, 3).escalate_to("handoff"))
1975 .stage("done")
1976 .terminal()
1977 .stage("handoff")
1979 .complete_when(Guard::is_true("handoff_complete"))
1980 .compile()
1981 .expect("compiles");
1982
1983 let mut sim = Sim::new(&convo, Enforcement::Enforce);
1984 assert!(sim.active().contains(&"collect".to_string()));
1985
1986 sim.turn();
1988 assert_eq!(sim.slot::<bool>("repair:collect:reprompt"), None);
1989 sim.turn();
1991 assert_eq!(sim.slot::<bool>("repair:collect:reprompt"), Some(true));
1992 assert!(sim.active().contains(&"collect".to_string()));
1993 sim.turn();
1995 assert_eq!(sim.slot::<bool>("repair:collect:escalate"), Some(true));
1996 assert!(sim.active().contains(&"handoff".to_string()));
1997 assert!(!sim.active().contains(&"collect".to_string()));
1998 }
1999
2000 #[tokio::test]
2001 async fn repair_signal_clears_when_stage_satisfied() {
2002 use crate::simulation::Sim;
2003
2004 let convo = Conversation::new("s")
2005 .stage("collect")
2006 .complete_when(Guard::is_true("info"))
2007 .next("done", Guard::is_true("info"))
2008 .repair(RepairPolicy::new(1, 9))
2009 .stage("done")
2010 .terminal()
2011 .require(["done"])
2012 .compile()
2013 .expect("compiles");
2014
2015 let mut sim = Sim::new(&convo, Enforcement::Enforce);
2016 sim.turn(); assert_eq!(sim.slot::<bool>("repair:collect:reprompt"), Some(true));
2018
2019 sim.set("info", true);
2022 sim.turn();
2023 sim.turn();
2024 assert_eq!(sim.slot::<bool>("repair:collect:reprompt"), Some(false));
2025 assert!(sim.is_complete());
2026 }
2027
2028 #[test]
2029 fn rejects_transition_to_unknown_stage() {
2030 let err = Conversation::new("x")
2031 .stage("a")
2032 .next("ghost", Guard::always())
2033 .stage("b")
2034 .terminal()
2035 .compile()
2036 .expect_err("unknown target must fail");
2037 assert!(matches!(err, ConversationError::Spec(_)));
2038 }
2039
2040 #[test]
2041 fn rejects_unguarded_commit_via_flow_compile() {
2042 let err = Conversation::new("x")
2044 .stage("s")
2045 .commit("pay", Guard::always())
2046 .complete_when(Guard::called_ok("pay"))
2047 .next("done", Guard::called_ok("pay"))
2048 .stage("done")
2049 .terminal()
2050 .compile()
2051 .expect_err("unguarded commit must fail");
2052 assert!(matches!(err, ConversationError::Compile(_)));
2053 }
2054}