llama-cpp-v3-agent-sdk 0.1.7

Agentic tool-use loop on top of llama-cpp-v3 — local LLM agents with built-in tools
Documentation
use crate::error::AgentError;
use crate::tool::{Tool, ToolResult};

/// Search-and-replace editing in files.
pub struct EditFileTool;

impl Tool for EditFileTool {
    fn name(&self) -> &str {
        "edit"
    }

    fn description(&self) -> &str {
        "Edit a file by searching for an exact string and replacing it. \
         The 'old' string must match exactly (including whitespace). \
         Only the first occurrence is replaced unless 'all' is set to true."
    }

    fn parameters_schema(&self) -> serde_json::Value {
        serde_json::json!({
            "type": "object",
            "properties": {
                "path": {
                    "type": "string",
                    "description": "Path to the file to edit"
                },
                "old": {
                    "type": "string",
                    "description": "The exact text to search for"
                },
                "new": {
                    "type": "string",
                    "description": "The replacement text"
                },
                "all": {
                    "type": "boolean",
                    "description": "If true, replace all occurrences. Default: false."
                }
            },
            "required": ["path", "old", "new"]
        })
    }

    fn execute(&self, args: &serde_json::Value) -> Result<ToolResult, AgentError> {
        let path = args["path"].as_str().ok_or_else(|| AgentError::Tool {
            tool: "edit".to_string(),
            message: "Missing 'path' argument".to_string(),
        })?;

        let old = args["old"].as_str().ok_or_else(|| AgentError::Tool {
            tool: "edit".to_string(),
            message: "Missing 'old' argument".to_string(),
        })?;

        let new = args["new"].as_str().ok_or_else(|| AgentError::Tool {
            tool: "edit".to_string(),
            message: "Missing 'new' argument".to_string(),
        })?;

        let replace_all = args["all"].as_bool().unwrap_or(false);

        let content = match std::fs::read_to_string(path) {
            Ok(c) => c,
            Err(e) => {
                return Ok(ToolResult::err(format!(
                    "Failed to read '{}': {}",
                    path, e
                )))
            }
        };

        if !content.contains(old) {
            return Ok(ToolResult::err(format!(
                "Search string not found in '{}'. Make sure the 'old' text matches exactly (including whitespace and newlines).",
                path
            )));
        }

        let (new_content, count) = if replace_all {
            let count = content.matches(old).count();
            (content.replace(old, new), count)
        } else {
            (content.replacen(old, new, 1), 1)
        };

        match std::fs::write(path, &new_content) {
            Ok(_) => Ok(ToolResult::ok(format!(
                "Replaced {} occurrence(s) in '{}'",
                count, path
            ))),
            Err(e) => Ok(ToolResult::err(format!(
                "Failed to write '{}': {}",
                path, e
            ))),
        }
    }

    fn requires_permission(&self) -> bool {
        true
    }
}