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