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