vv-agent 0.1.3

VectorVein agent runtime, SDK, CLI, tools, and workspace backends
Documentation
use std::sync::Arc;

use serde_json::json;

use crate::tools::base::{ToolContext, ToolSpec};
use crate::tools::common::{
    coerce_truthy_arg, parse_integer_arg, path_escapes_workspace_error, replace_n,
    stringify_tool_arg, tool_error,
};
use crate::types::{ToolArguments, ToolExecutionResult};

use super::workspace_backend_error;

pub fn file_str_replace(
    context: &mut ToolContext,
    arguments: &ToolArguments,
) -> ToolExecutionResult {
    let spec = file_str_replace_tool();
    (spec.handler)(context, arguments)
}

pub(crate) fn file_str_replace_tool() -> ToolSpec {
    let mut spec = ToolSpec::new(
        "file_str_replace",
        "Replace text in a workspace file.",
        Arc::new(|context, arguments| {
            if !arguments.contains_key("path") {
                return tool_error("missing required argument: path");
            }
            let path = stringify_tool_arg(arguments.get("path"), "");
            if let Err(error) = context.resolve_workspace_path(&path) {
                return path_escapes_workspace_error(error);
            }
            let backend = context.effective_workspace_backend();
            match backend.file_info(&path) {
                Ok(Some(info)) if info.is_file => {}
                Ok(_) => return tool_error(format!("file not found: {path}")),
                Err(error) => return workspace_backend_error(error),
            }
            let old_str = stringify_tool_arg(arguments.get("old_str"), "");
            if old_str.is_empty() {
                return tool_error("`old_str` cannot be empty");
            }
            let new_str = stringify_tool_arg(arguments.get("new_str"), "");
            let replace_all = coerce_truthy_arg(arguments.get("replace_all"), false);
            let max_replacements = match arguments.get("max_replacements") {
                Some(value) => match parse_integer_arg(value) {
                    Ok(limit) => limit.max(1) as usize,
                    Err(_) => return tool_error("`max_replacements` must be an integer"),
                },
                None => 1,
            };
            match backend.read_text(&path) {
                Ok(text) => {
                    let occurrence_count = text.matches(&old_str).count();
                    if occurrence_count == 0 {
                        return tool_error("`old_str` not found in file");
                    }
                    let replaced_count = if replace_all {
                        occurrence_count
                    } else {
                        occurrence_count.min(max_replacements)
                    };
                    let replaced_text = if replace_all {
                        text.replace(&old_str, &new_str)
                    } else {
                        replace_n(&text, &old_str, &new_str, max_replacements)
                    };
                    match backend.write_text(&path, &replaced_text, false) {
                        Ok(_) => crate::types::ToolExecutionResult::success(
                            "",
                            json!({
                                "ok": true,
                                "path": path,
                                "replaced_count": replaced_count,
                            })
                            .to_string(),
                        ),
                        Err(error) => workspace_backend_error(error),
                    }
                }
                Err(error) => workspace_backend_error(error),
            }
        }),
    );
    if let Some(schema) = crate::tools::schemas::schema_for("file_str_replace") {
        spec.schema = schema;
    }
    spec
}