adk_tool/builtin/
bypass.rs1use crate::AgentTool;
22use adk_core::{Agent, Result, Tool, ToolContext};
23use async_trait::async_trait;
24use serde_json::{Value, json};
25use std::sync::Arc;
26
27pub trait BypassMultiToolsLimit: Sized {
61 fn bypass_name(&self) -> String;
65
66 fn bypass_description(&self) -> String;
68
69 fn bypass_parameters_schema(&self) -> Value;
71
72 fn bypass_query_field(&self) -> String;
75
76 fn with_bypass_multi_tools_limit(self, agent: Arc<dyn Agent>) -> Arc<dyn Tool> {
82 Arc::new(BypassBuiltinTool::new(
83 self.bypass_name(),
84 self.bypass_description(),
85 self.bypass_parameters_schema(),
86 self.bypass_query_field(),
87 agent,
88 ))
89 }
90}
91
92pub struct BypassBuiltinTool {
111 name: String,
112 description: String,
113 parameters_schema: Value,
114 query_field: String,
115 inner: AgentTool,
116}
117
118impl BypassBuiltinTool {
119 pub fn new(
128 name: impl Into<String>,
129 description: impl Into<String>,
130 parameters_schema: Value,
131 query_field: impl Into<String>,
132 agent: Arc<dyn Agent>,
133 ) -> Self {
134 Self {
135 name: name.into(),
136 description: description.into(),
137 parameters_schema,
138 query_field: query_field.into(),
139 inner: AgentTool::new(agent).skip_summarization(true),
140 }
141 }
142
143 fn extract_query(&self, args: &Value) -> String {
145 if let Some(query) = args.get(&self.query_field).and_then(Value::as_str) {
146 return query.to_string();
147 }
148 match args {
149 Value::String(s) => s.clone(),
150 _ => serde_json::to_string(args).unwrap_or_default(),
151 }
152 }
153}
154
155#[async_trait]
156impl Tool for BypassBuiltinTool {
157 fn name(&self) -> &str {
158 &self.name
159 }
160
161 fn description(&self) -> &str {
162 &self.description
163 }
164
165 fn is_builtin(&self) -> bool {
168 false
169 }
170
171 fn parameters_schema(&self) -> Option<Value> {
172 Some(self.parameters_schema.clone())
173 }
174
175 async fn execute(&self, ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value> {
176 let query = self.extract_query(&args);
179 self.inner.execute(ctx, json!({ "request": query })).await
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use crate::builtin::{GeminiFileSearchTool, GoogleSearchTool, UrlContextTool};
187 use adk_core::{
188 Artifacts, CallbackContext, Content, Event, EventActions, InvocationContext, MemoryEntry,
189 ReadonlyContext,
190 };
191 use std::sync::Mutex;
192
193 struct MockSearchAgent;
195
196 #[async_trait]
197 impl Agent for MockSearchAgent {
198 fn name(&self) -> &str {
199 "google_search_agent"
200 }
201
202 fn description(&self) -> &str {
203 "Performs grounded Google search."
204 }
205
206 fn sub_agents(&self) -> &[Arc<dyn Agent>] {
207 &[]
208 }
209
210 async fn run(&self, _ctx: Arc<dyn InvocationContext>) -> Result<adk_core::EventStream> {
211 use async_stream::stream;
212 let s = stream! {
213 let mut event = Event::new("mock-inv");
214 event.author = "google_search_agent".to_string();
215 event.llm_response.content =
216 Some(Content::new("model").with_text("grounded answer"));
217 yield Ok(event);
218 };
219 Ok(Box::pin(s))
220 }
221 }
222
223 struct MockToolContext {
224 actions: Mutex<EventActions>,
225 content: Content,
226 }
227
228 impl MockToolContext {
229 fn new() -> Self {
230 Self { actions: Mutex::new(EventActions::default()), content: Content::new("user") }
231 }
232 }
233
234 #[async_trait]
235 impl ReadonlyContext for MockToolContext {
236 fn invocation_id(&self) -> &str {
237 "inv-1"
238 }
239 fn agent_name(&self) -> &str {
240 "test-agent"
241 }
242 fn user_id(&self) -> &str {
243 "user-1"
244 }
245 fn app_name(&self) -> &str {
246 "test-app"
247 }
248 fn session_id(&self) -> &str {
249 "session-1"
250 }
251 fn branch(&self) -> &str {
252 ""
253 }
254 fn user_content(&self) -> &Content {
255 &self.content
256 }
257 }
258
259 #[async_trait]
260 impl CallbackContext for MockToolContext {
261 fn artifacts(&self) -> Option<Arc<dyn Artifacts>> {
262 None
263 }
264 }
265
266 #[async_trait]
267 impl ToolContext for MockToolContext {
268 fn function_call_id(&self) -> &str {
269 "call-1"
270 }
271 fn actions(&self) -> EventActions {
272 self.actions.lock().unwrap().clone()
273 }
274 fn set_actions(&self, actions: EventActions) {
275 *self.actions.lock().unwrap() = actions;
276 }
277 async fn search_memory(&self, _query: &str) -> Result<Vec<MemoryEntry>> {
278 Ok(vec![])
279 }
280 }
281
282 #[test]
283 fn bypass_reports_not_builtin() {
284 let tool = GoogleSearchTool::new().with_bypass_multi_tools_limit(Arc::new(MockSearchAgent));
285 assert!(!tool.is_builtin(), "bypass tool must report is_builtin() == false");
286 }
287
288 #[test]
289 fn bypass_declares_function_query_param() {
290 let tool = GoogleSearchTool::new().with_bypass_multi_tools_limit(Arc::new(MockSearchAgent));
291
292 assert_eq!(tool.name(), "google_search");
293
294 let schema = tool.parameters_schema().expect("bypass tool must declare a function schema");
295 assert_eq!(schema["type"], "object");
296 assert_eq!(schema["properties"]["query"]["type"], "string");
297 assert_eq!(schema["required"][0], "query");
298
299 let decl = tool.declaration();
301 assert!(decl.get("x-adk-gemini-tool").is_none());
302 assert!(decl.get("parameters").is_some());
303 }
304
305 #[tokio::test]
306 async fn bypass_executes_via_internal_agent() {
307 let tool = GoogleSearchTool::new().with_bypass_multi_tools_limit(Arc::new(MockSearchAgent));
308 let ctx = Arc::new(MockToolContext::new()) as Arc<dyn ToolContext>;
309
310 let result = tool
311 .execute(ctx, json!({ "query": "what is adk-rust" }))
312 .await
313 .expect("bypass execution should succeed");
314
315 assert_eq!(result["response"], "grounded answer");
316 }
317
318 #[test]
319 fn url_context_bypass_reports_not_builtin_and_declares_url_param() {
320 let tool = UrlContextTool::new().with_bypass_multi_tools_limit(Arc::new(MockSearchAgent));
321
322 assert!(!tool.is_builtin(), "bypassed url_context must report is_builtin() == false");
323 assert_eq!(tool.name(), "url_context");
324
325 let schema = tool.parameters_schema().expect("bypass tool must declare a function schema");
326 assert_eq!(schema["type"], "object");
327 assert_eq!(schema["properties"]["url"]["type"], "string");
328 assert_eq!(schema["required"][0], "url");
329
330 let decl = tool.declaration();
332 assert!(decl.get("x-adk-gemini-tool").is_none());
333 assert!(decl.get("parameters").is_some());
334 }
335
336 #[test]
337 fn file_search_bypass_reports_not_builtin_and_declares_query_param() {
338 let tool = GeminiFileSearchTool::new(["my-store"])
339 .with_bypass_multi_tools_limit(Arc::new(MockSearchAgent));
340
341 assert!(!tool.is_builtin(), "bypassed file_search must report is_builtin() == false");
342 assert_eq!(tool.name(), "gemini_file_search");
343
344 let schema = tool.parameters_schema().expect("bypass tool must declare a function schema");
345 assert_eq!(schema["type"], "object");
346 assert_eq!(schema["properties"]["query"]["type"], "string");
347 assert_eq!(schema["required"][0], "query");
348
349 let decl = tool.declaration();
350 assert!(decl.get("x-adk-gemini-tool").is_none());
351 assert!(decl.get("parameters").is_some());
352 }
353
354 #[test]
358 fn trait_path_is_uniform_across_tools() {
359 let tools: Vec<Arc<dyn Tool>> = vec![
360 GoogleSearchTool::new().with_bypass_multi_tools_limit(Arc::new(MockSearchAgent)),
361 UrlContextTool::new().with_bypass_multi_tools_limit(Arc::new(MockSearchAgent)),
362 GeminiFileSearchTool::new(["store"])
363 .with_bypass_multi_tools_limit(Arc::new(MockSearchAgent)),
364 ];
365
366 for tool in &tools {
367 assert!(!tool.is_builtin(), "{} must not be built-in after bypass", tool.name());
368 assert!(
369 tool.parameters_schema().is_some(),
370 "{} must declare a function schema after bypass",
371 tool.name()
372 );
373 assert!(
374 tool.declaration().get("x-adk-gemini-tool").is_none(),
375 "{} must not retain built-in metadata after bypass",
376 tool.name()
377 );
378 }
379 }
380
381 #[tokio::test]
382 async fn url_context_bypass_executes_via_internal_agent() {
383 let tool = UrlContextTool::new().with_bypass_multi_tools_limit(Arc::new(MockSearchAgent));
384 let ctx = Arc::new(MockToolContext::new()) as Arc<dyn ToolContext>;
385
386 let result = tool
387 .execute(ctx, json!({ "url": "https://example.com" }))
388 .await
389 .expect("bypass execution should succeed");
390
391 assert_eq!(result["response"], "grounded answer");
392 }
393}