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