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 = "memory")]
11use crate::memory::{MemoryConfig, MemoryStore};
12
13#[cfg(feature = "skill")]
14use crate::skill::{LazySkillPrompter, Skill, SkillPrompter};
15
16fn build_agent_instructions_prompt(paths: &[PathBuf]) -> Option<String> {
21 let mut sections = Vec::new();
22
23 for path in paths {
24 if let Ok(content) = std::fs::read_to_string(path) {
25 let trimmed = content.trim();
26 if !trimmed.is_empty() {
27 sections.push(trimmed.to_string());
28 }
29 }
30 }
31
32 if sections.is_empty() {
33 return None;
34 }
35
36 sections.reverse();
38 Some(sections.join("\n\n---\n\n"))
39}
40
41#[cfg(feature = "multi_agent")]
43pub type MultiAgentToolFactory =
44 Arc<dyn Fn(Arc<MultiAgentRuntime>) -> Vec<Arc<dyn Tool>> + Send + Sync>;
45
46#[cfg(feature = "skill")]
48pub type SkillDetailToolFactory =
49 Arc<dyn Fn(Vec<Arc<dyn Skill>>, String) -> Arc<dyn Tool> + Send + Sync>;
50
51#[cfg(feature = "skill")]
53pub type ListSkillsToolFactory =
54 Arc<dyn Fn(Arc<crate::skill::SkillRegistry>) -> Arc<dyn Tool> + Send + Sync>;
55
56pub struct AgentBuilder {
57 inner: agent_base::AgentBuilder,
58 client: Arc<dyn agent_base::llm_trait::LlmProvider>,
59 system_prompt: Option<String>,
60 tool_names: HashSet<String>,
61 business_tools: Vec<Arc<dyn Tool>>,
63 #[cfg(feature = "multi_agent")]
65 multi_agent_config: Option<MultiAgentConfig>,
66 #[cfg(feature = "multi_agent")]
68 multi_agent_tool_factory: Option<MultiAgentToolFactory>,
69 error_recovery: Option<Arc<dyn agent_base::ToolErrorRecovery>>,
71 language: Option<agent_base::Language>,
73 #[cfg(feature = "skill")]
74 skills: Vec<Arc<dyn Skill>>,
75 #[cfg(feature = "skill")]
76 skill_prompter: Option<Arc<dyn SkillPrompter>>,
77 #[cfg(feature = "skill")]
78 skill_detail_tool_name: String,
79 #[cfg(feature = "skill")]
83 skill_detail_tool_factory: Option<SkillDetailToolFactory>,
84 #[cfg(feature = "skill")]
85 list_skills_tool_factory: Option<ListSkillsToolFactory>,
86 #[cfg(feature = "skill")]
87 disable_skill_prompt_injection: bool,
88 context_compactor: Option<Arc<dyn agent_base::ContextCompaction>>,
90 agent_instructions_paths: Vec<PathBuf>,
95 #[cfg(feature = "memory")]
98 memory_config: Option<MemoryConfig>,
99}
100
101impl AgentBuilder {
102 pub fn new(client: Arc<dyn agent_base::llm_trait::LlmProvider>) -> Self {
103 Self {
104 inner: agent_base::AgentBuilder::new(client.clone()),
105 client,
106 system_prompt: None,
107 tool_names: HashSet::new(),
108 business_tools: Vec::new(),
109 #[cfg(feature = "multi_agent")]
110 multi_agent_config: None,
111 #[cfg(feature = "multi_agent")]
112 multi_agent_tool_factory: None,
113 error_recovery: None,
114 language: None,
115 #[cfg(feature = "skill")]
116 skills: Vec::new(),
117 #[cfg(feature = "skill")]
118 skill_prompter: None,
119 #[cfg(feature = "skill")]
120 skill_detail_tool_name: "get_skill_detail".to_string(),
121 #[cfg(feature = "skill")]
122 skill_detail_tool_factory: None,
123 #[cfg(feature = "skill")]
124 list_skills_tool_factory: None,
125 #[cfg(feature = "skill")]
126 disable_skill_prompt_injection: false,
127 context_compactor: None,
128 agent_instructions_paths: Vec::new(),
129 #[cfg(feature = "memory")]
130 memory_config: None,
131 }
132 }
133
134 #[cfg(feature = "multi_agent")]
139 pub fn with_multi_agent(mut self, config: MultiAgentConfig) -> Self {
140 self.multi_agent_config = Some(config);
141 self
142 }
143
144 #[cfg(feature = "multi_agent")]
149 pub fn without_multi_agent(mut self) -> Self {
150 self.multi_agent_config = None;
151 self.multi_agent_tool_factory = None;
152 self
153 }
154
155 #[cfg(feature = "multi_agent")]
161 pub fn with_multi_agent_tool_factory(mut self, factory: MultiAgentToolFactory) -> Self {
162 self.multi_agent_tool_factory = Some(factory);
163 self
164 }
165
166 #[cfg(feature = "skill")]
171 pub fn with_skill_detail_tool_factory(mut self, factory: SkillDetailToolFactory) -> Self {
172 self.skill_detail_tool_factory = Some(factory);
173 self
174 }
175
176 #[cfg(feature = "skill")]
180 pub fn with_list_skills_tool_factory(mut self, factory: ListSkillsToolFactory) -> Self {
181 self.list_skills_tool_factory = Some(factory);
182 self
183 }
184
185 pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
186 let prompt = prompt.into();
187 self.inner = self.inner.system_prompt(prompt.clone());
188 self.system_prompt = Some(prompt);
189 self
190 }
191
192 pub fn enable_thought(self, enable: bool) -> Self {
193 Self {
194 inner: self.inner.enable_thought(enable),
195 ..self
196 }
197 }
198
199 pub fn reasoning(self, config: agent_base::ReasoningConfig) -> Self {
200 Self {
201 inner: self.inner.reasoning(config),
202 ..self
203 }
204 }
205
206 pub fn enable_thinking(self, enable: bool) -> Self {
207 Self {
208 inner: self.inner.enable_thinking(enable),
209 ..self
210 }
211 }
212
213 pub fn thinking_budget(self, budget: u64) -> Self {
214 Self {
215 inner: self.inner.thinking_budget(budget),
216 ..self
217 }
218 }
219
220 pub fn tool_timeout(self, timeout_ms: u64) -> Self {
221 Self {
222 inner: self.inner.tool_timeout(timeout_ms),
223 ..self
224 }
225 }
226
227 pub fn max_tool_output_chars(self, max_chars: usize) -> Self {
228 Self {
229 inner: self.inner.max_tool_output_chars(max_chars),
230 ..self
231 }
232 }
233
234 pub fn max_sessions(self, max: usize) -> Self {
235 Self {
236 inner: self.inner.max_sessions(max),
237 ..self
238 }
239 }
240
241 pub fn max_turns_per_session(self, max: usize) -> Self {
242 Self {
243 inner: self.inner.max_turns_per_session(max),
244 ..self
245 }
246 }
247
248 pub fn execution_max_turns(self, max: u32) -> Self {
249 Self {
250 inner: self.inner.execution_max_turns(max),
251 ..self
252 }
253 }
254
255 pub fn max_message_tokens(self, max: usize) -> Self {
256 Self {
257 inner: self.inner.max_message_tokens(max),
258 ..self
259 }
260 }
261
262 pub fn register_tool(mut self, tool: impl Tool + 'static) -> Self {
263 let tool_arc: Arc<dyn Tool> = Arc::new(tool);
264 self.tool_names.insert(tool_arc.name().to_string());
265 self.business_tools.push(tool_arc.clone());
266 self.inner = self.inner.register_tool_arc(tool_arc);
267 self
268 }
269
270 pub fn register_tool_arc(mut self, tool: Arc<dyn Tool>) -> Self {
271 self.tool_names.insert(tool.name().to_string());
272 self.business_tools.push(tool.clone());
273 self.inner = self.inner.register_tool_arc(tool);
274 self
275 }
276
277 pub fn approval_handler(self, handler: Arc<dyn agent_base::ApprovalHandler>) -> Self {
278 Self {
279 inner: self.inner.approval_handler(handler),
280 ..self
281 }
282 }
283
284 pub fn tool_policy(self, policy: Arc<dyn agent_base::ToolPolicy>) -> Self {
285 Self {
286 inner: self.inner.tool_policy(policy),
287 ..self
288 }
289 }
290
291 pub fn middleware(self, mw: impl agent_base::Middleware + 'static) -> Self {
292 Self {
293 inner: self.inner.middleware(mw),
294 ..self
295 }
296 }
297
298 pub fn guard(self, guard: impl agent_base::ReactLoopGuard + 'static) -> Self {
299 Self {
300 inner: self.inner.guard(guard),
301 ..self
302 }
303 }
304
305 pub fn context_window(self, max_tokens: usize) -> Self {
306 Self {
307 inner: self.inner.context_window(max_tokens),
308 ..self
309 }
310 }
311
312 pub fn context_window_manager(self, manager: agent_base::ContextWindowManager) -> Self {
313 Self {
314 inner: self.inner.context_window_manager(manager),
315 ..self
316 }
317 }
318
319 pub fn response_format(self, format: agent_base::ResponseFormat) -> Self {
320 Self {
321 inner: self.inner.response_format(format),
322 ..self
323 }
324 }
325
326 pub fn llm_retry(self, retry: agent_base::RetryConfig) -> Self {
327 Self {
328 inner: self.inner.llm_retry(retry),
329 ..self
330 }
331 }
332
333 pub fn session_store(self, store: Arc<dyn agent_base::SessionStore>) -> Self {
334 Self {
335 inner: self.inner.session_store(store),
336 ..self
337 }
338 }
339
340 pub fn error_recovery(mut self, recovery: Arc<dyn agent_base::ToolErrorRecovery>) -> Self {
341 self.error_recovery = Some(recovery.clone());
342 self.inner = self.inner.error_recovery(recovery);
343 self
344 }
345
346 pub fn tool_error_retry_prompt(self, prompt: impl Into<String>) -> Self {
347 Self {
348 inner: self.inner.tool_error_retry_prompt(prompt),
349 ..self
350 }
351 }
352
353 pub fn language(mut self, language: agent_base::Language) -> Self {
354 self.language = Some(language.clone());
355 self.inner = self.inner.language(language);
356 self
357 }
358
359 pub fn context_compactor(mut self, compactor: Arc<dyn agent_base::ContextCompaction>) -> Self {
364 self.context_compactor = Some(compactor);
365 self
366 }
367
368 pub fn event_bus_capacity(self, capacity: usize) -> Self {
369 Self {
370 inner: self.inner.event_bus_capacity(capacity),
371 ..self
372 }
373 }
374
375 pub fn session_id_generator(
376 self,
377 generator: Arc<dyn agent_base::types::SessionIdGenerator>,
378 ) -> Self {
379 Self {
380 inner: self.inner.session_id_generator(generator),
381 ..self
382 }
383 }
384
385 pub fn apply_if<T>(self, value: Option<T>, f: impl FnOnce(Self, T) -> Self) -> Self {
393 match value {
394 Some(v) => f(self, v),
395 None => self,
396 }
397 }
398
399 #[cfg(feature = "skill")]
400 pub fn register_skill(mut self, skill: impl Skill + 'static) -> Self {
401 self.skills.push(Arc::new(skill));
402 self
403 }
404
405 #[cfg(feature = "skill")]
406 pub fn register_skills(mut self, skills: Vec<Arc<dyn Skill>>) -> Self {
407 self.skills.extend(skills);
408 self
409 }
410
411 #[cfg(feature = "skill")]
412 pub fn skill_prompter(mut self, prompter: Arc<dyn SkillPrompter>) -> Self {
413 self.skill_prompter = Some(prompter);
414 self
415 }
416
417 #[cfg(feature = "skill")]
418 pub fn disable_skill_prompt_injection(mut self) -> Self {
419 self.disable_skill_prompt_injection = true;
420 self
421 }
422
423 pub fn agent_instructions_paths(mut self, paths: Vec<PathBuf>) -> Self {
440 self.agent_instructions_paths = paths;
441 self
442 }
443
444 #[cfg(feature = "skill")]
445 pub fn skill_detail_tool_name(mut self, name: impl Into<String>) -> Self {
446 self.skill_detail_tool_name = name.into();
447 self
448 }
449
450 #[cfg(feature = "memory")]
457 pub fn memory_config(mut self, config: MemoryConfig) -> Self {
458 self.memory_config = Some(config);
459 self
460 }
461
462 #[cfg(feature = "memory")]
464 pub fn without_memory_config(mut self) -> Self {
465 self.memory_config = None;
466 self
467 }
468
469 #[cfg(feature = "memory")]
474 fn apply_memory_config(mut self) -> Self {
475 let Some(config) = self.memory_config.take() else {
476 return self;
477 };
478
479 let store = Arc::new(MemoryStore::new(
480 config.memory_root.clone(),
481 config.index_filename.clone(),
482 ));
483
484 for tool in crate::tools::create_memory_tools(Arc::clone(&store)) {
486 let tool_name = tool.name().to_string();
487 if self.tool_names.contains(&tool_name) {
488 tracing::warn!(tool = %tool_name, "memory tool name conflict; skipping registration");
489 continue;
490 }
491 self.tool_names.insert(tool_name);
492 self.inner = self.inner.register_tool_arc(tool);
493 }
494
495 let index = store.read_index();
497 if index.len() > 10_240 {
498 tracing::warn!(
499 index_bytes = index.len(),
500 root = %config.memory_root.display(),
501 "memory index is unusually large (>10KB); consider cleaning up stale memories"
502 );
503 }
504 let mem_prompt = build_memory_system_prompt_with_config(&config, &index);
505 tracing::info!(
506 memory_root = %config.memory_root.display(),
507 index_bytes = index.len(),
508 tools = 4,
509 "injecting memory prompt + tools into system prompt"
510 );
511 let new_prompt = match self.system_prompt.take() {
512 Some(existing) => format!("{existing}\n\n---\n\n{mem_prompt}"),
513 None => mem_prompt,
514 };
515 tracing::info!(
516 prompt_len = new_prompt.len(),
517 "system prompt after memory injection (full text logged at DEBUG level)"
518 );
519 tracing::debug!(system_prompt = %new_prompt, "full system prompt after memory injection");
520 self.system_prompt = Some(new_prompt.clone());
521 self.inner = self.inner.system_prompt(new_prompt);
522 self
523 }
524
525 pub fn build(self) -> AgentResult<AgentRuntime> {
528 #[cfg(feature = "skill")]
529 {
530 self.build_with_skills()
531 }
532 #[cfg(not(feature = "skill"))]
533 {
534 self.build_inner()
535 }
536 }
537
538 #[cfg(feature = "multi_agent")]
544 pub fn build_with_ma(self) -> AgentResult<(AgentRuntime, Option<Arc<MultiAgentRuntime>>)> {
545 #[cfg(feature = "skill")]
546 {
547 self.build_with_skills_ma()
548 }
549 #[cfg(not(feature = "skill"))]
550 {
551 self.build_inner_ma()
552 }
553 }
554
555 #[allow(dead_code, unused_mut)]
556 fn build_inner(mut self) -> AgentResult<AgentRuntime> {
557 if self.inner.get_guard().is_none() {
560 self.inner = self
561 .inner
562 .guard(crate::guard::DefaultGuard::with_llm_client(
563 crate::guard::DefaultGuardConfig::default(),
564 self.client.clone(),
565 ));
566 }
567
568 if !self.agent_instructions_paths.is_empty()
570 && let Some(instructions) =
571 build_agent_instructions_prompt(&self.agent_instructions_paths)
572 {
573 tracing::info!(
574 paths = ?self.agent_instructions_paths,
575 instructions_len = instructions.len(),
576 "injecting agent instructions into system prompt"
577 );
578 let new_prompt = match self.system_prompt.take() {
579 Some(existing) => format!("{}\n\n---\n\n{}", existing, instructions),
580 None => instructions,
581 };
582 self.system_prompt = Some(new_prompt.clone());
583 self.inner = self.inner.system_prompt(new_prompt);
584 }
585
586 #[cfg(feature = "memory")]
588 {
589 self = self.apply_memory_config();
590 }
591
592 #[cfg(feature = "multi_agent")]
593 let lang = self.language.clone().unwrap_or_default();
594 #[cfg(feature = "multi_agent")]
595 let business_tools = std::mem::take(&mut self.business_tools);
596 #[cfg(feature = "multi_agent")]
597 let error_recovery = self.error_recovery.clone();
598 #[cfg(feature = "multi_agent")]
599 let tool_names = self.tool_names.clone();
600
601 #[cfg(feature = "multi_agent")]
602 let ma_config = self.multi_agent_config.clone();
603 #[cfg(feature = "multi_agent")]
604 let ma_tool_factory = self.multi_agent_tool_factory.take();
605
606 #[cfg(feature = "multi_agent")]
608 if ma_config.as_ref().map(|c| c.enabled).unwrap_or(false) {
609 let ma_prompt = build_multi_agent_system_prompt();
610 let new_prompt = match self.system_prompt.take() {
611 Some(existing) => format!("{}\n\n---\n\n{}", existing, ma_prompt),
612 None => ma_prompt,
613 };
614 self.inner = self.inner.system_prompt(new_prompt);
615 }
616
617 if let Some(compactor) = self.context_compactor.take() {
619 self.inner = self.inner.context_compactor(compactor);
620 }
621
622 let runtime = self.inner.build()?;
623
624 #[cfg(feature = "multi_agent")]
626 if let Some(config) = ma_config
627 && config.enabled
628 {
629 setup_multi_agent(
630 &runtime,
631 config,
632 lang,
633 business_tools,
634 error_recovery,
635 &tool_names,
636 ma_tool_factory,
637 )?;
638 }
639
640 Ok(runtime)
641 }
642
643 #[cfg(feature = "multi_agent")]
645 #[allow(dead_code, unused_mut)]
646 fn build_inner_ma(mut self) -> AgentResult<(AgentRuntime, Option<Arc<MultiAgentRuntime>>)> {
647 if self.inner.get_guard().is_none() {
648 self.inner = self
649 .inner
650 .guard(crate::guard::DefaultGuard::with_llm_client(
651 crate::guard::DefaultGuardConfig::default(),
652 self.client.clone(),
653 ));
654 }
655
656 #[cfg(feature = "memory")]
658 {
659 self = self.apply_memory_config();
660 }
661
662 let lang = self.language.clone().unwrap_or_default();
663 let business_tools = std::mem::take(&mut self.business_tools);
664 let error_recovery = self.error_recovery.clone();
665 let tool_names = self.tool_names.clone();
666 let ma_config = self.multi_agent_config.clone();
667 let ma_tool_factory = self.multi_agent_tool_factory.take();
668
669 if ma_config.as_ref().map(|c| c.enabled).unwrap_or(false) {
670 let ma_prompt = build_multi_agent_system_prompt();
671 let new_prompt = match self.system_prompt.take() {
672 Some(existing) => format!("{}\n\n---\n\n{}", existing, ma_prompt),
673 None => ma_prompt,
674 };
675 self.inner = self.inner.system_prompt(new_prompt);
676 }
677
678 if let Some(compactor) = self.context_compactor.take() {
679 self.inner = self.inner.context_compactor(compactor);
680 }
681
682 let runtime = self.inner.build()?;
683
684 let ma_runtime_out = if let Some(config) = ma_config
685 && config.enabled
686 {
687 Some(setup_multi_agent(
688 &runtime,
689 config,
690 lang,
691 business_tools,
692 error_recovery,
693 &tool_names,
694 ma_tool_factory,
695 )?)
696 } else {
697 None
698 };
699
700 Ok((runtime, ma_runtime_out))
701 }
702
703 #[cfg(feature = "skill")]
715 fn build_with_skills(mut self) -> AgentResult<AgentRuntime> {
716 if self.inner.get_guard().is_none() {
719 self.inner = self
720 .inner
721 .guard(crate::guard::DefaultGuard::with_llm_client(
722 crate::guard::DefaultGuardConfig::default(),
723 self.client.clone(),
724 ));
725 }
726
727 if !self.agent_instructions_paths.is_empty()
729 && let Some(instructions) =
730 build_agent_instructions_prompt(&self.agent_instructions_paths)
731 {
732 tracing::info!(
733 paths = ?self.agent_instructions_paths,
734 instructions_len = instructions.len(),
735 "injecting agent instructions into system prompt"
736 );
737 let new_prompt = match self.system_prompt.take() {
738 Some(existing) => format!("{}\n\n---\n\n{}", existing, instructions),
739 None => instructions,
740 };
741 self.system_prompt = Some(new_prompt.clone());
742 self.inner = self.inner.system_prompt(new_prompt);
743 }
744
745 #[cfg(feature = "memory")]
747 {
748 self = self.apply_memory_config();
749 }
750
751 let mut ab = self.inner;
752 #[cfg(feature = "multi_agent")]
753 let lang = self.language.clone().unwrap_or_default();
754 #[cfg(feature = "multi_agent")]
755 let business_tools = std::mem::take(&mut self.business_tools);
756 #[cfg(feature = "multi_agent")]
757 let error_recovery = self.error_recovery.clone();
758 #[cfg(feature = "multi_agent")]
759 let tool_names = self.tool_names.clone();
760
761 #[cfg(feature = "multi_agent")]
762 let ma_config = self.multi_agent_config.clone();
763 #[cfg(feature = "multi_agent")]
764 let ma_tool_factory = self.multi_agent_tool_factory.take();
765
766 if !self.skills.is_empty() {
768 let prompter: Arc<dyn SkillPrompter> = self
769 .skill_prompter
770 .take()
771 .unwrap_or_else(|| Arc::new(LazySkillPrompter::new()));
772
773 let mut skill_refs: Vec<Arc<dyn Skill>> = Vec::new();
774
775 for skill in self.skills {
776 for tool in skill.tools() {
777 let tool_name = tool.name().to_string();
778 if self.tool_names.contains(&tool_name) {
779 return Err(agent_base::AgentError::internal(format!(
780 "Tool name conflict: `{}` (Skill `{}`)",
781 tool_name,
782 skill.name()
783 )));
784 }
785 self.tool_names.insert(tool_name);
786 ab = ab.register_tool_arc(tool);
787 }
788 skill_refs.push(skill);
789 }
790
791 if !self.disable_skill_prompt_injection {
792 let skill_prompt = prompter.build_prompt(&skill_refs, &self.skill_detail_tool_name);
793 if !skill_prompt.is_empty() {
794 let new_prompt = match self.system_prompt.take() {
795 Some(existing) => format!("{}\n\n---\n\n{}", existing, skill_prompt),
796 None => skill_prompt,
797 };
798 self.system_prompt = Some(new_prompt.clone());
799 ab = ab.system_prompt(new_prompt);
800 }
801 }
802
803 if let Some(factory) = self.skill_detail_tool_factory.take() {
806 let detail_tool = factory(skill_refs.clone(), self.skill_detail_tool_name);
807 ab = ab.register_tool_arc(detail_tool);
808 }
809
810 if let Some(factory) = self.list_skills_tool_factory.take() {
812 let registry = Arc::new(crate::skill::SkillRegistry::new());
813 for skill in &skill_refs {
814 tokio::task::block_in_place(|| {
815 tokio::runtime::Handle::current().block_on(async {
816 registry.register(skill.clone()).await;
817 })
818 });
819 }
820 let list_tool = factory(registry);
821 ab = ab.register_tool_arc(list_tool);
822 }
823 }
824
825 #[cfg(feature = "multi_agent")]
827 if ma_config.as_ref().map(|c| c.enabled).unwrap_or(false) {
828 let ma_prompt = build_multi_agent_system_prompt();
829 let new_prompt = match self.system_prompt.take() {
830 Some(existing) => format!("{}\n\n---\n\n{}", existing, ma_prompt),
831 None => ma_prompt,
832 };
833 ab = ab.system_prompt(new_prompt);
834 }
835
836 if let Some(compactor) = self.context_compactor.take() {
838 ab = ab.context_compactor(compactor);
839 }
840
841 let runtime = ab.build()?;
842
843 #[cfg(feature = "multi_agent")]
845 if let Some(config) = ma_config
846 && config.enabled
847 {
848 setup_multi_agent(
849 &runtime,
850 config,
851 lang,
852 business_tools,
853 error_recovery,
854 &tool_names,
855 ma_tool_factory,
856 )?;
857 }
858
859 Ok(runtime)
860 }
861
862 #[cfg(all(feature = "skill", feature = "multi_agent"))]
864 fn build_with_skills_ma(
865 mut self,
866 ) -> AgentResult<(AgentRuntime, Option<Arc<MultiAgentRuntime>>)> {
867 if self.inner.get_guard().is_none() {
868 self.inner = self
869 .inner
870 .guard(crate::guard::DefaultGuard::with_llm_client(
871 crate::guard::DefaultGuardConfig::default(),
872 self.client.clone(),
873 ));
874 }
875
876 if !self.agent_instructions_paths.is_empty()
878 && let Some(instructions) =
879 build_agent_instructions_prompt(&self.agent_instructions_paths)
880 {
881 tracing::info!(
882 paths = ?self.agent_instructions_paths,
883 instructions_len = instructions.len(),
884 "injecting agent instructions into system prompt"
885 );
886 let new_prompt = match self.system_prompt.take() {
887 Some(existing) => format!("{}\n\n---\n\n{}", existing, instructions),
888 None => instructions,
889 };
890 self.system_prompt = Some(new_prompt.clone());
891 self.inner = self.inner.system_prompt(new_prompt);
892 }
893
894 #[cfg(feature = "memory")]
896 {
897 self = self.apply_memory_config();
898 }
899
900 let mut ab = self.inner;
901 let lang = self.language.clone().unwrap_or_default();
902 let business_tools = std::mem::take(&mut self.business_tools);
903 let error_recovery = self.error_recovery.clone();
904 let tool_names = self.tool_names.clone();
905 let ma_config = self.multi_agent_config.clone();
906 let ma_tool_factory = self.multi_agent_tool_factory.take();
907
908 if !self.skills.is_empty() {
910 let prompter: Arc<dyn SkillPrompter> = self
911 .skill_prompter
912 .take()
913 .unwrap_or_else(|| Arc::new(LazySkillPrompter::new()));
914
915 let mut skill_refs: Vec<Arc<dyn Skill>> = Vec::new();
916
917 for skill in self.skills {
918 for tool in skill.tools() {
919 let tool_name = tool.name().to_string();
920 if self.tool_names.contains(&tool_name) {
921 return Err(agent_base::AgentError::internal(format!(
922 "Tool name conflict: `{}` (Skill `{}`)",
923 tool_name,
924 skill.name()
925 )));
926 }
927 self.tool_names.insert(tool_name);
928 ab = ab.register_tool_arc(tool);
929 }
930 skill_refs.push(skill);
931 }
932
933 if !self.disable_skill_prompt_injection {
934 let skill_prompt = prompter.build_prompt(&skill_refs, &self.skill_detail_tool_name);
935 if !skill_prompt.is_empty() {
936 let new_prompt = match self.system_prompt.take() {
937 Some(existing) => format!("{}\n\n---\n\n{}", existing, skill_prompt),
938 None => skill_prompt,
939 };
940 self.system_prompt = Some(new_prompt.clone());
941 ab = ab.system_prompt(new_prompt);
942 }
943 }
944
945 if let Some(factory) = self.skill_detail_tool_factory.take() {
946 let detail_tool = factory(skill_refs.clone(), self.skill_detail_tool_name);
947 ab = ab.register_tool_arc(detail_tool);
948 }
949
950 if let Some(factory) = self.list_skills_tool_factory.take() {
951 let registry = Arc::new(crate::skill::SkillRegistry::new());
952 for skill in &skill_refs {
953 tokio::task::block_in_place(|| {
954 tokio::runtime::Handle::current().block_on(async {
955 registry.register(skill.clone()).await;
956 })
957 });
958 }
959 let list_tool = factory(registry);
960 ab = ab.register_tool_arc(list_tool);
961 }
962 }
963
964 if ma_config.as_ref().map(|c| c.enabled).unwrap_or(false) {
966 let ma_prompt = build_multi_agent_system_prompt();
967 let new_prompt = match self.system_prompt.take() {
968 Some(existing) => format!("{}\n\n---\n\n{}", existing, ma_prompt),
969 None => ma_prompt,
970 };
971 ab = ab.system_prompt(new_prompt);
972 }
973
974 if let Some(compactor) = self.context_compactor.take() {
975 ab = ab.context_compactor(compactor);
976 }
977
978 let runtime = ab.build()?;
979
980 let ma_runtime_out = if let Some(config) = ma_config
981 && config.enabled
982 {
983 Some(setup_multi_agent(
984 &runtime,
985 config,
986 lang,
987 business_tools,
988 error_recovery,
989 &tool_names,
990 ma_tool_factory,
991 )?)
992 } else {
993 None
994 };
995
996 Ok((runtime, ma_runtime_out))
997 }
998}
999
1000#[cfg(feature = "multi_agent")]
1011pub fn setup_multi_agent(
1012 runtime: &AgentRuntime,
1013 config: MultiAgentConfig,
1014 lang: agent_base::Language,
1015 business_tools: Vec<Arc<dyn Tool>>,
1016 error_recovery: Option<Arc<dyn agent_base::ToolErrorRecovery>>,
1017 existing_tool_names: &HashSet<String>,
1018 tool_factory: Option<MultiAgentToolFactory>,
1019) -> AgentResult<Arc<MultiAgentRuntime>> {
1020 let client = runtime.provider();
1021 let cancel_token = runtime.cancel_token();
1022 let tool_policy = runtime.tool_policy().cloned();
1023 let approval_handler = runtime.approval_handler().cloned();
1024
1025 let ma_runtime = Arc::new(MultiAgentRuntime::new(
1026 config.clone(),
1027 client,
1028 business_tools,
1029 cancel_token,
1030 error_recovery,
1031 lang,
1032 tool_policy,
1033 approval_handler,
1034 ));
1035
1036 ma_runtime.set_session_manager(Arc::new(runtime.session_manager().clone()));
1038
1039 let (event_tx, mut event_rx) =
1041 tokio::sync::mpsc::unbounded_channel::<agent_base::RuntimeEvent>();
1042 ma_runtime.set_event_sender(event_tx);
1043 let parent_runtime = runtime.clone();
1044 tokio::spawn(async move {
1045 while let Some(event) = event_rx.recv().await {
1046 parent_runtime.emit_event(event);
1047 }
1048 });
1049
1050 if let Some(factory) = tool_factory {
1052 let tools = factory(ma_runtime.clone());
1053 let registry = runtime.tools_mut();
1054 let mut reg = tokio::task::block_in_place(|| registry.blocking_write());
1055 for tool in tools {
1056 let tool_name = tool.name().to_string();
1057 if !existing_tool_names.contains(&tool_name) {
1058 reg.register_arc(tool);
1059 }
1060 }
1061 drop(reg);
1062 }
1063
1064 Ok(ma_runtime)
1065}
1066
1067#[cfg(feature = "multi_agent")]
1069pub fn build_multi_agent_system_prompt() -> String {
1070 r#"## Multi-Agent Capabilities
1071
1072You 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.
1073
1074### Tools
1075
1076- `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"})`.
1077- `send_message`: Send follow-up context or tasks to a sub-agent (`trigger: true` starts execution).
1078- `list_agents`: List active sub-agents with their assigned task and status — use it to answer "what is agent X doing?".
1079- `close_agent`: Close a sub-agent when done.
1080
1081### How Results Reach You
1082
1083Sub-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.
1084
1085### How Sub-Agents Work
1086
1087Sub-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.
1088
1089### When to Spawn
1090
1091- Parallel tasks: analyze multiple codebases, compare implementations, etc.
1092- Tasks you want to delegate while you continue other work
1093
1094### When NOT to Spawn
1095
1096- Simple lookups (just use read_file or repo_map directly)
1097- Tasks that need your current conversation context"#
1098 .to_string()
1099}
1100
1101pub fn build_memory_system_prompt() -> String {
1110 r#"## Memory
1111
1112You have a persistent file-based memory at `.phi/memory/`. Use `read_file` and `write_file` to manage it — there are no dedicated memory tools.
1113
1114### How Memory Works
1115
1116- `MEMORY.md` is the index — it lists all memories with one-line descriptions. Read it first when you need to recall something.
1117- Each memory is a separate `.md` file with YAML frontmatter:
1118 ```yaml
1119 ---
1120 name: <short-kebab-case-slug>
1121 description: <one-line summary — used to decide relevance during recall>
1122 metadata:
1123 node_type: memory
1124 type: user | feedback | project | reference
1125 ---
1126
1127 <the fact or instruction>
1128 ```
1129- 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.
1130- Link related memories with `[[memory-name]]` in the body.
1131- `user` type = who the user is (role, expertise, preferences).
1132- `feedback` type = guidance the user has given on how you should work.
1133- `project` type = ongoing work, goals, or constraints.
1134- `reference` type = pointers to external resources (URLs, dashboards, tickets).
1135
1136### When to Use Memory
1137
1138- The user explicitly asks you to remember something ("remember this", "save that")
1139- You learn something important about the user's preferences or workflow
1140- After completing a significant task, save context that would help in future sessions
1141- The user gives you feedback on how to work — save it as `feedback` type
1142
1143### When NOT to Use Memory
1144
1145- For transient information that won't be useful beyond this session
1146- For facts already recorded in the codebase (code structure, git history, config files)
1147- For items that only matter to the current conversation
1148
1149### Pro Tips
1150
1151- When creating your first memory of a new type, you can read template files for format reference (check `.phi/templates/memory/` if available).
1152- Keep the MEMORY.md index concise — it's loaded into context every session.
1153- Before writing a new memory, check if an existing file already covers it — update instead of duplicating.
1154
1155### Workflow
1156
1157**To recall:** read `MEMORY.md` → find relevant entries by description → read the specific `.md` files you need.
1158**To remember:** create a new `.md` file with proper frontmatter → update `MEMORY.md` with a new entry.
1159**To update:** edit the existing `.md` file (don't create a duplicate).
1160**To forget:** delete the `.md` file → remove its entry from `MEMORY.md`."#
1161 .to_string()
1162}
1163
1164#[cfg(feature = "memory")]
1177pub fn build_memory_system_prompt_with_config(
1178 config: &crate::memory::MemoryConfig,
1179 index_content: &str,
1180) -> String {
1181 let index = if index_content.trim().is_empty() {
1182 "(no memories yet — memory_write creates the first one)".to_string()
1183 } else {
1184 index_content.to_string()
1185 };
1186 config
1187 .prompt_template
1188 .replace("{memory_root}", &config.memory_root.to_string_lossy())
1189 .replace("{index_content}", &index)
1190 .replace(
1191 "{tools_description}",
1192 crate::tools::MEMORY_TOOLS_DESCRIPTION,
1193 )
1194}
1195
1196#[cfg(test)]
1197mod tests {
1198 use super::*;
1199 use agent_base::Content;
1200 use agent_base::llm_trait::response::FinishReason;
1201 use agent_base::llm_trait::types::UsageInfo;
1202 use agent_base::llm_trait::{
1203 Capabilities, ChatRequest, ChatResponse, ChatStream, LlmError, LlmProvider, ProviderInfo,
1204 };
1205
1206 struct StubProvider;
1209
1210 #[async_trait::async_trait]
1211 impl LlmProvider for StubProvider {
1212 async fn stream(&self, _request: ChatRequest) -> Result<ChatStream, LlmError> {
1213 let chunks = vec![
1214 Ok(agent_base::StreamChunk::Text("ok".to_string())),
1215 Ok(agent_base::StreamChunk::Stop {
1216 finish_reason: Some("stop".to_string()),
1217 }),
1218 ];
1219 Ok(ChatStream::new(Box::pin(futures_util::stream::iter(
1220 chunks,
1221 ))))
1222 }
1223
1224 async fn chat(&self, _request: ChatRequest) -> Result<ChatResponse, LlmError> {
1225 Ok(ChatResponse {
1226 content: "ok".to_string(),
1227 tool_calls: vec![],
1228 usage: UsageInfo::default(),
1229 finish_reason: FinishReason::Stop,
1230 raw: None,
1231 reasoning_content: None,
1232 thinking_signature: None,
1233 })
1234 }
1235
1236 fn capabilities(&self) -> Capabilities {
1237 Capabilities {
1238 supports_streaming: true,
1239 supports_tools: true,
1240 ..Default::default()
1241 }
1242 }
1243
1244 fn info(&self) -> ProviderInfo {
1245 ProviderInfo {
1246 name: "stub".to_string(),
1247 model: "stub-model".to_string(),
1248 version: None,
1249 }
1250 }
1251 }
1252
1253 fn make_client() -> Arc<dyn LlmProvider> {
1254 Arc::new(StubProvider)
1255 }
1256
1257 #[cfg(feature = "multi_agent")]
1260 #[tokio::test(flavor = "multi_thread")]
1261 async fn test_setup_multi_agent_without_factory_registers_no_tools() {
1262 let client = make_client();
1263 let runtime = agent_base::AgentBuilder::new(client.clone())
1264 .build()
1265 .unwrap();
1266 let config = MultiAgentConfig::enabled();
1267
1268 let result = setup_multi_agent(
1269 &runtime,
1270 config,
1271 agent_base::Language::En,
1272 vec![],
1273 None,
1274 &HashSet::new(),
1275 None, );
1277 assert!(result.is_ok());
1278 let ma_runtime = result.unwrap();
1279 let agents = ma_runtime.list_agents();
1281 assert!(agents.is_empty());
1282 }
1283
1284 #[cfg(feature = "multi_agent")]
1285 #[tokio::test(flavor = "multi_thread")]
1286 async fn test_setup_multi_agent_with_factory_registers_tools() {
1287 let client = make_client();
1288 let runtime = agent_base::AgentBuilder::new(client.clone())
1289 .build()
1290 .unwrap();
1291 let config = MultiAgentConfig::enabled();
1292
1293 let factory: MultiAgentToolFactory = Arc::new(|_rt| {
1294 struct FakeTool;
1296 #[async_trait::async_trait]
1297 impl Tool for FakeTool {
1298 fn name(&self) -> &'static str {
1299 "fake_tool"
1300 }
1301 fn description(&self) -> &'static str {
1302 ""
1303 }
1304 fn schema(&self) -> serde_json::Value {
1305 serde_json::json!({})
1306 }
1307 async fn call(
1308 &self,
1309 _args: &serde_json::Value,
1310 _ctx: &agent_base::ToolContext,
1311 ) -> AgentResult<Vec<Content>> {
1312 Ok(vec![Content::text("ok")])
1313 }
1314 }
1315 vec![Arc::new(FakeTool)]
1316 });
1317
1318 let result = setup_multi_agent(
1319 &runtime,
1320 config,
1321 agent_base::Language::En,
1322 vec![],
1323 None,
1324 &HashSet::new(),
1325 Some(factory),
1326 );
1327 assert!(result.is_ok());
1328
1329 let tools: Vec<String> = tokio::task::block_in_place(|| {
1331 let tools = runtime.tools_mut();
1332 let guard = tools.blocking_read();
1333 guard.metadatas().into_iter().map(|m| m.name).collect()
1334 });
1335 assert!(tools.contains(&"fake_tool".to_string()));
1336 }
1337
1338 #[cfg(feature = "multi_agent")]
1339 #[tokio::test(flavor = "multi_thread")]
1340 async fn test_setup_multi_agent_skips_duplicate_tool_names() {
1341 let client = make_client();
1342 let runtime = agent_base::AgentBuilder::new(client.clone())
1343 .build()
1344 .unwrap();
1345
1346 struct DupTool;
1348 #[async_trait::async_trait]
1349 impl Tool for DupTool {
1350 fn name(&self) -> &'static str {
1351 "dup_tool"
1352 }
1353 fn description(&self) -> &'static str {
1354 ""
1355 }
1356 fn schema(&self) -> serde_json::Value {
1357 serde_json::json!({})
1358 }
1359 async fn call(
1360 &self,
1361 _args: &serde_json::Value,
1362 _ctx: &agent_base::ToolContext,
1363 ) -> AgentResult<Vec<Content>> {
1364 Ok(vec![Content::text("ok")])
1365 }
1366 }
1367 {
1368 let tools = runtime.tools_mut();
1369 let mut reg = tokio::task::block_in_place(|| tools.blocking_write());
1370 reg.register(DupTool);
1371 }
1372
1373 let factory: MultiAgentToolFactory = Arc::new(|_rt| {
1374 struct FakeTool;
1375 #[async_trait::async_trait]
1376 impl Tool for FakeTool {
1377 fn name(&self) -> &'static str {
1378 "dup_tool"
1379 }
1380 fn description(&self) -> &'static str {
1381 ""
1382 }
1383 fn schema(&self) -> serde_json::Value {
1384 serde_json::json!({})
1385 }
1386 async fn call(
1387 &self,
1388 _args: &serde_json::Value,
1389 _ctx: &agent_base::ToolContext,
1390 ) -> AgentResult<Vec<Content>> {
1391 Ok(vec![Content::text("ok")])
1392 }
1393 }
1394 vec![Arc::new(FakeTool)]
1395 });
1396
1397 let mut existing = HashSet::new();
1398 existing.insert("dup_tool".to_string());
1399
1400 let result = setup_multi_agent(
1401 &runtime,
1402 MultiAgentConfig::enabled(),
1403 agent_base::Language::En,
1404 vec![],
1405 None,
1406 &existing,
1407 Some(factory),
1408 );
1409 assert!(result.is_ok());
1410 let tools = tokio::task::block_in_place(|| {
1412 let tools = runtime.tools_mut();
1413 let guard = tools.blocking_read();
1414 guard
1415 .metadatas()
1416 .into_iter()
1417 .map(|m| m.name)
1418 .collect::<Vec<String>>()
1419 });
1420 let count = tools.iter().filter(|n| n.as_str() == "dup_tool").count();
1421 assert_eq!(count, 1);
1422 }
1423
1424 #[cfg(feature = "multi_agent")]
1427 #[tokio::test(flavor = "multi_thread")]
1428 async fn test_builder_with_multi_agent_without_factory_builds_ok() {
1429 let client = make_client();
1430 let runtime = AgentBuilder::new(client)
1431 .with_multi_agent(MultiAgentConfig::enabled())
1432 .build()
1433 .unwrap();
1434 let tools = tokio::task::block_in_place(|| {
1436 let tools = runtime.tools_mut();
1437 let guard = tools.blocking_read();
1438 guard
1439 .metadatas()
1440 .into_iter()
1441 .map(|m| m.name)
1442 .collect::<Vec<String>>()
1443 });
1444 assert!(!tools.contains(&"spawn_agent".to_string()));
1446 }
1447
1448 #[cfg(feature = "multi_agent")]
1449 #[tokio::test(flavor = "multi_thread")]
1450 async fn test_builder_with_factory_registers_tools() {
1451 let client = make_client();
1452 let factory: MultiAgentToolFactory = Arc::new(|_rt| {
1454 struct TestTool;
1455 #[async_trait::async_trait]
1456 impl Tool for TestTool {
1457 fn name(&self) -> &'static str {
1458 "factory_test_tool"
1459 }
1460 fn description(&self) -> &'static str {
1461 ""
1462 }
1463 fn schema(&self) -> serde_json::Value {
1464 serde_json::json!({})
1465 }
1466 async fn call(
1467 &self,
1468 _args: &serde_json::Value,
1469 _ctx: &agent_base::ToolContext,
1470 ) -> AgentResult<Vec<Content>> {
1471 Ok(vec![Content::text("ok")])
1472 }
1473 }
1474 vec![Arc::new(TestTool)]
1475 });
1476
1477 let runtime = AgentBuilder::new(client)
1478 .with_multi_agent(MultiAgentConfig::enabled())
1479 .with_multi_agent_tool_factory(factory)
1480 .build()
1481 .unwrap();
1482
1483 let tools = tokio::task::block_in_place(|| {
1484 let tools = runtime.tools_mut();
1485 let guard = tools.blocking_read();
1486 guard
1487 .metadatas()
1488 .into_iter()
1489 .map(|m| m.name)
1490 .collect::<Vec<String>>()
1491 });
1492 assert!(tools.contains(&"factory_test_tool".to_string()));
1493 }
1494
1495 #[cfg(feature = "multi_agent")]
1496 #[test]
1497 fn test_builder_disabled_multi_agent_skips_factory() {
1498 let client = make_client();
1499 let factory: MultiAgentToolFactory = Arc::new(|_rt| {
1500 panic!("factory should not be called when multi-agent is not configured");
1501 });
1502
1503 let runtime = AgentBuilder::new(client)
1504 .with_multi_agent_tool_factory(factory)
1505 .build()
1507 .unwrap();
1508
1509 let tools = tokio::task::block_in_place(|| {
1510 let tools = runtime.tools_mut();
1511 let guard = tools.blocking_read();
1512 guard
1513 .metadatas()
1514 .into_iter()
1515 .map(|m| m.name)
1516 .collect::<Vec<String>>()
1517 });
1518 assert!(!tools.contains(&"spawn_agent".to_string()));
1519 }
1520
1521 #[cfg(feature = "multi_agent")]
1524 #[test]
1525 fn test_system_prompt_contains_tool_names() {
1526 let prompt = build_multi_agent_system_prompt();
1527 assert!(prompt.contains("spawn_agent"));
1528 assert!(prompt.contains("send_message"));
1529 assert!(prompt.contains("trigger"));
1530 assert!(prompt.contains("list_agents"));
1531 assert!(prompt.contains("close_agent"));
1532 assert!(!prompt.contains("followup_task"));
1535 assert!(!prompt.contains("wait_agent"));
1538 assert!(prompt.contains("pushed into your context automatically"));
1539 }
1540
1541 #[cfg(feature = "multi_agent")]
1542 #[test]
1543 fn test_system_prompt_contains_guidance() {
1544 let prompt = build_multi_agent_system_prompt();
1545 assert!(prompt.contains("When to Spawn"));
1546 assert!(prompt.contains("When NOT to Spawn"));
1547 assert!(prompt.contains("How Results Reach You"));
1548 }
1549
1550 #[cfg(feature = "multi_agent")]
1553 #[tokio::test(flavor = "multi_thread")]
1554 async fn test_without_multi_agent_clears_config_and_factory() {
1555 let client = make_client();
1556
1557 let factory: MultiAgentToolFactory = Arc::new(|_rt| {
1559 panic!("factory should not be called when multi-agent is cleared");
1560 });
1561
1562 let runtime = AgentBuilder::new(client)
1563 .with_multi_agent(MultiAgentConfig::enabled())
1564 .with_multi_agent_tool_factory(factory)
1565 .without_multi_agent() .build()
1567 .unwrap();
1568
1569 let tools = tokio::task::block_in_place(|| {
1570 let tools = runtime.tools_mut();
1571 let guard = tools.blocking_read();
1572 guard
1573 .metadatas()
1574 .into_iter()
1575 .map(|m| m.name)
1576 .collect::<Vec<String>>()
1577 });
1578 assert!(!tools.contains(&"spawn_agent".to_string()));
1579 }
1580
1581 #[test]
1584 fn test_apply_if_some_applies_transformation() {
1585 let client = make_client();
1586 let builder = AgentBuilder::new(client)
1587 .apply_if(Some("custom prompt"), |b, prompt| b.system_prompt(prompt));
1588 assert!(builder.system_prompt.unwrap().contains("custom prompt"));
1590 }
1591
1592 #[test]
1593 fn test_apply_if_none_passes_through() {
1594 let client = make_client();
1595 let builder = AgentBuilder::new(client).apply_if(None as Option<&str>, |_b, _prompt| {
1596 panic!("should not be called when value is None");
1597 });
1598 assert!(builder.system_prompt.is_none());
1599 }
1600
1601 #[test]
1604 fn test_build_memory_system_prompt_non_empty() {
1605 let prompt = build_memory_system_prompt();
1606 assert!(!prompt.is_empty());
1607 assert!(prompt.contains("Memory"));
1608 assert!(prompt.contains("MEMORY.md"));
1609 assert!(prompt.contains("read_file"));
1610 assert!(prompt.contains("write_file"));
1611 }
1612
1613 #[cfg(feature = "memory")]
1616 mod memory_tests {
1617 use super::*;
1618 use crate::memory::{CLAUDE_COMPATIBLE_TEMPLATE, MemoryConfig, MemoryStore};
1619
1620 fn temp_config() -> (tempfile::TempDir, MemoryConfig) {
1621 let dir = tempfile::tempdir().unwrap();
1622 let config = MemoryConfig::custom(
1623 dir.path().join("memory"),
1624 CLAUDE_COMPATIBLE_TEMPLATE.to_string(),
1625 );
1626 (dir, config)
1627 }
1628
1629 #[test]
1630 fn prompt_with_config_substitutes_all_placeholders() {
1631 let (_dir, config) = temp_config();
1632 let prompt =
1633 build_memory_system_prompt_with_config(&config, "- [alpha](alpha.md) — first\n");
1634 assert!(
1635 prompt.contains(config.memory_root.to_string_lossy().as_ref()),
1636 "{prompt}"
1637 );
1638 assert!(prompt.contains("- [alpha](alpha.md) — first"), "{prompt}");
1639 assert!(prompt.contains("memory_write"), "{prompt}");
1640 assert!(prompt.contains("memory_delete"), "{prompt}");
1641 assert!(!prompt.contains("{memory_root}"), "{prompt}");
1642 assert!(!prompt.contains("{index_content}"), "{prompt}");
1643 assert!(!prompt.contains("{tools_description}"), "{prompt}");
1644 }
1645
1646 #[test]
1647 fn prompt_with_config_handles_empty_index() {
1648 let (_dir, config) = temp_config();
1649 let prompt = build_memory_system_prompt_with_config(&config, "");
1650 assert!(prompt.contains("no memories yet"), "{prompt}");
1651 }
1652
1653 #[test]
1654 fn memory_disabled_by_default_registers_nothing() {
1655 let client = make_client();
1656 let runtime = AgentBuilder::new(client).build().unwrap();
1657 let names = runtime_tool_names(&runtime);
1658 assert!(!names.iter().any(|n| n.starts_with("memory_")), "{names:?}");
1659 assert!(runtime.config().system_prompt.is_none());
1660 }
1661
1662 #[test]
1663 fn without_memory_config_clears_previous_config() {
1664 let (_dir, config) = temp_config();
1665 let client = make_client();
1666 let builder = AgentBuilder::new(client)
1667 .memory_config(config)
1668 .without_memory_config();
1669 assert!(builder.memory_config.is_none());
1670 }
1671
1672 #[tokio::test(flavor = "multi_thread")]
1673 async fn memory_config_registers_tools_and_injects_prompt() {
1674 let (dir, config) = temp_config();
1675
1676 let store = MemoryStore::new(config.memory_root.clone(), "MEMORY.md");
1678 store
1679 .write_memory("seed-note", "seeded before build", "project", "body", None)
1680 .unwrap();
1681
1682 let client = make_client();
1683 let runtime = AgentBuilder::new(client)
1684 .system_prompt("base prompt")
1685 .memory_config(config)
1686 .build()
1687 .unwrap();
1688
1689 let names = runtime_tool_names(&runtime);
1691 for expected in [
1692 "memory_write",
1693 "memory_read",
1694 "memory_list",
1695 "memory_delete",
1696 ] {
1697 assert!(names.contains(&expected.to_string()), "{names:?}");
1698 }
1699
1700 let prompt =
1702 tokio::task::block_in_place(|| runtime.config().system_prompt.clone().unwrap());
1703 assert!(prompt.starts_with("base prompt"), "{prompt}");
1704 assert!(
1705 prompt.contains("- [seed-note](seed-note.md) — seeded before build"),
1706 "{prompt}"
1707 );
1708 assert!(
1709 prompt.contains(dir.path().join("memory").to_string_lossy().as_ref()),
1710 "{prompt}"
1711 );
1712 }
1713 }
1714
1715 struct NamedTool(&'static str);
1718
1719 #[async_trait::async_trait]
1720 impl Tool for NamedTool {
1721 fn name(&self) -> &'static str {
1722 self.0
1723 }
1724
1725 fn description(&self) -> &'static str {
1726 ""
1727 }
1728
1729 fn schema(&self) -> serde_json::Value {
1730 serde_json::json!({})
1731 }
1732
1733 async fn call(
1734 &self,
1735 _args: &serde_json::Value,
1736 _ctx: &agent_base::ToolContext,
1737 ) -> AgentResult<Vec<Content>> {
1738 Ok(vec![Content::text("ok")])
1739 }
1740 }
1741
1742 fn runtime_tool_names(runtime: &AgentRuntime) -> Vec<String> {
1743 tokio::task::block_in_place(|| {
1744 let tools = runtime.tools_mut();
1745 let guard = tools.blocking_read();
1746 guard.metadatas().into_iter().map(|m| m.name).collect()
1747 })
1748 }
1749
1750 #[tokio::test(flavor = "multi_thread")]
1751 async fn test_builder_scalar_passthrough_methods() {
1752 let client = make_client();
1753 let runtime = AgentBuilder::new(client)
1754 .enable_thought(true)
1755 .reasoning(agent_base::ReasoningConfig::default())
1756 .enable_thinking(false)
1757 .thinking_budget(1000)
1758 .tool_timeout(5000)
1759 .max_tool_output_chars(4000)
1760 .max_sessions(16)
1761 .max_turns_per_session(20)
1762 .execution_max_turns(10)
1763 .max_message_tokens(8000)
1764 .context_window(64_000)
1765 .context_window_manager(agent_base::ContextWindowManager::new(64_000))
1766 .response_format(agent_base::ResponseFormat::JsonObject)
1767 .llm_retry(agent_base::RetryConfig::default())
1768 .tool_error_retry_prompt("please retry")
1769 .language(agent_base::Language::En)
1770 .event_bus_capacity(256)
1771 .build()
1772 .unwrap();
1773
1774 assert!(runtime.provider().capabilities().supports_streaming);
1775 }
1776
1777 #[tokio::test(flavor = "multi_thread")]
1778 async fn test_register_tool_variants() {
1779 let client = make_client();
1780 let runtime = AgentBuilder::new(client)
1781 .register_tool(NamedTool("tool_by_value"))
1782 .register_tool_arc(Arc::new(NamedTool("tool_by_arc")))
1783 .build()
1784 .unwrap();
1785
1786 let names = runtime_tool_names(&runtime);
1787 assert!(names.contains(&"tool_by_value".to_string()));
1788 assert!(names.contains(&"tool_by_arc".to_string()));
1789 }
1790
1791 #[cfg(feature = "skill")]
1792 mod skill_tests {
1793 use super::*;
1794 use crate::skill::Skill;
1795
1796 struct TestSkill;
1797
1798 impl Skill for TestSkill {
1799 fn name(&self) -> &'static str {
1800 "test_skill"
1801 }
1802
1803 fn brief_description(&self) -> String {
1804 "a test skill".to_string()
1805 }
1806
1807 fn detailed_description(&self) -> String {
1808 "detailed test skill".to_string()
1809 }
1810
1811 fn tools(&self) -> Vec<Arc<dyn Tool>> {
1812 vec![]
1813 }
1814 }
1815
1816 struct ToolSkill;
1817
1818 impl Skill for ToolSkill {
1819 fn name(&self) -> &'static str {
1820 "tool_skill"
1821 }
1822
1823 fn brief_description(&self) -> String {
1824 "skill with a tool".to_string()
1825 }
1826
1827 fn detailed_description(&self) -> String {
1828 "skill that provides a tool".to_string()
1829 }
1830
1831 fn tools(&self) -> Vec<Arc<dyn Tool>> {
1832 vec![Arc::new(NamedTool("skill_provided_tool"))]
1833 }
1834 }
1835
1836 #[tokio::test(flavor = "multi_thread")]
1837 async fn test_register_skill_builds_ok() {
1838 let client = make_client();
1839 let runtime = AgentBuilder::new(client)
1840 .register_skill(TestSkill)
1841 .build()
1842 .unwrap();
1843 assert!(runtime_tool_names(&runtime).is_empty());
1845 }
1846
1847 #[tokio::test(flavor = "multi_thread")]
1848 async fn test_register_skill_with_tool_registers_tool() {
1849 let client = make_client();
1850 let runtime = AgentBuilder::new(client)
1851 .register_skill(ToolSkill)
1852 .build()
1853 .unwrap();
1854 assert!(runtime_tool_names(&runtime).contains(&"skill_provided_tool".to_string()));
1855 }
1856
1857 #[tokio::test(flavor = "multi_thread")]
1858 async fn test_register_skill_tool_name_conflict() {
1859 let client = make_client();
1860 let result = AgentBuilder::new(client)
1861 .register_tool(NamedTool("skill_provided_tool"))
1862 .register_skill(ToolSkill)
1863 .build();
1864 let err = result.err().unwrap();
1865 assert!(format!("{err}").contains("Tool name conflict"));
1866 }
1867
1868 #[tokio::test(flavor = "multi_thread")]
1869 async fn test_skill_detail_tool_factory_registers_tool() {
1870 let client = make_client();
1871 let factory: SkillDetailToolFactory = Arc::new(|_skills, name| {
1872 assert_eq!(name, "get_skill_detail");
1873 Arc::new(NamedTool("detail_tool"))
1874 });
1875 let runtime = AgentBuilder::new(client)
1876 .register_skill(TestSkill)
1877 .with_skill_detail_tool_factory(factory)
1878 .build()
1879 .unwrap();
1880 assert!(runtime_tool_names(&runtime).contains(&"detail_tool".to_string()));
1881 }
1882
1883 #[tokio::test(flavor = "multi_thread")]
1884 async fn test_list_skills_tool_factory_registers_tool() {
1885 let client = make_client();
1886 let factory: ListSkillsToolFactory =
1887 Arc::new(|_registry| Arc::new(NamedTool("list_skills_tool")));
1888 let runtime = AgentBuilder::new(client)
1889 .register_skill(TestSkill)
1890 .with_list_skills_tool_factory(factory)
1891 .build()
1892 .unwrap();
1893 assert!(runtime_tool_names(&runtime).contains(&"list_skills_tool".to_string()));
1894 }
1895
1896 #[tokio::test(flavor = "multi_thread")]
1897 async fn test_disable_skill_prompt_injection_builds() {
1898 let client = make_client();
1899 let runtime = AgentBuilder::new(client)
1900 .register_skill(ToolSkill)
1901 .disable_skill_prompt_injection()
1902 .build()
1903 .unwrap();
1904 assert!(runtime_tool_names(&runtime).contains(&"skill_provided_tool".to_string()));
1906 }
1907 }
1908
1909 #[test]
1912 fn test_build_agent_instructions_prompt_reads_files() {
1913 let tmp = tempfile::tempdir().unwrap();
1914 let file1 = tmp.path().join("CLAUDE.md");
1915 let file2 = tmp.path().join("INSTRUCTIONS.md");
1916 std::fs::write(&file1, "# User instructions\nBe helpful.").unwrap();
1917 std::fs::write(&file2, "# Project instructions\nFollow conventions.").unwrap();
1918
1919 let prompt = build_agent_instructions_prompt(&[file1, file2]);
1920 assert!(prompt.is_some());
1921 let prompt = prompt.unwrap();
1922 assert!(prompt.starts_with("# Project instructions"));
1924 assert!(prompt.contains("# User instructions"));
1925 assert!(prompt.contains("Be helpful."));
1926 assert!(prompt.contains("Follow conventions."));
1927 }
1928
1929 #[test]
1930 fn test_build_agent_instructions_prompt_skips_missing_files() {
1931 let prompt = build_agent_instructions_prompt(&[
1932 PathBuf::from("/nonexistent/CLAUDE.md"),
1933 PathBuf::from("/nonexistent/INSTRUCTIONS.md"),
1934 ]);
1935 assert!(prompt.is_none());
1936 }
1937
1938 #[test]
1939 fn test_build_agent_instructions_prompt_skips_empty_files() {
1940 let tmp = tempfile::tempdir().unwrap();
1941 let file = tmp.path().join("empty.md");
1942 std::fs::write(&file, " \n ").unwrap();
1943
1944 let prompt = build_agent_instructions_prompt(&[file]);
1945 assert!(prompt.is_none());
1946 }
1947}