gemini_adk_fluent_rs/live/introspect.rs
1//! Reading a configured [`Live`] builder back.
2//!
3//! The builder had 103 setters and no getters, which made three things
4//! impossible: an extension could not assert its own wiring, a caller could not
5//! see what a chain of `with_*` helpers had installed on their behalf, and
6//! static checks like [`check_live`](crate::testing::check_live) had nothing to
7//! read. Every accessor here is a plain borrow of already-public-in-spirit
8//! configuration — nothing is computed, nothing connects.
9//!
10//! Tool names are the one place with a caveat worth stating: MCP, A2A, OpenAPI
11//! and agent tools resolve asynchronously **at connect**, so before then
12//! [`declared_tool_names`](Live::declared_tool_names) reports what is knowable
13//! and [`pending_tool_count`](Live::pending_tool_count) reports how much is not.
14//! Anything validating tool names must either run after connect resolution or
15//! treat the deferred count as an admission of incompleteness.
16
17use gemini_adk_rs::flow::{Enforcement, Flow};
18use gemini_adk_rs::live::Phase;
19use gemini_adk_rs::State;
20use gemini_genai_rs::prelude::Tool;
21
22use super::Live;
23
24/// Pull every function name out of a set of wire tool declarations, including
25/// the built-in grounding tools that carry no function declaration.
26pub(crate) fn declaration_names(tools: &[Tool]) -> Vec<String> {
27 let mut names = Vec::new();
28 for tool in tools {
29 if let Some(functions) = &tool.function_declarations {
30 names.extend(functions.iter().map(|f| f.name.clone()));
31 }
32 if tool.google_search.is_some() {
33 names.push("google_search".into());
34 }
35 if tool.code_execution.is_some() {
36 names.push("code_execution".into());
37 }
38 if tool.url_context.is_some() {
39 names.push("url_context".into());
40 }
41 }
42 names
43}
44
45impl Live {
46 /// Every tool name this builder can name *without connecting*.
47 ///
48 /// Covers config-level declarations (`google_search`, `code_execution`, …),
49 /// everything registered on the dispatcher, and deferred **agent** tools,
50 /// whose names are known up front even though the tool is built at connect.
51 ///
52 /// Does **not** cover MCP/A2A/OpenAPI tools, whose names live on the far
53 /// side of an async handshake — see [`pending_tool_count`](Self::pending_tool_count).
54 /// Duplicates are removed; order is not meaningful.
55 pub fn declared_tool_names(&self) -> Vec<String> {
56 let mut names = declaration_names(&self.config.tools);
57 if let Some(dispatcher) = &self.dispatcher {
58 names.extend(declaration_names(&dispatcher.to_tool_declarations()));
59 }
60 names.extend(self.deferred_agent_tools.iter().map(|t| t.name.clone()));
61 names.sort();
62 names.dedup();
63 names
64 }
65
66 /// How many tools are still unresolved, and so absent from
67 /// [`declared_tool_names`](Self::declared_tool_names).
68 ///
69 /// Non-zero means any name-based check run now is working from a partial
70 /// picture. Zero means `declared_tool_names` is the whole set.
71 pub fn pending_tool_count(&self) -> usize {
72 self.deferred_tools.len()
73 }
74
75 /// The governing flow, if [`govern`](Live::govern) or
76 /// [`observe`](Live::observe) was called.
77 ///
78 /// The flow's own `ambient` list is as the caller wrote it; tools registered
79 /// through [`ambient_tools`](Live::ambient_tools) are merged in at connect
80 /// and are readable separately via
81 /// [`ambient_tool_names`](Live::ambient_tools).
82 pub fn flow(&self) -> Option<&Flow> {
83 self.flow.as_ref()
84 }
85
86 /// Whether an attached flow is enforced or merely observed.
87 pub fn flow_enforcement(&self) -> Enforcement {
88 self.flow_mode
89 }
90
91 /// The configured phases, in declaration order.
92 pub fn phases(&self) -> &[Phase] {
93 &self.phases
94 }
95
96 /// The initial phase name, if one was set.
97 ///
98 /// A phase machine with phases but no initial phase never starts, which is
99 /// why this is worth being able to ask about.
100 pub fn initial_phase_name(&self) -> Option<&str> {
101 self.initial_phase.as_deref()
102 }
103
104 /// The state keys under watch, via [`watch`](Live::watch).
105 pub fn watched_keys(&self) -> Vec<String> {
106 let mut keys: Vec<String> = self.watchers.observed_keys().iter().cloned().collect();
107 keys.sort();
108 keys
109 }
110
111 /// How many turn extractors are installed.
112 ///
113 /// Extractors are opaque trait objects — this reports presence, which is
114 /// enough to tell a session that will populate state from one that will not.
115 pub fn extractor_count(&self) -> usize {
116 self.extractors.len()
117 }
118
119 /// Whether a session persistence backend is attached.
120 pub fn has_persistence(&self) -> bool {
121 self.persistence.is_some()
122 }
123
124 /// The caller-supplied session `State`, if [`with_state`](Live::with_state)
125 /// was called.
126 pub fn shared_state(&self) -> Option<&State> {
127 self.state.as_ref()
128 }
129
130 /// How many additive teardown hooks are registered, via
131 /// [`on_teardown`](Live::on_teardown).
132 ///
133 /// Lets an extension assert its own end-of-session wiring: `with_memory`
134 /// installs one here, and a session that reports zero will not persist
135 /// anything it learned.
136 pub fn teardown_hook_count(&self) -> usize {
137 self.callbacks.on_teardown.len()
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use async_trait::async_trait;
144 use gemini_adk_rs::flow::Guard;
145 use gemini_adk_rs::llm::{BaseLlm, LlmError, LlmRequest, LlmResponse};
146 use serde_json::json;
147
148 use crate::compose::T;
149 use crate::live::Live;
150
151 fn sample_tool() -> crate::compose::tools::ToolComposite {
152 T::simple("book_table", "Book a table", |_| async {
153 Ok(json!({"ok": true}))
154 })
155 }
156
157 /// Never called: these tests read configuration, they do not run agents.
158 struct InertLlm;
159
160 #[async_trait]
161 impl BaseLlm for InertLlm {
162 fn model_id(&self) -> &str {
163 "inert"
164 }
165 async fn generate(&self, _request: LlmRequest) -> Result<LlmResponse, LlmError> {
166 Err(LlmError::Other("inert".into()))
167 }
168 }
169
170 #[test]
171 fn declared_tool_names_sees_dispatcher_and_builtins() {
172 let live = Live::builder().with_tools(sample_tool() | T::google_search());
173 let names = live.declared_tool_names();
174 assert!(names.contains(&"book_table".to_string()), "{names:?}");
175 assert!(names.contains(&"google_search".to_string()), "{names:?}");
176 assert_eq!(
177 live.pending_tool_count(),
178 0,
179 "nothing here needs an async handshake"
180 );
181 }
182
183 #[test]
184 fn declared_tool_names_covers_agent_tools_before_connect() {
185 // Agent tools are built at connect but named up front, so a name-based
186 // check must not report them missing.
187 let verifier = crate::builder::AgentBuilder::new("verifier")
188 .instruction("Verify the caller")
189 .build(std::sync::Arc::new(InertLlm));
190 let live = Live::builder().agent_tool_arc("verify_identity", "Verify caller", verifier);
191 assert!(live
192 .declared_tool_names()
193 .contains(&"verify_identity".to_string()));
194 }
195
196 #[test]
197 fn flow_and_phases_read_back() {
198 let flow = gemini_adk_rs::flow::Flow::new()
199 .step("book")
200 .allow(["book_table"])
201 .done(Guard::called_ok("book_table"))
202 .build()
203 .expect("valid");
204 let live = Live::builder()
205 .govern(flow)
206 .phase("greet")
207 .instruction("Say hello")
208 .done()
209 .initial_phase("greet");
210
211 assert_eq!(live.flow().map(|f| f.steps.len()), Some(1));
212 assert_eq!(
213 live.flow_enforcement(),
214 gemini_adk_rs::flow::Enforcement::Enforce
215 );
216 assert_eq!(live.phases().len(), 1);
217 assert_eq!(live.initial_phase_name(), Some("greet"));
218 }
219
220 #[test]
221 fn a_shared_state_is_the_one_the_session_will_run_on() {
222 // The gap this closes: tools capture a `State` the caller built, the
223 // session ran on a different one, and a `Guard::is_true(..)` reading a
224 // key a tool had written never fired — so a governed flow stalled at
225 // its first step with every downstream tool refused by a gate whose
226 // condition was in fact satisfied.
227 let state = gemini_adk_rs::State::new();
228 let _ = state.set("identity_verified", true);
229 let live = Live::builder().with_state(state.clone());
230 assert_eq!(
231 live.shared_state()
232 .and_then(|s| s.get::<bool>("identity_verified")),
233 Some(true),
234 "the builder must hold the caller's own state, not a copy or a fresh one"
235 );
236 }
237
238 #[test]
239 fn an_unconfigured_builder_reports_nothing() {
240 let live = Live::builder();
241 assert!(live.flow().is_none());
242 assert!(live.phases().is_empty());
243 assert!(live.initial_phase_name().is_none());
244 assert!(live.watched_keys().is_empty());
245 assert_eq!(live.extractor_count(), 0);
246 assert!(!live.has_persistence());
247 assert!(live.shared_state().is_none());
248 }
249}