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}