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