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