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::{
7    ApprovalPolicy, ApprovalRequirement, CanUseToolPredicate, ToolContext, ToolExecutor,
8    ToolPolicy, ToolRunContext,
9};
10use crate::types::{Metadata, ToolCall, ToolDirective, ToolExecutionResult, ToolResultStatus};
11
12pub type BeforeToolDispatch = Arc<
13    dyn Fn(&ToolCall, &mut ToolContext) -> Result<(), crate::tools::ToolError>
14        + Send
15        + Sync
16        + 'static,
17>;
18
19#[derive(Clone, Default)]
20pub struct ToolRunOptions {
21    planned_tools: Option<Vec<String>>,
22    allowed_tools: Option<Vec<String>>,
23    disallowed_tools: Vec<String>,
24    can_use_tool: Option<CanUseToolPredicate>,
25    approval: ApprovalPolicy,
26    idempotency_key: Option<String>,
27    before_dispatch: Option<BeforeToolDispatch>,
28}
29
30impl ToolRunOptions {
31    pub fn from_policy(policy: &ToolPolicy) -> Self {
32        Self {
33            planned_tools: None,
34            allowed_tools: policy.allowed_tools.clone(),
35            disallowed_tools: policy.disallowed_tools.clone(),
36            can_use_tool: policy.can_use_tool.clone(),
37            approval: policy.approval,
38            idempotency_key: None,
39            before_dispatch: None,
40        }
41    }
42
43    pub fn planned_names(mut self, tools: impl IntoIterator<Item = impl Into<String>>) -> Self {
44        self.planned_tools = Some(tools.into_iter().map(Into::into).collect());
45        self
46    }
47
48    pub fn allow_only(mut self, tools: impl IntoIterator<Item = impl Into<String>>) -> Self {
49        self.allowed_tools = Some(tools.into_iter().map(Into::into).collect());
50        self
51    }
52
53    pub fn disallow(mut self, tool: impl Into<String>) -> Self {
54        self.disallowed_tools.push(tool.into());
55        self
56    }
57
58    pub fn can_use_tool<F>(mut self, predicate: F) -> Self
59    where
60        F: Fn(&str, &crate::types::ToolArguments) -> bool + Send + Sync + 'static,
61    {
62        self.can_use_tool = Some(Arc::new(predicate));
63        self
64    }
65
66    pub fn idempotency_key(mut self, idempotency_key: Option<String>) -> Self {
67        self.idempotency_key = idempotency_key;
68        self
69    }
70
71    pub fn before_dispatch(mut self, callback: BeforeToolDispatch) -> Self {
72        self.before_dispatch = Some(callback);
73        self
74    }
75}
76
77#[derive(Clone, Default)]
78pub struct ToolOrchestrator {
79    tools: BTreeMap<String, Arc<dyn ToolExecutor>>,
80}
81
82impl ToolOrchestrator {
83    pub fn from_tools(tools: Vec<Arc<dyn ToolExecutor>>) -> Self {
84        let tools = tools
85            .into_iter()
86            .map(|tool| (tool.name().to_string(), tool))
87            .collect();
88        Self { tools }
89    }
90
91    pub async fn run_one(
92        &self,
93        call: ToolCall,
94        context: &mut ToolContext,
95        options: ToolRunOptions,
96    ) -> Result<ToolExecutionResult, crate::tools::ToolError> {
97        self.run_one_with_approval(call, context, options, |_call, _requirement, _context| None)
98            .await
99    }
100
101    pub(crate) async fn run_one_with_approval<F>(
102        &self,
103        call: ToolCall,
104        context: &mut ToolContext,
105        options: ToolRunOptions,
106        approval: F,
107    ) -> Result<ToolExecutionResult, crate::tools::ToolError>
108    where
109        F: FnOnce(&ToolCall, ApprovalRequirement, &ToolContext) -> Option<ToolExecutionResult>,
110    {
111        self.run_one_with_approval_and_metadata(
112            call,
113            context,
114            options,
115            |call, requirement, context, _metadata| approval(call, requirement, context),
116        )
117        .await
118    }
119
120    pub(crate) async fn run_one_with_approval_and_metadata<F>(
121        &self,
122        call: ToolCall,
123        context: &mut ToolContext,
124        options: ToolRunOptions,
125        approval: F,
126    ) -> Result<ToolExecutionResult, crate::tools::ToolError>
127    where
128        F: FnOnce(
129            &ToolCall,
130            ApprovalRequirement,
131            &ToolContext,
132            &Metadata,
133        ) -> Option<ToolExecutionResult>,
134    {
135        if let Some(allowed) = options.allowed_tools.as_ref() {
136            if !allowed.iter().any(|tool| tool == &call.name) {
137                return Ok(policy_denial(&call, "allowed_tools"));
138            }
139        }
140        if options
141            .disallowed_tools
142            .iter()
143            .any(|tool| tool == &call.name)
144        {
145            return Ok(policy_denial(&call, "disallowed_tools"));
146        }
147        if options
148            .can_use_tool
149            .as_ref()
150            .is_some_and(|predicate| !predicate(&call.name, &call.arguments))
151        {
152            return Ok(policy_denial(&call, "can_use_tool"));
153        }
154        if options
155            .planned_tools
156            .as_ref()
157            .is_some_and(|planned| !planned.iter().any(|tool| tool == &call.name))
158        {
159            return Ok(policy_denial(&call, "planned_name"));
160        }
161
162        let Some(tool) = self.tools.get(&call.name) else {
163            return Ok(tool_error(
164                &call,
165                "tool_not_found",
166                format!("Unknown tool: {}", call.name),
167                None,
168            ));
169        };
170
171        context.begin_tool_call(&call);
172        context.idempotency_key = options.idempotency_key.clone();
173        let approval_requirement = match options.approval {
174            ApprovalPolicy::Never => ApprovalRequirement::NotRequired,
175            ApprovalPolicy::Always => ApprovalRequirement::Required,
176            ApprovalPolicy::Default | ApprovalPolicy::OnRequest => {
177                tool.approval_requirement(&call, &ToolRunContext::new(context))
178            }
179        };
180        let approval_result = approval(&call, approval_requirement, context, tool.metadata());
181        if let Some(mut result) = approval_result {
182            if result.tool_call_id.trim().is_empty() || result.tool_call_id == "pending" {
183                result.tool_call_id = call.id;
184            }
185            return Ok(result);
186        }
187
188        if let Some(callback) = options.before_dispatch.as_ref() {
189            callback(&call, context)?;
190        }
191
192        let future = tool.run(call.clone(), ToolRunContext::new(context));
193        let mut result = if let Some(timeout) = tool.timeout() {
194            match tokio::time::timeout(timeout, future).await {
195                Ok(result) => result?,
196                Err(_) => {
197                    return Ok(crate::tools::ToolOutput::error(format!(
198                        "Tool {} timed out after {} seconds.",
199                        call.name,
200                        timeout.as_secs_f64()
201                    ))
202                    .with_code("tool_timeout")
203                    .retryable(true)
204                    .to_result(&call.id));
205                }
206            }
207        } else {
208            future.await?
209        };
210        if result.tool_call_id.trim().is_empty() || result.tool_call_id == "pending" {
211            result.tool_call_id = call.id;
212        }
213        Ok(result)
214    }
215}
216
217fn policy_denial(call: &ToolCall, policy_source: &str) -> ToolExecutionResult {
218    tool_error(
219        call,
220        "tool_not_allowed",
221        format!("Tool {} is not allowed for these arguments.", call.name),
222        Some(policy_source),
223    )
224}
225
226fn tool_error(
227    call: &ToolCall,
228    error_code: &str,
229    message: impl Into<String>,
230    policy_source: Option<&str>,
231) -> ToolExecutionResult {
232    let error_code = error_code.to_string();
233    let message = message.into();
234    let mut metadata = BTreeMap::new();
235    if let Some(policy_source) = policy_source {
236        metadata.insert("mode".to_string(), json!("permission_denied"));
237        metadata.insert("policy_source".to_string(), json!(policy_source));
238        metadata.insert("tool_name".to_string(), json!(call.name));
239        metadata.insert("arguments".to_string(), json!(call.arguments));
240        metadata.insert("message".to_string(), json!(message));
241    }
242    ToolExecutionResult {
243        tool_call_id: call.id.clone(),
244        content: json!({
245            "ok": false,
246            "error": message,
247            "error_code": error_code,
248            "tool_name": call.name,
249        })
250        .to_string(),
251        status: ToolResultStatus::Error,
252        directive: ToolDirective::Continue,
253        error_code: Some(error_code),
254        metadata,
255        image_url: None,
256        image_path: None,
257    }
258}
259
260#[cfg(test)]
261mod tests {
262    use serde_json::{json, Value};
263
264    use super::*;
265    use crate::tools::{FunctionTool, ToolOutput};
266
267    #[tokio::test]
268    async fn metadata_aware_approval_callback_receives_effective_requirement() {
269        let tool = FunctionTool::builder("guarded")
270            .metadata("risk_level", json!("high"))
271            .needs_approval_if(|_context, _arguments| true)
272            .handler(|_context, _arguments: Value| async { Ok(ToolOutput::text("ran")) })
273            .build()
274            .expect("guarded tool");
275        let orchestrator = ToolOrchestrator::from_tools(vec![tool.to_executor()]);
276        let mut context = ToolContext::new("./workspace");
277
278        let result = orchestrator
279            .run_one_with_approval_and_metadata(
280                ToolCall::from_raw_arguments("guarded_call", "guarded", json!({})),
281                &mut context,
282                ToolRunOptions::default(),
283                |_call, requirement, _context, metadata| {
284                    assert_eq!(requirement, ApprovalRequirement::Required);
285                    assert_eq!(metadata["risk_level"], json!("high"));
286                    None
287                },
288            )
289            .await
290            .expect("tool result");
291
292        assert_eq!(result.status, ToolResultStatus::Success);
293    }
294}