Skip to main content

agent_works/
builder.rs

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/// 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 StreamClient>,
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}
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    /// Enable multi-agent support with the given configuration.
91    ///
92    /// Also sets the tool factory to create the 6 multi-agent tools.
93    /// Callers should use `phi_kernel_tools::multi_agent::create_all_tools` as the factory.
94    #[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    /// Disable multi-agent support.
101    ///
102    /// Removes any previously set multi-agent configuration. No multi-agent tools
103    /// will be registered and the system prompt will not mention multi-agent capabilities.
104    #[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    /// Set a custom factory for creating multi-agent tools.
112    ///
113    /// The factory receives the `MultiAgentRuntime` and returns the tools to register.
114    /// If not set but multi-agent is enabled, no tools are registered (caller must
115    /// set this for multi-agent to work).
116    #[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    /// Set a custom factory for creating the skill detail tool.
123    ///
124    /// The factory receives the skill list and tool name, and returns the tool.
125    /// If not set but skills are registered, no detail tool is added.
126    #[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    /// Set a custom factory for creating the list-skills tool.
133    ///
134    /// The factory receives the SkillRegistry and returns the tool.
135    #[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    /// Conditionally apply a transformation when `value` is `Some`.
333    ///
334    /// This is a convenience for option-chaining builder patterns:
335    ///
336    /// ```ignore
337    /// builder.apply_if(args.thinking_budget, |b, budget| b.thinking_budget(budget))
338    /// ```
339    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    // ── Build ──
377
378    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        // Inject default guard if none was set by the consumer.
392        // Uses the same LLM client for the judge (enabled by default).
393        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        // Inject multi-agent prompt before build
417        #[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        // Post-build: register multi-agent tools if enabled and factory is set
430        #[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    /// Build the runtime with skill support.
449    ///
450    /// # Runtime requirement
451    ///
452    /// This method uses [`tokio::task::block_in_place`] to populate the skill
453    /// registry from a synchronous context. It **requires** a multi-threaded
454    /// tokio runtime. Calling it on a `#[tokio::main]` single-threaded
455    /// (`current_thread`) runtime will panic.
456    ///
457    /// The phi-agent CLI and all examples use the default multi-threaded runtime,
458    /// so this is safe in practice.
459    #[cfg(feature = "skill")]
460    fn build_with_skills(mut self) -> AgentResult<AgentRuntime> {
461        // Inject default guard if none was set by the consumer.
462        // Uses the same LLM client for the judge (enabled by default).
463        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        // Process skills
488        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            // Use injected factory if available, otherwise skip — prompt-injection
525            // mode uses read_file instead of a dedicated detail tool.
526            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            // Create SkillRegistry and populate it for the list-skills tool
532            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        // Inject multi-agent prompt
547        #[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        // Post-build: register multi-agent tools
560        #[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/// Set up the MultiAgentRuntime, event bridge, and register tools on an already-built runtime.
580///
581/// # Safety / Runtime Requirement
582///
583/// This function uses [`tokio::task::block_in_place`] to register tools synchronously.
584/// It **requires** a multi-threaded tokio runtime. Calling it on a
585/// `#[tokio::main]` single-threaded (`current_thread`) runtime will panic.
586///
587/// The phi-agent CLI and all examples use the default multi-threaded runtime,
588/// so this is safe in practice.
589#[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    // Set parent session manager for fork_history support
616    ma_runtime.set_session_manager(Arc::new(runtime.session_manager().clone()));
617
618    // Set up event bridge: child events → parent event bus
619    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    // Register multi-agent tools if a factory is provided
630    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/// Build the multi-agent system prompt guidance for the main agent.
647#[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
681/// Build the memory system prompt guidance.
682///
683/// Tells the LLM how to use the file-based persistent memory system.
684/// Memory is stored as markdown files — the LLM uses `read_file` / `write_file`
685/// to manage them, following the same convention as Claude Code Memory.
686///
687/// This is prompt-injection only — no dedicated memory tools are registered.
688/// The LLM uses the general-purpose file tools to read/write memory files.
689pub 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    // ── Stub LLM client ──
751
752    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    // ── setup_multi_agent tests ──
801
802    #[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, // no factory
819        );
820        assert!(result.is_ok());
821        let ma_runtime = result.unwrap();
822        // Verify no tools were registered (the 6 multi-agent tools are absent)
823        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            // Minimal factory returning a single fake tool
838            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        // Check the tool was registered on the runtime
873        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        // Pre-register a tool with a conflicting name
890        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        // dup_tool should NOT have been registered twice
954        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    // ── AgentBuilder factory methods ──
968
969    #[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        // Should succeed even without a factory (no tools registered)
978        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        // No multi-agent tools registered
988        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        // Create a simple factory that registers one recognizable tool
996        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            // Don't enable multi-agent — default (None) means disabled
1049            .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    // ── build_multi_agent_system_prompt ──
1065
1066    #[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    // ── without_multi_agent ──
1088
1089    #[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        // Set up a factory that would panic if called — without_multi_agent should prevent it
1095        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() // clear both
1103            .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    // ── apply_if ──
1119
1120    #[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        // system_prompt is stored in self.system_prompt; verify it was set
1126        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    // ── build_memory_system_prompt ──
1139
1140    #[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    // ── Named tool for register_tool / skill tests ──
1151
1152    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            // Prompt injection is applied during build; no tools provided.
1279            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            // Tool still registered; prompt injection skipped.
1340            assert!(runtime_tool_names(&runtime).contains(&"skill_provided_tool".to_string()));
1341        }
1342    }
1343}