Skip to main content

llm_core/
tools.rs

1use crate::types::{Tool, ToolCall, ToolResult};
2
3/// Registry of built-in tools (`llm_version`, `llm_time`).
4///
5/// Lifted out of `llm-cli` so the WASM and Python bindings can offer the
6/// same builtins without pulling in CLI-only concerns. The version string
7/// is taken at construction time so each caller reports its own crate
8/// version (`env!("CARGO_PKG_VERSION")`).
9pub struct BuiltinToolRegistry {
10    tools: Vec<Tool>,
11    version: &'static str,
12}
13
14impl BuiltinToolRegistry {
15    #[must_use]
16    pub fn new(version: &'static str) -> Self {
17        Self {
18            tools: vec![
19                Tool {
20                    name: "llm_version".into(),
21                    description: "Returns the current LLM CLI version".into(),
22                    input_schema: serde_json::json!({
23                        "type": "object",
24                        "properties": {},
25                    }),
26                },
27                Tool {
28                    name: "llm_time".into(),
29                    description: "Returns the current date and time".into(),
30                    input_schema: serde_json::json!({
31                        "type": "object",
32                        "properties": {},
33                    }),
34                },
35            ],
36            version,
37        }
38    }
39
40    #[must_use]
41    pub fn list(&self) -> &[Tool] {
42        &self.tools
43    }
44
45    #[must_use]
46    pub fn get(&self, name: &str) -> Option<&Tool> {
47        self.tools.iter().find(|t| t.name == name)
48    }
49
50    /// Returns `true` if `name` is a known builtin tool.
51    #[must_use]
52    pub fn contains(&self, name: &str) -> bool {
53        self.get(name).is_some()
54    }
55
56    /// Execute a builtin tool call. Returns an error result for unknown tools.
57    #[must_use]
58    pub fn execute_tool(&self, call: &ToolCall) -> ToolResult {
59        let output = match call.name.as_str() {
60            "llm_version" => self.version.to_string(),
61            "llm_time" => {
62                let utc = chrono::Utc::now();
63                let local = chrono::Local::now();
64                let tz = local.format("%Z").to_string();
65                serde_json::json!({
66                    "utc_time": utc.to_rfc3339(),
67                    "local_time": local.to_rfc3339(),
68                    "timezone": tz,
69                })
70                .to_string()
71            }
72            _ => {
73                return ToolResult {
74                    name: call.name.clone(),
75                    output: String::new(),
76                    tool_call_id: call.tool_call_id.clone(),
77                    error: Some(format!("unknown tool: {}", call.name)),
78                };
79            }
80        };
81
82        ToolResult {
83            name: call.name.clone(),
84            output,
85            tool_call_id: call.tool_call_id.clone(),
86            error: None,
87        }
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    fn registry() -> BuiltinToolRegistry {
96        BuiltinToolRegistry::new("9.9.9-test")
97    }
98
99    #[test]
100    fn registry_has_two_builtin_tools() {
101        assert_eq!(registry().list().len(), 2);
102    }
103
104    #[test]
105    fn llm_version_returns_constructor_version() {
106        let call = ToolCall {
107            name: "llm_version".into(),
108            arguments: serde_json::json!({}),
109            tool_call_id: Some("tc_1".into()),
110        };
111        let result = registry().execute_tool(&call);
112        assert!(result.error.is_none());
113        assert_eq!(result.output, "9.9.9-test");
114    }
115
116    #[test]
117    fn llm_time_returns_time_info() {
118        let call = ToolCall {
119            name: "llm_time".into(),
120            arguments: serde_json::json!({}),
121            tool_call_id: None,
122        };
123        let result = registry().execute_tool(&call);
124        assert!(result.error.is_none());
125        let parsed: serde_json::Value = serde_json::from_str(&result.output).unwrap();
126        assert!(parsed.get("utc_time").is_some());
127        assert!(parsed.get("local_time").is_some());
128        assert!(parsed.get("timezone").is_some());
129    }
130
131    #[test]
132    fn unknown_tool_returns_error_result() {
133        let call = ToolCall {
134            name: "nonexistent".into(),
135            arguments: serde_json::json!({}),
136            tool_call_id: None,
137        };
138        let result = registry().execute_tool(&call);
139        assert!(result.error.is_some());
140        assert!(result.error.unwrap().contains("unknown tool"));
141    }
142
143    #[test]
144    fn registry_get_finds_tool() {
145        let r = registry();
146        assert!(r.get("llm_version").is_some());
147        assert!(r.get("llm_time").is_some());
148        assert!(r.get("nonexistent").is_none());
149        assert!(r.contains("llm_time"));
150    }
151}