1use std::collections::BTreeMap;
35use std::sync::Arc;
36use std::time::Instant;
37
38use serde::{Deserialize, Serialize};
39use serde_json::Value;
40
41use gemini_adk_rs::decision::{Decision, Question};
42use gemini_adk_rs::flow::{DECISION_TURN_KEY, Enforcement, FlowExplanation};
43use gemini_adk_rs::live::{TranscriptTurn, TurnExtractor};
44use gemini_adk_rs::state::State;
45
46use crate::conversation::{CompiledConversation, FlowStack};
47
48struct BoundExtractor {
49 extractor: Arc<dyn TurnExtractor>,
50 fields: Vec<(String, String)>,
52}
53
54pub struct Sim {
56 stack: FlowStack,
57 extractors: Vec<BoundExtractor>,
58 state: State,
59 turn_no: u32,
60 pending_tools: Vec<(String, u32)>,
62 decisions: BTreeMap<String, Decision>,
64 caller_turns: u32,
66}
67
68impl Sim {
69 pub fn new(convo: &CompiledConversation, mode: Enforcement) -> Self {
71 let extractors = convo
72 .all_extractors()
73 .into_iter()
74 .map(|e| BoundExtractor {
75 fields: e.field_state_keys(),
76 extractor: e.into_extractor(),
77 })
78 .collect();
79 Self {
80 stack: convo.stack(mode),
81 extractors,
82 state: State::new(),
83 turn_no: 0,
84 pending_tools: Vec::new(),
85 decisions: BTreeMap::new(),
86 caller_turns: 0,
87 }
88 }
89
90 pub fn set(&self, key: impl Into<String>, value: impl Serialize) -> &Self {
93 let _ = self.state.set(key, value);
94 self
95 }
96
97 pub async fn user(&mut self, utterance: &str) -> &mut Self {
100 self.next_caller_turn();
101 let window = [TranscriptTurn {
102 turn_number: self.turn_no,
103 user: utterance.to_string(),
104 model: String::new(),
105 tool_calls: Vec::new(),
106 timestamp: Instant::now(),
107 }];
108 for bound in &self.extractors {
109 if let Ok(Value::Object(obj)) = bound
110 .extractor
111 .extract_with_state(&window, &self.state)
112 .await
113 {
114 for (name, key) in &bound.fields {
115 if let Some(v) = obj.get(name)
116 && !v.is_null()
117 {
118 let _ = self.state.set(key.clone(), v.clone());
119 }
120 }
121 }
122 }
123 self.advance();
124 self
125 }
126
127 pub fn turn(&mut self) -> &mut Self {
129 self.advance();
130 self
131 }
132
133 pub fn tool_ok(&mut self, tool: &str) -> &mut Self {
136 self.stack.on_tool_ok(tool, &self.state);
137 self.advance();
138 self
139 }
140
141 pub fn tool_failed(&mut self, tool: &str) -> &mut Self {
144 self.stack.observe_tool(tool, false, &self.state);
145 self
146 }
147
148 pub fn interrupt(&mut self) -> &mut Self {
151 self.stack.on_interrupted(&self.state);
152 self
153 }
154
155 pub fn schedule_tool(&mut self, tool: impl Into<String>, after: u32) -> &mut Self {
157 self.pending_tools
158 .push((tool.into(), self.turn_no + after.max(1)));
159 self
160 }
161
162 #[must_use]
166 pub fn with_decisions(mut self, bank: BTreeMap<String, Decision>) -> Self {
167 self.decisions = bank;
168 self
169 }
170
171 pub fn decide(&mut self, answers: &BTreeMap<String, Value>) -> &mut Self {
178 let turn = self.next_caller_turn();
179 for (id, answer) in answers {
180 match self.decisions.get(id) {
181 Some(decision) => decision.record_scripted(id, answer, &self.state, &turn),
182 None => Decision::new(Question::boolean("")).record_scripted(
183 id,
184 answer,
185 &self.state,
186 &turn,
187 ),
188 }
189 }
190 self.stack.on_decisions(&self.state);
191 self
192 }
193
194 fn next_caller_turn(&mut self) -> String {
196 self.caller_turns += 1;
197 let turn = format!("scenario:{}", self.caller_turns);
198 let _ = self.state.set(DECISION_TURN_KEY, turn.clone());
199 turn
200 }
201
202 fn advance(&mut self) {
203 self.turn_no += 1;
204 let due: Vec<String> = self
206 .pending_tools
207 .iter()
208 .filter(|(_, at)| *at <= self.turn_no)
209 .map(|(t, _)| t.clone())
210 .collect();
211 self.pending_tools.retain(|(_, at)| *at > self.turn_no);
212 for tool in due {
213 self.stack.on_tool_ok(&tool, &self.state);
214 }
215 self.stack.on_turn(&self.state);
216 }
217
218 pub async fn apply(&mut self, step: &SimStep) -> Result<(), String> {
223 match step {
224 SimStep::User(text) => {
225 self.user(text).await;
226 }
227 SimStep::Set { key, value } => {
228 self.set(key.clone(), value.clone());
229 }
230 SimStep::Remove { key } => {
231 let _ = self.state.remove(key);
232 }
233 SimStep::Decide(answers) => {
234 self.decide(answers);
235 }
236 SimStep::ToolOk(tool) => {
237 self.tool_ok(tool);
238 }
239 SimStep::ToolFailed(tool) => {
240 self.tool_failed(tool);
241 }
242 SimStep::Interrupt => {
243 self.interrupt();
244 }
245 SimStep::ToolResult { tool, ok } => {
246 self.stack.observe_tool(tool, *ok, &self.state);
247 }
248 SimStep::ScheduleTool { tool, after } => {
249 self.schedule_tool(tool.clone(), *after);
250 }
251 SimStep::Turn => {
252 self.turn();
253 }
254 SimStep::ExpectActive(expected) => {
255 let active = self.active();
256 for e in expected {
257 if !active.contains(e) {
258 return Err(format!("expected active '{e}', got {active:?}"));
259 }
260 }
261 }
262 SimStep::ExpectDenied(tool) => {
263 if self.allowed(tool) {
264 return Err(format!("expected '{tool}' denied, but it was admitted"));
265 }
266 }
267 SimStep::ExpectAllowed(tool) => {
268 if !self.allowed(tool) {
269 let why = self.denied().get(tool).cloned().unwrap_or_default();
270 return Err(format!("expected '{tool}' allowed, but denied: {why}"));
271 }
272 }
273 SimStep::ExpectSlot { key, value } => {
274 let got = self.state().get_raw(key);
275 if got.as_ref() != Some(value) {
276 return Err(format!("expected slot '{key}' = {value}, got {got:?}"));
277 }
278 }
279 SimStep::ExpectComplete => {
280 if !self.is_complete() {
281 return Err("expected conversation complete".into());
282 }
283 }
284 }
285 Ok(())
286 }
287
288 pub fn active(&self) -> Vec<String> {
290 self.stack.explain(&self.state).active
291 }
292
293 pub fn active_overlay(&self) -> Option<&str> {
295 self.stack.active_overlay()
296 }
297
298 pub fn postures(&self) -> Vec<String> {
305 self.stack.active_postures(&self.state)
306 }
307
308 pub fn is_terminated(&self) -> bool {
311 self.stack.is_terminated()
312 }
313
314 pub fn allowed(&self, tool: &str) -> bool {
316 self.stack.admits_tool(tool, &self.state).is_ok()
317 }
318
319 pub fn denied(&self) -> BTreeMap<String, String> {
321 self.stack.explain(&self.state).blocked_tools
322 }
323
324 pub fn is_complete(&self) -> bool {
326 self.stack.is_complete()
327 }
328
329 pub fn slot<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
331 self.state.get(key)
332 }
333
334 pub fn explain(&self) -> FlowExplanation {
336 self.stack.explain(&self.state)
337 }
338
339 pub fn state(&self) -> &State {
341 &self.state
342 }
343}
344
345#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
347#[serde(rename_all = "snake_case")]
348pub enum SimStep {
349 User(String),
351 Set {
353 key: String,
355 value: Value,
357 },
358 Remove {
360 key: String,
362 },
363 ToolOk(String),
365 ToolFailed(String),
368 Interrupt,
371 ToolResult {
376 tool: String,
378 ok: bool,
380 },
381 ScheduleTool {
383 tool: String,
385 after: u32,
387 },
388 Decide(BTreeMap<String, Value>),
392 Turn,
394 ExpectActive(Vec<String>),
396 ExpectDenied(String),
398 ExpectAllowed(String),
400 ExpectSlot {
402 key: String,
404 value: Value,
406 },
407 ExpectComplete,
409}
410
411#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
414pub struct Scenario {
415 pub name: String,
417 pub steps: Vec<SimStep>,
419}
420
421const RUNTIME_PREFIXES: &[&str] = &[
424 "session:",
425 "flow:",
426 "derived:",
427 "repair:",
428 "correction:",
429 "state_meta:",
430 "idempotency:",
431 "compensated:",
432 "verbatim:",
433 "turn:",
434 "bg:",
435 "decision:",
436];
437
438fn runtime_owned(key: &str) -> bool {
439 RUNTIME_PREFIXES.iter().any(|p| key.starts_with(p))
440}
441
442impl Scenario {
443 pub fn from_journal(
468 name: impl Into<String>,
469 journal: &[gemini_adk_rs::state::StateMutation],
470 ) -> Self {
471 use gemini_adk_rs::flow::{TOOL_CALL_KEY, TOOL_DENIED_KEY, TOOL_RESULT_KEY};
472
473 let tool_of = |v: &Option<Value>| {
474 v.as_ref()
475 .and_then(|v| v["tool"].as_str())
476 .map(str::to_string)
477 };
478 let mut steps = Vec::new();
479 let mut pending: Vec<(String, Option<Value>)> = Vec::new();
482 let flush = |pending: &mut Vec<(String, Option<Value>)>, steps: &mut Vec<SimStep>| {
483 for (key, value) in pending.drain(..) {
484 steps.push(match value {
485 Some(value) => SimStep::Set { key, value },
486 None => SimStep::Remove { key },
487 });
488 }
489 };
490 let mut ordered: Vec<&gemini_adk_rs::state::StateMutation> = journal.iter().collect();
491 ordered.sort_by_key(|m| m.sequence);
492 let mut decided: BTreeMap<String, Value> = BTreeMap::new();
494 let mut decided_turn: Option<String> = None;
495 for m in ordered {
496 if let Some(question) = m.key.strip_prefix(gemini_adk_rs::flow::DECISION_PREFIX)
497 && question != "turn"
498 && let Some(record) = m.new.clone().and_then(|v| {
499 serde_json::from_value::<gemini_adk_rs::flow::DecisionRecord>(v).ok()
500 })
501 {
502 use gemini_adk_rs::flow::Outcome;
503 if decided_turn.as_ref().is_some_and(|t| *t != record.turn) && !decided.is_empty() {
504 flush(&mut pending, &mut steps);
505 steps.push(SimStep::Decide(std::mem::take(&mut decided)));
506 }
507 decided_turn = Some(record.turn.clone());
508 let answer = match record.outcome {
509 Outcome::Yes => Value::Bool(true),
510 Outcome::No => Value::Bool(false),
511 Outcome::None => Value::String(gemini_adk_rs::decision::NONE_OPTION.into()),
512 Outcome::Chosen | Outcome::Scored => record.value.clone(),
513 Outcome::Unsure => Value::Null,
514 };
515 decided.insert(question.to_string(), answer);
516 continue;
517 }
518 let boundary = !runtime_owned(&m.key)
521 || [
522 "flow:active",
523 TOOL_CALL_KEY,
524 TOOL_DENIED_KEY,
525 TOOL_RESULT_KEY,
526 ]
527 .contains(&m.key.as_str());
528 if boundary && !decided.is_empty() {
529 flush(&mut pending, &mut steps);
530 steps.push(SimStep::Decide(std::mem::take(&mut decided)));
531 }
532 match m.key.as_str() {
533 "flow:active" => {
534 flush(&mut pending, &mut steps);
535 steps.push(SimStep::Turn);
536 let active: Vec<String> = m
537 .new
538 .clone()
539 .and_then(|v| serde_json::from_value(v).ok())
540 .unwrap_or_default();
541 steps.push(SimStep::ExpectActive(active));
542 }
543 TOOL_CALL_KEY => {
544 if let Some(tool) = tool_of(&m.new) {
545 flush(&mut pending, &mut steps);
546 steps.push(SimStep::ExpectAllowed(tool));
547 }
548 }
549 TOOL_DENIED_KEY => {
550 if let Some(tool) = tool_of(&m.new) {
551 flush(&mut pending, &mut steps);
552 steps.push(SimStep::ExpectDenied(tool));
553 }
554 }
555 TOOL_RESULT_KEY => {
556 if let Some(tool) = tool_of(&m.new) {
557 flush(&mut pending, &mut steps);
558 let ok = m.new.as_ref().is_some_and(|v| v["ok"] == true);
559 steps.push(SimStep::ToolResult { tool, ok });
560 }
561 }
562 key if runtime_owned(key) => {}
563 key => {
564 let value = m.new.clone();
565 match pending.iter_mut().find(|(k, _)| k == key) {
567 Some(slot) => slot.1 = value,
568 None => pending.push((key.to_string(), value)),
569 }
570 }
571 }
572 }
573 flush(&mut pending, &mut steps);
574 if !decided.is_empty() {
575 steps.push(SimStep::Decide(decided));
576 }
577 Self {
578 name: name.into(),
579 steps,
580 }
581 }
582
583 pub async fn run(&self, convo: &CompiledConversation, mode: Enforcement) -> Result<(), String> {
586 self.run_in(Sim::new(convo, mode)).await
587 }
588
589 pub async fn run_in(&self, mut sim: Sim) -> Result<(), String> {
592 for (i, step) in self.steps.iter().enumerate() {
593 sim.apply(step)
594 .await
595 .map_err(|msg| format!("[{}] step {i} ({step:?}): {msg}", self.name))?;
596 }
597 Ok(())
598 }
599}
600
601#[cfg(test)]
602mod tests {
603 use super::*;
604 use crate::conversation::Conversation;
605 use gemini_adk_rs::flow::Guard;
606 use gemini_adk_rs::frame::{Frame, FrameSpec, SlotRecognizer, SlotSpec};
607
608 struct Booking;
609 impl Frame for Booking {
610 fn frame() -> FrameSpec {
611 FrameSpec {
612 name: "booking".into(),
613 slots: vec![SlotSpec {
614 recognizer: Some(SlotRecognizer::IntegerNear(vec!["people".into()])),
615 ..SlotSpec::new("party_size")
616 }],
617 }
618 }
619 }
620
621 fn booking() -> CompiledConversation {
622 Conversation::new("booking")
623 .stage("collect")
624 .collect_frame::<Booking>()
625 .next("confirm", Guard::captured(["party_size"]))
626 .stage("confirm")
627 .commit("book", Guard::is_true("user_confirmed"))
628 .next("done", Guard::called_ok("book"))
629 .stage("done")
630 .terminal()
631 .require(["done"])
632 .compile()
633 .expect("compiles")
634 }
635
636 #[tokio::test]
637 async fn fake_user_fills_slots_and_gates_commit() {
638 let convo = booking();
639 let mut sim = Sim::new(&convo, Enforcement::Enforce);
640
641 assert!(sim.active().contains(&"collect".to_string()));
642 assert!(!sim.allowed("book"));
643
644 sim.user("a table for 4 people").await;
646 assert_eq!(sim.slot::<u32>("party_size"), Some(4));
647 assert!(sim.active().contains(&"confirm".to_string()));
648
649 assert!(!sim.allowed("book"));
651 sim.set("user_confirmed", true);
652 sim.turn();
653 assert!(sim.allowed("book"));
654
655 sim.tool_ok("book");
656 assert!(sim.is_complete());
657 }
658
659 #[tokio::test]
660 async fn scenario_runs_and_round_trips() {
661 let scenario = Scenario {
662 name: "happy_path".into(),
663 steps: vec![
664 SimStep::ExpectActive(vec!["collect".into()]),
665 SimStep::ExpectDenied("book".into()),
666 SimStep::User("party of 4 people".into()),
667 SimStep::ExpectSlot {
668 key: "party_size".into(),
669 value: serde_json::json!(4),
670 },
671 SimStep::ExpectActive(vec!["confirm".into()]),
672 SimStep::Set {
673 key: "user_confirmed".into(),
674 value: serde_json::json!(true),
675 },
676 SimStep::Turn,
677 SimStep::ExpectAllowed("book".into()),
678 SimStep::ToolOk("book".into()),
679 SimStep::ExpectComplete,
680 ],
681 };
682
683 scenario
684 .run(&booking(), Enforcement::Enforce)
685 .await
686 .expect("scenario passes");
687
688 let json = serde_json::to_string(&scenario).unwrap();
690 let back: Scenario = serde_json::from_str(&json).unwrap();
691 back.run(&booking(), Enforcement::Enforce)
692 .await
693 .expect("round-tripped scenario passes");
694 }
695
696 #[tokio::test]
697 async fn scenario_reports_failed_expectation() {
698 let scenario = Scenario {
699 name: "bad".into(),
700 steps: vec![SimStep::ExpectComplete], };
702 let err = scenario
703 .run(&booking(), Enforcement::Enforce)
704 .await
705 .unwrap_err();
706 assert!(err.contains("expected conversation complete"));
707 }
708
709 #[tokio::test]
710 async fn decided_answers_gate_a_commit_for_their_turn_only() {
711 let convo = Conversation::new("booking")
712 .stage("offer")
713 .commit("book", Guard::decided("confirmed"))
714 .next("done", Guard::called_ok("book"))
715 .stage("done")
716 .terminal()
717 .require(["done"])
718 .compile()
719 .expect("compiles");
720 let bank = BTreeMap::from([
721 (
722 "confirmed".to_string(),
723 Decision::new(Question::boolean("The caller agreed to book.")),
724 ),
725 (
726 "slot_pick".to_string(),
727 Decision::new(Question::choice(
728 "Which time did the caller pick?",
729 [("x", "x")],
730 ))
731 .options_from("offered")
732 .or_none("No offered time was picked.")
733 .writes("slot"),
734 ),
735 ]);
736 let mut sim = Sim::new(&convo, Enforcement::Enforce).with_decisions(bank);
737 sim.set(
738 "offered",
739 serde_json::json!([{ "id": "9am", "start": "09:00" }, { "id": "2pm", "start": "14:00" }]),
740 );
741 assert!(!sim.allowed("book"));
742
743 let answers = |pairs: &[(&str, Value)]| {
744 pairs
745 .iter()
746 .map(|(k, v)| ((*k).to_string(), v.clone()))
747 .collect::<BTreeMap<_, _>>()
748 };
749 sim.decide(&answers(&[
750 ("confirmed", serde_json::json!(true)),
751 ("slot_pick", serde_json::json!("2pm")),
752 ]));
753 assert!(sim.allowed("book"));
754 assert_eq!(
755 sim.slot::<Value>("slot"),
756 Some(serde_json::json!({ "id": "2pm", "start": "14:00" })),
757 "a pick writes the offered option whole"
758 );
759
760 sim.user("actually hold on").await;
762 assert!(!sim.allowed("book"));
763 sim.decide(&answers(&[("confirmed", Value::Null)]));
764 assert!(!sim.allowed("book"), "unsure satisfies nothing");
765 }
766
767 #[test]
768 fn journal_decisions_replay_as_decide_steps() {
769 use gemini_adk_rs::flow::{DecisionRecord, Outcome, TOOL_CALL_KEY, decision_key};
770 use gemini_adk_rs::state::MemoryJournalSink;
771 let journal = std::sync::Arc::new(MemoryJournalSink::new());
772 let state = State::new().with_journal_sink(journal.clone());
773 let record = |q: &str, outcome, value: Value| {
774 let record = DecisionRecord {
775 outcome,
776 value,
777 confidence: Some(0.97),
778 turn: "3:ab".into(),
779 };
780 let _ = state.set(decision_key(q), serde_json::to_value(record).unwrap());
781 };
782 let _ = state.set(DECISION_TURN_KEY, "3:ab");
783 record("confirmed", Outcome::Yes, Value::Null);
784 record("slot_pick", Outcome::None, Value::Null);
785 record("anger", Outcome::Scored, serde_json::json!(1.5));
786 let _ = state.set(TOOL_CALL_KEY, serde_json::json!({ "tool": "book" }));
787
788 let scenario = Scenario::from_journal("incident", &journal.entries());
789 assert_eq!(
790 serde_json::to_value(&scenario.steps).unwrap(),
791 serde_json::json!([
792 { "decide": { "anger": 1.5, "confirmed": true, "slot_pick": "none_of_these" } },
793 { "expect_allowed": "book" },
794 ])
795 );
796 }
797
798 #[tokio::test]
799 async fn tool_latency_resolves_after_delay() {
800 let convo = booking();
801 let mut sim = Sim::new(&convo, Enforcement::Enforce);
802 sim.user("4 people").await;
803 sim.set("user_confirmed", true);
804 sim.turn();
805 sim.schedule_tool("book", 2);
807 assert!(!sim.is_complete());
808 sim.turn();
809 assert!(!sim.is_complete());
810 sim.turn();
811 assert!(sim.is_complete());
812 }
813}