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