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