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