gemini_adk_fluent_rs/live_builders.rs
1//! Sub-builders for the fluent [`Live`] API.
2//!
3//! These builders use a "move self, return `Live`" pattern so that the
4//! caller's chain stays fully typed and fluent:
5//!
6//! ```ignore
7//! Live::builder()
8//! .phase("greeting")
9//! .instruction("Welcome the user")
10//! .transition("main", |s| s.get::<bool>("greeted").unwrap_or(false))
11//! .done()
12//! .phase("main")
13//! .instruction("Handle the conversation")
14//! .terminal()
15//! .done()
16//! .initial_phase("greeting")
17//! .connect_vertex(project, location, token)
18//! .await?;
19//! ```
20
21use std::future::Future;
22use std::sync::Arc;
23
24use serde_json::Value;
25
26use gemini_adk_rs::live::{
27 InstructionModifier, Phase, PhaseInstruction, PhasePreparation, TranscriptWindow, Transition,
28 WatchPredicate, Watcher,
29};
30use gemini_adk_rs::State;
31use gemini_genai_rs::prelude::Content;
32use gemini_genai_rs::session::SessionWriter;
33
34use crate::live::Live;
35
36// ── PhaseDefaults ────────────────────────────────────────────────────────────
37
38/// Default modifiers and settings inherited by all phases.
39///
40/// Created by [`Live::phase_defaults`] and applied in [`PhaseBuilder::done`].
41pub struct PhaseDefaults {
42 pub(crate) modifiers: Vec<InstructionModifier>,
43 pub(crate) prompt_on_enter: bool,
44}
45
46impl PhaseDefaults {
47 pub(crate) fn new() -> Self {
48 Self {
49 modifiers: Vec::new(),
50 prompt_on_enter: false,
51 }
52 }
53
54 /// Append state keys to every phase's instruction at runtime.
55 pub fn with_state(mut self, keys: &[&str]) -> Self {
56 self.modifiers.push(InstructionModifier::StateAppend(
57 keys.iter().map(|s| s.to_string()).collect(),
58 ));
59 self
60 }
61
62 /// Conditionally append text to every phase when predicate is true.
63 pub fn when(
64 mut self,
65 predicate: impl Fn(&State) -> bool + Send + Sync + 'static,
66 text: impl Into<String>,
67 ) -> Self {
68 self.modifiers.push(InstructionModifier::Conditional {
69 predicate: Arc::new(predicate),
70 text: text.into(),
71 });
72 self
73 }
74
75 /// Append custom formatted context to every phase's instruction.
76 pub fn with_context(mut self, f: impl Fn(&State) -> String + Send + Sync + 'static) -> Self {
77 self.modifiers
78 .push(InstructionModifier::CustomAppend(Arc::new(f)));
79 self
80 }
81
82 /// Append a declarative [`gemini_adk_rs::live::context_builder::ContextBuilder`] to every phase's instruction.
83 ///
84 /// The builder renders accumulated state as a natural-language summary,
85 /// giving the model situational awareness across all phases.
86 ///
87 /// # Example
88 ///
89 /// ```ignore
90 /// .phase_defaults(|d| d.context(
91 /// Ctx::builder()
92 /// .section("Caller")
93 /// .field("caller_name", "Name")
94 /// .flag("is_known_contact", "Known contact")
95 /// .build()
96 /// ))
97 /// ```
98 pub fn context(mut self, ctx: gemini_adk_rs::live::context_builder::ContextBuilder) -> Self {
99 self.modifiers.push(ctx.into_modifier());
100 self
101 }
102
103 /// Include phase navigation context in every phase's instruction.
104 ///
105 /// Appends the output of `PhaseMachine::describe_navigation()` to the
106 /// instruction, giving the model awareness of its current position,
107 /// phase history, missing state keys, and possible transitions.
108 pub fn navigation(mut self) -> Self {
109 self.modifiers
110 .push(InstructionModifier::CustomAppend(Arc::new(
111 |state: &State| {
112 state
113 .session()
114 .get::<String>("navigation_context")
115 .unwrap_or_default()
116 },
117 )));
118 self
119 }
120
121 /// Enable `prompt_on_enter` for all phases (model responds immediately on entry).
122 pub fn prompt_on_enter(mut self, enabled: bool) -> Self {
123 self.prompt_on_enter = enabled;
124 self
125 }
126}
127
128// ── PhaseBuilder ─────────────────────────────────────────────────────────────
129
130/// Builder for a conversation phase.
131///
132/// Created by [`Live::phase`] and returned to the `Live` chain via [`done`](Self::done).
133pub struct PhaseBuilder {
134 live: Live,
135 name: String,
136 instruction: Option<PhaseInstruction>,
137 tools_enabled: Option<Vec<String>>,
138 guard: Option<gemini_adk_rs::live::StateGuard>,
139 on_enter: Option<gemini_adk_rs::live::PhaseHook>,
140 on_exit: Option<gemini_adk_rs::live::PhaseHook>,
141 transitions: Vec<Transition>,
142 terminal: bool,
143 modifiers: Vec<InstructionModifier>,
144 prompt_on_enter_flag: bool,
145 on_enter_context_fn: Option<gemini_adk_rs::live::EnterContextFn>,
146 needs: Vec<String>,
147 requires: Vec<String>,
148 preparations: Vec<PhasePreparation>,
149 presents: Vec<String>,
150 clear_on_enter: Vec<String>,
151}
152
153impl PhaseBuilder {
154 pub(crate) fn new(live: Live, name: impl Into<String>) -> Self {
155 Self {
156 live,
157 name: name.into(),
158 instruction: None,
159 tools_enabled: None,
160 guard: None,
161 on_enter: None,
162 on_exit: None,
163 transitions: Vec::new(),
164 terminal: false,
165 modifiers: Vec::new(),
166 prompt_on_enter_flag: false,
167 on_enter_context_fn: None,
168 needs: Vec::new(),
169 requires: Vec::new(),
170 preparations: Vec::new(),
171 presents: Vec::new(),
172 clear_on_enter: Vec::new(),
173 }
174 }
175
176 /// Declare what state keys this phase is responsible for gathering.
177 ///
178 /// Purely informational — does not enforce transitions or block progress.
179 /// The [`ContextBuilder`](gemini_adk_rs::live::context_builder::ContextBuilder)
180 /// reads these to append a "\[Gathering\] key1, key2" line to the instruction,
181 /// so the model knows what to focus on in the current phase.
182 ///
183 /// # Example
184 ///
185 /// ```ignore
186 /// .phase("identify_caller")
187 /// .instruction("Get the caller's name and organization.")
188 /// .needs(&["caller_name", "caller_organization"])
189 /// .transition("determine_purpose", S::is_set("caller_name"))
190 /// .done()
191 /// ```
192 pub fn needs(mut self, keys: &[&str]) -> Self {
193 self.needs = keys.iter().map(|k| k.to_string()).collect();
194 self
195 }
196
197 /// Declare state keys that must exist before this phase can be entered.
198 ///
199 /// This is a hard phase-machine gate, unlike [`needs`](Self::needs), which
200 /// is only conversational guidance. Use `requires` for authoritative facts
201 /// that must be produced by tools, callbacks, retrieval, or other runtime
202 /// mechanisms before the model can operate in the phase.
203 ///
204 /// ```ignore
205 /// .phase("quote_price")
206 /// .requires(&["catalog_item_loaded", "price"])
207 /// .instruction("Quote only the loaded catalog price.")
208 /// .done()
209 /// ```
210 pub fn requires(mut self, keys: &[&str]) -> Self {
211 self.requires = keys.iter().map(|k| k.to_string()).collect();
212 self
213 }
214
215 /// Add a preparation effect that runs before this phase is entered when
216 /// its required state is missing.
217 ///
218 /// Preparations are run by the phase lifecycle after an outbound transition
219 /// guard selects this phase, but before the phase is committed. If the
220 /// preparation does not satisfy this phase's `requires`, the transition
221 /// remains blocked.
222 pub fn prepare<F, Fut>(mut self, name: impl Into<String>, produces: &[&str], f: F) -> Self
223 where
224 F: Fn(State, Arc<dyn SessionWriter>) -> Fut + Send + Sync + 'static,
225 Fut: Future<Output = ()> + Send + 'static,
226 {
227 self.preparations.push(PhasePreparation {
228 name: name.into(),
229 produces: produces.iter().map(|k| k.to_string()).collect(),
230 run: Arc::new(move |s, w| Box::pin(f(s, w))),
231 });
232 self
233 }
234
235 /// Declare semantic concepts presented to the user by this phase.
236 ///
237 /// On phase entry the runtime writes `presented:<concept> = true`. Use this
238 /// with [`transition_after_presented`](Self::transition_after_presented) to
239 /// avoid accepting stale acknowledgements from earlier phases.
240 pub fn presents(mut self, concepts: &[&str]) -> Self {
241 self.presents = concepts.iter().map(|c| c.to_string()).collect();
242 self
243 }
244
245 /// Clear state keys on phase entry.
246 ///
247 /// This is useful for removing stale acknowledgements or intents that were
248 /// extracted before the current phase's concept was presented.
249 pub fn clear_on_enter(mut self, keys: &[&str]) -> Self {
250 self.clear_on_enter = keys.iter().map(|k| k.to_string()).collect();
251 self
252 }
253
254 /// Set a static instruction for this phase.
255 pub fn instruction(mut self, instruction: impl Into<String>) -> Self {
256 self.instruction = Some(PhaseInstruction::Static(instruction.into()));
257 self
258 }
259
260 /// Set a dynamic instruction that is resolved from state at transition time.
261 pub fn dynamic_instruction<F>(mut self, f: F) -> Self
262 where
263 F: Fn(&State) -> String + Send + Sync + 'static,
264 {
265 self.instruction = Some(PhaseInstruction::Dynamic(Arc::new(f)));
266 self
267 }
268
269 /// Set the tool filter for this phase. Only these tools will be enabled.
270 pub fn tools(mut self, tools: Vec<String>) -> Self {
271 self.tools_enabled = Some(tools);
272 self
273 }
274
275 /// Set a guard that must return `true` for this phase to be entered.
276 pub fn guard<F>(mut self, f: F) -> Self
277 where
278 F: Fn(&State) -> bool + Send + Sync + 'static,
279 {
280 self.guard = Some(Arc::new(f));
281 self
282 }
283
284 /// Set an async callback to run when entering this phase.
285 pub fn on_enter<F, Fut>(mut self, f: F) -> Self
286 where
287 F: Fn(State, Arc<dyn SessionWriter>) -> Fut + Send + Sync + 'static,
288 Fut: Future<Output = ()> + Send + 'static,
289 {
290 self.on_enter = Some(Arc::new(move |s, w| Box::pin(f(s, w))));
291 self
292 }
293
294 /// Set an async callback to run when exiting this phase.
295 pub fn on_exit<F, Fut>(mut self, f: F) -> Self
296 where
297 F: Fn(State, Arc<dyn SessionWriter>) -> Fut + Send + Sync + 'static,
298 Fut: Future<Output = ()> + Send + 'static,
299 {
300 self.on_exit = Some(Arc::new(move |s, w| Box::pin(f(s, w))));
301 self
302 }
303
304 /// Add a guard-based transition to a target phase.
305 pub fn transition(
306 mut self,
307 target: &str,
308 guard: impl Fn(&State) -> bool + Send + Sync + 'static,
309 ) -> Self {
310 self.transitions.push(Transition {
311 target: target.to_string(),
312 guard: Arc::new(guard),
313 description: None,
314 });
315 self
316 }
317
318 /// Add a guard-based transition with a human-readable description.
319 ///
320 /// The description is used by `PhaseMachine::describe_navigation()` to help
321 /// the model understand what paths are available from the current phase.
322 pub fn transition_with(
323 mut self,
324 target: &str,
325 guard: impl Fn(&State) -> bool + Send + Sync + 'static,
326 description: impl Into<String>,
327 ) -> Self {
328 self.transitions.push(Transition {
329 target: target.to_string(),
330 guard: Arc::new(guard),
331 description: Some(description.into()),
332 });
333 self
334 }
335
336 /// Add a transition that only fires after a semantic concept was presented
337 /// and an acknowledgement key is true.
338 pub fn transition_after_presented(
339 self,
340 target: &str,
341 concept: &str,
342 ack_key: &str,
343 description: impl Into<String>,
344 ) -> Self {
345 let concept = concept.to_string();
346 let ack_key = ack_key.to_string();
347 self.transition_with(
348 target,
349 move |state| {
350 Phase::is_presented(state, &concept) && state.get::<bool>(&ack_key).unwrap_or(false)
351 },
352 description,
353 )
354 }
355
356 /// Mark this phase as terminal (no outbound transitions will be evaluated).
357 pub fn terminal(mut self) -> Self {
358 self.terminal = true;
359 self
360 }
361
362 /// Append state keys to the instruction at runtime.
363 /// Renders as `[Context: key1=val1, key2=val2, ...]`.
364 pub fn with_state(mut self, keys: &[&str]) -> Self {
365 self.modifiers.push(InstructionModifier::StateAppend(
366 keys.iter().map(|s| s.to_string()).collect(),
367 ));
368 self
369 }
370
371 /// Conditionally append text when a predicate is true.
372 pub fn when(
373 mut self,
374 predicate: impl Fn(&State) -> bool + Send + Sync + 'static,
375 text: impl Into<String>,
376 ) -> Self {
377 self.modifiers.push(InstructionModifier::Conditional {
378 predicate: Arc::new(predicate),
379 text: text.into(),
380 });
381 self
382 }
383
384 /// Append the result of a custom formatter to the instruction.
385 pub fn with_context(mut self, f: impl Fn(&State) -> String + Send + Sync + 'static) -> Self {
386 self.modifiers
387 .push(InstructionModifier::CustomAppend(Arc::new(f)));
388 self
389 }
390
391 /// Append a declarative [`gemini_adk_rs::live::context_builder::ContextBuilder`] to this phase's instruction.
392 pub fn context(mut self, ctx: gemini_adk_rs::live::context_builder::ContextBuilder) -> Self {
393 self.modifiers.push(ctx.into_modifier());
394 self
395 }
396
397 /// Send `turnComplete: true` after instruction + context on phase entry,
398 /// causing the model to generate a response immediately.
399 pub fn prompt_on_enter(mut self, enabled: bool) -> Self {
400 self.prompt_on_enter_flag = enabled;
401 self
402 }
403
404 /// Set a context injection callback for phase entry.
405 /// Returns `Content` to send as `client_content` before prompting.
406 pub fn on_enter_context<F>(mut self, f: F) -> Self
407 where
408 F: Fn(&State, &TranscriptWindow) -> Option<Vec<Content>> + Send + Sync + 'static,
409 {
410 self.on_enter_context_fn = Some(Arc::new(f));
411 self
412 }
413
414 /// Inject a model-role bridge message on phase entry and prompt immediately.
415 ///
416 /// Combines `on_enter_context` + `prompt_on_enter(true)` into a single call,
417 /// eliminating the need to import `Content` in application code.
418 ///
419 /// ```ignore
420 /// .phase("verify_identity")
421 /// .instruction(VERIFY_IDENTITY_INSTRUCTION)
422 /// .enter_prompt("The caller confirmed the disclosure. I'll now verify their identity.")
423 /// .done()
424 /// ```
425 pub fn enter_prompt(mut self, message: impl Into<String>) -> Self {
426 let msg = message.into();
427 self.on_enter_context_fn = Some(Arc::new(move |_, _| {
428 Some(vec![Content::model(msg.clone())])
429 }));
430 self.prompt_on_enter_flag = true;
431 self
432 }
433
434 /// Like [`enter_prompt`](Self::enter_prompt) but with a state-aware closure.
435 ///
436 /// ```ignore
437 /// .enter_prompt_fn(|state, _tw| {
438 /// if state.get::<bool>("cease_desist_requested").unwrap_or(false) {
439 /// "Cease-and-desist requested. Closing call respectfully.".into()
440 /// } else {
441 /// "Wrapping up the call.".into()
442 /// }
443 /// })
444 /// ```
445 pub fn enter_prompt_fn<F>(mut self, f: F) -> Self
446 where
447 F: Fn(&State, &TranscriptWindow) -> String + Send + Sync + 'static,
448 {
449 self.on_enter_context_fn = Some(Arc::new(move |state, tw| {
450 Some(vec![Content::model(f(state, tw))])
451 }));
452 self.prompt_on_enter_flag = true;
453 self
454 }
455
456 /// Include phase navigation context in this phase's instruction.
457 pub fn navigation(mut self) -> Self {
458 self.modifiers
459 .push(InstructionModifier::CustomAppend(Arc::new(
460 |state: &State| {
461 state
462 .session()
463 .get::<String>("navigation_context")
464 .unwrap_or_default()
465 },
466 )));
467 self
468 }
469
470 /// Apply a slice of pre-built instruction modifiers to this phase.
471 ///
472 /// Use with `P::with_state()`, `P::when()`, `P::context_fn()` factories.
473 ///
474 /// ```ignore
475 /// .phase("disclosure")
476 /// .modifiers(&[P::with_state(KEYS), P::when(pred, "warning")])
477 /// .done()
478 /// ```
479 pub fn modifiers(mut self, mods: &[InstructionModifier]) -> Self {
480 self.modifiers.extend(mods.iter().cloned());
481 self
482 }
483
484 /// Finish building this phase and return the `Live` builder.
485 ///
486 /// Merges phase defaults (from [`Live::phase_defaults`]) with phase-specific
487 /// settings. Defaults are prepended so phase-specific modifiers take priority.
488 pub fn done(mut self) -> Live {
489 // Merge defaults: prepend default modifiers, inherit prompt_on_enter if not set.
490 let mut merged_modifiers = self.live.phase_default_modifiers.clone();
491 merged_modifiers.append(&mut self.modifiers);
492
493 let prompt = self.prompt_on_enter_flag || self.live.phase_default_prompt_on_enter;
494
495 let phase = Phase {
496 name: self.name,
497 instruction: self
498 .instruction
499 .unwrap_or(PhaseInstruction::Static(String::new())),
500 tools_enabled: self.tools_enabled,
501 guard: self.guard,
502 on_enter: self.on_enter,
503 on_exit: self.on_exit,
504 transitions: self.transitions,
505 terminal: self.terminal,
506 modifiers: merged_modifiers,
507 prompt_on_enter: prompt,
508 on_enter_context: self.on_enter_context_fn,
509 needs: self.needs,
510 requires: self.requires,
511 preparations: self.preparations,
512 presents: self.presents,
513 clear_on_enter: self.clear_on_enter,
514 };
515 self.live.add_phase(phase);
516 self.live
517 }
518}
519
520// ── WatchBuilder ─────────────────────────────────────────────────────────────
521
522/// Builder for a state watcher.
523///
524/// Created by [`Live::watch`] and returned to the `Live` chain via [`then`](Self::then).
525pub struct WatchBuilder {
526 live: Live,
527 key: String,
528 predicate: Option<WatchPredicate>,
529 blocking: bool,
530}
531
532impl WatchBuilder {
533 pub(crate) fn new(live: Live, key: impl Into<String>) -> Self {
534 Self {
535 live,
536 key: key.into(),
537 predicate: None,
538 blocking: false,
539 }
540 }
541
542 /// Fire on any change to the watched key (default).
543 pub fn changed(mut self) -> Self {
544 self.predicate = Some(WatchPredicate::Changed);
545 self
546 }
547
548 /// Fire when the new value equals the given value.
549 pub fn changed_to(mut self, value: Value) -> Self {
550 self.predicate = Some(WatchPredicate::ChangedTo(value));
551 self
552 }
553
554 /// Fire when the value crosses above the given threshold.
555 pub fn crossed_above(mut self, threshold: f64) -> Self {
556 self.predicate = Some(WatchPredicate::CrossedAbove(threshold));
557 self
558 }
559
560 /// Fire when the value crosses below the given threshold.
561 pub fn crossed_below(mut self, threshold: f64) -> Self {
562 self.predicate = Some(WatchPredicate::CrossedBelow(threshold));
563 self
564 }
565
566 /// Fire when the value changes from non-true to true.
567 pub fn became_true(mut self) -> Self {
568 self.predicate = Some(WatchPredicate::BecameTrue);
569 self
570 }
571
572 /// Fire when the value changes from true to non-true.
573 pub fn became_false(mut self) -> Self {
574 self.predicate = Some(WatchPredicate::BecameFalse);
575 self
576 }
577
578 /// Make this watcher blocking (awaited sequentially on the control lane).
579 pub fn blocking(mut self) -> Self {
580 self.blocking = true;
581 self
582 }
583
584 /// Set the action and finish building the watcher, returning the `Live` builder.
585 ///
586 /// The action receives `(old_value, new_value, state)`.
587 pub fn then<F, Fut>(mut self, f: F) -> Live
588 where
589 F: Fn(Value, Value, State) -> Fut + Send + Sync + 'static,
590 Fut: Future<Output = ()> + Send + 'static,
591 {
592 let watcher = Watcher {
593 key: self.key,
594 predicate: self.predicate.unwrap_or(WatchPredicate::Changed),
595 action: Arc::new(move |old, new, state| Box::pin(f(old, new, state))),
596 blocking: self.blocking,
597 };
598 self.live.add_watcher(watcher);
599 self.live
600 }
601}