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 ///
287 /// Calls to other tools are refused. Under
288 /// [`SteeringMode::ContextUpdate`](gemini_adk_rs::live::SteeringMode::ContextUpdate)
289 /// the model is also offered only these tools while the phase is active.
290 pub fn tools(mut self, tools: Vec<String>) -> Self {
291 self.tools_enabled = Some(tools);
292 self
293 }
294
295 /// Set a guard that must return `true` for this phase to be entered.
296 pub fn guard<F>(mut self, f: F) -> Self
297 where
298 F: Fn(&State) -> bool + Send + Sync + 'static,
299 {
300 self.guard = Some(Arc::new(f));
301 self
302 }
303
304 /// Set an async callback to run when entering this phase.
305 pub fn on_enter<F, Fut>(mut self, f: F) -> Self
306 where
307 F: Fn(State, Arc<dyn SessionWriter>) -> Fut + Send + Sync + 'static,
308 Fut: Future<Output = ()> + Send + 'static,
309 {
310 self.on_enter = Some(Arc::new(move |s, w| Box::pin(f(s, w))));
311 self
312 }
313
314 /// Set an async callback to run when exiting this phase.
315 pub fn on_exit<F, Fut>(mut self, f: F) -> Self
316 where
317 F: Fn(State, Arc<dyn SessionWriter>) -> Fut + Send + Sync + 'static,
318 Fut: Future<Output = ()> + Send + 'static,
319 {
320 self.on_exit = Some(Arc::new(move |s, w| Box::pin(f(s, w))));
321 self
322 }
323
324 /// Add a guard-based transition to a target phase.
325 pub fn transition(
326 mut self,
327 target: &str,
328 guard: impl Fn(&State) -> bool + Send + Sync + 'static,
329 ) -> Self {
330 self.transitions.push(Transition {
331 target: target.to_string(),
332 guard: Arc::new(guard),
333 description: None,
334 });
335 self
336 }
337
338 /// Add a guard-based transition with a human-readable description.
339 ///
340 /// The description is used by `PhaseMachine::describe_navigation()` to help
341 /// the model understand what paths are available from the current phase.
342 pub fn transition_with(
343 mut self,
344 target: &str,
345 guard: impl Fn(&State) -> bool + Send + Sync + 'static,
346 description: impl Into<String>,
347 ) -> Self {
348 self.transitions.push(Transition {
349 target: target.to_string(),
350 guard: Arc::new(guard),
351 description: Some(description.into()),
352 });
353 self
354 }
355
356 /// Add a transition that only fires after a semantic concept was presented
357 /// and an acknowledgement key is true.
358 pub fn transition_after_presented(
359 self,
360 target: &str,
361 concept: &str,
362 ack_key: &str,
363 description: impl Into<String>,
364 ) -> Self {
365 let concept = concept.to_string();
366 let ack_key = ack_key.to_string();
367 self.transition_with(
368 target,
369 move |state| {
370 Phase::is_presented(state, &concept) && state.get::<bool>(&ack_key).unwrap_or(false)
371 },
372 description,
373 )
374 }
375
376 /// Mark this phase as terminal (no outbound transitions will be evaluated).
377 pub fn terminal(mut self) -> Self {
378 self.terminal = true;
379 self
380 }
381
382 /// Show the given state keys to the model by appending them to this
383 /// phase's instruction at runtime, rendered as
384 /// `[Context: key1=val1, key2=val2, ...]`.
385 pub fn show_state(mut self, keys: &[&str]) -> Self {
386 self.modifiers.push(InstructionModifier::StateAppend(
387 keys.iter().map(std::string::ToString::to_string).collect(),
388 ));
389 self
390 }
391
392 /// Conditionally append text when a predicate is true.
393 pub fn when(
394 mut self,
395 predicate: impl Fn(&State) -> bool + Send + Sync + 'static,
396 text: impl Into<String>,
397 ) -> Self {
398 self.modifiers.push(InstructionModifier::Conditional {
399 predicate: Arc::new(predicate),
400 text: text.into(),
401 });
402 self
403 }
404
405 /// Append the result of a custom formatter to the instruction.
406 pub fn with_context(mut self, f: impl Fn(&State) -> String + Send + Sync + 'static) -> Self {
407 self.modifiers
408 .push(InstructionModifier::CustomAppend(Arc::new(f)));
409 self
410 }
411
412 /// Append a declarative [`gemini_adk_rs::live::context_builder::ContextBuilder`] to this phase's instruction.
413 pub fn context(mut self, ctx: gemini_adk_rs::live::context_builder::ContextBuilder) -> Self {
414 self.modifiers.push(ctx.into_modifier());
415 self
416 }
417
418 /// Send `turnComplete: true` after instruction + context on phase entry,
419 /// causing the model to generate a response immediately.
420 pub fn prompt_on_enter(mut self) -> Self {
421 self.prompt_on_enter_flag = true;
422 self
423 }
424
425 /// Set a context injection callback for phase entry.
426 /// Returns `Content` to send as `client_content` before prompting.
427 pub fn on_enter_context<F>(mut self, f: F) -> Self
428 where
429 F: Fn(&State, &TranscriptWindow) -> Option<Vec<Content>> + Send + Sync + 'static,
430 {
431 self.on_enter_context_fn = Some(Arc::new(f));
432 self
433 }
434
435 /// Inject a model-role bridge message on phase entry and prompt immediately.
436 ///
437 /// Combines `on_enter_context` + `prompt_on_enter()` into a single call,
438 /// eliminating the need to import `Content` in application code.
439 ///
440 /// ```no_run
441 /// # use gemini_adk_fluent_rs::prelude::*;
442 /// # const VERIFY_IDENTITY_INSTRUCTION: &str = "Verify the caller's identity.";
443 /// Live::builder()
444 /// .phase("verify_identity")
445 /// .instruction(VERIFY_IDENTITY_INSTRUCTION)
446 /// .enter_prompt("The caller confirmed the disclosure. I'll now verify their identity.")
447 /// .done();
448 /// ```
449 pub fn enter_prompt(mut self, message: impl Into<String>) -> Self {
450 let msg = message.into();
451 self.on_enter_context_fn = Some(Arc::new(move |_, _| {
452 Some(vec![Content::model(msg.clone())])
453 }));
454 self.prompt_on_enter_flag = true;
455 self
456 }
457
458 /// Like [`enter_prompt`](Self::enter_prompt) but with a state-aware closure.
459 ///
460 /// ```no_run
461 /// # use gemini_adk_fluent_rs::prelude::*;
462 /// Live::builder()
463 /// .phase("close")
464 /// .enter_prompt_fn(|state, _tw| {
465 /// if state.get::<bool>("cease_desist_requested").unwrap_or(false) {
466 /// "Cease-and-desist requested. Closing call respectfully.".into()
467 /// } else {
468 /// "Wrapping up the call.".into()
469 /// }
470 /// })
471 /// .done();
472 /// ```
473 pub fn enter_prompt_fn<F>(mut self, f: F) -> Self
474 where
475 F: Fn(&State, &TranscriptWindow) -> String + Send + Sync + 'static,
476 {
477 self.on_enter_context_fn = Some(Arc::new(move |state, tw| {
478 Some(vec![Content::model(f(state, tw))])
479 }));
480 self.prompt_on_enter_flag = true;
481 self
482 }
483
484 /// Include phase navigation context in this phase's instruction.
485 pub fn navigation(mut self) -> Self {
486 self.modifiers
487 .push(InstructionModifier::CustomAppend(Arc::new(
488 |state: &State| {
489 state
490 .session()
491 .get::<String>("navigation_context")
492 .unwrap_or_default()
493 },
494 )));
495 self
496 }
497
498 /// Apply a slice of pre-built instruction modifiers to this phase.
499 ///
500 /// Use with `P::show_state()`, `P::when()`, `P::context_fn()` factories.
501 ///
502 /// ```no_run
503 /// # use gemini_adk_fluent_rs::prelude::*;
504 /// Live::builder()
505 /// .phase("disclosure")
506 /// .modifiers(&[P::show_state(&["balance"]), P::when(|_| true, "warning")])
507 /// .done();
508 /// ```
509 pub fn modifiers(mut self, mods: &[InstructionModifier]) -> Self {
510 self.modifiers.extend(mods.iter().cloned());
511 self
512 }
513
514 /// Finish building this phase and return the `Live` builder.
515 ///
516 /// Merges phase defaults (from [`Live::phase_defaults`]) with phase-specific
517 /// settings. Defaults are prepended so phase-specific modifiers take priority.
518 pub fn done(mut self) -> Live {
519 // Merge defaults: prepend default modifiers, inherit prompt_on_enter if not set.
520 let mut merged_modifiers = self.live.phase_default_modifiers.clone();
521 merged_modifiers.append(&mut self.modifiers);
522
523 let prompt = self.prompt_on_enter_flag || self.live.phase_default_prompt_on_enter;
524
525 let phase = Phase {
526 name: self.name,
527 instruction: self
528 .instruction
529 .unwrap_or(PhaseInstruction::Static(String::new())),
530 tools_enabled: self.tools_enabled,
531 guard: self.guard,
532 on_enter: self.on_enter,
533 on_exit: self.on_exit,
534 transitions: self.transitions,
535 terminal: self.terminal,
536 modifiers: merged_modifiers,
537 prompt_on_enter: prompt,
538 on_enter_context: self.on_enter_context_fn,
539 needs: self.needs,
540 requires: self.requires,
541 preparations: self.preparations,
542 presents: self.presents,
543 clear_on_enter: self.clear_on_enter,
544 };
545 self.live.add_phase(phase);
546 self.live
547 }
548}
549
550// ── WatchBuilder ─────────────────────────────────────────────────────────────
551
552/// Builder for a state watcher.
553///
554/// Created by [`Live::watch`] and returned to the `Live` chain via [`then`](Self::then).
555pub struct WatchBuilder {
556 live: Live,
557 key: String,
558 predicate: Option<WatchPredicate>,
559 blocking: bool,
560}
561
562impl WatchBuilder {
563 pub(crate) fn new(live: Live, key: impl Into<String>) -> Self {
564 Self {
565 live,
566 key: key.into(),
567 predicate: None,
568 blocking: false,
569 }
570 }
571
572 /// Fire on any change to the watched key (default).
573 pub fn changed(mut self) -> Self {
574 self.predicate = Some(WatchPredicate::Changed);
575 self
576 }
577
578 /// Fire when the new value equals the given value.
579 pub fn changed_to(mut self, value: Value) -> Self {
580 self.predicate = Some(WatchPredicate::ChangedTo(value));
581 self
582 }
583
584 /// Fire when the value crosses above the given threshold.
585 pub fn crossed_above(mut self, threshold: f64) -> Self {
586 self.predicate = Some(WatchPredicate::CrossedAbove(threshold));
587 self
588 }
589
590 /// Fire when the value crosses below the given threshold.
591 pub fn crossed_below(mut self, threshold: f64) -> Self {
592 self.predicate = Some(WatchPredicate::CrossedBelow(threshold));
593 self
594 }
595
596 /// Fire when the value changes from non-true to true.
597 pub fn became_true(mut self) -> Self {
598 self.predicate = Some(WatchPredicate::BecameTrue);
599 self
600 }
601
602 /// Fire when the value changes from true to non-true.
603 pub fn became_false(mut self) -> Self {
604 self.predicate = Some(WatchPredicate::BecameFalse);
605 self
606 }
607
608 /// Make this watcher blocking (awaited sequentially on the control lane).
609 pub fn blocking(mut self) -> Self {
610 self.blocking = true;
611 self
612 }
613
614 /// Set the action and finish building the watcher, returning the `Live` builder.
615 ///
616 /// The action receives `(old_value, new_value, state)`.
617 pub fn then<F, Fut>(self, f: F) -> Live
618 where
619 F: Fn(Value, Value, State) -> Fut + Send + Sync + 'static,
620 Fut: Future<Output = ()> + Send + 'static,
621 {
622 self.then_with_writer(move |old, new, state, _writer| f(old, new, state))
623 }
624
625 /// Like [`then`](Self::then), but the action also receives the live
626 /// session writer — so a watcher can inject steering context or prompt
627 /// the model, not only mutate state.
628 ///
629 /// The action receives `(old_value, new_value, state, writer)`.
630 pub fn then_with_writer<F, Fut>(mut self, f: F) -> Live
631 where
632 F: Fn(Value, Value, State, Arc<dyn gemini_genai_rs::session::SessionWriter>) -> Fut
633 + Send
634 + Sync
635 + 'static,
636 Fut: Future<Output = ()> + Send + 'static,
637 {
638 let watcher = Watcher {
639 key: self.key,
640 predicate: self.predicate.unwrap_or(WatchPredicate::Changed),
641 action: Arc::new(move |old, new, state, writer| Box::pin(f(old, new, state, writer))),
642 blocking: self.blocking,
643 };
644 self.live.add_watcher(watcher);
645 self.live
646 }
647}