Skip to main content

nemo_relay_plugin/
lib.rs

1// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4#![deny(rustdoc::broken_intra_doc_links, rustdoc::private_intra_doc_links)]
5
6//! Stable native plugin ABI and Rust authoring helpers for NeMo Relay.
7//!
8//! This crate intentionally does not depend on the `nemo-relay` runtime crate.
9//! Native plugins built with it communicate with a host through versioned
10//! C-compatible tables and host-owned string handles.
11
12use 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, DataSchema, Event, EventCategory, EventSanitizeFields, PendingMarkSpec,
21    ScopeCategory,
22};
23pub use nemo_relay_types::api::llm::{LlmAttributes, LlmRequest, LlmRequestInterceptOutcome};
24pub use nemo_relay_types::api::scope::{HandleAttributes, ScopeAttributes, ScopeType};
25pub use nemo_relay_types::api::tool::{ToolAttributes, ToolExecutionInterceptOutcome};
26pub use nemo_relay_types::codec::optimization::{
27    LlmOptimizationContribution, LlmOptimizationEvidenceQuality, LlmOptimizationKind,
28    LlmOptimizationModel, LlmOptimizationModelTransition, LlmOptimizationPayload,
29    LlmOptimizationSummary, LlmOptimizationSummaryStatus, LlmOptimizationTokenImpact,
30    LlmOptimizationTokens,
31};
32pub use nemo_relay_types::codec::request::AnnotatedLlmRequest;
33pub use nemo_relay_types::codec::response::AnnotatedLlmResponse;
34pub use nemo_relay_types::plugin::{ConfigDiagnostic, DiagnosticLevel};
35use serde::{Serialize, de::DeserializeOwned};
36use serde_json::Map;
37
38/// Native plugin ABI version supported by this crate.
39pub const NEMO_RELAY_NATIVE_ABI_VERSION: u32 = 1;
40
41/// Status codes returned by stable native ABI functions.
42#[repr(i32)]
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum NemoRelayStatus {
45    /// Operation completed successfully.
46    Ok = 0,
47    /// A resource with the given name already exists.
48    AlreadyExists = 1,
49    /// The requested resource was not found.
50    NotFound = 2,
51    /// The scope stack is empty.
52    ScopeStackEmpty = 3,
53    /// A guardrail rejected the operation.
54    GuardrailRejected = 4,
55    /// An internal runtime error occurred.
56    Internal = 5,
57    /// A required pointer argument was null.
58    NullPointer = 6,
59    /// A JSON string argument could not be parsed.
60    InvalidJson = 7,
61    /// A string argument contained invalid UTF-8.
62    InvalidUtf8 = 8,
63    /// A function argument had an invalid value.
64    InvalidArg = 9,
65    /// A stream reached end-of-stream and has no chunk to return.
66    StreamEnd = 10,
67}
68
69/// Opaque host-owned UTF-8 string or JSON byte buffer.
70#[repr(C)]
71pub struct NemoRelayNativeString {
72    _private: [u8; 0],
73    _marker: PhantomData<(*mut u8, PhantomPinned)>,
74}
75
76/// Opaque plugin registration context borrowed from the host during registration.
77#[repr(C)]
78pub struct NemoRelayNativePluginContext {
79    _private: [u8; 0],
80    _marker: PhantomData<(*mut u8, PhantomPinned)>,
81}
82
83/// Opaque host-owned scope handle.
84#[repr(C)]
85pub struct NemoRelayNativeScopeHandle {
86    _private: [u8; 0],
87    _marker: PhantomData<(*mut u8, PhantomPinned)>,
88}
89
90/// Opaque host-owned scope stack handle.
91#[repr(C)]
92pub struct NemoRelayNativeScopeStack {
93    _private: [u8; 0],
94    _marker: PhantomData<(*mut u8, PhantomPinned)>,
95}
96
97/// Opaque host-owned captured scope-stack binding.
98#[repr(C)]
99pub struct NemoRelayNativeScopeStackBinding {
100    _private: [u8; 0],
101    _marker: PhantomData<(*mut u8, PhantomPinned)>,
102}
103
104/// Scope category used by native plugins when opening scopes.
105#[repr(i32)]
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub enum NemoRelayNativeScopeType {
108    /// Top-level agent scope.
109    Agent = 0,
110    /// Generic function scope.
111    Function = 1,
112    /// Tool invocation scope.
113    Tool = 2,
114    /// LLM call scope.
115    Llm = 3,
116    /// Retriever scope.
117    Retriever = 4,
118    /// Embedder scope.
119    Embedder = 5,
120    /// Reranker scope.
121    Reranker = 6,
122    /// Guardrail evaluation scope.
123    Guardrail = 7,
124    /// Evaluator scope.
125    Evaluator = 8,
126    /// User-defined custom scope.
127    Custom = 9,
128    /// Unknown or unspecified scope type.
129    Unknown = 10,
130}
131
132/// Optional destructor for user data captured by native callbacks.
133pub type NemoRelayNativeFreeFn = Option<unsafe extern "C" fn(user_data: *mut c_void)>;
134
135/// Native callback executed while a host scope stack is temporarily active.
136pub type NemoRelayNativeWithScopeStackCb =
137    unsafe extern "C" fn(user_data: *mut c_void) -> NemoRelayStatus;
138
139/// Runtime-provided continuation for tool execution intercepts.
140pub type NemoRelayNativeToolNextFn = unsafe extern "C" fn(
141    args_json: *const NemoRelayNativeString,
142    next_ctx: *mut c_void,
143    out_json: *mut *mut NemoRelayNativeString,
144) -> NemoRelayStatus;
145
146/// Runtime-provided continuation for LLM execution intercepts.
147pub type NemoRelayNativeLlmNextFn = unsafe extern "C" fn(
148    request_json: *const NemoRelayNativeString,
149    next_ctx: *mut c_void,
150    out_json: *mut *mut NemoRelayNativeString,
151) -> NemoRelayStatus;
152
153/// Native stream poll callback.
154///
155/// Return [`NemoRelayStatus::Ok`] with `out_json` set for one chunk,
156/// [`NemoRelayStatus::StreamEnd`] with `out_json` null at end of stream, or an
157/// error status for stream failure.
158pub type NemoRelayNativeLlmStreamPollFn = unsafe extern "C" fn(
159    user_data: *mut c_void,
160    out_json: *mut *mut NemoRelayNativeString,
161) -> NemoRelayStatus;
162
163/// Optional native stream cancellation callback.
164pub type NemoRelayNativeLlmStreamCancelFn =
165    Option<unsafe extern "C" fn(user_data: *mut c_void) -> NemoRelayStatus>;
166
167/// Optional native stream destructor callback.
168pub type NemoRelayNativeLlmStreamDropFn = Option<unsafe extern "C" fn(user_data: *mut c_void)>;
169
170/// Native LLM JSON stream handle table.
171#[repr(C)]
172pub struct NemoRelayNativeLlmStreamV1 {
173    /// Size of this struct as seen by the producer.
174    pub struct_size: usize,
175    /// Stream state passed back to poll/cancel/drop callbacks.
176    pub user_data: *mut c_void,
177    /// Polls the next stream chunk.
178    pub next: Option<NemoRelayNativeLlmStreamPollFn>,
179    /// Cancels an in-flight stream when a consumer stops before stream end.
180    pub cancel: NemoRelayNativeLlmStreamCancelFn,
181    /// Drops stream state after stream completion, error, or cancellation.
182    pub drop: NemoRelayNativeLlmStreamDropFn,
183}
184
185impl Default for NemoRelayNativeLlmStreamV1 {
186    fn default() -> Self {
187        Self {
188            struct_size: std::mem::size_of::<Self>(),
189            user_data: ptr::null_mut(),
190            next: None,
191            cancel: None,
192            drop: None,
193        }
194    }
195}
196
197/// Runtime-provided continuation for LLM stream execution intercepts.
198pub type NemoRelayNativeLlmStreamNextFn = unsafe extern "C" fn(
199    request_json: *const NemoRelayNativeString,
200    next_ctx: *mut c_void,
201    out_stream: *mut NemoRelayNativeLlmStreamV1,
202) -> NemoRelayStatus;
203
204/// Native event subscriber callback.
205pub type NemoRelayNativeEventSubscriberCb = unsafe extern "C" fn(
206    user_data: *mut c_void,
207    event_json: *const NemoRelayNativeString,
208) -> NemoRelayStatus;
209
210/// Native event observability-field sanitizer callback.
211pub type NemoRelayNativeEventSanitizeCb = unsafe extern "C" fn(
212    user_data: *mut c_void,
213    event_json: *const NemoRelayNativeString,
214    fields_json: *const NemoRelayNativeString,
215    out_fields_json: *mut *mut NemoRelayNativeString,
216) -> NemoRelayStatus;
217
218/// Native JSON transform callback for tool request/response sanitizers and tool request intercepts.
219pub type NemoRelayNativeToolJsonCb = unsafe extern "C" fn(
220    user_data: *mut c_void,
221    name: *const NemoRelayNativeString,
222    payload_json: *const NemoRelayNativeString,
223    out_json: *mut *mut NemoRelayNativeString,
224) -> NemoRelayStatus;
225
226/// Native tool conditional-execution callback.
227pub type NemoRelayNativeToolConditionalCb = unsafe extern "C" fn(
228    user_data: *mut c_void,
229    name: *const NemoRelayNativeString,
230    args_json: *const NemoRelayNativeString,
231    out_reason: *mut *mut NemoRelayNativeString,
232) -> NemoRelayStatus;
233
234/// Native tool execution intercept callback.
235pub type NemoRelayNativeToolExecutionCb = unsafe extern "C" fn(
236    user_data: *mut c_void,
237    name: *const NemoRelayNativeString,
238    args_json: *const NemoRelayNativeString,
239    next_fn: NemoRelayNativeToolNextFn,
240    next_ctx: *mut c_void,
241    out_outcome_json: *mut *mut NemoRelayNativeString,
242) -> NemoRelayStatus;
243
244/// Native LLM request transform callback for request sanitizers.
245pub type NemoRelayNativeLlmRequestCb = unsafe extern "C" fn(
246    user_data: *mut c_void,
247    request_json: *const NemoRelayNativeString,
248    out_request_json: *mut *mut NemoRelayNativeString,
249) -> NemoRelayStatus;
250
251/// Native JSON transform callback for LLM response sanitizers.
252pub type NemoRelayNativeJsonCb = unsafe extern "C" fn(
253    user_data: *mut c_void,
254    payload_json: *const NemoRelayNativeString,
255    out_json: *mut *mut NemoRelayNativeString,
256) -> NemoRelayStatus;
257
258/// Native LLM conditional-execution callback.
259pub type NemoRelayNativeLlmConditionalCb = unsafe extern "C" fn(
260    user_data: *mut c_void,
261    request_json: *const NemoRelayNativeString,
262    out_reason: *mut *mut NemoRelayNativeString,
263) -> NemoRelayStatus;
264
265/// Native LLM request intercept callback.
266pub type NemoRelayNativeLlmRequestInterceptCb = unsafe extern "C" fn(
267    user_data: *mut c_void,
268    name: *const NemoRelayNativeString,
269    request_json: *const NemoRelayNativeString,
270    annotated_json: *const NemoRelayNativeString,
271    out_outcome_json: *mut *mut NemoRelayNativeString,
272) -> NemoRelayStatus;
273
274/// Native LLM execution intercept callback.
275pub type NemoRelayNativeLlmExecutionCb = unsafe extern "C" fn(
276    user_data: *mut c_void,
277    name: *const NemoRelayNativeString,
278    request_json: *const NemoRelayNativeString,
279    next_fn: NemoRelayNativeLlmNextFn,
280    next_ctx: *mut c_void,
281    out_json: *mut *mut NemoRelayNativeString,
282) -> NemoRelayStatus;
283
284/// Native LLM stream execution intercept callback.
285pub type NemoRelayNativeLlmStreamExecutionCb = unsafe extern "C" fn(
286    user_data: *mut c_void,
287    name: *const NemoRelayNativeString,
288    request_json: *const NemoRelayNativeString,
289    next_fn: NemoRelayNativeLlmStreamNextFn,
290    next_ctx: *mut c_void,
291    out_stream: *mut NemoRelayNativeLlmStreamV1,
292) -> NemoRelayStatus;
293
294/// Native plugin validation callback.
295pub type NemoRelayNativePluginValidateFn = unsafe extern "C" fn(
296    user_data: *mut c_void,
297    plugin_config_json: *const NemoRelayNativeString,
298    out_diagnostics_json: *mut *mut NemoRelayNativeString,
299) -> NemoRelayStatus;
300
301/// Native plugin registration callback.
302pub type NemoRelayNativePluginRegisterFn = unsafe extern "C" fn(
303    user_data: *mut c_void,
304    plugin_config_json: *const NemoRelayNativeString,
305    ctx: *mut NemoRelayNativePluginContext,
306) -> NemoRelayStatus;
307
308/// Native plugin drop callback.
309pub type NemoRelayNativePluginDropFn = Option<unsafe extern "C" fn(user_data: *mut c_void)>;
310
311/// Versioned host API table passed to native plugin entry symbols.
312#[repr(C)]
313#[derive(Clone, Copy)]
314pub struct NemoRelayNativeHostApiV1 {
315    /// ABI version implemented by this table.
316    pub abi_version: u32,
317    /// Size of this struct as seen by the host.
318    pub struct_size: usize,
319    /// Null-terminated host Relay version string.
320    pub relay_version: *const c_char,
321    /// Allocates a host-owned string from UTF-8 bytes.
322    pub string_new: unsafe extern "C" fn(
323        data: *const u8,
324        len: usize,
325        out: *mut *mut NemoRelayNativeString,
326    ) -> NemoRelayStatus,
327    /// Returns the string data pointer for a host-owned string.
328    pub string_data: unsafe extern "C" fn(value: *const NemoRelayNativeString) -> *const u8,
329    /// Returns the byte length for a host-owned string.
330    pub string_len: unsafe extern "C" fn(value: *const NemoRelayNativeString) -> usize,
331    /// Frees a host-owned string.
332    pub string_free: unsafe extern "C" fn(value: *mut NemoRelayNativeString),
333    /// Clears the host thread-local native ABI error message.
334    pub last_error_clear: unsafe extern "C" fn(),
335    /// Sets the host thread-local native ABI error message.
336    pub last_error_set: unsafe extern "C" fn(message: *const NemoRelayNativeString),
337    /// Registers an event subscriber through the plugin context.
338    pub plugin_context_register_subscriber: unsafe extern "C" fn(
339        ctx: *mut NemoRelayNativePluginContext,
340        name: *const NemoRelayNativeString,
341        cb: NemoRelayNativeEventSubscriberCb,
342        user_data: *mut c_void,
343        free_fn: NemoRelayNativeFreeFn,
344    ) -> NemoRelayStatus,
345    /// Registers a tool sanitize-request guardrail through the plugin context.
346    pub plugin_context_register_tool_sanitize_request_guardrail:
347        unsafe extern "C" fn(
348            ctx: *mut NemoRelayNativePluginContext,
349            name: *const NemoRelayNativeString,
350            priority: i32,
351            cb: NemoRelayNativeToolJsonCb,
352            user_data: *mut c_void,
353            free_fn: NemoRelayNativeFreeFn,
354        ) -> NemoRelayStatus,
355    /// Registers a tool sanitize-response guardrail through the plugin context.
356    pub plugin_context_register_tool_sanitize_response_guardrail:
357        unsafe extern "C" fn(
358            ctx: *mut NemoRelayNativePluginContext,
359            name: *const NemoRelayNativeString,
360            priority: i32,
361            cb: NemoRelayNativeToolJsonCb,
362            user_data: *mut c_void,
363            free_fn: NemoRelayNativeFreeFn,
364        ) -> NemoRelayStatus,
365    /// Registers a tool conditional-execution guardrail through the plugin context.
366    pub plugin_context_register_tool_conditional_execution_guardrail:
367        unsafe extern "C" fn(
368            ctx: *mut NemoRelayNativePluginContext,
369            name: *const NemoRelayNativeString,
370            priority: i32,
371            cb: NemoRelayNativeToolConditionalCb,
372            user_data: *mut c_void,
373            free_fn: NemoRelayNativeFreeFn,
374        ) -> NemoRelayStatus,
375    /// Registers a tool request intercept through the plugin context.
376    pub plugin_context_register_tool_request_intercept: unsafe extern "C" fn(
377        ctx: *mut NemoRelayNativePluginContext,
378        name: *const NemoRelayNativeString,
379        priority: i32,
380        break_chain: bool,
381        cb: NemoRelayNativeToolJsonCb,
382        user_data: *mut c_void,
383        free_fn: NemoRelayNativeFreeFn,
384    )
385        -> NemoRelayStatus,
386    /// Registers a tool execution intercept through the plugin context.
387    pub plugin_context_register_tool_execution_intercept: unsafe extern "C" fn(
388        ctx: *mut NemoRelayNativePluginContext,
389        name: *const NemoRelayNativeString,
390        priority: i32,
391        cb: NemoRelayNativeToolExecutionCb,
392        user_data: *mut c_void,
393        free_fn: NemoRelayNativeFreeFn,
394    )
395        -> NemoRelayStatus,
396    /// Registers an LLM sanitize-request guardrail through the plugin context.
397    pub plugin_context_register_llm_sanitize_request_guardrail:
398        unsafe extern "C" fn(
399            ctx: *mut NemoRelayNativePluginContext,
400            name: *const NemoRelayNativeString,
401            priority: i32,
402            cb: NemoRelayNativeLlmRequestCb,
403            user_data: *mut c_void,
404            free_fn: NemoRelayNativeFreeFn,
405        ) -> NemoRelayStatus,
406    /// Registers an LLM sanitize-response guardrail through the plugin context.
407    pub plugin_context_register_llm_sanitize_response_guardrail:
408        unsafe extern "C" fn(
409            ctx: *mut NemoRelayNativePluginContext,
410            name: *const NemoRelayNativeString,
411            priority: i32,
412            cb: NemoRelayNativeJsonCb,
413            user_data: *mut c_void,
414            free_fn: NemoRelayNativeFreeFn,
415        ) -> NemoRelayStatus,
416    /// Registers an LLM conditional-execution guardrail through the plugin context.
417    pub plugin_context_register_llm_conditional_execution_guardrail:
418        unsafe extern "C" fn(
419            ctx: *mut NemoRelayNativePluginContext,
420            name: *const NemoRelayNativeString,
421            priority: i32,
422            cb: NemoRelayNativeLlmConditionalCb,
423            user_data: *mut c_void,
424            free_fn: NemoRelayNativeFreeFn,
425        ) -> NemoRelayStatus,
426    /// Registers an LLM request intercept through the plugin context.
427    pub plugin_context_register_llm_request_intercept: unsafe extern "C" fn(
428        ctx: *mut NemoRelayNativePluginContext,
429        name: *const NemoRelayNativeString,
430        priority: i32,
431        break_chain: bool,
432        cb: NemoRelayNativeLlmRequestInterceptCb,
433        user_data: *mut c_void,
434        free_fn: NemoRelayNativeFreeFn,
435    ) -> NemoRelayStatus,
436    /// Registers an LLM execution intercept through the plugin context.
437    pub plugin_context_register_llm_execution_intercept: unsafe extern "C" fn(
438        ctx: *mut NemoRelayNativePluginContext,
439        name: *const NemoRelayNativeString,
440        priority: i32,
441        cb: NemoRelayNativeLlmExecutionCb,
442        user_data: *mut c_void,
443        free_fn: NemoRelayNativeFreeFn,
444    )
445        -> NemoRelayStatus,
446    /// Registers an LLM stream execution intercept through the plugin context.
447    pub plugin_context_register_llm_stream_execution_intercept:
448        unsafe extern "C" fn(
449            ctx: *mut NemoRelayNativePluginContext,
450            name: *const NemoRelayNativeString,
451            priority: i32,
452            cb: NemoRelayNativeLlmStreamExecutionCb,
453            user_data: *mut c_void,
454            free_fn: NemoRelayNativeFreeFn,
455        ) -> NemoRelayStatus,
456    /// Frees a host-owned scope handle.
457    pub scope_handle_free: unsafe extern "C" fn(handle: *mut NemoRelayNativeScopeHandle),
458    /// Retrieves the current scope handle from the active stack.
459    pub scope_get_current:
460        unsafe extern "C" fn(out: *mut *mut NemoRelayNativeScopeHandle) -> NemoRelayStatus,
461    /// Pushes a scope, emits its start event, and returns its handle.
462    pub scope_push: unsafe extern "C" fn(
463        name: *const NemoRelayNativeString,
464        scope_type: NemoRelayNativeScopeType,
465        parent: *const NemoRelayNativeScopeHandle,
466        attributes: u32,
467        data_json: *const NemoRelayNativeString,
468        metadata_json: *const NemoRelayNativeString,
469        input_json: *const NemoRelayNativeString,
470        timestamp_unix_micros: *const i64,
471        out: *mut *mut NemoRelayNativeScopeHandle,
472    ) -> NemoRelayStatus,
473    /// Pops a scope handle, emits its end event, and clears scope-local registrations.
474    pub scope_pop: unsafe extern "C" fn(
475        handle: *const NemoRelayNativeScopeHandle,
476        output_json: *const NemoRelayNativeString,
477        metadata_json: *const NemoRelayNativeString,
478        timestamp_unix_micros: *const i64,
479    ) -> NemoRelayStatus,
480    /// Emits a mark event under the current or provided parent scope.
481    pub emit_mark: unsafe extern "C" fn(
482        name: *const NemoRelayNativeString,
483        parent: *const NemoRelayNativeScopeHandle,
484        data_json: *const NemoRelayNativeString,
485        metadata_json: *const NemoRelayNativeString,
486        timestamp_unix_micros: *const i64,
487    ) -> NemoRelayStatus,
488    /// Creates a new independent scope stack with its own root scope.
489    pub scope_stack_create:
490        unsafe extern "C" fn(out: *mut *mut NemoRelayNativeScopeStack) -> NemoRelayStatus,
491    /// Frees a host-owned scope stack handle.
492    pub scope_stack_free: unsafe extern "C" fn(stack: *mut NemoRelayNativeScopeStack),
493    /// Binds a scope stack to the current OS thread.
494    pub scope_stack_set_thread:
495        unsafe extern "C" fn(stack: *const NemoRelayNativeScopeStack) -> NemoRelayStatus,
496    /// Captures the current thread-local scope-stack binding.
497    pub scope_stack_capture_thread:
498        unsafe extern "C" fn(out: *mut *mut NemoRelayNativeScopeStackBinding) -> NemoRelayStatus,
499    /// Restores and frees a captured thread-local scope-stack binding.
500    pub scope_stack_restore_thread:
501        unsafe extern "C" fn(binding: *mut NemoRelayNativeScopeStackBinding) -> NemoRelayStatus,
502    /// Frees a captured thread-local binding without restoring it.
503    pub scope_stack_binding_free:
504        unsafe extern "C" fn(binding: *mut NemoRelayNativeScopeStackBinding),
505    /// Returns whether the current context has an explicitly active scope stack.
506    pub scope_stack_active: unsafe extern "C" fn() -> bool,
507    /// Runs a callback with the provided scope stack visible to host runtime APIs.
508    pub scope_stack_with_current: unsafe extern "C" fn(
509        stack: *const NemoRelayNativeScopeStack,
510        cb: NemoRelayNativeWithScopeStackCb,
511        user_data: *mut c_void,
512    ) -> NemoRelayStatus,
513    /// Registers a mark event sanitizer through the plugin context.
514    pub plugin_context_register_mark_sanitize_guardrail: unsafe extern "C" fn(
515        ctx: *mut NemoRelayNativePluginContext,
516        name: *const NemoRelayNativeString,
517        priority: i32,
518        cb: NemoRelayNativeEventSanitizeCb,
519        user_data: *mut c_void,
520        free_fn: NemoRelayNativeFreeFn,
521    )
522        -> NemoRelayStatus,
523    /// Registers a scope-start event sanitizer through the plugin context.
524    pub plugin_context_register_scope_sanitize_start_guardrail:
525        unsafe extern "C" fn(
526            ctx: *mut NemoRelayNativePluginContext,
527            name: *const NemoRelayNativeString,
528            priority: i32,
529            cb: NemoRelayNativeEventSanitizeCb,
530            user_data: *mut c_void,
531            free_fn: NemoRelayNativeFreeFn,
532        ) -> NemoRelayStatus,
533    /// Registers a scope-end event sanitizer through the plugin context.
534    pub plugin_context_register_scope_sanitize_end_guardrail:
535        unsafe extern "C" fn(
536            ctx: *mut NemoRelayNativePluginContext,
537            name: *const NemoRelayNativeString,
538            priority: i32,
539            cb: NemoRelayNativeEventSanitizeCb,
540            user_data: *mut c_void,
541            free_fn: NemoRelayNativeFreeFn,
542        ) -> NemoRelayStatus,
543}
544
545// The host API table is immutable after construction. Function pointers and
546// the null-terminated version string pointer are safe to share across threads.
547unsafe impl Send for NemoRelayNativeHostApiV1 {}
548unsafe impl Sync for NemoRelayNativeHostApiV1 {}
549
550/// Versioned plugin descriptor returned by native plugin entry symbols.
551#[repr(C)]
552pub struct NemoRelayNativePluginV1 {
553    /// Size of this struct as seen by the plugin.
554    pub struct_size: usize,
555    /// Host-owned plugin kind string.
556    pub plugin_kind: *mut NemoRelayNativeString,
557    /// Whether this plugin kind supports multiple configured components.
558    pub allows_multiple_components: bool,
559    /// Plugin-owned state pointer passed to callbacks.
560    pub user_data: *mut c_void,
561    /// Optional validation callback.
562    pub validate: Option<NemoRelayNativePluginValidateFn>,
563    /// Required registration callback.
564    pub register: Option<NemoRelayNativePluginRegisterFn>,
565    /// Optional plugin-owned state destructor.
566    pub drop: NemoRelayNativePluginDropFn,
567}
568
569impl Default for NemoRelayNativePluginV1 {
570    fn default() -> Self {
571        Self {
572            struct_size: std::mem::size_of::<Self>(),
573            plugin_kind: ptr::null_mut(),
574            allows_multiple_components: true,
575            user_data: ptr::null_mut(),
576            validate: None,
577            register: None,
578            drop: None,
579        }
580    }
581}
582
583/// Native entry symbol type loaded by the host.
584pub type NemoRelayNativePluginEntry = unsafe extern "C" fn(
585    host: *const NemoRelayNativeHostApiV1,
586    out: *mut NemoRelayNativePluginV1,
587) -> NemoRelayStatus;
588
589/// Result type used by the Rust native plugin SDK.
590pub type Result<T> = std::result::Result<T, String>;
591
592/// Synchronous JSON chunk stream used by native LLM stream intercept helpers.
593pub type LlmJsonStream = Box<dyn Iterator<Item = Result<Json>> + Send>;
594
595/// Cloneable high-level runtime handle for host APIs available to native plugins.
596#[derive(Clone)]
597pub struct PluginRuntime {
598    host: NemoRelayNativeHostApiV1,
599}
600
601impl PluginRuntime {
602    /// Creates a runtime handle from the host ABI table.
603    pub fn new(host: &NemoRelayNativeHostApiV1) -> Self {
604        Self { host: *host }
605    }
606
607    /// Returns the underlying host ABI table.
608    pub fn host_api(&self) -> &NemoRelayNativeHostApiV1 {
609        &self.host
610    }
611
612    /// Retrieves the current scope handle.
613    pub fn current_scope(&self) -> Result<ScopeHandle<'_>> {
614        current_scope(&self.host)
615    }
616
617    /// Pushes a scope and emits its start event.
618    pub fn push_scope(
619        &self,
620        name: &str,
621        scope_type: ScopeType,
622        data: Option<&Json>,
623        metadata: Option<&Json>,
624        input: Option<&Json>,
625    ) -> Result<ScopeHandle<'_>> {
626        push_scope(&self.host, name, scope_type.into(), data, metadata, input)
627    }
628
629    /// Pops a scope and emits its end event.
630    pub fn pop_scope(
631        &self,
632        handle: &ScopeHandle<'_>,
633        output: Option<&Json>,
634        metadata: Option<&Json>,
635    ) -> Result<()> {
636        pop_scope(&self.host, handle, output, metadata)
637    }
638
639    /// Opens a scope that is popped automatically when the guard is closed or dropped.
640    pub fn scope(
641        &self,
642        name: &str,
643        scope_type: ScopeType,
644        data: Option<&Json>,
645        metadata: Option<&Json>,
646        input: Option<&Json>,
647    ) -> Result<ScopeGuard<'_>> {
648        let handle = self.push_scope(name, scope_type, data, metadata, input)?;
649        Ok(ScopeGuard {
650            runtime: self,
651            handle: Some(handle),
652        })
653    }
654
655    /// Emits a mark event under the current scope.
656    pub fn emit_mark(
657        &self,
658        name: &str,
659        data: Option<&Json>,
660        metadata: Option<&Json>,
661    ) -> Result<()> {
662        emit_mark(&self.host, name, data, metadata)
663    }
664
665    /// Creates a new independent scope stack.
666    pub fn create_scope_stack(&self) -> Result<ScopeStack<'_>> {
667        create_scope_stack(&self.host)
668    }
669
670    /// Captures the current thread-local scope-stack binding.
671    pub fn capture_scope_stack_thread(&self) -> Result<ScopeStackBinding<'_>> {
672        capture_scope_stack_thread(&self.host)
673    }
674
675    /// Returns whether the current context has an explicitly active scope stack.
676    pub fn scope_stack_active(&self) -> bool {
677        unsafe { (self.host.scope_stack_active)() }
678    }
679
680    /// Binds `stack` to the current OS thread until the returned guard is dropped.
681    pub fn bind_scope_stack_thread<'a>(
682        &'a self,
683        stack: &'a ScopeStack<'a>,
684    ) -> Result<ThreadScopeStackGuard<'a>> {
685        let previous = self.capture_scope_stack_thread()?;
686        let status = stack.set_thread();
687        if status == NemoRelayStatus::Ok {
688            Ok(ThreadScopeStackGuard {
689                previous: Some(previous),
690            })
691        } else {
692            let _ = previous.restore();
693            Err(format!("scope_stack_set_thread failed: {status:?}"))
694        }
695    }
696}
697
698impl From<ScopeType> for NemoRelayNativeScopeType {
699    fn from(value: ScopeType) -> Self {
700        match value {
701            ScopeType::Agent => Self::Agent,
702            ScopeType::Function => Self::Function,
703            ScopeType::Tool => Self::Tool,
704            ScopeType::Llm => Self::Llm,
705            ScopeType::Retriever => Self::Retriever,
706            ScopeType::Embedder => Self::Embedder,
707            ScopeType::Reranker => Self::Reranker,
708            ScopeType::Guardrail => Self::Guardrail,
709            ScopeType::Evaluator => Self::Evaluator,
710            ScopeType::Custom => Self::Custom,
711            ScopeType::Unknown => Self::Unknown,
712        }
713    }
714}
715
716/// RAII guard for a host scope opened by [`PluginRuntime::scope`].
717pub struct ScopeGuard<'a> {
718    runtime: &'a PluginRuntime,
719    handle: Option<ScopeHandle<'a>>,
720}
721
722impl<'a> ScopeGuard<'a> {
723    /// Returns the active scope handle.
724    pub fn handle(&self) -> Option<&ScopeHandle<'a>> {
725        self.handle.as_ref()
726    }
727
728    /// Pops the scope with optional output and metadata.
729    pub fn close(&mut self, output: Option<&Json>, metadata: Option<&Json>) -> Result<()> {
730        let Some(handle) = self.handle.as_ref() else {
731            return Ok(());
732        };
733        self.runtime.pop_scope(handle, output, metadata)?;
734        self.handle.take();
735        Ok(())
736    }
737}
738
739impl Drop for ScopeGuard<'_> {
740    fn drop(&mut self) {
741        if let Some(handle) = self.handle.take() {
742            let _ = self.runtime.pop_scope(&handle, None, None);
743        }
744    }
745}
746
747/// RAII guard that restores the previous thread-local scope stack on drop.
748pub struct ThreadScopeStackGuard<'a> {
749    previous: Option<ScopeStackBinding<'a>>,
750}
751
752impl ThreadScopeStackGuard<'_> {
753    /// Restores the previous thread-local scope stack immediately.
754    pub fn restore(mut self) -> Result<()> {
755        let Some(previous) = self.previous.take() else {
756            return Ok(());
757        };
758        let status = previous.restore();
759        if status == NemoRelayStatus::Ok {
760            Ok(())
761        } else {
762            Err(format!("scope_stack_restore_thread failed: {status:?}"))
763        }
764    }
765}
766
767impl Drop for ThreadScopeStackGuard<'_> {
768    fn drop(&mut self) {
769        if let Some(previous) = self.previous.take() {
770            let _ = previous.restore();
771        }
772    }
773}
774
775/// Typed continuation passed to tool execution intercepts.
776pub struct ToolNext<'a> {
777    host: &'a NemoRelayNativeHostApiV1,
778    next_fn: NemoRelayNativeToolNextFn,
779    next_ctx: *mut c_void,
780}
781
782impl ToolNext<'_> {
783    /// Continues the tool execution chain with replacement arguments.
784    pub fn call(&self, args: Json) -> Result<Json> {
785        let args = HostString::from_json(self.host, &args)
786            .ok_or_else(|| "failed to allocate tool next args".to_string())?;
787        let mut out = ptr::null_mut();
788        let status = unsafe { (self.next_fn)(args.as_ptr(), self.next_ctx, &mut out) };
789        if status != NemoRelayStatus::Ok {
790            return Err(format!("tool next failed: {status:?}"));
791        }
792        if out.is_null() {
793            return Err("tool next returned null output".into());
794        }
795        let result = read_json_value(self.host, out, "tool next result");
796        unsafe { (self.host.string_free)(out) };
797        result.map_err(|status| format!("tool next returned invalid JSON: {status:?}"))
798    }
799}
800
801/// Typed continuation passed to LLM execution intercepts.
802pub struct LlmNext<'a> {
803    host: &'a NemoRelayNativeHostApiV1,
804    next_fn: NemoRelayNativeLlmNextFn,
805    next_ctx: *mut c_void,
806}
807
808impl LlmNext<'_> {
809    /// Continues the LLM execution chain with a replacement request.
810    pub fn call(&self, request: LlmRequest) -> Result<Json> {
811        let request = HostString::from_json(self.host, &request)
812            .ok_or_else(|| "failed to allocate LLM next request".to_string())?;
813        let mut out = ptr::null_mut();
814        let status = unsafe { (self.next_fn)(request.as_ptr(), self.next_ctx, &mut out) };
815        if status != NemoRelayStatus::Ok {
816            return Err(format!("llm next failed: {status:?}"));
817        }
818        if out.is_null() {
819            return Err("llm next returned null output".into());
820        }
821        let result = read_json_value(self.host, out, "llm next result");
822        unsafe { (self.host.string_free)(out) };
823        result.map_err(|status| format!("llm next returned invalid JSON: {status:?}"))
824    }
825}
826
827/// Typed continuation passed to LLM stream execution intercepts.
828pub struct LlmStreamNext<'a> {
829    host: &'a NemoRelayNativeHostApiV1,
830    next_fn: NemoRelayNativeLlmStreamNextFn,
831    next_ctx: *mut c_void,
832}
833
834impl LlmStreamNext<'_> {
835    /// Continues the LLM stream execution chain with a replacement request.
836    pub fn call(&self, request: LlmRequest) -> Result<LlmStream> {
837        let request = HostString::from_json(self.host, &request)
838            .ok_or_else(|| "failed to allocate LLM stream next request".to_string())?;
839        let mut raw = NemoRelayNativeLlmStreamV1::default();
840        let status = unsafe { (self.next_fn)(request.as_ptr(), self.next_ctx, &mut raw) };
841        if status != NemoRelayStatus::Ok {
842            return Err(format!("llm stream next failed: {status:?}"));
843        }
844        unsafe { LlmStream::from_raw(self.host, raw) }
845    }
846}
847
848/// Host- or plugin-owned stream returned across the native LLM stream ABI.
849pub struct LlmStream {
850    host: NemoRelayNativeHostApiV1,
851    raw: NemoRelayNativeLlmStreamV1,
852    finished: bool,
853}
854
855// The host ABI table is Send, and stream ownership is exclusive through this wrapper.
856unsafe impl Send for LlmStream {}
857
858impl LlmStream {
859    /// Creates a typed stream wrapper from a raw stream table.
860    ///
861    /// # Safety
862    /// `raw` must contain callbacks and `user_data` produced by the same host
863    /// and must not be used again after it is moved into this wrapper.
864    pub unsafe fn from_raw(
865        host: &NemoRelayNativeHostApiV1,
866        mut raw: NemoRelayNativeLlmStreamV1,
867    ) -> Result<Self> {
868        let expected_size = std::mem::size_of::<NemoRelayNativeLlmStreamV1>();
869        if raw.struct_size != expected_size {
870            if raw.struct_size >= expected_size {
871                unsafe { drop_raw_llm_stream(&mut raw) };
872            }
873            return Err(format!(
874                "unsupported LLM stream struct size: {}",
875                raw.struct_size
876            ));
877        }
878        if raw.next.is_none() {
879            unsafe { drop_raw_llm_stream(&mut raw) };
880            return Err("LLM stream next callback was null".into());
881        }
882        Ok(Self {
883            host: *host,
884            raw,
885            finished: false,
886        })
887    }
888
889    /// Polls the next stream chunk.
890    pub fn next_chunk(&mut self) -> Result<Option<Json>> {
891        if self.finished {
892            return Ok(None);
893        }
894        let next = self
895            .raw
896            .next
897            .expect("LLM stream next callback is validated on construction");
898        let mut out = ptr::null_mut();
899        let status = unsafe { next(self.raw.user_data, &mut out) };
900        match status {
901            NemoRelayStatus::Ok => {
902                if out.is_null() {
903                    self.finished = true;
904                    return Err("LLM stream returned null chunk".into());
905                }
906                let result = read_json_value(&self.host, out, "LLM stream chunk");
907                unsafe { (self.host.string_free)(out) };
908                match result {
909                    Ok(chunk) => Ok(Some(chunk)),
910                    Err(status) => {
911                        self.finished = true;
912                        Err(format!("LLM stream returned invalid JSON: {status:?}"))
913                    }
914                }
915            }
916            NemoRelayStatus::StreamEnd => {
917                if !out.is_null() {
918                    unsafe { (self.host.string_free)(out) };
919                }
920                self.finished = true;
921                Ok(None)
922            }
923            other => {
924                if !out.is_null() {
925                    unsafe { (self.host.string_free)(out) };
926                }
927                self.finished = true;
928                Err(format!("LLM stream failed: {other:?}"))
929            }
930        }
931    }
932
933    /// Cancels the stream if it has not reached end-of-stream.
934    pub fn cancel(&mut self) -> Result<()> {
935        if self.finished {
936            return Ok(());
937        }
938        if let Some(cancel) = self.raw.cancel {
939            let status = unsafe { cancel(self.raw.user_data) };
940            if status != NemoRelayStatus::Ok {
941                return Err(format!("LLM stream cancel failed: {status:?}"));
942            }
943        }
944        self.finished = true;
945        Ok(())
946    }
947}
948
949impl Iterator for LlmStream {
950    type Item = Result<Json>;
951
952    fn next(&mut self) -> Option<Self::Item> {
953        match self.next_chunk() {
954            Ok(Some(chunk)) => Some(Ok(chunk)),
955            Ok(None) => None,
956            Err(message) => Some(Err(message)),
957        }
958    }
959}
960
961unsafe fn drop_raw_llm_stream(raw: &mut NemoRelayNativeLlmStreamV1) {
962    if let Some(drop_fn) = raw.drop.take() {
963        unsafe { drop_fn(raw.user_data) };
964    }
965    raw.user_data = ptr::null_mut();
966}
967
968impl Drop for LlmStream {
969    fn drop(&mut self) {
970        if !self.finished {
971            if let Some(cancel) = self.raw.cancel {
972                let _ = unsafe { cancel(self.raw.user_data) };
973            }
974            self.finished = true;
975        }
976        unsafe { drop_raw_llm_stream(&mut self.raw) };
977    }
978}
979
980/// Host-owned scope handle returned by native scope APIs.
981pub struct ScopeHandle<'a> {
982    host: &'a NemoRelayNativeHostApiV1,
983    ptr: *mut NemoRelayNativeScopeHandle,
984}
985
986impl<'a> ScopeHandle<'a> {
987    /// Returns the raw ABI pointer.
988    pub fn as_ptr(&self) -> *const NemoRelayNativeScopeHandle {
989        self.ptr
990    }
991}
992
993impl Drop for ScopeHandle<'_> {
994    fn drop(&mut self) {
995        unsafe { (self.host.scope_handle_free)(self.ptr) };
996    }
997}
998
999/// Host-owned isolated scope stack returned by native scope-stack APIs.
1000pub struct ScopeStack<'a> {
1001    host: &'a NemoRelayNativeHostApiV1,
1002    ptr: *mut NemoRelayNativeScopeStack,
1003}
1004
1005impl<'a> ScopeStack<'a> {
1006    /// Returns the raw ABI pointer.
1007    pub fn as_ptr(&self) -> *const NemoRelayNativeScopeStack {
1008        self.ptr
1009    }
1010
1011    fn set_thread(&self) -> NemoRelayStatus {
1012        unsafe { (self.host.scope_stack_set_thread)(self.ptr) }
1013    }
1014
1015    /// Executes `f` while this stack is visible to host runtime APIs.
1016    pub fn with_current<F>(&self, f: F) -> Result<()>
1017    where
1018        F: FnOnce() -> Result<()>,
1019    {
1020        struct State<F> {
1021            f: Option<F>,
1022            error: Option<String>,
1023        }
1024
1025        unsafe extern "C" fn trampoline<F>(user_data: *mut c_void) -> NemoRelayStatus
1026        where
1027            F: FnOnce() -> Result<()>,
1028        {
1029            if user_data.is_null() {
1030                return NemoRelayStatus::NullPointer;
1031            }
1032            let state = unsafe { &mut *(user_data as *mut State<F>) };
1033            let result = catch_unwind(AssertUnwindSafe(|| {
1034                let Some(f) = state.f.take() else {
1035                    return Err("scope-stack callback was already consumed".to_string());
1036                };
1037                f()
1038            }));
1039            match result {
1040                Ok(Ok(())) => NemoRelayStatus::Ok,
1041                Ok(Err(message)) => {
1042                    state.error = Some(message);
1043                    NemoRelayStatus::Internal
1044                }
1045                Err(_) => {
1046                    state.error = Some("scope-stack callback panicked".into());
1047                    NemoRelayStatus::Internal
1048                }
1049            }
1050        }
1051
1052        let mut state = State {
1053            f: Some(f),
1054            error: None,
1055        };
1056        let status = unsafe {
1057            (self.host.scope_stack_with_current)(
1058                self.ptr,
1059                trampoline::<F>,
1060                (&mut state as *mut State<_>).cast(),
1061            )
1062        };
1063        if status == NemoRelayStatus::Ok {
1064            Ok(())
1065        } else {
1066            Err(state
1067                .error
1068                .unwrap_or_else(|| format!("scope_stack_with_current failed: {status:?}")))
1069        }
1070    }
1071}
1072
1073impl Drop for ScopeStack<'_> {
1074    fn drop(&mut self) {
1075        unsafe { (self.host.scope_stack_free)(self.ptr) };
1076    }
1077}
1078
1079/// Captured thread-local scope-stack binding.
1080pub struct ScopeStackBinding<'a> {
1081    host: &'a NemoRelayNativeHostApiV1,
1082    ptr: *mut NemoRelayNativeScopeStackBinding,
1083}
1084
1085impl<'a> ScopeStackBinding<'a> {
1086    /// Restores and consumes this binding.
1087    pub fn restore(mut self) -> NemoRelayStatus {
1088        let ptr = std::mem::replace(&mut self.ptr, ptr::null_mut());
1089        unsafe { (self.host.scope_stack_restore_thread)(ptr) }
1090    }
1091}
1092
1093impl Drop for ScopeStackBinding<'_> {
1094    fn drop(&mut self) {
1095        if !self.ptr.is_null() {
1096            unsafe { (self.host.scope_stack_binding_free)(self.ptr) };
1097        }
1098    }
1099}
1100
1101/// Retrieves the current scope handle.
1102pub fn current_scope(host: &NemoRelayNativeHostApiV1) -> Result<ScopeHandle<'_>> {
1103    let mut out = ptr::null_mut();
1104    let status = unsafe { (host.scope_get_current)(&mut out) };
1105    if status == NemoRelayStatus::Ok && !out.is_null() {
1106        Ok(ScopeHandle { host, ptr: out })
1107    } else {
1108        Err(format!("scope_get_current failed: {status:?}"))
1109    }
1110}
1111
1112/// Pushes a scope and emits its start event.
1113pub fn push_scope<'a>(
1114    host: &'a NemoRelayNativeHostApiV1,
1115    name: &str,
1116    scope_type: NemoRelayNativeScopeType,
1117    data: Option<&Json>,
1118    metadata: Option<&Json>,
1119    input: Option<&Json>,
1120) -> Result<ScopeHandle<'a>> {
1121    let name =
1122        HostString::new(host, name).ok_or_else(|| "failed to allocate scope name".to_string())?;
1123    let data = OptionalHostJson::new(host, data)?;
1124    let metadata = OptionalHostJson::new(host, metadata)?;
1125    let input = OptionalHostJson::new(host, input)?;
1126    let mut out = ptr::null_mut();
1127    let status = unsafe {
1128        (host.scope_push)(
1129            name.as_ptr(),
1130            scope_type,
1131            ptr::null(),
1132            0,
1133            data.as_ptr(),
1134            metadata.as_ptr(),
1135            input.as_ptr(),
1136            ptr::null(),
1137            &mut out,
1138        )
1139    };
1140    if status == NemoRelayStatus::Ok && !out.is_null() {
1141        Ok(ScopeHandle { host, ptr: out })
1142    } else {
1143        Err(format!("scope_push failed: {status:?}"))
1144    }
1145}
1146
1147/// Pops a scope and emits its end event.
1148pub fn pop_scope(
1149    host: &NemoRelayNativeHostApiV1,
1150    handle: &ScopeHandle<'_>,
1151    output: Option<&Json>,
1152    metadata: Option<&Json>,
1153) -> Result<()> {
1154    let output = OptionalHostJson::new(host, output)?;
1155    let metadata = OptionalHostJson::new(host, metadata)?;
1156    let status = unsafe {
1157        (host.scope_pop)(
1158            handle.as_ptr(),
1159            output.as_ptr(),
1160            metadata.as_ptr(),
1161            ptr::null(),
1162        )
1163    };
1164    if status == NemoRelayStatus::Ok {
1165        Ok(())
1166    } else {
1167        Err(format!("scope_pop failed: {status:?}"))
1168    }
1169}
1170
1171/// Emits a mark event under the current scope.
1172pub fn emit_mark(
1173    host: &NemoRelayNativeHostApiV1,
1174    name: &str,
1175    data: Option<&Json>,
1176    metadata: Option<&Json>,
1177) -> Result<()> {
1178    let name =
1179        HostString::new(host, name).ok_or_else(|| "failed to allocate mark name".to_string())?;
1180    let data = OptionalHostJson::new(host, data)?;
1181    let metadata = OptionalHostJson::new(host, metadata)?;
1182    let status = unsafe {
1183        (host.emit_mark)(
1184            name.as_ptr(),
1185            ptr::null(),
1186            data.as_ptr(),
1187            metadata.as_ptr(),
1188            ptr::null(),
1189        )
1190    };
1191    if status == NemoRelayStatus::Ok {
1192        Ok(())
1193    } else {
1194        Err(format!("emit_mark failed: {status:?}"))
1195    }
1196}
1197
1198/// Creates a new independent scope stack.
1199pub fn create_scope_stack(host: &NemoRelayNativeHostApiV1) -> Result<ScopeStack<'_>> {
1200    let mut out = ptr::null_mut();
1201    let status = unsafe { (host.scope_stack_create)(&mut out) };
1202    if status == NemoRelayStatus::Ok && !out.is_null() {
1203        Ok(ScopeStack { host, ptr: out })
1204    } else {
1205        Err(format!("scope_stack_create failed: {status:?}"))
1206    }
1207}
1208
1209/// Captures the current thread-local scope-stack binding.
1210pub fn capture_scope_stack_thread(
1211    host: &NemoRelayNativeHostApiV1,
1212) -> Result<ScopeStackBinding<'_>> {
1213    let mut out = ptr::null_mut();
1214    let status = unsafe { (host.scope_stack_capture_thread)(&mut out) };
1215    if status == NemoRelayStatus::Ok && !out.is_null() {
1216        Ok(ScopeStackBinding { host, ptr: out })
1217    } else {
1218        Err(format!("scope_stack_capture_thread failed: {status:?}"))
1219    }
1220}
1221
1222/// Trait implemented by Rust native plugins.
1223pub trait NativePlugin: Send + 'static {
1224    /// Returns the stable plugin kind.
1225    fn plugin_kind(&self) -> &str;
1226
1227    /// Returns whether the plugin allows multiple configured components.
1228    fn allows_multiple_components(&self) -> bool {
1229        true
1230    }
1231
1232    /// Validates one component-local JSON config object.
1233    fn validate(&self, _plugin_config: &Map<String, Json>) -> Vec<ConfigDiagnostic> {
1234        vec![]
1235    }
1236
1237    /// Registers runtime behavior through the component-scoped plugin context.
1238    fn register(
1239        &mut self,
1240        plugin_config: &Map<String, Json>,
1241        ctx: &mut PluginContext<'_>,
1242    ) -> Result<()>;
1243}
1244
1245/// Borrowed safe wrapper around a host plugin registration context.
1246pub struct PluginContext<'a> {
1247    host: &'a NemoRelayNativeHostApiV1,
1248    raw: *mut NemoRelayNativePluginContext,
1249}
1250
1251#[allow(clippy::not_unsafe_ptr_arg_deref)]
1252impl<'a> PluginContext<'a> {
1253    /// Creates a plugin context wrapper from raw ABI parts.
1254    ///
1255    /// # Safety
1256    /// `host` and `raw` must remain valid for the lifetime of this wrapper.
1257    pub unsafe fn from_raw(
1258        host: &'a NemoRelayNativeHostApiV1,
1259        raw: *mut NemoRelayNativePluginContext,
1260    ) -> Self {
1261        Self { host, raw }
1262    }
1263
1264    /// Returns the host ABI table backing this registration context.
1265    pub fn host_api(&self) -> &'a NemoRelayNativeHostApiV1 {
1266        self.host
1267    }
1268
1269    /// Returns a cloneable high-level runtime handle.
1270    pub fn runtime(&self) -> PluginRuntime {
1271        PluginRuntime::new(self.host)
1272    }
1273
1274    /// Registers a typed event subscriber callback.
1275    pub fn register_subscriber<F>(&mut self, name: &str, callback: F) -> Result<()>
1276    where
1277        F: Fn(&Event) + Send + Sync + 'static,
1278    {
1279        let user_data = typed_callback_user_data(self.host, callback);
1280        let status = unsafe {
1281            self.register_subscriber_raw(
1282                name,
1283                typed_subscriber_trampoline::<F>,
1284                user_data,
1285                Some(drop_typed_callback::<F>),
1286            )
1287        };
1288        finish_typed_registration::<F>(self.host, status, user_data, "subscriber")
1289    }
1290
1291    fn register_event_sanitizer<F>(
1292        &mut self,
1293        name: &str,
1294        priority: i32,
1295        callback: F,
1296        register: unsafe extern "C" fn(
1297            *mut NemoRelayNativePluginContext,
1298            *const NemoRelayNativeString,
1299            i32,
1300            NemoRelayNativeEventSanitizeCb,
1301            *mut c_void,
1302            NemoRelayNativeFreeFn,
1303        ) -> NemoRelayStatus,
1304        label: &str,
1305    ) -> Result<()>
1306    where
1307        F: Fn(&Event, EventSanitizeFields) -> EventSanitizeFields + Send + Sync + 'static,
1308    {
1309        let user_data = typed_callback_user_data(self.host, callback);
1310        let status = self.with_name(name, |_, name| unsafe {
1311            register(
1312                self.raw,
1313                name,
1314                priority,
1315                typed_event_sanitize_trampoline::<F>,
1316                user_data,
1317                Some(drop_typed_callback::<F>),
1318            )
1319        });
1320        finish_typed_registration::<F>(self.host, status, user_data, label)
1321    }
1322
1323    /// Registers a typed mark event sanitizer.
1324    pub fn register_mark_sanitize_guardrail<F>(
1325        &mut self,
1326        name: &str,
1327        priority: i32,
1328        callback: F,
1329    ) -> Result<()>
1330    where
1331        F: Fn(&Event, EventSanitizeFields) -> EventSanitizeFields + Send + Sync + 'static,
1332    {
1333        self.register_event_sanitizer(
1334            name,
1335            priority,
1336            callback,
1337            self.host.plugin_context_register_mark_sanitize_guardrail,
1338            "mark sanitize guardrail",
1339        )
1340    }
1341
1342    /// Registers a typed scope-start event sanitizer.
1343    pub fn register_scope_sanitize_start_guardrail<F>(
1344        &mut self,
1345        name: &str,
1346        priority: i32,
1347        callback: F,
1348    ) -> Result<()>
1349    where
1350        F: Fn(&Event, EventSanitizeFields) -> EventSanitizeFields + Send + Sync + 'static,
1351    {
1352        self.register_event_sanitizer(
1353            name,
1354            priority,
1355            callback,
1356            self.host
1357                .plugin_context_register_scope_sanitize_start_guardrail,
1358            "scope-start sanitize guardrail",
1359        )
1360    }
1361
1362    /// Registers a typed scope-end event sanitizer.
1363    pub fn register_scope_sanitize_end_guardrail<F>(
1364        &mut self,
1365        name: &str,
1366        priority: i32,
1367        callback: F,
1368    ) -> Result<()>
1369    where
1370        F: Fn(&Event, EventSanitizeFields) -> EventSanitizeFields + Send + Sync + 'static,
1371    {
1372        self.register_event_sanitizer(
1373            name,
1374            priority,
1375            callback,
1376            self.host
1377                .plugin_context_register_scope_sanitize_end_guardrail,
1378            "scope-end sanitize guardrail",
1379        )
1380    }
1381
1382    /// Registers a typed tool sanitize-request guardrail.
1383    pub fn register_tool_sanitize_request_guardrail<F>(
1384        &mut self,
1385        name: &str,
1386        priority: i32,
1387        callback: F,
1388    ) -> Result<()>
1389    where
1390        F: Fn(&str, Json) -> Json + Send + Sync + 'static,
1391    {
1392        let user_data = typed_callback_user_data(self.host, callback);
1393        let status = unsafe {
1394            self.register_tool_sanitize_request_guardrail_raw(
1395                name,
1396                priority,
1397                typed_tool_sanitize_trampoline::<F>,
1398                user_data,
1399                Some(drop_typed_callback::<F>),
1400            )
1401        };
1402        finish_typed_registration::<F>(
1403            self.host,
1404            status,
1405            user_data,
1406            "tool sanitize request guardrail",
1407        )
1408    }
1409
1410    /// Registers a typed tool sanitize-response guardrail.
1411    pub fn register_tool_sanitize_response_guardrail<F>(
1412        &mut self,
1413        name: &str,
1414        priority: i32,
1415        callback: F,
1416    ) -> Result<()>
1417    where
1418        F: Fn(&str, Json) -> Json + Send + Sync + 'static,
1419    {
1420        let user_data = typed_callback_user_data(self.host, callback);
1421        let status = unsafe {
1422            self.register_tool_sanitize_response_guardrail_raw(
1423                name,
1424                priority,
1425                typed_tool_sanitize_trampoline::<F>,
1426                user_data,
1427                Some(drop_typed_callback::<F>),
1428            )
1429        };
1430        finish_typed_registration::<F>(
1431            self.host,
1432            status,
1433            user_data,
1434            "tool sanitize response guardrail",
1435        )
1436    }
1437
1438    /// Registers a typed tool conditional-execution guardrail.
1439    pub fn register_tool_conditional_execution_guardrail<F>(
1440        &mut self,
1441        name: &str,
1442        priority: i32,
1443        callback: F,
1444    ) -> Result<()>
1445    where
1446        F: Fn(&str, &Json) -> Result<Option<String>> + Send + Sync + 'static,
1447    {
1448        let user_data = typed_callback_user_data(self.host, callback);
1449        let status = unsafe {
1450            self.register_tool_conditional_execution_guardrail_raw(
1451                name,
1452                priority,
1453                typed_tool_conditional_trampoline::<F>,
1454                user_data,
1455                Some(drop_typed_callback::<F>),
1456            )
1457        };
1458        finish_typed_registration::<F>(
1459            self.host,
1460            status,
1461            user_data,
1462            "tool conditional execution guardrail",
1463        )
1464    }
1465
1466    /// Registers a typed tool request intercept.
1467    pub fn register_tool_request_intercept<F>(
1468        &mut self,
1469        name: &str,
1470        priority: i32,
1471        break_chain: bool,
1472        callback: F,
1473    ) -> Result<()>
1474    where
1475        F: Fn(&str, Json) -> Result<Json> + Send + Sync + 'static,
1476    {
1477        let user_data = typed_callback_user_data(self.host, callback);
1478        let status = unsafe {
1479            self.register_tool_request_intercept_raw(
1480                name,
1481                priority,
1482                break_chain,
1483                typed_tool_intercept_trampoline::<F>,
1484                user_data,
1485                Some(drop_typed_callback::<F>),
1486            )
1487        };
1488        finish_typed_registration::<F>(self.host, status, user_data, "tool request intercept")
1489    }
1490
1491    /// Registers a typed tool execution intercept.
1492    ///
1493    /// The callback returns a [`ToolExecutionInterceptOutcome`]. Calling
1494    /// [`ToolNext::call`] continues the chain and returns only the raw
1495    /// downstream result JSON; Relay retains downstream pending marks.
1496    pub fn register_tool_execution_intercept<F>(
1497        &mut self,
1498        name: &str,
1499        priority: i32,
1500        callback: F,
1501    ) -> Result<()>
1502    where
1503        F: for<'next> Fn(&str, Json, ToolNext<'next>) -> Result<ToolExecutionInterceptOutcome>
1504            + Send
1505            + Sync
1506            + 'static,
1507    {
1508        let user_data = typed_callback_user_data(self.host, callback);
1509        let status = unsafe {
1510            self.register_tool_execution_intercept_raw(
1511                name,
1512                priority,
1513                typed_tool_execution_trampoline::<F>,
1514                user_data,
1515                Some(drop_typed_callback::<F>),
1516            )
1517        };
1518        finish_typed_registration::<F>(self.host, status, user_data, "tool execution intercept")
1519    }
1520
1521    /// Registers a typed LLM sanitize-request guardrail.
1522    pub fn register_llm_sanitize_request_guardrail<F>(
1523        &mut self,
1524        name: &str,
1525        priority: i32,
1526        callback: F,
1527    ) -> Result<()>
1528    where
1529        F: Fn(LlmRequest) -> LlmRequest + Send + Sync + 'static,
1530    {
1531        let user_data = typed_callback_user_data(self.host, callback);
1532        let status = unsafe {
1533            self.register_llm_sanitize_request_guardrail_raw(
1534                name,
1535                priority,
1536                typed_llm_sanitize_request_trampoline::<F>,
1537                user_data,
1538                Some(drop_typed_callback::<F>),
1539            )
1540        };
1541        finish_typed_registration::<F>(
1542            self.host,
1543            status,
1544            user_data,
1545            "llm sanitize request guardrail",
1546        )
1547    }
1548
1549    /// Registers a typed LLM sanitize-response guardrail.
1550    pub fn register_llm_sanitize_response_guardrail<F>(
1551        &mut self,
1552        name: &str,
1553        priority: i32,
1554        callback: F,
1555    ) -> Result<()>
1556    where
1557        F: Fn(Json) -> Json + Send + Sync + 'static,
1558    {
1559        let user_data = typed_callback_user_data(self.host, callback);
1560        let status = unsafe {
1561            self.register_llm_sanitize_response_guardrail_raw(
1562                name,
1563                priority,
1564                typed_llm_sanitize_response_trampoline::<F>,
1565                user_data,
1566                Some(drop_typed_callback::<F>),
1567            )
1568        };
1569        finish_typed_registration::<F>(
1570            self.host,
1571            status,
1572            user_data,
1573            "llm sanitize response guardrail",
1574        )
1575    }
1576
1577    /// Registers a typed LLM conditional-execution guardrail.
1578    pub fn register_llm_conditional_execution_guardrail<F>(
1579        &mut self,
1580        name: &str,
1581        priority: i32,
1582        callback: F,
1583    ) -> Result<()>
1584    where
1585        F: Fn(&LlmRequest) -> Result<Option<String>> + Send + Sync + 'static,
1586    {
1587        let user_data = typed_callback_user_data(self.host, callback);
1588        let status = unsafe {
1589            self.register_llm_conditional_execution_guardrail_raw(
1590                name,
1591                priority,
1592                typed_llm_conditional_trampoline::<F>,
1593                user_data,
1594                Some(drop_typed_callback::<F>),
1595            )
1596        };
1597        finish_typed_registration::<F>(
1598            self.host,
1599            status,
1600            user_data,
1601            "llm conditional execution guardrail",
1602        )
1603    }
1604
1605    /// Registers a typed LLM request intercept.
1606    pub fn register_llm_request_intercept<F>(
1607        &mut self,
1608        name: &str,
1609        priority: i32,
1610        break_chain: bool,
1611        callback: F,
1612    ) -> Result<()>
1613    where
1614        F: Fn(&str, LlmRequest, Option<AnnotatedLlmRequest>) -> Result<LlmRequestInterceptOutcome>
1615            + Send
1616            + Sync
1617            + 'static,
1618    {
1619        let user_data = typed_callback_user_data(self.host, callback);
1620        let status = unsafe {
1621            self.register_llm_request_intercept_raw(
1622                name,
1623                priority,
1624                break_chain,
1625                typed_llm_request_intercept_trampoline::<F>,
1626                user_data,
1627                Some(drop_typed_callback::<F>),
1628            )
1629        };
1630        finish_typed_registration::<F>(self.host, status, user_data, "llm request intercept")
1631    }
1632
1633    /// Registers a typed LLM execution intercept.
1634    pub fn register_llm_execution_intercept<F>(
1635        &mut self,
1636        name: &str,
1637        priority: i32,
1638        callback: F,
1639    ) -> Result<()>
1640    where
1641        F: for<'next> Fn(&str, LlmRequest, LlmNext<'next>) -> Result<Json> + Send + Sync + 'static,
1642    {
1643        let user_data = typed_callback_user_data(self.host, callback);
1644        let status = unsafe {
1645            self.register_llm_execution_intercept_raw(
1646                name,
1647                priority,
1648                typed_llm_execution_trampoline::<F>,
1649                user_data,
1650                Some(drop_typed_callback::<F>),
1651            )
1652        };
1653        finish_typed_registration::<F>(self.host, status, user_data, "llm execution intercept")
1654    }
1655
1656    /// Registers a typed LLM stream execution intercept.
1657    ///
1658    /// Native ABI v1 represents stream execution as one JSON result. The host
1659    /// wraps that result as a one-chunk stream.
1660    pub fn register_llm_stream_execution_intercept<F>(
1661        &mut self,
1662        name: &str,
1663        priority: i32,
1664        callback: F,
1665    ) -> Result<()>
1666    where
1667        F: for<'next> Fn(&str, LlmRequest, LlmStreamNext<'next>) -> Result<LlmJsonStream>
1668            + Send
1669            + Sync
1670            + 'static,
1671    {
1672        let user_data = typed_callback_user_data(self.host, callback);
1673        let status = unsafe {
1674            self.register_llm_stream_execution_intercept_raw(
1675                name,
1676                priority,
1677                typed_llm_stream_execution_trampoline::<F>,
1678                user_data,
1679                Some(drop_typed_callback::<F>),
1680            )
1681        };
1682        finish_typed_registration::<F>(
1683            self.host,
1684            status,
1685            user_data,
1686            "llm stream execution intercept",
1687        )
1688    }
1689
1690    /// Registers a raw event subscriber callback.
1691    ///
1692    /// # Safety
1693    /// `cb`, `user_data`, and `free_fn` must remain valid for every host
1694    /// callback invocation until the host deregisters the callback or calls
1695    /// `free_fn`. `free_fn` must match the allocation behind `user_data`.
1696    pub unsafe fn register_subscriber_raw(
1697        &mut self,
1698        name: &str,
1699        cb: NemoRelayNativeEventSubscriberCb,
1700        user_data: *mut c_void,
1701        free_fn: NemoRelayNativeFreeFn,
1702    ) -> NemoRelayStatus {
1703        self.with_name(name, |host, name| unsafe {
1704            (host.plugin_context_register_subscriber)(self.raw, name, cb, user_data, free_fn)
1705        })
1706    }
1707
1708    /// Registers a raw mark event sanitizer callback.
1709    ///
1710    /// # Safety
1711    /// `cb`, `user_data`, and `free_fn` must remain valid for every host
1712    /// callback invocation until the host deregisters the callback or calls
1713    /// `free_fn`. `free_fn` must match the allocation behind `user_data`.
1714    pub unsafe fn register_mark_sanitize_guardrail_raw(
1715        &mut self,
1716        name: &str,
1717        priority: i32,
1718        cb: NemoRelayNativeEventSanitizeCb,
1719        user_data: *mut c_void,
1720        free_fn: NemoRelayNativeFreeFn,
1721    ) -> NemoRelayStatus {
1722        self.with_name(name, |host, name| unsafe {
1723            (host.plugin_context_register_mark_sanitize_guardrail)(
1724                self.raw, name, priority, cb, user_data, free_fn,
1725            )
1726        })
1727    }
1728
1729    /// Registers a raw scope-start event sanitizer callback.
1730    ///
1731    /// # Safety
1732    /// `cb`, `user_data`, and `free_fn` must remain valid for every host
1733    /// callback invocation until the host deregisters the callback or calls
1734    /// `free_fn`. `free_fn` must match the allocation behind `user_data`.
1735    pub unsafe fn register_scope_sanitize_start_guardrail_raw(
1736        &mut self,
1737        name: &str,
1738        priority: i32,
1739        cb: NemoRelayNativeEventSanitizeCb,
1740        user_data: *mut c_void,
1741        free_fn: NemoRelayNativeFreeFn,
1742    ) -> NemoRelayStatus {
1743        self.with_name(name, |host, name| unsafe {
1744            (host.plugin_context_register_scope_sanitize_start_guardrail)(
1745                self.raw, name, priority, cb, user_data, free_fn,
1746            )
1747        })
1748    }
1749
1750    /// Registers a raw scope-end event sanitizer callback.
1751    ///
1752    /// # Safety
1753    /// `cb`, `user_data`, and `free_fn` must remain valid for every host
1754    /// callback invocation until the host deregisters the callback or calls
1755    /// `free_fn`. `free_fn` must match the allocation behind `user_data`.
1756    pub unsafe fn register_scope_sanitize_end_guardrail_raw(
1757        &mut self,
1758        name: &str,
1759        priority: i32,
1760        cb: NemoRelayNativeEventSanitizeCb,
1761        user_data: *mut c_void,
1762        free_fn: NemoRelayNativeFreeFn,
1763    ) -> NemoRelayStatus {
1764        self.with_name(name, |host, name| unsafe {
1765            (host.plugin_context_register_scope_sanitize_end_guardrail)(
1766                self.raw, name, priority, cb, user_data, free_fn,
1767            )
1768        })
1769    }
1770
1771    /// Registers a raw tool sanitize-request guardrail callback.
1772    ///
1773    /// # Safety
1774    /// `cb`, `user_data`, and `free_fn` must remain valid for every host
1775    /// callback invocation until the host deregisters the callback or calls
1776    /// `free_fn`. `free_fn` must match the allocation behind `user_data`.
1777    pub unsafe fn register_tool_sanitize_request_guardrail_raw(
1778        &mut self,
1779        name: &str,
1780        priority: i32,
1781        cb: NemoRelayNativeToolJsonCb,
1782        user_data: *mut c_void,
1783        free_fn: NemoRelayNativeFreeFn,
1784    ) -> NemoRelayStatus {
1785        self.with_name(name, |host, name| unsafe {
1786            (host.plugin_context_register_tool_sanitize_request_guardrail)(
1787                self.raw, name, priority, cb, user_data, free_fn,
1788            )
1789        })
1790    }
1791
1792    /// Registers a raw tool sanitize-response guardrail callback.
1793    ///
1794    /// # Safety
1795    /// `cb`, `user_data`, and `free_fn` must remain valid for every host
1796    /// callback invocation until the host deregisters the callback or calls
1797    /// `free_fn`. `free_fn` must match the allocation behind `user_data`.
1798    pub unsafe fn register_tool_sanitize_response_guardrail_raw(
1799        &mut self,
1800        name: &str,
1801        priority: i32,
1802        cb: NemoRelayNativeToolJsonCb,
1803        user_data: *mut c_void,
1804        free_fn: NemoRelayNativeFreeFn,
1805    ) -> NemoRelayStatus {
1806        self.with_name(name, |host, name| unsafe {
1807            (host.plugin_context_register_tool_sanitize_response_guardrail)(
1808                self.raw, name, priority, cb, user_data, free_fn,
1809            )
1810        })
1811    }
1812
1813    /// Registers a raw tool conditional-execution guardrail callback.
1814    ///
1815    /// # Safety
1816    /// `cb`, `user_data`, and `free_fn` must remain valid for every host
1817    /// callback invocation until the host deregisters the callback or calls
1818    /// `free_fn`. `free_fn` must match the allocation behind `user_data`.
1819    pub unsafe fn register_tool_conditional_execution_guardrail_raw(
1820        &mut self,
1821        name: &str,
1822        priority: i32,
1823        cb: NemoRelayNativeToolConditionalCb,
1824        user_data: *mut c_void,
1825        free_fn: NemoRelayNativeFreeFn,
1826    ) -> NemoRelayStatus {
1827        self.with_name(name, |host, name| unsafe {
1828            (host.plugin_context_register_tool_conditional_execution_guardrail)(
1829                self.raw, name, priority, cb, user_data, free_fn,
1830            )
1831        })
1832    }
1833
1834    /// Registers a raw tool request intercept callback.
1835    ///
1836    /// # Safety
1837    /// `cb`, `user_data`, and `free_fn` must remain valid for every host
1838    /// callback invocation until the host deregisters the callback or calls
1839    /// `free_fn`. `free_fn` must match the allocation behind `user_data`.
1840    pub unsafe fn register_tool_request_intercept_raw(
1841        &mut self,
1842        name: &str,
1843        priority: i32,
1844        break_chain: bool,
1845        cb: NemoRelayNativeToolJsonCb,
1846        user_data: *mut c_void,
1847        free_fn: NemoRelayNativeFreeFn,
1848    ) -> NemoRelayStatus {
1849        self.with_name(name, |host, name| unsafe {
1850            (host.plugin_context_register_tool_request_intercept)(
1851                self.raw,
1852                name,
1853                priority,
1854                break_chain,
1855                cb,
1856                user_data,
1857                free_fn,
1858            )
1859        })
1860    }
1861
1862    /// Registers a raw tool execution intercept callback.
1863    ///
1864    /// # Safety
1865    /// `cb`, `user_data`, and `free_fn` must remain valid for every host
1866    /// callback invocation until the host deregisters the callback or calls
1867    /// `free_fn`. `free_fn` must match the allocation behind `user_data`.
1868    pub unsafe fn register_tool_execution_intercept_raw(
1869        &mut self,
1870        name: &str,
1871        priority: i32,
1872        cb: NemoRelayNativeToolExecutionCb,
1873        user_data: *mut c_void,
1874        free_fn: NemoRelayNativeFreeFn,
1875    ) -> NemoRelayStatus {
1876        self.with_name(name, |host, name| unsafe {
1877            (host.plugin_context_register_tool_execution_intercept)(
1878                self.raw, name, priority, cb, user_data, free_fn,
1879            )
1880        })
1881    }
1882
1883    /// Registers a raw LLM sanitize-request guardrail callback.
1884    ///
1885    /// # Safety
1886    /// `cb`, `user_data`, and `free_fn` must remain valid for every host
1887    /// callback invocation until the host deregisters the callback or calls
1888    /// `free_fn`. `free_fn` must match the allocation behind `user_data`.
1889    pub unsafe fn register_llm_sanitize_request_guardrail_raw(
1890        &mut self,
1891        name: &str,
1892        priority: i32,
1893        cb: NemoRelayNativeLlmRequestCb,
1894        user_data: *mut c_void,
1895        free_fn: NemoRelayNativeFreeFn,
1896    ) -> NemoRelayStatus {
1897        self.with_name(name, |host, name| unsafe {
1898            (host.plugin_context_register_llm_sanitize_request_guardrail)(
1899                self.raw, name, priority, cb, user_data, free_fn,
1900            )
1901        })
1902    }
1903
1904    /// Registers a raw LLM sanitize-response guardrail callback.
1905    ///
1906    /// # Safety
1907    /// `cb`, `user_data`, and `free_fn` must remain valid for every host
1908    /// callback invocation until the host deregisters the callback or calls
1909    /// `free_fn`. `free_fn` must match the allocation behind `user_data`.
1910    pub unsafe fn register_llm_sanitize_response_guardrail_raw(
1911        &mut self,
1912        name: &str,
1913        priority: i32,
1914        cb: NemoRelayNativeJsonCb,
1915        user_data: *mut c_void,
1916        free_fn: NemoRelayNativeFreeFn,
1917    ) -> NemoRelayStatus {
1918        self.with_name(name, |host, name| unsafe {
1919            (host.plugin_context_register_llm_sanitize_response_guardrail)(
1920                self.raw, name, priority, cb, user_data, free_fn,
1921            )
1922        })
1923    }
1924
1925    /// Registers a raw LLM conditional-execution guardrail callback.
1926    ///
1927    /// # Safety
1928    /// `cb`, `user_data`, and `free_fn` must remain valid for every host
1929    /// callback invocation until the host deregisters the callback or calls
1930    /// `free_fn`. `free_fn` must match the allocation behind `user_data`.
1931    pub unsafe fn register_llm_conditional_execution_guardrail_raw(
1932        &mut self,
1933        name: &str,
1934        priority: i32,
1935        cb: NemoRelayNativeLlmConditionalCb,
1936        user_data: *mut c_void,
1937        free_fn: NemoRelayNativeFreeFn,
1938    ) -> NemoRelayStatus {
1939        self.with_name(name, |host, name| unsafe {
1940            (host.plugin_context_register_llm_conditional_execution_guardrail)(
1941                self.raw, name, priority, cb, user_data, free_fn,
1942            )
1943        })
1944    }
1945
1946    /// Registers a raw LLM request intercept callback.
1947    ///
1948    /// # Safety
1949    /// `cb`, `user_data`, and `free_fn` must remain valid for every host
1950    /// callback invocation until the host deregisters the callback or calls
1951    /// `free_fn`. `free_fn` must match the allocation behind `user_data`.
1952    pub unsafe fn register_llm_request_intercept_raw(
1953        &mut self,
1954        name: &str,
1955        priority: i32,
1956        break_chain: bool,
1957        cb: NemoRelayNativeLlmRequestInterceptCb,
1958        user_data: *mut c_void,
1959        free_fn: NemoRelayNativeFreeFn,
1960    ) -> NemoRelayStatus {
1961        self.with_name(name, |host, name| unsafe {
1962            (host.plugin_context_register_llm_request_intercept)(
1963                self.raw,
1964                name,
1965                priority,
1966                break_chain,
1967                cb,
1968                user_data,
1969                free_fn,
1970            )
1971        })
1972    }
1973
1974    /// Registers a raw LLM execution intercept callback.
1975    ///
1976    /// # Safety
1977    /// `cb`, `user_data`, and `free_fn` must remain valid for every host
1978    /// callback invocation until the host deregisters the callback or calls
1979    /// `free_fn`. `free_fn` must match the allocation behind `user_data`.
1980    pub unsafe fn register_llm_execution_intercept_raw(
1981        &mut self,
1982        name: &str,
1983        priority: i32,
1984        cb: NemoRelayNativeLlmExecutionCb,
1985        user_data: *mut c_void,
1986        free_fn: NemoRelayNativeFreeFn,
1987    ) -> NemoRelayStatus {
1988        self.with_name(name, |host, name| unsafe {
1989            (host.plugin_context_register_llm_execution_intercept)(
1990                self.raw, name, priority, cb, user_data, free_fn,
1991            )
1992        })
1993    }
1994
1995    /// Registers a raw LLM stream execution intercept callback.
1996    ///
1997    /// # Safety
1998    /// `cb`, `user_data`, and `free_fn` must remain valid for every host
1999    /// callback invocation until the host deregisters the callback or calls
2000    /// `free_fn`. `free_fn` must match the allocation behind `user_data`.
2001    pub unsafe fn register_llm_stream_execution_intercept_raw(
2002        &mut self,
2003        name: &str,
2004        priority: i32,
2005        cb: NemoRelayNativeLlmStreamExecutionCb,
2006        user_data: *mut c_void,
2007        free_fn: NemoRelayNativeFreeFn,
2008    ) -> NemoRelayStatus {
2009        self.with_name(name, |host, name| unsafe {
2010            (host.plugin_context_register_llm_stream_execution_intercept)(
2011                self.raw, name, priority, cb, user_data, free_fn,
2012            )
2013        })
2014    }
2015
2016    fn with_name(
2017        &self,
2018        name: &str,
2019        f: impl FnOnce(&NemoRelayNativeHostApiV1, *const NemoRelayNativeString) -> NemoRelayStatus,
2020    ) -> NemoRelayStatus {
2021        let name = match HostString::try_new(self.host, name) {
2022            Ok(name) => name,
2023            Err(status) => return status,
2024        };
2025        f(self.host, name.as_ptr())
2026    }
2027}
2028
2029struct TypedCallback<F> {
2030    host: NemoRelayNativeHostApiV1,
2031    callback: F,
2032}
2033
2034fn typed_callback_user_data<F>(host: &NemoRelayNativeHostApiV1, callback: F) -> *mut c_void {
2035    Box::into_raw(Box::new(TypedCallback {
2036        host: *host,
2037        callback,
2038    })) as *mut c_void
2039}
2040
2041unsafe extern "C" fn drop_typed_callback<F>(user_data: *mut c_void) {
2042    if !user_data.is_null() {
2043        let callback = unsafe { Box::from_raw(user_data as *mut TypedCallback<F>) };
2044        let host = callback.host;
2045        if catch_unwind(AssertUnwindSafe(|| drop(callback))).is_err() {
2046            set_last_error(&host, "native plugin typed callback state drop panicked");
2047        }
2048    }
2049}
2050
2051fn finish_typed_registration<F>(
2052    host: &NemoRelayNativeHostApiV1,
2053    status: NemoRelayStatus,
2054    user_data: *mut c_void,
2055    label: &str,
2056) -> Result<()> {
2057    if status == NemoRelayStatus::Ok {
2058        Ok(())
2059    } else {
2060        unsafe { drop_typed_callback::<F>(user_data) };
2061        Err(status_error(host, status, label))
2062    }
2063}
2064
2065fn status_error(host: &NemoRelayNativeHostApiV1, status: NemoRelayStatus, label: &str) -> String {
2066    debug_assert_ne!(status, NemoRelayStatus::Ok);
2067    set_last_error(host, &format!("{label} failed: {status:?}"));
2068    format!("{label} failed: {status:?}")
2069}
2070
2071fn callback_error(host: &NemoRelayNativeHostApiV1, message: String) -> NemoRelayStatus {
2072    set_last_error(host, &message);
2073    NemoRelayStatus::Internal
2074}
2075
2076fn callback_panic(host: &NemoRelayNativeHostApiV1, label: &str) -> NemoRelayStatus {
2077    set_last_error(host, &format!("{label} panicked"));
2078    NemoRelayStatus::Internal
2079}
2080
2081unsafe extern "C" fn typed_subscriber_trampoline<F>(
2082    user_data: *mut c_void,
2083    event_json: *const NemoRelayNativeString,
2084) -> NemoRelayStatus
2085where
2086    F: Fn(&Event) + Send + Sync + 'static,
2087{
2088    if user_data.is_null() {
2089        return NemoRelayStatus::NullPointer;
2090    }
2091    let state = unsafe { &*(user_data as *const TypedCallback<F>) };
2092    let result = catch_unwind(AssertUnwindSafe(|| {
2093        let event: Event = read_json_value(&state.host, event_json, "event")?;
2094        (state.callback)(&event);
2095        Ok::<_, NemoRelayStatus>(())
2096    }));
2097    match result {
2098        Ok(Ok(())) => NemoRelayStatus::Ok,
2099        Ok(Err(status)) => status,
2100        Err(_) => callback_panic(&state.host, "subscriber callback"),
2101    }
2102}
2103
2104unsafe extern "C" fn typed_event_sanitize_trampoline<F>(
2105    user_data: *mut c_void,
2106    event_json: *const NemoRelayNativeString,
2107    fields_json: *const NemoRelayNativeString,
2108    out_fields_json: *mut *mut NemoRelayNativeString,
2109) -> NemoRelayStatus
2110where
2111    F: Fn(&Event, EventSanitizeFields) -> EventSanitizeFields + Send + Sync + 'static,
2112{
2113    if user_data.is_null() || out_fields_json.is_null() {
2114        return NemoRelayStatus::NullPointer;
2115    }
2116    unsafe { *out_fields_json = ptr::null_mut() };
2117    let state = unsafe { &*(user_data as *const TypedCallback<F>) };
2118    let result = catch_unwind(AssertUnwindSafe(|| {
2119        let event: Event = read_json_value(&state.host, event_json, "event")?;
2120        let fields: EventSanitizeFields =
2121            read_json_value(&state.host, fields_json, "event sanitize fields")?;
2122        let output = (state.callback)(&event, fields);
2123        Ok::<_, NemoRelayStatus>(write_json(&state.host, &output, out_fields_json))
2124    }));
2125    match result {
2126        Ok(Ok(status)) => status,
2127        Ok(Err(status)) => status,
2128        Err(_) => callback_panic(&state.host, "event sanitize callback"),
2129    }
2130}
2131
2132unsafe extern "C" fn typed_tool_sanitize_trampoline<F>(
2133    user_data: *mut c_void,
2134    name: *const NemoRelayNativeString,
2135    payload_json: *const NemoRelayNativeString,
2136    out_json: *mut *mut NemoRelayNativeString,
2137) -> NemoRelayStatus
2138where
2139    F: Fn(&str, Json) -> Json + Send + Sync + 'static,
2140{
2141    if user_data.is_null() || out_json.is_null() {
2142        return NemoRelayStatus::NullPointer;
2143    }
2144    unsafe { *out_json = ptr::null_mut() };
2145    let state = unsafe { &*(user_data as *const TypedCallback<F>) };
2146    let result = catch_unwind(AssertUnwindSafe(|| {
2147        let name = read_required_host_string(&state.host, name, "tool name")?;
2148        let payload: Json = read_json_value(&state.host, payload_json, "tool payload")?;
2149        let output = (state.callback)(&name, payload);
2150        Ok::<_, NemoRelayStatus>(write_json(&state.host, &output, out_json))
2151    }));
2152    match result {
2153        Ok(Ok(status)) => status,
2154        Ok(Err(status)) => status,
2155        Err(_) => callback_panic(&state.host, "tool sanitize callback"),
2156    }
2157}
2158
2159unsafe extern "C" fn typed_tool_intercept_trampoline<F>(
2160    user_data: *mut c_void,
2161    name: *const NemoRelayNativeString,
2162    payload_json: *const NemoRelayNativeString,
2163    out_json: *mut *mut NemoRelayNativeString,
2164) -> NemoRelayStatus
2165where
2166    F: Fn(&str, Json) -> Result<Json> + Send + Sync + 'static,
2167{
2168    if user_data.is_null() || out_json.is_null() {
2169        return NemoRelayStatus::NullPointer;
2170    }
2171    unsafe { *out_json = ptr::null_mut() };
2172    let state = unsafe { &*(user_data as *const TypedCallback<F>) };
2173    let result = catch_unwind(AssertUnwindSafe(|| {
2174        let name = read_required_host_string(&state.host, name, "tool name")?;
2175        let payload: Json = read_json_value(&state.host, payload_json, "tool payload")?;
2176        match (state.callback)(&name, payload) {
2177            Ok(output) => Ok::<_, NemoRelayStatus>(write_json(&state.host, &output, out_json)),
2178            Err(message) => Ok(callback_error(&state.host, message)),
2179        }
2180    }));
2181    match result {
2182        Ok(Ok(status)) => status,
2183        Ok(Err(status)) => status,
2184        Err(_) => callback_panic(&state.host, "tool intercept callback"),
2185    }
2186}
2187
2188unsafe extern "C" fn typed_tool_conditional_trampoline<F>(
2189    user_data: *mut c_void,
2190    name: *const NemoRelayNativeString,
2191    args_json: *const NemoRelayNativeString,
2192    out_reason: *mut *mut NemoRelayNativeString,
2193) -> NemoRelayStatus
2194where
2195    F: Fn(&str, &Json) -> Result<Option<String>> + Send + Sync + 'static,
2196{
2197    if user_data.is_null() || out_reason.is_null() {
2198        return NemoRelayStatus::NullPointer;
2199    }
2200    unsafe { *out_reason = ptr::null_mut() };
2201    let state = unsafe { &*(user_data as *const TypedCallback<F>) };
2202    let result = catch_unwind(AssertUnwindSafe(|| {
2203        let name = read_required_host_string(&state.host, name, "tool name")?;
2204        let args: Json = read_json_value(&state.host, args_json, "tool args")?;
2205        match (state.callback)(&name, &args) {
2206            Ok(Some(reason)) => {
2207                let reason =
2208                    HostString::new(&state.host, &reason).ok_or(NemoRelayStatus::Internal)?;
2209                unsafe { *out_reason = reason.ptr };
2210                std::mem::forget(reason);
2211                Ok(NemoRelayStatus::Ok)
2212            }
2213            Ok(None) => {
2214                unsafe { *out_reason = ptr::null_mut() };
2215                Ok(NemoRelayStatus::Ok)
2216            }
2217            Err(message) => Ok(callback_error(&state.host, message)),
2218        }
2219    }));
2220    match result {
2221        Ok(Ok(status)) => status,
2222        Ok(Err(status)) => status,
2223        Err(_) => callback_panic(&state.host, "tool conditional callback"),
2224    }
2225}
2226
2227unsafe extern "C" fn typed_tool_execution_trampoline<F>(
2228    user_data: *mut c_void,
2229    name: *const NemoRelayNativeString,
2230    args_json: *const NemoRelayNativeString,
2231    next_fn: NemoRelayNativeToolNextFn,
2232    next_ctx: *mut c_void,
2233    out_outcome_json: *mut *mut NemoRelayNativeString,
2234) -> NemoRelayStatus
2235where
2236    F: for<'next> Fn(&str, Json, ToolNext<'next>) -> Result<ToolExecutionInterceptOutcome>
2237        + Send
2238        + Sync
2239        + 'static,
2240{
2241    if user_data.is_null() || out_outcome_json.is_null() {
2242        return NemoRelayStatus::NullPointer;
2243    }
2244    unsafe { *out_outcome_json = ptr::null_mut() };
2245    let state = unsafe { &*(user_data as *const TypedCallback<F>) };
2246    let result = catch_unwind(AssertUnwindSafe(|| {
2247        let name = read_required_host_string(&state.host, name, "tool name")?;
2248        let args: Json = read_json_value(&state.host, args_json, "tool args")?;
2249        let next = ToolNext {
2250            host: &state.host,
2251            next_fn,
2252            next_ctx,
2253        };
2254        match (state.callback)(&name, args, next) {
2255            Ok(outcome) => {
2256                let Some(outcome) = HostString::from_json(&state.host, &outcome) else {
2257                    set_last_error(&state.host, "failed to allocate tool execution outcome");
2258                    return Ok(NemoRelayStatus::Internal);
2259                };
2260                unsafe { *out_outcome_json = outcome.ptr };
2261                std::mem::forget(outcome);
2262                Ok(NemoRelayStatus::Ok)
2263            }
2264            Err(message) => Ok(callback_error(&state.host, message)),
2265        }
2266    }));
2267    match result {
2268        Ok(Ok(status)) => status,
2269        Ok(Err(status)) => status,
2270        Err(_) => callback_panic(&state.host, "tool execution callback"),
2271    }
2272}
2273
2274unsafe extern "C" fn typed_llm_sanitize_request_trampoline<F>(
2275    user_data: *mut c_void,
2276    request_json: *const NemoRelayNativeString,
2277    out_request_json: *mut *mut NemoRelayNativeString,
2278) -> NemoRelayStatus
2279where
2280    F: Fn(LlmRequest) -> LlmRequest + Send + Sync + 'static,
2281{
2282    if user_data.is_null() || out_request_json.is_null() {
2283        return NemoRelayStatus::NullPointer;
2284    }
2285    unsafe { *out_request_json = ptr::null_mut() };
2286    let state = unsafe { &*(user_data as *const TypedCallback<F>) };
2287    let result = catch_unwind(AssertUnwindSafe(|| {
2288        let request: LlmRequest = read_json_value(&state.host, request_json, "LLM request")?;
2289        let output = (state.callback)(request);
2290        Ok::<_, NemoRelayStatus>(write_json(&state.host, &output, out_request_json))
2291    }));
2292    match result {
2293        Ok(Ok(status)) => status,
2294        Ok(Err(status)) => status,
2295        Err(_) => callback_panic(&state.host, "LLM sanitize request callback"),
2296    }
2297}
2298
2299unsafe extern "C" fn typed_llm_sanitize_response_trampoline<F>(
2300    user_data: *mut c_void,
2301    payload_json: *const NemoRelayNativeString,
2302    out_json: *mut *mut NemoRelayNativeString,
2303) -> NemoRelayStatus
2304where
2305    F: Fn(Json) -> Json + Send + Sync + 'static,
2306{
2307    if user_data.is_null() || out_json.is_null() {
2308        return NemoRelayStatus::NullPointer;
2309    }
2310    unsafe { *out_json = ptr::null_mut() };
2311    let state = unsafe { &*(user_data as *const TypedCallback<F>) };
2312    let result = catch_unwind(AssertUnwindSafe(|| {
2313        let payload: Json = read_json_value(&state.host, payload_json, "LLM response")?;
2314        let output = (state.callback)(payload);
2315        Ok::<_, NemoRelayStatus>(write_json(&state.host, &output, out_json))
2316    }));
2317    match result {
2318        Ok(Ok(status)) => status,
2319        Ok(Err(status)) => status,
2320        Err(_) => callback_panic(&state.host, "LLM sanitize response callback"),
2321    }
2322}
2323
2324unsafe extern "C" fn typed_llm_conditional_trampoline<F>(
2325    user_data: *mut c_void,
2326    request_json: *const NemoRelayNativeString,
2327    out_reason: *mut *mut NemoRelayNativeString,
2328) -> NemoRelayStatus
2329where
2330    F: Fn(&LlmRequest) -> Result<Option<String>> + Send + Sync + 'static,
2331{
2332    if user_data.is_null() || out_reason.is_null() {
2333        return NemoRelayStatus::NullPointer;
2334    }
2335    unsafe { *out_reason = ptr::null_mut() };
2336    let state = unsafe { &*(user_data as *const TypedCallback<F>) };
2337    let result = catch_unwind(AssertUnwindSafe(|| {
2338        let request: LlmRequest = read_json_value(&state.host, request_json, "LLM request")?;
2339        match (state.callback)(&request) {
2340            Ok(Some(reason)) => {
2341                let reason =
2342                    HostString::new(&state.host, &reason).ok_or(NemoRelayStatus::Internal)?;
2343                unsafe { *out_reason = reason.ptr };
2344                std::mem::forget(reason);
2345                Ok(NemoRelayStatus::Ok)
2346            }
2347            Ok(None) => {
2348                unsafe { *out_reason = ptr::null_mut() };
2349                Ok(NemoRelayStatus::Ok)
2350            }
2351            Err(message) => Ok(callback_error(&state.host, message)),
2352        }
2353    }));
2354    match result {
2355        Ok(Ok(status)) => status,
2356        Ok(Err(status)) => status,
2357        Err(_) => callback_panic(&state.host, "LLM conditional callback"),
2358    }
2359}
2360
2361unsafe extern "C" fn typed_llm_request_intercept_trampoline<F>(
2362    user_data: *mut c_void,
2363    name: *const NemoRelayNativeString,
2364    request_json: *const NemoRelayNativeString,
2365    annotated_json: *const NemoRelayNativeString,
2366    out_outcome_json: *mut *mut NemoRelayNativeString,
2367) -> NemoRelayStatus
2368where
2369    F: Fn(&str, LlmRequest, Option<AnnotatedLlmRequest>) -> Result<LlmRequestInterceptOutcome>
2370        + Send
2371        + Sync
2372        + 'static,
2373{
2374    if user_data.is_null() || out_outcome_json.is_null() {
2375        return NemoRelayStatus::NullPointer;
2376    }
2377    unsafe {
2378        *out_outcome_json = ptr::null_mut();
2379    }
2380    let state = unsafe { &*(user_data as *const TypedCallback<F>) };
2381    let result = catch_unwind(AssertUnwindSafe(|| {
2382        let name = read_required_host_string(&state.host, name, "LLM name")?;
2383        let request: LlmRequest = read_json_value(&state.host, request_json, "LLM request")?;
2384        let annotated: Option<AnnotatedLlmRequest> =
2385            read_optional_json_value(&state.host, annotated_json, "annotated LLM request")?;
2386        match (state.callback)(&name, request, annotated) {
2387            Ok(outcome) => {
2388                let Some(outcome) = HostString::from_json(&state.host, &outcome) else {
2389                    set_last_error(&state.host, "failed to allocate LLM request outcome");
2390                    return Ok(NemoRelayStatus::Internal);
2391                };
2392                unsafe {
2393                    *out_outcome_json = outcome.ptr;
2394                }
2395                std::mem::forget(outcome);
2396                Ok(NemoRelayStatus::Ok)
2397            }
2398            Err(message) => Ok(callback_error(&state.host, message)),
2399        }
2400    }));
2401    match result {
2402        Ok(Ok(status)) => status,
2403        Ok(Err(status)) => status,
2404        Err(_) => callback_panic(&state.host, "LLM request intercept callback"),
2405    }
2406}
2407
2408unsafe extern "C" fn typed_llm_execution_trampoline<F>(
2409    user_data: *mut c_void,
2410    name: *const NemoRelayNativeString,
2411    request_json: *const NemoRelayNativeString,
2412    next_fn: NemoRelayNativeLlmNextFn,
2413    next_ctx: *mut c_void,
2414    out_json: *mut *mut NemoRelayNativeString,
2415) -> NemoRelayStatus
2416where
2417    F: for<'next> Fn(&str, LlmRequest, LlmNext<'next>) -> Result<Json> + Send + Sync + 'static,
2418{
2419    if user_data.is_null() || out_json.is_null() {
2420        return NemoRelayStatus::NullPointer;
2421    }
2422    unsafe { *out_json = ptr::null_mut() };
2423    let state = unsafe { &*(user_data as *const TypedCallback<F>) };
2424    let result = catch_unwind(AssertUnwindSafe(|| {
2425        let name = read_required_host_string(&state.host, name, "LLM name")?;
2426        let request: LlmRequest = read_json_value(&state.host, request_json, "LLM request")?;
2427        let next = LlmNext {
2428            host: &state.host,
2429            next_fn,
2430            next_ctx,
2431        };
2432        match (state.callback)(&name, request, next) {
2433            Ok(output) => Ok::<_, NemoRelayStatus>(write_json(&state.host, &output, out_json)),
2434            Err(message) => Ok(callback_error(&state.host, message)),
2435        }
2436    }));
2437    match result {
2438        Ok(Ok(status)) => status,
2439        Ok(Err(status)) => status,
2440        Err(_) => callback_panic(&state.host, "LLM execution callback"),
2441    }
2442}
2443
2444struct TypedLlmJsonStream {
2445    host: NemoRelayNativeHostApiV1,
2446    state: Mutex<TypedLlmJsonStreamState>,
2447}
2448
2449struct TypedLlmJsonStreamState {
2450    iter: LlmJsonStream,
2451    finished: bool,
2452}
2453
2454fn native_stream_from_iter(
2455    host: &NemoRelayNativeHostApiV1,
2456    iter: LlmJsonStream,
2457) -> NemoRelayNativeLlmStreamV1 {
2458    let state = Box::new(TypedLlmJsonStream {
2459        host: *host,
2460        state: Mutex::new(TypedLlmJsonStreamState {
2461            iter,
2462            finished: false,
2463        }),
2464    });
2465    NemoRelayNativeLlmStreamV1 {
2466        struct_size: std::mem::size_of::<NemoRelayNativeLlmStreamV1>(),
2467        user_data: Box::into_raw(state).cast(),
2468        next: Some(poll_typed_llm_json_stream),
2469        cancel: Some(cancel_typed_llm_json_stream),
2470        drop: Some(drop_typed_llm_json_stream),
2471    }
2472}
2473
2474unsafe extern "C" fn poll_typed_llm_json_stream(
2475    user_data: *mut c_void,
2476    out_json: *mut *mut NemoRelayNativeString,
2477) -> NemoRelayStatus {
2478    if user_data.is_null() || out_json.is_null() {
2479        return NemoRelayStatus::NullPointer;
2480    }
2481    unsafe { *out_json = ptr::null_mut() };
2482    let stream = unsafe { &*(user_data as *const TypedLlmJsonStream) };
2483    let result = catch_unwind(AssertUnwindSafe(|| {
2484        let mut state = match stream.state.lock() {
2485            Ok(state) => state,
2486            Err(_) => {
2487                set_last_error(&stream.host, "native plugin stream state lock poisoned");
2488                return NemoRelayStatus::Internal;
2489            }
2490        };
2491        if state.finished {
2492            return NemoRelayStatus::StreamEnd;
2493        }
2494        match state.iter.next() {
2495            Some(Ok(chunk)) => {
2496                let status = write_json(&stream.host, &chunk, out_json);
2497                if status != NemoRelayStatus::Ok {
2498                    state.finished = true;
2499                }
2500                status
2501            }
2502            Some(Err(message)) => {
2503                state.finished = true;
2504                callback_error(&stream.host, message)
2505            }
2506            None => {
2507                state.finished = true;
2508                NemoRelayStatus::StreamEnd
2509            }
2510        }
2511    }));
2512    result.unwrap_or_else(|_| callback_panic(&stream.host, "LLM stream callback"))
2513}
2514
2515unsafe extern "C" fn cancel_typed_llm_json_stream(user_data: *mut c_void) -> NemoRelayStatus {
2516    if user_data.is_null() {
2517        return NemoRelayStatus::NullPointer;
2518    }
2519    let stream = unsafe { &*(user_data as *const TypedLlmJsonStream) };
2520    let result = catch_unwind(AssertUnwindSafe(|| {
2521        let mut state = match stream.state.lock() {
2522            Ok(state) => state,
2523            Err(_) => {
2524                set_last_error(&stream.host, "native plugin stream state lock poisoned");
2525                return NemoRelayStatus::Internal;
2526            }
2527        };
2528        state.finished = true;
2529        NemoRelayStatus::Ok
2530    }));
2531    result.unwrap_or_else(|_| callback_panic(&stream.host, "LLM stream cancel callback"))
2532}
2533
2534unsafe extern "C" fn drop_typed_llm_json_stream(user_data: *mut c_void) {
2535    if !user_data.is_null() {
2536        let stream = unsafe { Box::from_raw(user_data as *mut TypedLlmJsonStream) };
2537        let host = stream.host;
2538        if catch_unwind(AssertUnwindSafe(|| drop(stream))).is_err() {
2539            set_last_error(&host, "native plugin LLM stream state drop panicked");
2540        }
2541    }
2542}
2543
2544unsafe extern "C" fn typed_llm_stream_execution_trampoline<F>(
2545    user_data: *mut c_void,
2546    name: *const NemoRelayNativeString,
2547    request_json: *const NemoRelayNativeString,
2548    next_fn: NemoRelayNativeLlmStreamNextFn,
2549    next_ctx: *mut c_void,
2550    out_stream: *mut NemoRelayNativeLlmStreamV1,
2551) -> NemoRelayStatus
2552where
2553    F: for<'next> Fn(&str, LlmRequest, LlmStreamNext<'next>) -> Result<LlmJsonStream>
2554        + Send
2555        + Sync
2556        + 'static,
2557{
2558    if user_data.is_null() || out_stream.is_null() {
2559        return NemoRelayStatus::NullPointer;
2560    }
2561    unsafe { *out_stream = NemoRelayNativeLlmStreamV1::default() };
2562    let state = unsafe { &*(user_data as *const TypedCallback<F>) };
2563    let result = catch_unwind(AssertUnwindSafe(|| {
2564        let name = read_required_host_string(&state.host, name, "LLM name")?;
2565        let request: LlmRequest = read_json_value(&state.host, request_json, "LLM request")?;
2566        let next = LlmStreamNext {
2567            host: &state.host,
2568            next_fn,
2569            next_ctx,
2570        };
2571        match (state.callback)(&name, request, next) {
2572            Ok(stream) => {
2573                unsafe { *out_stream = native_stream_from_iter(&state.host, stream) };
2574                Ok::<_, NemoRelayStatus>(NemoRelayStatus::Ok)
2575            }
2576            Err(message) => Ok(callback_error(&state.host, message)),
2577        }
2578    }));
2579    match result {
2580        Ok(Ok(status)) => status,
2581        Ok(Err(status)) => status,
2582        Err(_) => callback_panic(&state.host, "LLM stream execution callback"),
2583    }
2584}
2585
2586struct HostString<'a> {
2587    host: &'a NemoRelayNativeHostApiV1,
2588    ptr: *mut NemoRelayNativeString,
2589}
2590
2591impl<'a> HostString<'a> {
2592    fn try_new(
2593        host: &'a NemoRelayNativeHostApiV1,
2594        value: &str,
2595    ) -> std::result::Result<Self, NemoRelayStatus> {
2596        let mut out = ptr::null_mut();
2597        let status = unsafe { (host.string_new)(value.as_ptr(), value.len(), &mut out) };
2598        if status != NemoRelayStatus::Ok {
2599            return Err(status);
2600        }
2601        if out.is_null() {
2602            return Err(NemoRelayStatus::Internal);
2603        }
2604        Ok(Self { host, ptr: out })
2605    }
2606
2607    fn new(host: &'a NemoRelayNativeHostApiV1, value: &str) -> Option<Self> {
2608        Self::try_new(host, value).ok()
2609    }
2610
2611    fn from_json<T: Serialize>(host: &'a NemoRelayNativeHostApiV1, value: &T) -> Option<Self> {
2612        serde_json::to_string(value)
2613            .ok()
2614            .and_then(|json| Self::new(host, &json))
2615    }
2616
2617    fn as_ptr(&self) -> *const NemoRelayNativeString {
2618        self.ptr
2619    }
2620}
2621
2622impl Drop for HostString<'_> {
2623    fn drop(&mut self) {
2624        unsafe { (self.host.string_free)(self.ptr) };
2625    }
2626}
2627
2628struct OptionalHostJson<'a>(Option<HostString<'a>>);
2629
2630impl<'a> OptionalHostJson<'a> {
2631    fn new(host: &'a NemoRelayNativeHostApiV1, value: Option<&Json>) -> Result<Self> {
2632        match value {
2633            Some(value) => HostString::from_json(host, value)
2634                .map(|value| Self(Some(value)))
2635                .ok_or_else(|| "failed to allocate JSON host string".into()),
2636            None => Ok(Self(None)),
2637        }
2638    }
2639
2640    fn as_ptr(&self) -> *const NemoRelayNativeString {
2641        self.0
2642            .as_ref()
2643            .map(HostString::as_ptr)
2644            .unwrap_or(ptr::null())
2645    }
2646}
2647
2648struct PluginState<P> {
2649    host: NemoRelayNativeHostApiV1,
2650    plugin: Mutex<P>,
2651}
2652
2653unsafe extern "C" fn drop_plugin_state<P: NativePlugin>(user_data: *mut c_void) {
2654    if !user_data.is_null() {
2655        let state = unsafe { Box::from_raw(user_data as *mut PluginState<P>) };
2656        let host = state.host;
2657        if catch_unwind(AssertUnwindSafe(|| drop(state))).is_err() {
2658            set_last_error(&host, "native plugin state drop panicked");
2659        }
2660    }
2661}
2662
2663unsafe extern "C" fn validate_trampoline<P: NativePlugin>(
2664    user_data: *mut c_void,
2665    plugin_config_json: *const NemoRelayNativeString,
2666    out_diagnostics_json: *mut *mut NemoRelayNativeString,
2667) -> NemoRelayStatus {
2668    if user_data.is_null() || out_diagnostics_json.is_null() {
2669        return NemoRelayStatus::NullPointer;
2670    }
2671    unsafe { *out_diagnostics_json = ptr::null_mut() };
2672    let state = unsafe { &*(user_data as *const PluginState<P>) };
2673    let result = catch_unwind(AssertUnwindSafe(|| {
2674        let config = match read_json_object(&state.host, plugin_config_json) {
2675            Ok(config) => config,
2676            Err(status) => return status,
2677        };
2678        let plugin = match state.plugin.lock() {
2679            Ok(plugin) => plugin,
2680            Err(_) => {
2681                set_last_error(&state.host, "native plugin state lock poisoned");
2682                return NemoRelayStatus::Internal;
2683            }
2684        };
2685        let diagnostics = plugin.validate(&config);
2686        write_json(&state.host, &diagnostics, out_diagnostics_json)
2687    }));
2688    result.unwrap_or_else(|_| {
2689        set_last_error(&state.host, "native plugin validate callback panicked");
2690        NemoRelayStatus::Internal
2691    })
2692}
2693
2694unsafe extern "C" fn register_trampoline<P: NativePlugin>(
2695    user_data: *mut c_void,
2696    plugin_config_json: *const NemoRelayNativeString,
2697    ctx: *mut NemoRelayNativePluginContext,
2698) -> NemoRelayStatus {
2699    if user_data.is_null() || ctx.is_null() {
2700        return NemoRelayStatus::NullPointer;
2701    }
2702    let state = unsafe { &*(user_data as *const PluginState<P>) };
2703    let result = catch_unwind(AssertUnwindSafe(|| {
2704        let config = match read_json_object(&state.host, plugin_config_json) {
2705            Ok(config) => config,
2706            Err(status) => return status,
2707        };
2708        let mut ctx = unsafe { PluginContext::from_raw(&state.host, ctx) };
2709        let mut plugin = match state.plugin.lock() {
2710            Ok(plugin) => plugin,
2711            Err(_) => {
2712                set_last_error(&state.host, "native plugin state lock poisoned");
2713                return NemoRelayStatus::Internal;
2714            }
2715        };
2716        match plugin.register(&config, &mut ctx) {
2717            Ok(()) => NemoRelayStatus::Ok,
2718            Err(message) => {
2719                set_last_error(&state.host, &message);
2720                NemoRelayStatus::Internal
2721            }
2722        }
2723    }));
2724    result.unwrap_or_else(|_| {
2725        set_last_error(&state.host, "native plugin register callback panicked");
2726        NemoRelayStatus::Internal
2727    })
2728}
2729
2730fn read_json_object(
2731    host: &NemoRelayNativeHostApiV1,
2732    value: *const NemoRelayNativeString,
2733) -> std::result::Result<Map<String, Json>, NemoRelayStatus> {
2734    let value: Json = read_json_value(host, value, "plugin config")?;
2735    match value {
2736        Json::Object(map) => Ok(map),
2737        _ => {
2738            set_last_error(host, "plugin config must be a JSON object");
2739            Err(NemoRelayStatus::InvalidJson)
2740        }
2741    }
2742}
2743
2744fn read_json_value<T: DeserializeOwned>(
2745    host: &NemoRelayNativeHostApiV1,
2746    value: *const NemoRelayNativeString,
2747    label: &str,
2748) -> std::result::Result<T, NemoRelayStatus> {
2749    let text = read_required_host_string(host, value, label)?;
2750    serde_json::from_str::<T>(&text).map_err(|error| {
2751        set_last_error(host, &format!("{label} was invalid JSON: {error}"));
2752        NemoRelayStatus::InvalidJson
2753    })
2754}
2755
2756fn read_optional_json_value<T: DeserializeOwned>(
2757    host: &NemoRelayNativeHostApiV1,
2758    value: *const NemoRelayNativeString,
2759    label: &str,
2760) -> std::result::Result<Option<T>, NemoRelayStatus> {
2761    if value.is_null() {
2762        Ok(None)
2763    } else {
2764        read_json_value(host, value, label).map(Some)
2765    }
2766}
2767
2768enum HostStringReadError {
2769    Null,
2770    InvalidUtf8,
2771}
2772
2773fn read_required_host_string(
2774    host: &NemoRelayNativeHostApiV1,
2775    value: *const NemoRelayNativeString,
2776    label: &str,
2777) -> std::result::Result<String, NemoRelayStatus> {
2778    match read_host_string(host, value) {
2779        Ok(value) => Ok(value),
2780        Err(HostStringReadError::Null) => {
2781            set_last_error(host, &format!("{label} was null"));
2782            Err(NemoRelayStatus::NullPointer)
2783        }
2784        Err(HostStringReadError::InvalidUtf8) => {
2785            set_last_error(host, &format!("{label} contained invalid UTF-8"));
2786            Err(NemoRelayStatus::InvalidUtf8)
2787        }
2788    }
2789}
2790
2791fn read_host_string(
2792    host: &NemoRelayNativeHostApiV1,
2793    value: *const NemoRelayNativeString,
2794) -> std::result::Result<String, HostStringReadError> {
2795    if value.is_null() {
2796        return Err(HostStringReadError::Null);
2797    }
2798    let len = unsafe { (host.string_len)(value) };
2799    let data = unsafe { (host.string_data)(value) };
2800    if data.is_null() && len > 0 {
2801        return Err(HostStringReadError::InvalidUtf8);
2802    }
2803    let bytes = if len == 0 {
2804        &[][..]
2805    } else {
2806        unsafe { std::slice::from_raw_parts(data, len) }
2807    };
2808    std::str::from_utf8(bytes)
2809        .map(str::to_owned)
2810        .map_err(|_| HostStringReadError::InvalidUtf8)
2811}
2812
2813fn write_json<T: Serialize>(
2814    host: &NemoRelayNativeHostApiV1,
2815    value: &T,
2816    out: *mut *mut NemoRelayNativeString,
2817) -> NemoRelayStatus {
2818    if out.is_null() {
2819        return NemoRelayStatus::NullPointer;
2820    }
2821    unsafe { *out = ptr::null_mut() };
2822    let json = serde_json::to_value(value).expect("Relay DTOs and serde_json::Value serialize");
2823    let Some(handle) = HostString::from_json(host, &json) else {
2824        set_last_error(host, "failed to allocate host string");
2825        return NemoRelayStatus::Internal;
2826    };
2827    unsafe { *out = handle.ptr };
2828    std::mem::forget(handle);
2829    NemoRelayStatus::Ok
2830}
2831
2832fn set_last_error(host: &NemoRelayNativeHostApiV1, message: &str) {
2833    if let Some(message) = HostString::new(host, message) {
2834        unsafe { (host.last_error_set)(message.as_ptr()) };
2835    }
2836}
2837
2838/// Sets a host last-error message from generated entry symbols.
2839///
2840/// # Safety
2841/// `host` must be null or point to a valid [`NemoRelayNativeHostApiV1`].
2842#[doc(hidden)]
2843pub unsafe fn __set_last_error_from_entry(host: *const NemoRelayNativeHostApiV1, message: &str) {
2844    if !host.is_null() {
2845        set_last_error(unsafe { &*host }, message);
2846    }
2847}
2848
2849/// Initializes a native plugin descriptor for a Rust SDK plugin value.
2850///
2851/// # Safety
2852/// `host` must point to a valid [`NemoRelayNativeHostApiV1`] for the duration
2853/// of the call, and `out` must point to writable memory for one
2854/// [`NemoRelayNativePluginV1`] descriptor.
2855pub unsafe fn export_plugin<P: NativePlugin>(
2856    host: *const NemoRelayNativeHostApiV1,
2857    out: *mut NemoRelayNativePluginV1,
2858    plugin: P,
2859) -> NemoRelayStatus {
2860    if host.is_null() || out.is_null() {
2861        return NemoRelayStatus::NullPointer;
2862    }
2863    unsafe { *out = NemoRelayNativePluginV1::default() };
2864    let host_ref = unsafe { &*host };
2865    export_plugin_checked(host_ref, out, || plugin)
2866}
2867
2868/// Initializes a native plugin descriptor from a constructor callback.
2869///
2870/// # Safety
2871/// `host` must point to a valid [`NemoRelayNativeHostApiV1`] for the duration
2872/// of the call, and `out` must point to writable memory for one
2873/// [`NemoRelayNativePluginV1`] descriptor.
2874#[doc(hidden)]
2875pub unsafe fn __export_plugin_from_constructor<P, F>(
2876    host: *const NemoRelayNativeHostApiV1,
2877    out: *mut NemoRelayNativePluginV1,
2878    constructor: F,
2879) -> NemoRelayStatus
2880where
2881    P: NativePlugin,
2882    F: FnOnce() -> P,
2883{
2884    if host.is_null() || out.is_null() {
2885        return NemoRelayStatus::NullPointer;
2886    }
2887    unsafe { *out = NemoRelayNativePluginV1::default() };
2888    let host_ref = unsafe { &*host };
2889    export_plugin_checked(host_ref, out, constructor)
2890}
2891
2892fn export_plugin_checked<P, F>(
2893    host_ref: &NemoRelayNativeHostApiV1,
2894    out: *mut NemoRelayNativePluginV1,
2895    constructor: F,
2896) -> NemoRelayStatus
2897where
2898    P: NativePlugin,
2899    F: FnOnce() -> P,
2900{
2901    if host_ref.abi_version != NEMO_RELAY_NATIVE_ABI_VERSION {
2902        return NemoRelayStatus::InvalidArg;
2903    }
2904    if host_ref.struct_size < std::mem::size_of::<NemoRelayNativeHostApiV1>() {
2905        return NemoRelayStatus::InvalidArg;
2906    }
2907
2908    let plugin = constructor();
2909    let kind = plugin.plugin_kind().to_owned();
2910    let allows_multiple_components = plugin.allows_multiple_components();
2911    let Some(kind_handle) = HostString::new(host_ref, &kind) else {
2912        return NemoRelayStatus::Internal;
2913    };
2914    let state = Box::new(PluginState {
2915        host: *host_ref,
2916        plugin: Mutex::new(plugin),
2917    });
2918    unsafe {
2919        *out = NemoRelayNativePluginV1 {
2920            struct_size: std::mem::size_of::<NemoRelayNativePluginV1>(),
2921            plugin_kind: kind_handle.ptr,
2922            allows_multiple_components,
2923            user_data: Box::into_raw(state) as *mut c_void,
2924            validate: Some(validate_trampoline::<P>),
2925            register: Some(register_trampoline::<P>),
2926            drop: Some(drop_plugin_state::<P>),
2927        };
2928    }
2929    std::mem::forget(kind_handle);
2930    NemoRelayStatus::Ok
2931}
2932
2933/// Exports a concrete plugin constructor as a native plugin entry symbol body.
2934#[macro_export]
2935macro_rules! nemo_relay_plugin {
2936    ($symbol:ident, $constructor:expr) => {
2937        #[doc = "Native plugin entry symbol generated by `nemo_relay_plugin!`."]
2938        #[unsafe(no_mangle)]
2939        pub unsafe extern "C" fn $symbol(
2940            host: *const $crate::NemoRelayNativeHostApiV1,
2941            out: *mut $crate::NemoRelayNativePluginV1,
2942        ) -> $crate::NemoRelayStatus {
2943            match ::std::panic::catch_unwind(::std::panic::AssertUnwindSafe(|| unsafe {
2944                $crate::__export_plugin_from_constructor(host, out, $constructor)
2945            })) {
2946                Ok(status) => status,
2947                Err(_) => {
2948                    unsafe {
2949                        $crate::__set_last_error_from_entry(
2950                            host,
2951                            "native plugin entry callback panicked",
2952                        )
2953                    };
2954                    $crate::NemoRelayStatus::Internal
2955                }
2956            }
2957        }
2958    };
2959}