gemini_adk_rs/tools/
transfer_to_agent.rs

1//! Transfer-to-agent tool — enables agent handoff in multi-agent systems.
2//!
3//! Mirrors ADK-Python's `TransferToAgentTool`. Provides enum-constrained
4//! agent names to prevent LLM hallucination of invalid agent names.
5
6use async_trait::async_trait;
7
8use crate::error::ToolError;
9use crate::tool::ToolFunction;
10
11/// Tool that transfers control to another agent.
12///
13/// The `agent_name` parameter is constrained to a set of valid agent names
14/// via JSON Schema enum, preventing the model from hallucinating invalid names.
15#[derive(Debug, Clone)]
16pub struct TransferToAgentTool {
17    /// Valid agent names that can be transferred to.
18    agent_names: Vec<String>,
19}
20
21impl TransferToAgentTool {
22    /// Create a new transfer tool with the given valid agent names.
23    pub fn new(agent_names: Vec<String>) -> Self {
24        Self { agent_names }
25    }
26
27    /// Returns the list of valid agent names.
28    pub fn agent_names(&self) -> &[String] {
29        &self.agent_names
30    }
31}
32
33#[async_trait]
34impl ToolFunction for TransferToAgentTool {
35    fn name(&self) -> &str {
36        "transfer_to_agent"
37    }
38
39    fn description(&self) -> &str {
40        "Transfer the question to another agent. Use this tool to hand off control \
41         to a more suitable agent based on their description."
42    }
43
44    fn parameters(&self) -> Option<serde_json::Value> {
45        Some(serde_json::json!({
46            "type": "object",
47            "properties": {
48                "agent_name": {
49                    "type": "string",
50                    "description": "The name of the agent to transfer to.",
51                    "enum": self.agent_names
52                }
53            },
54            "required": ["agent_name"]
55        }))
56    }
57
58    async fn call(&self, args: serde_json::Value) -> Result<serde_json::Value, ToolError> {
59        let agent_name = args
60            .get("agent_name")
61            .and_then(|v| v.as_str())
62            .ok_or_else(|| ToolError::InvalidArgs("Missing agent_name".into()))?;
63
64        if !self.agent_names.iter().any(|n| n == agent_name) {
65            return Err(ToolError::InvalidArgs(format!(
66                "Invalid agent name '{}'. Valid agents: {:?}",
67                agent_name, self.agent_names
68            )));
69        }
70
71        // The actual transfer is handled by the runtime checking
72        // the tool response for the transfer signal.
73        Ok(serde_json::json!({
74            "status": "transferred",
75            "agent_name": agent_name
76        }))
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83    use serde_json::json;
84
85    #[test]
86    fn parameters_include_enum() {
87        let tool = TransferToAgentTool::new(vec!["agent_a".into(), "agent_b".into()]);
88        let params = tool.parameters().unwrap();
89        let enums = params["properties"]["agent_name"]["enum"]
90            .as_array()
91            .unwrap();
92        assert_eq!(enums.len(), 2);
93        assert_eq!(enums[0], "agent_a");
94        assert_eq!(enums[1], "agent_b");
95    }
96
97    #[tokio::test]
98    async fn valid_transfer() {
99        let tool = TransferToAgentTool::new(vec!["support".into(), "billing".into()]);
100        let result = tool.call(json!({"agent_name": "support"})).await.unwrap();
101        assert_eq!(result["status"], "transferred");
102        assert_eq!(result["agent_name"], "support");
103    }
104
105    #[tokio::test]
106    async fn invalid_agent_name() {
107        let tool = TransferToAgentTool::new(vec!["support".into()]);
108        let result = tool.call(json!({"agent_name": "hacker"})).await;
109        assert!(result.is_err());
110    }
111
112    #[tokio::test]
113    async fn missing_agent_name() {
114        let tool = TransferToAgentTool::new(vec!["support".into()]);
115        let result = tool.call(json!({})).await;
116        assert!(result.is_err());
117    }
118
119    #[test]
120    fn agent_names_accessor() {
121        let tool = TransferToAgentTool::new(vec!["a".into(), "b".into()]);
122        assert_eq!(tool.agent_names(), &["a", "b"]);
123    }
124}