Skip to main content

robit_agent/tool/
mod.rs

1//! Tool system: trait, registry, result types, and context.
2
3pub mod bash;
4pub mod read;
5pub mod write;
6pub mod edit;
7pub mod generate_image;
8pub mod load_skill;
9pub mod ls;
10pub mod find;
11pub mod grep;
12pub mod memory;
13pub mod search_history;
14
15use async_trait::async_trait;
16use robit_ai::ChatCompletionTools;
17use serde_json::Value;
18use std::collections::HashMap;
19use std::any::Any;
20use std::path::{Path, PathBuf};
21use std::sync::Arc;
22
23use crate::error::Result;
24use crate::event::SessionId;
25use crate::frontend::Frontend;
26
27// ============================================================================
28// Tool trait
29// ============================================================================
30
31/// A tool that can be called by the LLM and executed by the Agent.
32#[async_trait]
33pub trait Tool: Send + Sync {
34    /// Tool name — LLM calls the tool by this name.
35    fn name(&self) -> &str;
36
37    /// Tool description — injected into system prompt for LLM understanding.
38    fn description(&self) -> &str;
39
40    /// JSON Schema for tool parameters — LLM generates arguments based on this.
41    fn parameters_schema(&self) -> Value;
42
43    /// Whether this tool requires user confirmation before execution.
44    fn requires_confirmation(&self) -> bool;
45
46    /// Execute the tool with parsed arguments. Returns ToolResult for LLM consumption.
47    async fn execute(&self, args: Value, ctx: &ToolContext) -> Result<ToolResult>;
48}
49
50// ============================================================================
51// ToolResult
52// ============================================================================
53
54/// Result returned to the LLM after tool execution.
55#[derive(Debug, Clone)]
56pub struct ToolResult {
57    /// Text content — LLM will read this.
58    pub content: String,
59    /// Whether this is an error (LLM can see errors and adjust strategy).
60    pub is_error: bool,
61}
62
63impl ToolResult {
64    pub fn success(content: impl Into<String>) -> Self {
65        Self {
66            content: content.into(),
67            is_error: false,
68        }
69    }
70
71    pub fn error(content: impl Into<String>) -> Self {
72        Self {
73            content: content.into(),
74            is_error: true,
75        }
76    }
77}
78
79// ============================================================================
80// Shared helpers
81// ============================================================================
82
83/// Resolve a file path relative to the working directory.
84pub fn resolve_path(file_path: &str, working_dir: &Path) -> PathBuf {
85    let p = PathBuf::from(file_path);
86    if p.is_absolute() {
87        p
88    } else {
89        working_dir.join(p)
90    }
91}
92
93// ============================================================================
94// ToolContext
95// ============================================================================
96
97/// Runtime context passed to tools during execution.
98pub struct ToolContext {
99    /// Current working directory.
100    pub working_dir: PathBuf,
101    /// Current session ID.
102    pub session_id: SessionId,
103    /// Frontend for user interaction (e.g., asking for input during tool execution).
104    pub frontend: Arc<dyn Frontend>,
105    /// Platform-specific extensions, keyed by extension ID.
106    /// Chatbot platforms populate this; GUI/TUI leave it empty.
107    /// Keys like "chatbot.platform_ext" map to Arc<dyn PlatformExt>.
108    pub extensions: HashMap<String, Arc<dyn Any + Send + Sync>>,
109}
110
111// ============================================================================
112// ToolCallInfo (for confirmation requests)
113// ============================================================================
114
115/// Information about a tool call, used for confirmation requests.
116#[derive(Debug, Clone)]
117pub struct ToolCallInfo {
118    pub id: String,
119    pub name: String,
120    pub arguments: String,
121}
122
123// ============================================================================
124// ToolRegistry
125// ============================================================================
126
127/// Registry that manages all available tools.
128pub struct ToolRegistry {
129    tools: HashMap<String, Box<dyn Tool>>,
130}
131
132impl ToolRegistry {
133    pub fn new() -> Self {
134        Self {
135            tools: HashMap::new(),
136        }
137    }
138
139    /// Register a tool. Overwrites any existing tool with the same name.
140    pub fn register(&mut self, tool: impl Tool + 'static) {
141        self.tools.insert(tool.name().to_string(), Box::new(tool));
142    }
143
144    /// Get a list of all registered tool names.
145    pub fn tool_names(&self) -> Vec<&str> {
146        self.tools.keys().map(|s| s.as_str()).collect()
147    }
148
149    /// Check if a tool exists.
150    pub fn contains(&self, name: &str) -> bool {
151        self.tools.contains_key(name)
152    }
153
154    /// Generate OpenAI function calling schemas for all registered tools.
155    pub fn tool_schemas(&self) -> Vec<ChatCompletionTools> {
156        self.tools
157            .values()
158            .map(|tool| {
159                let function = serde_json::json!({
160                    "name": tool.name(),
161                    "description": tool.description(),
162                    "parameters": tool.parameters_schema(),
163                });
164
165                // Construct ChatCompletionTool via JSON deserialization
166                let tool_json = serde_json::json!({
167                    "type": "function",
168                    "function": function,
169                });
170
171                serde_json::from_value(tool_json)
172                    .expect("tool schema should be valid ChatCompletionTools")
173            })
174            .collect()
175    }
176
177    /// Execute a tool by name. Returns an error ToolResult if the tool doesn't exist.
178    pub async fn execute(
179        &self,
180        name: &str,
181        args: Value,
182        ctx: &ToolContext,
183    ) -> ToolResult {
184        tracing::info!("ToolRegistry.execute called: name='{}', args={:?}", name, args);
185        tracing::debug!("Available tools: {:?}", self.tool_names());
186
187        match self.tools.get(name) {
188            Some(tool) => {
189                tracing::debug!("Found tool '{}', executing...", name);
190                let started = std::time::Instant::now();
191                let outcome = tool.execute(args, ctx).await;
192                let elapsed = started.elapsed();
193                match &outcome {
194                    Ok(result) => tracing::trace!(
195                        "[tool:{}] execution finished in {:?}: is_error={}, content_len={}",
196                        name,
197                        elapsed,
198                        result.is_error,
199                        result.content.len()
200                    ),
201                    Err(e) => tracing::warn!(
202                        "[tool:{}] execution returned error after {:?}: {}",
203                        name,
204                        elapsed,
205                        e
206                    ),
207                }
208                match outcome {
209                    Ok(result) => result,
210                    Err(e) => ToolResult::error(format!("Tool execution error: {}", e)),
211                }
212            },
213            None => {
214                let available: Vec<&str> = self.tools.keys().map(|s| s.as_str()).collect();
215                tracing::error!("Tool '{}' not found! Available tools: {:?}", name, available);
216                ToolResult::error(format!(
217                    "Tool '{}' not found. Available tools: {:?}",
218                    name, available
219                ))
220            }
221        }
222    }
223
224    /// Check if a tool requires confirmation.
225    pub fn requires_confirmation(&self, name: &str) -> bool {
226        self.tools
227            .get(name)
228            .map(|t| t.requires_confirmation())
229            .unwrap_or(false)
230    }
231
232    /// Get references to all tools (for prompt building).
233    pub fn tools(&self) -> Vec<&dyn Tool> {
234        self.tools.values().map(|t| t.as_ref()).collect()
235    }
236}
237
238impl Default for ToolRegistry {
239    fn default() -> Self {
240        Self::new()
241    }
242}