Skip to main content

nemo_relay/api/runtime/
scope_stack.rs

1// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Scope stack storage and propagation helpers.
5//!
6//! The runtime tracks the current scope hierarchy through a shared
7//! [`ScopeStack`] stored in task-local or thread-local state. Advanced callers
8//! can use this module to inspect the active scope chain or propagate scope
9//! context into worker threads.
10
11use std::cell::RefCell;
12use std::collections::{HashMap, HashSet};
13use std::future::Future;
14use std::sync::{Arc, RwLock};
15
16use serde::{Deserialize, Serialize};
17use uuid::Uuid;
18
19use crate::api::runtime::callbacks::EventSubscriberFn;
20use crate::api::scope::{ScopeHandle, ScopeType};
21use crate::context::registries::ScopeLocalRegistries;
22use crate::error::{FlowError, Result};
23use crate::registry::{RegistryEntry, SortedRegistry};
24
25/// Mutable stack of active scopes plus their scope-local registries.
26///
27/// The stack always contains an implicit root agent scope. It owns freshness
28/// for work that is not nested under an explicit agent; non-agent scopes inherit
29/// their nearest agent's freshness instead of creating a separate budget.
30/// Additional scopes are pushed as the public API opens lifecycle spans and
31/// removed when those spans close.
32pub struct ScopeStack {
33    stack: Vec<ScopeHandle>,
34    scope_registries: HashMap<Uuid, ScopeLocalRegistries>,
35    fresh_agents: HashSet<Uuid>,
36    propagated_parent_uuid: Option<Uuid>,
37}
38
39/// Versioned, transport-neutral causal context for crossing a Relay boundary.
40///
41/// Applications are responsible for serializing, transporting, authenticating,
42/// and trusting this value. It intentionally contains only Relay identifiers;
43/// OpenTelemetry `traceparent` and `tracestate` remain transport sidecars. A
44/// context without a `root_uuid` preserves Relay event parentage when imported.
45/// The first local OpenTelemetry span created after import starts a new trace.
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47pub struct PropagationContext {
48    /// Wire-format version. Version 1 is the only currently supported value.
49    pub version: u16,
50    /// Stable session root when the sending application knows one. When this
51    /// root is omitted, the first local OpenTelemetry span after import starts
52    /// a new trace.
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub root_uuid: Option<Uuid>,
55    /// Immediate Relay event or scope that caused the boundary crossing.
56    pub parent_uuid: Uuid,
57}
58
59impl PropagationContext {
60    /// The current wire-format version.
61    pub const VERSION: u16 = 1;
62
63    /// Serialize this validated context for application-managed transport.
64    pub fn to_json(&self) -> Result<String> {
65        self.validate()?;
66        Ok(serde_json::to_string(self).expect("PropagationContext is always JSON serializable"))
67    }
68
69    /// Deserialize and validate a context received from application-managed transport.
70    pub fn from_json(value: &str) -> Result<Self> {
71        let context: Self = serde_json::from_str(value).map_err(|error| {
72            FlowError::InvalidArgument(format!("invalid propagation context JSON: {error}"))
73        })?;
74        context.validate()?;
75        Ok(context)
76    }
77
78    /// Validate a context received from an untrusted transport.
79    pub fn validate(&self) -> Result<()> {
80        if self.version != Self::VERSION {
81            return Err(FlowError::InvalidArgument(format!(
82                "unsupported propagation context version {}; expected {}",
83                self.version,
84                Self::VERSION
85            )));
86        }
87        for (name, uuid) in [("parent_uuid", self.parent_uuid)]
88            .into_iter()
89            .chain(self.root_uuid.map(|uuid| ("root_uuid", uuid)))
90        {
91            let bytes = uuid.as_bytes();
92            if bytes.iter().all(|byte| *byte == 0) || bytes[8..].iter().all(|byte| *byte == 0) {
93                return Err(FlowError::InvalidArgument(format!(
94                    "propagation context {name} is not a usable Relay identifier"
95                )));
96            }
97        }
98        Ok(())
99    }
100}
101
102impl ScopeStack {
103    fn snapshot(&self) -> Self {
104        Self {
105            stack: self.stack.clone(),
106            scope_registries: self.scope_registries.clone(),
107            fresh_agents: self.fresh_agents.clone(),
108            propagated_parent_uuid: self.propagated_parent_uuid,
109        }
110    }
111
112    /// Create a new scope stack containing only the implicit root scope.
113    ///
114    /// # Returns
115    /// A [`ScopeStack`] initialized with a single root scope and no
116    /// scope-local registries.
117    pub fn new() -> Self {
118        let root = ScopeHandle::builder()
119            .name("root")
120            .scope_type(ScopeType::Agent)
121            .build();
122        let root_uuid = root.uuid;
123        Self {
124            stack: vec![root],
125            scope_registries: HashMap::new(),
126            fresh_agents: HashSet::from([root_uuid]),
127            propagated_parent_uuid: None,
128        }
129    }
130
131    fn from_propagation(context: &PropagationContext) -> Result<Self> {
132        context.validate()?;
133        let (root, parent) = match context.root_uuid {
134            Some(root_uuid) => {
135                let root = ScopeHandle::builder()
136                    .uuid(root_uuid)
137                    .name("propagated-root")
138                    .scope_type(ScopeType::Agent)
139                    .build();
140                let parent = (root_uuid != context.parent_uuid).then(|| {
141                    ScopeHandle::builder()
142                        .uuid(context.parent_uuid)
143                        .parent_uuid(root_uuid)
144                        .name("propagated-parent")
145                        .scope_type(ScopeType::Unknown)
146                        .build()
147                });
148                (root, parent)
149            }
150            None => (
151                ScopeHandle::builder()
152                    .uuid(context.parent_uuid)
153                    .name("propagated-root")
154                    .scope_type(ScopeType::Agent)
155                    .build(),
156                None,
157            ),
158        };
159        let root_uuid = root.uuid;
160        let mut stack = vec![root];
161        if let Some(parent) = parent {
162            stack.push(parent);
163        }
164        Ok(Self {
165            stack,
166            scope_registries: HashMap::new(),
167            fresh_agents: HashSet::from([root_uuid]),
168            propagated_parent_uuid: context.root_uuid.map(|_| context.parent_uuid),
169        })
170    }
171
172    /// Push a scope handle onto the top of the stack.
173    ///
174    /// # Parameters
175    /// - `handle`: Scope handle to make the new top-most active scope.
176    pub fn push(&mut self, handle: ScopeHandle) {
177        if matches!(handle.scope_type, ScopeType::Agent) {
178            self.fresh_agents.insert(handle.uuid);
179        }
180        self.stack.push(handle);
181    }
182
183    /// Return the current top-most scope handle.
184    ///
185    /// # Returns
186    /// A shared reference to the active scope at the top of the stack.
187    ///
188    /// # Notes
189    /// This function never returns `None` because the implicit root scope is
190    /// always present.
191    pub fn top(&self) -> &ScopeHandle {
192        self.stack
193            .last()
194            .expect("scope stack should never be empty")
195    }
196
197    /// Return the current top-most scope handle mutably.
198    ///
199    /// # Returns
200    /// A mutable reference to the active scope at the top of the stack.
201    pub fn top_mut(&mut self) -> &mut ScopeHandle {
202        self.stack
203            .last_mut()
204            .expect("scope stack should never be empty")
205    }
206
207    /// Return the UUID of the implicit root scope.
208    ///
209    /// # Returns
210    /// The stable UUID of the root scope stored at the bottom of the stack.
211    pub fn root_uuid(&self) -> Uuid {
212        self.stack
213            .first()
214            .expect("scope stack should never be empty")
215            .uuid
216    }
217
218    /// Whether `uuid` is the synthetic parent imported from propagation.
219    pub fn is_propagated_parent(&self, uuid: Uuid) -> bool {
220        self.propagated_parent_uuid == Some(uuid)
221    }
222
223    /// Return the full ordered stack of scope handles.
224    ///
225    /// # Returns
226    /// A slice of scopes ordered from root to the current top-most scope.
227    pub fn scopes(&self) -> &[ScopeHandle] {
228        &self.stack
229    }
230
231    /// Find a scope handle by UUID.
232    ///
233    /// # Parameters
234    /// - `uuid`: UUID of the scope to search for.
235    ///
236    /// # Returns
237    /// `Some(&ScopeHandle)` when the scope is active on this stack and `None`
238    /// otherwise.
239    pub fn find(&self, uuid: &Uuid) -> Option<&ScopeHandle> {
240        self.stack.iter().find(|handle| handle.uuid == *uuid)
241    }
242
243    /// Remove the current top scope if it matches `uuid`.
244    ///
245    /// # Parameters
246    /// - `uuid`: UUID of the scope expected to be at the top of the stack.
247    ///
248    /// # Returns
249    /// A [`Result`] containing the removed [`ScopeHandle`].
250    ///
251    /// # Errors
252    /// Returns [`FlowError::InvalidArgument`] when the scope exists but is not
253    /// the current top of the stack or when the caller attempts to remove the
254    /// implicit root scope. Returns [`FlowError::NotFound`] when the UUID is
255    /// not present on the stack.
256    pub fn remove(&mut self, uuid: &Uuid) -> Result<ScopeHandle> {
257        let top = self
258            .stack
259            .last()
260            .expect("scope stack should never be empty");
261        if top.uuid == *uuid {
262            if self.stack.len() == 1 {
263                return Err(FlowError::InvalidArgument(
264                    "root scope cannot be removed".into(),
265                ));
266            }
267            self.scope_registries.remove(uuid);
268            self.fresh_agents.remove(uuid);
269            return Ok(self
270                .stack
271                .pop()
272                .expect("scope stack should contain a removable top scope"));
273        }
274
275        if self.stack.iter().any(|handle| handle.uuid == *uuid) {
276            return Err(FlowError::InvalidArgument(
277                "scope handle is not at the top of the stack".into(),
278            ));
279        }
280
281        Err(FlowError::NotFound("scope handle not found".into()))
282    }
283
284    fn owning_agent_uuid(&self, parent_uuid: Option<Uuid>) -> Uuid {
285        let search_end = parent_uuid
286            .and_then(|parent_uuid| {
287                self.stack
288                    .iter()
289                    .position(|scope| scope.uuid == parent_uuid)
290            })
291            .map_or(self.stack.len(), |index| index + 1);
292        self.stack[..search_end]
293            .iter()
294            .rev()
295            .find(|scope| matches!(scope.scope_type, ScopeType::Agent))
296            .map(|scope| scope.uuid)
297            .expect("scope stack should always contain an owning agent")
298    }
299
300    /// Return whether the owning agent is fresh, then mark it stale.
301    pub(crate) fn take_agent_freshness(&mut self, parent_uuid: Option<Uuid>) -> bool {
302        let uuid = self.owning_agent_uuid(parent_uuid);
303        self.fresh_agents.remove(&uuid)
304    }
305
306    /// Mark the agent that owns a compaction event as fresh.
307    pub(crate) fn mark_agent_fresh(&mut self, parent_uuid: Option<Uuid>) {
308        let uuid = self.owning_agent_uuid(parent_uuid);
309        self.fresh_agents.insert(uuid);
310    }
311
312    /// Get or create the scope-local registries for an active scope.
313    ///
314    /// # Parameters
315    /// - `uuid`: UUID of an active scope on this stack.
316    ///
317    /// # Returns
318    /// `Some(&mut ScopeLocalRegistries)` when the scope is active and `None`
319    /// otherwise.
320    ///
321    /// # Notes
322    /// When the scope is active but has no registries yet, this function
323    /// creates an empty scope-local registry set first.
324    pub(crate) fn local_registries_mut(
325        &mut self,
326        uuid: &Uuid,
327    ) -> Option<&mut ScopeLocalRegistries> {
328        if !self.stack.iter().any(|handle| handle.uuid == *uuid) {
329            return None;
330        }
331        Some(self.scope_registries.entry(*uuid).or_default())
332    }
333
334    /// Collect one registry field from every active scope that owns it.
335    ///
336    /// # Parameters
337    /// - `field`: Projection function selecting the registry field to collect
338    ///   from each scope-local registry.
339    ///
340    /// # Returns
341    /// A vector of registry references ordered from root toward the current
342    /// top-most scope.
343    pub(crate) fn collect_scope_local_registries<'a, T: RegistryEntry>(
344        &'a self,
345        field: impl Fn(&'a ScopeLocalRegistries) -> &'a SortedRegistry<T>,
346    ) -> Vec<&'a SortedRegistry<T>> {
347        self.stack
348            .iter()
349            .filter_map(|handle| self.scope_registries.get(&handle.uuid))
350            .map(field)
351            .collect()
352    }
353
354    /// Collect all scope-local subscribers visible from the active stack.
355    ///
356    /// # Returns
357    /// A vector of subscribers collected from each active scope that owns
358    /// scope-local registries.
359    pub(crate) fn collect_scope_local_subscribers(&self) -> Vec<EventSubscriberFn> {
360        self.stack
361            .iter()
362            .filter_map(|handle| self.scope_registries.get(&handle.uuid))
363            .flat_map(|registries| registries.event_subscribers.values().cloned())
364            .collect()
365    }
366}
367
368impl std::fmt::Debug for ScopeStack {
369    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
370        f.debug_struct("ScopeStack")
371            .field("stack", &self.stack)
372            .field("scope_registries_count", &self.scope_registries.len())
373            .field("fresh_agent_count", &self.fresh_agents.len())
374            .finish()
375    }
376}
377
378impl Default for ScopeStack {
379    fn default() -> Self {
380        Self::new()
381    }
382}
383
384/// Shared handle type for the runtime scope stack.
385///
386/// The runtime stores the active [`ScopeStack`] behind an [`Arc`] and [`RwLock`]
387/// so bindings can propagate it across execution contexts while still allowing
388/// concurrent readers.
389pub type ScopeStackHandle = Arc<RwLock<ScopeStack>>;
390
391/// Captured thread-local scope stack binding.
392///
393/// This preserves both the visible scope stack handle and whether it was
394/// explicitly installed on the current thread.
395#[derive(Clone)]
396pub struct ThreadScopeStackBinding {
397    stack: ScopeStackHandle,
398    explicit: bool,
399}
400
401impl ThreadScopeStackBinding {
402    /// Return the captured thread-local scope stack handle.
403    pub fn stack(&self) -> ScopeStackHandle {
404        self.stack.clone()
405    }
406}
407
408/// Create a new scope stack handle with an implicit root scope.
409///
410/// The returned handle wraps a freshly initialized [`ScopeStack`] inside an
411/// [`Arc`] and [`RwLock`] so it can be shared across async tasks or threads.
412///
413/// # Returns
414/// A new [`ScopeStackHandle`] containing exactly one implicit root scope.
415///
416/// # Notes
417/// The root scope is always present and cannot be removed.
418pub fn create_scope_stack() -> ScopeStackHandle {
419    Arc::new(RwLock::new(ScopeStack::new()))
420}
421
422/// Clone a scope stack into an isolated emission-time snapshot.
423#[doc(hidden)]
424pub(crate) fn snapshot_scope_stack(handle: &ScopeStackHandle) -> Result<ScopeStackHandle> {
425    let stack = handle
426        .read()
427        .unwrap_or_else(|error| error.into_inner())
428        .snapshot();
429    Ok(Arc::new(RwLock::new(stack)))
430}
431
432/// Create an isolated scope stack rooted below a supplied propagation context.
433///
434/// The imported handles are synthetic bookkeeping only; Relay never emits their
435/// lifecycle events or transfers scope-local registrations across the boundary.
436pub fn create_scope_stack_from_propagation(
437    context: &PropagationContext,
438) -> Result<ScopeStackHandle> {
439    Ok(Arc::new(RwLock::new(ScopeStack::from_propagation(
440        context,
441    )?)))
442}
443
444/// Create an isolated scope stack below the current causal parent.
445///
446/// Capture the parent before spawning concurrent work, then install the
447/// returned stack with `TASK_SCOPE_STACK.scope(...)`. The fork preserves event
448/// parentage but does not transfer scope-local registrations. Because the fork
449/// does not assert a root UUID, its first local OpenTelemetry span starts a new
450/// trace.
451///
452/// # Examples
453///
454/// ```no_run
455/// # async fn example() -> nemo_relay::error::Result<()> {
456/// use nemo_relay::api::runtime::{TASK_SCOPE_STACK, fork_scope_stack};
457///
458/// let stack = fork_scope_stack()?;
459/// tokio::spawn(TASK_SCOPE_STACK.scope(stack, async {
460///     // Relay work in an isolated child task.
461/// }));
462/// # Ok(())
463/// # }
464/// ```
465pub fn fork_scope_stack() -> Result<ScopeStackHandle> {
466    let context = capture_propagation_context()?;
467    create_scope_stack_from_propagation(&context)
468}
469
470/// Capture the current causal parent without asserting a session root.
471///
472/// Importing the returned context preserves Relay event parentage but starts a
473/// new local OpenTelemetry trace. Use [`capture_propagation_context_with_root`]
474/// when the receiver should participate in a Relay-derived trace rooted at a
475/// stable application UUID.
476pub fn capture_propagation_context() -> Result<PropagationContext> {
477    capture_propagation_context_with_root(None)
478}
479
480/// Capture the current causal parent and an application-supplied session root.
481pub fn capture_propagation_context_with_root(
482    root_uuid: Option<Uuid>,
483) -> Result<PropagationContext> {
484    let context = PropagationContext {
485        version: PropagationContext::VERSION,
486        root_uuid,
487        parent_uuid: ACTIVE_EVENT_UUID
488            .try_with(|uuid| *uuid)
489            .unwrap_or_else(|_| task_scope_top().uuid),
490    };
491    context.validate()?;
492    Ok(context)
493}
494
495tokio::task_local! {
496    /// Task-local scope stack handle used by async execution contexts.
497    pub static TASK_SCOPE_STACK: ScopeStackHandle;
498    /// Managed tool or LLM event currently executing in this task.
499    static ACTIVE_EVENT_UUID: Uuid;
500}
501
502/// Run a future with `uuid` as the causally active managed event.
503pub async fn with_active_event_uuid<T>(uuid: Uuid, future: impl Future<Output = T>) -> T {
504    ACTIVE_EVENT_UUID.scope(uuid, future).await
505}
506
507pub(crate) fn active_event_uuid() -> Option<Uuid> {
508    ACTIVE_EVENT_UUID.try_with(|uuid| *uuid).ok()
509}
510
511thread_local! {
512    /// Synchronous override used by native plugin callbacks that need to run a
513    /// bounded block with an isolated stack even inside a task-local context.
514    static SCOPE_STACK_OVERRIDE: RefCell<Option<ScopeStackHandle>> = const { RefCell::new(None) };
515    /// Thread-local fallback scope stack for non-task contexts.
516    static THREAD_SCOPE_STACK: RefCell<ScopeStackHandle> = RefCell::new(create_scope_stack());
517    /// Whether the current thread explicitly owns a scope stack.
518    static THREAD_SCOPE_STACK_EXPLICIT: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
519}
520
521/// Return the scope stack visible to the current execution context.
522///
523/// This resolves task-local scope state first and otherwise falls back to the
524/// current thread-local scope stack handle.
525///
526/// # Returns
527/// The active [`ScopeStackHandle`] for the current async task or thread.
528///
529/// # Notes
530/// When no explicit thread-local stack has been installed yet, the default
531/// per-thread root-only stack is returned.
532pub fn current_scope_stack() -> ScopeStackHandle {
533    if let Some(stack) = SCOPE_STACK_OVERRIDE.with(|stack| stack.borrow().clone()) {
534        return stack;
535    }
536    TASK_SCOPE_STACK
537        .try_with(|stack| stack.clone())
538        .unwrap_or_else(|_| THREAD_SCOPE_STACK.with(|stack| stack.borrow().clone()))
539}
540
541/// Return a scope stack explicitly bound to the current task or override.
542///
543/// Unlike [`current_scope_stack`], this does not fall back to ambient
544/// thread-local state. Continuation adapters use it to distinguish an
545/// intentional per-call scope selection from an unrelated runtime-worker
546/// thread binding.
547pub(crate) fn current_context_scope_stack() -> Option<ScopeStackHandle> {
548    SCOPE_STACK_OVERRIDE
549        .with(|stack| stack.borrow().clone())
550        .or_else(|| TASK_SCOPE_STACK.try_with(Clone::clone).ok())
551}
552
553/// Run a synchronous callback with `handle` as the visible scope stack.
554///
555/// This override takes precedence over task-local and thread-local stacks for
556/// the duration of the callback and is restored even when the callback panics.
557pub fn with_scope_stack<T>(handle: ScopeStackHandle, f: impl FnOnce() -> T) -> T {
558    struct OverrideGuard {
559        previous: Option<ScopeStackHandle>,
560    }
561
562    impl Drop for OverrideGuard {
563        fn drop(&mut self) {
564            let previous = self.previous.take();
565            SCOPE_STACK_OVERRIDE.with(|stack| *stack.borrow_mut() = previous);
566        }
567    }
568
569    let previous = SCOPE_STACK_OVERRIDE.with(|stack| stack.replace(Some(handle)));
570    let _guard = OverrideGuard { previous };
571    f()
572}
573
574/// Install an explicit scope stack for the current thread.
575///
576/// This replaces the thread-local scope stack handle and marks the current
577/// thread as explicitly scope-aware for later propagation checks.
578///
579/// # Parameters
580/// - `handle`: Scope stack handle to install for the current thread.
581///
582/// # Returns
583/// `()`.
584///
585/// # Notes
586/// Use this when propagating an existing scope stack into worker threads.
587pub fn set_thread_scope_stack(handle: ScopeStackHandle) {
588    THREAD_SCOPE_STACK.with(|stack| *stack.borrow_mut() = handle);
589    THREAD_SCOPE_STACK_EXPLICIT.with(|flag| flag.set(true));
590}
591
592/// Capture the current thread-local scope stack binding.
593///
594/// This is intended for foreign runtimes that temporarily bind a scope stack to
595/// an OS thread and need to restore the exact previous state before releasing
596/// that thread back to their scheduler.
597///
598/// # Returns
599/// A [`ThreadScopeStackBinding`] containing the current thread-local stack and
600/// explicit-binding flag.
601pub fn capture_thread_scope_stack() -> ThreadScopeStackBinding {
602    let stack = THREAD_SCOPE_STACK.with(|stack| stack.borrow().clone());
603    let explicit = THREAD_SCOPE_STACK_EXPLICIT.with(|flag| flag.get());
604    ThreadScopeStackBinding { stack, explicit }
605}
606
607/// Restore a previously captured thread-local scope stack binding.
608///
609/// # Parameters
610/// - `binding`: Captured binding to restore on the current thread.
611///
612/// # Returns
613/// `()`.
614pub fn restore_thread_scope_stack(binding: ThreadScopeStackBinding) {
615    THREAD_SCOPE_STACK.with(|stack| *stack.borrow_mut() = binding.stack);
616    THREAD_SCOPE_STACK_EXPLICIT.with(|flag| flag.set(binding.explicit));
617}
618
619/// Synchronize the thread-local scope stack without marking it explicit.
620///
621/// This updates the thread-local slot used by native runtime code while
622/// preserving whether the thread was explicitly marked as owning a scope stack.
623///
624/// # Parameters
625/// - `handle`: Scope stack handle to synchronize into thread-local storage.
626///
627/// # Returns
628/// `()`.
629///
630/// # Notes
631/// Python bindings use this to mirror `ContextVar` state into Rust without
632/// forcing `scope_stack_active()` to become `true` for the thread.
633pub fn sync_thread_scope_stack(handle: ScopeStackHandle) {
634    THREAD_SCOPE_STACK.with(|stack| *stack.borrow_mut() = handle);
635}
636
637/// Report whether the current context has an explicitly active scope stack.
638///
639/// This checks task-local state first and otherwise falls back to the
640/// thread-local explicit flag.
641///
642/// # Returns
643/// `true` when the current async task or thread already owns an active scope
644/// stack and `false` otherwise.
645///
646/// # Notes
647/// A synchronized thread-local stack does not count as explicit unless it was
648/// installed through [`set_thread_scope_stack`].
649pub fn scope_stack_active() -> bool {
650    if SCOPE_STACK_OVERRIDE.with(|stack| stack.borrow().is_some()) {
651        return true;
652    }
653    TASK_SCOPE_STACK
654        .try_with(|_| true)
655        .unwrap_or_else(|_| THREAD_SCOPE_STACK_EXPLICIT.with(|flag| flag.get()))
656}
657
658/// Capture the current scope stack handle for use in another thread.
659///
660/// This returns the handle currently visible to the caller so it can be passed
661/// into [`set_thread_scope_stack`] elsewhere.
662///
663/// # Returns
664/// A [`Result`] containing the active [`ScopeStackHandle`].
665///
666/// # Errors
667/// Returns an error when the current context does not yet own an active scope
668/// stack.
669///
670/// # Notes
671/// The returned handle is shared; it does not clone the underlying stack.
672pub fn propagate_scope_to_thread() -> Result<ScopeStackHandle> {
673    if !scope_stack_active() {
674        return Err(FlowError::Internal(
675            "no active scope stack in current context; call create_scope_stack() and set_thread_scope_stack() first"
676                .into(),
677        ));
678    }
679    Ok(current_scope_stack())
680}
681
682/// Clone the current top-most scope handle from the active stack.
683///
684/// # Returns
685/// A cloned [`ScopeHandle`] representing the current active scope.
686pub fn task_scope_top() -> ScopeHandle {
687    let stack = current_scope_stack();
688    let guard = stack.read().expect("scope stack lock poisoned");
689    guard.top().clone()
690}
691
692/// Push a scope handle onto the active stack.
693///
694/// # Parameters
695/// - `handle`: Scope handle to push onto the current execution context's stack.
696pub fn task_scope_push(handle: ScopeHandle) {
697    let stack = current_scope_stack();
698    let mut guard = stack.write().expect("scope stack lock poisoned");
699    guard.push(handle);
700}
701
702/// Remove a scope handle from the active stack.
703///
704/// # Parameters
705/// - `uuid`: UUID of the scope expected to be at the top of the active stack.
706///
707/// # Returns
708/// A [`Result`] containing the removed [`ScopeHandle`].
709///
710/// # Errors
711/// Propagates the same errors returned by [`ScopeStack::remove`].
712pub fn task_scope_remove(uuid: &Uuid) -> Result<ScopeHandle> {
713    let stack = current_scope_stack();
714    let mut guard = stack.write().expect("scope stack lock poisoned");
715    guard.remove(uuid)
716}