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