Skip to main content

oxi_agent/
tools.rs

1#![allow(unused_doc_comments)]
2/// Agent tools system
3/// This module provides the tool abstraction layer and built-in tools.
4use crate::types::ToolDefinition;
5use async_trait::async_trait;
6use serde_json::Value;
7use std::fmt;
8use std::future::Future;
9use std::path::{Path, PathBuf};
10use std::pin::Pin;
11use std::sync::Arc;
12use tokio::sync::oneshot;
13
14// ═══════════════════════════════════════════════════════════════════════════
15// Capability traits — lightweight interfaces tools need, implemented by the
16// composition root (oxi-cli) bridging to SDK ports. oxi-agent does NOT depend
17// on oxi-sdk, so these are defined here.
18// ═══════════════════════════════════════════════════════════════════════════
19
20/// A single memory item returned by [`MemoryBackend`].
21#[derive(Debug, Clone, serde::Serialize)]
22pub struct MemoryItem {
23    /// Unique identifier.
24    pub id: String,
25    /// Memory kind: "fact", "preference", "context", "summary".
26    pub kind: String,
27    /// The memory content text.
28    pub content: String,
29    /// Project/scope identifier.
30    pub subject: String,
31}
32
33/// Memory backend for the `memory_*` tools. The composition root implements
34/// this, bridging to `oxi_sdk::ports::MemoryStore` + `EmbeddingProvider`.
35pub trait MemoryBackend: Send + Sync + std::fmt::Debug {
36    fn put<'a>(
37        &'a self,
38        content: &'a str,
39        kind: &'a str,
40        subject: &'a str,
41    ) -> Pin<Box<dyn Future<Output = Result<String, ToolError>> + Send + 'a>>;
42    fn search<'a>(
43        &'a self,
44        query: &'a str,
45        k: usize,
46    ) -> Pin<Box<dyn Future<Output = Result<Vec<MemoryItem>, ToolError>> + Send + 'a>>;
47    fn list<'a>(
48        &'a self,
49        subject: &'a str,
50    ) -> Pin<Box<dyn Future<Output = Result<Vec<MemoryItem>, ToolError>> + Send + 'a>>;
51    fn delete<'a>(
52        &'a self,
53        id: &'a str,
54    ) -> Pin<Box<dyn Future<Output = Result<(), ToolError>> + Send + 'a>>;
55}
56
57pub struct ResolvedContent {
58    /// The resolved text content.
59    pub content: String,
60    /// MIME type: "text/markdown", "application/json", "text/plain".
61    pub content_type: String,
62    /// True if the content is uneditable (suppresses hashline anchors).
63    pub immutable: bool,
64}
65
66/// URL resolver for internal protocol schemes. The composition root
67/// implements this, bridging to `oxi_sdk::ports::InternalUrlRouter`.
68pub trait UrlResolver: Send + Sync + std::fmt::Debug {
69    fn can_resolve(&self, input: &str) -> bool;
70    fn resolve<'a>(
71        &'a self,
72        uri: &'a str,
73    ) -> Pin<Box<dyn Future<Output = Result<ResolvedContent, ToolError>> + Send + 'a>>;
74}
75
76/// Todo state access capability. Implemented by the composition root
77/// (oxi-cli) bridging to the session-scoped todo state. Used by the
78/// `todo` agent tool and the TUI sticky panel.
79pub trait TodoStateProvider: Send + Sync + std::fmt::Debug {
80    /// Return a snapshot of the current phase list (read-only, for TUI).
81    fn get_phases(&self) -> Vec<crate::tools::todo::TodoPhase>;
82
83    /// Apply a sequence of todo ops, returning the updated state, the
84    /// newly-completed transitions (for strikethrough animation), and
85    /// any error messages from ambiguous op references.
86    fn apply_ops<'a>(
87        &'a self,
88        ops: Vec<crate::tools::todo::TodoOp>,
89    ) -> Pin<
90        Box<dyn Future<Output = Result<crate::tools::todo::TodoUpdateResult, String>> + Send + 'a>,
91    >;
92}
93
94// ── Agent Hub capability (⑥) ──────────────────────────────────────────
95
96/// Agent kind for Hub display.
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub enum AgentKind {
99    /// Main conversation agent.
100    Main,
101    /// Task-spawned sub-agent.
102    Task,
103    /// Observation-only advisor.
104    Advisor,
105}
106
107/// Hub display status.
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub enum AgentHubStatus {
110    /// Currently executing.
111    Running,
112    /// Finished, idle.
113    Idle,
114    /// Parked (memory retained, not running).
115    Parked,
116    /// Abnormal termination.
117    Aborted,
118}
119
120/// Read-only agent info for Hub display.
121#[derive(Debug, Clone)]
122pub struct AgentInfo {
123    /// Unique identifier.
124    pub id: String,
125    /// Display name.
126    pub display_name: String,
127    /// Agent kind.
128    pub kind: AgentKind,
129    /// Current status.
130    pub status: AgentHubStatus,
131    /// Current task description (if any).
132    pub current_task: Option<String>,
133}
134
135/// Agent pool access capability. Implemented by the composition root
136/// to expose live sub-agent info to the Hub overlay and todo matching.
137pub trait AgentPoolProvider: Send + Sync + std::fmt::Debug {
138    /// List all known agents (main + sub-agents).
139    fn list_agents(&self) -> Vec<AgentInfo>;
140    /// Get a specific agent by ID.
141    fn get_agent(&self, id: &str) -> Option<AgentInfo>;
142}
143
144// ── LSP capability (⑧) ────────────────────────────────────────────────
145
146/// LSP action enum — the 14 operations the `lsp` tool supports.
147#[derive(Debug, Clone)]
148pub enum LspAction {
149    /// Get diagnostics for a file.
150    Diagnostics { file: String },
151    /// Go to definition.
152    Definition {
153        file: String,
154        line: u32,
155        symbol: Option<String>,
156    },
157    /// Find references.
158    References {
159        file: String,
160        line: u32,
161        symbol: Option<String>,
162    },
163    /// Hover info.
164    Hover {
165        file: String,
166        line: u32,
167        symbol: Option<String>,
168    },
169    /// Rename symbol.
170    Rename {
171        file: String,
172        line: u32,
173        symbol: String,
174        new_name: String,
175        apply: bool,
176    },
177    /// Get workspace/document symbols.
178    Symbols { file: String, query: Option<String> },
179    /// Get server status.
180    Status,
181}
182
183/// LSP access capability. Implemented by an `oxi-lsp` crate (feature-gated)
184/// or stubbed with `None` when LSP is disabled.
185pub trait LspProvider: Send + Sync + std::fmt::Debug {
186    /// Execute an LSP action and return formatted text output.
187    fn execute_action<'a>(
188        &'a self,
189        action: &'a LspAction,
190    ) -> Pin<Box<dyn Future<Output = Result<String, ToolError>> + Send + 'a>>;
191}
192
193/// Context passed to tools at execution time.
194///
195/// This allows tools to operate on a specific workspace without being
196/// rebuilt. When `root_dir` is `Some`, tools use it as their base directory.
197/// When `None`, tools should fall back to `workspace_dir`.
198#[derive(Clone)]
199pub struct ToolContext {
200    /// Primary workspace directory (used when root_dir is None).
201    pub workspace_dir: PathBuf,
202    /// Optional explicit root directory for file tools.
203    /// Takes priority over workspace_dir if present.
204    pub root_dir: Option<PathBuf>,
205    /// Session identifier for logging/tracing.
206    pub session_id: Option<String>,
207    /// Snapshot store for hashline tag emission/validation.
208    /// When `None`, hashline edit mode is unavailable.
209    pub snapshot_store: Option<Arc<dyn oxi_hashline::SnapshotStore>>,
210    /// Memory backend for `memory_*` tools.
211    /// When `None`, memory tools return an error.
212    pub memory: Option<Arc<dyn MemoryBackend>>,
213    /// URL resolver for internal protocol schemes (`issue://`, `pr://`, etc.).
214    /// When `None`, URL-prefixed paths are treated as regular file paths.
215    pub url_resolver: Option<Arc<dyn UrlResolver>>,
216    /// Todo state for the `todo` agent tool.
217    /// When `None`, the `todo` tool returns an error.
218    pub todo: Option<Arc<dyn TodoStateProvider>>,
219    /// Agent pool for Hub display and todo sub-agent matching.
220    pub agent_pool: Option<Arc<dyn AgentPoolProvider>>,
221    /// LSP provider for the `lsp` tool.
222    pub lsp: Option<Arc<dyn LspProvider>>,
223}
224
225impl fmt::Debug for ToolContext {
226    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227        f.debug_struct("ToolContext")
228            .field("workspace_dir", &self.workspace_dir)
229            .field("root_dir", &self.root_dir)
230            .field("session_id", &self.session_id)
231            .field(
232                "snapshot_store",
233                &self.snapshot_store.as_ref().map(|_| "<dyn SnapshotStore>"),
234            )
235            .field(
236                "memory",
237                &self.memory.as_ref().map(|_| "<dyn MemoryBackend>"),
238            )
239            .field(
240                "url_resolver",
241                &self.url_resolver.as_ref().map(|_| "<dyn UrlResolver>"),
242            )
243            .finish()
244    }
245}
246
247impl ToolContext {
248    /// Create a new context with the given workspace.
249    pub fn new(workspace_dir: impl Into<PathBuf>) -> Self {
250        Self {
251            workspace_dir: workspace_dir.into(),
252            root_dir: None,
253            session_id: None,
254            snapshot_store: None,
255            memory: None,
256            url_resolver: None,
257            todo: None,
258            agent_pool: None,
259            lsp: None,
260        }
261    }
262
263    /// Get the effective root directory.
264    /// Returns root_dir if set, otherwise workspace_dir.
265    pub fn root(&self) -> &Path {
266        self.root_dir.as_deref().unwrap_or(&self.workspace_dir)
267    }
268
269    /// Set a session ID.
270    pub fn with_session(mut self, session_id: impl Into<String>) -> Self {
271        self.session_id = Some(session_id.into());
272        self
273    }
274
275    /// Set an explicit root directory.
276    pub fn with_root(mut self, root_dir: impl Into<PathBuf>) -> Self {
277        self.root_dir = Some(root_dir.into());
278        self
279    }
280
281    /// Set the snapshot store (enables hashline edit mode).
282    pub fn with_snapshot_store(mut self, store: Arc<dyn oxi_hashline::SnapshotStore>) -> Self {
283        self.snapshot_store = Some(store);
284        self
285    }
286
287    /// Set the memory backend (enables memory tools).
288    pub fn with_memory(mut self, memory: Arc<dyn MemoryBackend>) -> Self {
289        self.memory = Some(memory);
290        self
291    }
292
293    /// Set the URL resolver (enables internal URL dispatch).
294    pub fn with_url_resolver(mut self, resolver: Arc<dyn UrlResolver>) -> Self {
295        self.url_resolver = Some(resolver);
296        self
297    }
298
299    /// Set the todo state (enables the `todo` agent tool).
300    pub fn with_todo(mut self, todo: Arc<dyn TodoStateProvider>) -> Self {
301        self.todo = Some(todo);
302        self
303    }
304}
305
306impl Default for ToolContext {
307    fn default() -> Self {
308        Self {
309            workspace_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
310            root_dir: None,
311            session_id: None,
312            snapshot_store: None,
313            memory: None,
314            url_resolver: None,
315            todo: None,
316            agent_pool: None,
317            lsp: None,
318        }
319    }
320}
321
322/// Result type for tool execution
323pub type ToolError = String;
324
325/// Result of tool execution
326#[derive(Debug)]
327pub struct AgentToolResult {
328    /// pub.
329    pub success: bool,
330    /// pub.
331    pub output: String,
332    /// pub.
333    pub metadata: Option<serde_json::Value>,
334    /// Optional content blocks (e.g., image blocks) to include in the tool result message.
335    /// When present, these are used as the content of the ToolResultMessage instead of
336    /// wrapping `output` in a Text block.
337    pub content_blocks: Option<Vec<oxi_ai::ContentBlock>>,
338    /// When `true`, signals that the agent loop should terminate after this batch
339    /// of tool calls completes.  Defaults to `false` so that the loop continues
340    /// unless a tool explicitly opts-in to termination.
341    pub terminate: bool,
342}
343
344impl AgentToolResult {
345    /// Creates a successful tool result with the given output text.
346    pub fn success(output: impl Into<String>) -> Self {
347        Self {
348            success: true,
349            output: output.into(),
350            metadata: None,
351            content_blocks: None,
352            terminate: false,
353        }
354    }
355
356    /// Creates an error tool result with the given error message.
357    pub fn error(output: impl Into<String>) -> Self {
358        Self {
359            success: false,
360            output: output.into(),
361            metadata: None,
362            content_blocks: None,
363            terminate: false,
364        }
365    }
366
367    /// Attaches structured metadata (JSON) to this result.
368    pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
369        self.metadata = Some(metadata);
370        self
371    }
372
373    /// Attaches rich content blocks (images, code, etc.) to this result.
374    pub fn with_content_blocks(mut self, blocks: Vec<oxi_ai::ContentBlock>) -> Self {
375        self.content_blocks = Some(blocks);
376        self
377    }
378
379    /// Mark this result as requesting agent-loop termination.
380    pub fn with_terminate(mut self) -> Self {
381        self.terminate = true;
382        self
383    }
384}
385
386impl fmt::Display for AgentToolResult {
387    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
388        write!(f, "{}", self.output)
389    }
390}
391
392/// Callback type for progress updates
393pub type ProgressCallback = Arc<dyn Fn(String) + Send + Sync>;
394
395/// Tool execution mode for parallel safety.
396#[derive(Debug, Clone)]
397pub enum ToolExecutionMode {
398    /// Safe to run in parallel with any other tool
399    ParallelSafe,
400    /// Must run sequentially — no parallel execution
401    SequentialOnly,
402    /// Mutates a specific file — file_mutation_queue serializes same-file access
403    MutatesFile(std::path::PathBuf),
404    /// Read-only — always parallel safe
405    ReadOnly,
406}
407
408/// Render output for TUI visualization.
409#[derive(Debug, Clone)]
410pub struct RenderOutput {
411    /// Rendered text content (markdown or plain)
412    pub content: String,
413    /// Whether to show collapsed by default
414    pub collapsed: bool,
415    /// Optional summary text for TUI footer
416    pub summary: Option<String>,
417}
418
419/// Core trait for all agent tools
420#[async_trait]
421pub trait AgentTool: Send + Sync {
422    /// Tool name (used in function calls)
423    fn name(&self) -> &str;
424
425    /// Human-readable label
426    fn label(&self) -> &str;
427
428    /// Description for the model
429    fn description(&self) -> &str;
430
431    /// JSON Schema for parameters
432    fn parameters_schema(&self) -> Value;
433
434    /// Whether this tool is essential (cannot be disabled).
435    /// Essential tools: read, write, edit, bash, grep, find, ls
436    /// Optional tools: web_search, github, subagent, etc.
437    fn essential(&self) -> bool {
438        false
439    }
440
441    /// Execute the tool with the given tool call ID and parameters.
442    ///
443    /// The `ctx` parameter provides workspace information. File tools should
444    /// use `ctx.root()` to get the effective directory. Custom tools can use
445    /// `ctx.workspace_dir` for workspace-relative operations.
446    ///
447    /// # Examples
448    ///
449    /// ```ignore
450    /// use oxi_agent::{AgentTool, AgentToolResult, ToolContext};
451    /// use serde_json::json;
452    /// struct MyTool;
453    ///
454    /// #[async_trait]
455    /// impl AgentTool for MyTool {
456    ///     fn name(&self) -> &str { "my_tool" }
457    ///     fn label(&self) -> &str { "My Tool" }
458    ///     fn description(&self) -> &str { "A custom tool" }
459    ///     fn parameters_schema(&self) -> Value { json!({
460    ///         "type": "object",
461    ///         "properties": {}
462    ///     }) }
463    ///
464    ///     async fn execute(&self, tool_call_id: &str, params: Value, _signal: Option<oneshot::Receiver<()>>, ctx: &ToolContext) -> Result<AgentToolResult, String> {
465    ///         println!("Tool '{}' called with params: {:?}, workspace: {:?}", tool_call_id, params, ctx.workspace_dir);
466    ///         Ok(AgentToolResult::success("Done!"))
467    ///     }
468    /// }
469    /// ```
470    async fn execute(
471        &self,
472        tool_call_id: &str,
473        params: Value,
474        signal: Option<oneshot::Receiver<()>>,
475        ctx: &ToolContext,
476    ) -> Result<AgentToolResult, ToolError>;
477
478    /// Called with progress updates during execution.
479    /// Tools can override this to emit streaming updates.
480    fn on_progress(&self, _callback: ProgressCallback) {
481        // Default no-op
482    }
483
484    /// Structured browse progress callback for browser tool context enrichment.
485    /// Default implementation is no-op. Only browse tools override this to
486    /// register a callback that enriches `ToolCallContext` with structured
487    /// data from `BrowseProgress` events.
488    fn on_browse_progress(&self, _callback: crate::tools::browse::BrowseProgressCallback) {}
489
490    /// Custom rendering for tool call (TUI visualization).
491    /// Return None to use the default tool_renderer.rs formatter.
492    fn render_call(&self, _params: &serde_json::Value) -> Option<RenderOutput> {
493        None
494    }
495
496    /// Custom rendering for tool result (TUI visualization).
497    /// Return None to use the default tool_renderer.rs formatter.
498    fn render_result(&self, _result: &AgentToolResult) -> Option<RenderOutput> {
499        None
500    }
501
502    /// Execution mode for parallel safety.
503    /// Defaults to ParallelSafe. Override for file-mutating or sequential tools.
504    fn execution_mode(&self) -> ToolExecutionMode {
505        ToolExecutionMode::ParallelSafe
506    }
507
508    /// Return the current active tab ID, if this tool manages browser tabs.
509    /// Defaults to `None`. Browser tools override this to return the tab ID
510    /// of the currently-open tab during execution, so the agent loop can
511    /// populate `ToolExecutionUpdate.tab_id`.
512    fn current_tab_id(&self) -> Option<uuid::Uuid> {
513        None
514    }
515
516    /// Receive a shared slot where the tool can write the current tab ID.
517    /// The agent loop creates the slot and passes it before `on_progress`;
518    /// the tool writes `Some(tab_id)` when it opens a tab and `None` when
519    /// it closes it. Defaults to a no-op — only tab-aware tools override.
520    fn set_tab_id_slot(&self, _slot: Arc<parking_lot::Mutex<Option<uuid::Uuid>>>) {}
521
522    /// Convert to ToolDefinition
523    fn to_definition(&self) -> ToolDefinition {
524        ToolDefinition {
525            name: self.name().to_string(),
526            description: self.description().to_string(),
527            input_schema: serde_json::from_value(self.parameters_schema()).unwrap_or_default(),
528        }
529    }
530}
531
532// Built-in tools
533/// Bash shell execution tool.
534pub mod bash;
535/// Browser tools (engine abstraction always compiled).
536pub mod browse;
537/// Conventional-commit tool (deterministic scope + LLM analysis).
538pub mod commit;
539/// Context7 documentation tools.
540pub mod context7;
541/// In-place file edit tool.
542pub mod edit;
543/// Diff-based edit helpers.
544pub mod edit_diff;
545/// Serialised file-mutation queue.
546pub mod file_mutation_queue;
547/// File-fsystem find tool.
548pub mod find;
549/// Image generation tool (OpenRouter API).
550pub mod generate_image;
551/// GitHub integration tool (gh CLI-based).
552pub mod github;
553/// GitHub repository search tool (legacy REST API).
554pub mod github_search;
555/// Content search (grep) tool.
556pub mod grep;
557/// TokioHashlineFs — tokio::fs-backed HashlineFs implementation.
558pub mod hashline_fs;
559/// Shared HTTP client singleton.
560pub mod http_client;
561/// Directory listing tool.
562pub mod ls;
563/// LSP tool (requires LspProvider capability).
564pub mod lsp;
565/// Memory edit tool — update or delete a memory item.
566pub mod memory_edit;
567/// Memory recall tool — semantic search over stored memories.
568pub mod memory_recall;
569/// Memory reflect tool — persist a session summary to memory.
570pub mod memory_reflect;
571/// Memory retain tool — persist a memory item to the backend.
572pub mod memory_retain;
573/// Path security (traversal protection).
574pub mod path_security;
575/// Path manipulation utilities.
576pub mod path_utils;
577/// Questionnaire tool — interactive multi-question TUI overlay.
578pub mod questionnaire;
579/// File reading tool.
580pub mod read;
581/// Rendering utilities for tool output.
582pub mod render_utils;
583/// Search result cache and get_search_results tool.
584pub mod search_cache;
585/// Sub-agent delegation tool.
586pub mod subagent;
587/// Phased todo tool (init/start/done/drop/rm/append/view).
588pub mod todo;
589/// Tool definition wrapper helpers.
590pub mod tool_definition_wrapper;
591/// Output truncation helpers.
592pub mod truncate;
593/// Multi-engine web search tool (oxibrowser search module).
594pub mod web_search;
595/// File writing tool.
596pub mod write;
597
598// Re-export for convenience
599pub use bash::BashTool;
600pub use edit::EditTool;
601pub use find::FindTool;
602pub use grep::GrepTool;
603pub use ls::LsTool;
604pub use read::ReadTool;
605// pub use search_cache;
606
607pub use crate::mcp::McpTool;
608pub use commit::CommitTool;
609pub use context7::{Context7QueryDocsTool, Context7ResolveLibraryIdTool};
610pub use memory_edit::MemoryEditTool;
611pub use memory_recall::MemoryRecallTool;
612pub use memory_reflect::MemoryReflectTool;
613pub use memory_retain::MemoryRetainTool;
614pub use questionnaire::{QuestionnaireBridge, QuestionnaireTool};
615pub use subagent::SubagentTool;
616pub use write::WriteTool;
617
618/// Tool registry for managing available tools
619#[derive(Clone)]
620pub struct ToolRegistry {
621    tools: Arc<parking_lot::RwLock<std::collections::HashMap<String, Arc<dyn AgentTool>>>>,
622    /// Optional MCP manager, set by `with_builtins_cwd()` so the TUI and
623    /// other consumers can reach the live MCP state (Phase 2+).
624    mcp_manager: Arc<parking_lot::RwLock<Option<Arc<crate::mcp::McpManager>>>>,
625}
626
627impl Default for ToolRegistry {
628    fn default() -> Self {
629        Self::new()
630    }
631}
632
633impl ToolRegistry {
634    /// Creates an empty tool registry.
635    pub fn new() -> Self {
636        Self {
637            tools: Arc::new(parking_lot::RwLock::new(std::collections::HashMap::new())),
638            mcp_manager: Arc::new(parking_lot::RwLock::new(None)),
639        }
640    }
641
642    /// Attach an `McpManager` to this registry. Replaces any previous one.
643    pub fn set_mcp_manager(&self, mgr: Arc<crate::mcp::McpManager>) {
644        *self.mcp_manager.write() = Some(mgr);
645    }
646
647    /// Get the attached `McpManager`, if any.
648    pub fn mcp_manager(&self) -> Option<Arc<crate::mcp::McpManager>> {
649        self.mcp_manager.read().clone()
650    }
651
652    /// Register a tool
653    pub fn register(&self, tool: impl AgentTool + 'static) {
654        let name = tool.name().to_string();
655        self.tools.write().insert(name, Arc::new(tool));
656    }
657
658    /// Register a tool that is already wrapped in an `Arc`.
659    /// This is the primary path for extensions that produce `Arc<dyn AgentTool>`.
660    pub fn register_arc(&self, tool: Arc<dyn AgentTool>) {
661        let name = tool.name().to_string();
662        self.tools.write().insert(name, tool);
663    }
664
665    /// Get a tool by name
666    pub fn get(&self, name: &str) -> Option<Arc<dyn AgentTool>> {
667        self.tools.read().get(name).cloned()
668    }
669
670    /// Unregister a tool by name.
671    /// Returns `true` if the tool was present and removed.
672    pub fn unregister(&self, name: &str) -> bool {
673        self.tools.write().remove(name).is_some()
674    }
675
676    /// List all registered tool names
677    pub fn names(&self) -> Vec<String> {
678        self.tools.read().keys().cloned().collect()
679    }
680
681    /// Get all tool definitions
682    pub fn definitions(&self) -> Vec<ToolDefinition> {
683        self.tools
684            .read()
685            .values()
686            .map(|t| t.to_definition())
687            .collect()
688    }
689
690    /// Get all tools as a slice
691    pub fn get_tools(&self) -> Vec<Arc<dyn AgentTool>> {
692        self.tools.read().values().cloned().collect()
693    }
694
695    /// Check whether all tools in `required` are registered.
696    ///
697    /// Useful for validating program/module dependencies before execution.
698    ///
699    /// # Example
700    ///
701    /// ```
702    /// use oxi_agent::ToolRegistry;
703    /// let registry = ToolRegistry::new();
704    /// assert!(!registry.has_all(&["read", "write"]));
705    /// ```
706    pub fn has_all(&self, required: &[&str]) -> bool {
707        let tools = self.tools.read();
708        required.iter().all(|name| tools.contains_key(*name))
709    }
710
711    /// Return the subset of `required` tool names that are **not** registered.
712    ///
713    /// # Example
714    ///
715    /// ```
716    /// use oxi_agent::ToolRegistry;
717    /// let registry = ToolRegistry::new();
718    /// let missing = registry.missing(&["read", "exec", "nonexistent"]);
719    /// assert_eq!(missing, vec!["read", "exec", "nonexistent"]);
720    /// ```
721    pub fn missing<'a>(&self, required: &[&'a str]) -> Vec<&'a str> {
722        let tools = self.tools.read();
723        required
724            .iter()
725            .filter(|name| !tools.contains_key(**name))
726            .copied()
727            .collect()
728    }
729
730    /// Create a registry with all built-in tools
731    ///
732    /// # Examples
733    ///
734    /// ```
735    /// use oxi_agent::ToolRegistry;
736    /// let registry = ToolRegistry::with_builtins();
737    /// let tools = registry.names();
738    /// assert!(tools.contains(&"read".to_string()));
739    /// assert!(tools.contains(&"write".to_string()));
740    /// assert!(tools.contains(&"bash".to_string()));
741    /// ```
742    pub fn with_builtins() -> Self {
743        Self::with_builtins_cwd(PathBuf::from("."), &[])
744    }
745
746    /// Create a registry with all built-in tools, using the given cwd.
747    ///
748    /// Pass `disabled_tools` to selectively disable built-in tools
749    /// (e.g. `["web_search", "github_search"]` for a minimal setup).
750    pub fn with_builtins_cwd(cwd: PathBuf, disabled_tools: &[String]) -> Self {
751        let registry = Self::new();
752        let disabled: std::collections::HashSet<&str> =
753            disabled_tools.iter().map(|s| s.as_str()).collect();
754
755        // Helper to create shared cache on demand
756        let cache_once: std::cell::OnceCell<Arc<search_cache::SearchCache>> =
757            std::cell::OnceCell::new();
758
759        // MCP: use OnceCell to avoid re-creating McpManager on repeated calls
760        let mcp_once: std::cell::OnceCell<Arc<crate::mcp::McpManager>> = std::cell::OnceCell::new();
761        let mcp_manager = mcp_once.get_or_init(crate::mcp::McpManager::spawn).clone();
762
763        // Register all builtin tools — essential ones ignore disabled list
764        let mut all_tools: Vec<Box<dyn AgentTool>> = vec![
765            Box::new(ReadTool::with_cwd(cwd.clone())),
766            Box::new(WriteTool::with_cwd(cwd.clone())),
767            Box::new(EditTool::with_cwd(cwd.clone())),
768            Box::new(BashTool::with_cwd(cwd.clone())),
769            Box::new(GrepTool::with_cwd(cwd.clone())),
770            Box::new(FindTool::with_cwd(cwd.clone())),
771            Box::new(LsTool::with_cwd(cwd.clone())),
772            Box::new(web_search::WebSearchTool::new(
773                cache_once
774                    .get_or_init(|| Arc::new(search_cache::SearchCache::new()))
775                    .clone(),
776            )),
777            Box::new(search_cache::GetSearchResultsTool::new(
778                cache_once
779                    .get_or_init(|| Arc::new(search_cache::SearchCache::new()))
780                    .clone(),
781            )),
782            Box::new(github::GitHubTool::new(
783                cache_once
784                    .get_or_init(|| Arc::new(search_cache::SearchCache::new()))
785                    .clone(),
786            )),
787            Box::new(SubagentTool::with_cwd(cwd.clone())),
788            Box::new(todo::TodoTool),
789            Box::new(memory_recall::MemoryRecallTool),
790            Box::new(memory_reflect::MemoryReflectTool),
791            Box::new(memory_retain::MemoryRetainTool),
792            Box::new(memory_edit::MemoryEditTool),
793        ];
794
795        all_tools.push(Box::new(crate::mcp::McpTool::new(mcp_manager.clone())));
796
797        // Phase 3: register direct MCP tools from the metadata cache.
798        for def in mcp_manager.direct_tools_from_cache() {
799            all_tools.push(Box::new(crate::mcp::McpDirectTool::new(
800                mcp_manager.clone(),
801                def,
802            )));
803        }
804
805        // Remember the manager on the registry so the TUI can reach it.
806        registry.set_mcp_manager(mcp_manager);
807
808        all_tools.push(Box::new(context7::Context7ResolveLibraryIdTool::new()));
809        all_tools.push(Box::new(context7::Context7QueryDocsTool::new()));
810        all_tools.push(Box::new(generate_image::GenerateImageTool::new()));
811        all_tools.push(Box::new(commit::CommitTool::unconfigured()));
812
813        for tool in all_tools {
814            if tool.essential() || !disabled.contains(tool.name()) {
815                // web_search ↔ get_search_results coupling
816                if tool.name() == "get_search_results" && disabled.contains("web_search") {
817                    continue;
818                }
819                registry.register_arc(Arc::from(tool));
820            }
821        }
822
823        registry
824    }
825
826    /// Extend this registry with all tools from another registry.
827    ///
828    /// Useful for composing tool sets from multiple sources
829    /// (e.g., coding tools + kernel tools + browser tools).
830    ///
831    /// # Example
832    ///
833    /// ```ignore
834    /// let base = ToolRegistry::new();
835    /// base.extend_from(&other_registry);
836    /// ```
837    pub fn extend_from(&self, other: &ToolRegistry) {
838        for name in other.names() {
839            if let Some(tool) = other.get(&name) {
840                self.register_arc(tool);
841            }
842        }
843    }
844
845    /// Create registry with selected builtins only.
846    pub fn with_selected_tools(cwd: PathBuf, names: &[&str]) -> Self {
847        let full = Self::with_builtins_cwd(cwd, &[]);
848        let registry = Self::new();
849        let set: std::collections::HashSet<&str> = names.iter().copied().collect();
850        for name in full.names() {
851            if set.contains(name.as_str())
852                && let Some(tool) = full.get(&name)
853            {
854                registry.register_arc(tool);
855            }
856        }
857        registry
858    }
859}