1#![deny(rustdoc::broken_intra_doc_links, rustdoc::private_intra_doc_links)]
5
6mod async_sdk;
13
14pub use async_sdk::{LlmJsonAsyncStream, LlmNext, LlmStreamNext, NativeExecutorConfig, ToolNext};
15
16use std::ffi::{c_char, c_void};
17use std::marker::{PhantomData, PhantomPinned};
18use std::panic::{AssertUnwindSafe, catch_unwind};
19use std::ptr;
20use std::sync::{Arc, Mutex};
21
22pub use nemo_relay_types::Json;
23pub use nemo_relay_types::api::event::{
24 CategoryProfile, DataSchema, Event, EventCategory, EventSanitizeFields, LogSeverity,
25 METRIC_DATA_SCHEMA_NAME, METRIC_DATA_SCHEMA_VERSION, MetricEnvelope, MetricKind,
26 MetricMeasurement, MetricValueType, PendingMarkSpec, ScopeCategory,
27};
28pub use nemo_relay_types::api::llm::{LlmAttributes, LlmRequest, LlmRequestInterceptOutcome};
29pub use nemo_relay_types::api::registry::{
30 RuntimeRegistrationIdentity, RuntimeRegistrationKind, RuntimeRegistrationOwner,
31 RuntimeRegistrationOwnerKind,
32};
33pub use nemo_relay_types::api::scope::{HandleAttributes, ScopeAttributes, ScopeType};
34pub use nemo_relay_types::api::tool::{
35 TOOL_EXECUTION_INTERCEPT_OUTCOME_SCHEMA, TOOL_EXECUTION_RESULT_SCHEMA, ToolAttributes,
36 ToolExecutionInterceptOutcome, ToolExecutionResult,
37};
38pub use nemo_relay_types::codec::identity::{BuiltinLlmCodec, LlmCodecIdentity};
39pub use nemo_relay_types::codec::optimization::{
40 LlmOptimizationContribution, LlmOptimizationEvidenceQuality, LlmOptimizationKind,
41 LlmOptimizationModel, LlmOptimizationModelTransition, LlmOptimizationPayload,
42 LlmOptimizationSummary, LlmOptimizationSummaryStatus, LlmOptimizationTokenImpact,
43 LlmOptimizationTokens,
44};
45pub use nemo_relay_types::codec::request::AnnotatedLlmRequest;
46pub use nemo_relay_types::codec::response::AnnotatedLlmResponse;
47pub use nemo_relay_types::plugin::{ConfigDiagnostic, DiagnosticLevel};
48use serde::{Serialize, de::DeserializeOwned};
49use serde_json::Map;
50
51pub const NEMO_RELAY_NATIVE_ABI_VERSION: u32 = 4;
59pub const NEMO_RELAY_NATIVE_ABI_VERSION_ASYNC_MIDDLEWARE: u32 = 3;
61
62pub const NEMO_RELAY_NATIVE_ABI_VERSION_LEGACY: u32 = 2;
64
65pub struct LlmSanitizeRequestContext<'a> {
67 pub codec: LlmCodecIdentity,
69 resolved: Option<LlmSanitizeRequestCodec<'a>>,
70}
71unsafe impl Send for LlmSanitizeRequestContext<'_> {}
74
75pub struct LlmSanitizeResponseContext<'a> {
77 pub codec: LlmCodecIdentity,
79 resolved: Option<LlmSanitizeResponseCodec<'a>>,
80}
81unsafe impl Send for LlmSanitizeResponseContext<'_> {}
84
85#[repr(i32)]
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum NemoRelayStatus {
89 Ok = 0,
91 AlreadyExists = 1,
93 NotFound = 2,
95 ScopeStackEmpty = 3,
97 GuardrailRejected = 4,
99 Internal = 5,
101 NullPointer = 6,
103 InvalidJson = 7,
105 InvalidUtf8 = 8,
107 InvalidArg = 9,
109 StreamEnd = 10,
111 Backpressured = 11,
113}
114
115#[repr(C)]
117pub struct NemoRelayNativeString {
118 _private: [u8; 0],
119 _marker: PhantomData<(*mut u8, PhantomPinned)>,
120}
121
122#[repr(C)]
124pub struct NemoRelayNativeLlmRequestCodec {
125 _private: [u8; 0],
126 _marker: PhantomData<(*mut u8, PhantomPinned)>,
127}
128
129#[repr(C)]
131pub struct NemoRelayNativeLlmResponseCodec {
132 _private: [u8; 0],
133 _marker: PhantomData<(*mut u8, PhantomPinned)>,
134}
135
136#[repr(u32)]
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139pub enum NemoRelayNativeLlmCodecKind {
140 None = 0,
142 BuiltIn = 1,
144 Runtime = 2,
146 Opaque = 3,
148}
149
150#[repr(C)]
157#[derive(Debug, Clone, Copy)]
158pub struct NemoRelayNativeLlmSanitizeRequestContext {
159 pub codec_kind: NemoRelayNativeLlmCodecKind,
161 pub codec_id: *const NemoRelayNativeString,
163 pub codec: *const NemoRelayNativeLlmRequestCodec,
165}
166
167#[repr(C)]
169#[derive(Debug, Clone, Copy)]
170pub struct NemoRelayNativeLlmSanitizeResponseContext {
171 pub codec_kind: NemoRelayNativeLlmCodecKind,
173 pub codec_id: *const NemoRelayNativeString,
175 pub codec: *const NemoRelayNativeLlmResponseCodec,
177}
178
179pub struct LlmSanitizeRequestCodec<'a> {
181 async_host: NemoRelayNativeHostApiV4,
182 completion: *const NemoRelayNativeAsyncCompletion,
183 completion_release: unsafe extern "C" fn(*const NemoRelayNativeAsyncCompletion),
184 _lifetime: PhantomData<&'a NemoRelayNativeLlmRequestCodec>,
185}
186unsafe impl Send for LlmSanitizeRequestCodec<'_> {}
189unsafe impl Sync for LlmSanitizeRequestCodec<'_> {}
192
193impl Drop for LlmSanitizeRequestCodec<'_> {
194 fn drop(&mut self) {
195 unsafe { (self.completion_release)(self.completion) };
196 }
197}
198
199impl LlmSanitizeRequestCodec<'_> {
200 pub fn decode(&self, request: &LlmRequest) -> Result<AnnotatedLlmRequest> {
202 native_codec_call(&self.async_host.v3.v1, |out| unsafe {
203 let request = HostString::from_json(&self.async_host.v3.v1, request)
204 .ok_or_else(|| "failed to serialize LLM request".to_string())?;
205 let status = (self.async_host.async_completion_llm_request_codec_decode)(
206 self.completion,
207 request.as_ptr(),
208 out,
209 );
210 codec_status(&self.async_host.v3.v1, status)
211 })
212 }
213
214 pub fn encode(
216 &self,
217 annotated: &AnnotatedLlmRequest,
218 original: &LlmRequest,
219 ) -> Result<LlmRequest> {
220 native_codec_call(&self.async_host.v3.v1, |out| unsafe {
221 let annotated = HostString::from_json(&self.async_host.v3.v1, annotated)
222 .ok_or_else(|| "failed to serialize annotated request".to_string())?;
223 let original = HostString::from_json(&self.async_host.v3.v1, original)
224 .ok_or_else(|| "failed to serialize original request".to_string())?;
225 let status = (self.async_host.async_completion_llm_request_codec_encode)(
226 self.completion,
227 annotated.as_ptr(),
228 original.as_ptr(),
229 out,
230 );
231 codec_status(&self.async_host.v3.v1, status)
232 })
233 }
234}
235
236pub struct LlmSanitizeResponseCodec<'a> {
238 async_host: NemoRelayNativeHostApiV4,
239 completion: *const NemoRelayNativeAsyncCompletion,
240 completion_release: unsafe extern "C" fn(*const NemoRelayNativeAsyncCompletion),
241 _lifetime: PhantomData<&'a NemoRelayNativeLlmResponseCodec>,
242}
243unsafe impl Send for LlmSanitizeResponseCodec<'_> {}
246unsafe impl Sync for LlmSanitizeResponseCodec<'_> {}
249
250impl Drop for LlmSanitizeResponseCodec<'_> {
251 fn drop(&mut self) {
252 unsafe { (self.completion_release)(self.completion) };
253 }
254}
255
256impl LlmSanitizeResponseCodec<'_> {
257 pub fn decode(&self, response: &Json) -> Result<AnnotatedLlmResponse> {
259 native_codec_call(&self.async_host.v3.v1, |out| unsafe {
260 let response = HostString::from_json(&self.async_host.v3.v1, response)
261 .ok_or_else(|| "failed to serialize LLM response".to_string())?;
262 let status = (self.async_host.async_completion_llm_response_codec_decode)(
263 self.completion,
264 response.as_ptr(),
265 out,
266 );
267 codec_status(&self.async_host.v3.v1, status)
268 })
269 }
270}
271
272impl<'a> LlmSanitizeRequestContext<'a> {
273 #[must_use]
275 pub fn resolve_codec(&self) -> Option<&LlmSanitizeRequestCodec<'a>> {
276 self.resolved.as_ref()
277 }
278}
279
280impl<'a> LlmSanitizeResponseContext<'a> {
281 #[must_use]
283 pub fn resolve_codec(&self) -> Option<&LlmSanitizeResponseCodec<'a>> {
284 self.resolved.as_ref()
285 }
286}
287
288#[repr(C)]
290pub struct NemoRelayNativePluginContext {
291 _private: [u8; 0],
292 _marker: PhantomData<(*mut u8, PhantomPinned)>,
293}
294
295#[repr(C)]
297pub struct NemoRelayNativePluginRuntime {
298 _private: [u8; 0],
299 _marker: PhantomData<(*mut u8, PhantomPinned)>,
300}
301
302#[repr(C)]
304pub struct NemoRelayNativeScopeHandle {
305 _private: [u8; 0],
306 _marker: PhantomData<(*mut u8, PhantomPinned)>,
307}
308
309#[repr(C)]
311pub struct NemoRelayNativeScopeStack {
312 _private: [u8; 0],
313 _marker: PhantomData<(*mut u8, PhantomPinned)>,
314}
315
316#[repr(C)]
318pub struct NemoRelayNativeScopeStackBinding {
319 _private: [u8; 0],
320 _marker: PhantomData<(*mut u8, PhantomPinned)>,
321}
322
323#[repr(i32)]
325#[derive(Debug, Clone, Copy, PartialEq, Eq)]
326pub enum NemoRelayNativeScopeType {
327 Agent = 0,
329 Function = 1,
331 Tool = 2,
333 Llm = 3,
335 Retriever = 4,
337 Embedder = 5,
339 Reranker = 6,
341 Guardrail = 7,
343 Evaluator = 8,
345 Custom = 9,
347 Unknown = 10,
349}
350
351pub type NemoRelayNativeFreeFn = Option<unsafe extern "C" fn(user_data: *mut c_void)>;
353
354pub type NemoRelayNativeWithScopeStackCb =
356 unsafe extern "C" fn(user_data: *mut c_void) -> NemoRelayStatus;
357
358pub type NemoRelayNativeToolNextFn = unsafe extern "C" fn(
364 args_json: *const NemoRelayNativeString,
365 next_ctx: *mut c_void,
366 out_json: *mut *mut NemoRelayNativeString,
367) -> NemoRelayStatus;
368
369pub type NemoRelayNativeLlmNextFn = unsafe extern "C" fn(
371 request_json: *const NemoRelayNativeString,
372 next_ctx: *mut c_void,
373 out_json: *mut *mut NemoRelayNativeString,
374) -> NemoRelayStatus;
375
376pub type NemoRelayNativeLlmStreamPollFn = unsafe extern "C" fn(
382 user_data: *mut c_void,
383 out_json: *mut *mut NemoRelayNativeString,
384) -> NemoRelayStatus;
385
386pub type NemoRelayNativeLlmStreamCancelFn =
388 Option<unsafe extern "C" fn(user_data: *mut c_void) -> NemoRelayStatus>;
389
390pub type NemoRelayNativeLlmStreamDropFn = Option<unsafe extern "C" fn(user_data: *mut c_void)>;
392
393#[repr(C)]
395pub struct NemoRelayNativeLlmStreamV1 {
396 pub struct_size: usize,
398 pub user_data: *mut c_void,
400 pub next: Option<NemoRelayNativeLlmStreamPollFn>,
402 pub cancel: NemoRelayNativeLlmStreamCancelFn,
404 pub drop: NemoRelayNativeLlmStreamDropFn,
406}
407
408impl Default for NemoRelayNativeLlmStreamV1 {
409 fn default() -> Self {
410 Self {
411 struct_size: std::mem::size_of::<Self>(),
412 user_data: ptr::null_mut(),
413 next: None,
414 cancel: None,
415 drop: None,
416 }
417 }
418}
419
420pub type NemoRelayNativeLlmStreamNextFn = unsafe extern "C" fn(
422 request_json: *const NemoRelayNativeString,
423 next_ctx: *mut c_void,
424 out_stream: *mut NemoRelayNativeLlmStreamV1,
425) -> NemoRelayStatus;
426
427pub type NemoRelayNativeEventSubscriberCb = unsafe extern "C" fn(
429 user_data: *mut c_void,
430 event_json: *const NemoRelayNativeString,
431) -> NemoRelayStatus;
432
433pub type NemoRelayNativeEventSanitizeCb = unsafe extern "C" fn(
435 user_data: *mut c_void,
436 event_json: *const NemoRelayNativeString,
437 fields_json: *const NemoRelayNativeString,
438 out_fields_json: *mut *mut NemoRelayNativeString,
439) -> NemoRelayStatus;
440
441pub type NemoRelayNativeToolJsonCb = unsafe extern "C" fn(
443 user_data: *mut c_void,
444 name: *const NemoRelayNativeString,
445 payload_json: *const NemoRelayNativeString,
446 out_json: *mut *mut NemoRelayNativeString,
447) -> NemoRelayStatus;
448
449pub type NemoRelayNativeToolConditionalCb = unsafe extern "C" fn(
451 user_data: *mut c_void,
452 name: *const NemoRelayNativeString,
453 args_json: *const NemoRelayNativeString,
454 out_reason: *mut *mut NemoRelayNativeString,
455) -> NemoRelayStatus;
456
457pub type NemoRelayNativeConditionalMiddlewareCb = unsafe extern "C" fn(
465 user_data: *mut c_void,
466 kinds_json: *const NemoRelayNativeString,
467 registration_name: *const NemoRelayNativeString,
468 out_reason: *mut *mut NemoRelayNativeString,
469) -> NemoRelayStatus;
470
471pub type NemoRelayNativeToolExecutionCb = unsafe extern "C" fn(
478 user_data: *mut c_void,
479 name: *const NemoRelayNativeString,
480 args_json: *const NemoRelayNativeString,
481 next_fn: NemoRelayNativeToolNextFn,
482 next_ctx: *mut c_void,
483 out_outcome_json: *mut *mut NemoRelayNativeString,
484) -> NemoRelayStatus;
485
486pub type NemoRelayNativeLlmSanitizeRequestCb = unsafe extern "C" fn(
492 user_data: *mut c_void,
493 request_json: *const NemoRelayNativeString,
494 context: NemoRelayNativeLlmSanitizeRequestContext,
495 out_request_json: *mut *mut NemoRelayNativeString,
496) -> NemoRelayStatus;
497
498pub type NemoRelayNativeLlmSanitizeResponseCb = unsafe extern "C" fn(
504 user_data: *mut c_void,
505 payload_json: *const NemoRelayNativeString,
506 context: NemoRelayNativeLlmSanitizeResponseContext,
507 out_json: *mut *mut NemoRelayNativeString,
508) -> NemoRelayStatus;
509
510pub type NemoRelayNativeLlmConditionalCb = unsafe extern "C" fn(
512 user_data: *mut c_void,
513 request_json: *const NemoRelayNativeString,
514 out_reason: *mut *mut NemoRelayNativeString,
515) -> NemoRelayStatus;
516
517pub type NemoRelayNativeLlmRequestInterceptCb = unsafe extern "C" fn(
519 user_data: *mut c_void,
520 name: *const NemoRelayNativeString,
521 request_json: *const NemoRelayNativeString,
522 annotated_json: *const NemoRelayNativeString,
523 out_outcome_json: *mut *mut NemoRelayNativeString,
524) -> NemoRelayStatus;
525
526pub type NemoRelayNativeLlmExecutionCb = unsafe extern "C" fn(
528 user_data: *mut c_void,
529 name: *const NemoRelayNativeString,
530 request_json: *const NemoRelayNativeString,
531 next_fn: NemoRelayNativeLlmNextFn,
532 next_ctx: *mut c_void,
533 out_json: *mut *mut NemoRelayNativeString,
534) -> NemoRelayStatus;
535
536pub type NemoRelayNativeLlmStreamExecutionCb = unsafe extern "C" fn(
538 user_data: *mut c_void,
539 name: *const NemoRelayNativeString,
540 request_json: *const NemoRelayNativeString,
541 next_fn: NemoRelayNativeLlmStreamNextFn,
542 next_ctx: *mut c_void,
543 out_stream: *mut NemoRelayNativeLlmStreamV1,
544) -> NemoRelayStatus;
545
546pub type NemoRelayNativePluginValidateFn = unsafe extern "C" fn(
548 user_data: *mut c_void,
549 plugin_config_json: *const NemoRelayNativeString,
550 out_diagnostics_json: *mut *mut NemoRelayNativeString,
551) -> NemoRelayStatus;
552
553pub type NemoRelayNativePluginRegisterFn = unsafe extern "C" fn(
555 user_data: *mut c_void,
556 plugin_config_json: *const NemoRelayNativeString,
557 ctx: *mut NemoRelayNativePluginContext,
558) -> NemoRelayStatus;
559
560pub type NemoRelayNativePluginDropFn = Option<unsafe extern "C" fn(user_data: *mut c_void)>;
562
563#[repr(C)]
565#[derive(Clone, Copy)]
566pub struct NemoRelayNativeHostApiV1 {
567 pub abi_version: u32,
569 pub struct_size: usize,
571 pub relay_version: *const c_char,
573 pub string_new: unsafe extern "C" fn(
575 data: *const u8,
576 len: usize,
577 out: *mut *mut NemoRelayNativeString,
578 ) -> NemoRelayStatus,
579 pub string_data: unsafe extern "C" fn(value: *const NemoRelayNativeString) -> *const u8,
581 pub string_len: unsafe extern "C" fn(value: *const NemoRelayNativeString) -> usize,
583 pub string_free: unsafe extern "C" fn(value: *mut NemoRelayNativeString),
585 pub last_error_clear: unsafe extern "C" fn(),
587 pub last_error_set: unsafe extern "C" fn(message: *const NemoRelayNativeString),
589 pub llm_request_codec_decode: unsafe extern "C" fn(
591 codec: *const NemoRelayNativeLlmRequestCodec,
592 request_json: *const NemoRelayNativeString,
593 out: *mut *mut NemoRelayNativeString,
594 ) -> NemoRelayStatus,
595 pub llm_request_codec_encode: unsafe extern "C" fn(
597 codec: *const NemoRelayNativeLlmRequestCodec,
598 annotated_json: *const NemoRelayNativeString,
599 original_json: *const NemoRelayNativeString,
600 out: *mut *mut NemoRelayNativeString,
601 ) -> NemoRelayStatus,
602 pub llm_response_codec_decode: unsafe extern "C" fn(
604 codec: *const NemoRelayNativeLlmResponseCodec,
605 response_json: *const NemoRelayNativeString,
606 out: *mut *mut NemoRelayNativeString,
607 ) -> NemoRelayStatus,
608 pub plugin_context_register_subscriber: unsafe extern "C" fn(
610 ctx: *mut NemoRelayNativePluginContext,
611 name: *const NemoRelayNativeString,
612 cb: NemoRelayNativeEventSubscriberCb,
613 user_data: *mut c_void,
614 free_fn: NemoRelayNativeFreeFn,
615 ) -> NemoRelayStatus,
616 pub plugin_context_register_tool_sanitize_request_guardrail:
618 unsafe extern "C" fn(
619 ctx: *mut NemoRelayNativePluginContext,
620 name: *const NemoRelayNativeString,
621 priority: i32,
622 cb: NemoRelayNativeToolJsonCb,
623 user_data: *mut c_void,
624 free_fn: NemoRelayNativeFreeFn,
625 ) -> NemoRelayStatus,
626 pub plugin_context_register_tool_sanitize_response_guardrail:
628 unsafe extern "C" fn(
629 ctx: *mut NemoRelayNativePluginContext,
630 name: *const NemoRelayNativeString,
631 priority: i32,
632 cb: NemoRelayNativeToolJsonCb,
633 user_data: *mut c_void,
634 free_fn: NemoRelayNativeFreeFn,
635 ) -> NemoRelayStatus,
636 pub plugin_context_register_tool_conditional_execution_guardrail:
638 unsafe extern "C" fn(
639 ctx: *mut NemoRelayNativePluginContext,
640 name: *const NemoRelayNativeString,
641 priority: i32,
642 cb: NemoRelayNativeToolConditionalCb,
643 user_data: *mut c_void,
644 free_fn: NemoRelayNativeFreeFn,
645 ) -> NemoRelayStatus,
646 pub plugin_context_register_tool_request_intercept: unsafe extern "C" fn(
648 ctx: *mut NemoRelayNativePluginContext,
649 name: *const NemoRelayNativeString,
650 priority: i32,
651 break_chain: bool,
652 cb: NemoRelayNativeToolJsonCb,
653 user_data: *mut c_void,
654 free_fn: NemoRelayNativeFreeFn,
655 )
656 -> NemoRelayStatus,
657 pub plugin_context_register_tool_execution_intercept: unsafe extern "C" fn(
659 ctx: *mut NemoRelayNativePluginContext,
660 name: *const NemoRelayNativeString,
661 priority: i32,
662 cb: NemoRelayNativeToolExecutionCb,
663 user_data: *mut c_void,
664 free_fn: NemoRelayNativeFreeFn,
665 )
666 -> NemoRelayStatus,
667 pub plugin_context_register_llm_sanitize_request_guardrail:
669 unsafe extern "C" fn(
670 ctx: *mut NemoRelayNativePluginContext,
671 name: *const NemoRelayNativeString,
672 priority: i32,
673 cb: NemoRelayNativeLlmSanitizeRequestCb,
674 user_data: *mut c_void,
675 free_fn: NemoRelayNativeFreeFn,
676 ) -> NemoRelayStatus,
677 pub plugin_context_register_llm_sanitize_response_guardrail:
679 unsafe extern "C" fn(
680 ctx: *mut NemoRelayNativePluginContext,
681 name: *const NemoRelayNativeString,
682 priority: i32,
683 cb: NemoRelayNativeLlmSanitizeResponseCb,
684 user_data: *mut c_void,
685 free_fn: NemoRelayNativeFreeFn,
686 ) -> NemoRelayStatus,
687 pub plugin_context_register_llm_conditional_execution_guardrail:
689 unsafe extern "C" fn(
690 ctx: *mut NemoRelayNativePluginContext,
691 name: *const NemoRelayNativeString,
692 priority: i32,
693 cb: NemoRelayNativeLlmConditionalCb,
694 user_data: *mut c_void,
695 free_fn: NemoRelayNativeFreeFn,
696 ) -> NemoRelayStatus,
697 pub plugin_context_register_llm_request_intercept: unsafe extern "C" fn(
699 ctx: *mut NemoRelayNativePluginContext,
700 name: *const NemoRelayNativeString,
701 priority: i32,
702 break_chain: bool,
703 cb: NemoRelayNativeLlmRequestInterceptCb,
704 user_data: *mut c_void,
705 free_fn: NemoRelayNativeFreeFn,
706 ) -> NemoRelayStatus,
707 pub plugin_context_register_llm_execution_intercept: unsafe extern "C" fn(
709 ctx: *mut NemoRelayNativePluginContext,
710 name: *const NemoRelayNativeString,
711 priority: i32,
712 cb: NemoRelayNativeLlmExecutionCb,
713 user_data: *mut c_void,
714 free_fn: NemoRelayNativeFreeFn,
715 )
716 -> NemoRelayStatus,
717 pub plugin_context_register_llm_stream_execution_intercept:
719 unsafe extern "C" fn(
720 ctx: *mut NemoRelayNativePluginContext,
721 name: *const NemoRelayNativeString,
722 priority: i32,
723 cb: NemoRelayNativeLlmStreamExecutionCb,
724 user_data: *mut c_void,
725 free_fn: NemoRelayNativeFreeFn,
726 ) -> NemoRelayStatus,
727 pub scope_handle_free: unsafe extern "C" fn(handle: *mut NemoRelayNativeScopeHandle),
729 pub scope_get_current:
731 unsafe extern "C" fn(out: *mut *mut NemoRelayNativeScopeHandle) -> NemoRelayStatus,
732 pub scope_push: unsafe extern "C" fn(
734 name: *const NemoRelayNativeString,
735 scope_type: NemoRelayNativeScopeType,
736 parent: *const NemoRelayNativeScopeHandle,
737 attributes: u32,
738 data_json: *const NemoRelayNativeString,
739 metadata_json: *const NemoRelayNativeString,
740 input_json: *const NemoRelayNativeString,
741 timestamp_unix_micros: *const i64,
742 out: *mut *mut NemoRelayNativeScopeHandle,
743 ) -> NemoRelayStatus,
744 pub scope_pop: unsafe extern "C" fn(
746 handle: *const NemoRelayNativeScopeHandle,
747 output_json: *const NemoRelayNativeString,
748 metadata_json: *const NemoRelayNativeString,
749 timestamp_unix_micros: *const i64,
750 ) -> NemoRelayStatus,
751 pub emit_mark: unsafe extern "C" fn(
753 name: *const NemoRelayNativeString,
754 parent: *const NemoRelayNativeScopeHandle,
755 data_json: *const NemoRelayNativeString,
756 metadata_json: *const NemoRelayNativeString,
757 timestamp_unix_micros: *const i64,
758 ) -> NemoRelayStatus,
759 pub scope_stack_create:
761 unsafe extern "C" fn(out: *mut *mut NemoRelayNativeScopeStack) -> NemoRelayStatus,
762 pub scope_stack_free: unsafe extern "C" fn(stack: *mut NemoRelayNativeScopeStack),
764 pub scope_stack_set_thread:
766 unsafe extern "C" fn(stack: *const NemoRelayNativeScopeStack) -> NemoRelayStatus,
767 pub scope_stack_capture_thread:
769 unsafe extern "C" fn(out: *mut *mut NemoRelayNativeScopeStackBinding) -> NemoRelayStatus,
770 pub scope_stack_restore_thread:
772 unsafe extern "C" fn(binding: *mut NemoRelayNativeScopeStackBinding) -> NemoRelayStatus,
773 pub scope_stack_binding_free:
775 unsafe extern "C" fn(binding: *mut NemoRelayNativeScopeStackBinding),
776 pub scope_stack_active: unsafe extern "C" fn() -> bool,
778 pub scope_stack_with_current: unsafe extern "C" fn(
780 stack: *const NemoRelayNativeScopeStack,
781 cb: NemoRelayNativeWithScopeStackCb,
782 user_data: *mut c_void,
783 ) -> NemoRelayStatus,
784 pub plugin_context_register_mark_sanitize_guardrail: unsafe extern "C" fn(
786 ctx: *mut NemoRelayNativePluginContext,
787 name: *const NemoRelayNativeString,
788 priority: i32,
789 cb: NemoRelayNativeEventSanitizeCb,
790 user_data: *mut c_void,
791 free_fn: NemoRelayNativeFreeFn,
792 )
793 -> NemoRelayStatus,
794 pub plugin_context_register_scope_sanitize_start_guardrail:
796 unsafe extern "C" fn(
797 ctx: *mut NemoRelayNativePluginContext,
798 name: *const NemoRelayNativeString,
799 priority: i32,
800 cb: NemoRelayNativeEventSanitizeCb,
801 user_data: *mut c_void,
802 free_fn: NemoRelayNativeFreeFn,
803 ) -> NemoRelayStatus,
804 pub plugin_context_register_scope_sanitize_end_guardrail:
806 unsafe extern "C" fn(
807 ctx: *mut NemoRelayNativePluginContext,
808 name: *const NemoRelayNativeString,
809 priority: i32,
810 cb: NemoRelayNativeEventSanitizeCb,
811 user_data: *mut c_void,
812 free_fn: NemoRelayNativeFreeFn,
813 ) -> NemoRelayStatus,
814}
815
816#[repr(u32)]
822#[derive(Debug, Clone, Copy, PartialEq, Eq)]
823pub enum NemoRelayNativeAsyncMiddlewareKind {
824 ToolSanitizeRequest = 0,
826 ToolSanitizeResponse = 1,
828 ToolConditionalExecution = 2,
830 ToolRequestIntercept = 3,
832 ToolExecutionIntercept = 4,
834 LlmSanitizeRequest = 5,
836 LlmSanitizeResponse = 6,
838 LlmConditionalExecution = 7,
840 LlmRequestIntercept = 8,
842 LlmExecutionIntercept = 9,
844 LlmStreamExecutionIntercept = 10,
850 MarkSanitize = 11,
852 ScopeSanitizeStart = 12,
854 ScopeSanitizeEnd = 13,
856 EventMetadataInjector = 14,
858}
859
860impl TryFrom<u32> for NemoRelayNativeAsyncMiddlewareKind {
861 type Error = ();
862
863 fn try_from(value: u32) -> std::result::Result<Self, Self::Error> {
864 match value {
865 0 => Ok(Self::ToolSanitizeRequest),
866 1 => Ok(Self::ToolSanitizeResponse),
867 2 => Ok(Self::ToolConditionalExecution),
868 3 => Ok(Self::ToolRequestIntercept),
869 4 => Ok(Self::ToolExecutionIntercept),
870 5 => Ok(Self::LlmSanitizeRequest),
871 6 => Ok(Self::LlmSanitizeResponse),
872 7 => Ok(Self::LlmConditionalExecution),
873 8 => Ok(Self::LlmRequestIntercept),
874 9 => Ok(Self::LlmExecutionIntercept),
875 10 => Ok(Self::LlmStreamExecutionIntercept),
876 11 => Ok(Self::MarkSanitize),
877 12 => Ok(Self::ScopeSanitizeStart),
878 13 => Ok(Self::ScopeSanitizeEnd),
879 14 => Ok(Self::EventMetadataInjector),
880 _ => Err(()),
881 }
882 }
883}
884
885#[repr(u32)]
887#[derive(Debug, Clone, Copy, PartialEq, Eq)]
888pub enum NemoRelayNativeAsyncCallbackState {
889 Complete = 0,
891 Pending = 1,
893}
894
895impl TryFrom<u32> for NemoRelayNativeAsyncCallbackState {
896 type Error = ();
897
898 fn try_from(value: u32) -> std::result::Result<Self, Self::Error> {
899 match value {
900 0 => Ok(Self::Complete),
901 1 => Ok(Self::Pending),
902 _ => Err(()),
903 }
904 }
905}
906
907#[repr(C)]
909pub struct NemoRelayNativeAsyncCompletion {
910 _private: [u8; 0],
911 _marker: PhantomData<(*mut u8, PhantomPinned)>,
912}
913
914#[repr(C)]
916pub struct NemoRelayNativeAsyncNext {
917 _private: [u8; 0],
918 _marker: PhantomData<(*mut u8, PhantomPinned)>,
919}
920
921#[repr(C)]
923pub struct NemoRelayNativeAsyncStream {
924 _private: [u8; 0],
925 _marker: PhantomData<(*mut u8, PhantomPinned)>,
926}
927
928#[repr(C)]
930pub struct NemoRelayNativeLlmAsyncStream {
931 _private: [u8; 0],
932 _marker: PhantomData<(*mut u8, PhantomPinned)>,
933}
934
935pub type NemoRelayNativeAsyncLlmStreamOpenCb = unsafe extern "C" fn(
940 user_data: *mut c_void,
941 stream: *const NemoRelayNativeLlmAsyncStream,
942 error: *const NemoRelayNativeString,
943);
944
945pub type NemoRelayNativeAsyncLlmStreamPullCb = unsafe extern "C" fn(
951 user_data: *mut c_void,
952 chunk_json: *const NemoRelayNativeString,
953 error: *const NemoRelayNativeString,
954 done: bool,
955);
956
957pub type NemoRelayNativeAsyncNextStreamCb = unsafe extern "C" fn(
965 user_data: *mut c_void,
966 chunk_json: *const NemoRelayNativeString,
967 error: *const NemoRelayNativeString,
968 done: bool,
969) -> bool;
970
971pub type NemoRelayNativeAsyncNextResultCb = unsafe extern "C" fn(
979 user_data: *mut c_void,
980 value_json: *const NemoRelayNativeString,
981 error: *const NemoRelayNativeString,
982);
983
984pub type NemoRelayNativeAsyncStreamMiddlewareCb = unsafe extern "C" fn(
999 user_data: *mut c_void,
1000 invocation_json: *const NemoRelayNativeString,
1001 next: *const NemoRelayNativeAsyncNext,
1002 stream: *const NemoRelayNativeAsyncStream,
1003) -> u32;
1004
1005pub type NemoRelayNativeAsyncMiddlewareCb = unsafe extern "C" fn(
1027 user_data: *mut c_void,
1028 invocation_json: *const NemoRelayNativeString,
1029 next: *const NemoRelayNativeAsyncNext,
1030 completion: *const NemoRelayNativeAsyncCompletion,
1031) -> u32;
1032
1033#[repr(C)]
1038#[derive(Clone, Copy)]
1039pub struct NemoRelayNativeHostApiV3 {
1040 pub v1: NemoRelayNativeHostApiV1,
1042 pub async_completion_resolve_json: unsafe extern "C" fn(
1047 completion: *const NemoRelayNativeAsyncCompletion,
1048 value_json: *const NemoRelayNativeString,
1049 ) -> NemoRelayStatus,
1050 pub async_completion_reject: unsafe extern "C" fn(
1052 completion: *const NemoRelayNativeAsyncCompletion,
1053 message: *const NemoRelayNativeString,
1054 ) -> NemoRelayStatus,
1055 pub async_completion_is_cancelled:
1057 unsafe extern "C" fn(completion: *const NemoRelayNativeAsyncCompletion) -> bool,
1058 pub async_completion_release:
1060 unsafe extern "C" fn(completion: *const NemoRelayNativeAsyncCompletion),
1061 pub async_next_invoke: unsafe extern "C" fn(
1069 next: *const NemoRelayNativeAsyncNext,
1070 invocation_json: *const NemoRelayNativeString,
1071 completion: *const NemoRelayNativeAsyncCompletion,
1072 ) -> NemoRelayStatus,
1073 pub async_next_release: unsafe extern "C" fn(next: *const NemoRelayNativeAsyncNext),
1078 pub plugin_context_register_async_middleware: unsafe extern "C" fn(
1085 ctx: *mut NemoRelayNativePluginContext,
1086 kind: u32,
1087 name: *const NemoRelayNativeString,
1088 priority: i32,
1089 break_chain: bool,
1090 cb: NemoRelayNativeAsyncMiddlewareCb,
1091 user_data: *mut c_void,
1092 free_fn: NemoRelayNativeFreeFn,
1093 ) -> NemoRelayStatus,
1094 pub async_stream_push_json: unsafe extern "C" fn(
1099 stream: *const NemoRelayNativeAsyncStream,
1100 chunk_json: *const NemoRelayNativeString,
1101 ) -> NemoRelayStatus,
1102 pub async_stream_finish:
1104 unsafe extern "C" fn(stream: *const NemoRelayNativeAsyncStream) -> NemoRelayStatus,
1105 pub async_stream_reject: unsafe extern "C" fn(
1110 stream: *const NemoRelayNativeAsyncStream,
1111 message: *const NemoRelayNativeString,
1112 ) -> NemoRelayStatus,
1113 pub async_stream_is_cancelled:
1115 unsafe extern "C" fn(stream: *const NemoRelayNativeAsyncStream) -> bool,
1116 pub async_stream_release: unsafe extern "C" fn(stream: *const NemoRelayNativeAsyncStream),
1118 pub async_next_invoke_stream: unsafe extern "C" fn(
1126 next: *const NemoRelayNativeAsyncNext,
1127 invocation_json: *const NemoRelayNativeString,
1128 stream: *const NemoRelayNativeAsyncStream,
1129 cb: NemoRelayNativeAsyncNextStreamCb,
1130 user_data: *mut c_void,
1131 ) -> NemoRelayStatus,
1132 pub plugin_context_register_async_stream_middleware: unsafe extern "C" fn(
1134 ctx: *mut NemoRelayNativePluginContext,
1135 name: *const NemoRelayNativeString,
1136 priority: i32,
1137 cb: NemoRelayNativeAsyncStreamMiddlewareCb,
1138 user_data: *mut c_void,
1139 free_fn: NemoRelayNativeFreeFn,
1140 )
1141 -> NemoRelayStatus,
1142 pub async_next_invoke_result: unsafe extern "C" fn(
1149 next: *const NemoRelayNativeAsyncNext,
1150 invocation_json: *const NemoRelayNativeString,
1151 cb: NemoRelayNativeAsyncNextResultCb,
1152 user_data: *mut c_void,
1153 ) -> NemoRelayStatus,
1154}
1155
1156pub type NemoRelayNativeEmitMarkV2Fn = unsafe extern "C" fn(
1158 name: *const NemoRelayNativeString,
1159 parent: *const NemoRelayNativeScopeHandle,
1160 data_json: *const NemoRelayNativeString,
1161 metadata_json: *const NemoRelayNativeString,
1162 data_schema_json: *const NemoRelayNativeString,
1163 severity: *const NemoRelayNativeString,
1164 timestamp_unix_micros: *const i64,
1165) -> NemoRelayStatus;
1166
1167pub type NemoRelayNativeGetRuntimeDiagnosticsFn =
1169 unsafe extern "C" fn(out_json: *mut *mut NemoRelayNativeString) -> NemoRelayStatus;
1170
1171#[repr(C)]
1176#[derive(Clone, Copy)]
1177pub struct NemoRelayNativeHostApiV4 {
1178 pub v3: NemoRelayNativeHostApiV3,
1180 pub async_completion_llm_request_codec_decode: unsafe extern "C" fn(
1182 completion: *const NemoRelayNativeAsyncCompletion,
1183 request_json: *const NemoRelayNativeString,
1184 out: *mut *mut NemoRelayNativeString,
1185 ) -> NemoRelayStatus,
1186 pub async_completion_llm_request_codec_encode: unsafe extern "C" fn(
1188 completion: *const NemoRelayNativeAsyncCompletion,
1189 annotated_json: *const NemoRelayNativeString,
1190 original_json: *const NemoRelayNativeString,
1191 out: *mut *mut NemoRelayNativeString,
1192 ) -> NemoRelayStatus,
1193 pub async_completion_llm_response_codec_decode: unsafe extern "C" fn(
1195 completion: *const NemoRelayNativeAsyncCompletion,
1196 response_json: *const NemoRelayNativeString,
1197 out: *mut *mut NemoRelayNativeString,
1198 ) -> NemoRelayStatus,
1199 pub async_next_open_llm_stream: unsafe extern "C" fn(
1201 next: *const NemoRelayNativeAsyncNext,
1202 request_json: *const NemoRelayNativeString,
1203 cb: NemoRelayNativeAsyncLlmStreamOpenCb,
1204 user_data: *mut c_void,
1205 ) -> NemoRelayStatus,
1206 pub async_llm_stream_pull: unsafe extern "C" fn(
1208 stream: *const NemoRelayNativeLlmAsyncStream,
1209 cb: NemoRelayNativeAsyncLlmStreamPullCb,
1210 user_data: *mut c_void,
1211 ) -> NemoRelayStatus,
1212 pub async_llm_stream_cancel:
1214 unsafe extern "C" fn(stream: *const NemoRelayNativeLlmAsyncStream) -> NemoRelayStatus,
1215 pub async_llm_stream_release:
1217 unsafe extern "C" fn(stream: *const NemoRelayNativeLlmAsyncStream),
1218 pub async_completion_retain:
1221 unsafe extern "C" fn(completion: *const NemoRelayNativeAsyncCompletion) -> NemoRelayStatus,
1222 pub async_stream_is_backpressured:
1227 unsafe extern "C" fn(stream: *const NemoRelayNativeAsyncStream) -> bool,
1228 pub emit_mark_v2: NemoRelayNativeEmitMarkV2Fn,
1230 pub get_runtime_diagnostics: NemoRelayNativeGetRuntimeDiagnosticsFn,
1232 pub plugin_context_runtime: unsafe extern "C" fn(
1237 ctx: *mut NemoRelayNativePluginContext,
1238 out: *mut *const NemoRelayNativePluginRuntime,
1239 ) -> NemoRelayStatus,
1240 pub plugin_runtime_retain:
1244 unsafe extern "C" fn(runtime: *const NemoRelayNativePluginRuntime) -> NemoRelayStatus,
1245 pub plugin_runtime_release: unsafe extern "C" fn(runtime: *const NemoRelayNativePluginRuntime),
1247 pub plugin_runtime_list_registrations: unsafe extern "C" fn(
1249 runtime: *const NemoRelayNativePluginRuntime,
1250 kinds_json: *const NemoRelayNativeString,
1251 out_json: *mut *mut NemoRelayNativeString,
1252 ) -> NemoRelayStatus,
1253 pub plugin_runtime_register_conditional_middleware_guardrail:
1255 unsafe extern "C" fn(
1256 runtime: *const NemoRelayNativePluginRuntime,
1257 name: *const NemoRelayNativeString,
1258 kinds_json: *const NemoRelayNativeString,
1259 registration_name: *const NemoRelayNativeString,
1260 reason: *const NemoRelayNativeString,
1261 out_handle: *mut *mut NemoRelayNativeString,
1262 ) -> NemoRelayStatus,
1263 pub plugin_runtime_deregister_conditional_middleware_guardrail:
1265 unsafe extern "C" fn(
1266 runtime: *const NemoRelayNativePluginRuntime,
1267 handle: *const NemoRelayNativeString,
1268 out_removed: *mut bool,
1269 ) -> NemoRelayStatus,
1270 pub plugin_context_register_conditional_middleware_guardrail:
1272 unsafe extern "C" fn(
1273 ctx: *mut NemoRelayNativePluginContext,
1274 name: *const NemoRelayNativeString,
1275 kinds_json: *const NemoRelayNativeString,
1276 registration_name: *const NemoRelayNativeString,
1277 reason: *const NemoRelayNativeString,
1278 ) -> NemoRelayStatus,
1279 pub plugin_runtime_register_conditional_middleware_guardrail_callback:
1284 unsafe extern "C" fn(
1285 runtime: *const NemoRelayNativePluginRuntime,
1286 name: *const NemoRelayNativeString,
1287 kinds_json: *const NemoRelayNativeString,
1288 registration_name: *const NemoRelayNativeString,
1289 cb: NemoRelayNativeConditionalMiddlewareCb,
1290 user_data: *mut c_void,
1291 free_fn: NemoRelayNativeFreeFn,
1292 out_handle: *mut *mut NemoRelayNativeString,
1293 ) -> NemoRelayStatus,
1294 pub plugin_context_register_conditional_middleware_guardrail_callback:
1299 unsafe extern "C" fn(
1300 ctx: *mut NemoRelayNativePluginContext,
1301 name: *const NemoRelayNativeString,
1302 kinds_json: *const NemoRelayNativeString,
1303 registration_name: *const NemoRelayNativeString,
1304 cb: NemoRelayNativeConditionalMiddlewareCb,
1305 user_data: *mut c_void,
1306 free_fn: NemoRelayNativeFreeFn,
1307 ) -> NemoRelayStatus,
1308}
1309
1310unsafe impl Send for NemoRelayNativeHostApiV3 {}
1311unsafe impl Sync for NemoRelayNativeHostApiV3 {}
1312unsafe impl Send for NemoRelayNativeHostApiV4 {}
1315unsafe impl Sync for NemoRelayNativeHostApiV4 {}
1316
1317unsafe impl Send for NemoRelayNativeHostApiV1 {}
1320unsafe impl Sync for NemoRelayNativeHostApiV1 {}
1321
1322#[repr(C)]
1324pub struct NemoRelayNativePluginV1 {
1325 pub struct_size: usize,
1327 pub plugin_kind: *mut NemoRelayNativeString,
1329 pub allows_multiple_components: bool,
1331 pub user_data: *mut c_void,
1333 pub validate: Option<NemoRelayNativePluginValidateFn>,
1335 pub register: Option<NemoRelayNativePluginRegisterFn>,
1337 pub drop: NemoRelayNativePluginDropFn,
1339}
1340
1341impl Default for NemoRelayNativePluginV1 {
1342 fn default() -> Self {
1343 Self {
1344 struct_size: std::mem::size_of::<Self>(),
1345 plugin_kind: ptr::null_mut(),
1346 allows_multiple_components: true,
1347 user_data: ptr::null_mut(),
1348 validate: None,
1349 register: None,
1350 drop: None,
1351 }
1352 }
1353}
1354
1355pub type NemoRelayNativePluginEntry = unsafe extern "C" fn(
1357 host: *const NemoRelayNativeHostApiV1,
1358 out: *mut NemoRelayNativePluginV1,
1359) -> NemoRelayStatus;
1360
1361pub type Result<T> = std::result::Result<T, String>;
1363
1364#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
1366pub struct RuntimeDiagnostic {
1367 pub code: String,
1369 pub message: String,
1371 pub count: u64,
1373}
1374
1375#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, serde::Deserialize)]
1377pub struct RuntimeDiagnostics {
1378 entries: Vec<RuntimeDiagnostic>,
1379}
1380
1381#[derive(Debug, Clone, PartialEq, Eq)]
1383pub struct ConditionalMiddlewareGuardrailHandle(String);
1384
1385impl RuntimeDiagnostics {
1386 pub fn entries(&self) -> &[RuntimeDiagnostic] {
1388 &self.entries
1389 }
1390
1391 pub fn get(&self, code: &str) -> Option<&RuntimeDiagnostic> {
1393 self.entries
1394 .iter()
1395 .find(|diagnostic| diagnostic.code == code)
1396 }
1397}
1398
1399pub type LlmJsonStream = Box<dyn Iterator<Item = Result<Json>> + Send>;
1401
1402pub struct PluginRuntime {
1404 host: NemoRelayNativeHostApiV1,
1405 emit_mark_v2: Option<NemoRelayNativeEmitMarkV2Fn>,
1406 get_runtime_diagnostics: Option<NemoRelayNativeGetRuntimeDiagnosticsFn>,
1407 v4: Option<NemoRelayNativeHostApiV4>,
1408 capability: *const NemoRelayNativePluginRuntime,
1409}
1410
1411unsafe impl Send for PluginRuntime {}
1415unsafe impl Sync for PluginRuntime {}
1416
1417impl Clone for PluginRuntime {
1418 fn clone(&self) -> Self {
1419 let mut capability = self.capability;
1420 if let (Some(v4), false) = (self.v4, self.capability.is_null())
1421 && unsafe { (v4.plugin_runtime_retain)(self.capability) } != NemoRelayStatus::Ok
1422 {
1423 capability = ptr::null();
1424 }
1425 Self {
1426 host: self.host,
1427 emit_mark_v2: self.emit_mark_v2,
1428 get_runtime_diagnostics: self.get_runtime_diagnostics,
1429 v4: self.v4,
1430 capability,
1431 }
1432 }
1433}
1434
1435impl Drop for PluginRuntime {
1436 fn drop(&mut self) {
1437 if let (Some(v4), false) = (self.v4, self.capability.is_null()) {
1438 unsafe { (v4.plugin_runtime_release)(self.capability) };
1439 }
1440 }
1441}
1442
1443impl PluginRuntime {
1444 pub fn new(host: &NemoRelayNativeHostApiV1) -> Self {
1446 let v4 = (host.abi_version >= NEMO_RELAY_NATIVE_ABI_VERSION
1447 && host.struct_size >= std::mem::size_of::<NemoRelayNativeHostApiV4>())
1448 .then(|| unsafe { *(host as *const _ as *const NemoRelayNativeHostApiV4) });
1449 Self {
1450 host: *host,
1451 emit_mark_v2: v4.map(|host| host.emit_mark_v2),
1452 get_runtime_diagnostics: v4.map(|host| host.get_runtime_diagnostics),
1453 v4,
1454 capability: ptr::null(),
1455 }
1456 }
1457
1458 fn from_context(
1459 host: &NemoRelayNativeHostApiV1,
1460 ctx: *mut NemoRelayNativePluginContext,
1461 ) -> Self {
1462 let mut runtime = Self::new(host);
1463 let Some(v4) = runtime.v4 else {
1464 return runtime;
1465 };
1466 let mut capability = ptr::null();
1467 if unsafe { (v4.plugin_context_runtime)(ctx, &mut capability) } == NemoRelayStatus::Ok {
1468 runtime.capability = capability;
1469 }
1470 runtime
1471 }
1472
1473 pub fn list_runtime_registrations(
1475 &self,
1476 kinds: Option<&std::collections::BTreeSet<RuntimeRegistrationKind>>,
1477 ) -> Result<Vec<RuntimeRegistrationIdentity>> {
1478 let v4 = self.runtime_v4()?;
1479 let kinds = match kinds {
1480 Some(kinds) => Some(
1481 HostString::from_json(&self.host, kinds)
1482 .ok_or_else(|| "failed to serialize runtime registration kinds".to_string())?,
1483 ),
1484 None => None,
1485 };
1486 let mut out = ptr::null_mut();
1487 let status = unsafe {
1488 (v4.plugin_runtime_list_registrations)(
1489 self.capability,
1490 kinds.as_ref().map_or(ptr::null(), HostString::as_ptr),
1491 &mut out,
1492 )
1493 };
1494 status_result(&self.host, status, "list runtime registrations")?;
1495 take_host_json(&self.host, out)
1496 }
1497
1498 pub fn register_conditional_middleware_guardrail<F>(
1503 &self,
1504 name: &str,
1505 kinds: &std::collections::BTreeSet<RuntimeRegistrationKind>,
1506 registration_name: &str,
1507 callback: F,
1508 ) -> Result<ConditionalMiddlewareGuardrailHandle>
1509 where
1510 F: Fn(&std::collections::BTreeSet<RuntimeRegistrationKind>, &str) -> Option<String>
1511 + Send
1512 + Sync
1513 + 'static,
1514 {
1515 let v4 = self.runtime_v4()?;
1516 let name = HostString::new(&self.host, name)
1517 .ok_or_else(|| "failed to allocate gate name".to_string())?;
1518 let kinds = HostString::from_json(&self.host, kinds)
1519 .ok_or_else(|| "failed to serialize runtime registration kinds".to_string())?;
1520 let registration_name = HostString::new(&self.host, registration_name)
1521 .ok_or_else(|| "failed to allocate target name".to_string())?;
1522 let user_data = typed_callback_user_data(&self.host, callback);
1523 let mut out = ptr::null_mut();
1524 let status = unsafe {
1525 (v4.plugin_runtime_register_conditional_middleware_guardrail_callback)(
1526 self.capability,
1527 name.as_ptr(),
1528 kinds.as_ptr(),
1529 registration_name.as_ptr(),
1530 typed_conditional_middleware_trampoline::<F>,
1531 user_data,
1532 Some(drop_typed_callback::<F>),
1533 &mut out,
1534 )
1535 };
1536 finish_typed_registration(
1537 &self.host,
1538 status,
1539 user_data,
1540 "register conditional middleware guardrail",
1541 )?;
1542 take_host_string(&self.host, out).map(ConditionalMiddlewareGuardrailHandle)
1543 }
1544
1545 pub fn deregister_conditional_middleware_guardrail(
1547 &self,
1548 handle: &ConditionalMiddlewareGuardrailHandle,
1549 ) -> Result<bool> {
1550 let v4 = self.runtime_v4()?;
1551 let handle = HostString::new(&self.host, &handle.0)
1552 .ok_or_else(|| "failed to allocate gate handle".to_string())?;
1553 let mut removed = false;
1554 let status = unsafe {
1555 (v4.plugin_runtime_deregister_conditional_middleware_guardrail)(
1556 self.capability,
1557 handle.as_ptr(),
1558 &mut removed,
1559 )
1560 };
1561 status_result(
1562 &self.host,
1563 status,
1564 "deregister conditional middleware guardrail",
1565 )?;
1566 Ok(removed)
1567 }
1568
1569 fn runtime_v4(&self) -> Result<NemoRelayNativeHostApiV4> {
1570 self.v4
1571 .filter(|_| !self.capability.is_null())
1572 .ok_or_else(|| "host does not support activation-owned runtime gate control".into())
1573 }
1574
1575 pub fn host_api(&self) -> &NemoRelayNativeHostApiV1 {
1577 &self.host
1578 }
1579
1580 pub fn current_scope(&self) -> Result<ScopeHandle<'_>> {
1582 current_scope(&self.host)
1583 }
1584
1585 pub fn push_scope(
1587 &self,
1588 name: &str,
1589 scope_type: ScopeType,
1590 data: Option<&Json>,
1591 metadata: Option<&Json>,
1592 input: Option<&Json>,
1593 ) -> Result<ScopeHandle<'_>> {
1594 push_scope(&self.host, name, scope_type.into(), data, metadata, input)
1595 }
1596
1597 pub fn pop_scope(
1599 &self,
1600 handle: &ScopeHandle<'_>,
1601 output: Option<&Json>,
1602 metadata: Option<&Json>,
1603 ) -> Result<()> {
1604 pop_scope(&self.host, handle, output, metadata)
1605 }
1606
1607 pub fn scope(
1609 &self,
1610 name: &str,
1611 scope_type: ScopeType,
1612 data: Option<&Json>,
1613 metadata: Option<&Json>,
1614 input: Option<&Json>,
1615 ) -> Result<ScopeGuard<'_>> {
1616 let handle = self.push_scope(name, scope_type, data, metadata, input)?;
1617 Ok(ScopeGuard {
1618 runtime: self,
1619 handle: Some(handle),
1620 })
1621 }
1622
1623 pub fn emit_mark(
1625 &self,
1626 name: &str,
1627 data: Option<&Json>,
1628 metadata: Option<&Json>,
1629 ) -> Result<()> {
1630 emit_mark(&self.host, name, data, metadata)
1631 }
1632
1633 pub fn emit_mark_with_options(
1638 &self,
1639 name: &str,
1640 data: Option<&Json>,
1641 metadata: Option<&Json>,
1642 data_schema: Option<&DataSchema>,
1643 severity: Option<LogSeverity>,
1644 ) -> Result<()> {
1645 match self.emit_mark_v2 {
1646 Some(emit_mark_v2) => emit_mark_v2_call(
1647 &self.host,
1648 emit_mark_v2,
1649 name,
1650 data,
1651 metadata,
1652 data_schema,
1653 severity,
1654 ),
1655 None if data_schema.is_none() && severity.is_none() => {
1656 emit_mark(&self.host, name, data, metadata)
1657 }
1658 None => Err("mark data_schema and severity require native host ABI v4".into()),
1659 }
1660 }
1661
1662 pub fn emit_metric(
1664 &self,
1665 name: &str,
1666 measurements: Vec<MetricMeasurement>,
1667 metadata: Option<&Json>,
1668 ) -> Result<()> {
1669 let envelope = MetricEnvelope { measurements };
1670 envelope.validate().map_err(|err| err.to_string())?;
1671 let data = serde_json::to_value(envelope)
1672 .map_err(|err| format!("failed to serialize metric mark: {err}"))?;
1673 let data_schema = DataSchema::builder()
1674 .name(METRIC_DATA_SCHEMA_NAME)
1675 .version(METRIC_DATA_SCHEMA_VERSION)
1676 .build();
1677 self.emit_mark_with_options(name, Some(&data), metadata, Some(&data_schema), None)
1678 }
1679
1680 pub fn runtime_diagnostics(&self) -> Result<RuntimeDiagnostics> {
1682 let Some(get_runtime_diagnostics) = self.get_runtime_diagnostics else {
1683 return Err(
1684 "runtime diagnostics require the native host ABI v4 diagnostics extension".into(),
1685 );
1686 };
1687 native_json_call(&self.host, "runtime diagnostics", |out| {
1688 let status = unsafe { get_runtime_diagnostics(out) };
1689 codec_status(&self.host, status)
1690 })
1691 }
1692
1693 pub fn create_scope_stack(&self) -> Result<ScopeStack<'_>> {
1695 create_scope_stack(&self.host)
1696 }
1697
1698 pub fn capture_scope_stack_thread(&self) -> Result<ScopeStackBinding<'_>> {
1700 capture_scope_stack_thread(&self.host)
1701 }
1702
1703 pub fn scope_stack_active(&self) -> bool {
1705 unsafe { (self.host.scope_stack_active)() }
1706 }
1707
1708 pub fn bind_scope_stack_thread<'a>(
1710 &'a self,
1711 stack: &'a ScopeStack<'a>,
1712 ) -> Result<ThreadScopeStackGuard<'a>> {
1713 let previous = self.capture_scope_stack_thread()?;
1714 let status = stack.set_thread();
1715 if status == NemoRelayStatus::Ok {
1716 Ok(ThreadScopeStackGuard {
1717 previous: Some(previous),
1718 })
1719 } else {
1720 let _ = previous.restore();
1721 Err(format!("scope_stack_set_thread failed: {status:?}"))
1722 }
1723 }
1724}
1725
1726impl From<ScopeType> for NemoRelayNativeScopeType {
1727 fn from(value: ScopeType) -> Self {
1728 match value {
1729 ScopeType::Agent => Self::Agent,
1730 ScopeType::Function => Self::Function,
1731 ScopeType::Tool => Self::Tool,
1732 ScopeType::Llm => Self::Llm,
1733 ScopeType::Retriever => Self::Retriever,
1734 ScopeType::Embedder => Self::Embedder,
1735 ScopeType::Reranker => Self::Reranker,
1736 ScopeType::Guardrail => Self::Guardrail,
1737 ScopeType::Evaluator => Self::Evaluator,
1738 ScopeType::Custom => Self::Custom,
1739 ScopeType::Unknown => Self::Unknown,
1740 }
1741 }
1742}
1743
1744pub struct ScopeGuard<'a> {
1750 runtime: &'a PluginRuntime,
1751 handle: Option<ScopeHandle<'a>>,
1752}
1753unsafe impl Send for ScopeGuard<'_> {}
1754
1755impl<'a> ScopeGuard<'a> {
1756 pub fn handle(&self) -> Option<&ScopeHandle<'a>> {
1758 self.handle.as_ref()
1759 }
1760
1761 pub fn close(&mut self, output: Option<&Json>, metadata: Option<&Json>) -> Result<()> {
1763 let Some(handle) = self.handle.as_ref() else {
1764 return Ok(());
1765 };
1766 self.runtime.pop_scope(handle, output, metadata)?;
1767 self.handle.take();
1768 Ok(())
1769 }
1770}
1771
1772impl Drop for ScopeGuard<'_> {
1773 fn drop(&mut self) {
1774 if let Some(handle) = self.handle.take() {
1775 let _ = self.runtime.pop_scope(&handle, None, None);
1776 }
1777 }
1778}
1779
1780pub struct ThreadScopeStackGuard<'a> {
1782 previous: Option<ScopeStackBinding<'a>>,
1783}
1784
1785impl ThreadScopeStackGuard<'_> {
1786 pub fn restore(mut self) -> Result<()> {
1788 let Some(previous) = self.previous.take() else {
1789 return Ok(());
1790 };
1791 let status = previous.restore();
1792 if status == NemoRelayStatus::Ok {
1793 Ok(())
1794 } else {
1795 Err(format!("scope_stack_restore_thread failed: {status:?}"))
1796 }
1797 }
1798}
1799
1800impl Drop for ThreadScopeStackGuard<'_> {
1801 fn drop(&mut self) {
1802 if let Some(previous) = self.previous.take() {
1803 let _ = previous.restore();
1804 }
1805 }
1806}
1807
1808pub struct LlmStream {
1810 host: NemoRelayNativeHostApiV1,
1811 raw: NemoRelayNativeLlmStreamV1,
1812 finished: bool,
1813}
1814
1815unsafe impl Send for LlmStream {}
1817
1818impl LlmStream {
1819 pub unsafe fn from_raw(
1825 host: &NemoRelayNativeHostApiV1,
1826 mut raw: NemoRelayNativeLlmStreamV1,
1827 ) -> Result<Self> {
1828 let expected_size = std::mem::size_of::<NemoRelayNativeLlmStreamV1>();
1829 if raw.struct_size != expected_size {
1830 if raw.struct_size >= expected_size {
1831 unsafe { drop_raw_llm_stream(&mut raw) };
1832 }
1833 return Err(format!(
1834 "unsupported LLM stream struct size: {}",
1835 raw.struct_size
1836 ));
1837 }
1838 if raw.next.is_none() {
1839 unsafe { drop_raw_llm_stream(&mut raw) };
1840 return Err("LLM stream next callback was null".into());
1841 }
1842 Ok(Self {
1843 host: *host,
1844 raw,
1845 finished: false,
1846 })
1847 }
1848
1849 pub fn next_chunk(&mut self) -> Result<Option<Json>> {
1851 if self.finished {
1852 return Ok(None);
1853 }
1854 let next = self
1855 .raw
1856 .next
1857 .expect("LLM stream next callback is validated on construction");
1858 let mut out = ptr::null_mut();
1859 let status = unsafe { next(self.raw.user_data, &mut out) };
1860 match status {
1861 NemoRelayStatus::Ok => {
1862 if out.is_null() {
1863 self.finished = true;
1864 return Err("LLM stream returned null chunk".into());
1865 }
1866 let result = read_json_value(&self.host, out, "LLM stream chunk");
1867 unsafe { (self.host.string_free)(out) };
1868 match result {
1869 Ok(chunk) => Ok(Some(chunk)),
1870 Err(status) => {
1871 self.finished = true;
1872 Err(format!("LLM stream returned invalid JSON: {status:?}"))
1873 }
1874 }
1875 }
1876 NemoRelayStatus::StreamEnd => {
1877 if !out.is_null() {
1878 unsafe { (self.host.string_free)(out) };
1879 }
1880 self.finished = true;
1881 Ok(None)
1882 }
1883 other => {
1884 if !out.is_null() {
1885 unsafe { (self.host.string_free)(out) };
1886 }
1887 self.finished = true;
1888 Err(format!("LLM stream failed: {other:?}"))
1889 }
1890 }
1891 }
1892
1893 pub fn cancel(&mut self) -> Result<()> {
1895 if self.finished {
1896 return Ok(());
1897 }
1898 if let Some(cancel) = self.raw.cancel {
1899 let status = unsafe { cancel(self.raw.user_data) };
1900 if status != NemoRelayStatus::Ok {
1901 return Err(format!("LLM stream cancel failed: {status:?}"));
1902 }
1903 }
1904 self.finished = true;
1905 Ok(())
1906 }
1907}
1908
1909impl Iterator for LlmStream {
1910 type Item = Result<Json>;
1911
1912 fn next(&mut self) -> Option<Self::Item> {
1913 match self.next_chunk() {
1914 Ok(Some(chunk)) => Some(Ok(chunk)),
1915 Ok(None) => None,
1916 Err(message) => Some(Err(message)),
1917 }
1918 }
1919}
1920
1921unsafe fn drop_raw_llm_stream(raw: &mut NemoRelayNativeLlmStreamV1) {
1922 if let Some(drop_fn) = raw.drop.take() {
1923 unsafe { drop_fn(raw.user_data) };
1924 }
1925 raw.user_data = ptr::null_mut();
1926}
1927
1928impl Drop for LlmStream {
1929 fn drop(&mut self) {
1930 if !self.finished {
1931 if let Some(cancel) = self.raw.cancel {
1932 let _ = unsafe { cancel(self.raw.user_data) };
1933 }
1934 self.finished = true;
1935 }
1936 unsafe { drop_raw_llm_stream(&mut self.raw) };
1937 }
1938}
1939
1940pub struct ScopeHandle<'a> {
1942 host: &'a NemoRelayNativeHostApiV1,
1943 ptr: *mut NemoRelayNativeScopeHandle,
1944}
1945unsafe impl Send for ScopeHandle<'_> {}
1946
1947impl<'a> ScopeHandle<'a> {
1948 pub fn as_ptr(&self) -> *const NemoRelayNativeScopeHandle {
1950 self.ptr
1951 }
1952}
1953
1954impl Drop for ScopeHandle<'_> {
1955 fn drop(&mut self) {
1956 unsafe { (self.host.scope_handle_free)(self.ptr) };
1957 }
1958}
1959
1960pub struct ScopeStack<'a> {
1962 host: &'a NemoRelayNativeHostApiV1,
1963 ptr: *mut NemoRelayNativeScopeStack,
1964}
1965unsafe impl Send for ScopeStack<'_> {}
1966
1967impl<'a> ScopeStack<'a> {
1968 pub fn as_ptr(&self) -> *const NemoRelayNativeScopeStack {
1970 self.ptr
1971 }
1972
1973 pub fn set_thread(&self) -> NemoRelayStatus {
1979 unsafe { (self.host.scope_stack_set_thread)(self.ptr) }
1980 }
1981
1982 pub fn with_current<F>(&self, f: F) -> Result<()>
1984 where
1985 F: FnOnce() -> Result<()>,
1986 {
1987 struct State<F> {
1988 f: Option<F>,
1989 error: Option<String>,
1990 }
1991
1992 unsafe extern "C" fn trampoline<F>(user_data: *mut c_void) -> NemoRelayStatus
1993 where
1994 F: FnOnce() -> Result<()>,
1995 {
1996 if user_data.is_null() {
1997 return NemoRelayStatus::NullPointer;
1998 }
1999 let state = unsafe { &mut *(user_data as *mut State<F>) };
2000 let result = catch_unwind(AssertUnwindSafe(|| {
2001 let Some(f) = state.f.take() else {
2002 return Err("scope-stack callback was already consumed".to_string());
2003 };
2004 f()
2005 }));
2006 match result {
2007 Ok(Ok(())) => NemoRelayStatus::Ok,
2008 Ok(Err(message)) => {
2009 state.error = Some(message);
2010 NemoRelayStatus::Internal
2011 }
2012 Err(_) => {
2013 state.error = Some("scope-stack callback panicked".into());
2014 NemoRelayStatus::Internal
2015 }
2016 }
2017 }
2018
2019 let mut state = State {
2020 f: Some(f),
2021 error: None,
2022 };
2023 let status = unsafe {
2024 (self.host.scope_stack_with_current)(
2025 self.ptr,
2026 trampoline::<F>,
2027 (&mut state as *mut State<_>).cast(),
2028 )
2029 };
2030 if status == NemoRelayStatus::Ok {
2031 Ok(())
2032 } else {
2033 Err(state
2034 .error
2035 .unwrap_or_else(|| format!("scope_stack_with_current failed: {status:?}")))
2036 }
2037 }
2038}
2039
2040impl Drop for ScopeStack<'_> {
2041 fn drop(&mut self) {
2042 unsafe { (self.host.scope_stack_free)(self.ptr) };
2043 }
2044}
2045
2046pub struct ScopeStackBinding<'a> {
2048 host: &'a NemoRelayNativeHostApiV1,
2049 ptr: *mut NemoRelayNativeScopeStackBinding,
2050}
2051unsafe impl Send for ScopeStackBinding<'_> {}
2052
2053impl<'a> ScopeStackBinding<'a> {
2054 pub fn restore(mut self) -> NemoRelayStatus {
2056 let ptr = std::mem::replace(&mut self.ptr, ptr::null_mut());
2057 unsafe { (self.host.scope_stack_restore_thread)(ptr) }
2058 }
2059}
2060
2061impl Drop for ScopeStackBinding<'_> {
2062 fn drop(&mut self) {
2063 if !self.ptr.is_null() {
2064 unsafe { (self.host.scope_stack_binding_free)(self.ptr) };
2065 }
2066 }
2067}
2068
2069pub fn current_scope(host: &NemoRelayNativeHostApiV1) -> Result<ScopeHandle<'_>> {
2071 let mut out = ptr::null_mut();
2072 let status = unsafe { (host.scope_get_current)(&mut out) };
2073 if status == NemoRelayStatus::Ok && !out.is_null() {
2074 Ok(ScopeHandle { host, ptr: out })
2075 } else {
2076 Err(format!("scope_get_current failed: {status:?}"))
2077 }
2078}
2079
2080pub fn push_scope<'a>(
2082 host: &'a NemoRelayNativeHostApiV1,
2083 name: &str,
2084 scope_type: NemoRelayNativeScopeType,
2085 data: Option<&Json>,
2086 metadata: Option<&Json>,
2087 input: Option<&Json>,
2088) -> Result<ScopeHandle<'a>> {
2089 let name =
2090 HostString::new(host, name).ok_or_else(|| "failed to allocate scope name".to_string())?;
2091 let data = OptionalHostJson::new(host, data)?;
2092 let metadata = OptionalHostJson::new(host, metadata)?;
2093 let input = OptionalHostJson::new(host, input)?;
2094 let mut out = ptr::null_mut();
2095 let status = unsafe {
2096 (host.scope_push)(
2097 name.as_ptr(),
2098 scope_type,
2099 ptr::null(),
2100 0,
2101 data.as_ptr(),
2102 metadata.as_ptr(),
2103 input.as_ptr(),
2104 ptr::null(),
2105 &mut out,
2106 )
2107 };
2108 if status == NemoRelayStatus::Ok && !out.is_null() {
2109 Ok(ScopeHandle { host, ptr: out })
2110 } else {
2111 Err(format!("scope_push failed: {status:?}"))
2112 }
2113}
2114
2115pub fn pop_scope(
2117 host: &NemoRelayNativeHostApiV1,
2118 handle: &ScopeHandle<'_>,
2119 output: Option<&Json>,
2120 metadata: Option<&Json>,
2121) -> Result<()> {
2122 let output = OptionalHostJson::new(host, output)?;
2123 let metadata = OptionalHostJson::new(host, metadata)?;
2124 let status = unsafe {
2125 (host.scope_pop)(
2126 handle.as_ptr(),
2127 output.as_ptr(),
2128 metadata.as_ptr(),
2129 ptr::null(),
2130 )
2131 };
2132 if status == NemoRelayStatus::Ok {
2133 Ok(())
2134 } else {
2135 Err(format!("scope_pop failed: {status:?}"))
2136 }
2137}
2138
2139pub fn emit_mark(
2141 host: &NemoRelayNativeHostApiV1,
2142 name: &str,
2143 data: Option<&Json>,
2144 metadata: Option<&Json>,
2145) -> Result<()> {
2146 let name =
2147 HostString::new(host, name).ok_or_else(|| "failed to allocate mark name".to_string())?;
2148 let data = OptionalHostJson::new(host, data)?;
2149 let metadata = OptionalHostJson::new(host, metadata)?;
2150 let status = unsafe {
2151 (host.emit_mark)(
2152 name.as_ptr(),
2153 ptr::null(),
2154 data.as_ptr(),
2155 metadata.as_ptr(),
2156 ptr::null(),
2157 )
2158 };
2159 if status == NemoRelayStatus::Ok {
2160 Ok(())
2161 } else {
2162 Err(format!("emit_mark failed: {status:?}"))
2163 }
2164}
2165
2166#[allow(clippy::too_many_arguments)] fn emit_mark_v2_call(
2168 host: &NemoRelayNativeHostApiV1,
2169 emit_mark_v2: NemoRelayNativeEmitMarkV2Fn,
2170 name: &str,
2171 data: Option<&Json>,
2172 metadata: Option<&Json>,
2173 data_schema: Option<&DataSchema>,
2174 severity: Option<LogSeverity>,
2175) -> Result<()> {
2176 let name =
2177 HostString::new(host, name).ok_or_else(|| "failed to allocate mark name".to_string())?;
2178 let data = OptionalHostJson::new(host, data)?;
2179 let metadata = OptionalHostJson::new(host, metadata)?;
2180 let data_schema = data_schema
2181 .map(|value| {
2182 HostString::from_json(host, value)
2183 .ok_or_else(|| "failed to serialize mark data schema".to_string())
2184 })
2185 .transpose()?;
2186 let severity = severity
2187 .map(|value| {
2188 serde_json::to_value(value)
2189 .map_err(|err| format!("failed to serialize mark severity: {err}"))
2190 .and_then(|value| {
2191 value
2192 .as_str()
2193 .ok_or_else(|| "mark severity did not serialize as a string".to_string())
2194 .and_then(|value| {
2195 HostString::new(host, value)
2196 .ok_or_else(|| "failed to allocate mark severity".to_string())
2197 })
2198 })
2199 })
2200 .transpose()?;
2201 let status = unsafe {
2202 emit_mark_v2(
2203 name.as_ptr(),
2204 ptr::null(),
2205 data.as_ptr(),
2206 metadata.as_ptr(),
2207 data_schema
2208 .as_ref()
2209 .map(HostString::as_ptr)
2210 .unwrap_or(ptr::null()),
2211 severity
2212 .as_ref()
2213 .map(HostString::as_ptr)
2214 .unwrap_or(ptr::null()),
2215 ptr::null(),
2216 )
2217 };
2218 if status == NemoRelayStatus::Ok {
2219 Ok(())
2220 } else {
2221 Err(format!("emit_mark_v2 failed: {status:?}"))
2222 }
2223}
2224
2225pub fn create_scope_stack(host: &NemoRelayNativeHostApiV1) -> Result<ScopeStack<'_>> {
2227 let mut out = ptr::null_mut();
2228 let status = unsafe { (host.scope_stack_create)(&mut out) };
2229 if status == NemoRelayStatus::Ok && !out.is_null() {
2230 Ok(ScopeStack { host, ptr: out })
2231 } else {
2232 Err(format!("scope_stack_create failed: {status:?}"))
2233 }
2234}
2235
2236pub fn capture_scope_stack_thread(
2238 host: &NemoRelayNativeHostApiV1,
2239) -> Result<ScopeStackBinding<'_>> {
2240 let mut out = ptr::null_mut();
2241 let status = unsafe { (host.scope_stack_capture_thread)(&mut out) };
2242 if status == NemoRelayStatus::Ok && !out.is_null() {
2243 Ok(ScopeStackBinding { host, ptr: out })
2244 } else {
2245 Err(format!("scope_stack_capture_thread failed: {status:?}"))
2246 }
2247}
2248
2249pub trait NativePlugin: Send + 'static {
2251 fn plugin_kind(&self) -> &str;
2253
2254 fn allows_multiple_components(&self) -> bool {
2256 true
2257 }
2258
2259 fn executor_config(&self) -> NativeExecutorConfig {
2265 NativeExecutorConfig::default()
2266 }
2267
2268 fn executor_config_for_component(
2274 &self,
2275 plugin_config: &Map<String, Json>,
2276 ) -> Result<NativeExecutorConfig> {
2277 self.executor_config().with_component_config(plugin_config)
2278 }
2279
2280 fn validate(&self, plugin_config: &Map<String, Json>) -> Vec<ConfigDiagnostic> {
2282 self.executor_config_for_component(plugin_config)
2283 .err()
2284 .map(|message| ConfigDiagnostic {
2285 level: DiagnosticLevel::Error,
2286 code: "native_executor_config.invalid".into(),
2287 component: None,
2288 field: Some("executor.worker_threads".into()),
2289 message,
2290 })
2291 .into_iter()
2292 .collect()
2293 }
2294
2295 fn register(
2297 &mut self,
2298 plugin_config: &Map<String, Json>,
2299 ctx: &mut PluginContext<'_>,
2300 ) -> Result<()>;
2301}
2302
2303pub struct PluginContext<'a> {
2305 host: &'a NemoRelayNativeHostApiV1,
2306 raw: *mut NemoRelayNativePluginContext,
2307 executor: Arc<async_sdk::NativeExecutor>,
2308}
2309
2310#[allow(clippy::not_unsafe_ptr_arg_deref)]
2311impl<'a> PluginContext<'a> {
2312 pub unsafe fn from_raw(
2317 host: &'a NemoRelayNativeHostApiV1,
2318 raw: *mut NemoRelayNativePluginContext,
2319 ) -> Self {
2320 Self {
2321 host,
2322 raw,
2323 executor: async_sdk::NativeExecutor::new(NativeExecutorConfig::default(), "standalone"),
2324 }
2325 }
2326
2327 unsafe fn from_raw_with_executor(
2328 host: &'a NemoRelayNativeHostApiV1,
2329 raw: *mut NemoRelayNativePluginContext,
2330 executor: Arc<async_sdk::NativeExecutor>,
2331 ) -> Self {
2332 Self {
2333 host,
2334 raw,
2335 executor,
2336 }
2337 }
2338
2339 pub fn host_api(&self) -> &'a NemoRelayNativeHostApiV1 {
2341 self.host
2342 }
2343
2344 pub fn runtime(&self) -> PluginRuntime {
2346 PluginRuntime::from_context(self.host, self.raw)
2347 }
2348
2349 pub fn register_conditional_middleware_guardrail<F>(
2354 &mut self,
2355 name: &str,
2356 kinds: &std::collections::BTreeSet<RuntimeRegistrationKind>,
2357 registration_name: &str,
2358 callback: F,
2359 ) -> Result<()>
2360 where
2361 F: Fn(&std::collections::BTreeSet<RuntimeRegistrationKind>, &str) -> Option<String>
2362 + Send
2363 + Sync
2364 + 'static,
2365 {
2366 if self.host.abi_version < NEMO_RELAY_NATIVE_ABI_VERSION
2367 || self.host.struct_size < std::mem::size_of::<NemoRelayNativeHostApiV4>()
2368 {
2369 return Err("host does not support conditional middleware guardrails".into());
2370 }
2371 let v4 = unsafe { &*(self.host as *const _ as *const NemoRelayNativeHostApiV4) };
2372 let name = HostString::new(self.host, name)
2373 .ok_or_else(|| "failed to allocate gate name".to_string())?;
2374 let kinds = HostString::from_json(self.host, kinds)
2375 .ok_or_else(|| "failed to serialize runtime registration kinds".to_string())?;
2376 let registration_name = HostString::new(self.host, registration_name)
2377 .ok_or_else(|| "failed to allocate target name".to_string())?;
2378 let user_data = typed_callback_user_data(self.host, callback);
2379 let status = unsafe {
2380 (v4.plugin_context_register_conditional_middleware_guardrail_callback)(
2381 self.raw,
2382 name.as_ptr(),
2383 kinds.as_ptr(),
2384 registration_name.as_ptr(),
2385 typed_conditional_middleware_trampoline::<F>,
2386 user_data,
2387 Some(drop_typed_callback::<F>),
2388 )
2389 };
2390 finish_typed_registration(
2391 self.host,
2392 status,
2393 user_data,
2394 "register conditional middleware guardrail",
2395 )
2396 }
2397
2398 pub fn register_subscriber<F>(&mut self, name: &str, callback: F) -> Result<()>
2400 where
2401 F: Fn(&Event) + Send + Sync + 'static,
2402 {
2403 let user_data = typed_callback_user_data(self.host, callback);
2404 let status = unsafe {
2405 self.register_subscriber_raw(
2406 name,
2407 typed_subscriber_trampoline::<F>,
2408 user_data,
2409 Some(drop_typed_callback::<F>),
2410 )
2411 };
2412 finish_typed_registration(self.host, status, user_data, "subscriber")
2413 }
2414
2415 pub unsafe fn register_subscriber_raw(
2422 &mut self,
2423 name: &str,
2424 cb: NemoRelayNativeEventSubscriberCb,
2425 user_data: *mut c_void,
2426 free_fn: NemoRelayNativeFreeFn,
2427 ) -> NemoRelayStatus {
2428 self.with_name_and_callback(name, user_data, free_fn, |host, name| unsafe {
2429 (host.plugin_context_register_subscriber)(self.raw, name, cb, user_data, free_fn)
2430 })
2431 }
2432
2433 pub unsafe fn register_mark_sanitize_guardrail_raw(
2440 &mut self,
2441 name: &str,
2442 priority: i32,
2443 cb: NemoRelayNativeEventSanitizeCb,
2444 user_data: *mut c_void,
2445 free_fn: NemoRelayNativeFreeFn,
2446 ) -> NemoRelayStatus {
2447 self.with_name_and_callback(name, user_data, free_fn, |host, name| unsafe {
2448 (host.plugin_context_register_mark_sanitize_guardrail)(
2449 self.raw, name, priority, cb, user_data, free_fn,
2450 )
2451 })
2452 }
2453
2454 pub unsafe fn register_scope_sanitize_start_guardrail_raw(
2461 &mut self,
2462 name: &str,
2463 priority: i32,
2464 cb: NemoRelayNativeEventSanitizeCb,
2465 user_data: *mut c_void,
2466 free_fn: NemoRelayNativeFreeFn,
2467 ) -> NemoRelayStatus {
2468 self.with_name_and_callback(name, user_data, free_fn, |host, name| unsafe {
2469 (host.plugin_context_register_scope_sanitize_start_guardrail)(
2470 self.raw, name, priority, cb, user_data, free_fn,
2471 )
2472 })
2473 }
2474
2475 pub unsafe fn register_scope_sanitize_end_guardrail_raw(
2482 &mut self,
2483 name: &str,
2484 priority: i32,
2485 cb: NemoRelayNativeEventSanitizeCb,
2486 user_data: *mut c_void,
2487 free_fn: NemoRelayNativeFreeFn,
2488 ) -> NemoRelayStatus {
2489 self.with_name_and_callback(name, user_data, free_fn, |host, name| unsafe {
2490 (host.plugin_context_register_scope_sanitize_end_guardrail)(
2491 self.raw, name, priority, cb, user_data, free_fn,
2492 )
2493 })
2494 }
2495
2496 pub unsafe fn register_tool_sanitize_request_guardrail_raw(
2503 &mut self,
2504 name: &str,
2505 priority: i32,
2506 cb: NemoRelayNativeToolJsonCb,
2507 user_data: *mut c_void,
2508 free_fn: NemoRelayNativeFreeFn,
2509 ) -> NemoRelayStatus {
2510 self.with_name_and_callback(name, user_data, free_fn, |host, name| unsafe {
2511 (host.plugin_context_register_tool_sanitize_request_guardrail)(
2512 self.raw, name, priority, cb, user_data, free_fn,
2513 )
2514 })
2515 }
2516
2517 pub unsafe fn register_tool_sanitize_response_guardrail_raw(
2524 &mut self,
2525 name: &str,
2526 priority: i32,
2527 cb: NemoRelayNativeToolJsonCb,
2528 user_data: *mut c_void,
2529 free_fn: NemoRelayNativeFreeFn,
2530 ) -> NemoRelayStatus {
2531 self.with_name_and_callback(name, user_data, free_fn, |host, name| unsafe {
2532 (host.plugin_context_register_tool_sanitize_response_guardrail)(
2533 self.raw, name, priority, cb, user_data, free_fn,
2534 )
2535 })
2536 }
2537
2538 pub unsafe fn register_tool_conditional_execution_guardrail_raw(
2545 &mut self,
2546 name: &str,
2547 priority: i32,
2548 cb: NemoRelayNativeToolConditionalCb,
2549 user_data: *mut c_void,
2550 free_fn: NemoRelayNativeFreeFn,
2551 ) -> NemoRelayStatus {
2552 self.with_name_and_callback(name, user_data, free_fn, |host, name| unsafe {
2553 (host.plugin_context_register_tool_conditional_execution_guardrail)(
2554 self.raw, name, priority, cb, user_data, free_fn,
2555 )
2556 })
2557 }
2558
2559 pub unsafe fn register_tool_request_intercept_raw(
2566 &mut self,
2567 name: &str,
2568 priority: i32,
2569 break_chain: bool,
2570 cb: NemoRelayNativeToolJsonCb,
2571 user_data: *mut c_void,
2572 free_fn: NemoRelayNativeFreeFn,
2573 ) -> NemoRelayStatus {
2574 self.with_name_and_callback(name, user_data, free_fn, |host, name| unsafe {
2575 (host.plugin_context_register_tool_request_intercept)(
2576 self.raw,
2577 name,
2578 priority,
2579 break_chain,
2580 cb,
2581 user_data,
2582 free_fn,
2583 )
2584 })
2585 }
2586
2587 pub unsafe fn register_tool_execution_intercept_raw(
2594 &mut self,
2595 name: &str,
2596 priority: i32,
2597 cb: NemoRelayNativeToolExecutionCb,
2598 user_data: *mut c_void,
2599 free_fn: NemoRelayNativeFreeFn,
2600 ) -> NemoRelayStatus {
2601 self.with_name_and_callback(name, user_data, free_fn, |host, name| unsafe {
2602 (host.plugin_context_register_tool_execution_intercept)(
2603 self.raw, name, priority, cb, user_data, free_fn,
2604 )
2605 })
2606 }
2607
2608 pub unsafe fn register_llm_sanitize_request_guardrail_raw(
2615 &mut self,
2616 name: &str,
2617 priority: i32,
2618 cb: NemoRelayNativeLlmSanitizeRequestCb,
2619 user_data: *mut c_void,
2620 free_fn: NemoRelayNativeFreeFn,
2621 ) -> NemoRelayStatus {
2622 self.with_name_and_callback(name, user_data, free_fn, |host, name| unsafe {
2623 (host.plugin_context_register_llm_sanitize_request_guardrail)(
2624 self.raw, name, priority, cb, user_data, free_fn,
2625 )
2626 })
2627 }
2628
2629 pub unsafe fn register_llm_sanitize_response_guardrail_raw(
2636 &mut self,
2637 name: &str,
2638 priority: i32,
2639 cb: NemoRelayNativeLlmSanitizeResponseCb,
2640 user_data: *mut c_void,
2641 free_fn: NemoRelayNativeFreeFn,
2642 ) -> NemoRelayStatus {
2643 self.with_name_and_callback(name, user_data, free_fn, |host, name| unsafe {
2644 (host.plugin_context_register_llm_sanitize_response_guardrail)(
2645 self.raw, name, priority, cb, user_data, free_fn,
2646 )
2647 })
2648 }
2649
2650 pub unsafe fn register_llm_conditional_execution_guardrail_raw(
2657 &mut self,
2658 name: &str,
2659 priority: i32,
2660 cb: NemoRelayNativeLlmConditionalCb,
2661 user_data: *mut c_void,
2662 free_fn: NemoRelayNativeFreeFn,
2663 ) -> NemoRelayStatus {
2664 self.with_name_and_callback(name, user_data, free_fn, |host, name| unsafe {
2665 (host.plugin_context_register_llm_conditional_execution_guardrail)(
2666 self.raw, name, priority, cb, user_data, free_fn,
2667 )
2668 })
2669 }
2670
2671 pub unsafe fn register_llm_request_intercept_raw(
2678 &mut self,
2679 name: &str,
2680 priority: i32,
2681 break_chain: bool,
2682 cb: NemoRelayNativeLlmRequestInterceptCb,
2683 user_data: *mut c_void,
2684 free_fn: NemoRelayNativeFreeFn,
2685 ) -> NemoRelayStatus {
2686 self.with_name_and_callback(name, user_data, free_fn, |host, name| unsafe {
2687 (host.plugin_context_register_llm_request_intercept)(
2688 self.raw,
2689 name,
2690 priority,
2691 break_chain,
2692 cb,
2693 user_data,
2694 free_fn,
2695 )
2696 })
2697 }
2698
2699 pub unsafe fn register_llm_execution_intercept_raw(
2706 &mut self,
2707 name: &str,
2708 priority: i32,
2709 cb: NemoRelayNativeLlmExecutionCb,
2710 user_data: *mut c_void,
2711 free_fn: NemoRelayNativeFreeFn,
2712 ) -> NemoRelayStatus {
2713 self.with_name_and_callback(name, user_data, free_fn, |host, name| unsafe {
2714 (host.plugin_context_register_llm_execution_intercept)(
2715 self.raw, name, priority, cb, user_data, free_fn,
2716 )
2717 })
2718 }
2719
2720 pub unsafe fn register_llm_stream_execution_intercept_raw(
2727 &mut self,
2728 name: &str,
2729 priority: i32,
2730 cb: NemoRelayNativeLlmStreamExecutionCb,
2731 user_data: *mut c_void,
2732 free_fn: NemoRelayNativeFreeFn,
2733 ) -> NemoRelayStatus {
2734 self.with_name_and_callback(name, user_data, free_fn, |host, name| unsafe {
2735 (host.plugin_context_register_llm_stream_execution_intercept)(
2736 self.raw, name, priority, cb, user_data, free_fn,
2737 )
2738 })
2739 }
2740
2741 #[allow(clippy::too_many_arguments)] pub unsafe fn register_async_middleware_raw(
2756 &mut self,
2757 kind: NemoRelayNativeAsyncMiddlewareKind,
2758 name: &str,
2759 priority: i32,
2760 break_chain: bool,
2761 cb: NemoRelayNativeAsyncMiddlewareCb,
2762 user_data: *mut c_void,
2763 free_fn: NemoRelayNativeFreeFn,
2764 ) -> NemoRelayStatus {
2765 if self.host.abi_version < NEMO_RELAY_NATIVE_ABI_VERSION_ASYNC_MIDDLEWARE
2766 || self.host.struct_size < std::mem::size_of::<NemoRelayNativeHostApiV3>()
2767 {
2768 if let Some(free_fn) = free_fn {
2769 unsafe { free_fn(user_data) };
2770 }
2771 return NemoRelayStatus::InvalidArg;
2772 }
2773 let host = unsafe { &*(self.host as *const _ as *const NemoRelayNativeHostApiV3) };
2774 self.with_name_and_callback(name, user_data, free_fn, |_, name| unsafe {
2775 (host.plugin_context_register_async_middleware)(
2776 self.raw,
2777 kind as u32,
2778 name,
2779 priority,
2780 break_chain,
2781 cb,
2782 user_data,
2783 free_fn,
2784 )
2785 })
2786 }
2787
2788 pub unsafe fn register_async_stream_middleware_raw(
2800 &mut self,
2801 name: &str,
2802 priority: i32,
2803 cb: NemoRelayNativeAsyncStreamMiddlewareCb,
2804 user_data: *mut c_void,
2805 free_fn: NemoRelayNativeFreeFn,
2806 ) -> NemoRelayStatus {
2807 if self.host.abi_version < NEMO_RELAY_NATIVE_ABI_VERSION_ASYNC_MIDDLEWARE
2808 || self.host.struct_size < std::mem::size_of::<NemoRelayNativeHostApiV3>()
2809 {
2810 if let Some(free_fn) = free_fn {
2811 unsafe { free_fn(user_data) };
2812 }
2813 return NemoRelayStatus::InvalidArg;
2814 }
2815 let host = unsafe { &*(self.host as *const _ as *const NemoRelayNativeHostApiV3) };
2816 self.with_name_and_callback(name, user_data, free_fn, |_, name| unsafe {
2817 (host.plugin_context_register_async_stream_middleware)(
2818 self.raw, name, priority, cb, user_data, free_fn,
2819 )
2820 })
2821 }
2822
2823 fn with_name_and_callback(
2824 &self,
2825 name: &str,
2826 user_data: *mut c_void,
2827 free_fn: NemoRelayNativeFreeFn,
2828 f: impl FnOnce(&NemoRelayNativeHostApiV1, *const NemoRelayNativeString) -> NemoRelayStatus,
2829 ) -> NemoRelayStatus {
2830 let name = match HostString::try_new(self.host, name) {
2831 Ok(name) => name,
2832 Err(status) => {
2833 if let Some(free_fn) = free_fn {
2834 unsafe { free_fn(user_data) };
2835 }
2836 return status;
2837 }
2838 };
2839 f(self.host, name.as_ptr())
2840 }
2841}
2842
2843struct TypedCallback<F> {
2844 host: NemoRelayNativeHostApiV1,
2845 callback: F,
2846}
2847
2848fn typed_callback_user_data<F>(host: &NemoRelayNativeHostApiV1, callback: F) -> *mut c_void {
2849 Box::into_raw(Box::new(TypedCallback {
2850 host: *host,
2851 callback,
2852 })) as *mut c_void
2853}
2854
2855unsafe extern "C" fn drop_typed_callback<F>(user_data: *mut c_void) {
2856 if !user_data.is_null() {
2857 let callback = unsafe { Box::from_raw(user_data as *mut TypedCallback<F>) };
2858 let host = callback.host;
2859 if catch_unwind(AssertUnwindSafe(|| drop(callback))).is_err() {
2860 set_last_error(&host, "native plugin typed callback state drop panicked");
2861 }
2862 }
2863}
2864
2865fn finish_typed_registration(
2866 host: &NemoRelayNativeHostApiV1,
2867 status: NemoRelayStatus,
2868 user_data: *mut c_void,
2869 label: &str,
2870) -> Result<()> {
2871 let _ = user_data;
2872 if status == NemoRelayStatus::Ok {
2873 Ok(())
2874 } else {
2875 Err(status_error(host, status, label))
2876 }
2877}
2878
2879fn status_error(host: &NemoRelayNativeHostApiV1, status: NemoRelayStatus, label: &str) -> String {
2880 debug_assert_ne!(status, NemoRelayStatus::Ok);
2881 set_last_error(host, &format!("{label} failed: {status:?}"));
2882 format!("{label} failed: {status:?}")
2883}
2884
2885fn status_result(
2886 host: &NemoRelayNativeHostApiV1,
2887 status: NemoRelayStatus,
2888 label: &str,
2889) -> Result<()> {
2890 if status == NemoRelayStatus::Ok {
2891 Ok(())
2892 } else {
2893 Err(status_error(host, status, label))
2894 }
2895}
2896
2897fn callback_panic(host: &NemoRelayNativeHostApiV1, label: &str) -> NemoRelayStatus {
2898 set_last_error(host, &format!("{label} panicked"));
2899 NemoRelayStatus::Internal
2900}
2901
2902unsafe extern "C" fn typed_subscriber_trampoline<F>(
2903 user_data: *mut c_void,
2904 event_json: *const NemoRelayNativeString,
2905) -> NemoRelayStatus
2906where
2907 F: Fn(&Event) + Send + Sync + 'static,
2908{
2909 if user_data.is_null() {
2910 return NemoRelayStatus::NullPointer;
2911 }
2912 let state = unsafe { &*(user_data as *const TypedCallback<F>) };
2913 let result = catch_unwind(AssertUnwindSafe(|| {
2914 let event: Event = read_json_value(&state.host, event_json, "event")?;
2915 (state.callback)(&event);
2916 Ok::<_, NemoRelayStatus>(())
2917 }));
2918 match result {
2919 Ok(Ok(())) => NemoRelayStatus::Ok,
2920 Ok(Err(status)) => status,
2921 Err(_) => callback_panic(&state.host, "subscriber callback"),
2922 }
2923}
2924
2925unsafe extern "C" fn typed_conditional_middleware_trampoline<F>(
2926 user_data: *mut c_void,
2927 kinds_json: *const NemoRelayNativeString,
2928 registration_name: *const NemoRelayNativeString,
2929 out_reason: *mut *mut NemoRelayNativeString,
2930) -> NemoRelayStatus
2931where
2932 F: Fn(&std::collections::BTreeSet<RuntimeRegistrationKind>, &str) -> Option<String>
2933 + Send
2934 + Sync
2935 + 'static,
2936{
2937 if user_data.is_null() || out_reason.is_null() {
2938 return NemoRelayStatus::NullPointer;
2939 }
2940 unsafe { *out_reason = ptr::null_mut() };
2941 let state = unsafe { &*(user_data as *const TypedCallback<F>) };
2942 let result = catch_unwind(AssertUnwindSafe(|| {
2943 let kinds = read_json_value(&state.host, kinds_json, "runtime registration kinds")?;
2944 let registration_name =
2945 read_required_host_string(&state.host, registration_name, "registration name")?;
2946 let Some(reason) = (state.callback)(&kinds, ®istration_name) else {
2947 return Ok(NemoRelayStatus::Ok);
2948 };
2949 let status = unsafe { (state.host.string_new)(reason.as_ptr(), reason.len(), out_reason) };
2950 Ok(status)
2951 }));
2952 match result {
2953 Ok(Ok(status)) => status,
2954 Ok(Err(status)) => status,
2955 Err(_) => callback_panic(&state.host, "conditional middleware callback"),
2956 }
2957}
2958
2959struct HostString<'a> {
2960 host: &'a NemoRelayNativeHostApiV1,
2961 ptr: *mut NemoRelayNativeString,
2962}
2963unsafe impl Send for HostString<'_> {}
2964
2965impl<'a> HostString<'a> {
2966 fn try_new(
2967 host: &'a NemoRelayNativeHostApiV1,
2968 value: &str,
2969 ) -> std::result::Result<Self, NemoRelayStatus> {
2970 let mut out = ptr::null_mut();
2971 let status = unsafe { (host.string_new)(value.as_ptr(), value.len(), &mut out) };
2972 if status != NemoRelayStatus::Ok {
2973 return Err(status);
2974 }
2975 if out.is_null() {
2976 return Err(NemoRelayStatus::Internal);
2977 }
2978 Ok(Self { host, ptr: out })
2979 }
2980
2981 fn new(host: &'a NemoRelayNativeHostApiV1, value: &str) -> Option<Self> {
2982 Self::try_new(host, value).ok()
2983 }
2984
2985 fn from_json<T: Serialize>(host: &'a NemoRelayNativeHostApiV1, value: &T) -> Option<Self> {
2986 serde_json::to_string(value)
2987 .ok()
2988 .and_then(|json| Self::new(host, &json))
2989 }
2990
2991 fn as_ptr(&self) -> *const NemoRelayNativeString {
2992 self.ptr
2993 }
2994}
2995
2996impl Drop for HostString<'_> {
2997 fn drop(&mut self) {
2998 unsafe { (self.host.string_free)(self.ptr) };
2999 }
3000}
3001
3002fn codec_status(host: &NemoRelayNativeHostApiV1, status: NemoRelayStatus) -> Result<()> {
3003 if status == NemoRelayStatus::Ok {
3004 Ok(())
3005 } else {
3006 Err(status_error(host, status, "LLM codec operation"))
3007 }
3008}
3009
3010fn native_codec_call<T: DeserializeOwned>(
3011 host: &NemoRelayNativeHostApiV1,
3012 call: impl FnOnce(*mut *mut NemoRelayNativeString) -> Result<()>,
3013) -> Result<T> {
3014 native_json_call(host, "LLM codec operation", call)
3015}
3016
3017fn native_json_call<T: DeserializeOwned>(
3018 host: &NemoRelayNativeHostApiV1,
3019 operation: &str,
3020 call: impl FnOnce(*mut *mut NemoRelayNativeString) -> Result<()>,
3021) -> Result<T> {
3022 let mut out = ptr::null_mut();
3023 call(&mut out)?;
3024 if out.is_null() {
3025 return Err(format!("{operation} returned null"));
3026 }
3027 let out = HostString { host, ptr: out };
3028 let text = read_host_string(host, out.as_ptr())
3029 .map_err(|_| format!("{operation} returned invalid UTF-8"))?;
3030 serde_json::from_str(&text).map_err(|error| format!("invalid {operation} result: {error}"))
3031}
3032
3033struct OptionalHostJson<'a>(Option<HostString<'a>>);
3034
3035impl<'a> OptionalHostJson<'a> {
3036 fn new(host: &'a NemoRelayNativeHostApiV1, value: Option<&Json>) -> Result<Self> {
3037 match value {
3038 Some(value) => HostString::from_json(host, value)
3039 .map(|value| Self(Some(value)))
3040 .ok_or_else(|| "failed to allocate JSON host string".into()),
3041 None => Ok(Self(None)),
3042 }
3043 }
3044
3045 fn as_ptr(&self) -> *const NemoRelayNativeString {
3046 self.0
3047 .as_ref()
3048 .map(HostString::as_ptr)
3049 .unwrap_or(ptr::null())
3050 }
3051}
3052
3053enum OwnedHostApi {
3054 V1(NemoRelayNativeHostApiV1),
3055 V3(NemoRelayNativeHostApiV3),
3056 V4(NemoRelayNativeHostApiV4),
3057}
3058
3059impl OwnedHostApi {
3060 unsafe fn copy_from(host: &NemoRelayNativeHostApiV1) -> Self {
3061 if host.abi_version >= NEMO_RELAY_NATIVE_ABI_VERSION
3062 && host.struct_size >= std::mem::size_of::<NemoRelayNativeHostApiV4>()
3063 {
3064 Self::V4(unsafe { *(host as *const _ as *const NemoRelayNativeHostApiV4) })
3065 } else if host.abi_version >= NEMO_RELAY_NATIVE_ABI_VERSION_ASYNC_MIDDLEWARE
3066 && host.struct_size >= std::mem::size_of::<NemoRelayNativeHostApiV3>()
3067 {
3068 Self::V3(unsafe { *(host as *const _ as *const NemoRelayNativeHostApiV3) })
3069 } else {
3070 Self::V1(*host)
3071 }
3072 }
3073
3074 fn v1(&self) -> &NemoRelayNativeHostApiV1 {
3075 match self {
3076 Self::V1(host) => host,
3077 Self::V3(host) => &host.v1,
3078 Self::V4(host) => &host.v3.v1,
3079 }
3080 }
3081}
3082
3083struct PluginState<P> {
3084 host: OwnedHostApi,
3085 plugin: Mutex<P>,
3086}
3087
3088unsafe extern "C" fn drop_plugin_state<P: NativePlugin>(user_data: *mut c_void) {
3089 if !user_data.is_null() {
3090 let state = unsafe { Box::from_raw(user_data as *mut PluginState<P>) };
3091 let host = *state.host.v1();
3092 if catch_unwind(AssertUnwindSafe(|| drop(state))).is_err() {
3093 set_last_error(&host, "native plugin state drop panicked");
3094 }
3095 }
3096}
3097
3098unsafe extern "C" fn validate_trampoline<P: NativePlugin>(
3099 user_data: *mut c_void,
3100 plugin_config_json: *const NemoRelayNativeString,
3101 out_diagnostics_json: *mut *mut NemoRelayNativeString,
3102) -> NemoRelayStatus {
3103 if user_data.is_null() || out_diagnostics_json.is_null() {
3104 return NemoRelayStatus::NullPointer;
3105 }
3106 unsafe { *out_diagnostics_json = ptr::null_mut() };
3107 let state = unsafe { &*(user_data as *const PluginState<P>) };
3108 let result = catch_unwind(AssertUnwindSafe(|| {
3109 let host = state.host.v1();
3110 let config = match read_json_object(host, plugin_config_json) {
3111 Ok(config) => config,
3112 Err(status) => return status,
3113 };
3114 let plugin = match state.plugin.lock() {
3115 Ok(plugin) => plugin,
3116 Err(_) => {
3117 set_last_error(host, "native plugin state lock poisoned");
3118 return NemoRelayStatus::Internal;
3119 }
3120 };
3121 let diagnostics = plugin.validate(&config);
3122 write_json(host, &diagnostics, out_diagnostics_json)
3123 }));
3124 result.unwrap_or_else(|_| {
3125 set_last_error(state.host.v1(), "native plugin validate callback panicked");
3126 NemoRelayStatus::Internal
3127 })
3128}
3129
3130unsafe extern "C" fn register_trampoline<P: NativePlugin>(
3131 user_data: *mut c_void,
3132 plugin_config_json: *const NemoRelayNativeString,
3133 ctx: *mut NemoRelayNativePluginContext,
3134) -> NemoRelayStatus {
3135 if user_data.is_null() || ctx.is_null() {
3136 return NemoRelayStatus::NullPointer;
3137 }
3138 let state = unsafe { &*(user_data as *const PluginState<P>) };
3139 let result = catch_unwind(AssertUnwindSafe(|| {
3140 let host = state.host.v1();
3141 let config = match read_json_object(host, plugin_config_json) {
3142 Ok(config) => config,
3143 Err(status) => return status,
3144 };
3145 let mut plugin = match state.plugin.lock() {
3146 Ok(plugin) => plugin,
3147 Err(_) => {
3148 set_last_error(host, "native plugin state lock poisoned");
3149 return NemoRelayStatus::Internal;
3150 }
3151 };
3152 let executor_config = match plugin.executor_config_for_component(&config) {
3153 Ok(config) => config,
3154 Err(error) => {
3155 set_last_error(host, &error);
3156 return NemoRelayStatus::InvalidArg;
3157 }
3158 };
3159 let mut ctx = unsafe {
3160 PluginContext::from_raw_with_executor(
3161 host,
3162 ctx,
3163 async_sdk::NativeExecutor::new(executor_config, plugin.plugin_kind()),
3164 )
3165 };
3166 match plugin.register(&config, &mut ctx) {
3167 Ok(()) => NemoRelayStatus::Ok,
3168 Err(message) => {
3169 set_last_error(host, &message);
3170 NemoRelayStatus::Internal
3171 }
3172 }
3173 }));
3174 result.unwrap_or_else(|_| {
3175 set_last_error(state.host.v1(), "native plugin register callback panicked");
3176 NemoRelayStatus::Internal
3177 })
3178}
3179
3180fn read_json_object(
3181 host: &NemoRelayNativeHostApiV1,
3182 value: *const NemoRelayNativeString,
3183) -> std::result::Result<Map<String, Json>, NemoRelayStatus> {
3184 let value: Json = read_json_value(host, value, "plugin config")?;
3185 match value {
3186 Json::Object(map) => Ok(map),
3187 _ => {
3188 set_last_error(host, "plugin config must be a JSON object");
3189 Err(NemoRelayStatus::InvalidJson)
3190 }
3191 }
3192}
3193
3194fn read_json_value<T: DeserializeOwned>(
3195 host: &NemoRelayNativeHostApiV1,
3196 value: *const NemoRelayNativeString,
3197 label: &str,
3198) -> std::result::Result<T, NemoRelayStatus> {
3199 let text = read_required_host_string(host, value, label)?;
3200 serde_json::from_str::<T>(&text).map_err(|error| {
3201 set_last_error(host, &format!("{label} was invalid JSON: {error}"));
3202 NemoRelayStatus::InvalidJson
3203 })
3204}
3205
3206#[derive(Debug)]
3207enum HostStringReadError {
3208 Null,
3209 InvalidUtf8,
3210}
3211
3212fn read_required_host_string(
3213 host: &NemoRelayNativeHostApiV1,
3214 value: *const NemoRelayNativeString,
3215 label: &str,
3216) -> std::result::Result<String, NemoRelayStatus> {
3217 match read_host_string(host, value) {
3218 Ok(value) => Ok(value),
3219 Err(HostStringReadError::Null) => {
3220 set_last_error(host, &format!("{label} was null"));
3221 Err(NemoRelayStatus::NullPointer)
3222 }
3223 Err(HostStringReadError::InvalidUtf8) => {
3224 set_last_error(host, &format!("{label} contained invalid UTF-8"));
3225 Err(NemoRelayStatus::InvalidUtf8)
3226 }
3227 }
3228}
3229
3230fn read_host_string(
3231 host: &NemoRelayNativeHostApiV1,
3232 value: *const NemoRelayNativeString,
3233) -> std::result::Result<String, HostStringReadError> {
3234 if value.is_null() {
3235 return Err(HostStringReadError::Null);
3236 }
3237 let len = unsafe { (host.string_len)(value) };
3238 let data = unsafe { (host.string_data)(value) };
3239 if data.is_null() && len > 0 {
3240 return Err(HostStringReadError::InvalidUtf8);
3241 }
3242 let bytes = if len == 0 {
3243 &[][..]
3244 } else {
3245 unsafe { std::slice::from_raw_parts(data, len) }
3246 };
3247 std::str::from_utf8(bytes)
3248 .map(str::to_owned)
3249 .map_err(|_| HostStringReadError::InvalidUtf8)
3250}
3251
3252fn take_host_string(
3253 host: &NemoRelayNativeHostApiV1,
3254 value: *mut NemoRelayNativeString,
3255) -> Result<String> {
3256 let result = read_host_string(host, value)
3257 .map_err(|error| format!("host returned an invalid string: {error:?}"));
3258 if !value.is_null() {
3259 unsafe { (host.string_free)(value) };
3260 }
3261 result
3262}
3263
3264fn take_host_json<T: DeserializeOwned>(
3265 host: &NemoRelayNativeHostApiV1,
3266 value: *mut NemoRelayNativeString,
3267) -> Result<T> {
3268 let text = take_host_string(host, value)?;
3269 serde_json::from_str(&text).map_err(|error| format!("host returned invalid JSON: {error}"))
3270}
3271
3272fn write_json<T: Serialize>(
3273 host: &NemoRelayNativeHostApiV1,
3274 value: &T,
3275 out: *mut *mut NemoRelayNativeString,
3276) -> NemoRelayStatus {
3277 if out.is_null() {
3278 return NemoRelayStatus::NullPointer;
3279 }
3280 unsafe { *out = ptr::null_mut() };
3281 let json = serde_json::to_value(value).expect("Relay DTOs and serde_json::Value serialize");
3282 let Some(handle) = HostString::from_json(host, &json) else {
3283 set_last_error(host, "failed to allocate host string");
3284 return NemoRelayStatus::Internal;
3285 };
3286 unsafe { *out = handle.ptr };
3287 std::mem::forget(handle);
3288 NemoRelayStatus::Ok
3289}
3290
3291fn set_last_error(host: &NemoRelayNativeHostApiV1, message: &str) {
3292 if let Some(message) = HostString::new(host, message) {
3293 unsafe { (host.last_error_set)(message.as_ptr()) };
3294 }
3295}
3296
3297#[doc(hidden)]
3302pub unsafe fn __set_last_error_from_entry(host: *const NemoRelayNativeHostApiV1, message: &str) {
3303 if !host.is_null() {
3304 set_last_error(unsafe { &*host }, message);
3305 }
3306}
3307
3308pub unsafe fn export_plugin<P: NativePlugin>(
3315 host: *const NemoRelayNativeHostApiV1,
3316 out: *mut NemoRelayNativePluginV1,
3317 plugin: P,
3318) -> NemoRelayStatus {
3319 if host.is_null() || out.is_null() {
3320 return NemoRelayStatus::NullPointer;
3321 }
3322 unsafe { *out = NemoRelayNativePluginV1::default() };
3323 let host_ref = unsafe { &*host };
3324 export_plugin_checked(host_ref, out, || plugin)
3325}
3326
3327#[doc(hidden)]
3334pub unsafe fn __export_plugin_from_constructor<P, F>(
3335 host: *const NemoRelayNativeHostApiV1,
3336 out: *mut NemoRelayNativePluginV1,
3337 constructor: F,
3338) -> NemoRelayStatus
3339where
3340 P: NativePlugin,
3341 F: FnOnce() -> P,
3342{
3343 if host.is_null() || out.is_null() {
3344 return NemoRelayStatus::NullPointer;
3345 }
3346 unsafe { *out = NemoRelayNativePluginV1::default() };
3347 let host_ref = unsafe { &*host };
3348 export_plugin_checked(host_ref, out, constructor)
3349}
3350
3351fn export_plugin_checked<P, F>(
3352 host_ref: &NemoRelayNativeHostApiV1,
3353 out: *mut NemoRelayNativePluginV1,
3354 constructor: F,
3355) -> NemoRelayStatus
3356where
3357 P: NativePlugin,
3358 F: FnOnce() -> P,
3359{
3360 let supported_abi = (NEMO_RELAY_NATIVE_ABI_VERSION_LEGACY..=NEMO_RELAY_NATIVE_ABI_VERSION)
3361 .contains(&host_ref.abi_version);
3362 if !supported_abi {
3363 return NemoRelayStatus::InvalidArg;
3364 }
3365 if host_ref.struct_size < std::mem::size_of::<NemoRelayNativeHostApiV1>() {
3366 return NemoRelayStatus::InvalidArg;
3367 }
3368
3369 let plugin = constructor();
3370 let kind = plugin.plugin_kind().to_owned();
3371 let allows_multiple_components = plugin.allows_multiple_components();
3372 let Some(kind_handle) = HostString::new(host_ref, &kind) else {
3373 return NemoRelayStatus::Internal;
3374 };
3375 let state = Box::new(PluginState {
3376 host: unsafe { OwnedHostApi::copy_from(host_ref) },
3377 plugin: Mutex::new(plugin),
3378 });
3379 unsafe {
3380 *out = NemoRelayNativePluginV1 {
3381 struct_size: std::mem::size_of::<NemoRelayNativePluginV1>(),
3382 plugin_kind: kind_handle.ptr,
3383 allows_multiple_components,
3384 user_data: Box::into_raw(state) as *mut c_void,
3385 validate: Some(validate_trampoline::<P>),
3386 register: Some(register_trampoline::<P>),
3387 drop: Some(drop_plugin_state::<P>),
3388 };
3389 }
3390 std::mem::forget(kind_handle);
3391 NemoRelayStatus::Ok
3392}
3393
3394#[macro_export]
3396macro_rules! nemo_relay_plugin {
3397 ($symbol:ident, $constructor:expr) => {
3398 #[doc = "Native plugin entry symbol generated by `nemo_relay_plugin!`."]
3399 #[unsafe(no_mangle)]
3400 pub unsafe extern "C" fn $symbol(
3401 host: *const $crate::NemoRelayNativeHostApiV1,
3402 out: *mut $crate::NemoRelayNativePluginV1,
3403 ) -> $crate::NemoRelayStatus {
3404 match ::std::panic::catch_unwind(::std::panic::AssertUnwindSafe(|| unsafe {
3405 $crate::__export_plugin_from_constructor(host, out, $constructor)
3406 })) {
3407 Ok(status) => status,
3408 Err(_) => {
3409 unsafe {
3410 $crate::__set_last_error_from_entry(
3411 host,
3412 "native plugin entry callback panicked",
3413 )
3414 };
3415 $crate::NemoRelayStatus::Internal
3416 }
3417 }
3418 }
3419 };
3420}