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