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::path::{Path, PathBuf};
9use std::sync::Arc;
10use tokio::sync::oneshot;
11
12/// Context passed to tools at execution time.
13///
14/// This allows tools to operate on a specific workspace without being
15/// rebuilt. When `root_dir` is `Some`, tools use it as their base directory.
16/// When `None`, tools should fall back to `workspace_dir`.
17#[derive(Debug, Clone)]
18pub struct ToolContext {
19    /// Primary workspace directory (used when root_dir is None).
20    pub workspace_dir: PathBuf,
21    /// Optional explicit root directory for file tools.
22    /// Takes priority over workspace_dir if present.
23    pub root_dir: Option<PathBuf>,
24    /// Session identifier for logging/tracing.
25    pub session_id: Option<String>,
26}
27
28impl ToolContext {
29    /// Create a new context with the given workspace.
30    pub fn new(workspace_dir: impl Into<PathBuf>) -> Self {
31        Self {
32            workspace_dir: workspace_dir.into(),
33            root_dir: None,
34            session_id: None,
35        }
36    }
37
38    /// Get the effective root directory.
39    /// Returns root_dir if set, otherwise workspace_dir.
40    pub fn root(&self) -> &Path {
41        self.root_dir.as_deref().unwrap_or(&self.workspace_dir)
42    }
43
44    /// Set a session ID.
45    pub fn with_session(mut self, session_id: impl Into<String>) -> Self {
46        self.session_id = Some(session_id.into());
47        self
48    }
49
50    /// Set an explicit root directory.
51    pub fn with_root(mut self, root_dir: impl Into<PathBuf>) -> Self {
52        self.root_dir = Some(root_dir.into());
53        self
54    }
55}
56
57impl Default for ToolContext {
58    fn default() -> Self {
59        Self {
60            workspace_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
61            root_dir: None,
62            session_id: None,
63        }
64    }
65}
66
67/// Result type for tool execution
68pub type ToolError = String;
69
70/// Result of tool execution
71#[derive(Debug)]
72pub struct AgentToolResult {
73    /// pub.
74    pub success: bool,
75    /// pub.
76    pub output: String,
77    /// pub.
78    pub metadata: Option<serde_json::Value>,
79    /// Optional content blocks (e.g., image blocks) to include in the tool result message.
80    /// When present, these are used as the content of the ToolResultMessage instead of
81    /// wrapping `output` in a Text block.
82    pub content_blocks: Option<Vec<oxi_ai::ContentBlock>>,
83    /// When `true`, signals that the agent loop should terminate after this batch
84    /// of tool calls completes.  Defaults to `false` so that the loop continues
85    /// unless a tool explicitly opts-in to termination.
86    pub terminate: bool,
87}
88
89impl AgentToolResult {
90    /// Creates a successful tool result with the given output text.
91    pub fn success(output: impl Into<String>) -> Self {
92        Self {
93            success: true,
94            output: output.into(),
95            metadata: None,
96            content_blocks: None,
97            terminate: false,
98        }
99    }
100
101    /// Creates an error tool result with the given error message.
102    pub fn error(output: impl Into<String>) -> Self {
103        Self {
104            success: false,
105            output: output.into(),
106            metadata: None,
107            content_blocks: None,
108            terminate: false,
109        }
110    }
111
112    /// Attaches structured metadata (JSON) to this result.
113    pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
114        self.metadata = Some(metadata);
115        self
116    }
117
118    /// Attaches rich content blocks (images, code, etc.) to this result.
119    pub fn with_content_blocks(mut self, blocks: Vec<oxi_ai::ContentBlock>) -> Self {
120        self.content_blocks = Some(blocks);
121        self
122    }
123
124    /// Mark this result as requesting agent-loop termination.
125    pub fn with_terminate(mut self) -> Self {
126        self.terminate = true;
127        self
128    }
129}
130
131impl fmt::Display for AgentToolResult {
132    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133        write!(f, "{}", self.output)
134    }
135}
136
137/// Callback type for progress updates
138pub type ProgressCallback = Arc<dyn Fn(String) + Send + Sync>;
139
140/// Structured progress event for tool execution streaming.
141#[derive(Debug, Clone)]
142pub enum ToolProgress {
143    /// Status message (progress in progress)
144    Status {
145        /// The status text.
146        message: String,
147    },
148    /// Partial output (e.g., bash stdout streaming)
149    PartialOutput {
150        /// The partial output text.
151        output: String,
152        /// Whether this came from stderr.
153        is_error: bool,
154    },
155    /// Progress percentage (0.0 - 1.0)
156    Percentage {
157        /// Current progress value.
158        current: f64,
159        /// Optional total value.
160        total: Option<f64>,
161        /// Optional human-readable message.
162        message: Option<String>,
163    },
164    /// File operation progress
165    FileOperation {
166        /// Type of file operation.
167        operation: FileOp,
168        /// File path being operated on.
169        path: std::path::PathBuf,
170        /// Bytes processed so far.
171        bytes_processed: Option<u64>,
172        /// Total bytes to process.
173        total_bytes: Option<u64>,
174    },
175}
176
177/// File operation types for progress reporting.
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
179pub enum FileOp {
180    /// Reading a file.
181    Reading,
182    /// Writing a file.
183    Writing,
184    /// Searching file contents.
185    Searching,
186    /// Editing a file.
187    Editing,
188}
189
190/// Tool execution mode for parallel safety.
191#[derive(Debug, Clone)]
192pub enum ToolExecutionMode {
193    /// Safe to run in parallel with any other tool
194    ParallelSafe,
195    /// Must run sequentially — no parallel execution
196    SequentialOnly,
197    /// Mutates a specific file — file_mutation_queue serializes same-file access
198    MutatesFile(std::path::PathBuf),
199    /// Read-only — always parallel safe
200    ReadOnly,
201}
202
203/// Render output for TUI visualization.
204#[derive(Debug, Clone)]
205pub struct RenderOutput {
206    /// Rendered text content (markdown or plain)
207    pub content: String,
208    /// Whether to show collapsed by default
209    pub collapsed: bool,
210    /// Optional summary text for TUI footer
211    pub summary: Option<String>,
212}
213
214/// Structured progress callback (alongside existing String callback)
215pub type StructuredProgressCallback = Arc<dyn Fn(ToolProgress) + Send + Sync>;
216
217/// Core trait for all agent tools
218#[async_trait]
219pub trait AgentTool: Send + Sync {
220    /// Tool name (used in function calls)
221    fn name(&self) -> &str;
222
223    /// Human-readable label
224    fn label(&self) -> &str;
225
226    /// Description for the model
227    fn description(&self) -> &str;
228
229    /// JSON Schema for parameters
230    fn parameters_schema(&self) -> Value;
231
232    /// Whether this tool is essential (cannot be disabled).
233    /// Essential tools: read, write, edit, bash, grep, find, ls
234    /// Optional tools: web_search, github, subagent, etc.
235    fn essential(&self) -> bool {
236        false
237    }
238
239    /// Execute the tool with the given tool call ID and parameters.
240    ///
241    /// The `ctx` parameter provides workspace information. File tools should
242    /// use `ctx.root()` to get the effective directory. Custom tools can use
243    /// `ctx.workspace_dir` for workspace-relative operations.
244    ///
245    /// # Examples
246    ///
247    /// ```ignore
248    /// use oxi_agent::{AgentTool, AgentToolResult, ToolContext};
249    /// use serde_json::json;
250    /// use async_trait::async_trait;
251    ///
252    /// struct MyTool;
253    ///
254    /// #[async_trait]
255    /// impl AgentTool for MyTool {
256    ///     fn name(&self) -> &str { "my_tool" }
257    ///     fn label(&self) -> &str { "My Tool" }
258    ///     fn description(&self) -> &str { "A custom tool" }
259    ///     fn parameters_schema(&self) -> Value { json!({
260    ///         "type": "object",
261    ///         "properties": {}
262    ///     }) }
263    ///
264    ///     async fn execute(&self, tool_call_id: &str, params: Value, _signal: Option<oneshot::Receiver<()>>, ctx: &ToolContext) -> Result<AgentToolResult, String> {
265    ///         println!("Tool '{}' called with params: {:?}, workspace: {:?}", tool_call_id, params, ctx.workspace_dir);
266    ///         Ok(AgentToolResult::success("Done!"))
267    ///     }
268    /// }
269    /// ```
270    async fn execute(
271        &self,
272        tool_call_id: &str,
273        params: Value,
274        signal: Option<oneshot::Receiver<()>>,
275        ctx: &ToolContext,
276    ) -> Result<AgentToolResult, ToolError>;
277
278    /// Called with progress updates during execution.
279    /// Tools can override this to emit streaming updates.
280    fn on_progress(&self, _callback: ProgressCallback) {
281        // Default no-op
282    }
283
284    /// Structured progress callback for streaming tool execution updates.
285    /// Default implementation is no-op. Override in tools that support
286    /// structured progress (e.g., BashTool for partial output streaming).
287    fn on_structured_progress(&self, _callback: StructuredProgressCallback) {}
288
289    /// Custom rendering for tool call (TUI visualization).
290    /// Return None to use the default tool_renderer.rs formatter.
291    fn render_call(&self, _params: &serde_json::Value) -> Option<RenderOutput> {
292        None
293    }
294
295    /// Custom rendering for tool result (TUI visualization).
296    /// Return None to use the default tool_renderer.rs formatter.
297    fn render_result(&self, _result: &AgentToolResult) -> Option<RenderOutput> {
298        None
299    }
300
301    /// Execution mode for parallel safety.
302    /// Defaults to ParallelSafe. Override for file-mutating or sequential tools.
303    fn execution_mode(&self) -> ToolExecutionMode {
304        ToolExecutionMode::ParallelSafe
305    }
306
307    /// Return the current active tab ID, if this tool manages browser tabs.
308    /// Defaults to `None`. Browser tools override this to return the tab ID
309    /// of the currently-open tab during execution, so the agent loop can
310    /// populate `ToolExecutionUpdate.tab_id`.
311    fn current_tab_id(&self) -> Option<uuid::Uuid> {
312        None
313    }
314
315    /// Receive a shared slot where the tool can write the current tab ID.
316    /// The agent loop creates the slot and passes it before `on_progress`;
317    /// the tool writes `Some(tab_id)` when it opens a tab and `None` when
318    /// it closes it. Defaults to a no-op — only tab-aware tools override.
319    fn set_tab_id_slot(&self, _slot: Arc<parking_lot::Mutex<Option<uuid::Uuid>>>) {}
320
321    /// Convert to ToolDefinition
322    fn to_definition(&self) -> ToolDefinition {
323        ToolDefinition {
324            name: self.name().to_string(),
325            description: self.description().to_string(),
326            input_schema: serde_json::from_value(self.parameters_schema()).unwrap_or_default(),
327        }
328    }
329}
330
331// Built-in tools
332/// Bash shell execution tool.
333pub mod bash;
334/// Browser tools (engine abstraction always compiled).
335pub mod browse;
336/// Context7 documentation tools.
337pub mod context7;
338/// In-place file edit tool.
339pub mod edit;
340/// Diff-based edit helpers.
341pub mod edit_diff;
342/// Serialised file-mutation queue.
343pub mod file_mutation_queue;
344/// File-fsystem find tool.
345pub mod find;
346/// Image generation tool (OpenRouter API).
347pub mod generate_image;
348/// GitHub integration tool (gh CLI-based).
349pub mod github;
350/// GitHub repository search tool (legacy REST API).
351pub mod github_search;
352/// Content search (grep) tool.
353pub mod grep;
354/// Shared HTTP client singleton.
355pub mod http_client;
356/// Directory listing tool.
357pub mod ls;
358/// Path security (traversal protection).
359pub mod path_security;
360/// Path manipulation utilities.
361pub mod path_utils;
362/// Questionnaire tool — interactive multi-question TUI overlay.
363pub mod questionnaire;
364/// File reading tool.
365pub mod read;
366/// Rendering utilities for tool output.
367pub mod render_utils;
368/// Search result cache and get_search_results tool.
369pub mod search_cache;
370/// Sub-agent delegation tool.
371pub mod subagent;
372/// Tool definition wrapper helpers.
373pub mod tool_definition_wrapper;
374/// Output truncation helpers.
375pub mod truncate;
376/// Multi-engine web search tool (a3s-search library + DuckDuckGo fallback).
377pub mod web_search;
378/// File writing tool.
379pub mod write;
380
381// Re-export for convenience
382pub use bash::BashTool;
383pub use edit::EditTool;
384pub use find::FindTool;
385pub use grep::GrepTool;
386pub use ls::LsTool;
387pub use read::ReadTool;
388// pub use search_cache;
389
390pub use crate::mcp::McpTool;
391pub use context7::{Context7QueryDocsTool, Context7ResolveLibraryIdTool};
392pub use questionnaire::{QuestionnaireBridge, QuestionnaireTool};
393pub use subagent::SubagentTool;
394pub use write::WriteTool;
395
396/// Tool registry for managing available tools
397#[derive(Clone)]
398pub struct ToolRegistry {
399    tools: Arc<parking_lot::RwLock<std::collections::HashMap<String, Arc<dyn AgentTool>>>>,
400}
401
402impl Default for ToolRegistry {
403    fn default() -> Self {
404        Self::new()
405    }
406}
407
408impl ToolRegistry {
409    /// Creates an empty tool registry.
410    pub fn new() -> Self {
411        Self {
412            tools: Arc::new(parking_lot::RwLock::new(std::collections::HashMap::new())),
413        }
414    }
415
416    /// Register a tool
417    pub fn register(&self, tool: impl AgentTool + 'static) {
418        let name = tool.name().to_string();
419        self.tools.write().insert(name, Arc::new(tool));
420    }
421
422    /// Register a tool that is already wrapped in an `Arc`.
423    /// This is the primary path for extensions that produce `Arc<dyn AgentTool>`.
424    pub fn register_arc(&self, tool: Arc<dyn AgentTool>) {
425        let name = tool.name().to_string();
426        self.tools.write().insert(name, tool);
427    }
428
429    /// Get a tool by name
430    pub fn get(&self, name: &str) -> Option<Arc<dyn AgentTool>> {
431        self.tools.read().get(name).cloned()
432    }
433
434    /// Unregister a tool by name.
435    /// Returns `true` if the tool was present and removed.
436    pub fn unregister(&self, name: &str) -> bool {
437        self.tools.write().remove(name).is_some()
438    }
439
440    /// List all registered tool names
441    pub fn names(&self) -> Vec<String> {
442        self.tools.read().keys().cloned().collect()
443    }
444
445    /// Get all tool definitions
446    pub fn definitions(&self) -> Vec<ToolDefinition> {
447        self.tools
448            .read()
449            .values()
450            .map(|t| t.to_definition())
451            .collect()
452    }
453
454    /// Get all tools as a slice
455    pub fn get_tools(&self) -> Vec<Arc<dyn AgentTool>> {
456        self.tools.read().values().cloned().collect()
457    }
458
459    /// Check whether all tools in `required` are registered.
460    ///
461    /// Useful for validating program/module dependencies before execution.
462    ///
463    /// # Example
464    ///
465    /// ```
466    /// use oxi_agent::ToolRegistry;
467    /// let registry = ToolRegistry::new();
468    /// assert!(!registry.has_all(&["read", "write"]));
469    /// ```
470    pub fn has_all(&self, required: &[&str]) -> bool {
471        let tools = self.tools.read();
472        required.iter().all(|name| tools.contains_key(*name))
473    }
474
475    /// Return the subset of `required` tool names that are **not** registered.
476    ///
477    /// # Example
478    ///
479    /// ```
480    /// use oxi_agent::ToolRegistry;
481    /// let registry = ToolRegistry::new();
482    /// let missing = registry.missing(&["read", "exec", "nonexistent"]);
483    /// assert_eq!(missing, vec!["read", "exec", "nonexistent"]);
484    /// ```
485    pub fn missing<'a>(&self, required: &[&'a str]) -> Vec<&'a str> {
486        let tools = self.tools.read();
487        required
488            .iter()
489            .filter(|name| !tools.contains_key(**name))
490            .copied()
491            .collect()
492    }
493
494    /// Create a registry with all built-in tools
495    ///
496    /// # Examples
497    ///
498    /// ```
499    /// use oxi_agent::ToolRegistry;
500    /// let registry = ToolRegistry::with_builtins();
501    /// let tools = registry.names();
502    /// assert!(tools.contains(&"read".to_string()));
503    /// assert!(tools.contains(&"write".to_string()));
504    /// assert!(tools.contains(&"bash".to_string()));
505    /// ```
506    pub fn with_builtins() -> Self {
507        Self::with_builtins_cwd(PathBuf::from("."), &[])
508    }
509
510    /// Create a registry with all built-in tools, using the given cwd.
511    ///
512    /// Pass `disabled_tools` to selectively disable built-in tools
513    /// (e.g. `["web_search", "github_search"]` for a minimal setup).
514    pub fn with_builtins_cwd(cwd: PathBuf, disabled_tools: &[String]) -> Self {
515        let registry = Self::new();
516        let disabled: std::collections::HashSet<&str> =
517            disabled_tools.iter().map(|s| s.as_str()).collect();
518
519        // Helper to create shared cache on demand
520        let cache_once: std::cell::OnceCell<Arc<search_cache::SearchCache>> =
521            std::cell::OnceCell::new();
522
523        // MCP: use OnceCell to avoid re-creating McpManager on repeated calls
524        let mcp_once: std::cell::OnceCell<Arc<crate::mcp::McpManager>> = std::cell::OnceCell::new();
525        let mcp_manager = mcp_once
526            .get_or_init(|| Arc::new(crate::mcp::McpManager::new()))
527            .clone();
528
529        // Register all builtin tools — essential ones ignore disabled list
530        let mut all_tools: Vec<Box<dyn AgentTool>> = vec![
531            Box::new(ReadTool::with_cwd(cwd.clone())),
532            Box::new(WriteTool::with_cwd(cwd.clone())),
533            Box::new(EditTool::with_cwd(cwd.clone())),
534            Box::new(BashTool::with_cwd(cwd.clone())),
535            Box::new(GrepTool::with_cwd(cwd.clone())),
536            Box::new(FindTool::with_cwd(cwd.clone())),
537            Box::new(LsTool::with_cwd(cwd.clone())),
538            Box::new(web_search::WebSearchTool::new(
539                cache_once
540                    .get_or_init(|| Arc::new(search_cache::SearchCache::new()))
541                    .clone(),
542            )),
543            Box::new(search_cache::GetSearchResultsTool::new(
544                cache_once
545                    .get_or_init(|| Arc::new(search_cache::SearchCache::new()))
546                    .clone(),
547            )),
548            Box::new(github::GitHubTool::new(
549                cache_once
550                    .get_or_init(|| Arc::new(search_cache::SearchCache::new()))
551                    .clone(),
552            )),
553            Box::new(SubagentTool::with_cwd(cwd)),
554        ];
555
556        all_tools.push(Box::new(crate::mcp::McpTool::new(mcp_manager)));
557        all_tools.push(Box::new(context7::Context7ResolveLibraryIdTool::new()));
558        all_tools.push(Box::new(context7::Context7QueryDocsTool::new()));
559        all_tools.push(Box::new(generate_image::GenerateImageTool::new()));
560
561        for tool in all_tools {
562            if tool.essential() || !disabled.contains(tool.name()) {
563                // web_search ↔ get_search_results coupling
564                if tool.name() == "get_search_results" && disabled.contains("web_search") {
565                    continue;
566                }
567                registry.register_arc(Arc::from(tool));
568            }
569        }
570
571        registry
572    }
573
574    /// Extend this registry with all tools from another registry.
575    ///
576    /// Useful for composing tool sets from multiple sources
577    /// (e.g., coding tools + kernel tools + browser tools).
578    ///
579    /// # Example
580    ///
581    /// ```ignore
582    /// let base = ToolRegistry::new();
583    /// base.extend_from(&other_registry);
584    /// ```
585    pub fn extend_from(&self, other: &ToolRegistry) {
586        for name in other.names() {
587            if let Some(tool) = other.get(&name) {
588                self.register_arc(tool);
589            }
590        }
591    }
592
593    /// Create registry with selected builtins only.
594    pub fn with_selected_tools(cwd: PathBuf, names: &[&str]) -> Self {
595        let full = Self::with_builtins_cwd(cwd, &[]);
596        let registry = Self::new();
597        let set: std::collections::HashSet<&str> = names.iter().copied().collect();
598        for name in full.names() {
599            if set.contains(name.as_str()) {
600                if let Some(tool) = full.get(&name) {
601                    registry.register_arc(tool);
602                }
603            }
604        }
605        registry
606    }
607}