1use std::sync::Arc;
7
8use gemini_adk_rs::llm::BaseLlm;
9use gemini_adk_rs::text::{LlmTextAgent, TextAgent};
10use gemini_adk_rs::tool::{ToolDispatcher, ToolFunction, ToolKind};
11use gemini_genai_rs::prelude::{GeminiModel, Modality, Tool, Voice};
12
13use crate::compose::context::ContextPolicy;
14use crate::compose::guards::GComposite;
15use crate::compose::tools::ToolComposite;
16
17#[derive(Clone)]
19struct AgentBuilderInner {
20 name: String,
21 model: Option<GeminiModel>,
22 instruction: Option<String>,
23 voice: Option<Voice>,
24 temperature: Option<f32>,
25 top_p: Option<f32>,
26 top_k: Option<u32>,
27 max_output_tokens: Option<u32>,
28 stop_sequences: Vec<String>,
29 response_modalities: Option<Vec<Modality>>,
30 thinking_budget: Option<u32>,
31 tools: Vec<ToolEntry>,
32 built_in_tools: Vec<Tool>,
33 writes: Vec<String>,
34 reads: Vec<String>,
35 sub_agents: Vec<AgentBuilder>,
36 isolate: bool,
37 stay: bool,
38 description: Option<String>,
39 output_schema: Option<serde_json::Value>,
40 output_key: Option<String>,
41 transfer_to_agent: Option<String>,
42}
43
44#[derive(Clone)]
46pub enum ToolEntry {
47 Runtime(Arc<dyn ToolEntryTrait>),
49 Declaration(Tool),
51}
52
53pub trait ToolEntryTrait: Send + Sync + 'static {
55 fn name(&self) -> &str;
57 fn to_tool_kind(&self) -> ToolKind;
59}
60
61pub type Agent = AgentBuilder;
63
64#[derive(Clone)]
145pub struct AgentBuilder {
146 inner: Arc<AgentBuilderInner>,
147}
148
149impl AgentBuilder {
150 pub fn new(name: impl Into<String>) -> Self {
152 Self {
153 inner: Arc::new(AgentBuilderInner {
154 name: name.into(),
155 model: None,
156 instruction: None,
157 voice: None,
158 temperature: None,
159 top_p: None,
160 top_k: None,
161 max_output_tokens: None,
162 stop_sequences: Vec::new(),
163 response_modalities: None,
164 thinking_budget: None,
165 tools: Vec::new(),
166 built_in_tools: Vec::new(),
167 writes: Vec::new(),
168 reads: Vec::new(),
169 sub_agents: Vec::new(),
170 isolate: false,
171 stay: false,
172 description: None,
173 output_schema: None,
174 output_key: None,
175 transfer_to_agent: None,
176 }),
177 }
178 }
179
180 fn mutate(&self) -> AgentBuilderInner {
183 (*self.inner).clone()
184 }
185
186 fn with(inner: AgentBuilderInner) -> Self {
187 Self {
188 inner: Arc::new(inner),
189 }
190 }
191
192 pub fn name(&self) -> &str {
196 &self.inner.name
197 }
198
199 pub fn get_model(&self) -> Option<&GeminiModel> {
201 self.inner.model.as_ref()
202 }
203
204 pub fn get_instruction(&self) -> Option<&str> {
206 self.inner.instruction.as_deref()
207 }
208
209 pub fn get_voice(&self) -> Option<&Voice> {
211 self.inner.voice.as_ref()
212 }
213
214 pub fn get_temperature(&self) -> Option<f32> {
216 self.inner.temperature
217 }
218
219 pub fn is_text_only(&self) -> bool {
221 self.inner
222 .response_modalities
223 .as_ref()
224 .map(|m| m == &[Modality::Text])
225 .unwrap_or(false)
226 }
227
228 pub fn get_thinking_budget(&self) -> Option<u32> {
230 self.inner.thinking_budget
231 }
232
233 pub fn get_writes(&self) -> &[String] {
235 &self.inner.writes
236 }
237
238 pub fn get_reads(&self) -> &[String] {
240 &self.inner.reads
241 }
242
243 pub fn get_sub_agents(&self) -> &[AgentBuilder] {
245 &self.inner.sub_agents
246 }
247
248 pub fn is_isolated(&self) -> bool {
250 self.inner.isolate
251 }
252
253 pub fn is_stay(&self) -> bool {
255 self.inner.stay
256 }
257
258 pub fn tool_count(&self) -> usize {
260 self.inner.tools.len() + self.inner.built_in_tools.len()
261 }
262
263 pub fn get_top_p(&self) -> Option<f32> {
265 self.inner.top_p
266 }
267
268 pub fn get_top_k(&self) -> Option<u32> {
270 self.inner.top_k
271 }
272
273 pub fn get_max_output_tokens(&self) -> Option<u32> {
275 self.inner.max_output_tokens
276 }
277
278 pub fn get_stop_sequences(&self) -> &[String] {
280 &self.inner.stop_sequences
281 }
282
283 pub fn get_description(&self) -> Option<&str> {
285 self.inner.description.as_deref()
286 }
287
288 pub fn get_output_schema(&self) -> Option<&serde_json::Value> {
290 self.inner.output_schema.as_ref()
291 }
292
293 pub fn get_output_key(&self) -> Option<&str> {
295 self.inner.output_key.as_deref()
296 }
297
298 pub fn get_transfer_to(&self) -> Option<&str> {
300 self.inner.transfer_to_agent.as_deref()
301 }
302
303 pub fn model(self, model: GeminiModel) -> Self {
307 let mut inner = self.mutate();
308 inner.model = Some(model);
309 Self::with(inner)
310 }
311
312 pub fn instruction(self, inst: impl Into<String>) -> Self {
314 let mut inner = self.mutate();
315 inner.instruction = Some(inst.into());
316 Self::with(inner)
317 }
318
319 pub fn voice(self, voice: Voice) -> Self {
321 let mut inner = self.mutate();
322 inner.voice = Some(voice);
323 Self::with(inner)
324 }
325
326 pub fn temperature(self, t: f32) -> Self {
328 let mut inner = self.mutate();
329 inner.temperature = Some(t);
330 Self::with(inner)
331 }
332
333 pub fn text_only(self) -> Self {
335 let mut inner = self.mutate();
336 inner.response_modalities = Some(vec![Modality::Text]);
337 Self::with(inner)
338 }
339
340 pub fn response_modalities(self, modalities: Vec<Modality>) -> Self {
342 let mut inner = self.mutate();
343 inner.response_modalities = Some(modalities);
344 Self::with(inner)
345 }
346
347 pub fn thinking(self, budget: u32) -> Self {
349 let mut inner = self.mutate();
350 inner.thinking_budget = Some(budget);
351 Self::with(inner)
352 }
353
354 pub fn url_context(self) -> Self {
356 let mut inner = self.mutate();
357 inner.built_in_tools.push(Tool::url_context());
358 Self::with(inner)
359 }
360
361 pub fn google_search(self) -> Self {
363 let mut inner = self.mutate();
364 inner.built_in_tools.push(Tool::google_search());
365 Self::with(inner)
366 }
367
368 pub fn code_execution(self) -> Self {
370 let mut inner = self.mutate();
371 inner.built_in_tools.push(Tool::code_execution());
372 Self::with(inner)
373 }
374
375 pub fn writes(self, key: impl Into<String>) -> Self {
377 let mut inner = self.mutate();
378 inner.writes.push(key.into());
379 Self::with(inner)
380 }
381
382 pub fn reads(self, key: impl Into<String>) -> Self {
384 let mut inner = self.mutate();
385 inner.reads.push(key.into());
386 Self::with(inner)
387 }
388
389 pub fn sub_agent(self, agent: AgentBuilder) -> Self {
391 let mut inner = self.mutate();
392 inner.sub_agents.push(agent);
393 Self::with(inner)
394 }
395
396 pub fn isolate(self) -> Self {
398 let mut inner = self.mutate();
399 inner.isolate = true;
400 Self::with(inner)
401 }
402
403 pub fn stay(self) -> Self {
405 let mut inner = self.mutate();
406 inner.stay = true;
407 Self::with(inner)
408 }
409
410 pub fn top_p(self, p: f32) -> Self {
412 let mut inner = self.mutate();
413 inner.top_p = Some(p);
414 Self::with(inner)
415 }
416
417 pub fn top_k(self, k: u32) -> Self {
419 let mut inner = self.mutate();
420 inner.top_k = Some(k);
421 Self::with(inner)
422 }
423
424 pub fn max_output_tokens(self, n: u32) -> Self {
426 let mut inner = self.mutate();
427 inner.max_output_tokens = Some(n);
428 Self::with(inner)
429 }
430
431 pub fn stop_sequences(self, seqs: Vec<String>) -> Self {
433 let mut inner = self.mutate();
434 inner.stop_sequences = seqs;
435 Self::with(inner)
436 }
437
438 pub fn description(self, desc: impl Into<String>) -> Self {
440 let mut inner = self.mutate();
441 inner.description = Some(desc.into());
442 Self::with(inner)
443 }
444
445 pub fn output_schema(self, schema: serde_json::Value) -> Self {
447 let mut inner = self.mutate();
448 inner.output_schema = Some(schema);
449 Self::with(inner)
450 }
451
452 pub fn output_key(self, key: impl Into<String>) -> Self {
454 let mut inner = self.mutate();
455 inner.output_key = Some(key.into());
456 Self::with(inner)
457 }
458
459 pub fn transfer_to(self, agent_name: impl Into<String>) -> Self {
461 let mut inner = self.mutate();
462 inner.transfer_to_agent = Some(agent_name.into());
463 Self::with(inner)
464 }
465
466 pub fn instruct(self, inst: impl Into<String>) -> Self {
470 self.instruction(inst)
471 }
472
473 pub fn describe(self, desc: impl Into<String>) -> Self {
475 self.description(desc)
476 }
477
478 pub fn tool(self, f: Arc<dyn ToolFunction>) -> Self {
484 let mut inner = self.mutate();
485 inner
486 .tools
487 .push(ToolEntry::Runtime(Arc::new(ToolFunctionEntry(f))));
488 Self::with(inner)
489 }
490
491 pub fn tools(self, composite: ToolComposite) -> Self {
499 use crate::compose::tools::ToolCompositeEntry;
500 let mut inner = self.mutate();
501 for entry in composite.entries {
502 match entry {
503 ToolCompositeEntry::Function(f) => {
504 inner
505 .tools
506 .push(ToolEntry::Runtime(Arc::new(ToolFunctionEntry(f))));
507 }
508 ToolCompositeEntry::BuiltIn(t) => {
509 inner.built_in_tools.push(t);
510 }
511 _ => {
513 }
516 }
517 }
518 Self::with(inner)
519 }
520
521 pub fn guard(self, _guard: GComposite) -> Self {
526 self
529 }
530
531 pub fn context(self, _policy: ContextPolicy) -> Self {
533 self
535 }
536
537 pub fn no_peers(self) -> Self {
539 self.isolate()
540 }
541
542 pub fn build(self, llm: Arc<dyn BaseLlm>) -> Arc<dyn TextAgent> {
559 let mut agent = LlmTextAgent::new(&self.inner.name, llm);
560
561 if let Some(inst) = &self.inner.instruction {
562 agent = agent.instruction(inst);
563 }
564 if let Some(t) = self.inner.temperature {
565 agent = agent.temperature(t);
566 }
567 if let Some(n) = self.inner.max_output_tokens {
568 agent = agent.max_output_tokens(n);
569 }
570
571 if !self.inner.tools.is_empty() {
573 let mut dispatcher = ToolDispatcher::new();
574 for entry in &self.inner.tools {
575 match entry {
576 ToolEntry::Runtime(t) => {
577 let kind = t.to_tool_kind();
578 match kind {
579 ToolKind::Function(f) => dispatcher.register_function(f),
580 ToolKind::Streaming(s) => dispatcher.register_streaming(s),
581 ToolKind::InputStream(i) => dispatcher.register_input_streaming(i),
582 }
583 }
584 ToolEntry::Declaration(_) => {
585 }
588 }
589 }
590 if !dispatcher.is_empty() {
591 agent = agent.tools(Arc::new(dispatcher));
592 }
593 }
594
595 Arc::new(agent)
596 }
597}
598
599#[derive(Clone)]
601struct ToolFunctionEntry(Arc<dyn ToolFunction>);
602
603impl ToolEntryTrait for ToolFunctionEntry {
604 fn name(&self) -> &str {
605 self.0.name()
606 }
607
608 fn to_tool_kind(&self) -> ToolKind {
609 ToolKind::Function(self.0.clone())
610 }
611}
612
613impl std::fmt::Debug for AgentBuilder {
614 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
615 f.debug_struct("AgentBuilder")
616 .field("name", &self.inner.name)
617 .field("model", &self.inner.model)
618 .field("instruction", &self.inner.instruction)
619 .field("temperature", &self.inner.temperature)
620 .field("text_only", &self.is_text_only())
621 .field("tool_count", &self.tool_count())
622 .field("sub_agents", &self.inner.sub_agents.len())
623 .finish()
624 }
625}
626
627#[cfg(test)]
628mod tests {
629 use super::*;
630 use async_trait::async_trait;
631 use gemini_adk_rs::llm::{LlmError, LlmRequest, LlmResponse};
632 use gemini_genai_rs::prelude::{Content, Part, Role};
633
634 struct MockLlm(String);
636
637 #[async_trait]
638 impl BaseLlm for MockLlm {
639 fn model_id(&self) -> &str {
640 "mock"
641 }
642 async fn generate(&self, _req: LlmRequest) -> Result<LlmResponse, LlmError> {
643 Ok(LlmResponse {
644 content: Content {
645 role: Some(Role::Model),
646 parts: vec![Part::Text {
647 text: self.0.clone(),
648 }],
649 },
650 finish_reason: Some("STOP".into()),
651 usage: None,
652 })
653 }
654 }
655
656 #[test]
657 fn builder_creates_with_name() {
658 let b = AgentBuilder::new("test-agent");
659 assert_eq!(b.name(), "test-agent");
660 }
661
662 #[test]
663 fn fluent_chaining_works() {
664 let b = AgentBuilder::new("agent")
665 .instruction("Be helpful")
666 .temperature(0.7)
667 .model(GeminiModel::Gemini2_0FlashLive);
668
669 assert_eq!(b.get_instruction(), Some("Be helpful"));
670 assert_eq!(b.get_temperature(), Some(0.7));
671 assert_eq!(b.get_model(), Some(&GeminiModel::Gemini2_0FlashLive));
672 }
673
674 #[test]
675 fn copy_on_write_clone_independence() {
676 let base = AgentBuilder::new("base").temperature(0.5);
677 let variant = base.clone().temperature(0.9);
678
679 assert_eq!(base.get_temperature(), Some(0.5));
681 assert_eq!(variant.get_temperature(), Some(0.9));
683 }
684
685 #[test]
686 fn text_only_sets_modalities() {
687 let b = AgentBuilder::new("text").text_only();
688 assert!(b.is_text_only());
689 }
690
691 #[test]
692 fn url_context_adds_tool() {
693 let b = AgentBuilder::new("search").url_context();
694 assert_eq!(b.tool_count(), 1);
695 }
696
697 #[test]
698 fn google_search_adds_tool() {
699 let b = AgentBuilder::new("search").google_search();
700 assert_eq!(b.tool_count(), 1);
701 }
702
703 #[test]
704 fn code_execution_adds_tool() {
705 let b = AgentBuilder::new("code").code_execution();
706 assert_eq!(b.tool_count(), 1);
707 }
708
709 #[test]
710 fn thinking_sets_budget() {
711 let b = AgentBuilder::new("thinker").thinking(2048);
712 assert_eq!(b.get_thinking_budget(), Some(2048));
713 }
714
715 #[test]
716 fn writes_and_reads_keys() {
717 let b = AgentBuilder::new("data").writes("output").reads("input");
718 assert_eq!(b.get_writes(), &["output"]);
719 assert_eq!(b.get_reads(), &["input"]);
720 }
721
722 #[test]
723 fn sub_agent_registration() {
724 let child = AgentBuilder::new("child");
725 let parent = AgentBuilder::new("parent").sub_agent(child);
726 assert_eq!(parent.get_sub_agents().len(), 1);
727 assert_eq!(parent.get_sub_agents()[0].name(), "child");
728 }
729
730 #[test]
731 fn isolate_and_stay() {
732 let b = AgentBuilder::new("agent").isolate().stay();
733 assert!(b.is_isolated());
734 assert!(b.is_stay());
735 }
736
737 #[test]
738 fn debug_display() {
739 let b = AgentBuilder::new("debug-test");
740 let debug = format!("{:?}", b);
741 assert!(debug.contains("debug-test"));
742 }
743
744 #[test]
745 fn top_p_sets_value() {
746 let b = AgentBuilder::new("agent").top_p(0.95);
747 assert_eq!(b.get_top_p(), Some(0.95));
748 }
749
750 #[test]
751 fn top_k_sets_value() {
752 let b = AgentBuilder::new("agent").top_k(40);
753 assert_eq!(b.get_top_k(), Some(40));
754 }
755
756 #[test]
757 fn max_output_tokens_sets_value() {
758 let b = AgentBuilder::new("agent").max_output_tokens(4096);
759 assert_eq!(b.get_max_output_tokens(), Some(4096));
760 }
761
762 #[test]
763 fn stop_sequences_sets_value() {
764 let b =
765 AgentBuilder::new("agent").stop_sequences(vec!["END".to_string(), "STOP".to_string()]);
766 assert_eq!(b.get_stop_sequences().len(), 2);
767 }
768
769 #[test]
770 fn description_sets_value() {
771 let b = AgentBuilder::new("agent").description("A helpful agent");
772 assert_eq!(b.get_description(), Some("A helpful agent"));
773 }
774
775 #[test]
776 fn output_schema_sets_value() {
777 let schema = serde_json::json!({"type": "object"});
778 let b = AgentBuilder::new("agent").output_schema(schema.clone());
779 assert_eq!(b.get_output_schema(), Some(&schema));
780 }
781
782 #[test]
783 fn transfer_to_sets_value() {
784 let b = AgentBuilder::new("agent").transfer_to("target-agent");
785 assert_eq!(b.get_transfer_to(), Some("target-agent"));
786 }
787
788 #[test]
789 fn full_fluent_chain() {
790 let b = AgentBuilder::new("full-agent")
791 .model(GeminiModel::Gemini2_0FlashLive)
792 .instruction("Be helpful")
793 .temperature(0.7)
794 .top_p(0.95)
795 .top_k(40)
796 .max_output_tokens(4096)
797 .thinking(2048)
798 .description("A fully configured agent")
799 .google_search()
800 .writes("output")
801 .reads("input");
802
803 assert_eq!(b.name(), "full-agent");
804 assert_eq!(b.get_temperature(), Some(0.7));
805 assert_eq!(b.get_top_p(), Some(0.95));
806 assert_eq!(b.get_top_k(), Some(40));
807 assert_eq!(b.get_max_output_tokens(), Some(4096));
808 assert_eq!(b.get_thinking_budget(), Some(2048));
809 assert_eq!(b.get_description(), Some("A fully configured agent"));
810 assert_eq!(b.tool_count(), 1);
811 }
812
813 #[tokio::test]
816 async fn build_produces_executable_agent() {
817 let llm: Arc<dyn BaseLlm> = Arc::new(MockLlm("built agent output".into()));
818 let agent = AgentBuilder::new("test")
819 .instruction("Be helpful")
820 .temperature(0.5)
821 .build(llm);
822
823 assert_eq!(agent.name(), "test");
824 let state = gemini_adk_rs::State::new();
825 let result = agent.run(&state).await.unwrap();
826 assert_eq!(result, "built agent output");
827 }
828
829 #[tokio::test]
830 async fn build_stores_output_in_state() {
831 let llm: Arc<dyn BaseLlm> = Arc::new(MockLlm("state output".into()));
832 let agent = AgentBuilder::new("test").build(llm);
833 let state = gemini_adk_rs::State::new();
834 agent.run(&state).await.unwrap();
835 assert_eq!(state.get::<String>("output"), Some("state output".into()));
836 }
837
838 #[tokio::test]
839 async fn build_reads_input_from_state() {
840 use gemini_adk_rs::llm::LlmRequest;
841
842 struct EchoLlm;
844 #[async_trait]
845 impl BaseLlm for EchoLlm {
846 fn model_id(&self) -> &str {
847 "echo"
848 }
849 async fn generate(&self, req: LlmRequest) -> Result<LlmResponse, LlmError> {
850 let text: String = req
851 .contents
852 .iter()
853 .flat_map(|c| &c.parts)
854 .filter_map(|p| match p {
855 Part::Text { text } => Some(text.as_str()),
856 _ => None,
857 })
858 .collect::<Vec<_>>()
859 .join("");
860 Ok(LlmResponse {
861 content: Content {
862 role: Some(Role::Model),
863 parts: vec![Part::Text { text }],
864 },
865 finish_reason: Some("STOP".into()),
866 usage: None,
867 })
868 }
869 }
870
871 let agent = AgentBuilder::new("echo").build(Arc::new(EchoLlm));
872 let state = gemini_adk_rs::State::new();
873 state.set("input", "hello from state");
874 let result = agent.run(&state).await.unwrap();
875 assert!(result.contains("hello from state"));
876 }
877}