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