toast_api/tools/
mod.rs

1//! DGM-inspired tool system for toast
2//! 
3//! This module provides a flexible tool system that allows the AI agent to
4//! interact with the system through various tools like bash commands and file editing.
5
6use anyhow::Result;
7use async_trait::async_trait;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11pub mod bash;
12pub mod editor;
13
14/// Information about a tool that can be used by the agent
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct ToolInfo {
17    pub name: String,
18    pub description: String,
19    pub input_schema: serde_json::Value,
20}
21
22/// Base trait for all tools
23#[async_trait]
24pub trait Tool: Send + Sync {
25    /// Get information about this tool
26    fn info(&self) -> ToolInfo;
27    
28    /// Execute the tool with given parameters
29    async fn execute(&self, params: serde_json::Value) -> Result<String>;
30}
31
32/// Tool registry that manages all available tools
33pub struct ToolRegistry {
34    tools: HashMap<String, Box<dyn Tool>>,
35}
36
37impl Default for ToolRegistry {
38    fn default() -> Self {
39        Self::new()
40    }
41}
42
43impl ToolRegistry {
44    /// Create a new tool registry with all built-in tools
45    pub fn new() -> Self {
46        let mut tools: HashMap<String, Box<dyn Tool>> = HashMap::new();
47        
48        // Register bash tool
49        let bash_tool = bash::BashTool::new();
50        tools.insert(bash_tool.info().name.clone(), Box::new(bash_tool));
51        
52        // Register editor tool
53        let editor_tool = editor::EditorTool::new();
54        tools.insert(editor_tool.info().name.clone(), Box::new(editor_tool));
55        
56        Self { tools }
57    }
58    
59    /// Get a tool by name
60    pub fn get(&self, name: &str) -> Option<&dyn Tool> {
61        self.tools.get(name).map(|tool| tool.as_ref())
62    }
63    
64    /// Get all available tools
65    pub fn all_tools(&self) -> Vec<ToolInfo> {
66        self.tools.values().map(|t| t.info()).collect()
67    }
68    
69    /// Execute a tool by name with parameters
70    pub async fn execute(&self, name: &str, params: serde_json::Value) -> Result<String> {
71        match self.tools.get(name) {
72            Some(tool) => tool.execute(params).await,
73            None => Err(anyhow::anyhow!("Tool '{}' not found", name)),
74        }
75    }
76}
77
78/// Extract tool calls from a response
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct ToolCall {
81    pub tool: String,
82    pub params: serde_json::Value,
83}
84
85/// Parse tool calls from agent response
86pub fn parse_tool_calls(response: &str) -> Vec<ToolCall> {
87    let mut tool_calls = Vec::new();
88    
89    // Look for tool invocations in the format:
90    // <tool_use>
91    // {"tool": "bash", "params": {"command": "ls -la"}}
92    // </tool_use>
93    
94    let tool_use_re = regex::Regex::new(r"(?s)<tool_use>\s*(\{.*?\})\s*</tool_use>").unwrap();
95    
96    for cap in tool_use_re.captures_iter(response) {
97        if let Ok(tool_call) = serde_json::from_str::<ToolCall>(&cap[1]) {
98            tool_calls.push(tool_call);
99        }
100    }
101    
102    // Also support the existing # exec and # read_file format for backwards compatibility
103    let (reads, execs) = crate::utils::extract_commands(response);
104    
105    // Convert read_file commands to editor tool calls
106    for path in reads {
107        tool_calls.push(ToolCall {
108            tool: "editor".to_string(),
109            params: serde_json::json!({
110                "command": "view",
111                "path": path
112            }),
113        });
114    }
115    
116    // Convert exec commands to bash tool calls
117    for cmd in execs {
118        tool_calls.push(ToolCall {
119            tool: "bash".to_string(),
120            params: serde_json::json!({
121                "command": cmd
122            }),
123        });
124    }
125    
126    tool_calls
127}
128
129/// Format tool output for display
130pub fn format_tool_output(tool_name: &str, output: &str) -> String {
131    // Color code based on tool type
132    let colored_header = match tool_name {
133        "read_file" => format!("\x1b[34m[{}]\x1b[0m", tool_name.to_uppercase()),
134        "bash" | "exec" => format!("\x1b[38;5;208m[{}]\x1b[0m", tool_name.to_uppercase()),
135        _ => format!("[{}]", tool_name.to_uppercase()),
136    };
137    
138    // Return formatted output without separator lines
139    format!("\n{}\n{}", colored_header, output)
140}