Skip to main content

codetether_agent/tool/
mod.rs

1//! Tool system
2//!
3//! Tools are the executable capabilities available to agents.
4
5pub mod advanced_edit;
6pub mod agent;
7pub mod alias;
8pub mod avatar;
9pub mod bash;
10#[path = "bash_github/mod.rs"]
11mod bash_github;
12mod bash_identity;
13mod bash_shell;
14pub mod batch;
15pub mod browserctl;
16pub mod codesearch;
17pub mod confirm_edit;
18pub mod confirm_multiedit;
19pub mod context_browse;
20pub mod context_reset;
21pub mod edit;
22pub mod file;
23pub mod file_extras;
24pub mod go;
25pub mod image;
26pub mod invalid;
27pub mod k8s_tool;
28pub mod lsp;
29pub mod mcp_bridge;
30pub mod mcp_tools;
31pub mod memory;
32pub mod morph_backend;
33pub mod multiedit;
34pub mod okr;
35pub mod patch;
36pub mod plan;
37pub mod podcast;
38pub mod prd;
39pub mod question;
40pub mod ralph;
41pub mod readonly;
42pub mod relay_autochat;
43pub mod rlm;
44pub mod sandbox;
45pub mod search;
46pub mod search_router;
47pub mod session_recall;
48pub mod session_task;
49pub mod skill;
50pub mod swarm_execute;
51pub mod swarm_share;
52pub mod task;
53pub mod todo;
54pub mod undo;
55pub mod voice;
56pub mod webfetch;
57pub mod websearch;
58pub mod youtube;
59
60use anyhow::Result;
61use async_trait::async_trait;
62use serde::{Deserialize, Serialize};
63use serde_json::Value;
64use std::collections::HashMap;
65use std::sync::Arc;
66
67use crate::provider::Provider;
68pub use mcp_tools::{McpToolManager, McpToolWrapper};
69pub use sandbox::{PluginManifest, PluginRegistry, SigningKey, hash_bytes, hash_file};
70
71/// A tool that can be executed by an agent
72#[async_trait]
73pub trait Tool: Send + Sync {
74    /// Tool identifier
75    fn id(&self) -> &str;
76
77    /// Human-readable name
78    fn name(&self) -> &str;
79
80    /// Description for the LLM
81    fn description(&self) -> &str;
82
83    /// JSON Schema for parameters
84    fn parameters(&self) -> Value;
85
86    /// Execute the tool with given arguments
87    async fn execute(&self, args: Value) -> Result<ToolResult>;
88}
89
90/// Result from tool execution
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct ToolResult {
93    pub output: String,
94    pub success: bool,
95    #[serde(default)]
96    pub metadata: HashMap<String, Value>,
97}
98
99impl ToolResult {
100    pub fn success(output: impl Into<String>) -> Self {
101        Self {
102            output: output.into(),
103            success: true,
104            metadata: HashMap::new(),
105        }
106    }
107
108    pub fn error(message: impl Into<String>) -> Self {
109        Self {
110            output: message.into(),
111            success: false,
112            metadata: HashMap::new(),
113        }
114    }
115
116    /// Create a structured error with code, tool name, missing fields, and example
117    ///
118    /// This helps LLMs self-correct by providing actionable information about what went wrong.
119    pub fn structured_error(
120        code: &str,
121        tool: &str,
122        message: &str,
123        missing_fields: Option<Vec<&str>>,
124        example: Option<Value>,
125    ) -> Self {
126        let mut error_obj = serde_json::json!({
127            "code": code,
128            "tool": tool,
129            "message": message,
130        });
131
132        if let Some(fields) = missing_fields {
133            error_obj["missing_fields"] = serde_json::json!(fields);
134        }
135
136        if let Some(ex) = example {
137            error_obj["example"] = ex;
138        }
139
140        let output = serde_json::to_string_pretty(&serde_json::json!({
141            "error": error_obj
142        }))
143        .unwrap_or_else(|_| format!("Error: {}", message));
144
145        let mut metadata = HashMap::new();
146        metadata.insert("error_code".to_string(), serde_json::json!(code));
147        metadata.insert("tool".to_string(), serde_json::json!(tool));
148
149        Self {
150            output,
151            success: false,
152            metadata,
153        }
154    }
155
156    pub fn with_metadata(mut self, key: impl Into<String>, value: Value) -> Self {
157        self.metadata.insert(key.into(), value);
158        self
159    }
160
161    /// Truncate `output` to at most `max_bytes`, tagging `metadata.truncated`
162    /// with the original length when truncation occurs.
163    ///
164    /// Uses UTF-8-safe truncation. When the output fits, returns `self`
165    /// unchanged.
166    ///
167    /// # Examples
168    ///
169    /// ```rust
170    /// use codetether_agent::tool::ToolResult;
171    ///
172    /// let r = ToolResult::success("x".repeat(1_000)).truncate_to(100);
173    /// assert!(r.output.len() <= 100 + 32); // + marker
174    /// assert!(r.metadata.contains_key("truncated"));
175    /// ```
176    pub fn truncate_to(mut self, max_bytes: usize) -> Self {
177        if self.output.len() <= max_bytes {
178            return self;
179        }
180        let original_len = self.output.len();
181        let head = crate::util::truncate_bytes_safe(&self.output, max_bytes);
182        self.output =
183            format!("{head}\n…[truncated: {original_len} bytes, showing first {max_bytes}]");
184        self.metadata.insert(
185            "truncated".to_string(),
186            serde_json::json!({
187                "original_bytes": original_len,
188                "shown_bytes": max_bytes,
189            }),
190        );
191        self
192    }
193}
194
195/// Default per-tool-output byte budget. Tunable at runtime via the
196/// `CODETETHER_TOOL_OUTPUT_MAX_BYTES` environment variable. Chosen to keep a
197/// single tool result well under typical provider context windows even after
198/// JSON re-encoding overhead.
199pub const DEFAULT_TOOL_OUTPUT_MAX_BYTES: usize = 64 * 1024;
200
201/// Resolve the current tool-output byte budget from env, falling back to
202/// [`DEFAULT_TOOL_OUTPUT_MAX_BYTES`]. Invalid values fall back to the default.
203pub fn tool_output_budget() -> usize {
204    std::env::var("CODETETHER_TOOL_OUTPUT_MAX_BYTES")
205        .ok()
206        .and_then(|v| v.parse().ok())
207        .unwrap_or(DEFAULT_TOOL_OUTPUT_MAX_BYTES)
208}
209
210/// Registry of available tools
211pub struct ToolRegistry {
212    tools: HashMap<String, Arc<dyn Tool>>,
213    plugin_registry: PluginRegistry,
214}
215
216impl std::fmt::Debug for ToolRegistry {
217    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
218        f.debug_struct("ToolRegistry")
219            .field("tools", &self.tools.keys().collect::<Vec<_>>())
220            .finish()
221    }
222}
223
224impl ToolRegistry {
225    pub fn new() -> Self {
226        let _ = std::any::TypeId::of::<McpToolManager>();
227        let _ = std::any::TypeId::of::<McpToolWrapper>();
228        Self {
229            tools: HashMap::new(),
230            plugin_registry: PluginRegistry::from_env(),
231        }
232    }
233
234    /// Get a reference to the plugin registry for managing signed plugins.
235    pub fn plugins(&self) -> &PluginRegistry {
236        &self.plugin_registry
237    }
238
239    /// Register a tool
240    pub fn register(&mut self, tool: Arc<dyn Tool>) {
241        self.tools.insert(tool.id().to_string(), tool);
242    }
243
244    fn register_compat_alias(&mut self, id: &str, inner: Arc<dyn Tool>) {
245        self.register(Arc::new(alias::AliasTool::new(id, inner)));
246    }
247
248    fn register_compat_aliases(&mut self) {
249        self.register_compat_alias("patch", Arc::new(patch::ApplyPatchTool::new()));
250        self.register_compat_alias("file_info", Arc::new(file_extras::FileInfoTool::new()));
251        self.register_compat_alias("head_tail", Arc::new(file_extras::HeadTailTool::new()));
252        self.register_compat_alias("todo_read", Arc::new(todo::TodoReadTool::new()));
253        self.register_compat_alias("todo_write", Arc::new(todo::TodoWriteTool::new()));
254        self.register_compat_alias("mcp_bridge", Arc::new(mcp_bridge::McpBridgeTool::new()));
255        self.register_compat_alias("k8s_tool", Arc::new(k8s_tool::K8sTool::new()));
256    }
257
258    /// Get a tool by ID
259    pub fn get(&self, id: &str) -> Option<Arc<dyn Tool>> {
260        self.tools.get(id).cloned()
261    }
262
263    /// List all tool IDs
264    pub fn list(&self) -> Vec<&str> {
265        self.tools.keys().map(|s| s.as_str()).collect()
266    }
267
268    /// Get tool definitions for LLM
269    pub fn definitions(&self) -> Vec<crate::provider::ToolDefinition> {
270        self.tools
271            .values()
272            .map(|t| crate::provider::ToolDefinition {
273                name: t.id().to_string(),
274                description: t.description().to_string(),
275                parameters: t.parameters(),
276            })
277            .collect()
278    }
279
280    /// Register multiple tools at once
281    pub fn register_all(&mut self, tools: Vec<Arc<dyn Tool>>) {
282        for tool in tools {
283            self.register(tool);
284        }
285    }
286
287    /// Remove a tool by ID
288    pub fn unregister(&mut self, id: &str) -> Option<Arc<dyn Tool>> {
289        self.tools.remove(id)
290    }
291
292    /// Check if a tool exists
293    pub fn contains(&self, id: &str) -> bool {
294        self.tools.contains_key(id)
295    }
296
297    /// Get the number of registered tools
298    pub fn len(&self) -> usize {
299        self.tools.len()
300    }
301
302    /// Check if the registry is empty
303    pub fn is_empty(&self) -> bool {
304        self.tools.is_empty()
305    }
306
307    /// Create registry with all default tools (without batch)
308    pub fn with_defaults() -> Self {
309        let mut registry = Self::new();
310
311        registry.register(Arc::new(file::ReadTool::new()));
312        registry.register(Arc::new(file::WriteTool::new()));
313        registry.register(Arc::new(file::ListTool::new()));
314        registry.register(Arc::new(file::GlobTool::new()));
315        registry.register(Arc::new(file_extras::TreeTool::new()));
316        registry.register(Arc::new(file_extras::FileInfoTool::new()));
317        registry.register(Arc::new(file_extras::HeadTailTool::new()));
318        registry.register(Arc::new(file_extras::DiffTool::new()));
319        registry.register(Arc::new(search::GrepTool::new()));
320        registry.register(Arc::new(advanced_edit::AdvancedEditTool::new()));
321        registry.register(Arc::new(edit::EditTool::new()));
322        registry.register(Arc::new(bash::BashTool::new()));
323        registry.register(Arc::new(lsp::LspTool::with_root(
324            std::env::current_dir()
325                .map(|p| format!("file://{}", p.display()))
326                .unwrap_or_default(),
327        )));
328        registry.register(Arc::new(webfetch::WebFetchTool::new()));
329        registry.register(Arc::new(multiedit::MultiEditTool::new()));
330        registry.register(Arc::new(websearch::WebSearchTool::new()));
331        registry.register(Arc::new(browserctl::BrowserCtlTool::new()));
332        registry.register(Arc::new(codesearch::CodeSearchTool::new()));
333        registry.register(Arc::new(patch::ApplyPatchTool::new()));
334        registry.register(Arc::new(todo::TodoReadTool::new()));
335        registry.register(Arc::new(todo::TodoWriteTool::new()));
336        registry.register(Arc::new(session_task::SessionTaskTool::new()));
337        registry.register(Arc::new(context_reset::ContextResetTool));
338        registry.register(Arc::new(context_browse::ContextBrowseTool));
339        registry.register(Arc::new(question::QuestionTool::new()));
340        registry.register(Arc::new(task::TaskTool::new()));
341        registry.register(Arc::new(plan::PlanEnterTool::new()));
342        registry.register(Arc::new(plan::PlanExitTool::new()));
343        registry.register(Arc::new(skill::SkillTool::new()));
344        registry.register(Arc::new(memory::MemoryTool::new()));
345        registry.register(Arc::new(ralph::RalphTool::new()));
346        registry.register(Arc::new(prd::PrdTool::new()));
347        registry.register(Arc::new(undo::UndoTool));
348        registry.register(Arc::new(voice::VoiceTool::new()));
349        registry.register(Arc::new(podcast::PodcastTool::new()));
350        registry.register(Arc::new(youtube::YouTubeTool::new()));
351        registry.register(Arc::new(avatar::AvatarTool::new()));
352        registry.register(Arc::new(image::ImageTool::new()));
353        registry.register(Arc::new(mcp_bridge::McpBridgeTool::new()));
354        registry.register(Arc::new(okr::OkrTool::new()));
355        // Edit tools with confirmation (diff display before applying)
356        registry.register(Arc::new(confirm_edit::ConfirmEditTool::new()));
357        registry.register(Arc::new(confirm_multiedit::ConfirmMultiEditTool::new()));
358        // Swarm result sharing between sub-agents
359        registry.register(Arc::new(swarm_share::SwarmShareTool::with_defaults()));
360        // Register the invalid tool handler for graceful error handling
361        registry.register(Arc::new(invalid::InvalidTool::new()));
362        // Agent orchestration tool
363        registry.register(Arc::new(agent::AgentTool::new()));
364        // Swarm execution tool for parallel task execution
365        registry.register(Arc::new(swarm_execute::SwarmExecuteTool::new()));
366        // Relay autochat tool for autonomous agent communication
367        registry.register(Arc::new(relay_autochat::RelayAutoChatTool::new()));
368        // Go tool for autonomous OKR→PRD→Ralph pipeline
369        registry.register(Arc::new(go::GoTool::new()));
370        // Kubernetes management tool
371        registry.register(Arc::new(k8s_tool::K8sTool::new()));
372        registry.register_compat_aliases();
373
374        registry
375    }
376
377    /// Create registry with provider for tools that need it (like RalphTool)
378    pub fn with_provider(provider: Arc<dyn Provider>, model: String) -> Self {
379        let mut registry = Self::new();
380
381        registry.register(Arc::new(file::ReadTool::new()));
382        registry.register(Arc::new(file::WriteTool::new()));
383        registry.register(Arc::new(file::ListTool::new()));
384        registry.register(Arc::new(file::GlobTool::new()));
385        registry.register(Arc::new(file_extras::TreeTool::new()));
386        registry.register(Arc::new(file_extras::FileInfoTool::new()));
387        registry.register(Arc::new(file_extras::HeadTailTool::new()));
388        registry.register(Arc::new(file_extras::DiffTool::new()));
389        registry.register(Arc::new(search::GrepTool::new()));
390        registry.register(Arc::new(advanced_edit::AdvancedEditTool::new()));
391        registry.register(Arc::new(edit::EditTool::new()));
392        registry.register(Arc::new(bash::BashTool::new()));
393        registry.register(Arc::new(lsp::LspTool::with_root(
394            std::env::current_dir()
395                .map(|p| format!("file://{}", p.display()))
396                .unwrap_or_default(),
397        )));
398        registry.register(Arc::new(webfetch::WebFetchTool::new()));
399        registry.register(Arc::new(multiedit::MultiEditTool::new()));
400        registry.register(Arc::new(websearch::WebSearchTool::new()));
401        registry.register(Arc::new(browserctl::BrowserCtlTool::new()));
402        registry.register(Arc::new(codesearch::CodeSearchTool::new()));
403        registry.register(Arc::new(patch::ApplyPatchTool::new()));
404        registry.register(Arc::new(todo::TodoReadTool::new()));
405        registry.register(Arc::new(todo::TodoWriteTool::new()));
406        registry.register(Arc::new(session_task::SessionTaskTool::new()));
407        registry.register(Arc::new(context_reset::ContextResetTool));
408        registry.register(Arc::new(context_browse::ContextBrowseTool));
409        registry.register(Arc::new(question::QuestionTool::new()));
410        registry.register(Arc::new(task::TaskTool::new()));
411        registry.register(Arc::new(plan::PlanEnterTool::new()));
412        registry.register(Arc::new(plan::PlanExitTool::new()));
413        registry.register(Arc::new(skill::SkillTool::new()));
414        registry.register(Arc::new(memory::MemoryTool::new()));
415        registry.register(Arc::new(rlm::RlmTool::new(
416            Arc::clone(&provider),
417            model.clone(),
418            crate::rlm::RlmConfig::default(),
419        )));
420        registry.register(Arc::new(session_recall::SessionRecallTool::new(
421            Arc::clone(&provider),
422            model.clone(),
423            crate::rlm::RlmConfig::default(),
424        )));
425        // RalphTool with provider for autonomous execution
426        registry.register(Arc::new(ralph::RalphTool::with_provider(provider, model)));
427        registry.register(Arc::new(prd::PrdTool::new()));
428        registry.register(Arc::new(undo::UndoTool));
429        registry.register(Arc::new(voice::VoiceTool::new()));
430        registry.register(Arc::new(podcast::PodcastTool::new()));
431        registry.register(Arc::new(youtube::YouTubeTool::new()));
432        registry.register(Arc::new(avatar::AvatarTool::new()));
433        registry.register(Arc::new(image::ImageTool::new()));
434        registry.register(Arc::new(mcp_bridge::McpBridgeTool::new()));
435        registry.register(Arc::new(okr::OkrTool::new()));
436        // Edit tools with confirmation (diff display before applying)
437        registry.register(Arc::new(confirm_edit::ConfirmEditTool::new()));
438        registry.register(Arc::new(confirm_multiedit::ConfirmMultiEditTool::new()));
439        // Swarm result sharing between sub-agents
440        registry.register(Arc::new(swarm_share::SwarmShareTool::with_defaults()));
441        // Register the invalid tool handler for graceful error handling
442        registry.register(Arc::new(invalid::InvalidTool::new()));
443        // Agent orchestration tool
444        registry.register(Arc::new(agent::AgentTool::new()));
445        // Swarm execution tool for parallel task execution
446        registry.register(Arc::new(swarm_execute::SwarmExecuteTool::new()));
447        // Relay autochat tool for autonomous agent communication
448        registry.register(Arc::new(relay_autochat::RelayAutoChatTool::new()));
449        // Go tool for autonomous OKR→PRD→Ralph pipeline
450        registry.register(Arc::new(go::GoTool::new()));
451        // Kubernetes management tool
452        registry.register(Arc::new(k8s_tool::K8sTool::new()));
453        registry.register_compat_aliases();
454
455        registry
456    }
457
458    /// Create Arc-wrapped registry with batch tool properly initialized.
459    /// The batch tool needs a weak reference to the registry, so we use
460    /// a two-phase initialization pattern.
461    pub fn with_defaults_arc() -> Arc<Self> {
462        let mut registry = Self::with_defaults();
463
464        // Create batch tool without registry reference
465        let batch_tool = Arc::new(batch::BatchTool::new());
466        registry.register(batch_tool.clone());
467
468        // Wrap registry in Arc
469        let registry = Arc::new(registry);
470
471        // Now give batch tool a weak reference to the registry
472        batch_tool.set_registry(Arc::downgrade(&registry));
473
474        registry
475    }
476
477    /// Create Arc-wrapped registry with provider and batch tool properly initialized.
478    /// The batch tool needs a weak reference to the registry, so we use
479    /// a two-phase initialization pattern.
480    #[allow(dead_code)]
481    pub fn with_provider_arc(provider: Arc<dyn Provider>, model: String) -> Arc<Self> {
482        let mut registry = Self::with_provider(Arc::clone(&provider), model);
483
484        // Create batch tool without registry reference
485        let batch_tool = Arc::new(batch::BatchTool::new());
486        registry.register(batch_tool.clone());
487
488        // Auto-register the LLM-routed `search` tool. Build a single-provider
489        // `ProviderRegistry` from the provider we already have so the router
490        // can call back into it without the full vault registry.
491        let mut providers = crate::provider::ProviderRegistry::new();
492        providers.register(Arc::clone(&provider));
493        registry.register(Arc::new(search_router::SearchTool::new(Arc::new(
494            providers,
495        ))));
496
497        // Wrap registry in Arc
498        let registry = Arc::new(registry);
499
500        // Now give batch tool a weak reference to the registry
501        batch_tool.set_registry(Arc::downgrade(&registry));
502
503        registry
504    }
505}
506
507impl Default for ToolRegistry {
508    fn default() -> Self {
509        Self::with_defaults()
510    }
511}
512
513impl ToolRegistry {
514    /// Register the LLM-routed `search` tool.
515    ///
516    /// Needs the full [`crate::provider::ProviderRegistry`] because the
517    /// router may talk to multiple providers to pick a model.
518    ///
519    /// # Examples
520    ///
521    /// ```rust,no_run
522    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
523    /// use std::sync::Arc;
524    /// use codetether_agent::provider::ProviderRegistry;
525    /// use codetether_agent::tool::ToolRegistry;
526    ///
527    /// let mut registry = ToolRegistry::with_defaults();
528    /// let providers = Arc::new(ProviderRegistry::from_vault().await.unwrap());
529    /// registry.register_search(providers);
530    /// assert!(registry.contains("search"));
531    /// # });
532    /// ```
533    pub fn register_search(&mut self, providers: Arc<crate::provider::ProviderRegistry>) {
534        self.register(Arc::new(search_router::SearchTool::new(providers)));
535    }
536}