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}