harn_vm/vm/async_builtin.rs
1use std::cell::RefCell;
2
3use super::Vm;
4
5thread_local! {
6 pub(super) static CURRENT_ASYNC_BUILTIN_CHILD_VM: RefCell<Vec<Vm>> =
7 const { RefCell::new(Vec::new()) };
8}
9
10/// Clone the VM at the top of the async-builtin child VM stack, returning a
11/// fresh `Vm` instance the caller owns. Enables concurrent tool-handler
12/// execution within a single agent_loop iteration — the VM shares its heavy
13/// state (env, builtins, bridge, module_cache) via `Arc`/`Rc`, so cloning is
14/// cheap and each handler gets its own execution context.
15///
16/// Returns `None` if no parent VM is currently pushed on the stack.
17pub fn clone_async_builtin_child_vm() -> Option<Vm> {
18 CURRENT_ASYNC_BUILTIN_CHILD_VM.with(|slot| slot.borrow().last().map(|vm| vm.child_vm()))
19}
20
21/// Forward captured output from a transient child VM (typically created via
22/// `clone_async_builtin_child_vm` and used to invoke a closure) back into the
23/// async-builtin's host stack entry. The dispatch loop drains that entry's
24/// output back to the original parent VM after the async builtin returns.
25///
26/// Without this hook, `log()`/`print()` calls inside `post_turn_callback`
27/// closures, tool handlers, and other VM-side closures invoked from async
28/// builtins would silently disappear because the transient cb_vm.output
29/// buffer is dropped on scope exit.
30pub fn forward_child_output_to_parent(text: &str) {
31 if text.is_empty() {
32 return;
33 }
34 CURRENT_ASYNC_BUILTIN_CHILD_VM.with(|slot| {
35 if let Some(top) = slot.borrow_mut().last_mut() {
36 top.append_output(text);
37 }
38 });
39}
40
41pub struct AsyncBuiltinChildVmGuard;
42
43pub fn install_async_builtin_child_vm(vm: Vm) -> AsyncBuiltinChildVmGuard {
44 CURRENT_ASYNC_BUILTIN_CHILD_VM.with(|slot| {
45 slot.borrow_mut().push(vm);
46 });
47 AsyncBuiltinChildVmGuard
48}
49
50impl Drop for AsyncBuiltinChildVmGuard {
51 fn drop(&mut self) {
52 CURRENT_ASYNC_BUILTIN_CHILD_VM.with(|slot| {
53 slot.borrow_mut().pop();
54 });
55 }
56}
57
58/// Legacy API preserved for out-of-tree callers; new code should use
59/// `clone_async_builtin_child_vm()`. `take/restore` serialized concurrent
60/// callers because only one could hold the popped value at a time.
61#[deprecated(
62 note = "use clone_async_builtin_child_vm() — take/restore serialized concurrent callers"
63)]
64pub fn take_async_builtin_child_vm() -> Option<Vm> {
65 clone_async_builtin_child_vm()
66}
67
68/// Legacy no-op retained for backward compatibility.
69#[deprecated(note = "clone_async_builtin_child_vm does not need a matching restore call")]
70pub fn restore_async_builtin_child_vm(_vm: Vm) {
71 CURRENT_ASYNC_BUILTIN_CHILD_VM.with(|slot| {
72 let _ = slot;
73 });
74}