Skip to main content

agent_works/
builder.rs

1use std::collections::HashSet;
2use std::sync::Arc;
3
4use agent_base::{AgentResult, AgentRuntime, 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/// Factory type for creating multi-agent tools from a MultiAgentRuntime.
13#[cfg(feature = "multi_agent")]
14pub type MultiAgentToolFactory =
15    Arc<dyn Fn(Arc<MultiAgentRuntime>) -> Vec<Arc<dyn Tool>> + Send + Sync>;
16
17/// Factory type for creating a skill detail tool from skills and a tool name.
18#[cfg(feature = "skill")]
19pub type SkillDetailToolFactory =
20    Arc<dyn Fn(Vec<Arc<dyn Skill>>, String) -> Arc<dyn Tool> + Send + Sync>;
21
22/// Factory type for creating a list-skills tool from a SkillRegistry.
23#[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 agent_base::llm_trait::LlmProvider>,
30    system_prompt: Option<String>,
31    tool_names: HashSet<String>,
32    /// Business tools to pass to child agents (all registered tools).
33    business_tools: Vec<Arc<dyn Tool>>,
34    /// Multi-agent configuration (None = disabled).
35    #[cfg(feature = "multi_agent")]
36    multi_agent_config: Option<MultiAgentConfig>,
37    /// Factory to create multi-agent tools (injected by phi-kernel-tools).
38    #[cfg(feature = "multi_agent")]
39    multi_agent_tool_factory: Option<MultiAgentToolFactory>,
40    /// Error recovery (stored for multi-agent child inheritance).
41    error_recovery: Option<Arc<dyn agent_base::ToolErrorRecovery>>,
42    /// Language preference.
43    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    /// Optional: inject a custom skill-detail tool (old tool-based mode).
51    /// In default prompt-injection mode, the LLM reads `SKILL.md` via
52    /// `read_file` — no dedicated detail tool is needed.
53    #[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    /// Optional inline context compactor for the react loop.
60    context_compactor: Option<Arc<dyn agent_base::ContextCompaction>>,
61}
62
63impl AgentBuilder {
64    pub fn new(client: Arc<dyn agent_base::llm_trait::LlmProvider>) -> Self {
65        Self {
66            inner: agent_base::AgentBuilder::new(client.clone()),
67            client,
68            system_prompt: None,
69            tool_names: HashSet::new(),
70            business_tools: Vec::new(),
71            #[cfg(feature = "multi_agent")]
72            multi_agent_config: None,
73            #[cfg(feature = "multi_agent")]
74            multi_agent_tool_factory: None,
75            error_recovery: None,
76            language: None,
77            #[cfg(feature = "skill")]
78            skills: Vec::new(),
79            #[cfg(feature = "skill")]
80            skill_prompter: None,
81            #[cfg(feature = "skill")]
82            skill_detail_tool_name: "get_skill_detail".to_string(),
83            #[cfg(feature = "skill")]
84            skill_detail_tool_factory: None,
85            #[cfg(feature = "skill")]
86            list_skills_tool_factory: None,
87            #[cfg(feature = "skill")]
88            disable_skill_prompt_injection: false,
89            context_compactor: None,
90        }
91    }
92
93    /// Enable multi-agent support with the given configuration.
94    ///
95    /// Also sets the tool factory to create the 5 multi-agent tools.
96    /// Callers should use `phi_kernel_tools::multi_agent::create_all_tools` as the factory.
97    #[cfg(feature = "multi_agent")]
98    pub fn with_multi_agent(mut self, config: MultiAgentConfig) -> Self {
99        self.multi_agent_config = Some(config);
100        self
101    }
102
103    /// Disable multi-agent support.
104    ///
105    /// Removes any previously set multi-agent configuration. No multi-agent tools
106    /// will be registered and the system prompt will not mention multi-agent capabilities.
107    #[cfg(feature = "multi_agent")]
108    pub fn without_multi_agent(mut self) -> Self {
109        self.multi_agent_config = None;
110        self.multi_agent_tool_factory = None;
111        self
112    }
113
114    /// Set a custom factory for creating multi-agent tools.
115    ///
116    /// The factory receives the `MultiAgentRuntime` and returns the tools to register.
117    /// If not set but multi-agent is enabled, no tools are registered (caller must
118    /// set this for multi-agent to work).
119    #[cfg(feature = "multi_agent")]
120    pub fn with_multi_agent_tool_factory(mut self, factory: MultiAgentToolFactory) -> Self {
121        self.multi_agent_tool_factory = Some(factory);
122        self
123    }
124
125    /// Set a custom factory for creating the skill detail tool.
126    ///
127    /// The factory receives the skill list and tool name, and returns the tool.
128    /// If not set but skills are registered, no detail tool is added.
129    #[cfg(feature = "skill")]
130    pub fn with_skill_detail_tool_factory(mut self, factory: SkillDetailToolFactory) -> Self {
131        self.skill_detail_tool_factory = Some(factory);
132        self
133    }
134
135    /// Set a custom factory for creating the list-skills tool.
136    ///
137    /// The factory receives the SkillRegistry and returns the tool.
138    #[cfg(feature = "skill")]
139    pub fn with_list_skills_tool_factory(mut self, factory: ListSkillsToolFactory) -> Self {
140        self.list_skills_tool_factory = Some(factory);
141        self
142    }
143
144    pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
145        let prompt = prompt.into();
146        self.inner = self.inner.system_prompt(prompt.clone());
147        self.system_prompt = Some(prompt);
148        self
149    }
150
151    pub fn enable_thought(self, enable: bool) -> Self {
152        Self {
153            inner: self.inner.enable_thought(enable),
154            ..self
155        }
156    }
157
158    pub fn reasoning(self, config: agent_base::ReasoningConfig) -> Self {
159        Self {
160            inner: self.inner.reasoning(config),
161            ..self
162        }
163    }
164
165    pub fn enable_thinking(self, enable: bool) -> Self {
166        Self {
167            inner: self.inner.enable_thinking(enable),
168            ..self
169        }
170    }
171
172    pub fn thinking_budget(self, budget: u64) -> Self {
173        Self {
174            inner: self.inner.thinking_budget(budget),
175            ..self
176        }
177    }
178
179    pub fn tool_timeout(self, timeout_ms: u64) -> Self {
180        Self {
181            inner: self.inner.tool_timeout(timeout_ms),
182            ..self
183        }
184    }
185
186    pub fn max_tool_output_chars(self, max_chars: usize) -> Self {
187        Self {
188            inner: self.inner.max_tool_output_chars(max_chars),
189            ..self
190        }
191    }
192
193    pub fn max_sessions(self, max: usize) -> Self {
194        Self {
195            inner: self.inner.max_sessions(max),
196            ..self
197        }
198    }
199
200    pub fn max_turns_per_session(self, max: usize) -> Self {
201        Self {
202            inner: self.inner.max_turns_per_session(max),
203            ..self
204        }
205    }
206
207    pub fn execution_max_turns(self, max: u32) -> Self {
208        Self {
209            inner: self.inner.execution_max_turns(max),
210            ..self
211        }
212    }
213
214    pub fn max_message_tokens(self, max: usize) -> Self {
215        Self {
216            inner: self.inner.max_message_tokens(max),
217            ..self
218        }
219    }
220
221    pub fn register_tool(mut self, tool: impl Tool + 'static) -> Self {
222        let tool_arc: Arc<dyn Tool> = Arc::new(tool);
223        self.tool_names.insert(tool_arc.name().to_string());
224        self.business_tools.push(tool_arc.clone());
225        self.inner = self.inner.register_tool_arc(tool_arc);
226        self
227    }
228
229    pub fn register_tool_arc(mut self, tool: Arc<dyn Tool>) -> Self {
230        self.tool_names.insert(tool.name().to_string());
231        self.business_tools.push(tool.clone());
232        self.inner = self.inner.register_tool_arc(tool);
233        self
234    }
235
236    pub fn approval_handler(self, handler: Arc<dyn agent_base::ApprovalHandler>) -> Self {
237        Self {
238            inner: self.inner.approval_handler(handler),
239            ..self
240        }
241    }
242
243    pub fn tool_policy(self, policy: Arc<dyn agent_base::ToolPolicy>) -> Self {
244        Self {
245            inner: self.inner.tool_policy(policy),
246            ..self
247        }
248    }
249
250    pub fn middleware(self, mw: impl agent_base::Middleware + 'static) -> Self {
251        Self {
252            inner: self.inner.middleware(mw),
253            ..self
254        }
255    }
256
257    pub fn guard(self, guard: impl agent_base::ReactLoopGuard + 'static) -> Self {
258        Self {
259            inner: self.inner.guard(guard),
260            ..self
261        }
262    }
263
264    pub fn context_window(self, max_tokens: usize) -> Self {
265        Self {
266            inner: self.inner.context_window(max_tokens),
267            ..self
268        }
269    }
270
271    pub fn context_window_manager(self, manager: agent_base::ContextWindowManager) -> Self {
272        Self {
273            inner: self.inner.context_window_manager(manager),
274            ..self
275        }
276    }
277
278    pub fn response_format(self, format: agent_base::ResponseFormat) -> Self {
279        Self {
280            inner: self.inner.response_format(format),
281            ..self
282        }
283    }
284
285    pub fn llm_retry(self, retry: agent_base::RetryConfig) -> Self {
286        Self {
287            inner: self.inner.llm_retry(retry),
288            ..self
289        }
290    }
291
292    pub fn session_store(self, store: Arc<dyn agent_base::SessionStore>) -> Self {
293        Self {
294            inner: self.inner.session_store(store),
295            ..self
296        }
297    }
298
299    pub fn error_recovery(mut self, recovery: Arc<dyn agent_base::ToolErrorRecovery>) -> Self {
300        self.error_recovery = Some(recovery.clone());
301        self.inner = self.inner.error_recovery(recovery);
302        self
303    }
304
305    pub fn tool_error_retry_prompt(self, prompt: impl Into<String>) -> Self {
306        Self {
307            inner: self.inner.tool_error_retry_prompt(prompt),
308            ..self
309        }
310    }
311
312    pub fn language(mut self, language: agent_base::Language) -> Self {
313        self.language = Some(language.clone());
314        self.inner = self.inner.language(language);
315        self
316    }
317
318    /// Set an inline context compactor for the react loop.
319    ///
320    /// When set, the react loop will check token count after each tool turn
321    /// and compact the session if it exceeds the configured threshold.
322    pub fn context_compactor(mut self, compactor: Arc<dyn agent_base::ContextCompaction>) -> Self {
323        self.context_compactor = Some(compactor);
324        self
325    }
326
327    pub fn event_bus_capacity(self, capacity: usize) -> Self {
328        Self {
329            inner: self.inner.event_bus_capacity(capacity),
330            ..self
331        }
332    }
333
334    pub fn session_id_generator(
335        self,
336        generator: Arc<dyn agent_base::types::SessionIdGenerator>,
337    ) -> Self {
338        Self {
339            inner: self.inner.session_id_generator(generator),
340            ..self
341        }
342    }
343
344    /// Conditionally apply a transformation when `value` is `Some`.
345    ///
346    /// This is a convenience for option-chaining builder patterns:
347    ///
348    /// ```ignore
349    /// builder.apply_if(args.thinking_budget, |b, budget| b.thinking_budget(budget))
350    /// ```
351    pub fn apply_if<T>(self, value: Option<T>, f: impl FnOnce(Self, T) -> Self) -> Self {
352        match value {
353            Some(v) => f(self, v),
354            None => self,
355        }
356    }
357
358    #[cfg(feature = "skill")]
359    pub fn register_skill(mut self, skill: impl Skill + 'static) -> Self {
360        self.skills.push(Arc::new(skill));
361        self
362    }
363
364    #[cfg(feature = "skill")]
365    pub fn register_skills(mut self, skills: Vec<Arc<dyn Skill>>) -> Self {
366        self.skills.extend(skills);
367        self
368    }
369
370    #[cfg(feature = "skill")]
371    pub fn skill_prompter(mut self, prompter: Arc<dyn SkillPrompter>) -> Self {
372        self.skill_prompter = Some(prompter);
373        self
374    }
375
376    #[cfg(feature = "skill")]
377    pub fn disable_skill_prompt_injection(mut self) -> Self {
378        self.disable_skill_prompt_injection = true;
379        self
380    }
381
382    #[cfg(feature = "skill")]
383    pub fn skill_detail_tool_name(mut self, name: impl Into<String>) -> Self {
384        self.skill_detail_tool_name = name.into();
385        self
386    }
387
388    // ── Build ──
389
390    pub fn build(self) -> AgentResult<AgentRuntime> {
391        #[cfg(feature = "skill")]
392        {
393            self.build_with_skills()
394        }
395        #[cfg(not(feature = "skill"))]
396        {
397            self.build_inner()
398        }
399    }
400
401    /// Build the runtime and return the multi-agent runtime (if enabled).
402    ///
403    /// Same as [`build`](Self::build) but also returns the `MultiAgentRuntime`
404    /// created during setup. Callers that need to start the watcher task or
405    /// access the mailbox should use this method.
406    #[cfg(feature = "multi_agent")]
407    pub fn build_with_ma(self) -> AgentResult<(AgentRuntime, Option<Arc<MultiAgentRuntime>>)> {
408        #[cfg(feature = "skill")]
409        {
410            self.build_with_skills_ma()
411        }
412        #[cfg(not(feature = "skill"))]
413        {
414            self.build_inner_ma()
415        }
416    }
417
418    #[allow(dead_code, unused_mut)]
419    fn build_inner(mut self) -> AgentResult<AgentRuntime> {
420        // Inject default guard if none was set by the consumer.
421        // Uses the same LLM client for the judge (enabled by default).
422        if self.inner.get_guard().is_none() {
423            self.inner = self
424                .inner
425                .guard(crate::guard::DefaultGuard::with_llm_client(
426                    crate::guard::DefaultGuardConfig::default(),
427                    self.client.clone(),
428                ));
429        }
430
431        #[cfg(feature = "multi_agent")]
432        let lang = self.language.clone().unwrap_or_default();
433        #[cfg(feature = "multi_agent")]
434        let business_tools = std::mem::take(&mut self.business_tools);
435        #[cfg(feature = "multi_agent")]
436        let error_recovery = self.error_recovery.clone();
437        #[cfg(feature = "multi_agent")]
438        let tool_names = self.tool_names.clone();
439
440        #[cfg(feature = "multi_agent")]
441        let ma_config = self.multi_agent_config.clone();
442        #[cfg(feature = "multi_agent")]
443        let ma_tool_factory = self.multi_agent_tool_factory.take();
444
445        // Inject multi-agent prompt before build
446        #[cfg(feature = "multi_agent")]
447        if ma_config.as_ref().map(|c| c.enabled).unwrap_or(false) {
448            let ma_prompt = build_multi_agent_system_prompt();
449            let new_prompt = match self.system_prompt.take() {
450                Some(existing) => format!("{}\n\n---\n\n{}", existing, ma_prompt),
451                None => ma_prompt,
452            };
453            self.inner = self.inner.system_prompt(new_prompt);
454        }
455
456        // Inject inline context compactor if set
457        if let Some(compactor) = self.context_compactor.take() {
458            self.inner = self.inner.context_compactor(compactor);
459        }
460
461        let runtime = self.inner.build()?;
462
463        // Post-build: register multi-agent tools if enabled and factory is set
464        #[cfg(feature = "multi_agent")]
465        if let Some(config) = ma_config
466            && config.enabled
467        {
468            setup_multi_agent(
469                &runtime,
470                config,
471                lang,
472                business_tools,
473                error_recovery,
474                &tool_names,
475                ma_tool_factory,
476            )?;
477        }
478
479        Ok(runtime)
480    }
481
482    /// Like `build_inner` but also returns the `MultiAgentRuntime`.
483    #[cfg(feature = "multi_agent")]
484    #[allow(dead_code, unused_mut)]
485    fn build_inner_ma(mut self) -> AgentResult<(AgentRuntime, Option<Arc<MultiAgentRuntime>>)> {
486        if self.inner.get_guard().is_none() {
487            self.inner = self
488                .inner
489                .guard(crate::guard::DefaultGuard::with_llm_client(
490                    crate::guard::DefaultGuardConfig::default(),
491                    self.client.clone(),
492                ));
493        }
494
495        let lang = self.language.clone().unwrap_or_default();
496        let business_tools = std::mem::take(&mut self.business_tools);
497        let error_recovery = self.error_recovery.clone();
498        let tool_names = self.tool_names.clone();
499        let ma_config = self.multi_agent_config.clone();
500        let ma_tool_factory = self.multi_agent_tool_factory.take();
501
502        if ma_config.as_ref().map(|c| c.enabled).unwrap_or(false) {
503            let ma_prompt = build_multi_agent_system_prompt();
504            let new_prompt = match self.system_prompt.take() {
505                Some(existing) => format!("{}\n\n---\n\n{}", existing, ma_prompt),
506                None => ma_prompt,
507            };
508            self.inner = self.inner.system_prompt(new_prompt);
509        }
510
511        if let Some(compactor) = self.context_compactor.take() {
512            self.inner = self.inner.context_compactor(compactor);
513        }
514
515        let runtime = self.inner.build()?;
516
517        let ma_runtime_out = if let Some(config) = ma_config
518            && config.enabled
519        {
520            Some(setup_multi_agent(
521                &runtime,
522                config,
523                lang,
524                business_tools,
525                error_recovery,
526                &tool_names,
527                ma_tool_factory,
528            )?)
529        } else {
530            None
531        };
532
533        Ok((runtime, ma_runtime_out))
534    }
535
536    /// Build the runtime with skill support.
537    ///
538    /// # Runtime requirement
539    ///
540    /// This method uses [`tokio::task::block_in_place`] to populate the skill
541    /// registry from a synchronous context. It **requires** a multi-threaded
542    /// tokio runtime. Calling it on a `#[tokio::main]` single-threaded
543    /// (`current_thread`) runtime will panic.
544    ///
545    /// The phi-agent CLI and all examples use the default multi-threaded runtime,
546    /// so this is safe in practice.
547    #[cfg(feature = "skill")]
548    fn build_with_skills(mut self) -> AgentResult<AgentRuntime> {
549        // Inject default guard if none was set by the consumer.
550        // Uses the same LLM client for the judge (enabled by default).
551        if self.inner.get_guard().is_none() {
552            self.inner = self
553                .inner
554                .guard(crate::guard::DefaultGuard::with_llm_client(
555                    crate::guard::DefaultGuardConfig::default(),
556                    self.client.clone(),
557                ));
558        }
559
560        let mut ab = self.inner;
561        #[cfg(feature = "multi_agent")]
562        let lang = self.language.clone().unwrap_or_default();
563        #[cfg(feature = "multi_agent")]
564        let business_tools = std::mem::take(&mut self.business_tools);
565        #[cfg(feature = "multi_agent")]
566        let error_recovery = self.error_recovery.clone();
567        #[cfg(feature = "multi_agent")]
568        let tool_names = self.tool_names.clone();
569
570        #[cfg(feature = "multi_agent")]
571        let ma_config = self.multi_agent_config.clone();
572        #[cfg(feature = "multi_agent")]
573        let ma_tool_factory = self.multi_agent_tool_factory.take();
574
575        // Process skills
576        if !self.skills.is_empty() {
577            let prompter: Arc<dyn SkillPrompter> = self
578                .skill_prompter
579                .take()
580                .unwrap_or_else(|| Arc::new(LazySkillPrompter::new()));
581
582            let mut skill_refs: Vec<Arc<dyn Skill>> = Vec::new();
583
584            for skill in self.skills {
585                for tool in skill.tools() {
586                    let tool_name = tool.name().to_string();
587                    if self.tool_names.contains(&tool_name) {
588                        return Err(agent_base::AgentError::internal(format!(
589                            "Tool name conflict: `{}` (Skill `{}`)",
590                            tool_name,
591                            skill.name()
592                        )));
593                    }
594                    self.tool_names.insert(tool_name);
595                    ab = ab.register_tool_arc(tool);
596                }
597                skill_refs.push(skill);
598            }
599
600            if !self.disable_skill_prompt_injection {
601                let skill_prompt = prompter.build_prompt(&skill_refs, &self.skill_detail_tool_name);
602                if !skill_prompt.is_empty() {
603                    let new_prompt = match self.system_prompt.take() {
604                        Some(existing) => format!("{}\n\n---\n\n{}", existing, skill_prompt),
605                        None => skill_prompt,
606                    };
607                    self.system_prompt = Some(new_prompt.clone());
608                    ab = ab.system_prompt(new_prompt);
609                }
610            }
611
612            // Use injected factory if available, otherwise skip — prompt-injection
613            // mode uses read_file instead of a dedicated detail tool.
614            if let Some(factory) = self.skill_detail_tool_factory.take() {
615                let detail_tool = factory(skill_refs.clone(), self.skill_detail_tool_name);
616                ab = ab.register_tool_arc(detail_tool);
617            }
618
619            // Create SkillRegistry and populate it for the list-skills tool
620            if let Some(factory) = self.list_skills_tool_factory.take() {
621                let registry = Arc::new(crate::skill::SkillRegistry::new());
622                for skill in &skill_refs {
623                    tokio::task::block_in_place(|| {
624                        tokio::runtime::Handle::current().block_on(async {
625                            registry.register(skill.clone()).await;
626                        })
627                    });
628                }
629                let list_tool = factory(registry);
630                ab = ab.register_tool_arc(list_tool);
631            }
632        }
633
634        // Inject multi-agent prompt
635        #[cfg(feature = "multi_agent")]
636        if ma_config.as_ref().map(|c| c.enabled).unwrap_or(false) {
637            let ma_prompt = build_multi_agent_system_prompt();
638            let new_prompt = match self.system_prompt.take() {
639                Some(existing) => format!("{}\n\n---\n\n{}", existing, ma_prompt),
640                None => ma_prompt,
641            };
642            ab = ab.system_prompt(new_prompt);
643        }
644
645        // Inject inline context compactor if set
646        if let Some(compactor) = self.context_compactor.take() {
647            ab = ab.context_compactor(compactor);
648        }
649
650        let runtime = ab.build()?;
651
652        // Post-build: register multi-agent tools
653        #[cfg(feature = "multi_agent")]
654        if let Some(config) = ma_config
655            && config.enabled
656        {
657            setup_multi_agent(
658                &runtime,
659                config,
660                lang,
661                business_tools,
662                error_recovery,
663                &tool_names,
664                ma_tool_factory,
665            )?;
666        }
667
668        Ok(runtime)
669    }
670
671    /// Like `build_with_skills` but also returns the `MultiAgentRuntime`.
672    #[cfg(all(feature = "skill", feature = "multi_agent"))]
673    fn build_with_skills_ma(
674        mut self,
675    ) -> AgentResult<(AgentRuntime, Option<Arc<MultiAgentRuntime>>)> {
676        if self.inner.get_guard().is_none() {
677            self.inner = self
678                .inner
679                .guard(crate::guard::DefaultGuard::with_llm_client(
680                    crate::guard::DefaultGuardConfig::default(),
681                    self.client.clone(),
682                ));
683        }
684
685        let mut ab = self.inner;
686        let lang = self.language.clone().unwrap_or_default();
687        let business_tools = std::mem::take(&mut self.business_tools);
688        let error_recovery = self.error_recovery.clone();
689        let tool_names = self.tool_names.clone();
690        let ma_config = self.multi_agent_config.clone();
691        let ma_tool_factory = self.multi_agent_tool_factory.take();
692
693        // Process skills
694        if !self.skills.is_empty() {
695            let prompter: Arc<dyn SkillPrompter> = self
696                .skill_prompter
697                .take()
698                .unwrap_or_else(|| Arc::new(LazySkillPrompter::new()));
699
700            let mut skill_refs: Vec<Arc<dyn Skill>> = Vec::new();
701
702            for skill in self.skills {
703                for tool in skill.tools() {
704                    let tool_name = tool.name().to_string();
705                    if self.tool_names.contains(&tool_name) {
706                        return Err(agent_base::AgentError::internal(format!(
707                            "Tool name conflict: `{}` (Skill `{}`)",
708                            tool_name,
709                            skill.name()
710                        )));
711                    }
712                    self.tool_names.insert(tool_name);
713                    ab = ab.register_tool_arc(tool);
714                }
715                skill_refs.push(skill);
716            }
717
718            if !self.disable_skill_prompt_injection {
719                let skill_prompt = prompter.build_prompt(&skill_refs, &self.skill_detail_tool_name);
720                if !skill_prompt.is_empty() {
721                    let new_prompt = match self.system_prompt.take() {
722                        Some(existing) => format!("{}\n\n---\n\n{}", existing, skill_prompt),
723                        None => skill_prompt,
724                    };
725                    self.system_prompt = Some(new_prompt.clone());
726                    ab = ab.system_prompt(new_prompt);
727                }
728            }
729
730            if let Some(factory) = self.skill_detail_tool_factory.take() {
731                let detail_tool = factory(skill_refs.clone(), self.skill_detail_tool_name);
732                ab = ab.register_tool_arc(detail_tool);
733            }
734
735            if let Some(factory) = self.list_skills_tool_factory.take() {
736                let registry = Arc::new(crate::skill::SkillRegistry::new());
737                for skill in &skill_refs {
738                    tokio::task::block_in_place(|| {
739                        tokio::runtime::Handle::current().block_on(async {
740                            registry.register(skill.clone()).await;
741                        })
742                    });
743                }
744                let list_tool = factory(registry);
745                ab = ab.register_tool_arc(list_tool);
746            }
747        }
748
749        // Inject multi-agent prompt
750        if ma_config.as_ref().map(|c| c.enabled).unwrap_or(false) {
751            let ma_prompt = build_multi_agent_system_prompt();
752            let new_prompt = match self.system_prompt.take() {
753                Some(existing) => format!("{}\n\n---\n\n{}", existing, ma_prompt),
754                None => ma_prompt,
755            };
756            ab = ab.system_prompt(new_prompt);
757        }
758
759        if let Some(compactor) = self.context_compactor.take() {
760            ab = ab.context_compactor(compactor);
761        }
762
763        let runtime = ab.build()?;
764
765        let ma_runtime_out = if let Some(config) = ma_config
766            && config.enabled
767        {
768            Some(setup_multi_agent(
769                &runtime,
770                config,
771                lang,
772                business_tools,
773                error_recovery,
774                &tool_names,
775                ma_tool_factory,
776            )?)
777        } else {
778            None
779        };
780
781        Ok((runtime, ma_runtime_out))
782    }
783}
784
785/// Set up the MultiAgentRuntime, event bridge, and register tools on an already-built runtime.
786///
787/// # Safety / Runtime Requirement
788///
789/// This function uses [`tokio::task::block_in_place`] to register tools synchronously.
790/// It **requires** a multi-threaded tokio runtime. Calling it on a
791/// `#[tokio::main]` single-threaded (`current_thread`) runtime will panic.
792///
793/// The phi-agent CLI and all examples use the default multi-threaded runtime,
794/// so this is safe in practice.
795#[cfg(feature = "multi_agent")]
796pub fn setup_multi_agent(
797    runtime: &AgentRuntime,
798    config: MultiAgentConfig,
799    lang: agent_base::Language,
800    business_tools: Vec<Arc<dyn Tool>>,
801    error_recovery: Option<Arc<dyn agent_base::ToolErrorRecovery>>,
802    existing_tool_names: &HashSet<String>,
803    tool_factory: Option<MultiAgentToolFactory>,
804) -> AgentResult<Arc<MultiAgentRuntime>> {
805    let client = runtime.provider();
806    let cancel_token = runtime.cancel_token();
807    let tool_policy = runtime.tool_policy().cloned();
808    let approval_handler = runtime.approval_handler().cloned();
809
810    let ma_runtime = Arc::new(MultiAgentRuntime::new(
811        config.clone(),
812        client,
813        business_tools,
814        cancel_token,
815        error_recovery,
816        lang,
817        tool_policy,
818        approval_handler,
819    ));
820
821    // Set parent session manager for fork_history support
822    ma_runtime.set_session_manager(Arc::new(runtime.session_manager().clone()));
823
824    // Set up event bridge: child events → parent event bus
825    let (event_tx, mut event_rx) =
826        tokio::sync::mpsc::unbounded_channel::<agent_base::RuntimeEvent>();
827    ma_runtime.set_event_sender(event_tx);
828    let parent_runtime = runtime.clone();
829    tokio::spawn(async move {
830        while let Some(event) = event_rx.recv().await {
831            parent_runtime.emit_event(event);
832        }
833    });
834
835    // Register multi-agent tools if a factory is provided
836    if let Some(factory) = tool_factory {
837        let tools = factory(ma_runtime.clone());
838        let registry = runtime.tools_mut();
839        let mut reg = tokio::task::block_in_place(|| registry.blocking_write());
840        for tool in tools {
841            let tool_name = tool.name().to_string();
842            if !existing_tool_names.contains(&tool_name) {
843                reg.register_arc(tool);
844            }
845        }
846        drop(reg);
847    }
848
849    Ok(ma_runtime)
850}
851
852/// Build the multi-agent system prompt guidance for the main agent.
853#[cfg(feature = "multi_agent")]
854pub fn build_multi_agent_system_prompt() -> String {
855    r#"## Multi-Agent Capabilities
856
857You can spawn sub-agents to work on tasks concurrently. Each sub-agent is an independent assistant with its own tools (repo_map, read_file, list_files, execute_command, etc.) and can reason through problems autonomously.
858
859### Tools
860
861- `spawn_agent`: Create a sub-agent. Use the `task` field to describe what you want done — the sub-agent will figure out the steps. Example: `spawn_agent({task_name: "analyze-codex", task: "analyze the codex project structure and output a structured report"})`.
862- `send_message`: Send follow-up context or tasks to a sub-agent (`trigger: true` starts execution).
863- `list_agents`: List active sub-agents with their assigned task and status — use it to answer "what is agent X doing?".
864- `close_agent`: Close a sub-agent when done.
865
866### How Results Reach You
867
868Sub-agent results are **pushed into your context automatically** when they complete — there is no wait tool. To wait, simply end your turn: you will be woken with the result as a new message. Keep working on other things instead of polling.
869
870### How Sub-Agents Work
871
872Sub-agents are capable — they can explore codebases, read files, run commands, and produce structured analysis. You don't need to plan their steps or specify which tools to use. Just describe the goal in `task`, and the sub-agent handles the rest.
873
874### When to Spawn
875
876- Parallel tasks: analyze multiple codebases, compare implementations, etc.
877- Tasks you want to delegate while you continue other work
878
879### When NOT to Spawn
880
881- Simple lookups (just use read_file or repo_map directly)
882- Tasks that need your current conversation context"#
883        .to_string()
884}
885
886/// Build the memory system prompt guidance.
887///
888/// Tells the LLM how to use the file-based persistent memory system.
889/// Memory is stored as markdown files — the LLM uses `read_file` / `write_file`
890/// to manage them, following the same convention as Claude Code Memory.
891///
892/// This is prompt-injection only — no dedicated memory tools are registered.
893/// The LLM uses the general-purpose file tools to read/write memory files.
894pub fn build_memory_system_prompt() -> String {
895    r#"## Memory
896
897You have a persistent file-based memory at `.phi/memory/`. Use `read_file` and `write_file` to manage it — there are no dedicated memory tools.
898
899### How Memory Works
900
901- `MEMORY.md` is the index — it lists all memories with one-line descriptions. Read it first when you need to recall something.
902- Each memory is a separate `.md` file with YAML frontmatter:
903  ```yaml
904  ---
905  name: <short-kebab-case-slug>
906  description: <one-line summary — used to decide relevance during recall>
907  metadata:
908    node_type: memory
909    type: user | feedback | project | reference
910  ---
911
912  <the fact or instruction>
913  ```
914- 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.
915- Link related memories with `[[memory-name]]` in the body.
916- `user` type = who the user is (role, expertise, preferences).
917- `feedback` type = guidance the user has given on how you should work.
918- `project` type = ongoing work, goals, or constraints.
919- `reference` type = pointers to external resources (URLs, dashboards, tickets).
920
921### When to Use Memory
922
923- The user explicitly asks you to remember something ("remember this", "save that")
924- You learn something important about the user's preferences or workflow
925- After completing a significant task, save context that would help in future sessions
926- The user gives you feedback on how to work — save it as `feedback` type
927
928### When NOT to Use Memory
929
930- For transient information that won't be useful beyond this session
931- For facts already recorded in the codebase (code structure, git history, config files)
932- For items that only matter to the current conversation
933
934### Pro Tips
935
936- When creating your first memory of a new type, you can read template files for format reference (check `.phi/templates/memory/` if available).
937- Keep the MEMORY.md index concise — it's loaded into context every session.
938- Before writing a new memory, check if an existing file already covers it — update instead of duplicating.
939
940### Workflow
941
942**To recall:** read `MEMORY.md` → find relevant entries by description → read the specific `.md` files you need.
943**To remember:** create a new `.md` file with proper frontmatter → update `MEMORY.md` with a new entry.
944**To update:** edit the existing `.md` file (don't create a duplicate).
945**To forget:** delete the `.md` file → remove its entry from `MEMORY.md`."#
946        .to_string()
947}
948
949#[cfg(test)]
950mod tests {
951    use super::*;
952    use agent_base::Content;
953    use agent_base::llm_trait::response::FinishReason;
954    use agent_base::llm_trait::types::UsageInfo;
955    use agent_base::llm_trait::{
956        Capabilities, ChatRequest, ChatResponse, ChatStream, LlmError, LlmProvider, ProviderInfo,
957    };
958
959    // ── Stub LLM provider ──
960
961    struct StubProvider;
962
963    #[async_trait::async_trait]
964    impl LlmProvider for StubProvider {
965        async fn stream(&self, _request: ChatRequest) -> Result<ChatStream, LlmError> {
966            let chunks = vec![
967                Ok(agent_base::StreamChunk::Text("ok".to_string())),
968                Ok(agent_base::StreamChunk::Stop {
969                    finish_reason: Some("stop".to_string()),
970                }),
971            ];
972            Ok(ChatStream::new(Box::pin(futures_util::stream::iter(
973                chunks,
974            ))))
975        }
976
977        async fn chat(&self, _request: ChatRequest) -> Result<ChatResponse, LlmError> {
978            Ok(ChatResponse {
979                content: "ok".to_string(),
980                tool_calls: vec![],
981                usage: UsageInfo::default(),
982                finish_reason: FinishReason::Stop,
983                raw: None,
984                reasoning_content: None,
985                thinking_signature: None,
986            })
987        }
988
989        fn capabilities(&self) -> Capabilities {
990            Capabilities {
991                supports_streaming: true,
992                supports_tools: true,
993                ..Default::default()
994            }
995        }
996
997        fn info(&self) -> ProviderInfo {
998            ProviderInfo {
999                name: "stub".to_string(),
1000                model: "stub-model".to_string(),
1001                version: None,
1002            }
1003        }
1004    }
1005
1006    fn make_client() -> Arc<dyn LlmProvider> {
1007        Arc::new(StubProvider)
1008    }
1009
1010    // ── setup_multi_agent tests ──
1011
1012    #[cfg(feature = "multi_agent")]
1013    #[tokio::test(flavor = "multi_thread")]
1014    async fn test_setup_multi_agent_without_factory_registers_no_tools() {
1015        let client = make_client();
1016        let runtime = agent_base::AgentBuilder::new(client.clone())
1017            .build()
1018            .unwrap();
1019        let config = MultiAgentConfig::enabled();
1020
1021        let result = setup_multi_agent(
1022            &runtime,
1023            config,
1024            agent_base::Language::En,
1025            vec![],
1026            None,
1027            &HashSet::new(),
1028            None, // no factory
1029        );
1030        assert!(result.is_ok());
1031        let ma_runtime = result.unwrap();
1032        // Verify no tools were registered (the 5 multi-agent tools are absent)
1033        let agents = ma_runtime.list_agents();
1034        assert!(agents.is_empty());
1035    }
1036
1037    #[cfg(feature = "multi_agent")]
1038    #[tokio::test(flavor = "multi_thread")]
1039    async fn test_setup_multi_agent_with_factory_registers_tools() {
1040        let client = make_client();
1041        let runtime = agent_base::AgentBuilder::new(client.clone())
1042            .build()
1043            .unwrap();
1044        let config = MultiAgentConfig::enabled();
1045
1046        let factory: MultiAgentToolFactory = Arc::new(|_rt| {
1047            // Minimal factory returning a single fake tool
1048            struct FakeTool;
1049            #[async_trait::async_trait]
1050            impl Tool for FakeTool {
1051                fn name(&self) -> &'static str {
1052                    "fake_tool"
1053                }
1054                fn description(&self) -> &'static str {
1055                    ""
1056                }
1057                fn schema(&self) -> serde_json::Value {
1058                    serde_json::json!({})
1059                }
1060                async fn call(
1061                    &self,
1062                    _args: &serde_json::Value,
1063                    _ctx: &agent_base::ToolContext,
1064                ) -> AgentResult<Vec<Content>> {
1065                    Ok(vec![Content::text("ok")])
1066                }
1067            }
1068            vec![Arc::new(FakeTool)]
1069        });
1070
1071        let result = setup_multi_agent(
1072            &runtime,
1073            config,
1074            agent_base::Language::En,
1075            vec![],
1076            None,
1077            &HashSet::new(),
1078            Some(factory),
1079        );
1080        assert!(result.is_ok());
1081
1082        // Check the tool was registered on the runtime
1083        let tools: Vec<String> = tokio::task::block_in_place(|| {
1084            let tools = runtime.tools_mut();
1085            let guard = tools.blocking_read();
1086            guard.metadatas().into_iter().map(|m| m.name).collect()
1087        });
1088        assert!(tools.contains(&"fake_tool".to_string()));
1089    }
1090
1091    #[cfg(feature = "multi_agent")]
1092    #[tokio::test(flavor = "multi_thread")]
1093    async fn test_setup_multi_agent_skips_duplicate_tool_names() {
1094        let client = make_client();
1095        let runtime = agent_base::AgentBuilder::new(client.clone())
1096            .build()
1097            .unwrap();
1098
1099        // Pre-register a tool with a conflicting name
1100        struct DupTool;
1101        #[async_trait::async_trait]
1102        impl Tool for DupTool {
1103            fn name(&self) -> &'static str {
1104                "dup_tool"
1105            }
1106            fn description(&self) -> &'static str {
1107                ""
1108            }
1109            fn schema(&self) -> serde_json::Value {
1110                serde_json::json!({})
1111            }
1112            async fn call(
1113                &self,
1114                _args: &serde_json::Value,
1115                _ctx: &agent_base::ToolContext,
1116            ) -> AgentResult<Vec<Content>> {
1117                Ok(vec![Content::text("ok")])
1118            }
1119        }
1120        {
1121            let tools = runtime.tools_mut();
1122            let mut reg = tokio::task::block_in_place(|| tools.blocking_write());
1123            reg.register(DupTool);
1124        }
1125
1126        let factory: MultiAgentToolFactory = Arc::new(|_rt| {
1127            struct FakeTool;
1128            #[async_trait::async_trait]
1129            impl Tool for FakeTool {
1130                fn name(&self) -> &'static str {
1131                    "dup_tool"
1132                }
1133                fn description(&self) -> &'static str {
1134                    ""
1135                }
1136                fn schema(&self) -> serde_json::Value {
1137                    serde_json::json!({})
1138                }
1139                async fn call(
1140                    &self,
1141                    _args: &serde_json::Value,
1142                    _ctx: &agent_base::ToolContext,
1143                ) -> AgentResult<Vec<Content>> {
1144                    Ok(vec![Content::text("ok")])
1145                }
1146            }
1147            vec![Arc::new(FakeTool)]
1148        });
1149
1150        let mut existing = HashSet::new();
1151        existing.insert("dup_tool".to_string());
1152
1153        let result = setup_multi_agent(
1154            &runtime,
1155            MultiAgentConfig::enabled(),
1156            agent_base::Language::En,
1157            vec![],
1158            None,
1159            &existing,
1160            Some(factory),
1161        );
1162        assert!(result.is_ok());
1163        // dup_tool should NOT have been registered twice
1164        let tools = tokio::task::block_in_place(|| {
1165            let tools = runtime.tools_mut();
1166            let guard = tools.blocking_read();
1167            guard
1168                .metadatas()
1169                .into_iter()
1170                .map(|m| m.name)
1171                .collect::<Vec<String>>()
1172        });
1173        let count = tools.iter().filter(|n| n.as_str() == "dup_tool").count();
1174        assert_eq!(count, 1);
1175    }
1176
1177    // ── AgentBuilder factory methods ──
1178
1179    #[cfg(feature = "multi_agent")]
1180    #[tokio::test(flavor = "multi_thread")]
1181    async fn test_builder_with_multi_agent_without_factory_builds_ok() {
1182        let client = make_client();
1183        let runtime = AgentBuilder::new(client)
1184            .with_multi_agent(MultiAgentConfig::enabled())
1185            .build()
1186            .unwrap();
1187        // Should succeed even without a factory (no tools registered)
1188        let tools = tokio::task::block_in_place(|| {
1189            let tools = runtime.tools_mut();
1190            let guard = tools.blocking_read();
1191            guard
1192                .metadatas()
1193                .into_iter()
1194                .map(|m| m.name)
1195                .collect::<Vec<String>>()
1196        });
1197        // No multi-agent tools registered
1198        assert!(!tools.contains(&"spawn_agent".to_string()));
1199    }
1200
1201    #[cfg(feature = "multi_agent")]
1202    #[tokio::test(flavor = "multi_thread")]
1203    async fn test_builder_with_factory_registers_tools() {
1204        let client = make_client();
1205        // Create a simple factory that registers one recognizable tool
1206        let factory: MultiAgentToolFactory = Arc::new(|_rt| {
1207            struct TestTool;
1208            #[async_trait::async_trait]
1209            impl Tool for TestTool {
1210                fn name(&self) -> &'static str {
1211                    "factory_test_tool"
1212                }
1213                fn description(&self) -> &'static str {
1214                    ""
1215                }
1216                fn schema(&self) -> serde_json::Value {
1217                    serde_json::json!({})
1218                }
1219                async fn call(
1220                    &self,
1221                    _args: &serde_json::Value,
1222                    _ctx: &agent_base::ToolContext,
1223                ) -> AgentResult<Vec<Content>> {
1224                    Ok(vec![Content::text("ok")])
1225                }
1226            }
1227            vec![Arc::new(TestTool)]
1228        });
1229
1230        let runtime = AgentBuilder::new(client)
1231            .with_multi_agent(MultiAgentConfig::enabled())
1232            .with_multi_agent_tool_factory(factory)
1233            .build()
1234            .unwrap();
1235
1236        let tools = tokio::task::block_in_place(|| {
1237            let tools = runtime.tools_mut();
1238            let guard = tools.blocking_read();
1239            guard
1240                .metadatas()
1241                .into_iter()
1242                .map(|m| m.name)
1243                .collect::<Vec<String>>()
1244        });
1245        assert!(tools.contains(&"factory_test_tool".to_string()));
1246    }
1247
1248    #[cfg(feature = "multi_agent")]
1249    #[test]
1250    fn test_builder_disabled_multi_agent_skips_factory() {
1251        let client = make_client();
1252        let factory: MultiAgentToolFactory = Arc::new(|_rt| {
1253            panic!("factory should not be called when multi-agent is not configured");
1254        });
1255
1256        let runtime = AgentBuilder::new(client)
1257            .with_multi_agent_tool_factory(factory)
1258            // Don't enable multi-agent — default (None) means disabled
1259            .build()
1260            .unwrap();
1261
1262        let tools = tokio::task::block_in_place(|| {
1263            let tools = runtime.tools_mut();
1264            let guard = tools.blocking_read();
1265            guard
1266                .metadatas()
1267                .into_iter()
1268                .map(|m| m.name)
1269                .collect::<Vec<String>>()
1270        });
1271        assert!(!tools.contains(&"spawn_agent".to_string()));
1272    }
1273
1274    // ── build_multi_agent_system_prompt ──
1275
1276    #[cfg(feature = "multi_agent")]
1277    #[test]
1278    fn test_system_prompt_contains_tool_names() {
1279        let prompt = build_multi_agent_system_prompt();
1280        assert!(prompt.contains("spawn_agent"));
1281        assert!(prompt.contains("send_message"));
1282        assert!(prompt.contains("trigger"));
1283        assert!(prompt.contains("list_agents"));
1284        assert!(prompt.contains("close_agent"));
1285        // followup_task is gone (§8.3) — guidance must not advertise a tool
1286        // the factory no longer registers.
1287        assert!(!prompt.contains("followup_task"));
1288        // wait_agent is gone too — results are pushed, the parent waits by
1289        // ending its turn.
1290        assert!(!prompt.contains("wait_agent"));
1291        assert!(prompt.contains("pushed into your context automatically"));
1292    }
1293
1294    #[cfg(feature = "multi_agent")]
1295    #[test]
1296    fn test_system_prompt_contains_guidance() {
1297        let prompt = build_multi_agent_system_prompt();
1298        assert!(prompt.contains("When to Spawn"));
1299        assert!(prompt.contains("When NOT to Spawn"));
1300        assert!(prompt.contains("How Results Reach You"));
1301    }
1302
1303    // ── without_multi_agent ──
1304
1305    #[cfg(feature = "multi_agent")]
1306    #[tokio::test(flavor = "multi_thread")]
1307    async fn test_without_multi_agent_clears_config_and_factory() {
1308        let client = make_client();
1309
1310        // Set up a factory that would panic if called — without_multi_agent should prevent it
1311        let factory: MultiAgentToolFactory = Arc::new(|_rt| {
1312            panic!("factory should not be called when multi-agent is cleared");
1313        });
1314
1315        let runtime = AgentBuilder::new(client)
1316            .with_multi_agent(MultiAgentConfig::enabled())
1317            .with_multi_agent_tool_factory(factory)
1318            .without_multi_agent() // clear both
1319            .build()
1320            .unwrap();
1321
1322        let tools = tokio::task::block_in_place(|| {
1323            let tools = runtime.tools_mut();
1324            let guard = tools.blocking_read();
1325            guard
1326                .metadatas()
1327                .into_iter()
1328                .map(|m| m.name)
1329                .collect::<Vec<String>>()
1330        });
1331        assert!(!tools.contains(&"spawn_agent".to_string()));
1332    }
1333
1334    // ── apply_if ──
1335
1336    #[test]
1337    fn test_apply_if_some_applies_transformation() {
1338        let client = make_client();
1339        let builder = AgentBuilder::new(client)
1340            .apply_if(Some("custom prompt"), |b, prompt| b.system_prompt(prompt));
1341        // system_prompt is stored in self.system_prompt; verify it was set
1342        assert!(builder.system_prompt.unwrap().contains("custom prompt"));
1343    }
1344
1345    #[test]
1346    fn test_apply_if_none_passes_through() {
1347        let client = make_client();
1348        let builder = AgentBuilder::new(client).apply_if(None as Option<&str>, |_b, _prompt| {
1349            panic!("should not be called when value is None");
1350        });
1351        assert!(builder.system_prompt.is_none());
1352    }
1353
1354    // ── build_memory_system_prompt ──
1355
1356    #[test]
1357    fn test_build_memory_system_prompt_non_empty() {
1358        let prompt = build_memory_system_prompt();
1359        assert!(!prompt.is_empty());
1360        assert!(prompt.contains("Memory"));
1361        assert!(prompt.contains("MEMORY.md"));
1362        assert!(prompt.contains("read_file"));
1363        assert!(prompt.contains("write_file"));
1364    }
1365
1366    // ── Named tool for register_tool / skill tests ──
1367
1368    struct NamedTool(&'static str);
1369
1370    #[async_trait::async_trait]
1371    impl Tool for NamedTool {
1372        fn name(&self) -> &'static str {
1373            self.0
1374        }
1375
1376        fn description(&self) -> &'static str {
1377            ""
1378        }
1379
1380        fn schema(&self) -> serde_json::Value {
1381            serde_json::json!({})
1382        }
1383
1384        async fn call(
1385            &self,
1386            _args: &serde_json::Value,
1387            _ctx: &agent_base::ToolContext,
1388        ) -> AgentResult<Vec<Content>> {
1389            Ok(vec![Content::text("ok")])
1390        }
1391    }
1392
1393    fn runtime_tool_names(runtime: &AgentRuntime) -> Vec<String> {
1394        tokio::task::block_in_place(|| {
1395            let tools = runtime.tools_mut();
1396            let guard = tools.blocking_read();
1397            guard.metadatas().into_iter().map(|m| m.name).collect()
1398        })
1399    }
1400
1401    #[tokio::test(flavor = "multi_thread")]
1402    async fn test_builder_scalar_passthrough_methods() {
1403        let client = make_client();
1404        let runtime = AgentBuilder::new(client)
1405            .enable_thought(true)
1406            .reasoning(agent_base::ReasoningConfig::default())
1407            .enable_thinking(false)
1408            .thinking_budget(1000)
1409            .tool_timeout(5000)
1410            .max_tool_output_chars(4000)
1411            .max_sessions(16)
1412            .max_turns_per_session(20)
1413            .execution_max_turns(10)
1414            .max_message_tokens(8000)
1415            .context_window(64_000)
1416            .context_window_manager(agent_base::ContextWindowManager::new(64_000))
1417            .response_format(agent_base::ResponseFormat::JsonObject)
1418            .llm_retry(agent_base::RetryConfig::default())
1419            .tool_error_retry_prompt("please retry")
1420            .language(agent_base::Language::En)
1421            .event_bus_capacity(256)
1422            .build()
1423            .unwrap();
1424
1425        assert!(runtime.provider().capabilities().supports_streaming);
1426    }
1427
1428    #[tokio::test(flavor = "multi_thread")]
1429    async fn test_register_tool_variants() {
1430        let client = make_client();
1431        let runtime = AgentBuilder::new(client)
1432            .register_tool(NamedTool("tool_by_value"))
1433            .register_tool_arc(Arc::new(NamedTool("tool_by_arc")))
1434            .build()
1435            .unwrap();
1436
1437        let names = runtime_tool_names(&runtime);
1438        assert!(names.contains(&"tool_by_value".to_string()));
1439        assert!(names.contains(&"tool_by_arc".to_string()));
1440    }
1441
1442    #[cfg(feature = "skill")]
1443    mod skill_tests {
1444        use super::*;
1445        use crate::skill::Skill;
1446
1447        struct TestSkill;
1448
1449        impl Skill for TestSkill {
1450            fn name(&self) -> &'static str {
1451                "test_skill"
1452            }
1453
1454            fn brief_description(&self) -> String {
1455                "a test skill".to_string()
1456            }
1457
1458            fn detailed_description(&self) -> String {
1459                "detailed test skill".to_string()
1460            }
1461
1462            fn tools(&self) -> Vec<Arc<dyn Tool>> {
1463                vec![]
1464            }
1465        }
1466
1467        struct ToolSkill;
1468
1469        impl Skill for ToolSkill {
1470            fn name(&self) -> &'static str {
1471                "tool_skill"
1472            }
1473
1474            fn brief_description(&self) -> String {
1475                "skill with a tool".to_string()
1476            }
1477
1478            fn detailed_description(&self) -> String {
1479                "skill that provides a tool".to_string()
1480            }
1481
1482            fn tools(&self) -> Vec<Arc<dyn Tool>> {
1483                vec![Arc::new(NamedTool("skill_provided_tool"))]
1484            }
1485        }
1486
1487        #[tokio::test(flavor = "multi_thread")]
1488        async fn test_register_skill_builds_ok() {
1489            let client = make_client();
1490            let runtime = AgentBuilder::new(client)
1491                .register_skill(TestSkill)
1492                .build()
1493                .unwrap();
1494            // Prompt injection is applied during build; no tools provided.
1495            assert!(runtime_tool_names(&runtime).is_empty());
1496        }
1497
1498        #[tokio::test(flavor = "multi_thread")]
1499        async fn test_register_skill_with_tool_registers_tool() {
1500            let client = make_client();
1501            let runtime = AgentBuilder::new(client)
1502                .register_skill(ToolSkill)
1503                .build()
1504                .unwrap();
1505            assert!(runtime_tool_names(&runtime).contains(&"skill_provided_tool".to_string()));
1506        }
1507
1508        #[tokio::test(flavor = "multi_thread")]
1509        async fn test_register_skill_tool_name_conflict() {
1510            let client = make_client();
1511            let result = AgentBuilder::new(client)
1512                .register_tool(NamedTool("skill_provided_tool"))
1513                .register_skill(ToolSkill)
1514                .build();
1515            let err = result.err().unwrap();
1516            assert!(format!("{err}").contains("Tool name conflict"));
1517        }
1518
1519        #[tokio::test(flavor = "multi_thread")]
1520        async fn test_skill_detail_tool_factory_registers_tool() {
1521            let client = make_client();
1522            let factory: SkillDetailToolFactory = Arc::new(|_skills, name| {
1523                assert_eq!(name, "get_skill_detail");
1524                Arc::new(NamedTool("detail_tool"))
1525            });
1526            let runtime = AgentBuilder::new(client)
1527                .register_skill(TestSkill)
1528                .with_skill_detail_tool_factory(factory)
1529                .build()
1530                .unwrap();
1531            assert!(runtime_tool_names(&runtime).contains(&"detail_tool".to_string()));
1532        }
1533
1534        #[tokio::test(flavor = "multi_thread")]
1535        async fn test_list_skills_tool_factory_registers_tool() {
1536            let client = make_client();
1537            let factory: ListSkillsToolFactory =
1538                Arc::new(|_registry| Arc::new(NamedTool("list_skills_tool")));
1539            let runtime = AgentBuilder::new(client)
1540                .register_skill(TestSkill)
1541                .with_list_skills_tool_factory(factory)
1542                .build()
1543                .unwrap();
1544            assert!(runtime_tool_names(&runtime).contains(&"list_skills_tool".to_string()));
1545        }
1546
1547        #[tokio::test(flavor = "multi_thread")]
1548        async fn test_disable_skill_prompt_injection_builds() {
1549            let client = make_client();
1550            let runtime = AgentBuilder::new(client)
1551                .register_skill(ToolSkill)
1552                .disable_skill_prompt_injection()
1553                .build()
1554                .unwrap();
1555            // Tool still registered; prompt injection skipped.
1556            assert!(runtime_tool_names(&runtime).contains(&"skill_provided_tool".to_string()));
1557        }
1558    }
1559}