1#![deny(rustdoc::broken_intra_doc_links, rustdoc::private_intra_doc_links)]
5
6use std::ffi::{c_char, c_void};
13use std::marker::{PhantomData, PhantomPinned};
14use std::panic::{AssertUnwindSafe, catch_unwind};
15use std::ptr;
16use std::sync::Mutex;
17
18pub use nemo_relay_types::Json;
19pub use nemo_relay_types::api::event::{
20 CategoryProfile, Event, EventCategory, PendingMarkSpec, ScopeCategory,
21};
22pub use nemo_relay_types::api::llm::{LlmAttributes, LlmRequest, LlmRequestInterceptOutcome};
23pub use nemo_relay_types::api::scope::{HandleAttributes, ScopeAttributes, ScopeType};
24pub use nemo_relay_types::api::tool::{ToolAttributes, ToolExecutionInterceptOutcome};
25pub use nemo_relay_types::codec::request::AnnotatedLlmRequest;
26pub use nemo_relay_types::codec::response::AnnotatedLlmResponse;
27pub use nemo_relay_types::plugin::{ConfigDiagnostic, DiagnosticLevel};
28use serde::{Serialize, de::DeserializeOwned};
29use serde_json::Map;
30
31pub const NEMO_RELAY_NATIVE_ABI_VERSION: u32 = 1;
33
34#[repr(i32)]
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum NemoRelayStatus {
38 Ok = 0,
40 AlreadyExists = 1,
42 NotFound = 2,
44 ScopeStackEmpty = 3,
46 GuardrailRejected = 4,
48 Internal = 5,
50 NullPointer = 6,
52 InvalidJson = 7,
54 InvalidUtf8 = 8,
56 InvalidArg = 9,
58 StreamEnd = 10,
60}
61
62#[repr(C)]
64pub struct NemoRelayNativeString {
65 _private: [u8; 0],
66 _marker: PhantomData<(*mut u8, PhantomPinned)>,
67}
68
69#[repr(C)]
71pub struct NemoRelayNativePluginContext {
72 _private: [u8; 0],
73 _marker: PhantomData<(*mut u8, PhantomPinned)>,
74}
75
76#[repr(C)]
78pub struct NemoRelayNativeScopeHandle {
79 _private: [u8; 0],
80 _marker: PhantomData<(*mut u8, PhantomPinned)>,
81}
82
83#[repr(C)]
85pub struct NemoRelayNativeScopeStack {
86 _private: [u8; 0],
87 _marker: PhantomData<(*mut u8, PhantomPinned)>,
88}
89
90#[repr(C)]
92pub struct NemoRelayNativeScopeStackBinding {
93 _private: [u8; 0],
94 _marker: PhantomData<(*mut u8, PhantomPinned)>,
95}
96
97#[repr(i32)]
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub enum NemoRelayNativeScopeType {
101 Agent = 0,
103 Function = 1,
105 Tool = 2,
107 Llm = 3,
109 Retriever = 4,
111 Embedder = 5,
113 Reranker = 6,
115 Guardrail = 7,
117 Evaluator = 8,
119 Custom = 9,
121 Unknown = 10,
123}
124
125pub type NemoRelayNativeFreeFn = Option<unsafe extern "C" fn(user_data: *mut c_void)>;
127
128pub type NemoRelayNativeWithScopeStackCb =
130 unsafe extern "C" fn(user_data: *mut c_void) -> NemoRelayStatus;
131
132pub type NemoRelayNativeToolNextFn = unsafe extern "C" fn(
134 args_json: *const NemoRelayNativeString,
135 next_ctx: *mut c_void,
136 out_json: *mut *mut NemoRelayNativeString,
137) -> NemoRelayStatus;
138
139pub type NemoRelayNativeLlmNextFn = unsafe extern "C" fn(
141 request_json: *const NemoRelayNativeString,
142 next_ctx: *mut c_void,
143 out_json: *mut *mut NemoRelayNativeString,
144) -> NemoRelayStatus;
145
146pub type NemoRelayNativeLlmStreamPollFn = unsafe extern "C" fn(
152 user_data: *mut c_void,
153 out_json: *mut *mut NemoRelayNativeString,
154) -> NemoRelayStatus;
155
156pub type NemoRelayNativeLlmStreamCancelFn =
158 Option<unsafe extern "C" fn(user_data: *mut c_void) -> NemoRelayStatus>;
159
160pub type NemoRelayNativeLlmStreamDropFn = Option<unsafe extern "C" fn(user_data: *mut c_void)>;
162
163#[repr(C)]
165pub struct NemoRelayNativeLlmStreamV1 {
166 pub struct_size: usize,
168 pub user_data: *mut c_void,
170 pub next: Option<NemoRelayNativeLlmStreamPollFn>,
172 pub cancel: NemoRelayNativeLlmStreamCancelFn,
174 pub drop: NemoRelayNativeLlmStreamDropFn,
176}
177
178impl Default for NemoRelayNativeLlmStreamV1 {
179 fn default() -> Self {
180 Self {
181 struct_size: std::mem::size_of::<Self>(),
182 user_data: ptr::null_mut(),
183 next: None,
184 cancel: None,
185 drop: None,
186 }
187 }
188}
189
190pub type NemoRelayNativeLlmStreamNextFn = unsafe extern "C" fn(
192 request_json: *const NemoRelayNativeString,
193 next_ctx: *mut c_void,
194 out_stream: *mut NemoRelayNativeLlmStreamV1,
195) -> NemoRelayStatus;
196
197pub type NemoRelayNativeEventSubscriberCb = unsafe extern "C" fn(
199 user_data: *mut c_void,
200 event_json: *const NemoRelayNativeString,
201) -> NemoRelayStatus;
202
203pub type NemoRelayNativeToolJsonCb = unsafe extern "C" fn(
205 user_data: *mut c_void,
206 name: *const NemoRelayNativeString,
207 payload_json: *const NemoRelayNativeString,
208 out_json: *mut *mut NemoRelayNativeString,
209) -> NemoRelayStatus;
210
211pub type NemoRelayNativeToolConditionalCb = unsafe extern "C" fn(
213 user_data: *mut c_void,
214 name: *const NemoRelayNativeString,
215 args_json: *const NemoRelayNativeString,
216 out_reason: *mut *mut NemoRelayNativeString,
217) -> NemoRelayStatus;
218
219pub type NemoRelayNativeToolExecutionCb = unsafe extern "C" fn(
221 user_data: *mut c_void,
222 name: *const NemoRelayNativeString,
223 args_json: *const NemoRelayNativeString,
224 next_fn: NemoRelayNativeToolNextFn,
225 next_ctx: *mut c_void,
226 out_outcome_json: *mut *mut NemoRelayNativeString,
227) -> NemoRelayStatus;
228
229pub type NemoRelayNativeLlmRequestCb = unsafe extern "C" fn(
231 user_data: *mut c_void,
232 request_json: *const NemoRelayNativeString,
233 out_request_json: *mut *mut NemoRelayNativeString,
234) -> NemoRelayStatus;
235
236pub type NemoRelayNativeJsonCb = unsafe extern "C" fn(
238 user_data: *mut c_void,
239 payload_json: *const NemoRelayNativeString,
240 out_json: *mut *mut NemoRelayNativeString,
241) -> NemoRelayStatus;
242
243pub type NemoRelayNativeLlmConditionalCb = unsafe extern "C" fn(
245 user_data: *mut c_void,
246 request_json: *const NemoRelayNativeString,
247 out_reason: *mut *mut NemoRelayNativeString,
248) -> NemoRelayStatus;
249
250pub type NemoRelayNativeLlmRequestInterceptCb = unsafe extern "C" fn(
252 user_data: *mut c_void,
253 name: *const NemoRelayNativeString,
254 request_json: *const NemoRelayNativeString,
255 annotated_json: *const NemoRelayNativeString,
256 out_outcome_json: *mut *mut NemoRelayNativeString,
257) -> NemoRelayStatus;
258
259pub type NemoRelayNativeLlmExecutionCb = unsafe extern "C" fn(
261 user_data: *mut c_void,
262 name: *const NemoRelayNativeString,
263 request_json: *const NemoRelayNativeString,
264 next_fn: NemoRelayNativeLlmNextFn,
265 next_ctx: *mut c_void,
266 out_json: *mut *mut NemoRelayNativeString,
267) -> NemoRelayStatus;
268
269pub type NemoRelayNativeLlmStreamExecutionCb = unsafe extern "C" fn(
271 user_data: *mut c_void,
272 name: *const NemoRelayNativeString,
273 request_json: *const NemoRelayNativeString,
274 next_fn: NemoRelayNativeLlmStreamNextFn,
275 next_ctx: *mut c_void,
276 out_stream: *mut NemoRelayNativeLlmStreamV1,
277) -> NemoRelayStatus;
278
279pub type NemoRelayNativePluginValidateFn = unsafe extern "C" fn(
281 user_data: *mut c_void,
282 plugin_config_json: *const NemoRelayNativeString,
283 out_diagnostics_json: *mut *mut NemoRelayNativeString,
284) -> NemoRelayStatus;
285
286pub type NemoRelayNativePluginRegisterFn = unsafe extern "C" fn(
288 user_data: *mut c_void,
289 plugin_config_json: *const NemoRelayNativeString,
290 ctx: *mut NemoRelayNativePluginContext,
291) -> NemoRelayStatus;
292
293pub type NemoRelayNativePluginDropFn = Option<unsafe extern "C" fn(user_data: *mut c_void)>;
295
296#[repr(C)]
298#[derive(Clone, Copy)]
299pub struct NemoRelayNativeHostApiV1 {
300 pub abi_version: u32,
302 pub struct_size: usize,
304 pub relay_version: *const c_char,
306 pub string_new: unsafe extern "C" fn(
308 data: *const u8,
309 len: usize,
310 out: *mut *mut NemoRelayNativeString,
311 ) -> NemoRelayStatus,
312 pub string_data: unsafe extern "C" fn(value: *const NemoRelayNativeString) -> *const u8,
314 pub string_len: unsafe extern "C" fn(value: *const NemoRelayNativeString) -> usize,
316 pub string_free: unsafe extern "C" fn(value: *mut NemoRelayNativeString),
318 pub last_error_clear: unsafe extern "C" fn(),
320 pub last_error_set: unsafe extern "C" fn(message: *const NemoRelayNativeString),
322 pub plugin_context_register_subscriber: unsafe extern "C" fn(
324 ctx: *mut NemoRelayNativePluginContext,
325 name: *const NemoRelayNativeString,
326 cb: NemoRelayNativeEventSubscriberCb,
327 user_data: *mut c_void,
328 free_fn: NemoRelayNativeFreeFn,
329 ) -> NemoRelayStatus,
330 pub plugin_context_register_tool_sanitize_request_guardrail:
332 unsafe extern "C" fn(
333 ctx: *mut NemoRelayNativePluginContext,
334 name: *const NemoRelayNativeString,
335 priority: i32,
336 cb: NemoRelayNativeToolJsonCb,
337 user_data: *mut c_void,
338 free_fn: NemoRelayNativeFreeFn,
339 ) -> NemoRelayStatus,
340 pub plugin_context_register_tool_sanitize_response_guardrail:
342 unsafe extern "C" fn(
343 ctx: *mut NemoRelayNativePluginContext,
344 name: *const NemoRelayNativeString,
345 priority: i32,
346 cb: NemoRelayNativeToolJsonCb,
347 user_data: *mut c_void,
348 free_fn: NemoRelayNativeFreeFn,
349 ) -> NemoRelayStatus,
350 pub plugin_context_register_tool_conditional_execution_guardrail:
352 unsafe extern "C" fn(
353 ctx: *mut NemoRelayNativePluginContext,
354 name: *const NemoRelayNativeString,
355 priority: i32,
356 cb: NemoRelayNativeToolConditionalCb,
357 user_data: *mut c_void,
358 free_fn: NemoRelayNativeFreeFn,
359 ) -> NemoRelayStatus,
360 pub plugin_context_register_tool_request_intercept: unsafe extern "C" fn(
362 ctx: *mut NemoRelayNativePluginContext,
363 name: *const NemoRelayNativeString,
364 priority: i32,
365 break_chain: bool,
366 cb: NemoRelayNativeToolJsonCb,
367 user_data: *mut c_void,
368 free_fn: NemoRelayNativeFreeFn,
369 )
370 -> NemoRelayStatus,
371 pub plugin_context_register_tool_execution_intercept: unsafe extern "C" fn(
373 ctx: *mut NemoRelayNativePluginContext,
374 name: *const NemoRelayNativeString,
375 priority: i32,
376 cb: NemoRelayNativeToolExecutionCb,
377 user_data: *mut c_void,
378 free_fn: NemoRelayNativeFreeFn,
379 )
380 -> NemoRelayStatus,
381 pub plugin_context_register_llm_sanitize_request_guardrail:
383 unsafe extern "C" fn(
384 ctx: *mut NemoRelayNativePluginContext,
385 name: *const NemoRelayNativeString,
386 priority: i32,
387 cb: NemoRelayNativeLlmRequestCb,
388 user_data: *mut c_void,
389 free_fn: NemoRelayNativeFreeFn,
390 ) -> NemoRelayStatus,
391 pub plugin_context_register_llm_sanitize_response_guardrail:
393 unsafe extern "C" fn(
394 ctx: *mut NemoRelayNativePluginContext,
395 name: *const NemoRelayNativeString,
396 priority: i32,
397 cb: NemoRelayNativeJsonCb,
398 user_data: *mut c_void,
399 free_fn: NemoRelayNativeFreeFn,
400 ) -> NemoRelayStatus,
401 pub plugin_context_register_llm_conditional_execution_guardrail:
403 unsafe extern "C" fn(
404 ctx: *mut NemoRelayNativePluginContext,
405 name: *const NemoRelayNativeString,
406 priority: i32,
407 cb: NemoRelayNativeLlmConditionalCb,
408 user_data: *mut c_void,
409 free_fn: NemoRelayNativeFreeFn,
410 ) -> NemoRelayStatus,
411 pub plugin_context_register_llm_request_intercept: unsafe extern "C" fn(
413 ctx: *mut NemoRelayNativePluginContext,
414 name: *const NemoRelayNativeString,
415 priority: i32,
416 break_chain: bool,
417 cb: NemoRelayNativeLlmRequestInterceptCb,
418 user_data: *mut c_void,
419 free_fn: NemoRelayNativeFreeFn,
420 ) -> NemoRelayStatus,
421 pub plugin_context_register_llm_execution_intercept: unsafe extern "C" fn(
423 ctx: *mut NemoRelayNativePluginContext,
424 name: *const NemoRelayNativeString,
425 priority: i32,
426 cb: NemoRelayNativeLlmExecutionCb,
427 user_data: *mut c_void,
428 free_fn: NemoRelayNativeFreeFn,
429 )
430 -> NemoRelayStatus,
431 pub plugin_context_register_llm_stream_execution_intercept:
433 unsafe extern "C" fn(
434 ctx: *mut NemoRelayNativePluginContext,
435 name: *const NemoRelayNativeString,
436 priority: i32,
437 cb: NemoRelayNativeLlmStreamExecutionCb,
438 user_data: *mut c_void,
439 free_fn: NemoRelayNativeFreeFn,
440 ) -> NemoRelayStatus,
441 pub scope_handle_free: unsafe extern "C" fn(handle: *mut NemoRelayNativeScopeHandle),
443 pub scope_get_current:
445 unsafe extern "C" fn(out: *mut *mut NemoRelayNativeScopeHandle) -> NemoRelayStatus,
446 pub scope_push: unsafe extern "C" fn(
448 name: *const NemoRelayNativeString,
449 scope_type: NemoRelayNativeScopeType,
450 parent: *const NemoRelayNativeScopeHandle,
451 attributes: u32,
452 data_json: *const NemoRelayNativeString,
453 metadata_json: *const NemoRelayNativeString,
454 input_json: *const NemoRelayNativeString,
455 timestamp_unix_micros: *const i64,
456 out: *mut *mut NemoRelayNativeScopeHandle,
457 ) -> NemoRelayStatus,
458 pub scope_pop: unsafe extern "C" fn(
460 handle: *const NemoRelayNativeScopeHandle,
461 output_json: *const NemoRelayNativeString,
462 metadata_json: *const NemoRelayNativeString,
463 timestamp_unix_micros: *const i64,
464 ) -> NemoRelayStatus,
465 pub emit_mark: unsafe extern "C" fn(
467 name: *const NemoRelayNativeString,
468 parent: *const NemoRelayNativeScopeHandle,
469 data_json: *const NemoRelayNativeString,
470 metadata_json: *const NemoRelayNativeString,
471 timestamp_unix_micros: *const i64,
472 ) -> NemoRelayStatus,
473 pub scope_stack_create:
475 unsafe extern "C" fn(out: *mut *mut NemoRelayNativeScopeStack) -> NemoRelayStatus,
476 pub scope_stack_free: unsafe extern "C" fn(stack: *mut NemoRelayNativeScopeStack),
478 pub scope_stack_set_thread:
480 unsafe extern "C" fn(stack: *const NemoRelayNativeScopeStack) -> NemoRelayStatus,
481 pub scope_stack_capture_thread:
483 unsafe extern "C" fn(out: *mut *mut NemoRelayNativeScopeStackBinding) -> NemoRelayStatus,
484 pub scope_stack_restore_thread:
486 unsafe extern "C" fn(binding: *mut NemoRelayNativeScopeStackBinding) -> NemoRelayStatus,
487 pub scope_stack_binding_free:
489 unsafe extern "C" fn(binding: *mut NemoRelayNativeScopeStackBinding),
490 pub scope_stack_active: unsafe extern "C" fn() -> bool,
492 pub scope_stack_with_current: unsafe extern "C" fn(
494 stack: *const NemoRelayNativeScopeStack,
495 cb: NemoRelayNativeWithScopeStackCb,
496 user_data: *mut c_void,
497 ) -> NemoRelayStatus,
498}
499
500unsafe impl Send for NemoRelayNativeHostApiV1 {}
503unsafe impl Sync for NemoRelayNativeHostApiV1 {}
504
505#[repr(C)]
507pub struct NemoRelayNativePluginV1 {
508 pub struct_size: usize,
510 pub plugin_kind: *mut NemoRelayNativeString,
512 pub allows_multiple_components: bool,
514 pub user_data: *mut c_void,
516 pub validate: Option<NemoRelayNativePluginValidateFn>,
518 pub register: Option<NemoRelayNativePluginRegisterFn>,
520 pub drop: NemoRelayNativePluginDropFn,
522}
523
524impl Default for NemoRelayNativePluginV1 {
525 fn default() -> Self {
526 Self {
527 struct_size: std::mem::size_of::<Self>(),
528 plugin_kind: ptr::null_mut(),
529 allows_multiple_components: true,
530 user_data: ptr::null_mut(),
531 validate: None,
532 register: None,
533 drop: None,
534 }
535 }
536}
537
538pub type NemoRelayNativePluginEntry = unsafe extern "C" fn(
540 host: *const NemoRelayNativeHostApiV1,
541 out: *mut NemoRelayNativePluginV1,
542) -> NemoRelayStatus;
543
544pub type Result<T> = std::result::Result<T, String>;
546
547pub type LlmJsonStream = Box<dyn Iterator<Item = Result<Json>> + Send>;
549
550#[derive(Clone)]
552pub struct PluginRuntime {
553 host: NemoRelayNativeHostApiV1,
554}
555
556impl PluginRuntime {
557 pub fn new(host: &NemoRelayNativeHostApiV1) -> Self {
559 Self { host: *host }
560 }
561
562 pub fn host_api(&self) -> &NemoRelayNativeHostApiV1 {
564 &self.host
565 }
566
567 pub fn current_scope(&self) -> Result<ScopeHandle<'_>> {
569 current_scope(&self.host)
570 }
571
572 pub fn push_scope(
574 &self,
575 name: &str,
576 scope_type: ScopeType,
577 data: Option<&Json>,
578 metadata: Option<&Json>,
579 input: Option<&Json>,
580 ) -> Result<ScopeHandle<'_>> {
581 push_scope(&self.host, name, scope_type.into(), data, metadata, input)
582 }
583
584 pub fn pop_scope(
586 &self,
587 handle: &ScopeHandle<'_>,
588 output: Option<&Json>,
589 metadata: Option<&Json>,
590 ) -> Result<()> {
591 pop_scope(&self.host, handle, output, metadata)
592 }
593
594 pub fn scope(
596 &self,
597 name: &str,
598 scope_type: ScopeType,
599 data: Option<&Json>,
600 metadata: Option<&Json>,
601 input: Option<&Json>,
602 ) -> Result<ScopeGuard<'_>> {
603 let handle = self.push_scope(name, scope_type, data, metadata, input)?;
604 Ok(ScopeGuard {
605 runtime: self,
606 handle: Some(handle),
607 })
608 }
609
610 pub fn emit_mark(
612 &self,
613 name: &str,
614 data: Option<&Json>,
615 metadata: Option<&Json>,
616 ) -> Result<()> {
617 emit_mark(&self.host, name, data, metadata)
618 }
619
620 pub fn create_scope_stack(&self) -> Result<ScopeStack<'_>> {
622 create_scope_stack(&self.host)
623 }
624
625 pub fn capture_scope_stack_thread(&self) -> Result<ScopeStackBinding<'_>> {
627 capture_scope_stack_thread(&self.host)
628 }
629
630 pub fn scope_stack_active(&self) -> bool {
632 unsafe { (self.host.scope_stack_active)() }
633 }
634
635 pub fn bind_scope_stack_thread<'a>(
637 &'a self,
638 stack: &'a ScopeStack<'a>,
639 ) -> Result<ThreadScopeStackGuard<'a>> {
640 let previous = self.capture_scope_stack_thread()?;
641 let status = stack.set_thread();
642 if status == NemoRelayStatus::Ok {
643 Ok(ThreadScopeStackGuard {
644 previous: Some(previous),
645 })
646 } else {
647 let _ = previous.restore();
648 Err(format!("scope_stack_set_thread failed: {status:?}"))
649 }
650 }
651}
652
653impl From<ScopeType> for NemoRelayNativeScopeType {
654 fn from(value: ScopeType) -> Self {
655 match value {
656 ScopeType::Agent => Self::Agent,
657 ScopeType::Function => Self::Function,
658 ScopeType::Tool => Self::Tool,
659 ScopeType::Llm => Self::Llm,
660 ScopeType::Retriever => Self::Retriever,
661 ScopeType::Embedder => Self::Embedder,
662 ScopeType::Reranker => Self::Reranker,
663 ScopeType::Guardrail => Self::Guardrail,
664 ScopeType::Evaluator => Self::Evaluator,
665 ScopeType::Custom => Self::Custom,
666 ScopeType::Unknown => Self::Unknown,
667 }
668 }
669}
670
671pub struct ScopeGuard<'a> {
673 runtime: &'a PluginRuntime,
674 handle: Option<ScopeHandle<'a>>,
675}
676
677impl<'a> ScopeGuard<'a> {
678 pub fn handle(&self) -> Option<&ScopeHandle<'a>> {
680 self.handle.as_ref()
681 }
682
683 pub fn close(&mut self, output: Option<&Json>, metadata: Option<&Json>) -> Result<()> {
685 let Some(handle) = self.handle.as_ref() else {
686 return Ok(());
687 };
688 self.runtime.pop_scope(handle, output, metadata)?;
689 self.handle.take();
690 Ok(())
691 }
692}
693
694impl Drop for ScopeGuard<'_> {
695 fn drop(&mut self) {
696 if let Some(handle) = self.handle.take() {
697 let _ = self.runtime.pop_scope(&handle, None, None);
698 }
699 }
700}
701
702pub struct ThreadScopeStackGuard<'a> {
704 previous: Option<ScopeStackBinding<'a>>,
705}
706
707impl ThreadScopeStackGuard<'_> {
708 pub fn restore(mut self) -> Result<()> {
710 let Some(previous) = self.previous.take() else {
711 return Ok(());
712 };
713 let status = previous.restore();
714 if status == NemoRelayStatus::Ok {
715 Ok(())
716 } else {
717 Err(format!("scope_stack_restore_thread failed: {status:?}"))
718 }
719 }
720}
721
722impl Drop for ThreadScopeStackGuard<'_> {
723 fn drop(&mut self) {
724 if let Some(previous) = self.previous.take() {
725 let _ = previous.restore();
726 }
727 }
728}
729
730pub struct ToolNext<'a> {
732 host: &'a NemoRelayNativeHostApiV1,
733 next_fn: NemoRelayNativeToolNextFn,
734 next_ctx: *mut c_void,
735}
736
737impl ToolNext<'_> {
738 pub fn call(&self, args: Json) -> Result<Json> {
740 let args = HostString::from_json(self.host, &args)
741 .ok_or_else(|| "failed to allocate tool next args".to_string())?;
742 let mut out = ptr::null_mut();
743 let status = unsafe { (self.next_fn)(args.as_ptr(), self.next_ctx, &mut out) };
744 if status != NemoRelayStatus::Ok {
745 return Err(format!("tool next failed: {status:?}"));
746 }
747 if out.is_null() {
748 return Err("tool next returned null output".into());
749 }
750 let result = read_json_value(self.host, out, "tool next result");
751 unsafe { (self.host.string_free)(out) };
752 result.map_err(|status| format!("tool next returned invalid JSON: {status:?}"))
753 }
754}
755
756pub struct LlmNext<'a> {
758 host: &'a NemoRelayNativeHostApiV1,
759 next_fn: NemoRelayNativeLlmNextFn,
760 next_ctx: *mut c_void,
761}
762
763impl LlmNext<'_> {
764 pub fn call(&self, request: LlmRequest) -> Result<Json> {
766 let request = HostString::from_json(self.host, &request)
767 .ok_or_else(|| "failed to allocate LLM next request".to_string())?;
768 let mut out = ptr::null_mut();
769 let status = unsafe { (self.next_fn)(request.as_ptr(), self.next_ctx, &mut out) };
770 if status != NemoRelayStatus::Ok {
771 return Err(format!("llm next failed: {status:?}"));
772 }
773 if out.is_null() {
774 return Err("llm next returned null output".into());
775 }
776 let result = read_json_value(self.host, out, "llm next result");
777 unsafe { (self.host.string_free)(out) };
778 result.map_err(|status| format!("llm next returned invalid JSON: {status:?}"))
779 }
780}
781
782pub struct LlmStreamNext<'a> {
784 host: &'a NemoRelayNativeHostApiV1,
785 next_fn: NemoRelayNativeLlmStreamNextFn,
786 next_ctx: *mut c_void,
787}
788
789impl LlmStreamNext<'_> {
790 pub fn call(&self, request: LlmRequest) -> Result<LlmStream> {
792 let request = HostString::from_json(self.host, &request)
793 .ok_or_else(|| "failed to allocate LLM stream next request".to_string())?;
794 let mut raw = NemoRelayNativeLlmStreamV1::default();
795 let status = unsafe { (self.next_fn)(request.as_ptr(), self.next_ctx, &mut raw) };
796 if status != NemoRelayStatus::Ok {
797 return Err(format!("llm stream next failed: {status:?}"));
798 }
799 unsafe { LlmStream::from_raw(self.host, raw) }
800 }
801}
802
803pub struct LlmStream {
805 host: NemoRelayNativeHostApiV1,
806 raw: NemoRelayNativeLlmStreamV1,
807 finished: bool,
808}
809
810unsafe impl Send for LlmStream {}
812
813impl LlmStream {
814 pub unsafe fn from_raw(
820 host: &NemoRelayNativeHostApiV1,
821 mut raw: NemoRelayNativeLlmStreamV1,
822 ) -> Result<Self> {
823 let expected_size = std::mem::size_of::<NemoRelayNativeLlmStreamV1>();
824 if raw.struct_size != expected_size {
825 if raw.struct_size >= expected_size {
826 unsafe { drop_raw_llm_stream(&mut raw) };
827 }
828 return Err(format!(
829 "unsupported LLM stream struct size: {}",
830 raw.struct_size
831 ));
832 }
833 if raw.next.is_none() {
834 unsafe { drop_raw_llm_stream(&mut raw) };
835 return Err("LLM stream next callback was null".into());
836 }
837 Ok(Self {
838 host: *host,
839 raw,
840 finished: false,
841 })
842 }
843
844 pub fn next_chunk(&mut self) -> Result<Option<Json>> {
846 if self.finished {
847 return Ok(None);
848 }
849 let next = self
850 .raw
851 .next
852 .expect("LLM stream next callback is validated on construction");
853 let mut out = ptr::null_mut();
854 let status = unsafe { next(self.raw.user_data, &mut out) };
855 match status {
856 NemoRelayStatus::Ok => {
857 if out.is_null() {
858 self.finished = true;
859 return Err("LLM stream returned null chunk".into());
860 }
861 let result = read_json_value(&self.host, out, "LLM stream chunk");
862 unsafe { (self.host.string_free)(out) };
863 match result {
864 Ok(chunk) => Ok(Some(chunk)),
865 Err(status) => {
866 self.finished = true;
867 Err(format!("LLM stream returned invalid JSON: {status:?}"))
868 }
869 }
870 }
871 NemoRelayStatus::StreamEnd => {
872 if !out.is_null() {
873 unsafe { (self.host.string_free)(out) };
874 }
875 self.finished = true;
876 Ok(None)
877 }
878 other => {
879 if !out.is_null() {
880 unsafe { (self.host.string_free)(out) };
881 }
882 self.finished = true;
883 Err(format!("LLM stream failed: {other:?}"))
884 }
885 }
886 }
887
888 pub fn cancel(&mut self) -> Result<()> {
890 if self.finished {
891 return Ok(());
892 }
893 if let Some(cancel) = self.raw.cancel {
894 let status = unsafe { cancel(self.raw.user_data) };
895 if status != NemoRelayStatus::Ok {
896 return Err(format!("LLM stream cancel failed: {status:?}"));
897 }
898 }
899 self.finished = true;
900 Ok(())
901 }
902}
903
904impl Iterator for LlmStream {
905 type Item = Result<Json>;
906
907 fn next(&mut self) -> Option<Self::Item> {
908 match self.next_chunk() {
909 Ok(Some(chunk)) => Some(Ok(chunk)),
910 Ok(None) => None,
911 Err(message) => Some(Err(message)),
912 }
913 }
914}
915
916unsafe fn drop_raw_llm_stream(raw: &mut NemoRelayNativeLlmStreamV1) {
917 if let Some(drop_fn) = raw.drop.take() {
918 unsafe { drop_fn(raw.user_data) };
919 }
920 raw.user_data = ptr::null_mut();
921}
922
923impl Drop for LlmStream {
924 fn drop(&mut self) {
925 if !self.finished {
926 if let Some(cancel) = self.raw.cancel {
927 let _ = unsafe { cancel(self.raw.user_data) };
928 }
929 self.finished = true;
930 }
931 unsafe { drop_raw_llm_stream(&mut self.raw) };
932 }
933}
934
935pub struct ScopeHandle<'a> {
937 host: &'a NemoRelayNativeHostApiV1,
938 ptr: *mut NemoRelayNativeScopeHandle,
939}
940
941impl<'a> ScopeHandle<'a> {
942 pub fn as_ptr(&self) -> *const NemoRelayNativeScopeHandle {
944 self.ptr
945 }
946}
947
948impl Drop for ScopeHandle<'_> {
949 fn drop(&mut self) {
950 unsafe { (self.host.scope_handle_free)(self.ptr) };
951 }
952}
953
954pub struct ScopeStack<'a> {
956 host: &'a NemoRelayNativeHostApiV1,
957 ptr: *mut NemoRelayNativeScopeStack,
958}
959
960impl<'a> ScopeStack<'a> {
961 pub fn as_ptr(&self) -> *const NemoRelayNativeScopeStack {
963 self.ptr
964 }
965
966 fn set_thread(&self) -> NemoRelayStatus {
967 unsafe { (self.host.scope_stack_set_thread)(self.ptr) }
968 }
969
970 pub fn with_current<F>(&self, f: F) -> Result<()>
972 where
973 F: FnOnce() -> Result<()>,
974 {
975 struct State<F> {
976 f: Option<F>,
977 error: Option<String>,
978 }
979
980 unsafe extern "C" fn trampoline<F>(user_data: *mut c_void) -> NemoRelayStatus
981 where
982 F: FnOnce() -> Result<()>,
983 {
984 if user_data.is_null() {
985 return NemoRelayStatus::NullPointer;
986 }
987 let state = unsafe { &mut *(user_data as *mut State<F>) };
988 let result = catch_unwind(AssertUnwindSafe(|| {
989 let Some(f) = state.f.take() else {
990 return Err("scope-stack callback was already consumed".to_string());
991 };
992 f()
993 }));
994 match result {
995 Ok(Ok(())) => NemoRelayStatus::Ok,
996 Ok(Err(message)) => {
997 state.error = Some(message);
998 NemoRelayStatus::Internal
999 }
1000 Err(_) => {
1001 state.error = Some("scope-stack callback panicked".into());
1002 NemoRelayStatus::Internal
1003 }
1004 }
1005 }
1006
1007 let mut state = State {
1008 f: Some(f),
1009 error: None,
1010 };
1011 let status = unsafe {
1012 (self.host.scope_stack_with_current)(
1013 self.ptr,
1014 trampoline::<F>,
1015 (&mut state as *mut State<_>).cast(),
1016 )
1017 };
1018 if status == NemoRelayStatus::Ok {
1019 Ok(())
1020 } else {
1021 Err(state
1022 .error
1023 .unwrap_or_else(|| format!("scope_stack_with_current failed: {status:?}")))
1024 }
1025 }
1026}
1027
1028impl Drop for ScopeStack<'_> {
1029 fn drop(&mut self) {
1030 unsafe { (self.host.scope_stack_free)(self.ptr) };
1031 }
1032}
1033
1034pub struct ScopeStackBinding<'a> {
1036 host: &'a NemoRelayNativeHostApiV1,
1037 ptr: *mut NemoRelayNativeScopeStackBinding,
1038}
1039
1040impl<'a> ScopeStackBinding<'a> {
1041 pub fn restore(mut self) -> NemoRelayStatus {
1043 let ptr = std::mem::replace(&mut self.ptr, ptr::null_mut());
1044 unsafe { (self.host.scope_stack_restore_thread)(ptr) }
1045 }
1046}
1047
1048impl Drop for ScopeStackBinding<'_> {
1049 fn drop(&mut self) {
1050 if !self.ptr.is_null() {
1051 unsafe { (self.host.scope_stack_binding_free)(self.ptr) };
1052 }
1053 }
1054}
1055
1056pub fn current_scope(host: &NemoRelayNativeHostApiV1) -> Result<ScopeHandle<'_>> {
1058 let mut out = ptr::null_mut();
1059 let status = unsafe { (host.scope_get_current)(&mut out) };
1060 if status == NemoRelayStatus::Ok && !out.is_null() {
1061 Ok(ScopeHandle { host, ptr: out })
1062 } else {
1063 Err(format!("scope_get_current failed: {status:?}"))
1064 }
1065}
1066
1067pub fn push_scope<'a>(
1069 host: &'a NemoRelayNativeHostApiV1,
1070 name: &str,
1071 scope_type: NemoRelayNativeScopeType,
1072 data: Option<&Json>,
1073 metadata: Option<&Json>,
1074 input: Option<&Json>,
1075) -> Result<ScopeHandle<'a>> {
1076 let name =
1077 HostString::new(host, name).ok_or_else(|| "failed to allocate scope name".to_string())?;
1078 let data = OptionalHostJson::new(host, data)?;
1079 let metadata = OptionalHostJson::new(host, metadata)?;
1080 let input = OptionalHostJson::new(host, input)?;
1081 let mut out = ptr::null_mut();
1082 let status = unsafe {
1083 (host.scope_push)(
1084 name.as_ptr(),
1085 scope_type,
1086 ptr::null(),
1087 0,
1088 data.as_ptr(),
1089 metadata.as_ptr(),
1090 input.as_ptr(),
1091 ptr::null(),
1092 &mut out,
1093 )
1094 };
1095 if status == NemoRelayStatus::Ok && !out.is_null() {
1096 Ok(ScopeHandle { host, ptr: out })
1097 } else {
1098 Err(format!("scope_push failed: {status:?}"))
1099 }
1100}
1101
1102pub fn pop_scope(
1104 host: &NemoRelayNativeHostApiV1,
1105 handle: &ScopeHandle<'_>,
1106 output: Option<&Json>,
1107 metadata: Option<&Json>,
1108) -> Result<()> {
1109 let output = OptionalHostJson::new(host, output)?;
1110 let metadata = OptionalHostJson::new(host, metadata)?;
1111 let status = unsafe {
1112 (host.scope_pop)(
1113 handle.as_ptr(),
1114 output.as_ptr(),
1115 metadata.as_ptr(),
1116 ptr::null(),
1117 )
1118 };
1119 if status == NemoRelayStatus::Ok {
1120 Ok(())
1121 } else {
1122 Err(format!("scope_pop failed: {status:?}"))
1123 }
1124}
1125
1126pub fn emit_mark(
1128 host: &NemoRelayNativeHostApiV1,
1129 name: &str,
1130 data: Option<&Json>,
1131 metadata: Option<&Json>,
1132) -> Result<()> {
1133 let name =
1134 HostString::new(host, name).ok_or_else(|| "failed to allocate mark name".to_string())?;
1135 let data = OptionalHostJson::new(host, data)?;
1136 let metadata = OptionalHostJson::new(host, metadata)?;
1137 let status = unsafe {
1138 (host.emit_mark)(
1139 name.as_ptr(),
1140 ptr::null(),
1141 data.as_ptr(),
1142 metadata.as_ptr(),
1143 ptr::null(),
1144 )
1145 };
1146 if status == NemoRelayStatus::Ok {
1147 Ok(())
1148 } else {
1149 Err(format!("emit_mark failed: {status:?}"))
1150 }
1151}
1152
1153pub fn create_scope_stack(host: &NemoRelayNativeHostApiV1) -> Result<ScopeStack<'_>> {
1155 let mut out = ptr::null_mut();
1156 let status = unsafe { (host.scope_stack_create)(&mut out) };
1157 if status == NemoRelayStatus::Ok && !out.is_null() {
1158 Ok(ScopeStack { host, ptr: out })
1159 } else {
1160 Err(format!("scope_stack_create failed: {status:?}"))
1161 }
1162}
1163
1164pub fn capture_scope_stack_thread(
1166 host: &NemoRelayNativeHostApiV1,
1167) -> Result<ScopeStackBinding<'_>> {
1168 let mut out = ptr::null_mut();
1169 let status = unsafe { (host.scope_stack_capture_thread)(&mut out) };
1170 if status == NemoRelayStatus::Ok && !out.is_null() {
1171 Ok(ScopeStackBinding { host, ptr: out })
1172 } else {
1173 Err(format!("scope_stack_capture_thread failed: {status:?}"))
1174 }
1175}
1176
1177pub trait NativePlugin: Send + 'static {
1179 fn plugin_kind(&self) -> &str;
1181
1182 fn allows_multiple_components(&self) -> bool {
1184 true
1185 }
1186
1187 fn validate(&self, _plugin_config: &Map<String, Json>) -> Vec<ConfigDiagnostic> {
1189 vec![]
1190 }
1191
1192 fn register(
1194 &mut self,
1195 plugin_config: &Map<String, Json>,
1196 ctx: &mut PluginContext<'_>,
1197 ) -> Result<()>;
1198}
1199
1200pub struct PluginContext<'a> {
1202 host: &'a NemoRelayNativeHostApiV1,
1203 raw: *mut NemoRelayNativePluginContext,
1204}
1205
1206#[allow(clippy::not_unsafe_ptr_arg_deref)]
1207impl<'a> PluginContext<'a> {
1208 pub unsafe fn from_raw(
1213 host: &'a NemoRelayNativeHostApiV1,
1214 raw: *mut NemoRelayNativePluginContext,
1215 ) -> Self {
1216 Self { host, raw }
1217 }
1218
1219 pub fn host_api(&self) -> &'a NemoRelayNativeHostApiV1 {
1221 self.host
1222 }
1223
1224 pub fn runtime(&self) -> PluginRuntime {
1226 PluginRuntime::new(self.host)
1227 }
1228
1229 pub fn register_subscriber<F>(&mut self, name: &str, callback: F) -> Result<()>
1231 where
1232 F: Fn(&Event) + Send + Sync + 'static,
1233 {
1234 let user_data = typed_callback_user_data(self.host, callback);
1235 let status = unsafe {
1236 self.register_subscriber_raw(
1237 name,
1238 typed_subscriber_trampoline::<F>,
1239 user_data,
1240 Some(drop_typed_callback::<F>),
1241 )
1242 };
1243 finish_typed_registration::<F>(self.host, status, user_data, "subscriber")
1244 }
1245
1246 pub fn register_tool_sanitize_request_guardrail<F>(
1248 &mut self,
1249 name: &str,
1250 priority: i32,
1251 callback: F,
1252 ) -> Result<()>
1253 where
1254 F: Fn(&str, Json) -> Json + Send + Sync + 'static,
1255 {
1256 let user_data = typed_callback_user_data(self.host, callback);
1257 let status = unsafe {
1258 self.register_tool_sanitize_request_guardrail_raw(
1259 name,
1260 priority,
1261 typed_tool_sanitize_trampoline::<F>,
1262 user_data,
1263 Some(drop_typed_callback::<F>),
1264 )
1265 };
1266 finish_typed_registration::<F>(
1267 self.host,
1268 status,
1269 user_data,
1270 "tool sanitize request guardrail",
1271 )
1272 }
1273
1274 pub fn register_tool_sanitize_response_guardrail<F>(
1276 &mut self,
1277 name: &str,
1278 priority: i32,
1279 callback: F,
1280 ) -> Result<()>
1281 where
1282 F: Fn(&str, Json) -> Json + Send + Sync + 'static,
1283 {
1284 let user_data = typed_callback_user_data(self.host, callback);
1285 let status = unsafe {
1286 self.register_tool_sanitize_response_guardrail_raw(
1287 name,
1288 priority,
1289 typed_tool_sanitize_trampoline::<F>,
1290 user_data,
1291 Some(drop_typed_callback::<F>),
1292 )
1293 };
1294 finish_typed_registration::<F>(
1295 self.host,
1296 status,
1297 user_data,
1298 "tool sanitize response guardrail",
1299 )
1300 }
1301
1302 pub fn register_tool_conditional_execution_guardrail<F>(
1304 &mut self,
1305 name: &str,
1306 priority: i32,
1307 callback: F,
1308 ) -> Result<()>
1309 where
1310 F: Fn(&str, &Json) -> Result<Option<String>> + Send + Sync + 'static,
1311 {
1312 let user_data = typed_callback_user_data(self.host, callback);
1313 let status = unsafe {
1314 self.register_tool_conditional_execution_guardrail_raw(
1315 name,
1316 priority,
1317 typed_tool_conditional_trampoline::<F>,
1318 user_data,
1319 Some(drop_typed_callback::<F>),
1320 )
1321 };
1322 finish_typed_registration::<F>(
1323 self.host,
1324 status,
1325 user_data,
1326 "tool conditional execution guardrail",
1327 )
1328 }
1329
1330 pub fn register_tool_request_intercept<F>(
1332 &mut self,
1333 name: &str,
1334 priority: i32,
1335 break_chain: bool,
1336 callback: F,
1337 ) -> Result<()>
1338 where
1339 F: Fn(&str, Json) -> Result<Json> + Send + Sync + 'static,
1340 {
1341 let user_data = typed_callback_user_data(self.host, callback);
1342 let status = unsafe {
1343 self.register_tool_request_intercept_raw(
1344 name,
1345 priority,
1346 break_chain,
1347 typed_tool_intercept_trampoline::<F>,
1348 user_data,
1349 Some(drop_typed_callback::<F>),
1350 )
1351 };
1352 finish_typed_registration::<F>(self.host, status, user_data, "tool request intercept")
1353 }
1354
1355 pub fn register_tool_execution_intercept<F>(
1361 &mut self,
1362 name: &str,
1363 priority: i32,
1364 callback: F,
1365 ) -> Result<()>
1366 where
1367 F: for<'next> Fn(&str, Json, ToolNext<'next>) -> Result<ToolExecutionInterceptOutcome>
1368 + Send
1369 + Sync
1370 + 'static,
1371 {
1372 let user_data = typed_callback_user_data(self.host, callback);
1373 let status = unsafe {
1374 self.register_tool_execution_intercept_raw(
1375 name,
1376 priority,
1377 typed_tool_execution_trampoline::<F>,
1378 user_data,
1379 Some(drop_typed_callback::<F>),
1380 )
1381 };
1382 finish_typed_registration::<F>(self.host, status, user_data, "tool execution intercept")
1383 }
1384
1385 pub fn register_llm_sanitize_request_guardrail<F>(
1387 &mut self,
1388 name: &str,
1389 priority: i32,
1390 callback: F,
1391 ) -> Result<()>
1392 where
1393 F: Fn(LlmRequest) -> LlmRequest + Send + Sync + 'static,
1394 {
1395 let user_data = typed_callback_user_data(self.host, callback);
1396 let status = unsafe {
1397 self.register_llm_sanitize_request_guardrail_raw(
1398 name,
1399 priority,
1400 typed_llm_sanitize_request_trampoline::<F>,
1401 user_data,
1402 Some(drop_typed_callback::<F>),
1403 )
1404 };
1405 finish_typed_registration::<F>(
1406 self.host,
1407 status,
1408 user_data,
1409 "llm sanitize request guardrail",
1410 )
1411 }
1412
1413 pub fn register_llm_sanitize_response_guardrail<F>(
1415 &mut self,
1416 name: &str,
1417 priority: i32,
1418 callback: F,
1419 ) -> Result<()>
1420 where
1421 F: Fn(Json) -> Json + Send + Sync + 'static,
1422 {
1423 let user_data = typed_callback_user_data(self.host, callback);
1424 let status = unsafe {
1425 self.register_llm_sanitize_response_guardrail_raw(
1426 name,
1427 priority,
1428 typed_llm_sanitize_response_trampoline::<F>,
1429 user_data,
1430 Some(drop_typed_callback::<F>),
1431 )
1432 };
1433 finish_typed_registration::<F>(
1434 self.host,
1435 status,
1436 user_data,
1437 "llm sanitize response guardrail",
1438 )
1439 }
1440
1441 pub fn register_llm_conditional_execution_guardrail<F>(
1443 &mut self,
1444 name: &str,
1445 priority: i32,
1446 callback: F,
1447 ) -> Result<()>
1448 where
1449 F: Fn(&LlmRequest) -> Result<Option<String>> + Send + Sync + 'static,
1450 {
1451 let user_data = typed_callback_user_data(self.host, callback);
1452 let status = unsafe {
1453 self.register_llm_conditional_execution_guardrail_raw(
1454 name,
1455 priority,
1456 typed_llm_conditional_trampoline::<F>,
1457 user_data,
1458 Some(drop_typed_callback::<F>),
1459 )
1460 };
1461 finish_typed_registration::<F>(
1462 self.host,
1463 status,
1464 user_data,
1465 "llm conditional execution guardrail",
1466 )
1467 }
1468
1469 pub fn register_llm_request_intercept<F>(
1471 &mut self,
1472 name: &str,
1473 priority: i32,
1474 break_chain: bool,
1475 callback: F,
1476 ) -> Result<()>
1477 where
1478 F: Fn(&str, LlmRequest, Option<AnnotatedLlmRequest>) -> Result<LlmRequestInterceptOutcome>
1479 + Send
1480 + Sync
1481 + 'static,
1482 {
1483 let user_data = typed_callback_user_data(self.host, callback);
1484 let status = unsafe {
1485 self.register_llm_request_intercept_raw(
1486 name,
1487 priority,
1488 break_chain,
1489 typed_llm_request_intercept_trampoline::<F>,
1490 user_data,
1491 Some(drop_typed_callback::<F>),
1492 )
1493 };
1494 finish_typed_registration::<F>(self.host, status, user_data, "llm request intercept")
1495 }
1496
1497 pub fn register_llm_execution_intercept<F>(
1499 &mut self,
1500 name: &str,
1501 priority: i32,
1502 callback: F,
1503 ) -> Result<()>
1504 where
1505 F: for<'next> Fn(&str, LlmRequest, LlmNext<'next>) -> Result<Json> + Send + Sync + 'static,
1506 {
1507 let user_data = typed_callback_user_data(self.host, callback);
1508 let status = unsafe {
1509 self.register_llm_execution_intercept_raw(
1510 name,
1511 priority,
1512 typed_llm_execution_trampoline::<F>,
1513 user_data,
1514 Some(drop_typed_callback::<F>),
1515 )
1516 };
1517 finish_typed_registration::<F>(self.host, status, user_data, "llm execution intercept")
1518 }
1519
1520 pub fn register_llm_stream_execution_intercept<F>(
1525 &mut self,
1526 name: &str,
1527 priority: i32,
1528 callback: F,
1529 ) -> Result<()>
1530 where
1531 F: for<'next> Fn(&str, LlmRequest, LlmStreamNext<'next>) -> Result<LlmJsonStream>
1532 + Send
1533 + Sync
1534 + 'static,
1535 {
1536 let user_data = typed_callback_user_data(self.host, callback);
1537 let status = unsafe {
1538 self.register_llm_stream_execution_intercept_raw(
1539 name,
1540 priority,
1541 typed_llm_stream_execution_trampoline::<F>,
1542 user_data,
1543 Some(drop_typed_callback::<F>),
1544 )
1545 };
1546 finish_typed_registration::<F>(
1547 self.host,
1548 status,
1549 user_data,
1550 "llm stream execution intercept",
1551 )
1552 }
1553
1554 pub unsafe fn register_subscriber_raw(
1561 &mut self,
1562 name: &str,
1563 cb: NemoRelayNativeEventSubscriberCb,
1564 user_data: *mut c_void,
1565 free_fn: NemoRelayNativeFreeFn,
1566 ) -> NemoRelayStatus {
1567 self.with_name(name, |host, name| unsafe {
1568 (host.plugin_context_register_subscriber)(self.raw, name, cb, user_data, free_fn)
1569 })
1570 }
1571
1572 pub unsafe fn register_tool_sanitize_request_guardrail_raw(
1579 &mut self,
1580 name: &str,
1581 priority: i32,
1582 cb: NemoRelayNativeToolJsonCb,
1583 user_data: *mut c_void,
1584 free_fn: NemoRelayNativeFreeFn,
1585 ) -> NemoRelayStatus {
1586 self.with_name(name, |host, name| unsafe {
1587 (host.plugin_context_register_tool_sanitize_request_guardrail)(
1588 self.raw, name, priority, cb, user_data, free_fn,
1589 )
1590 })
1591 }
1592
1593 pub unsafe fn register_tool_sanitize_response_guardrail_raw(
1600 &mut self,
1601 name: &str,
1602 priority: i32,
1603 cb: NemoRelayNativeToolJsonCb,
1604 user_data: *mut c_void,
1605 free_fn: NemoRelayNativeFreeFn,
1606 ) -> NemoRelayStatus {
1607 self.with_name(name, |host, name| unsafe {
1608 (host.plugin_context_register_tool_sanitize_response_guardrail)(
1609 self.raw, name, priority, cb, user_data, free_fn,
1610 )
1611 })
1612 }
1613
1614 pub unsafe fn register_tool_conditional_execution_guardrail_raw(
1621 &mut self,
1622 name: &str,
1623 priority: i32,
1624 cb: NemoRelayNativeToolConditionalCb,
1625 user_data: *mut c_void,
1626 free_fn: NemoRelayNativeFreeFn,
1627 ) -> NemoRelayStatus {
1628 self.with_name(name, |host, name| unsafe {
1629 (host.plugin_context_register_tool_conditional_execution_guardrail)(
1630 self.raw, name, priority, cb, user_data, free_fn,
1631 )
1632 })
1633 }
1634
1635 pub unsafe fn register_tool_request_intercept_raw(
1642 &mut self,
1643 name: &str,
1644 priority: i32,
1645 break_chain: bool,
1646 cb: NemoRelayNativeToolJsonCb,
1647 user_data: *mut c_void,
1648 free_fn: NemoRelayNativeFreeFn,
1649 ) -> NemoRelayStatus {
1650 self.with_name(name, |host, name| unsafe {
1651 (host.plugin_context_register_tool_request_intercept)(
1652 self.raw,
1653 name,
1654 priority,
1655 break_chain,
1656 cb,
1657 user_data,
1658 free_fn,
1659 )
1660 })
1661 }
1662
1663 pub unsafe fn register_tool_execution_intercept_raw(
1670 &mut self,
1671 name: &str,
1672 priority: i32,
1673 cb: NemoRelayNativeToolExecutionCb,
1674 user_data: *mut c_void,
1675 free_fn: NemoRelayNativeFreeFn,
1676 ) -> NemoRelayStatus {
1677 self.with_name(name, |host, name| unsafe {
1678 (host.plugin_context_register_tool_execution_intercept)(
1679 self.raw, name, priority, cb, user_data, free_fn,
1680 )
1681 })
1682 }
1683
1684 pub unsafe fn register_llm_sanitize_request_guardrail_raw(
1691 &mut self,
1692 name: &str,
1693 priority: i32,
1694 cb: NemoRelayNativeLlmRequestCb,
1695 user_data: *mut c_void,
1696 free_fn: NemoRelayNativeFreeFn,
1697 ) -> NemoRelayStatus {
1698 self.with_name(name, |host, name| unsafe {
1699 (host.plugin_context_register_llm_sanitize_request_guardrail)(
1700 self.raw, name, priority, cb, user_data, free_fn,
1701 )
1702 })
1703 }
1704
1705 pub unsafe fn register_llm_sanitize_response_guardrail_raw(
1712 &mut self,
1713 name: &str,
1714 priority: i32,
1715 cb: NemoRelayNativeJsonCb,
1716 user_data: *mut c_void,
1717 free_fn: NemoRelayNativeFreeFn,
1718 ) -> NemoRelayStatus {
1719 self.with_name(name, |host, name| unsafe {
1720 (host.plugin_context_register_llm_sanitize_response_guardrail)(
1721 self.raw, name, priority, cb, user_data, free_fn,
1722 )
1723 })
1724 }
1725
1726 pub unsafe fn register_llm_conditional_execution_guardrail_raw(
1733 &mut self,
1734 name: &str,
1735 priority: i32,
1736 cb: NemoRelayNativeLlmConditionalCb,
1737 user_data: *mut c_void,
1738 free_fn: NemoRelayNativeFreeFn,
1739 ) -> NemoRelayStatus {
1740 self.with_name(name, |host, name| unsafe {
1741 (host.plugin_context_register_llm_conditional_execution_guardrail)(
1742 self.raw, name, priority, cb, user_data, free_fn,
1743 )
1744 })
1745 }
1746
1747 pub unsafe fn register_llm_request_intercept_raw(
1754 &mut self,
1755 name: &str,
1756 priority: i32,
1757 break_chain: bool,
1758 cb: NemoRelayNativeLlmRequestInterceptCb,
1759 user_data: *mut c_void,
1760 free_fn: NemoRelayNativeFreeFn,
1761 ) -> NemoRelayStatus {
1762 self.with_name(name, |host, name| unsafe {
1763 (host.plugin_context_register_llm_request_intercept)(
1764 self.raw,
1765 name,
1766 priority,
1767 break_chain,
1768 cb,
1769 user_data,
1770 free_fn,
1771 )
1772 })
1773 }
1774
1775 pub unsafe fn register_llm_execution_intercept_raw(
1782 &mut self,
1783 name: &str,
1784 priority: i32,
1785 cb: NemoRelayNativeLlmExecutionCb,
1786 user_data: *mut c_void,
1787 free_fn: NemoRelayNativeFreeFn,
1788 ) -> NemoRelayStatus {
1789 self.with_name(name, |host, name| unsafe {
1790 (host.plugin_context_register_llm_execution_intercept)(
1791 self.raw, name, priority, cb, user_data, free_fn,
1792 )
1793 })
1794 }
1795
1796 pub unsafe fn register_llm_stream_execution_intercept_raw(
1803 &mut self,
1804 name: &str,
1805 priority: i32,
1806 cb: NemoRelayNativeLlmStreamExecutionCb,
1807 user_data: *mut c_void,
1808 free_fn: NemoRelayNativeFreeFn,
1809 ) -> NemoRelayStatus {
1810 self.with_name(name, |host, name| unsafe {
1811 (host.plugin_context_register_llm_stream_execution_intercept)(
1812 self.raw, name, priority, cb, user_data, free_fn,
1813 )
1814 })
1815 }
1816
1817 fn with_name(
1818 &self,
1819 name: &str,
1820 f: impl FnOnce(&NemoRelayNativeHostApiV1, *const NemoRelayNativeString) -> NemoRelayStatus,
1821 ) -> NemoRelayStatus {
1822 let name = match HostString::try_new(self.host, name) {
1823 Ok(name) => name,
1824 Err(status) => return status,
1825 };
1826 f(self.host, name.as_ptr())
1827 }
1828}
1829
1830struct TypedCallback<F> {
1831 host: NemoRelayNativeHostApiV1,
1832 callback: F,
1833}
1834
1835fn typed_callback_user_data<F>(host: &NemoRelayNativeHostApiV1, callback: F) -> *mut c_void {
1836 Box::into_raw(Box::new(TypedCallback {
1837 host: *host,
1838 callback,
1839 })) as *mut c_void
1840}
1841
1842unsafe extern "C" fn drop_typed_callback<F>(user_data: *mut c_void) {
1843 if !user_data.is_null() {
1844 let callback = unsafe { Box::from_raw(user_data as *mut TypedCallback<F>) };
1845 let host = callback.host;
1846 if catch_unwind(AssertUnwindSafe(|| drop(callback))).is_err() {
1847 set_last_error(&host, "native plugin typed callback state drop panicked");
1848 }
1849 }
1850}
1851
1852fn finish_typed_registration<F>(
1853 host: &NemoRelayNativeHostApiV1,
1854 status: NemoRelayStatus,
1855 user_data: *mut c_void,
1856 label: &str,
1857) -> Result<()> {
1858 if status == NemoRelayStatus::Ok {
1859 Ok(())
1860 } else {
1861 unsafe { drop_typed_callback::<F>(user_data) };
1862 Err(status_error(host, status, label))
1863 }
1864}
1865
1866fn status_error(host: &NemoRelayNativeHostApiV1, status: NemoRelayStatus, label: &str) -> String {
1867 debug_assert_ne!(status, NemoRelayStatus::Ok);
1868 set_last_error(host, &format!("{label} failed: {status:?}"));
1869 format!("{label} failed: {status:?}")
1870}
1871
1872fn callback_error(host: &NemoRelayNativeHostApiV1, message: String) -> NemoRelayStatus {
1873 set_last_error(host, &message);
1874 NemoRelayStatus::Internal
1875}
1876
1877fn callback_panic(host: &NemoRelayNativeHostApiV1, label: &str) -> NemoRelayStatus {
1878 set_last_error(host, &format!("{label} panicked"));
1879 NemoRelayStatus::Internal
1880}
1881
1882unsafe extern "C" fn typed_subscriber_trampoline<F>(
1883 user_data: *mut c_void,
1884 event_json: *const NemoRelayNativeString,
1885) -> NemoRelayStatus
1886where
1887 F: Fn(&Event) + Send + Sync + 'static,
1888{
1889 if user_data.is_null() {
1890 return NemoRelayStatus::NullPointer;
1891 }
1892 let state = unsafe { &*(user_data as *const TypedCallback<F>) };
1893 let result = catch_unwind(AssertUnwindSafe(|| {
1894 let event: Event = read_json_value(&state.host, event_json, "event")?;
1895 (state.callback)(&event);
1896 Ok::<_, NemoRelayStatus>(())
1897 }));
1898 match result {
1899 Ok(Ok(())) => NemoRelayStatus::Ok,
1900 Ok(Err(status)) => status,
1901 Err(_) => callback_panic(&state.host, "subscriber callback"),
1902 }
1903}
1904
1905unsafe extern "C" fn typed_tool_sanitize_trampoline<F>(
1906 user_data: *mut c_void,
1907 name: *const NemoRelayNativeString,
1908 payload_json: *const NemoRelayNativeString,
1909 out_json: *mut *mut NemoRelayNativeString,
1910) -> NemoRelayStatus
1911where
1912 F: Fn(&str, Json) -> Json + Send + Sync + 'static,
1913{
1914 if user_data.is_null() || out_json.is_null() {
1915 return NemoRelayStatus::NullPointer;
1916 }
1917 unsafe { *out_json = ptr::null_mut() };
1918 let state = unsafe { &*(user_data as *const TypedCallback<F>) };
1919 let result = catch_unwind(AssertUnwindSafe(|| {
1920 let name = read_required_host_string(&state.host, name, "tool name")?;
1921 let payload: Json = read_json_value(&state.host, payload_json, "tool payload")?;
1922 let output = (state.callback)(&name, payload);
1923 Ok::<_, NemoRelayStatus>(write_json(&state.host, &output, out_json))
1924 }));
1925 match result {
1926 Ok(Ok(status)) => status,
1927 Ok(Err(status)) => status,
1928 Err(_) => callback_panic(&state.host, "tool sanitize callback"),
1929 }
1930}
1931
1932unsafe extern "C" fn typed_tool_intercept_trampoline<F>(
1933 user_data: *mut c_void,
1934 name: *const NemoRelayNativeString,
1935 payload_json: *const NemoRelayNativeString,
1936 out_json: *mut *mut NemoRelayNativeString,
1937) -> NemoRelayStatus
1938where
1939 F: Fn(&str, Json) -> Result<Json> + Send + Sync + 'static,
1940{
1941 if user_data.is_null() || out_json.is_null() {
1942 return NemoRelayStatus::NullPointer;
1943 }
1944 unsafe { *out_json = ptr::null_mut() };
1945 let state = unsafe { &*(user_data as *const TypedCallback<F>) };
1946 let result = catch_unwind(AssertUnwindSafe(|| {
1947 let name = read_required_host_string(&state.host, name, "tool name")?;
1948 let payload: Json = read_json_value(&state.host, payload_json, "tool payload")?;
1949 match (state.callback)(&name, payload) {
1950 Ok(output) => Ok::<_, NemoRelayStatus>(write_json(&state.host, &output, out_json)),
1951 Err(message) => Ok(callback_error(&state.host, message)),
1952 }
1953 }));
1954 match result {
1955 Ok(Ok(status)) => status,
1956 Ok(Err(status)) => status,
1957 Err(_) => callback_panic(&state.host, "tool intercept callback"),
1958 }
1959}
1960
1961unsafe extern "C" fn typed_tool_conditional_trampoline<F>(
1962 user_data: *mut c_void,
1963 name: *const NemoRelayNativeString,
1964 args_json: *const NemoRelayNativeString,
1965 out_reason: *mut *mut NemoRelayNativeString,
1966) -> NemoRelayStatus
1967where
1968 F: Fn(&str, &Json) -> Result<Option<String>> + Send + Sync + 'static,
1969{
1970 if user_data.is_null() || out_reason.is_null() {
1971 return NemoRelayStatus::NullPointer;
1972 }
1973 unsafe { *out_reason = ptr::null_mut() };
1974 let state = unsafe { &*(user_data as *const TypedCallback<F>) };
1975 let result = catch_unwind(AssertUnwindSafe(|| {
1976 let name = read_required_host_string(&state.host, name, "tool name")?;
1977 let args: Json = read_json_value(&state.host, args_json, "tool args")?;
1978 match (state.callback)(&name, &args) {
1979 Ok(Some(reason)) => {
1980 let reason =
1981 HostString::new(&state.host, &reason).ok_or(NemoRelayStatus::Internal)?;
1982 unsafe { *out_reason = reason.ptr };
1983 std::mem::forget(reason);
1984 Ok(NemoRelayStatus::Ok)
1985 }
1986 Ok(None) => {
1987 unsafe { *out_reason = ptr::null_mut() };
1988 Ok(NemoRelayStatus::Ok)
1989 }
1990 Err(message) => Ok(callback_error(&state.host, message)),
1991 }
1992 }));
1993 match result {
1994 Ok(Ok(status)) => status,
1995 Ok(Err(status)) => status,
1996 Err(_) => callback_panic(&state.host, "tool conditional callback"),
1997 }
1998}
1999
2000unsafe extern "C" fn typed_tool_execution_trampoline<F>(
2001 user_data: *mut c_void,
2002 name: *const NemoRelayNativeString,
2003 args_json: *const NemoRelayNativeString,
2004 next_fn: NemoRelayNativeToolNextFn,
2005 next_ctx: *mut c_void,
2006 out_outcome_json: *mut *mut NemoRelayNativeString,
2007) -> NemoRelayStatus
2008where
2009 F: for<'next> Fn(&str, Json, ToolNext<'next>) -> Result<ToolExecutionInterceptOutcome>
2010 + Send
2011 + Sync
2012 + 'static,
2013{
2014 if user_data.is_null() || out_outcome_json.is_null() {
2015 return NemoRelayStatus::NullPointer;
2016 }
2017 unsafe { *out_outcome_json = ptr::null_mut() };
2018 let state = unsafe { &*(user_data as *const TypedCallback<F>) };
2019 let result = catch_unwind(AssertUnwindSafe(|| {
2020 let name = read_required_host_string(&state.host, name, "tool name")?;
2021 let args: Json = read_json_value(&state.host, args_json, "tool args")?;
2022 let next = ToolNext {
2023 host: &state.host,
2024 next_fn,
2025 next_ctx,
2026 };
2027 match (state.callback)(&name, args, next) {
2028 Ok(outcome) => {
2029 let Some(outcome) = HostString::from_json(&state.host, &outcome) else {
2030 set_last_error(&state.host, "failed to allocate tool execution outcome");
2031 return Ok(NemoRelayStatus::Internal);
2032 };
2033 unsafe { *out_outcome_json = outcome.ptr };
2034 std::mem::forget(outcome);
2035 Ok(NemoRelayStatus::Ok)
2036 }
2037 Err(message) => Ok(callback_error(&state.host, message)),
2038 }
2039 }));
2040 match result {
2041 Ok(Ok(status)) => status,
2042 Ok(Err(status)) => status,
2043 Err(_) => callback_panic(&state.host, "tool execution callback"),
2044 }
2045}
2046
2047unsafe extern "C" fn typed_llm_sanitize_request_trampoline<F>(
2048 user_data: *mut c_void,
2049 request_json: *const NemoRelayNativeString,
2050 out_request_json: *mut *mut NemoRelayNativeString,
2051) -> NemoRelayStatus
2052where
2053 F: Fn(LlmRequest) -> LlmRequest + Send + Sync + 'static,
2054{
2055 if user_data.is_null() || out_request_json.is_null() {
2056 return NemoRelayStatus::NullPointer;
2057 }
2058 unsafe { *out_request_json = ptr::null_mut() };
2059 let state = unsafe { &*(user_data as *const TypedCallback<F>) };
2060 let result = catch_unwind(AssertUnwindSafe(|| {
2061 let request: LlmRequest = read_json_value(&state.host, request_json, "LLM request")?;
2062 let output = (state.callback)(request);
2063 Ok::<_, NemoRelayStatus>(write_json(&state.host, &output, out_request_json))
2064 }));
2065 match result {
2066 Ok(Ok(status)) => status,
2067 Ok(Err(status)) => status,
2068 Err(_) => callback_panic(&state.host, "LLM sanitize request callback"),
2069 }
2070}
2071
2072unsafe extern "C" fn typed_llm_sanitize_response_trampoline<F>(
2073 user_data: *mut c_void,
2074 payload_json: *const NemoRelayNativeString,
2075 out_json: *mut *mut NemoRelayNativeString,
2076) -> NemoRelayStatus
2077where
2078 F: Fn(Json) -> Json + Send + Sync + 'static,
2079{
2080 if user_data.is_null() || out_json.is_null() {
2081 return NemoRelayStatus::NullPointer;
2082 }
2083 unsafe { *out_json = ptr::null_mut() };
2084 let state = unsafe { &*(user_data as *const TypedCallback<F>) };
2085 let result = catch_unwind(AssertUnwindSafe(|| {
2086 let payload: Json = read_json_value(&state.host, payload_json, "LLM response")?;
2087 let output = (state.callback)(payload);
2088 Ok::<_, NemoRelayStatus>(write_json(&state.host, &output, out_json))
2089 }));
2090 match result {
2091 Ok(Ok(status)) => status,
2092 Ok(Err(status)) => status,
2093 Err(_) => callback_panic(&state.host, "LLM sanitize response callback"),
2094 }
2095}
2096
2097unsafe extern "C" fn typed_llm_conditional_trampoline<F>(
2098 user_data: *mut c_void,
2099 request_json: *const NemoRelayNativeString,
2100 out_reason: *mut *mut NemoRelayNativeString,
2101) -> NemoRelayStatus
2102where
2103 F: Fn(&LlmRequest) -> Result<Option<String>> + Send + Sync + 'static,
2104{
2105 if user_data.is_null() || out_reason.is_null() {
2106 return NemoRelayStatus::NullPointer;
2107 }
2108 unsafe { *out_reason = ptr::null_mut() };
2109 let state = unsafe { &*(user_data as *const TypedCallback<F>) };
2110 let result = catch_unwind(AssertUnwindSafe(|| {
2111 let request: LlmRequest = read_json_value(&state.host, request_json, "LLM request")?;
2112 match (state.callback)(&request) {
2113 Ok(Some(reason)) => {
2114 let reason =
2115 HostString::new(&state.host, &reason).ok_or(NemoRelayStatus::Internal)?;
2116 unsafe { *out_reason = reason.ptr };
2117 std::mem::forget(reason);
2118 Ok(NemoRelayStatus::Ok)
2119 }
2120 Ok(None) => {
2121 unsafe { *out_reason = ptr::null_mut() };
2122 Ok(NemoRelayStatus::Ok)
2123 }
2124 Err(message) => Ok(callback_error(&state.host, message)),
2125 }
2126 }));
2127 match result {
2128 Ok(Ok(status)) => status,
2129 Ok(Err(status)) => status,
2130 Err(_) => callback_panic(&state.host, "LLM conditional callback"),
2131 }
2132}
2133
2134unsafe extern "C" fn typed_llm_request_intercept_trampoline<F>(
2135 user_data: *mut c_void,
2136 name: *const NemoRelayNativeString,
2137 request_json: *const NemoRelayNativeString,
2138 annotated_json: *const NemoRelayNativeString,
2139 out_outcome_json: *mut *mut NemoRelayNativeString,
2140) -> NemoRelayStatus
2141where
2142 F: Fn(&str, LlmRequest, Option<AnnotatedLlmRequest>) -> Result<LlmRequestInterceptOutcome>
2143 + Send
2144 + Sync
2145 + 'static,
2146{
2147 if user_data.is_null() || out_outcome_json.is_null() {
2148 return NemoRelayStatus::NullPointer;
2149 }
2150 unsafe {
2151 *out_outcome_json = ptr::null_mut();
2152 }
2153 let state = unsafe { &*(user_data as *const TypedCallback<F>) };
2154 let result = catch_unwind(AssertUnwindSafe(|| {
2155 let name = read_required_host_string(&state.host, name, "LLM name")?;
2156 let request: LlmRequest = read_json_value(&state.host, request_json, "LLM request")?;
2157 let annotated: Option<AnnotatedLlmRequest> =
2158 read_optional_json_value(&state.host, annotated_json, "annotated LLM request")?;
2159 match (state.callback)(&name, request, annotated) {
2160 Ok(outcome) => {
2161 let Some(outcome) = HostString::from_json(&state.host, &outcome) else {
2162 set_last_error(&state.host, "failed to allocate LLM request outcome");
2163 return Ok(NemoRelayStatus::Internal);
2164 };
2165 unsafe {
2166 *out_outcome_json = outcome.ptr;
2167 }
2168 std::mem::forget(outcome);
2169 Ok(NemoRelayStatus::Ok)
2170 }
2171 Err(message) => Ok(callback_error(&state.host, message)),
2172 }
2173 }));
2174 match result {
2175 Ok(Ok(status)) => status,
2176 Ok(Err(status)) => status,
2177 Err(_) => callback_panic(&state.host, "LLM request intercept callback"),
2178 }
2179}
2180
2181unsafe extern "C" fn typed_llm_execution_trampoline<F>(
2182 user_data: *mut c_void,
2183 name: *const NemoRelayNativeString,
2184 request_json: *const NemoRelayNativeString,
2185 next_fn: NemoRelayNativeLlmNextFn,
2186 next_ctx: *mut c_void,
2187 out_json: *mut *mut NemoRelayNativeString,
2188) -> NemoRelayStatus
2189where
2190 F: for<'next> Fn(&str, LlmRequest, LlmNext<'next>) -> Result<Json> + Send + Sync + 'static,
2191{
2192 if user_data.is_null() || out_json.is_null() {
2193 return NemoRelayStatus::NullPointer;
2194 }
2195 unsafe { *out_json = ptr::null_mut() };
2196 let state = unsafe { &*(user_data as *const TypedCallback<F>) };
2197 let result = catch_unwind(AssertUnwindSafe(|| {
2198 let name = read_required_host_string(&state.host, name, "LLM name")?;
2199 let request: LlmRequest = read_json_value(&state.host, request_json, "LLM request")?;
2200 let next = LlmNext {
2201 host: &state.host,
2202 next_fn,
2203 next_ctx,
2204 };
2205 match (state.callback)(&name, request, next) {
2206 Ok(output) => Ok::<_, NemoRelayStatus>(write_json(&state.host, &output, out_json)),
2207 Err(message) => Ok(callback_error(&state.host, message)),
2208 }
2209 }));
2210 match result {
2211 Ok(Ok(status)) => status,
2212 Ok(Err(status)) => status,
2213 Err(_) => callback_panic(&state.host, "LLM execution callback"),
2214 }
2215}
2216
2217struct TypedLlmJsonStream {
2218 host: NemoRelayNativeHostApiV1,
2219 state: Mutex<TypedLlmJsonStreamState>,
2220}
2221
2222struct TypedLlmJsonStreamState {
2223 iter: LlmJsonStream,
2224 finished: bool,
2225}
2226
2227fn native_stream_from_iter(
2228 host: &NemoRelayNativeHostApiV1,
2229 iter: LlmJsonStream,
2230) -> NemoRelayNativeLlmStreamV1 {
2231 let state = Box::new(TypedLlmJsonStream {
2232 host: *host,
2233 state: Mutex::new(TypedLlmJsonStreamState {
2234 iter,
2235 finished: false,
2236 }),
2237 });
2238 NemoRelayNativeLlmStreamV1 {
2239 struct_size: std::mem::size_of::<NemoRelayNativeLlmStreamV1>(),
2240 user_data: Box::into_raw(state).cast(),
2241 next: Some(poll_typed_llm_json_stream),
2242 cancel: Some(cancel_typed_llm_json_stream),
2243 drop: Some(drop_typed_llm_json_stream),
2244 }
2245}
2246
2247unsafe extern "C" fn poll_typed_llm_json_stream(
2248 user_data: *mut c_void,
2249 out_json: *mut *mut NemoRelayNativeString,
2250) -> NemoRelayStatus {
2251 if user_data.is_null() || out_json.is_null() {
2252 return NemoRelayStatus::NullPointer;
2253 }
2254 unsafe { *out_json = ptr::null_mut() };
2255 let stream = unsafe { &*(user_data as *const TypedLlmJsonStream) };
2256 let result = catch_unwind(AssertUnwindSafe(|| {
2257 let mut state = match stream.state.lock() {
2258 Ok(state) => state,
2259 Err(_) => {
2260 set_last_error(&stream.host, "native plugin stream state lock poisoned");
2261 return NemoRelayStatus::Internal;
2262 }
2263 };
2264 if state.finished {
2265 return NemoRelayStatus::StreamEnd;
2266 }
2267 match state.iter.next() {
2268 Some(Ok(chunk)) => {
2269 let status = write_json(&stream.host, &chunk, out_json);
2270 if status != NemoRelayStatus::Ok {
2271 state.finished = true;
2272 }
2273 status
2274 }
2275 Some(Err(message)) => {
2276 state.finished = true;
2277 callback_error(&stream.host, message)
2278 }
2279 None => {
2280 state.finished = true;
2281 NemoRelayStatus::StreamEnd
2282 }
2283 }
2284 }));
2285 result.unwrap_or_else(|_| callback_panic(&stream.host, "LLM stream callback"))
2286}
2287
2288unsafe extern "C" fn cancel_typed_llm_json_stream(user_data: *mut c_void) -> NemoRelayStatus {
2289 if user_data.is_null() {
2290 return NemoRelayStatus::NullPointer;
2291 }
2292 let stream = unsafe { &*(user_data as *const TypedLlmJsonStream) };
2293 let result = catch_unwind(AssertUnwindSafe(|| {
2294 let mut state = match stream.state.lock() {
2295 Ok(state) => state,
2296 Err(_) => {
2297 set_last_error(&stream.host, "native plugin stream state lock poisoned");
2298 return NemoRelayStatus::Internal;
2299 }
2300 };
2301 state.finished = true;
2302 NemoRelayStatus::Ok
2303 }));
2304 result.unwrap_or_else(|_| callback_panic(&stream.host, "LLM stream cancel callback"))
2305}
2306
2307unsafe extern "C" fn drop_typed_llm_json_stream(user_data: *mut c_void) {
2308 if !user_data.is_null() {
2309 let stream = unsafe { Box::from_raw(user_data as *mut TypedLlmJsonStream) };
2310 let host = stream.host;
2311 if catch_unwind(AssertUnwindSafe(|| drop(stream))).is_err() {
2312 set_last_error(&host, "native plugin LLM stream state drop panicked");
2313 }
2314 }
2315}
2316
2317unsafe extern "C" fn typed_llm_stream_execution_trampoline<F>(
2318 user_data: *mut c_void,
2319 name: *const NemoRelayNativeString,
2320 request_json: *const NemoRelayNativeString,
2321 next_fn: NemoRelayNativeLlmStreamNextFn,
2322 next_ctx: *mut c_void,
2323 out_stream: *mut NemoRelayNativeLlmStreamV1,
2324) -> NemoRelayStatus
2325where
2326 F: for<'next> Fn(&str, LlmRequest, LlmStreamNext<'next>) -> Result<LlmJsonStream>
2327 + Send
2328 + Sync
2329 + 'static,
2330{
2331 if user_data.is_null() || out_stream.is_null() {
2332 return NemoRelayStatus::NullPointer;
2333 }
2334 unsafe { *out_stream = NemoRelayNativeLlmStreamV1::default() };
2335 let state = unsafe { &*(user_data as *const TypedCallback<F>) };
2336 let result = catch_unwind(AssertUnwindSafe(|| {
2337 let name = read_required_host_string(&state.host, name, "LLM name")?;
2338 let request: LlmRequest = read_json_value(&state.host, request_json, "LLM request")?;
2339 let next = LlmStreamNext {
2340 host: &state.host,
2341 next_fn,
2342 next_ctx,
2343 };
2344 match (state.callback)(&name, request, next) {
2345 Ok(stream) => {
2346 unsafe { *out_stream = native_stream_from_iter(&state.host, stream) };
2347 Ok::<_, NemoRelayStatus>(NemoRelayStatus::Ok)
2348 }
2349 Err(message) => Ok(callback_error(&state.host, message)),
2350 }
2351 }));
2352 match result {
2353 Ok(Ok(status)) => status,
2354 Ok(Err(status)) => status,
2355 Err(_) => callback_panic(&state.host, "LLM stream execution callback"),
2356 }
2357}
2358
2359struct HostString<'a> {
2360 host: &'a NemoRelayNativeHostApiV1,
2361 ptr: *mut NemoRelayNativeString,
2362}
2363
2364impl<'a> HostString<'a> {
2365 fn try_new(
2366 host: &'a NemoRelayNativeHostApiV1,
2367 value: &str,
2368 ) -> std::result::Result<Self, NemoRelayStatus> {
2369 let mut out = ptr::null_mut();
2370 let status = unsafe { (host.string_new)(value.as_ptr(), value.len(), &mut out) };
2371 if status != NemoRelayStatus::Ok {
2372 return Err(status);
2373 }
2374 if out.is_null() {
2375 return Err(NemoRelayStatus::Internal);
2376 }
2377 Ok(Self { host, ptr: out })
2378 }
2379
2380 fn new(host: &'a NemoRelayNativeHostApiV1, value: &str) -> Option<Self> {
2381 Self::try_new(host, value).ok()
2382 }
2383
2384 fn from_json<T: Serialize>(host: &'a NemoRelayNativeHostApiV1, value: &T) -> Option<Self> {
2385 serde_json::to_string(value)
2386 .ok()
2387 .and_then(|json| Self::new(host, &json))
2388 }
2389
2390 fn as_ptr(&self) -> *const NemoRelayNativeString {
2391 self.ptr
2392 }
2393}
2394
2395impl Drop for HostString<'_> {
2396 fn drop(&mut self) {
2397 unsafe { (self.host.string_free)(self.ptr) };
2398 }
2399}
2400
2401struct OptionalHostJson<'a>(Option<HostString<'a>>);
2402
2403impl<'a> OptionalHostJson<'a> {
2404 fn new(host: &'a NemoRelayNativeHostApiV1, value: Option<&Json>) -> Result<Self> {
2405 match value {
2406 Some(value) => HostString::from_json(host, value)
2407 .map(|value| Self(Some(value)))
2408 .ok_or_else(|| "failed to allocate JSON host string".into()),
2409 None => Ok(Self(None)),
2410 }
2411 }
2412
2413 fn as_ptr(&self) -> *const NemoRelayNativeString {
2414 self.0
2415 .as_ref()
2416 .map(HostString::as_ptr)
2417 .unwrap_or(ptr::null())
2418 }
2419}
2420
2421struct PluginState<P> {
2422 host: NemoRelayNativeHostApiV1,
2423 plugin: Mutex<P>,
2424}
2425
2426unsafe extern "C" fn drop_plugin_state<P: NativePlugin>(user_data: *mut c_void) {
2427 if !user_data.is_null() {
2428 let state = unsafe { Box::from_raw(user_data as *mut PluginState<P>) };
2429 let host = state.host;
2430 if catch_unwind(AssertUnwindSafe(|| drop(state))).is_err() {
2431 set_last_error(&host, "native plugin state drop panicked");
2432 }
2433 }
2434}
2435
2436unsafe extern "C" fn validate_trampoline<P: NativePlugin>(
2437 user_data: *mut c_void,
2438 plugin_config_json: *const NemoRelayNativeString,
2439 out_diagnostics_json: *mut *mut NemoRelayNativeString,
2440) -> NemoRelayStatus {
2441 if user_data.is_null() || out_diagnostics_json.is_null() {
2442 return NemoRelayStatus::NullPointer;
2443 }
2444 unsafe { *out_diagnostics_json = ptr::null_mut() };
2445 let state = unsafe { &*(user_data as *const PluginState<P>) };
2446 let result = catch_unwind(AssertUnwindSafe(|| {
2447 let config = match read_json_object(&state.host, plugin_config_json) {
2448 Ok(config) => config,
2449 Err(status) => return status,
2450 };
2451 let plugin = match state.plugin.lock() {
2452 Ok(plugin) => plugin,
2453 Err(_) => {
2454 set_last_error(&state.host, "native plugin state lock poisoned");
2455 return NemoRelayStatus::Internal;
2456 }
2457 };
2458 let diagnostics = plugin.validate(&config);
2459 write_json(&state.host, &diagnostics, out_diagnostics_json)
2460 }));
2461 result.unwrap_or_else(|_| {
2462 set_last_error(&state.host, "native plugin validate callback panicked");
2463 NemoRelayStatus::Internal
2464 })
2465}
2466
2467unsafe extern "C" fn register_trampoline<P: NativePlugin>(
2468 user_data: *mut c_void,
2469 plugin_config_json: *const NemoRelayNativeString,
2470 ctx: *mut NemoRelayNativePluginContext,
2471) -> NemoRelayStatus {
2472 if user_data.is_null() || ctx.is_null() {
2473 return NemoRelayStatus::NullPointer;
2474 }
2475 let state = unsafe { &*(user_data as *const PluginState<P>) };
2476 let result = catch_unwind(AssertUnwindSafe(|| {
2477 let config = match read_json_object(&state.host, plugin_config_json) {
2478 Ok(config) => config,
2479 Err(status) => return status,
2480 };
2481 let mut ctx = unsafe { PluginContext::from_raw(&state.host, ctx) };
2482 let mut plugin = match state.plugin.lock() {
2483 Ok(plugin) => plugin,
2484 Err(_) => {
2485 set_last_error(&state.host, "native plugin state lock poisoned");
2486 return NemoRelayStatus::Internal;
2487 }
2488 };
2489 match plugin.register(&config, &mut ctx) {
2490 Ok(()) => NemoRelayStatus::Ok,
2491 Err(message) => {
2492 set_last_error(&state.host, &message);
2493 NemoRelayStatus::Internal
2494 }
2495 }
2496 }));
2497 result.unwrap_or_else(|_| {
2498 set_last_error(&state.host, "native plugin register callback panicked");
2499 NemoRelayStatus::Internal
2500 })
2501}
2502
2503fn read_json_object(
2504 host: &NemoRelayNativeHostApiV1,
2505 value: *const NemoRelayNativeString,
2506) -> std::result::Result<Map<String, Json>, NemoRelayStatus> {
2507 let value: Json = read_json_value(host, value, "plugin config")?;
2508 match value {
2509 Json::Object(map) => Ok(map),
2510 _ => {
2511 set_last_error(host, "plugin config must be a JSON object");
2512 Err(NemoRelayStatus::InvalidJson)
2513 }
2514 }
2515}
2516
2517fn read_json_value<T: DeserializeOwned>(
2518 host: &NemoRelayNativeHostApiV1,
2519 value: *const NemoRelayNativeString,
2520 label: &str,
2521) -> std::result::Result<T, NemoRelayStatus> {
2522 let text = read_required_host_string(host, value, label)?;
2523 serde_json::from_str::<T>(&text).map_err(|error| {
2524 set_last_error(host, &format!("{label} was invalid JSON: {error}"));
2525 NemoRelayStatus::InvalidJson
2526 })
2527}
2528
2529fn read_optional_json_value<T: DeserializeOwned>(
2530 host: &NemoRelayNativeHostApiV1,
2531 value: *const NemoRelayNativeString,
2532 label: &str,
2533) -> std::result::Result<Option<T>, NemoRelayStatus> {
2534 if value.is_null() {
2535 Ok(None)
2536 } else {
2537 read_json_value(host, value, label).map(Some)
2538 }
2539}
2540
2541enum HostStringReadError {
2542 Null,
2543 InvalidUtf8,
2544}
2545
2546fn read_required_host_string(
2547 host: &NemoRelayNativeHostApiV1,
2548 value: *const NemoRelayNativeString,
2549 label: &str,
2550) -> std::result::Result<String, NemoRelayStatus> {
2551 match read_host_string(host, value) {
2552 Ok(value) => Ok(value),
2553 Err(HostStringReadError::Null) => {
2554 set_last_error(host, &format!("{label} was null"));
2555 Err(NemoRelayStatus::NullPointer)
2556 }
2557 Err(HostStringReadError::InvalidUtf8) => {
2558 set_last_error(host, &format!("{label} contained invalid UTF-8"));
2559 Err(NemoRelayStatus::InvalidUtf8)
2560 }
2561 }
2562}
2563
2564fn read_host_string(
2565 host: &NemoRelayNativeHostApiV1,
2566 value: *const NemoRelayNativeString,
2567) -> std::result::Result<String, HostStringReadError> {
2568 if value.is_null() {
2569 return Err(HostStringReadError::Null);
2570 }
2571 let len = unsafe { (host.string_len)(value) };
2572 let data = unsafe { (host.string_data)(value) };
2573 if data.is_null() && len > 0 {
2574 return Err(HostStringReadError::InvalidUtf8);
2575 }
2576 let bytes = if len == 0 {
2577 &[][..]
2578 } else {
2579 unsafe { std::slice::from_raw_parts(data, len) }
2580 };
2581 std::str::from_utf8(bytes)
2582 .map(str::to_owned)
2583 .map_err(|_| HostStringReadError::InvalidUtf8)
2584}
2585
2586fn write_json<T: Serialize>(
2587 host: &NemoRelayNativeHostApiV1,
2588 value: &T,
2589 out: *mut *mut NemoRelayNativeString,
2590) -> NemoRelayStatus {
2591 if out.is_null() {
2592 return NemoRelayStatus::NullPointer;
2593 }
2594 unsafe { *out = ptr::null_mut() };
2595 let json = serde_json::to_value(value).expect("Relay DTOs and serde_json::Value serialize");
2596 let Some(handle) = HostString::from_json(host, &json) else {
2597 set_last_error(host, "failed to allocate host string");
2598 return NemoRelayStatus::Internal;
2599 };
2600 unsafe { *out = handle.ptr };
2601 std::mem::forget(handle);
2602 NemoRelayStatus::Ok
2603}
2604
2605fn set_last_error(host: &NemoRelayNativeHostApiV1, message: &str) {
2606 if let Some(message) = HostString::new(host, message) {
2607 unsafe { (host.last_error_set)(message.as_ptr()) };
2608 }
2609}
2610
2611#[doc(hidden)]
2616pub unsafe fn __set_last_error_from_entry(host: *const NemoRelayNativeHostApiV1, message: &str) {
2617 if !host.is_null() {
2618 set_last_error(unsafe { &*host }, message);
2619 }
2620}
2621
2622pub unsafe fn export_plugin<P: NativePlugin>(
2629 host: *const NemoRelayNativeHostApiV1,
2630 out: *mut NemoRelayNativePluginV1,
2631 plugin: P,
2632) -> NemoRelayStatus {
2633 if host.is_null() || out.is_null() {
2634 return NemoRelayStatus::NullPointer;
2635 }
2636 unsafe { *out = NemoRelayNativePluginV1::default() };
2637 let host_ref = unsafe { &*host };
2638 export_plugin_checked(host_ref, out, || plugin)
2639}
2640
2641#[doc(hidden)]
2648pub unsafe fn __export_plugin_from_constructor<P, F>(
2649 host: *const NemoRelayNativeHostApiV1,
2650 out: *mut NemoRelayNativePluginV1,
2651 constructor: F,
2652) -> NemoRelayStatus
2653where
2654 P: NativePlugin,
2655 F: FnOnce() -> P,
2656{
2657 if host.is_null() || out.is_null() {
2658 return NemoRelayStatus::NullPointer;
2659 }
2660 unsafe { *out = NemoRelayNativePluginV1::default() };
2661 let host_ref = unsafe { &*host };
2662 export_plugin_checked(host_ref, out, constructor)
2663}
2664
2665fn export_plugin_checked<P, F>(
2666 host_ref: &NemoRelayNativeHostApiV1,
2667 out: *mut NemoRelayNativePluginV1,
2668 constructor: F,
2669) -> NemoRelayStatus
2670where
2671 P: NativePlugin,
2672 F: FnOnce() -> P,
2673{
2674 if host_ref.abi_version != NEMO_RELAY_NATIVE_ABI_VERSION {
2675 return NemoRelayStatus::InvalidArg;
2676 }
2677 if host_ref.struct_size < std::mem::size_of::<NemoRelayNativeHostApiV1>() {
2678 return NemoRelayStatus::InvalidArg;
2679 }
2680
2681 let plugin = constructor();
2682 let kind = plugin.plugin_kind().to_owned();
2683 let allows_multiple_components = plugin.allows_multiple_components();
2684 let Some(kind_handle) = HostString::new(host_ref, &kind) else {
2685 return NemoRelayStatus::Internal;
2686 };
2687 let state = Box::new(PluginState {
2688 host: *host_ref,
2689 plugin: Mutex::new(plugin),
2690 });
2691 unsafe {
2692 *out = NemoRelayNativePluginV1 {
2693 struct_size: std::mem::size_of::<NemoRelayNativePluginV1>(),
2694 plugin_kind: kind_handle.ptr,
2695 allows_multiple_components,
2696 user_data: Box::into_raw(state) as *mut c_void,
2697 validate: Some(validate_trampoline::<P>),
2698 register: Some(register_trampoline::<P>),
2699 drop: Some(drop_plugin_state::<P>),
2700 };
2701 }
2702 std::mem::forget(kind_handle);
2703 NemoRelayStatus::Ok
2704}
2705
2706#[macro_export]
2708macro_rules! nemo_relay_plugin {
2709 ($symbol:ident, $constructor:expr) => {
2710 #[doc = "Native plugin entry symbol generated by `nemo_relay_plugin!`."]
2711 #[unsafe(no_mangle)]
2712 pub unsafe extern "C" fn $symbol(
2713 host: *const $crate::NemoRelayNativeHostApiV1,
2714 out: *mut $crate::NemoRelayNativePluginV1,
2715 ) -> $crate::NemoRelayStatus {
2716 match ::std::panic::catch_unwind(::std::panic::AssertUnwindSafe(|| unsafe {
2717 $crate::__export_plugin_from_constructor(host, out, $constructor)
2718 })) {
2719 Ok(status) => status,
2720 Err(_) => {
2721 unsafe {
2722 $crate::__set_last_error_from_entry(
2723 host,
2724 "native plugin entry callback panicked",
2725 )
2726 };
2727 $crate::NemoRelayStatus::Internal
2728 }
2729 }
2730 }
2731 };
2732}