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
395 .inner
396 .guard(crate::guard::DefaultGuard::with_llm_client(
397 crate::guard::DefaultGuardConfig::default(),
398 self.client.clone(),
399 ));
400 }
401
402 #[cfg(feature = "multi_agent")]
403 let lang = self.language.clone().unwrap_or_default();
404 #[cfg(feature = "multi_agent")]
405 let business_tools = std::mem::take(&mut self.business_tools);
406 #[cfg(feature = "multi_agent")]
407 let error_recovery = self.error_recovery.clone();
408 #[cfg(feature = "multi_agent")]
409 let tool_names = self.tool_names.clone();
410
411 #[cfg(feature = "multi_agent")]
412 let ma_config = self.multi_agent_config.clone();
413 #[cfg(feature = "multi_agent")]
414 let ma_tool_factory = self.multi_agent_tool_factory.take();
415
416 #[cfg(feature = "multi_agent")]
418 if ma_config.as_ref().map(|c| c.enabled).unwrap_or(false) {
419 let ma_prompt = build_multi_agent_system_prompt();
420 let new_prompt = match self.system_prompt.take() {
421 Some(existing) => format!("{}\n\n---\n\n{}", existing, ma_prompt),
422 None => ma_prompt,
423 };
424 self.inner = self.inner.system_prompt(new_prompt);
425 }
426
427 let runtime = self.inner.build()?;
428
429 #[cfg(feature = "multi_agent")]
431 if let Some(config) = ma_config
432 && config.enabled
433 {
434 setup_multi_agent(
435 &runtime,
436 config,
437 lang,
438 business_tools,
439 error_recovery,
440 &tool_names,
441 ma_tool_factory,
442 )?;
443 }
444
445 Ok(runtime)
446 }
447
448 #[cfg(feature = "skill")]
460 fn build_with_skills(mut self) -> AgentResult<AgentRuntime> {
461 if self.inner.get_guard().is_none() {
464 self.inner = self
465 .inner
466 .guard(crate::guard::DefaultGuard::with_llm_client(
467 crate::guard::DefaultGuardConfig::default(),
468 self.client.clone(),
469 ));
470 }
471
472 let mut ab = self.inner;
473 #[cfg(feature = "multi_agent")]
474 let lang = self.language.clone().unwrap_or_default();
475 #[cfg(feature = "multi_agent")]
476 let business_tools = std::mem::take(&mut self.business_tools);
477 #[cfg(feature = "multi_agent")]
478 let error_recovery = self.error_recovery.clone();
479 #[cfg(feature = "multi_agent")]
480 let tool_names = self.tool_names.clone();
481
482 #[cfg(feature = "multi_agent")]
483 let ma_config = self.multi_agent_config.clone();
484 #[cfg(feature = "multi_agent")]
485 let ma_tool_factory = self.multi_agent_tool_factory.take();
486
487 if !self.skills.is_empty() {
489 let prompter: Arc<dyn SkillPrompter> = self
490 .skill_prompter
491 .take()
492 .unwrap_or_else(|| Arc::new(LazySkillPrompter::new()));
493
494 let mut skill_refs: Vec<Arc<dyn Skill>> = Vec::new();
495
496 for skill in self.skills {
497 for tool in skill.tools() {
498 let tool_name = tool.name().to_string();
499 if self.tool_names.contains(&tool_name) {
500 return Err(agent_base::AgentError::internal(format!(
501 "Tool name conflict: `{}` (Skill `{}`)",
502 tool_name,
503 skill.name()
504 )));
505 }
506 self.tool_names.insert(tool_name);
507 ab = ab.register_tool_arc(tool);
508 }
509 skill_refs.push(skill);
510 }
511
512 if !self.disable_skill_prompt_injection {
513 let skill_prompt = prompter.build_prompt(&skill_refs, &self.skill_detail_tool_name);
514 if !skill_prompt.is_empty() {
515 let new_prompt = match self.system_prompt.take() {
516 Some(existing) => format!("{}\n\n---\n\n{}", existing, skill_prompt),
517 None => skill_prompt,
518 };
519 self.system_prompt = Some(new_prompt.clone());
520 ab = ab.system_prompt(new_prompt);
521 }
522 }
523
524 if let Some(factory) = self.skill_detail_tool_factory.take() {
527 let detail_tool = factory(skill_refs.clone(), self.skill_detail_tool_name);
528 ab = ab.register_tool_arc(detail_tool);
529 }
530
531 if let Some(factory) = self.list_skills_tool_factory.take() {
533 let registry = Arc::new(crate::skill::SkillRegistry::new());
534 for skill in &skill_refs {
535 tokio::task::block_in_place(|| {
536 tokio::runtime::Handle::current().block_on(async {
537 registry.register(skill.clone()).await;
538 })
539 });
540 }
541 let list_tool = factory(registry);
542 ab = ab.register_tool_arc(list_tool);
543 }
544 }
545
546 #[cfg(feature = "multi_agent")]
548 if ma_config.as_ref().map(|c| c.enabled).unwrap_or(false) {
549 let ma_prompt = build_multi_agent_system_prompt();
550 let new_prompt = match self.system_prompt.take() {
551 Some(existing) => format!("{}\n\n---\n\n{}", existing, ma_prompt),
552 None => ma_prompt,
553 };
554 ab = ab.system_prompt(new_prompt);
555 }
556
557 let runtime = ab.build()?;
558
559 #[cfg(feature = "multi_agent")]
561 if let Some(config) = ma_config
562 && config.enabled
563 {
564 setup_multi_agent(
565 &runtime,
566 config,
567 lang,
568 business_tools,
569 error_recovery,
570 &tool_names,
571 ma_tool_factory,
572 )?;
573 }
574
575 Ok(runtime)
576 }
577}
578
579#[cfg(feature = "multi_agent")]
590pub fn setup_multi_agent(
591 runtime: &AgentRuntime,
592 config: MultiAgentConfig,
593 lang: agent_base::Language,
594 business_tools: Vec<Arc<dyn Tool>>,
595 error_recovery: Option<Arc<dyn agent_base::ToolErrorRecovery>>,
596 existing_tool_names: &HashSet<String>,
597 tool_factory: Option<MultiAgentToolFactory>,
598) -> AgentResult<Arc<MultiAgentRuntime>> {
599 let client = runtime.client();
600 let cancel_token = runtime.cancel_token();
601 let tool_policy = runtime.tool_policy().cloned();
602 let approval_handler = runtime.approval_handler().cloned();
603
604 let ma_runtime = Arc::new(MultiAgentRuntime::new(
605 config.clone(),
606 client,
607 business_tools,
608 cancel_token,
609 error_recovery,
610 lang,
611 tool_policy,
612 approval_handler,
613 ));
614
615 ma_runtime.set_session_manager(Arc::new(runtime.session_manager().clone()));
617
618 let (event_tx, mut event_rx) =
620 tokio::sync::mpsc::unbounded_channel::<agent_base::RuntimeEvent>();
621 ma_runtime.set_event_sender(event_tx);
622 let parent_runtime = runtime.clone();
623 tokio::spawn(async move {
624 while let Some(event) = event_rx.recv().await {
625 parent_runtime.emit_event(event);
626 }
627 });
628
629 if let Some(factory) = tool_factory {
631 let tools = factory(ma_runtime.clone());
632 let registry = runtime.tools_mut();
633 let mut reg = tokio::task::block_in_place(|| registry.blocking_write());
634 for tool in tools {
635 let tool_name = tool.name().to_string();
636 if !existing_tool_names.contains(&tool_name) {
637 reg.register_arc(tool);
638 }
639 }
640 drop(reg);
641 }
642
643 Ok(ma_runtime)
644}
645
646#[cfg(feature = "multi_agent")]
648pub fn build_multi_agent_system_prompt() -> String {
649 r#"## Multi-Agent Capabilities
650
651You have the ability to spawn sub-agents to execute tasks concurrently. Use these tools to delegate work:
652
653- `spawn_agent`: Create a new sub-agent with a specific role. The agent runs independently.
654- `send_message`: Send a message to a sub-agent without triggering execution.
655- `followup_task`: Assign a task to a sub-agent and trigger its execution. Returns immediately.
656- `wait_agent`: Wait for a sub-agent's result. Blocks until the agent completes or timeout.
657- `list_agents`: List all active sub-agents and their status.
658- `close_agent`: Close a sub-agent and release its resources.
659
660### When to Spawn
661
662- Tasks that can run independently and in parallel (e.g., "research X and Y simultaneously")
663- Long-running tasks where you want to check intermediate results
664- Decomposing complex tasks into sub-tasks for focused execution
665
666### When NOT to Spawn
667
668- Simple lookups or single-tool calls (just use the tool directly)
669- Sequential dependencies where the next step requires the previous result
670- Tasks that need your full context or reasoning
671
672### Communication Pattern
673
6741. `spawn_agent` → create the sub-agent
6752. `followup_task` → assign work (can call multiple times)
6763. `wait_agent` → collect results
6774. `close_agent` → clean up when done"#
678 .to_string()
679}
680
681pub fn build_memory_system_prompt() -> String {
690 r#"## Memory
691
692You have a persistent file-based memory at `.phi/memory/`. Use `read_file` and `write_file` to manage it — there are no dedicated memory tools.
693
694### How Memory Works
695
696- `MEMORY.md` is the index — it lists all memories with one-line descriptions. Read it first when you need to recall something.
697- Each memory is a separate `.md` file with YAML frontmatter:
698 ```yaml
699 ---
700 name: <short-kebab-case-slug>
701 description: <one-line summary — used to decide relevance during recall>
702 metadata:
703 node_type: memory
704 type: user | feedback | project | reference
705 ---
706
707 <the fact or instruction>
708 ```
709- 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.
710- Link related memories with `[[memory-name]]` in the body.
711- `user` type = who the user is (role, expertise, preferences).
712- `feedback` type = guidance the user has given on how you should work.
713- `project` type = ongoing work, goals, or constraints.
714- `reference` type = pointers to external resources (URLs, dashboards, tickets).
715
716### When to Use Memory
717
718- The user explicitly asks you to remember something ("remember this", "save that")
719- You learn something important about the user's preferences or workflow
720- After completing a significant task, save context that would help in future sessions
721- The user gives you feedback on how to work — save it as `feedback` type
722
723### When NOT to Use Memory
724
725- For transient information that won't be useful beyond this session
726- For facts already recorded in the codebase (code structure, git history, config files)
727- For items that only matter to the current conversation
728
729### Pro Tips
730
731- When creating your first memory of a new type, you can read template files for format reference (check `.phi/templates/memory/` if available).
732- Keep the MEMORY.md index concise — it's loaded into context every session.
733- Before writing a new memory, check if an existing file already covers it — update instead of duplicating.
734
735### Workflow
736
737**To recall:** read `MEMORY.md` → find relevant entries by description → read the specific `.md` files you need.
738**To remember:** create a new `.md` file with proper frontmatter → update `MEMORY.md` with a new entry.
739**To update:** edit the existing `.md` file (don't create a duplicate).
740**To forget:** delete the `.md` file → remove its entry from `MEMORY.md`."#
741 .to_string()
742}
743
744#[cfg(test)]
745mod tests {
746 use super::*;
747 use agent_base::{Content, LlmClient};
748 use std::pin::Pin;
749
750 struct StubClient;
753
754 #[async_trait::async_trait]
755 impl LlmClient for StubClient {
756 async fn chat(
757 &self,
758 _messages: &[agent_base::ChatMessage],
759 _tools: &[serde_json::Value],
760 _reasoning: Option<&agent_base::ReasoningConfig>,
761 _response_format: Option<&agent_base::ResponseFormat>,
762 ) -> AgentResult<serde_json::Value> {
763 Ok(serde_json::json!({"choices": [{"message": {"content": "ok"}}]}))
764 }
765
766 async fn chat_stream(
767 &self,
768 _messages: &[agent_base::ChatMessage],
769 _tools: &[serde_json::Value],
770 _reasoning: Option<&agent_base::ReasoningConfig>,
771 _response_format: Option<&agent_base::ResponseFormat>,
772 ) -> AgentResult<
773 Pin<Box<dyn futures_core::Stream<Item = AgentResult<agent_base::StreamChunk>> + Send>>,
774 > {
775 let chunks: Vec<AgentResult<agent_base::StreamChunk>> = vec![
776 Ok(agent_base::StreamChunk::Text("ok".to_string())),
777 Ok(agent_base::StreamChunk::Stop {
778 finish_reason: Some("stop".to_string()),
779 }),
780 ];
781 Ok(Box::pin(futures_util::stream::iter(chunks)))
782 }
783
784 fn capabilities(&self) -> agent_base::LlmCapabilities {
785 agent_base::LlmCapabilities {
786 supports_streaming: true,
787 supports_tools: true,
788 supports_vision: false,
789 supports_thinking: false,
790 max_context_tokens: None,
791 max_output_tokens: None,
792 }
793 }
794 }
795
796 fn make_client() -> Arc<dyn StreamClient> {
797 agent_base::llm::adapt(Arc::new(StubClient))
798 }
799
800 #[cfg(feature = "multi_agent")]
803 #[tokio::test(flavor = "multi_thread")]
804 async fn test_setup_multi_agent_without_factory_registers_no_tools() {
805 let client = make_client();
806 let runtime = agent_base::AgentBuilder::new(client.clone())
807 .build()
808 .unwrap();
809 let config = MultiAgentConfig::enabled();
810
811 let result = setup_multi_agent(
812 &runtime,
813 config,
814 agent_base::Language::En,
815 vec![],
816 None,
817 &HashSet::new(),
818 None, );
820 assert!(result.is_ok());
821 let ma_runtime = result.unwrap();
822 let agents = ma_runtime.list_agents();
824 assert!(agents.is_empty());
825 }
826
827 #[cfg(feature = "multi_agent")]
828 #[tokio::test(flavor = "multi_thread")]
829 async fn test_setup_multi_agent_with_factory_registers_tools() {
830 let client = make_client();
831 let runtime = agent_base::AgentBuilder::new(client.clone())
832 .build()
833 .unwrap();
834 let config = MultiAgentConfig::enabled();
835
836 let factory: MultiAgentToolFactory = Arc::new(|_rt| {
837 struct FakeTool;
839 #[async_trait::async_trait]
840 impl Tool for FakeTool {
841 fn name(&self) -> &'static str {
842 "fake_tool"
843 }
844 fn description(&self) -> &'static str {
845 ""
846 }
847 fn schema(&self) -> serde_json::Value {
848 serde_json::json!({})
849 }
850 async fn call(
851 &self,
852 _args: &serde_json::Value,
853 _ctx: &agent_base::ToolContext,
854 ) -> AgentResult<Vec<Content>> {
855 Ok(vec![Content::text("ok")])
856 }
857 }
858 vec![Arc::new(FakeTool)]
859 });
860
861 let result = setup_multi_agent(
862 &runtime,
863 config,
864 agent_base::Language::En,
865 vec![],
866 None,
867 &HashSet::new(),
868 Some(factory),
869 );
870 assert!(result.is_ok());
871
872 let tools: Vec<String> = tokio::task::block_in_place(|| {
874 let tools = runtime.tools_mut();
875 let guard = tools.blocking_read();
876 guard.metadatas().into_iter().map(|m| m.name).collect()
877 });
878 assert!(tools.contains(&"fake_tool".to_string()));
879 }
880
881 #[cfg(feature = "multi_agent")]
882 #[tokio::test(flavor = "multi_thread")]
883 async fn test_setup_multi_agent_skips_duplicate_tool_names() {
884 let client = make_client();
885 let runtime = agent_base::AgentBuilder::new(client.clone())
886 .build()
887 .unwrap();
888
889 struct DupTool;
891 #[async_trait::async_trait]
892 impl Tool for DupTool {
893 fn name(&self) -> &'static str {
894 "dup_tool"
895 }
896 fn description(&self) -> &'static str {
897 ""
898 }
899 fn schema(&self) -> serde_json::Value {
900 serde_json::json!({})
901 }
902 async fn call(
903 &self,
904 _args: &serde_json::Value,
905 _ctx: &agent_base::ToolContext,
906 ) -> AgentResult<Vec<Content>> {
907 Ok(vec![Content::text("ok")])
908 }
909 }
910 {
911 let tools = runtime.tools_mut();
912 let mut reg = tokio::task::block_in_place(|| tools.blocking_write());
913 reg.register(DupTool);
914 }
915
916 let factory: MultiAgentToolFactory = Arc::new(|_rt| {
917 struct FakeTool;
918 #[async_trait::async_trait]
919 impl Tool for FakeTool {
920 fn name(&self) -> &'static str {
921 "dup_tool"
922 }
923 fn description(&self) -> &'static str {
924 ""
925 }
926 fn schema(&self) -> serde_json::Value {
927 serde_json::json!({})
928 }
929 async fn call(
930 &self,
931 _args: &serde_json::Value,
932 _ctx: &agent_base::ToolContext,
933 ) -> AgentResult<Vec<Content>> {
934 Ok(vec![Content::text("ok")])
935 }
936 }
937 vec![Arc::new(FakeTool)]
938 });
939
940 let mut existing = HashSet::new();
941 existing.insert("dup_tool".to_string());
942
943 let result = setup_multi_agent(
944 &runtime,
945 MultiAgentConfig::enabled(),
946 agent_base::Language::En,
947 vec![],
948 None,
949 &existing,
950 Some(factory),
951 );
952 assert!(result.is_ok());
953 let tools = tokio::task::block_in_place(|| {
955 let tools = runtime.tools_mut();
956 let guard = tools.blocking_read();
957 guard
958 .metadatas()
959 .into_iter()
960 .map(|m| m.name)
961 .collect::<Vec<String>>()
962 });
963 let count = tools.iter().filter(|n| n.as_str() == "dup_tool").count();
964 assert_eq!(count, 1);
965 }
966
967 #[cfg(feature = "multi_agent")]
970 #[tokio::test(flavor = "multi_thread")]
971 async fn test_builder_with_multi_agent_without_factory_builds_ok() {
972 let client = make_client();
973 let runtime = AgentBuilder::new(client)
974 .with_multi_agent(MultiAgentConfig::enabled())
975 .build()
976 .unwrap();
977 let tools = tokio::task::block_in_place(|| {
979 let tools = runtime.tools_mut();
980 let guard = tools.blocking_read();
981 guard
982 .metadatas()
983 .into_iter()
984 .map(|m| m.name)
985 .collect::<Vec<String>>()
986 });
987 assert!(!tools.contains(&"spawn_agent".to_string()));
989 }
990
991 #[cfg(feature = "multi_agent")]
992 #[tokio::test(flavor = "multi_thread")]
993 async fn test_builder_with_factory_registers_tools() {
994 let client = make_client();
995 let factory: MultiAgentToolFactory = Arc::new(|_rt| {
997 struct TestTool;
998 #[async_trait::async_trait]
999 impl Tool for TestTool {
1000 fn name(&self) -> &'static str {
1001 "factory_test_tool"
1002 }
1003 fn description(&self) -> &'static str {
1004 ""
1005 }
1006 fn schema(&self) -> serde_json::Value {
1007 serde_json::json!({})
1008 }
1009 async fn call(
1010 &self,
1011 _args: &serde_json::Value,
1012 _ctx: &agent_base::ToolContext,
1013 ) -> AgentResult<Vec<Content>> {
1014 Ok(vec![Content::text("ok")])
1015 }
1016 }
1017 vec![Arc::new(TestTool)]
1018 });
1019
1020 let runtime = AgentBuilder::new(client)
1021 .with_multi_agent(MultiAgentConfig::enabled())
1022 .with_multi_agent_tool_factory(factory)
1023 .build()
1024 .unwrap();
1025
1026 let tools = tokio::task::block_in_place(|| {
1027 let tools = runtime.tools_mut();
1028 let guard = tools.blocking_read();
1029 guard
1030 .metadatas()
1031 .into_iter()
1032 .map(|m| m.name)
1033 .collect::<Vec<String>>()
1034 });
1035 assert!(tools.contains(&"factory_test_tool".to_string()));
1036 }
1037
1038 #[cfg(feature = "multi_agent")]
1039 #[test]
1040 fn test_builder_disabled_multi_agent_skips_factory() {
1041 let client = make_client();
1042 let factory: MultiAgentToolFactory = Arc::new(|_rt| {
1043 panic!("factory should not be called when multi-agent is not configured");
1044 });
1045
1046 let runtime = AgentBuilder::new(client)
1047 .with_multi_agent_tool_factory(factory)
1048 .build()
1050 .unwrap();
1051
1052 let tools = tokio::task::block_in_place(|| {
1053 let tools = runtime.tools_mut();
1054 let guard = tools.blocking_read();
1055 guard
1056 .metadatas()
1057 .into_iter()
1058 .map(|m| m.name)
1059 .collect::<Vec<String>>()
1060 });
1061 assert!(!tools.contains(&"spawn_agent".to_string()));
1062 }
1063
1064 #[cfg(feature = "multi_agent")]
1067 #[test]
1068 fn test_system_prompt_contains_tool_names() {
1069 let prompt = build_multi_agent_system_prompt();
1070 assert!(prompt.contains("spawn_agent"));
1071 assert!(prompt.contains("send_message"));
1072 assert!(prompt.contains("followup_task"));
1073 assert!(prompt.contains("wait_agent"));
1074 assert!(prompt.contains("list_agents"));
1075 assert!(prompt.contains("close_agent"));
1076 }
1077
1078 #[cfg(feature = "multi_agent")]
1079 #[test]
1080 fn test_system_prompt_contains_guidance() {
1081 let prompt = build_multi_agent_system_prompt();
1082 assert!(prompt.contains("When to Spawn"));
1083 assert!(prompt.contains("When NOT to Spawn"));
1084 assert!(prompt.contains("Communication Pattern"));
1085 }
1086
1087 #[cfg(feature = "multi_agent")]
1090 #[tokio::test(flavor = "multi_thread")]
1091 async fn test_without_multi_agent_clears_config_and_factory() {
1092 let client = make_client();
1093
1094 let factory: MultiAgentToolFactory = Arc::new(|_rt| {
1096 panic!("factory should not be called when multi-agent is cleared");
1097 });
1098
1099 let runtime = AgentBuilder::new(client)
1100 .with_multi_agent(MultiAgentConfig::enabled())
1101 .with_multi_agent_tool_factory(factory)
1102 .without_multi_agent() .build()
1104 .unwrap();
1105
1106 let tools = tokio::task::block_in_place(|| {
1107 let tools = runtime.tools_mut();
1108 let guard = tools.blocking_read();
1109 guard
1110 .metadatas()
1111 .into_iter()
1112 .map(|m| m.name)
1113 .collect::<Vec<String>>()
1114 });
1115 assert!(!tools.contains(&"spawn_agent".to_string()));
1116 }
1117
1118 #[test]
1121 fn test_apply_if_some_applies_transformation() {
1122 let client = make_client();
1123 let builder = AgentBuilder::new(client)
1124 .apply_if(Some("custom prompt"), |b, prompt| b.system_prompt(prompt));
1125 assert!(builder.system_prompt.unwrap().contains("custom prompt"));
1127 }
1128
1129 #[test]
1130 fn test_apply_if_none_passes_through() {
1131 let client = make_client();
1132 let builder = AgentBuilder::new(client).apply_if(None as Option<&str>, |_b, _prompt| {
1133 panic!("should not be called when value is None");
1134 });
1135 assert!(builder.system_prompt.is_none());
1136 }
1137
1138 #[test]
1141 fn test_build_memory_system_prompt_non_empty() {
1142 let prompt = build_memory_system_prompt();
1143 assert!(!prompt.is_empty());
1144 assert!(prompt.contains("Memory"));
1145 assert!(prompt.contains("MEMORY.md"));
1146 assert!(prompt.contains("read_file"));
1147 assert!(prompt.contains("write_file"));
1148 }
1149
1150 struct NamedTool(&'static str);
1153
1154 #[async_trait::async_trait]
1155 impl Tool for NamedTool {
1156 fn name(&self) -> &'static str {
1157 self.0
1158 }
1159
1160 fn description(&self) -> &'static str {
1161 ""
1162 }
1163
1164 fn schema(&self) -> serde_json::Value {
1165 serde_json::json!({})
1166 }
1167
1168 async fn call(
1169 &self,
1170 _args: &serde_json::Value,
1171 _ctx: &agent_base::ToolContext,
1172 ) -> AgentResult<Vec<Content>> {
1173 Ok(vec![Content::text("ok")])
1174 }
1175 }
1176
1177 fn runtime_tool_names(runtime: &AgentRuntime) -> Vec<String> {
1178 tokio::task::block_in_place(|| {
1179 let tools = runtime.tools_mut();
1180 let guard = tools.blocking_read();
1181 guard.metadatas().into_iter().map(|m| m.name).collect()
1182 })
1183 }
1184
1185 #[tokio::test(flavor = "multi_thread")]
1186 async fn test_builder_scalar_passthrough_methods() {
1187 let client = make_client();
1188 let runtime = AgentBuilder::new(client)
1189 .enable_thought(true)
1190 .reasoning(agent_base::ReasoningConfig::default())
1191 .enable_thinking(false)
1192 .thinking_budget(1000)
1193 .tool_timeout(5000)
1194 .max_tool_output_chars(4000)
1195 .max_sessions(16)
1196 .max_turns_per_session(20)
1197 .execution_max_turns(10)
1198 .max_message_tokens(8000)
1199 .context_window(64_000)
1200 .context_window_manager(agent_base::ContextWindowManager::new(64_000))
1201 .response_format(agent_base::ResponseFormat::JsonObject)
1202 .llm_retry(agent_base::RetryConfig::default())
1203 .tool_error_retry_prompt("please retry")
1204 .language(agent_base::Language::En)
1205 .event_bus_capacity(256)
1206 .build()
1207 .unwrap();
1208
1209 assert!(runtime.client().capabilities().supports_streaming);
1210 }
1211
1212 #[tokio::test(flavor = "multi_thread")]
1213 async fn test_register_tool_variants() {
1214 let client = make_client();
1215 let runtime = AgentBuilder::new(client)
1216 .register_tool(NamedTool("tool_by_value"))
1217 .register_tool_arc(Arc::new(NamedTool("tool_by_arc")))
1218 .build()
1219 .unwrap();
1220
1221 let names = runtime_tool_names(&runtime);
1222 assert!(names.contains(&"tool_by_value".to_string()));
1223 assert!(names.contains(&"tool_by_arc".to_string()));
1224 }
1225
1226 #[cfg(feature = "skill")]
1227 mod skill_tests {
1228 use super::*;
1229 use crate::skill::Skill;
1230
1231 struct TestSkill;
1232
1233 impl Skill for TestSkill {
1234 fn name(&self) -> &'static str {
1235 "test_skill"
1236 }
1237
1238 fn brief_description(&self) -> String {
1239 "a test skill".to_string()
1240 }
1241
1242 fn detailed_description(&self) -> String {
1243 "detailed test skill".to_string()
1244 }
1245
1246 fn tools(&self) -> Vec<Arc<dyn Tool>> {
1247 vec![]
1248 }
1249 }
1250
1251 struct ToolSkill;
1252
1253 impl Skill for ToolSkill {
1254 fn name(&self) -> &'static str {
1255 "tool_skill"
1256 }
1257
1258 fn brief_description(&self) -> String {
1259 "skill with a tool".to_string()
1260 }
1261
1262 fn detailed_description(&self) -> String {
1263 "skill that provides a tool".to_string()
1264 }
1265
1266 fn tools(&self) -> Vec<Arc<dyn Tool>> {
1267 vec![Arc::new(NamedTool("skill_provided_tool"))]
1268 }
1269 }
1270
1271 #[tokio::test(flavor = "multi_thread")]
1272 async fn test_register_skill_builds_ok() {
1273 let client = make_client();
1274 let runtime = AgentBuilder::new(client)
1275 .register_skill(TestSkill)
1276 .build()
1277 .unwrap();
1278 assert!(runtime_tool_names(&runtime).is_empty());
1280 }
1281
1282 #[tokio::test(flavor = "multi_thread")]
1283 async fn test_register_skill_with_tool_registers_tool() {
1284 let client = make_client();
1285 let runtime = AgentBuilder::new(client)
1286 .register_skill(ToolSkill)
1287 .build()
1288 .unwrap();
1289 assert!(runtime_tool_names(&runtime).contains(&"skill_provided_tool".to_string()));
1290 }
1291
1292 #[tokio::test(flavor = "multi_thread")]
1293 async fn test_register_skill_tool_name_conflict() {
1294 let client = make_client();
1295 let result = AgentBuilder::new(client)
1296 .register_tool(NamedTool("skill_provided_tool"))
1297 .register_skill(ToolSkill)
1298 .build();
1299 let err = result.err().unwrap();
1300 assert!(format!("{err}").contains("Tool name conflict"));
1301 }
1302
1303 #[tokio::test(flavor = "multi_thread")]
1304 async fn test_skill_detail_tool_factory_registers_tool() {
1305 let client = make_client();
1306 let factory: SkillDetailToolFactory = Arc::new(|_skills, name| {
1307 assert_eq!(name, "get_skill_detail");
1308 Arc::new(NamedTool("detail_tool"))
1309 });
1310 let runtime = AgentBuilder::new(client)
1311 .register_skill(TestSkill)
1312 .with_skill_detail_tool_factory(factory)
1313 .build()
1314 .unwrap();
1315 assert!(runtime_tool_names(&runtime).contains(&"detail_tool".to_string()));
1316 }
1317
1318 #[tokio::test(flavor = "multi_thread")]
1319 async fn test_list_skills_tool_factory_registers_tool() {
1320 let client = make_client();
1321 let factory: ListSkillsToolFactory =
1322 Arc::new(|_registry| Arc::new(NamedTool("list_skills_tool")));
1323 let runtime = AgentBuilder::new(client)
1324 .register_skill(TestSkill)
1325 .with_list_skills_tool_factory(factory)
1326 .build()
1327 .unwrap();
1328 assert!(runtime_tool_names(&runtime).contains(&"list_skills_tool".to_string()));
1329 }
1330
1331 #[tokio::test(flavor = "multi_thread")]
1332 async fn test_disable_skill_prompt_injection_builds() {
1333 let client = make_client();
1334 let runtime = AgentBuilder::new(client)
1335 .register_skill(ToolSkill)
1336 .disable_skill_prompt_injection()
1337 .build()
1338 .unwrap();
1339 assert!(runtime_tool_names(&runtime).contains(&"skill_provided_tool".to_string()));
1341 }
1342 }
1343}