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}