Skip to main content

vv_agent/tools/
executor.rs

1use std::any::Any;
2use std::future::Future;
3use std::pin::Pin;
4use std::sync::Arc;
5use std::time::Duration;
6
7use crate::context::RunContext;
8use crate::tools::{ToolContext, ToolMetadata, ToolSpec};
9use crate::types::{Metadata, ToolArguments, ToolCall, ToolExecutionResult};
10
11pub type ToolFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, ToolError>> + Send + 'a>>;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum ToolExposure {
15    /// Model-visible and executable through the runtime.
16    Direct,
17    /// Reserved for deferred discovery. It currently has the same visibility and execution
18    /// semantics as `Direct` under the shared SDK contract.
19    Deferred,
20    /// Reserved for model-origin-only invocation. It currently has the same visibility and
21    /// execution semantics as `Direct` under the shared SDK contract.
22    DirectModelOnly,
23    /// Not model-visible, but still available for explicit runtime invocation.
24    Hidden,
25}
26
27#[derive(Clone, Default)]
28pub struct ToolEnablementContext {
29    pub run: RunContext,
30    pub app_state: Option<Arc<dyn Any + Send + Sync>>,
31}
32
33impl ToolEnablementContext {
34    pub fn new(run: RunContext) -> Self {
35        Self {
36            run,
37            app_state: None,
38        }
39    }
40
41    pub fn with_app_state(mut self, app_state: Option<Arc<dyn Any + Send + Sync>>) -> Self {
42        self.app_state = app_state;
43        self
44    }
45
46    pub fn app_state<T: Send + Sync + 'static>(&self) -> Option<&T> {
47        self.app_state.as_ref()?.downcast_ref::<T>()
48    }
49}
50
51pub type ToolEnablementPredicate =
52    Arc<dyn Fn(&ToolEnablementContext) -> bool + Send + Sync + 'static>;
53
54#[derive(Clone)]
55pub enum ToolEnablementRule {
56    Static(bool),
57    Predicate(ToolEnablementPredicate),
58}
59
60impl ToolEnablementRule {
61    pub fn predicate<F>(predicate: F) -> Self
62    where
63        F: Fn(&ToolEnablementContext) -> bool + Send + Sync + 'static,
64    {
65        Self::Predicate(Arc::new(predicate))
66    }
67
68    pub fn is_enabled(&self, context: &ToolEnablementContext) -> bool {
69        match self {
70            Self::Static(enabled) => *enabled,
71            Self::Predicate(predicate) => predicate(context),
72        }
73    }
74}
75
76impl Default for ToolEnablementRule {
77    fn default() -> Self {
78        Self::Static(true)
79    }
80}
81
82impl From<bool> for ToolEnablementRule {
83    fn from(enabled: bool) -> Self {
84        Self::Static(enabled)
85    }
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub enum ApprovalRequirement {
90    NotRequired,
91    Required,
92    Provider,
93}
94
95pub type ApprovalPredicate =
96    Arc<dyn Fn(&ToolContext, &ToolArguments) -> bool + Send + Sync + 'static>;
97
98#[derive(Clone)]
99pub enum ToolApprovalRule {
100    Static(ApprovalRequirement),
101    Predicate(ApprovalPredicate),
102}
103
104impl ToolApprovalRule {
105    pub fn predicate<F>(predicate: F) -> Self
106    where
107        F: Fn(&ToolContext, &ToolArguments) -> bool + Send + Sync + 'static,
108    {
109        Self::Predicate(Arc::new(predicate))
110    }
111
112    pub fn requirement(
113        &self,
114        context: &ToolContext,
115        arguments: &ToolArguments,
116    ) -> ApprovalRequirement {
117        match self {
118            Self::Static(requirement) => *requirement,
119            Self::Predicate(predicate) => {
120                if predicate(context, arguments) {
121                    ApprovalRequirement::Required
122                } else {
123                    ApprovalRequirement::NotRequired
124                }
125            }
126        }
127    }
128}
129
130impl Default for ToolApprovalRule {
131    fn default() -> Self {
132        Self::Static(ApprovalRequirement::NotRequired)
133    }
134}
135
136impl From<bool> for ToolApprovalRule {
137    fn from(needs_approval: bool) -> Self {
138        Self::Static(if needs_approval {
139            ApprovalRequirement::Required
140        } else {
141            ApprovalRequirement::NotRequired
142        })
143    }
144}
145
146impl From<ApprovalRequirement> for ToolApprovalRule {
147    fn from(requirement: ApprovalRequirement) -> Self {
148        Self::Static(requirement)
149    }
150}
151
152#[derive(Debug, Clone, Default)]
153pub struct ToolSpecContext;
154
155pub struct ToolRunContext<'a> {
156    pub context: &'a mut ToolContext,
157}
158
159impl<'a> ToolRunContext<'a> {
160    pub fn new(context: &'a mut ToolContext) -> Self {
161        Self { context }
162    }
163}
164
165#[derive(Debug, Clone, PartialEq, Eq)]
166pub struct ToolError {
167    message: String,
168}
169
170impl ToolError {
171    pub fn new(message: impl Into<String>) -> Self {
172        Self {
173            message: message.into(),
174        }
175    }
176}
177
178impl std::fmt::Display for ToolError {
179    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
180        formatter.write_str(&self.message)
181    }
182}
183
184impl std::error::Error for ToolError {}
185
186pub trait ToolExecutor: Send + Sync {
187    fn name(&self) -> &str;
188    fn description(&self) -> &str;
189    fn metadata(&self) -> &Metadata {
190        static EMPTY_METADATA: std::sync::OnceLock<Metadata> = std::sync::OnceLock::new();
191        EMPTY_METADATA.get_or_init(Metadata::new)
192    }
193    fn exposure(&self) -> ToolExposure {
194        ToolExposure::Direct
195    }
196    fn timeout(&self) -> Option<Duration> {
197        None
198    }
199    fn tool_metadata(&self) -> Option<&ToolMetadata> {
200        None
201    }
202    fn spec(&self, _ctx: &ToolSpecContext) -> Result<ToolSpec, ToolError>;
203    fn approval_requirement(
204        &self,
205        _call: &ToolCall,
206        _ctx: &ToolRunContext<'_>,
207    ) -> ApprovalRequirement {
208        ApprovalRequirement::NotRequired
209    }
210    fn validate_arguments(
211        &self,
212        call: &ToolCall,
213    ) -> Result<Option<ToolExecutionResult>, ToolError> {
214        let spec = self.spec(&ToolSpecContext)?;
215        let schema = crate::tools::argument_validation::close_object_schemas(&spec.schema);
216        let validator = crate::tools::argument_validation::validator_for_tool_schema(&schema)
217            .map_err(ToolError::new)?;
218        Ok(crate::tools::argument_validation::invalid_tool_arguments_result(&validator, call))
219    }
220    fn run<'a>(
221        &'a self,
222        call: ToolCall,
223        ctx: ToolRunContext<'a>,
224    ) -> ToolFuture<'a, ToolExecutionResult>;
225}
226
227#[derive(Clone)]
228pub struct ToolSpecExecutor {
229    spec: ToolSpec,
230    exposure: ToolExposure,
231    argument_validator: Result<jsonschema::Validator, String>,
232}
233
234impl ToolSpecExecutor {
235    pub fn new(mut spec: ToolSpec) -> Self {
236        spec.schema = crate::tools::argument_validation::close_object_schemas(&spec.schema);
237        let exposure = spec.exposure;
238        let argument_validator =
239            crate::tools::argument_validation::validator_for_tool_schema(&spec.schema);
240        Self {
241            spec,
242            exposure,
243            argument_validator,
244        }
245    }
246
247    pub fn with_exposure(mut self, exposure: ToolExposure) -> Self {
248        self.exposure = exposure;
249        self
250    }
251
252    pub fn into_arc(self) -> Arc<dyn ToolExecutor> {
253        Arc::new(self)
254    }
255}
256
257impl ToolExecutor for ToolSpecExecutor {
258    fn name(&self) -> &str {
259        &self.spec.name
260    }
261
262    fn description(&self) -> &str {
263        &self.spec.description
264    }
265
266    fn metadata(&self) -> &Metadata {
267        &self.spec.metadata
268    }
269
270    fn exposure(&self) -> ToolExposure {
271        self.exposure
272    }
273
274    fn timeout(&self) -> Option<Duration> {
275        self.spec.timeout
276    }
277
278    fn tool_metadata(&self) -> Option<&ToolMetadata> {
279        self.spec.tool_metadata.as_ref()
280    }
281
282    fn spec(&self, _ctx: &ToolSpecContext) -> Result<ToolSpec, ToolError> {
283        Ok(self.spec.clone())
284    }
285
286    fn approval_requirement(
287        &self,
288        call: &ToolCall,
289        ctx: &ToolRunContext<'_>,
290    ) -> ApprovalRequirement {
291        self.spec.approval.requirement(ctx.context, &call.arguments)
292    }
293
294    fn validate_arguments(
295        &self,
296        call: &ToolCall,
297    ) -> Result<Option<ToolExecutionResult>, ToolError> {
298        let validator = self
299            .argument_validator
300            .as_ref()
301            .map_err(|error| ToolError::new(error.clone()))?;
302        Ok(crate::tools::argument_validation::invalid_tool_arguments_result(validator, call))
303    }
304
305    fn run<'a>(
306        &'a self,
307        call: ToolCall,
308        ctx: ToolRunContext<'a>,
309    ) -> ToolFuture<'a, ToolExecutionResult> {
310        let handler = self.spec.handler.clone();
311        Box::pin(async move {
312            if let Some(result) = self.validate_arguments(&call)? {
313                return Ok(result);
314            }
315            ctx.context.begin_tool_call(&call);
316            if self.spec.timeout.is_none() {
317                let mut result = handler(ctx.context, &call.arguments);
318                if result.tool_call_id.trim().is_empty() || result.tool_call_id == "pending" {
319                    result.tool_call_id = call.id;
320                }
321                return Ok(result);
322            }
323
324            let arguments = call.arguments.clone();
325            let mut isolated_context = ctx.context.clone();
326            let (mut result, updated_context) = tokio::task::spawn_blocking(move || {
327                let result = handler(&mut isolated_context, &arguments);
328                (result, isolated_context)
329            })
330            .await
331            .map_err(|error| ToolError::new(format!("tool task failed: {error}")))?;
332            *ctx.context = updated_context;
333            if result.tool_call_id.trim().is_empty() || result.tool_call_id == "pending" {
334                result.tool_call_id = call.id;
335            }
336            Ok(result)
337        })
338    }
339}