gemini_adk_rs/
agent_tool.rs1use std::sync::Arc;
11
12use async_trait::async_trait;
13use serde_json::json;
14use tokio::sync::broadcast;
15
16use gemini_genai_rs::session::SessionEvent;
17
18use crate::agent::Agent;
19use crate::agent_session::{AgentSession, NoOpSessionWriter};
20use crate::context::{AgentEvent, InvocationContext};
21use crate::error::ToolError;
22use crate::tool::ToolFunction;
23
24pub struct AgentTool {
30 agent: Arc<dyn Agent>,
31 description: String,
32 parameters: Option<serde_json::Value>,
33}
34
35impl AgentTool {
36 pub fn new(agent: impl Agent + 'static) -> Self {
38 let description = format!("Delegate to the {} agent", agent.name());
39 Self {
40 agent: Arc::new(agent),
41 description,
42 parameters: Some(json!({
43 "type": "object",
44 "properties": {
45 "request": {
46 "type": "string",
47 "description": "The request to send to the agent"
48 }
49 },
50 "required": ["request"]
51 })),
52 }
53 }
54
55 pub fn from_arc(agent: Arc<dyn Agent>) -> Self {
57 let description = format!("Delegate to the {} agent", agent.name());
58 Self {
59 agent,
60 description,
61 parameters: Some(json!({
62 "type": "object",
63 "properties": {
64 "request": {
65 "type": "string",
66 "description": "The request to send to the agent"
67 }
68 },
69 "required": ["request"]
70 })),
71 }
72 }
73
74 pub fn with_description(mut self, desc: impl Into<String>) -> Self {
76 self.description = desc.into();
77 self
78 }
79
80 pub fn with_parameters(mut self, params: serde_json::Value) -> Self {
82 self.parameters = Some(params);
83 self
84 }
85}
86
87#[async_trait]
88impl ToolFunction for AgentTool {
89 fn name(&self) -> &str {
90 self.agent.name()
91 }
92
93 fn description(&self) -> &str {
94 &self.description
95 }
96
97 fn parameters(&self) -> Option<serde_json::Value> {
98 self.parameters.clone()
99 }
100
101 async fn call(&self, args: serde_json::Value) -> Result<serde_json::Value, ToolError> {
102 let start = std::time::Instant::now();
103 let agent_name = self.agent.name().to_string();
104
105 crate::telemetry::logging::log_agent_tool_dispatch("parent", &agent_name);
107
108 let (event_tx, _) = broadcast::channel::<SessionEvent>(64);
110 let noop_writer: Arc<dyn gemini_genai_rs::session::SessionWriter> =
111 Arc::new(NoOpSessionWriter);
112 let isolated_session = AgentSession::from_writer(noop_writer, event_tx);
113
114 if let Some(request) = args.get("request").and_then(|r| r.as_str()) {
116 let _ = isolated_session.state().set("request_text", request);
117 }
118 let _ = isolated_session.state().set("request", &args);
119
120 let mut ctx = InvocationContext::new(isolated_session);
122
123 let mut events = ctx.subscribe();
125
126 let agent = self.agent.clone();
128 let run_result = tokio::spawn(async move { agent.run_live(&mut ctx).await }).await;
129
130 let mut output_parts = Vec::new();
132 while let Ok(event) = events.try_recv() {
133 match event {
134 AgentEvent::Session(SessionEvent::TextDelta(text)) => {
135 output_parts.push(text);
136 }
137 AgentEvent::Session(SessionEvent::TextComplete(text)) => {
138 if output_parts.is_empty() {
139 output_parts.push(text);
140 }
141 }
144 _ => {}
145 }
146 }
147
148 let elapsed = start.elapsed();
149 crate::telemetry::metrics::record_agent_tool_dispatch(
150 "parent",
151 &agent_name,
152 elapsed.as_millis() as f64,
153 );
154
155 match run_result {
157 Ok(Ok(())) => {
158 let output = if output_parts.is_empty() {
159 json!({"status": "completed"})
160 } else {
161 json!({"result": output_parts.join("")})
162 };
163 Ok(output)
164 }
165 Ok(Err(e)) => Err(ToolError::ExecutionFailed(format!(
166 "Agent '{agent_name}' failed: {e}"
167 ))),
168 Err(e) => Err(ToolError::ExecutionFailed(format!(
169 "Agent '{agent_name}' task panicked: {e}"
170 ))),
171 }
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use crate::error::AgentError;
179
180 struct EchoAgent {
181 name: String,
182 }
183
184 #[async_trait]
185 impl Agent for EchoAgent {
186 fn name(&self) -> &str {
187 &self.name
188 }
189 async fn run_live(&self, ctx: &mut InvocationContext) -> Result<(), AgentError> {
190 let request = ctx
192 .state()
193 .get::<String>("request_text")
194 .unwrap_or_else(|| "no request".to_string());
195 ctx.emit(AgentEvent::Session(SessionEvent::TextDelta(format!(
196 "Echo: {request}"
197 ))));
198 ctx.emit(AgentEvent::Session(SessionEvent::TurnComplete));
199 Ok(())
200 }
201 }
202
203 struct FailingAgent;
204
205 #[async_trait]
206 impl Agent for FailingAgent {
207 fn name(&self) -> &str {
208 "failing"
209 }
210 async fn run_live(&self, _ctx: &mut InvocationContext) -> Result<(), AgentError> {
211 Err(AgentError::Other("intentional failure".to_string()))
212 }
213 }
214
215 struct SilentAgent;
216
217 #[async_trait]
218 impl Agent for SilentAgent {
219 fn name(&self) -> &str {
220 "silent"
221 }
222 async fn run_live(&self, _ctx: &mut InvocationContext) -> Result<(), AgentError> {
223 Ok(())
224 }
225 }
226
227 #[tokio::test]
228 async fn agent_tool_runs_agent_in_isolation() {
229 let agent = EchoAgent {
230 name: "echo".to_string(),
231 };
232 let tool = AgentTool::new(agent);
233
234 assert_eq!(tool.name(), "echo");
235 assert!(tool.description().contains("echo"));
236 }
237
238 #[tokio::test]
239 async fn agent_tool_collects_text_output() {
240 let agent = EchoAgent {
241 name: "echo".to_string(),
242 };
243 let tool = AgentTool::new(agent);
244
245 let result = tool.call(json!({"request": "hello world"})).await.unwrap();
246 assert_eq!(result["result"], "Echo: hello world");
247 }
248
249 #[tokio::test]
250 async fn agent_tool_propagates_errors() {
251 let tool = AgentTool::new(FailingAgent);
252 let result = tool.call(json!({"request": "test"})).await;
253 assert!(result.is_err());
254 let err = result.unwrap_err();
255 match err {
256 ToolError::ExecutionFailed(msg) => {
257 assert!(msg.contains("intentional failure"));
258 }
259 other => panic!("expected ExecutionFailed, got: {other:?}"),
260 }
261 }
262
263 #[tokio::test]
264 async fn agent_tool_returns_completed_when_no_output() {
265 let tool = AgentTool::new(SilentAgent);
266 let result = tool.call(json!({"request": "test"})).await.unwrap();
267 assert_eq!(result["status"], "completed");
268 }
269
270 #[tokio::test]
271 async fn agent_tool_state_injection() {
272 struct StateCheckAgent;
274
275 #[async_trait]
276 impl Agent for StateCheckAgent {
277 fn name(&self) -> &str {
278 "state_check"
279 }
280 async fn run_live(&self, ctx: &mut InvocationContext) -> Result<(), AgentError> {
281 let request_text = ctx.state().get::<String>("request_text");
282 let request = ctx.state().get::<serde_json::Value>("request");
283
284 assert!(request_text.is_some());
285 assert!(request.is_some());
286 assert_eq!(request_text.unwrap(), "check state");
287
288 ctx.emit(AgentEvent::Session(SessionEvent::TextDelta(
289 "state ok".to_string(),
290 )));
291 Ok(())
292 }
293 }
294
295 let tool = AgentTool::new(StateCheckAgent);
296 let result = tool.call(json!({"request": "check state"})).await.unwrap();
297 assert_eq!(result["result"], "state ok");
298 }
299
300 #[tokio::test]
301 async fn agent_tool_with_custom_description() {
302 let tool = AgentTool::new(SilentAgent).with_description("Custom description");
303 assert_eq!(tool.description(), "Custom description");
304 }
305
306 #[tokio::test]
307 async fn agent_tool_with_custom_parameters() {
308 let params = json!({
309 "type": "object",
310 "properties": {
311 "query": { "type": "string" }
312 }
313 });
314 let tool = AgentTool::new(SilentAgent).with_parameters(params.clone());
315 assert_eq!(tool.parameters().unwrap(), params);
316 }
317}