1use serde_json::json;
5
6use crate::api::event::{BaseEvent, Event, MarkEvent, PendingMarkSpec};
7use crate::api::runtime::NemoRelayContextState;
8use crate::api::runtime::current_scope_stack;
9use crate::api::runtime::global_context;
10use crate::api::runtime::subscriber_dispatcher::{
11 PendingPublication, dispatch_sanitized_event, dispatch_transformed_event,
12 register_pending_publication,
13};
14use crate::api::runtime::{
15 EventSubscriberFn, ScopeStackHandle, ToolExecutionNextFn, with_active_event_uuid,
16};
17use crate::api::scope::event;
18use crate::api::scope::{EmitMarkEventParams, ScopeHandle};
19use crate::api::shared::{
20 ensure_runtime_owner, metadata_with_otel_error, metadata_with_otel_status, resolve_parent_uuid,
21 snapshot_event_sanitizers, snapshot_event_subscribers,
22};
23use crate::api::skill_load;
24use crate::error::{FlowError, Result};
25use crate::json::Json;
26use chrono::{DateTime, TimeDelta, Utc};
27use serde::{Deserialize, Serialize};
28use typed_builder::TypedBuilder;
29use uuid::Uuid;
30
31pub use nemo_relay_types::api::tool::{ToolAttributes, ToolExecutionInterceptOutcome};
32
33fn queue_sanitized_event(event: Event, subscribers: &[EventSubscriberFn]) -> bool {
34 let scope_stack = current_scope_stack();
35 queue_sanitized_event_with_scope_stack(event, subscribers, scope_stack)
36}
37
38fn queue_sanitized_event_with_scope_stack(
39 event: Event,
40 subscribers: &[EventSubscriberFn],
41 scope_stack: ScopeStackHandle,
42) -> bool {
43 let sanitizers = snapshot_event_sanitizers(&event, &scope_stack).unwrap_or_default();
44 dispatch_sanitized_event(event, sanitizers, subscribers, scope_stack)
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize, TypedBuilder)]
49#[builder(field_defaults(setter(strip_option(ignore_invalid, fallback_suffix = "_opt"))))]
50pub struct ToolHandle {
51 #[builder(default = Uuid::now_v7())]
53 pub uuid: Uuid,
54 #[builder(default = Utc::now())]
56 pub started_at: DateTime<Utc>,
57 #[builder(setter(into))]
59 pub name: String,
60 #[builder(default)]
62 pub data: Option<Json>,
63 #[builder(default)]
65 pub metadata: Option<Json>,
66 #[builder(default = ToolAttributes::empty())]
68 pub attributes: ToolAttributes,
69 #[builder(default)]
71 pub parent_uuid: Option<Uuid>,
72 #[builder(default, setter(into))]
74 pub tool_call_id: Option<String>,
75}
76
77#[derive(Debug, Clone, TypedBuilder)]
79#[builder(field_defaults(setter(strip_option(ignore_invalid, fallback_suffix = "_opt"))))]
80pub struct CreateToolHandleParams<'a> {
81 pub name: &'a str,
83 #[builder(default)]
85 pub parent_uuid: Option<uuid::Uuid>,
86 #[builder(default = ToolAttributes::empty())]
88 pub attributes: ToolAttributes,
89 #[builder(default)]
91 pub data: Option<Json>,
92 #[builder(default)]
94 pub metadata: Option<Json>,
95 #[builder(default, setter(into))]
97 pub tool_call_id: Option<String>,
98 #[builder(default)]
101 pub timestamp: Option<DateTime<Utc>>,
102}
103
104fn resolve_skill_loads(
105 name: &str,
106 args: &Json,
107 metadata: Option<&Json>,
108) -> Vec<skill_load::SkillLoad> {
109 let already_handled = metadata
110 .and_then(Json::as_object)
111 .and_then(|metadata| metadata.get(skill_load::HANDLED_METADATA_KEY))
112 .and_then(Json::as_bool)
113 .unwrap_or(false);
114 if already_handled {
115 Vec::new()
116 } else if let Some(skill_loads) = skill_load::precomputed(metadata) {
117 skill_loads
118 } else {
119 skill_load::detect(name, args)
120 }
121}
122
123#[derive(Debug, Clone, TypedBuilder)]
125#[builder(field_defaults(setter(strip_option(ignore_invalid, fallback_suffix = "_opt"))))]
126pub struct EndToolHandleParams<'a> {
127 pub handle: &'a ToolHandle,
129 #[builder(default)]
131 pub data: Option<Json>,
132 #[builder(default)]
134 pub metadata: Option<Json>,
135 #[builder(default)]
139 pub timestamp: Option<DateTime<Utc>>,
140}
141
142#[derive(TypedBuilder)]
144#[builder(field_defaults(setter(strip_option(ignore_invalid, fallback_suffix = "_opt"))))]
145pub struct ToolCallParams<'a> {
146 pub name: &'a str,
148 pub args: Json,
150 #[builder(default)]
152 pub parent: Option<&'a ScopeHandle>,
153 #[builder(default = ToolAttributes::empty())]
155 pub attributes: ToolAttributes,
156 #[builder(default)]
159 pub data: Option<Json>,
160 #[builder(default)]
162 pub metadata: Option<Json>,
163 #[builder(default, setter(into))]
165 pub tool_call_id: Option<String>,
166 #[builder(default)]
169 pub timestamp: Option<DateTime<Utc>>,
170}
171
172#[derive(TypedBuilder)]
174#[builder(field_defaults(setter(strip_option(ignore_invalid, fallback_suffix = "_opt"))))]
175pub struct ToolCallExecuteParams {
176 #[builder(setter(into))]
178 pub name: String,
179 pub args: Json,
181 pub func: ToolExecutionNextFn,
183 #[builder(default)]
185 pub parent: Option<ScopeHandle>,
186 #[builder(default = ToolAttributes::empty())]
188 pub attributes: ToolAttributes,
189 #[builder(default)]
192 pub data: Option<Json>,
193 #[builder(default)]
195 pub metadata: Option<Json>,
196}
197
198#[derive(TypedBuilder)]
200#[builder(field_defaults(setter(strip_option(ignore_invalid, fallback_suffix = "_opt"))))]
201pub struct ToolCallEndParams<'a> {
202 pub handle: &'a ToolHandle,
204 pub result: Json,
206 #[builder(default)]
209 pub data: Option<Json>,
210 #[builder(default)]
212 pub metadata: Option<Json>,
213 #[builder(default)]
217 pub timestamp: Option<DateTime<Utc>>,
218}
219
220pub fn tool_call(params: ToolCallParams<'_>) -> Result<ToolHandle> {
251 ensure_runtime_owner()?;
252 let scope_stack = current_scope_stack();
253 let (entries, subscribers) = {
254 let scope_guard = scope_stack
255 .read()
256 .map_err(|error| FlowError::Internal(error.to_string()))?;
257 let scope_locals = scope_guard.collect_scope_local_registries(|registries| {
258 ®istries.tool_sanitize_request_guardrails
259 });
260 let subscribers =
261 snapshot_event_subscribers(scope_guard.collect_scope_local_subscribers())?;
262 let context = global_context();
263 let state = context
264 .read()
265 .map_err(|error| FlowError::Internal(error.to_string()))?;
266 (
267 state.tool_sanitize_request_entries(&scope_locals),
268 subscribers,
269 )
270 };
271 let skill_loads = resolve_skill_loads(params.name, ¶ms.args, params.metadata.as_ref());
272 let raw_args = params.args;
273 let (handle, event, marks) = {
274 let context = global_context();
275 let state = context
276 .read()
277 .map_err(|error| FlowError::Internal(error.to_string()))?;
278 let handle = state.create_tool_handle(
279 CreateToolHandleParams::builder()
280 .name(params.name)
281 .parent_uuid_opt(resolve_parent_uuid(params.parent))
282 .attributes(params.attributes)
283 .data_opt(params.data)
284 .metadata_opt(params.metadata)
285 .tool_call_id_opt(params.tool_call_id)
286 .timestamp_opt(params.timestamp)
287 .build(),
288 );
289 let event = state.build_tool_start_event(&handle, None);
290 let marks = skill_loads
291 .into_iter()
292 .map(|skill_load| {
293 state.create_event(MarkEvent::new(
294 BaseEvent::builder()
295 .name("skill.load")
296 .parent_uuid(handle.uuid)
297 .timestamp(handle.started_at)
298 .data(json!({"skill_name": skill_load.name}))
299 .metadata(json!({
300 "skill_load_source": <&str>::from(skill_load.source),
301 "tool_name": handle.name,
302 }))
303 .build(),
304 None,
305 None,
306 ))
307 })
308 .collect::<Vec<_>>();
309 (handle, event, marks)
310 };
311 let tool_name = handle.name.clone();
312 let event_sanitizers = snapshot_event_sanitizers(&event, &scope_stack).unwrap_or_default();
313 dispatch_transformed_event(
314 event,
315 Box::new(move |mut event| {
316 Box::pin(async move {
317 let sanitized = NemoRelayContextState::tool_sanitize_request_snapshot_chain(
318 &tool_name, raw_args, &entries,
319 )
320 .await;
321 let mut fields = event.sanitize_fields();
322 fields.data = sanitized;
323 event.apply_sanitize_fields(fields);
324 event
325 })
326 }),
327 event_sanitizers,
328 &subscribers,
329 scope_stack.clone(),
330 );
331 for mark in marks {
332 let sanitizers = snapshot_event_sanitizers(&mark, &scope_stack).unwrap_or_default();
333 dispatch_sanitized_event(mark, sanitizers, &subscribers, scope_stack.clone());
334 }
335 Ok(handle)
336}
337
338async fn tool_call_with_subscriber_snapshot(
339 params: ToolCallParams<'_>,
340) -> Result<(ToolHandle, Vec<EventSubscriberFn>)> {
341 ensure_runtime_owner()?;
342 let parent_uuid = resolve_parent_uuid(params.parent);
343 let (entries, subscribers) = {
344 let scope_stack = current_scope_stack();
345 let scope_guard = scope_stack.read().expect("scope stack lock poisoned");
346 let scope_locals = scope_guard.collect_scope_local_registries(|registries| {
347 ®istries.tool_sanitize_request_guardrails
348 });
349 let scope_subscribers = scope_guard.collect_scope_local_subscribers();
350 let subscribers = snapshot_event_subscribers(scope_subscribers)?;
351 let context = global_context();
352 let state = context
353 .read()
354 .map_err(|error| FlowError::Internal(error.to_string()))?;
355 let entries = state.tool_sanitize_request_entries(&scope_locals);
356 (entries, subscribers)
357 };
358 let skill_loads = resolve_skill_loads(params.name, ¶ms.args, params.metadata.as_ref());
359 let sanitized_args = NemoRelayContextState::tool_sanitize_request_snapshot_chain(
360 params.name,
361 params.args,
362 &entries,
363 )
364 .await;
365 let (handle, event, marks) = {
366 let context = global_context();
367 let state = context
368 .read()
369 .map_err(|error| FlowError::Internal(error.to_string()))?;
370 let handle_params = CreateToolHandleParams::builder()
371 .name(params.name)
372 .parent_uuid_opt(parent_uuid)
373 .attributes(params.attributes)
374 .data_opt(params.data)
375 .metadata_opt(params.metadata)
376 .tool_call_id_opt(params.tool_call_id)
377 .timestamp_opt(params.timestamp)
378 .build();
379 let handle = state.create_tool_handle(handle_params);
380 let event = state.build_tool_start_event(&handle, sanitized_args);
381 let marks = skill_loads
382 .into_iter()
383 .map(|skill_load| {
384 state.create_event(MarkEvent::new(
385 BaseEvent::builder()
386 .name("skill.load")
387 .parent_uuid(handle.uuid)
388 .timestamp(handle.started_at)
389 .data(json!({"skill_name": skill_load.name}))
390 .metadata(json!({
391 "skill_load_source": <&str>::from(skill_load.source),
392 "tool_name": handle.name,
393 }))
394 .build(),
395 None,
396 None,
397 ))
398 })
399 .collect::<Vec<_>>();
400 (handle, event, marks)
401 };
402 queue_sanitized_event(event, &subscribers);
403 for mark in marks {
404 queue_sanitized_event(mark, &subscribers);
405 }
406 Ok((handle, subscribers))
407}
408
409pub fn tool_call_end(params: ToolCallEndParams<'_>) -> Result<()> {
439 ensure_runtime_owner()?;
440 let scope_stack = current_scope_stack();
441 let (entries, subscribers) = {
442 let scope_guard = scope_stack
443 .read()
444 .map_err(|error| FlowError::Internal(error.to_string()))?;
445 let scope_locals = scope_guard.collect_scope_local_registries(|registries| {
446 ®istries.tool_sanitize_response_guardrails
447 });
448 let subscribers =
449 snapshot_event_subscribers(scope_guard.collect_scope_local_subscribers())?;
450 let context = global_context();
451 let state = context
452 .read()
453 .map_err(|error| FlowError::Internal(error.to_string()))?;
454 (
455 state.tool_sanitize_response_entries(&scope_locals),
456 subscribers,
457 )
458 };
459 let result = params.result;
460 let fallback = params.data;
461 let event = {
462 let context = global_context();
463 let state = context
464 .read()
465 .map_err(|error| FlowError::Internal(error.to_string()))?;
466 state.build_tool_end_event(
467 EndToolHandleParams::builder()
468 .handle(params.handle)
469 .data(Json::Null)
470 .metadata_opt(params.metadata)
471 .timestamp_opt(params.timestamp)
472 .build(),
473 )
474 };
475 let tool_name = params.handle.name.clone();
476 let event_sanitizers = snapshot_event_sanitizers(&event, &scope_stack).unwrap_or_default();
477 dispatch_transformed_event(
478 event,
479 Box::new(move |mut event| {
480 Box::pin(async move {
481 let sanitized = NemoRelayContextState::tool_sanitize_response_snapshot_chain(
482 &tool_name, result, &entries,
483 )
484 .await;
485 let mut fields = event.sanitize_fields();
486 fields.data = sanitized.and_then(|value| {
487 if value.is_null() {
488 fallback
489 } else {
490 Some(value)
491 }
492 });
493 event.apply_sanitize_fields(fields);
494 event
495 })
496 }),
497 event_sanitizers,
498 &subscribers,
499 scope_stack,
500 );
501 Ok(())
502}
503
504async fn tool_call_end_with_pending_marks(
505 params: ToolCallEndParams<'_>,
506 pending_marks: Vec<PendingMarkSpec>,
507 lifecycle_subscribers: Option<&[EventSubscriberFn]>,
508) -> Result<()> {
509 ensure_runtime_owner()?;
510 let (entries, subscribers) = {
511 let scope_stack = current_scope_stack();
512 let scope_guard = scope_stack.read().expect("scope stack lock poisoned");
513 let scope_locals = scope_guard.collect_scope_local_registries(|registries| {
514 ®istries.tool_sanitize_response_guardrails
515 });
516 let subscribers = if lifecycle_subscribers.is_some() {
517 Vec::new()
518 } else {
519 snapshot_event_subscribers(scope_guard.collect_scope_local_subscribers())?
520 };
521 let context = global_context();
522 let state = context
523 .read()
524 .map_err(|error| FlowError::Internal(error.to_string()))?;
525 let entries = state.tool_sanitize_response_entries(&scope_locals);
526 (entries, subscribers)
527 };
528 let subscribers = lifecycle_subscribers.unwrap_or(&subscribers);
529 let sanitized_result = NemoRelayContextState::tool_sanitize_response_snapshot_chain(
530 ¶ms.handle.name,
531 params.result,
532 &entries,
533 )
534 .await;
535 let data = sanitized_result.and_then(|value| {
536 if value.is_null() {
537 params.data
538 } else {
539 Some(value)
540 }
541 });
542 let event = {
543 let context = global_context();
544 let state = context
545 .read()
546 .map_err(|error| FlowError::Internal(error.to_string()))?;
547 state.build_tool_end_event(
548 EndToolHandleParams::builder()
549 .handle(params.handle)
550 .data_opt(data)
551 .metadata_opt(params.metadata)
552 .timestamp_opt(params.timestamp)
553 .build(),
554 )
555 };
556 let marks = pending_marks
557 .into_iter()
558 .enumerate()
559 .map(|(index, mark)| {
560 let timestamp = *event.timestamp()
561 + TimeDelta::microseconds(i64::try_from(index).unwrap_or_default() + 1);
562 Event::Mark(MarkEvent::new(
563 BaseEvent::builder()
564 .name(mark.name)
565 .parent_uuid(params.handle.uuid)
566 .timestamp(timestamp)
567 .data_opt(mark.data)
568 .metadata_opt(mark.metadata)
569 .build(),
570 mark.category,
571 mark.category_profile,
572 ))
573 })
574 .collect::<Vec<_>>();
575 queue_sanitized_event(event, subscribers);
576 for mark in marks {
577 queue_sanitized_event(mark, subscribers);
578 }
579 Ok(())
580}
581
582fn emit_tool_end_without_output(
583 handle: &ToolHandle,
584 metadata: Option<Json>,
585 lifecycle_subscribers: &[EventSubscriberFn],
586 scope_stack: ScopeStackHandle,
587) -> Result<()> {
588 ensure_runtime_owner()?;
589 let event = {
590 let context = global_context();
591 let state = context
592 .read()
593 .map_err(|error| FlowError::Internal(error.to_string()))?;
594 state.end_tool_handle(handle, handle.data.clone(), metadata)
595 };
596 queue_sanitized_event_with_scope_stack(event, lifecycle_subscribers, scope_stack);
597 Ok(())
598}
599
600struct ManagedToolCompletion {
601 handle: Option<ToolHandle>,
602 metadata: Option<Json>,
603 subscribers: Vec<EventSubscriberFn>,
604 scope_stack: ScopeStackHandle,
605 pending_publication: Option<PendingPublication>,
606}
607
608impl ManagedToolCompletion {
609 fn new(
610 handle: &ToolHandle,
611 metadata: Option<Json>,
612 subscribers: &[EventSubscriberFn],
613 scope_stack: ScopeStackHandle,
614 ) -> Self {
615 Self {
616 handle: Some(handle.clone()),
617 metadata,
618 subscribers: subscribers.to_vec(),
619 scope_stack,
620 pending_publication: (!subscribers.is_empty())
621 .then(register_pending_publication)
622 .flatten(),
623 }
624 }
625
626 fn disarm(&mut self) {
627 self.handle = None;
628 drop(self.pending_publication.take());
629 }
630}
631
632impl Drop for ManagedToolCompletion {
633 fn drop(&mut self) {
634 let pending_publication = self.pending_publication.take();
635 let Some(handle) = self.handle.take() else {
636 return;
637 };
638 let metadata = metadata_with_otel_status(
639 self.metadata.take(),
640 "ERROR",
641 Some("tool execution cancelled".into()),
642 );
643 let _ = emit_tool_end_without_output(
644 &handle,
645 metadata,
646 &self.subscribers,
647 self.scope_stack.clone(),
648 );
649 drop(pending_publication);
650 }
651}
652
653pub async fn tool_call_execute(params: ToolCallExecuteParams) -> Result<Json> {
682 let ToolCallExecuteParams {
683 name,
684 args,
685 func,
686 parent,
687 attributes,
688 data,
689 metadata,
690 } = params;
691 ensure_runtime_owner()?;
692 {
693 let (entries, subscribers, parent_uuid, guardrail_metadata) = {
694 let scope_stack = current_scope_stack();
695 let scope_guard = scope_stack.read().expect("scope stack lock poisoned");
696 let scope_locals = scope_guard.collect_scope_local_registries(|registries| {
697 ®istries.tool_conditional_execution_guardrails
698 });
699 let scope_subscribers = scope_guard.collect_scope_local_subscribers();
700 let context = global_context();
701 let state = context
702 .read()
703 .map_err(|error| FlowError::Internal(error.to_string()))?;
704 let entries = state.tool_conditional_execution_entries(&scope_locals);
705 let subscribers = state.collect_event_subscribers(&scope_subscribers);
706 (
707 entries,
708 subscribers,
709 resolve_parent_uuid(parent.as_ref()),
710 metadata.clone(),
711 )
712 };
713 if let Some(error) = NemoRelayContextState::tool_conditional_execution_snapshot_chain(
714 &name,
715 &args,
716 &entries,
717 &subscribers,
718 parent_uuid,
719 guardrail_metadata,
720 )
721 .await?
722 {
723 let mut rejection_data = json!({});
724 if let Some(object) = rejection_data.as_object_mut() {
725 object.insert("rejected".into(), json!(true));
726 object.insert("rejection_reason".into(), json!(&error));
727 }
728 let _ = event(
729 EmitMarkEventParams::builder()
730 .name(&name)
731 .parent_opt(parent.as_ref())
732 .data(rejection_data)
733 .metadata_opt(metadata.clone())
734 .build(),
735 );
736 return Err(FlowError::GuardrailRejected(error));
737 }
738 }
739
740 let intercept_entries = {
741 let scope_stack = current_scope_stack();
742 let scope_guard = scope_stack.read().expect("scope stack lock poisoned");
743 let scope_locals = scope_guard
744 .collect_scope_local_registries(|registries| ®istries.tool_request_intercepts);
745 let context = global_context();
746 let state = context
747 .read()
748 .map_err(|error| FlowError::Internal(error.to_string()))?;
749 state.tool_request_intercept_entries(&scope_locals)
750 };
751 let intercepted_args = NemoRelayContextState::tool_request_intercepts_snapshot_chain(
752 &name,
753 args,
754 &intercept_entries,
755 )
756 .await?;
757
758 let (handle, lifecycle_subscribers) = tool_call_with_subscriber_snapshot(
759 ToolCallParams::builder()
760 .name(name.as_str())
761 .args(intercepted_args.clone())
762 .parent_opt(parent.as_ref())
763 .attributes(attributes)
764 .data_opt(data.clone())
765 .metadata_opt(metadata.clone())
766 .build(),
767 )
768 .await?;
769
770 let lifecycle_scope_stack = current_scope_stack();
771 let mut completion = ManagedToolCompletion::new(
772 &handle,
773 metadata.clone(),
774 &lifecycle_subscribers,
775 lifecycle_scope_stack.clone(),
776 );
777 let execution_name = name.clone();
778 let execution = with_active_event_uuid(handle.uuid, async move {
779 let execution = {
780 let scope_stack = current_scope_stack();
781 let scope_guard = scope_stack.read().expect("scope stack lock poisoned");
782 let scope_locals = scope_guard
783 .collect_scope_local_registries(|registries| ®istries.tool_execution_intercepts);
784 let context = global_context();
785 let state = context
786 .read()
787 .map_err(|error| FlowError::Internal(error.to_string()))?;
788 state.tool_build_execution_chain(&execution_name, func, &scope_locals)
789 };
790 execution(intercepted_args).await
791 })
792 .await;
793 match execution {
794 Ok(outcome) => {
795 let ToolExecutionInterceptOutcome {
796 result,
797 pending_marks,
798 } = outcome;
799 let end_metadata = metadata_with_otel_status(metadata, "OK", None);
800 tool_call_end_with_pending_marks(
801 ToolCallEndParams::builder()
802 .handle(&handle)
803 .result(result.clone())
804 .data_opt(data)
805 .metadata_opt(end_metadata)
806 .build(),
807 pending_marks,
808 Some(&lifecycle_subscribers),
809 )
810 .await?;
811 completion.disarm();
812 Ok(result)
813 }
814 Err(error) => {
815 let end_metadata = metadata_with_otel_error(metadata, &error);
816 let _ = emit_tool_end_without_output(
817 &handle,
818 end_metadata,
819 &lifecycle_subscribers,
820 lifecycle_scope_stack,
821 );
822 completion.disarm();
823 Err(error)
824 }
825 }
826}
827
828pub async fn tool_request_intercepts(name: &str, args: Json) -> Result<Json> {
846 ensure_runtime_owner()?;
847 let entries = {
848 let scope_stack = current_scope_stack();
849 let scope_guard = scope_stack.read().expect("scope stack lock poisoned");
850 let scope_locals = scope_guard
851 .collect_scope_local_registries(|registries| ®istries.tool_request_intercepts);
852 let context = global_context();
853 let state = context
854 .read()
855 .map_err(|error| FlowError::Internal(error.to_string()))?;
856 state.tool_request_intercept_entries(&scope_locals)
857 };
858 NemoRelayContextState::tool_request_intercepts_snapshot_chain(name, args, &entries).await
859}
860
861pub async fn tool_conditional_execution(name: &str, args: &Json) -> Result<()> {
883 ensure_runtime_owner()?;
884 let (entries, subscribers, parent_uuid) = {
885 let scope_stack = current_scope_stack();
886 let scope_guard = scope_stack.read().expect("scope stack lock poisoned");
887 let scope_locals = scope_guard.collect_scope_local_registries(|registries| {
888 ®istries.tool_conditional_execution_guardrails
889 });
890 let scope_subscribers = scope_guard.collect_scope_local_subscribers();
891 let context = global_context();
892 let state = context
893 .read()
894 .map_err(|error| FlowError::Internal(error.to_string()))?;
895 let entries = state.tool_conditional_execution_entries(&scope_locals);
896 let subscribers = state.collect_event_subscribers(&scope_subscribers);
897 (entries, subscribers, resolve_parent_uuid(None))
898 };
899 if let Some(error) = NemoRelayContextState::tool_conditional_execution_snapshot_chain(
900 name,
901 args,
902 &entries,
903 &subscribers,
904 parent_uuid,
905 None,
906 )
907 .await?
908 {
909 return Err(FlowError::GuardrailRejected(error));
910 }
911 Ok(())
912}
913
914#[cfg(test)]
915#[path = "../../tests/unit/tool_api_tests.rs"]
916mod tests;