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