Skip to main content

nemo_relay/api/runtime/
state.rs

1// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Process-global runtime state and middleware-chain builders.
5//!
6//! [`NemoRelayContextState`] owns the registries and helper methods that power
7//! the public scope, tool, and LLM APIs. Advanced integrations can use this
8//! type directly to register middleware, attach runtime extensions, and build
9//! the resolved callback chains that the higher-level API layer executes.
10
11use std::any::Any;
12use std::collections::HashMap;
13use std::sync::Arc;
14
15use crate::api::event::{
16    BaseEvent, CategoryProfile, Event, EventCategory, MarkEvent, ScopeCategory, ScopeEvent,
17    llm_attributes_to_strings, scope_attributes_to_strings, tool_attributes_to_strings,
18};
19use crate::api::llm::{CreateLlmHandleParams, EndLlmHandleParams};
20use crate::api::llm::{LlmHandle, LlmRequest};
21use crate::api::registry::{ExecutionIntercept, Guardrail, Intercept};
22use crate::api::runtime::callbacks::{
23    EventSubscriberFn, LlmConditionalFn, LlmExecutionFn, LlmExecutionNextFn, LlmRequestInterceptFn,
24    LlmSanitizeRequestFn, LlmSanitizeResponseFn, LlmStreamExecutionFn, LlmStreamExecutionNextFn,
25    LlmStreamExecutionRegistryRefs, ToolConditionalFn, ToolExecutionFn, ToolExecutionNextFn,
26    ToolInterceptFn, ToolSanitizeFn,
27};
28use crate::api::runtime::subscriber_dispatcher;
29use crate::api::scope::{CreateScopeHandleParams, EndScopeHandleParams, ScopeHandle, ScopeType};
30use crate::api::tool::ToolHandle;
31use crate::api::tool::{CreateToolHandleParams, EndToolHandleParams};
32use crate::codec::request::AnnotatedLlmRequest;
33use crate::codec::response::AnnotatedLlmResponse;
34use crate::context::registries::{
35    merge_execution_intercept_callables, merge_guardrail_entries, merge_intercept_entries,
36};
37use crate::json::{Json, merge_json};
38use crate::registry::SortedRegistry;
39use chrono::{Duration, Utc};
40use serde_json::json;
41use uuid::Uuid;
42
43/// Process-global runtime state backing middleware and event emission.
44///
45/// The public API layer stores one shared instance of this type for the
46/// process. It contains global middleware registries, lifecycle subscribers,
47/// and arbitrary extension slots used by bindings or integrations.
48pub struct NemoRelayContextState {
49    /// Global tool request sanitizers applied to emitted tool-start payloads.
50    pub(crate) tool_sanitize_request_guardrails: SortedRegistry<Guardrail<ToolSanitizeFn>>,
51    /// Global tool response sanitizers applied to emitted tool-end payloads.
52    pub(crate) tool_sanitize_response_guardrails: SortedRegistry<Guardrail<ToolSanitizeFn>>,
53    /// Global tool guardrails that can reject execution before the callback runs.
54    pub(crate) tool_conditional_execution_guardrails: SortedRegistry<Guardrail<ToolConditionalFn>>,
55    /// Global tool request intercepts that can rewrite arguments before execution.
56    pub(crate) tool_request_intercepts: SortedRegistry<Intercept<ToolInterceptFn>>,
57    /// Global tool execution intercepts that wrap or replace callback execution.
58    pub(crate) tool_execution_intercepts: SortedRegistry<ExecutionIntercept<ToolExecutionFn>>,
59    /// Global LLM request sanitizers applied to emitted LLM-start payloads.
60    pub(crate) llm_sanitize_request_guardrails: SortedRegistry<Guardrail<LlmSanitizeRequestFn>>,
61    /// Global LLM response sanitizers applied to emitted LLM-end payloads.
62    pub(crate) llm_sanitize_response_guardrails: SortedRegistry<Guardrail<LlmSanitizeResponseFn>>,
63    /// Global LLM guardrails that can reject execution before the provider callback runs.
64    pub(crate) llm_conditional_execution_guardrails: SortedRegistry<Guardrail<LlmConditionalFn>>,
65    /// Global LLM request intercepts that can rewrite or annotate requests.
66    pub(crate) llm_request_intercepts: SortedRegistry<Intercept<LlmRequestInterceptFn>>,
67    /// Global non-streaming LLM execution intercepts that wrap callback execution.
68    pub(crate) llm_execution_intercepts: SortedRegistry<ExecutionIntercept<LlmExecutionFn>>,
69    /// Global streaming LLM execution intercepts that wrap stream-producing callbacks.
70    pub(crate) llm_stream_execution_intercepts:
71        SortedRegistry<ExecutionIntercept<LlmStreamExecutionFn>>,
72    /// Global lifecycle subscribers notified after runtime events are emitted.
73    pub(crate) event_subscribers: HashMap<String, EventSubscriberFn>,
74    /// Arbitrary binding- or integration-specific runtime extensions.
75    pub(crate) extensions: HashMap<String, Box<dyn Any + Send + Sync>>,
76}
77
78impl NemoRelayContextState {
79    /// Create an empty runtime state with no registered middleware.
80    ///
81    /// # Returns
82    /// A [`NemoRelayContextState`] with empty registries, no subscribers, and no
83    /// extensions.
84    pub fn new() -> Self {
85        Self {
86            tool_sanitize_request_guardrails: SortedRegistry::new(),
87            tool_sanitize_response_guardrails: SortedRegistry::new(),
88            tool_conditional_execution_guardrails: SortedRegistry::new(),
89            tool_request_intercepts: SortedRegistry::new(),
90            tool_execution_intercepts: SortedRegistry::new(),
91            llm_sanitize_request_guardrails: SortedRegistry::new(),
92            llm_sanitize_response_guardrails: SortedRegistry::new(),
93            llm_conditional_execution_guardrails: SortedRegistry::new(),
94            llm_request_intercepts: SortedRegistry::new(),
95            llm_execution_intercepts: SortedRegistry::new(),
96            llm_stream_execution_intercepts: SortedRegistry::new(),
97            event_subscribers: HashMap::new(),
98            extensions: HashMap::new(),
99        }
100    }
101
102    /// Store an arbitrary runtime extension under `key`.
103    ///
104    /// Extensions let bindings or integrations attach shared state to the
105    /// process-global runtime without adding new first-class fields.
106    ///
107    /// # Parameters
108    /// - `key`: Stable identifier for the extension slot.
109    /// - `value`: Typed extension value to store.
110    pub fn set_extension<T: Any + Send + Sync>(&mut self, key: impl Into<String>, value: T) {
111        self.extensions.insert(key.into(), Box::new(value));
112    }
113
114    /// Borrow a typed runtime extension by key.
115    ///
116    /// # Parameters
117    /// - `key`: Extension slot name.
118    ///
119    /// # Returns
120    /// `Some(&T)` when an extension exists under `key` with the requested type
121    /// and `None` otherwise.
122    pub fn get_extension<T: Any + Send + Sync>(&self, key: &str) -> Option<&T> {
123        self.extensions
124            .get(key)
125            .and_then(|value| value.downcast_ref::<T>())
126    }
127
128    /// Mutably borrow a typed runtime extension by key.
129    ///
130    /// # Parameters
131    /// - `key`: Extension slot name.
132    ///
133    /// # Returns
134    /// `Some(&mut T)` when an extension exists under `key` with the requested
135    /// type and `None` otherwise.
136    pub fn get_extension_mut<T: Any + Send + Sync>(&mut self, key: &str) -> Option<&mut T> {
137        self.extensions
138            .get_mut(key)
139            .and_then(|value| value.downcast_mut::<T>())
140    }
141
142    /// Remove a runtime extension by key.
143    ///
144    /// # Parameters
145    /// - `key`: Extension slot name.
146    ///
147    /// # Returns
148    /// `true` when an extension was removed and `false` when no extension was
149    /// stored under `key`.
150    pub fn remove_extension(&mut self, key: &str) -> bool {
151        self.extensions.remove(key).is_some()
152    }
153
154    /// Combine global and scope-local subscribers into one delivery list.
155    ///
156    /// # Parameters
157    /// - `scope_local_subscribers`: Subscribers collected from the active scope
158    ///   stack.
159    ///
160    /// # Returns
161    /// A vector containing all global subscribers followed by the provided
162    /// scope-local subscribers.
163    pub(crate) fn collect_event_subscribers(
164        &self,
165        scope_local_subscribers: &[EventSubscriberFn],
166    ) -> Vec<EventSubscriberFn> {
167        let mut subscribers =
168            Vec::with_capacity(self.event_subscribers.len() + scope_local_subscribers.len());
169        subscribers.extend(self.event_subscribers.values().cloned());
170        subscribers.extend(scope_local_subscribers.iter().cloned());
171        subscribers
172    }
173
174    /// Deliver an event to every subscriber in order.
175    ///
176    /// # Parameters
177    /// - `event`: Fully constructed lifecycle event to deliver.
178    /// - `subscribers`: Subscribers that should observe the event.
179    pub(crate) fn emit_event(event: &Event, subscribers: &[EventSubscriberFn]) {
180        subscriber_dispatcher::dispatch_event(event, subscribers);
181    }
182
183    /// Build a standalone mark event.
184    ///
185    /// # Parameters
186    /// - `params`: A pre-built [`MarkEvent`] to wrap in an [`Event`].
187    ///
188    /// # Returns
189    /// A mark [`Event`] containing the provided [`MarkEvent`].
190    pub fn create_event(&self, params: MarkEvent) -> Event {
191        Event::Mark(params)
192    }
193
194    /// Create a new scope handle.
195    ///
196    /// # Parameters
197    /// - `name`: Human-readable scope name.
198    /// - `parent_uuid`: Optional parent scope UUID.
199    /// - `scope_type`: Semantic category of the scope.
200    /// - `attributes`: Scope attribute bitflags.
201    /// - `data`: Optional application payload stored on the handle.
202    /// - `metadata`: Optional metadata stored on the handle.
203    /// - `timestamp`: Optional handle start time. When omitted, the current
204    ///   UTC time is used.
205    ///
206    /// # Returns
207    /// A new [`ScopeHandle`] with a fresh UUID.
208    pub fn create_scope_handle(&self, params: CreateScopeHandleParams<'_>) -> ScopeHandle {
209        ScopeHandle::builder()
210            .name(params.name)
211            .scope_type(params.scope_type)
212            .started_at(params.timestamp.unwrap_or_else(Utc::now))
213            .attributes(params.attributes)
214            .parent_uuid_opt(params.parent_uuid)
215            .data_opt(params.data)
216            .metadata_opt(params.metadata)
217            .build()
218    }
219
220    /// Build a scope-start event from a handle.
221    ///
222    /// # Parameters
223    /// - `handle`: Scope handle to serialize into an event.
224    /// - `data`: Optional semantic input payload exported on the start event.
225    ///
226    /// # Returns
227    /// A scope-start [`Event`] derived from the provided handle.
228    pub fn build_scope_start_event(&self, handle: &ScopeHandle, data: Option<Json>) -> Event {
229        Event::Scope(ScopeEvent::new(
230            BaseEvent::builder()
231                .parent_uuid_opt(handle.parent_uuid)
232                .uuid(handle.uuid)
233                .timestamp(handle.started_at)
234                .name(handle.name.as_str())
235                .data_opt(data)
236                .metadata_opt(handle.metadata.clone())
237                .build(),
238            ScopeCategory::Start,
239            scope_attributes_to_strings(handle.attributes),
240            EventCategory::from(handle.scope_type),
241            None,
242        ))
243    }
244
245    /// Build a scope-end event from a handle.
246    ///
247    /// # Parameters
248    /// - `handle`: Scope handle to serialize into an event.
249    /// - `data`: Optional data payload returned from the scope.
250    ///
251    /// # Returns
252    /// A scope-end [`Event`] derived from the provided handle.
253    pub fn end_scope_handle(&self, handle: &ScopeHandle, data: Option<Json>) -> Event {
254        self.build_scope_end_event(
255            EndScopeHandleParams::builder()
256                .handle(handle)
257                .data_opt(data)
258                .build(),
259        )
260    }
261
262    /// Build a scope-end event from builder parameters.
263    ///
264    /// # Parameters
265    /// - `params`: Scope end-event builder parameters.
266    ///
267    /// # Returns
268    /// A scope-end [`Event`] derived from the provided parameters.
269    pub fn build_scope_end_event(&self, params: EndScopeHandleParams<'_>) -> Event {
270        let handle = params.handle;
271        Event::Scope(ScopeEvent::new(
272            BaseEvent::builder()
273                .parent_uuid_opt(handle.parent_uuid)
274                .uuid(handle.uuid)
275                .timestamp(
276                    params
277                        .timestamp
278                        .unwrap_or_else(|| end_timestamp_after(handle.started_at)),
279                )
280                .name(handle.name.as_str())
281                .data_opt(params.data)
282                .metadata_opt(handle.metadata.clone())
283                .build(),
284            ScopeCategory::End,
285            scope_attributes_to_strings(handle.attributes),
286            EventCategory::from(handle.scope_type),
287            None,
288        ))
289    }
290
291    /// Create a new tool handle.
292    ///
293    /// # Parameters
294    /// - `name`: Tool name recorded on emitted events.
295    /// - `parent_uuid`: Optional parent scope UUID.
296    /// - `attributes`: Tool attribute bitflags.
297    /// - `data`: Optional application payload stored on the handle.
298    /// - `metadata`: Optional metadata stored on the handle.
299    /// - `tool_call_id`: Optional provider-specific correlation identifier.
300    /// - `timestamp`: Optional handle start time. When omitted, the current
301    ///   UTC time is used.
302    ///
303    /// # Returns
304    /// A new [`ToolHandle`] with a fresh UUID.
305    pub fn create_tool_handle(&self, params: CreateToolHandleParams<'_>) -> ToolHandle {
306        ToolHandle::builder()
307            .name(params.name)
308            .started_at(params.timestamp.unwrap_or_else(Utc::now))
309            .attributes(params.attributes)
310            .parent_uuid_opt(params.parent_uuid)
311            .data_opt(params.data)
312            .metadata_opt(params.metadata)
313            .tool_call_id_opt(params.tool_call_id)
314            .build()
315    }
316
317    /// Build a tool-start event from a handle.
318    ///
319    /// # Parameters
320    /// - `handle`: Tool handle to serialize into an event.
321    /// - `data`: Optional tool input payload.
322    ///
323    /// # Returns
324    /// A tool-start [`Event`] derived from the provided handle.
325    pub fn build_tool_start_event(&self, handle: &ToolHandle, data: Option<Json>) -> Event {
326        Event::Scope(ScopeEvent::new(
327            BaseEvent::builder()
328                .parent_uuid_opt(handle.parent_uuid)
329                .uuid(handle.uuid)
330                .timestamp(handle.started_at)
331                .name(handle.name.as_str())
332                .data_opt(data)
333                .metadata_opt(handle.metadata.clone())
334                .build(),
335            ScopeCategory::Start,
336            tool_attributes_to_strings(handle.attributes),
337            EventCategory::tool(),
338            Some(
339                CategoryProfile::builder()
340                    .tool_call_id_opt(handle.tool_call_id.clone())
341                    .build(),
342            ),
343        ))
344    }
345
346    /// Build a tool-end event from a handle and optional overrides.
347    ///
348    /// # Parameters
349    /// - `handle`: Tool handle to serialize into an event.
350    /// - `data`: Optional end-event data payload.
351    /// - `metadata`: Optional metadata payload merged over `handle.metadata`.
352    ///
353    /// # Returns
354    /// A tool-end [`Event`] derived from the provided handle.
355    pub fn end_tool_handle(
356        &self,
357        handle: &ToolHandle,
358        data: Option<Json>,
359        metadata: Option<Json>,
360    ) -> Event {
361        self.build_tool_end_event(
362            EndToolHandleParams::builder()
363                .handle(handle)
364                .data_opt(data)
365                .metadata_opt(metadata)
366                .build(),
367        )
368    }
369
370    /// Build a tool-end event from builder parameters.
371    ///
372    /// The `metadata` payload is merged over the metadata already stored on
373    /// the handle.
374    ///
375    /// # Parameters
376    /// - `params`: Tool end-event builder parameters.
377    ///
378    /// # Returns
379    /// A tool-end [`Event`] derived from the provided parameters.
380    pub fn build_tool_end_event(&self, params: EndToolHandleParams<'_>) -> Event {
381        let handle = params.handle;
382        Event::Scope(ScopeEvent::new(
383            BaseEvent::builder()
384                .parent_uuid_opt(handle.parent_uuid)
385                .uuid(handle.uuid)
386                .timestamp(
387                    params
388                        .timestamp
389                        .unwrap_or_else(|| end_timestamp_after(handle.started_at)),
390                )
391                .name(handle.name.as_str())
392                .data_opt(params.data)
393                .metadata_opt(merge_json(handle.metadata.clone(), params.metadata))
394                .build(),
395            ScopeCategory::End,
396            tool_attributes_to_strings(handle.attributes),
397            EventCategory::tool(),
398            Some(
399                CategoryProfile::builder()
400                    .tool_call_id_opt(handle.tool_call_id.clone())
401                    .build(),
402            ),
403        ))
404    }
405
406    /// Create a new LLM handle.
407    ///
408    /// # Parameters
409    /// - `name`: Logical provider or model family name.
410    /// - `parent_uuid`: Optional parent scope UUID.
411    /// - `attributes`: LLM attribute bitflags.
412    /// - `data`: Optional application payload stored on the handle.
413    /// - `metadata`: Optional metadata stored on the handle.
414    /// - `model_name`: Optional normalized model name stored on the handle.
415    /// - `timestamp`: Optional handle start time. When omitted, the current
416    ///   UTC time is used.
417    ///
418    /// # Returns
419    /// A new [`LlmHandle`] with a fresh UUID.
420    pub fn create_llm_handle(&self, params: CreateLlmHandleParams<'_>) -> LlmHandle {
421        LlmHandle::builder()
422            .name(params.name)
423            .started_at(params.timestamp.unwrap_or_else(Utc::now))
424            .attributes(params.attributes)
425            .parent_uuid_opt(params.parent_uuid)
426            .data_opt(params.data)
427            .metadata_opt(params.metadata)
428            .model_name_opt(params.model_name)
429            .build()
430    }
431
432    /// Build an LLM-start event from a handle.
433    ///
434    /// # Parameters
435    /// - `handle`: LLM handle to serialize into an event.
436    /// - `data`: Sanitized LLM request payload.
437    /// - `annotated_request`: Optional normalized request annotation.
438    ///
439    /// # Returns
440    /// An LLM-start [`Event`] derived from the provided handle.
441    pub fn build_llm_start_event(
442        &self,
443        handle: &LlmHandle,
444        data: Option<Json>,
445        annotated_request: Option<Arc<AnnotatedLlmRequest>>,
446    ) -> Event {
447        Event::Scope(ScopeEvent::new(
448            BaseEvent::builder()
449                .parent_uuid_opt(handle.parent_uuid)
450                .uuid(handle.uuid)
451                .timestamp(handle.started_at)
452                .name(handle.name.as_str())
453                .data_opt(data)
454                .metadata_opt(handle.metadata.clone())
455                .build(),
456            ScopeCategory::Start,
457            llm_attributes_to_strings(handle.attributes),
458            EventCategory::llm(),
459            Some(
460                CategoryProfile::builder()
461                    .model_name_opt(handle.model_name.clone())
462                    .annotated_request_opt(annotated_request)
463                    .build(),
464            ),
465        ))
466    }
467
468    /// Build an LLM-end event from a handle and optional overrides.
469    ///
470    /// # Parameters
471    /// - `handle`: LLM handle to serialize into an event.
472    /// - `data`: Sanitized LLM response payload.
473    /// - `metadata`: Optional metadata payload merged over `handle.metadata`.
474    /// - `annotated_response`: Optional normalized response annotation.
475    ///
476    /// # Returns
477    /// An LLM-end [`Event`] derived from the provided handle.
478    pub fn end_llm_handle(
479        &self,
480        handle: &LlmHandle,
481        data: Option<Json>,
482        metadata: Option<Json>,
483        annotated_response: Option<Arc<AnnotatedLlmResponse>>,
484    ) -> Event {
485        self.build_llm_end_event(
486            EndLlmHandleParams::builder()
487                .handle(handle)
488                .data_opt(data)
489                .metadata_opt(metadata)
490                .annotated_response_opt(annotated_response)
491                .build(),
492        )
493    }
494
495    /// Build an LLM-end event from builder parameters.
496    ///
497    /// The `metadata` payload is merged over the metadata already stored on
498    /// the handle.
499    ///
500    /// # Parameters
501    /// - `params`: LLM end-event builder parameters.
502    ///
503    /// # Returns
504    /// An LLM-end [`Event`] derived from the provided parameters.
505    pub fn build_llm_end_event(&self, params: EndLlmHandleParams<'_>) -> Event {
506        let handle = params.handle;
507        Event::Scope(ScopeEvent::new(
508            BaseEvent::builder()
509                .parent_uuid_opt(handle.parent_uuid)
510                .uuid(handle.uuid)
511                .timestamp(
512                    params
513                        .timestamp
514                        .unwrap_or_else(|| end_timestamp_after(handle.started_at)),
515                )
516                .name(handle.name.as_str())
517                .data_opt(params.data)
518                .metadata_opt(merge_json(handle.metadata.clone(), params.metadata))
519                .build(),
520            ScopeCategory::End,
521            llm_attributes_to_strings(handle.attributes),
522            EventCategory::llm(),
523            Some(
524                CategoryProfile::builder()
525                    .model_name_opt(handle.model_name.clone())
526                    .annotated_response_opt(params.annotated_response)
527                    .build(),
528            ),
529        ))
530    }
531
532    fn emit_guardrail_scope_start(
533        name: &str,
534        parent_uuid: Option<Uuid>,
535        metadata: Option<Json>,
536        input: Json,
537        subscribers: &[EventSubscriberFn],
538    ) -> ScopeHandle {
539        let handle = ScopeHandle::builder()
540            .name(name)
541            .scope_type(ScopeType::Guardrail)
542            .parent_uuid_opt(parent_uuid)
543            .metadata_opt(metadata)
544            .build();
545        let event = Event::Scope(ScopeEvent::new(
546            BaseEvent::builder()
547                .parent_uuid_opt(handle.parent_uuid)
548                .uuid(handle.uuid)
549                .timestamp(handle.started_at)
550                .name(handle.name.as_str())
551                .data(input)
552                .metadata_opt(handle.metadata.clone())
553                .build(),
554            ScopeCategory::Start,
555            scope_attributes_to_strings(handle.attributes),
556            EventCategory::from(handle.scope_type),
557            None,
558        ));
559        Self::emit_event(&event, subscribers);
560        handle
561    }
562
563    fn emit_guardrail_scope_end(
564        handle: &ScopeHandle,
565        output: Json,
566        subscribers: &[EventSubscriberFn],
567    ) {
568        let event = Event::Scope(ScopeEvent::new(
569            BaseEvent::builder()
570                .parent_uuid_opt(handle.parent_uuid)
571                .uuid(handle.uuid)
572                .timestamp(end_timestamp_after(handle.started_at))
573                .name(handle.name.as_str())
574                .data(output)
575                .metadata_opt(handle.metadata.clone())
576                .build(),
577            ScopeCategory::End,
578            scope_attributes_to_strings(handle.attributes),
579            EventCategory::from(handle.scope_type),
580            None,
581        ));
582        Self::emit_event(&event, subscribers);
583    }
584
585    /// Run tool request sanitizers across global and scope-local registries.
586    ///
587    /// # Parameters
588    /// - `name`: Tool name associated with the request.
589    /// - `args`: Raw tool arguments to sanitize for observability.
590    /// - `scope_locals`: Scope-local sanitizer registries collected from the
591    ///   active scope stack.
592    ///
593    /// # Returns
594    /// The sanitized JSON payload after every matching guardrail has run.
595    pub(crate) fn tool_sanitize_request_chain(
596        &self,
597        name: &str,
598        args: Json,
599        scope_locals: &[&SortedRegistry<Guardrail<ToolSanitizeFn>>],
600    ) -> Json {
601        let entries = merge_guardrail_entries(&self.tool_sanitize_request_guardrails, scope_locals);
602        let mut value = args;
603        for entry in entries {
604            value = (entry.payload)(name, value);
605        }
606        value
607    }
608
609    /// Run tool response sanitizers across global and scope-local registries.
610    ///
611    /// # Parameters
612    /// - `name`: Tool name associated with the response.
613    /// - `result`: Raw tool result to sanitize for observability.
614    /// - `scope_locals`: Scope-local sanitizer registries collected from the
615    ///   active scope stack.
616    ///
617    /// # Returns
618    /// The sanitized JSON payload after every matching guardrail has run.
619    pub(crate) fn tool_sanitize_response_chain(
620        &self,
621        name: &str,
622        result: Json,
623        scope_locals: &[&SortedRegistry<Guardrail<ToolSanitizeFn>>],
624    ) -> Json {
625        let entries =
626            merge_guardrail_entries(&self.tool_sanitize_response_guardrails, scope_locals);
627        let mut value = result;
628        for entry in entries {
629            value = (entry.payload)(name, value);
630        }
631        value
632    }
633
634    /// Snapshot tool conditional-execution guardrails in priority order.
635    ///
636    /// # Parameters
637    /// - `scope_locals`: Scope-local conditional guardrail registries collected
638    ///   from the active scope stack.
639    ///
640    /// # Returns
641    /// Named guardrail snapshots that can be evaluated after registry locks
642    /// are released.
643    pub(crate) fn tool_conditional_execution_entries(
644        &self,
645        scope_locals: &[&SortedRegistry<Guardrail<ToolConditionalFn>>],
646    ) -> Vec<Guardrail<ToolConditionalFn>> {
647        merge_guardrail_entries(&self.tool_conditional_execution_guardrails, scope_locals)
648            .into_iter()
649            .cloned()
650            .collect()
651    }
652
653    /// Evaluate a snapshot of tool conditional-execution guardrails in priority order.
654    ///
655    /// This function emits guardrail scope start/end events while evaluating
656    /// the provided entries. Callers should pass entries snapped from the
657    /// global and scope-local registries so subscriber callbacks run without
658    /// registry locks held. If `entries` is empty, no guardrail scopes are
659    /// emitted. Guardrail start events identify the guardrail and target but
660    /// intentionally omit raw tool arguments from their event data.
661    ///
662    /// # Parameters
663    /// - `name`: Tool name associated with the request.
664    /// - `args`: Tool arguments to validate.
665    /// - `entries`: Borrowed conditional guardrail snapshots to evaluate.
666    /// - `subscribers`: Event subscribers that should observe guardrail scope
667    ///   start/end events.
668    /// - `parent_uuid`: Optional parent scope UUID for emitted guardrail
669    ///   scopes.
670    /// - `metadata`: Optional metadata attached to emitted guardrail scopes.
671    ///
672    /// # Returns
673    /// A [`Result`](crate::error::Result) containing `Ok(None)` when execution
674    /// is allowed or `Ok(Some(reason))` when a guardrail rejects the call.
675    ///
676    /// # Errors
677    /// Propagates any error returned by a guardrail callback after emitting the
678    /// corresponding guardrail scope end event.
679    pub(crate) fn tool_conditional_execution_snapshot_chain(
680        name: &str,
681        args: &Json,
682        entries: &[Guardrail<ToolConditionalFn>],
683        subscribers: &[EventSubscriberFn],
684        parent_uuid: Option<Uuid>,
685        metadata: Option<Json>,
686    ) -> crate::error::Result<Option<String>> {
687        for entry in entries {
688            let handle = Self::emit_guardrail_scope_start(
689                &entry.name,
690                parent_uuid,
691                metadata.clone(),
692                json!({
693                    "kind": "tool_conditional_execution",
694                    "target_name": name,
695                }),
696                subscribers,
697            );
698            let result = (entry.payload)(name, args);
699            let output = match &result {
700                Ok(Some(reason)) => json!({
701                    "allowed": false,
702                    "rejected": true,
703                    "rejection_reason": reason,
704                }),
705                Ok(None) => json!({
706                    "allowed": true,
707                    "rejected": false,
708                }),
709                Err(error) => json!({
710                    "allowed": false,
711                    "error": error.to_string(),
712                }),
713            };
714            Self::emit_guardrail_scope_end(&handle, output, subscribers);
715            if let Some(error) = result? {
716                return Ok(Some(error));
717            }
718        }
719        Ok(None)
720    }
721
722    /// Run tool request intercepts in priority order.
723    ///
724    /// # Parameters
725    /// - `name`: Tool name associated with the request.
726    /// - `args`: Tool arguments to pass through the intercept chain.
727    /// - `scope_locals`: Scope-local request intercept registries collected
728    ///   from the active scope stack.
729    ///
730    /// # Returns
731    /// A [`Result`] containing the final JSON argument payload.
732    ///
733    /// # Errors
734    /// Propagates any error returned by an intercept callback.
735    ///
736    /// # Notes
737    /// If an intercept entry has `break_chain` enabled, later intercepts are
738    /// skipped after that entry runs.
739    pub(crate) fn tool_request_intercepts_chain(
740        &self,
741        name: &str,
742        args: Json,
743        scope_locals: &[&SortedRegistry<Intercept<ToolInterceptFn>>],
744    ) -> crate::error::Result<Json> {
745        let entries = merge_intercept_entries(&self.tool_request_intercepts, scope_locals);
746        let mut value = args;
747        for entry in entries {
748            value = (entry.payload.callable)(name, value)?;
749            if entry.payload.break_chain {
750                break;
751            }
752        }
753        Ok(value)
754    }
755
756    /// Build the composed tool execution continuation chain.
757    ///
758    /// # Parameters
759    /// - `name`: Tool name passed into each execution intercept.
760    /// - `default_fn`: Base tool callback that should run after all intercepts.
761    /// - `scope_locals`: Scope-local execution intercept registries collected
762    ///   from the active scope stack.
763    ///
764    /// # Returns
765    /// A composed [`ToolExecutionNextFn`] that wraps `default_fn` in every
766    /// matching execution intercept.
767    pub(crate) fn tool_build_execution_chain(
768        &self,
769        name: &str,
770        default_fn: ToolExecutionNextFn,
771        scope_locals: &[&SortedRegistry<ExecutionIntercept<ToolExecutionFn>>],
772    ) -> ToolExecutionNextFn {
773        let matching =
774            merge_execution_intercept_callables(&self.tool_execution_intercepts, scope_locals);
775        let mut next = default_fn;
776        let name = name.to_string();
777        for (callable, _) in matching.into_iter().rev() {
778            let current_next = next.clone();
779            let current_name = name.clone();
780            next = Arc::new(move |args| callable(&current_name, args, current_next.clone()));
781        }
782        next
783    }
784
785    /// Run LLM request sanitizers across global and scope-local registries.
786    ///
787    /// # Parameters
788    /// - `request`: Raw LLM request to sanitize for observability.
789    /// - `scope_locals`: Scope-local sanitizer registries collected from the
790    ///   active scope stack.
791    ///
792    /// # Returns
793    /// The sanitized [`LlmRequest`] after every matching guardrail has run.
794    pub(crate) fn llm_sanitize_request_chain(
795        &self,
796        request: LlmRequest,
797        scope_locals: &[&SortedRegistry<Guardrail<LlmSanitizeRequestFn>>],
798    ) -> LlmRequest {
799        let entries = merge_guardrail_entries(&self.llm_sanitize_request_guardrails, scope_locals);
800        let mut value = request;
801        for entry in entries {
802            value = (entry.payload)(value);
803        }
804        value
805    }
806
807    /// Run LLM response sanitizers across global and scope-local registries.
808    ///
809    /// # Parameters
810    /// - `response`: Raw response payload to sanitize for observability.
811    /// - `scope_locals`: Scope-local sanitizer registries collected from the
812    ///   active scope stack.
813    ///
814    /// # Returns
815    /// The sanitized response payload after every matching guardrail has run.
816    pub(crate) fn llm_sanitize_response_chain(
817        &self,
818        response: Json,
819        scope_locals: &[&SortedRegistry<Guardrail<LlmSanitizeResponseFn>>],
820    ) -> Json {
821        let entries = merge_guardrail_entries(&self.llm_sanitize_response_guardrails, scope_locals);
822        let mut value = response;
823        for entry in entries {
824            value = (entry.payload)(value);
825        }
826        value
827    }
828
829    /// Snapshot LLM conditional-execution guardrails in priority order.
830    ///
831    /// # Parameters
832    /// - `scope_locals`: Scope-local conditional guardrail registries collected
833    ///   from the active scope stack.
834    ///
835    /// # Returns
836    /// Named guardrail snapshots that can be evaluated after registry locks
837    /// are released.
838    pub(crate) fn llm_conditional_execution_entries(
839        &self,
840        scope_locals: &[&SortedRegistry<Guardrail<LlmConditionalFn>>],
841    ) -> Vec<Guardrail<LlmConditionalFn>> {
842        merge_guardrail_entries(&self.llm_conditional_execution_guardrails, scope_locals)
843            .into_iter()
844            .cloned()
845            .collect()
846    }
847
848    /// Evaluate a snapshot of LLM conditional-execution guardrails in priority order.
849    ///
850    /// This function emits guardrail scope start/end events while evaluating
851    /// the provided entries. Callers should pass entries snapped from the
852    /// global and scope-local registries so subscriber callbacks run without
853    /// registry locks held. If `entries` is empty, no guardrail scopes are
854    /// emitted. Guardrail start events identify the guardrail but intentionally
855    /// omit raw LLM requests from their event data.
856    ///
857    /// # Parameters
858    /// - `request`: LLM request to validate.
859    /// - `entries`: Borrowed conditional guardrail snapshots to evaluate.
860    /// - `subscribers`: Event subscribers that should observe guardrail scope
861    ///   start/end events.
862    /// - `parent_uuid`: Optional parent scope UUID for emitted guardrail
863    ///   scopes.
864    /// - `metadata`: Optional metadata attached to emitted guardrail scopes.
865    ///
866    /// # Returns
867    /// A [`Result`](crate::error::Result) containing `Ok(None)` when execution
868    /// is allowed or `Ok(Some(reason))` when a guardrail rejects the call.
869    ///
870    /// # Errors
871    /// Propagates any error returned by a guardrail callback after emitting the
872    /// corresponding guardrail scope end event.
873    pub(crate) fn llm_conditional_execution_snapshot_chain(
874        request: &LlmRequest,
875        entries: &[Guardrail<LlmConditionalFn>],
876        subscribers: &[EventSubscriberFn],
877        parent_uuid: Option<Uuid>,
878        metadata: Option<Json>,
879    ) -> crate::error::Result<Option<String>> {
880        for entry in entries {
881            let handle = Self::emit_guardrail_scope_start(
882                &entry.name,
883                parent_uuid,
884                metadata.clone(),
885                json!({
886                    "kind": "llm_conditional_execution",
887                }),
888                subscribers,
889            );
890            let result = (entry.payload)(request);
891            let output = match &result {
892                Ok(Some(reason)) => json!({
893                    "allowed": false,
894                    "rejected": true,
895                    "rejection_reason": reason,
896                }),
897                Ok(None) => json!({
898                    "allowed": true,
899                    "rejected": false,
900                }),
901                Err(error) => json!({
902                    "allowed": false,
903                    "error": error.to_string(),
904                }),
905            };
906            Self::emit_guardrail_scope_end(&handle, output, subscribers);
907            if let Some(error) = result? {
908                return Ok(Some(error));
909            }
910        }
911        Ok(None)
912    }
913
914    /// Run LLM request intercepts in priority order.
915    ///
916    /// # Parameters
917    /// - `name`: Logical provider or model family name.
918    /// - `request`: LLM request to pass through the intercept chain.
919    /// - `annotated`: Optional normalized request annotation to carry through
920    ///   the chain.
921    /// - `scope_locals`: Scope-local request intercept registries collected
922    ///   from the active scope stack.
923    ///
924    /// # Returns
925    /// A [`Result`] containing the final request and annotation pair.
926    ///
927    /// # Errors
928    /// Propagates any error returned by an intercept callback.
929    ///
930    /// # Notes
931    /// If an intercept entry has `break_chain` enabled, later intercepts are
932    /// skipped after that entry runs.
933    pub(crate) fn llm_request_intercepts_chain(
934        &self,
935        name: &str,
936        request: LlmRequest,
937        annotated: Option<AnnotatedLlmRequest>,
938        scope_locals: &[&SortedRegistry<Intercept<LlmRequestInterceptFn>>],
939    ) -> crate::error::Result<(LlmRequest, Option<AnnotatedLlmRequest>)> {
940        let entries = merge_intercept_entries(&self.llm_request_intercepts, scope_locals);
941        let mut request_value = request;
942        let mut annotated_value = annotated;
943        for entry in entries {
944            let (new_request, new_annotated) =
945                (entry.payload.callable)(name, request_value, annotated_value)?;
946            request_value = new_request;
947            annotated_value = new_annotated;
948            if entry.payload.break_chain {
949                break;
950            }
951        }
952        Ok((request_value, annotated_value))
953    }
954
955    /// Build the composed non-streaming LLM execution continuation chain.
956    ///
957    /// # Parameters
958    /// - `name`: Logical provider or model family name passed into each
959    ///   execution intercept.
960    /// - `default_fn`: Base provider callback that should run after all
961    ///   intercepts.
962    /// - `scope_locals`: Scope-local execution intercept registries collected
963    ///   from the active scope stack.
964    ///
965    /// # Returns
966    /// A composed [`LlmExecutionNextFn`] that wraps `default_fn` in every
967    /// matching execution intercept.
968    pub(crate) fn llm_build_execution_chain(
969        &self,
970        name: &str,
971        default_fn: LlmExecutionNextFn,
972        scope_locals: &[&SortedRegistry<ExecutionIntercept<LlmExecutionFn>>],
973    ) -> LlmExecutionNextFn {
974        let matching =
975            merge_execution_intercept_callables(&self.llm_execution_intercepts, scope_locals);
976        let mut next = default_fn;
977        let name = name.to_string();
978        for (callable, _) in matching.into_iter().rev() {
979            let current_next = next.clone();
980            let current_name = name.clone();
981            next = Arc::new(move |request| callable(&current_name, request, current_next.clone()));
982        }
983        next
984    }
985
986    /// Build the composed streaming LLM execution continuation chain.
987    ///
988    /// # Parameters
989    /// - `name`: Logical provider or model family name passed into each
990    ///   execution intercept.
991    /// - `default_fn`: Base stream-producing callback that should run after all
992    ///   intercepts.
993    /// - `scope_locals`: Scope-local execution intercept registries collected
994    ///   from the active scope stack.
995    ///
996    /// # Returns
997    /// A composed [`LlmStreamExecutionNextFn`] that wraps `default_fn` in every
998    /// matching execution intercept.
999    pub(crate) fn llm_stream_build_execution_chain(
1000        &self,
1001        name: &str,
1002        default_fn: LlmStreamExecutionNextFn,
1003        scope_locals: LlmStreamExecutionRegistryRefs<'_>,
1004    ) -> LlmStreamExecutionNextFn {
1005        let matching = merge_execution_intercept_callables(
1006            &self.llm_stream_execution_intercepts,
1007            scope_locals,
1008        );
1009        let mut next = default_fn;
1010        let name = name.to_string();
1011        for (callable, _) in matching.into_iter().rev() {
1012            let current_next = next.clone();
1013            let current_name = name.clone();
1014            next = Arc::new(move |request| callable(&current_name, request, current_next.clone()));
1015        }
1016        next
1017    }
1018}
1019
1020fn end_timestamp_after(started_at: chrono::DateTime<Utc>) -> chrono::DateTime<Utc> {
1021    let now = Utc::now();
1022    if now > started_at {
1023        now
1024    } else {
1025        started_at + Duration::microseconds(1)
1026    }
1027}
1028
1029impl Default for NemoRelayContextState {
1030    fn default() -> Self {
1031        Self::new()
1032    }
1033}