use crate::AgentTool;
use adk_core::{Agent, Result, Tool, ToolContext};
use async_trait::async_trait;
use serde_json::{Value, json};
use std::sync::Arc;
pub trait BypassMultiToolsLimit: Sized {
fn bypass_name(&self) -> String;
fn bypass_description(&self) -> String;
fn bypass_parameters_schema(&self) -> Value;
fn bypass_query_field(&self) -> String;
fn with_bypass_multi_tools_limit(self, agent: Arc<dyn Agent>) -> Arc<dyn Tool> {
Arc::new(BypassBuiltinTool::new(
self.bypass_name(),
self.bypass_description(),
self.bypass_parameters_schema(),
self.bypass_query_field(),
agent,
))
}
}
pub struct BypassBuiltinTool {
name: String,
description: String,
parameters_schema: Value,
query_field: String,
inner: AgentTool,
}
impl BypassBuiltinTool {
pub fn new(
name: impl Into<String>,
description: impl Into<String>,
parameters_schema: Value,
query_field: impl Into<String>,
agent: Arc<dyn Agent>,
) -> Self {
Self {
name: name.into(),
description: description.into(),
parameters_schema,
query_field: query_field.into(),
inner: AgentTool::new(agent).skip_summarization(true),
}
}
fn extract_query(&self, args: &Value) -> String {
if let Some(query) = args.get(&self.query_field).and_then(Value::as_str) {
return query.to_string();
}
match args {
Value::String(s) => s.clone(),
_ => serde_json::to_string(args).unwrap_or_default(),
}
}
}
#[async_trait]
impl Tool for BypassBuiltinTool {
fn name(&self) -> &str {
&self.name
}
fn description(&self) -> &str {
&self.description
}
fn is_builtin(&self) -> bool {
false
}
fn parameters_schema(&self) -> Option<Value> {
Some(self.parameters_schema.clone())
}
async fn execute(&self, ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value> {
let query = self.extract_query(&args);
self.inner.execute(ctx, json!({ "request": query })).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::builtin::{GeminiFileSearchTool, GoogleSearchTool, UrlContextTool};
use adk_core::{
Artifacts, CallbackContext, Content, Event, EventActions, InvocationContext, MemoryEntry,
ReadonlyContext,
};
use std::sync::Mutex;
struct MockSearchAgent;
#[async_trait]
impl Agent for MockSearchAgent {
fn name(&self) -> &str {
"google_search_agent"
}
fn description(&self) -> &str {
"Performs grounded Google search."
}
fn sub_agents(&self) -> &[Arc<dyn Agent>] {
&[]
}
async fn run(&self, _ctx: Arc<dyn InvocationContext>) -> Result<adk_core::EventStream> {
use async_stream::stream;
let s = stream! {
let mut event = Event::new("mock-inv");
event.author = "google_search_agent".to_string();
event.llm_response.content =
Some(Content::new("model").with_text("grounded answer"));
yield Ok(event);
};
Ok(Box::pin(s))
}
}
struct MockToolContext {
actions: Mutex<EventActions>,
content: Content,
}
impl MockToolContext {
fn new() -> Self {
Self { actions: Mutex::new(EventActions::default()), content: Content::new("user") }
}
}
#[async_trait]
impl ReadonlyContext for MockToolContext {
fn invocation_id(&self) -> &str {
"inv-1"
}
fn agent_name(&self) -> &str {
"test-agent"
}
fn user_id(&self) -> &str {
"user-1"
}
fn app_name(&self) -> &str {
"test-app"
}
fn session_id(&self) -> &str {
"session-1"
}
fn branch(&self) -> &str {
""
}
fn user_content(&self) -> &Content {
&self.content
}
}
#[async_trait]
impl CallbackContext for MockToolContext {
fn artifacts(&self) -> Option<Arc<dyn Artifacts>> {
None
}
}
#[async_trait]
impl ToolContext for MockToolContext {
fn function_call_id(&self) -> &str {
"call-1"
}
fn actions(&self) -> EventActions {
self.actions.lock().unwrap().clone()
}
fn set_actions(&self, actions: EventActions) {
*self.actions.lock().unwrap() = actions;
}
async fn search_memory(&self, _query: &str) -> Result<Vec<MemoryEntry>> {
Ok(vec![])
}
}
#[test]
fn bypass_reports_not_builtin() {
let tool = GoogleSearchTool::new().with_bypass_multi_tools_limit(Arc::new(MockSearchAgent));
assert!(!tool.is_builtin(), "bypass tool must report is_builtin() == false");
}
#[test]
fn bypass_declares_function_query_param() {
let tool = GoogleSearchTool::new().with_bypass_multi_tools_limit(Arc::new(MockSearchAgent));
assert_eq!(tool.name(), "google_search");
let schema = tool.parameters_schema().expect("bypass tool must declare a function schema");
assert_eq!(schema["type"], "object");
assert_eq!(schema["properties"]["query"]["type"], "string");
assert_eq!(schema["required"][0], "query");
let decl = tool.declaration();
assert!(decl.get("x-adk-gemini-tool").is_none());
assert!(decl.get("parameters").is_some());
}
#[tokio::test]
async fn bypass_executes_via_internal_agent() {
let tool = GoogleSearchTool::new().with_bypass_multi_tools_limit(Arc::new(MockSearchAgent));
let ctx = Arc::new(MockToolContext::new()) as Arc<dyn ToolContext>;
let result = tool
.execute(ctx, json!({ "query": "what is adk-rust" }))
.await
.expect("bypass execution should succeed");
assert_eq!(result["response"], "grounded answer");
}
#[test]
fn url_context_bypass_reports_not_builtin_and_declares_url_param() {
let tool = UrlContextTool::new().with_bypass_multi_tools_limit(Arc::new(MockSearchAgent));
assert!(!tool.is_builtin(), "bypassed url_context must report is_builtin() == false");
assert_eq!(tool.name(), "url_context");
let schema = tool.parameters_schema().expect("bypass tool must declare a function schema");
assert_eq!(schema["type"], "object");
assert_eq!(schema["properties"]["url"]["type"], "string");
assert_eq!(schema["required"][0], "url");
let decl = tool.declaration();
assert!(decl.get("x-adk-gemini-tool").is_none());
assert!(decl.get("parameters").is_some());
}
#[test]
fn file_search_bypass_reports_not_builtin_and_declares_query_param() {
let tool = GeminiFileSearchTool::new(["my-store"])
.with_bypass_multi_tools_limit(Arc::new(MockSearchAgent));
assert!(!tool.is_builtin(), "bypassed file_search must report is_builtin() == false");
assert_eq!(tool.name(), "gemini_file_search");
let schema = tool.parameters_schema().expect("bypass tool must declare a function schema");
assert_eq!(schema["type"], "object");
assert_eq!(schema["properties"]["query"]["type"], "string");
assert_eq!(schema["required"][0], "query");
let decl = tool.declaration();
assert!(decl.get("x-adk-gemini-tool").is_none());
assert!(decl.get("parameters").is_some());
}
#[test]
fn trait_path_is_uniform_across_tools() {
let tools: Vec<Arc<dyn Tool>> = vec![
GoogleSearchTool::new().with_bypass_multi_tools_limit(Arc::new(MockSearchAgent)),
UrlContextTool::new().with_bypass_multi_tools_limit(Arc::new(MockSearchAgent)),
GeminiFileSearchTool::new(["store"])
.with_bypass_multi_tools_limit(Arc::new(MockSearchAgent)),
];
for tool in &tools {
assert!(!tool.is_builtin(), "{} must not be built-in after bypass", tool.name());
assert!(
tool.parameters_schema().is_some(),
"{} must declare a function schema after bypass",
tool.name()
);
assert!(
tool.declaration().get("x-adk-gemini-tool").is_none(),
"{} must not retain built-in metadata after bypass",
tool.name()
);
}
}
#[tokio::test]
async fn url_context_bypass_executes_via_internal_agent() {
let tool = UrlContextTool::new().with_bypass_multi_tools_limit(Arc::new(MockSearchAgent));
let ctx = Arc::new(MockToolContext::new()) as Arc<dyn ToolContext>;
let result = tool
.execute(ctx, json!({ "url": "https://example.com" }))
.await
.expect("bypass execution should succeed");
assert_eq!(result["response"], "grounded answer");
}
}