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