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