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 schedule_tool(&mut self, tool: impl Into<String>, after: u32) -> &mut Self {
135 self.pending_tools
136 .push((tool.into(), self.turn_no + after.max(1)));
137 self
138 }
139
140 fn advance(&mut self) {
141 self.turn_no += 1;
142 let due: Vec<String> = self
144 .pending_tools
145 .iter()
146 .filter(|(_, at)| *at <= self.turn_no)
147 .map(|(t, _)| t.clone())
148 .collect();
149 self.pending_tools.retain(|(_, at)| *at > self.turn_no);
150 for tool in due {
151 self.stack.on_tool_ok(&tool, &self.state);
152 }
153 self.stack.on_turn(&self.state);
154 }
155
156 pub fn active(&self) -> Vec<String> {
158 self.stack.explain(&self.state).active
159 }
160
161 pub fn active_overlay(&self) -> Option<&str> {
163 self.stack.active_overlay()
164 }
165
166 pub fn allowed(&self, tool: &str) -> bool {
168 self.stack.admits_tool(tool, &self.state).is_ok()
169 }
170
171 pub fn denied(&self) -> BTreeMap<String, String> {
173 self.stack.explain(&self.state).blocked_tools
174 }
175
176 pub fn is_complete(&self) -> bool {
178 self.stack.is_complete()
179 }
180
181 pub fn slot<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
183 self.state.get(key)
184 }
185
186 pub fn explain(&self) -> FlowExplanation {
188 self.stack.explain(&self.state)
189 }
190
191 pub fn state(&self) -> &State {
193 &self.state
194 }
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize)]
199#[serde(rename_all = "snake_case")]
200pub enum SimStep {
201 User(String),
203 Set {
205 key: String,
207 value: Value,
209 },
210 ToolOk(String),
212 ScheduleTool {
214 tool: String,
216 after: u32,
218 },
219 Turn,
221 ExpectActive(Vec<String>),
223 ExpectDenied(String),
225 ExpectAllowed(String),
227 ExpectSlot {
229 key: String,
231 value: Value,
233 },
234 ExpectComplete,
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct Scenario {
242 pub name: String,
244 pub steps: Vec<SimStep>,
246}
247
248impl Scenario {
249 pub async fn run(&self, convo: &CompiledConversation, mode: Enforcement) -> Result<(), String> {
252 let mut sim = Sim::new(convo, mode);
253 for (i, step) in self.steps.iter().enumerate() {
254 let fail = |msg: String| Err(format!("[{}] step {i} ({step:?}): {msg}", self.name));
255 match step {
256 SimStep::User(text) => {
257 sim.user(text).await;
258 }
259 SimStep::Set { key, value } => {
260 sim.set(key.clone(), value.clone());
261 }
262 SimStep::ToolOk(tool) => {
263 sim.tool_ok(tool);
264 }
265 SimStep::ScheduleTool { tool, after } => {
266 sim.schedule_tool(tool.clone(), *after);
267 }
268 SimStep::Turn => {
269 sim.turn();
270 }
271 SimStep::ExpectActive(expected) => {
272 let active = sim.active();
273 for e in expected {
274 if !active.contains(e) {
275 return fail(format!("expected active '{e}', got {active:?}"));
276 }
277 }
278 }
279 SimStep::ExpectDenied(tool) => {
280 if sim.allowed(tool) {
281 return fail(format!("expected '{tool}' denied, but it was admitted"));
282 }
283 }
284 SimStep::ExpectAllowed(tool) => {
285 if !sim.allowed(tool) {
286 let why = sim.denied().get(tool).cloned().unwrap_or_default();
287 return fail(format!("expected '{tool}' allowed, but denied: {why}"));
288 }
289 }
290 SimStep::ExpectSlot { key, value } => {
291 let got = sim.state().get_raw(key);
292 if got.as_ref() != Some(value) {
293 return fail(format!("expected slot '{key}' = {value}, got {got:?}"));
294 }
295 }
296 SimStep::ExpectComplete => {
297 if !sim.is_complete() {
298 return fail("expected conversation complete".into());
299 }
300 }
301 }
302 }
303 Ok(())
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310 use crate::conversation::Conversation;
311 use gemini_adk_rs::flow::Guard;
312 use gemini_adk_rs::frame::{Frame, FrameSpec, SlotRecognizer, SlotSpec};
313
314 struct Booking;
315 impl Frame for Booking {
316 fn frame() -> FrameSpec {
317 FrameSpec {
318 name: "booking".into(),
319 slots: vec![SlotSpec {
320 recognizer: Some(SlotRecognizer::IntegerNear(vec!["people".into()])),
321 ..SlotSpec::new("party_size")
322 }],
323 }
324 }
325 }
326
327 fn booking() -> CompiledConversation {
328 Conversation::new("booking")
329 .stage("collect")
330 .collect_frame::<Booking>()
331 .next("confirm", Guard::captured(["party_size"]))
332 .stage("confirm")
333 .commit("book", Guard::is_true("user_confirmed"))
334 .next("done", Guard::called_ok("book"))
335 .stage("done")
336 .terminal()
337 .require(["done"])
338 .compile()
339 .expect("compiles")
340 }
341
342 #[tokio::test]
343 async fn fake_user_fills_slots_and_gates_commit() {
344 let convo = booking();
345 let mut sim = Sim::new(&convo, Enforcement::Enforce);
346
347 assert!(sim.active().contains(&"collect".to_string()));
348 assert!(!sim.allowed("book"));
349
350 sim.user("a table for 4 people").await;
352 assert_eq!(sim.slot::<u32>("party_size"), Some(4));
353 assert!(sim.active().contains(&"confirm".to_string()));
354
355 assert!(!sim.allowed("book"));
357 sim.set("user_confirmed", true);
358 sim.turn();
359 assert!(sim.allowed("book"));
360
361 sim.tool_ok("book");
362 assert!(sim.is_complete());
363 }
364
365 #[tokio::test]
366 async fn scenario_runs_and_round_trips() {
367 let scenario = Scenario {
368 name: "happy_path".into(),
369 steps: vec![
370 SimStep::ExpectActive(vec!["collect".into()]),
371 SimStep::ExpectDenied("book".into()),
372 SimStep::User("party of 4 people".into()),
373 SimStep::ExpectSlot {
374 key: "party_size".into(),
375 value: serde_json::json!(4),
376 },
377 SimStep::ExpectActive(vec!["confirm".into()]),
378 SimStep::Set {
379 key: "user_confirmed".into(),
380 value: serde_json::json!(true),
381 },
382 SimStep::Turn,
383 SimStep::ExpectAllowed("book".into()),
384 SimStep::ToolOk("book".into()),
385 SimStep::ExpectComplete,
386 ],
387 };
388
389 scenario
390 .run(&booking(), Enforcement::Enforce)
391 .await
392 .expect("scenario passes");
393
394 let json = serde_json::to_string(&scenario).unwrap();
396 let back: Scenario = serde_json::from_str(&json).unwrap();
397 back.run(&booking(), Enforcement::Enforce)
398 .await
399 .expect("round-tripped scenario passes");
400 }
401
402 #[tokio::test]
403 async fn scenario_reports_failed_expectation() {
404 let scenario = Scenario {
405 name: "bad".into(),
406 steps: vec![SimStep::ExpectComplete], };
408 let err = scenario
409 .run(&booking(), Enforcement::Enforce)
410 .await
411 .unwrap_err();
412 assert!(err.contains("expected conversation complete"));
413 }
414
415 #[tokio::test]
416 async fn tool_latency_resolves_after_delay() {
417 let convo = booking();
418 let mut sim = Sim::new(&convo, Enforcement::Enforce);
419 sim.user("4 people").await;
420 sim.set("user_confirmed", true);
421 sim.turn();
422 sim.schedule_tool("book", 2);
424 assert!(!sim.is_complete());
425 sim.turn();
426 assert!(!sim.is_complete());
427 sim.turn();
428 assert!(sim.is_complete());
429 }
430}