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 std::time::Duration;

use async_trait::async_trait;
use serde_json::{json, Value};

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

const DEFAULT_TIMEOUT_SECS: u64 = 120;


pub struct BashTool;

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

    fn definition(&self) -> ToolDefinition {
        ToolDefinition {
            name: self.name().to_string(),
            description: "Run a shell command via `/bin/sh -c`. Captures stdout+stderr. \
                          Default timeout 120s."
                .into(),
            parameters: json!({
                "type": "object",
                "properties": {
                    "command":      { "type": "string" },
                    "timeout_secs": { "type": "integer", "minimum": 1, "maximum": 600 }
                },
                "required": ["command"]
            }),
        }
    }

    async fn call_json(&self, args: Value) -> Result<String, String> {
        let command = args
            .get("command")
            .and_then(Value::as_str)
            .ok_or_else(|| "Bash: missing string `command`".to_string())?;
        let timeout = args
            .get("timeout_secs")
            .and_then(Value::as_u64)
            .unwrap_or(DEFAULT_TIMEOUT_SECS);

        let mut cmd = tokio::process::Command::new("/bin/sh");
        cmd.arg("-c").arg(command);
        cmd.stdout(std::process::Stdio::piped())
            .stderr(std::process::Stdio::piped());

        let child_fut = cmd.output();
        let out = match tokio::time::timeout(Duration::from_secs(timeout), child_fut).await {
            Ok(Ok(out)) => out,
            Ok(Err(e)) => return Err(format!("Bash: spawn error: {e}")),
            Err(_) => return Err(format!("Bash: timed out after {timeout}s")),
        };
        let stdout = String::from_utf8_lossy(&out.stdout);
        let stderr = String::from_utf8_lossy(&out.stderr);
        let status = out.status.code().unwrap_or(-1);
        Ok(format!(
            "exit={status}\n--- stdout ---\n{stdout}--- stderr ---\n{stderr}"
        ))
    }
}