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::State;
18use gemini_adk_rs::flow::{Enforcement, Flow};
19use gemini_adk_rs::live::Phase;
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    /// `T::confirm(..)` tools registered with no confirmation provider to
67    /// decide them — each would run unconfirmed.
68    pub(crate) fn unconfirmed_tools(&self) -> Vec<String> {
69        if self.confirmation_provider.is_some() {
70            return Vec::new();
71        }
72        self.dispatcher
73            .as_ref()
74            .map(|d| d.gated_tools().map(str::to_owned).collect())
75            .unwrap_or_default()
76    }
77
78    /// How many tools are still unresolved, and so absent from
79    /// [`declared_tool_names`](Self::declared_tool_names).
80    ///
81    /// Non-zero means any name-based check run now is working from a partial
82    /// picture. Zero means `declared_tool_names` is the whole set.
83    pub fn pending_tool_count(&self) -> usize {
84        self.deferred_tools.len()
85    }
86
87    /// The governing flow, if [`govern`](Live::govern) or
88    /// [`observe`](Live::observe) was called.
89    ///
90    /// The flow's own `ambient` list is as the caller wrote it; tools registered
91    /// through [`ambient_tools`](Live::ambient_tools) are merged in at connect
92    /// and are readable separately via
93    /// [`ambient_tool_names`](Live::ambient_tools).
94    pub fn flow(&self) -> Option<&Flow> {
95        self.flow.as_ref()
96    }
97
98    /// Whether an attached flow is enforced or merely observed.
99    pub fn flow_enforcement(&self) -> Enforcement {
100        self.flow_mode
101    }
102
103    /// The digressions that will be installed on the session's flow stack, in
104    /// trigger-priority order. Set by [`converse`](Live::converse); empty for
105    /// a bare `govern`/`observe`.
106    pub fn digressions(&self) -> &[gemini_adk_rs::flow::Overlay] {
107        &self.digressions
108    }
109
110    /// The per-step repair policies that will be installed on the session's
111    /// flow stack. Set by [`converse`](Live::converse).
112    pub fn repair_policies(
113        &self,
114    ) -> &std::collections::BTreeMap<String, gemini_adk_rs::flow::RepairPolicy> {
115        &self.repair_policies
116    }
117
118    /// The configured phases, in declaration order.
119    pub fn phases(&self) -> &[Phase] {
120        &self.phases
121    }
122
123    /// The initial phase name, if one was set.
124    ///
125    /// A phase machine with phases but no initial phase never starts, which is
126    /// why this is worth being able to ask about.
127    pub fn initial_phase_name(&self) -> Option<&str> {
128        self.initial_phase.as_deref()
129    }
130
131    /// The state keys under watch, via [`watch`](Live::watch).
132    pub fn watched_keys(&self) -> Vec<String> {
133        let mut keys: Vec<String> = self.watchers.observed_keys().iter().cloned().collect();
134        keys.sort();
135        keys
136    }
137
138    /// How many turn extractors are installed.
139    ///
140    /// Extractors are opaque trait objects — this reports presence, which is
141    /// enough to tell a session that will populate state from one that will not.
142    pub fn extractor_count(&self) -> usize {
143        self.extractors.len()
144    }
145
146    /// Whether a session persistence backend is attached.
147    pub fn has_persistence(&self) -> bool {
148        self.persistence.is_some()
149    }
150
151    /// The caller-supplied session `State`, if [`state`](Live::state)
152    /// was called.
153    pub fn shared_state(&self) -> Option<&State> {
154        self.state.as_ref()
155    }
156
157    /// How many additive teardown hooks are registered, via
158    /// [`on_teardown`](Live::on_teardown).
159    ///
160    /// Lets an extension assert its own end-of-session wiring: `with_memory`
161    /// installs one here, and a session that reports zero will not persist
162    /// anything it learned.
163    pub fn teardown_hook_count(&self) -> usize {
164        self.callbacks.on_teardown.len()
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use async_trait::async_trait;
171    use gemini_adk_rs::flow::Guard;
172    use gemini_adk_rs::llm::{BaseLlm, LlmError, LlmRequest, LlmResponse};
173    use serde_json::json;
174
175    use crate::compose::T;
176    use crate::live::Live;
177
178    fn sample_tool() -> crate::compose::tools::ToolComposite {
179        T::simple("book_table", "Book a table", |_| async {
180            Ok(json!({"ok": true}))
181        })
182    }
183
184    /// Never called: these tests read configuration, they do not run agents.
185    struct InertLlm;
186
187    #[async_trait]
188    impl BaseLlm for InertLlm {
189        fn model_id(&self) -> &str {
190            "inert"
191        }
192        async fn generate(&self, _request: LlmRequest) -> Result<LlmResponse, LlmError> {
193            Err(LlmError::Other("inert".into()))
194        }
195    }
196
197    #[test]
198    fn a_custom_dispatcher_keeps_tools_registered_before_it() {
199        let mut custom = gemini_adk_rs::tool::ToolDispatcher::new();
200        custom.register_function(std::sync::Arc::new(gemini_adk_rs::tool::SimpleTool::new(
201            "stream_quotes",
202            "Stream quotes",
203            None,
204            |_| async { Ok(json!({})) },
205        )));
206        let live = Live::builder().tools(sample_tool()).dispatcher(custom);
207        let names = live.declared_tool_names();
208        assert!(names.contains(&"book_table".to_string()), "{names:?}");
209        assert!(names.contains(&"stream_quotes".to_string()), "{names:?}");
210    }
211
212    /// `connect` refuses a `T::confirm` tool with no provider; the static
213    /// check says so before connecting.
214    #[test]
215    fn a_confirm_tool_without_a_provider_is_reported() {
216        let gated = T::confirm(sample_tool(), "Books cost money");
217        let live = Live::builder().tools(gated.clone());
218        assert!(
219            crate::testing::check_live(&live).contains(
220                &crate::testing::LiveViolation::UnconfirmedTools {
221                    tools: vec!["book_table".into()]
222                }
223            ),
224            "{:?}",
225            crate::testing::check_live(&live)
226        );
227
228        let confirmed = Live::builder().tools(gated).confirmation_provider(
229            gemini_adk_rs::confirmation::StaticConfirmation::deny_all("no"),
230        );
231        assert!(confirmed.unconfirmed_tools().is_empty());
232    }
233
234    #[test]
235    fn declared_tool_names_sees_dispatcher_and_builtins() {
236        let live = Live::builder().tools(sample_tool() + T::google_search());
237        let names = live.declared_tool_names();
238        assert!(names.contains(&"book_table".to_string()), "{names:?}");
239        assert!(names.contains(&"google_search".to_string()), "{names:?}");
240        assert_eq!(
241            live.pending_tool_count(),
242            0,
243            "nothing here needs an async handshake"
244        );
245    }
246
247    #[test]
248    fn declared_tool_names_covers_agent_tools_before_connect() {
249        // Agent tools are built at connect but named up front, so a name-based
250        // check must not report them missing.
251        let verifier = crate::builder::AgentBuilder::new("verifier")
252            .instruction("Verify the caller")
253            .build(std::sync::Arc::new(InertLlm))
254            .expect("builds");
255        let live = Live::builder().agent_tool("verify_identity", "Verify caller", verifier);
256        assert!(
257            live.declared_tool_names()
258                .contains(&"verify_identity".to_string())
259        );
260    }
261
262    #[test]
263    fn flow_and_phases_read_back() {
264        let flow = gemini_adk_rs::flow::Flow::new()
265            .step("book")
266            .allow(["book_table"])
267            .done(Guard::called_ok("book_table"))
268            .build()
269            .expect("valid");
270        let live = Live::builder()
271            .govern(flow)
272            .phase("greet")
273            .instruction("Say hello")
274            .done()
275            .initial_phase("greet");
276
277        assert_eq!(live.flow().map(|f| f.steps.len()), Some(1));
278        assert_eq!(
279            live.flow_enforcement(),
280            gemini_adk_rs::flow::Enforcement::Enforce
281        );
282        assert_eq!(live.phases().len(), 1);
283        assert_eq!(live.initial_phase_name(), Some("greet"));
284    }
285
286    #[test]
287    fn a_shared_state_is_the_one_the_session_will_run_on() {
288        // The gap this closes: tools capture a `State` the caller built, the
289        // session ran on a different one, and a `Guard::is_true(..)` reading a
290        // key a tool had written never fired — so a governed flow stalled at
291        // its first step with every downstream tool refused by a gate whose
292        // condition was in fact satisfied.
293        let state = gemini_adk_rs::State::new();
294        let _ = state.set("identity_verified", true);
295        let live = Live::builder().state(state.clone());
296        assert_eq!(
297            live.shared_state()
298                .and_then(|s| s.get::<bool>("identity_verified")),
299            Some(true),
300            "the builder must hold the caller's own state, not a copy or a fresh one"
301        );
302    }
303
304    #[test]
305    fn an_unconfigured_builder_reports_nothing() {
306        let live = Live::builder();
307        assert!(live.flow().is_none());
308        assert!(live.phases().is_empty());
309        assert!(live.initial_phase_name().is_none());
310        assert!(live.watched_keys().is_empty());
311        assert_eq!(live.extractor_count(), 0);
312        assert!(!live.has_persistence());
313        assert!(live.shared_state().is_none());
314    }
315}