1use std::collections::HashSet;
2use std::sync::Arc;
3
4use agent_base::{AgentResult, AgentRuntime, Tool};
5
6#[cfg(feature = "multi_agent")]
7use crate::multi_agent::{MultiAgentConfig, MultiAgentRuntime};
8
9#[cfg(feature = "skill")]
10use crate::skill::{LazySkillPrompter, Skill, SkillPrompter};
11
12#[cfg(feature = "multi_agent")]
14pub type MultiAgentToolFactory =
15 Arc<dyn Fn(Arc<MultiAgentRuntime>) -> Vec<Arc<dyn Tool>> + Send + Sync>;
16
17#[cfg(feature = "skill")]
19pub type SkillDetailToolFactory =
20 Arc<dyn Fn(Vec<Arc<dyn Skill>>, String) -> Arc<dyn Tool> + Send + Sync>;
21
22#[cfg(feature = "skill")]
24pub type ListSkillsToolFactory =
25 Arc<dyn Fn(Arc<crate::skill::SkillRegistry>) -> Arc<dyn Tool> + Send + Sync>;
26
27pub struct AgentBuilder {
28 inner: agent_base::AgentBuilder,
29 client: Arc<dyn agent_base::llm_trait::LlmProvider>,
30 system_prompt: Option<String>,
31 tool_names: HashSet<String>,
32 business_tools: Vec<Arc<dyn Tool>>,
34 #[cfg(feature = "multi_agent")]
36 multi_agent_config: Option<MultiAgentConfig>,
37 #[cfg(feature = "multi_agent")]
39 multi_agent_tool_factory: Option<MultiAgentToolFactory>,
40 error_recovery: Option<Arc<dyn agent_base::ToolErrorRecovery>>,
42 language: Option<agent_base::Language>,
44 #[cfg(feature = "skill")]
45 skills: Vec<Arc<dyn Skill>>,
46 #[cfg(feature = "skill")]
47 skill_prompter: Option<Arc<dyn SkillPrompter>>,
48 #[cfg(feature = "skill")]
49 skill_detail_tool_name: String,
50 #[cfg(feature = "skill")]
54 skill_detail_tool_factory: Option<SkillDetailToolFactory>,
55 #[cfg(feature = "skill")]
56 list_skills_tool_factory: Option<ListSkillsToolFactory>,
57 #[cfg(feature = "skill")]
58 disable_skill_prompt_injection: bool,
59 context_compactor: Option<Arc<dyn agent_base::ContextCompaction>>,
61}
62
63impl AgentBuilder {
64 pub fn new(client: Arc<dyn agent_base::llm_trait::LlmProvider>) -> Self {
65 Self {
66 inner: agent_base::AgentBuilder::new(client.clone()),
67 client,
68 system_prompt: None,
69 tool_names: HashSet::new(),
70 business_tools: Vec::new(),
71 #[cfg(feature = "multi_agent")]
72 multi_agent_config: None,
73 #[cfg(feature = "multi_agent")]
74 multi_agent_tool_factory: None,
75 error_recovery: None,
76 language: None,
77 #[cfg(feature = "skill")]
78 skills: Vec::new(),
79 #[cfg(feature = "skill")]
80 skill_prompter: None,
81 #[cfg(feature = "skill")]
82 skill_detail_tool_name: "get_skill_detail".to_string(),
83 #[cfg(feature = "skill")]
84 skill_detail_tool_factory: None,
85 #[cfg(feature = "skill")]
86 list_skills_tool_factory: None,
87 #[cfg(feature = "skill")]
88 disable_skill_prompt_injection: false,
89 context_compactor: None,
90 }
91 }
92
93 #[cfg(feature = "multi_agent")]
98 pub fn with_multi_agent(mut self, config: MultiAgentConfig) -> Self {
99 self.multi_agent_config = Some(config);
100 self
101 }
102
103 #[cfg(feature = "multi_agent")]
108 pub fn without_multi_agent(mut self) -> Self {
109 self.multi_agent_config = None;
110 self.multi_agent_tool_factory = None;
111 self
112 }
113
114 #[cfg(feature = "multi_agent")]
120 pub fn with_multi_agent_tool_factory(mut self, factory: MultiAgentToolFactory) -> Self {
121 self.multi_agent_tool_factory = Some(factory);
122 self
123 }
124
125 #[cfg(feature = "skill")]
130 pub fn with_skill_detail_tool_factory(mut self, factory: SkillDetailToolFactory) -> Self {
131 self.skill_detail_tool_factory = Some(factory);
132 self
133 }
134
135 #[cfg(feature = "skill")]
139 pub fn with_list_skills_tool_factory(mut self, factory: ListSkillsToolFactory) -> Self {
140 self.list_skills_tool_factory = Some(factory);
141 self
142 }
143
144 pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
145 let prompt = prompt.into();
146 self.inner = self.inner.system_prompt(prompt.clone());
147 self.system_prompt = Some(prompt);
148 self
149 }
150
151 pub fn enable_thought(self, enable: bool) -> Self {
152 Self {
153 inner: self.inner.enable_thought(enable),
154 ..self
155 }
156 }
157
158 pub fn reasoning(self, config: agent_base::ReasoningConfig) -> Self {
159 Self {
160 inner: self.inner.reasoning(config),
161 ..self
162 }
163 }
164
165 pub fn enable_thinking(self, enable: bool) -> Self {
166 Self {
167 inner: self.inner.enable_thinking(enable),
168 ..self
169 }
170 }
171
172 pub fn thinking_budget(self, budget: u64) -> Self {
173 Self {
174 inner: self.inner.thinking_budget(budget),
175 ..self
176 }
177 }
178
179 pub fn tool_timeout(self, timeout_ms: u64) -> Self {
180 Self {
181 inner: self.inner.tool_timeout(timeout_ms),
182 ..self
183 }
184 }
185
186 pub fn max_tool_output_chars(self, max_chars: usize) -> Self {
187 Self {
188 inner: self.inner.max_tool_output_chars(max_chars),
189 ..self
190 }
191 }
192
193 pub fn max_sessions(self, max: usize) -> Self {
194 Self {
195 inner: self.inner.max_sessions(max),
196 ..self
197 }
198 }
199
200 pub fn max_turns_per_session(self, max: usize) -> Self {
201 Self {
202 inner: self.inner.max_turns_per_session(max),
203 ..self
204 }
205 }
206
207 pub fn execution_max_turns(self, max: u32) -> Self {
208 Self {
209 inner: self.inner.execution_max_turns(max),
210 ..self
211 }
212 }
213
214 pub fn max_message_tokens(self, max: usize) -> Self {
215 Self {
216 inner: self.inner.max_message_tokens(max),
217 ..self
218 }
219 }
220
221 pub fn register_tool(mut self, tool: impl Tool + 'static) -> Self {
222 let tool_arc: Arc<dyn Tool> = Arc::new(tool);
223 self.tool_names.insert(tool_arc.name().to_string());
224 self.business_tools.push(tool_arc.clone());
225 self.inner = self.inner.register_tool_arc(tool_arc);
226 self
227 }
228
229 pub fn register_tool_arc(mut self, tool: Arc<dyn Tool>) -> Self {
230 self.tool_names.insert(tool.name().to_string());
231 self.business_tools.push(tool.clone());
232 self.inner = self.inner.register_tool_arc(tool);
233 self
234 }
235
236 pub fn approval_handler(self, handler: Arc<dyn agent_base::ApprovalHandler>) -> Self {
237 Self {
238 inner: self.inner.approval_handler(handler),
239 ..self
240 }
241 }
242
243 pub fn tool_policy(self, policy: Arc<dyn agent_base::ToolPolicy>) -> Self {
244 Self {
245 inner: self.inner.tool_policy(policy),
246 ..self
247 }
248 }
249
250 pub fn middleware(self, mw: impl agent_base::Middleware + 'static) -> Self {
251 Self {
252 inner: self.inner.middleware(mw),
253 ..self
254 }
255 }
256
257 pub fn guard(self, guard: impl agent_base::ReactLoopGuard + 'static) -> Self {
258 Self {
259 inner: self.inner.guard(guard),
260 ..self
261 }
262 }
263
264 pub fn context_window(self, max_tokens: usize) -> Self {
265 Self {
266 inner: self.inner.context_window(max_tokens),
267 ..self
268 }
269 }
270
271 pub fn context_window_manager(self, manager: agent_base::ContextWindowManager) -> Self {
272 Self {
273 inner: self.inner.context_window_manager(manager),
274 ..self
275 }
276 }
277
278 pub fn response_format(self, format: agent_base::ResponseFormat) -> Self {
279 Self {
280 inner: self.inner.response_format(format),
281 ..self
282 }
283 }
284
285 pub fn llm_retry(self, retry: agent_base::RetryConfig) -> Self {
286 Self {
287 inner: self.inner.llm_retry(retry),
288 ..self
289 }
290 }
291
292 pub fn session_store(self, store: Arc<dyn agent_base::SessionStore>) -> Self {
293 Self {
294 inner: self.inner.session_store(store),
295 ..self
296 }
297 }
298
299 pub fn error_recovery(mut self, recovery: Arc<dyn agent_base::ToolErrorRecovery>) -> Self {
300 self.error_recovery = Some(recovery.clone());
301 self.inner = self.inner.error_recovery(recovery);
302 self
303 }
304
305 pub fn tool_error_retry_prompt(self, prompt: impl Into<String>) -> Self {
306 Self {
307 inner: self.inner.tool_error_retry_prompt(prompt),
308 ..self
309 }
310 }
311
312 pub fn language(mut self, language: agent_base::Language) -> Self {
313 self.language = Some(language.clone());
314 self.inner = self.inner.language(language);
315 self
316 }
317
318 pub fn context_compactor(mut self, compactor: Arc<dyn agent_base::ContextCompaction>) -> Self {
323 self.context_compactor = Some(compactor);
324 self
325 }
326
327 pub fn event_bus_capacity(self, capacity: usize) -> Self {
328 Self {
329 inner: self.inner.event_bus_capacity(capacity),
330 ..self
331 }
332 }
333
334 pub fn session_id_generator(
335 self,
336 generator: Arc<dyn agent_base::types::SessionIdGenerator>,
337 ) -> Self {
338 Self {
339 inner: self.inner.session_id_generator(generator),
340 ..self
341 }
342 }
343
344 pub fn apply_if<T>(self, value: Option<T>, f: impl FnOnce(Self, T) -> Self) -> Self {
352 match value {
353 Some(v) => f(self, v),
354 None => self,
355 }
356 }
357
358 #[cfg(feature = "skill")]
359 pub fn register_skill(mut self, skill: impl Skill + 'static) -> Self {
360 self.skills.push(Arc::new(skill));
361 self
362 }
363
364 #[cfg(feature = "skill")]
365 pub fn register_skills(mut self, skills: Vec<Arc<dyn Skill>>) -> Self {
366 self.skills.extend(skills);
367 self
368 }
369
370 #[cfg(feature = "skill")]
371 pub fn skill_prompter(mut self, prompter: Arc<dyn SkillPrompter>) -> Self {
372 self.skill_prompter = Some(prompter);
373 self
374 }
375
376 #[cfg(feature = "skill")]
377 pub fn disable_skill_prompt_injection(mut self) -> Self {
378 self.disable_skill_prompt_injection = true;
379 self
380 }
381
382 #[cfg(feature = "skill")]
383 pub fn skill_detail_tool_name(mut self, name: impl Into<String>) -> Self {
384 self.skill_detail_tool_name = name.into();
385 self
386 }
387
388 pub fn build(self) -> AgentResult<AgentRuntime> {
391 #[cfg(feature = "skill")]
392 {
393 self.build_with_skills()
394 }
395 #[cfg(not(feature = "skill"))]
396 {
397 self.build_inner()
398 }
399 }
400
401 #[cfg(feature = "multi_agent")]
407 pub fn build_with_ma(self) -> AgentResult<(AgentRuntime, Option<Arc<MultiAgentRuntime>>)> {
408 #[cfg(feature = "skill")]
409 {
410 self.build_with_skills_ma()
411 }
412 #[cfg(not(feature = "skill"))]
413 {
414 self.build_inner_ma()
415 }
416 }
417
418 #[allow(dead_code, unused_mut)]
419 fn build_inner(mut self) -> AgentResult<AgentRuntime> {
420 if self.inner.get_guard().is_none() {
423 self.inner = self
424 .inner
425 .guard(crate::guard::DefaultGuard::with_llm_client(
426 crate::guard::DefaultGuardConfig::default(),
427 self.client.clone(),
428 ));
429 }
430
431 #[cfg(feature = "multi_agent")]
432 let lang = self.language.clone().unwrap_or_default();
433 #[cfg(feature = "multi_agent")]
434 let business_tools = std::mem::take(&mut self.business_tools);
435 #[cfg(feature = "multi_agent")]
436 let error_recovery = self.error_recovery.clone();
437 #[cfg(feature = "multi_agent")]
438 let tool_names = self.tool_names.clone();
439
440 #[cfg(feature = "multi_agent")]
441 let ma_config = self.multi_agent_config.clone();
442 #[cfg(feature = "multi_agent")]
443 let ma_tool_factory = self.multi_agent_tool_factory.take();
444
445 #[cfg(feature = "multi_agent")]
447 if ma_config.as_ref().map(|c| c.enabled).unwrap_or(false) {
448 let ma_prompt = build_multi_agent_system_prompt();
449 let new_prompt = match self.system_prompt.take() {
450 Some(existing) => format!("{}\n\n---\n\n{}", existing, ma_prompt),
451 None => ma_prompt,
452 };
453 self.inner = self.inner.system_prompt(new_prompt);
454 }
455
456 if let Some(compactor) = self.context_compactor.take() {
458 self.inner = self.inner.context_compactor(compactor);
459 }
460
461 let runtime = self.inner.build()?;
462
463 #[cfg(feature = "multi_agent")]
465 if let Some(config) = ma_config
466 && config.enabled
467 {
468 setup_multi_agent(
469 &runtime,
470 config,
471 lang,
472 business_tools,
473 error_recovery,
474 &tool_names,
475 ma_tool_factory,
476 )?;
477 }
478
479 Ok(runtime)
480 }
481
482 #[cfg(feature = "multi_agent")]
484 #[allow(dead_code, unused_mut)]
485 fn build_inner_ma(mut self) -> AgentResult<(AgentRuntime, Option<Arc<MultiAgentRuntime>>)> {
486 if self.inner.get_guard().is_none() {
487 self.inner = self
488 .inner
489 .guard(crate::guard::DefaultGuard::with_llm_client(
490 crate::guard::DefaultGuardConfig::default(),
491 self.client.clone(),
492 ));
493 }
494
495 let lang = self.language.clone().unwrap_or_default();
496 let business_tools = std::mem::take(&mut self.business_tools);
497 let error_recovery = self.error_recovery.clone();
498 let tool_names = self.tool_names.clone();
499 let ma_config = self.multi_agent_config.clone();
500 let ma_tool_factory = self.multi_agent_tool_factory.take();
501
502 if ma_config.as_ref().map(|c| c.enabled).unwrap_or(false) {
503 let ma_prompt = build_multi_agent_system_prompt();
504 let new_prompt = match self.system_prompt.take() {
505 Some(existing) => format!("{}\n\n---\n\n{}", existing, ma_prompt),
506 None => ma_prompt,
507 };
508 self.inner = self.inner.system_prompt(new_prompt);
509 }
510
511 if let Some(compactor) = self.context_compactor.take() {
512 self.inner = self.inner.context_compactor(compactor);
513 }
514
515 let runtime = self.inner.build()?;
516
517 let ma_runtime_out = if let Some(config) = ma_config
518 && config.enabled
519 {
520 Some(setup_multi_agent(
521 &runtime,
522 config,
523 lang,
524 business_tools,
525 error_recovery,
526 &tool_names,
527 ma_tool_factory,
528 )?)
529 } else {
530 None
531 };
532
533 Ok((runtime, ma_runtime_out))
534 }
535
536 #[cfg(feature = "skill")]
548 fn build_with_skills(mut self) -> AgentResult<AgentRuntime> {
549 if self.inner.get_guard().is_none() {
552 self.inner = self
553 .inner
554 .guard(crate::guard::DefaultGuard::with_llm_client(
555 crate::guard::DefaultGuardConfig::default(),
556 self.client.clone(),
557 ));
558 }
559
560 let mut ab = self.inner;
561 #[cfg(feature = "multi_agent")]
562 let lang = self.language.clone().unwrap_or_default();
563 #[cfg(feature = "multi_agent")]
564 let business_tools = std::mem::take(&mut self.business_tools);
565 #[cfg(feature = "multi_agent")]
566 let error_recovery = self.error_recovery.clone();
567 #[cfg(feature = "multi_agent")]
568 let tool_names = self.tool_names.clone();
569
570 #[cfg(feature = "multi_agent")]
571 let ma_config = self.multi_agent_config.clone();
572 #[cfg(feature = "multi_agent")]
573 let ma_tool_factory = self.multi_agent_tool_factory.take();
574
575 if !self.skills.is_empty() {
577 let prompter: Arc<dyn SkillPrompter> = self
578 .skill_prompter
579 .take()
580 .unwrap_or_else(|| Arc::new(LazySkillPrompter::new()));
581
582 let mut skill_refs: Vec<Arc<dyn Skill>> = Vec::new();
583
584 for skill in self.skills {
585 for tool in skill.tools() {
586 let tool_name = tool.name().to_string();
587 if self.tool_names.contains(&tool_name) {
588 return Err(agent_base::AgentError::internal(format!(
589 "Tool name conflict: `{}` (Skill `{}`)",
590 tool_name,
591 skill.name()
592 )));
593 }
594 self.tool_names.insert(tool_name);
595 ab = ab.register_tool_arc(tool);
596 }
597 skill_refs.push(skill);
598 }
599
600 if !self.disable_skill_prompt_injection {
601 let skill_prompt = prompter.build_prompt(&skill_refs, &self.skill_detail_tool_name);
602 if !skill_prompt.is_empty() {
603 let new_prompt = match self.system_prompt.take() {
604 Some(existing) => format!("{}\n\n---\n\n{}", existing, skill_prompt),
605 None => skill_prompt,
606 };
607 self.system_prompt = Some(new_prompt.clone());
608 ab = ab.system_prompt(new_prompt);
609 }
610 }
611
612 if let Some(factory) = self.skill_detail_tool_factory.take() {
615 let detail_tool = factory(skill_refs.clone(), self.skill_detail_tool_name);
616 ab = ab.register_tool_arc(detail_tool);
617 }
618
619 if let Some(factory) = self.list_skills_tool_factory.take() {
621 let registry = Arc::new(crate::skill::SkillRegistry::new());
622 for skill in &skill_refs {
623 tokio::task::block_in_place(|| {
624 tokio::runtime::Handle::current().block_on(async {
625 registry.register(skill.clone()).await;
626 })
627 });
628 }
629 let list_tool = factory(registry);
630 ab = ab.register_tool_arc(list_tool);
631 }
632 }
633
634 #[cfg(feature = "multi_agent")]
636 if ma_config.as_ref().map(|c| c.enabled).unwrap_or(false) {
637 let ma_prompt = build_multi_agent_system_prompt();
638 let new_prompt = match self.system_prompt.take() {
639 Some(existing) => format!("{}\n\n---\n\n{}", existing, ma_prompt),
640 None => ma_prompt,
641 };
642 ab = ab.system_prompt(new_prompt);
643 }
644
645 if let Some(compactor) = self.context_compactor.take() {
647 ab = ab.context_compactor(compactor);
648 }
649
650 let runtime = ab.build()?;
651
652 #[cfg(feature = "multi_agent")]
654 if let Some(config) = ma_config
655 && config.enabled
656 {
657 setup_multi_agent(
658 &runtime,
659 config,
660 lang,
661 business_tools,
662 error_recovery,
663 &tool_names,
664 ma_tool_factory,
665 )?;
666 }
667
668 Ok(runtime)
669 }
670
671 #[cfg(all(feature = "skill", feature = "multi_agent"))]
673 fn build_with_skills_ma(
674 mut self,
675 ) -> AgentResult<(AgentRuntime, Option<Arc<MultiAgentRuntime>>)> {
676 if self.inner.get_guard().is_none() {
677 self.inner = self
678 .inner
679 .guard(crate::guard::DefaultGuard::with_llm_client(
680 crate::guard::DefaultGuardConfig::default(),
681 self.client.clone(),
682 ));
683 }
684
685 let mut ab = self.inner;
686 let lang = self.language.clone().unwrap_or_default();
687 let business_tools = std::mem::take(&mut self.business_tools);
688 let error_recovery = self.error_recovery.clone();
689 let tool_names = self.tool_names.clone();
690 let ma_config = self.multi_agent_config.clone();
691 let ma_tool_factory = self.multi_agent_tool_factory.take();
692
693 if !self.skills.is_empty() {
695 let prompter: Arc<dyn SkillPrompter> = self
696 .skill_prompter
697 .take()
698 .unwrap_or_else(|| Arc::new(LazySkillPrompter::new()));
699
700 let mut skill_refs: Vec<Arc<dyn Skill>> = Vec::new();
701
702 for skill in self.skills {
703 for tool in skill.tools() {
704 let tool_name = tool.name().to_string();
705 if self.tool_names.contains(&tool_name) {
706 return Err(agent_base::AgentError::internal(format!(
707 "Tool name conflict: `{}` (Skill `{}`)",
708 tool_name,
709 skill.name()
710 )));
711 }
712 self.tool_names.insert(tool_name);
713 ab = ab.register_tool_arc(tool);
714 }
715 skill_refs.push(skill);
716 }
717
718 if !self.disable_skill_prompt_injection {
719 let skill_prompt = prompter.build_prompt(&skill_refs, &self.skill_detail_tool_name);
720 if !skill_prompt.is_empty() {
721 let new_prompt = match self.system_prompt.take() {
722 Some(existing) => format!("{}\n\n---\n\n{}", existing, skill_prompt),
723 None => skill_prompt,
724 };
725 self.system_prompt = Some(new_prompt.clone());
726 ab = ab.system_prompt(new_prompt);
727 }
728 }
729
730 if let Some(factory) = self.skill_detail_tool_factory.take() {
731 let detail_tool = factory(skill_refs.clone(), self.skill_detail_tool_name);
732 ab = ab.register_tool_arc(detail_tool);
733 }
734
735 if let Some(factory) = self.list_skills_tool_factory.take() {
736 let registry = Arc::new(crate::skill::SkillRegistry::new());
737 for skill in &skill_refs {
738 tokio::task::block_in_place(|| {
739 tokio::runtime::Handle::current().block_on(async {
740 registry.register(skill.clone()).await;
741 })
742 });
743 }
744 let list_tool = factory(registry);
745 ab = ab.register_tool_arc(list_tool);
746 }
747 }
748
749 if ma_config.as_ref().map(|c| c.enabled).unwrap_or(false) {
751 let ma_prompt = build_multi_agent_system_prompt();
752 let new_prompt = match self.system_prompt.take() {
753 Some(existing) => format!("{}\n\n---\n\n{}", existing, ma_prompt),
754 None => ma_prompt,
755 };
756 ab = ab.system_prompt(new_prompt);
757 }
758
759 if let Some(compactor) = self.context_compactor.take() {
760 ab = ab.context_compactor(compactor);
761 }
762
763 let runtime = ab.build()?;
764
765 let ma_runtime_out = if let Some(config) = ma_config
766 && config.enabled
767 {
768 Some(setup_multi_agent(
769 &runtime,
770 config,
771 lang,
772 business_tools,
773 error_recovery,
774 &tool_names,
775 ma_tool_factory,
776 )?)
777 } else {
778 None
779 };
780
781 Ok((runtime, ma_runtime_out))
782 }
783}
784
785#[cfg(feature = "multi_agent")]
796pub fn setup_multi_agent(
797 runtime: &AgentRuntime,
798 config: MultiAgentConfig,
799 lang: agent_base::Language,
800 business_tools: Vec<Arc<dyn Tool>>,
801 error_recovery: Option<Arc<dyn agent_base::ToolErrorRecovery>>,
802 existing_tool_names: &HashSet<String>,
803 tool_factory: Option<MultiAgentToolFactory>,
804) -> AgentResult<Arc<MultiAgentRuntime>> {
805 let client = runtime.provider();
806 let cancel_token = runtime.cancel_token();
807 let tool_policy = runtime.tool_policy().cloned();
808 let approval_handler = runtime.approval_handler().cloned();
809
810 let ma_runtime = Arc::new(MultiAgentRuntime::new(
811 config.clone(),
812 client,
813 business_tools,
814 cancel_token,
815 error_recovery,
816 lang,
817 tool_policy,
818 approval_handler,
819 ));
820
821 ma_runtime.set_session_manager(Arc::new(runtime.session_manager().clone()));
823
824 let (event_tx, mut event_rx) =
826 tokio::sync::mpsc::unbounded_channel::<agent_base::RuntimeEvent>();
827 ma_runtime.set_event_sender(event_tx);
828 let parent_runtime = runtime.clone();
829 tokio::spawn(async move {
830 while let Some(event) = event_rx.recv().await {
831 parent_runtime.emit_event(event);
832 }
833 });
834
835 if let Some(factory) = tool_factory {
837 let tools = factory(ma_runtime.clone());
838 let registry = runtime.tools_mut();
839 let mut reg = tokio::task::block_in_place(|| registry.blocking_write());
840 for tool in tools {
841 let tool_name = tool.name().to_string();
842 if !existing_tool_names.contains(&tool_name) {
843 reg.register_arc(tool);
844 }
845 }
846 drop(reg);
847 }
848
849 Ok(ma_runtime)
850}
851
852#[cfg(feature = "multi_agent")]
854pub fn build_multi_agent_system_prompt() -> String {
855 r#"## Multi-Agent Capabilities
856
857You can spawn sub-agents to work on tasks concurrently. Each sub-agent is an independent assistant with its own tools (repo_map, read_file, list_files, execute_command, etc.) and can reason through problems autonomously.
858
859### Tools
860
861- `spawn_agent`: Create a sub-agent. Use the `task` field to describe what you want done — the sub-agent will figure out the steps. Example: `spawn_agent({task_name: "analyze-codex", task: "analyze the codex project structure and output a structured report"})`.
862- `send_message`: Send follow-up context or tasks to a sub-agent (`trigger: true` starts execution).
863- `list_agents`: List active sub-agents with their assigned task and status — use it to answer "what is agent X doing?".
864- `close_agent`: Close a sub-agent when done.
865
866### How Results Reach You
867
868Sub-agent results are **pushed into your context automatically** when they complete — there is no wait tool. To wait, simply end your turn: you will be woken with the result as a new message. Keep working on other things instead of polling.
869
870### How Sub-Agents Work
871
872Sub-agents are capable — they can explore codebases, read files, run commands, and produce structured analysis. You don't need to plan their steps or specify which tools to use. Just describe the goal in `task`, and the sub-agent handles the rest.
873
874### When to Spawn
875
876- Parallel tasks: analyze multiple codebases, compare implementations, etc.
877- Tasks you want to delegate while you continue other work
878
879### When NOT to Spawn
880
881- Simple lookups (just use read_file or repo_map directly)
882- Tasks that need your current conversation context"#
883 .to_string()
884}
885
886pub fn build_memory_system_prompt() -> String {
895 r#"## Memory
896
897You have a persistent file-based memory at `.phi/memory/`. Use `read_file` and `write_file` to manage it — there are no dedicated memory tools.
898
899### How Memory Works
900
901- `MEMORY.md` is the index — it lists all memories with one-line descriptions. Read it first when you need to recall something.
902- Each memory is a separate `.md` file with YAML frontmatter:
903 ```yaml
904 ---
905 name: <short-kebab-case-slug>
906 description: <one-line summary — used to decide relevance during recall>
907 metadata:
908 node_type: memory
909 type: user | feedback | project | reference
910 ---
911
912 <the fact or instruction>
913 ```
914- The `description` field is the key for recall — write it so you can tell at a glance whether this memory is relevant to the current task.
915- Link related memories with `[[memory-name]]` in the body.
916- `user` type = who the user is (role, expertise, preferences).
917- `feedback` type = guidance the user has given on how you should work.
918- `project` type = ongoing work, goals, or constraints.
919- `reference` type = pointers to external resources (URLs, dashboards, tickets).
920
921### When to Use Memory
922
923- The user explicitly asks you to remember something ("remember this", "save that")
924- You learn something important about the user's preferences or workflow
925- After completing a significant task, save context that would help in future sessions
926- The user gives you feedback on how to work — save it as `feedback` type
927
928### When NOT to Use Memory
929
930- For transient information that won't be useful beyond this session
931- For facts already recorded in the codebase (code structure, git history, config files)
932- For items that only matter to the current conversation
933
934### Pro Tips
935
936- When creating your first memory of a new type, you can read template files for format reference (check `.phi/templates/memory/` if available).
937- Keep the MEMORY.md index concise — it's loaded into context every session.
938- Before writing a new memory, check if an existing file already covers it — update instead of duplicating.
939
940### Workflow
941
942**To recall:** read `MEMORY.md` → find relevant entries by description → read the specific `.md` files you need.
943**To remember:** create a new `.md` file with proper frontmatter → update `MEMORY.md` with a new entry.
944**To update:** edit the existing `.md` file (don't create a duplicate).
945**To forget:** delete the `.md` file → remove its entry from `MEMORY.md`."#
946 .to_string()
947}
948
949#[cfg(test)]
950mod tests {
951 use super::*;
952 use agent_base::Content;
953 use agent_base::llm_trait::response::FinishReason;
954 use agent_base::llm_trait::types::UsageInfo;
955 use agent_base::llm_trait::{
956 Capabilities, ChatRequest, ChatResponse, ChatStream, LlmError, LlmProvider, ProviderInfo,
957 };
958
959 struct StubProvider;
962
963 #[async_trait::async_trait]
964 impl LlmProvider for StubProvider {
965 async fn stream(&self, _request: ChatRequest) -> Result<ChatStream, LlmError> {
966 let chunks = vec![
967 Ok(agent_base::StreamChunk::Text("ok".to_string())),
968 Ok(agent_base::StreamChunk::Stop {
969 finish_reason: Some("stop".to_string()),
970 }),
971 ];
972 Ok(ChatStream::new(Box::pin(futures_util::stream::iter(
973 chunks,
974 ))))
975 }
976
977 async fn chat(&self, _request: ChatRequest) -> Result<ChatResponse, LlmError> {
978 Ok(ChatResponse {
979 content: "ok".to_string(),
980 tool_calls: vec![],
981 usage: UsageInfo::default(),
982 finish_reason: FinishReason::Stop,
983 raw: None,
984 reasoning_content: None,
985 thinking_signature: None,
986 })
987 }
988
989 fn capabilities(&self) -> Capabilities {
990 Capabilities {
991 supports_streaming: true,
992 supports_tools: true,
993 ..Default::default()
994 }
995 }
996
997 fn info(&self) -> ProviderInfo {
998 ProviderInfo {
999 name: "stub".to_string(),
1000 model: "stub-model".to_string(),
1001 version: None,
1002 }
1003 }
1004 }
1005
1006 fn make_client() -> Arc<dyn LlmProvider> {
1007 Arc::new(StubProvider)
1008 }
1009
1010 #[cfg(feature = "multi_agent")]
1013 #[tokio::test(flavor = "multi_thread")]
1014 async fn test_setup_multi_agent_without_factory_registers_no_tools() {
1015 let client = make_client();
1016 let runtime = agent_base::AgentBuilder::new(client.clone())
1017 .build()
1018 .unwrap();
1019 let config = MultiAgentConfig::enabled();
1020
1021 let result = setup_multi_agent(
1022 &runtime,
1023 config,
1024 agent_base::Language::En,
1025 vec![],
1026 None,
1027 &HashSet::new(),
1028 None, );
1030 assert!(result.is_ok());
1031 let ma_runtime = result.unwrap();
1032 let agents = ma_runtime.list_agents();
1034 assert!(agents.is_empty());
1035 }
1036
1037 #[cfg(feature = "multi_agent")]
1038 #[tokio::test(flavor = "multi_thread")]
1039 async fn test_setup_multi_agent_with_factory_registers_tools() {
1040 let client = make_client();
1041 let runtime = agent_base::AgentBuilder::new(client.clone())
1042 .build()
1043 .unwrap();
1044 let config = MultiAgentConfig::enabled();
1045
1046 let factory: MultiAgentToolFactory = Arc::new(|_rt| {
1047 struct FakeTool;
1049 #[async_trait::async_trait]
1050 impl Tool for FakeTool {
1051 fn name(&self) -> &'static str {
1052 "fake_tool"
1053 }
1054 fn description(&self) -> &'static str {
1055 ""
1056 }
1057 fn schema(&self) -> serde_json::Value {
1058 serde_json::json!({})
1059 }
1060 async fn call(
1061 &self,
1062 _args: &serde_json::Value,
1063 _ctx: &agent_base::ToolContext,
1064 ) -> AgentResult<Vec<Content>> {
1065 Ok(vec![Content::text("ok")])
1066 }
1067 }
1068 vec![Arc::new(FakeTool)]
1069 });
1070
1071 let result = setup_multi_agent(
1072 &runtime,
1073 config,
1074 agent_base::Language::En,
1075 vec![],
1076 None,
1077 &HashSet::new(),
1078 Some(factory),
1079 );
1080 assert!(result.is_ok());
1081
1082 let tools: Vec<String> = tokio::task::block_in_place(|| {
1084 let tools = runtime.tools_mut();
1085 let guard = tools.blocking_read();
1086 guard.metadatas().into_iter().map(|m| m.name).collect()
1087 });
1088 assert!(tools.contains(&"fake_tool".to_string()));
1089 }
1090
1091 #[cfg(feature = "multi_agent")]
1092 #[tokio::test(flavor = "multi_thread")]
1093 async fn test_setup_multi_agent_skips_duplicate_tool_names() {
1094 let client = make_client();
1095 let runtime = agent_base::AgentBuilder::new(client.clone())
1096 .build()
1097 .unwrap();
1098
1099 struct DupTool;
1101 #[async_trait::async_trait]
1102 impl Tool for DupTool {
1103 fn name(&self) -> &'static str {
1104 "dup_tool"
1105 }
1106 fn description(&self) -> &'static str {
1107 ""
1108 }
1109 fn schema(&self) -> serde_json::Value {
1110 serde_json::json!({})
1111 }
1112 async fn call(
1113 &self,
1114 _args: &serde_json::Value,
1115 _ctx: &agent_base::ToolContext,
1116 ) -> AgentResult<Vec<Content>> {
1117 Ok(vec![Content::text("ok")])
1118 }
1119 }
1120 {
1121 let tools = runtime.tools_mut();
1122 let mut reg = tokio::task::block_in_place(|| tools.blocking_write());
1123 reg.register(DupTool);
1124 }
1125
1126 let factory: MultiAgentToolFactory = Arc::new(|_rt| {
1127 struct FakeTool;
1128 #[async_trait::async_trait]
1129 impl Tool for FakeTool {
1130 fn name(&self) -> &'static str {
1131 "dup_tool"
1132 }
1133 fn description(&self) -> &'static str {
1134 ""
1135 }
1136 fn schema(&self) -> serde_json::Value {
1137 serde_json::json!({})
1138 }
1139 async fn call(
1140 &self,
1141 _args: &serde_json::Value,
1142 _ctx: &agent_base::ToolContext,
1143 ) -> AgentResult<Vec<Content>> {
1144 Ok(vec![Content::text("ok")])
1145 }
1146 }
1147 vec![Arc::new(FakeTool)]
1148 });
1149
1150 let mut existing = HashSet::new();
1151 existing.insert("dup_tool".to_string());
1152
1153 let result = setup_multi_agent(
1154 &runtime,
1155 MultiAgentConfig::enabled(),
1156 agent_base::Language::En,
1157 vec![],
1158 None,
1159 &existing,
1160 Some(factory),
1161 );
1162 assert!(result.is_ok());
1163 let tools = tokio::task::block_in_place(|| {
1165 let tools = runtime.tools_mut();
1166 let guard = tools.blocking_read();
1167 guard
1168 .metadatas()
1169 .into_iter()
1170 .map(|m| m.name)
1171 .collect::<Vec<String>>()
1172 });
1173 let count = tools.iter().filter(|n| n.as_str() == "dup_tool").count();
1174 assert_eq!(count, 1);
1175 }
1176
1177 #[cfg(feature = "multi_agent")]
1180 #[tokio::test(flavor = "multi_thread")]
1181 async fn test_builder_with_multi_agent_without_factory_builds_ok() {
1182 let client = make_client();
1183 let runtime = AgentBuilder::new(client)
1184 .with_multi_agent(MultiAgentConfig::enabled())
1185 .build()
1186 .unwrap();
1187 let tools = tokio::task::block_in_place(|| {
1189 let tools = runtime.tools_mut();
1190 let guard = tools.blocking_read();
1191 guard
1192 .metadatas()
1193 .into_iter()
1194 .map(|m| m.name)
1195 .collect::<Vec<String>>()
1196 });
1197 assert!(!tools.contains(&"spawn_agent".to_string()));
1199 }
1200
1201 #[cfg(feature = "multi_agent")]
1202 #[tokio::test(flavor = "multi_thread")]
1203 async fn test_builder_with_factory_registers_tools() {
1204 let client = make_client();
1205 let factory: MultiAgentToolFactory = Arc::new(|_rt| {
1207 struct TestTool;
1208 #[async_trait::async_trait]
1209 impl Tool for TestTool {
1210 fn name(&self) -> &'static str {
1211 "factory_test_tool"
1212 }
1213 fn description(&self) -> &'static str {
1214 ""
1215 }
1216 fn schema(&self) -> serde_json::Value {
1217 serde_json::json!({})
1218 }
1219 async fn call(
1220 &self,
1221 _args: &serde_json::Value,
1222 _ctx: &agent_base::ToolContext,
1223 ) -> AgentResult<Vec<Content>> {
1224 Ok(vec![Content::text("ok")])
1225 }
1226 }
1227 vec![Arc::new(TestTool)]
1228 });
1229
1230 let runtime = AgentBuilder::new(client)
1231 .with_multi_agent(MultiAgentConfig::enabled())
1232 .with_multi_agent_tool_factory(factory)
1233 .build()
1234 .unwrap();
1235
1236 let tools = tokio::task::block_in_place(|| {
1237 let tools = runtime.tools_mut();
1238 let guard = tools.blocking_read();
1239 guard
1240 .metadatas()
1241 .into_iter()
1242 .map(|m| m.name)
1243 .collect::<Vec<String>>()
1244 });
1245 assert!(tools.contains(&"factory_test_tool".to_string()));
1246 }
1247
1248 #[cfg(feature = "multi_agent")]
1249 #[test]
1250 fn test_builder_disabled_multi_agent_skips_factory() {
1251 let client = make_client();
1252 let factory: MultiAgentToolFactory = Arc::new(|_rt| {
1253 panic!("factory should not be called when multi-agent is not configured");
1254 });
1255
1256 let runtime = AgentBuilder::new(client)
1257 .with_multi_agent_tool_factory(factory)
1258 .build()
1260 .unwrap();
1261
1262 let tools = tokio::task::block_in_place(|| {
1263 let tools = runtime.tools_mut();
1264 let guard = tools.blocking_read();
1265 guard
1266 .metadatas()
1267 .into_iter()
1268 .map(|m| m.name)
1269 .collect::<Vec<String>>()
1270 });
1271 assert!(!tools.contains(&"spawn_agent".to_string()));
1272 }
1273
1274 #[cfg(feature = "multi_agent")]
1277 #[test]
1278 fn test_system_prompt_contains_tool_names() {
1279 let prompt = build_multi_agent_system_prompt();
1280 assert!(prompt.contains("spawn_agent"));
1281 assert!(prompt.contains("send_message"));
1282 assert!(prompt.contains("trigger"));
1283 assert!(prompt.contains("list_agents"));
1284 assert!(prompt.contains("close_agent"));
1285 assert!(!prompt.contains("followup_task"));
1288 assert!(!prompt.contains("wait_agent"));
1291 assert!(prompt.contains("pushed into your context automatically"));
1292 }
1293
1294 #[cfg(feature = "multi_agent")]
1295 #[test]
1296 fn test_system_prompt_contains_guidance() {
1297 let prompt = build_multi_agent_system_prompt();
1298 assert!(prompt.contains("When to Spawn"));
1299 assert!(prompt.contains("When NOT to Spawn"));
1300 assert!(prompt.contains("How Results Reach You"));
1301 }
1302
1303 #[cfg(feature = "multi_agent")]
1306 #[tokio::test(flavor = "multi_thread")]
1307 async fn test_without_multi_agent_clears_config_and_factory() {
1308 let client = make_client();
1309
1310 let factory: MultiAgentToolFactory = Arc::new(|_rt| {
1312 panic!("factory should not be called when multi-agent is cleared");
1313 });
1314
1315 let runtime = AgentBuilder::new(client)
1316 .with_multi_agent(MultiAgentConfig::enabled())
1317 .with_multi_agent_tool_factory(factory)
1318 .without_multi_agent() .build()
1320 .unwrap();
1321
1322 let tools = tokio::task::block_in_place(|| {
1323 let tools = runtime.tools_mut();
1324 let guard = tools.blocking_read();
1325 guard
1326 .metadatas()
1327 .into_iter()
1328 .map(|m| m.name)
1329 .collect::<Vec<String>>()
1330 });
1331 assert!(!tools.contains(&"spawn_agent".to_string()));
1332 }
1333
1334 #[test]
1337 fn test_apply_if_some_applies_transformation() {
1338 let client = make_client();
1339 let builder = AgentBuilder::new(client)
1340 .apply_if(Some("custom prompt"), |b, prompt| b.system_prompt(prompt));
1341 assert!(builder.system_prompt.unwrap().contains("custom prompt"));
1343 }
1344
1345 #[test]
1346 fn test_apply_if_none_passes_through() {
1347 let client = make_client();
1348 let builder = AgentBuilder::new(client).apply_if(None as Option<&str>, |_b, _prompt| {
1349 panic!("should not be called when value is None");
1350 });
1351 assert!(builder.system_prompt.is_none());
1352 }
1353
1354 #[test]
1357 fn test_build_memory_system_prompt_non_empty() {
1358 let prompt = build_memory_system_prompt();
1359 assert!(!prompt.is_empty());
1360 assert!(prompt.contains("Memory"));
1361 assert!(prompt.contains("MEMORY.md"));
1362 assert!(prompt.contains("read_file"));
1363 assert!(prompt.contains("write_file"));
1364 }
1365
1366 struct NamedTool(&'static str);
1369
1370 #[async_trait::async_trait]
1371 impl Tool for NamedTool {
1372 fn name(&self) -> &'static str {
1373 self.0
1374 }
1375
1376 fn description(&self) -> &'static str {
1377 ""
1378 }
1379
1380 fn schema(&self) -> serde_json::Value {
1381 serde_json::json!({})
1382 }
1383
1384 async fn call(
1385 &self,
1386 _args: &serde_json::Value,
1387 _ctx: &agent_base::ToolContext,
1388 ) -> AgentResult<Vec<Content>> {
1389 Ok(vec![Content::text("ok")])
1390 }
1391 }
1392
1393 fn runtime_tool_names(runtime: &AgentRuntime) -> Vec<String> {
1394 tokio::task::block_in_place(|| {
1395 let tools = runtime.tools_mut();
1396 let guard = tools.blocking_read();
1397 guard.metadatas().into_iter().map(|m| m.name).collect()
1398 })
1399 }
1400
1401 #[tokio::test(flavor = "multi_thread")]
1402 async fn test_builder_scalar_passthrough_methods() {
1403 let client = make_client();
1404 let runtime = AgentBuilder::new(client)
1405 .enable_thought(true)
1406 .reasoning(agent_base::ReasoningConfig::default())
1407 .enable_thinking(false)
1408 .thinking_budget(1000)
1409 .tool_timeout(5000)
1410 .max_tool_output_chars(4000)
1411 .max_sessions(16)
1412 .max_turns_per_session(20)
1413 .execution_max_turns(10)
1414 .max_message_tokens(8000)
1415 .context_window(64_000)
1416 .context_window_manager(agent_base::ContextWindowManager::new(64_000))
1417 .response_format(agent_base::ResponseFormat::JsonObject)
1418 .llm_retry(agent_base::RetryConfig::default())
1419 .tool_error_retry_prompt("please retry")
1420 .language(agent_base::Language::En)
1421 .event_bus_capacity(256)
1422 .build()
1423 .unwrap();
1424
1425 assert!(runtime.provider().capabilities().supports_streaming);
1426 }
1427
1428 #[tokio::test(flavor = "multi_thread")]
1429 async fn test_register_tool_variants() {
1430 let client = make_client();
1431 let runtime = AgentBuilder::new(client)
1432 .register_tool(NamedTool("tool_by_value"))
1433 .register_tool_arc(Arc::new(NamedTool("tool_by_arc")))
1434 .build()
1435 .unwrap();
1436
1437 let names = runtime_tool_names(&runtime);
1438 assert!(names.contains(&"tool_by_value".to_string()));
1439 assert!(names.contains(&"tool_by_arc".to_string()));
1440 }
1441
1442 #[cfg(feature = "skill")]
1443 mod skill_tests {
1444 use super::*;
1445 use crate::skill::Skill;
1446
1447 struct TestSkill;
1448
1449 impl Skill for TestSkill {
1450 fn name(&self) -> &'static str {
1451 "test_skill"
1452 }
1453
1454 fn brief_description(&self) -> String {
1455 "a test skill".to_string()
1456 }
1457
1458 fn detailed_description(&self) -> String {
1459 "detailed test skill".to_string()
1460 }
1461
1462 fn tools(&self) -> Vec<Arc<dyn Tool>> {
1463 vec![]
1464 }
1465 }
1466
1467 struct ToolSkill;
1468
1469 impl Skill for ToolSkill {
1470 fn name(&self) -> &'static str {
1471 "tool_skill"
1472 }
1473
1474 fn brief_description(&self) -> String {
1475 "skill with a tool".to_string()
1476 }
1477
1478 fn detailed_description(&self) -> String {
1479 "skill that provides a tool".to_string()
1480 }
1481
1482 fn tools(&self) -> Vec<Arc<dyn Tool>> {
1483 vec![Arc::new(NamedTool("skill_provided_tool"))]
1484 }
1485 }
1486
1487 #[tokio::test(flavor = "multi_thread")]
1488 async fn test_register_skill_builds_ok() {
1489 let client = make_client();
1490 let runtime = AgentBuilder::new(client)
1491 .register_skill(TestSkill)
1492 .build()
1493 .unwrap();
1494 assert!(runtime_tool_names(&runtime).is_empty());
1496 }
1497
1498 #[tokio::test(flavor = "multi_thread")]
1499 async fn test_register_skill_with_tool_registers_tool() {
1500 let client = make_client();
1501 let runtime = AgentBuilder::new(client)
1502 .register_skill(ToolSkill)
1503 .build()
1504 .unwrap();
1505 assert!(runtime_tool_names(&runtime).contains(&"skill_provided_tool".to_string()));
1506 }
1507
1508 #[tokio::test(flavor = "multi_thread")]
1509 async fn test_register_skill_tool_name_conflict() {
1510 let client = make_client();
1511 let result = AgentBuilder::new(client)
1512 .register_tool(NamedTool("skill_provided_tool"))
1513 .register_skill(ToolSkill)
1514 .build();
1515 let err = result.err().unwrap();
1516 assert!(format!("{err}").contains("Tool name conflict"));
1517 }
1518
1519 #[tokio::test(flavor = "multi_thread")]
1520 async fn test_skill_detail_tool_factory_registers_tool() {
1521 let client = make_client();
1522 let factory: SkillDetailToolFactory = Arc::new(|_skills, name| {
1523 assert_eq!(name, "get_skill_detail");
1524 Arc::new(NamedTool("detail_tool"))
1525 });
1526 let runtime = AgentBuilder::new(client)
1527 .register_skill(TestSkill)
1528 .with_skill_detail_tool_factory(factory)
1529 .build()
1530 .unwrap();
1531 assert!(runtime_tool_names(&runtime).contains(&"detail_tool".to_string()));
1532 }
1533
1534 #[tokio::test(flavor = "multi_thread")]
1535 async fn test_list_skills_tool_factory_registers_tool() {
1536 let client = make_client();
1537 let factory: ListSkillsToolFactory =
1538 Arc::new(|_registry| Arc::new(NamedTool("list_skills_tool")));
1539 let runtime = AgentBuilder::new(client)
1540 .register_skill(TestSkill)
1541 .with_list_skills_tool_factory(factory)
1542 .build()
1543 .unwrap();
1544 assert!(runtime_tool_names(&runtime).contains(&"list_skills_tool".to_string()));
1545 }
1546
1547 #[tokio::test(flavor = "multi_thread")]
1548 async fn test_disable_skill_prompt_injection_builds() {
1549 let client = make_client();
1550 let runtime = AgentBuilder::new(client)
1551 .register_skill(ToolSkill)
1552 .disable_skill_prompt_injection()
1553 .build()
1554 .unwrap();
1555 assert!(runtime_tool_names(&runtime).contains(&"skill_provided_tool".to_string()));
1557 }
1558 }
1559}