1use std::collections::HashSet;
2use std::path::PathBuf;
3use std::sync::Arc;
4
5use agent_base::{AgentResult, AgentRuntime, Tool};
6
7#[cfg(feature = "multi_agent")]
8use crate::multi_agent::{MultiAgentConfig, MultiAgentRuntime};
9
10#[cfg(feature = "skill")]
11use crate::skill::{LazySkillPrompter, Skill, SkillPrompter};
12
13fn build_agent_instructions_prompt(paths: &[PathBuf]) -> Option<String> {
18 let mut sections = Vec::new();
19
20 for path in paths {
21 if let Ok(content) = std::fs::read_to_string(path) {
22 let trimmed = content.trim();
23 if !trimmed.is_empty() {
24 sections.push(trimmed.to_string());
25 }
26 }
27 }
28
29 if sections.is_empty() {
30 return None;
31 }
32
33 sections.reverse();
35 Some(sections.join("\n\n---\n\n"))
36}
37
38#[cfg(feature = "multi_agent")]
40pub type MultiAgentToolFactory =
41 Arc<dyn Fn(Arc<MultiAgentRuntime>) -> Vec<Arc<dyn Tool>> + Send + Sync>;
42
43#[cfg(feature = "skill")]
45pub type SkillDetailToolFactory =
46 Arc<dyn Fn(Vec<Arc<dyn Skill>>, String) -> Arc<dyn Tool> + Send + Sync>;
47
48#[cfg(feature = "skill")]
50pub type ListSkillsToolFactory =
51 Arc<dyn Fn(Arc<crate::skill::SkillRegistry>) -> Arc<dyn Tool> + Send + Sync>;
52
53pub struct AgentBuilder {
54 inner: agent_base::AgentBuilder,
55 client: Arc<dyn agent_base::llm_trait::LlmProvider>,
56 system_prompt: Option<String>,
57 tool_names: HashSet<String>,
58 business_tools: Vec<Arc<dyn Tool>>,
60 #[cfg(feature = "multi_agent")]
62 multi_agent_config: Option<MultiAgentConfig>,
63 #[cfg(feature = "multi_agent")]
65 multi_agent_tool_factory: Option<MultiAgentToolFactory>,
66 error_recovery: Option<Arc<dyn agent_base::ToolErrorRecovery>>,
68 language: Option<agent_base::Language>,
70 #[cfg(feature = "skill")]
71 skills: Vec<Arc<dyn Skill>>,
72 #[cfg(feature = "skill")]
73 skill_prompter: Option<Arc<dyn SkillPrompter>>,
74 #[cfg(feature = "skill")]
75 skill_detail_tool_name: String,
76 #[cfg(feature = "skill")]
80 skill_detail_tool_factory: Option<SkillDetailToolFactory>,
81 #[cfg(feature = "skill")]
82 list_skills_tool_factory: Option<ListSkillsToolFactory>,
83 #[cfg(feature = "skill")]
84 disable_skill_prompt_injection: bool,
85 context_compactor: Option<Arc<dyn agent_base::ContextCompaction>>,
87 agent_instructions_paths: Vec<PathBuf>,
92}
93
94impl AgentBuilder {
95 pub fn new(client: Arc<dyn agent_base::llm_trait::LlmProvider>) -> Self {
96 Self {
97 inner: agent_base::AgentBuilder::new(client.clone()),
98 client,
99 system_prompt: None,
100 tool_names: HashSet::new(),
101 business_tools: Vec::new(),
102 #[cfg(feature = "multi_agent")]
103 multi_agent_config: None,
104 #[cfg(feature = "multi_agent")]
105 multi_agent_tool_factory: None,
106 error_recovery: None,
107 language: None,
108 #[cfg(feature = "skill")]
109 skills: Vec::new(),
110 #[cfg(feature = "skill")]
111 skill_prompter: None,
112 #[cfg(feature = "skill")]
113 skill_detail_tool_name: "get_skill_detail".to_string(),
114 #[cfg(feature = "skill")]
115 skill_detail_tool_factory: None,
116 #[cfg(feature = "skill")]
117 list_skills_tool_factory: None,
118 #[cfg(feature = "skill")]
119 disable_skill_prompt_injection: false,
120 context_compactor: None,
121 agent_instructions_paths: Vec::new(),
122 }
123 }
124
125 #[cfg(feature = "multi_agent")]
130 pub fn with_multi_agent(mut self, config: MultiAgentConfig) -> Self {
131 self.multi_agent_config = Some(config);
132 self
133 }
134
135 #[cfg(feature = "multi_agent")]
140 pub fn without_multi_agent(mut self) -> Self {
141 self.multi_agent_config = None;
142 self.multi_agent_tool_factory = None;
143 self
144 }
145
146 #[cfg(feature = "multi_agent")]
152 pub fn with_multi_agent_tool_factory(mut self, factory: MultiAgentToolFactory) -> Self {
153 self.multi_agent_tool_factory = Some(factory);
154 self
155 }
156
157 #[cfg(feature = "skill")]
162 pub fn with_skill_detail_tool_factory(mut self, factory: SkillDetailToolFactory) -> Self {
163 self.skill_detail_tool_factory = Some(factory);
164 self
165 }
166
167 #[cfg(feature = "skill")]
171 pub fn with_list_skills_tool_factory(mut self, factory: ListSkillsToolFactory) -> Self {
172 self.list_skills_tool_factory = Some(factory);
173 self
174 }
175
176 pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
177 let prompt = prompt.into();
178 self.inner = self.inner.system_prompt(prompt.clone());
179 self.system_prompt = Some(prompt);
180 self
181 }
182
183 pub fn enable_thought(self, enable: bool) -> Self {
184 Self {
185 inner: self.inner.enable_thought(enable),
186 ..self
187 }
188 }
189
190 pub fn reasoning(self, config: agent_base::ReasoningConfig) -> Self {
191 Self {
192 inner: self.inner.reasoning(config),
193 ..self
194 }
195 }
196
197 pub fn enable_thinking(self, enable: bool) -> Self {
198 Self {
199 inner: self.inner.enable_thinking(enable),
200 ..self
201 }
202 }
203
204 pub fn thinking_budget(self, budget: u64) -> Self {
205 Self {
206 inner: self.inner.thinking_budget(budget),
207 ..self
208 }
209 }
210
211 pub fn tool_timeout(self, timeout_ms: u64) -> Self {
212 Self {
213 inner: self.inner.tool_timeout(timeout_ms),
214 ..self
215 }
216 }
217
218 pub fn max_tool_output_chars(self, max_chars: usize) -> Self {
219 Self {
220 inner: self.inner.max_tool_output_chars(max_chars),
221 ..self
222 }
223 }
224
225 pub fn max_sessions(self, max: usize) -> Self {
226 Self {
227 inner: self.inner.max_sessions(max),
228 ..self
229 }
230 }
231
232 pub fn max_turns_per_session(self, max: usize) -> Self {
233 Self {
234 inner: self.inner.max_turns_per_session(max),
235 ..self
236 }
237 }
238
239 pub fn execution_max_turns(self, max: u32) -> Self {
240 Self {
241 inner: self.inner.execution_max_turns(max),
242 ..self
243 }
244 }
245
246 pub fn max_message_tokens(self, max: usize) -> Self {
247 Self {
248 inner: self.inner.max_message_tokens(max),
249 ..self
250 }
251 }
252
253 pub fn register_tool(mut self, tool: impl Tool + 'static) -> Self {
254 let tool_arc: Arc<dyn Tool> = Arc::new(tool);
255 self.tool_names.insert(tool_arc.name().to_string());
256 self.business_tools.push(tool_arc.clone());
257 self.inner = self.inner.register_tool_arc(tool_arc);
258 self
259 }
260
261 pub fn register_tool_arc(mut self, tool: Arc<dyn Tool>) -> Self {
262 self.tool_names.insert(tool.name().to_string());
263 self.business_tools.push(tool.clone());
264 self.inner = self.inner.register_tool_arc(tool);
265 self
266 }
267
268 pub fn approval_handler(self, handler: Arc<dyn agent_base::ApprovalHandler>) -> Self {
269 Self {
270 inner: self.inner.approval_handler(handler),
271 ..self
272 }
273 }
274
275 pub fn tool_policy(self, policy: Arc<dyn agent_base::ToolPolicy>) -> Self {
276 Self {
277 inner: self.inner.tool_policy(policy),
278 ..self
279 }
280 }
281
282 pub fn middleware(self, mw: impl agent_base::Middleware + 'static) -> Self {
283 Self {
284 inner: self.inner.middleware(mw),
285 ..self
286 }
287 }
288
289 pub fn guard(self, guard: impl agent_base::ReactLoopGuard + 'static) -> Self {
290 Self {
291 inner: self.inner.guard(guard),
292 ..self
293 }
294 }
295
296 pub fn context_window(self, max_tokens: usize) -> Self {
297 Self {
298 inner: self.inner.context_window(max_tokens),
299 ..self
300 }
301 }
302
303 pub fn context_window_manager(self, manager: agent_base::ContextWindowManager) -> Self {
304 Self {
305 inner: self.inner.context_window_manager(manager),
306 ..self
307 }
308 }
309
310 pub fn response_format(self, format: agent_base::ResponseFormat) -> Self {
311 Self {
312 inner: self.inner.response_format(format),
313 ..self
314 }
315 }
316
317 pub fn llm_retry(self, retry: agent_base::RetryConfig) -> Self {
318 Self {
319 inner: self.inner.llm_retry(retry),
320 ..self
321 }
322 }
323
324 pub fn session_store(self, store: Arc<dyn agent_base::SessionStore>) -> Self {
325 Self {
326 inner: self.inner.session_store(store),
327 ..self
328 }
329 }
330
331 pub fn error_recovery(mut self, recovery: Arc<dyn agent_base::ToolErrorRecovery>) -> Self {
332 self.error_recovery = Some(recovery.clone());
333 self.inner = self.inner.error_recovery(recovery);
334 self
335 }
336
337 pub fn tool_error_retry_prompt(self, prompt: impl Into<String>) -> Self {
338 Self {
339 inner: self.inner.tool_error_retry_prompt(prompt),
340 ..self
341 }
342 }
343
344 pub fn language(mut self, language: agent_base::Language) -> Self {
345 self.language = Some(language.clone());
346 self.inner = self.inner.language(language);
347 self
348 }
349
350 pub fn context_compactor(mut self, compactor: Arc<dyn agent_base::ContextCompaction>) -> Self {
355 self.context_compactor = Some(compactor);
356 self
357 }
358
359 pub fn event_bus_capacity(self, capacity: usize) -> Self {
360 Self {
361 inner: self.inner.event_bus_capacity(capacity),
362 ..self
363 }
364 }
365
366 pub fn session_id_generator(
367 self,
368 generator: Arc<dyn agent_base::types::SessionIdGenerator>,
369 ) -> Self {
370 Self {
371 inner: self.inner.session_id_generator(generator),
372 ..self
373 }
374 }
375
376 pub fn apply_if<T>(self, value: Option<T>, f: impl FnOnce(Self, T) -> Self) -> Self {
384 match value {
385 Some(v) => f(self, v),
386 None => self,
387 }
388 }
389
390 #[cfg(feature = "skill")]
391 pub fn register_skill(mut self, skill: impl Skill + 'static) -> Self {
392 self.skills.push(Arc::new(skill));
393 self
394 }
395
396 #[cfg(feature = "skill")]
397 pub fn register_skills(mut self, skills: Vec<Arc<dyn Skill>>) -> Self {
398 self.skills.extend(skills);
399 self
400 }
401
402 #[cfg(feature = "skill")]
403 pub fn skill_prompter(mut self, prompter: Arc<dyn SkillPrompter>) -> Self {
404 self.skill_prompter = Some(prompter);
405 self
406 }
407
408 #[cfg(feature = "skill")]
409 pub fn disable_skill_prompt_injection(mut self) -> Self {
410 self.disable_skill_prompt_injection = true;
411 self
412 }
413
414 pub fn agent_instructions_paths(mut self, paths: Vec<PathBuf>) -> Self {
431 self.agent_instructions_paths = paths;
432 self
433 }
434
435 #[cfg(feature = "skill")]
436 pub fn skill_detail_tool_name(mut self, name: impl Into<String>) -> Self {
437 self.skill_detail_tool_name = name.into();
438 self
439 }
440
441 pub fn build(self) -> AgentResult<AgentRuntime> {
444 #[cfg(feature = "skill")]
445 {
446 self.build_with_skills()
447 }
448 #[cfg(not(feature = "skill"))]
449 {
450 self.build_inner()
451 }
452 }
453
454 #[cfg(feature = "multi_agent")]
460 pub fn build_with_ma(self) -> AgentResult<(AgentRuntime, Option<Arc<MultiAgentRuntime>>)> {
461 #[cfg(feature = "skill")]
462 {
463 self.build_with_skills_ma()
464 }
465 #[cfg(not(feature = "skill"))]
466 {
467 self.build_inner_ma()
468 }
469 }
470
471 #[allow(dead_code, unused_mut)]
472 fn build_inner(mut self) -> AgentResult<AgentRuntime> {
473 if self.inner.get_guard().is_none() {
476 self.inner = self
477 .inner
478 .guard(crate::guard::DefaultGuard::with_llm_client(
479 crate::guard::DefaultGuardConfig::default(),
480 self.client.clone(),
481 ));
482 }
483
484 if !self.agent_instructions_paths.is_empty()
486 && let Some(instructions) =
487 build_agent_instructions_prompt(&self.agent_instructions_paths)
488 {
489 tracing::info!(
490 paths = ?self.agent_instructions_paths,
491 instructions_len = instructions.len(),
492 "injecting agent instructions into system prompt"
493 );
494 let new_prompt = match self.system_prompt.take() {
495 Some(existing) => format!("{}\n\n---\n\n{}", existing, instructions),
496 None => instructions,
497 };
498 self.system_prompt = Some(new_prompt.clone());
499 self.inner = self.inner.system_prompt(new_prompt);
500 }
501
502 #[cfg(feature = "multi_agent")]
503 let lang = self.language.clone().unwrap_or_default();
504 #[cfg(feature = "multi_agent")]
505 let business_tools = std::mem::take(&mut self.business_tools);
506 #[cfg(feature = "multi_agent")]
507 let error_recovery = self.error_recovery.clone();
508 #[cfg(feature = "multi_agent")]
509 let tool_names = self.tool_names.clone();
510
511 #[cfg(feature = "multi_agent")]
512 let ma_config = self.multi_agent_config.clone();
513 #[cfg(feature = "multi_agent")]
514 let ma_tool_factory = self.multi_agent_tool_factory.take();
515
516 #[cfg(feature = "multi_agent")]
518 if ma_config.as_ref().map(|c| c.enabled).unwrap_or(false) {
519 let ma_prompt = build_multi_agent_system_prompt();
520 let new_prompt = match self.system_prompt.take() {
521 Some(existing) => format!("{}\n\n---\n\n{}", existing, ma_prompt),
522 None => ma_prompt,
523 };
524 self.inner = self.inner.system_prompt(new_prompt);
525 }
526
527 if let Some(compactor) = self.context_compactor.take() {
529 self.inner = self.inner.context_compactor(compactor);
530 }
531
532 let runtime = self.inner.build()?;
533
534 #[cfg(feature = "multi_agent")]
536 if let Some(config) = ma_config
537 && config.enabled
538 {
539 setup_multi_agent(
540 &runtime,
541 config,
542 lang,
543 business_tools,
544 error_recovery,
545 &tool_names,
546 ma_tool_factory,
547 )?;
548 }
549
550 Ok(runtime)
551 }
552
553 #[cfg(feature = "multi_agent")]
555 #[allow(dead_code, unused_mut)]
556 fn build_inner_ma(mut self) -> AgentResult<(AgentRuntime, Option<Arc<MultiAgentRuntime>>)> {
557 if self.inner.get_guard().is_none() {
558 self.inner = self
559 .inner
560 .guard(crate::guard::DefaultGuard::with_llm_client(
561 crate::guard::DefaultGuardConfig::default(),
562 self.client.clone(),
563 ));
564 }
565
566 let lang = self.language.clone().unwrap_or_default();
567 let business_tools = std::mem::take(&mut self.business_tools);
568 let error_recovery = self.error_recovery.clone();
569 let tool_names = self.tool_names.clone();
570 let ma_config = self.multi_agent_config.clone();
571 let ma_tool_factory = self.multi_agent_tool_factory.take();
572
573 if ma_config.as_ref().map(|c| c.enabled).unwrap_or(false) {
574 let ma_prompt = build_multi_agent_system_prompt();
575 let new_prompt = match self.system_prompt.take() {
576 Some(existing) => format!("{}\n\n---\n\n{}", existing, ma_prompt),
577 None => ma_prompt,
578 };
579 self.inner = self.inner.system_prompt(new_prompt);
580 }
581
582 if let Some(compactor) = self.context_compactor.take() {
583 self.inner = self.inner.context_compactor(compactor);
584 }
585
586 let runtime = self.inner.build()?;
587
588 let ma_runtime_out = if let Some(config) = ma_config
589 && config.enabled
590 {
591 Some(setup_multi_agent(
592 &runtime,
593 config,
594 lang,
595 business_tools,
596 error_recovery,
597 &tool_names,
598 ma_tool_factory,
599 )?)
600 } else {
601 None
602 };
603
604 Ok((runtime, ma_runtime_out))
605 }
606
607 #[cfg(feature = "skill")]
619 fn build_with_skills(mut self) -> AgentResult<AgentRuntime> {
620 if self.inner.get_guard().is_none() {
623 self.inner = self
624 .inner
625 .guard(crate::guard::DefaultGuard::with_llm_client(
626 crate::guard::DefaultGuardConfig::default(),
627 self.client.clone(),
628 ));
629 }
630
631 if !self.agent_instructions_paths.is_empty()
633 && let Some(instructions) =
634 build_agent_instructions_prompt(&self.agent_instructions_paths)
635 {
636 tracing::info!(
637 paths = ?self.agent_instructions_paths,
638 instructions_len = instructions.len(),
639 "injecting agent instructions into system prompt"
640 );
641 let new_prompt = match self.system_prompt.take() {
642 Some(existing) => format!("{}\n\n---\n\n{}", existing, instructions),
643 None => instructions,
644 };
645 self.system_prompt = Some(new_prompt.clone());
646 self.inner = self.inner.system_prompt(new_prompt);
647 }
648
649 let mut ab = self.inner;
650 #[cfg(feature = "multi_agent")]
651 let lang = self.language.clone().unwrap_or_default();
652 #[cfg(feature = "multi_agent")]
653 let business_tools = std::mem::take(&mut self.business_tools);
654 #[cfg(feature = "multi_agent")]
655 let error_recovery = self.error_recovery.clone();
656 #[cfg(feature = "multi_agent")]
657 let tool_names = self.tool_names.clone();
658
659 #[cfg(feature = "multi_agent")]
660 let ma_config = self.multi_agent_config.clone();
661 #[cfg(feature = "multi_agent")]
662 let ma_tool_factory = self.multi_agent_tool_factory.take();
663
664 if !self.skills.is_empty() {
666 let prompter: Arc<dyn SkillPrompter> = self
667 .skill_prompter
668 .take()
669 .unwrap_or_else(|| Arc::new(LazySkillPrompter::new()));
670
671 let mut skill_refs: Vec<Arc<dyn Skill>> = Vec::new();
672
673 for skill in self.skills {
674 for tool in skill.tools() {
675 let tool_name = tool.name().to_string();
676 if self.tool_names.contains(&tool_name) {
677 return Err(agent_base::AgentError::internal(format!(
678 "Tool name conflict: `{}` (Skill `{}`)",
679 tool_name,
680 skill.name()
681 )));
682 }
683 self.tool_names.insert(tool_name);
684 ab = ab.register_tool_arc(tool);
685 }
686 skill_refs.push(skill);
687 }
688
689 if !self.disable_skill_prompt_injection {
690 let skill_prompt = prompter.build_prompt(&skill_refs, &self.skill_detail_tool_name);
691 if !skill_prompt.is_empty() {
692 let new_prompt = match self.system_prompt.take() {
693 Some(existing) => format!("{}\n\n---\n\n{}", existing, skill_prompt),
694 None => skill_prompt,
695 };
696 self.system_prompt = Some(new_prompt.clone());
697 ab = ab.system_prompt(new_prompt);
698 }
699 }
700
701 if let Some(factory) = self.skill_detail_tool_factory.take() {
704 let detail_tool = factory(skill_refs.clone(), self.skill_detail_tool_name);
705 ab = ab.register_tool_arc(detail_tool);
706 }
707
708 if let Some(factory) = self.list_skills_tool_factory.take() {
710 let registry = Arc::new(crate::skill::SkillRegistry::new());
711 for skill in &skill_refs {
712 tokio::task::block_in_place(|| {
713 tokio::runtime::Handle::current().block_on(async {
714 registry.register(skill.clone()).await;
715 })
716 });
717 }
718 let list_tool = factory(registry);
719 ab = ab.register_tool_arc(list_tool);
720 }
721 }
722
723 #[cfg(feature = "multi_agent")]
725 if ma_config.as_ref().map(|c| c.enabled).unwrap_or(false) {
726 let ma_prompt = build_multi_agent_system_prompt();
727 let new_prompt = match self.system_prompt.take() {
728 Some(existing) => format!("{}\n\n---\n\n{}", existing, ma_prompt),
729 None => ma_prompt,
730 };
731 ab = ab.system_prompt(new_prompt);
732 }
733
734 if let Some(compactor) = self.context_compactor.take() {
736 ab = ab.context_compactor(compactor);
737 }
738
739 let runtime = ab.build()?;
740
741 #[cfg(feature = "multi_agent")]
743 if let Some(config) = ma_config
744 && config.enabled
745 {
746 setup_multi_agent(
747 &runtime,
748 config,
749 lang,
750 business_tools,
751 error_recovery,
752 &tool_names,
753 ma_tool_factory,
754 )?;
755 }
756
757 Ok(runtime)
758 }
759
760 #[cfg(all(feature = "skill", feature = "multi_agent"))]
762 fn build_with_skills_ma(
763 mut self,
764 ) -> AgentResult<(AgentRuntime, Option<Arc<MultiAgentRuntime>>)> {
765 if self.inner.get_guard().is_none() {
766 self.inner = self
767 .inner
768 .guard(crate::guard::DefaultGuard::with_llm_client(
769 crate::guard::DefaultGuardConfig::default(),
770 self.client.clone(),
771 ));
772 }
773
774 if !self.agent_instructions_paths.is_empty()
776 && let Some(instructions) =
777 build_agent_instructions_prompt(&self.agent_instructions_paths)
778 {
779 tracing::info!(
780 paths = ?self.agent_instructions_paths,
781 instructions_len = instructions.len(),
782 "injecting agent instructions into system prompt"
783 );
784 let new_prompt = match self.system_prompt.take() {
785 Some(existing) => format!("{}\n\n---\n\n{}", existing, instructions),
786 None => instructions,
787 };
788 self.system_prompt = Some(new_prompt.clone());
789 self.inner = self.inner.system_prompt(new_prompt);
790 }
791
792 let mut ab = self.inner;
793 let lang = self.language.clone().unwrap_or_default();
794 let business_tools = std::mem::take(&mut self.business_tools);
795 let error_recovery = self.error_recovery.clone();
796 let tool_names = self.tool_names.clone();
797 let ma_config = self.multi_agent_config.clone();
798 let ma_tool_factory = self.multi_agent_tool_factory.take();
799
800 if !self.skills.is_empty() {
802 let prompter: Arc<dyn SkillPrompter> = self
803 .skill_prompter
804 .take()
805 .unwrap_or_else(|| Arc::new(LazySkillPrompter::new()));
806
807 let mut skill_refs: Vec<Arc<dyn Skill>> = Vec::new();
808
809 for skill in self.skills {
810 for tool in skill.tools() {
811 let tool_name = tool.name().to_string();
812 if self.tool_names.contains(&tool_name) {
813 return Err(agent_base::AgentError::internal(format!(
814 "Tool name conflict: `{}` (Skill `{}`)",
815 tool_name,
816 skill.name()
817 )));
818 }
819 self.tool_names.insert(tool_name);
820 ab = ab.register_tool_arc(tool);
821 }
822 skill_refs.push(skill);
823 }
824
825 if !self.disable_skill_prompt_injection {
826 let skill_prompt = prompter.build_prompt(&skill_refs, &self.skill_detail_tool_name);
827 if !skill_prompt.is_empty() {
828 let new_prompt = match self.system_prompt.take() {
829 Some(existing) => format!("{}\n\n---\n\n{}", existing, skill_prompt),
830 None => skill_prompt,
831 };
832 self.system_prompt = Some(new_prompt.clone());
833 ab = ab.system_prompt(new_prompt);
834 }
835 }
836
837 if let Some(factory) = self.skill_detail_tool_factory.take() {
838 let detail_tool = factory(skill_refs.clone(), self.skill_detail_tool_name);
839 ab = ab.register_tool_arc(detail_tool);
840 }
841
842 if let Some(factory) = self.list_skills_tool_factory.take() {
843 let registry = Arc::new(crate::skill::SkillRegistry::new());
844 for skill in &skill_refs {
845 tokio::task::block_in_place(|| {
846 tokio::runtime::Handle::current().block_on(async {
847 registry.register(skill.clone()).await;
848 })
849 });
850 }
851 let list_tool = factory(registry);
852 ab = ab.register_tool_arc(list_tool);
853 }
854 }
855
856 if ma_config.as_ref().map(|c| c.enabled).unwrap_or(false) {
858 let ma_prompt = build_multi_agent_system_prompt();
859 let new_prompt = match self.system_prompt.take() {
860 Some(existing) => format!("{}\n\n---\n\n{}", existing, ma_prompt),
861 None => ma_prompt,
862 };
863 ab = ab.system_prompt(new_prompt);
864 }
865
866 if let Some(compactor) = self.context_compactor.take() {
867 ab = ab.context_compactor(compactor);
868 }
869
870 let runtime = ab.build()?;
871
872 let ma_runtime_out = if let Some(config) = ma_config
873 && config.enabled
874 {
875 Some(setup_multi_agent(
876 &runtime,
877 config,
878 lang,
879 business_tools,
880 error_recovery,
881 &tool_names,
882 ma_tool_factory,
883 )?)
884 } else {
885 None
886 };
887
888 Ok((runtime, ma_runtime_out))
889 }
890}
891
892#[cfg(feature = "multi_agent")]
903pub fn setup_multi_agent(
904 runtime: &AgentRuntime,
905 config: MultiAgentConfig,
906 lang: agent_base::Language,
907 business_tools: Vec<Arc<dyn Tool>>,
908 error_recovery: Option<Arc<dyn agent_base::ToolErrorRecovery>>,
909 existing_tool_names: &HashSet<String>,
910 tool_factory: Option<MultiAgentToolFactory>,
911) -> AgentResult<Arc<MultiAgentRuntime>> {
912 let client = runtime.provider();
913 let cancel_token = runtime.cancel_token();
914 let tool_policy = runtime.tool_policy().cloned();
915 let approval_handler = runtime.approval_handler().cloned();
916
917 let ma_runtime = Arc::new(MultiAgentRuntime::new(
918 config.clone(),
919 client,
920 business_tools,
921 cancel_token,
922 error_recovery,
923 lang,
924 tool_policy,
925 approval_handler,
926 ));
927
928 ma_runtime.set_session_manager(Arc::new(runtime.session_manager().clone()));
930
931 let (event_tx, mut event_rx) =
933 tokio::sync::mpsc::unbounded_channel::<agent_base::RuntimeEvent>();
934 ma_runtime.set_event_sender(event_tx);
935 let parent_runtime = runtime.clone();
936 tokio::spawn(async move {
937 while let Some(event) = event_rx.recv().await {
938 parent_runtime.emit_event(event);
939 }
940 });
941
942 if let Some(factory) = tool_factory {
944 let tools = factory(ma_runtime.clone());
945 let registry = runtime.tools_mut();
946 let mut reg = tokio::task::block_in_place(|| registry.blocking_write());
947 for tool in tools {
948 let tool_name = tool.name().to_string();
949 if !existing_tool_names.contains(&tool_name) {
950 reg.register_arc(tool);
951 }
952 }
953 drop(reg);
954 }
955
956 Ok(ma_runtime)
957}
958
959#[cfg(feature = "multi_agent")]
961pub fn build_multi_agent_system_prompt() -> String {
962 r#"## Multi-Agent Capabilities
963
964You 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.
965
966### Tools
967
968- `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"})`.
969- `send_message`: Send follow-up context or tasks to a sub-agent (`trigger: true` starts execution).
970- `list_agents`: List active sub-agents with their assigned task and status — use it to answer "what is agent X doing?".
971- `close_agent`: Close a sub-agent when done.
972
973### How Results Reach You
974
975Sub-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.
976
977### How Sub-Agents Work
978
979Sub-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.
980
981### When to Spawn
982
983- Parallel tasks: analyze multiple codebases, compare implementations, etc.
984- Tasks you want to delegate while you continue other work
985
986### When NOT to Spawn
987
988- Simple lookups (just use read_file or repo_map directly)
989- Tasks that need your current conversation context"#
990 .to_string()
991}
992
993pub fn build_memory_system_prompt() -> String {
1002 r#"## Memory
1003
1004You have a persistent file-based memory at `.phi/memory/`. Use `read_file` and `write_file` to manage it — there are no dedicated memory tools.
1005
1006### How Memory Works
1007
1008- `MEMORY.md` is the index — it lists all memories with one-line descriptions. Read it first when you need to recall something.
1009- Each memory is a separate `.md` file with YAML frontmatter:
1010 ```yaml
1011 ---
1012 name: <short-kebab-case-slug>
1013 description: <one-line summary — used to decide relevance during recall>
1014 metadata:
1015 node_type: memory
1016 type: user | feedback | project | reference
1017 ---
1018
1019 <the fact or instruction>
1020 ```
1021- 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.
1022- Link related memories with `[[memory-name]]` in the body.
1023- `user` type = who the user is (role, expertise, preferences).
1024- `feedback` type = guidance the user has given on how you should work.
1025- `project` type = ongoing work, goals, or constraints.
1026- `reference` type = pointers to external resources (URLs, dashboards, tickets).
1027
1028### When to Use Memory
1029
1030- The user explicitly asks you to remember something ("remember this", "save that")
1031- You learn something important about the user's preferences or workflow
1032- After completing a significant task, save context that would help in future sessions
1033- The user gives you feedback on how to work — save it as `feedback` type
1034
1035### When NOT to Use Memory
1036
1037- For transient information that won't be useful beyond this session
1038- For facts already recorded in the codebase (code structure, git history, config files)
1039- For items that only matter to the current conversation
1040
1041### Pro Tips
1042
1043- When creating your first memory of a new type, you can read template files for format reference (check `.phi/templates/memory/` if available).
1044- Keep the MEMORY.md index concise — it's loaded into context every session.
1045- Before writing a new memory, check if an existing file already covers it — update instead of duplicating.
1046
1047### Workflow
1048
1049**To recall:** read `MEMORY.md` → find relevant entries by description → read the specific `.md` files you need.
1050**To remember:** create a new `.md` file with proper frontmatter → update `MEMORY.md` with a new entry.
1051**To update:** edit the existing `.md` file (don't create a duplicate).
1052**To forget:** delete the `.md` file → remove its entry from `MEMORY.md`."#
1053 .to_string()
1054}
1055
1056#[cfg(test)]
1057mod tests {
1058 use super::*;
1059 use agent_base::Content;
1060 use agent_base::llm_trait::response::FinishReason;
1061 use agent_base::llm_trait::types::UsageInfo;
1062 use agent_base::llm_trait::{
1063 Capabilities, ChatRequest, ChatResponse, ChatStream, LlmError, LlmProvider, ProviderInfo,
1064 };
1065
1066 struct StubProvider;
1069
1070 #[async_trait::async_trait]
1071 impl LlmProvider for StubProvider {
1072 async fn stream(&self, _request: ChatRequest) -> Result<ChatStream, LlmError> {
1073 let chunks = vec![
1074 Ok(agent_base::StreamChunk::Text("ok".to_string())),
1075 Ok(agent_base::StreamChunk::Stop {
1076 finish_reason: Some("stop".to_string()),
1077 }),
1078 ];
1079 Ok(ChatStream::new(Box::pin(futures_util::stream::iter(
1080 chunks,
1081 ))))
1082 }
1083
1084 async fn chat(&self, _request: ChatRequest) -> Result<ChatResponse, LlmError> {
1085 Ok(ChatResponse {
1086 content: "ok".to_string(),
1087 tool_calls: vec![],
1088 usage: UsageInfo::default(),
1089 finish_reason: FinishReason::Stop,
1090 raw: None,
1091 reasoning_content: None,
1092 thinking_signature: None,
1093 })
1094 }
1095
1096 fn capabilities(&self) -> Capabilities {
1097 Capabilities {
1098 supports_streaming: true,
1099 supports_tools: true,
1100 ..Default::default()
1101 }
1102 }
1103
1104 fn info(&self) -> ProviderInfo {
1105 ProviderInfo {
1106 name: "stub".to_string(),
1107 model: "stub-model".to_string(),
1108 version: None,
1109 }
1110 }
1111 }
1112
1113 fn make_client() -> Arc<dyn LlmProvider> {
1114 Arc::new(StubProvider)
1115 }
1116
1117 #[cfg(feature = "multi_agent")]
1120 #[tokio::test(flavor = "multi_thread")]
1121 async fn test_setup_multi_agent_without_factory_registers_no_tools() {
1122 let client = make_client();
1123 let runtime = agent_base::AgentBuilder::new(client.clone())
1124 .build()
1125 .unwrap();
1126 let config = MultiAgentConfig::enabled();
1127
1128 let result = setup_multi_agent(
1129 &runtime,
1130 config,
1131 agent_base::Language::En,
1132 vec![],
1133 None,
1134 &HashSet::new(),
1135 None, );
1137 assert!(result.is_ok());
1138 let ma_runtime = result.unwrap();
1139 let agents = ma_runtime.list_agents();
1141 assert!(agents.is_empty());
1142 }
1143
1144 #[cfg(feature = "multi_agent")]
1145 #[tokio::test(flavor = "multi_thread")]
1146 async fn test_setup_multi_agent_with_factory_registers_tools() {
1147 let client = make_client();
1148 let runtime = agent_base::AgentBuilder::new(client.clone())
1149 .build()
1150 .unwrap();
1151 let config = MultiAgentConfig::enabled();
1152
1153 let factory: MultiAgentToolFactory = Arc::new(|_rt| {
1154 struct FakeTool;
1156 #[async_trait::async_trait]
1157 impl Tool for FakeTool {
1158 fn name(&self) -> &'static str {
1159 "fake_tool"
1160 }
1161 fn description(&self) -> &'static str {
1162 ""
1163 }
1164 fn schema(&self) -> serde_json::Value {
1165 serde_json::json!({})
1166 }
1167 async fn call(
1168 &self,
1169 _args: &serde_json::Value,
1170 _ctx: &agent_base::ToolContext,
1171 ) -> AgentResult<Vec<Content>> {
1172 Ok(vec![Content::text("ok")])
1173 }
1174 }
1175 vec![Arc::new(FakeTool)]
1176 });
1177
1178 let result = setup_multi_agent(
1179 &runtime,
1180 config,
1181 agent_base::Language::En,
1182 vec![],
1183 None,
1184 &HashSet::new(),
1185 Some(factory),
1186 );
1187 assert!(result.is_ok());
1188
1189 let tools: Vec<String> = tokio::task::block_in_place(|| {
1191 let tools = runtime.tools_mut();
1192 let guard = tools.blocking_read();
1193 guard.metadatas().into_iter().map(|m| m.name).collect()
1194 });
1195 assert!(tools.contains(&"fake_tool".to_string()));
1196 }
1197
1198 #[cfg(feature = "multi_agent")]
1199 #[tokio::test(flavor = "multi_thread")]
1200 async fn test_setup_multi_agent_skips_duplicate_tool_names() {
1201 let client = make_client();
1202 let runtime = agent_base::AgentBuilder::new(client.clone())
1203 .build()
1204 .unwrap();
1205
1206 struct DupTool;
1208 #[async_trait::async_trait]
1209 impl Tool for DupTool {
1210 fn name(&self) -> &'static str {
1211 "dup_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 {
1228 let tools = runtime.tools_mut();
1229 let mut reg = tokio::task::block_in_place(|| tools.blocking_write());
1230 reg.register(DupTool);
1231 }
1232
1233 let factory: MultiAgentToolFactory = Arc::new(|_rt| {
1234 struct FakeTool;
1235 #[async_trait::async_trait]
1236 impl Tool for FakeTool {
1237 fn name(&self) -> &'static str {
1238 "dup_tool"
1239 }
1240 fn description(&self) -> &'static str {
1241 ""
1242 }
1243 fn schema(&self) -> serde_json::Value {
1244 serde_json::json!({})
1245 }
1246 async fn call(
1247 &self,
1248 _args: &serde_json::Value,
1249 _ctx: &agent_base::ToolContext,
1250 ) -> AgentResult<Vec<Content>> {
1251 Ok(vec![Content::text("ok")])
1252 }
1253 }
1254 vec![Arc::new(FakeTool)]
1255 });
1256
1257 let mut existing = HashSet::new();
1258 existing.insert("dup_tool".to_string());
1259
1260 let result = setup_multi_agent(
1261 &runtime,
1262 MultiAgentConfig::enabled(),
1263 agent_base::Language::En,
1264 vec![],
1265 None,
1266 &existing,
1267 Some(factory),
1268 );
1269 assert!(result.is_ok());
1270 let tools = tokio::task::block_in_place(|| {
1272 let tools = runtime.tools_mut();
1273 let guard = tools.blocking_read();
1274 guard
1275 .metadatas()
1276 .into_iter()
1277 .map(|m| m.name)
1278 .collect::<Vec<String>>()
1279 });
1280 let count = tools.iter().filter(|n| n.as_str() == "dup_tool").count();
1281 assert_eq!(count, 1);
1282 }
1283
1284 #[cfg(feature = "multi_agent")]
1287 #[tokio::test(flavor = "multi_thread")]
1288 async fn test_builder_with_multi_agent_without_factory_builds_ok() {
1289 let client = make_client();
1290 let runtime = AgentBuilder::new(client)
1291 .with_multi_agent(MultiAgentConfig::enabled())
1292 .build()
1293 .unwrap();
1294 let tools = tokio::task::block_in_place(|| {
1296 let tools = runtime.tools_mut();
1297 let guard = tools.blocking_read();
1298 guard
1299 .metadatas()
1300 .into_iter()
1301 .map(|m| m.name)
1302 .collect::<Vec<String>>()
1303 });
1304 assert!(!tools.contains(&"spawn_agent".to_string()));
1306 }
1307
1308 #[cfg(feature = "multi_agent")]
1309 #[tokio::test(flavor = "multi_thread")]
1310 async fn test_builder_with_factory_registers_tools() {
1311 let client = make_client();
1312 let factory: MultiAgentToolFactory = Arc::new(|_rt| {
1314 struct TestTool;
1315 #[async_trait::async_trait]
1316 impl Tool for TestTool {
1317 fn name(&self) -> &'static str {
1318 "factory_test_tool"
1319 }
1320 fn description(&self) -> &'static str {
1321 ""
1322 }
1323 fn schema(&self) -> serde_json::Value {
1324 serde_json::json!({})
1325 }
1326 async fn call(
1327 &self,
1328 _args: &serde_json::Value,
1329 _ctx: &agent_base::ToolContext,
1330 ) -> AgentResult<Vec<Content>> {
1331 Ok(vec![Content::text("ok")])
1332 }
1333 }
1334 vec![Arc::new(TestTool)]
1335 });
1336
1337 let runtime = AgentBuilder::new(client)
1338 .with_multi_agent(MultiAgentConfig::enabled())
1339 .with_multi_agent_tool_factory(factory)
1340 .build()
1341 .unwrap();
1342
1343 let tools = tokio::task::block_in_place(|| {
1344 let tools = runtime.tools_mut();
1345 let guard = tools.blocking_read();
1346 guard
1347 .metadatas()
1348 .into_iter()
1349 .map(|m| m.name)
1350 .collect::<Vec<String>>()
1351 });
1352 assert!(tools.contains(&"factory_test_tool".to_string()));
1353 }
1354
1355 #[cfg(feature = "multi_agent")]
1356 #[test]
1357 fn test_builder_disabled_multi_agent_skips_factory() {
1358 let client = make_client();
1359 let factory: MultiAgentToolFactory = Arc::new(|_rt| {
1360 panic!("factory should not be called when multi-agent is not configured");
1361 });
1362
1363 let runtime = AgentBuilder::new(client)
1364 .with_multi_agent_tool_factory(factory)
1365 .build()
1367 .unwrap();
1368
1369 let tools = tokio::task::block_in_place(|| {
1370 let tools = runtime.tools_mut();
1371 let guard = tools.blocking_read();
1372 guard
1373 .metadatas()
1374 .into_iter()
1375 .map(|m| m.name)
1376 .collect::<Vec<String>>()
1377 });
1378 assert!(!tools.contains(&"spawn_agent".to_string()));
1379 }
1380
1381 #[cfg(feature = "multi_agent")]
1384 #[test]
1385 fn test_system_prompt_contains_tool_names() {
1386 let prompt = build_multi_agent_system_prompt();
1387 assert!(prompt.contains("spawn_agent"));
1388 assert!(prompt.contains("send_message"));
1389 assert!(prompt.contains("trigger"));
1390 assert!(prompt.contains("list_agents"));
1391 assert!(prompt.contains("close_agent"));
1392 assert!(!prompt.contains("followup_task"));
1395 assert!(!prompt.contains("wait_agent"));
1398 assert!(prompt.contains("pushed into your context automatically"));
1399 }
1400
1401 #[cfg(feature = "multi_agent")]
1402 #[test]
1403 fn test_system_prompt_contains_guidance() {
1404 let prompt = build_multi_agent_system_prompt();
1405 assert!(prompt.contains("When to Spawn"));
1406 assert!(prompt.contains("When NOT to Spawn"));
1407 assert!(prompt.contains("How Results Reach You"));
1408 }
1409
1410 #[cfg(feature = "multi_agent")]
1413 #[tokio::test(flavor = "multi_thread")]
1414 async fn test_without_multi_agent_clears_config_and_factory() {
1415 let client = make_client();
1416
1417 let factory: MultiAgentToolFactory = Arc::new(|_rt| {
1419 panic!("factory should not be called when multi-agent is cleared");
1420 });
1421
1422 let runtime = AgentBuilder::new(client)
1423 .with_multi_agent(MultiAgentConfig::enabled())
1424 .with_multi_agent_tool_factory(factory)
1425 .without_multi_agent() .build()
1427 .unwrap();
1428
1429 let tools = tokio::task::block_in_place(|| {
1430 let tools = runtime.tools_mut();
1431 let guard = tools.blocking_read();
1432 guard
1433 .metadatas()
1434 .into_iter()
1435 .map(|m| m.name)
1436 .collect::<Vec<String>>()
1437 });
1438 assert!(!tools.contains(&"spawn_agent".to_string()));
1439 }
1440
1441 #[test]
1444 fn test_apply_if_some_applies_transformation() {
1445 let client = make_client();
1446 let builder = AgentBuilder::new(client)
1447 .apply_if(Some("custom prompt"), |b, prompt| b.system_prompt(prompt));
1448 assert!(builder.system_prompt.unwrap().contains("custom prompt"));
1450 }
1451
1452 #[test]
1453 fn test_apply_if_none_passes_through() {
1454 let client = make_client();
1455 let builder = AgentBuilder::new(client).apply_if(None as Option<&str>, |_b, _prompt| {
1456 panic!("should not be called when value is None");
1457 });
1458 assert!(builder.system_prompt.is_none());
1459 }
1460
1461 #[test]
1464 fn test_build_memory_system_prompt_non_empty() {
1465 let prompt = build_memory_system_prompt();
1466 assert!(!prompt.is_empty());
1467 assert!(prompt.contains("Memory"));
1468 assert!(prompt.contains("MEMORY.md"));
1469 assert!(prompt.contains("read_file"));
1470 assert!(prompt.contains("write_file"));
1471 }
1472
1473 struct NamedTool(&'static str);
1476
1477 #[async_trait::async_trait]
1478 impl Tool for NamedTool {
1479 fn name(&self) -> &'static str {
1480 self.0
1481 }
1482
1483 fn description(&self) -> &'static str {
1484 ""
1485 }
1486
1487 fn schema(&self) -> serde_json::Value {
1488 serde_json::json!({})
1489 }
1490
1491 async fn call(
1492 &self,
1493 _args: &serde_json::Value,
1494 _ctx: &agent_base::ToolContext,
1495 ) -> AgentResult<Vec<Content>> {
1496 Ok(vec![Content::text("ok")])
1497 }
1498 }
1499
1500 fn runtime_tool_names(runtime: &AgentRuntime) -> Vec<String> {
1501 tokio::task::block_in_place(|| {
1502 let tools = runtime.tools_mut();
1503 let guard = tools.blocking_read();
1504 guard.metadatas().into_iter().map(|m| m.name).collect()
1505 })
1506 }
1507
1508 #[tokio::test(flavor = "multi_thread")]
1509 async fn test_builder_scalar_passthrough_methods() {
1510 let client = make_client();
1511 let runtime = AgentBuilder::new(client)
1512 .enable_thought(true)
1513 .reasoning(agent_base::ReasoningConfig::default())
1514 .enable_thinking(false)
1515 .thinking_budget(1000)
1516 .tool_timeout(5000)
1517 .max_tool_output_chars(4000)
1518 .max_sessions(16)
1519 .max_turns_per_session(20)
1520 .execution_max_turns(10)
1521 .max_message_tokens(8000)
1522 .context_window(64_000)
1523 .context_window_manager(agent_base::ContextWindowManager::new(64_000))
1524 .response_format(agent_base::ResponseFormat::JsonObject)
1525 .llm_retry(agent_base::RetryConfig::default())
1526 .tool_error_retry_prompt("please retry")
1527 .language(agent_base::Language::En)
1528 .event_bus_capacity(256)
1529 .build()
1530 .unwrap();
1531
1532 assert!(runtime.provider().capabilities().supports_streaming);
1533 }
1534
1535 #[tokio::test(flavor = "multi_thread")]
1536 async fn test_register_tool_variants() {
1537 let client = make_client();
1538 let runtime = AgentBuilder::new(client)
1539 .register_tool(NamedTool("tool_by_value"))
1540 .register_tool_arc(Arc::new(NamedTool("tool_by_arc")))
1541 .build()
1542 .unwrap();
1543
1544 let names = runtime_tool_names(&runtime);
1545 assert!(names.contains(&"tool_by_value".to_string()));
1546 assert!(names.contains(&"tool_by_arc".to_string()));
1547 }
1548
1549 #[cfg(feature = "skill")]
1550 mod skill_tests {
1551 use super::*;
1552 use crate::skill::Skill;
1553
1554 struct TestSkill;
1555
1556 impl Skill for TestSkill {
1557 fn name(&self) -> &'static str {
1558 "test_skill"
1559 }
1560
1561 fn brief_description(&self) -> String {
1562 "a test skill".to_string()
1563 }
1564
1565 fn detailed_description(&self) -> String {
1566 "detailed test skill".to_string()
1567 }
1568
1569 fn tools(&self) -> Vec<Arc<dyn Tool>> {
1570 vec![]
1571 }
1572 }
1573
1574 struct ToolSkill;
1575
1576 impl Skill for ToolSkill {
1577 fn name(&self) -> &'static str {
1578 "tool_skill"
1579 }
1580
1581 fn brief_description(&self) -> String {
1582 "skill with a tool".to_string()
1583 }
1584
1585 fn detailed_description(&self) -> String {
1586 "skill that provides a tool".to_string()
1587 }
1588
1589 fn tools(&self) -> Vec<Arc<dyn Tool>> {
1590 vec![Arc::new(NamedTool("skill_provided_tool"))]
1591 }
1592 }
1593
1594 #[tokio::test(flavor = "multi_thread")]
1595 async fn test_register_skill_builds_ok() {
1596 let client = make_client();
1597 let runtime = AgentBuilder::new(client)
1598 .register_skill(TestSkill)
1599 .build()
1600 .unwrap();
1601 assert!(runtime_tool_names(&runtime).is_empty());
1603 }
1604
1605 #[tokio::test(flavor = "multi_thread")]
1606 async fn test_register_skill_with_tool_registers_tool() {
1607 let client = make_client();
1608 let runtime = AgentBuilder::new(client)
1609 .register_skill(ToolSkill)
1610 .build()
1611 .unwrap();
1612 assert!(runtime_tool_names(&runtime).contains(&"skill_provided_tool".to_string()));
1613 }
1614
1615 #[tokio::test(flavor = "multi_thread")]
1616 async fn test_register_skill_tool_name_conflict() {
1617 let client = make_client();
1618 let result = AgentBuilder::new(client)
1619 .register_tool(NamedTool("skill_provided_tool"))
1620 .register_skill(ToolSkill)
1621 .build();
1622 let err = result.err().unwrap();
1623 assert!(format!("{err}").contains("Tool name conflict"));
1624 }
1625
1626 #[tokio::test(flavor = "multi_thread")]
1627 async fn test_skill_detail_tool_factory_registers_tool() {
1628 let client = make_client();
1629 let factory: SkillDetailToolFactory = Arc::new(|_skills, name| {
1630 assert_eq!(name, "get_skill_detail");
1631 Arc::new(NamedTool("detail_tool"))
1632 });
1633 let runtime = AgentBuilder::new(client)
1634 .register_skill(TestSkill)
1635 .with_skill_detail_tool_factory(factory)
1636 .build()
1637 .unwrap();
1638 assert!(runtime_tool_names(&runtime).contains(&"detail_tool".to_string()));
1639 }
1640
1641 #[tokio::test(flavor = "multi_thread")]
1642 async fn test_list_skills_tool_factory_registers_tool() {
1643 let client = make_client();
1644 let factory: ListSkillsToolFactory =
1645 Arc::new(|_registry| Arc::new(NamedTool("list_skills_tool")));
1646 let runtime = AgentBuilder::new(client)
1647 .register_skill(TestSkill)
1648 .with_list_skills_tool_factory(factory)
1649 .build()
1650 .unwrap();
1651 assert!(runtime_tool_names(&runtime).contains(&"list_skills_tool".to_string()));
1652 }
1653
1654 #[tokio::test(flavor = "multi_thread")]
1655 async fn test_disable_skill_prompt_injection_builds() {
1656 let client = make_client();
1657 let runtime = AgentBuilder::new(client)
1658 .register_skill(ToolSkill)
1659 .disable_skill_prompt_injection()
1660 .build()
1661 .unwrap();
1662 assert!(runtime_tool_names(&runtime).contains(&"skill_provided_tool".to_string()));
1664 }
1665 }
1666
1667 #[test]
1670 fn test_build_agent_instructions_prompt_reads_files() {
1671 let tmp = tempfile::tempdir().unwrap();
1672 let file1 = tmp.path().join("CLAUDE.md");
1673 let file2 = tmp.path().join("INSTRUCTIONS.md");
1674 std::fs::write(&file1, "# User instructions\nBe helpful.").unwrap();
1675 std::fs::write(&file2, "# Project instructions\nFollow conventions.").unwrap();
1676
1677 let prompt = build_agent_instructions_prompt(&[file1, file2]);
1678 assert!(prompt.is_some());
1679 let prompt = prompt.unwrap();
1680 assert!(prompt.starts_with("# Project instructions"));
1682 assert!(prompt.contains("# User instructions"));
1683 assert!(prompt.contains("Be helpful."));
1684 assert!(prompt.contains("Follow conventions."));
1685 }
1686
1687 #[test]
1688 fn test_build_agent_instructions_prompt_skips_missing_files() {
1689 let prompt = build_agent_instructions_prompt(&[
1690 PathBuf::from("/nonexistent/CLAUDE.md"),
1691 PathBuf::from("/nonexistent/INSTRUCTIONS.md"),
1692 ]);
1693 assert!(prompt.is_none());
1694 }
1695
1696 #[test]
1697 fn test_build_agent_instructions_prompt_skips_empty_files() {
1698 let tmp = tempfile::tempdir().unwrap();
1699 let file = tmp.path().join("empty.md");
1700 std::fs::write(&file, " \n ").unwrap();
1701
1702 let prompt = build_agent_instructions_prompt(&[file]);
1703 assert!(prompt.is_none());
1704 }
1705}