1use std::sync::Arc;
7
8use gemini_adk_rs::error::ConfigError;
9use gemini_adk_rs::llm::BaseLlm;
10use gemini_adk_rs::middleware::Middleware;
11use gemini_adk_rs::text::{LlmTextAgent, TextAgent};
12use gemini_adk_rs::tool::{ToolDispatcher, ToolFunction, ToolKind};
13use gemini_genai_rs::prelude::{Modality, ModelId, Tool, Voice};
14
15use crate::compose::context::ContextComposite;
16use crate::compose::guards::GuardComposite;
17use crate::compose::middleware::MiddlewareComposite;
18use crate::compose::tools::ToolComposite;
19
20type LlmProviderFn = Arc<dyn Fn(&gemini_adk_rs::State) -> Arc<dyn BaseLlm> + Send + Sync>;
21
22#[derive(Clone)]
24struct AgentBuilderInner {
25 name: String,
26 model: Option<ModelId>,
27 instruction: Option<String>,
28 instruction_provider: Option<Arc<dyn gemini_adk_rs::instruction::InstructionProvider>>,
29 llm_provider: Option<LlmProviderFn>,
30 voice: Option<Voice>,
31 temperature: Option<f32>,
32 top_p: Option<f32>,
33 top_k: Option<u32>,
34 max_output_tokens: Option<u32>,
35 stop_sequences: Vec<String>,
36 response_modalities: Option<Vec<Modality>>,
37 thinking_budget: Option<u32>,
38 tools: Vec<ToolEntry>,
39 built_in_tools: Vec<Tool>,
40 writes: Vec<String>,
41 reads: Vec<String>,
42 sub_agents: Vec<AgentBuilder>,
43 isolate: bool,
44 stay: bool,
45 description: Option<String>,
46 output_schema: Option<serde_json::Value>,
47 output_key: Option<String>,
48 transfer_to_agent: Option<String>,
49 middleware_layers: Vec<Arc<dyn Middleware>>,
51 config_errors: Vec<String>,
54}
55
56#[derive(Clone)]
58pub enum ToolEntry {
59 Runtime(Arc<dyn ToolEntryTrait>),
61 Declaration(Tool),
63}
64
65pub trait ToolEntryTrait: Send + Sync + 'static {
67 fn name(&self) -> &str;
69 fn to_tool_kind(&self) -> ToolKind;
71}
72
73#[derive(Clone)]
154pub struct AgentBuilder {
155 inner: Arc<AgentBuilderInner>,
156}
157
158impl AgentBuilder {
159 pub fn new(name: impl Into<String>) -> Self {
161 Self {
162 inner: Arc::new(AgentBuilderInner {
163 name: name.into(),
164 model: None,
165 instruction: None,
166 instruction_provider: None,
167 llm_provider: None,
168 voice: None,
169 temperature: None,
170 top_p: None,
171 top_k: None,
172 max_output_tokens: None,
173 stop_sequences: Vec::new(),
174 response_modalities: None,
175 thinking_budget: None,
176 tools: Vec::new(),
177 built_in_tools: Vec::new(),
178 writes: Vec::new(),
179 reads: Vec::new(),
180 sub_agents: Vec::new(),
181 isolate: false,
182 stay: false,
183 description: None,
184 output_schema: None,
185 output_key: None,
186 transfer_to_agent: None,
187 middleware_layers: Vec::new(),
188 config_errors: Vec::new(),
189 }),
190 }
191 }
192
193 fn mutate(&self) -> AgentBuilderInner {
196 (*self.inner).clone()
197 }
198
199 fn with(inner: AgentBuilderInner) -> Self {
200 Self {
201 inner: Arc::new(inner),
202 }
203 }
204
205 pub fn name(&self) -> &str {
209 &self.inner.name
210 }
211
212 pub fn get_model(&self) -> Option<&ModelId> {
214 self.inner.model.as_ref()
215 }
216
217 pub fn get_instruction(&self) -> Option<&str> {
219 self.inner.instruction.as_deref()
220 }
221
222 pub fn get_voice(&self) -> Option<&Voice> {
224 self.inner.voice.as_ref()
225 }
226
227 pub fn get_temperature(&self) -> Option<f32> {
229 self.inner.temperature
230 }
231
232 pub fn is_text_only(&self) -> bool {
234 self.inner
235 .response_modalities
236 .as_ref()
237 .map(|m| m == &[Modality::Text])
238 .unwrap_or(false)
239 }
240
241 pub fn get_thinking_budget(&self) -> Option<u32> {
243 self.inner.thinking_budget
244 }
245
246 pub fn get_writes(&self) -> &[String] {
248 &self.inner.writes
249 }
250
251 pub fn get_reads(&self) -> &[String] {
253 &self.inner.reads
254 }
255
256 pub fn get_sub_agents(&self) -> &[AgentBuilder] {
258 &self.inner.sub_agents
259 }
260
261 pub fn is_isolated(&self) -> bool {
263 self.inner.isolate
264 }
265
266 pub fn is_stay(&self) -> bool {
268 self.inner.stay
269 }
270
271 pub fn tool_count(&self) -> usize {
273 self.inner.tools.len() + self.inner.built_in_tools.len()
274 }
275
276 pub fn get_top_p(&self) -> Option<f32> {
278 self.inner.top_p
279 }
280
281 pub fn get_top_k(&self) -> Option<u32> {
283 self.inner.top_k
284 }
285
286 pub fn get_max_output_tokens(&self) -> Option<u32> {
288 self.inner.max_output_tokens
289 }
290
291 pub fn get_stop_sequences(&self) -> &[String] {
293 &self.inner.stop_sequences
294 }
295
296 pub fn get_description(&self) -> Option<&str> {
298 self.inner.description.as_deref()
299 }
300
301 pub fn get_output_schema(&self) -> Option<&serde_json::Value> {
303 self.inner.output_schema.as_ref()
304 }
305
306 pub fn get_output_key(&self) -> Option<&str> {
308 self.inner.output_key.as_deref()
309 }
310
311 pub fn get_transfer_to(&self) -> Option<&str> {
313 self.inner.transfer_to_agent.as_deref()
314 }
315
316 pub fn middleware_layer_count(&self) -> usize {
318 self.inner.middleware_layers.len()
319 }
320
321 pub fn model(self, model: ModelId) -> Self {
325 let mut inner = self.mutate();
326 inner.model = Some(model);
327 Self::with(inner)
328 }
329
330 pub fn instruction(self, inst: impl Into<String>) -> Self {
332 let mut inner = self.mutate();
333 inner.instruction = Some(inst.into());
334 Self::with(inner)
335 }
336
337 pub fn instruction_provider(
342 self,
343 provider: impl gemini_adk_rs::instruction::InstructionProvider + 'static,
344 ) -> Self {
345 let mut inner = self.mutate();
346 inner.instruction_provider = Some(Arc::new(provider));
347 Self::with(inner)
348 }
349
350 pub fn llm_provider(
355 self,
356 provider: impl Fn(&gemini_adk_rs::State) -> Arc<dyn BaseLlm> + Send + Sync + 'static,
357 ) -> Self {
358 let mut inner = self.mutate();
359 inner.llm_provider = Some(Arc::new(provider));
360 Self::with(inner)
361 }
362
363 pub fn voice(self, voice: Voice) -> Self {
365 let mut inner = self.mutate();
366 inner.voice = Some(voice);
367 Self::with(inner)
368 }
369
370 pub fn temperature(self, t: f32) -> Self {
372 let mut inner = self.mutate();
373 inner.temperature = Some(t);
374 Self::with(inner)
375 }
376
377 pub fn text_only(self) -> Self {
379 let mut inner = self.mutate();
380 inner.response_modalities = Some(vec![Modality::Text]);
381 Self::with(inner)
382 }
383
384 pub fn response_modalities(self, modalities: Vec<Modality>) -> Self {
386 let mut inner = self.mutate();
387 inner.response_modalities = Some(modalities);
388 Self::with(inner)
389 }
390
391 pub fn thinking(self, budget: u32) -> Self {
393 let mut inner = self.mutate();
394 inner.thinking_budget = Some(budget);
395 Self::with(inner)
396 }
397
398 pub fn url_context(self) -> Self {
400 let mut inner = self.mutate();
401 inner.built_in_tools.push(Tool::url_context());
402 Self::with(inner)
403 }
404
405 pub fn google_search(self) -> Self {
407 let mut inner = self.mutate();
408 inner.built_in_tools.push(Tool::google_search());
409 Self::with(inner)
410 }
411
412 pub fn code_execution(self) -> Self {
414 let mut inner = self.mutate();
415 inner.built_in_tools.push(Tool::code_execution());
416 Self::with(inner)
417 }
418
419 pub fn writes(self, key: impl Into<String>) -> Self {
421 let mut inner = self.mutate();
422 inner.writes.push(key.into());
423 Self::with(inner)
424 }
425
426 pub fn reads(self, key: impl Into<String>) -> Self {
428 let mut inner = self.mutate();
429 inner.reads.push(key.into());
430 Self::with(inner)
431 }
432
433 pub fn sub_agent(self, agent: AgentBuilder) -> Self {
435 let mut inner = self.mutate();
436 inner.sub_agents.push(agent);
437 Self::with(inner)
438 }
439
440 pub fn isolate(self) -> Self {
442 let mut inner = self.mutate();
443 inner.isolate = true;
444 Self::with(inner)
445 }
446
447 pub fn stay(self) -> Self {
449 let mut inner = self.mutate();
450 inner.stay = true;
451 Self::with(inner)
452 }
453
454 pub fn top_p(self, p: f32) -> Self {
456 let mut inner = self.mutate();
457 inner.top_p = Some(p);
458 Self::with(inner)
459 }
460
461 pub fn top_k(self, k: u32) -> Self {
463 let mut inner = self.mutate();
464 inner.top_k = Some(k);
465 Self::with(inner)
466 }
467
468 pub fn max_output_tokens(self, n: u32) -> Self {
470 let mut inner = self.mutate();
471 inner.max_output_tokens = Some(n);
472 Self::with(inner)
473 }
474
475 pub fn stop_sequences(self, seqs: Vec<String>) -> Self {
477 let mut inner = self.mutate();
478 inner.stop_sequences = seqs;
479 Self::with(inner)
480 }
481
482 pub fn description(self, desc: impl Into<String>) -> Self {
484 let mut inner = self.mutate();
485 inner.description = Some(desc.into());
486 Self::with(inner)
487 }
488
489 pub fn output_schema(self, schema: serde_json::Value) -> Self {
491 let mut inner = self.mutate();
492 inner.output_schema = Some(schema);
493 Self::with(inner)
494 }
495
496 pub fn output_key(self, key: impl Into<String>) -> Self {
498 let mut inner = self.mutate();
499 inner.output_key = Some(key.into());
500 Self::with(inner)
501 }
502
503 pub fn transfer_to(self, agent_name: impl Into<String>) -> Self {
505 let mut inner = self.mutate();
506 inner.transfer_to_agent = Some(agent_name.into());
507 Self::with(inner)
508 }
509
510 pub fn instruct(self, inst: impl Into<String>) -> Self {
514 self.instruction(inst)
515 }
516
517 pub fn describe(self, desc: impl Into<String>) -> Self {
519 self.description(desc)
520 }
521
522 pub fn tool(self, f: impl ToolFunction + 'static) -> Self {
536 self.tools(ToolComposite::from_function(Arc::new(f)))
537 }
538
539 pub fn tools(self, tools: impl Into<ToolComposite>) -> Self {
554 use crate::compose::tools::{DeferredTool, ToolResolution};
555 let mut inner = self.mutate();
556 for entry in tools.into().entries {
557 match entry.classify() {
558 ToolResolution::Runtime(f) => {
559 inner
560 .tools
561 .push(ToolEntry::Runtime(Arc::new(ToolFunctionEntry(f))));
562 }
563 ToolResolution::BuiltIn(t) => {
564 inner.built_in_tools.push(t);
565 }
566 ToolResolution::Agent {
567 name,
568 description,
569 agent,
570 } => {
571 let tool = gemini_adk_rs::TextAgentTool::from_arc(
573 name,
574 description,
575 agent,
576 gemini_adk_rs::State::new(),
577 );
578 inner
579 .tools
580 .push(ToolEntry::Runtime(Arc::new(ToolFunctionEntry(Arc::new(
581 tool,
582 )))));
583 }
584 ToolResolution::Deferred(DeferredTool::Mcp { params }) => {
585 inner.config_errors.push(format!(
591 "T::mcp({params:?}) cannot be attached to a text AgentBuilder: MCP \
592 toolsets need an async connection, which only a Live session performs \
593 (`Live::builder().tools(T::mcp(..))`)"
594 ));
595 }
596 }
597 }
598 Self::with(inner)
599 }
600
601 pub fn guard(self, guard: impl Into<GuardComposite>) -> Self {
615 let mut inner = self.mutate();
616 inner.middleware_layers.push(guard.into().into_middleware());
617 Self::with(inner)
618 }
619
620 pub fn context(self, policy: impl Into<ContextComposite>) -> Self {
632 let mut inner = self.mutate();
633 inner
634 .middleware_layers
635 .push(policy.into().into_middleware());
636 Self::with(inner)
637 }
638
639 pub fn no_peers(self) -> Self {
641 self.isolate()
642 }
643
644 pub fn middleware(self, middleware: impl Into<MiddlewareComposite>) -> Self {
659 let mut inner = self.mutate();
660 inner.middleware_layers.extend(middleware.into().layers);
661 Self::with(inner)
662 }
663
664 pub fn build(self, llm: Arc<dyn BaseLlm>) -> Result<Arc<dyn TextAgent>, ConfigError> {
693 if !self.inner.config_errors.is_empty() {
694 return Err(ConfigError {
695 issues: self.inner.config_errors.clone(),
696 });
697 }
698 let mut agent = LlmTextAgent::new(&self.inner.name, llm);
699
700 if let Some(inst) = &self.inner.instruction {
701 agent = agent.instruction(inst);
702 }
703 if let Some(provider) = &self.inner.instruction_provider {
704 agent = agent.instruction_provider(provider.clone());
705 }
706 if let Some(provider) = &self.inner.llm_provider {
707 let provider_clone = provider.clone();
708 agent = agent.llm_provider(move |state| provider_clone(state));
709 }
710 if let Some(t) = self.inner.temperature {
711 agent = agent.temperature(t);
712 }
713 if let Some(n) = self.inner.max_output_tokens {
714 agent = agent.max_output_tokens(n);
715 }
716
717 if !self.inner.tools.is_empty() {
719 let mut dispatcher = ToolDispatcher::new();
720 for entry in &self.inner.tools {
721 match entry {
722 ToolEntry::Runtime(t) => {
723 let kind = t.to_tool_kind();
724 match kind {
725 ToolKind::Function(f) => dispatcher.register_function(f),
726 ToolKind::Streaming(s) => dispatcher.register_streaming(s),
727 ToolKind::InputStream(i) => dispatcher.register_input_streaming(i),
728 }
729 }
730 ToolEntry::Declaration(_) => {
731 }
734 }
735 }
736 if !dispatcher.is_empty() {
737 agent = agent.tools(Arc::new(dispatcher));
738 }
739 }
740
741 for mw in &self.inner.middleware_layers {
743 agent = agent.add_middleware(mw.clone());
744 }
745
746 Ok(Arc::new(agent))
747 }
748}
749
750#[derive(Clone)]
752struct ToolFunctionEntry(Arc<dyn ToolFunction>);
753
754impl ToolEntryTrait for ToolFunctionEntry {
755 fn name(&self) -> &str {
756 self.0.name()
757 }
758
759 fn to_tool_kind(&self) -> ToolKind {
760 ToolKind::Function(self.0.clone())
761 }
762}
763
764impl std::fmt::Debug for AgentBuilder {
765 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
766 f.debug_struct("AgentBuilder")
767 .field("name", &self.inner.name)
768 .field("model", &self.inner.model)
769 .field("instruction", &self.inner.instruction)
770 .field("temperature", &self.inner.temperature)
771 .field("text_only", &self.is_text_only())
772 .field("tool_count", &self.tool_count())
773 .field("sub_agents", &self.inner.sub_agents.len())
774 .finish()
775 }
776}
777
778#[cfg(test)]
779mod tests {
780 use super::*;
781 use async_trait::async_trait;
782 use gemini_adk_rs::llm::{LlmError, LlmRequest, LlmResponse};
783 use gemini_genai_rs::prelude::{Content, Part, Role};
784
785 struct MockLlm(String);
787
788 #[async_trait]
789 impl BaseLlm for MockLlm {
790 fn model_id(&self) -> &str {
791 "mock"
792 }
793 async fn generate(&self, _req: LlmRequest) -> Result<LlmResponse, LlmError> {
794 Ok(LlmResponse {
795 content: Content {
796 role: Some(Role::Model),
797 parts: vec![Part::Text {
798 text: self.0.clone(),
799 }],
800 },
801 finish_reason: Some("STOP".into()),
802 usage: None,
803 })
804 }
805 }
806
807 #[test]
808 fn builder_creates_with_name() {
809 let b = AgentBuilder::new("test-agent");
810 assert_eq!(b.name(), "test-agent");
811 }
812
813 #[test]
814 fn fluent_chaining_works() {
815 let b = AgentBuilder::new("agent")
816 .instruction("Be helpful")
817 .temperature(0.7)
818 .model(ModelId::LIVE_2_5_FLASH_NATIVE_AUDIO);
819
820 assert_eq!(b.get_instruction(), Some("Be helpful"));
821 assert_eq!(b.get_temperature(), Some(0.7));
822 assert_eq!(b.get_model(), Some(&ModelId::LIVE_2_5_FLASH_NATIVE_AUDIO));
823 }
824
825 #[test]
826 fn copy_on_write_clone_independence() {
827 let base = AgentBuilder::new("base").temperature(0.5);
828 let variant = base.clone().temperature(0.9);
829
830 assert_eq!(base.get_temperature(), Some(0.5));
832 assert_eq!(variant.get_temperature(), Some(0.9));
834 }
835
836 #[test]
837 fn text_only_sets_modalities() {
838 let b = AgentBuilder::new("text").text_only();
839 assert!(b.is_text_only());
840 }
841
842 #[test]
843 fn url_context_adds_tool() {
844 let b = AgentBuilder::new("search").url_context();
845 assert_eq!(b.tool_count(), 1);
846 }
847
848 #[test]
849 fn google_search_adds_tool() {
850 let b = AgentBuilder::new("search").google_search();
851 assert_eq!(b.tool_count(), 1);
852 }
853
854 #[test]
855 fn code_execution_adds_tool() {
856 let b = AgentBuilder::new("code").code_execution();
857 assert_eq!(b.tool_count(), 1);
858 }
859
860 #[test]
861 fn thinking_sets_budget() {
862 let b = AgentBuilder::new("thinker").thinking(2048);
863 assert_eq!(b.get_thinking_budget(), Some(2048));
864 }
865
866 #[test]
867 fn writes_and_reads_keys() {
868 let b = AgentBuilder::new("data").writes("output").reads("input");
869 assert_eq!(b.get_writes(), &["output"]);
870 assert_eq!(b.get_reads(), &["input"]);
871 }
872
873 #[test]
874 fn sub_agent_registration() {
875 let child = AgentBuilder::new("child");
876 let parent = AgentBuilder::new("parent").sub_agent(child);
877 assert_eq!(parent.get_sub_agents().len(), 1);
878 assert_eq!(parent.get_sub_agents()[0].name(), "child");
879 }
880
881 #[test]
882 fn isolate_and_stay() {
883 let b = AgentBuilder::new("agent").isolate().stay();
884 assert!(b.is_isolated());
885 assert!(b.is_stay());
886 }
887
888 #[test]
889 fn debug_display() {
890 let b = AgentBuilder::new("debug-test");
891 let debug = format!("{b:?}");
892 assert!(debug.contains("debug-test"));
893 }
894
895 #[test]
896 fn top_p_sets_value() {
897 let b = AgentBuilder::new("agent").top_p(0.95);
898 assert_eq!(b.get_top_p(), Some(0.95));
899 }
900
901 #[test]
902 fn top_k_sets_value() {
903 let b = AgentBuilder::new("agent").top_k(40);
904 assert_eq!(b.get_top_k(), Some(40));
905 }
906
907 #[test]
908 fn max_output_tokens_sets_value() {
909 let b = AgentBuilder::new("agent").max_output_tokens(4096);
910 assert_eq!(b.get_max_output_tokens(), Some(4096));
911 }
912
913 #[test]
914 fn stop_sequences_sets_value() {
915 let b =
916 AgentBuilder::new("agent").stop_sequences(vec!["END".to_string(), "STOP".to_string()]);
917 assert_eq!(b.get_stop_sequences().len(), 2);
918 }
919
920 #[test]
921 fn description_sets_value() {
922 let b = AgentBuilder::new("agent").description("A helpful agent");
923 assert_eq!(b.get_description(), Some("A helpful agent"));
924 }
925
926 #[test]
927 fn output_schema_sets_value() {
928 let schema = serde_json::json!({"type": "object"});
929 let b = AgentBuilder::new("agent").output_schema(schema.clone());
930 assert_eq!(b.get_output_schema(), Some(&schema));
931 }
932
933 #[test]
934 fn transfer_to_sets_value() {
935 let b = AgentBuilder::new("agent").transfer_to("target-agent");
936 assert_eq!(b.get_transfer_to(), Some("target-agent"));
937 }
938
939 #[test]
940 fn full_fluent_chain() {
941 let b = AgentBuilder::new("full-agent")
942 .model(ModelId::LIVE_2_5_FLASH_NATIVE_AUDIO)
943 .instruction("Be helpful")
944 .temperature(0.7)
945 .top_p(0.95)
946 .top_k(40)
947 .max_output_tokens(4096)
948 .thinking(2048)
949 .description("A fully configured agent")
950 .google_search()
951 .writes("output")
952 .reads("input");
953
954 assert_eq!(b.name(), "full-agent");
955 assert_eq!(b.get_temperature(), Some(0.7));
956 assert_eq!(b.get_top_p(), Some(0.95));
957 assert_eq!(b.get_top_k(), Some(40));
958 assert_eq!(b.get_max_output_tokens(), Some(4096));
959 assert_eq!(b.get_thinking_budget(), Some(2048));
960 assert_eq!(b.get_description(), Some("A fully configured agent"));
961 assert_eq!(b.tool_count(), 1);
962 }
963
964 #[tokio::test]
967 async fn build_produces_executable_agent() {
968 let llm: Arc<dyn BaseLlm> = Arc::new(MockLlm("built agent output".into()));
969 let agent = AgentBuilder::new("test")
970 .instruction("Be helpful")
971 .temperature(0.5)
972 .build(llm)
973 .unwrap();
974
975 assert_eq!(agent.name(), "test");
976 let state = gemini_adk_rs::State::new();
977 let result = agent.run(&state).await.unwrap();
978 assert_eq!(result, "built agent output");
979 }
980
981 #[tokio::test]
982 async fn build_stores_output_in_state() {
983 let llm: Arc<dyn BaseLlm> = Arc::new(MockLlm("state output".into()));
984 let agent = AgentBuilder::new("test").build(llm).unwrap();
985 let state = gemini_adk_rs::State::new();
986 agent.run(&state).await.unwrap();
987 assert_eq!(state.get::<String>("output"), Some("state output".into()));
988 }
989
990 #[tokio::test]
991 async fn build_reads_input_from_state() {
992 use gemini_adk_rs::llm::LlmRequest;
993
994 struct EchoLlm;
996 #[async_trait]
997 impl BaseLlm for EchoLlm {
998 fn model_id(&self) -> &str {
999 "echo"
1000 }
1001 async fn generate(&self, req: LlmRequest) -> Result<LlmResponse, LlmError> {
1002 let text: String = req
1003 .contents
1004 .iter()
1005 .flat_map(|c| &c.parts)
1006 .filter_map(|p| match p {
1007 Part::Text { text } => Some(text.as_str()),
1008 _ => None,
1009 })
1010 .collect::<Vec<_>>()
1011 .join("");
1012 Ok(LlmResponse {
1013 content: Content {
1014 role: Some(Role::Model),
1015 parts: vec![Part::Text { text }],
1016 },
1017 finish_reason: Some("STOP".into()),
1018 usage: None,
1019 })
1020 }
1021 }
1022
1023 let agent = AgentBuilder::new("echo").build(Arc::new(EchoLlm)).unwrap();
1024 let state = gemini_adk_rs::State::new();
1025 let _ = state.set("input", "hello from state");
1026 let result = agent.run(&state).await.unwrap();
1027 assert!(result.contains("hello from state"));
1028 }
1029
1030 struct ToolCallingMockLlm {
1034 tool_name: &'static str,
1035 final_text: &'static str,
1036 }
1037
1038 #[async_trait]
1039 impl BaseLlm for ToolCallingMockLlm {
1040 fn model_id(&self) -> &str {
1041 "tool-mock"
1042 }
1043
1044 async fn generate(&self, req: LlmRequest) -> Result<LlmResponse, LlmError> {
1045 use gemini_genai_rs::prelude::FunctionCall;
1046
1047 let already_responded = req
1049 .contents
1050 .iter()
1051 .flat_map(|c| &c.parts)
1052 .any(|p| matches!(p, Part::FunctionResponse { .. }));
1053
1054 if already_responded {
1055 Ok(LlmResponse {
1056 content: Content {
1057 role: Some(Role::Model),
1058 parts: vec![Part::Text {
1059 text: self.final_text.to_string(),
1060 }],
1061 },
1062 finish_reason: Some("STOP".into()),
1063 usage: None,
1064 })
1065 } else {
1066 Ok(LlmResponse {
1067 content: Content {
1068 role: Some(Role::Model),
1069 parts: vec![Part::FunctionCall {
1070 function_call: FunctionCall {
1071 name: self.tool_name.to_string(),
1072 args: serde_json::json!({"x": 1}),
1073 id: Some("call-1".into()),
1074 },
1075 }],
1076 },
1077 finish_reason: None,
1078 usage: None,
1079 })
1080 }
1081 }
1082 }
1083
1084 #[tokio::test]
1086 async fn middleware_hooks_fire_end_to_end() {
1087 use crate::compose::middleware::M;
1088 use gemini_adk_rs::tool::SimpleTool;
1089 use std::sync::atomic::{AtomicUsize, Ordering};
1090
1091 let before_model_count = Arc::new(AtomicUsize::new(0));
1092 let after_tool_count = Arc::new(AtomicUsize::new(0));
1093
1094 let bm = before_model_count.clone();
1095 let at = after_tool_count.clone();
1096
1097 let mw = M::before_model(move |_req| {
1098 bm.fetch_add(1, Ordering::SeqCst);
1099 Ok(())
1100 }) | M::after_tool(move |_call, _result| {
1101 at.fetch_add(1, Ordering::SeqCst);
1102 Ok(())
1103 });
1104
1105 let llm: Arc<dyn BaseLlm> = Arc::new(ToolCallingMockLlm {
1106 tool_name: "echo_tool",
1107 final_text: "done",
1108 });
1109
1110 let agent = AgentBuilder::new("mw-test")
1111 .middleware(mw)
1112 .tool(SimpleTool::new(
1113 "echo_tool",
1114 "Echo tool",
1115 None,
1116 |_args| async move { Ok(serde_json::json!({"echo": true})) },
1117 ))
1118 .build(llm)
1119 .unwrap();
1120
1121 let state = gemini_adk_rs::State::new();
1122 let result = agent.run(&state).await.unwrap();
1123 assert_eq!(result, "done");
1124
1125 assert_eq!(
1127 before_model_count.load(Ordering::SeqCst),
1128 2,
1129 "before_model should fire for each generate() call"
1130 );
1131 assert_eq!(
1133 after_tool_count.load(Ordering::SeqCst),
1134 1,
1135 "after_tool should fire once for the tool dispatch"
1136 );
1137 }
1138
1139 #[test]
1141 fn middleware_copy_on_write() {
1142 use crate::compose::middleware::M;
1143
1144 let base = AgentBuilder::new("base").instruction("base");
1145 let with_mw = base.clone().middleware(M::log() | M::latency());
1146
1147 assert_eq!(base.middleware_layer_count(), 0);
1149 assert_eq!(with_mw.middleware_layer_count(), 2);
1151 }
1152
1153 #[tokio::test]
1155 async fn middleware_on_error_fires_on_failure() {
1156 use crate::compose::middleware::M;
1157 use gemini_adk_rs::llm::LlmError;
1158 use std::sync::atomic::{AtomicUsize, Ordering};
1159
1160 let error_count = Arc::new(AtomicUsize::new(0));
1161 let ec = error_count.clone();
1162
1163 let mw = M::on_error(move |_err| {
1164 ec.fetch_add(1, Ordering::SeqCst);
1165 Ok(())
1166 });
1167
1168 struct FailLlm;
1169 #[async_trait]
1170 impl BaseLlm for FailLlm {
1171 fn model_id(&self) -> &str {
1172 "fail"
1173 }
1174 async fn generate(&self, _req: LlmRequest) -> Result<LlmResponse, LlmError> {
1175 Err(LlmError::RequestFailed("boom".into()))
1176 }
1177 }
1178
1179 let agent = AgentBuilder::new("error-test")
1180 .middleware(mw)
1181 .build(Arc::new(FailLlm))
1182 .unwrap();
1183
1184 let state = gemini_adk_rs::State::new();
1185 let result = agent.run(&state).await;
1186 assert!(result.is_err(), "agent should fail");
1187 assert_eq!(
1188 error_count.load(Ordering::SeqCst),
1189 1,
1190 "on_error should fire exactly once"
1191 );
1192 }
1193
1194 struct RecordingLlm {
1199 text: &'static str,
1200 seen_len: Arc<std::sync::atomic::AtomicUsize>,
1201 }
1202
1203 #[async_trait]
1204 impl BaseLlm for RecordingLlm {
1205 fn model_id(&self) -> &str {
1206 "recording-mock"
1207 }
1208
1209 async fn generate(&self, req: LlmRequest) -> Result<LlmResponse, LlmError> {
1210 self.seen_len
1211 .store(req.contents.len(), std::sync::atomic::Ordering::SeqCst);
1212 Ok(LlmResponse {
1213 content: Content {
1214 role: Some(Role::Model),
1215 parts: vec![Part::Text {
1216 text: self.text.to_string(),
1217 }],
1218 },
1219 finish_reason: Some("STOP".into()),
1220 usage: None,
1221 })
1222 }
1223 }
1224
1225 #[tokio::test]
1226 async fn guard_blocks_violating_output() {
1227 use crate::compose::guards::G;
1228
1229 let llm: Arc<dyn BaseLlm> = Arc::new(RecordingLlm {
1230 text: "you can reach me at agent@example.com",
1231 seen_len: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
1232 });
1233
1234 let agent = AgentBuilder::new("guarded")
1235 .guard(G::pii())
1236 .build(llm)
1237 .unwrap();
1238
1239 let state = gemini_adk_rs::State::new();
1240 let err = agent.run(&state).await.unwrap_err();
1241 assert!(
1242 err.to_string().contains("guard violation"),
1243 "PII guard should veto the response, got: {err}"
1244 );
1245 }
1246
1247 #[tokio::test]
1248 async fn guard_allows_clean_output() {
1249 use crate::compose::guards::G;
1250
1251 let llm: Arc<dyn BaseLlm> = Arc::new(RecordingLlm {
1252 text: "all clean here",
1253 seen_len: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
1254 });
1255
1256 let agent = AgentBuilder::new("guarded")
1257 .guard(G::pii() | G::length(1, 1000))
1258 .build(llm)
1259 .unwrap();
1260
1261 let state = gemini_adk_rs::State::new();
1262 let result = agent.run(&state).await.unwrap();
1263 assert_eq!(result, "all clean here");
1264 }
1265
1266 #[tokio::test]
1267 async fn context_policy_rewrites_request_history() {
1268 use crate::compose::context::C;
1269
1270 let seen = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1273 let llm: Arc<dyn BaseLlm> = Arc::new(RecordingLlm {
1274 text: "ok",
1275 seen_len: seen.clone(),
1276 });
1277
1278 let agent = AgentBuilder::new("ctx")
1279 .context(C::prepend(Content::user("system preamble")))
1280 .build(llm)
1281 .unwrap();
1282
1283 let state = gemini_adk_rs::State::new();
1284 let _ = state.set("input", "hello");
1285 let _ = agent.run(&state).await.unwrap();
1286 assert_eq!(
1287 seen.load(std::sync::atomic::Ordering::SeqCst),
1288 2,
1289 "context policy should have prepended a turn before the model call"
1290 );
1291 }
1292
1293 #[tokio::test]
1294 async fn context_window_trims_history() {
1295 use crate::compose::context::C;
1296
1297 let seen = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1300 let llm: Arc<dyn BaseLlm> = Arc::new(RecordingLlm {
1301 text: "ok",
1302 seen_len: seen.clone(),
1303 });
1304
1305 let agent = AgentBuilder::new("ctx")
1306 .context(C::prepend(Content::user("a")) + C::prepend(Content::user("b")) + C::window(1))
1307 .build(llm)
1308 .unwrap();
1309
1310 let state = gemini_adk_rs::State::new();
1311 let _ = state.set("input", "hello");
1312 let _ = agent.run(&state).await.unwrap();
1313 assert_eq!(
1314 seen.load(std::sync::atomic::Ordering::SeqCst),
1315 1,
1316 "window(1) should trim history to the last turn"
1317 );
1318 }
1319}
1320
1321#[cfg(test)]
1322mod mcp_rejection_tests {
1323 use super::*;
1324 use gemini_adk_rs::llm::{LlmError, LlmRequest, LlmResponse};
1325
1326 struct NeverLlm;
1327 #[async_trait::async_trait]
1328 impl BaseLlm for NeverLlm {
1329 fn model_id(&self) -> &str {
1330 "never"
1331 }
1332 async fn generate(&self, _req: LlmRequest) -> Result<LlmResponse, LlmError> {
1333 Err(LlmError::RequestFailed("never".into()))
1334 }
1335 }
1336
1337 #[test]
1340 fn mcp_toolset_is_a_build_error() {
1341 use crate::compose::tools::T;
1342 let err = AgentBuilder::new("text")
1343 .tools(T::mcp("node ./server.js"))
1344 .build(Arc::new(NeverLlm))
1345 .err()
1346 .expect("build must fail");
1347 assert!(err.to_string().contains("T::mcp"), "{err}");
1348 assert!(err.to_string().contains("Live"), "{err}");
1349 }
1350
1351 #[test]
1353 fn tool_accepts_values_and_arcs() {
1354 use gemini_adk_rs::tool::SimpleTool;
1355 let arc: Arc<dyn ToolFunction> = Arc::new(SimpleTool::new("a", "a", None, |_| async {
1356 Ok(serde_json::json!({}))
1357 }));
1358 let b = AgentBuilder::new("t")
1359 .tool(SimpleTool::new("b", "b", None, |_| async {
1360 Ok(serde_json::json!({}))
1361 }))
1362 .tool(arc.clone())
1363 .tools(arc);
1364 assert_eq!(b.tool_count(), 3);
1365 }
1366}