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}