Skip to main content

oxi_agent/
tools.rs

1#![allow(unused_doc_comments)]
2/// Agent tools system
3/// This module provides the tool abstraction layer and built-in tools.
4use crate::types::ToolDefinition;
5use async_trait::async_trait;
6use serde_json::Value;
7use std::fmt;
8use std::future::Future;
9use std::path::{Path, PathBuf};
10use std::pin::Pin;
11use std::sync::Arc;
12use tokio::sync::oneshot;
13
14// ═══════════════════════════════════════════════════════════════════════════
15// Capability traits — lightweight interfaces tools need, implemented by the
16// composition root (oxi-cli) bridging to SDK ports. oxi-agent does NOT depend
17// on oxi-sdk, so these are defined here.
18// ═══════════════════════════════════════════════════════════════════════════
19
20/// A single memory item returned by [`MemoryBackend`].
21#[derive(Debug, Clone, serde::Serialize)]
22pub struct MemoryItem {
23    /// Unique identifier.
24    pub id: String,
25    /// Memory kind: "fact", "preference", "context", "summary".
26    pub kind: String,
27    /// The memory content text.
28    pub content: String,
29    /// Project/scope identifier.
30    pub subject: String,
31}
32
33/// Memory backend for the `memory_*` tools. The composition root implements
34/// this, bridging to `oxi_sdk::ports::MemoryStore` + `EmbeddingProvider`.
35pub trait MemoryBackend: Send + Sync + std::fmt::Debug + 'static {
36    /// Store a memory item, returning its new ID.
37    fn put<'a>(
38        &'a self,
39        content: &'a str,
40        kind: &'a str,
41        subject: &'a str,
42    ) -> Pin<Box<dyn Future<Output = Result<String, ToolError>> + Send + 'a>>;
43    /// Semantic-search stored memories, returning up to `k` matches.
44    fn search<'a>(
45        &'a self,
46        query: &'a str,
47        k: usize,
48    ) -> Pin<Box<dyn Future<Output = Result<Vec<MemoryItem>, ToolError>> + Send + 'a>>;
49    /// List memory items for the given subject.
50    fn list<'a>(
51        &'a self,
52        subject: &'a str,
53    ) -> Pin<Box<dyn Future<Output = Result<Vec<MemoryItem>, ToolError>> + Send + 'a>>;
54    /// Delete the memory item with the given ID.
55    fn delete<'a>(
56        &'a self,
57        id: &'a str,
58    ) -> Pin<Box<dyn Future<Output = Result<(), ToolError>> + Send + 'a>>;
59
60    /// Human-readable memory status (None if not supported by this backend).
61    fn memory_info(&self) -> Option<String> {
62        None
63    }
64    /// Trigger sleep consolidation, returning a status message.
65    fn trigger_consolidation(&self) -> Option<String> {
66        None
67    }
68    /// Trigger SHMR harmonization, returning a status message.
69    fn trigger_harmonize(&self) -> Option<String> {
70        None
71    }
72}
73
74/// Content resolved from an internal protocol URL (e.g. `skill://`, `issue://`).
75pub struct ResolvedContent {
76    /// The resolved text content.
77    pub content: String,
78    /// MIME type: "text/markdown", "application/json", "text/plain".
79    pub content_type: String,
80    /// True if the content is uneditable (suppresses hashline anchors).
81    pub immutable: bool,
82}
83
84/// URL resolver for internal protocol schemes. The composition root
85/// implements this, bridging to `oxi_sdk::ports::InternalUrlRouter`.
86pub trait UrlResolver: Send + Sync + std::fmt::Debug {
87    /// Whether this resolver handles the given input URI.
88    fn can_resolve(&self, input: &str) -> bool;
89    /// Resolve an internal URI to its content, asynchronously.
90    fn resolve<'a>(
91        &'a self,
92        uri: &'a str,
93    ) -> Pin<Box<dyn Future<Output = Result<ResolvedContent, ToolError>> + Send + 'a>>;
94}
95
96/// Todo state access capability. Implemented by the composition root
97/// (oxi-cli) bridging to the session-scoped todo state. Used by the
98/// `todo` agent tool and the TUI sticky panel.
99pub trait TodoStateProvider: Send + Sync + std::fmt::Debug {
100    /// Return a snapshot of the current phase list (read-only, for TUI).
101    fn get_phases(&self) -> Vec<crate::tools::todo::TodoPhase>;
102
103    /// Apply a sequence of todo ops, returning the updated state, the
104    /// newly-completed transitions (for strikethrough animation), and
105    /// any error messages from ambiguous op references.
106    fn apply_ops<'a>(
107        &'a self,
108        ops: Vec<crate::tools::todo::TodoOp>,
109    ) -> Pin<
110        Box<dyn Future<Output = Result<crate::tools::todo::TodoUpdateResult, String>> + Send + 'a>,
111    >;
112}
113
114// ── Agent Hub capability (⑥) ──────────────────────────────────────────
115
116/// Agent kind for Hub display.
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118pub enum AgentKind {
119    /// Main conversation agent.
120    Main,
121    /// Task-spawned sub-agent.
122    Task,
123    /// Observation-only advisor.
124    Advisor,
125}
126
127/// Hub display status.
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub enum AgentHubStatus {
130    /// Currently executing.
131    Running,
132    /// Finished, idle.
133    Idle,
134    /// Parked (memory retained, not running).
135    Parked,
136    /// Abnormal termination.
137    Aborted,
138}
139
140/// Read-only agent info for Hub display.
141#[derive(Debug, Clone)]
142pub struct AgentInfo {
143    /// Unique identifier.
144    pub id: String,
145    /// Display name.
146    pub display_name: String,
147    /// Agent kind.
148    pub kind: AgentKind,
149    /// Current status.
150    pub status: AgentHubStatus,
151    /// Current task description (if any).
152    pub current_task: Option<String>,
153}
154
155/// Agent pool access capability. Implemented by the composition root
156/// to expose live sub-agent info to the Hub overlay and todo matching.
157pub trait AgentPoolProvider: Send + Sync + std::fmt::Debug {
158    /// List all known agents (main + sub-agents).
159    fn list_agents(&self) -> Vec<AgentInfo>;
160    /// Get a specific agent by ID.
161    fn get_agent(&self, id: &str) -> Option<AgentInfo>;
162}
163
164// ── LSP capability (⑧) ────────────────────────────────────────────────
165
166/// 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    /// Intent trace for the current tool call.
447    /// Set by the agent loop before executing each tool, read by tools
448    /// that surface intent to users (e.g. `ask`).
449    pub intent: Option<String>,
450}
451
452impl fmt::Debug for ToolContext {
453    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
454        f.debug_struct("ToolContext")
455            .field("workspace_dir", &self.workspace_dir)
456            .field("root_dir", &self.root_dir)
457            .field("session_id", &self.session_id)
458            .field(
459                "snapshot_store",
460                &self.snapshot_store.as_ref().map(|_| "<dyn SnapshotStore>"),
461            )
462            .field(
463                "memory",
464                &self.memory.as_ref().map(|_| "<dyn MemoryBackend>"),
465            )
466            .field(
467                "url_resolver",
468                &self.url_resolver.as_ref().map(|_| "<dyn UrlResolver>"),
469            )
470            .finish()
471    }
472}
473
474impl ToolContext {
475    /// Create a new context with the given workspace.
476    pub fn new(workspace_dir: impl Into<PathBuf>) -> Self {
477        Self {
478            workspace_dir: workspace_dir.into(),
479            root_dir: None,
480            session_id: None,
481            snapshot_store: None,
482            memory: None,
483            url_resolver: None,
484            todo: None,
485            agent_pool: None,
486            lsp: None,
487            subagent_runner: None,
488            subagent_depth: 0,
489            intent: None,
490        }
491    }
492
493    /// Get the effective root directory.
494    /// Returns root_dir if set, otherwise workspace_dir.
495    pub fn root(&self) -> &Path {
496        self.root_dir.as_deref().unwrap_or(&self.workspace_dir)
497    }
498
499    /// Set a session ID.
500    pub fn with_session(mut self, session_id: impl Into<String>) -> Self {
501        self.session_id = Some(session_id.into());
502        self
503    }
504
505    /// Set an explicit root directory.
506    pub fn with_root(mut self, root_dir: impl Into<PathBuf>) -> Self {
507        self.root_dir = Some(root_dir.into());
508        self
509    }
510
511    /// Set the snapshot store (enables hashline edit mode).
512    pub fn with_snapshot_store(mut self, store: Arc<dyn oxi_hashline::SnapshotStore>) -> Self {
513        self.snapshot_store = Some(store);
514        self
515    }
516
517    /// Set the memory backend (enables memory tools).
518    pub fn with_memory(mut self, memory: Arc<dyn MemoryBackend>) -> Self {
519        self.memory = Some(memory);
520        self
521    }
522
523    /// Set the URL resolver (enables internal URL dispatch).
524    pub fn with_url_resolver(mut self, resolver: Arc<dyn UrlResolver>) -> Self {
525        self.url_resolver = Some(resolver);
526        self
527    }
528
529    /// Set the todo state (enables the `todo` agent tool).
530    pub fn with_todo(mut self, todo: Arc<dyn TodoStateProvider>) -> Self {
531        self.todo = Some(todo);
532        self
533    }
534
535    /// Set the in-process sub-agent runner (enables library-native
536    /// delegation — issue #28 gap 3).
537    pub fn with_subagent_runner(mut self, runner: Arc<dyn SubagentRunner>) -> Self {
538        self.subagent_runner = Some(runner);
539        self
540    }
541
542    /// Attach an intent trace to this context.
543    pub fn with_intent(mut self, intent: impl Into<String>) -> Self {
544        self.intent = Some(intent.into());
545        self
546    }
547}
548
549impl Default for ToolContext {
550    fn default() -> Self {
551        Self {
552            workspace_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
553            root_dir: None,
554            session_id: None,
555            snapshot_store: None,
556            memory: None,
557            url_resolver: None,
558            todo: None,
559            agent_pool: None,
560            lsp: None,
561            subagent_runner: None,
562            subagent_depth: 0,
563            intent: None,
564        }
565    }
566}
567
568/// Result type for tool execution
569pub type ToolError = String;
570
571/// Result of tool execution
572#[derive(Debug)]
573pub struct AgentToolResult {
574    /// pub.
575    pub success: bool,
576    /// pub.
577    pub output: String,
578    /// pub.
579    pub metadata: Option<serde_json::Value>,
580    /// Optional content blocks (e.g., image blocks) to include in the tool result message.
581    /// When present, these are used as the content of the ToolResultMessage instead of
582    /// wrapping `output` in a Text block.
583    pub content_blocks: Option<Vec<oxi_ai::ContentBlock>>,
584    /// When `true`, signals that the agent loop should terminate after this batch
585    /// of tool calls completes.  Defaults to `false` so that the loop continues
586    /// unless a tool explicitly opts-in to termination.
587    pub terminate: bool,
588    /// Intent trace — a concise description of what this specific tool call did.
589    /// Set by the agent loop from the tool's static `intent()` or by the tool
590    /// itself for dynamic intent. Included in `ToolExecutionEnd` events.
591    pub intent: Option<String>,
592}
593
594impl AgentToolResult {
595    /// Creates a successful tool result with the given output text.
596    pub fn success(output: impl Into<String>) -> Self {
597        Self {
598            success: true,
599            output: output.into(),
600            metadata: None,
601            content_blocks: None,
602            terminate: false,
603            intent: None,
604        }
605    }
606
607    /// Creates an error tool result with the given error message.
608    pub fn error(output: impl Into<String>) -> Self {
609        Self {
610            success: false,
611            output: output.into(),
612            metadata: None,
613            content_blocks: None,
614            terminate: false,
615            intent: None,
616        }
617    }
618
619    /// Attaches structured metadata (JSON) to this result.
620    pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
621        self.metadata = Some(metadata);
622        self
623    }
624
625    /// Attaches rich content blocks (images, code, etc.) to this result.
626    pub fn with_content_blocks(mut self, blocks: Vec<oxi_ai::ContentBlock>) -> Self {
627        self.content_blocks = Some(blocks);
628        self
629    }
630
631    /// Mark this result as requesting agent-loop termination.
632    pub fn with_terminate(mut self) -> Self {
633        self.terminate = true;
634        self
635    }
636
637    /// Attach an intent trace to this result.
638    pub fn with_intent(mut self, intent: impl Into<String>) -> Self {
639        self.intent = Some(intent.into());
640        self
641    }
642}
643
644impl fmt::Display for AgentToolResult {
645    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
646        write!(f, "{}", self.output)
647    }
648}
649
650/// Callback type for progress updates
651pub type ProgressCallback = Arc<dyn Fn(String) + Send + Sync>;
652
653/// Tool execution mode for parallel safety.
654#[derive(Debug, Clone)]
655pub enum ToolExecutionMode {
656    /// Safe to run in parallel with any other tool
657    ParallelSafe,
658    /// Must run sequentially — no parallel execution
659    SequentialOnly,
660    /// Mutates a specific file — file_mutation_queue serializes same-file access
661    MutatesFile(std::path::PathBuf),
662    /// Read-only — always parallel safe
663    ReadOnly,
664}
665
666/// Render output for TUI visualization.
667#[derive(Debug, Clone)]
668pub struct RenderOutput {
669    /// Rendered text content (markdown or plain)
670    pub content: String,
671    /// Whether to show collapsed by default
672    pub collapsed: bool,
673    /// Optional summary text for TUI footer
674    pub summary: Option<String>,
675}
676
677/// Core trait for all agent tools
678/// Risk tier for approval gating.
679///
680/// Determines which approval tiers gate a tool call.
681/// - `Read`  — no side effects (lookup, search, inspection).
682/// - `Write` — mutates data (creates, edits, commits).
683/// - `Exec`  — arbitrary side effects (shell, eval, network).
684#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
685pub enum ToolTier {
686    /// Read-only — inspection, search, lookup.
687    Read,
688    /// Data mutation — create, edit, commit.
689    Write,
690    /// Arbitrary execution — shell, eval, network, subagent.
691    #[default]
692    Exec,
693}
694
695/// Core trait for all agent tools
696#[async_trait]
697pub trait AgentTool: Send + Sync {
698    /// Tool name (used in function calls)
699    fn name(&self) -> &str;
700
701    /// Human-readable label
702    fn label(&self) -> &str;
703
704    /// Description for the model
705    fn description(&self) -> &str;
706
707    /// JSON Schema for parameters
708    fn parameters_schema(&self) -> Value;
709
710    /// Whether this tool is essential (cannot be disabled).
711    /// Essential tools: read, write, edit, bash, grep, find, ls
712    /// Optional tools: web_search, github, subagent, etc.
713    fn essential(&self) -> bool {
714        false
715    }
716
717    /// Execute the tool with the given tool call ID and parameters.
718    ///
719    /// The `ctx` parameter provides workspace information. File tools should
720    /// use `ctx.root()` to get the effective directory. Custom tools can use
721    /// `ctx.workspace_dir` for workspace-relative operations.
722    ///
723    /// # Examples
724    ///
725    /// ```ignore
726    /// use oxi_agent::{AgentTool, AgentToolResult, ToolContext};
727    /// use serde_json::json;
728    /// struct MyTool;
729    ///
730    /// #[async_trait]
731    /// impl AgentTool for MyTool {
732    ///     fn name(&self) -> &str { "my_tool" }
733    ///     fn label(&self) -> &str { "My Tool" }
734    ///     fn description(&self) -> &str { "A custom tool" }
735    ///     fn parameters_schema(&self) -> Value { json!({
736    ///         "type": "object",
737    ///         "properties": {}
738    ///     }) }
739    ///
740    ///     async fn execute(&self, tool_call_id: &str, params: Value, _signal: Option<oneshot::Receiver<()>>, ctx: &ToolContext) -> Result<AgentToolResult, String> {
741    ///         println!("Tool '{}' called with params: {:?}, workspace: {:?}", tool_call_id, params, ctx.workspace_dir);
742    ///         Ok(AgentToolResult::success("Done!"))
743    ///     }
744    /// }
745    /// ```
746    async fn execute(
747        &self,
748        tool_call_id: &str,
749        params: Value,
750        signal: Option<oneshot::Receiver<()>>,
751        ctx: &ToolContext,
752    ) -> Result<AgentToolResult, ToolError>;
753
754    /// Called with progress updates during execution.
755    /// Tools can override this to emit streaming updates.
756    fn on_progress(&self, _callback: ProgressCallback) {
757        // Default no-op
758    }
759
760    /// Structured browse progress callback for browser tool context enrichment.
761    /// Default implementation is no-op. Only browse tools override this to
762    /// register a callback that enriches `ToolCallContext` with structured
763    /// data from `BrowseProgress` events.
764    fn on_browse_progress(&self, _callback: crate::tools::browse::BrowseProgressCallback) {}
765
766    /// Custom rendering for tool call (TUI visualization).
767    /// Return None to use the default tool_renderer.rs formatter.
768    fn render_call(&self, _params: &serde_json::Value) -> Option<RenderOutput> {
769        None
770    }
771
772    /// Custom rendering for tool result (TUI visualization).
773    /// Return None to use the default tool_renderer.rs formatter.
774    fn render_result(&self, _result: &AgentToolResult) -> Option<RenderOutput> {
775        None
776    }
777
778    /// Intent trace — a concise description of what this tool does.
779    /// Returned value is included in `ToolExecutionStart` / `ToolExecutionEnd`
780    /// events so the agent loop can surface intent to users or telemetry.
781    /// Default `None` (no intent tracing).
782    fn intent(&self) -> Option<&str> {
783        None
784    }
785
786    /// Execution mode for parallel safety.
787    /// Defaults to ParallelSafe. Override for file-mutating or sequential tools.
788    fn execution_mode(&self) -> ToolExecutionMode {
789        ToolExecutionMode::ParallelSafe
790    }
791
792    /// Risk tier for approval gating.
793    ///
794    /// - `Read`  — no side effects (lookup, search, inspection).
795    /// - `Write` — mutates data (creates, edits, commits).
796    /// - `Exec`  — arbitrary side effects (shell, eval, network).
797    ///
798    /// Default: `Exec` (safest default — requires explicit opt-down).
799    fn tool_tier(&self) -> ToolTier {
800        ToolTier::Exec
801    }
802
803    /// Return the current active tab ID, if this tool manages browser tabs.
804    /// Defaults to `None`. Browser tools override this to return the tab ID
805    /// of the currently-open tab during execution, so the agent loop can
806    /// populate `ToolExecutionUpdate.tab_id`.
807    fn current_tab_id(&self) -> Option<uuid::Uuid> {
808        None
809    }
810
811    /// Receive a shared slot where the tool can write the current tab ID.
812    /// The agent loop creates the slot and passes it before `on_progress`;
813    /// the tool writes `Some(tab_id)` when it opens a tab and `None` when
814    /// it closes it. Defaults to a no-op — only tab-aware tools override.
815    fn set_tab_id_slot(&self, _slot: Arc<parking_lot::Mutex<Option<uuid::Uuid>>>) {}
816
817    /// Convert to ToolDefinition
818    fn to_definition(&self) -> ToolDefinition {
819        ToolDefinition {
820            name: self.name().to_string(),
821            description: self.description().to_string(),
822            input_schema: serde_json::from_value(self.parameters_schema()).unwrap_or_default(),
823        }
824    }
825}
826
827// Built-in tools
828/// Ask tool — ask the user one or more clarifying questions via the TUI overlay.
829pub mod ask;
830/// AST-aware structural code rewriting tool (ast-grep backed).
831pub mod ast_edit;
832/// AST structural search tool (wraps the `sg` CLI).
833pub mod ast_grep;
834/// Bash shell execution tool.
835pub mod bash;
836/// Browser tools (engine abstraction always compiled).
837pub mod browse;
838/// Checkpoint and Rewind tools — save/restore investigation state.
839pub mod checkpoint_tool;
840/// Conventional-commit tool (deterministic scope + LLM analysis).
841pub mod commit;
842/// Computer tool — computer control using Vision AI.
843pub mod computer_tool;
844/// Context7 documentation tools.
845pub mod context7;
846/// Debug tool — DAP-backed debugger integration (scaffold).
847pub mod debug_tool;
848/// In-place file edit tool.
849pub mod edit;
850/// Diff-based edit helpers.
851pub mod edit_diff;
852/// Eval tool — persistent-kernel code execution (scaffold).
853pub mod eval_tool;
854/// Serialised file-mutation queue.
855pub mod file_mutation_queue;
856/// File-fsystem find tool.
857pub mod find;
858/// Image generation tool (OpenRouter API).
859pub mod generate_image;
860/// GitHub integration tool (gh CLI-based).
861pub mod github;
862/// GitHub repository search tool (legacy REST API).
863pub mod github_search;
864/// Goal tool — manage investigation goals with token budgets.
865pub mod goal_tool;
866/// Content search (grep) tool.
867pub mod grep;
868/// TokioHashlineFs — tokio::fs-backed HashlineFs implementation.
869pub mod hashline_fs;
870/// Shared HTTP client singleton.
871pub mod http_client;
872/// Hub tool — agent coordination for peer messaging and job management.
873pub mod hub_tool;
874/// Inspect Image tool — analyze images using Vision LLM capabilities.
875pub mod inspect_image_tool;
876/// Learn tool — capture a reusable lesson to memory and optionally create a managed skill.
877pub mod learn_tool;
878/// Directory listing tool.
879pub mod ls;
880/// LSP tool (requires LspProvider capability).
881pub mod lsp;
882/// Manage Skill tool — create, update, or delete isolated managed SKILL.md files.
883pub mod manage_skill_tool;
884/// Memory edit tool — update or delete a memory item.
885pub mod memory_edit;
886/// Memory recall tool — semantic search over stored memories.
887pub mod memory_recall;
888/// Memory reflect tool — persist a session summary to memory.
889pub mod memory_reflect;
890/// Memory retain tool — persist a memory item to the backend.
891pub mod memory_retain;
892/// Path security (traversal protection).
893pub mod path_security;
894/// Path manipulation utilities.
895pub mod path_utils;
896/// File reading tool.
897pub mod read;
898/// Rendering utilities for tool output.
899pub mod render_utils;
900/// Review tool — request code review with focus areas and priorities.
901pub mod review_tool;
902/// Search result cache and get_search_results tool.
903pub mod search_cache;
904/// Sub-agent delegation tool.
905pub mod subagent;
906/// Phased todo tool (init/start/done/drop/rm/append/view).
907pub mod todo;
908/// Tool definition wrapper helpers.
909pub mod tool_definition_wrapper;
910/// Output truncation helpers.
911pub mod truncate;
912/// TTS tool — text-to-speech synthesis.
913pub mod tts_tool;
914/// Vibe tool — manage persistent worker sessions.
915pub mod vibe_tool;
916/// Multi-engine web search tool (oxibrowser search module).
917pub mod web_search;
918/// File writing tool.
919pub mod write;
920/// Yield tool — subagent result submission.
921pub mod yield_tool;
922
923// Re-export for convenience
924pub use bash::BashTool;
925pub use debug_tool::DebugTool;
926pub use edit::EditTool;
927pub use eval_tool::EvalTool;
928pub use find::FindTool;
929pub use grep::GrepTool;
930pub use ls::LsTool;
931pub use read::ReadTool;
932// pub use search_cache;
933
934pub use crate::mcp::McpTool;
935pub use ask::{AskBridge, AskTool};
936pub use ast_edit::AstEditTool;
937pub use ast_grep::AstGrepTool;
938pub use commit::CommitTool;
939pub use context7::{Context7QueryDocsTool, Context7ResolveLibraryIdTool};
940pub use memory_edit::MemoryEditTool;
941pub use memory_recall::MemoryRecallTool;
942pub use memory_reflect::MemoryReflectTool;
943pub use memory_retain::MemoryRetainTool;
944pub use subagent::SubagentTool;
945pub use write::WriteTool;
946
947/// Tool registry for managing available tools
948#[derive(Clone)]
949pub struct ToolRegistry {
950    tools: Arc<parking_lot::RwLock<std::collections::HashMap<String, Arc<dyn AgentTool>>>>,
951    /// Optional MCP manager, set by `with_builtins_cwd()` so the TUI and
952    /// other consumers can reach the live MCP state (Phase 2+).
953    mcp_manager: Arc<parking_lot::RwLock<Option<Arc<crate::mcp::McpManager>>>>,
954}
955
956impl Default for ToolRegistry {
957    fn default() -> Self {
958        Self::new()
959    }
960}
961
962impl ToolRegistry {
963    /// Creates an empty tool registry.
964    pub fn new() -> Self {
965        Self {
966            tools: Arc::new(parking_lot::RwLock::new(std::collections::HashMap::new())),
967            mcp_manager: Arc::new(parking_lot::RwLock::new(None)),
968        }
969    }
970
971    /// Attach an `McpManager` to this registry. Replaces any previous one.
972    pub fn set_mcp_manager(&self, mgr: Arc<crate::mcp::McpManager>) {
973        *self.mcp_manager.write() = Some(mgr);
974    }
975
976    /// Get the attached `McpManager`, if any.
977    pub fn mcp_manager(&self) -> Option<Arc<crate::mcp::McpManager>> {
978        self.mcp_manager.read().clone()
979    }
980
981    /// Register a tool
982    pub fn register(&self, tool: impl AgentTool + 'static) {
983        let name = tool.name().to_string();
984        self.tools.write().insert(name, Arc::new(tool));
985    }
986
987    /// Register a tool that is already wrapped in an `Arc`.
988    /// This is the primary path for extensions that produce `Arc<dyn AgentTool>`.
989    pub fn register_arc(&self, tool: Arc<dyn AgentTool>) {
990        let name = tool.name().to_string();
991        self.tools.write().insert(name, tool);
992    }
993
994    /// Get a tool by name
995    pub fn get(&self, name: &str) -> Option<Arc<dyn AgentTool>> {
996        self.tools.read().get(name).cloned()
997    }
998
999    /// Unregister a tool by name.
1000    /// Returns `true` if the tool was present and removed.
1001    pub fn unregister(&self, name: &str) -> bool {
1002        self.tools.write().remove(name).is_some()
1003    }
1004
1005    /// List all registered tool names
1006    pub fn names(&self) -> Vec<String> {
1007        self.tools.read().keys().cloned().collect()
1008    }
1009
1010    /// Get all tool definitions
1011    pub fn definitions(&self) -> Vec<ToolDefinition> {
1012        self.tools
1013            .read()
1014            .values()
1015            .map(|t| t.to_definition())
1016            .collect()
1017    }
1018
1019    /// Get all tools as a slice
1020    pub fn get_tools(&self) -> Vec<Arc<dyn AgentTool>> {
1021        self.tools.read().values().cloned().collect()
1022    }
1023
1024    /// Check whether all tools in `required` are registered.
1025    ///
1026    /// Useful for validating program/module dependencies before execution.
1027    ///
1028    /// # Example
1029    ///
1030    /// ```
1031    /// use oxi_agent::ToolRegistry;
1032    /// let registry = ToolRegistry::new();
1033    /// assert!(!registry.has_all(&["read", "write"]));
1034    /// ```
1035    pub fn has_all(&self, required: &[&str]) -> bool {
1036        let tools = self.tools.read();
1037        required.iter().all(|name| tools.contains_key(*name))
1038    }
1039
1040    /// Return the subset of `required` tool names that are **not** registered.
1041    ///
1042    /// # Example
1043    ///
1044    /// ```
1045    /// use oxi_agent::ToolRegistry;
1046    /// let registry = ToolRegistry::new();
1047    /// let missing = registry.missing(&["read", "exec", "nonexistent"]);
1048    /// assert_eq!(missing, vec!["read", "exec", "nonexistent"]);
1049    /// ```
1050    pub fn missing<'a>(&self, required: &[&'a str]) -> Vec<&'a str> {
1051        let tools = self.tools.read();
1052        required
1053            .iter()
1054            .filter(|name| !tools.contains_key(**name))
1055            .copied()
1056            .collect()
1057    }
1058
1059    /// Create a registry with all built-in tools
1060    ///
1061    /// # Examples
1062    ///
1063    /// ```
1064    /// use oxi_agent::ToolRegistry;
1065    /// let registry = ToolRegistry::with_builtins();
1066    /// let tools = registry.names();
1067    /// assert!(tools.contains(&"read".to_string()));
1068    /// assert!(tools.contains(&"write".to_string()));
1069    /// assert!(tools.contains(&"bash".to_string()));
1070    /// ```
1071    pub fn with_builtins() -> Self {
1072        Self::with_builtins_cwd(PathBuf::from("."), &[])
1073    }
1074
1075    /// Create a registry with all built-in tools, using the given cwd.
1076    ///
1077    /// Pass `disabled_tools` to selectively disable built-in tools
1078    /// (e.g. `["web_search", "github_search"]` for a minimal setup).
1079    pub fn with_builtins_cwd(cwd: PathBuf, disabled_tools: &[String]) -> Self {
1080        let registry = Self::new();
1081        let disabled: std::collections::HashSet<&str> =
1082            disabled_tools.iter().map(|s| s.as_str()).collect();
1083
1084        // Helper to create shared cache on demand
1085        let cache_once: std::cell::OnceCell<Arc<search_cache::SearchCache>> =
1086            std::cell::OnceCell::new();
1087
1088        // MCP: use OnceCell to avoid re-creating McpManager on repeated calls
1089        let mcp_once: std::cell::OnceCell<Arc<crate::mcp::McpManager>> = std::cell::OnceCell::new();
1090        let mcp_manager = mcp_once.get_or_init(crate::mcp::McpManager::spawn).clone();
1091
1092        // Register all builtin tools — essential ones ignore disabled list
1093        let mut all_tools: Vec<Box<dyn AgentTool>> = vec![
1094            Box::new(ReadTool::with_cwd(cwd.clone())),
1095            Box::new(WriteTool::with_cwd(cwd.clone())),
1096            Box::new(AstGrepTool::with_cwd(cwd.clone())),
1097            Box::new(BashTool::with_cwd(cwd.clone())),
1098            Box::new(EditTool::with_cwd(cwd.clone())),
1099            Box::new(GrepTool::with_cwd(cwd.clone())),
1100            Box::new(FindTool::with_cwd(cwd.clone())),
1101            Box::new(LsTool::with_cwd(cwd.clone())),
1102            Box::new(web_search::WebSearchTool::new(
1103                cache_once
1104                    .get_or_init(|| Arc::new(search_cache::SearchCache::new()))
1105                    .clone(),
1106            )),
1107            Box::new(search_cache::GetSearchResultsTool::new(
1108                cache_once
1109                    .get_or_init(|| Arc::new(search_cache::SearchCache::new()))
1110                    .clone(),
1111            )),
1112            Box::new(github::GitHubTool::new(
1113                cache_once
1114                    .get_or_init(|| Arc::new(search_cache::SearchCache::new()))
1115                    .clone(),
1116            )),
1117            Box::new(SubagentTool::with_cwd(cwd.clone())),
1118            Box::new(todo::TodoTool),
1119            Box::new(memory_recall::MemoryRecallTool),
1120            Box::new(memory_reflect::MemoryReflectTool),
1121            Box::new(memory_retain::MemoryRetainTool),
1122            Box::new(memory_edit::MemoryEditTool),
1123        ];
1124
1125        all_tools.push(Box::new(crate::mcp::McpTool::new(mcp_manager.clone())));
1126
1127        // Phase 3: register direct MCP tools from the metadata cache.
1128        for def in mcp_manager.direct_tools_from_cache() {
1129            all_tools.push(Box::new(crate::mcp::McpDirectTool::new(
1130                mcp_manager.clone(),
1131                def,
1132            )));
1133        }
1134
1135        // Remember the manager on the registry so the TUI can reach it.
1136        registry.set_mcp_manager(mcp_manager);
1137
1138        all_tools.push(Box::new(context7::Context7ResolveLibraryIdTool::new()));
1139        all_tools.push(Box::new(context7::Context7QueryDocsTool::new()));
1140        all_tools.push(Box::new(generate_image::GenerateImageTool::new()));
1141        all_tools.push(Box::new(commit::CommitTool::unconfigured()));
1142        all_tools.push(Box::new(ast_edit::AstEditTool::new()));
1143        all_tools.push(Box::new(lsp::LspTool));
1144        all_tools.push(Box::new(eval_tool::EvalTool));
1145        all_tools.push(Box::new(checkpoint_tool::CheckpointTool));
1146        all_tools.push(Box::new(checkpoint_tool::RewindTool));
1147        all_tools.push(Box::new(hub_tool::HubTool));
1148        all_tools.push(Box::new(yield_tool::YieldTool));
1149        all_tools.push(Box::new(goal_tool::GoalTool));
1150        all_tools.push(Box::new(review_tool::ReviewTool));
1151        all_tools.push(Box::new(learn_tool::LearnTool));
1152        all_tools.push(Box::new(manage_skill_tool::ManageSkillTool));
1153        all_tools.push(Box::new(inspect_image_tool::InspectImageTool));
1154        all_tools.push(Box::new(computer_tool::ComputerTool));
1155        all_tools.push(Box::new(tts_tool::TtsTool));
1156        all_tools.push(Box::new(vibe_tool::VibeTool));
1157        // debug_tool — DAP-backed debugger integration.
1158        // Most actions are validated scaffolds (route through xd://debug);
1159        // real launch/attach/breakpoint control is wired via the harness device.
1160        all_tools.push(Box::new(debug_tool::DebugTool));
1161
1162        for tool in all_tools {
1163            if tool.essential() || !disabled.contains(tool.name()) {
1164                // web_search ↔ get_search_results coupling
1165                if tool.name() == "get_search_results" && disabled.contains("web_search") {
1166                    continue;
1167                }
1168                registry.register_arc(Arc::from(tool));
1169            }
1170        }
1171
1172        registry
1173    }
1174
1175    /// Extend this registry with all tools from another registry.
1176    ///
1177    /// Useful for composing tool sets from multiple sources
1178    /// (e.g., coding tools + kernel tools + browser tools).
1179    ///
1180    /// # Example
1181    ///
1182    /// ```ignore
1183    /// let base = ToolRegistry::new();
1184    /// base.extend_from(&other_registry);
1185    /// ```
1186    pub fn extend_from(&self, other: &ToolRegistry) {
1187        for name in other.names() {
1188            if let Some(tool) = other.get(&name) {
1189                self.register_arc(tool);
1190            }
1191        }
1192    }
1193
1194    /// Create registry with selected builtins only.
1195    pub fn with_selected_tools(cwd: PathBuf, names: &[&str]) -> Self {
1196        let full = Self::with_builtins_cwd(cwd, &[]);
1197        let registry = Self::new();
1198        let set: std::collections::HashSet<&str> = names.iter().copied().collect();
1199        for name in full.names() {
1200            if set.contains(name.as_str())
1201                && let Some(tool) = full.get(&name)
1202            {
1203                registry.register_arc(tool);
1204            }
1205        }
1206        registry
1207    }
1208}