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