1use std::future::Future;
2use std::pin::Pin;
3use std::rc::Rc;
4
5use crate::value::{ErrorCategory, VmClosure, VmError, VmValue};
6use crate::BuiltinId;
7
8use super::async_builtin::CURRENT_ASYNC_BUILTIN_CHILD_VM;
9use super::{ScopeSpan, Vm, VmBuiltinDispatch, VmBuiltinEntry, VmBuiltinKind, VmBuiltinMetadata};
10
11impl Vm {
12 fn index_builtin_id(&mut self, name: &str, dispatch: VmBuiltinDispatch) {
13 let id = BuiltinId::from_name(name);
14 if self.builtin_id_collisions.contains(&id) {
15 return;
16 }
17 if let Some(existing) = self.builtins_by_id.get(&id) {
18 if existing.name.as_ref() != name {
19 Rc::make_mut(&mut self.builtins_by_id).remove(&id);
20 Rc::make_mut(&mut self.builtin_id_collisions).insert(id);
21 return;
22 }
23 }
24 Rc::make_mut(&mut self.builtins_by_id).insert(
25 id,
26 VmBuiltinEntry {
27 name: Rc::from(name),
28 dispatch,
29 },
30 );
31 }
32
33 fn refresh_builtin_id(&mut self, name: &str) {
34 if let Some(builtin) = self.builtins.get(name).cloned() {
35 self.index_builtin_id(name, VmBuiltinDispatch::Sync(builtin));
36 } else if let Some(async_builtin) = self.async_builtins.get(name).cloned() {
37 self.index_builtin_id(name, VmBuiltinDispatch::Async(async_builtin));
38 } else {
39 let id = BuiltinId::from_name(name);
40 if self
41 .builtins_by_id
42 .get(&id)
43 .is_some_and(|entry| entry.name.as_ref() == name)
44 {
45 Rc::make_mut(&mut self.builtins_by_id).remove(&id);
46 }
47 }
48 }
49
50 pub fn register_builtin<F>(&mut self, name: &str, f: F)
52 where
53 F: Fn(&[VmValue], &mut String) -> Result<VmValue, VmError> + 'static,
54 {
55 Rc::make_mut(&mut self.builtins).insert(name.to_string(), Rc::new(f));
56 Rc::make_mut(&mut self.builtin_metadata)
57 .insert(name.to_string(), VmBuiltinMetadata::sync(name.to_string()));
58 self.refresh_builtin_id(name);
59 }
60
61 pub fn register_builtin_with_metadata<F>(&mut self, metadata: VmBuiltinMetadata, f: F)
63 where
64 F: Fn(&[VmValue], &mut String) -> Result<VmValue, VmError> + 'static,
65 {
66 let name = metadata.name().to_string();
67 Rc::make_mut(&mut self.builtins).insert(name.clone(), Rc::new(f));
68 Rc::make_mut(&mut self.builtin_metadata)
69 .insert(name.clone(), metadata.with_kind(VmBuiltinKind::Sync));
70 self.refresh_builtin_id(&name);
71 }
72
73 pub fn unregister_builtin(&mut self, name: &str) {
75 Rc::make_mut(&mut self.builtins).remove(name);
76 if self.async_builtins.contains_key(name) {
77 Rc::make_mut(&mut self.builtin_metadata).insert(
78 name.to_string(),
79 VmBuiltinMetadata::async_builtin(name.to_string()),
80 );
81 } else {
82 Rc::make_mut(&mut self.builtin_metadata).remove(name);
83 }
84 self.refresh_builtin_id(name);
85 }
86
87 pub fn register_async_builtin<F, Fut>(&mut self, name: &str, f: F)
89 where
90 F: Fn(Vec<VmValue>) -> Fut + 'static,
91 Fut: Future<Output = Result<VmValue, VmError>> + 'static,
92 {
93 Rc::make_mut(&mut self.async_builtins)
94 .insert(name.to_string(), Rc::new(move |args| Box::pin(f(args))));
95 Rc::make_mut(&mut self.builtin_metadata).insert(
96 name.to_string(),
97 VmBuiltinMetadata::async_builtin(name.to_string()),
98 );
99 self.refresh_builtin_id(name);
100 }
101
102 pub fn register_async_builtin_with_metadata<F, Fut>(
104 &mut self,
105 metadata: VmBuiltinMetadata,
106 f: F,
107 ) where
108 F: Fn(Vec<VmValue>) -> Fut + 'static,
109 Fut: Future<Output = Result<VmValue, VmError>> + 'static,
110 {
111 let name = metadata.name().to_string();
112 Rc::make_mut(&mut self.async_builtins)
113 .insert(name.clone(), Rc::new(move |args| Box::pin(f(args))));
114 Rc::make_mut(&mut self.builtin_metadata)
115 .insert(name.clone(), metadata.with_kind(VmBuiltinKind::Async));
116 self.refresh_builtin_id(&name);
117 }
118
119 pub(crate) fn registered_builtin_id(&self, name: &str) -> Option<BuiltinId> {
120 let id = BuiltinId::from_name(name);
121 if self
122 .builtins_by_id
123 .get(&id)
124 .is_some_and(|entry| entry.name.as_ref() == name)
125 {
126 Some(id)
127 } else {
128 None
129 }
130 }
131
132 pub(crate) fn call_closure<'a>(
135 &'a mut self,
136 closure: &'a VmClosure,
137 args: &'a [VmValue],
138 ) -> Pin<Box<dyn Future<Output = Result<VmValue, VmError>> + 'a>> {
139 Box::pin(async move {
140 crate::typecheck::validate_user_call(&closure.func, args, None)?;
141 let saved_env = self.env.clone();
142 let mut call_env = self.closure_call_env_for_current_frame(closure);
143 let saved_frames = std::mem::take(&mut self.frames);
144 let saved_handlers = std::mem::take(&mut self.exception_handlers);
145 let saved_iterators = std::mem::take(&mut self.iterators);
146 let saved_deadlines = std::mem::take(&mut self.deadlines);
147 let active_context = (!crate::step_runtime::is_tracked_function(&closure.func.name))
148 .then(crate::step_runtime::take_active_context);
149
150 call_env.push_scope();
151
152 self.env = call_env;
153 let argc = args.len();
154 let mut local_slots = Self::fresh_local_slots(&closure.func.chunk);
155 Self::bind_param_slots(&mut local_slots, &closure.func, args, false);
156 let saved_source_dir = if let Some(ref dir) = closure.source_dir {
157 let prev = crate::stdlib::process::VM_SOURCE_DIR.with(|sd| sd.borrow().clone());
158 crate::stdlib::set_thread_source_dir(dir);
159 prev
160 } else {
161 None
162 };
163 let result = self
164 .run_chunk_ref(
165 Rc::clone(&closure.func.chunk),
166 argc,
167 saved_source_dir,
168 closure.module_functions.clone(),
169 closure.module_state.clone(),
170 Some(local_slots),
171 )
172 .await;
173
174 self.env = saved_env;
175 self.frames = saved_frames;
176 self.exception_handlers = saved_handlers;
177 self.iterators = saved_iterators;
178 self.deadlines = saved_deadlines;
179 if let Some(active_context) = active_context {
180 crate::step_runtime::restore_active_context(active_context);
181 }
182
183 result
184 })
185 }
186
187 #[allow(clippy::manual_async_fn)]
192 pub(crate) fn call_callable_value<'a>(
193 &'a mut self,
194 callable: &'a VmValue,
195 args: &'a [VmValue],
196 ) -> Pin<Box<dyn Future<Output = Result<VmValue, VmError>> + 'a>> {
197 Box::pin(async move {
198 match callable {
199 VmValue::Closure(closure) => self.call_closure(closure, args).await,
200 VmValue::BuiltinRef(name) => {
201 if !crate::autonomy::needs_async_side_effect_enforcement(name) {
202 if let Some(result) = self.call_sync_builtin_by_ref(name, args) {
203 return result;
204 }
205 }
206 self.call_named_builtin(name, args.to_vec()).await
207 }
208 VmValue::BuiltinRefId { id, name } => {
209 self.call_builtin_id_or_name(*id, name, args.to_vec()).await
210 }
211 other => Err(VmError::TypeError(format!(
212 "expected callable, got {}",
213 other.type_name()
214 ))),
215 }
216 })
217 }
218
219 fn call_sync_builtin_by_ref(
220 &mut self,
221 name: &str,
222 args: &[VmValue],
223 ) -> Option<Result<VmValue, VmError>> {
224 let builtin = self.builtins.get(name).cloned()?;
225
226 let span_kind = match name {
227 "llm_call" | "llm_stream" | "llm_stream_call" | "agent_loop" | "agent_turn" => {
228 Some(crate::tracing::SpanKind::LlmCall)
229 }
230 "mcp_call" => Some(crate::tracing::SpanKind::ToolCall),
231 _ => None,
232 };
233 let _span = span_kind.map(|kind| ScopeSpan::new(kind, name.to_string()));
234
235 if self.denied_builtins.contains(name) {
236 return Some(Err(VmError::CategorizedError {
237 message: format!("Tool '{}' is not permitted.", name),
238 category: ErrorCategory::ToolRejected,
239 }));
240 }
241 if let Err(err) = crate::orchestration::enforce_current_policy_for_builtin(name, args) {
242 return Some(Err(err));
243 }
244 if let Err(err) = crate::typecheck::validate_builtin_call(name, args, None) {
245 return Some(Err(err));
246 }
247
248 Some(builtin(args, &mut self.output))
249 }
250
251 pub(crate) fn is_callable_value(v: &VmValue) -> bool {
253 matches!(
254 v,
255 VmValue::Closure(_) | VmValue::BuiltinRef(_) | VmValue::BuiltinRefId { .. }
256 )
257 }
258
259 pub async fn call_closure_pub(
262 &mut self,
263 closure: &VmClosure,
264 args: &[VmValue],
265 ) -> Result<VmValue, VmError> {
266 self.cancel_grace_instructions_remaining = None;
267 self.call_closure(closure, args).await
268 }
269
270 pub(crate) async fn call_named_builtin(
273 &mut self,
274 name: &str,
275 args: Vec<VmValue>,
276 ) -> Result<VmValue, VmError> {
277 self.call_builtin_impl(name, args, None).await
278 }
279
280 pub(crate) async fn call_builtin_id_or_name(
281 &mut self,
282 id: BuiltinId,
283 name: &str,
284 args: Vec<VmValue>,
285 ) -> Result<VmValue, VmError> {
286 self.call_builtin_impl(name, args, Some(id)).await
287 }
288
289 async fn call_builtin_impl(
290 &mut self,
291 name: &str,
292 args: Vec<VmValue>,
293 direct_id: Option<BuiltinId>,
294 ) -> Result<VmValue, VmError> {
295 let span_kind = match name {
297 "llm_call" | "llm_stream" | "llm_stream_call" | "agent_loop" => {
298 Some(crate::tracing::SpanKind::LlmCall)
299 }
300 "mcp_call" => Some(crate::tracing::SpanKind::ToolCall),
301 _ => None,
302 };
303 let _span = span_kind.map(|kind| ScopeSpan::new(kind, name.to_string()));
304
305 if self.denied_builtins.contains(name) {
307 return Err(VmError::CategorizedError {
308 message: format!("Tool '{}' is not permitted.", name),
309 category: ErrorCategory::ToolRejected,
310 });
311 }
312 let autonomy = if crate::autonomy::needs_async_side_effect_enforcement(name) {
313 crate::autonomy::enforce_builtin_side_effect_boxed(name, &args).await?
314 } else {
315 None
316 };
317 if let Some(crate::autonomy::AutonomyDecision::Skip(value)) = autonomy {
318 return Ok(value);
319 }
320 if !matches!(
321 autonomy,
322 Some(crate::autonomy::AutonomyDecision::AllowApproved)
323 ) {
324 crate::orchestration::enforce_current_policy_for_builtin(name, &args)?;
325 }
326 crate::typecheck::validate_builtin_call(name, &args, None)?;
327
328 if let Some(result) =
329 crate::runtime_context::dispatch_runtime_context_builtin(self, name, &args)
330 {
331 return result;
332 }
333
334 if let Some(id) = direct_id {
335 if let Some(entry) = self.builtins_by_id.get(&id).cloned() {
336 if entry.name.as_ref() == name {
337 return self.call_builtin_entry(entry.dispatch, args).await;
338 }
339 }
340 }
341
342 if let Some(builtin) = self.builtins.get(name).cloned() {
343 self.call_builtin_entry(VmBuiltinDispatch::Sync(builtin), args)
344 .await
345 } else if let Some(async_builtin) = self.async_builtins.get(name).cloned() {
346 self.call_builtin_entry(VmBuiltinDispatch::Async(async_builtin), args)
347 .await
348 } else if let Some(bridge) = &self.bridge {
349 crate::orchestration::enforce_current_policy_for_bridge_builtin(name)?;
350 let args_json: Vec<serde_json::Value> =
351 args.iter().map(crate::llm::vm_value_to_json).collect();
352 let result = bridge
353 .call(
354 "builtin_call",
355 serde_json::json!({"name": name, "args": args_json}),
356 )
357 .await?;
358 Ok(crate::bridge::json_result_to_vm_value(&result))
359 } else {
360 let all_builtins = self
361 .builtins
362 .keys()
363 .chain(self.async_builtins.keys())
364 .map(|s| s.as_str());
365 if let Some(suggestion) = crate::value::closest_match(name, all_builtins) {
366 return Err(VmError::Runtime(format!(
367 "Undefined builtin: {name} (did you mean `{suggestion}`?)"
368 )));
369 }
370 Err(VmError::UndefinedBuiltin(name.to_string()))
371 }
372 }
373
374 async fn call_builtin_entry(
375 &mut self,
376 dispatch: VmBuiltinDispatch,
377 args: Vec<VmValue>,
378 ) -> Result<VmValue, VmError> {
379 match dispatch {
380 VmBuiltinDispatch::Sync(builtin) => builtin(&args, &mut self.output),
381 VmBuiltinDispatch::Async(async_builtin) => {
382 CURRENT_ASYNC_BUILTIN_CHILD_VM.with(|slot| {
383 slot.borrow_mut().push(self.child_vm());
384 });
385 let result = async_builtin(args).await;
386 let captured = CURRENT_ASYNC_BUILTIN_CHILD_VM.with(|slot| {
387 let mut stack = slot.borrow_mut();
388 let mut top = stack.pop();
389 top.as_mut().map(|vm| vm.take_output()).unwrap_or_default()
390 });
391 if !captured.is_empty() {
392 self.output.push_str(&captured);
393 }
394 result
395 }
396 }
397 }
398}