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// ── Sub-agent delegation (issue #28 gap 3) ─────────────────────────────
236
237/// Result of an in-process isolated sub-agent fork run.
238///
239/// Produced by [`SubagentRunner::run_isolated`]. The sub-agent runs
240/// with a **fresh, empty context** — its conversation history is
241/// completely isolated from the parent agent. Only the final text and
242/// usage statistics are returned, keeping the parent's context small.
243///
244/// This is the library-native alternative to shelling out to the `oxi`
245/// CLI binary. Library consumers (e.g. Oxios) that embed `oxi-agent`
246/// without an `oxi` subprocess implement this trait so the `subagent`
247/// tool works in-process.
248#[derive(Debug, Clone, Default)]
249pub struct ForkResult {
250 /// Final response text from the sub-agent.
251 pub text: String,
252 /// Input tokens consumed (last reported turn).
253 pub input_tokens: usize,
254 /// Output tokens consumed (last reported turn).
255 pub output_tokens: usize,
256 /// Number of agent turns executed.
257 pub turns: u32,
258 /// Model ID used by the sub-agent.
259 pub model: Option<String>,
260 /// Error message if the run failed.
261 pub error: Option<String>,
262}
263
264/// In-process sub-agent runner — the library-native delegation backend.
265///
266/// When wired into [`ToolContext`] via
267/// [`ToolContext::with_subagent_runner`], the `subagent` tool prefers
268/// this in-process path over shelling out to the `oxi` CLI binary.
269/// This is essential for library consumers (Oxios) that embed
270/// `oxi-agent` as a kernel without an `oxi` subprocess.
271///
272/// The SDK provides a ready-made implementation
273/// (`oxi_sdk::SdkSubagentRunner`) that wraps an `Oxi` instance and
274/// creates a fresh `Agent` for each invocation.
275#[async_trait::async_trait]
276#[allow(clippy::too_many_arguments)]
277pub trait SubagentRunner: Send + Sync + std::fmt::Debug {
278 /// Run a single agent task with an isolated (empty) context.
279 ///
280 /// # Arguments
281 /// * `agent_name` — Agent definition name (for logging / display).
282 /// * `task` — The task prompt to execute.
283 /// * `system_prompt` — Optional system prompt override.
284 /// * `model` — Optional model ID override (e.g. `"anthropic/claude-...`).
285 /// * `tools` — Optional tool whitelist (empty = all registered tools).
286 /// * `cwd` — Working directory for file tools.
287 /// * `depth` — Current sub-agent nesting depth. The runner sets
288 /// the forked agent's `subagent_depth` to `depth + 1` so the
289 /// fork's own subagent tool can enforce a recursion cap without
290 /// env vars (issue #28 gap 3 — concurrent `set_var` is UB).
291 async fn run_isolated(
292 &self,
293 agent_name: &str,
294 task: &str,
295 system_prompt: Option<&str>,
296 model: Option<&str>,
297 tools: &[String],
298 cwd: &Path,
299 depth: u8,
300 ) -> anyhow::Result<ForkResult>;
301}
302
303/// Context passed to tools at execution time.
304///
305/// This allows tools to operate on a specific workspace without being
306/// rebuilt. When `root_dir` is `Some`, tools use it as their base directory.
307/// When `None`, tools should fall back to `workspace_dir`.
308#[derive(Clone)]
309pub struct ToolContext {
310 /// Primary workspace directory (used when root_dir is None).
311 pub workspace_dir: PathBuf,
312 /// Optional explicit root directory for file tools.
313 /// Takes priority over workspace_dir if present.
314 pub root_dir: Option<PathBuf>,
315 /// Session identifier for logging/tracing.
316 pub session_id: Option<String>,
317 /// Snapshot store for hashline tag emission/validation.
318 /// When `None`, hashline edit mode is unavailable.
319 pub snapshot_store: Option<Arc<dyn oxi_hashline::SnapshotStore>>,
320 /// Memory backend for `memory_*` tools.
321 /// When `None`, memory tools return an error.
322 pub memory: Option<Arc<dyn MemoryBackend>>,
323 /// URL resolver for internal protocol schemes (`issue://`, `pr://`, etc.).
324 /// When `None`, URL-prefixed paths are treated as regular file paths.
325 pub url_resolver: Option<Arc<dyn UrlResolver>>,
326 /// Todo state for the `todo` agent tool.
327 /// When `None`, the `todo` tool returns an error.
328 pub todo: Option<Arc<dyn TodoStateProvider>>,
329 /// Agent pool for Hub display and todo sub-agent matching.
330 pub agent_pool: Option<Arc<dyn AgentPoolProvider>>,
331 /// LSP provider for the `lsp` tool.
332 pub lsp: Option<Arc<dyn LspProvider>>,
333 /// In-process sub-agent runner (issue #28 gap 3).
334 /// When `Some`, the `subagent` tool prefers an in-process isolated
335 /// run over shelling out to the CLI binary. Library consumers
336 /// (e.g. Oxios) that embed `oxi-agent` without an `oxi` subprocess
337 /// set this so delegation works. When `None`, the CLI backend is
338 /// used (the default for `oxi-cli`).
339 pub subagent_runner: Option<Arc<dyn SubagentRunner>>,
340 /// Current sub-agent nesting depth for the in-process path
341 /// (issue #28 gap 3). The CLI path uses env vars instead.
342 /// Default 0 (top-level agent).
343 pub subagent_depth: u8,
344}
345
346impl fmt::Debug for ToolContext {
347 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
348 f.debug_struct("ToolContext")
349 .field("workspace_dir", &self.workspace_dir)
350 .field("root_dir", &self.root_dir)
351 .field("session_id", &self.session_id)
352 .field(
353 "snapshot_store",
354 &self.snapshot_store.as_ref().map(|_| "<dyn SnapshotStore>"),
355 )
356 .field(
357 "memory",
358 &self.memory.as_ref().map(|_| "<dyn MemoryBackend>"),
359 )
360 .field(
361 "url_resolver",
362 &self.url_resolver.as_ref().map(|_| "<dyn UrlResolver>"),
363 )
364 .finish()
365 }
366}
367
368impl ToolContext {
369 /// Create a new context with the given workspace.
370 pub fn new(workspace_dir: impl Into<PathBuf>) -> Self {
371 Self {
372 workspace_dir: workspace_dir.into(),
373 root_dir: None,
374 session_id: None,
375 snapshot_store: None,
376 memory: None,
377 url_resolver: None,
378 todo: None,
379 agent_pool: None,
380 lsp: None,
381 subagent_runner: None,
382 subagent_depth: 0,
383 }
384 }
385
386 /// Get the effective root directory.
387 /// Returns root_dir if set, otherwise workspace_dir.
388 pub fn root(&self) -> &Path {
389 self.root_dir.as_deref().unwrap_or(&self.workspace_dir)
390 }
391
392 /// Set a session ID.
393 pub fn with_session(mut self, session_id: impl Into<String>) -> Self {
394 self.session_id = Some(session_id.into());
395 self
396 }
397
398 /// Set an explicit root directory.
399 pub fn with_root(mut self, root_dir: impl Into<PathBuf>) -> Self {
400 self.root_dir = Some(root_dir.into());
401 self
402 }
403
404 /// Set the snapshot store (enables hashline edit mode).
405 pub fn with_snapshot_store(mut self, store: Arc<dyn oxi_hashline::SnapshotStore>) -> Self {
406 self.snapshot_store = Some(store);
407 self
408 }
409
410 /// Set the memory backend (enables memory tools).
411 pub fn with_memory(mut self, memory: Arc<dyn MemoryBackend>) -> Self {
412 self.memory = Some(memory);
413 self
414 }
415
416 /// Set the URL resolver (enables internal URL dispatch).
417 pub fn with_url_resolver(mut self, resolver: Arc<dyn UrlResolver>) -> Self {
418 self.url_resolver = Some(resolver);
419 self
420 }
421
422 /// Set the todo state (enables the `todo` agent tool).
423 pub fn with_todo(mut self, todo: Arc<dyn TodoStateProvider>) -> Self {
424 self.todo = Some(todo);
425 self
426 }
427
428 /// Set the in-process sub-agent runner (enables library-native
429 /// delegation — issue #28 gap 3).
430 pub fn with_subagent_runner(mut self, runner: Arc<dyn SubagentRunner>) -> Self {
431 self.subagent_runner = Some(runner);
432 self
433 }
434}
435
436impl Default for ToolContext {
437 fn default() -> Self {
438 Self {
439 workspace_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
440 root_dir: None,
441 session_id: None,
442 snapshot_store: None,
443 memory: None,
444 url_resolver: None,
445 todo: None,
446 agent_pool: None,
447 lsp: None,
448 subagent_runner: None,
449 subagent_depth: 0,
450 }
451 }
452}
453
454/// Result type for tool execution
455pub type ToolError = String;
456
457/// Result of tool execution
458#[derive(Debug)]
459pub struct AgentToolResult {
460 /// pub.
461 pub success: bool,
462 /// pub.
463 pub output: String,
464 /// pub.
465 pub metadata: Option<serde_json::Value>,
466 /// Optional content blocks (e.g., image blocks) to include in the tool result message.
467 /// When present, these are used as the content of the ToolResultMessage instead of
468 /// wrapping `output` in a Text block.
469 pub content_blocks: Option<Vec<oxi_ai::ContentBlock>>,
470 /// When `true`, signals that the agent loop should terminate after this batch
471 /// of tool calls completes. Defaults to `false` so that the loop continues
472 /// unless a tool explicitly opts-in to termination.
473 pub terminate: bool,
474}
475
476impl AgentToolResult {
477 /// Creates a successful tool result with the given output text.
478 pub fn success(output: impl Into<String>) -> Self {
479 Self {
480 success: true,
481 output: output.into(),
482 metadata: None,
483 content_blocks: None,
484 terminate: false,
485 }
486 }
487
488 /// Creates an error tool result with the given error message.
489 pub fn error(output: impl Into<String>) -> Self {
490 Self {
491 success: false,
492 output: output.into(),
493 metadata: None,
494 content_blocks: None,
495 terminate: false,
496 }
497 }
498
499 /// Attaches structured metadata (JSON) to this result.
500 pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
501 self.metadata = Some(metadata);
502 self
503 }
504
505 /// Attaches rich content blocks (images, code, etc.) to this result.
506 pub fn with_content_blocks(mut self, blocks: Vec<oxi_ai::ContentBlock>) -> Self {
507 self.content_blocks = Some(blocks);
508 self
509 }
510
511 /// Mark this result as requesting agent-loop termination.
512 pub fn with_terminate(mut self) -> Self {
513 self.terminate = true;
514 self
515 }
516}
517
518impl fmt::Display for AgentToolResult {
519 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
520 write!(f, "{}", self.output)
521 }
522}
523
524/// Callback type for progress updates
525pub type ProgressCallback = Arc<dyn Fn(String) + Send + Sync>;
526
527/// Tool execution mode for parallel safety.
528#[derive(Debug, Clone)]
529pub enum ToolExecutionMode {
530 /// Safe to run in parallel with any other tool
531 ParallelSafe,
532 /// Must run sequentially — no parallel execution
533 SequentialOnly,
534 /// Mutates a specific file — file_mutation_queue serializes same-file access
535 MutatesFile(std::path::PathBuf),
536 /// Read-only — always parallel safe
537 ReadOnly,
538}
539
540/// Render output for TUI visualization.
541#[derive(Debug, Clone)]
542pub struct RenderOutput {
543 /// Rendered text content (markdown or plain)
544 pub content: String,
545 /// Whether to show collapsed by default
546 pub collapsed: bool,
547 /// Optional summary text for TUI footer
548 pub summary: Option<String>,
549}
550
551/// Core trait for all agent tools
552#[async_trait]
553pub trait AgentTool: Send + Sync {
554 /// Tool name (used in function calls)
555 fn name(&self) -> &str;
556
557 /// Human-readable label
558 fn label(&self) -> &str;
559
560 /// Description for the model
561 fn description(&self) -> &str;
562
563 /// JSON Schema for parameters
564 fn parameters_schema(&self) -> Value;
565
566 /// Whether this tool is essential (cannot be disabled).
567 /// Essential tools: read, write, edit, bash, grep, find, ls
568 /// Optional tools: web_search, github, subagent, etc.
569 fn essential(&self) -> bool {
570 false
571 }
572
573 /// Execute the tool with the given tool call ID and parameters.
574 ///
575 /// The `ctx` parameter provides workspace information. File tools should
576 /// use `ctx.root()` to get the effective directory. Custom tools can use
577 /// `ctx.workspace_dir` for workspace-relative operations.
578 ///
579 /// # Examples
580 ///
581 /// ```ignore
582 /// use oxi_agent::{AgentTool, AgentToolResult, ToolContext};
583 /// use serde_json::json;
584 /// struct MyTool;
585 ///
586 /// #[async_trait]
587 /// impl AgentTool for MyTool {
588 /// fn name(&self) -> &str { "my_tool" }
589 /// fn label(&self) -> &str { "My Tool" }
590 /// fn description(&self) -> &str { "A custom tool" }
591 /// fn parameters_schema(&self) -> Value { json!({
592 /// "type": "object",
593 /// "properties": {}
594 /// }) }
595 ///
596 /// async fn execute(&self, tool_call_id: &str, params: Value, _signal: Option<oneshot::Receiver<()>>, ctx: &ToolContext) -> Result<AgentToolResult, String> {
597 /// println!("Tool '{}' called with params: {:?}, workspace: {:?}", tool_call_id, params, ctx.workspace_dir);
598 /// Ok(AgentToolResult::success("Done!"))
599 /// }
600 /// }
601 /// ```
602 async fn execute(
603 &self,
604 tool_call_id: &str,
605 params: Value,
606 signal: Option<oneshot::Receiver<()>>,
607 ctx: &ToolContext,
608 ) -> Result<AgentToolResult, ToolError>;
609
610 /// Called with progress updates during execution.
611 /// Tools can override this to emit streaming updates.
612 fn on_progress(&self, _callback: ProgressCallback) {
613 // Default no-op
614 }
615
616 /// Structured browse progress callback for browser tool context enrichment.
617 /// Default implementation is no-op. Only browse tools override this to
618 /// register a callback that enriches `ToolCallContext` with structured
619 /// data from `BrowseProgress` events.
620 fn on_browse_progress(&self, _callback: crate::tools::browse::BrowseProgressCallback) {}
621
622 /// Custom rendering for tool call (TUI visualization).
623 /// Return None to use the default tool_renderer.rs formatter.
624 fn render_call(&self, _params: &serde_json::Value) -> Option<RenderOutput> {
625 None
626 }
627
628 /// Custom rendering for tool result (TUI visualization).
629 /// Return None to use the default tool_renderer.rs formatter.
630 fn render_result(&self, _result: &AgentToolResult) -> Option<RenderOutput> {
631 None
632 }
633
634 /// Execution mode for parallel safety.
635 /// Defaults to ParallelSafe. Override for file-mutating or sequential tools.
636 fn execution_mode(&self) -> ToolExecutionMode {
637 ToolExecutionMode::ParallelSafe
638 }
639
640 /// Return the current active tab ID, if this tool manages browser tabs.
641 /// Defaults to `None`. Browser tools override this to return the tab ID
642 /// of the currently-open tab during execution, so the agent loop can
643 /// populate `ToolExecutionUpdate.tab_id`.
644 fn current_tab_id(&self) -> Option<uuid::Uuid> {
645 None
646 }
647
648 /// Receive a shared slot where the tool can write the current tab ID.
649 /// The agent loop creates the slot and passes it before `on_progress`;
650 /// the tool writes `Some(tab_id)` when it opens a tab and `None` when
651 /// it closes it. Defaults to a no-op — only tab-aware tools override.
652 fn set_tab_id_slot(&self, _slot: Arc<parking_lot::Mutex<Option<uuid::Uuid>>>) {}
653
654 /// Convert to ToolDefinition
655 fn to_definition(&self) -> ToolDefinition {
656 ToolDefinition {
657 name: self.name().to_string(),
658 description: self.description().to_string(),
659 input_schema: serde_json::from_value(self.parameters_schema()).unwrap_or_default(),
660 }
661 }
662}
663
664// Built-in tools
665/// Ask tool — ask the user one or more clarifying questions via the TUI overlay.
666pub mod ask;
667/// Bash shell execution tool.
668pub mod bash;
669/// Browser tools (engine abstraction always compiled).
670pub mod browse;
671/// Conventional-commit tool (deterministic scope + LLM analysis).
672pub mod commit;
673/// Context7 documentation tools.
674pub mod context7;
675/// In-place file edit tool.
676pub mod edit;
677/// Diff-based edit helpers.
678pub mod edit_diff;
679/// Serialised file-mutation queue.
680pub mod file_mutation_queue;
681/// File-fsystem find tool.
682pub mod find;
683/// Image generation tool (OpenRouter API).
684pub mod generate_image;
685/// GitHub integration tool (gh CLI-based).
686pub mod github;
687/// GitHub repository search tool (legacy REST API).
688pub mod github_search;
689/// Content search (grep) tool.
690pub mod grep;
691/// TokioHashlineFs — tokio::fs-backed HashlineFs implementation.
692pub mod hashline_fs;
693/// Shared HTTP client singleton.
694pub mod http_client;
695/// Directory listing tool.
696pub mod ls;
697/// LSP tool (requires LspProvider capability).
698pub mod lsp;
699/// Memory edit tool — update or delete a memory item.
700pub mod memory_edit;
701/// Memory recall tool — semantic search over stored memories.
702pub mod memory_recall;
703/// Memory reflect tool — persist a session summary to memory.
704pub mod memory_reflect;
705/// Memory retain tool — persist a memory item to the backend.
706pub mod memory_retain;
707/// Path security (traversal protection).
708pub mod path_security;
709/// Path manipulation utilities.
710pub mod path_utils;
711/// File reading tool.
712pub mod read;
713/// Rendering utilities for tool output.
714pub mod render_utils;
715/// Search result cache and get_search_results tool.
716pub mod search_cache;
717/// Sub-agent delegation tool.
718pub mod subagent;
719/// Phased todo tool (init/start/done/drop/rm/append/view).
720pub mod todo;
721/// Tool definition wrapper helpers.
722pub mod tool_definition_wrapper;
723/// Output truncation helpers.
724pub mod truncate;
725/// Multi-engine web search tool (oxibrowser search module).
726pub mod web_search;
727/// File writing tool.
728pub mod write;
729
730// Re-export for convenience
731pub use bash::BashTool;
732pub use edit::EditTool;
733pub use find::FindTool;
734pub use grep::GrepTool;
735pub use ls::LsTool;
736pub use read::ReadTool;
737// pub use search_cache;
738
739pub use crate::mcp::McpTool;
740pub use ask::{AskBridge, AskTool};
741pub use commit::CommitTool;
742pub use context7::{Context7QueryDocsTool, Context7ResolveLibraryIdTool};
743pub use memory_edit::MemoryEditTool;
744pub use memory_recall::MemoryRecallTool;
745pub use memory_reflect::MemoryReflectTool;
746pub use memory_retain::MemoryRetainTool;
747pub use subagent::SubagentTool;
748pub use write::WriteTool;
749
750/// Tool registry for managing available tools
751#[derive(Clone)]
752pub struct ToolRegistry {
753 tools: Arc<parking_lot::RwLock<std::collections::HashMap<String, Arc<dyn AgentTool>>>>,
754 /// Optional MCP manager, set by `with_builtins_cwd()` so the TUI and
755 /// other consumers can reach the live MCP state (Phase 2+).
756 mcp_manager: Arc<parking_lot::RwLock<Option<Arc<crate::mcp::McpManager>>>>,
757}
758
759impl Default for ToolRegistry {
760 fn default() -> Self {
761 Self::new()
762 }
763}
764
765impl ToolRegistry {
766 /// Creates an empty tool registry.
767 pub fn new() -> Self {
768 Self {
769 tools: Arc::new(parking_lot::RwLock::new(std::collections::HashMap::new())),
770 mcp_manager: Arc::new(parking_lot::RwLock::new(None)),
771 }
772 }
773
774 /// Attach an `McpManager` to this registry. Replaces any previous one.
775 pub fn set_mcp_manager(&self, mgr: Arc<crate::mcp::McpManager>) {
776 *self.mcp_manager.write() = Some(mgr);
777 }
778
779 /// Get the attached `McpManager`, if any.
780 pub fn mcp_manager(&self) -> Option<Arc<crate::mcp::McpManager>> {
781 self.mcp_manager.read().clone()
782 }
783
784 /// Register a tool
785 pub fn register(&self, tool: impl AgentTool + 'static) {
786 let name = tool.name().to_string();
787 self.tools.write().insert(name, Arc::new(tool));
788 }
789
790 /// Register a tool that is already wrapped in an `Arc`.
791 /// This is the primary path for extensions that produce `Arc<dyn AgentTool>`.
792 pub fn register_arc(&self, tool: Arc<dyn AgentTool>) {
793 let name = tool.name().to_string();
794 self.tools.write().insert(name, tool);
795 }
796
797 /// Get a tool by name
798 pub fn get(&self, name: &str) -> Option<Arc<dyn AgentTool>> {
799 self.tools.read().get(name).cloned()
800 }
801
802 /// Unregister a tool by name.
803 /// Returns `true` if the tool was present and removed.
804 pub fn unregister(&self, name: &str) -> bool {
805 self.tools.write().remove(name).is_some()
806 }
807
808 /// List all registered tool names
809 pub fn names(&self) -> Vec<String> {
810 self.tools.read().keys().cloned().collect()
811 }
812
813 /// Get all tool definitions
814 pub fn definitions(&self) -> Vec<ToolDefinition> {
815 self.tools
816 .read()
817 .values()
818 .map(|t| t.to_definition())
819 .collect()
820 }
821
822 /// Get all tools as a slice
823 pub fn get_tools(&self) -> Vec<Arc<dyn AgentTool>> {
824 self.tools.read().values().cloned().collect()
825 }
826
827 /// Check whether all tools in `required` are registered.
828 ///
829 /// Useful for validating program/module dependencies before execution.
830 ///
831 /// # Example
832 ///
833 /// ```
834 /// use oxi_agent::ToolRegistry;
835 /// let registry = ToolRegistry::new();
836 /// assert!(!registry.has_all(&["read", "write"]));
837 /// ```
838 pub fn has_all(&self, required: &[&str]) -> bool {
839 let tools = self.tools.read();
840 required.iter().all(|name| tools.contains_key(*name))
841 }
842
843 /// Return the subset of `required` tool names that are **not** registered.
844 ///
845 /// # Example
846 ///
847 /// ```
848 /// use oxi_agent::ToolRegistry;
849 /// let registry = ToolRegistry::new();
850 /// let missing = registry.missing(&["read", "exec", "nonexistent"]);
851 /// assert_eq!(missing, vec!["read", "exec", "nonexistent"]);
852 /// ```
853 pub fn missing<'a>(&self, required: &[&'a str]) -> Vec<&'a str> {
854 let tools = self.tools.read();
855 required
856 .iter()
857 .filter(|name| !tools.contains_key(**name))
858 .copied()
859 .collect()
860 }
861
862 /// Create a registry with all built-in tools
863 ///
864 /// # Examples
865 ///
866 /// ```
867 /// use oxi_agent::ToolRegistry;
868 /// let registry = ToolRegistry::with_builtins();
869 /// let tools = registry.names();
870 /// assert!(tools.contains(&"read".to_string()));
871 /// assert!(tools.contains(&"write".to_string()));
872 /// assert!(tools.contains(&"bash".to_string()));
873 /// ```
874 pub fn with_builtins() -> Self {
875 Self::with_builtins_cwd(PathBuf::from("."), &[])
876 }
877
878 /// Create a registry with all built-in tools, using the given cwd.
879 ///
880 /// Pass `disabled_tools` to selectively disable built-in tools
881 /// (e.g. `["web_search", "github_search"]` for a minimal setup).
882 pub fn with_builtins_cwd(cwd: PathBuf, disabled_tools: &[String]) -> Self {
883 let registry = Self::new();
884 let disabled: std::collections::HashSet<&str> =
885 disabled_tools.iter().map(|s| s.as_str()).collect();
886
887 // Helper to create shared cache on demand
888 let cache_once: std::cell::OnceCell<Arc<search_cache::SearchCache>> =
889 std::cell::OnceCell::new();
890
891 // MCP: use OnceCell to avoid re-creating McpManager on repeated calls
892 let mcp_once: std::cell::OnceCell<Arc<crate::mcp::McpManager>> = std::cell::OnceCell::new();
893 let mcp_manager = mcp_once.get_or_init(crate::mcp::McpManager::spawn).clone();
894
895 // Register all builtin tools — essential ones ignore disabled list
896 let mut all_tools: Vec<Box<dyn AgentTool>> = vec![
897 Box::new(ReadTool::with_cwd(cwd.clone())),
898 Box::new(WriteTool::with_cwd(cwd.clone())),
899 Box::new(EditTool::with_cwd(cwd.clone())),
900 Box::new(BashTool::with_cwd(cwd.clone())),
901 Box::new(GrepTool::with_cwd(cwd.clone())),
902 Box::new(FindTool::with_cwd(cwd.clone())),
903 Box::new(LsTool::with_cwd(cwd.clone())),
904 Box::new(web_search::WebSearchTool::new(
905 cache_once
906 .get_or_init(|| Arc::new(search_cache::SearchCache::new()))
907 .clone(),
908 )),
909 Box::new(search_cache::GetSearchResultsTool::new(
910 cache_once
911 .get_or_init(|| Arc::new(search_cache::SearchCache::new()))
912 .clone(),
913 )),
914 Box::new(github::GitHubTool::new(
915 cache_once
916 .get_or_init(|| Arc::new(search_cache::SearchCache::new()))
917 .clone(),
918 )),
919 Box::new(SubagentTool::with_cwd(cwd.clone())),
920 Box::new(todo::TodoTool),
921 Box::new(memory_recall::MemoryRecallTool),
922 Box::new(memory_reflect::MemoryReflectTool),
923 Box::new(memory_retain::MemoryRetainTool),
924 Box::new(memory_edit::MemoryEditTool),
925 ];
926
927 all_tools.push(Box::new(crate::mcp::McpTool::new(mcp_manager.clone())));
928
929 // Phase 3: register direct MCP tools from the metadata cache.
930 for def in mcp_manager.direct_tools_from_cache() {
931 all_tools.push(Box::new(crate::mcp::McpDirectTool::new(
932 mcp_manager.clone(),
933 def,
934 )));
935 }
936
937 // Remember the manager on the registry so the TUI can reach it.
938 registry.set_mcp_manager(mcp_manager);
939
940 all_tools.push(Box::new(context7::Context7ResolveLibraryIdTool::new()));
941 all_tools.push(Box::new(context7::Context7QueryDocsTool::new()));
942 all_tools.push(Box::new(generate_image::GenerateImageTool::new()));
943 all_tools.push(Box::new(commit::CommitTool::unconfigured()));
944
945 for tool in all_tools {
946 if tool.essential() || !disabled.contains(tool.name()) {
947 // web_search ↔ get_search_results coupling
948 if tool.name() == "get_search_results" && disabled.contains("web_search") {
949 continue;
950 }
951 registry.register_arc(Arc::from(tool));
952 }
953 }
954
955 registry
956 }
957
958 /// Extend this registry with all tools from another registry.
959 ///
960 /// Useful for composing tool sets from multiple sources
961 /// (e.g., coding tools + kernel tools + browser tools).
962 ///
963 /// # Example
964 ///
965 /// ```ignore
966 /// let base = ToolRegistry::new();
967 /// base.extend_from(&other_registry);
968 /// ```
969 pub fn extend_from(&self, other: &ToolRegistry) {
970 for name in other.names() {
971 if let Some(tool) = other.get(&name) {
972 self.register_arc(tool);
973 }
974 }
975 }
976
977 /// Create registry with selected builtins only.
978 pub fn with_selected_tools(cwd: PathBuf, names: &[&str]) -> Self {
979 let full = Self::with_builtins_cwd(cwd, &[]);
980 let registry = Self::new();
981 let set: std::collections::HashSet<&str> = names.iter().copied().collect();
982 for name in full.names() {
983 if set.contains(name.as_str())
984 && let Some(tool) = full.get(&name)
985 {
986 registry.register_arc(tool);
987 }
988 }
989 registry
990 }
991}