Skip to main content

oxicode_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 (oxicode-cli) bridging to SDK ports. oxicode-agent does NOT depend
17// on oxicode-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 `oxicode_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    /// Delete every memory item in the backend. Returns the number of
74    /// items removed.
75    ///
76    /// The default implementation is **unsupported**: it returns an
77    /// honest error rather than silently deleting zero rows. Backends
78    /// that can perform a true bulk erase must override this method;
79    /// backends without a list-all subject primitive must surface
80    /// that limitation rather than guess.
81    ///
82    /// This is a destructive operation; the `/memory clear` slash
83    /// command requires an explicit confirmation flag before invoking.
84    fn clear_all<'a>(
85        &'a self,
86    ) -> Pin<Box<dyn Future<Output = Result<usize, ToolError>> + Send + 'a>> {
87        Box::pin(async move {
88            Err(
89                "clear_all not supported by this backend (no list-all subject primitive)"
90                    .to_string(),
91            )
92        })
93    }
94
95    /// Force a consolidation (rebuild) job. Returns a status message
96    /// describing what was dispatched, or `Err` when the backend has
97    /// no in-process capability to enqueue work.
98    ///
99    /// Default: `Err` ("not supported"). Backends that expose
100    /// consolidation via the engine (e.g. Mnemopi sleep) should
101    /// override and run the real operation.
102    fn enqueue_consolidation<'a>(
103        &'a self,
104    ) -> Pin<Box<dyn Future<Output = Result<String, ToolError>> + Send + 'a>> {
105        Box::pin(
106            async move { Err("enqueue_consolidation not supported by this backend".to_string()) },
107        )
108    }
109}
110
111/// Content resolved from an internal protocol URL (e.g. `skill://`, `issue://`).
112pub struct ResolvedContent {
113    /// The resolved text content.
114    pub content: String,
115    /// MIME type: "text/markdown", "application/json", "text/plain".
116    pub content_type: String,
117    /// True if the content is uneditable (suppresses hashline anchors).
118    pub immutable: bool,
119}
120
121/// URL resolver for internal protocol schemes. The composition root
122/// implements this, bridging to `oxicode_sdk::ports::InternalUrlRouter`.
123pub trait UrlResolver: Send + Sync + std::fmt::Debug {
124    /// Whether this resolver handles the given input URI.
125    fn can_resolve(&self, input: &str) -> bool;
126    /// Resolve an internal URI to its content, asynchronously.
127    fn resolve<'a>(
128        &'a self,
129        uri: &'a str,
130    ) -> Pin<Box<dyn Future<Output = Result<ResolvedContent, ToolError>> + Send + 'a>>;
131}
132
133/// Todo state access capability. Implemented by the composition root
134/// (oxicode-cli) bridging to the session-scoped todo state. Used by the
135/// `todo` agent tool and the TUI sticky panel.
136pub trait TodoStateProvider: Send + Sync + std::fmt::Debug {
137    /// Return a snapshot of the current phase list (read-only, for TUI).
138    fn get_phases(&self) -> Vec<crate::tools::todo::TodoPhase>;
139
140    /// Apply a sequence of todo ops, returning the updated state, the
141    /// newly-completed transitions (for strikethrough animation), and
142    /// any error messages from ambiguous op references.
143    fn apply_ops<'a>(
144        &'a self,
145        ops: Vec<crate::tools::todo::TodoOp>,
146    ) -> Pin<
147        Box<dyn Future<Output = Result<crate::tools::todo::TodoUpdateResult, String>> + Send + 'a>,
148    >;
149}
150
151// ── Agent Hub capability (⑥) ──────────────────────────────────────────
152
153/// Agent kind for Hub display.
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub enum AgentKind {
156    /// Main conversation agent.
157    Main,
158    /// Task-spawned sub-agent.
159    Task,
160    /// Observation-only advisor.
161    Advisor,
162}
163
164/// Hub display status.
165#[derive(Debug, Clone, Copy, PartialEq, Eq)]
166pub enum AgentHubStatus {
167    /// Currently executing.
168    Running,
169    /// Finished, idle.
170    Idle,
171    /// Parked (memory retained, not running).
172    Parked,
173    /// Abnormal termination.
174    Aborted,
175}
176
177/// Read-only agent info for Hub display.
178#[derive(Debug, Clone)]
179pub struct AgentInfo {
180    /// Unique identifier.
181    pub id: String,
182    /// Display name.
183    pub display_name: String,
184    /// Agent kind.
185    pub kind: AgentKind,
186    /// Current status.
187    pub status: AgentHubStatus,
188    /// Current task description (if any).
189    pub current_task: Option<String>,
190}
191
192/// Agent pool access capability. Implemented by the composition root
193/// to expose live sub-agent info to the Hub overlay and todo matching.
194pub trait AgentPoolProvider: Send + Sync + std::fmt::Debug {
195    /// List all known agents (main + sub-agents).
196    fn list_agents(&self) -> Vec<AgentInfo>;
197    /// Get a specific agent by ID.
198    fn get_agent(&self, id: &str) -> Option<AgentInfo>;
199}
200
201// ── LSP capability (⑧) ────────────────────────────────────────────────
202
203/// Aggregated diagnostics across one or more files (returned by
204/// [`LspProvider::drain_diagnostics`]). Counts severity buckets so callers
205/// can surface a quick "0 errors / 3 warnings" summary without scanning
206/// every diagnostic.
207#[derive(Debug, Clone, Default)]
208pub struct DiagnosticsSummary {
209    /// Total number of fresh diagnostics after filtering.
210    pub count: usize,
211    /// Number of error-severity diagnostics.
212    pub errors: usize,
213    /// Number of warning-severity diagnostics.
214    pub warnings: usize,
215    /// Per-file entries; empty when no diagnostics have arrived yet.
216    pub entries: Vec<FileDiagnosticEntry>,
217}
218
219/// Diagnostics for one file. Path is the LSP document URI as the server
220/// reported it (may be `file://`-prefixed); `diagnostics` is the raw payload
221/// from `textDocument/publishDiagnostics`.
222#[derive(Debug, Clone)]
223pub struct FileDiagnosticEntry {
224    /// Document URI (typically `file://<absolute path>`).
225    pub uri: String,
226    /// Path-relative display of the file (best effort).
227    pub path: String,
228    /// Diagnostics reported by the server for this file.
229    pub diagnostics: serde_json::Value,
230}
231
232/// LSP action enum — the operations the `lsp` tool supports.
233#[derive(Debug, Clone)]
234pub enum LspAction {
235    /// Get diagnostics for a file.
236    Diagnostics {
237        /// Path to the file to inspect.
238        file: String,
239    },
240    /// Go to definition.
241    Definition {
242        /// Path to the file containing the symbol.
243        file: String,
244        /// 1-based line number of the symbol.
245        line: u32,
246        /// Optional symbol text to resolve (for disambiguation).
247        symbol: Option<String>,
248    },
249    /// Find references.
250    References {
251        /// Path to the file containing the symbol.
252        file: String,
253        /// 1-based line number of the symbol.
254        line: u32,
255        /// Optional symbol text to find references for.
256        symbol: Option<String>,
257    },
258    /// Hover info.
259    Hover {
260        /// Path to the file containing the symbol.
261        file: String,
262        /// 1-based line number of the symbol.
263        line: u32,
264        /// Optional symbol text to hover.
265        symbol: Option<String>,
266    },
267    /// Rename symbol.
268    Rename {
269        /// Path to the file containing the symbol.
270        file: String,
271        /// 1-based line number of the symbol.
272        line: u32,
273        /// Symbol text to rename.
274        symbol: String,
275        /// New name for the symbol.
276        new_name: String,
277        /// If true, apply the rename; otherwise just preview.
278        apply: bool,
279    },
280    /// Get workspace/document symbols.
281    Symbols {
282        /// Path to the file to inspect (workspace symbols if query-only).
283        file: String,
284        /// Optional filter query for symbols.
285        query: Option<String>,
286    },
287    /// Get server status.
288    Status,
289    /// Available code actions at a position.
290    CodeActions {
291        /// Path to the file containing the position.
292        file: String,
293        /// 1-based line number.
294        line: u32,
295        /// Optional symbol hint for disambiguation.
296        symbol: Option<String>,
297    },
298    /// Go to type definition.
299    TypeDefinition {
300        /// Path to the file containing the symbol.
301        file: String,
302        /// 1-based line number.
303        line: u32,
304        /// Optional symbol hint.
305        symbol: Option<String>,
306    },
307    /// Go to implementation.
308    Implementation {
309        /// Path to the file containing the symbol.
310        file: String,
311        /// 1-based line number.
312        line: u32,
313        /// Optional symbol hint.
314        symbol: Option<String>,
315    },
316    /// Rename a file (workspace/willRenameFiles + applyWorkspaceEdit).
317    FileRename {
318        /// Current path on disk.
319        old_path: String,
320        /// Target path on disk.
321        new_path: String,
322        /// If true, apply the rename; otherwise just preview.
323        apply: bool,
324    },
325    /// Reload the LSP server (e.g. rust-analyzer/reloadWorkspace).
326    Reload,
327    /// Dump the server's capabilities (from the initialize handshake).
328    Capabilities,
329    /// Send a raw LSP request (method name + optional JSON params).
330    Request {
331        /// LSP method name (e.g. "workspace/symbol").
332        query: String,
333        /// Optional JSON payload. If absent, empty params are sent.
334        payload: Option<serde_json::Value>,
335    },
336}
337
338/// LSP access capability. Implemented by an `oxicode-lsp` crate (feature-gated)
339/// or stubbed with `None` when LSP is disabled.
340pub trait LspProvider: Send + Sync + std::fmt::Debug {
341    /// Kick off background initialisation (servers start but `ensure_ready`
342    /// isn't awaited). Idempotent.
343    fn ensure_started_background<'a>(&'a self) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>>;
344
345    /// Block until at least the configured LSP servers have finished their
346    /// `initialize` handshake (or the operation times out per the
347    /// provider's internal budget).
348    fn ensure_ready<'a>(&'a self) -> Pin<Box<dyn Future<Output = Result<(), String>> + Send + 'a>>;
349
350    /// Drain the most recent batch of diagnostics that arrived via
351    /// `textDocument/publishDiagnostics`. Returns `None` when nothing
352    /// fresh has arrived within `timeout`.
353    fn drain_diagnostics<'a>(
354        &'a self,
355        timeout: std::time::Duration,
356    ) -> Pin<Box<dyn Future<Output = Option<DiagnosticsSummary>> + Send + 'a>>;
357
358    /// Read the most recent cached diagnostics for the given file paths
359    /// (zero-copy snapshot — no waiting). Paths that have no fresh
360    /// diagnostics are omitted from the returned vec.
361    fn read_diagnostics<'a>(
362        &'a self,
363        paths: &'a [std::path::PathBuf],
364    ) -> Pin<Box<dyn Future<Output = Vec<FileDiagnosticEntry>> + Send + 'a>>;
365
366    /// Notify the LSP manager that the contents of `path` changed. The
367    /// manager is responsible for forwarding a `workspace/didChange` to
368    /// every server that owns the file. Default implementation is a no-op
369    /// so lightweight providers (e.g. test stubs) don't have to wire it.
370    fn notify_file_changed<'a>(
371        &'a self,
372        _path: &'a std::path::Path,
373        _content: &'a str,
374    ) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
375        Box::pin(async {})
376    }
377
378    /// Execute an LSP action and return formatted text output.
379    fn execute_action<'a>(
380        &'a self,
381        action: &'a LspAction,
382    ) -> Pin<Box<dyn Future<Output = Result<String, ToolError>> + Send + 'a>>;
383}
384
385// ── Sub-agent delegation (issue #28 gap 3) ─────────────────────────────
386
387/// Result of an in-process isolated sub-agent fork run.
388///
389/// Produced by [`SubagentRunner::run_isolated`]. The sub-agent runs
390/// with a **fresh, empty context** — its conversation history is
391/// completely isolated from the parent agent. Only the final text and
392/// usage statistics are returned, keeping the parent's context small.
393///
394/// This is the library-native alternative to shelling out to the `oxicode`
395/// CLI binary. Library consumers (e.g. Oxios) that embed `oxicode-agent`
396/// without an `oxicode` subprocess implement this trait so the `subagent`
397/// tool works in-process.
398#[derive(Debug, Clone, Default)]
399pub struct ForkResult {
400    /// Final response text from the sub-agent.
401    pub text: String,
402    /// Input tokens consumed (last reported turn).
403    pub input_tokens: usize,
404    /// Output tokens consumed (last reported turn).
405    pub output_tokens: usize,
406    /// Number of agent turns executed.
407    pub turns: u32,
408    /// Model ID used by the sub-agent.
409    pub model: Option<String>,
410    /// Error message if the run failed.
411    pub error: Option<String>,
412}
413
414/// In-process sub-agent runner — the library-native delegation backend.
415///
416/// When wired into [`ToolContext`] via
417/// [`ToolContext::with_subagent_runner`], the `subagent` tool prefers
418/// this in-process path over shelling out to the `oxicode` CLI binary.
419/// This is essential for library consumers (Oxios) that embed
420/// `oxicode-agent` as a kernel without an `oxicode` subprocess.
421///
422/// The SDK provides a ready-made implementation
423/// (`oxicode_sdk::SdkSubagentRunner`) that wraps an `Oxicode` instance and
424/// creates a fresh `Agent` for each invocation.
425#[async_trait::async_trait]
426#[allow(clippy::too_many_arguments)]
427pub trait SubagentRunner: Send + Sync + std::fmt::Debug {
428    /// Run a single agent task with an isolated (empty) context.
429    ///
430    /// # Arguments
431    /// * `agent_name` — Agent definition name (for logging / display).
432    /// * `task` — The task prompt to execute.
433    /// * `system_prompt` — Optional system prompt override.
434    /// * `model` — Optional model ID override (e.g. `"anthropic/claude-...`).
435    /// * `tools` — Optional tool whitelist (empty = all registered tools).
436    /// * `cwd` — Working directory for file tools.
437    /// * `depth` — Current sub-agent nesting depth. The runner sets
438    ///   the forked agent's `subagent_depth` to `depth + 1` so the
439    ///   fork's own subagent tool can enforce a recursion cap without
440    ///   env vars (issue #28 gap 3 — concurrent `set_var` is UB).
441    async fn run_isolated(
442        &self,
443        agent_name: &str,
444        task: &str,
445        system_prompt: Option<&str>,
446        model: Option<&str>,
447        tools: &[String],
448        cwd: &Path,
449        depth: u8,
450    ) -> anyhow::Result<ForkResult>;
451}
452
453/// Context passed to tools at execution time.
454///
455/// This allows tools to operate on a specific workspace without being
456/// rebuilt. When `root_dir` is `Some`, tools use it as their base directory.
457/// When `None`, tools should fall back to `workspace_dir`.
458#[derive(Clone)]
459pub struct ToolContext {
460    /// Primary workspace directory (used when root_dir is None).
461    pub workspace_dir: PathBuf,
462    /// Optional explicit root directory for file tools.
463    /// Takes priority over workspace_dir if present.
464    pub root_dir: Option<PathBuf>,
465    /// Session identifier for logging/tracing.
466    pub session_id: Option<String>,
467    /// Snapshot store for hashline tag emission/validation.
468    /// When `None`, hashline edit mode is unavailable.
469    pub snapshot_store: Option<Arc<dyn oxicode_hashline::SnapshotStore>>,
470    /// Memory backend for `memory_*` tools.
471    /// When `None`, memory tools return an error.
472    pub memory: Option<Arc<dyn MemoryBackend>>,
473    /// URL resolver for internal protocol schemes (`issue://`, `pr://`, etc.).
474    /// When `None`, URL-prefixed paths are treated as regular file paths.
475    pub url_resolver: Option<Arc<dyn UrlResolver>>,
476    /// Todo state for the `todo` agent tool.
477    /// When `None`, the `todo` tool returns an error.
478    pub todo: Option<Arc<dyn TodoStateProvider>>,
479    /// Agent pool for Hub display and todo sub-agent matching.
480    pub agent_pool: Option<Arc<dyn AgentPoolProvider>>,
481    /// LSP provider for the `lsp` tool.
482    pub lsp: Option<Arc<dyn LspProvider>>,
483    /// In-process sub-agent runner (issue #28 gap 3).
484    /// When `Some`, the `subagent` tool prefers an in-process isolated
485    /// run over shelling out to the CLI binary. Library consumers
486    /// (e.g. Oxios) that embed `oxicode-agent` without an `oxicode` subprocess
487    /// set this so delegation works. When `None`, the CLI backend is
488    /// used (the default for `oxicode-cli`).
489    pub subagent_runner: Option<Arc<dyn SubagentRunner>>,
490    /// Current sub-agent nesting depth for the in-process path
491    /// (issue #28 gap 3). The CLI path uses env vars instead.
492    /// Default 0 (top-level agent).
493    pub subagent_depth: u8,
494    /// Intent trace for the current tool call.
495    /// Set by the agent loop before executing each tool, read by tools
496    /// that surface intent to users (e.g. `ask`).
497    pub intent: Option<String>,
498}
499
500impl fmt::Debug for ToolContext {
501    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
502        f.debug_struct("ToolContext")
503            .field("workspace_dir", &self.workspace_dir)
504            .field("root_dir", &self.root_dir)
505            .field("session_id", &self.session_id)
506            .field(
507                "snapshot_store",
508                &self.snapshot_store.as_ref().map(|_| "<dyn SnapshotStore>"),
509            )
510            .field(
511                "memory",
512                &self.memory.as_ref().map(|_| "<dyn MemoryBackend>"),
513            )
514            .field(
515                "url_resolver",
516                &self.url_resolver.as_ref().map(|_| "<dyn UrlResolver>"),
517            )
518            .finish()
519    }
520}
521
522impl ToolContext {
523    /// Create a new context with the given workspace.
524    pub fn new(workspace_dir: impl Into<PathBuf>) -> Self {
525        Self {
526            workspace_dir: workspace_dir.into(),
527            root_dir: None,
528            session_id: None,
529            snapshot_store: None,
530            memory: None,
531            url_resolver: None,
532            todo: None,
533            agent_pool: None,
534            lsp: None,
535            subagent_runner: None,
536            subagent_depth: 0,
537            intent: None,
538        }
539    }
540
541    /// Get the effective root directory.
542    /// Returns root_dir if set, otherwise workspace_dir.
543    pub fn root(&self) -> &Path {
544        self.root_dir.as_deref().unwrap_or(&self.workspace_dir)
545    }
546
547    /// Set a session ID.
548    pub fn with_session(mut self, session_id: impl Into<String>) -> Self {
549        self.session_id = Some(session_id.into());
550        self
551    }
552
553    /// Set an explicit root directory.
554    pub fn with_root(mut self, root_dir: impl Into<PathBuf>) -> Self {
555        self.root_dir = Some(root_dir.into());
556        self
557    }
558
559    /// Set the snapshot store (enables hashline edit mode).
560    pub fn with_snapshot_store(mut self, store: Arc<dyn oxicode_hashline::SnapshotStore>) -> Self {
561        self.snapshot_store = Some(store);
562        self
563    }
564
565    /// Set the memory backend (enables memory tools).
566    pub fn with_memory(mut self, memory: Arc<dyn MemoryBackend>) -> Self {
567        self.memory = Some(memory);
568        self
569    }
570
571    /// Set the URL resolver (enables internal URL dispatch).
572    pub fn with_url_resolver(mut self, resolver: Arc<dyn UrlResolver>) -> Self {
573        self.url_resolver = Some(resolver);
574        self
575    }
576
577    /// Set the todo state (enables the `todo` agent tool).
578    pub fn with_todo(mut self, todo: Arc<dyn TodoStateProvider>) -> Self {
579        self.todo = Some(todo);
580        self
581    }
582
583    /// Set the in-process sub-agent runner (enables library-native
584    /// delegation — issue #28 gap 3).
585    pub fn with_subagent_runner(mut self, runner: Arc<dyn SubagentRunner>) -> Self {
586        self.subagent_runner = Some(runner);
587        self
588    }
589
590    /// Attach an intent trace to this context.
591    pub fn with_intent(mut self, intent: impl Into<String>) -> Self {
592        self.intent = Some(intent.into());
593        self
594    }
595}
596
597impl Default for ToolContext {
598    fn default() -> Self {
599        Self {
600            workspace_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
601            root_dir: None,
602            session_id: None,
603            snapshot_store: None,
604            memory: None,
605            url_resolver: None,
606            todo: None,
607            agent_pool: None,
608            lsp: None,
609            subagent_runner: None,
610            subagent_depth: 0,
611            intent: None,
612        }
613    }
614}
615
616/// Result type for tool execution
617pub type ToolError = String;
618
619/// Result of tool execution
620#[derive(Debug)]
621pub struct AgentToolResult {
622    /// pub.
623    pub success: bool,
624    /// pub.
625    pub output: String,
626    /// pub.
627    pub metadata: Option<serde_json::Value>,
628    /// Optional content blocks (e.g., image blocks) to include in the tool result message.
629    /// When present, these are used as the content of the ToolResultMessage instead of
630    /// wrapping `output` in a Text block.
631    pub content_blocks: Option<Vec<oxicode_ai::ContentBlock>>,
632    /// When `true`, signals that the agent loop should terminate after this batch
633    /// of tool calls completes.  Defaults to `false` so that the loop continues
634    /// unless a tool explicitly opts-in to termination.
635    pub terminate: bool,
636    /// Intent trace — a concise description of what this specific tool call did.
637    /// Set by the agent loop from the tool's static `intent()` or by the tool
638    /// itself for dynamic intent. Included in `ToolExecutionEnd` events.
639    pub intent: Option<String>,
640}
641
642impl AgentToolResult {
643    /// Creates a successful tool result with the given output text.
644    pub fn success(output: impl Into<String>) -> Self {
645        Self {
646            success: true,
647            output: output.into(),
648            metadata: None,
649            content_blocks: None,
650            terminate: false,
651            intent: None,
652        }
653    }
654
655    /// Creates an error tool result with the given error message.
656    pub fn error(output: impl Into<String>) -> Self {
657        Self {
658            success: false,
659            output: output.into(),
660            metadata: None,
661            content_blocks: None,
662            terminate: false,
663            intent: None,
664        }
665    }
666
667    /// Attaches structured metadata (JSON) to this result.
668    pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
669        self.metadata = Some(metadata);
670        self
671    }
672
673    /// Attaches rich content blocks (images, code, etc.) to this result.
674    pub fn with_content_blocks(mut self, blocks: Vec<oxicode_ai::ContentBlock>) -> Self {
675        self.content_blocks = Some(blocks);
676        self
677    }
678
679    /// Mark this result as requesting agent-loop termination.
680    pub fn with_terminate(mut self) -> Self {
681        self.terminate = true;
682        self
683    }
684
685    /// Attach an intent trace to this result.
686    pub fn with_intent(mut self, intent: impl Into<String>) -> Self {
687        self.intent = Some(intent.into());
688        self
689    }
690}
691
692impl fmt::Display for AgentToolResult {
693    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
694        write!(f, "{}", self.output)
695    }
696}
697
698/// Callback type for progress updates
699pub type ProgressCallback = Arc<dyn Fn(String) + Send + Sync>;
700
701/// Tool execution mode for parallel safety.
702#[derive(Debug, Clone)]
703pub enum ToolExecutionMode {
704    /// Safe to run in parallel with any other tool
705    ParallelSafe,
706    /// Must run sequentially — no parallel execution
707    SequentialOnly,
708    /// Mutates a specific file — file_mutation_queue serializes same-file access
709    MutatesFile(std::path::PathBuf),
710    /// Read-only — always parallel safe
711    ReadOnly,
712}
713
714/// Render output for TUI visualization.
715#[derive(Debug, Clone)]
716pub struct RenderOutput {
717    /// Rendered text content (markdown or plain)
718    pub content: String,
719    /// Whether to show collapsed by default
720    pub collapsed: bool,
721    /// Optional summary text for TUI footer
722    pub summary: Option<String>,
723}
724
725/// Core trait for all agent tools
726/// Risk tier for approval gating.
727///
728/// Determines which approval tiers gate a tool call.
729/// - `Read`  — no side effects (lookup, search, inspection).
730/// - `Write` — mutates data (creates, edits, commits).
731/// - `Exec`  — arbitrary side effects (shell, eval, network).
732#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
733pub enum ToolTier {
734    /// Read-only — inspection, search, lookup.
735    Read,
736    /// Data mutation — create, edit, commit.
737    Write,
738    /// Arbitrary execution — shell, eval, network, subagent.
739    #[default]
740    Exec,
741}
742
743/// Core trait for all agent tools
744#[async_trait]
745pub trait AgentTool: Send + Sync {
746    /// Tool name (used in function calls)
747    fn name(&self) -> &str;
748
749    /// Human-readable label
750    fn label(&self) -> &str;
751
752    /// Description for the model
753    fn description(&self) -> &str;
754
755    /// JSON Schema for parameters
756    fn parameters_schema(&self) -> Value;
757
758    /// Whether this tool is essential (cannot be disabled).
759    /// Essential tools: read, write, edit, bash, grep, find, ls
760    /// Optional tools: web_search, github, subagent, etc.
761    fn essential(&self) -> bool {
762        false
763    }
764
765    /// Execute the tool with the given tool call ID and parameters.
766    ///
767    /// The `ctx` parameter provides workspace information. File tools should
768    /// use `ctx.root()` to get the effective directory. Custom tools can use
769    /// `ctx.workspace_dir` for workspace-relative operations.
770    ///
771    /// # Examples
772    ///
773    /// ```ignore
774    /// use oxicode_agent::{AgentTool, AgentToolResult, ToolContext};
775    /// use serde_json::json;
776    /// struct MyTool;
777    ///
778    /// #[async_trait]
779    /// impl AgentTool for MyTool {
780    ///     fn name(&self) -> &str { "my_tool" }
781    ///     fn label(&self) -> &str { "My Tool" }
782    ///     fn description(&self) -> &str { "A custom tool" }
783    ///     fn parameters_schema(&self) -> Value { json!({
784    ///         "type": "object",
785    ///         "properties": {}
786    ///     }) }
787    ///
788    ///     async fn execute(&self, tool_call_id: &str, params: Value, _signal: Option<oneshot::Receiver<()>>, ctx: &ToolContext) -> Result<AgentToolResult, String> {
789    ///         println!("Tool '{}' called with params: {:?}, workspace: {:?}", tool_call_id, params, ctx.workspace_dir);
790    ///         Ok(AgentToolResult::success("Done!"))
791    ///     }
792    /// }
793    /// ```
794    async fn execute(
795        &self,
796        tool_call_id: &str,
797        params: Value,
798        signal: Option<oneshot::Receiver<()>>,
799        ctx: &ToolContext,
800    ) -> Result<AgentToolResult, ToolError>;
801
802    /// Called with progress updates during execution.
803    /// Tools can override this to emit streaming updates.
804    fn on_progress(&self, _callback: ProgressCallback) {
805        // Default no-op
806    }
807
808    /// Structured browse progress callback for browser tool context enrichment.
809    /// Default implementation is no-op. Only browse tools override this to
810    /// register a callback that enriches `ToolCallContext` with structured
811    /// data from `BrowseProgress` events.
812    fn on_browse_progress(&self, _callback: crate::tools::browse::BrowseProgressCallback) {}
813
814    /// Custom rendering for tool call (TUI visualization).
815    /// Return None to use the default tool_renderer.rs formatter.
816    fn render_call(&self, _params: &serde_json::Value) -> Option<RenderOutput> {
817        None
818    }
819
820    /// Custom rendering for tool result (TUI visualization).
821    /// Return None to use the default tool_renderer.rs formatter.
822    fn render_result(&self, _result: &AgentToolResult) -> Option<RenderOutput> {
823        None
824    }
825
826    /// Intent trace — a concise description of what this tool does.
827    /// Returned value is included in `ToolExecutionStart` / `ToolExecutionEnd`
828    /// events so the agent loop can surface intent to users or telemetry.
829    /// Default `None` (no intent tracing).
830    fn intent(&self) -> Option<&str> {
831        None
832    }
833
834    /// Execution mode for parallel safety.
835    /// Defaults to ParallelSafe. Override for file-mutating or sequential tools.
836    fn execution_mode(&self) -> ToolExecutionMode {
837        ToolExecutionMode::ParallelSafe
838    }
839
840    /// Risk tier for approval gating.
841    ///
842    /// - `Read`  — no side effects (lookup, search, inspection).
843    /// - `Write` — mutates data (creates, edits, commits).
844    /// - `Exec`  — arbitrary side effects (shell, eval, network).
845    ///
846    /// Default: `Exec` (safest default — requires explicit opt-down).
847    fn tool_tier(&self) -> ToolTier {
848        ToolTier::Exec
849    }
850
851    /// Return the current active tab ID, if this tool manages browser tabs.
852    /// Defaults to `None`. Browser tools override this to return the tab ID
853    /// of the currently-open tab during execution, so the agent loop can
854    /// populate `ToolExecutionUpdate.tab_id`.
855    fn current_tab_id(&self) -> Option<uuid::Uuid> {
856        None
857    }
858
859    /// Receive a shared slot where the tool can write the current tab ID.
860    /// The agent loop creates the slot and passes it before `on_progress`;
861    /// the tool writes `Some(tab_id)` when it opens a tab and `None` when
862    /// it closes it. Defaults to a no-op — only tab-aware tools override.
863    fn set_tab_id_slot(&self, _slot: Arc<parking_lot::Mutex<Option<uuid::Uuid>>>) {}
864
865    /// Convert to ToolDefinition
866    fn to_definition(&self) -> ToolDefinition {
867        ToolDefinition {
868            name: self.name().to_string(),
869            description: self.description().to_string(),
870            input_schema: serde_json::from_value(self.parameters_schema()).unwrap_or_default(),
871        }
872    }
873}
874
875// Built-in tools
876/// Ask tool — ask the user one or more clarifying questions via the TUI overlay.
877pub mod ask;
878/// AST-aware structural code rewriting tool (ast-grep backed).
879pub mod ast_edit;
880/// AST structural search tool (wraps the `sg` CLI).
881pub mod ast_grep;
882/// Bash shell execution tool.
883pub mod bash;
884/// Browser tools (engine abstraction always compiled).
885pub mod browse;
886/// Checkpoint and Rewind tools — save/restore investigation state.
887pub mod checkpoint_tool;
888/// Conventional-commit tool (deterministic scope + LLM analysis).
889pub mod commit;
890/// Computer tool — computer control using Vision AI.
891pub mod computer_tool;
892/// Context7 documentation tools.
893pub mod context7;
894/// Debug tool — DAP-backed debugger integration (scaffold).
895pub mod debug_tool;
896/// In-place file edit tool.
897pub mod edit;
898/// Diff-based edit helpers.
899pub mod edit_diff;
900/// Eval tool — persistent-kernel code execution (scaffold).
901pub mod eval_tool;
902/// Serialised file-mutation queue.
903pub mod file_mutation_queue;
904/// File-fsystem find tool.
905pub mod find;
906/// Image generation tool (OpenRouter API).
907pub mod generate_image;
908/// GitHub integration tool (gh CLI-based).
909pub mod github;
910/// GitHub repository search tool (legacy REST API).
911pub mod github_search;
912/// Goal tool — manage investigation goals with token budgets.
913pub mod goal_tool;
914/// Content search (grep) tool.
915pub mod grep;
916/// TokioHashlineFs — tokio::fs-backed HashlineFs implementation.
917pub mod hashline_fs;
918/// Shared HTTP client singleton.
919pub mod http_client;
920/// Hub tool — agent coordination for peer messaging and job management.
921pub mod hub_tool;
922/// Inspect Image tool — analyze images using Vision LLM capabilities.
923pub mod inspect_image_tool;
924/// Learn tool — capture a reusable lesson to memory and optionally create a managed skill.
925pub mod learn_tool;
926/// Directory listing tool.
927pub mod ls;
928/// LSP tool (requires LspProvider capability).
929pub mod lsp;
930/// Manage Skill tool — create, update, or delete isolated managed SKILL.md files.
931pub mod manage_skill_tool;
932/// Memory edit tool — update or delete a memory item.
933pub mod memory_edit;
934/// Memory recall tool — semantic search over stored memories.
935pub mod memory_recall;
936/// Memory reflect tool — persist a session summary to memory.
937pub mod memory_reflect;
938/// Memory retain tool — persist a memory item to the backend.
939pub mod memory_retain;
940/// Path security (traversal protection).
941pub mod path_security;
942/// Path manipulation utilities.
943pub mod path_utils;
944/// File reading tool.
945pub mod read;
946/// Rendering utilities for tool output.
947pub mod render_utils;
948/// Review tool — request code review with focus areas and priorities.
949pub mod review_tool;
950/// Search result cache and get_search_results tool.
951pub mod search_cache;
952/// Sub-agent delegation tool.
953pub mod subagent;
954/// Phased todo tool (init/start/done/drop/rm/append/view).
955pub mod todo;
956/// Tool definition wrapper helpers.
957pub mod tool_definition_wrapper;
958/// Output truncation helpers.
959pub mod truncate;
960/// TTS tool — text-to-speech synthesis.
961pub mod tts_tool;
962/// Vibe tool — manage persistent worker sessions.
963pub mod vibe_tool;
964/// Multi-engine web search tool (oxibrowser search module).
965pub mod web_search;
966/// File writing tool.
967pub mod write;
968/// Yield tool — subagent result submission.
969pub mod yield_tool;
970
971// Re-export for convenience
972pub use bash::BashTool;
973pub use debug_tool::DebugTool;
974pub use edit::EditTool;
975pub use eval_tool::EvalTool;
976pub use find::FindTool;
977pub use grep::GrepTool;
978pub use ls::LsTool;
979pub use read::ReadTool;
980// pub use search_cache;
981
982pub use crate::mcp::McpTool;
983pub use ask::{AskBridge, AskTool};
984pub use ast_edit::AstEditTool;
985pub use ast_grep::AstGrepTool;
986pub use commit::CommitTool;
987pub use context7::{Context7QueryDocsTool, Context7ResolveLibraryIdTool};
988pub use memory_edit::MemoryEditTool;
989pub use memory_recall::MemoryRecallTool;
990pub use memory_reflect::MemoryReflectTool;
991pub use memory_retain::MemoryRetainTool;
992pub use subagent::SubagentTool;
993pub use write::WriteTool;
994
995/// Tool registry for managing available tools
996#[derive(Clone)]
997pub struct ToolRegistry {
998    tools: Arc<parking_lot::RwLock<std::collections::HashMap<String, Arc<dyn AgentTool>>>>,
999    /// Optional MCP manager, set by `with_builtins_cwd()` so the TUI and
1000    /// other consumers can reach the live MCP state (Phase 2+).
1001    mcp_manager: Arc<parking_lot::RwLock<Option<Arc<crate::mcp::McpManager>>>>,
1002}
1003
1004impl Default for ToolRegistry {
1005    fn default() -> Self {
1006        Self::new()
1007    }
1008}
1009
1010impl ToolRegistry {
1011    /// Creates an empty tool registry.
1012    pub fn new() -> Self {
1013        Self {
1014            tools: Arc::new(parking_lot::RwLock::new(std::collections::HashMap::new())),
1015            mcp_manager: Arc::new(parking_lot::RwLock::new(None)),
1016        }
1017    }
1018
1019    /// Attach an `McpManager` to this registry. Replaces any previous one.
1020    pub fn set_mcp_manager(&self, mgr: Arc<crate::mcp::McpManager>) {
1021        *self.mcp_manager.write() = Some(mgr);
1022    }
1023
1024    /// Get the attached `McpManager`, if any.
1025    pub fn mcp_manager(&self) -> Option<Arc<crate::mcp::McpManager>> {
1026        self.mcp_manager.read().clone()
1027    }
1028
1029    /// Register a tool
1030    pub fn register(&self, tool: impl AgentTool + 'static) {
1031        let name = tool.name().to_string();
1032        self.tools.write().insert(name, Arc::new(tool));
1033    }
1034
1035    /// Register a tool that is already wrapped in an `Arc`.
1036    /// This is the primary path for extensions that produce `Arc<dyn AgentTool>`.
1037    pub fn register_arc(&self, tool: Arc<dyn AgentTool>) {
1038        let name = tool.name().to_string();
1039        self.tools.write().insert(name, tool);
1040    }
1041
1042    /// Get a tool by name
1043    pub fn get(&self, name: &str) -> Option<Arc<dyn AgentTool>> {
1044        self.tools.read().get(name).cloned()
1045    }
1046
1047    /// Unregister a tool by name.
1048    /// Returns `true` if the tool was present and removed.
1049    pub fn unregister(&self, name: &str) -> bool {
1050        self.tools.write().remove(name).is_some()
1051    }
1052
1053    /// List all registered tool names
1054    pub fn names(&self) -> Vec<String> {
1055        self.tools.read().keys().cloned().collect()
1056    }
1057
1058    /// Get all tool definitions
1059    pub fn definitions(&self) -> Vec<ToolDefinition> {
1060        self.tools
1061            .read()
1062            .values()
1063            .map(|t| t.to_definition())
1064            .collect()
1065    }
1066
1067    /// Get all tools as a slice
1068    pub fn get_tools(&self) -> Vec<Arc<dyn AgentTool>> {
1069        self.tools.read().values().cloned().collect()
1070    }
1071
1072    /// Check whether all tools in `required` are registered.
1073    ///
1074    /// Useful for validating program/module dependencies before execution.
1075    ///
1076    /// # Example
1077    ///
1078    /// ```
1079    /// use oxicode_agent::ToolRegistry;
1080    /// let registry = ToolRegistry::new();
1081    /// assert!(!registry.has_all(&["read", "write"]));
1082    /// ```
1083    pub fn has_all(&self, required: &[&str]) -> bool {
1084        let tools = self.tools.read();
1085        required.iter().all(|name| tools.contains_key(*name))
1086    }
1087
1088    /// Return the subset of `required` tool names that are **not** registered.
1089    ///
1090    /// # Example
1091    ///
1092    /// ```
1093    /// use oxicode_agent::ToolRegistry;
1094    /// let registry = ToolRegistry::new();
1095    /// let missing = registry.missing(&["read", "exec", "nonexistent"]);
1096    /// assert_eq!(missing, vec!["read", "exec", "nonexistent"]);
1097    /// ```
1098    pub fn missing<'a>(&self, required: &[&'a str]) -> Vec<&'a str> {
1099        let tools = self.tools.read();
1100        required
1101            .iter()
1102            .filter(|name| !tools.contains_key(**name))
1103            .copied()
1104            .collect()
1105    }
1106
1107    /// Create a registry with all built-in tools
1108    ///
1109    /// # Examples
1110    ///
1111    /// ```
1112    /// use oxicode_agent::ToolRegistry;
1113    /// let registry = ToolRegistry::with_builtins();
1114    /// let tools = registry.names();
1115    /// assert!(tools.contains(&"read".to_string()));
1116    /// assert!(tools.contains(&"write".to_string()));
1117    /// assert!(tools.contains(&"bash".to_string()));
1118    /// ```
1119    pub fn with_builtins() -> Self {
1120        Self::with_builtins_cwd(PathBuf::from("."), &[])
1121    }
1122
1123    /// Create a registry with all built-in tools, using the given cwd.
1124    ///
1125    /// Pass `disabled_tools` to selectively disable built-in tools
1126    /// (e.g. `["web_search", "github_search"]` for a minimal setup).
1127    pub fn with_builtins_cwd(cwd: PathBuf, disabled_tools: &[String]) -> Self {
1128        let registry = Self::new();
1129        let disabled: std::collections::HashSet<&str> =
1130            disabled_tools.iter().map(|s| s.as_str()).collect();
1131
1132        // Helper to create shared cache on demand
1133        let cache_once: std::cell::OnceCell<Arc<search_cache::SearchCache>> =
1134            std::cell::OnceCell::new();
1135
1136        // MCP: use OnceCell to avoid re-creating McpManager on repeated calls
1137        let mcp_once: std::cell::OnceCell<Arc<crate::mcp::McpManager>> = std::cell::OnceCell::new();
1138        let mcp_manager = mcp_once.get_or_init(crate::mcp::McpManager::spawn).clone();
1139
1140        // Register all builtin tools — essential ones ignore disabled list
1141        let mut all_tools: Vec<Box<dyn AgentTool>> = vec![
1142            Box::new(ReadTool::with_cwd(cwd.clone())),
1143            Box::new(WriteTool::with_cwd(cwd.clone())),
1144            Box::new(AstGrepTool::with_cwd(cwd.clone())),
1145            Box::new(BashTool::with_cwd(cwd.clone())),
1146            Box::new(EditTool::with_cwd(cwd.clone())),
1147            Box::new(GrepTool::with_cwd(cwd.clone())),
1148            Box::new(FindTool::with_cwd(cwd.clone())),
1149            Box::new(LsTool::with_cwd(cwd.clone())),
1150            Box::new(web_search::WebSearchTool::new(
1151                cache_once
1152                    .get_or_init(|| Arc::new(search_cache::SearchCache::new()))
1153                    .clone(),
1154            )),
1155            Box::new(search_cache::GetSearchResultsTool::new(
1156                cache_once
1157                    .get_or_init(|| Arc::new(search_cache::SearchCache::new()))
1158                    .clone(),
1159            )),
1160            Box::new(github::GitHubTool::new(
1161                cache_once
1162                    .get_or_init(|| Arc::new(search_cache::SearchCache::new()))
1163                    .clone(),
1164            )),
1165            Box::new(SubagentTool::with_cwd(cwd.clone())),
1166            Box::new(todo::TodoTool),
1167            Box::new(memory_recall::MemoryRecallTool),
1168            Box::new(memory_reflect::MemoryReflectTool),
1169            Box::new(memory_retain::MemoryRetainTool),
1170            Box::new(memory_edit::MemoryEditTool),
1171        ];
1172
1173        all_tools.push(Box::new(crate::mcp::McpTool::new(mcp_manager.clone())));
1174
1175        // Phase 3: register direct MCP tools from the metadata cache.
1176        for def in mcp_manager.direct_tools_from_cache() {
1177            all_tools.push(Box::new(crate::mcp::McpDirectTool::new(
1178                mcp_manager.clone(),
1179                def,
1180            )));
1181        }
1182
1183        // Remember the manager on the registry so the TUI can reach it.
1184        registry.set_mcp_manager(mcp_manager);
1185
1186        all_tools.push(Box::new(context7::Context7ResolveLibraryIdTool::new()));
1187        all_tools.push(Box::new(context7::Context7QueryDocsTool::new()));
1188        all_tools.push(Box::new(generate_image::GenerateImageTool::new()));
1189        all_tools.push(Box::new(commit::CommitTool::unconfigured()));
1190        all_tools.push(Box::new(ast_edit::AstEditTool::new()));
1191        all_tools.push(Box::new(lsp::LspTool));
1192        all_tools.push(Box::new(eval_tool::EvalTool));
1193        all_tools.push(Box::new(checkpoint_tool::CheckpointTool));
1194        all_tools.push(Box::new(checkpoint_tool::RewindTool));
1195        all_tools.push(Box::new(hub_tool::HubTool));
1196        all_tools.push(Box::new(yield_tool::YieldTool));
1197        all_tools.push(Box::new(goal_tool::GoalTool));
1198        all_tools.push(Box::new(review_tool::ReviewTool));
1199        all_tools.push(Box::new(learn_tool::LearnTool));
1200        all_tools.push(Box::new(manage_skill_tool::ManageSkillTool));
1201        all_tools.push(Box::new(inspect_image_tool::InspectImageTool));
1202        all_tools.push(Box::new(computer_tool::ComputerTool));
1203        all_tools.push(Box::new(tts_tool::TtsTool));
1204        all_tools.push(Box::new(vibe_tool::VibeTool));
1205        // debug_tool — DAP-backed debugger integration.
1206        // Most actions are validated scaffolds (route through xd://debug);
1207        // real launch/attach/breakpoint control is wired via the harness device.
1208        all_tools.push(Box::new(debug_tool::DebugTool));
1209
1210        for tool in all_tools {
1211            if tool.essential() || !disabled.contains(tool.name()) {
1212                // web_search ↔ get_search_results coupling
1213                if tool.name() == "get_search_results" && disabled.contains("web_search") {
1214                    continue;
1215                }
1216                registry.register_arc(Arc::from(tool));
1217            }
1218        }
1219
1220        registry
1221    }
1222
1223    /// Extend this registry with all tools from another registry.
1224    ///
1225    /// Useful for composing tool sets from multiple sources
1226    /// (e.g., coding tools + kernel tools + browser tools).
1227    ///
1228    /// # Example
1229    ///
1230    /// ```ignore
1231    /// let base = ToolRegistry::new();
1232    /// base.extend_from(&other_registry);
1233    /// ```
1234    pub fn extend_from(&self, other: &ToolRegistry) {
1235        for name in other.names() {
1236            if let Some(tool) = other.get(&name) {
1237                self.register_arc(tool);
1238            }
1239        }
1240    }
1241
1242    /// Create registry with selected builtins only.
1243    pub fn with_selected_tools(cwd: PathBuf, names: &[&str]) -> Self {
1244        let full = Self::with_builtins_cwd(cwd, &[]);
1245        let registry = Self::new();
1246        let set: std::collections::HashSet<&str> = names.iter().copied().collect();
1247        for name in full.names() {
1248            if set.contains(name.as_str())
1249                && let Some(tool) = full.get(&name)
1250            {
1251                registry.register_arc(tool);
1252            }
1253        }
1254        registry
1255    }
1256}