deepseek-loop 0.3.2

Claude-Code-shaped agent loop over the DeepSeek API: built-in tools, permission modes, cron scheduler with /loop semantics, streaming SdkMessage events
Documentation
use async_trait::async_trait;
use serde_json::{json, Value};

use crate::agent::tool::{Tool, ToolDefinition};


pub struct EditTool;

#[async_trait]
impl Tool for EditTool {
    fn name(&self) -> &str {
        "Edit"
    }

    fn definition(&self) -> ToolDefinition {
        ToolDefinition {
            name: self.name().to_string(),
            description: "Replace one occurrence (or all, with `replace_all`) of an exact \
                          string in a file. Errors if `old_string` is missing or appears \
                          multiple times unless `replace_all` is set."
                .into(),
            parameters: json!({
                "type": "object",
                "properties": {
                    "path":        { "type": "string" },
                    "old_string":  { "type": "string" },
                    "new_string":  { "type": "string" },
                    "replace_all": { "type": "boolean", "default": false }
                },
                "required": ["path", "old_string", "new_string"]
            }),
        }
    }

    async fn call_json(&self, args: Value) -> Result<String, String> {
        let path = args
            .get("path")
            .and_then(Value::as_str)
            .ok_or_else(|| "Edit: missing string `path`".to_string())?;
        let old = args
            .get("old_string")
            .and_then(Value::as_str)
            .ok_or_else(|| "Edit: missing string `old_string`".to_string())?;
        let new = args
            .get("new_string")
            .and_then(Value::as_str)
            .ok_or_else(|| "Edit: missing string `new_string`".to_string())?;
        let replace_all = args
            .get("replace_all")
            .and_then(Value::as_bool)
            .unwrap_or(false);

        let body = tokio::fs::read_to_string(path)
            .await
            .map_err(|e| format!("Edit({path}): {e}"))?;

        let count = body.matches(old).count();
        if count == 0 {
            return Err(format!("Edit({path}): `old_string` not found"));
        }
        if count > 1 && !replace_all {
            return Err(format!(
                "Edit({path}): `old_string` matches {count} times; \
                 set `replace_all=true` or supply more context"
            ));
        }
        let updated = if replace_all {
            body.replace(old, new)
        } else {
            body.replacen(old, new, 1)
        };
        tokio::fs::write(path, updated)
            .await
            .map_err(|e| format!("Edit({path}): write: {e}"))?;
        Ok(format!(
            "Edited {path}: replaced {} occurrence(s)",
            if replace_all { count } else { 1 }
        ))
    }
}