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