Skip to main content

vv_agent/tools/
orchestrator.rs

1use std::collections::BTreeMap;
2use std::sync::Arc;
3
4use serde_json::json;
5
6use crate::tools::{ToolContext, ToolExecutor, ToolRunContext};
7use crate::types::{ToolCall, ToolDirective, ToolExecutionResult, ToolResultStatus};
8
9#[derive(Clone, Default)]
10pub struct ToolRunOptions {
11    allowed_tools: Option<Vec<String>>,
12    disallowed_tools: Vec<String>,
13}
14
15impl ToolRunOptions {
16    pub fn allow_only(mut self, tools: impl IntoIterator<Item = impl Into<String>>) -> Self {
17        self.allowed_tools = Some(tools.into_iter().map(Into::into).collect());
18        self
19    }
20
21    pub fn disallow(mut self, tool: impl Into<String>) -> Self {
22        self.disallowed_tools.push(tool.into());
23        self
24    }
25}
26
27#[derive(Clone, Default)]
28pub struct ToolOrchestrator {
29    tools: BTreeMap<String, Arc<dyn ToolExecutor>>,
30}
31
32impl ToolOrchestrator {
33    pub fn from_tools(tools: Vec<Arc<dyn ToolExecutor>>) -> Self {
34        let tools = tools
35            .into_iter()
36            .map(|tool| (tool.name().to_string(), tool))
37            .collect();
38        Self { tools }
39    }
40
41    pub async fn run_one(
42        &self,
43        call: ToolCall,
44        context: &mut ToolContext,
45        options: ToolRunOptions,
46    ) -> Result<ToolExecutionResult, crate::tools::ToolError> {
47        if let Some(allowed) = options.allowed_tools.as_ref() {
48            if !allowed.iter().any(|tool| tool == &call.name) {
49                return Ok(tool_error(
50                    &call,
51                    "tool_not_allowed",
52                    "Tool is not in the allowed tool list.",
53                ));
54            }
55        }
56        if options
57            .disallowed_tools
58            .iter()
59            .any(|tool| tool == &call.name)
60        {
61            return Ok(tool_error(
62                &call,
63                "tool_disallowed",
64                "Tool is disallowed by policy.",
65            ));
66        }
67
68        let Some(tool) = self.tools.get(&call.name) else {
69            return Ok(tool_error(
70                &call,
71                "tool_not_found",
72                format!("Unknown tool: {}", call.name),
73            ));
74        };
75
76        let mut result = tool.run(call.clone(), ToolRunContext::new(context)).await?;
77        if result.tool_call_id.trim().is_empty() || result.tool_call_id == "pending" {
78            result.tool_call_id = call.id;
79        }
80        Ok(result)
81    }
82}
83
84fn tool_error(
85    call: &ToolCall,
86    error_code: &str,
87    message: impl Into<String>,
88) -> ToolExecutionResult {
89    let error_code = error_code.to_string();
90    ToolExecutionResult {
91        tool_call_id: call.id.clone(),
92        content: json!({
93            "ok": false,
94            "error": message.into(),
95            "error_code": error_code,
96            "tool_name": call.name,
97        })
98        .to_string(),
99        status: ToolResultStatus::Error,
100        directive: ToolDirective::Continue,
101        error_code: Some(error_code),
102        metadata: BTreeMap::new(),
103        image_url: None,
104        image_path: None,
105    }
106}