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}