| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256 |
- use runtime::{
- edit_file, execute_bash, glob_search, grep_search, read_file, write_file, BashCommandInput,
- GrepSearchInput,
- };
- use serde::Deserialize;
- use serde_json::{json, Value};
- #[derive(Debug, Clone, PartialEq, Eq)]
- pub struct ToolManifestEntry {
- pub name: String,
- pub source: ToolSource,
- }
- #[derive(Debug, Clone, Copy, PartialEq, Eq)]
- pub enum ToolSource {
- Base,
- Conditional,
- }
- #[derive(Debug, Clone, Default, PartialEq, Eq)]
- pub struct ToolRegistry {
- entries: Vec<ToolManifestEntry>,
- }
- impl ToolRegistry {
- #[must_use]
- pub fn new(entries: Vec<ToolManifestEntry>) -> Self {
- Self { entries }
- }
- #[must_use]
- pub fn entries(&self) -> &[ToolManifestEntry] {
- &self.entries
- }
- }
- #[derive(Debug, Clone, PartialEq, Eq)]
- pub struct ToolSpec {
- pub name: &'static str,
- pub description: &'static str,
- pub input_schema: Value,
- }
- #[must_use]
- pub fn mvp_tool_specs() -> Vec<ToolSpec> {
- vec![
- ToolSpec {
- name: "bash",
- description: "Execute a shell command in the current workspace.",
- input_schema: json!({
- "type": "object",
- "properties": {
- "command": { "type": "string" },
- "timeout": { "type": "integer", "minimum": 1 },
- "description": { "type": "string" },
- "run_in_background": { "type": "boolean" },
- "dangerouslyDisableSandbox": { "type": "boolean" }
- },
- "required": ["command"],
- "additionalProperties": false
- }),
- },
- ToolSpec {
- name: "read_file",
- description: "Read a text file from the workspace.",
- input_schema: json!({
- "type": "object",
- "properties": {
- "path": { "type": "string" },
- "offset": { "type": "integer", "minimum": 0 },
- "limit": { "type": "integer", "minimum": 1 }
- },
- "required": ["path"],
- "additionalProperties": false
- }),
- },
- ToolSpec {
- name: "write_file",
- description: "Write a text file in the workspace.",
- input_schema: json!({
- "type": "object",
- "properties": {
- "path": { "type": "string" },
- "content": { "type": "string" }
- },
- "required": ["path", "content"],
- "additionalProperties": false
- }),
- },
- ToolSpec {
- name: "edit_file",
- description: "Replace text in a workspace file.",
- input_schema: json!({
- "type": "object",
- "properties": {
- "path": { "type": "string" },
- "old_string": { "type": "string" },
- "new_string": { "type": "string" },
- "replace_all": { "type": "boolean" }
- },
- "required": ["path", "old_string", "new_string"],
- "additionalProperties": false
- }),
- },
- ToolSpec {
- name: "glob_search",
- description: "Find files by glob pattern.",
- input_schema: json!({
- "type": "object",
- "properties": {
- "pattern": { "type": "string" },
- "path": { "type": "string" }
- },
- "required": ["pattern"],
- "additionalProperties": false
- }),
- },
- ToolSpec {
- name: "grep_search",
- description: "Search file contents with a regex pattern.",
- input_schema: json!({
- "type": "object",
- "properties": {
- "pattern": { "type": "string" },
- "path": { "type": "string" },
- "glob": { "type": "string" },
- "output_mode": { "type": "string" },
- "-B": { "type": "integer", "minimum": 0 },
- "-A": { "type": "integer", "minimum": 0 },
- "-C": { "type": "integer", "minimum": 0 },
- "context": { "type": "integer", "minimum": 0 },
- "-n": { "type": "boolean" },
- "-i": { "type": "boolean" },
- "type": { "type": "string" },
- "head_limit": { "type": "integer", "minimum": 1 },
- "offset": { "type": "integer", "minimum": 0 },
- "multiline": { "type": "boolean" }
- },
- "required": ["pattern"],
- "additionalProperties": false
- }),
- },
- ]
- }
- pub fn execute_tool(name: &str, input: &Value) -> Result<String, String> {
- match name {
- "bash" => from_value::<BashCommandInput>(input).and_then(run_bash),
- "read_file" => from_value::<ReadFileInput>(input).and_then(|input| run_read_file(&input)),
- "write_file" => {
- from_value::<WriteFileInput>(input).and_then(|input| run_write_file(&input))
- }
- "edit_file" => from_value::<EditFileInput>(input).and_then(|input| run_edit_file(&input)),
- "glob_search" => {
- from_value::<GlobSearchInputValue>(input).and_then(|input| run_glob_search(&input))
- }
- "grep_search" => {
- from_value::<GrepSearchInput>(input).and_then(|input| run_grep_search(&input))
- }
- _ => Err(format!("unsupported tool: {name}")),
- }
- }
- fn from_value<T: for<'de> Deserialize<'de>>(input: &Value) -> Result<T, String> {
- serde_json::from_value(input.clone()).map_err(|error| error.to_string())
- }
- fn run_bash(input: BashCommandInput) -> Result<String, String> {
- serde_json::to_string_pretty(&execute_bash(input).map_err(|error| error.to_string())?)
- .map_err(|error| error.to_string())
- }
- fn run_read_file(input: &ReadFileInput) -> Result<String, String> {
- to_pretty_json(
- read_file(&input.path, input.offset, input.limit).map_err(|error| error.to_string())?,
- )
- }
- fn run_write_file(input: &WriteFileInput) -> Result<String, String> {
- to_pretty_json(write_file(&input.path, &input.content).map_err(|error| error.to_string())?)
- }
- fn run_edit_file(input: &EditFileInput) -> Result<String, String> {
- to_pretty_json(
- edit_file(
- &input.path,
- &input.old_string,
- &input.new_string,
- input.replace_all.unwrap_or(false),
- )
- .map_err(|error| error.to_string())?,
- )
- }
- fn run_glob_search(input: &GlobSearchInputValue) -> Result<String, String> {
- to_pretty_json(
- glob_search(&input.pattern, input.path.as_deref()).map_err(|error| error.to_string())?,
- )
- }
- fn run_grep_search(input: &GrepSearchInput) -> Result<String, String> {
- to_pretty_json(grep_search(input).map_err(|error| error.to_string())?)
- }
- fn to_pretty_json<T: serde::Serialize>(value: T) -> Result<String, String> {
- serde_json::to_string_pretty(&value).map_err(|error| error.to_string())
- }
- #[derive(Debug, Deserialize)]
- struct ReadFileInput {
- path: String,
- offset: Option<usize>,
- limit: Option<usize>,
- }
- #[derive(Debug, Deserialize)]
- struct WriteFileInput {
- path: String,
- content: String,
- }
- #[derive(Debug, Deserialize)]
- struct EditFileInput {
- path: String,
- old_string: String,
- new_string: String,
- replace_all: Option<bool>,
- }
- #[derive(Debug, Deserialize)]
- struct GlobSearchInputValue {
- pattern: String,
- path: Option<String>,
- }
- #[cfg(test)]
- mod tests {
- use super::{execute_tool, mvp_tool_specs};
- use serde_json::json;
- #[test]
- fn exposes_mvp_tools() {
- let names = mvp_tool_specs()
- .into_iter()
- .map(|spec| spec.name)
- .collect::<Vec<_>>();
- assert!(names.contains(&"bash"));
- assert!(names.contains(&"read_file"));
- }
- #[test]
- fn rejects_unknown_tool_names() {
- let error = execute_tool("nope", &json!({})).expect_err("tool should be rejected");
- assert!(error.contains("unsupported tool"));
- }
- }
|