Skip to main content

nemo_relay_plugin/
lib.rs

1// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4#![deny(rustdoc::broken_intra_doc_links, rustdoc::private_intra_doc_links)]
5
6//! Stable native plugin ABI and Rust authoring helpers for NeMo Relay.
7//!
8//! This crate intentionally does not depend on the `nemo-relay` runtime crate.
9//! Native plugins built with it communicate with a host through versioned
10//! C-compatible tables and host-owned string handles.
11
12use std::ffi::{c_char, c_void};
13use std::marker::{PhantomData, PhantomPinned};
14use std::panic::{AssertUnwindSafe, catch_unwind};
15use std::ptr;
16use std::sync::Mutex;
17
18pub use nemo_relay_types::Json;
19pub use nemo_relay_types::api::event::{
20    CategoryProfile, DataSchema, Event, EventCategory, EventSanitizeFields, PendingMarkSpec,
21    ScopeCategory,
22};
23pub use nemo_relay_types::api::llm::{LlmAttributes, LlmRequest, LlmRequestInterceptOutcome};
24pub use nemo_relay_types::api::scope::{HandleAttributes, ScopeAttributes, ScopeType};
25pub use nemo_relay_types::api::tool::{ToolAttributes, ToolExecutionInterceptOutcome};
26pub use nemo_relay_types::codec::optimization::{
27    LlmOptimizationContribution, LlmOptimizationEvidenceQuality, LlmOptimizationKind,
28    LlmOptimizationModel, LlmOptimizationModelTransition, LlmOptimizationPayload,
29    LlmOptimizationSummary, LlmOptimizationSummaryStatus, LlmOptimizationTokenImpact,
30    LlmOptimizationTokens,
31};
32pub use nemo_relay_types::codec::request::AnnotatedLlmRequest;
33pub use nemo_relay_types::codec::response::AnnotatedLlmResponse;
34pub use nemo_relay_types::plugin::{ConfigDiagnostic, DiagnosticLevel};
35use serde::{Serialize, de::DeserializeOwned};
36use serde_json::Map;
37
38/// Native plugin ABI version supported by this crate.
39///
40/// Version 3 reserves the native async middleware extension. Hosts retain a
41/// version-2 table for already-built plugins during entry-point negotiation.
42pub const NEMO_RELAY_NATIVE_ABI_VERSION: u32 = 3;
43/// ABI version that introduced completion-based asynchronous middleware.
44pub const NEMO_RELAY_NATIVE_ABI_VERSION_ASYNC_MIDDLEWARE: u32 = 3;
45
46/// Legacy native plugin ABI accepted by Relay hosts for compatibility.
47pub const NEMO_RELAY_NATIVE_ABI_VERSION_LEGACY: u32 = 2;
48
49/// Built-in LLM codec identities available to native plugins.
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, serde::Deserialize)]
51#[serde(rename_all = "snake_case")]
52pub enum BuiltinLlmCodec {
53    /// OpenAI Chat Completions.
54    #[serde(rename = "openai_chat")]
55    OpenAiChat,
56    /// OpenAI Responses.
57    #[serde(rename = "openai_responses")]
58    OpenAiResponses,
59    /// Anthropic Messages.
60    #[serde(rename = "anthropic_messages")]
61    AnthropicMessages,
62}
63
64/// Per-call LLM codec identity delivered to native plugins.
65#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, serde::Deserialize)]
66#[serde(tag = "kind", content = "id", rename_all = "snake_case")]
67pub enum LlmCodecIdentity {
68    /// No codec was active.
69    #[default]
70    None,
71    /// A Relay built-in codec was active.
72    #[serde(rename = "builtin")]
73    BuiltIn(BuiltinLlmCodec),
74    /// A runtime-registered codec was active, identified by its stable ID.
75    Runtime(String),
76    /// A codec was active but has no registered identity.
77    Opaque,
78}
79
80/// Per-call request codec context delivered to an LLM sanitizer.
81pub struct LlmSanitizeRequestContext<'a> {
82    /// Identity of the active codec.
83    pub codec: LlmCodecIdentity,
84    resolved: Option<LlmSanitizeRequestCodec<'a>>,
85}
86
87/// Per-call response codec context delivered to an LLM sanitizer.
88pub struct LlmSanitizeResponseContext<'a> {
89    /// Identity of the active codec.
90    pub codec: LlmCodecIdentity,
91    resolved: Option<LlmSanitizeResponseCodec<'a>>,
92}
93
94/// Status codes returned by stable native ABI functions.
95#[repr(i32)]
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub enum NemoRelayStatus {
98    /// Operation completed successfully.
99    Ok = 0,
100    /// A resource with the given name already exists.
101    AlreadyExists = 1,
102    /// The requested resource was not found.
103    NotFound = 2,
104    /// The scope stack is empty.
105    ScopeStackEmpty = 3,
106    /// A guardrail rejected the operation.
107    GuardrailRejected = 4,
108    /// An internal runtime error occurred.
109    Internal = 5,
110    /// A required pointer argument was null.
111    NullPointer = 6,
112    /// A JSON string argument could not be parsed.
113    InvalidJson = 7,
114    /// A string argument contained invalid UTF-8.
115    InvalidUtf8 = 8,
116    /// A function argument had an invalid value.
117    InvalidArg = 9,
118    /// A stream reached end-of-stream and has no chunk to return.
119    StreamEnd = 10,
120}
121
122/// Opaque host-owned UTF-8 string or JSON byte buffer.
123#[repr(C)]
124pub struct NemoRelayNativeString {
125    _private: [u8; 0],
126    _marker: PhantomData<(*mut u8, PhantomPinned)>,
127}
128
129/// Opaque callback-scoped request codec capability owned by the host.
130#[repr(C)]
131pub struct NemoRelayNativeLlmRequestCodec {
132    _private: [u8; 0],
133    _marker: PhantomData<(*mut u8, PhantomPinned)>,
134}
135
136/// Opaque callback-scoped response codec capability owned by the host.
137#[repr(C)]
138pub struct NemoRelayNativeLlmResponseCodec {
139    _private: [u8; 0],
140    _marker: PhantomData<(*mut u8, PhantomPinned)>,
141}
142
143/// Discriminator for the codec supplied to an LLM sanitizer over the native ABI.
144#[repr(u32)]
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub enum NemoRelayNativeLlmCodecKind {
147    /// No codec was active for this call.
148    None = 0,
149    /// A Relay built-in codec was active.
150    BuiltIn = 1,
151    /// A runtime-registered codec was active.
152    Runtime = 2,
153    /// A codec was active but has no registered identity.
154    Opaque = 3,
155}
156
157/// Per-call LLM sanitizer context passed over the native ABI.
158///
159/// `codec_id` is borrowed for the duration of the callback. It is null for
160/// [`NemoRelayNativeLlmCodecKind::None`] and
161/// [`NemoRelayNativeLlmCodecKind::Opaque`]. For `BuiltIn`, it is one of the
162/// stable built-in codec IDs; for `Runtime`, it is the registered codec ID.
163#[repr(C)]
164#[derive(Debug, Clone, Copy)]
165pub struct NemoRelayNativeLlmSanitizeRequestContext {
166    /// Discriminator for the active codec.
167    pub codec_kind: NemoRelayNativeLlmCodecKind,
168    /// Optional borrowed codec identifier.
169    pub codec_id: *const NemoRelayNativeString,
170    /// Borrowed request codec capability, or null when no codec is active.
171    pub codec: *const NemoRelayNativeLlmRequestCodec,
172}
173
174/// Per-call response sanitizer context passed over the native ABI.
175#[repr(C)]
176#[derive(Debug, Clone, Copy)]
177pub struct NemoRelayNativeLlmSanitizeResponseContext {
178    /// Discriminator for the active codec.
179    pub codec_kind: NemoRelayNativeLlmCodecKind,
180    /// Optional borrowed codec identifier.
181    pub codec_id: *const NemoRelayNativeString,
182    /// Borrowed response codec capability, or null when no codec is active.
183    pub codec: *const NemoRelayNativeLlmResponseCodec,
184}
185
186/// Safe callback-scoped request codec facade for typed native plugins.
187pub struct LlmSanitizeRequestCodec<'a> {
188    host: NemoRelayNativeHostApiV1,
189    handle: *const NemoRelayNativeLlmRequestCodec,
190    _lifetime: PhantomData<&'a NemoRelayNativeLlmRequestCodec>,
191}
192
193impl LlmSanitizeRequestCodec<'_> {
194    /// Decode an opaque request into Relay's normalized request model.
195    pub fn decode(&self, request: &LlmRequest) -> Result<AnnotatedLlmRequest> {
196        native_codec_call(&self.host, |out| unsafe {
197            let request = HostString::from_json(&self.host, request)
198                .ok_or_else(|| "failed to serialize LLM request".to_string())?;
199            codec_status(
200                &self.host,
201                (self.host.llm_request_codec_decode)(self.handle, request.as_ptr(), out),
202            )
203        })
204    }
205
206    /// Encode normalized changes onto the original opaque request.
207    pub fn encode(
208        &self,
209        annotated: &AnnotatedLlmRequest,
210        original: &LlmRequest,
211    ) -> Result<LlmRequest> {
212        native_codec_call(&self.host, |out| unsafe {
213            let annotated = HostString::from_json(&self.host, annotated)
214                .ok_or_else(|| "failed to serialize annotated request".to_string())?;
215            let original = HostString::from_json(&self.host, original)
216                .ok_or_else(|| "failed to serialize original request".to_string())?;
217            codec_status(
218                &self.host,
219                (self.host.llm_request_codec_encode)(
220                    self.handle,
221                    annotated.as_ptr(),
222                    original.as_ptr(),
223                    out,
224                ),
225            )
226        })
227    }
228}
229
230/// Safe callback-scoped response codec facade for typed native plugins.
231pub struct LlmSanitizeResponseCodec<'a> {
232    host: NemoRelayNativeHostApiV1,
233    handle: *const NemoRelayNativeLlmResponseCodec,
234    _lifetime: PhantomData<&'a NemoRelayNativeLlmResponseCodec>,
235}
236
237impl LlmSanitizeResponseCodec<'_> {
238    /// Decode an opaque response into Relay's normalized response model.
239    pub fn decode(&self, response: &Json) -> Result<AnnotatedLlmResponse> {
240        native_codec_call(&self.host, |out| unsafe {
241            let response = HostString::from_json(&self.host, response)
242                .ok_or_else(|| "failed to serialize LLM response".to_string())?;
243            codec_status(
244                &self.host,
245                (self.host.llm_response_codec_decode)(self.handle, response.as_ptr(), out),
246            )
247        })
248    }
249}
250
251impl<'a> LlmSanitizeRequestContext<'a> {
252    /// Resolve the active request codec capability.
253    #[must_use]
254    pub fn resolve_codec(&self) -> Option<&LlmSanitizeRequestCodec<'a>> {
255        self.resolved.as_ref()
256    }
257}
258
259impl<'a> LlmSanitizeResponseContext<'a> {
260    /// Resolve the active response codec capability.
261    #[must_use]
262    pub fn resolve_codec(&self) -> Option<&LlmSanitizeResponseCodec<'a>> {
263        self.resolved.as_ref()
264    }
265}
266
267/// Opaque plugin registration context borrowed from the host during registration.
268#[repr(C)]
269pub struct NemoRelayNativePluginContext {
270    _private: [u8; 0],
271    _marker: PhantomData<(*mut u8, PhantomPinned)>,
272}
273
274/// Opaque host-owned scope handle.
275#[repr(C)]
276pub struct NemoRelayNativeScopeHandle {
277    _private: [u8; 0],
278    _marker: PhantomData<(*mut u8, PhantomPinned)>,
279}
280
281/// Opaque host-owned scope stack handle.
282#[repr(C)]
283pub struct NemoRelayNativeScopeStack {
284    _private: [u8; 0],
285    _marker: PhantomData<(*mut u8, PhantomPinned)>,
286}
287
288/// Opaque host-owned captured scope-stack binding.
289#[repr(C)]
290pub struct NemoRelayNativeScopeStackBinding {
291    _private: [u8; 0],
292    _marker: PhantomData<(*mut u8, PhantomPinned)>,
293}
294
295/// Scope category used by native plugins when opening scopes.
296#[repr(i32)]
297#[derive(Debug, Clone, Copy, PartialEq, Eq)]
298pub enum NemoRelayNativeScopeType {
299    /// Top-level agent scope.
300    Agent = 0,
301    /// Generic function scope.
302    Function = 1,
303    /// Tool invocation scope.
304    Tool = 2,
305    /// LLM call scope.
306    Llm = 3,
307    /// Retriever scope.
308    Retriever = 4,
309    /// Embedder scope.
310    Embedder = 5,
311    /// Reranker scope.
312    Reranker = 6,
313    /// Guardrail evaluation scope.
314    Guardrail = 7,
315    /// Evaluator scope.
316    Evaluator = 8,
317    /// User-defined custom scope.
318    Custom = 9,
319    /// Unknown or unspecified scope type.
320    Unknown = 10,
321}
322
323/// Optional destructor for user data captured by native callbacks.
324pub type NemoRelayNativeFreeFn = Option<unsafe extern "C" fn(user_data: *mut c_void)>;
325
326/// Native callback executed while a host scope stack is temporarily active.
327pub type NemoRelayNativeWithScopeStackCb =
328    unsafe extern "C" fn(user_data: *mut c_void) -> NemoRelayStatus;
329
330/// Runtime-provided continuation for tool execution intercepts.
331pub type NemoRelayNativeToolNextFn = unsafe extern "C" fn(
332    args_json: *const NemoRelayNativeString,
333    next_ctx: *mut c_void,
334    out_json: *mut *mut NemoRelayNativeString,
335) -> NemoRelayStatus;
336
337/// Runtime-provided continuation for LLM execution intercepts.
338pub type NemoRelayNativeLlmNextFn = unsafe extern "C" fn(
339    request_json: *const NemoRelayNativeString,
340    next_ctx: *mut c_void,
341    out_json: *mut *mut NemoRelayNativeString,
342) -> NemoRelayStatus;
343
344/// Native stream poll callback.
345///
346/// Return [`NemoRelayStatus::Ok`] with `out_json` set for one chunk,
347/// [`NemoRelayStatus::StreamEnd`] with `out_json` null at end of stream, or an
348/// error status for stream failure.
349pub type NemoRelayNativeLlmStreamPollFn = unsafe extern "C" fn(
350    user_data: *mut c_void,
351    out_json: *mut *mut NemoRelayNativeString,
352) -> NemoRelayStatus;
353
354/// Optional native stream cancellation callback.
355pub type NemoRelayNativeLlmStreamCancelFn =
356    Option<unsafe extern "C" fn(user_data: *mut c_void) -> NemoRelayStatus>;
357
358/// Optional native stream destructor callback.
359pub type NemoRelayNativeLlmStreamDropFn = Option<unsafe extern "C" fn(user_data: *mut c_void)>;
360
361/// Native LLM JSON stream handle table.
362#[repr(C)]
363pub struct NemoRelayNativeLlmStreamV1 {
364    /// Size of this struct as seen by the producer.
365    pub struct_size: usize,
366    /// Stream state passed back to poll/cancel/drop callbacks.
367    pub user_data: *mut c_void,
368    /// Polls the next stream chunk.
369    pub next: Option<NemoRelayNativeLlmStreamPollFn>,
370    /// Cancels an in-flight stream when a consumer stops before stream end.
371    pub cancel: NemoRelayNativeLlmStreamCancelFn,
372    /// Drops stream state after stream completion, error, or cancellation.
373    pub drop: NemoRelayNativeLlmStreamDropFn,
374}
375
376impl Default for NemoRelayNativeLlmStreamV1 {
377    fn default() -> Self {
378        Self {
379            struct_size: std::mem::size_of::<Self>(),
380            user_data: ptr::null_mut(),
381            next: None,
382            cancel: None,
383            drop: None,
384        }
385    }
386}
387
388/// Runtime-provided continuation for LLM stream execution intercepts.
389pub type NemoRelayNativeLlmStreamNextFn = unsafe extern "C" fn(
390    request_json: *const NemoRelayNativeString,
391    next_ctx: *mut c_void,
392    out_stream: *mut NemoRelayNativeLlmStreamV1,
393) -> NemoRelayStatus;
394
395/// Native event subscriber callback.
396pub type NemoRelayNativeEventSubscriberCb = unsafe extern "C" fn(
397    user_data: *mut c_void,
398    event_json: *const NemoRelayNativeString,
399) -> NemoRelayStatus;
400
401/// Native event observability-field sanitizer callback.
402pub type NemoRelayNativeEventSanitizeCb = unsafe extern "C" fn(
403    user_data: *mut c_void,
404    event_json: *const NemoRelayNativeString,
405    fields_json: *const NemoRelayNativeString,
406    out_fields_json: *mut *mut NemoRelayNativeString,
407) -> NemoRelayStatus;
408
409/// Native JSON transform callback for tool request/response sanitizers and tool request intercepts.
410pub type NemoRelayNativeToolJsonCb = unsafe extern "C" fn(
411    user_data: *mut c_void,
412    name: *const NemoRelayNativeString,
413    payload_json: *const NemoRelayNativeString,
414    out_json: *mut *mut NemoRelayNativeString,
415) -> NemoRelayStatus;
416
417/// Native tool conditional-execution callback.
418pub type NemoRelayNativeToolConditionalCb = unsafe extern "C" fn(
419    user_data: *mut c_void,
420    name: *const NemoRelayNativeString,
421    args_json: *const NemoRelayNativeString,
422    out_reason: *mut *mut NemoRelayNativeString,
423) -> NemoRelayStatus;
424
425/// Native tool execution intercept callback.
426pub type NemoRelayNativeToolExecutionCb = unsafe extern "C" fn(
427    user_data: *mut c_void,
428    name: *const NemoRelayNativeString,
429    args_json: *const NemoRelayNativeString,
430    next_fn: NemoRelayNativeToolNextFn,
431    next_ctx: *mut c_void,
432    out_outcome_json: *mut *mut NemoRelayNativeString,
433) -> NemoRelayStatus;
434
435/// Native LLM request sanitizer callback. Return a successful null output to
436/// omit the observability payload and annotation. `request_json` is borrowed,
437/// but may be written directly to `out_request_json` as a pass-through; the
438/// host releases an aliased input/output once. Any other non-null output must
439/// be host-allocated and transfers ownership to the host.
440pub type NemoRelayNativeLlmSanitizeRequestCb = unsafe extern "C" fn(
441    user_data: *mut c_void,
442    request_json: *const NemoRelayNativeString,
443    context: NemoRelayNativeLlmSanitizeRequestContext,
444    out_request_json: *mut *mut NemoRelayNativeString,
445) -> NemoRelayStatus;
446
447/// Native LLM response sanitizer callback. Return a successful null output to
448/// omit the observability payload and annotation. `payload_json` is borrowed,
449/// but may be written directly to `out_json` as a pass-through; the host
450/// releases an aliased input/output once. Any other non-null output must be
451/// host-allocated and transfers ownership to the host.
452pub type NemoRelayNativeLlmSanitizeResponseCb = unsafe extern "C" fn(
453    user_data: *mut c_void,
454    payload_json: *const NemoRelayNativeString,
455    context: NemoRelayNativeLlmSanitizeResponseContext,
456    out_json: *mut *mut NemoRelayNativeString,
457) -> NemoRelayStatus;
458
459/// Native LLM conditional-execution callback.
460pub type NemoRelayNativeLlmConditionalCb = unsafe extern "C" fn(
461    user_data: *mut c_void,
462    request_json: *const NemoRelayNativeString,
463    out_reason: *mut *mut NemoRelayNativeString,
464) -> NemoRelayStatus;
465
466/// Native LLM request intercept callback.
467pub type NemoRelayNativeLlmRequestInterceptCb = unsafe extern "C" fn(
468    user_data: *mut c_void,
469    name: *const NemoRelayNativeString,
470    request_json: *const NemoRelayNativeString,
471    annotated_json: *const NemoRelayNativeString,
472    out_outcome_json: *mut *mut NemoRelayNativeString,
473) -> NemoRelayStatus;
474
475/// Native LLM execution intercept callback.
476pub type NemoRelayNativeLlmExecutionCb = unsafe extern "C" fn(
477    user_data: *mut c_void,
478    name: *const NemoRelayNativeString,
479    request_json: *const NemoRelayNativeString,
480    next_fn: NemoRelayNativeLlmNextFn,
481    next_ctx: *mut c_void,
482    out_json: *mut *mut NemoRelayNativeString,
483) -> NemoRelayStatus;
484
485/// Native LLM stream execution intercept callback.
486pub type NemoRelayNativeLlmStreamExecutionCb = unsafe extern "C" fn(
487    user_data: *mut c_void,
488    name: *const NemoRelayNativeString,
489    request_json: *const NemoRelayNativeString,
490    next_fn: NemoRelayNativeLlmStreamNextFn,
491    next_ctx: *mut c_void,
492    out_stream: *mut NemoRelayNativeLlmStreamV1,
493) -> NemoRelayStatus;
494
495/// Native plugin validation callback.
496pub type NemoRelayNativePluginValidateFn = unsafe extern "C" fn(
497    user_data: *mut c_void,
498    plugin_config_json: *const NemoRelayNativeString,
499    out_diagnostics_json: *mut *mut NemoRelayNativeString,
500) -> NemoRelayStatus;
501
502/// Native plugin registration callback.
503pub type NemoRelayNativePluginRegisterFn = unsafe extern "C" fn(
504    user_data: *mut c_void,
505    plugin_config_json: *const NemoRelayNativeString,
506    ctx: *mut NemoRelayNativePluginContext,
507) -> NemoRelayStatus;
508
509/// Native plugin drop callback.
510pub type NemoRelayNativePluginDropFn = Option<unsafe extern "C" fn(user_data: *mut c_void)>;
511
512/// Versioned host API table passed to native plugin entry symbols.
513#[repr(C)]
514#[derive(Clone, Copy)]
515pub struct NemoRelayNativeHostApiV1 {
516    /// ABI version implemented by this table.
517    pub abi_version: u32,
518    /// Size of this struct as seen by the host.
519    pub struct_size: usize,
520    /// Null-terminated host Relay version string.
521    pub relay_version: *const c_char,
522    /// Allocates a host-owned string from UTF-8 bytes.
523    pub string_new: unsafe extern "C" fn(
524        data: *const u8,
525        len: usize,
526        out: *mut *mut NemoRelayNativeString,
527    ) -> NemoRelayStatus,
528    /// Returns the string data pointer for a host-owned string.
529    pub string_data: unsafe extern "C" fn(value: *const NemoRelayNativeString) -> *const u8,
530    /// Returns the byte length for a host-owned string.
531    pub string_len: unsafe extern "C" fn(value: *const NemoRelayNativeString) -> usize,
532    /// Frees a host-owned string.
533    pub string_free: unsafe extern "C" fn(value: *mut NemoRelayNativeString),
534    /// Clears the host thread-local native ABI error message.
535    pub last_error_clear: unsafe extern "C" fn(),
536    /// Sets the host thread-local native ABI error message.
537    pub last_error_set: unsafe extern "C" fn(message: *const NemoRelayNativeString),
538    /// Decodes an LLM request through a callback-scoped codec capability.
539    pub llm_request_codec_decode: unsafe extern "C" fn(
540        codec: *const NemoRelayNativeLlmRequestCodec,
541        request_json: *const NemoRelayNativeString,
542        out: *mut *mut NemoRelayNativeString,
543    ) -> NemoRelayStatus,
544    /// Encodes normalized request changes through a callback-scoped codec capability.
545    pub llm_request_codec_encode: unsafe extern "C" fn(
546        codec: *const NemoRelayNativeLlmRequestCodec,
547        annotated_json: *const NemoRelayNativeString,
548        original_json: *const NemoRelayNativeString,
549        out: *mut *mut NemoRelayNativeString,
550    ) -> NemoRelayStatus,
551    /// Decodes an LLM response through a callback-scoped codec capability.
552    pub llm_response_codec_decode: unsafe extern "C" fn(
553        codec: *const NemoRelayNativeLlmResponseCodec,
554        response_json: *const NemoRelayNativeString,
555        out: *mut *mut NemoRelayNativeString,
556    ) -> NemoRelayStatus,
557    /// Registers an event subscriber through the plugin context.
558    pub plugin_context_register_subscriber: unsafe extern "C" fn(
559        ctx: *mut NemoRelayNativePluginContext,
560        name: *const NemoRelayNativeString,
561        cb: NemoRelayNativeEventSubscriberCb,
562        user_data: *mut c_void,
563        free_fn: NemoRelayNativeFreeFn,
564    ) -> NemoRelayStatus,
565    /// Registers a tool sanitize-request guardrail through the plugin context.
566    pub plugin_context_register_tool_sanitize_request_guardrail:
567        unsafe extern "C" fn(
568            ctx: *mut NemoRelayNativePluginContext,
569            name: *const NemoRelayNativeString,
570            priority: i32,
571            cb: NemoRelayNativeToolJsonCb,
572            user_data: *mut c_void,
573            free_fn: NemoRelayNativeFreeFn,
574        ) -> NemoRelayStatus,
575    /// Registers a tool sanitize-response guardrail through the plugin context.
576    pub plugin_context_register_tool_sanitize_response_guardrail:
577        unsafe extern "C" fn(
578            ctx: *mut NemoRelayNativePluginContext,
579            name: *const NemoRelayNativeString,
580            priority: i32,
581            cb: NemoRelayNativeToolJsonCb,
582            user_data: *mut c_void,
583            free_fn: NemoRelayNativeFreeFn,
584        ) -> NemoRelayStatus,
585    /// Registers a tool conditional-execution guardrail through the plugin context.
586    pub plugin_context_register_tool_conditional_execution_guardrail:
587        unsafe extern "C" fn(
588            ctx: *mut NemoRelayNativePluginContext,
589            name: *const NemoRelayNativeString,
590            priority: i32,
591            cb: NemoRelayNativeToolConditionalCb,
592            user_data: *mut c_void,
593            free_fn: NemoRelayNativeFreeFn,
594        ) -> NemoRelayStatus,
595    /// Registers a tool request intercept through the plugin context.
596    pub plugin_context_register_tool_request_intercept: unsafe extern "C" fn(
597        ctx: *mut NemoRelayNativePluginContext,
598        name: *const NemoRelayNativeString,
599        priority: i32,
600        break_chain: bool,
601        cb: NemoRelayNativeToolJsonCb,
602        user_data: *mut c_void,
603        free_fn: NemoRelayNativeFreeFn,
604    )
605        -> NemoRelayStatus,
606    /// Registers a tool execution intercept through the plugin context.
607    pub plugin_context_register_tool_execution_intercept: unsafe extern "C" fn(
608        ctx: *mut NemoRelayNativePluginContext,
609        name: *const NemoRelayNativeString,
610        priority: i32,
611        cb: NemoRelayNativeToolExecutionCb,
612        user_data: *mut c_void,
613        free_fn: NemoRelayNativeFreeFn,
614    )
615        -> NemoRelayStatus,
616    /// Registers an LLM sanitize-request guardrail through the plugin context.
617    pub plugin_context_register_llm_sanitize_request_guardrail:
618        unsafe extern "C" fn(
619            ctx: *mut NemoRelayNativePluginContext,
620            name: *const NemoRelayNativeString,
621            priority: i32,
622            cb: NemoRelayNativeLlmSanitizeRequestCb,
623            user_data: *mut c_void,
624            free_fn: NemoRelayNativeFreeFn,
625        ) -> NemoRelayStatus,
626    /// Registers an LLM sanitize-response guardrail through the plugin context.
627    pub plugin_context_register_llm_sanitize_response_guardrail:
628        unsafe extern "C" fn(
629            ctx: *mut NemoRelayNativePluginContext,
630            name: *const NemoRelayNativeString,
631            priority: i32,
632            cb: NemoRelayNativeLlmSanitizeResponseCb,
633            user_data: *mut c_void,
634            free_fn: NemoRelayNativeFreeFn,
635        ) -> NemoRelayStatus,
636    /// Registers an LLM conditional-execution guardrail through the plugin context.
637    pub plugin_context_register_llm_conditional_execution_guardrail:
638        unsafe extern "C" fn(
639            ctx: *mut NemoRelayNativePluginContext,
640            name: *const NemoRelayNativeString,
641            priority: i32,
642            cb: NemoRelayNativeLlmConditionalCb,
643            user_data: *mut c_void,
644            free_fn: NemoRelayNativeFreeFn,
645        ) -> NemoRelayStatus,
646    /// Registers an LLM request intercept through the plugin context.
647    pub plugin_context_register_llm_request_intercept: unsafe extern "C" fn(
648        ctx: *mut NemoRelayNativePluginContext,
649        name: *const NemoRelayNativeString,
650        priority: i32,
651        break_chain: bool,
652        cb: NemoRelayNativeLlmRequestInterceptCb,
653        user_data: *mut c_void,
654        free_fn: NemoRelayNativeFreeFn,
655    ) -> NemoRelayStatus,
656    /// Registers an LLM execution intercept through the plugin context.
657    pub plugin_context_register_llm_execution_intercept: unsafe extern "C" fn(
658        ctx: *mut NemoRelayNativePluginContext,
659        name: *const NemoRelayNativeString,
660        priority: i32,
661        cb: NemoRelayNativeLlmExecutionCb,
662        user_data: *mut c_void,
663        free_fn: NemoRelayNativeFreeFn,
664    )
665        -> NemoRelayStatus,
666    /// Registers an LLM stream execution intercept through the plugin context.
667    pub plugin_context_register_llm_stream_execution_intercept:
668        unsafe extern "C" fn(
669            ctx: *mut NemoRelayNativePluginContext,
670            name: *const NemoRelayNativeString,
671            priority: i32,
672            cb: NemoRelayNativeLlmStreamExecutionCb,
673            user_data: *mut c_void,
674            free_fn: NemoRelayNativeFreeFn,
675        ) -> NemoRelayStatus,
676    /// Frees a host-owned scope handle.
677    pub scope_handle_free: unsafe extern "C" fn(handle: *mut NemoRelayNativeScopeHandle),
678    /// Retrieves the current scope handle from the active stack.
679    pub scope_get_current:
680        unsafe extern "C" fn(out: *mut *mut NemoRelayNativeScopeHandle) -> NemoRelayStatus,
681    /// Pushes a scope, emits its start event, and returns its handle.
682    pub scope_push: unsafe extern "C" fn(
683        name: *const NemoRelayNativeString,
684        scope_type: NemoRelayNativeScopeType,
685        parent: *const NemoRelayNativeScopeHandle,
686        attributes: u32,
687        data_json: *const NemoRelayNativeString,
688        metadata_json: *const NemoRelayNativeString,
689        input_json: *const NemoRelayNativeString,
690        timestamp_unix_micros: *const i64,
691        out: *mut *mut NemoRelayNativeScopeHandle,
692    ) -> NemoRelayStatus,
693    /// Pops a scope handle, emits its end event, and clears scope-local registrations.
694    pub scope_pop: unsafe extern "C" fn(
695        handle: *const NemoRelayNativeScopeHandle,
696        output_json: *const NemoRelayNativeString,
697        metadata_json: *const NemoRelayNativeString,
698        timestamp_unix_micros: *const i64,
699    ) -> NemoRelayStatus,
700    /// Emits a mark event under the current or provided parent scope.
701    pub emit_mark: unsafe extern "C" fn(
702        name: *const NemoRelayNativeString,
703        parent: *const NemoRelayNativeScopeHandle,
704        data_json: *const NemoRelayNativeString,
705        metadata_json: *const NemoRelayNativeString,
706        timestamp_unix_micros: *const i64,
707    ) -> NemoRelayStatus,
708    /// Creates a new independent scope stack with its own root scope.
709    pub scope_stack_create:
710        unsafe extern "C" fn(out: *mut *mut NemoRelayNativeScopeStack) -> NemoRelayStatus,
711    /// Frees a host-owned scope stack handle.
712    pub scope_stack_free: unsafe extern "C" fn(stack: *mut NemoRelayNativeScopeStack),
713    /// Binds a scope stack to the current OS thread.
714    pub scope_stack_set_thread:
715        unsafe extern "C" fn(stack: *const NemoRelayNativeScopeStack) -> NemoRelayStatus,
716    /// Captures the current thread-local scope-stack binding.
717    pub scope_stack_capture_thread:
718        unsafe extern "C" fn(out: *mut *mut NemoRelayNativeScopeStackBinding) -> NemoRelayStatus,
719    /// Restores and frees a captured thread-local scope-stack binding.
720    pub scope_stack_restore_thread:
721        unsafe extern "C" fn(binding: *mut NemoRelayNativeScopeStackBinding) -> NemoRelayStatus,
722    /// Frees a captured thread-local binding without restoring it.
723    pub scope_stack_binding_free:
724        unsafe extern "C" fn(binding: *mut NemoRelayNativeScopeStackBinding),
725    /// Returns whether the current context has an explicitly active scope stack.
726    pub scope_stack_active: unsafe extern "C" fn() -> bool,
727    /// Runs a callback with the provided scope stack visible to host runtime APIs.
728    pub scope_stack_with_current: unsafe extern "C" fn(
729        stack: *const NemoRelayNativeScopeStack,
730        cb: NemoRelayNativeWithScopeStackCb,
731        user_data: *mut c_void,
732    ) -> NemoRelayStatus,
733    /// Registers a mark event sanitizer through the plugin context.
734    pub plugin_context_register_mark_sanitize_guardrail: unsafe extern "C" fn(
735        ctx: *mut NemoRelayNativePluginContext,
736        name: *const NemoRelayNativeString,
737        priority: i32,
738        cb: NemoRelayNativeEventSanitizeCb,
739        user_data: *mut c_void,
740        free_fn: NemoRelayNativeFreeFn,
741    )
742        -> NemoRelayStatus,
743    /// Registers a scope-start event sanitizer through the plugin context.
744    pub plugin_context_register_scope_sanitize_start_guardrail:
745        unsafe extern "C" fn(
746            ctx: *mut NemoRelayNativePluginContext,
747            name: *const NemoRelayNativeString,
748            priority: i32,
749            cb: NemoRelayNativeEventSanitizeCb,
750            user_data: *mut c_void,
751            free_fn: NemoRelayNativeFreeFn,
752        ) -> NemoRelayStatus,
753    /// Registers a scope-end event sanitizer through the plugin context.
754    pub plugin_context_register_scope_sanitize_end_guardrail:
755        unsafe extern "C" fn(
756            ctx: *mut NemoRelayNativePluginContext,
757            name: *const NemoRelayNativeString,
758            priority: i32,
759            cb: NemoRelayNativeEventSanitizeCb,
760            user_data: *mut c_void,
761            free_fn: NemoRelayNativeFreeFn,
762        ) -> NemoRelayStatus,
763}
764
765/// Middleware surface selected by the native async registration hook.
766///
767/// The host only exposes this through the ABI-v3 extension table.  It keeps
768/// every asynchronous callback shape uniform while allowing the host to
769/// deserialize the surface-specific invocation and result payloads.
770#[repr(u32)]
771#[derive(Debug, Clone, Copy, PartialEq, Eq)]
772pub enum NemoRelayNativeAsyncMiddlewareKind {
773    /// Tool start-event request sanitizer.
774    ToolSanitizeRequest = 0,
775    /// Tool end-event response sanitizer.
776    ToolSanitizeResponse = 1,
777    /// Tool execution admission guardrail.
778    ToolConditionalExecution = 2,
779    /// Tool request rewrite intercept.
780    ToolRequestIntercept = 3,
781    /// Tool execution intercept with a continuation.
782    ToolExecutionIntercept = 4,
783    /// LLM start-event request sanitizer.
784    LlmSanitizeRequest = 5,
785    /// LLM end-event response sanitizer.
786    LlmSanitizeResponse = 6,
787    /// LLM execution admission guardrail.
788    LlmConditionalExecution = 7,
789    /// LLM request rewrite intercept.
790    LlmRequestIntercept = 8,
791    /// LLM execution intercept with a continuation.
792    LlmExecutionIntercept = 9,
793    /// Reserved legacy discriminant for streaming LLM execution intercepts.
794    ///
795    /// Hosts reject this kind from the generic completion-based registration
796    /// hook. Use `plugin_context_register_async_stream_middleware` so chunks
797    /// remain incremental.
798    LlmStreamExecutionIntercept = 10,
799    /// Mark event sanitizer.
800    MarkSanitize = 11,
801    /// Scope-start event sanitizer.
802    ScopeSanitizeStart = 12,
803    /// Scope-end event sanitizer.
804    ScopeSanitizeEnd = 13,
805}
806
807impl TryFrom<u32> for NemoRelayNativeAsyncMiddlewareKind {
808    type Error = ();
809
810    fn try_from(value: u32) -> std::result::Result<Self, Self::Error> {
811        match value {
812            0 => Ok(Self::ToolSanitizeRequest),
813            1 => Ok(Self::ToolSanitizeResponse),
814            2 => Ok(Self::ToolConditionalExecution),
815            3 => Ok(Self::ToolRequestIntercept),
816            4 => Ok(Self::ToolExecutionIntercept),
817            5 => Ok(Self::LlmSanitizeRequest),
818            6 => Ok(Self::LlmSanitizeResponse),
819            7 => Ok(Self::LlmConditionalExecution),
820            8 => Ok(Self::LlmRequestIntercept),
821            9 => Ok(Self::LlmExecutionIntercept),
822            10 => Ok(Self::LlmStreamExecutionIntercept),
823            11 => Ok(Self::MarkSanitize),
824            12 => Ok(Self::ScopeSanitizeStart),
825            13 => Ok(Self::ScopeSanitizeEnd),
826            _ => Err(()),
827        }
828    }
829}
830
831/// Indicates whether an asynchronous native callback settled before returning.
832#[repr(u32)]
833#[derive(Debug, Clone, Copy, PartialEq, Eq)]
834pub enum NemoRelayNativeAsyncCallbackState {
835    /// The callback settled its completion before returning.
836    Complete = 0,
837    /// The callback retained its completion for later settlement.
838    Pending = 1,
839}
840
841impl TryFrom<u32> for NemoRelayNativeAsyncCallbackState {
842    type Error = ();
843
844    fn try_from(value: u32) -> std::result::Result<Self, Self::Error> {
845        match value {
846            0 => Ok(Self::Complete),
847            1 => Ok(Self::Pending),
848            _ => Err(()),
849        }
850    }
851}
852
853/// Opaque one-shot completion retained by a pending native callback.
854#[repr(C)]
855pub struct NemoRelayNativeAsyncCompletion {
856    _private: [u8; 0],
857    _marker: PhantomData<(*mut u8, PhantomPinned)>,
858}
859
860/// Opaque native execution continuation supplied only to execution intercepts.
861#[repr(C)]
862pub struct NemoRelayNativeAsyncNext {
863    _private: [u8; 0],
864    _marker: PhantomData<(*mut u8, PhantomPinned)>,
865}
866
867/// Opaque incremental output channel supplied to native async stream intercepts.
868#[repr(C)]
869pub struct NemoRelayNativeAsyncStream {
870    _private: [u8; 0],
871    _marker: PhantomData<(*mut u8, PhantomPinned)>,
872}
873
874/// Receives one downstream stream item. `chunk_json` is non-null for a chunk,
875/// `error` is non-null for failure or consumer cancellation, and `done` marks
876/// clean completion. Unless the callback itself returns `false`, the host
877/// invokes one terminal callback so the plugin can reclaim `user_data`.
878/// Return `false` to cancel downstream production after the current callback;
879/// in that case, reclaim `user_data` before returning because no later callback
880/// is made.
881pub type NemoRelayNativeAsyncNextStreamCb = unsafe extern "C" fn(
882    user_data: *mut c_void,
883    chunk_json: *const NemoRelayNativeString,
884    error: *const NemoRelayNativeString,
885    done: bool,
886) -> bool;
887
888/// Receives one completion from a unary execution-continuation invocation.
889///
890/// Exactly one of `value_json` and `error` is non-null. The callback owns its
891/// `user_data` and is invoked exactly once after a successful
892/// `async_next_invoke_result` call, including when the owning interceptor
893/// settles and cancels unfinished downstream work.
894pub type NemoRelayNativeAsyncNextResultCb = unsafe extern "C" fn(
895    user_data: *mut c_void,
896    value_json: *const NemoRelayNativeString,
897    error: *const NemoRelayNativeString,
898);
899
900/// Incremental native LLM stream intercept callback.
901///
902/// The callback owns `next` and `stream` and must release each exactly once.
903/// It may push chunks before returning or retain the handles and return
904/// `Pending`; no implicit timeout is applied. Relay can invoke separate
905/// middleware calls concurrently without stable OS-thread affinity. Retained
906/// handles may be used from a plugin-owned thread, while callbacks supplied to
907/// `async_next_invoke_stream` run on a Relay runtime worker. The output stream
908/// owns the callback lifetime: `next` may be invoked repeatedly or concurrently
909/// until that stream finishes, rejects, or is cancelled, and each invocation
910/// has independent callback state. Relay rejects or cancels unfinished and
911/// later invocations after settlement. The plugin must synchronize shared
912/// `user_data` and callback state and serialize each handle's final release
913/// after its last operation returns.
914pub type NemoRelayNativeAsyncStreamMiddlewareCb = unsafe extern "C" fn(
915    user_data: *mut c_void,
916    invocation_json: *const NemoRelayNativeString,
917    next: *const NemoRelayNativeAsyncNext,
918    stream: *const NemoRelayNativeAsyncStream,
919) -> u32;
920
921/// Completion-based native middleware callback.
922///
923/// `invocation_json` is borrowed for the call. A callback that returns
924/// [`NemoRelayNativeAsyncCallbackState::Pending`] as a `u32` owns one
925/// completion reference and must settle it then call the v3
926/// `async_completion_release` hook. The host validates the returned
927/// discriminant. When `next` is non-null, the callback owns that handle for
928/// the invocation and must call `async_next_release` exactly once after its
929/// final use, regardless of whether it returns `Complete` or `Pending`. The
930/// host never reclaims a `next` handle after handing it to the callback.
931/// `next` is null for non-execution middleware. Relay invokes the callback on
932/// the Tokio runtime worker polling that middleware invocation, without stable
933/// OS-thread affinity; separate invocations may run concurrently. After
934/// returning `Pending`, retained completion and `next` handles may be used from
935/// a plugin-owned thread until the completion settles. Every `next` operation
936/// must finish before resolving or rejecting the completion; Relay rejects or
937/// cancels unfinished and later continuation calls. The plugin must synchronize
938/// shared `user_data` and callback state and serialize each handle's final
939/// release after its last operation returns.
940pub type NemoRelayNativeAsyncMiddlewareCb = unsafe extern "C" fn(
941    user_data: *mut c_void,
942    invocation_json: *const NemoRelayNativeString,
943    next: *const NemoRelayNativeAsyncNext,
944    completion: *const NemoRelayNativeAsyncCompletion,
945) -> u32;
946
947/// ABI-v3 host extension appended to [`NemoRelayNativeHostApiV1`].
948///
949/// Its first field is the complete v1/v2 table, so legacy plugins can keep
950/// treating the pointer as a [`NemoRelayNativeHostApiV1`].
951#[repr(C)]
952#[derive(Clone, Copy)]
953pub struct NemoRelayNativeHostApiV3 {
954    /// Compatibility prefix for ABI-v1/v2 plugins.
955    pub v1: NemoRelayNativeHostApiV1,
956    /// Resolves an async callback completion with a JSON value.
957    pub async_completion_resolve_json: unsafe extern "C" fn(
958        completion: *const NemoRelayNativeAsyncCompletion,
959        value_json: *const NemoRelayNativeString,
960    ) -> NemoRelayStatus,
961    /// Rejects an async callback completion with a UTF-8 message.
962    pub async_completion_reject: unsafe extern "C" fn(
963        completion: *const NemoRelayNativeAsyncCompletion,
964        message: *const NemoRelayNativeString,
965    ) -> NemoRelayStatus,
966    /// Returns true after the awaiting runtime has cancelled the invocation.
967    pub async_completion_is_cancelled:
968        unsafe extern "C" fn(completion: *const NemoRelayNativeAsyncCompletion) -> bool,
969    /// Releases the callback-owned reference after a pending completion settles.
970    pub async_completion_release:
971        unsafe extern "C" fn(completion: *const NemoRelayNativeAsyncCompletion),
972    /// Invokes an execution continuation and settles a supplied completion.
973    ///
974    /// Cancellation of that completion aborts an in-flight continuation. This
975    /// legacy convenience hook is one-shot because its result settles the
976    /// middleware completion; use `async_next_invoke_result` for repeated or
977    /// concurrent calls.
978    pub async_next_invoke: unsafe extern "C" fn(
979        next: *const NemoRelayNativeAsyncNext,
980        invocation_json: *const NemoRelayNativeString,
981        completion: *const NemoRelayNativeAsyncCompletion,
982    ) -> NemoRelayStatus,
983    /// Releases the callback-owned continuation reference.
984    ///
985    /// Execution callbacks must call this exactly once after their final use
986    /// for both `Complete` and `Pending` return states.
987    pub async_next_release: unsafe extern "C" fn(next: *const NemoRelayNativeAsyncNext),
988    /// Registers a completion-based asynchronous middleware surface.
989    ///
990    /// `kind` must be a valid [`NemoRelayNativeAsyncMiddlewareKind`]
991    /// discriminant. The host rejects unknown `u32` values and
992    /// [`NemoRelayNativeAsyncMiddlewareKind::LlmStreamExecutionIntercept`],
993    /// which must use `plugin_context_register_async_stream_middleware`.
994    pub plugin_context_register_async_middleware: unsafe extern "C" fn(
995        ctx: *mut NemoRelayNativePluginContext,
996        kind: u32,
997        name: *const NemoRelayNativeString,
998        priority: i32,
999        break_chain: bool,
1000        cb: NemoRelayNativeAsyncMiddlewareCb,
1001        user_data: *mut c_void,
1002        free_fn: NemoRelayNativeFreeFn,
1003    ) -> NemoRelayStatus,
1004    /// Pushes one JSON chunk to an incremental native stream without blocking.
1005    ///
1006    /// A full bounded host queue returns [`NemoRelayStatus::Internal`] and
1007    /// records a backpressure message in the host's last-error slot. The
1008    /// producer may retry the logical chunk after the consumer advances.
1009    pub async_stream_push_json: unsafe extern "C" fn(
1010        stream: *const NemoRelayNativeAsyncStream,
1011        chunk_json: *const NemoRelayNativeString,
1012    ) -> NemoRelayStatus,
1013    /// Finishes an incremental native stream successfully.
1014    pub async_stream_finish:
1015        unsafe extern "C" fn(stream: *const NemoRelayNativeAsyncStream) -> NemoRelayStatus,
1016    /// Rejects an incremental native stream without blocking.
1017    ///
1018    /// A full bounded queue returns [`NemoRelayStatus::Internal`]; the caller
1019    /// may retry the rejection after the consumer advances.
1020    pub async_stream_reject: unsafe extern "C" fn(
1021        stream: *const NemoRelayNativeAsyncStream,
1022        message: *const NemoRelayNativeString,
1023    ) -> NemoRelayStatus,
1024    /// Returns true when the consumer cancelled or released the stream.
1025    pub async_stream_is_cancelled:
1026        unsafe extern "C" fn(stream: *const NemoRelayNativeAsyncStream) -> bool,
1027    /// Releases the callback-owned incremental stream reference.
1028    pub async_stream_release: unsafe extern "C" fn(stream: *const NemoRelayNativeAsyncStream),
1029    /// Invokes a downstream stream and reports chunks incrementally.
1030    ///
1031    /// The host reports consumer cancellation through one terminal callback
1032    /// with a non-null error. If a result callback returns `false`, it must
1033    /// reclaim its own `user_data` before returning because no terminal
1034    /// callback follows. This hook may be called repeatedly or concurrently
1035    /// with independent callback state while the output stream remains active.
1036    pub async_next_invoke_stream: unsafe extern "C" fn(
1037        next: *const NemoRelayNativeAsyncNext,
1038        invocation_json: *const NemoRelayNativeString,
1039        stream: *const NemoRelayNativeAsyncStream,
1040        cb: NemoRelayNativeAsyncNextStreamCb,
1041        user_data: *mut c_void,
1042    ) -> NemoRelayStatus,
1043    /// Registers an incremental asynchronous LLM stream intercept.
1044    pub plugin_context_register_async_stream_middleware: unsafe extern "C" fn(
1045        ctx: *mut NemoRelayNativePluginContext,
1046        name: *const NemoRelayNativeString,
1047        priority: i32,
1048        cb: NemoRelayNativeAsyncStreamMiddlewareCb,
1049        user_data: *mut c_void,
1050        free_fn: NemoRelayNativeFreeFn,
1051    )
1052        -> NemoRelayStatus,
1053    /// Invokes a unary execution continuation with an independent result sink.
1054    ///
1055    /// Unlike the legacy completion-coupled `async_next_invoke`, this hook may
1056    /// be called repeatedly or concurrently with distinct `user_data`.
1057    pub async_next_invoke_result: unsafe extern "C" fn(
1058        next: *const NemoRelayNativeAsyncNext,
1059        invocation_json: *const NemoRelayNativeString,
1060        cb: NemoRelayNativeAsyncNextResultCb,
1061        user_data: *mut c_void,
1062    ) -> NemoRelayStatus,
1063}
1064
1065unsafe impl Send for NemoRelayNativeHostApiV3 {}
1066unsafe impl Sync for NemoRelayNativeHostApiV3 {}
1067
1068// The host API table is immutable after construction. Function pointers and
1069// the null-terminated version string pointer are safe to share across threads.
1070unsafe impl Send for NemoRelayNativeHostApiV1 {}
1071unsafe impl Sync for NemoRelayNativeHostApiV1 {}
1072
1073/// Versioned plugin descriptor returned by native plugin entry symbols.
1074#[repr(C)]
1075pub struct NemoRelayNativePluginV1 {
1076    /// Size of this struct as seen by the plugin.
1077    pub struct_size: usize,
1078    /// Host-owned plugin kind string.
1079    pub plugin_kind: *mut NemoRelayNativeString,
1080    /// Whether this plugin kind supports multiple configured components.
1081    pub allows_multiple_components: bool,
1082    /// Plugin-owned state pointer passed to callbacks.
1083    pub user_data: *mut c_void,
1084    /// Optional validation callback.
1085    pub validate: Option<NemoRelayNativePluginValidateFn>,
1086    /// Required registration callback.
1087    pub register: Option<NemoRelayNativePluginRegisterFn>,
1088    /// Optional plugin-owned state destructor.
1089    pub drop: NemoRelayNativePluginDropFn,
1090}
1091
1092impl Default for NemoRelayNativePluginV1 {
1093    fn default() -> Self {
1094        Self {
1095            struct_size: std::mem::size_of::<Self>(),
1096            plugin_kind: ptr::null_mut(),
1097            allows_multiple_components: true,
1098            user_data: ptr::null_mut(),
1099            validate: None,
1100            register: None,
1101            drop: None,
1102        }
1103    }
1104}
1105
1106/// Native entry symbol type loaded by the host.
1107pub type NemoRelayNativePluginEntry = unsafe extern "C" fn(
1108    host: *const NemoRelayNativeHostApiV1,
1109    out: *mut NemoRelayNativePluginV1,
1110) -> NemoRelayStatus;
1111
1112/// Result type used by the Rust native plugin SDK.
1113pub type Result<T> = std::result::Result<T, String>;
1114
1115/// Synchronous JSON chunk stream used by native LLM stream intercept helpers.
1116pub type LlmJsonStream = Box<dyn Iterator<Item = Result<Json>> + Send>;
1117
1118/// Cloneable high-level runtime handle for host APIs available to native plugins.
1119#[derive(Clone)]
1120pub struct PluginRuntime {
1121    host: NemoRelayNativeHostApiV1,
1122}
1123
1124impl PluginRuntime {
1125    /// Creates a runtime handle from the host ABI table.
1126    pub fn new(host: &NemoRelayNativeHostApiV1) -> Self {
1127        Self { host: *host }
1128    }
1129
1130    /// Returns the underlying host ABI table.
1131    pub fn host_api(&self) -> &NemoRelayNativeHostApiV1 {
1132        &self.host
1133    }
1134
1135    /// Retrieves the current scope handle.
1136    pub fn current_scope(&self) -> Result<ScopeHandle<'_>> {
1137        current_scope(&self.host)
1138    }
1139
1140    /// Pushes a scope and emits its start event.
1141    pub fn push_scope(
1142        &self,
1143        name: &str,
1144        scope_type: ScopeType,
1145        data: Option<&Json>,
1146        metadata: Option<&Json>,
1147        input: Option<&Json>,
1148    ) -> Result<ScopeHandle<'_>> {
1149        push_scope(&self.host, name, scope_type.into(), data, metadata, input)
1150    }
1151
1152    /// Pops a scope and emits its end event.
1153    pub fn pop_scope(
1154        &self,
1155        handle: &ScopeHandle<'_>,
1156        output: Option<&Json>,
1157        metadata: Option<&Json>,
1158    ) -> Result<()> {
1159        pop_scope(&self.host, handle, output, metadata)
1160    }
1161
1162    /// Opens a scope that is popped automatically when the guard is closed or dropped.
1163    pub fn scope(
1164        &self,
1165        name: &str,
1166        scope_type: ScopeType,
1167        data: Option<&Json>,
1168        metadata: Option<&Json>,
1169        input: Option<&Json>,
1170    ) -> Result<ScopeGuard<'_>> {
1171        let handle = self.push_scope(name, scope_type, data, metadata, input)?;
1172        Ok(ScopeGuard {
1173            runtime: self,
1174            handle: Some(handle),
1175        })
1176    }
1177
1178    /// Emits a mark event under the current scope.
1179    pub fn emit_mark(
1180        &self,
1181        name: &str,
1182        data: Option<&Json>,
1183        metadata: Option<&Json>,
1184    ) -> Result<()> {
1185        emit_mark(&self.host, name, data, metadata)
1186    }
1187
1188    /// Creates a new independent scope stack.
1189    pub fn create_scope_stack(&self) -> Result<ScopeStack<'_>> {
1190        create_scope_stack(&self.host)
1191    }
1192
1193    /// Captures the current thread-local scope-stack binding.
1194    pub fn capture_scope_stack_thread(&self) -> Result<ScopeStackBinding<'_>> {
1195        capture_scope_stack_thread(&self.host)
1196    }
1197
1198    /// Returns whether the current context has an explicitly active scope stack.
1199    pub fn scope_stack_active(&self) -> bool {
1200        unsafe { (self.host.scope_stack_active)() }
1201    }
1202
1203    /// Binds `stack` to the current OS thread until the returned guard is dropped.
1204    pub fn bind_scope_stack_thread<'a>(
1205        &'a self,
1206        stack: &'a ScopeStack<'a>,
1207    ) -> Result<ThreadScopeStackGuard<'a>> {
1208        let previous = self.capture_scope_stack_thread()?;
1209        let status = stack.set_thread();
1210        if status == NemoRelayStatus::Ok {
1211            Ok(ThreadScopeStackGuard {
1212                previous: Some(previous),
1213            })
1214        } else {
1215            let _ = previous.restore();
1216            Err(format!("scope_stack_set_thread failed: {status:?}"))
1217        }
1218    }
1219}
1220
1221impl From<ScopeType> for NemoRelayNativeScopeType {
1222    fn from(value: ScopeType) -> Self {
1223        match value {
1224            ScopeType::Agent => Self::Agent,
1225            ScopeType::Function => Self::Function,
1226            ScopeType::Tool => Self::Tool,
1227            ScopeType::Llm => Self::Llm,
1228            ScopeType::Retriever => Self::Retriever,
1229            ScopeType::Embedder => Self::Embedder,
1230            ScopeType::Reranker => Self::Reranker,
1231            ScopeType::Guardrail => Self::Guardrail,
1232            ScopeType::Evaluator => Self::Evaluator,
1233            ScopeType::Custom => Self::Custom,
1234            ScopeType::Unknown => Self::Unknown,
1235        }
1236    }
1237}
1238
1239/// RAII guard for a host scope opened by [`PluginRuntime::scope`].
1240pub struct ScopeGuard<'a> {
1241    runtime: &'a PluginRuntime,
1242    handle: Option<ScopeHandle<'a>>,
1243}
1244
1245impl<'a> ScopeGuard<'a> {
1246    /// Returns the active scope handle.
1247    pub fn handle(&self) -> Option<&ScopeHandle<'a>> {
1248        self.handle.as_ref()
1249    }
1250
1251    /// Pops the scope with optional output and metadata.
1252    pub fn close(&mut self, output: Option<&Json>, metadata: Option<&Json>) -> Result<()> {
1253        let Some(handle) = self.handle.as_ref() else {
1254            return Ok(());
1255        };
1256        self.runtime.pop_scope(handle, output, metadata)?;
1257        self.handle.take();
1258        Ok(())
1259    }
1260}
1261
1262impl Drop for ScopeGuard<'_> {
1263    fn drop(&mut self) {
1264        if let Some(handle) = self.handle.take() {
1265            let _ = self.runtime.pop_scope(&handle, None, None);
1266        }
1267    }
1268}
1269
1270/// RAII guard that restores the previous thread-local scope stack on drop.
1271pub struct ThreadScopeStackGuard<'a> {
1272    previous: Option<ScopeStackBinding<'a>>,
1273}
1274
1275impl ThreadScopeStackGuard<'_> {
1276    /// Restores the previous thread-local scope stack immediately.
1277    pub fn restore(mut self) -> Result<()> {
1278        let Some(previous) = self.previous.take() else {
1279            return Ok(());
1280        };
1281        let status = previous.restore();
1282        if status == NemoRelayStatus::Ok {
1283            Ok(())
1284        } else {
1285            Err(format!("scope_stack_restore_thread failed: {status:?}"))
1286        }
1287    }
1288}
1289
1290impl Drop for ThreadScopeStackGuard<'_> {
1291    fn drop(&mut self) {
1292        if let Some(previous) = self.previous.take() {
1293            let _ = previous.restore();
1294        }
1295    }
1296}
1297
1298/// Typed continuation passed to tool execution intercepts.
1299pub struct ToolNext<'a> {
1300    host: &'a NemoRelayNativeHostApiV1,
1301    next_fn: NemoRelayNativeToolNextFn,
1302    next_ctx: *mut c_void,
1303}
1304
1305impl ToolNext<'_> {
1306    /// Continues the tool execution chain with replacement arguments.
1307    pub fn call(&self, args: Json) -> Result<Json> {
1308        let args = HostString::from_json(self.host, &args)
1309            .ok_or_else(|| "failed to allocate tool next args".to_string())?;
1310        let mut out = ptr::null_mut();
1311        let status = unsafe { (self.next_fn)(args.as_ptr(), self.next_ctx, &mut out) };
1312        if status != NemoRelayStatus::Ok {
1313            return Err(format!("tool next failed: {status:?}"));
1314        }
1315        if out.is_null() {
1316            return Err("tool next returned null output".into());
1317        }
1318        let result = read_json_value(self.host, out, "tool next result");
1319        unsafe { (self.host.string_free)(out) };
1320        result.map_err(|status| format!("tool next returned invalid JSON: {status:?}"))
1321    }
1322}
1323
1324/// Typed continuation passed to LLM execution intercepts.
1325pub struct LlmNext<'a> {
1326    host: &'a NemoRelayNativeHostApiV1,
1327    next_fn: NemoRelayNativeLlmNextFn,
1328    next_ctx: *mut c_void,
1329}
1330
1331impl LlmNext<'_> {
1332    /// Continues the LLM execution chain with a replacement request.
1333    pub fn call(&self, request: LlmRequest) -> Result<Json> {
1334        let request = HostString::from_json(self.host, &request)
1335            .ok_or_else(|| "failed to allocate LLM next request".to_string())?;
1336        let mut out = ptr::null_mut();
1337        let status = unsafe { (self.next_fn)(request.as_ptr(), self.next_ctx, &mut out) };
1338        if status != NemoRelayStatus::Ok {
1339            return Err(format!("llm next failed: {status:?}"));
1340        }
1341        if out.is_null() {
1342            return Err("llm next returned null output".into());
1343        }
1344        let result = read_json_value(self.host, out, "llm next result");
1345        unsafe { (self.host.string_free)(out) };
1346        result.map_err(|status| format!("llm next returned invalid JSON: {status:?}"))
1347    }
1348}
1349
1350/// Typed continuation passed to LLM stream execution intercepts.
1351pub struct LlmStreamNext<'a> {
1352    host: &'a NemoRelayNativeHostApiV1,
1353    next_fn: NemoRelayNativeLlmStreamNextFn,
1354    next_ctx: *mut c_void,
1355}
1356
1357impl LlmStreamNext<'_> {
1358    /// Continues the LLM stream execution chain with a replacement request.
1359    pub fn call(&self, request: LlmRequest) -> Result<LlmStream> {
1360        let request = HostString::from_json(self.host, &request)
1361            .ok_or_else(|| "failed to allocate LLM stream next request".to_string())?;
1362        let mut raw = NemoRelayNativeLlmStreamV1::default();
1363        let status = unsafe { (self.next_fn)(request.as_ptr(), self.next_ctx, &mut raw) };
1364        if status != NemoRelayStatus::Ok {
1365            return Err(format!("llm stream next failed: {status:?}"));
1366        }
1367        unsafe { LlmStream::from_raw(self.host, raw) }
1368    }
1369}
1370
1371/// Host- or plugin-owned stream returned across the native LLM stream ABI.
1372pub struct LlmStream {
1373    host: NemoRelayNativeHostApiV1,
1374    raw: NemoRelayNativeLlmStreamV1,
1375    finished: bool,
1376}
1377
1378// The host ABI table is Send, and stream ownership is exclusive through this wrapper.
1379unsafe impl Send for LlmStream {}
1380
1381impl LlmStream {
1382    /// Creates a typed stream wrapper from a raw stream table.
1383    ///
1384    /// # Safety
1385    /// `raw` must contain callbacks and `user_data` produced by the same host
1386    /// and must not be used again after it is moved into this wrapper.
1387    pub unsafe fn from_raw(
1388        host: &NemoRelayNativeHostApiV1,
1389        mut raw: NemoRelayNativeLlmStreamV1,
1390    ) -> Result<Self> {
1391        let expected_size = std::mem::size_of::<NemoRelayNativeLlmStreamV1>();
1392        if raw.struct_size != expected_size {
1393            if raw.struct_size >= expected_size {
1394                unsafe { drop_raw_llm_stream(&mut raw) };
1395            }
1396            return Err(format!(
1397                "unsupported LLM stream struct size: {}",
1398                raw.struct_size
1399            ));
1400        }
1401        if raw.next.is_none() {
1402            unsafe { drop_raw_llm_stream(&mut raw) };
1403            return Err("LLM stream next callback was null".into());
1404        }
1405        Ok(Self {
1406            host: *host,
1407            raw,
1408            finished: false,
1409        })
1410    }
1411
1412    /// Polls the next stream chunk.
1413    pub fn next_chunk(&mut self) -> Result<Option<Json>> {
1414        if self.finished {
1415            return Ok(None);
1416        }
1417        let next = self
1418            .raw
1419            .next
1420            .expect("LLM stream next callback is validated on construction");
1421        let mut out = ptr::null_mut();
1422        let status = unsafe { next(self.raw.user_data, &mut out) };
1423        match status {
1424            NemoRelayStatus::Ok => {
1425                if out.is_null() {
1426                    self.finished = true;
1427                    return Err("LLM stream returned null chunk".into());
1428                }
1429                let result = read_json_value(&self.host, out, "LLM stream chunk");
1430                unsafe { (self.host.string_free)(out) };
1431                match result {
1432                    Ok(chunk) => Ok(Some(chunk)),
1433                    Err(status) => {
1434                        self.finished = true;
1435                        Err(format!("LLM stream returned invalid JSON: {status:?}"))
1436                    }
1437                }
1438            }
1439            NemoRelayStatus::StreamEnd => {
1440                if !out.is_null() {
1441                    unsafe { (self.host.string_free)(out) };
1442                }
1443                self.finished = true;
1444                Ok(None)
1445            }
1446            other => {
1447                if !out.is_null() {
1448                    unsafe { (self.host.string_free)(out) };
1449                }
1450                self.finished = true;
1451                Err(format!("LLM stream failed: {other:?}"))
1452            }
1453        }
1454    }
1455
1456    /// Cancels the stream if it has not reached end-of-stream.
1457    pub fn cancel(&mut self) -> Result<()> {
1458        if self.finished {
1459            return Ok(());
1460        }
1461        if let Some(cancel) = self.raw.cancel {
1462            let status = unsafe { cancel(self.raw.user_data) };
1463            if status != NemoRelayStatus::Ok {
1464                return Err(format!("LLM stream cancel failed: {status:?}"));
1465            }
1466        }
1467        self.finished = true;
1468        Ok(())
1469    }
1470}
1471
1472impl Iterator for LlmStream {
1473    type Item = Result<Json>;
1474
1475    fn next(&mut self) -> Option<Self::Item> {
1476        match self.next_chunk() {
1477            Ok(Some(chunk)) => Some(Ok(chunk)),
1478            Ok(None) => None,
1479            Err(message) => Some(Err(message)),
1480        }
1481    }
1482}
1483
1484unsafe fn drop_raw_llm_stream(raw: &mut NemoRelayNativeLlmStreamV1) {
1485    if let Some(drop_fn) = raw.drop.take() {
1486        unsafe { drop_fn(raw.user_data) };
1487    }
1488    raw.user_data = ptr::null_mut();
1489}
1490
1491impl Drop for LlmStream {
1492    fn drop(&mut self) {
1493        if !self.finished {
1494            if let Some(cancel) = self.raw.cancel {
1495                let _ = unsafe { cancel(self.raw.user_data) };
1496            }
1497            self.finished = true;
1498        }
1499        unsafe { drop_raw_llm_stream(&mut self.raw) };
1500    }
1501}
1502
1503/// Host-owned scope handle returned by native scope APIs.
1504pub struct ScopeHandle<'a> {
1505    host: &'a NemoRelayNativeHostApiV1,
1506    ptr: *mut NemoRelayNativeScopeHandle,
1507}
1508
1509impl<'a> ScopeHandle<'a> {
1510    /// Returns the raw ABI pointer.
1511    pub fn as_ptr(&self) -> *const NemoRelayNativeScopeHandle {
1512        self.ptr
1513    }
1514}
1515
1516impl Drop for ScopeHandle<'_> {
1517    fn drop(&mut self) {
1518        unsafe { (self.host.scope_handle_free)(self.ptr) };
1519    }
1520}
1521
1522/// Host-owned isolated scope stack returned by native scope-stack APIs.
1523pub struct ScopeStack<'a> {
1524    host: &'a NemoRelayNativeHostApiV1,
1525    ptr: *mut NemoRelayNativeScopeStack,
1526}
1527
1528impl<'a> ScopeStack<'a> {
1529    /// Returns the raw ABI pointer.
1530    pub fn as_ptr(&self) -> *const NemoRelayNativeScopeStack {
1531        self.ptr
1532    }
1533
1534    fn set_thread(&self) -> NemoRelayStatus {
1535        unsafe { (self.host.scope_stack_set_thread)(self.ptr) }
1536    }
1537
1538    /// Executes `f` while this stack is visible to host runtime APIs.
1539    pub fn with_current<F>(&self, f: F) -> Result<()>
1540    where
1541        F: FnOnce() -> Result<()>,
1542    {
1543        struct State<F> {
1544            f: Option<F>,
1545            error: Option<String>,
1546        }
1547
1548        unsafe extern "C" fn trampoline<F>(user_data: *mut c_void) -> NemoRelayStatus
1549        where
1550            F: FnOnce() -> Result<()>,
1551        {
1552            if user_data.is_null() {
1553                return NemoRelayStatus::NullPointer;
1554            }
1555            let state = unsafe { &mut *(user_data as *mut State<F>) };
1556            let result = catch_unwind(AssertUnwindSafe(|| {
1557                let Some(f) = state.f.take() else {
1558                    return Err("scope-stack callback was already consumed".to_string());
1559                };
1560                f()
1561            }));
1562            match result {
1563                Ok(Ok(())) => NemoRelayStatus::Ok,
1564                Ok(Err(message)) => {
1565                    state.error = Some(message);
1566                    NemoRelayStatus::Internal
1567                }
1568                Err(_) => {
1569                    state.error = Some("scope-stack callback panicked".into());
1570                    NemoRelayStatus::Internal
1571                }
1572            }
1573        }
1574
1575        let mut state = State {
1576            f: Some(f),
1577            error: None,
1578        };
1579        let status = unsafe {
1580            (self.host.scope_stack_with_current)(
1581                self.ptr,
1582                trampoline::<F>,
1583                (&mut state as *mut State<_>).cast(),
1584            )
1585        };
1586        if status == NemoRelayStatus::Ok {
1587            Ok(())
1588        } else {
1589            Err(state
1590                .error
1591                .unwrap_or_else(|| format!("scope_stack_with_current failed: {status:?}")))
1592        }
1593    }
1594}
1595
1596impl Drop for ScopeStack<'_> {
1597    fn drop(&mut self) {
1598        unsafe { (self.host.scope_stack_free)(self.ptr) };
1599    }
1600}
1601
1602/// Captured thread-local scope-stack binding.
1603pub struct ScopeStackBinding<'a> {
1604    host: &'a NemoRelayNativeHostApiV1,
1605    ptr: *mut NemoRelayNativeScopeStackBinding,
1606}
1607
1608impl<'a> ScopeStackBinding<'a> {
1609    /// Restores and consumes this binding.
1610    pub fn restore(mut self) -> NemoRelayStatus {
1611        let ptr = std::mem::replace(&mut self.ptr, ptr::null_mut());
1612        unsafe { (self.host.scope_stack_restore_thread)(ptr) }
1613    }
1614}
1615
1616impl Drop for ScopeStackBinding<'_> {
1617    fn drop(&mut self) {
1618        if !self.ptr.is_null() {
1619            unsafe { (self.host.scope_stack_binding_free)(self.ptr) };
1620        }
1621    }
1622}
1623
1624/// Retrieves the current scope handle.
1625pub fn current_scope(host: &NemoRelayNativeHostApiV1) -> Result<ScopeHandle<'_>> {
1626    let mut out = ptr::null_mut();
1627    let status = unsafe { (host.scope_get_current)(&mut out) };
1628    if status == NemoRelayStatus::Ok && !out.is_null() {
1629        Ok(ScopeHandle { host, ptr: out })
1630    } else {
1631        Err(format!("scope_get_current failed: {status:?}"))
1632    }
1633}
1634
1635/// Pushes a scope and emits its start event.
1636pub fn push_scope<'a>(
1637    host: &'a NemoRelayNativeHostApiV1,
1638    name: &str,
1639    scope_type: NemoRelayNativeScopeType,
1640    data: Option<&Json>,
1641    metadata: Option<&Json>,
1642    input: Option<&Json>,
1643) -> Result<ScopeHandle<'a>> {
1644    let name =
1645        HostString::new(host, name).ok_or_else(|| "failed to allocate scope name".to_string())?;
1646    let data = OptionalHostJson::new(host, data)?;
1647    let metadata = OptionalHostJson::new(host, metadata)?;
1648    let input = OptionalHostJson::new(host, input)?;
1649    let mut out = ptr::null_mut();
1650    let status = unsafe {
1651        (host.scope_push)(
1652            name.as_ptr(),
1653            scope_type,
1654            ptr::null(),
1655            0,
1656            data.as_ptr(),
1657            metadata.as_ptr(),
1658            input.as_ptr(),
1659            ptr::null(),
1660            &mut out,
1661        )
1662    };
1663    if status == NemoRelayStatus::Ok && !out.is_null() {
1664        Ok(ScopeHandle { host, ptr: out })
1665    } else {
1666        Err(format!("scope_push failed: {status:?}"))
1667    }
1668}
1669
1670/// Pops a scope and emits its end event.
1671pub fn pop_scope(
1672    host: &NemoRelayNativeHostApiV1,
1673    handle: &ScopeHandle<'_>,
1674    output: Option<&Json>,
1675    metadata: Option<&Json>,
1676) -> Result<()> {
1677    let output = OptionalHostJson::new(host, output)?;
1678    let metadata = OptionalHostJson::new(host, metadata)?;
1679    let status = unsafe {
1680        (host.scope_pop)(
1681            handle.as_ptr(),
1682            output.as_ptr(),
1683            metadata.as_ptr(),
1684            ptr::null(),
1685        )
1686    };
1687    if status == NemoRelayStatus::Ok {
1688        Ok(())
1689    } else {
1690        Err(format!("scope_pop failed: {status:?}"))
1691    }
1692}
1693
1694/// Emits a mark event under the current scope.
1695pub fn emit_mark(
1696    host: &NemoRelayNativeHostApiV1,
1697    name: &str,
1698    data: Option<&Json>,
1699    metadata: Option<&Json>,
1700) -> Result<()> {
1701    let name =
1702        HostString::new(host, name).ok_or_else(|| "failed to allocate mark name".to_string())?;
1703    let data = OptionalHostJson::new(host, data)?;
1704    let metadata = OptionalHostJson::new(host, metadata)?;
1705    let status = unsafe {
1706        (host.emit_mark)(
1707            name.as_ptr(),
1708            ptr::null(),
1709            data.as_ptr(),
1710            metadata.as_ptr(),
1711            ptr::null(),
1712        )
1713    };
1714    if status == NemoRelayStatus::Ok {
1715        Ok(())
1716    } else {
1717        Err(format!("emit_mark failed: {status:?}"))
1718    }
1719}
1720
1721/// Creates a new independent scope stack.
1722pub fn create_scope_stack(host: &NemoRelayNativeHostApiV1) -> Result<ScopeStack<'_>> {
1723    let mut out = ptr::null_mut();
1724    let status = unsafe { (host.scope_stack_create)(&mut out) };
1725    if status == NemoRelayStatus::Ok && !out.is_null() {
1726        Ok(ScopeStack { host, ptr: out })
1727    } else {
1728        Err(format!("scope_stack_create failed: {status:?}"))
1729    }
1730}
1731
1732/// Captures the current thread-local scope-stack binding.
1733pub fn capture_scope_stack_thread(
1734    host: &NemoRelayNativeHostApiV1,
1735) -> Result<ScopeStackBinding<'_>> {
1736    let mut out = ptr::null_mut();
1737    let status = unsafe { (host.scope_stack_capture_thread)(&mut out) };
1738    if status == NemoRelayStatus::Ok && !out.is_null() {
1739        Ok(ScopeStackBinding { host, ptr: out })
1740    } else {
1741        Err(format!("scope_stack_capture_thread failed: {status:?}"))
1742    }
1743}
1744
1745/// Trait implemented by Rust native plugins.
1746pub trait NativePlugin: Send + 'static {
1747    /// Returns the stable plugin kind.
1748    fn plugin_kind(&self) -> &str;
1749
1750    /// Returns whether the plugin allows multiple configured components.
1751    fn allows_multiple_components(&self) -> bool {
1752        true
1753    }
1754
1755    /// Validates one component-local JSON config object.
1756    fn validate(&self, _plugin_config: &Map<String, Json>) -> Vec<ConfigDiagnostic> {
1757        vec![]
1758    }
1759
1760    /// Registers runtime behavior through the component-scoped plugin context.
1761    fn register(
1762        &mut self,
1763        plugin_config: &Map<String, Json>,
1764        ctx: &mut PluginContext<'_>,
1765    ) -> Result<()>;
1766}
1767
1768/// Borrowed safe wrapper around a host plugin registration context.
1769pub struct PluginContext<'a> {
1770    host: &'a NemoRelayNativeHostApiV1,
1771    raw: *mut NemoRelayNativePluginContext,
1772}
1773
1774#[allow(clippy::not_unsafe_ptr_arg_deref)]
1775impl<'a> PluginContext<'a> {
1776    /// Creates a plugin context wrapper from raw ABI parts.
1777    ///
1778    /// # Safety
1779    /// `host` and `raw` must remain valid for the lifetime of this wrapper.
1780    pub unsafe fn from_raw(
1781        host: &'a NemoRelayNativeHostApiV1,
1782        raw: *mut NemoRelayNativePluginContext,
1783    ) -> Self {
1784        Self { host, raw }
1785    }
1786
1787    /// Returns the host ABI table backing this registration context.
1788    pub fn host_api(&self) -> &'a NemoRelayNativeHostApiV1 {
1789        self.host
1790    }
1791
1792    /// Returns a cloneable high-level runtime handle.
1793    pub fn runtime(&self) -> PluginRuntime {
1794        PluginRuntime::new(self.host)
1795    }
1796
1797    /// Registers a typed event subscriber callback.
1798    pub fn register_subscriber<F>(&mut self, name: &str, callback: F) -> Result<()>
1799    where
1800        F: Fn(&Event) + Send + Sync + 'static,
1801    {
1802        let user_data = typed_callback_user_data(self.host, callback);
1803        let status = unsafe {
1804            self.register_subscriber_raw(
1805                name,
1806                typed_subscriber_trampoline::<F>,
1807                user_data,
1808                Some(drop_typed_callback::<F>),
1809            )
1810        };
1811        finish_typed_registration::<F>(self.host, status, user_data, "subscriber")
1812    }
1813
1814    fn register_event_sanitizer<F>(
1815        &mut self,
1816        name: &str,
1817        priority: i32,
1818        callback: F,
1819        register: unsafe extern "C" fn(
1820            *mut NemoRelayNativePluginContext,
1821            *const NemoRelayNativeString,
1822            i32,
1823            NemoRelayNativeEventSanitizeCb,
1824            *mut c_void,
1825            NemoRelayNativeFreeFn,
1826        ) -> NemoRelayStatus,
1827        label: &str,
1828    ) -> Result<()>
1829    where
1830        F: Fn(&Event, EventSanitizeFields) -> EventSanitizeFields + Send + Sync + 'static,
1831    {
1832        let user_data = typed_callback_user_data(self.host, callback);
1833        let status = self.with_name(name, |_, name| unsafe {
1834            register(
1835                self.raw,
1836                name,
1837                priority,
1838                typed_event_sanitize_trampoline::<F>,
1839                user_data,
1840                Some(drop_typed_callback::<F>),
1841            )
1842        });
1843        finish_typed_registration::<F>(self.host, status, user_data, label)
1844    }
1845
1846    /// Registers a typed mark event sanitizer.
1847    pub fn register_mark_sanitize_guardrail<F>(
1848        &mut self,
1849        name: &str,
1850        priority: i32,
1851        callback: F,
1852    ) -> Result<()>
1853    where
1854        F: Fn(&Event, EventSanitizeFields) -> EventSanitizeFields + Send + Sync + 'static,
1855    {
1856        self.register_event_sanitizer(
1857            name,
1858            priority,
1859            callback,
1860            self.host.plugin_context_register_mark_sanitize_guardrail,
1861            "mark sanitize guardrail",
1862        )
1863    }
1864
1865    /// Registers a typed scope-start event sanitizer.
1866    pub fn register_scope_sanitize_start_guardrail<F>(
1867        &mut self,
1868        name: &str,
1869        priority: i32,
1870        callback: F,
1871    ) -> Result<()>
1872    where
1873        F: Fn(&Event, EventSanitizeFields) -> EventSanitizeFields + Send + Sync + 'static,
1874    {
1875        self.register_event_sanitizer(
1876            name,
1877            priority,
1878            callback,
1879            self.host
1880                .plugin_context_register_scope_sanitize_start_guardrail,
1881            "scope-start sanitize guardrail",
1882        )
1883    }
1884
1885    /// Registers a typed scope-end event sanitizer.
1886    pub fn register_scope_sanitize_end_guardrail<F>(
1887        &mut self,
1888        name: &str,
1889        priority: i32,
1890        callback: F,
1891    ) -> Result<()>
1892    where
1893        F: Fn(&Event, EventSanitizeFields) -> EventSanitizeFields + Send + Sync + 'static,
1894    {
1895        self.register_event_sanitizer(
1896            name,
1897            priority,
1898            callback,
1899            self.host
1900                .plugin_context_register_scope_sanitize_end_guardrail,
1901            "scope-end sanitize guardrail",
1902        )
1903    }
1904
1905    /// Registers a typed tool sanitize-request guardrail.
1906    pub fn register_tool_sanitize_request_guardrail<F>(
1907        &mut self,
1908        name: &str,
1909        priority: i32,
1910        callback: F,
1911    ) -> Result<()>
1912    where
1913        F: Fn(&str, Json) -> Json + Send + Sync + 'static,
1914    {
1915        let user_data = typed_callback_user_data(self.host, callback);
1916        let status = unsafe {
1917            self.register_tool_sanitize_request_guardrail_raw(
1918                name,
1919                priority,
1920                typed_tool_sanitize_trampoline::<F>,
1921                user_data,
1922                Some(drop_typed_callback::<F>),
1923            )
1924        };
1925        finish_typed_registration::<F>(
1926            self.host,
1927            status,
1928            user_data,
1929            "tool sanitize request guardrail",
1930        )
1931    }
1932
1933    /// Registers a typed tool sanitize-response guardrail.
1934    pub fn register_tool_sanitize_response_guardrail<F>(
1935        &mut self,
1936        name: &str,
1937        priority: i32,
1938        callback: F,
1939    ) -> Result<()>
1940    where
1941        F: Fn(&str, Json) -> Json + Send + Sync + 'static,
1942    {
1943        let user_data = typed_callback_user_data(self.host, callback);
1944        let status = unsafe {
1945            self.register_tool_sanitize_response_guardrail_raw(
1946                name,
1947                priority,
1948                typed_tool_sanitize_trampoline::<F>,
1949                user_data,
1950                Some(drop_typed_callback::<F>),
1951            )
1952        };
1953        finish_typed_registration::<F>(
1954            self.host,
1955            status,
1956            user_data,
1957            "tool sanitize response guardrail",
1958        )
1959    }
1960
1961    /// Registers a typed tool conditional-execution guardrail.
1962    pub fn register_tool_conditional_execution_guardrail<F>(
1963        &mut self,
1964        name: &str,
1965        priority: i32,
1966        callback: F,
1967    ) -> Result<()>
1968    where
1969        F: Fn(&str, &Json) -> Result<Option<String>> + Send + Sync + 'static,
1970    {
1971        let user_data = typed_callback_user_data(self.host, callback);
1972        let status = unsafe {
1973            self.register_tool_conditional_execution_guardrail_raw(
1974                name,
1975                priority,
1976                typed_tool_conditional_trampoline::<F>,
1977                user_data,
1978                Some(drop_typed_callback::<F>),
1979            )
1980        };
1981        finish_typed_registration::<F>(
1982            self.host,
1983            status,
1984            user_data,
1985            "tool conditional execution guardrail",
1986        )
1987    }
1988
1989    /// Registers a typed tool request intercept.
1990    pub fn register_tool_request_intercept<F>(
1991        &mut self,
1992        name: &str,
1993        priority: i32,
1994        break_chain: bool,
1995        callback: F,
1996    ) -> Result<()>
1997    where
1998        F: Fn(&str, Json) -> Result<Json> + Send + Sync + 'static,
1999    {
2000        let user_data = typed_callback_user_data(self.host, callback);
2001        let status = unsafe {
2002            self.register_tool_request_intercept_raw(
2003                name,
2004                priority,
2005                break_chain,
2006                typed_tool_intercept_trampoline::<F>,
2007                user_data,
2008                Some(drop_typed_callback::<F>),
2009            )
2010        };
2011        finish_typed_registration::<F>(self.host, status, user_data, "tool request intercept")
2012    }
2013
2014    /// Registers a typed tool execution intercept.
2015    ///
2016    /// The callback returns a [`ToolExecutionInterceptOutcome`]. Calling
2017    /// [`ToolNext::call`] continues the chain and returns only the raw
2018    /// downstream result JSON; Relay retains downstream pending marks.
2019    pub fn register_tool_execution_intercept<F>(
2020        &mut self,
2021        name: &str,
2022        priority: i32,
2023        callback: F,
2024    ) -> Result<()>
2025    where
2026        F: for<'next> Fn(&str, Json, ToolNext<'next>) -> Result<ToolExecutionInterceptOutcome>
2027            + Send
2028            + Sync
2029            + 'static,
2030    {
2031        let user_data = typed_callback_user_data(self.host, callback);
2032        let status = unsafe {
2033            self.register_tool_execution_intercept_raw(
2034                name,
2035                priority,
2036                typed_tool_execution_trampoline::<F>,
2037                user_data,
2038                Some(drop_typed_callback::<F>),
2039            )
2040        };
2041        finish_typed_registration::<F>(self.host, status, user_data, "tool execution intercept")
2042    }
2043
2044    /// Registers a typed LLM sanitize-request guardrail.
2045    pub fn register_llm_sanitize_request_guardrail<F>(
2046        &mut self,
2047        name: &str,
2048        priority: i32,
2049        callback: F,
2050    ) -> Result<()>
2051    where
2052        F: for<'ctx> Fn(LlmRequest, LlmSanitizeRequestContext<'ctx>) -> Option<LlmRequest>
2053            + Send
2054            + Sync
2055            + 'static,
2056    {
2057        let user_data = typed_callback_user_data(self.host, callback);
2058        let status = unsafe {
2059            self.register_llm_sanitize_request_guardrail_raw(
2060                name,
2061                priority,
2062                typed_llm_sanitize_request_trampoline::<F>,
2063                user_data,
2064                Some(drop_typed_callback::<F>),
2065            )
2066        };
2067        finish_typed_registration::<F>(
2068            self.host,
2069            status,
2070            user_data,
2071            "llm sanitize request guardrail",
2072        )
2073    }
2074
2075    /// Registers a typed LLM sanitize-response guardrail.
2076    pub fn register_llm_sanitize_response_guardrail<F>(
2077        &mut self,
2078        name: &str,
2079        priority: i32,
2080        callback: F,
2081    ) -> Result<()>
2082    where
2083        F: for<'ctx> Fn(Json, LlmSanitizeResponseContext<'ctx>) -> Option<Json>
2084            + Send
2085            + Sync
2086            + 'static,
2087    {
2088        let user_data = typed_callback_user_data(self.host, callback);
2089        let status = unsafe {
2090            self.register_llm_sanitize_response_guardrail_raw(
2091                name,
2092                priority,
2093                typed_llm_sanitize_response_trampoline::<F>,
2094                user_data,
2095                Some(drop_typed_callback::<F>),
2096            )
2097        };
2098        finish_typed_registration::<F>(
2099            self.host,
2100            status,
2101            user_data,
2102            "llm sanitize response guardrail",
2103        )
2104    }
2105
2106    /// Registers a typed LLM conditional-execution guardrail.
2107    pub fn register_llm_conditional_execution_guardrail<F>(
2108        &mut self,
2109        name: &str,
2110        priority: i32,
2111        callback: F,
2112    ) -> Result<()>
2113    where
2114        F: Fn(&LlmRequest) -> Result<Option<String>> + Send + Sync + 'static,
2115    {
2116        let user_data = typed_callback_user_data(self.host, callback);
2117        let status = unsafe {
2118            self.register_llm_conditional_execution_guardrail_raw(
2119                name,
2120                priority,
2121                typed_llm_conditional_trampoline::<F>,
2122                user_data,
2123                Some(drop_typed_callback::<F>),
2124            )
2125        };
2126        finish_typed_registration::<F>(
2127            self.host,
2128            status,
2129            user_data,
2130            "llm conditional execution guardrail",
2131        )
2132    }
2133
2134    /// Registers a typed LLM request intercept.
2135    pub fn register_llm_request_intercept<F>(
2136        &mut self,
2137        name: &str,
2138        priority: i32,
2139        break_chain: bool,
2140        callback: F,
2141    ) -> Result<()>
2142    where
2143        F: Fn(&str, LlmRequest, Option<AnnotatedLlmRequest>) -> Result<LlmRequestInterceptOutcome>
2144            + Send
2145            + Sync
2146            + 'static,
2147    {
2148        let user_data = typed_callback_user_data(self.host, callback);
2149        let status = unsafe {
2150            self.register_llm_request_intercept_raw(
2151                name,
2152                priority,
2153                break_chain,
2154                typed_llm_request_intercept_trampoline::<F>,
2155                user_data,
2156                Some(drop_typed_callback::<F>),
2157            )
2158        };
2159        finish_typed_registration::<F>(self.host, status, user_data, "llm request intercept")
2160    }
2161
2162    /// Registers a typed LLM execution intercept.
2163    pub fn register_llm_execution_intercept<F>(
2164        &mut self,
2165        name: &str,
2166        priority: i32,
2167        callback: F,
2168    ) -> Result<()>
2169    where
2170        F: for<'next> Fn(&str, LlmRequest, LlmNext<'next>) -> Result<Json> + Send + Sync + 'static,
2171    {
2172        let user_data = typed_callback_user_data(self.host, callback);
2173        let status = unsafe {
2174            self.register_llm_execution_intercept_raw(
2175                name,
2176                priority,
2177                typed_llm_execution_trampoline::<F>,
2178                user_data,
2179                Some(drop_typed_callback::<F>),
2180            )
2181        };
2182        finish_typed_registration::<F>(self.host, status, user_data, "llm execution intercept")
2183    }
2184
2185    /// Registers a typed LLM stream execution intercept.
2186    ///
2187    /// Native ABI v2 represents stream execution as one JSON result. The host
2188    /// wraps that result as a one-chunk stream.
2189    pub fn register_llm_stream_execution_intercept<F>(
2190        &mut self,
2191        name: &str,
2192        priority: i32,
2193        callback: F,
2194    ) -> Result<()>
2195    where
2196        F: for<'next> Fn(&str, LlmRequest, LlmStreamNext<'next>) -> Result<LlmJsonStream>
2197            + Send
2198            + Sync
2199            + 'static,
2200    {
2201        let user_data = typed_callback_user_data(self.host, callback);
2202        let status = unsafe {
2203            self.register_llm_stream_execution_intercept_raw(
2204                name,
2205                priority,
2206                typed_llm_stream_execution_trampoline::<F>,
2207                user_data,
2208                Some(drop_typed_callback::<F>),
2209            )
2210        };
2211        finish_typed_registration::<F>(
2212            self.host,
2213            status,
2214            user_data,
2215            "llm stream execution intercept",
2216        )
2217    }
2218
2219    /// Registers a raw event subscriber callback.
2220    ///
2221    /// # Safety
2222    /// `cb`, `user_data`, and `free_fn` must remain valid for every host
2223    /// callback invocation until the host deregisters the callback or calls
2224    /// `free_fn`. `free_fn` must match the allocation behind `user_data`.
2225    pub unsafe fn register_subscriber_raw(
2226        &mut self,
2227        name: &str,
2228        cb: NemoRelayNativeEventSubscriberCb,
2229        user_data: *mut c_void,
2230        free_fn: NemoRelayNativeFreeFn,
2231    ) -> NemoRelayStatus {
2232        self.with_name(name, |host, name| unsafe {
2233            (host.plugin_context_register_subscriber)(self.raw, name, cb, user_data, free_fn)
2234        })
2235    }
2236
2237    /// Registers a raw mark event sanitizer callback.
2238    ///
2239    /// # Safety
2240    /// `cb`, `user_data`, and `free_fn` must remain valid for every host
2241    /// callback invocation until the host deregisters the callback or calls
2242    /// `free_fn`. `free_fn` must match the allocation behind `user_data`.
2243    pub unsafe fn register_mark_sanitize_guardrail_raw(
2244        &mut self,
2245        name: &str,
2246        priority: i32,
2247        cb: NemoRelayNativeEventSanitizeCb,
2248        user_data: *mut c_void,
2249        free_fn: NemoRelayNativeFreeFn,
2250    ) -> NemoRelayStatus {
2251        self.with_name(name, |host, name| unsafe {
2252            (host.plugin_context_register_mark_sanitize_guardrail)(
2253                self.raw, name, priority, cb, user_data, free_fn,
2254            )
2255        })
2256    }
2257
2258    /// Registers a raw scope-start event sanitizer callback.
2259    ///
2260    /// # Safety
2261    /// `cb`, `user_data`, and `free_fn` must remain valid for every host
2262    /// callback invocation until the host deregisters the callback or calls
2263    /// `free_fn`. `free_fn` must match the allocation behind `user_data`.
2264    pub unsafe fn register_scope_sanitize_start_guardrail_raw(
2265        &mut self,
2266        name: &str,
2267        priority: i32,
2268        cb: NemoRelayNativeEventSanitizeCb,
2269        user_data: *mut c_void,
2270        free_fn: NemoRelayNativeFreeFn,
2271    ) -> NemoRelayStatus {
2272        self.with_name(name, |host, name| unsafe {
2273            (host.plugin_context_register_scope_sanitize_start_guardrail)(
2274                self.raw, name, priority, cb, user_data, free_fn,
2275            )
2276        })
2277    }
2278
2279    /// Registers a raw scope-end event sanitizer callback.
2280    ///
2281    /// # Safety
2282    /// `cb`, `user_data`, and `free_fn` must remain valid for every host
2283    /// callback invocation until the host deregisters the callback or calls
2284    /// `free_fn`. `free_fn` must match the allocation behind `user_data`.
2285    pub unsafe fn register_scope_sanitize_end_guardrail_raw(
2286        &mut self,
2287        name: &str,
2288        priority: i32,
2289        cb: NemoRelayNativeEventSanitizeCb,
2290        user_data: *mut c_void,
2291        free_fn: NemoRelayNativeFreeFn,
2292    ) -> NemoRelayStatus {
2293        self.with_name(name, |host, name| unsafe {
2294            (host.plugin_context_register_scope_sanitize_end_guardrail)(
2295                self.raw, name, priority, cb, user_data, free_fn,
2296            )
2297        })
2298    }
2299
2300    /// Registers a raw tool sanitize-request guardrail callback.
2301    ///
2302    /// # Safety
2303    /// `cb`, `user_data`, and `free_fn` must remain valid for every host
2304    /// callback invocation until the host deregisters the callback or calls
2305    /// `free_fn`. `free_fn` must match the allocation behind `user_data`.
2306    pub unsafe fn register_tool_sanitize_request_guardrail_raw(
2307        &mut self,
2308        name: &str,
2309        priority: i32,
2310        cb: NemoRelayNativeToolJsonCb,
2311        user_data: *mut c_void,
2312        free_fn: NemoRelayNativeFreeFn,
2313    ) -> NemoRelayStatus {
2314        self.with_name(name, |host, name| unsafe {
2315            (host.plugin_context_register_tool_sanitize_request_guardrail)(
2316                self.raw, name, priority, cb, user_data, free_fn,
2317            )
2318        })
2319    }
2320
2321    /// Registers a raw tool sanitize-response guardrail callback.
2322    ///
2323    /// # Safety
2324    /// `cb`, `user_data`, and `free_fn` must remain valid for every host
2325    /// callback invocation until the host deregisters the callback or calls
2326    /// `free_fn`. `free_fn` must match the allocation behind `user_data`.
2327    pub unsafe fn register_tool_sanitize_response_guardrail_raw(
2328        &mut self,
2329        name: &str,
2330        priority: i32,
2331        cb: NemoRelayNativeToolJsonCb,
2332        user_data: *mut c_void,
2333        free_fn: NemoRelayNativeFreeFn,
2334    ) -> NemoRelayStatus {
2335        self.with_name(name, |host, name| unsafe {
2336            (host.plugin_context_register_tool_sanitize_response_guardrail)(
2337                self.raw, name, priority, cb, user_data, free_fn,
2338            )
2339        })
2340    }
2341
2342    /// Registers a raw tool conditional-execution guardrail callback.
2343    ///
2344    /// # Safety
2345    /// `cb`, `user_data`, and `free_fn` must remain valid for every host
2346    /// callback invocation until the host deregisters the callback or calls
2347    /// `free_fn`. `free_fn` must match the allocation behind `user_data`.
2348    pub unsafe fn register_tool_conditional_execution_guardrail_raw(
2349        &mut self,
2350        name: &str,
2351        priority: i32,
2352        cb: NemoRelayNativeToolConditionalCb,
2353        user_data: *mut c_void,
2354        free_fn: NemoRelayNativeFreeFn,
2355    ) -> NemoRelayStatus {
2356        self.with_name(name, |host, name| unsafe {
2357            (host.plugin_context_register_tool_conditional_execution_guardrail)(
2358                self.raw, name, priority, cb, user_data, free_fn,
2359            )
2360        })
2361    }
2362
2363    /// Registers a raw tool request intercept callback.
2364    ///
2365    /// # Safety
2366    /// `cb`, `user_data`, and `free_fn` must remain valid for every host
2367    /// callback invocation until the host deregisters the callback or calls
2368    /// `free_fn`. `free_fn` must match the allocation behind `user_data`.
2369    pub unsafe fn register_tool_request_intercept_raw(
2370        &mut self,
2371        name: &str,
2372        priority: i32,
2373        break_chain: bool,
2374        cb: NemoRelayNativeToolJsonCb,
2375        user_data: *mut c_void,
2376        free_fn: NemoRelayNativeFreeFn,
2377    ) -> NemoRelayStatus {
2378        self.with_name(name, |host, name| unsafe {
2379            (host.plugin_context_register_tool_request_intercept)(
2380                self.raw,
2381                name,
2382                priority,
2383                break_chain,
2384                cb,
2385                user_data,
2386                free_fn,
2387            )
2388        })
2389    }
2390
2391    /// Registers a raw tool execution intercept callback.
2392    ///
2393    /// # Safety
2394    /// `cb`, `user_data`, and `free_fn` must remain valid for every host
2395    /// callback invocation until the host deregisters the callback or calls
2396    /// `free_fn`. `free_fn` must match the allocation behind `user_data`.
2397    pub unsafe fn register_tool_execution_intercept_raw(
2398        &mut self,
2399        name: &str,
2400        priority: i32,
2401        cb: NemoRelayNativeToolExecutionCb,
2402        user_data: *mut c_void,
2403        free_fn: NemoRelayNativeFreeFn,
2404    ) -> NemoRelayStatus {
2405        self.with_name(name, |host, name| unsafe {
2406            (host.plugin_context_register_tool_execution_intercept)(
2407                self.raw, name, priority, cb, user_data, free_fn,
2408            )
2409        })
2410    }
2411
2412    /// Registers a raw LLM sanitize-request guardrail callback.
2413    ///
2414    /// # Safety
2415    /// `cb`, `user_data`, and `free_fn` must remain valid for every host
2416    /// callback invocation until the host deregisters the callback or calls
2417    /// `free_fn`. `free_fn` must match the allocation behind `user_data`.
2418    pub unsafe fn register_llm_sanitize_request_guardrail_raw(
2419        &mut self,
2420        name: &str,
2421        priority: i32,
2422        cb: NemoRelayNativeLlmSanitizeRequestCb,
2423        user_data: *mut c_void,
2424        free_fn: NemoRelayNativeFreeFn,
2425    ) -> NemoRelayStatus {
2426        self.with_name(name, |host, name| unsafe {
2427            (host.plugin_context_register_llm_sanitize_request_guardrail)(
2428                self.raw, name, priority, cb, user_data, free_fn,
2429            )
2430        })
2431    }
2432
2433    /// Registers a raw LLM sanitize-response guardrail callback.
2434    ///
2435    /// # Safety
2436    /// `cb`, `user_data`, and `free_fn` must remain valid for every host
2437    /// callback invocation until the host deregisters the callback or calls
2438    /// `free_fn`. `free_fn` must match the allocation behind `user_data`.
2439    pub unsafe fn register_llm_sanitize_response_guardrail_raw(
2440        &mut self,
2441        name: &str,
2442        priority: i32,
2443        cb: NemoRelayNativeLlmSanitizeResponseCb,
2444        user_data: *mut c_void,
2445        free_fn: NemoRelayNativeFreeFn,
2446    ) -> NemoRelayStatus {
2447        self.with_name(name, |host, name| unsafe {
2448            (host.plugin_context_register_llm_sanitize_response_guardrail)(
2449                self.raw, name, priority, cb, user_data, free_fn,
2450            )
2451        })
2452    }
2453
2454    /// Registers a raw LLM conditional-execution guardrail callback.
2455    ///
2456    /// # Safety
2457    /// `cb`, `user_data`, and `free_fn` must remain valid for every host
2458    /// callback invocation until the host deregisters the callback or calls
2459    /// `free_fn`. `free_fn` must match the allocation behind `user_data`.
2460    pub unsafe fn register_llm_conditional_execution_guardrail_raw(
2461        &mut self,
2462        name: &str,
2463        priority: i32,
2464        cb: NemoRelayNativeLlmConditionalCb,
2465        user_data: *mut c_void,
2466        free_fn: NemoRelayNativeFreeFn,
2467    ) -> NemoRelayStatus {
2468        self.with_name(name, |host, name| unsafe {
2469            (host.plugin_context_register_llm_conditional_execution_guardrail)(
2470                self.raw, name, priority, cb, user_data, free_fn,
2471            )
2472        })
2473    }
2474
2475    /// Registers a raw LLM request intercept callback.
2476    ///
2477    /// # Safety
2478    /// `cb`, `user_data`, and `free_fn` must remain valid for every host
2479    /// callback invocation until the host deregisters the callback or calls
2480    /// `free_fn`. `free_fn` must match the allocation behind `user_data`.
2481    pub unsafe fn register_llm_request_intercept_raw(
2482        &mut self,
2483        name: &str,
2484        priority: i32,
2485        break_chain: bool,
2486        cb: NemoRelayNativeLlmRequestInterceptCb,
2487        user_data: *mut c_void,
2488        free_fn: NemoRelayNativeFreeFn,
2489    ) -> NemoRelayStatus {
2490        self.with_name(name, |host, name| unsafe {
2491            (host.plugin_context_register_llm_request_intercept)(
2492                self.raw,
2493                name,
2494                priority,
2495                break_chain,
2496                cb,
2497                user_data,
2498                free_fn,
2499            )
2500        })
2501    }
2502
2503    /// Registers a raw LLM execution intercept callback.
2504    ///
2505    /// # Safety
2506    /// `cb`, `user_data`, and `free_fn` must remain valid for every host
2507    /// callback invocation until the host deregisters the callback or calls
2508    /// `free_fn`. `free_fn` must match the allocation behind `user_data`.
2509    pub unsafe fn register_llm_execution_intercept_raw(
2510        &mut self,
2511        name: &str,
2512        priority: i32,
2513        cb: NemoRelayNativeLlmExecutionCb,
2514        user_data: *mut c_void,
2515        free_fn: NemoRelayNativeFreeFn,
2516    ) -> NemoRelayStatus {
2517        self.with_name(name, |host, name| unsafe {
2518            (host.plugin_context_register_llm_execution_intercept)(
2519                self.raw, name, priority, cb, user_data, free_fn,
2520            )
2521        })
2522    }
2523
2524    /// Registers a raw LLM stream execution intercept callback.
2525    ///
2526    /// # Safety
2527    /// `cb`, `user_data`, and `free_fn` must remain valid for every host
2528    /// callback invocation until the host deregisters the callback or calls
2529    /// `free_fn`. `free_fn` must match the allocation behind `user_data`.
2530    pub unsafe fn register_llm_stream_execution_intercept_raw(
2531        &mut self,
2532        name: &str,
2533        priority: i32,
2534        cb: NemoRelayNativeLlmStreamExecutionCb,
2535        user_data: *mut c_void,
2536        free_fn: NemoRelayNativeFreeFn,
2537    ) -> NemoRelayStatus {
2538        self.with_name(name, |host, name| unsafe {
2539            (host.plugin_context_register_llm_stream_execution_intercept)(
2540                self.raw, name, priority, cb, user_data, free_fn,
2541            )
2542        })
2543    }
2544
2545    /// Registers completion-based asynchronous middleware through the ABI-v3
2546    /// extension table.
2547    ///
2548    /// Plugins built against older hosts receive [`NemoRelayStatus::InvalidArg`]
2549    /// instead of attempting to read beyond the legacy host table.
2550    ///
2551    /// # Safety
2552    /// `cb`, `user_data`, and `free_fn` must remain valid until the host
2553    /// deregisters the callback or invokes `free_fn`. A callback returning
2554    /// `Pending` must settle and release its completion/next references.
2555    /// [`NemoRelayNativeAsyncMiddlewareKind::LlmStreamExecutionIntercept`] is
2556    /// rejected; use [`Self::register_async_stream_middleware_raw`] instead.
2557    #[allow(clippy::too_many_arguments)] // Mirrors the native C ABI registration callback.
2558    pub unsafe fn register_async_middleware_raw(
2559        &mut self,
2560        kind: NemoRelayNativeAsyncMiddlewareKind,
2561        name: &str,
2562        priority: i32,
2563        break_chain: bool,
2564        cb: NemoRelayNativeAsyncMiddlewareCb,
2565        user_data: *mut c_void,
2566        free_fn: NemoRelayNativeFreeFn,
2567    ) -> NemoRelayStatus {
2568        if self.host.abi_version < NEMO_RELAY_NATIVE_ABI_VERSION_ASYNC_MIDDLEWARE
2569            || self.host.struct_size < std::mem::size_of::<NemoRelayNativeHostApiV3>()
2570        {
2571            return NemoRelayStatus::InvalidArg;
2572        }
2573        let host = unsafe { &*(self.host as *const _ as *const NemoRelayNativeHostApiV3) };
2574        self.with_name(name, |_, name| unsafe {
2575            (host.plugin_context_register_async_middleware)(
2576                self.raw,
2577                kind as u32,
2578                name,
2579                priority,
2580                break_chain,
2581                cb,
2582                user_data,
2583                free_fn,
2584            )
2585        })
2586    }
2587
2588    /// Registers an incremental completion-based LLM stream intercept.
2589    ///
2590    /// # Safety
2591    /// The callback and user data must remain valid until deregistration or
2592    /// `free_fn`; callback-owned `next` and `stream` handles must each be
2593    /// released exactly once. Stream pushes and rejection are nonblocking:
2594    /// `Internal` with a host last-error containing `backpressured` means the
2595    /// bounded queue is full and the operation may be retried. The output
2596    /// stream owns the callback lifetime. `next` may be invoked repeatedly or
2597    /// concurrently until that stream settles; Relay then rejects or cancels
2598    /// unfinished and later calls.
2599    pub unsafe fn register_async_stream_middleware_raw(
2600        &mut self,
2601        name: &str,
2602        priority: i32,
2603        cb: NemoRelayNativeAsyncStreamMiddlewareCb,
2604        user_data: *mut c_void,
2605        free_fn: NemoRelayNativeFreeFn,
2606    ) -> NemoRelayStatus {
2607        if self.host.abi_version < NEMO_RELAY_NATIVE_ABI_VERSION_ASYNC_MIDDLEWARE
2608            || self.host.struct_size < std::mem::size_of::<NemoRelayNativeHostApiV3>()
2609        {
2610            return NemoRelayStatus::InvalidArg;
2611        }
2612        let host = unsafe { &*(self.host as *const _ as *const NemoRelayNativeHostApiV3) };
2613        self.with_name(name, |_, name| unsafe {
2614            (host.plugin_context_register_async_stream_middleware)(
2615                self.raw, name, priority, cb, user_data, free_fn,
2616            )
2617        })
2618    }
2619
2620    fn with_name(
2621        &self,
2622        name: &str,
2623        f: impl FnOnce(&NemoRelayNativeHostApiV1, *const NemoRelayNativeString) -> NemoRelayStatus,
2624    ) -> NemoRelayStatus {
2625        let name = match HostString::try_new(self.host, name) {
2626            Ok(name) => name,
2627            Err(status) => return status,
2628        };
2629        f(self.host, name.as_ptr())
2630    }
2631}
2632
2633struct TypedCallback<F> {
2634    host: NemoRelayNativeHostApiV1,
2635    callback: F,
2636}
2637
2638fn typed_callback_user_data<F>(host: &NemoRelayNativeHostApiV1, callback: F) -> *mut c_void {
2639    Box::into_raw(Box::new(TypedCallback {
2640        host: *host,
2641        callback,
2642    })) as *mut c_void
2643}
2644
2645unsafe extern "C" fn drop_typed_callback<F>(user_data: *mut c_void) {
2646    if !user_data.is_null() {
2647        let callback = unsafe { Box::from_raw(user_data as *mut TypedCallback<F>) };
2648        let host = callback.host;
2649        if catch_unwind(AssertUnwindSafe(|| drop(callback))).is_err() {
2650            set_last_error(&host, "native plugin typed callback state drop panicked");
2651        }
2652    }
2653}
2654
2655fn finish_typed_registration<F>(
2656    host: &NemoRelayNativeHostApiV1,
2657    status: NemoRelayStatus,
2658    user_data: *mut c_void,
2659    label: &str,
2660) -> Result<()> {
2661    if status == NemoRelayStatus::Ok {
2662        Ok(())
2663    } else {
2664        unsafe { drop_typed_callback::<F>(user_data) };
2665        Err(status_error(host, status, label))
2666    }
2667}
2668
2669fn status_error(host: &NemoRelayNativeHostApiV1, status: NemoRelayStatus, label: &str) -> String {
2670    debug_assert_ne!(status, NemoRelayStatus::Ok);
2671    set_last_error(host, &format!("{label} failed: {status:?}"));
2672    format!("{label} failed: {status:?}")
2673}
2674
2675fn callback_error(host: &NemoRelayNativeHostApiV1, message: String) -> NemoRelayStatus {
2676    set_last_error(host, &message);
2677    NemoRelayStatus::Internal
2678}
2679
2680fn callback_panic(host: &NemoRelayNativeHostApiV1, label: &str) -> NemoRelayStatus {
2681    set_last_error(host, &format!("{label} panicked"));
2682    NemoRelayStatus::Internal
2683}
2684
2685unsafe extern "C" fn typed_subscriber_trampoline<F>(
2686    user_data: *mut c_void,
2687    event_json: *const NemoRelayNativeString,
2688) -> NemoRelayStatus
2689where
2690    F: Fn(&Event) + Send + Sync + 'static,
2691{
2692    if user_data.is_null() {
2693        return NemoRelayStatus::NullPointer;
2694    }
2695    let state = unsafe { &*(user_data as *const TypedCallback<F>) };
2696    let result = catch_unwind(AssertUnwindSafe(|| {
2697        let event: Event = read_json_value(&state.host, event_json, "event")?;
2698        (state.callback)(&event);
2699        Ok::<_, NemoRelayStatus>(())
2700    }));
2701    match result {
2702        Ok(Ok(())) => NemoRelayStatus::Ok,
2703        Ok(Err(status)) => status,
2704        Err(_) => callback_panic(&state.host, "subscriber callback"),
2705    }
2706}
2707
2708unsafe extern "C" fn typed_event_sanitize_trampoline<F>(
2709    user_data: *mut c_void,
2710    event_json: *const NemoRelayNativeString,
2711    fields_json: *const NemoRelayNativeString,
2712    out_fields_json: *mut *mut NemoRelayNativeString,
2713) -> NemoRelayStatus
2714where
2715    F: Fn(&Event, EventSanitizeFields) -> EventSanitizeFields + Send + Sync + 'static,
2716{
2717    if user_data.is_null() || out_fields_json.is_null() {
2718        return NemoRelayStatus::NullPointer;
2719    }
2720    unsafe { *out_fields_json = ptr::null_mut() };
2721    let state = unsafe { &*(user_data as *const TypedCallback<F>) };
2722    let result = catch_unwind(AssertUnwindSafe(|| {
2723        let event: Event = read_json_value(&state.host, event_json, "event")?;
2724        let fields: EventSanitizeFields =
2725            read_json_value(&state.host, fields_json, "event sanitize fields")?;
2726        let output = (state.callback)(&event, fields);
2727        Ok::<_, NemoRelayStatus>(write_json(&state.host, &output, out_fields_json))
2728    }));
2729    match result {
2730        Ok(Ok(status)) => status,
2731        Ok(Err(status)) => status,
2732        Err(_) => callback_panic(&state.host, "event sanitize callback"),
2733    }
2734}
2735
2736unsafe extern "C" fn typed_tool_sanitize_trampoline<F>(
2737    user_data: *mut c_void,
2738    name: *const NemoRelayNativeString,
2739    payload_json: *const NemoRelayNativeString,
2740    out_json: *mut *mut NemoRelayNativeString,
2741) -> NemoRelayStatus
2742where
2743    F: Fn(&str, Json) -> Json + Send + Sync + 'static,
2744{
2745    if user_data.is_null() || out_json.is_null() {
2746        return NemoRelayStatus::NullPointer;
2747    }
2748    unsafe { *out_json = ptr::null_mut() };
2749    let state = unsafe { &*(user_data as *const TypedCallback<F>) };
2750    let result = catch_unwind(AssertUnwindSafe(|| {
2751        let name = read_required_host_string(&state.host, name, "tool name")?;
2752        let payload: Json = read_json_value(&state.host, payload_json, "tool payload")?;
2753        let output = (state.callback)(&name, payload);
2754        Ok::<_, NemoRelayStatus>(write_json(&state.host, &output, out_json))
2755    }));
2756    match result {
2757        Ok(Ok(status)) => status,
2758        Ok(Err(status)) => status,
2759        Err(_) => callback_panic(&state.host, "tool sanitize callback"),
2760    }
2761}
2762
2763unsafe extern "C" fn typed_tool_intercept_trampoline<F>(
2764    user_data: *mut c_void,
2765    name: *const NemoRelayNativeString,
2766    payload_json: *const NemoRelayNativeString,
2767    out_json: *mut *mut NemoRelayNativeString,
2768) -> NemoRelayStatus
2769where
2770    F: Fn(&str, Json) -> Result<Json> + Send + Sync + 'static,
2771{
2772    if user_data.is_null() || out_json.is_null() {
2773        return NemoRelayStatus::NullPointer;
2774    }
2775    unsafe { *out_json = ptr::null_mut() };
2776    let state = unsafe { &*(user_data as *const TypedCallback<F>) };
2777    let result = catch_unwind(AssertUnwindSafe(|| {
2778        let name = read_required_host_string(&state.host, name, "tool name")?;
2779        let payload: Json = read_json_value(&state.host, payload_json, "tool payload")?;
2780        match (state.callback)(&name, payload) {
2781            Ok(output) => Ok::<_, NemoRelayStatus>(write_json(&state.host, &output, out_json)),
2782            Err(message) => Ok(callback_error(&state.host, message)),
2783        }
2784    }));
2785    match result {
2786        Ok(Ok(status)) => status,
2787        Ok(Err(status)) => status,
2788        Err(_) => callback_panic(&state.host, "tool intercept callback"),
2789    }
2790}
2791
2792unsafe extern "C" fn typed_tool_conditional_trampoline<F>(
2793    user_data: *mut c_void,
2794    name: *const NemoRelayNativeString,
2795    args_json: *const NemoRelayNativeString,
2796    out_reason: *mut *mut NemoRelayNativeString,
2797) -> NemoRelayStatus
2798where
2799    F: Fn(&str, &Json) -> Result<Option<String>> + Send + Sync + 'static,
2800{
2801    if user_data.is_null() || out_reason.is_null() {
2802        return NemoRelayStatus::NullPointer;
2803    }
2804    unsafe { *out_reason = ptr::null_mut() };
2805    let state = unsafe { &*(user_data as *const TypedCallback<F>) };
2806    let result = catch_unwind(AssertUnwindSafe(|| {
2807        let name = read_required_host_string(&state.host, name, "tool name")?;
2808        let args: Json = read_json_value(&state.host, args_json, "tool args")?;
2809        match (state.callback)(&name, &args) {
2810            Ok(Some(reason)) => {
2811                let reason =
2812                    HostString::new(&state.host, &reason).ok_or(NemoRelayStatus::Internal)?;
2813                unsafe { *out_reason = reason.ptr };
2814                std::mem::forget(reason);
2815                Ok(NemoRelayStatus::Ok)
2816            }
2817            Ok(None) => {
2818                unsafe { *out_reason = ptr::null_mut() };
2819                Ok(NemoRelayStatus::Ok)
2820            }
2821            Err(message) => Ok(callback_error(&state.host, message)),
2822        }
2823    }));
2824    match result {
2825        Ok(Ok(status)) => status,
2826        Ok(Err(status)) => status,
2827        Err(_) => callback_panic(&state.host, "tool conditional callback"),
2828    }
2829}
2830
2831unsafe extern "C" fn typed_tool_execution_trampoline<F>(
2832    user_data: *mut c_void,
2833    name: *const NemoRelayNativeString,
2834    args_json: *const NemoRelayNativeString,
2835    next_fn: NemoRelayNativeToolNextFn,
2836    next_ctx: *mut c_void,
2837    out_outcome_json: *mut *mut NemoRelayNativeString,
2838) -> NemoRelayStatus
2839where
2840    F: for<'next> Fn(&str, Json, ToolNext<'next>) -> Result<ToolExecutionInterceptOutcome>
2841        + Send
2842        + Sync
2843        + 'static,
2844{
2845    if user_data.is_null() || out_outcome_json.is_null() {
2846        return NemoRelayStatus::NullPointer;
2847    }
2848    unsafe { *out_outcome_json = ptr::null_mut() };
2849    let state = unsafe { &*(user_data as *const TypedCallback<F>) };
2850    let result = catch_unwind(AssertUnwindSafe(|| {
2851        let name = read_required_host_string(&state.host, name, "tool name")?;
2852        let args: Json = read_json_value(&state.host, args_json, "tool args")?;
2853        let next = ToolNext {
2854            host: &state.host,
2855            next_fn,
2856            next_ctx,
2857        };
2858        match (state.callback)(&name, args, next) {
2859            Ok(outcome) => {
2860                let Some(outcome) = HostString::from_json(&state.host, &outcome) else {
2861                    set_last_error(&state.host, "failed to allocate tool execution outcome");
2862                    return Ok(NemoRelayStatus::Internal);
2863                };
2864                unsafe { *out_outcome_json = outcome.ptr };
2865                std::mem::forget(outcome);
2866                Ok(NemoRelayStatus::Ok)
2867            }
2868            Err(message) => Ok(callback_error(&state.host, message)),
2869        }
2870    }));
2871    match result {
2872        Ok(Ok(status)) => status,
2873        Ok(Err(status)) => status,
2874        Err(_) => callback_panic(&state.host, "tool execution callback"),
2875    }
2876}
2877
2878unsafe extern "C" fn typed_llm_sanitize_request_trampoline<F>(
2879    user_data: *mut c_void,
2880    request_json: *const NemoRelayNativeString,
2881    context: NemoRelayNativeLlmSanitizeRequestContext,
2882    out_request_json: *mut *mut NemoRelayNativeString,
2883) -> NemoRelayStatus
2884where
2885    F: for<'a> Fn(LlmRequest, LlmSanitizeRequestContext<'a>) -> Option<LlmRequest>
2886        + Send
2887        + Sync
2888        + 'static,
2889{
2890    if user_data.is_null() || out_request_json.is_null() {
2891        return NemoRelayStatus::NullPointer;
2892    }
2893    unsafe { *out_request_json = ptr::null_mut() };
2894    let state = unsafe { &*(user_data as *const TypedCallback<F>) };
2895    let result = catch_unwind(AssertUnwindSafe(|| {
2896        let context = llm_sanitize_request_context_from_native(&state.host, context)?;
2897        let request: LlmRequest = read_json_value(&state.host, request_json, "LLM request")?;
2898        match (state.callback)(request, context) {
2899            Some(output) => {
2900                Ok::<_, NemoRelayStatus>(write_json(&state.host, &output, out_request_json))
2901            }
2902            None => Ok(NemoRelayStatus::Ok),
2903        }
2904    }));
2905    match result {
2906        Ok(Ok(status)) => status,
2907        Ok(Err(status)) => status,
2908        Err(_) => callback_panic(&state.host, "LLM sanitize request callback"),
2909    }
2910}
2911
2912unsafe extern "C" fn typed_llm_sanitize_response_trampoline<F>(
2913    user_data: *mut c_void,
2914    payload_json: *const NemoRelayNativeString,
2915    context: NemoRelayNativeLlmSanitizeResponseContext,
2916    out_json: *mut *mut NemoRelayNativeString,
2917) -> NemoRelayStatus
2918where
2919    F: for<'a> Fn(Json, LlmSanitizeResponseContext<'a>) -> Option<Json> + Send + Sync + 'static,
2920{
2921    if user_data.is_null() || out_json.is_null() {
2922        return NemoRelayStatus::NullPointer;
2923    }
2924    unsafe { *out_json = ptr::null_mut() };
2925    let state = unsafe { &*(user_data as *const TypedCallback<F>) };
2926    let result = catch_unwind(AssertUnwindSafe(|| {
2927        let context = llm_sanitize_response_context_from_native(&state.host, context)?;
2928        let payload: Json = read_json_value(&state.host, payload_json, "LLM response")?;
2929        match (state.callback)(payload, context) {
2930            Some(output) => Ok::<_, NemoRelayStatus>(write_json(&state.host, &output, out_json)),
2931            None => Ok(NemoRelayStatus::Ok),
2932        }
2933    }));
2934    match result {
2935        Ok(Ok(status)) => status,
2936        Ok(Err(status)) => status,
2937        Err(_) => callback_panic(&state.host, "LLM sanitize response callback"),
2938    }
2939}
2940
2941unsafe extern "C" fn typed_llm_conditional_trampoline<F>(
2942    user_data: *mut c_void,
2943    request_json: *const NemoRelayNativeString,
2944    out_reason: *mut *mut NemoRelayNativeString,
2945) -> NemoRelayStatus
2946where
2947    F: Fn(&LlmRequest) -> Result<Option<String>> + Send + Sync + 'static,
2948{
2949    if user_data.is_null() || out_reason.is_null() {
2950        return NemoRelayStatus::NullPointer;
2951    }
2952    unsafe { *out_reason = ptr::null_mut() };
2953    let state = unsafe { &*(user_data as *const TypedCallback<F>) };
2954    let result = catch_unwind(AssertUnwindSafe(|| {
2955        let request: LlmRequest = read_json_value(&state.host, request_json, "LLM request")?;
2956        match (state.callback)(&request) {
2957            Ok(Some(reason)) => {
2958                let reason =
2959                    HostString::new(&state.host, &reason).ok_or(NemoRelayStatus::Internal)?;
2960                unsafe { *out_reason = reason.ptr };
2961                std::mem::forget(reason);
2962                Ok(NemoRelayStatus::Ok)
2963            }
2964            Ok(None) => {
2965                unsafe { *out_reason = ptr::null_mut() };
2966                Ok(NemoRelayStatus::Ok)
2967            }
2968            Err(message) => Ok(callback_error(&state.host, message)),
2969        }
2970    }));
2971    match result {
2972        Ok(Ok(status)) => status,
2973        Ok(Err(status)) => status,
2974        Err(_) => callback_panic(&state.host, "LLM conditional callback"),
2975    }
2976}
2977
2978unsafe extern "C" fn typed_llm_request_intercept_trampoline<F>(
2979    user_data: *mut c_void,
2980    name: *const NemoRelayNativeString,
2981    request_json: *const NemoRelayNativeString,
2982    annotated_json: *const NemoRelayNativeString,
2983    out_outcome_json: *mut *mut NemoRelayNativeString,
2984) -> NemoRelayStatus
2985where
2986    F: Fn(&str, LlmRequest, Option<AnnotatedLlmRequest>) -> Result<LlmRequestInterceptOutcome>
2987        + Send
2988        + Sync
2989        + 'static,
2990{
2991    if user_data.is_null() || out_outcome_json.is_null() {
2992        return NemoRelayStatus::NullPointer;
2993    }
2994    unsafe {
2995        *out_outcome_json = ptr::null_mut();
2996    }
2997    let state = unsafe { &*(user_data as *const TypedCallback<F>) };
2998    let result = catch_unwind(AssertUnwindSafe(|| {
2999        let name = read_required_host_string(&state.host, name, "LLM name")?;
3000        let request: LlmRequest = read_json_value(&state.host, request_json, "LLM request")?;
3001        let annotated: Option<AnnotatedLlmRequest> =
3002            read_optional_json_value(&state.host, annotated_json, "annotated LLM request")?;
3003        match (state.callback)(&name, request, annotated) {
3004            Ok(outcome) => {
3005                let Some(outcome) = HostString::from_json(&state.host, &outcome) else {
3006                    set_last_error(&state.host, "failed to allocate LLM request outcome");
3007                    return Ok(NemoRelayStatus::Internal);
3008                };
3009                unsafe {
3010                    *out_outcome_json = outcome.ptr;
3011                }
3012                std::mem::forget(outcome);
3013                Ok(NemoRelayStatus::Ok)
3014            }
3015            Err(message) => Ok(callback_error(&state.host, message)),
3016        }
3017    }));
3018    match result {
3019        Ok(Ok(status)) => status,
3020        Ok(Err(status)) => status,
3021        Err(_) => callback_panic(&state.host, "LLM request intercept callback"),
3022    }
3023}
3024
3025unsafe extern "C" fn typed_llm_execution_trampoline<F>(
3026    user_data: *mut c_void,
3027    name: *const NemoRelayNativeString,
3028    request_json: *const NemoRelayNativeString,
3029    next_fn: NemoRelayNativeLlmNextFn,
3030    next_ctx: *mut c_void,
3031    out_json: *mut *mut NemoRelayNativeString,
3032) -> NemoRelayStatus
3033where
3034    F: for<'next> Fn(&str, LlmRequest, LlmNext<'next>) -> Result<Json> + Send + Sync + 'static,
3035{
3036    if user_data.is_null() || out_json.is_null() {
3037        return NemoRelayStatus::NullPointer;
3038    }
3039    unsafe { *out_json = ptr::null_mut() };
3040    let state = unsafe { &*(user_data as *const TypedCallback<F>) };
3041    let result = catch_unwind(AssertUnwindSafe(|| {
3042        let name = read_required_host_string(&state.host, name, "LLM name")?;
3043        let request: LlmRequest = read_json_value(&state.host, request_json, "LLM request")?;
3044        let next = LlmNext {
3045            host: &state.host,
3046            next_fn,
3047            next_ctx,
3048        };
3049        match (state.callback)(&name, request, next) {
3050            Ok(output) => Ok::<_, NemoRelayStatus>(write_json(&state.host, &output, out_json)),
3051            Err(message) => Ok(callback_error(&state.host, message)),
3052        }
3053    }));
3054    match result {
3055        Ok(Ok(status)) => status,
3056        Ok(Err(status)) => status,
3057        Err(_) => callback_panic(&state.host, "LLM execution callback"),
3058    }
3059}
3060
3061struct TypedLlmJsonStream {
3062    host: NemoRelayNativeHostApiV1,
3063    state: Mutex<TypedLlmJsonStreamState>,
3064}
3065
3066struct TypedLlmJsonStreamState {
3067    iter: LlmJsonStream,
3068    finished: bool,
3069}
3070
3071fn native_stream_from_iter(
3072    host: &NemoRelayNativeHostApiV1,
3073    iter: LlmJsonStream,
3074) -> NemoRelayNativeLlmStreamV1 {
3075    let state = Box::new(TypedLlmJsonStream {
3076        host: *host,
3077        state: Mutex::new(TypedLlmJsonStreamState {
3078            iter,
3079            finished: false,
3080        }),
3081    });
3082    NemoRelayNativeLlmStreamV1 {
3083        struct_size: std::mem::size_of::<NemoRelayNativeLlmStreamV1>(),
3084        user_data: Box::into_raw(state).cast(),
3085        next: Some(poll_typed_llm_json_stream),
3086        cancel: Some(cancel_typed_llm_json_stream),
3087        drop: Some(drop_typed_llm_json_stream),
3088    }
3089}
3090
3091unsafe extern "C" fn poll_typed_llm_json_stream(
3092    user_data: *mut c_void,
3093    out_json: *mut *mut NemoRelayNativeString,
3094) -> NemoRelayStatus {
3095    if user_data.is_null() || out_json.is_null() {
3096        return NemoRelayStatus::NullPointer;
3097    }
3098    unsafe { *out_json = ptr::null_mut() };
3099    let stream = unsafe { &*(user_data as *const TypedLlmJsonStream) };
3100    let result = catch_unwind(AssertUnwindSafe(|| {
3101        let mut state = match stream.state.lock() {
3102            Ok(state) => state,
3103            Err(_) => {
3104                set_last_error(&stream.host, "native plugin stream state lock poisoned");
3105                return NemoRelayStatus::Internal;
3106            }
3107        };
3108        if state.finished {
3109            return NemoRelayStatus::StreamEnd;
3110        }
3111        match state.iter.next() {
3112            Some(Ok(chunk)) => {
3113                let status = write_json(&stream.host, &chunk, out_json);
3114                if status != NemoRelayStatus::Ok {
3115                    state.finished = true;
3116                }
3117                status
3118            }
3119            Some(Err(message)) => {
3120                state.finished = true;
3121                callback_error(&stream.host, message)
3122            }
3123            None => {
3124                state.finished = true;
3125                NemoRelayStatus::StreamEnd
3126            }
3127        }
3128    }));
3129    result.unwrap_or_else(|_| callback_panic(&stream.host, "LLM stream callback"))
3130}
3131
3132unsafe extern "C" fn cancel_typed_llm_json_stream(user_data: *mut c_void) -> NemoRelayStatus {
3133    if user_data.is_null() {
3134        return NemoRelayStatus::NullPointer;
3135    }
3136    let stream = unsafe { &*(user_data as *const TypedLlmJsonStream) };
3137    let result = catch_unwind(AssertUnwindSafe(|| {
3138        let mut state = match stream.state.lock() {
3139            Ok(state) => state,
3140            Err(_) => {
3141                set_last_error(&stream.host, "native plugin stream state lock poisoned");
3142                return NemoRelayStatus::Internal;
3143            }
3144        };
3145        state.finished = true;
3146        NemoRelayStatus::Ok
3147    }));
3148    result.unwrap_or_else(|_| callback_panic(&stream.host, "LLM stream cancel callback"))
3149}
3150
3151unsafe extern "C" fn drop_typed_llm_json_stream(user_data: *mut c_void) {
3152    if !user_data.is_null() {
3153        let stream = unsafe { Box::from_raw(user_data as *mut TypedLlmJsonStream) };
3154        let host = stream.host;
3155        if catch_unwind(AssertUnwindSafe(|| drop(stream))).is_err() {
3156            set_last_error(&host, "native plugin LLM stream state drop panicked");
3157        }
3158    }
3159}
3160
3161unsafe extern "C" fn typed_llm_stream_execution_trampoline<F>(
3162    user_data: *mut c_void,
3163    name: *const NemoRelayNativeString,
3164    request_json: *const NemoRelayNativeString,
3165    next_fn: NemoRelayNativeLlmStreamNextFn,
3166    next_ctx: *mut c_void,
3167    out_stream: *mut NemoRelayNativeLlmStreamV1,
3168) -> NemoRelayStatus
3169where
3170    F: for<'next> Fn(&str, LlmRequest, LlmStreamNext<'next>) -> Result<LlmJsonStream>
3171        + Send
3172        + Sync
3173        + 'static,
3174{
3175    if user_data.is_null() || out_stream.is_null() {
3176        return NemoRelayStatus::NullPointer;
3177    }
3178    unsafe { *out_stream = NemoRelayNativeLlmStreamV1::default() };
3179    let state = unsafe { &*(user_data as *const TypedCallback<F>) };
3180    let result = catch_unwind(AssertUnwindSafe(|| {
3181        let name = read_required_host_string(&state.host, name, "LLM name")?;
3182        let request: LlmRequest = read_json_value(&state.host, request_json, "LLM request")?;
3183        let next = LlmStreamNext {
3184            host: &state.host,
3185            next_fn,
3186            next_ctx,
3187        };
3188        match (state.callback)(&name, request, next) {
3189            Ok(stream) => {
3190                unsafe { *out_stream = native_stream_from_iter(&state.host, stream) };
3191                Ok::<_, NemoRelayStatus>(NemoRelayStatus::Ok)
3192            }
3193            Err(message) => Ok(callback_error(&state.host, message)),
3194        }
3195    }));
3196    match result {
3197        Ok(Ok(status)) => status,
3198        Ok(Err(status)) => status,
3199        Err(_) => callback_panic(&state.host, "LLM stream execution callback"),
3200    }
3201}
3202
3203struct HostString<'a> {
3204    host: &'a NemoRelayNativeHostApiV1,
3205    ptr: *mut NemoRelayNativeString,
3206}
3207
3208impl<'a> HostString<'a> {
3209    fn try_new(
3210        host: &'a NemoRelayNativeHostApiV1,
3211        value: &str,
3212    ) -> std::result::Result<Self, NemoRelayStatus> {
3213        let mut out = ptr::null_mut();
3214        let status = unsafe { (host.string_new)(value.as_ptr(), value.len(), &mut out) };
3215        if status != NemoRelayStatus::Ok {
3216            return Err(status);
3217        }
3218        if out.is_null() {
3219            return Err(NemoRelayStatus::Internal);
3220        }
3221        Ok(Self { host, ptr: out })
3222    }
3223
3224    fn new(host: &'a NemoRelayNativeHostApiV1, value: &str) -> Option<Self> {
3225        Self::try_new(host, value).ok()
3226    }
3227
3228    fn from_json<T: Serialize>(host: &'a NemoRelayNativeHostApiV1, value: &T) -> Option<Self> {
3229        serde_json::to_string(value)
3230            .ok()
3231            .and_then(|json| Self::new(host, &json))
3232    }
3233
3234    fn as_ptr(&self) -> *const NemoRelayNativeString {
3235        self.ptr
3236    }
3237}
3238
3239impl Drop for HostString<'_> {
3240    fn drop(&mut self) {
3241        unsafe { (self.host.string_free)(self.ptr) };
3242    }
3243}
3244
3245fn codec_status(host: &NemoRelayNativeHostApiV1, status: NemoRelayStatus) -> Result<()> {
3246    if status == NemoRelayStatus::Ok {
3247        Ok(())
3248    } else {
3249        Err(status_error(host, status, "LLM codec operation"))
3250    }
3251}
3252
3253fn native_codec_call<T: DeserializeOwned>(
3254    host: &NemoRelayNativeHostApiV1,
3255    call: impl FnOnce(*mut *mut NemoRelayNativeString) -> Result<()>,
3256) -> Result<T> {
3257    let mut out = ptr::null_mut();
3258    call(&mut out)?;
3259    if out.is_null() {
3260        return Err("LLM codec operation returned null".into());
3261    }
3262    let out = HostString { host, ptr: out };
3263    let text = read_host_string(host, out.as_ptr())
3264        .map_err(|_| "LLM codec operation returned invalid UTF-8".to_string())?;
3265    serde_json::from_str(&text).map_err(|error| format!("invalid LLM codec result: {error}"))
3266}
3267
3268struct OptionalHostJson<'a>(Option<HostString<'a>>);
3269
3270impl<'a> OptionalHostJson<'a> {
3271    fn new(host: &'a NemoRelayNativeHostApiV1, value: Option<&Json>) -> Result<Self> {
3272        match value {
3273            Some(value) => HostString::from_json(host, value)
3274                .map(|value| Self(Some(value)))
3275                .ok_or_else(|| "failed to allocate JSON host string".into()),
3276            None => Ok(Self(None)),
3277        }
3278    }
3279
3280    fn as_ptr(&self) -> *const NemoRelayNativeString {
3281        self.0
3282            .as_ref()
3283            .map(HostString::as_ptr)
3284            .unwrap_or(ptr::null())
3285    }
3286}
3287
3288enum OwnedHostApi {
3289    V1(NemoRelayNativeHostApiV1),
3290    V3(NemoRelayNativeHostApiV3),
3291}
3292
3293impl OwnedHostApi {
3294    unsafe fn copy_from(host: &NemoRelayNativeHostApiV1) -> Self {
3295        if host.abi_version >= NEMO_RELAY_NATIVE_ABI_VERSION_ASYNC_MIDDLEWARE
3296            && host.struct_size >= std::mem::size_of::<NemoRelayNativeHostApiV3>()
3297        {
3298            Self::V3(unsafe { *(host as *const _ as *const NemoRelayNativeHostApiV3) })
3299        } else {
3300            Self::V1(*host)
3301        }
3302    }
3303
3304    fn v1(&self) -> &NemoRelayNativeHostApiV1 {
3305        match self {
3306            Self::V1(host) => host,
3307            Self::V3(host) => &host.v1,
3308        }
3309    }
3310}
3311
3312struct PluginState<P> {
3313    host: OwnedHostApi,
3314    plugin: Mutex<P>,
3315}
3316
3317unsafe extern "C" fn drop_plugin_state<P: NativePlugin>(user_data: *mut c_void) {
3318    if !user_data.is_null() {
3319        let state = unsafe { Box::from_raw(user_data as *mut PluginState<P>) };
3320        let host = *state.host.v1();
3321        if catch_unwind(AssertUnwindSafe(|| drop(state))).is_err() {
3322            set_last_error(&host, "native plugin state drop panicked");
3323        }
3324    }
3325}
3326
3327unsafe extern "C" fn validate_trampoline<P: NativePlugin>(
3328    user_data: *mut c_void,
3329    plugin_config_json: *const NemoRelayNativeString,
3330    out_diagnostics_json: *mut *mut NemoRelayNativeString,
3331) -> NemoRelayStatus {
3332    if user_data.is_null() || out_diagnostics_json.is_null() {
3333        return NemoRelayStatus::NullPointer;
3334    }
3335    unsafe { *out_diagnostics_json = ptr::null_mut() };
3336    let state = unsafe { &*(user_data as *const PluginState<P>) };
3337    let result = catch_unwind(AssertUnwindSafe(|| {
3338        let host = state.host.v1();
3339        let config = match read_json_object(host, plugin_config_json) {
3340            Ok(config) => config,
3341            Err(status) => return status,
3342        };
3343        let plugin = match state.plugin.lock() {
3344            Ok(plugin) => plugin,
3345            Err(_) => {
3346                set_last_error(host, "native plugin state lock poisoned");
3347                return NemoRelayStatus::Internal;
3348            }
3349        };
3350        let diagnostics = plugin.validate(&config);
3351        write_json(host, &diagnostics, out_diagnostics_json)
3352    }));
3353    result.unwrap_or_else(|_| {
3354        set_last_error(state.host.v1(), "native plugin validate callback panicked");
3355        NemoRelayStatus::Internal
3356    })
3357}
3358
3359unsafe extern "C" fn register_trampoline<P: NativePlugin>(
3360    user_data: *mut c_void,
3361    plugin_config_json: *const NemoRelayNativeString,
3362    ctx: *mut NemoRelayNativePluginContext,
3363) -> NemoRelayStatus {
3364    if user_data.is_null() || ctx.is_null() {
3365        return NemoRelayStatus::NullPointer;
3366    }
3367    let state = unsafe { &*(user_data as *const PluginState<P>) };
3368    let result = catch_unwind(AssertUnwindSafe(|| {
3369        let host = state.host.v1();
3370        let config = match read_json_object(host, plugin_config_json) {
3371            Ok(config) => config,
3372            Err(status) => return status,
3373        };
3374        let mut ctx = unsafe { PluginContext::from_raw(host, ctx) };
3375        let mut plugin = match state.plugin.lock() {
3376            Ok(plugin) => plugin,
3377            Err(_) => {
3378                set_last_error(host, "native plugin state lock poisoned");
3379                return NemoRelayStatus::Internal;
3380            }
3381        };
3382        match plugin.register(&config, &mut ctx) {
3383            Ok(()) => NemoRelayStatus::Ok,
3384            Err(message) => {
3385                set_last_error(host, &message);
3386                NemoRelayStatus::Internal
3387            }
3388        }
3389    }));
3390    result.unwrap_or_else(|_| {
3391        set_last_error(state.host.v1(), "native plugin register callback panicked");
3392        NemoRelayStatus::Internal
3393    })
3394}
3395
3396fn read_json_object(
3397    host: &NemoRelayNativeHostApiV1,
3398    value: *const NemoRelayNativeString,
3399) -> std::result::Result<Map<String, Json>, NemoRelayStatus> {
3400    let value: Json = read_json_value(host, value, "plugin config")?;
3401    match value {
3402        Json::Object(map) => Ok(map),
3403        _ => {
3404            set_last_error(host, "plugin config must be a JSON object");
3405            Err(NemoRelayStatus::InvalidJson)
3406        }
3407    }
3408}
3409
3410fn read_json_value<T: DeserializeOwned>(
3411    host: &NemoRelayNativeHostApiV1,
3412    value: *const NemoRelayNativeString,
3413    label: &str,
3414) -> std::result::Result<T, NemoRelayStatus> {
3415    let text = read_required_host_string(host, value, label)?;
3416    serde_json::from_str::<T>(&text).map_err(|error| {
3417        set_last_error(host, &format!("{label} was invalid JSON: {error}"));
3418        NemoRelayStatus::InvalidJson
3419    })
3420}
3421
3422fn read_optional_json_value<T: DeserializeOwned>(
3423    host: &NemoRelayNativeHostApiV1,
3424    value: *const NemoRelayNativeString,
3425    label: &str,
3426) -> std::result::Result<Option<T>, NemoRelayStatus> {
3427    if value.is_null() {
3428        Ok(None)
3429    } else {
3430        read_json_value(host, value, label).map(Some)
3431    }
3432}
3433
3434fn llm_codec_identity_from_native(
3435    host: &NemoRelayNativeHostApiV1,
3436    codec_kind: NemoRelayNativeLlmCodecKind,
3437    codec_id: *const NemoRelayNativeString,
3438) -> std::result::Result<LlmCodecIdentity, NemoRelayStatus> {
3439    let codec = match codec_kind {
3440        NemoRelayNativeLlmCodecKind::None => LlmCodecIdentity::None,
3441        NemoRelayNativeLlmCodecKind::Opaque => LlmCodecIdentity::Opaque,
3442        NemoRelayNativeLlmCodecKind::BuiltIn => {
3443            let id = read_required_host_string(host, codec_id, "LLM built-in codec ID")?;
3444            let builtin = match id.as_str() {
3445                "openai_chat" => BuiltinLlmCodec::OpenAiChat,
3446                "openai_responses" => BuiltinLlmCodec::OpenAiResponses,
3447                "anthropic_messages" => BuiltinLlmCodec::AnthropicMessages,
3448                _ => {
3449                    set_last_error(host, &format!("unknown built-in LLM codec ID: {id}"));
3450                    return Err(NemoRelayStatus::InvalidArg);
3451                }
3452            };
3453            LlmCodecIdentity::BuiltIn(builtin)
3454        }
3455        NemoRelayNativeLlmCodecKind::Runtime => LlmCodecIdentity::Runtime(
3456            read_required_host_string(host, codec_id, "LLM runtime codec ID")?,
3457        ),
3458    };
3459    Ok(codec)
3460}
3461
3462fn llm_sanitize_request_context_from_native<'a>(
3463    host: &NemoRelayNativeHostApiV1,
3464    context: NemoRelayNativeLlmSanitizeRequestContext,
3465) -> std::result::Result<LlmSanitizeRequestContext<'a>, NemoRelayStatus> {
3466    let codec = llm_codec_identity_from_native(host, context.codec_kind, context.codec_id)?;
3467    let resolved = (!context.codec.is_null()).then_some(LlmSanitizeRequestCodec {
3468        host: *host,
3469        handle: context.codec,
3470        _lifetime: PhantomData,
3471    });
3472    Ok(LlmSanitizeRequestContext { codec, resolved })
3473}
3474
3475fn llm_sanitize_response_context_from_native<'a>(
3476    host: &NemoRelayNativeHostApiV1,
3477    context: NemoRelayNativeLlmSanitizeResponseContext,
3478) -> std::result::Result<LlmSanitizeResponseContext<'a>, NemoRelayStatus> {
3479    let codec = llm_codec_identity_from_native(host, context.codec_kind, context.codec_id)?;
3480    let resolved = (!context.codec.is_null()).then_some(LlmSanitizeResponseCodec {
3481        host: *host,
3482        handle: context.codec,
3483        _lifetime: PhantomData,
3484    });
3485    Ok(LlmSanitizeResponseContext { codec, resolved })
3486}
3487
3488enum HostStringReadError {
3489    Null,
3490    InvalidUtf8,
3491}
3492
3493fn read_required_host_string(
3494    host: &NemoRelayNativeHostApiV1,
3495    value: *const NemoRelayNativeString,
3496    label: &str,
3497) -> std::result::Result<String, NemoRelayStatus> {
3498    match read_host_string(host, value) {
3499        Ok(value) => Ok(value),
3500        Err(HostStringReadError::Null) => {
3501            set_last_error(host, &format!("{label} was null"));
3502            Err(NemoRelayStatus::NullPointer)
3503        }
3504        Err(HostStringReadError::InvalidUtf8) => {
3505            set_last_error(host, &format!("{label} contained invalid UTF-8"));
3506            Err(NemoRelayStatus::InvalidUtf8)
3507        }
3508    }
3509}
3510
3511fn read_host_string(
3512    host: &NemoRelayNativeHostApiV1,
3513    value: *const NemoRelayNativeString,
3514) -> std::result::Result<String, HostStringReadError> {
3515    if value.is_null() {
3516        return Err(HostStringReadError::Null);
3517    }
3518    let len = unsafe { (host.string_len)(value) };
3519    let data = unsafe { (host.string_data)(value) };
3520    if data.is_null() && len > 0 {
3521        return Err(HostStringReadError::InvalidUtf8);
3522    }
3523    let bytes = if len == 0 {
3524        &[][..]
3525    } else {
3526        unsafe { std::slice::from_raw_parts(data, len) }
3527    };
3528    std::str::from_utf8(bytes)
3529        .map(str::to_owned)
3530        .map_err(|_| HostStringReadError::InvalidUtf8)
3531}
3532
3533fn write_json<T: Serialize>(
3534    host: &NemoRelayNativeHostApiV1,
3535    value: &T,
3536    out: *mut *mut NemoRelayNativeString,
3537) -> NemoRelayStatus {
3538    if out.is_null() {
3539        return NemoRelayStatus::NullPointer;
3540    }
3541    unsafe { *out = ptr::null_mut() };
3542    let json = serde_json::to_value(value).expect("Relay DTOs and serde_json::Value serialize");
3543    let Some(handle) = HostString::from_json(host, &json) else {
3544        set_last_error(host, "failed to allocate host string");
3545        return NemoRelayStatus::Internal;
3546    };
3547    unsafe { *out = handle.ptr };
3548    std::mem::forget(handle);
3549    NemoRelayStatus::Ok
3550}
3551
3552fn set_last_error(host: &NemoRelayNativeHostApiV1, message: &str) {
3553    if let Some(message) = HostString::new(host, message) {
3554        unsafe { (host.last_error_set)(message.as_ptr()) };
3555    }
3556}
3557
3558/// Sets a host last-error message from generated entry symbols.
3559///
3560/// # Safety
3561/// `host` must be null or point to a valid [`NemoRelayNativeHostApiV1`].
3562#[doc(hidden)]
3563pub unsafe fn __set_last_error_from_entry(host: *const NemoRelayNativeHostApiV1, message: &str) {
3564    if !host.is_null() {
3565        set_last_error(unsafe { &*host }, message);
3566    }
3567}
3568
3569/// Initializes a native plugin descriptor for a Rust SDK plugin value.
3570///
3571/// # Safety
3572/// `host` must point to a valid [`NemoRelayNativeHostApiV1`] for the duration
3573/// of the call, and `out` must point to writable memory for one
3574/// [`NemoRelayNativePluginV1`] descriptor.
3575pub unsafe fn export_plugin<P: NativePlugin>(
3576    host: *const NemoRelayNativeHostApiV1,
3577    out: *mut NemoRelayNativePluginV1,
3578    plugin: P,
3579) -> NemoRelayStatus {
3580    if host.is_null() || out.is_null() {
3581        return NemoRelayStatus::NullPointer;
3582    }
3583    unsafe { *out = NemoRelayNativePluginV1::default() };
3584    let host_ref = unsafe { &*host };
3585    export_plugin_checked(host_ref, out, || plugin)
3586}
3587
3588/// Initializes a native plugin descriptor from a constructor callback.
3589///
3590/// # Safety
3591/// `host` must point to a valid [`NemoRelayNativeHostApiV1`] for the duration
3592/// of the call, and `out` must point to writable memory for one
3593/// [`NemoRelayNativePluginV1`] descriptor.
3594#[doc(hidden)]
3595pub unsafe fn __export_plugin_from_constructor<P, F>(
3596    host: *const NemoRelayNativeHostApiV1,
3597    out: *mut NemoRelayNativePluginV1,
3598    constructor: F,
3599) -> NemoRelayStatus
3600where
3601    P: NativePlugin,
3602    F: FnOnce() -> P,
3603{
3604    if host.is_null() || out.is_null() {
3605        return NemoRelayStatus::NullPointer;
3606    }
3607    unsafe { *out = NemoRelayNativePluginV1::default() };
3608    let host_ref = unsafe { &*host };
3609    export_plugin_checked(host_ref, out, constructor)
3610}
3611
3612fn export_plugin_checked<P, F>(
3613    host_ref: &NemoRelayNativeHostApiV1,
3614    out: *mut NemoRelayNativePluginV1,
3615    constructor: F,
3616) -> NemoRelayStatus
3617where
3618    P: NativePlugin,
3619    F: FnOnce() -> P,
3620{
3621    if host_ref.abi_version != NEMO_RELAY_NATIVE_ABI_VERSION {
3622        return NemoRelayStatus::InvalidArg;
3623    }
3624    if host_ref.struct_size < std::mem::size_of::<NemoRelayNativeHostApiV1>() {
3625        return NemoRelayStatus::InvalidArg;
3626    }
3627
3628    let plugin = constructor();
3629    let kind = plugin.plugin_kind().to_owned();
3630    let allows_multiple_components = plugin.allows_multiple_components();
3631    let Some(kind_handle) = HostString::new(host_ref, &kind) else {
3632        return NemoRelayStatus::Internal;
3633    };
3634    let state = Box::new(PluginState {
3635        host: unsafe { OwnedHostApi::copy_from(host_ref) },
3636        plugin: Mutex::new(plugin),
3637    });
3638    unsafe {
3639        *out = NemoRelayNativePluginV1 {
3640            struct_size: std::mem::size_of::<NemoRelayNativePluginV1>(),
3641            plugin_kind: kind_handle.ptr,
3642            allows_multiple_components,
3643            user_data: Box::into_raw(state) as *mut c_void,
3644            validate: Some(validate_trampoline::<P>),
3645            register: Some(register_trampoline::<P>),
3646            drop: Some(drop_plugin_state::<P>),
3647        };
3648    }
3649    std::mem::forget(kind_handle);
3650    NemoRelayStatus::Ok
3651}
3652
3653/// Exports a concrete plugin constructor as a native plugin entry symbol body.
3654#[macro_export]
3655macro_rules! nemo_relay_plugin {
3656    ($symbol:ident, $constructor:expr) => {
3657        #[doc = "Native plugin entry symbol generated by `nemo_relay_plugin!`."]
3658        #[unsafe(no_mangle)]
3659        pub unsafe extern "C" fn $symbol(
3660            host: *const $crate::NemoRelayNativeHostApiV1,
3661            out: *mut $crate::NemoRelayNativePluginV1,
3662        ) -> $crate::NemoRelayStatus {
3663            match ::std::panic::catch_unwind(::std::panic::AssertUnwindSafe(|| unsafe {
3664                $crate::__export_plugin_from_constructor(host, out, $constructor)
3665            })) {
3666                Ok(status) => status,
3667                Err(_) => {
3668                    unsafe {
3669                        $crate::__set_last_error_from_entry(
3670                            host,
3671                            "native plugin entry callback panicked",
3672                        )
3673                    };
3674                    $crate::NemoRelayStatus::Internal
3675                }
3676            }
3677        }
3678    };
3679}