vv-agent 0.6.0

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

use serde_json::json;

use crate::tools::{ToolContext, ToolExecutor, ToolRunContext};
use crate::types::{ToolCall, ToolDirective, ToolExecutionResult, ToolResultStatus};

#[derive(Clone, Default)]
pub struct ToolRunOptions {
    allowed_tools: Option<Vec<String>>,
    disallowed_tools: Vec<String>,
}

impl ToolRunOptions {
    pub fn allow_only(mut self, tools: impl IntoIterator<Item = impl Into<String>>) -> Self {
        self.allowed_tools = Some(tools.into_iter().map(Into::into).collect());
        self
    }

    pub fn disallow(mut self, tool: impl Into<String>) -> Self {
        self.disallowed_tools.push(tool.into());
        self
    }
}

#[derive(Clone, Default)]
pub struct ToolOrchestrator {
    tools: BTreeMap<String, Arc<dyn ToolExecutor>>,
}

impl ToolOrchestrator {
    pub fn from_tools(tools: Vec<Arc<dyn ToolExecutor>>) -> Self {
        let tools = tools
            .into_iter()
            .map(|tool| (tool.name().to_string(), tool))
            .collect();
        Self { tools }
    }

    pub async fn run_one(
        &self,
        call: ToolCall,
        context: &mut ToolContext,
        options: ToolRunOptions,
    ) -> Result<ToolExecutionResult, crate::tools::ToolError> {
        if let Some(allowed) = options.allowed_tools.as_ref() {
            if !allowed.iter().any(|tool| tool == &call.name) {
                return Ok(tool_error(
                    &call,
                    "tool_not_allowed",
                    "Tool is not in the allowed tool list.",
                ));
            }
        }
        if options
            .disallowed_tools
            .iter()
            .any(|tool| tool == &call.name)
        {
            return Ok(tool_error(
                &call,
                "tool_disallowed",
                "Tool is disallowed by policy.",
            ));
        }

        let Some(tool) = self.tools.get(&call.name) else {
            return Ok(tool_error(
                &call,
                "tool_not_found",
                format!("Unknown tool: {}", call.name),
            ));
        };

        let mut result = tool.run(call.clone(), ToolRunContext::new(context)).await?;
        if result.tool_call_id.trim().is_empty() || result.tool_call_id == "pending" {
            result.tool_call_id = call.id;
        }
        Ok(result)
    }
}

fn tool_error(
    call: &ToolCall,
    error_code: &str,
    message: impl Into<String>,
) -> ToolExecutionResult {
    let error_code = error_code.to_string();
    ToolExecutionResult {
        tool_call_id: call.id.clone(),
        content: json!({
            "ok": false,
            "error": message.into(),
            "error_code": error_code,
            "tool_name": call.name,
        })
        .to_string(),
        status: ToolResultStatus::Error,
        directive: ToolDirective::Continue,
        error_code: Some(error_code),
        metadata: BTreeMap::new(),
        image_url: None,
        image_path: None,
    }
}