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::flow::{Enforcement, FlowExplanation};
42use gemini_adk_rs::live::{TranscriptTurn, TurnExtractor};
43use gemini_adk_rs::state::State;
44
45use crate::conversation::{CompiledConversation, FlowStack};
46
47struct BoundExtractor {
48 extractor: Arc<dyn TurnExtractor>,
49 fields: Vec<(String, String)>,
51}
52
53pub struct Sim {
55 stack: FlowStack,
56 extractors: Vec<BoundExtractor>,
57 state: State,
58 turn_no: u32,
59 pending_tools: Vec<(String, u32)>,
61}
62
63impl Sim {
64 pub fn new(convo: &CompiledConversation, mode: Enforcement) -> Self {
66 let extractors = convo
67 .all_extractors()
68 .into_iter()
69 .map(|e| BoundExtractor {
70 fields: e.field_state_keys(),
71 extractor: e.into_extractor(),
72 })
73 .collect();
74 Self {
75 stack: convo.stack(mode),
76 extractors,
77 state: State::new(),
78 turn_no: 0,
79 pending_tools: Vec::new(),
80 }
81 }
82
83 pub fn set(&self, key: impl Into<String>, value: impl Serialize) -> &Self {
86 let _ = self.state.set(key, value);
87 self
88 }
89
90 pub async fn user(&mut self, utterance: &str) -> &mut Self {
93 let window = [TranscriptTurn {
94 turn_number: self.turn_no,
95 user: utterance.to_string(),
96 model: String::new(),
97 tool_calls: Vec::new(),
98 timestamp: Instant::now(),
99 }];
100 for bound in &self.extractors {
101 if let Ok(Value::Object(obj)) = bound
102 .extractor
103 .extract_with_state(&window, &self.state)
104 .await
105 {
106 for (name, key) in &bound.fields {
107 if let Some(v) = obj.get(name)
108 && !v.is_null()
109 {
110 let _ = self.state.set(key.clone(), v.clone());
111 }
112 }
113 }
114 }
115 self.advance();
116 self
117 }
118
119 pub fn turn(&mut self) -> &mut Self {
121 self.advance();
122 self
123 }
124
125 pub fn tool_ok(&mut self, tool: &str) -> &mut Self {
128 self.stack.on_tool_ok(tool, &self.state);
129 self.advance();
130 self
131 }
132
133 pub fn tool_failed(&mut self, tool: &str) -> &mut Self {
136 self.stack.observe_tool(tool, false, &self.state);
137 self
138 }
139
140 pub fn interrupt(&mut self) -> &mut Self {
143 self.stack.on_interrupted(&self.state);
144 self
145 }
146
147 pub fn schedule_tool(&mut self, tool: impl Into<String>, after: u32) -> &mut Self {
149 self.pending_tools
150 .push((tool.into(), self.turn_no + after.max(1)));
151 self
152 }
153
154 fn advance(&mut self) {
155 self.turn_no += 1;
156 let due: Vec<String> = self
158 .pending_tools
159 .iter()
160 .filter(|(_, at)| *at <= self.turn_no)
161 .map(|(t, _)| t.clone())
162 .collect();
163 self.pending_tools.retain(|(_, at)| *at > self.turn_no);
164 for tool in due {
165 self.stack.on_tool_ok(&tool, &self.state);
166 }
167 self.stack.on_turn(&self.state);
168 }
169
170 pub async fn apply(&mut self, step: &SimStep) -> Result<(), String> {
175 match step {
176 SimStep::User(text) => {
177 self.user(text).await;
178 }
179 SimStep::Set { key, value } => {
180 self.set(key.clone(), value.clone());
181 }
182 SimStep::Remove { key } => {
183 let _ = self.state.remove(key);
184 }
185 SimStep::ToolOk(tool) => {
186 self.tool_ok(tool);
187 }
188 SimStep::ToolFailed(tool) => {
189 self.tool_failed(tool);
190 }
191 SimStep::Interrupt => {
192 self.interrupt();
193 }
194 SimStep::ToolResult { tool, ok } => {
195 self.stack.observe_tool(tool, *ok, &self.state);
196 }
197 SimStep::ScheduleTool { tool, after } => {
198 self.schedule_tool(tool.clone(), *after);
199 }
200 SimStep::Turn => {
201 self.turn();
202 }
203 SimStep::ExpectActive(expected) => {
204 let active = self.active();
205 for e in expected {
206 if !active.contains(e) {
207 return Err(format!("expected active '{e}', got {active:?}"));
208 }
209 }
210 }
211 SimStep::ExpectDenied(tool) => {
212 if self.allowed(tool) {
213 return Err(format!("expected '{tool}' denied, but it was admitted"));
214 }
215 }
216 SimStep::ExpectAllowed(tool) => {
217 if !self.allowed(tool) {
218 let why = self.denied().get(tool).cloned().unwrap_or_default();
219 return Err(format!("expected '{tool}' allowed, but denied: {why}"));
220 }
221 }
222 SimStep::ExpectSlot { key, value } => {
223 let got = self.state().get_raw(key);
224 if got.as_ref() != Some(value) {
225 return Err(format!("expected slot '{key}' = {value}, got {got:?}"));
226 }
227 }
228 SimStep::ExpectComplete => {
229 if !self.is_complete() {
230 return Err("expected conversation complete".into());
231 }
232 }
233 }
234 Ok(())
235 }
236
237 pub fn active(&self) -> Vec<String> {
239 self.stack.explain(&self.state).active
240 }
241
242 pub fn active_overlay(&self) -> Option<&str> {
244 self.stack.active_overlay()
245 }
246
247 pub fn postures(&self) -> Vec<String> {
254 self.stack.active_postures(&self.state)
255 }
256
257 pub fn is_terminated(&self) -> bool {
260 self.stack.is_terminated()
261 }
262
263 pub fn allowed(&self, tool: &str) -> bool {
265 self.stack.admits_tool(tool, &self.state).is_ok()
266 }
267
268 pub fn denied(&self) -> BTreeMap<String, String> {
270 self.stack.explain(&self.state).blocked_tools
271 }
272
273 pub fn is_complete(&self) -> bool {
275 self.stack.is_complete()
276 }
277
278 pub fn slot<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
280 self.state.get(key)
281 }
282
283 pub fn explain(&self) -> FlowExplanation {
285 self.stack.explain(&self.state)
286 }
287
288 pub fn state(&self) -> &State {
290 &self.state
291 }
292}
293
294#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
296#[serde(rename_all = "snake_case")]
297pub enum SimStep {
298 User(String),
300 Set {
302 key: String,
304 value: Value,
306 },
307 Remove {
309 key: String,
311 },
312 ToolOk(String),
314 ToolFailed(String),
317 Interrupt,
320 ToolResult {
325 tool: String,
327 ok: bool,
329 },
330 ScheduleTool {
332 tool: String,
334 after: u32,
336 },
337 Turn,
339 ExpectActive(Vec<String>),
341 ExpectDenied(String),
343 ExpectAllowed(String),
345 ExpectSlot {
347 key: String,
349 value: Value,
351 },
352 ExpectComplete,
354}
355
356#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
359pub struct Scenario {
360 pub name: String,
362 pub steps: Vec<SimStep>,
364}
365
366const RUNTIME_PREFIXES: &[&str] = &[
369 "session:",
370 "flow:",
371 "derived:",
372 "repair:",
373 "correction:",
374 "state_meta:",
375 "idempotency:",
376 "compensated:",
377 "verbatim:",
378 "turn:",
379 "bg:",
380];
381
382fn runtime_owned(key: &str) -> bool {
383 RUNTIME_PREFIXES.iter().any(|p| key.starts_with(p))
384}
385
386impl Scenario {
387 pub fn from_journal(
412 name: impl Into<String>,
413 journal: &[gemini_adk_rs::state::StateMutation],
414 ) -> Self {
415 use gemini_adk_rs::flow::{TOOL_CALL_KEY, TOOL_DENIED_KEY, TOOL_RESULT_KEY};
416
417 let tool_of = |v: &Option<Value>| {
418 v.as_ref()
419 .and_then(|v| v["tool"].as_str())
420 .map(str::to_string)
421 };
422 let mut steps = Vec::new();
423 let mut pending: Vec<(String, Option<Value>)> = Vec::new();
426 let flush = |pending: &mut Vec<(String, Option<Value>)>, steps: &mut Vec<SimStep>| {
427 for (key, value) in pending.drain(..) {
428 steps.push(match value {
429 Some(value) => SimStep::Set { key, value },
430 None => SimStep::Remove { key },
431 });
432 }
433 };
434 let mut ordered: Vec<&gemini_adk_rs::state::StateMutation> = journal.iter().collect();
435 ordered.sort_by_key(|m| m.sequence);
436 for m in ordered {
437 match m.key.as_str() {
438 "flow:active" => {
439 flush(&mut pending, &mut steps);
440 steps.push(SimStep::Turn);
441 let active: Vec<String> = m
442 .new
443 .clone()
444 .and_then(|v| serde_json::from_value(v).ok())
445 .unwrap_or_default();
446 steps.push(SimStep::ExpectActive(active));
447 }
448 TOOL_CALL_KEY => {
449 if let Some(tool) = tool_of(&m.new) {
450 flush(&mut pending, &mut steps);
451 steps.push(SimStep::ExpectAllowed(tool));
452 }
453 }
454 TOOL_DENIED_KEY => {
455 if let Some(tool) = tool_of(&m.new) {
456 flush(&mut pending, &mut steps);
457 steps.push(SimStep::ExpectDenied(tool));
458 }
459 }
460 TOOL_RESULT_KEY => {
461 if let Some(tool) = tool_of(&m.new) {
462 flush(&mut pending, &mut steps);
463 let ok = m.new.as_ref().is_some_and(|v| v["ok"] == true);
464 steps.push(SimStep::ToolResult { tool, ok });
465 }
466 }
467 key if runtime_owned(key) => {}
468 key => {
469 let value = m.new.clone();
470 match pending.iter_mut().find(|(k, _)| k == key) {
472 Some(slot) => slot.1 = value,
473 None => pending.push((key.to_string(), value)),
474 }
475 }
476 }
477 }
478 flush(&mut pending, &mut steps);
479 Self {
480 name: name.into(),
481 steps,
482 }
483 }
484
485 pub async fn run(&self, convo: &CompiledConversation, mode: Enforcement) -> Result<(), String> {
488 let mut sim = Sim::new(convo, mode);
489 for (i, step) in self.steps.iter().enumerate() {
490 sim.apply(step)
491 .await
492 .map_err(|msg| format!("[{}] step {i} ({step:?}): {msg}", self.name))?;
493 }
494 Ok(())
495 }
496}
497
498#[cfg(test)]
499mod tests {
500 use super::*;
501 use crate::conversation::Conversation;
502 use gemini_adk_rs::flow::Guard;
503 use gemini_adk_rs::frame::{Frame, FrameSpec, SlotRecognizer, SlotSpec};
504
505 struct Booking;
506 impl Frame for Booking {
507 fn frame() -> FrameSpec {
508 FrameSpec {
509 name: "booking".into(),
510 slots: vec![SlotSpec {
511 recognizer: Some(SlotRecognizer::IntegerNear(vec!["people".into()])),
512 ..SlotSpec::new("party_size")
513 }],
514 }
515 }
516 }
517
518 fn booking() -> CompiledConversation {
519 Conversation::new("booking")
520 .stage("collect")
521 .collect_frame::<Booking>()
522 .next("confirm", Guard::captured(["party_size"]))
523 .stage("confirm")
524 .commit("book", Guard::is_true("user_confirmed"))
525 .next("done", Guard::called_ok("book"))
526 .stage("done")
527 .terminal()
528 .require(["done"])
529 .compile()
530 .expect("compiles")
531 }
532
533 #[tokio::test]
534 async fn fake_user_fills_slots_and_gates_commit() {
535 let convo = booking();
536 let mut sim = Sim::new(&convo, Enforcement::Enforce);
537
538 assert!(sim.active().contains(&"collect".to_string()));
539 assert!(!sim.allowed("book"));
540
541 sim.user("a table for 4 people").await;
543 assert_eq!(sim.slot::<u32>("party_size"), Some(4));
544 assert!(sim.active().contains(&"confirm".to_string()));
545
546 assert!(!sim.allowed("book"));
548 sim.set("user_confirmed", true);
549 sim.turn();
550 assert!(sim.allowed("book"));
551
552 sim.tool_ok("book");
553 assert!(sim.is_complete());
554 }
555
556 #[tokio::test]
557 async fn scenario_runs_and_round_trips() {
558 let scenario = Scenario {
559 name: "happy_path".into(),
560 steps: vec![
561 SimStep::ExpectActive(vec!["collect".into()]),
562 SimStep::ExpectDenied("book".into()),
563 SimStep::User("party of 4 people".into()),
564 SimStep::ExpectSlot {
565 key: "party_size".into(),
566 value: serde_json::json!(4),
567 },
568 SimStep::ExpectActive(vec!["confirm".into()]),
569 SimStep::Set {
570 key: "user_confirmed".into(),
571 value: serde_json::json!(true),
572 },
573 SimStep::Turn,
574 SimStep::ExpectAllowed("book".into()),
575 SimStep::ToolOk("book".into()),
576 SimStep::ExpectComplete,
577 ],
578 };
579
580 scenario
581 .run(&booking(), Enforcement::Enforce)
582 .await
583 .expect("scenario passes");
584
585 let json = serde_json::to_string(&scenario).unwrap();
587 let back: Scenario = serde_json::from_str(&json).unwrap();
588 back.run(&booking(), Enforcement::Enforce)
589 .await
590 .expect("round-tripped scenario passes");
591 }
592
593 #[tokio::test]
594 async fn scenario_reports_failed_expectation() {
595 let scenario = Scenario {
596 name: "bad".into(),
597 steps: vec![SimStep::ExpectComplete], };
599 let err = scenario
600 .run(&booking(), Enforcement::Enforce)
601 .await
602 .unwrap_err();
603 assert!(err.contains("expected conversation complete"));
604 }
605
606 #[tokio::test]
607 async fn tool_latency_resolves_after_delay() {
608 let convo = booking();
609 let mut sim = Sim::new(&convo, Enforcement::Enforce);
610 sim.user("4 people").await;
611 sim.set("user_confirmed", true);
612 sim.turn();
613 sim.schedule_tool("book", 2);
615 assert!(!sim.is_complete());
616 sim.turn();
617 assert!(!sim.is_complete());
618 sim.turn();
619 assert!(sim.is_complete());
620 }
621}