vv_agent/tools/
executor.rs1use 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 Direct,
18 Deferred,
21 DirectModelOnly,
24 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}