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