1use std::collections::HashSet;
2use std::sync::Arc;
3
4use agent_base::{AgentResult, AgentRuntime, StreamClient, Tool};
5
6#[cfg(feature = "multi_agent")]
7use crate::multi_agent::{MultiAgentConfig, MultiAgentRuntime};
8
9#[cfg(feature = "skill")]
10use crate::skill::{LazySkillPrompter, Skill, SkillPrompter};
11
12#[cfg(feature = "multi_agent")]
14pub type MultiAgentToolFactory =
15 Arc<dyn Fn(Arc<MultiAgentRuntime>) -> Vec<Arc<dyn Tool>> + Send + Sync>;
16
17#[cfg(feature = "skill")]
19pub type SkillDetailToolFactory =
20 Arc<dyn Fn(Vec<Arc<dyn Skill>>, String) -> Arc<dyn Tool> + Send + Sync>;
21
22#[cfg(feature = "skill")]
24pub type ListSkillsToolFactory =
25 Arc<dyn Fn(Arc<crate::skill::SkillRegistry>) -> Arc<dyn Tool> + Send + Sync>;
26
27pub struct AgentBuilder {
28 inner: agent_base::AgentBuilder,
29 client: Arc<dyn StreamClient>,
30 system_prompt: Option<String>,
31 tool_names: HashSet<String>,
32 business_tools: Vec<Arc<dyn Tool>>,
34 #[cfg(feature = "multi_agent")]
36 multi_agent_config: Option<MultiAgentConfig>,
37 #[cfg(feature = "multi_agent")]
39 multi_agent_tool_factory: Option<MultiAgentToolFactory>,
40 error_recovery: Option<Arc<dyn agent_base::ToolErrorRecovery>>,
42 language: Option<agent_base::Language>,
44 #[cfg(feature = "skill")]
45 skills: Vec<Arc<dyn Skill>>,
46 #[cfg(feature = "skill")]
47 skill_prompter: Option<Arc<dyn SkillPrompter>>,
48 #[cfg(feature = "skill")]
49 skill_detail_tool_name: String,
50 #[cfg(feature = "skill")]
54 skill_detail_tool_factory: Option<SkillDetailToolFactory>,
55 #[cfg(feature = "skill")]
56 list_skills_tool_factory: Option<ListSkillsToolFactory>,
57 #[cfg(feature = "skill")]
58 disable_skill_prompt_injection: bool,
59}
60
61impl AgentBuilder {
62 pub fn new(client: Arc<dyn StreamClient>) -> Self {
63 Self {
64 inner: agent_base::AgentBuilder::new(client.clone()),
65 client,
66 system_prompt: None,
67 tool_names: HashSet::new(),
68 business_tools: Vec::new(),
69 #[cfg(feature = "multi_agent")]
70 multi_agent_config: None,
71 #[cfg(feature = "multi_agent")]
72 multi_agent_tool_factory: None,
73 error_recovery: None,
74 language: None,
75 #[cfg(feature = "skill")]
76 skills: Vec::new(),
77 #[cfg(feature = "skill")]
78 skill_prompter: None,
79 #[cfg(feature = "skill")]
80 skill_detail_tool_name: "get_skill_detail".to_string(),
81 #[cfg(feature = "skill")]
82 skill_detail_tool_factory: None,
83 #[cfg(feature = "skill")]
84 list_skills_tool_factory: None,
85 #[cfg(feature = "skill")]
86 disable_skill_prompt_injection: false,
87 }
88 }
89
90 #[cfg(feature = "multi_agent")]
95 pub fn with_multi_agent(mut self, config: MultiAgentConfig) -> Self {
96 self.multi_agent_config = Some(config);
97 self
98 }
99
100 #[cfg(feature = "multi_agent")]
105 pub fn without_multi_agent(mut self) -> Self {
106 self.multi_agent_config = None;
107 self.multi_agent_tool_factory = None;
108 self
109 }
110
111 #[cfg(feature = "multi_agent")]
117 pub fn with_multi_agent_tool_factory(mut self, factory: MultiAgentToolFactory) -> Self {
118 self.multi_agent_tool_factory = Some(factory);
119 self
120 }
121
122 #[cfg(feature = "skill")]
127 pub fn with_skill_detail_tool_factory(mut self, factory: SkillDetailToolFactory) -> Self {
128 self.skill_detail_tool_factory = Some(factory);
129 self
130 }
131
132 #[cfg(feature = "skill")]
136 pub fn with_list_skills_tool_factory(mut self, factory: ListSkillsToolFactory) -> Self {
137 self.list_skills_tool_factory = Some(factory);
138 self
139 }
140
141 pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
142 let prompt = prompt.into();
143 self.inner = self.inner.system_prompt(prompt.clone());
144 self.system_prompt = Some(prompt);
145 self
146 }
147
148 pub fn enable_thought(self, enable: bool) -> Self {
149 Self {
150 inner: self.inner.enable_thought(enable),
151 ..self
152 }
153 }
154
155 pub fn reasoning(self, config: agent_base::ReasoningConfig) -> Self {
156 Self {
157 inner: self.inner.reasoning(config),
158 ..self
159 }
160 }
161
162 pub fn enable_thinking(self, enable: bool) -> Self {
163 Self {
164 inner: self.inner.enable_thinking(enable),
165 ..self
166 }
167 }
168
169 pub fn thinking_budget(self, budget: u64) -> Self {
170 Self {
171 inner: self.inner.thinking_budget(budget),
172 ..self
173 }
174 }
175
176 pub fn tool_timeout(self, timeout_ms: u64) -> Self {
177 Self {
178 inner: self.inner.tool_timeout(timeout_ms),
179 ..self
180 }
181 }
182
183 pub fn max_tool_output_chars(self, max_chars: usize) -> Self {
184 Self {
185 inner: self.inner.max_tool_output_chars(max_chars),
186 ..self
187 }
188 }
189
190 pub fn max_sessions(self, max: usize) -> Self {
191 Self {
192 inner: self.inner.max_sessions(max),
193 ..self
194 }
195 }
196
197 pub fn max_turns_per_session(self, max: usize) -> Self {
198 Self {
199 inner: self.inner.max_turns_per_session(max),
200 ..self
201 }
202 }
203
204 pub fn execution_max_turns(self, max: u32) -> Self {
205 Self {
206 inner: self.inner.execution_max_turns(max),
207 ..self
208 }
209 }
210
211 pub fn max_message_tokens(self, max: usize) -> Self {
212 Self {
213 inner: self.inner.max_message_tokens(max),
214 ..self
215 }
216 }
217
218 pub fn register_tool(mut self, tool: impl Tool + 'static) -> Self {
219 let tool_arc: Arc<dyn Tool> = Arc::new(tool);
220 self.tool_names.insert(tool_arc.name().to_string());
221 self.business_tools.push(tool_arc.clone());
222 self.inner = self.inner.register_tool_arc(tool_arc);
223 self
224 }
225
226 pub fn register_tool_arc(mut self, tool: Arc<dyn Tool>) -> Self {
227 self.tool_names.insert(tool.name().to_string());
228 self.business_tools.push(tool.clone());
229 self.inner = self.inner.register_tool_arc(tool);
230 self
231 }
232
233 pub fn approval_handler(self, handler: Arc<dyn agent_base::ApprovalHandler>) -> Self {
234 Self {
235 inner: self.inner.approval_handler(handler),
236 ..self
237 }
238 }
239
240 pub fn tool_policy(self, policy: Arc<dyn agent_base::ToolPolicy>) -> Self {
241 Self {
242 inner: self.inner.tool_policy(policy),
243 ..self
244 }
245 }
246
247 pub fn middleware(self, mw: impl agent_base::Middleware + 'static) -> Self {
248 Self {
249 inner: self.inner.middleware(mw),
250 ..self
251 }
252 }
253
254 pub fn guard(self, guard: impl agent_base::ReactLoopGuard + 'static) -> Self {
255 Self {
256 inner: self.inner.guard(guard),
257 ..self
258 }
259 }
260
261 pub fn context_window(self, max_tokens: usize) -> Self {
262 Self {
263 inner: self.inner.context_window(max_tokens),
264 ..self
265 }
266 }
267
268 pub fn context_window_manager(self, manager: agent_base::ContextWindowManager) -> Self {
269 Self {
270 inner: self.inner.context_window_manager(manager),
271 ..self
272 }
273 }
274
275 pub fn response_format(self, format: agent_base::ResponseFormat) -> Self {
276 Self {
277 inner: self.inner.response_format(format),
278 ..self
279 }
280 }
281
282 pub fn llm_retry(self, retry: agent_base::RetryConfig) -> Self {
283 Self {
284 inner: self.inner.llm_retry(retry),
285 ..self
286 }
287 }
288
289 pub fn session_store(self, store: Arc<dyn agent_base::SessionStore>) -> Self {
290 Self {
291 inner: self.inner.session_store(store),
292 ..self
293 }
294 }
295
296 pub fn error_recovery(mut self, recovery: Arc<dyn agent_base::ToolErrorRecovery>) -> Self {
297 self.error_recovery = Some(recovery.clone());
298 self.inner = self.inner.error_recovery(recovery);
299 self
300 }
301
302 pub fn tool_error_retry_prompt(self, prompt: impl Into<String>) -> Self {
303 Self {
304 inner: self.inner.tool_error_retry_prompt(prompt),
305 ..self
306 }
307 }
308
309 pub fn language(mut self, language: agent_base::Language) -> Self {
310 self.language = Some(language.clone());
311 self.inner = self.inner.language(language);
312 self
313 }
314
315 pub fn event_bus_capacity(self, capacity: usize) -> Self {
316 Self {
317 inner: self.inner.event_bus_capacity(capacity),
318 ..self
319 }
320 }
321
322 pub fn session_id_generator(
323 self,
324 generator: Arc<dyn agent_base::types::SessionIdGenerator>,
325 ) -> Self {
326 Self {
327 inner: self.inner.session_id_generator(generator),
328 ..self
329 }
330 }
331
332 pub fn apply_if<T>(self, value: Option<T>, f: impl FnOnce(Self, T) -> Self) -> Self {
340 match value {
341 Some(v) => f(self, v),
342 None => self,
343 }
344 }
345
346 #[cfg(feature = "skill")]
347 pub fn register_skill(mut self, skill: impl Skill + 'static) -> Self {
348 self.skills.push(Arc::new(skill));
349 self
350 }
351
352 #[cfg(feature = "skill")]
353 pub fn register_skills(mut self, skills: Vec<Arc<dyn Skill>>) -> Self {
354 self.skills.extend(skills);
355 self
356 }
357
358 #[cfg(feature = "skill")]
359 pub fn skill_prompter(mut self, prompter: Arc<dyn SkillPrompter>) -> Self {
360 self.skill_prompter = Some(prompter);
361 self
362 }
363
364 #[cfg(feature = "skill")]
365 pub fn disable_skill_prompt_injection(mut self) -> Self {
366 self.disable_skill_prompt_injection = true;
367 self
368 }
369
370 #[cfg(feature = "skill")]
371 pub fn skill_detail_tool_name(mut self, name: impl Into<String>) -> Self {
372 self.skill_detail_tool_name = name.into();
373 self
374 }
375
376 pub fn build(self) -> AgentResult<AgentRuntime> {
379 #[cfg(feature = "skill")]
380 {
381 self.build_with_skills()
382 }
383 #[cfg(not(feature = "skill"))]
384 {
385 self.build_inner()
386 }
387 }
388
389 #[allow(dead_code, unused_mut)]
390 fn build_inner(mut self) -> AgentResult<AgentRuntime> {
391 if self.inner.get_guard().is_none() {
394 self.inner = self.inner.guard(crate::guard::DefaultGuard::with_llm_client(
395 crate::guard::DefaultGuardConfig::default(),
396 self.client.clone(),
397 ));
398 }
399
400 #[cfg(feature = "multi_agent")]
401 let lang = self.language.clone().unwrap_or_default();
402 #[cfg(feature = "multi_agent")]
403 let business_tools = std::mem::take(&mut self.business_tools);
404 #[cfg(feature = "multi_agent")]
405 let error_recovery = self.error_recovery.clone();
406 #[cfg(feature = "multi_agent")]
407 let tool_names = self.tool_names.clone();
408
409 #[cfg(feature = "multi_agent")]
410 let ma_config = self.multi_agent_config.clone();
411 #[cfg(feature = "multi_agent")]
412 let ma_tool_factory = self.multi_agent_tool_factory.take();
413
414 #[cfg(feature = "multi_agent")]
416 if ma_config.as_ref().map(|c| c.enabled).unwrap_or(false) {
417 let ma_prompt = build_multi_agent_system_prompt();
418 let new_prompt = match self.system_prompt.take() {
419 Some(existing) => format!("{}\n\n---\n\n{}", existing, ma_prompt),
420 None => ma_prompt,
421 };
422 self.inner = self.inner.system_prompt(new_prompt);
423 }
424
425 let runtime = self.inner.build()?;
426
427 #[cfg(feature = "multi_agent")]
429 if let Some(config) = ma_config
430 && config.enabled
431 {
432 setup_multi_agent(
433 &runtime,
434 config,
435 lang,
436 business_tools,
437 error_recovery,
438 &tool_names,
439 ma_tool_factory,
440 )?;
441 }
442
443 Ok(runtime)
444 }
445
446 #[cfg(feature = "skill")]
458 fn build_with_skills(mut self) -> AgentResult<AgentRuntime> {
459 if self.inner.get_guard().is_none() {
462 self.inner = self.inner.guard(crate::guard::DefaultGuard::with_llm_client(
463 crate::guard::DefaultGuardConfig::default(),
464 self.client.clone(),
465 ));
466 }
467
468 let mut ab = self.inner;
469 #[cfg(feature = "multi_agent")]
470 let lang = self.language.clone().unwrap_or_default();
471 #[cfg(feature = "multi_agent")]
472 let business_tools = std::mem::take(&mut self.business_tools);
473 #[cfg(feature = "multi_agent")]
474 let error_recovery = self.error_recovery.clone();
475 #[cfg(feature = "multi_agent")]
476 let tool_names = self.tool_names.clone();
477
478 #[cfg(feature = "multi_agent")]
479 let ma_config = self.multi_agent_config.clone();
480 #[cfg(feature = "multi_agent")]
481 let ma_tool_factory = self.multi_agent_tool_factory.take();
482
483 if !self.skills.is_empty() {
485 let prompter: Arc<dyn SkillPrompter> = self
486 .skill_prompter
487 .take()
488 .unwrap_or_else(|| Arc::new(LazySkillPrompter::new()));
489
490 let mut skill_refs: Vec<Arc<dyn Skill>> = Vec::new();
491
492 for skill in self.skills {
493 for tool in skill.tools() {
494 let tool_name = tool.name().to_string();
495 if self.tool_names.contains(&tool_name) {
496 return Err(agent_base::AgentError::internal(format!(
497 "Tool name conflict: `{}` (Skill `{}`)",
498 tool_name,
499 skill.name()
500 )));
501 }
502 self.tool_names.insert(tool_name);
503 ab = ab.register_tool_arc(tool);
504 }
505 skill_refs.push(skill);
506 }
507
508 if !self.disable_skill_prompt_injection {
509 let skill_prompt = prompter.build_prompt(&skill_refs, &self.skill_detail_tool_name);
510 if !skill_prompt.is_empty() {
511 let new_prompt = match self.system_prompt.take() {
512 Some(existing) => format!("{}\n\n---\n\n{}", existing, skill_prompt),
513 None => skill_prompt,
514 };
515 self.system_prompt = Some(new_prompt.clone());
516 ab = ab.system_prompt(new_prompt);
517 }
518 }
519
520 if let Some(factory) = self.skill_detail_tool_factory.take() {
523 let detail_tool = factory(skill_refs.clone(), self.skill_detail_tool_name);
524 ab = ab.register_tool_arc(detail_tool);
525 }
526
527 if let Some(factory) = self.list_skills_tool_factory.take() {
529 let registry = Arc::new(crate::skill::SkillRegistry::new());
530 for skill in &skill_refs {
531 tokio::task::block_in_place(|| {
532 tokio::runtime::Handle::current().block_on(async {
533 registry.register(skill.clone()).await;
534 })
535 });
536 }
537 let list_tool = factory(registry);
538 ab = ab.register_tool_arc(list_tool);
539 }
540 }
541
542 #[cfg(feature = "multi_agent")]
544 if ma_config.as_ref().map(|c| c.enabled).unwrap_or(false) {
545 let ma_prompt = build_multi_agent_system_prompt();
546 let new_prompt = match self.system_prompt.take() {
547 Some(existing) => format!("{}\n\n---\n\n{}", existing, ma_prompt),
548 None => ma_prompt,
549 };
550 ab = ab.system_prompt(new_prompt);
551 }
552
553 let runtime = ab.build()?;
554
555 #[cfg(feature = "multi_agent")]
557 if let Some(config) = ma_config
558 && config.enabled
559 {
560 setup_multi_agent(
561 &runtime,
562 config,
563 lang,
564 business_tools,
565 error_recovery,
566 &tool_names,
567 ma_tool_factory,
568 )?;
569 }
570
571 Ok(runtime)
572 }
573}
574
575#[cfg(feature = "multi_agent")]
586pub fn setup_multi_agent(
587 runtime: &AgentRuntime,
588 config: MultiAgentConfig,
589 lang: agent_base::Language,
590 business_tools: Vec<Arc<dyn Tool>>,
591 error_recovery: Option<Arc<dyn agent_base::ToolErrorRecovery>>,
592 existing_tool_names: &HashSet<String>,
593 tool_factory: Option<MultiAgentToolFactory>,
594) -> AgentResult<Arc<MultiAgentRuntime>> {
595 let client = runtime.client();
596 let cancel_token = runtime.cancel_token();
597 let tool_policy = runtime.tool_policy().cloned();
598 let approval_handler = runtime.approval_handler().cloned();
599
600 let ma_runtime = Arc::new(MultiAgentRuntime::new(
601 config.clone(),
602 client,
603 business_tools,
604 cancel_token,
605 error_recovery,
606 lang,
607 tool_policy,
608 approval_handler,
609 ));
610
611 ma_runtime.set_session_manager(Arc::new(runtime.session_manager().clone()));
613
614 let (event_tx, mut event_rx) =
616 tokio::sync::mpsc::unbounded_channel::<agent_base::RuntimeEvent>();
617 ma_runtime.set_event_sender(event_tx);
618 let parent_runtime = runtime.clone();
619 tokio::spawn(async move {
620 while let Some(event) = event_rx.recv().await {
621 parent_runtime.emit_event(event);
622 }
623 });
624
625 if let Some(factory) = tool_factory {
627 let tools = factory(ma_runtime.clone());
628 let registry = runtime.tools_mut();
629 let mut reg = tokio::task::block_in_place(|| registry.blocking_write());
630 for tool in tools {
631 let tool_name = tool.name().to_string();
632 if !existing_tool_names.contains(&tool_name) {
633 reg.register_arc(tool);
634 }
635 }
636 drop(reg);
637 }
638
639 Ok(ma_runtime)
640}
641
642#[cfg(feature = "multi_agent")]
644pub fn build_multi_agent_system_prompt() -> String {
645 r#"## Multi-Agent Capabilities
646
647You have the ability to spawn sub-agents to execute tasks concurrently. Use these tools to delegate work:
648
649- `spawn_agent`: Create a new sub-agent with a specific role. The agent runs independently.
650- `send_message`: Send a message to a sub-agent without triggering execution.
651- `followup_task`: Assign a task to a sub-agent and trigger its execution. Returns immediately.
652- `wait_agent`: Wait for a sub-agent's result. Blocks until the agent completes or timeout.
653- `list_agents`: List all active sub-agents and their status.
654- `close_agent`: Close a sub-agent and release its resources.
655
656### When to Spawn
657
658- Tasks that can run independently and in parallel (e.g., "research X and Y simultaneously")
659- Long-running tasks where you want to check intermediate results
660- Decomposing complex tasks into sub-tasks for focused execution
661
662### When NOT to Spawn
663
664- Simple lookups or single-tool calls (just use the tool directly)
665- Sequential dependencies where the next step requires the previous result
666- Tasks that need your full context or reasoning
667
668### Communication Pattern
669
6701. `spawn_agent` → create the sub-agent
6712. `followup_task` → assign work (can call multiple times)
6723. `wait_agent` → collect results
6734. `close_agent` → clean up when done"#
674 .to_string()
675}
676
677pub fn build_memory_system_prompt() -> String {
686 r#"## Memory
687
688You have a persistent file-based memory at `.phi/memory/`. Use `read_file` and `write_file` to manage it — there are no dedicated memory tools.
689
690### How Memory Works
691
692- `MEMORY.md` is the index — it lists all memories with one-line descriptions. Read it first when you need to recall something.
693- Each memory is a separate `.md` file with YAML frontmatter:
694 ```yaml
695 ---
696 name: <short-kebab-case-slug>
697 description: <one-line summary — used to decide relevance during recall>
698 metadata:
699 node_type: memory
700 type: user | feedback | project | reference
701 ---
702
703 <the fact or instruction>
704 ```
705- 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.
706- Link related memories with `[[memory-name]]` in the body.
707- `user` type = who the user is (role, expertise, preferences).
708- `feedback` type = guidance the user has given on how you should work.
709- `project` type = ongoing work, goals, or constraints.
710- `reference` type = pointers to external resources (URLs, dashboards, tickets).
711
712### When to Use Memory
713
714- The user explicitly asks you to remember something ("remember this", "save that")
715- You learn something important about the user's preferences or workflow
716- After completing a significant task, save context that would help in future sessions
717- The user gives you feedback on how to work — save it as `feedback` type
718
719### When NOT to Use Memory
720
721- For transient information that won't be useful beyond this session
722- For facts already recorded in the codebase (code structure, git history, config files)
723- For items that only matter to the current conversation
724
725### Pro Tips
726
727- When creating your first memory of a new type, you can read template files for format reference (check `.phi/templates/memory/` if available).
728- Keep the MEMORY.md index concise — it's loaded into context every session.
729- Before writing a new memory, check if an existing file already covers it — update instead of duplicating.
730
731### Workflow
732
733**To recall:** read `MEMORY.md` → find relevant entries by description → read the specific `.md` files you need.
734**To remember:** create a new `.md` file with proper frontmatter → update `MEMORY.md` with a new entry.
735**To update:** edit the existing `.md` file (don't create a duplicate).
736**To forget:** delete the `.md` file → remove its entry from `MEMORY.md`."#
737 .to_string()
738}
739
740#[cfg(test)]
741mod tests {
742 use super::*;
743 use agent_base::{Content, LlmClient};
744 use std::pin::Pin;
745
746 struct StubClient;
749
750 #[async_trait::async_trait]
751 impl LlmClient for StubClient {
752 async fn chat(
753 &self,
754 _messages: &[agent_base::ChatMessage],
755 _tools: &[serde_json::Value],
756 _reasoning: Option<&agent_base::ReasoningConfig>,
757 _response_format: Option<&agent_base::ResponseFormat>,
758 ) -> AgentResult<serde_json::Value> {
759 Ok(serde_json::json!({"choices": [{"message": {"content": "ok"}}]}))
760 }
761
762 async fn chat_stream(
763 &self,
764 _messages: &[agent_base::ChatMessage],
765 _tools: &[serde_json::Value],
766 _reasoning: Option<&agent_base::ReasoningConfig>,
767 _response_format: Option<&agent_base::ResponseFormat>,
768 ) -> AgentResult<
769 Pin<Box<dyn futures_core::Stream<Item = AgentResult<agent_base::StreamChunk>> + Send>>,
770 > {
771 let chunks: Vec<AgentResult<agent_base::StreamChunk>> = vec![
772 Ok(agent_base::StreamChunk::Text("ok".to_string())),
773 Ok(agent_base::StreamChunk::Stop {
774 finish_reason: Some("stop".to_string()),
775 }),
776 ];
777 Ok(Box::pin(futures_util::stream::iter(chunks)))
778 }
779
780 fn capabilities(&self) -> agent_base::LlmCapabilities {
781 agent_base::LlmCapabilities {
782 supports_streaming: true,
783 supports_tools: true,
784 supports_vision: false,
785 supports_thinking: false,
786 max_context_tokens: None,
787 max_output_tokens: None,
788 }
789 }
790 }
791
792 fn make_client() -> Arc<dyn StreamClient> {
793 agent_base::llm::adapt(Arc::new(StubClient))
794 }
795
796 #[cfg(feature = "multi_agent")]
799 #[tokio::test(flavor = "multi_thread")]
800 async fn test_setup_multi_agent_without_factory_registers_no_tools() {
801 let client = make_client();
802 let runtime = agent_base::AgentBuilder::new(client.clone())
803 .build()
804 .unwrap();
805 let config = MultiAgentConfig::enabled();
806
807 let result = setup_multi_agent(
808 &runtime,
809 config,
810 agent_base::Language::En,
811 vec![],
812 None,
813 &HashSet::new(),
814 None, );
816 assert!(result.is_ok());
817 let ma_runtime = result.unwrap();
818 let agents = ma_runtime.list_agents();
820 assert!(agents.is_empty());
821 }
822
823 #[cfg(feature = "multi_agent")]
824 #[tokio::test(flavor = "multi_thread")]
825 async fn test_setup_multi_agent_with_factory_registers_tools() {
826 let client = make_client();
827 let runtime = agent_base::AgentBuilder::new(client.clone())
828 .build()
829 .unwrap();
830 let config = MultiAgentConfig::enabled();
831
832 let factory: MultiAgentToolFactory = Arc::new(|_rt| {
833 struct FakeTool;
835 #[async_trait::async_trait]
836 impl Tool for FakeTool {
837 fn name(&self) -> &'static str {
838 "fake_tool"
839 }
840 fn description(&self) -> &'static str {
841 ""
842 }
843 fn schema(&self) -> serde_json::Value {
844 serde_json::json!({})
845 }
846 async fn call(
847 &self,
848 _args: &serde_json::Value,
849 _ctx: &agent_base::ToolContext,
850 ) -> AgentResult<Vec<Content>> {
851 Ok(vec![Content::text("ok")])
852 }
853 }
854 vec![Arc::new(FakeTool)]
855 });
856
857 let result = setup_multi_agent(
858 &runtime,
859 config,
860 agent_base::Language::En,
861 vec![],
862 None,
863 &HashSet::new(),
864 Some(factory),
865 );
866 assert!(result.is_ok());
867
868 let tools: Vec<String> = tokio::task::block_in_place(|| {
870 let tools = runtime.tools_mut();
871 let guard = tools.blocking_read();
872 guard.metadatas().into_iter().map(|m| m.name).collect()
873 });
874 assert!(tools.contains(&"fake_tool".to_string()));
875 }
876
877 #[cfg(feature = "multi_agent")]
878 #[tokio::test(flavor = "multi_thread")]
879 async fn test_setup_multi_agent_skips_duplicate_tool_names() {
880 let client = make_client();
881 let runtime = agent_base::AgentBuilder::new(client.clone())
882 .build()
883 .unwrap();
884
885 struct DupTool;
887 #[async_trait::async_trait]
888 impl Tool for DupTool {
889 fn name(&self) -> &'static str {
890 "dup_tool"
891 }
892 fn description(&self) -> &'static str {
893 ""
894 }
895 fn schema(&self) -> serde_json::Value {
896 serde_json::json!({})
897 }
898 async fn call(
899 &self,
900 _args: &serde_json::Value,
901 _ctx: &agent_base::ToolContext,
902 ) -> AgentResult<Vec<Content>> {
903 Ok(vec![Content::text("ok")])
904 }
905 }
906 {
907 let tools = runtime.tools_mut();
908 let mut reg = tokio::task::block_in_place(|| tools.blocking_write());
909 reg.register(DupTool);
910 }
911
912 let factory: MultiAgentToolFactory = Arc::new(|_rt| {
913 struct FakeTool;
914 #[async_trait::async_trait]
915 impl Tool for FakeTool {
916 fn name(&self) -> &'static str {
917 "dup_tool"
918 }
919 fn description(&self) -> &'static str {
920 ""
921 }
922 fn schema(&self) -> serde_json::Value {
923 serde_json::json!({})
924 }
925 async fn call(
926 &self,
927 _args: &serde_json::Value,
928 _ctx: &agent_base::ToolContext,
929 ) -> AgentResult<Vec<Content>> {
930 Ok(vec![Content::text("ok")])
931 }
932 }
933 vec![Arc::new(FakeTool)]
934 });
935
936 let mut existing = HashSet::new();
937 existing.insert("dup_tool".to_string());
938
939 let result = setup_multi_agent(
940 &runtime,
941 MultiAgentConfig::enabled(),
942 agent_base::Language::En,
943 vec![],
944 None,
945 &existing,
946 Some(factory),
947 );
948 assert!(result.is_ok());
949 let tools = tokio::task::block_in_place(|| {
951 let tools = runtime.tools_mut();
952 let guard = tools.blocking_read();
953 guard
954 .metadatas()
955 .into_iter()
956 .map(|m| m.name)
957 .collect::<Vec<String>>()
958 });
959 let count = tools.iter().filter(|n| n.as_str() == "dup_tool").count();
960 assert_eq!(count, 1);
961 }
962
963 #[cfg(feature = "multi_agent")]
966 #[tokio::test(flavor = "multi_thread")]
967 async fn test_builder_with_multi_agent_without_factory_builds_ok() {
968 let client = make_client();
969 let runtime = AgentBuilder::new(client)
970 .with_multi_agent(MultiAgentConfig::enabled())
971 .build()
972 .unwrap();
973 let tools = tokio::task::block_in_place(|| {
975 let tools = runtime.tools_mut();
976 let guard = tools.blocking_read();
977 guard
978 .metadatas()
979 .into_iter()
980 .map(|m| m.name)
981 .collect::<Vec<String>>()
982 });
983 assert!(!tools.contains(&"spawn_agent".to_string()));
985 }
986
987 #[cfg(feature = "multi_agent")]
988 #[tokio::test(flavor = "multi_thread")]
989 async fn test_builder_with_factory_registers_tools() {
990 let client = make_client();
991 let factory: MultiAgentToolFactory = Arc::new(|_rt| {
993 struct TestTool;
994 #[async_trait::async_trait]
995 impl Tool for TestTool {
996 fn name(&self) -> &'static str {
997 "factory_test_tool"
998 }
999 fn description(&self) -> &'static str {
1000 ""
1001 }
1002 fn schema(&self) -> serde_json::Value {
1003 serde_json::json!({})
1004 }
1005 async fn call(
1006 &self,
1007 _args: &serde_json::Value,
1008 _ctx: &agent_base::ToolContext,
1009 ) -> AgentResult<Vec<Content>> {
1010 Ok(vec![Content::text("ok")])
1011 }
1012 }
1013 vec![Arc::new(TestTool)]
1014 });
1015
1016 let runtime = AgentBuilder::new(client)
1017 .with_multi_agent(MultiAgentConfig::enabled())
1018 .with_multi_agent_tool_factory(factory)
1019 .build()
1020 .unwrap();
1021
1022 let tools = tokio::task::block_in_place(|| {
1023 let tools = runtime.tools_mut();
1024 let guard = tools.blocking_read();
1025 guard
1026 .metadatas()
1027 .into_iter()
1028 .map(|m| m.name)
1029 .collect::<Vec<String>>()
1030 });
1031 assert!(tools.contains(&"factory_test_tool".to_string()));
1032 }
1033
1034 #[cfg(feature = "multi_agent")]
1035 #[test]
1036 fn test_builder_disabled_multi_agent_skips_factory() {
1037 let client = make_client();
1038 let factory: MultiAgentToolFactory = Arc::new(|_rt| {
1039 panic!("factory should not be called when multi-agent is not configured");
1040 });
1041
1042 let runtime = AgentBuilder::new(client)
1043 .with_multi_agent_tool_factory(factory)
1044 .build()
1046 .unwrap();
1047
1048 let tools = tokio::task::block_in_place(|| {
1049 let tools = runtime.tools_mut();
1050 let guard = tools.blocking_read();
1051 guard
1052 .metadatas()
1053 .into_iter()
1054 .map(|m| m.name)
1055 .collect::<Vec<String>>()
1056 });
1057 assert!(!tools.contains(&"spawn_agent".to_string()));
1058 }
1059
1060 #[cfg(feature = "multi_agent")]
1063 #[test]
1064 fn test_system_prompt_contains_tool_names() {
1065 let prompt = build_multi_agent_system_prompt();
1066 assert!(prompt.contains("spawn_agent"));
1067 assert!(prompt.contains("send_message"));
1068 assert!(prompt.contains("followup_task"));
1069 assert!(prompt.contains("wait_agent"));
1070 assert!(prompt.contains("list_agents"));
1071 assert!(prompt.contains("close_agent"));
1072 }
1073
1074 #[cfg(feature = "multi_agent")]
1075 #[test]
1076 fn test_system_prompt_contains_guidance() {
1077 let prompt = build_multi_agent_system_prompt();
1078 assert!(prompt.contains("When to Spawn"));
1079 assert!(prompt.contains("When NOT to Spawn"));
1080 assert!(prompt.contains("Communication Pattern"));
1081 }
1082
1083 #[cfg(feature = "multi_agent")]
1086 #[tokio::test(flavor = "multi_thread")]
1087 async fn test_without_multi_agent_clears_config_and_factory() {
1088 let client = make_client();
1089
1090 let factory: MultiAgentToolFactory = Arc::new(|_rt| {
1092 panic!("factory should not be called when multi-agent is cleared");
1093 });
1094
1095 let runtime = AgentBuilder::new(client)
1096 .with_multi_agent(MultiAgentConfig::enabled())
1097 .with_multi_agent_tool_factory(factory)
1098 .without_multi_agent() .build()
1100 .unwrap();
1101
1102 let tools = tokio::task::block_in_place(|| {
1103 let tools = runtime.tools_mut();
1104 let guard = tools.blocking_read();
1105 guard
1106 .metadatas()
1107 .into_iter()
1108 .map(|m| m.name)
1109 .collect::<Vec<String>>()
1110 });
1111 assert!(!tools.contains(&"spawn_agent".to_string()));
1112 }
1113
1114 #[test]
1117 fn test_apply_if_some_applies_transformation() {
1118 let client = make_client();
1119 let builder = AgentBuilder::new(client)
1120 .apply_if(Some("custom prompt"), |b, prompt| b.system_prompt(prompt));
1121 assert!(builder.system_prompt.unwrap().contains("custom prompt"));
1123 }
1124
1125 #[test]
1126 fn test_apply_if_none_passes_through() {
1127 let client = make_client();
1128 let builder = AgentBuilder::new(client).apply_if(None as Option<&str>, |_b, _prompt| {
1129 panic!("should not be called when value is None");
1130 });
1131 assert!(builder.system_prompt.is_none());
1132 }
1133
1134 #[test]
1137 fn test_build_memory_system_prompt_non_empty() {
1138 let prompt = build_memory_system_prompt();
1139 assert!(!prompt.is_empty());
1140 assert!(prompt.contains("Memory"));
1141 assert!(prompt.contains("MEMORY.md"));
1142 assert!(prompt.contains("read_file"));
1143 assert!(prompt.contains("write_file"));
1144 }
1145
1146 struct NamedTool(&'static str);
1149
1150 #[async_trait::async_trait]
1151 impl Tool for NamedTool {
1152 fn name(&self) -> &'static str {
1153 self.0
1154 }
1155
1156 fn description(&self) -> &'static str {
1157 ""
1158 }
1159
1160 fn schema(&self) -> serde_json::Value {
1161 serde_json::json!({})
1162 }
1163
1164 async fn call(
1165 &self,
1166 _args: &serde_json::Value,
1167 _ctx: &agent_base::ToolContext,
1168 ) -> AgentResult<Vec<Content>> {
1169 Ok(vec![Content::text("ok")])
1170 }
1171 }
1172
1173 fn runtime_tool_names(runtime: &AgentRuntime) -> Vec<String> {
1174 tokio::task::block_in_place(|| {
1175 let tools = runtime.tools_mut();
1176 let guard = tools.blocking_read();
1177 guard.metadatas().into_iter().map(|m| m.name).collect()
1178 })
1179 }
1180
1181 #[tokio::test(flavor = "multi_thread")]
1182 async fn test_builder_scalar_passthrough_methods() {
1183 let client = make_client();
1184 let runtime = AgentBuilder::new(client)
1185 .enable_thought(true)
1186 .reasoning(agent_base::ReasoningConfig::default())
1187 .enable_thinking(false)
1188 .thinking_budget(1000)
1189 .tool_timeout(5000)
1190 .max_tool_output_chars(4000)
1191 .max_sessions(16)
1192 .max_turns_per_session(20)
1193 .execution_max_turns(10)
1194 .max_message_tokens(8000)
1195 .context_window(64_000)
1196 .context_window_manager(agent_base::ContextWindowManager::new(64_000))
1197 .response_format(agent_base::ResponseFormat::JsonObject)
1198 .llm_retry(agent_base::RetryConfig::default())
1199 .tool_error_retry_prompt("please retry")
1200 .language(agent_base::Language::En)
1201 .event_bus_capacity(256)
1202 .build()
1203 .unwrap();
1204
1205 assert!(runtime.client().capabilities().supports_streaming);
1206 }
1207
1208 #[tokio::test(flavor = "multi_thread")]
1209 async fn test_register_tool_variants() {
1210 let client = make_client();
1211 let runtime = AgentBuilder::new(client)
1212 .register_tool(NamedTool("tool_by_value"))
1213 .register_tool_arc(Arc::new(NamedTool("tool_by_arc")))
1214 .build()
1215 .unwrap();
1216
1217 let names = runtime_tool_names(&runtime);
1218 assert!(names.contains(&"tool_by_value".to_string()));
1219 assert!(names.contains(&"tool_by_arc".to_string()));
1220 }
1221
1222 #[cfg(feature = "skill")]
1223 mod skill_tests {
1224 use super::*;
1225 use crate::skill::Skill;
1226
1227 struct TestSkill;
1228
1229 impl Skill for TestSkill {
1230 fn name(&self) -> &'static str {
1231 "test_skill"
1232 }
1233
1234 fn brief_description(&self) -> String {
1235 "a test skill".to_string()
1236 }
1237
1238 fn detailed_description(&self) -> String {
1239 "detailed test skill".to_string()
1240 }
1241
1242 fn tools(&self) -> Vec<Arc<dyn Tool>> {
1243 vec![]
1244 }
1245 }
1246
1247 struct ToolSkill;
1248
1249 impl Skill for ToolSkill {
1250 fn name(&self) -> &'static str {
1251 "tool_skill"
1252 }
1253
1254 fn brief_description(&self) -> String {
1255 "skill with a tool".to_string()
1256 }
1257
1258 fn detailed_description(&self) -> String {
1259 "skill that provides a tool".to_string()
1260 }
1261
1262 fn tools(&self) -> Vec<Arc<dyn Tool>> {
1263 vec![Arc::new(NamedTool("skill_provided_tool"))]
1264 }
1265 }
1266
1267 #[tokio::test(flavor = "multi_thread")]
1268 async fn test_register_skill_builds_ok() {
1269 let client = make_client();
1270 let runtime = AgentBuilder::new(client)
1271 .register_skill(TestSkill)
1272 .build()
1273 .unwrap();
1274 assert!(runtime_tool_names(&runtime).is_empty());
1276 }
1277
1278 #[tokio::test(flavor = "multi_thread")]
1279 async fn test_register_skill_with_tool_registers_tool() {
1280 let client = make_client();
1281 let runtime = AgentBuilder::new(client)
1282 .register_skill(ToolSkill)
1283 .build()
1284 .unwrap();
1285 assert!(runtime_tool_names(&runtime).contains(&"skill_provided_tool".to_string()));
1286 }
1287
1288 #[tokio::test(flavor = "multi_thread")]
1289 async fn test_register_skill_tool_name_conflict() {
1290 let client = make_client();
1291 let result = AgentBuilder::new(client)
1292 .register_tool(NamedTool("skill_provided_tool"))
1293 .register_skill(ToolSkill)
1294 .build();
1295 let err = result.err().unwrap();
1296 assert!(format!("{err}").contains("Tool name conflict"));
1297 }
1298
1299 #[tokio::test(flavor = "multi_thread")]
1300 async fn test_skill_detail_tool_factory_registers_tool() {
1301 let client = make_client();
1302 let factory: SkillDetailToolFactory = Arc::new(|_skills, name| {
1303 assert_eq!(name, "get_skill_detail");
1304 Arc::new(NamedTool("detail_tool"))
1305 });
1306 let runtime = AgentBuilder::new(client)
1307 .register_skill(TestSkill)
1308 .with_skill_detail_tool_factory(factory)
1309 .build()
1310 .unwrap();
1311 assert!(runtime_tool_names(&runtime).contains(&"detail_tool".to_string()));
1312 }
1313
1314 #[tokio::test(flavor = "multi_thread")]
1315 async fn test_list_skills_tool_factory_registers_tool() {
1316 let client = make_client();
1317 let factory: ListSkillsToolFactory =
1318 Arc::new(|_registry| Arc::new(NamedTool("list_skills_tool")));
1319 let runtime = AgentBuilder::new(client)
1320 .register_skill(TestSkill)
1321 .with_list_skills_tool_factory(factory)
1322 .build()
1323 .unwrap();
1324 assert!(runtime_tool_names(&runtime).contains(&"list_skills_tool".to_string()));
1325 }
1326
1327 #[tokio::test(flavor = "multi_thread")]
1328 async fn test_disable_skill_prompt_injection_builds() {
1329 let client = make_client();
1330 let runtime = AgentBuilder::new(client)
1331 .register_skill(ToolSkill)
1332 .disable_skill_prompt_injection()
1333 .build()
1334 .unwrap();
1335 assert!(runtime_tool_names(&runtime).contains(&"skill_provided_tool".to_string()));
1337 }
1338 }
1339}