Skip to main content

nemo_relay/plugin/dynamic/
native.rs

1// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Native dynamic plugin loader and host-side ABI adapter.
5
6#[cfg(test)]
7use std::cell::Cell;
8use std::cell::RefCell;
9use std::collections::HashMap;
10#[cfg(test)]
11use std::collections::HashSet;
12use std::ffi::c_void;
13use std::future::Future;
14use std::panic::{AssertUnwindSafe, catch_unwind};
15use std::path::{Path, PathBuf};
16use std::pin::Pin;
17use std::ptr;
18use std::sync::atomic::{AtomicBool, Ordering};
19use std::sync::{Arc, Mutex, OnceLock};
20use std::task::{Context, Poll};
21
22use futures_util::FutureExt;
23
24use crate::api::event::{Event, EventSanitizeFields};
25use crate::api::llm::{LlmRequest, LlmRequestInterceptOutcome};
26use crate::api::runtime::{
27    EventSanitizeFn, EventSubscriberFn, LlmCodecIdentity, LlmConditionalFn, LlmExecutionFn,
28    LlmExecutionNextFn, LlmJsonStream, LlmRequestInterceptFn, LlmSanitizeRequestContext,
29    LlmSanitizeRequestFn, LlmSanitizeResponseContext, LlmSanitizeResponseFn, LlmStreamExecutionFn,
30    LlmStreamExecutionNextFn, MiddlewareContinuationContext, ToolConditionalFn, ToolExecutionFn,
31    ToolExecutionNextFn, ToolInterceptFn, ToolSanitizeFn,
32};
33use crate::api::runtime::{
34    ScopeStackHandle, ThreadScopeStackBinding, capture_thread_scope_stack, create_scope_stack,
35    restore_thread_scope_stack, scope_stack_active, set_thread_scope_stack, with_scope_stack,
36};
37use crate::api::scope::{
38    EmitMarkEventParams, PopScopeParams, PushScopeParams, ScopeAttributes, ScopeHandle, ScopeType,
39};
40use crate::api::scope::{event as emit_scope_mark, get_handle, pop_scope, push_scope};
41use crate::api::tool::ToolExecutionInterceptOutcome;
42use crate::codec::request::AnnotatedLlmRequest;
43use crate::codec::traits::{LlmCodec, LlmResponseCodec};
44use crate::error::{FlowError, Result as FlowResult};
45use crate::plugin::{
46    ConfigDiagnostic, DiagnosticLevel, Plugin, PluginError, PluginRegistrationContext,
47    deregister_plugin_registration_checked, register_plugin_tracked,
48};
49use chrono::{DateTime, Utc};
50use libloading::{Library, Symbol};
51use nemo_relay_plugin::{
52    NEMO_RELAY_NATIVE_ABI_VERSION, NEMO_RELAY_NATIVE_ABI_VERSION_LEGACY,
53    NemoRelayNativeAsyncCallbackState, NemoRelayNativeAsyncCompletion,
54    NemoRelayNativeAsyncMiddlewareCb, NemoRelayNativeAsyncMiddlewareKind, NemoRelayNativeAsyncNext,
55    NemoRelayNativeAsyncNextResultCb, NemoRelayNativeAsyncNextStreamCb, NemoRelayNativeAsyncStream,
56    NemoRelayNativeAsyncStreamMiddlewareCb, NemoRelayNativeEventSanitizeCb,
57    NemoRelayNativeEventSubscriberCb, NemoRelayNativeFreeFn, NemoRelayNativeHostApiV1,
58    NemoRelayNativeHostApiV3, NemoRelayNativeLlmCodecKind, NemoRelayNativeLlmConditionalCb,
59    NemoRelayNativeLlmExecutionCb, NemoRelayNativeLlmRequestCodec,
60    NemoRelayNativeLlmRequestInterceptCb, NemoRelayNativeLlmResponseCodec,
61    NemoRelayNativeLlmSanitizeRequestCb, NemoRelayNativeLlmSanitizeRequestContext,
62    NemoRelayNativeLlmSanitizeResponseCb, NemoRelayNativeLlmSanitizeResponseContext,
63    NemoRelayNativeLlmStreamExecutionCb, NemoRelayNativeLlmStreamV1, NemoRelayNativePluginContext,
64    NemoRelayNativePluginEntry, NemoRelayNativePluginV1, NemoRelayNativeScopeHandle,
65    NemoRelayNativeScopeStack, NemoRelayNativeScopeStackBinding, NemoRelayNativeScopeType,
66    NemoRelayNativeString, NemoRelayNativeToolConditionalCb, NemoRelayNativeToolExecutionCb,
67    NemoRelayNativeToolJsonCb, NemoRelayNativeWithScopeStackCb, NemoRelayStatus,
68};
69use semver::{Version, VersionReq};
70use serde_json::{Map, Value as Json};
71use sha2::{Digest, Sha256};
72use tokio::runtime::Runtime;
73use tokio_stream::{Stream, StreamExt};
74
75use super::{
76    DynamicPluginKind, DynamicPluginManifest, DynamicPluginManifestLoad,
77    DynamicPluginTeardownOutcome, deregister_tracked_registrations_checked,
78    validate_annotated_request_consumer_compatibility,
79};
80
81/// Native plugin load request derived from host dynamic-plugin state.
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub struct NativePluginLoadSpec {
84    /// Expected plugin kind.
85    pub plugin_id: String,
86    /// Path to the authored `relay-plugin.toml`.
87    pub manifest_ref: String,
88}
89
90/// Owns native dynamic libraries registered into the plugin registry.
91///
92/// Dropping this value deregisters the native plugin kinds before unloading
93/// their libraries. Clear active plugin configuration before dropping it so
94/// runtime callbacks cannot outlive their code.
95pub struct NativePluginActivation {
96    plugins: Vec<Arc<NativePluginInstance>>,
97    plugin_registrations: Vec<(String, u64)>,
98}
99
100impl NativePluginActivation {
101    /// Returns `true` when no native plugins were loaded.
102    pub fn is_empty(&self) -> bool {
103        self.plugins.is_empty()
104    }
105
106    /// Consumes the activation and deregisters loaded plugin kinds.
107    pub fn clear(self) {}
108
109    pub(crate) fn deregister_plugin_kinds_checked(&mut self) -> DynamicPluginTeardownOutcome {
110        deregister_tracked_registrations_checked(&mut self.plugin_registrations, "native")
111    }
112
113    #[cfg(test)]
114    pub(super) fn with_plugin_kind_for_test(plugin_kind: impl Into<String>) -> Self {
115        Self {
116            plugins: Vec::new(),
117            plugin_registrations: vec![(plugin_kind.into(), 0)],
118        }
119    }
120}
121
122impl Drop for NativePluginActivation {
123    fn drop(&mut self) {
124        for (plugin_kind, registration_id) in self.plugin_registrations.iter().rev() {
125            let _ = deregister_plugin_registration_checked(plugin_kind, *registration_id);
126        }
127    }
128}
129
130/// Loads native dynamic plugins and registers their plugin kinds.
131///
132/// The returned activation must be kept alive until after active plugin
133/// configuration has been cleared.
134pub fn load_native_plugins<I>(specs: I) -> crate::plugin::Result<NativePluginActivation>
135where
136    I: IntoIterator<Item = NativePluginLoadSpec>,
137{
138    let mut activation = NativePluginActivation {
139        plugins: Vec::new(),
140        plugin_registrations: Vec::new(),
141    };
142    for spec in specs {
143        let instance = load_one_native_plugin(&spec)?;
144        let plugin_kind = instance.plugin_kind.clone();
145        let registration_id = register_plugin_tracked(Arc::new(NativePluginAdapter {
146            plugin_kind: plugin_kind.clone(),
147            allows_multiple_components: instance.allows_multiple_components,
148            instance: instance.clone(),
149        }))?;
150        activation.plugins.push(instance);
151        activation
152            .plugin_registrations
153            .push((plugin_kind, registration_id));
154    }
155    Ok(activation)
156}
157
158struct NativePluginAdapter {
159    plugin_kind: String,
160    allows_multiple_components: bool,
161    instance: Arc<NativePluginInstance>,
162}
163
164impl Plugin for NativePluginAdapter {
165    fn plugin_kind(&self) -> &str {
166        &self.plugin_kind
167    }
168
169    fn allows_multiple_components(&self) -> bool {
170        self.allows_multiple_components
171    }
172
173    fn validate(&self, plugin_config: &Map<String, Json>) -> Vec<ConfigDiagnostic> {
174        let plugin = self
175            .instance
176            .plugin
177            .lock()
178            .expect("native plugin lock poisoned");
179        let Some(validate) = plugin.validate else {
180            return vec![];
181        };
182        clear_native_last_error();
183        let Some(config_json) = native_string_from_json(&Json::Object(plugin_config.clone()))
184        else {
185            return vec![native_error_diagnostic(
186                &self.plugin_kind,
187                "plugin.native_validate_failed",
188                "failed to serialize plugin config",
189            )];
190        };
191        let mut out = ptr::null_mut();
192        let status = unsafe { validate(plugin.user_data, config_json, &mut out) };
193        unsafe { native_string_free(config_json) };
194        if status != NemoRelayStatus::Ok {
195            if !out.is_null() {
196                unsafe { native_string_free(out) };
197            }
198            let message = native_last_error_message()
199                .unwrap_or_else(|| format!("native validate callback returned {status:?}"));
200            return vec![native_error_diagnostic(
201                &self.plugin_kind,
202                "plugin.native_validate_failed",
203                &message,
204            )];
205        }
206        if out.is_null() {
207            return vec![];
208        }
209        let diagnostics = read_native_string(out)
210            .ok()
211            .and_then(|text| serde_json::from_str::<Vec<ConfigDiagnostic>>(&text).ok())
212            .unwrap_or_else(|| {
213                vec![native_error_diagnostic(
214                    &self.plugin_kind,
215                    "plugin.native_validate_failed",
216                    "native validate callback returned invalid diagnostics JSON",
217                )]
218            });
219        unsafe { native_string_free(out) };
220        diagnostics
221    }
222
223    fn register<'a>(
224        &'a self,
225        plugin_config: &Map<String, Json>,
226        ctx: &'a mut PluginRegistrationContext,
227    ) -> Pin<Box<dyn Future<Output = crate::plugin::Result<()>> + Send + 'a>> {
228        let plugin_config = plugin_config.clone();
229        Box::pin(async move {
230            let plugin = self.instance.plugin.lock().map_err(|err| {
231                PluginError::Internal(format!("native plugin lock poisoned: {err}"))
232            })?;
233            let register = plugin.register.ok_or_else(|| {
234                PluginError::RegistrationFailed(format!(
235                    "native plugin '{}' did not return a register callback",
236                    self.plugin_kind
237                ))
238            })?;
239            clear_native_last_error();
240            let config_json =
241                native_string_from_json(&Json::Object(plugin_config)).ok_or_else(|| {
242                    PluginError::RegistrationFailed("failed to serialize plugin config".into())
243                })?;
244            let mut native_ctx = NativeHostPluginContext {
245                ctx: ctx as *mut _,
246                instance: self.instance.clone(),
247            };
248            let status = unsafe {
249                register(
250                    plugin.user_data,
251                    config_json,
252                    &mut native_ctx as *mut _ as *mut NemoRelayNativePluginContext,
253                )
254            };
255            unsafe { native_string_free(config_json) };
256            if status == NemoRelayStatus::Ok {
257                Ok(())
258            } else {
259                let message = native_last_error_message()
260                    .unwrap_or_else(|| format!("native register callback returned {status:?}"));
261                Err(PluginError::RegistrationFailed(message))
262            }
263        })
264    }
265}
266
267fn native_error_diagnostic(plugin_kind: &str, code: &str, message: &str) -> ConfigDiagnostic {
268    ConfigDiagnostic {
269        level: DiagnosticLevel::Error,
270        code: code.into(),
271        component: Some(plugin_kind.into()),
272        field: None,
273        message: message.into(),
274    }
275}
276
277struct NativePluginInstance {
278    plugin_kind: String,
279    relay_compat: String,
280    allows_multiple_components: bool,
281    plugin: Mutex<NemoRelayNativePluginV1>,
282    _library: Library,
283}
284
285unsafe impl Send for NativePluginInstance {}
286unsafe impl Sync for NativePluginInstance {}
287
288impl Drop for NativePluginInstance {
289    fn drop(&mut self) {
290        if let Ok(mut plugin) = self.plugin.lock() {
291            drop_native_plugin_descriptor(&mut plugin);
292        }
293    }
294}
295
296fn drop_native_plugin_descriptor(plugin: &mut NemoRelayNativePluginV1) {
297    if let Some(drop_fn) = plugin.drop.take() {
298        unsafe { drop_fn(plugin.user_data) };
299        plugin.user_data = ptr::null_mut();
300    }
301    if !plugin.plugin_kind.is_null() {
302        unsafe { native_string_free(plugin.plugin_kind) };
303        plugin.plugin_kind = ptr::null_mut();
304    }
305}
306
307fn load_one_native_plugin(
308    spec: &NativePluginLoadSpec,
309) -> crate::plugin::Result<Arc<NativePluginInstance>> {
310    let (manifest, manifest_ref) = DynamicPluginManifest::load_from_path(&spec.manifest_ref)?;
311    if manifest.plugin.id.trim() != spec.plugin_id {
312        return Err(PluginError::InvalidConfig(format!(
313            "dynamic plugin manifest id '{}' does not match expected id '{}'",
314            manifest.plugin.id, spec.plugin_id
315        )));
316    }
317    if manifest.plugin.kind != DynamicPluginKind::RustDynamic {
318        return Err(PluginError::InvalidConfig(format!(
319            "dynamic plugin '{}' is kind {}; native loader only supports rust_dynamic",
320            spec.plugin_id, manifest.plugin.kind
321        )));
322    }
323    validate_relay_compatibility(manifest.compat.relay.as_deref())?;
324    let relay_compat = manifest
325        .compat
326        .relay
327        .as_deref()
328        .expect("validated native manifest must declare compat.relay")
329        .to_string();
330    if manifest.compat.native_api.as_deref().map(str::trim) != Some("1") {
331        return Err(PluginError::InvalidConfig(format!(
332            "dynamic plugin '{}' declares unsupported compat.native_api '{}'; expected 1",
333            spec.plugin_id,
334            manifest.compat.native_api.as_deref().unwrap_or("")
335        )));
336    }
337    let DynamicPluginManifestLoad::RustDynamic(load) = &manifest.load else {
338        return Err(PluginError::InvalidConfig(format!(
339            "dynamic plugin '{}' has invalid rust_dynamic load contract",
340            spec.plugin_id
341        )));
342    };
343    let manifest_path = PathBuf::from(&manifest_ref);
344    let library_path = resolve_manifest_relative_path(
345        &manifest_path,
346        load.library
347            .as_deref()
348            .ok_or_else(|| PluginError::InvalidConfig("load.library is required".into()))?,
349    );
350    if !library_path.exists() {
351        return Err(PluginError::NotFound(format!(
352            "native plugin library '{}' does not exist",
353            library_path.display()
354        )));
355    }
356    if let Some(expected_digest) = manifest
357        .integrity
358        .as_ref()
359        .and_then(|integrity| integrity.sha256.as_deref())
360    {
361        verify_sha256(&library_path, expected_digest)?;
362    }
363    let symbol = load
364        .symbol
365        .as_deref()
366        .ok_or_else(|| PluginError::InvalidConfig("load.symbol is required".into()))?;
367
368    let library = unsafe { Library::new(&library_path) }.map_err(|err| {
369        PluginError::Internal(format!(
370            "failed to load native plugin library '{}': {err}",
371            library_path.display()
372        ))
373    })?;
374    let mut plugin = NemoRelayNativePluginV1::default();
375    unsafe {
376        let entry: Symbol<NemoRelayNativePluginEntry> =
377            library.get(symbol.as_bytes()).map_err(|err| {
378                PluginError::NotFound(format!(
379                    "native plugin symbol '{symbol}' not found in '{}': {err}",
380                    library_path.display()
381                ))
382            })?;
383        let mut status = entry(native_host_api(), &mut plugin);
384        // SDKs compiled against ABI v2 correctly reject a v3 table. Retry
385        // their entry point with the frozen v2 prefix instead of making a
386        // runtime upgrade a breaking change for installed native plugins.
387        if status == NemoRelayStatus::InvalidArg {
388            drop_native_plugin_descriptor(&mut plugin);
389            status = entry(native_host_api_legacy(), &mut plugin);
390        }
391        if status != NemoRelayStatus::Ok {
392            drop_native_plugin_descriptor(&mut plugin);
393            return Err(PluginError::RegistrationFailed(format!(
394                "native plugin entry symbol '{symbol}' failed: {}",
395                native_last_error_message().unwrap_or_else(|| format!("{status:?}"))
396            )));
397        }
398    }
399    if let Err(err) = validate_plugin_descriptor(&spec.plugin_id, &plugin) {
400        drop_native_plugin_descriptor(&mut plugin);
401        return Err(err);
402    }
403    let plugin_kind = match read_native_string(plugin.plugin_kind) {
404        Ok(plugin_kind) => plugin_kind,
405        Err(err) => {
406            drop_native_plugin_descriptor(&mut plugin);
407            return Err(err);
408        }
409    };
410    if plugin_kind != spec.plugin_id {
411        drop_native_plugin_descriptor(&mut plugin);
412        return Err(PluginError::InvalidConfig(format!(
413            "native plugin returned kind '{plugin_kind}' but manifest id is '{}'",
414            spec.plugin_id
415        )));
416    }
417    Ok(Arc::new(NativePluginInstance {
418        plugin_kind,
419        relay_compat,
420        allows_multiple_components: plugin.allows_multiple_components,
421        plugin: Mutex::new(plugin),
422        _library: library,
423    }))
424}
425
426fn validate_relay_compatibility(relay: Option<&str>) -> crate::plugin::Result<()> {
427    let relay = relay
428        .map(str::trim)
429        .filter(|value| !value.is_empty())
430        .ok_or_else(|| PluginError::InvalidConfig("compat.relay is required".into()))?;
431    let req = VersionReq::parse(relay).map_err(|err| {
432        PluginError::InvalidConfig(format!("invalid compat.relay version requirement: {err}"))
433    })?;
434    let version = Version::parse(env!("CARGO_PKG_VERSION"))
435        .map_err(|err| PluginError::Internal(format!("failed to parse host version: {err}")))?;
436    if req.matches(&version) {
437        Ok(())
438    } else {
439        Err(PluginError::InvalidConfig(format!(
440            "native plugin requires relay '{relay}' but host version is {version}"
441        )))
442    }
443}
444
445fn validate_plugin_descriptor(
446    plugin_id: &str,
447    plugin: &NemoRelayNativePluginV1,
448) -> crate::plugin::Result<()> {
449    if plugin.struct_size < std::mem::size_of::<NemoRelayNativePluginV1>() {
450        return Err(PluginError::InvalidConfig(format!(
451            "native plugin '{plugin_id}' returned incompatible plugin descriptor size {}",
452            plugin.struct_size
453        )));
454    }
455    if plugin.plugin_kind.is_null() {
456        return Err(PluginError::InvalidConfig(format!(
457            "native plugin '{plugin_id}' returned a null plugin_kind"
458        )));
459    }
460    if plugin.register.is_none() {
461        return Err(PluginError::InvalidConfig(format!(
462            "native plugin '{plugin_id}' returned no register callback"
463        )));
464    }
465    Ok(())
466}
467
468fn resolve_manifest_relative_path(manifest_path: &Path, value: &str) -> PathBuf {
469    let path = PathBuf::from(value);
470    if path.is_absolute() {
471        path
472    } else {
473        manifest_path
474            .parent()
475            .map(|parent| parent.join(&path))
476            .unwrap_or(path)
477    }
478}
479
480fn verify_sha256(path: &Path, expected: &str) -> crate::plugin::Result<()> {
481    let expected = expected
482        .trim()
483        .strip_prefix("sha256:")
484        .unwrap_or(expected.trim());
485    let bytes = std::fs::read(path).map_err(|err| {
486        PluginError::Internal(format!("failed to read '{}': {err}", path.display()))
487    })?;
488    let actual = hex_digest(Sha256::digest(bytes));
489    if actual.eq_ignore_ascii_case(expected) {
490        Ok(())
491    } else {
492        Err(PluginError::InvalidConfig(format!(
493            "native plugin library '{}' sha256 mismatch",
494            path.display()
495        )))
496    }
497}
498
499fn hex_digest(bytes: impl AsRef<[u8]>) -> String {
500    const HEX: &[u8; 16] = b"0123456789abcdef";
501    let bytes = bytes.as_ref();
502    let mut out = String::with_capacity(bytes.len() * 2);
503    for byte in bytes {
504        out.push(HEX[(byte >> 4) as usize] as char);
505        out.push(HEX[(byte & 0x0f) as usize] as char);
506    }
507    out
508}
509
510#[repr(C)]
511struct NativeHostPluginContext {
512    ctx: *mut PluginRegistrationContext,
513    instance: Arc<NativePluginInstance>,
514}
515
516struct NativeHostString(Vec<u8>);
517
518struct NativeHostLlmRequestCodec(Arc<dyn LlmCodec>);
519struct NativeHostLlmResponseCodec(Arc<dyn LlmResponseCodec>);
520
521struct NativeHostScopeHandle(ScopeHandle);
522
523struct NativeHostScopeStack(ScopeStackHandle);
524
525struct NativeHostScopeStackBinding(ThreadScopeStackBinding);
526
527thread_local! {
528    static NATIVE_LAST_ERROR: RefCell<Option<String>> = const { RefCell::new(None) };
529    #[cfg(test)]
530    static NATIVE_STRING_LIVE_ALLOCATIONS: RefCell<HashSet<usize>> = RefCell::new(HashSet::new());
531    #[cfg(test)]
532    static NATIVE_STRING_FAIL_AFTER: Cell<Option<usize>> = const { Cell::new(None) };
533}
534
535fn set_native_last_error(message: impl Into<String>) {
536    NATIVE_LAST_ERROR.with(|cell| *cell.borrow_mut() = Some(message.into()));
537}
538
539fn clear_native_last_error() {
540    NATIVE_LAST_ERROR.with(|cell| *cell.borrow_mut() = None);
541}
542
543fn native_last_error_message() -> Option<String> {
544    NATIVE_LAST_ERROR.with(|cell| cell.borrow().clone())
545}
546
547#[cfg(test)]
548fn fail_native_string_allocation_after(successful_allocations: usize) {
549    NATIVE_STRING_FAIL_AFTER.with(|cell| cell.set(Some(successful_allocations)));
550}
551
552#[cfg(test)]
553fn native_string_live_allocations() -> usize {
554    NATIVE_STRING_LIVE_ALLOCATIONS.with(|allocations| allocations.borrow().len())
555}
556
557unsafe extern "C" fn native_string_new(
558    data: *const u8,
559    len: usize,
560    out: *mut *mut NemoRelayNativeString,
561) -> NemoRelayStatus {
562    if out.is_null() {
563        set_native_last_error("out string pointer is null");
564        return NemoRelayStatus::NullPointer;
565    }
566    unsafe { *out = ptr::null_mut() };
567    if data.is_null() && len > 0 {
568        set_native_last_error("string data pointer is null");
569        return NemoRelayStatus::NullPointer;
570    }
571    let bytes: &[u8] = if len == 0 {
572        &[]
573    } else {
574        unsafe { std::slice::from_raw_parts(data, len) }
575    };
576    if let Err(err) = std::str::from_utf8(bytes) {
577        set_native_last_error(format!("string data is not valid UTF-8: {err}"));
578        return NemoRelayStatus::InvalidUtf8;
579    }
580    #[cfg(test)]
581    let should_fail = NATIVE_STRING_FAIL_AFTER.with(|cell| match cell.get() {
582        Some(0) => {
583            cell.set(None);
584            true
585        }
586        Some(remaining) => {
587            cell.set(Some(remaining - 1));
588            false
589        }
590        None => false,
591    });
592    #[cfg(test)]
593    if should_fail {
594        set_native_last_error("injected native string allocation failure");
595        return NemoRelayStatus::Internal;
596    }
597    let handle = Box::new(NativeHostString(bytes.to_vec()));
598    unsafe { *out = Box::into_raw(handle) as *mut NemoRelayNativeString };
599    #[cfg(test)]
600    NATIVE_STRING_LIVE_ALLOCATIONS.with(|allocations| {
601        allocations.borrow_mut().insert(unsafe { *out } as usize);
602    });
603    NemoRelayStatus::Ok
604}
605
606unsafe extern "C" fn native_string_data(value: *const NemoRelayNativeString) -> *const u8 {
607    if value.is_null() {
608        return ptr::null();
609    }
610    let value = unsafe { &*(value as *const NativeHostString) };
611    value.0.as_ptr()
612}
613
614unsafe extern "C" fn native_string_len(value: *const NemoRelayNativeString) -> usize {
615    if value.is_null() {
616        return 0;
617    }
618    let value = unsafe { &*(value as *const NativeHostString) };
619    value.0.len()
620}
621
622unsafe extern "C" fn native_string_free(value: *mut NemoRelayNativeString) {
623    if !value.is_null() {
624        drop(unsafe { Box::from_raw(value as *mut NativeHostString) });
625        #[cfg(test)]
626        NATIVE_STRING_LIVE_ALLOCATIONS.with(|allocations| {
627            allocations.borrow_mut().remove(&(value as usize));
628        });
629    }
630}
631
632unsafe extern "C" fn native_last_error_clear() {
633    clear_native_last_error();
634}
635
636unsafe extern "C" fn native_last_error_set(message: *const NemoRelayNativeString) {
637    match read_native_string(message) {
638        Ok(message) => set_native_last_error(message),
639        Err(err) => set_native_last_error(err.to_string()),
640    }
641}
642
643unsafe extern "C" fn native_llm_request_codec_decode(
644    codec: *const NemoRelayNativeLlmRequestCodec,
645    request_json: *const NemoRelayNativeString,
646    out: *mut *mut NemoRelayNativeString,
647) -> NemoRelayStatus {
648    clear_native_last_error();
649    if out.is_null() {
650        set_native_last_error("request codec decode output pointer is null");
651        return NemoRelayStatus::NullPointer;
652    }
653    unsafe { *out = ptr::null_mut() };
654    if codec.is_null() {
655        set_native_last_error("request codec decode capability is null");
656        return NemoRelayStatus::NullPointer;
657    }
658    if request_json.is_null() {
659        set_native_last_error("request codec decode request is null");
660        return NemoRelayStatus::NullPointer;
661    }
662    let result = catch_unwind(AssertUnwindSafe(|| -> std::result::Result<_, String> {
663        let request: LlmRequest = serde_json::from_str(
664            &read_native_string(request_json).map_err(|error| error.to_string())?,
665        )
666        .map_err(|error| format!("invalid request JSON: {error}"))?;
667        let codec = unsafe { &*(codec as *const NativeHostLlmRequestCodec) };
668        let annotated = codec
669            .0
670            .decode(&request)
671            .map_err(|error| error.to_string())?;
672        let annotated = serde_json::to_value(annotated).map_err(|error| error.to_string())?;
673        native_string_from_json(&annotated)
674            .ok_or_else(|| "failed to allocate decoded request".to_string())
675    }));
676    match result {
677        Ok(Ok(value)) => {
678            unsafe { *out = value };
679            NemoRelayStatus::Ok
680        }
681        Ok(Err(error)) => {
682            set_native_last_error(format!("request codec decode failed: {error}"));
683            NemoRelayStatus::Internal
684        }
685        Err(_) => {
686            set_native_last_error("request codec decode panicked");
687            NemoRelayStatus::Internal
688        }
689    }
690}
691
692unsafe extern "C" fn native_llm_request_codec_encode(
693    codec: *const NemoRelayNativeLlmRequestCodec,
694    annotated_json: *const NemoRelayNativeString,
695    original_json: *const NemoRelayNativeString,
696    out: *mut *mut NemoRelayNativeString,
697) -> NemoRelayStatus {
698    clear_native_last_error();
699    if out.is_null() {
700        set_native_last_error("request codec encode output pointer is null");
701        return NemoRelayStatus::NullPointer;
702    }
703    unsafe { *out = ptr::null_mut() };
704    if codec.is_null() {
705        set_native_last_error("request codec encode capability is null");
706        return NemoRelayStatus::NullPointer;
707    }
708    if annotated_json.is_null() {
709        set_native_last_error("request codec encode annotated request is null");
710        return NemoRelayStatus::NullPointer;
711    }
712    if original_json.is_null() {
713        set_native_last_error("request codec encode original request is null");
714        return NemoRelayStatus::NullPointer;
715    }
716    let result = catch_unwind(AssertUnwindSafe(|| -> std::result::Result<_, String> {
717        let annotated: AnnotatedLlmRequest = serde_json::from_str(
718            &read_native_string(annotated_json).map_err(|error| error.to_string())?,
719        )
720        .map_err(|error| format!("invalid annotated request JSON: {error}"))?;
721        let original: LlmRequest = serde_json::from_str(
722            &read_native_string(original_json).map_err(|error| error.to_string())?,
723        )
724        .map_err(|error| format!("invalid original request JSON: {error}"))?;
725        let codec = unsafe { &*(codec as *const NativeHostLlmRequestCodec) };
726        let request = codec
727            .0
728            .encode(&annotated, &original)
729            .map_err(|error| error.to_string())?;
730        let request = serde_json::to_value(request).map_err(|error| error.to_string())?;
731        native_string_from_json(&request)
732            .ok_or_else(|| "failed to allocate encoded request".to_string())
733    }));
734    match result {
735        Ok(Ok(value)) => {
736            unsafe { *out = value };
737            NemoRelayStatus::Ok
738        }
739        Ok(Err(error)) => {
740            set_native_last_error(format!("request codec encode failed: {error}"));
741            NemoRelayStatus::Internal
742        }
743        Err(_) => {
744            set_native_last_error("request codec encode panicked");
745            NemoRelayStatus::Internal
746        }
747    }
748}
749
750unsafe extern "C" fn native_llm_response_codec_decode(
751    codec: *const NemoRelayNativeLlmResponseCodec,
752    response_json: *const NemoRelayNativeString,
753    out: *mut *mut NemoRelayNativeString,
754) -> NemoRelayStatus {
755    clear_native_last_error();
756    if out.is_null() {
757        set_native_last_error("response codec decode output pointer is null");
758        return NemoRelayStatus::NullPointer;
759    }
760    unsafe { *out = ptr::null_mut() };
761    if codec.is_null() {
762        set_native_last_error("response codec decode capability is null");
763        return NemoRelayStatus::NullPointer;
764    }
765    if response_json.is_null() {
766        set_native_last_error("response codec decode response is null");
767        return NemoRelayStatus::NullPointer;
768    }
769    let result = catch_unwind(AssertUnwindSafe(|| -> std::result::Result<_, String> {
770        let response: Json = serde_json::from_str(
771            &read_native_string(response_json).map_err(|error| error.to_string())?,
772        )
773        .map_err(|error| format!("invalid response JSON: {error}"))?;
774        let codec = unsafe { &*(codec as *const NativeHostLlmResponseCodec) };
775        let annotated = codec
776            .0
777            .decode_response(&response)
778            .map_err(|error| error.to_string())?;
779        let annotated = serde_json::to_value(annotated).map_err(|error| error.to_string())?;
780        native_string_from_json(&annotated)
781            .ok_or_else(|| "failed to allocate decoded response".to_string())
782    }));
783    match result {
784        Ok(Ok(value)) => {
785            unsafe { *out = value };
786            NemoRelayStatus::Ok
787        }
788        Ok(Err(error)) => {
789            set_native_last_error(format!("response codec decode failed: {error}"));
790            NemoRelayStatus::Internal
791        }
792        Err(_) => {
793            set_native_last_error("response codec decode panicked");
794            NemoRelayStatus::Internal
795        }
796    }
797}
798
799fn native_host_api() -> *const NemoRelayNativeHostApiV1 {
800    static HOST_API: OnceLock<NemoRelayNativeHostApiV3> = OnceLock::new();
801    &HOST_API.get_or_init(build_native_host_api_v3).v1 as *const NemoRelayNativeHostApiV1
802}
803
804fn native_host_api_legacy() -> *const NemoRelayNativeHostApiV1 {
805    static HOST_API: OnceLock<NemoRelayNativeHostApiV1> = OnceLock::new();
806    HOST_API.get_or_init(build_native_host_api_legacy) as *const _
807}
808
809fn build_native_host_api_legacy() -> NemoRelayNativeHostApiV1 {
810    static RELAY_VERSION: &[u8] = concat!(env!("CARGO_PKG_VERSION"), "\0").as_bytes();
811    NemoRelayNativeHostApiV1 {
812        abi_version: NEMO_RELAY_NATIVE_ABI_VERSION_LEGACY,
813        struct_size: std::mem::size_of::<NemoRelayNativeHostApiV1>(),
814        relay_version: RELAY_VERSION.as_ptr().cast(),
815        string_new: native_string_new,
816        string_data: native_string_data,
817        string_len: native_string_len,
818        string_free: native_string_free,
819        last_error_clear: native_last_error_clear,
820        last_error_set: native_last_error_set,
821        llm_request_codec_decode: native_llm_request_codec_decode,
822        llm_request_codec_encode: native_llm_request_codec_encode,
823        llm_response_codec_decode: native_llm_response_codec_decode,
824        plugin_context_register_subscriber: native_plugin_context_register_subscriber,
825        plugin_context_register_tool_sanitize_request_guardrail:
826            native_plugin_context_register_tool_sanitize_request_guardrail,
827        plugin_context_register_tool_sanitize_response_guardrail:
828            native_plugin_context_register_tool_sanitize_response_guardrail,
829        plugin_context_register_tool_conditional_execution_guardrail:
830            native_plugin_context_register_tool_conditional_execution_guardrail,
831        plugin_context_register_tool_request_intercept:
832            native_plugin_context_register_tool_request_intercept,
833        plugin_context_register_tool_execution_intercept:
834            native_plugin_context_register_tool_execution_intercept,
835        plugin_context_register_llm_sanitize_request_guardrail:
836            native_plugin_context_register_llm_sanitize_request_guardrail,
837        plugin_context_register_llm_sanitize_response_guardrail:
838            native_plugin_context_register_llm_sanitize_response_guardrail,
839        plugin_context_register_llm_conditional_execution_guardrail:
840            native_plugin_context_register_llm_conditional_execution_guardrail,
841        plugin_context_register_llm_request_intercept:
842            native_plugin_context_register_llm_request_intercept,
843        plugin_context_register_llm_execution_intercept:
844            native_plugin_context_register_llm_execution_intercept,
845        plugin_context_register_llm_stream_execution_intercept:
846            native_plugin_context_register_llm_stream_execution_intercept,
847        scope_handle_free: native_scope_handle_free,
848        scope_get_current: native_scope_get_current,
849        scope_push: native_scope_push,
850        scope_pop: native_scope_pop,
851        emit_mark: native_emit_mark,
852        scope_stack_create: native_scope_stack_create,
853        scope_stack_free: native_scope_stack_free,
854        scope_stack_set_thread: native_scope_stack_set_thread,
855        scope_stack_capture_thread: native_scope_stack_capture_thread,
856        scope_stack_restore_thread: native_scope_stack_restore_thread,
857        scope_stack_binding_free: native_scope_stack_binding_free,
858        scope_stack_active: native_scope_stack_active,
859        scope_stack_with_current: native_scope_stack_with_current,
860        plugin_context_register_mark_sanitize_guardrail:
861            native_plugin_context_register_mark_sanitize_guardrail,
862        plugin_context_register_scope_sanitize_start_guardrail:
863            native_plugin_context_register_scope_sanitize_start_guardrail,
864        plugin_context_register_scope_sanitize_end_guardrail:
865            native_plugin_context_register_scope_sanitize_end_guardrail,
866    }
867}
868
869fn build_native_host_api_v3() -> NemoRelayNativeHostApiV3 {
870    let mut v1 = build_native_host_api_legacy();
871    v1.abi_version = NEMO_RELAY_NATIVE_ABI_VERSION;
872    v1.struct_size = std::mem::size_of::<NemoRelayNativeHostApiV3>();
873    NemoRelayNativeHostApiV3 {
874        v1,
875        async_completion_resolve_json: native_async_completion_resolve_json,
876        async_completion_reject: native_async_completion_reject,
877        async_completion_is_cancelled: native_async_completion_is_cancelled,
878        async_completion_release: native_async_completion_release,
879        async_next_invoke: native_async_next_invoke,
880        async_next_release: native_async_next_release,
881        plugin_context_register_async_middleware: native_plugin_context_register_async_middleware,
882        async_stream_push_json: native_async_stream_push_json,
883        async_stream_finish: native_async_stream_finish,
884        async_stream_reject: native_async_stream_reject,
885        async_stream_is_cancelled: native_async_stream_is_cancelled,
886        async_stream_release: native_async_stream_release,
887        async_next_invoke_stream: native_async_next_invoke_stream,
888        plugin_context_register_async_stream_middleware:
889            native_plugin_context_register_async_stream_middleware,
890        async_next_invoke_result: native_async_next_invoke_result,
891    }
892}
893
894fn read_native_string(value: *const NemoRelayNativeString) -> crate::plugin::Result<String> {
895    if value.is_null() {
896        return Ok(String::new());
897    }
898    let value = unsafe { &*(value as *const NativeHostString) };
899    std::str::from_utf8(&value.0)
900        .map(str::to_owned)
901        .map_err(|err| {
902            PluginError::InvalidConfig(format!("native string is not valid UTF-8: {err}"))
903        })
904}
905
906fn native_string_from_str(value: &str) -> Option<*mut NemoRelayNativeString> {
907    let mut out = ptr::null_mut();
908    let status = unsafe { native_string_new(value.as_ptr(), value.len(), &mut out) };
909    (status == NemoRelayStatus::Ok).then_some(out)
910}
911
912fn native_string_from_json(value: &Json) -> Option<*mut NemoRelayNativeString> {
913    serde_json::to_string(value)
914        .ok()
915        .and_then(|value| native_string_from_str(&value))
916}
917
918fn json_from_native_string(value: *mut NemoRelayNativeString, fallback: &str) -> FlowResult<Json> {
919    if value.is_null() {
920        return Err(FlowError::Internal(
921            native_last_error_message().unwrap_or_else(|| fallback.into()),
922        ));
923    }
924    let text = read_native_string(value).map_err(|err| FlowError::Internal(err.to_string()))?;
925    serde_json::from_str(&text).map_err(|err| FlowError::Internal(format!("invalid JSON: {err}")))
926}
927
928fn take_native_string(value: *mut NemoRelayNativeString) -> FlowResult<String> {
929    let result = read_native_string(value).map_err(|err| FlowError::Internal(err.to_string()));
930    unsafe { native_string_free(value) };
931    result
932}
933
934fn take_json_from_native_string(
935    value: *mut NemoRelayNativeString,
936    fallback: &str,
937) -> FlowResult<Json> {
938    let result = json_from_native_string(value, fallback);
939    unsafe { native_string_free(value) };
940    result
941}
942
943unsafe fn free_native_sanitizer_strings(
944    input: *mut NemoRelayNativeString,
945    codec_id: Option<*mut NemoRelayNativeString>,
946    output: *mut NemoRelayNativeString,
947) {
948    unsafe { native_string_free(input) };
949    if let Some(codec_id) = codec_id
950        && codec_id != input
951    {
952        unsafe { native_string_free(codec_id) };
953    }
954    if !output.is_null() && output != input && Some(output) != codec_id {
955        unsafe { native_string_free(output) };
956    }
957}
958
959fn optional_json_from_native_string(
960    value: *const NemoRelayNativeString,
961    field: &str,
962) -> Result<Option<Json>, NemoRelayStatus> {
963    if value.is_null() {
964        return Ok(None);
965    }
966    let text = read_native_string(value).map_err(|err| {
967        set_native_last_error(err.to_string());
968        NemoRelayStatus::InvalidUtf8
969    })?;
970    serde_json::from_str(&text).map(Some).map_err(|err| {
971        set_native_last_error(format!("{field} is not valid JSON: {err}"));
972        NemoRelayStatus::InvalidJson
973    })
974}
975
976fn optional_timestamp_from_native(
977    timestamp_unix_micros: *const i64,
978) -> Result<Option<DateTime<Utc>>, NemoRelayStatus> {
979    if timestamp_unix_micros.is_null() {
980        return Ok(None);
981    }
982    DateTime::<Utc>::from_timestamp_micros(unsafe { ptr::read(timestamp_unix_micros) })
983        .map(Some)
984        .ok_or_else(|| {
985            set_native_last_error("timestamp unix microseconds are outside supported range");
986            NemoRelayStatus::InvalidArg
987        })
988}
989
990fn native_scope_type_to_core(scope_type: NemoRelayNativeScopeType) -> ScopeType {
991    match scope_type {
992        NemoRelayNativeScopeType::Agent => ScopeType::Agent,
993        NemoRelayNativeScopeType::Function => ScopeType::Function,
994        NemoRelayNativeScopeType::Tool => ScopeType::Tool,
995        NemoRelayNativeScopeType::Llm => ScopeType::Llm,
996        NemoRelayNativeScopeType::Retriever => ScopeType::Retriever,
997        NemoRelayNativeScopeType::Embedder => ScopeType::Embedder,
998        NemoRelayNativeScopeType::Reranker => ScopeType::Reranker,
999        NemoRelayNativeScopeType::Guardrail => ScopeType::Guardrail,
1000        NemoRelayNativeScopeType::Evaluator => ScopeType::Evaluator,
1001        NemoRelayNativeScopeType::Custom => ScopeType::Custom,
1002        NemoRelayNativeScopeType::Unknown => ScopeType::Unknown,
1003    }
1004}
1005
1006fn native_scope_ref<'a>(handle: *const NemoRelayNativeScopeHandle) -> Option<&'a ScopeHandle> {
1007    if handle.is_null() {
1008        return None;
1009    }
1010    Some(&unsafe { &*(handle as *const NativeHostScopeHandle) }.0)
1011}
1012
1013unsafe extern "C" fn native_scope_handle_free(handle: *mut NemoRelayNativeScopeHandle) {
1014    if !handle.is_null() {
1015        drop(unsafe { Box::from_raw(handle as *mut NativeHostScopeHandle) });
1016    }
1017}
1018
1019unsafe extern "C" fn native_scope_get_current(
1020    out: *mut *mut NemoRelayNativeScopeHandle,
1021) -> NemoRelayStatus {
1022    clear_native_last_error();
1023    if out.is_null() {
1024        set_native_last_error("out scope handle pointer is null");
1025        return NemoRelayStatus::NullPointer;
1026    }
1027    unsafe { *out = ptr::null_mut() };
1028    match get_handle() {
1029        Ok(handle) => {
1030            unsafe { *out = Box::into_raw(Box::new(NativeHostScopeHandle(handle))).cast() };
1031            NemoRelayStatus::Ok
1032        }
1033        Err(err) => status_from_flow_error(err),
1034    }
1035}
1036
1037unsafe extern "C" fn native_scope_push(
1038    name: *const NemoRelayNativeString,
1039    scope_type: NemoRelayNativeScopeType,
1040    parent: *const NemoRelayNativeScopeHandle,
1041    attributes: u32,
1042    data_json: *const NemoRelayNativeString,
1043    metadata_json: *const NemoRelayNativeString,
1044    input_json: *const NemoRelayNativeString,
1045    timestamp_unix_micros: *const i64,
1046    out: *mut *mut NemoRelayNativeScopeHandle,
1047) -> NemoRelayStatus {
1048    clear_native_last_error();
1049    if out.is_null() {
1050        set_native_last_error("out scope handle pointer is null");
1051        return NemoRelayStatus::NullPointer;
1052    }
1053    unsafe { *out = ptr::null_mut() };
1054    let name = match read_name(name) {
1055        Ok(name) => name,
1056        Err(status) => return status,
1057    };
1058    let data = match optional_json_from_native_string(data_json, "scope data") {
1059        Ok(data) => data,
1060        Err(status) => return status,
1061    };
1062    let metadata = match optional_json_from_native_string(metadata_json, "scope metadata") {
1063        Ok(metadata) => metadata,
1064        Err(status) => return status,
1065    };
1066    let input = match optional_json_from_native_string(input_json, "scope input") {
1067        Ok(input) => input,
1068        Err(status) => return status,
1069    };
1070    let timestamp = match optional_timestamp_from_native(timestamp_unix_micros) {
1071        Ok(timestamp) => timestamp,
1072        Err(status) => return status,
1073    };
1074    let parent_ref = native_scope_ref(parent);
1075    match push_scope(
1076        PushScopeParams::builder()
1077            .name(&name)
1078            .scope_type(native_scope_type_to_core(scope_type))
1079            .parent_opt(parent_ref)
1080            .attributes(ScopeAttributes::from_bits_truncate(attributes))
1081            .data_opt(data)
1082            .metadata_opt(metadata)
1083            .input_opt(input)
1084            .timestamp_opt(timestamp)
1085            .build(),
1086    ) {
1087        Ok(handle) => {
1088            unsafe { *out = Box::into_raw(Box::new(NativeHostScopeHandle(handle))).cast() };
1089            NemoRelayStatus::Ok
1090        }
1091        Err(err) => status_from_flow_error(err),
1092    }
1093}
1094
1095unsafe extern "C" fn native_scope_pop(
1096    handle: *const NemoRelayNativeScopeHandle,
1097    output_json: *const NemoRelayNativeString,
1098    metadata_json: *const NemoRelayNativeString,
1099    timestamp_unix_micros: *const i64,
1100) -> NemoRelayStatus {
1101    clear_native_last_error();
1102    if handle.is_null() {
1103        set_native_last_error("scope handle is null");
1104        return NemoRelayStatus::NullPointer;
1105    }
1106    let output = match optional_json_from_native_string(output_json, "scope output") {
1107        Ok(output) => output,
1108        Err(status) => return status,
1109    };
1110    let metadata = match optional_json_from_native_string(metadata_json, "scope metadata") {
1111        Ok(metadata) => metadata,
1112        Err(status) => return status,
1113    };
1114    let timestamp = match optional_timestamp_from_native(timestamp_unix_micros) {
1115        Ok(timestamp) => timestamp,
1116        Err(status) => return status,
1117    };
1118    let handle = unsafe { &*(handle as *const NativeHostScopeHandle) };
1119    match pop_scope(
1120        PopScopeParams::builder()
1121            .handle_uuid(&handle.0.uuid)
1122            .output_opt(output)
1123            .metadata_opt(metadata)
1124            .timestamp_opt(timestamp)
1125            .build(),
1126    ) {
1127        Ok(()) => NemoRelayStatus::Ok,
1128        Err(err) => status_from_flow_error(err),
1129    }
1130}
1131
1132unsafe extern "C" fn native_emit_mark(
1133    name: *const NemoRelayNativeString,
1134    parent: *const NemoRelayNativeScopeHandle,
1135    data_json: *const NemoRelayNativeString,
1136    metadata_json: *const NemoRelayNativeString,
1137    timestamp_unix_micros: *const i64,
1138) -> NemoRelayStatus {
1139    clear_native_last_error();
1140    let name = match read_name(name) {
1141        Ok(name) => name,
1142        Err(status) => return status,
1143    };
1144    let data = match optional_json_from_native_string(data_json, "mark data") {
1145        Ok(data) => data,
1146        Err(status) => return status,
1147    };
1148    let metadata = match optional_json_from_native_string(metadata_json, "mark metadata") {
1149        Ok(metadata) => metadata,
1150        Err(status) => return status,
1151    };
1152    let timestamp = match optional_timestamp_from_native(timestamp_unix_micros) {
1153        Ok(timestamp) => timestamp,
1154        Err(status) => return status,
1155    };
1156    let parent_ref = native_scope_ref(parent);
1157    match emit_scope_mark(
1158        EmitMarkEventParams::builder()
1159            .name(&name)
1160            .parent_opt(parent_ref)
1161            .data_opt(data)
1162            .metadata_opt(metadata)
1163            .timestamp_opt(timestamp)
1164            .build(),
1165    ) {
1166        Ok(()) => NemoRelayStatus::Ok,
1167        Err(err) => status_from_flow_error(err),
1168    }
1169}
1170
1171unsafe extern "C" fn native_scope_stack_create(
1172    out: *mut *mut NemoRelayNativeScopeStack,
1173) -> NemoRelayStatus {
1174    clear_native_last_error();
1175    if out.is_null() {
1176        set_native_last_error("out scope stack pointer is null");
1177        return NemoRelayStatus::NullPointer;
1178    }
1179    unsafe {
1180        *out = Box::into_raw(Box::new(NativeHostScopeStack(create_scope_stack()))).cast();
1181    }
1182    NemoRelayStatus::Ok
1183}
1184
1185unsafe extern "C" fn native_scope_stack_free(stack: *mut NemoRelayNativeScopeStack) {
1186    if !stack.is_null() {
1187        drop(unsafe { Box::from_raw(stack as *mut NativeHostScopeStack) });
1188    }
1189}
1190
1191unsafe extern "C" fn native_scope_stack_set_thread(
1192    stack: *const NemoRelayNativeScopeStack,
1193) -> NemoRelayStatus {
1194    clear_native_last_error();
1195    if stack.is_null() {
1196        set_native_last_error("scope stack is null");
1197        return NemoRelayStatus::NullPointer;
1198    }
1199    let stack = unsafe { &*(stack as *const NativeHostScopeStack) };
1200    set_thread_scope_stack(stack.0.clone());
1201    NemoRelayStatus::Ok
1202}
1203
1204unsafe extern "C" fn native_scope_stack_capture_thread(
1205    out: *mut *mut NemoRelayNativeScopeStackBinding,
1206) -> NemoRelayStatus {
1207    clear_native_last_error();
1208    if out.is_null() {
1209        set_native_last_error("out scope stack binding pointer is null");
1210        return NemoRelayStatus::NullPointer;
1211    }
1212    unsafe {
1213        *out = Box::into_raw(Box::new(NativeHostScopeStackBinding(
1214            capture_thread_scope_stack(),
1215        )))
1216        .cast();
1217    }
1218    NemoRelayStatus::Ok
1219}
1220
1221unsafe extern "C" fn native_scope_stack_restore_thread(
1222    binding: *mut NemoRelayNativeScopeStackBinding,
1223) -> NemoRelayStatus {
1224    clear_native_last_error();
1225    if binding.is_null() {
1226        set_native_last_error("scope stack binding is null");
1227        return NemoRelayStatus::NullPointer;
1228    }
1229    let binding = unsafe { Box::from_raw(binding as *mut NativeHostScopeStackBinding) };
1230    restore_thread_scope_stack(binding.0);
1231    NemoRelayStatus::Ok
1232}
1233
1234unsafe extern "C" fn native_scope_stack_binding_free(
1235    binding: *mut NemoRelayNativeScopeStackBinding,
1236) {
1237    if !binding.is_null() {
1238        drop(unsafe { Box::from_raw(binding as *mut NativeHostScopeStackBinding) });
1239    }
1240}
1241
1242unsafe extern "C" fn native_scope_stack_active() -> bool {
1243    scope_stack_active()
1244}
1245
1246unsafe extern "C" fn native_scope_stack_with_current(
1247    stack: *const NemoRelayNativeScopeStack,
1248    cb: NemoRelayNativeWithScopeStackCb,
1249    user_data: *mut c_void,
1250) -> NemoRelayStatus {
1251    clear_native_last_error();
1252    if stack.is_null() {
1253        set_native_last_error("scope stack is null");
1254        return NemoRelayStatus::NullPointer;
1255    }
1256    let stack = unsafe { &*(stack as *const NativeHostScopeStack) };
1257    let status = with_scope_stack(stack.0.clone(), || unsafe { cb(user_data) });
1258    if status != NemoRelayStatus::Ok && native_last_error_message().is_none() {
1259        set_native_last_error(format!("native scope-stack callback returned {status:?}"));
1260    }
1261    status
1262}
1263
1264fn flow_error_from_status(status: NemoRelayStatus, fallback: &str) -> FlowError {
1265    let message = native_last_error_message().unwrap_or_else(|| format!("{fallback}: {status:?}"));
1266    match status {
1267        NemoRelayStatus::AlreadyExists => FlowError::AlreadyExists(message),
1268        NemoRelayStatus::NotFound => FlowError::NotFound(message),
1269        NemoRelayStatus::ScopeStackEmpty => FlowError::ScopeStackEmpty,
1270        NemoRelayStatus::GuardrailRejected => FlowError::GuardrailRejected(message),
1271        NemoRelayStatus::InvalidArg => FlowError::InvalidArgument(message),
1272        _ => FlowError::Internal(message),
1273    }
1274}
1275
1276fn status_from_plugin_error(err: PluginError) -> NemoRelayStatus {
1277    set_native_last_error(err.to_string());
1278    match err {
1279        PluginError::NotFound(_) => NemoRelayStatus::NotFound,
1280        PluginError::Conflict(_) => NemoRelayStatus::AlreadyExists,
1281        PluginError::InvalidConfig(_) | PluginError::Serialization(_) => {
1282            NemoRelayStatus::InvalidArg
1283        }
1284        PluginError::Internal(_) | PluginError::RegistrationFailed(_) => NemoRelayStatus::Internal,
1285    }
1286}
1287
1288fn status_from_flow_error(err: FlowError) -> NemoRelayStatus {
1289    set_native_last_error(err.to_string());
1290    match err {
1291        FlowError::AlreadyExists(_) => NemoRelayStatus::AlreadyExists,
1292        FlowError::NotFound(_) => NemoRelayStatus::NotFound,
1293        FlowError::InvalidArgument(_) => NemoRelayStatus::InvalidArg,
1294        FlowError::ScopeStackEmpty => NemoRelayStatus::ScopeStackEmpty,
1295        FlowError::GuardrailRejected(_) => NemoRelayStatus::GuardrailRejected,
1296        FlowError::Upstream(_) | FlowError::Internal(_) | FlowError::CallbackException { .. } => {
1297            NemoRelayStatus::Internal
1298        }
1299    }
1300}
1301
1302fn panic_payload_message(payload: &(dyn std::any::Any + Send)) -> &str {
1303    payload
1304        .downcast_ref::<&str>()
1305        .copied()
1306        .or_else(|| payload.downcast_ref::<String>().map(String::as_str))
1307        .unwrap_or("unknown panic payload")
1308}
1309
1310fn native_runtime() -> &'static Runtime {
1311    static RUNTIME: OnceLock<Runtime> = OnceLock::new();
1312    RUNTIME.get_or_init(|| {
1313        tokio::runtime::Builder::new_current_thread()
1314            .enable_all()
1315            .build()
1316            .expect("native plugin runtime should build")
1317    })
1318}
1319
1320fn spawn_with_continuation_context<C, F, T>(
1321    context: MiddlewareContinuationContext,
1322    callback: C,
1323) -> std::thread::JoinHandle<T>
1324where
1325    C: FnOnce() -> F + Send + 'static,
1326    F: Future<Output = T> + Send + 'static,
1327    T: Send + 'static,
1328{
1329    let binding = capture_thread_scope_stack();
1330    std::thread::spawn(move || {
1331        restore_thread_scope_stack(binding);
1332        native_runtime().block_on(context.invoke(callback))
1333    })
1334}
1335
1336struct NativeCallbackUserData {
1337    ptr: *mut c_void,
1338    free_fn: NemoRelayNativeFreeFn,
1339    _instance: Option<Arc<NativePluginInstance>>,
1340}
1341
1342struct NativeCallbackUserDataGuard {
1343    ptr: *mut c_void,
1344    free_fn: NemoRelayNativeFreeFn,
1345    armed: bool,
1346}
1347
1348impl NativeCallbackUserDataGuard {
1349    fn new(ptr: *mut c_void, free_fn: NemoRelayNativeFreeFn) -> Self {
1350        Self {
1351            ptr,
1352            free_fn,
1353            armed: true,
1354        }
1355    }
1356
1357    fn transfer(mut self) -> (*mut c_void, NemoRelayNativeFreeFn) {
1358        self.armed = false;
1359        (self.ptr, self.free_fn)
1360    }
1361}
1362
1363impl Drop for NativeCallbackUserDataGuard {
1364    fn drop(&mut self) {
1365        if self.armed
1366            && let Some(free_fn) = self.free_fn
1367        {
1368            unsafe { free_fn(self.ptr) };
1369        }
1370    }
1371}
1372
1373unsafe impl Send for NativeCallbackUserData {}
1374unsafe impl Sync for NativeCallbackUserData {}
1375
1376impl Drop for NativeCallbackUserData {
1377    fn drop(&mut self) {
1378        if let Some(free_fn) = self.free_fn {
1379            unsafe { free_fn(self.ptr) };
1380        }
1381    }
1382}
1383
1384fn make_user_data(
1385    instance: Arc<NativePluginInstance>,
1386    user_data: *mut c_void,
1387    free_fn: NemoRelayNativeFreeFn,
1388) -> Arc<NativeCallbackUserData> {
1389    Arc::new(NativeCallbackUserData {
1390        ptr: user_data,
1391        free_fn,
1392        _instance: Some(instance),
1393    })
1394}
1395
1396const NATIVE_ASYNC_STREAM_CHANNEL_CAPACITY: usize = 64;
1397
1398struct NativeAsyncCompletion {
1399    sender: Mutex<Option<tokio::sync::oneshot::Sender<FlowResult<Json>>>>,
1400    cancelled: AtomicBool,
1401    next_invoked: AtomicBool,
1402    next_abort: Mutex<Option<tokio::task::AbortHandle>>,
1403    #[cfg(test)]
1404    before_settlement_lock: Option<Arc<std::sync::Barrier>>,
1405    // A pending native callback can continue running after its completion
1406    // wakes the awaiting task. Keep the callback's dynamic-library instance
1407    // alive until native code explicitly releases this handle.
1408    _callback_user_data: Option<Arc<NativeCallbackUserData>>,
1409}
1410
1411struct NativeAsyncWait {
1412    completion: Arc<NativeAsyncCompletion>,
1413    receiver: tokio::sync::oneshot::Receiver<FlowResult<Json>>,
1414    completed: bool,
1415}
1416
1417impl NativeAsyncWait {
1418    async fn receive(&mut self) -> FlowResult<Json> {
1419        let result = (&mut self.receiver).await.map_err(|_| {
1420            FlowError::Internal("native async callback dropped without settling".into())
1421        })?;
1422        self.completed = true;
1423        result
1424    }
1425}
1426
1427impl Drop for NativeAsyncWait {
1428    fn drop(&mut self) {
1429        if self.completed {
1430            return;
1431        }
1432        let mut next_abort = self
1433            .completion
1434            .next_abort
1435            .lock()
1436            .unwrap_or_else(|error| error.into_inner());
1437        self.completion.cancelled.store(true, Ordering::Release);
1438        if let Some(abort) = next_abort.take() {
1439            abort.abort();
1440        }
1441    }
1442}
1443
1444enum NativeAsyncNextInner {
1445    Tool(ToolExecutionNextFn),
1446    Llm(LlmExecutionNextFn),
1447    LlmStream(LlmStreamExecutionNextFn),
1448}
1449
1450struct NativeAsyncNext {
1451    inner: NativeAsyncNextInner,
1452    runtime: tokio::runtime::Handle,
1453    context: MiddlewareContinuationContext,
1454    // The native callback owns this handle independently of its completion.
1455    // Retaining the library here prevents an unload while it still uses `next`.
1456    _callback_user_data: Option<Arc<NativeCallbackUserData>>,
1457}
1458
1459impl NativeAsyncNext {
1460    fn new(
1461        inner: NativeAsyncNextInner,
1462        runtime: tokio::runtime::Handle,
1463        callback_user_data: Option<Arc<NativeCallbackUserData>>,
1464    ) -> Self {
1465        Self {
1466            inner,
1467            runtime,
1468            context: MiddlewareContinuationContext::capture(),
1469            _callback_user_data: callback_user_data,
1470        }
1471    }
1472}
1473
1474struct NativeAsyncStream {
1475    sender: Mutex<Option<tokio::sync::mpsc::Sender<FlowResult<Json>>>>,
1476    cancelled: AtomicBool,
1477    settled: AtomicBool,
1478    downstream_aborts: Mutex<HashMap<tokio::task::Id, tokio::task::AbortHandle>>,
1479    settlement: Mutex<()>,
1480    #[cfg(test)]
1481    before_settlement_lock: Option<Arc<std::sync::Barrier>>,
1482    _callback_user_data: Option<Arc<NativeCallbackUserData>>,
1483}
1484
1485struct NativeAsyncStreamReceiver {
1486    receiver: tokio::sync::mpsc::Receiver<FlowResult<Json>>,
1487    stream: Arc<NativeAsyncStream>,
1488}
1489
1490struct NativeAsyncStreamCallbackGuard {
1491    cb: NemoRelayNativeAsyncNextStreamCb,
1492    user_data: usize,
1493    stream: Arc<NativeAsyncStream>,
1494    _library_guard: Option<Arc<NativeCallbackUserData>>,
1495    active: bool,
1496}
1497
1498impl NativeAsyncStreamCallbackGuard {
1499    fn finish(&mut self) {
1500        self.active = false;
1501    }
1502
1503    fn fail(&mut self, error: &str) {
1504        if !self.active {
1505            return;
1506        }
1507        // Cancellation owns terminal delivery. Leave the guard active so its
1508        // Drop implementation can notify the plugin and release callback data.
1509        if self.stream.cancelled.load(Ordering::Acquire) {
1510            return;
1511        }
1512        if let Some(message) = native_string_from_str(error) {
1513            unsafe {
1514                let _ = (self.cb)(self.user_data as *mut c_void, ptr::null(), message, false);
1515                native_string_free(message);
1516            }
1517            self.active = false;
1518        }
1519    }
1520}
1521
1522impl Drop for NativeAsyncStreamCallbackGuard {
1523    fn drop(&mut self) {
1524        if !self.active {
1525            return;
1526        }
1527        if self.stream.cancelled.load(Ordering::Acquire) {
1528            if let Some(message) =
1529                native_string_from_str("native async stream continuation was cancelled")
1530            {
1531                unsafe {
1532                    let _ = (self.cb)(self.user_data as *mut c_void, ptr::null(), message, false);
1533                    native_string_free(message);
1534                }
1535            }
1536        } else if self.stream.settled.load(Ordering::Acquire) {
1537            if let Some(message) =
1538                native_string_from_str("native async stream continuation output settled")
1539            {
1540                unsafe {
1541                    let _ = (self.cb)(self.user_data as *mut c_void, ptr::null(), message, false);
1542                    native_string_free(message);
1543                }
1544            }
1545        } else {
1546            unsafe {
1547                let _ = (self.cb)(
1548                    self.user_data as *mut c_void,
1549                    ptr::null(),
1550                    ptr::null(),
1551                    true,
1552                );
1553            }
1554        }
1555    }
1556}
1557
1558impl Stream for NativeAsyncStreamReceiver {
1559    type Item = FlowResult<Json>;
1560
1561    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
1562        self.receiver.poll_recv(cx)
1563    }
1564}
1565
1566impl Drop for NativeAsyncStreamReceiver {
1567    fn drop(&mut self) {
1568        let _settlement = self
1569            .stream
1570            .settlement
1571            .lock()
1572            .unwrap_or_else(|error| error.into_inner());
1573        let mut downstream_aborts = self
1574            .stream
1575            .downstream_aborts
1576            .lock()
1577            .unwrap_or_else(|error| error.into_inner());
1578        self.stream.cancelled.store(true, Ordering::Release);
1579        for (_, abort) in downstream_aborts.drain() {
1580            abort.abort();
1581        }
1582        drop(downstream_aborts);
1583        self.stream
1584            .sender
1585            .lock()
1586            .unwrap_or_else(|error| error.into_inner())
1587            .take();
1588    }
1589}
1590
1591async fn invoke_native_async_callback(
1592    cb: NemoRelayNativeAsyncMiddlewareCb,
1593    user_data: Arc<NativeCallbackUserData>,
1594    invocation: Json,
1595    next: Option<NativeAsyncNextInner>,
1596) -> FlowResult<Json> {
1597    let runtime = if next.is_some() {
1598        Some(tokio::runtime::Handle::try_current().map_err(|error| {
1599            FlowError::Internal(format!(
1600                "native async intercept requires a Tokio runtime: {error}"
1601            ))
1602        })?)
1603    } else {
1604        None
1605    };
1606    let invocation = native_string_from_json(&invocation)
1607        .ok_or_else(|| FlowError::Internal("failed to allocate native async invocation".into()))?
1608        as usize;
1609    let (sender, receiver) = tokio::sync::oneshot::channel();
1610    let completion = Arc::new(NativeAsyncCompletion {
1611        sender: Mutex::new(Some(sender)),
1612        cancelled: AtomicBool::new(false),
1613        next_invoked: AtomicBool::new(false),
1614        next_abort: Mutex::new(None),
1615        #[cfg(test)]
1616        before_settlement_lock: None,
1617        _callback_user_data: Some(user_data.clone()),
1618    });
1619    let mut wait = NativeAsyncWait {
1620        completion: Arc::clone(&completion),
1621        receiver,
1622        completed: false,
1623    };
1624    let completion_ref = Arc::into_raw(completion.clone()) as usize;
1625    let next_ref = match (next, runtime) {
1626        (Some(inner), Some(runtime)) => Some(Arc::into_raw(Arc::new(NativeAsyncNext::new(
1627            inner,
1628            runtime,
1629            Some(user_data.clone()),
1630        ))) as usize),
1631        (None, None) => None,
1632        _ => unreachable!("runtime is present exactly for native async intercepts"),
1633    };
1634    let state = match catch_unwind(AssertUnwindSafe(|| unsafe {
1635        cb(
1636            user_data.ptr,
1637            invocation as *const NemoRelayNativeString,
1638            next_ref
1639                .map(|next| next as *const NemoRelayNativeAsyncNext)
1640                .unwrap_or(ptr::null()),
1641            completion_ref as *const NemoRelayNativeAsyncCompletion,
1642        )
1643    })) {
1644        Ok(state) => state,
1645        Err(_) => {
1646            unsafe {
1647                drop(Arc::from_raw(
1648                    completion_ref as *const NativeAsyncCompletion,
1649                ));
1650                native_string_free(invocation as *mut NemoRelayNativeString);
1651            }
1652            return Err(FlowError::Internal("native async callback panicked".into()));
1653        }
1654    };
1655    unsafe { native_string_free(invocation as *mut NemoRelayNativeString) };
1656    let state = match NemoRelayNativeAsyncCallbackState::try_from(state) {
1657        Ok(state) => state,
1658        Err(()) => {
1659            unsafe {
1660                drop(Arc::from_raw(
1661                    completion_ref as *const NativeAsyncCompletion,
1662                ));
1663            }
1664            return Err(FlowError::Internal(
1665                "native async callback returned an invalid state".into(),
1666            ));
1667        }
1668    };
1669    if state == NemoRelayNativeAsyncCallbackState::Complete {
1670        unsafe {
1671            drop(Arc::from_raw(
1672                completion_ref as *const NativeAsyncCompletion,
1673            ));
1674        }
1675        if completion
1676            .sender
1677            .lock()
1678            .unwrap_or_else(|error| error.into_inner())
1679            .is_some()
1680        {
1681            return Err(FlowError::Internal(
1682                "native async callback returned Complete without settling".into(),
1683            ));
1684        }
1685    }
1686    wait.receive().await
1687}
1688
1689unsafe extern "C" fn native_async_completion_resolve_json(
1690    completion: *const NemoRelayNativeAsyncCompletion,
1691    value_json: *const NemoRelayNativeString,
1692) -> NemoRelayStatus {
1693    let Some(completion) = (unsafe { (completion as *const NativeAsyncCompletion).as_ref() })
1694    else {
1695        return NemoRelayStatus::NullPointer;
1696    };
1697    let value = match parse_json_arg(value_json, "native async completion result") {
1698        Ok(value) => value,
1699        Err(status) => return status,
1700    };
1701    #[cfg(test)]
1702    if let Some(barrier) = &completion.before_settlement_lock {
1703        barrier.wait();
1704    }
1705    let mut next_abort = completion
1706        .next_abort
1707        .lock()
1708        .unwrap_or_else(|error| error.into_inner());
1709    if completion.cancelled.load(Ordering::Acquire) {
1710        return NemoRelayStatus::InvalidArg;
1711    }
1712    if let Some(abort) = next_abort.take() {
1713        abort.abort();
1714    }
1715    let Some(sender) = completion
1716        .sender
1717        .lock()
1718        .unwrap_or_else(|error| error.into_inner())
1719        .take()
1720    else {
1721        return NemoRelayStatus::InvalidArg;
1722    };
1723    let _ = sender.send(Ok(value));
1724    NemoRelayStatus::Ok
1725}
1726
1727unsafe extern "C" fn native_async_completion_reject(
1728    completion: *const NemoRelayNativeAsyncCompletion,
1729    message: *const NemoRelayNativeString,
1730) -> NemoRelayStatus {
1731    let Some(completion) = (unsafe { (completion as *const NativeAsyncCompletion).as_ref() })
1732    else {
1733        return NemoRelayStatus::NullPointer;
1734    };
1735    let message = if message.is_null() {
1736        "native async callback rejected".to_string()
1737    } else {
1738        match read_native_string(message) {
1739            Ok(message) => message,
1740            Err(error) => {
1741                set_native_last_error(error.to_string());
1742                return NemoRelayStatus::InvalidArg;
1743            }
1744        }
1745    };
1746    #[cfg(test)]
1747    if let Some(barrier) = &completion.before_settlement_lock {
1748        barrier.wait();
1749    }
1750    let mut next_abort = completion
1751        .next_abort
1752        .lock()
1753        .unwrap_or_else(|error| error.into_inner());
1754    if completion.cancelled.load(Ordering::Acquire) {
1755        return NemoRelayStatus::InvalidArg;
1756    }
1757    if let Some(abort) = next_abort.take() {
1758        abort.abort();
1759    }
1760    let Some(sender) = completion
1761        .sender
1762        .lock()
1763        .unwrap_or_else(|error| error.into_inner())
1764        .take()
1765    else {
1766        return NemoRelayStatus::InvalidArg;
1767    };
1768    let _ = sender.send(Err(FlowError::Internal(message)));
1769    NemoRelayStatus::Ok
1770}
1771
1772unsafe extern "C" fn native_async_completion_is_cancelled(
1773    completion: *const NemoRelayNativeAsyncCompletion,
1774) -> bool {
1775    unsafe { (completion as *const NativeAsyncCompletion).as_ref() }
1776        .is_none_or(|completion| completion.cancelled.load(Ordering::Acquire))
1777}
1778
1779unsafe extern "C" fn native_async_completion_release(
1780    completion: *const NemoRelayNativeAsyncCompletion,
1781) {
1782    if !completion.is_null() {
1783        unsafe { drop(Arc::from_raw(completion as *const NativeAsyncCompletion)) };
1784    }
1785}
1786
1787unsafe extern "C" fn native_async_next_release(next: *const NemoRelayNativeAsyncNext) {
1788    if !next.is_null() {
1789        unsafe { drop(Arc::from_raw(next as *const NativeAsyncNext)) };
1790    }
1791}
1792
1793unsafe extern "C" fn native_async_stream_push_json(
1794    stream: *const NemoRelayNativeAsyncStream,
1795    chunk_json: *const NemoRelayNativeString,
1796) -> NemoRelayStatus {
1797    clear_native_last_error();
1798    let Some(stream) = (unsafe { (stream as *const NativeAsyncStream).as_ref() }) else {
1799        return NemoRelayStatus::NullPointer;
1800    };
1801    if stream.cancelled.load(Ordering::Acquire) {
1802        return NemoRelayStatus::InvalidArg;
1803    }
1804    let chunk = match parse_json_arg(chunk_json, "native async stream chunk") {
1805        Ok(chunk) => chunk,
1806        Err(status) => return status,
1807    };
1808    #[cfg(test)]
1809    if let Some(barrier) = &stream.before_settlement_lock {
1810        barrier.wait();
1811    }
1812    let _settlement = stream
1813        .settlement
1814        .lock()
1815        .unwrap_or_else(|error| error.into_inner());
1816    if stream.cancelled.load(Ordering::Acquire) {
1817        return NemoRelayStatus::InvalidArg;
1818    }
1819    if stream.settled.load(Ordering::Acquire) {
1820        return NemoRelayStatus::InvalidArg;
1821    }
1822    let sender = stream
1823        .sender
1824        .lock()
1825        .unwrap_or_else(|error| error.into_inner())
1826        .clone();
1827    let Some(sender) = sender else {
1828        return NemoRelayStatus::InvalidArg;
1829    };
1830    match sender.try_send(Ok(chunk)) {
1831        Ok(()) => NemoRelayStatus::Ok,
1832        Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => {
1833            set_native_last_error(
1834                "native async stream is backpressured; retry the chunk after the consumer advances",
1835            );
1836            NemoRelayStatus::Internal
1837        }
1838        Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => NemoRelayStatus::InvalidArg,
1839    }
1840}
1841
1842unsafe extern "C" fn native_async_stream_finish(
1843    stream: *const NemoRelayNativeAsyncStream,
1844) -> NemoRelayStatus {
1845    let Some(stream) = (unsafe { (stream as *const NativeAsyncStream).as_ref() }) else {
1846        return NemoRelayStatus::NullPointer;
1847    };
1848    #[cfg(test)]
1849    if let Some(barrier) = &stream.before_settlement_lock {
1850        barrier.wait();
1851    }
1852    let _settlement = stream
1853        .settlement
1854        .lock()
1855        .unwrap_or_else(|error| error.into_inner());
1856    if stream.cancelled.load(Ordering::Acquire) {
1857        return NemoRelayStatus::InvalidArg;
1858    }
1859    if stream.settled.load(Ordering::Acquire) {
1860        return NemoRelayStatus::InvalidArg;
1861    }
1862    if stream
1863        .sender
1864        .lock()
1865        .unwrap_or_else(|error| error.into_inner())
1866        .take()
1867        .is_some()
1868    {
1869        stream.settled.store(true, Ordering::Release);
1870        let mut downstream_aborts = stream
1871            .downstream_aborts
1872            .lock()
1873            .unwrap_or_else(|error| error.into_inner());
1874        for (_, abort) in downstream_aborts.drain() {
1875            abort.abort();
1876        }
1877        NemoRelayStatus::Ok
1878    } else {
1879        NemoRelayStatus::InvalidArg
1880    }
1881}
1882
1883unsafe extern "C" fn native_async_stream_reject(
1884    stream: *const NemoRelayNativeAsyncStream,
1885    message: *const NemoRelayNativeString,
1886) -> NemoRelayStatus {
1887    clear_native_last_error();
1888    let Some(stream) = (unsafe { (stream as *const NativeAsyncStream).as_ref() }) else {
1889        return NemoRelayStatus::NullPointer;
1890    };
1891    let message =
1892        read_native_string(message).unwrap_or_else(|_| "native async stream rejected".to_string());
1893    #[cfg(test)]
1894    if let Some(barrier) = &stream.before_settlement_lock {
1895        barrier.wait();
1896    }
1897    let _settlement = stream
1898        .settlement
1899        .lock()
1900        .unwrap_or_else(|error| error.into_inner());
1901    if stream.cancelled.load(Ordering::Acquire) {
1902        return NemoRelayStatus::InvalidArg;
1903    }
1904    if stream.settled.load(Ordering::Acquire) {
1905        return NemoRelayStatus::InvalidArg;
1906    }
1907    let mut sender_guard = stream
1908        .sender
1909        .lock()
1910        .unwrap_or_else(|error| error.into_inner());
1911    let Some(sender) = sender_guard.as_ref() else {
1912        return NemoRelayStatus::InvalidArg;
1913    };
1914    match sender.try_send(Err(FlowError::Internal(message))) {
1915        Ok(()) => {
1916            sender_guard.take();
1917            stream.settled.store(true, Ordering::Release);
1918            let mut downstream_aborts = stream
1919                .downstream_aborts
1920                .lock()
1921                .unwrap_or_else(|error| error.into_inner());
1922            for (_, abort) in downstream_aborts.drain() {
1923                abort.abort();
1924            }
1925            NemoRelayStatus::Ok
1926        }
1927        Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => {
1928            set_native_last_error(
1929                "native async stream is backpressured; retry rejection after the consumer advances",
1930            );
1931            NemoRelayStatus::Internal
1932        }
1933        Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => NemoRelayStatus::InvalidArg,
1934    }
1935}
1936
1937unsafe extern "C" fn native_async_stream_is_cancelled(
1938    stream: *const NemoRelayNativeAsyncStream,
1939) -> bool {
1940    unsafe { (stream as *const NativeAsyncStream).as_ref() }
1941        .is_none_or(|stream| stream.cancelled.load(Ordering::Acquire))
1942}
1943
1944unsafe extern "C" fn native_async_stream_release(stream: *const NemoRelayNativeAsyncStream) {
1945    if !stream.is_null() {
1946        unsafe { drop(Arc::from_raw(stream as *const NativeAsyncStream)) };
1947    }
1948}
1949
1950/// Invokes the runtime continuation without blocking the calling native thread.
1951unsafe extern "C" fn native_async_next_invoke(
1952    next: *const NemoRelayNativeAsyncNext,
1953    invocation_json: *const NemoRelayNativeString,
1954    completion: *const NemoRelayNativeAsyncCompletion,
1955) -> NemoRelayStatus {
1956    let Some(next) = (unsafe { (next as *const NativeAsyncNext).as_ref() }) else {
1957        return NemoRelayStatus::NullPointer;
1958    };
1959    if completion.is_null() {
1960        return NemoRelayStatus::NullPointer;
1961    }
1962    let invocation = match parse_json_arg(invocation_json, "native async next invocation") {
1963        Ok(value) => value,
1964        Err(status) => return status,
1965    };
1966    if matches!(&next.inner, NativeAsyncNextInner::LlmStream(_)) {
1967        set_native_last_error(
1968            "stream continuations require async_next_invoke_stream; completion-based next cannot buffer a stream",
1969        );
1970        return NemoRelayStatus::InvalidArg;
1971    }
1972    enum Invocation {
1973        Tool(Json),
1974        Llm(LlmRequest),
1975    }
1976    let invocation = match &next.inner {
1977        NativeAsyncNextInner::Tool(_) => Invocation::Tool(invocation),
1978        NativeAsyncNextInner::Llm(_) => match serde_json::from_value(invocation) {
1979            Ok(request) => Invocation::Llm(request),
1980            Err(error) => {
1981                set_native_last_error(error.to_string());
1982                return NemoRelayStatus::InvalidJson;
1983            }
1984        },
1985        NativeAsyncNextInner::LlmStream(_) => unreachable!("stream continuations were rejected"),
1986    };
1987    unsafe { Arc::increment_strong_count(completion as *const NativeAsyncCompletion) };
1988    let completion = unsafe { Arc::from_raw(completion as *const NativeAsyncCompletion) };
1989    if completion.cancelled.load(Ordering::Acquire) {
1990        return NemoRelayStatus::InvalidArg;
1991    }
1992    let future: Pin<Box<dyn Future<Output = FlowResult<Json>> + Send>> =
1993        match (&next.inner, invocation) {
1994            (NativeAsyncNextInner::Tool(next), Invocation::Tool(invocation)) => {
1995                let next = next.clone();
1996                Box::pin(async move {
1997                    serde_json::to_value(ToolExecutionInterceptOutcome::new(
1998                        next(invocation).await?,
1999                    ))
2000                    .map_err(|error| {
2001                        FlowError::Internal(format!(
2002                            "failed to serialize native async tool outcome: {error}"
2003                        ))
2004                    })
2005                })
2006            }
2007            (NativeAsyncNextInner::Llm(next), Invocation::Llm(request)) => {
2008                let next = next.clone();
2009                Box::pin(async move { next(request).await })
2010            }
2011            _ => unreachable!("native next invocation kind matched its continuation"),
2012        };
2013    let continuation_context = match next.context.isolated_for_current_invocation() {
2014        Ok(context) => context,
2015        Err(error) => return status_from_flow_error(error),
2016    };
2017    let mut abort_guard = completion
2018        .next_abort
2019        .lock()
2020        .unwrap_or_else(|error| error.into_inner());
2021    if completion.cancelled.load(Ordering::Acquire) {
2022        return NemoRelayStatus::InvalidArg;
2023    }
2024    let unsettled = completion
2025        .sender
2026        .lock()
2027        .unwrap_or_else(|error| error.into_inner())
2028        .is_some();
2029    if !unsettled || completion.next_invoked.swap(true, Ordering::AcqRel) {
2030        set_native_last_error("native async next was already invoked for this completion");
2031        return NemoRelayStatus::InvalidArg;
2032    }
2033    let (start_tx, start_rx) = tokio::sync::oneshot::channel();
2034    let completion_for_task = Arc::clone(&completion);
2035    let task = next.runtime.spawn(async move {
2036        if start_rx.await.is_err() {
2037            return;
2038        }
2039        let result = AssertUnwindSafe(continuation_context.run(future))
2040            .catch_unwind()
2041            .await;
2042        let result = result.unwrap_or_else(|payload| {
2043            Err(FlowError::Internal(format!(
2044                "native async next continuation panicked: {}",
2045                panic_payload_message(payload.as_ref())
2046            )))
2047        });
2048        completion_for_task
2049            .next_abort
2050            .lock()
2051            .unwrap_or_else(|error| error.into_inner())
2052            .take();
2053        if let Some(sender) = completion_for_task
2054            .sender
2055            .lock()
2056            .unwrap_or_else(|error| error.into_inner())
2057            .take()
2058        {
2059            let _ = sender.send(result);
2060        }
2061    });
2062    let abort = task.abort_handle();
2063    *abort_guard = Some(abort.clone());
2064    if completion.cancelled.load(Ordering::Acquire) {
2065        abort_guard.take();
2066        abort.abort();
2067        return NemoRelayStatus::InvalidArg;
2068    }
2069    let _ = start_tx.send(());
2070    NemoRelayStatus::Ok
2071}
2072
2073/// Invokes a unary continuation with an independent per-call result callback.
2074unsafe extern "C" fn native_async_next_invoke_result(
2075    next: *const NemoRelayNativeAsyncNext,
2076    invocation_json: *const NemoRelayNativeString,
2077    cb: NemoRelayNativeAsyncNextResultCb,
2078    user_data: *mut c_void,
2079) -> NemoRelayStatus {
2080    let Some(next) = (unsafe { (next as *const NativeAsyncNext).as_ref() }) else {
2081        return NemoRelayStatus::NullPointer;
2082    };
2083    let invocation = match parse_json_arg(invocation_json, "native async next invocation") {
2084        Ok(value) => value,
2085        Err(status) => return status,
2086    };
2087    let future: Pin<Box<dyn Future<Output = FlowResult<Json>> + Send>> = match &next.inner {
2088        NativeAsyncNextInner::Tool(next_fn) => {
2089            let next_fn = next_fn.clone();
2090            Box::pin(async move { next_fn(invocation).await })
2091        }
2092        NativeAsyncNextInner::Llm(next_fn) => {
2093            let request = match serde_json::from_value(invocation) {
2094                Ok(request) => request,
2095                Err(error) => {
2096                    set_native_last_error(error.to_string());
2097                    return NemoRelayStatus::InvalidJson;
2098                }
2099            };
2100            let next_fn = next_fn.clone();
2101            Box::pin(async move { next_fn(request).await })
2102        }
2103        NativeAsyncNextInner::LlmStream(_) => {
2104            set_native_last_error(
2105                "stream continuations require async_next_invoke_stream; unary result callbacks cannot buffer a stream",
2106            );
2107            return NemoRelayStatus::InvalidArg;
2108        }
2109    };
2110    let continuation_context = match next.context.isolated_for_current_invocation() {
2111        Ok(context) => context,
2112        Err(error) => return status_from_flow_error(error),
2113    };
2114    let user_data = user_data as usize;
2115    let _library_guard = next._callback_user_data.clone();
2116    next.runtime.spawn(async move {
2117        let result = AssertUnwindSafe(continuation_context.run(future))
2118            .catch_unwind()
2119            .await
2120            .unwrap_or_else(|payload| {
2121                Err(FlowError::Internal(format!(
2122                    "native async next continuation panicked: {}",
2123                    panic_payload_message(payload.as_ref())
2124                )))
2125            });
2126        match result {
2127            Ok(value) => {
2128                if let Some(value) = native_string_from_json(&value) {
2129                    unsafe {
2130                        cb(user_data as *mut c_void, value, ptr::null());
2131                        native_string_free(value);
2132                    }
2133                } else if let Some(error) =
2134                    native_string_from_str("failed to allocate native async next result")
2135                {
2136                    unsafe {
2137                        cb(user_data as *mut c_void, ptr::null(), error);
2138                        native_string_free(error);
2139                    }
2140                }
2141            }
2142            Err(error) => {
2143                if let Some(error) = native_string_from_str(&error.to_string()) {
2144                    unsafe {
2145                        cb(user_data as *mut c_void, ptr::null(), error);
2146                        native_string_free(error);
2147                    }
2148                }
2149            }
2150        }
2151        drop(_library_guard);
2152    });
2153    NemoRelayStatus::Ok
2154}
2155
2156unsafe extern "C" fn native_async_next_invoke_stream(
2157    next: *const NemoRelayNativeAsyncNext,
2158    invocation_json: *const NemoRelayNativeString,
2159    output_stream: *const NemoRelayNativeAsyncStream,
2160    cb: NemoRelayNativeAsyncNextStreamCb,
2161    user_data: *mut c_void,
2162) -> NemoRelayStatus {
2163    let Some(next) = (unsafe { (next as *const NativeAsyncNext).as_ref() }) else {
2164        return NemoRelayStatus::NullPointer;
2165    };
2166    if output_stream.is_null() {
2167        return NemoRelayStatus::NullPointer;
2168    }
2169    unsafe { Arc::increment_strong_count(output_stream as *const NativeAsyncStream) };
2170    let output_stream = unsafe { Arc::from_raw(output_stream as *const NativeAsyncStream) };
2171    let NativeAsyncNextInner::LlmStream(next_fn) = &next.inner else {
2172        return NemoRelayStatus::InvalidArg;
2173    };
2174    let request = match parse_json_arg(invocation_json, "native async stream next invocation")
2175        .and_then(|value| {
2176            serde_json::from_value(value).map_err(|error| {
2177                set_native_last_error(error.to_string());
2178                NemoRelayStatus::InvalidJson
2179            })
2180        }) {
2181        Ok(request) => request,
2182        Err(status) => return status,
2183    };
2184    let continuation_context = match next.context.isolated_for_current_invocation() {
2185        Ok(context) => context,
2186        Err(error) => return status_from_flow_error(error),
2187    };
2188    let _settlement = output_stream
2189        .settlement
2190        .lock()
2191        .unwrap_or_else(|error| error.into_inner());
2192    if output_stream.cancelled.load(Ordering::Acquire)
2193        || output_stream.settled.load(Ordering::Acquire)
2194        || output_stream
2195            .sender
2196            .lock()
2197            .unwrap_or_else(|error| error.into_inner())
2198            .is_none()
2199    {
2200        return NemoRelayStatus::InvalidArg;
2201    }
2202    let mut downstream_aborts = output_stream
2203        .downstream_aborts
2204        .lock()
2205        .unwrap_or_else(|error| error.into_inner());
2206    let next_fn = next_fn.clone();
2207    let user_data = user_data as usize;
2208    let output_stream_for_task = Arc::clone(&output_stream);
2209    let output_stream_for_cleanup = Arc::clone(&output_stream);
2210    let callback_guard = NativeAsyncStreamCallbackGuard {
2211        cb,
2212        user_data,
2213        stream: output_stream_for_task,
2214        _library_guard: next._callback_user_data.clone(),
2215        active: true,
2216    };
2217    let (start_tx, start_rx) = tokio::sync::oneshot::channel();
2218    let task = next.runtime.spawn(async move {
2219        if start_rx.await.is_err() {
2220            return;
2221        }
2222        continuation_context
2223            .run(deliver_native_async_next_stream(
2224                next_fn,
2225                request,
2226                callback_guard,
2227            ))
2228            .await;
2229        output_stream_for_cleanup
2230            .downstream_aborts
2231            .lock()
2232            .unwrap_or_else(|error| error.into_inner())
2233            .remove(&tokio::task::id());
2234    });
2235    let abort = task.abort_handle();
2236    downstream_aborts.insert(task.id(), abort);
2237    let _ = start_tx.send(());
2238    NemoRelayStatus::Ok
2239}
2240
2241async fn deliver_native_async_next_stream(
2242    next_fn: LlmStreamExecutionNextFn,
2243    request: LlmRequest,
2244    mut callback_guard: NativeAsyncStreamCallbackGuard,
2245) {
2246    let result = AssertUnwindSafe(async {
2247        match next_fn(request).await {
2248            Ok(stream) => forward_native_async_next_stream(stream, &mut callback_guard).await,
2249            Err(error) => callback_guard.fail(&error.to_string()),
2250        }
2251    })
2252    .catch_unwind()
2253    .await;
2254    if let Err(payload) = result {
2255        callback_guard.fail(&format!(
2256            "native async stream continuation panicked: {}",
2257            panic_payload_message(payload.as_ref())
2258        ));
2259    }
2260}
2261
2262async fn forward_native_async_next_stream(
2263    stream: LlmJsonStream,
2264    callback_guard: &mut NativeAsyncStreamCallbackGuard,
2265) {
2266    forward_native_async_next_stream_with(stream, callback_guard, native_string_from_json).await;
2267}
2268
2269async fn forward_native_async_next_stream_with(
2270    mut stream: LlmJsonStream,
2271    callback_guard: &mut NativeAsyncStreamCallbackGuard,
2272    to_native_string: impl Fn(&Json) -> Option<*mut NemoRelayNativeString>,
2273) {
2274    while let Some(item) = stream.next().await {
2275        match item {
2276            Ok(chunk) => {
2277                let Some(chunk) = to_native_string(&chunk) else {
2278                    callback_guard.fail(
2279                        "failed to serialize or allocate native async stream continuation chunk",
2280                    );
2281                    return;
2282                };
2283                let keep_going = unsafe {
2284                    (callback_guard.cb)(
2285                        callback_guard.user_data as *mut c_void,
2286                        chunk,
2287                        ptr::null(),
2288                        false,
2289                    )
2290                };
2291                unsafe { native_string_free(chunk) };
2292                if !keep_going {
2293                    callback_guard.finish();
2294                    return;
2295                }
2296            }
2297            Err(error) => {
2298                callback_guard.fail(&error.to_string());
2299                return;
2300            }
2301        }
2302    }
2303    unsafe {
2304        let _ = (callback_guard.cb)(
2305            callback_guard.user_data as *mut c_void,
2306            ptr::null(),
2307            ptr::null(),
2308            true,
2309        );
2310    }
2311    callback_guard.finish();
2312}
2313
2314fn wrap_native_async_tool_json(
2315    instance: Arc<NativePluginInstance>,
2316    cb: NemoRelayNativeAsyncMiddlewareCb,
2317    user_data: *mut c_void,
2318    free_fn: NemoRelayNativeFreeFn,
2319) -> ToolSanitizeFn {
2320    let user_data = make_user_data(instance, user_data, free_fn);
2321    Arc::new(move |name, value| {
2322        let user_data = user_data.clone();
2323        Box::pin(async move {
2324            let value = invoke_native_async_callback(
2325                cb,
2326                user_data,
2327                serde_json::json!({"name": name, "value": value}),
2328                None,
2329            )
2330            .await?;
2331            Ok(value)
2332        })
2333    })
2334}
2335
2336fn wrap_native_async_tool_conditional(
2337    instance: Arc<NativePluginInstance>,
2338    cb: NemoRelayNativeAsyncMiddlewareCb,
2339    user_data: *mut c_void,
2340    free_fn: NemoRelayNativeFreeFn,
2341) -> ToolConditionalFn {
2342    let user_data = make_user_data(instance, user_data, free_fn);
2343    Arc::new(move |name, value| {
2344        let user_data = user_data.clone();
2345        Box::pin(async move {
2346            match invoke_native_async_callback(
2347                cb,
2348                user_data,
2349                serde_json::json!({"name": name, "value": value}),
2350                None,
2351            )
2352            .await?
2353            {
2354                Json::Null => Ok(None),
2355                Json::String(reason) => Ok(Some(reason)),
2356                other => Err(FlowError::Internal(format!(
2357                    "native async tool conditional callback returned {other}; expected string or null"
2358                ))),
2359            }
2360        })
2361    })
2362}
2363
2364fn wrap_native_async_llm_conditional(
2365    instance: Arc<NativePluginInstance>,
2366    cb: NemoRelayNativeAsyncMiddlewareCb,
2367    user_data: *mut c_void,
2368    free_fn: NemoRelayNativeFreeFn,
2369) -> LlmConditionalFn {
2370    let user_data = make_user_data(instance, user_data, free_fn);
2371    Arc::new(move |request| {
2372        let user_data = user_data.clone();
2373        Box::pin(async move {
2374            match invoke_native_async_callback(
2375                cb,
2376                user_data,
2377                serde_json::json!({"request": request}),
2378                None,
2379            )
2380            .await?
2381            {
2382                Json::Null => Ok(None),
2383                Json::String(reason) => Ok(Some(reason)),
2384                other => Err(FlowError::Internal(format!(
2385                    "native async LLM conditional callback returned {other}; expected string or null"
2386                ))),
2387            }
2388        })
2389    })
2390}
2391
2392fn wrap_native_async_llm_sanitize_request(
2393    instance: Arc<NativePluginInstance>,
2394    cb: NemoRelayNativeAsyncMiddlewareCb,
2395    user_data: *mut c_void,
2396    free_fn: NemoRelayNativeFreeFn,
2397) -> LlmSanitizeRequestFn {
2398    let user_data = make_user_data(instance, user_data, free_fn);
2399    Arc::new(move |request, context| {
2400        let user_data = user_data.clone();
2401        let codec = native_async_codec_identity(context.codec());
2402        Box::pin(async move {
2403            let value = invoke_native_async_callback(
2404                cb,
2405                user_data,
2406                serde_json::json!({"request": request, "context": codec}),
2407                None,
2408            )
2409            .await?;
2410            if value.is_null() {
2411                Ok(None)
2412            } else {
2413                serde_json::from_value(value)
2414                    .map(Some)
2415                    .map_err(|error| FlowError::Internal(error.to_string()))
2416            }
2417        })
2418    })
2419}
2420
2421fn wrap_native_async_llm_sanitize_response(
2422    instance: Arc<NativePluginInstance>,
2423    cb: NemoRelayNativeAsyncMiddlewareCb,
2424    user_data: *mut c_void,
2425    free_fn: NemoRelayNativeFreeFn,
2426) -> LlmSanitizeResponseFn {
2427    let user_data = make_user_data(instance, user_data, free_fn);
2428    Arc::new(move |response, context| {
2429        let user_data = user_data.clone();
2430        let codec = native_async_codec_identity(context.codec());
2431        Box::pin(async move {
2432            let value = invoke_native_async_callback(
2433                cb,
2434                user_data,
2435                serde_json::json!({"response": response, "context": codec}),
2436                None,
2437            )
2438            .await?;
2439            Ok((!value.is_null()).then_some(value))
2440        })
2441    })
2442}
2443
2444fn native_async_codec_identity(identity: &LlmCodecIdentity) -> Json {
2445    match identity {
2446        LlmCodecIdentity::None => {
2447            serde_json::json!({"codec_kind": "none", "codec_id": Json::Null})
2448        }
2449        LlmCodecIdentity::BuiltIn(codec) => {
2450            serde_json::json!({"codec_kind": "builtin", "codec_id": codec.id()})
2451        }
2452        LlmCodecIdentity::Runtime(id) => {
2453            serde_json::json!({"codec_kind": "runtime", "codec_id": id})
2454        }
2455        LlmCodecIdentity::Opaque => {
2456            serde_json::json!({"codec_kind": "opaque", "codec_id": Json::Null})
2457        }
2458    }
2459}
2460
2461fn wrap_native_async_llm_request_intercept(
2462    instance: Arc<NativePluginInstance>,
2463    cb: NemoRelayNativeAsyncMiddlewareCb,
2464    user_data: *mut c_void,
2465    free_fn: NemoRelayNativeFreeFn,
2466) -> LlmRequestInterceptFn {
2467    let user_data = make_user_data(instance, user_data, free_fn);
2468    Arc::new(move |name, request, annotated| {
2469        let user_data = user_data.clone();
2470        Box::pin(async move {
2471            serde_json::from_value(
2472                invoke_native_async_callback(
2473                    cb,
2474                    user_data,
2475                    serde_json::json!({
2476                        "name": name,
2477                        "request": request,
2478                        "annotated": annotated,
2479                    }),
2480                    None,
2481                )
2482                .await?,
2483            )
2484            .map_err(|error| {
2485                FlowError::Internal(format!(
2486                    "invalid native async LLM intercept outcome: {error}"
2487                ))
2488            })
2489        })
2490    })
2491}
2492
2493fn wrap_native_async_event_sanitize(
2494    instance: Arc<NativePluginInstance>,
2495    cb: NemoRelayNativeAsyncMiddlewareCb,
2496    user_data: *mut c_void,
2497    free_fn: NemoRelayNativeFreeFn,
2498) -> EventSanitizeFn {
2499    let user_data = make_user_data(instance, user_data, free_fn);
2500    Arc::new(move |event, fields| {
2501        let user_data = user_data.clone();
2502        Box::pin(async move {
2503            serde_json::from_value(
2504                invoke_native_async_callback(
2505                    cb,
2506                    user_data,
2507                    serde_json::json!({"event": event, "fields": fields}),
2508                    None,
2509                )
2510                .await?,
2511            )
2512            .map_err(|error| {
2513                FlowError::Internal(format!("invalid native async event fields: {error}"))
2514            })
2515        })
2516    })
2517}
2518
2519fn wrap_native_async_tool_execution(
2520    instance: Arc<NativePluginInstance>,
2521    cb: NemoRelayNativeAsyncMiddlewareCb,
2522    user_data: *mut c_void,
2523    free_fn: NemoRelayNativeFreeFn,
2524) -> ToolExecutionFn {
2525    let user_data = make_user_data(instance, user_data, free_fn);
2526    Arc::new(move |name, args, next| {
2527        let user_data = user_data.clone();
2528        let invocation = serde_json::json!({"name": name, "value": args});
2529        Box::pin(async move {
2530            serde_json::from_value(
2531                invoke_native_async_callback(
2532                    cb,
2533                    user_data,
2534                    invocation,
2535                    Some(NativeAsyncNextInner::Tool(next)),
2536                )
2537                .await?,
2538            )
2539            .map_err(|error| {
2540                FlowError::Internal(format!("invalid native async tool outcome: {error}"))
2541            })
2542        })
2543    })
2544}
2545
2546fn wrap_native_async_llm_execution(
2547    instance: Arc<NativePluginInstance>,
2548    cb: NemoRelayNativeAsyncMiddlewareCb,
2549    user_data: *mut c_void,
2550    free_fn: NemoRelayNativeFreeFn,
2551) -> LlmExecutionFn {
2552    let user_data = make_user_data(instance, user_data, free_fn);
2553    Arc::new(move |name, request, next| {
2554        let user_data = user_data.clone();
2555        let name = name.to_owned();
2556        Box::pin(async move {
2557            invoke_native_async_callback(
2558                cb,
2559                user_data,
2560                serde_json::json!({"name": name, "request": request}),
2561                Some(NativeAsyncNextInner::Llm(next)),
2562            )
2563            .await
2564        })
2565    })
2566}
2567
2568fn wrap_native_incremental_llm_stream_execution(
2569    instance: Arc<NativePluginInstance>,
2570    cb: NemoRelayNativeAsyncStreamMiddlewareCb,
2571    user_data: *mut c_void,
2572    free_fn: NemoRelayNativeFreeFn,
2573) -> LlmStreamExecutionFn {
2574    let user_data = make_user_data(instance, user_data, free_fn);
2575    wrap_native_incremental_llm_stream_execution_with_user_data(cb, user_data)
2576}
2577
2578fn wrap_native_incremental_llm_stream_execution_with_user_data(
2579    cb: NemoRelayNativeAsyncStreamMiddlewareCb,
2580    user_data: Arc<NativeCallbackUserData>,
2581) -> LlmStreamExecutionFn {
2582    Arc::new(move |name, request, next| {
2583        let user_data = user_data.clone();
2584        let name = name.to_owned();
2585        Box::pin(async move {
2586            let (sender, receiver) =
2587                tokio::sync::mpsc::channel(NATIVE_ASYNC_STREAM_CHANNEL_CAPACITY);
2588            let stream = Arc::new(NativeAsyncStream {
2589                sender: Mutex::new(Some(sender)),
2590                cancelled: AtomicBool::new(false),
2591                settled: AtomicBool::new(false),
2592                downstream_aborts: Mutex::new(HashMap::new()),
2593                settlement: Mutex::new(()),
2594                #[cfg(test)]
2595                before_settlement_lock: None,
2596                _callback_user_data: Some(user_data.clone()),
2597            });
2598            let output = NativeAsyncStreamReceiver {
2599                receiver,
2600                stream: Arc::clone(&stream),
2601            };
2602            let state = {
2603                let invocation =
2604                    native_string_from_json(&serde_json::json!({"name": name, "request": request}))
2605                        .ok_or_else(|| {
2606                            FlowError::Internal(
2607                                "failed to allocate native async stream invocation".into(),
2608                            )
2609                        })?;
2610                let runtime = tokio::runtime::Handle::try_current().map_err(|error| {
2611                    FlowError::Internal(format!(
2612                        "native async stream intercept requires a Tokio runtime: {error}"
2613                    ))
2614                })?;
2615                let next_ref = Arc::into_raw(Arc::new(NativeAsyncNext::new(
2616                    NativeAsyncNextInner::LlmStream(next),
2617                    runtime,
2618                    Some(user_data.clone()),
2619                )));
2620                let stream_ref = Arc::into_raw(stream.clone());
2621                let state = catch_unwind(AssertUnwindSafe(|| unsafe {
2622                    cb(
2623                        user_data.ptr,
2624                        invocation,
2625                        next_ref as *const NemoRelayNativeAsyncNext,
2626                        stream_ref as *const NemoRelayNativeAsyncStream,
2627                    )
2628                }));
2629                unsafe { native_string_free(invocation) };
2630                state
2631                    .ok()
2632                    .and_then(|state| NemoRelayNativeAsyncCallbackState::try_from(state).ok())
2633                    .ok_or_else(|| {
2634                        FlowError::Internal(
2635                            "native async stream callback panicked or returned an invalid state"
2636                                .into(),
2637                        )
2638                    })?
2639            };
2640            if state == NemoRelayNativeAsyncCallbackState::Complete
2641                && stream
2642                    .sender
2643                    .lock()
2644                    .unwrap_or_else(|error| error.into_inner())
2645                    .is_some()
2646            {
2647                return Err(FlowError::Internal(
2648                    "native async stream callback returned Complete without finishing".into(),
2649                ));
2650            }
2651            Ok(LlmJsonStream::new(output))
2652        })
2653    })
2654}
2655
2656unsafe extern "C" fn native_plugin_context_register_async_stream_middleware(
2657    ctx: *mut NemoRelayNativePluginContext,
2658    name: *const NemoRelayNativeString,
2659    priority: i32,
2660    cb: NemoRelayNativeAsyncStreamMiddlewareCb,
2661    user_data: *mut c_void,
2662    free_fn: NemoRelayNativeFreeFn,
2663) -> NemoRelayStatus {
2664    clear_native_last_error();
2665    let user_data_guard = NativeCallbackUserDataGuard::new(user_data, free_fn);
2666    let host_ctx = match host_ctx_mut(ctx) {
2667        Ok(ctx) => ctx,
2668        Err(status) => return status,
2669    };
2670    let instance = host_ctx.instance.clone();
2671    let name = match read_name(name) {
2672        Ok(name) => name,
2673        Err(status) => return status,
2674    };
2675    let (user_data, free_fn) = user_data_guard.transfer();
2676    let context = unsafe { &mut *host_ctx.ctx };
2677    match context.register_llm_stream_execution_intercept(
2678        &name,
2679        priority,
2680        wrap_native_incremental_llm_stream_execution(instance, cb, user_data, free_fn),
2681    ) {
2682        Ok(()) => NemoRelayStatus::Ok,
2683        Err(error) => status_from_plugin_error(error),
2684    }
2685}
2686
2687unsafe extern "C" fn native_plugin_context_register_async_middleware(
2688    ctx: *mut NemoRelayNativePluginContext,
2689    kind: u32,
2690    name: *const NemoRelayNativeString,
2691    priority: i32,
2692    break_chain: bool,
2693    cb: NemoRelayNativeAsyncMiddlewareCb,
2694    user_data: *mut c_void,
2695    free_fn: NemoRelayNativeFreeFn,
2696) -> NemoRelayStatus {
2697    clear_native_last_error();
2698    // The host owns callback user data as soon as registration is attempted,
2699    // including malformed and incompatible registrations.
2700    let user_data_guard = NativeCallbackUserDataGuard::new(user_data, free_fn);
2701    let host_ctx = match host_ctx_mut(ctx) {
2702        Ok(ctx) => ctx,
2703        Err(status) => return status,
2704    };
2705    let instance = host_ctx.instance.clone();
2706    let name = match read_name(name) {
2707        Ok(name) => name,
2708        Err(status) => return status,
2709    };
2710    let kind = match NemoRelayNativeAsyncMiddlewareKind::try_from(kind) {
2711        Ok(kind) => kind,
2712        Err(()) => {
2713            set_native_last_error("invalid native async middleware kind");
2714            return NemoRelayStatus::InvalidArg;
2715        }
2716    };
2717    if kind == NemoRelayNativeAsyncMiddlewareKind::LlmStreamExecutionIntercept {
2718        set_native_last_error(
2719            "completion-based LLM stream middleware is unsupported; use plugin_context_register_async_stream_middleware",
2720        );
2721        return NemoRelayStatus::InvalidArg;
2722    }
2723    if kind == NemoRelayNativeAsyncMiddlewareKind::LlmRequestIntercept
2724        && let Err(error) = validate_annotated_request_consumer_compatibility(
2725            &instance.relay_compat,
2726            &instance.plugin_kind,
2727        )
2728    {
2729        return status_from_plugin_error(error);
2730    }
2731    let (user_data, free_fn) = user_data_guard.transfer();
2732    let context = unsafe { &mut *host_ctx.ctx };
2733    let registration = match kind {
2734        NemoRelayNativeAsyncMiddlewareKind::ToolSanitizeRequest => context
2735            .register_tool_sanitize_request_guardrail(
2736                &name,
2737                priority,
2738                wrap_native_async_tool_json(instance, cb, user_data, free_fn),
2739            ),
2740        NemoRelayNativeAsyncMiddlewareKind::ToolSanitizeResponse => context
2741            .register_tool_sanitize_response_guardrail(
2742                &name,
2743                priority,
2744                wrap_native_async_tool_json(instance, cb, user_data, free_fn),
2745            ),
2746        NemoRelayNativeAsyncMiddlewareKind::ToolConditionalExecution => context
2747            .register_tool_conditional_execution_guardrail(
2748                &name,
2749                priority,
2750                wrap_native_async_tool_conditional(instance, cb, user_data, free_fn),
2751            ),
2752        NemoRelayNativeAsyncMiddlewareKind::ToolRequestIntercept => context
2753            .register_tool_request_intercept(
2754                &name,
2755                priority,
2756                break_chain,
2757                wrap_native_async_tool_json(instance, cb, user_data, free_fn),
2758            ),
2759        NemoRelayNativeAsyncMiddlewareKind::ToolExecutionIntercept => context
2760            .register_tool_execution_intercept(
2761                &name,
2762                priority,
2763                wrap_native_async_tool_execution(instance, cb, user_data, free_fn),
2764            ),
2765        NemoRelayNativeAsyncMiddlewareKind::LlmSanitizeRequest => context
2766            .register_llm_sanitize_request_guardrail(
2767                &name,
2768                priority,
2769                wrap_native_async_llm_sanitize_request(instance, cb, user_data, free_fn),
2770            ),
2771        NemoRelayNativeAsyncMiddlewareKind::LlmSanitizeResponse => context
2772            .register_llm_sanitize_response_guardrail(
2773                &name,
2774                priority,
2775                wrap_native_async_llm_sanitize_response(instance, cb, user_data, free_fn),
2776            ),
2777        NemoRelayNativeAsyncMiddlewareKind::LlmConditionalExecution => context
2778            .register_llm_conditional_execution_guardrail(
2779                &name,
2780                priority,
2781                wrap_native_async_llm_conditional(instance, cb, user_data, free_fn),
2782            ),
2783        NemoRelayNativeAsyncMiddlewareKind::LlmRequestIntercept => context
2784            .register_llm_request_intercept(
2785                &name,
2786                priority,
2787                break_chain,
2788                wrap_native_async_llm_request_intercept(instance, cb, user_data, free_fn),
2789            ),
2790        NemoRelayNativeAsyncMiddlewareKind::LlmExecutionIntercept => context
2791            .register_llm_execution_intercept(
2792                &name,
2793                priority,
2794                wrap_native_async_llm_execution(instance, cb, user_data, free_fn),
2795            ),
2796        NemoRelayNativeAsyncMiddlewareKind::LlmStreamExecutionIntercept => {
2797            unreachable!("completion-based stream middleware was rejected before registration")
2798        }
2799        NemoRelayNativeAsyncMiddlewareKind::MarkSanitize => context
2800            .register_mark_sanitize_guardrail(
2801                &name,
2802                priority,
2803                wrap_native_async_event_sanitize(instance, cb, user_data, free_fn),
2804            ),
2805        NemoRelayNativeAsyncMiddlewareKind::ScopeSanitizeStart => context
2806            .register_scope_sanitize_start_guardrail(
2807                &name,
2808                priority,
2809                wrap_native_async_event_sanitize(instance, cb, user_data, free_fn),
2810            ),
2811        NemoRelayNativeAsyncMiddlewareKind::ScopeSanitizeEnd => context
2812            .register_scope_sanitize_end_guardrail(
2813                &name,
2814                priority,
2815                wrap_native_async_event_sanitize(instance, cb, user_data, free_fn),
2816            ),
2817    };
2818    match registration {
2819        Ok(()) => NemoRelayStatus::Ok,
2820        Err(error) => status_from_plugin_error(error),
2821    }
2822}
2823
2824fn host_ctx_mut<'a>(
2825    ctx: *mut NemoRelayNativePluginContext,
2826) -> Result<&'a mut NativeHostPluginContext, NemoRelayStatus> {
2827    if ctx.is_null() {
2828        set_native_last_error("plugin context is null");
2829        return Err(NemoRelayStatus::NullPointer);
2830    }
2831    let ctx = unsafe { &mut *(ctx as *mut NativeHostPluginContext) };
2832    if ctx.ctx.is_null() {
2833        set_native_last_error("plugin context inner pointer is null");
2834        return Err(NemoRelayStatus::NullPointer);
2835    }
2836    Ok(ctx)
2837}
2838
2839fn read_name(name: *const NemoRelayNativeString) -> Result<String, NemoRelayStatus> {
2840    read_native_string(name).map_err(|err| {
2841        set_native_last_error(err.to_string());
2842        NemoRelayStatus::InvalidUtf8
2843    })
2844}
2845
2846unsafe extern "C" fn native_plugin_context_register_subscriber(
2847    ctx: *mut NemoRelayNativePluginContext,
2848    name: *const NemoRelayNativeString,
2849    cb: NemoRelayNativeEventSubscriberCb,
2850    user_data: *mut c_void,
2851    free_fn: NemoRelayNativeFreeFn,
2852) -> NemoRelayStatus {
2853    clear_native_last_error();
2854    let host_ctx = match host_ctx_mut(ctx) {
2855        Ok(ctx) => ctx,
2856        Err(status) => return status,
2857    };
2858    let instance = host_ctx.instance.clone();
2859    let ctx = unsafe { &mut *host_ctx.ctx };
2860    let name = match read_name(name) {
2861        Ok(name) => name,
2862        Err(status) => return status,
2863    };
2864    match ctx.register_subscriber(
2865        &name,
2866        wrap_event_subscriber(instance, cb, user_data, free_fn),
2867    ) {
2868        Ok(()) => NemoRelayStatus::Ok,
2869        Err(err) => status_from_plugin_error(err),
2870    }
2871}
2872
2873macro_rules! native_tool_json_context_register {
2874    ($fn_name:ident, $ctx_method:ident) => {
2875        unsafe extern "C" fn $fn_name(
2876            ctx: *mut NemoRelayNativePluginContext,
2877            name: *const NemoRelayNativeString,
2878            priority: i32,
2879            cb: NemoRelayNativeToolJsonCb,
2880            user_data: *mut c_void,
2881            free_fn: NemoRelayNativeFreeFn,
2882        ) -> NemoRelayStatus {
2883            clear_native_last_error();
2884            let host_ctx = match host_ctx_mut(ctx) {
2885                Ok(ctx) => ctx,
2886                Err(status) => return status,
2887            };
2888            let instance = host_ctx.instance.clone();
2889            let ctx = unsafe { &mut *host_ctx.ctx };
2890            let name = match read_name(name) {
2891                Ok(name) => name,
2892                Err(status) => return status,
2893            };
2894            match ctx.$ctx_method(
2895                &name,
2896                priority,
2897                wrap_tool_json_fn(instance, cb, user_data, free_fn),
2898            ) {
2899                Ok(()) => NemoRelayStatus::Ok,
2900                Err(err) => status_from_plugin_error(err),
2901            }
2902        }
2903    };
2904}
2905
2906native_tool_json_context_register!(
2907    native_plugin_context_register_tool_sanitize_request_guardrail,
2908    register_tool_sanitize_request_guardrail
2909);
2910native_tool_json_context_register!(
2911    native_plugin_context_register_tool_sanitize_response_guardrail,
2912    register_tool_sanitize_response_guardrail
2913);
2914
2915unsafe extern "C" fn native_plugin_context_register_tool_conditional_execution_guardrail(
2916    ctx: *mut NemoRelayNativePluginContext,
2917    name: *const NemoRelayNativeString,
2918    priority: i32,
2919    cb: NemoRelayNativeToolConditionalCb,
2920    user_data: *mut c_void,
2921    free_fn: NemoRelayNativeFreeFn,
2922) -> NemoRelayStatus {
2923    clear_native_last_error();
2924    let host_ctx = match host_ctx_mut(ctx) {
2925        Ok(ctx) => ctx,
2926        Err(status) => return status,
2927    };
2928    let instance = host_ctx.instance.clone();
2929    let ctx = unsafe { &mut *host_ctx.ctx };
2930    let name = match read_name(name) {
2931        Ok(name) => name,
2932        Err(status) => return status,
2933    };
2934    match ctx.register_tool_conditional_execution_guardrail(
2935        &name,
2936        priority,
2937        wrap_tool_conditional_fn(instance, cb, user_data, free_fn),
2938    ) {
2939        Ok(()) => NemoRelayStatus::Ok,
2940        Err(err) => status_from_plugin_error(err),
2941    }
2942}
2943
2944unsafe extern "C" fn native_plugin_context_register_tool_request_intercept(
2945    ctx: *mut NemoRelayNativePluginContext,
2946    name: *const NemoRelayNativeString,
2947    priority: i32,
2948    break_chain: bool,
2949    cb: NemoRelayNativeToolJsonCb,
2950    user_data: *mut c_void,
2951    free_fn: NemoRelayNativeFreeFn,
2952) -> NemoRelayStatus {
2953    clear_native_last_error();
2954    let host_ctx = match host_ctx_mut(ctx) {
2955        Ok(ctx) => ctx,
2956        Err(status) => return status,
2957    };
2958    let instance = host_ctx.instance.clone();
2959    let ctx = unsafe { &mut *host_ctx.ctx };
2960    let name = match read_name(name) {
2961        Ok(name) => name,
2962        Err(status) => return status,
2963    };
2964    match ctx.register_tool_request_intercept(
2965        &name,
2966        priority,
2967        break_chain,
2968        wrap_tool_intercept_fn(instance, cb, user_data, free_fn),
2969    ) {
2970        Ok(()) => NemoRelayStatus::Ok,
2971        Err(err) => status_from_plugin_error(err),
2972    }
2973}
2974
2975unsafe extern "C" fn native_plugin_context_register_tool_execution_intercept(
2976    ctx: *mut NemoRelayNativePluginContext,
2977    name: *const NemoRelayNativeString,
2978    priority: i32,
2979    cb: NemoRelayNativeToolExecutionCb,
2980    user_data: *mut c_void,
2981    free_fn: NemoRelayNativeFreeFn,
2982) -> NemoRelayStatus {
2983    clear_native_last_error();
2984    let host_ctx = match host_ctx_mut(ctx) {
2985        Ok(ctx) => ctx,
2986        Err(status) => return status,
2987    };
2988    let instance = host_ctx.instance.clone();
2989    let ctx = unsafe { &mut *host_ctx.ctx };
2990    let name = match read_name(name) {
2991        Ok(name) => name,
2992        Err(status) => return status,
2993    };
2994    match ctx.register_tool_execution_intercept(
2995        &name,
2996        priority,
2997        wrap_tool_execution_fn(instance, cb, user_data, free_fn),
2998    ) {
2999        Ok(()) => NemoRelayStatus::Ok,
3000        Err(err) => status_from_plugin_error(err),
3001    }
3002}
3003
3004unsafe extern "C" fn native_plugin_context_register_llm_sanitize_request_guardrail(
3005    ctx: *mut NemoRelayNativePluginContext,
3006    name: *const NemoRelayNativeString,
3007    priority: i32,
3008    cb: NemoRelayNativeLlmSanitizeRequestCb,
3009    user_data: *mut c_void,
3010    free_fn: NemoRelayNativeFreeFn,
3011) -> NemoRelayStatus {
3012    clear_native_last_error();
3013    let host_ctx = match host_ctx_mut(ctx) {
3014        Ok(ctx) => ctx,
3015        Err(status) => return status,
3016    };
3017    let instance = host_ctx.instance.clone();
3018    let ctx = unsafe { &mut *host_ctx.ctx };
3019    let name = match read_name(name) {
3020        Ok(name) => name,
3021        Err(status) => return status,
3022    };
3023    match ctx.register_llm_sanitize_request_guardrail(
3024        &name,
3025        priority,
3026        wrap_llm_sanitize_request_fn(instance, cb, user_data, free_fn),
3027    ) {
3028        Ok(()) => NemoRelayStatus::Ok,
3029        Err(err) => status_from_plugin_error(err),
3030    }
3031}
3032
3033unsafe extern "C" fn native_plugin_context_register_llm_sanitize_response_guardrail(
3034    ctx: *mut NemoRelayNativePluginContext,
3035    name: *const NemoRelayNativeString,
3036    priority: i32,
3037    cb: NemoRelayNativeLlmSanitizeResponseCb,
3038    user_data: *mut c_void,
3039    free_fn: NemoRelayNativeFreeFn,
3040) -> NemoRelayStatus {
3041    clear_native_last_error();
3042    let host_ctx = match host_ctx_mut(ctx) {
3043        Ok(ctx) => ctx,
3044        Err(status) => return status,
3045    };
3046    let instance = host_ctx.instance.clone();
3047    let ctx = unsafe { &mut *host_ctx.ctx };
3048    let name = match read_name(name) {
3049        Ok(name) => name,
3050        Err(status) => return status,
3051    };
3052    match ctx.register_llm_sanitize_response_guardrail(
3053        &name,
3054        priority,
3055        wrap_llm_sanitize_response_fn(instance, cb, user_data, free_fn),
3056    ) {
3057        Ok(()) => NemoRelayStatus::Ok,
3058        Err(err) => status_from_plugin_error(err),
3059    }
3060}
3061
3062unsafe extern "C" fn native_plugin_context_register_llm_conditional_execution_guardrail(
3063    ctx: *mut NemoRelayNativePluginContext,
3064    name: *const NemoRelayNativeString,
3065    priority: i32,
3066    cb: NemoRelayNativeLlmConditionalCb,
3067    user_data: *mut c_void,
3068    free_fn: NemoRelayNativeFreeFn,
3069) -> NemoRelayStatus {
3070    clear_native_last_error();
3071    let host_ctx = match host_ctx_mut(ctx) {
3072        Ok(ctx) => ctx,
3073        Err(status) => return status,
3074    };
3075    let instance = host_ctx.instance.clone();
3076    let ctx = unsafe { &mut *host_ctx.ctx };
3077    let name = match read_name(name) {
3078        Ok(name) => name,
3079        Err(status) => return status,
3080    };
3081    match ctx.register_llm_conditional_execution_guardrail(
3082        &name,
3083        priority,
3084        wrap_llm_conditional_fn(instance, cb, user_data, free_fn),
3085    ) {
3086        Ok(()) => NemoRelayStatus::Ok,
3087        Err(err) => status_from_plugin_error(err),
3088    }
3089}
3090
3091unsafe extern "C" fn native_plugin_context_register_llm_request_intercept(
3092    ctx: *mut NemoRelayNativePluginContext,
3093    name: *const NemoRelayNativeString,
3094    priority: i32,
3095    break_chain: bool,
3096    cb: NemoRelayNativeLlmRequestInterceptCb,
3097    user_data: *mut c_void,
3098    free_fn: NemoRelayNativeFreeFn,
3099) -> NemoRelayStatus {
3100    clear_native_last_error();
3101    let host_ctx = match host_ctx_mut(ctx) {
3102        Ok(ctx) => ctx,
3103        Err(status) => return status,
3104    };
3105    let instance = host_ctx.instance.clone();
3106    if let Err(error) = validate_annotated_request_consumer_compatibility(
3107        &instance.relay_compat,
3108        &instance.plugin_kind,
3109    ) {
3110        return status_from_plugin_error(error);
3111    }
3112    let ctx = unsafe { &mut *host_ctx.ctx };
3113    let name = match read_name(name) {
3114        Ok(name) => name,
3115        Err(status) => return status,
3116    };
3117    match ctx.register_llm_request_intercept(
3118        &name,
3119        priority,
3120        break_chain,
3121        wrap_llm_request_intercept_fn(instance, cb, user_data, free_fn),
3122    ) {
3123        Ok(()) => NemoRelayStatus::Ok,
3124        Err(err) => status_from_plugin_error(err),
3125    }
3126}
3127
3128unsafe extern "C" fn native_plugin_context_register_llm_execution_intercept(
3129    ctx: *mut NemoRelayNativePluginContext,
3130    name: *const NemoRelayNativeString,
3131    priority: i32,
3132    cb: NemoRelayNativeLlmExecutionCb,
3133    user_data: *mut c_void,
3134    free_fn: NemoRelayNativeFreeFn,
3135) -> NemoRelayStatus {
3136    clear_native_last_error();
3137    let host_ctx = match host_ctx_mut(ctx) {
3138        Ok(ctx) => ctx,
3139        Err(status) => return status,
3140    };
3141    let instance = host_ctx.instance.clone();
3142    let ctx = unsafe { &mut *host_ctx.ctx };
3143    let name = match read_name(name) {
3144        Ok(name) => name,
3145        Err(status) => return status,
3146    };
3147    match ctx.register_llm_execution_intercept(
3148        &name,
3149        priority,
3150        wrap_llm_execution_fn(instance, cb, user_data, free_fn),
3151    ) {
3152        Ok(()) => NemoRelayStatus::Ok,
3153        Err(err) => status_from_plugin_error(err),
3154    }
3155}
3156
3157unsafe extern "C" fn native_plugin_context_register_llm_stream_execution_intercept(
3158    ctx: *mut NemoRelayNativePluginContext,
3159    name: *const NemoRelayNativeString,
3160    priority: i32,
3161    cb: NemoRelayNativeLlmStreamExecutionCb,
3162    user_data: *mut c_void,
3163    free_fn: NemoRelayNativeFreeFn,
3164) -> NemoRelayStatus {
3165    clear_native_last_error();
3166    let host_ctx = match host_ctx_mut(ctx) {
3167        Ok(ctx) => ctx,
3168        Err(status) => return status,
3169    };
3170    let instance = host_ctx.instance.clone();
3171    let ctx = unsafe { &mut *host_ctx.ctx };
3172    let name = match read_name(name) {
3173        Ok(name) => name,
3174        Err(status) => return status,
3175    };
3176    match ctx.register_llm_stream_execution_intercept(
3177        &name,
3178        priority,
3179        wrap_llm_stream_execution_fn(instance, cb, user_data, free_fn),
3180    ) {
3181        Ok(()) => NemoRelayStatus::Ok,
3182        Err(err) => status_from_plugin_error(err),
3183    }
3184}
3185
3186macro_rules! native_event_sanitize_context_register {
3187    ($fn_name:ident, $ctx_method:ident) => {
3188        unsafe extern "C" fn $fn_name(
3189            ctx: *mut NemoRelayNativePluginContext,
3190            name: *const NemoRelayNativeString,
3191            priority: i32,
3192            cb: NemoRelayNativeEventSanitizeCb,
3193            user_data: *mut c_void,
3194            free_fn: NemoRelayNativeFreeFn,
3195        ) -> NemoRelayStatus {
3196            clear_native_last_error();
3197            let host_ctx = match host_ctx_mut(ctx) {
3198                Ok(ctx) => ctx,
3199                Err(status) => return status,
3200            };
3201            let instance = host_ctx.instance.clone();
3202            let ctx = unsafe { &mut *host_ctx.ctx };
3203            let name = match read_name(name) {
3204                Ok(name) => name,
3205                Err(status) => return status,
3206            };
3207            match ctx.$ctx_method(
3208                &name,
3209                priority,
3210                wrap_event_sanitize_fn(instance, cb, user_data, free_fn),
3211            ) {
3212                Ok(()) => NemoRelayStatus::Ok,
3213                Err(err) => status_from_plugin_error(err),
3214            }
3215        }
3216    };
3217}
3218
3219native_event_sanitize_context_register!(
3220    native_plugin_context_register_mark_sanitize_guardrail,
3221    register_mark_sanitize_guardrail
3222);
3223native_event_sanitize_context_register!(
3224    native_plugin_context_register_scope_sanitize_start_guardrail,
3225    register_scope_sanitize_start_guardrail
3226);
3227native_event_sanitize_context_register!(
3228    native_plugin_context_register_scope_sanitize_end_guardrail,
3229    register_scope_sanitize_end_guardrail
3230);
3231
3232fn wrap_event_subscriber(
3233    instance: Arc<NativePluginInstance>,
3234    cb: NemoRelayNativeEventSubscriberCb,
3235    user_data: *mut c_void,
3236    free_fn: NemoRelayNativeFreeFn,
3237) -> EventSubscriberFn {
3238    let user_data = make_user_data(instance, user_data, free_fn);
3239    Arc::new(move |event: &Event| {
3240        let event_json = serde_json::to_value(event).unwrap_or(Json::Null);
3241        if let Some(event_string) = native_string_from_json(&event_json) {
3242            let status = unsafe { cb(user_data.ptr, event_string) };
3243            if status != NemoRelayStatus::Ok {
3244                set_native_last_error(format!("native subscriber callback returned {status:?}"));
3245            }
3246            unsafe { native_string_free(event_string) };
3247        }
3248    })
3249}
3250
3251fn wrap_event_sanitize_fn(
3252    instance: Arc<NativePluginInstance>,
3253    cb: NemoRelayNativeEventSanitizeCb,
3254    user_data: *mut c_void,
3255    free_fn: NemoRelayNativeFreeFn,
3256) -> EventSanitizeFn {
3257    let user_data = make_user_data(instance, user_data, free_fn);
3258    Arc::new(move |event, fields| {
3259        let user_data = user_data.clone();
3260        Box::pin(async move { call_event_sanitize_callback(cb, user_data.ptr, &event, &fields) })
3261    })
3262}
3263
3264fn call_event_sanitize_callback(
3265    cb: NemoRelayNativeEventSanitizeCb,
3266    user_data: *mut c_void,
3267    event: &Event,
3268    fields: &EventSanitizeFields,
3269) -> FlowResult<EventSanitizeFields> {
3270    clear_native_last_error();
3271    let event_json = serde_json::to_value(event)
3272        .map_err(|err| FlowError::Internal(format!("failed to encode native event: {err}")))?;
3273    let fields_json = serde_json::to_value(fields).map_err(|err| {
3274        FlowError::Internal(format!("failed to encode native event fields: {err}"))
3275    })?;
3276    let event = native_string_from_json(&event_json)
3277        .ok_or_else(|| FlowError::Internal("failed to allocate native event".into()))?;
3278    let fields = match native_string_from_json(&fields_json) {
3279        Some(fields) => fields,
3280        None => {
3281            unsafe { native_string_free(event) };
3282            return Err(FlowError::Internal(
3283                "failed to allocate native event fields".into(),
3284            ));
3285        }
3286    };
3287    let mut out = ptr::null_mut();
3288    let status = unsafe { cb(user_data, event, fields, &mut out) };
3289    unsafe {
3290        native_string_free(event);
3291        native_string_free(fields);
3292    }
3293    if status != NemoRelayStatus::Ok {
3294        if !out.is_null() {
3295            unsafe { native_string_free(out) };
3296        }
3297        return Err(flow_error_from_status(
3298            status,
3299            "native event sanitizer failed",
3300        ));
3301    }
3302    let value = take_json_from_native_string(out, "native event sanitizer returned null")?;
3303    serde_json::from_value(value)
3304        .map_err(|err| FlowError::Internal(format!("invalid event sanitize fields: {err}")))
3305}
3306
3307fn wrap_tool_json_fn(
3308    instance: Arc<NativePluginInstance>,
3309    cb: NemoRelayNativeToolJsonCb,
3310    user_data: *mut c_void,
3311    free_fn: NemoRelayNativeFreeFn,
3312) -> ToolSanitizeFn {
3313    let user_data = make_user_data(instance, user_data, free_fn);
3314    Arc::new(move |name, payload| {
3315        let user_data = user_data.clone();
3316        Box::pin(async move { call_tool_json_callback(cb, user_data.ptr, &name, &payload) })
3317    })
3318}
3319
3320fn wrap_tool_intercept_fn(
3321    instance: Arc<NativePluginInstance>,
3322    cb: NemoRelayNativeToolJsonCb,
3323    user_data: *mut c_void,
3324    free_fn: NemoRelayNativeFreeFn,
3325) -> ToolInterceptFn {
3326    let user_data = make_user_data(instance, user_data, free_fn);
3327    Arc::new(move |name, payload| {
3328        let user_data = user_data.clone();
3329        Box::pin(async move { call_tool_json_callback(cb, user_data.ptr, &name, &payload) })
3330    })
3331}
3332
3333fn call_tool_json_callback(
3334    cb: NemoRelayNativeToolJsonCb,
3335    user_data: *mut c_void,
3336    name: &str,
3337    payload: &Json,
3338) -> FlowResult<Json> {
3339    clear_native_last_error();
3340    let name = native_string_from_str(name)
3341        .ok_or_else(|| FlowError::Internal("failed to allocate native name".into()))?;
3342    let payload = native_string_from_json(payload)
3343        .ok_or_else(|| FlowError::Internal("failed to allocate native payload".into()))?;
3344    let mut out = ptr::null_mut();
3345    let status = unsafe { cb(user_data, name, payload, &mut out) };
3346    unsafe {
3347        native_string_free(name);
3348        native_string_free(payload);
3349    }
3350    if status != NemoRelayStatus::Ok {
3351        if !out.is_null() {
3352            unsafe { native_string_free(out) };
3353        }
3354        return Err(flow_error_from_status(
3355            status,
3356            "native JSON callback failed",
3357        ));
3358    }
3359    take_json_from_native_string(out, "native JSON callback returned null")
3360}
3361
3362fn wrap_tool_conditional_fn(
3363    instance: Arc<NativePluginInstance>,
3364    cb: NemoRelayNativeToolConditionalCb,
3365    user_data: *mut c_void,
3366    free_fn: NemoRelayNativeFreeFn,
3367) -> ToolConditionalFn {
3368    let user_data = make_user_data(instance, user_data, free_fn);
3369    Arc::new(move |name, args| {
3370        let user_data = user_data.clone();
3371        Box::pin(async move {
3372            clear_native_last_error();
3373            let name_string = native_string_from_str(&name)
3374                .ok_or_else(|| FlowError::Internal("failed to allocate native name".into()))?;
3375            let args_string = native_string_from_json(&args)
3376                .ok_or_else(|| FlowError::Internal("failed to allocate native args".into()))?;
3377            let mut out = ptr::null_mut();
3378            let status = unsafe { cb(user_data.ptr, name_string, args_string, &mut out) };
3379            unsafe {
3380                native_string_free(name_string);
3381                native_string_free(args_string);
3382            }
3383            if status != NemoRelayStatus::Ok {
3384                if !out.is_null() {
3385                    unsafe { native_string_free(out) };
3386                }
3387                return Err(flow_error_from_status(
3388                    status,
3389                    "native tool conditional failed",
3390                ));
3391            }
3392            if out.is_null() {
3393                Ok(None)
3394            } else {
3395                let reason = take_native_string(out)?;
3396                Ok(Some(reason))
3397            }
3398        })
3399    })
3400}
3401
3402fn wrap_tool_execution_fn(
3403    instance: Arc<NativePluginInstance>,
3404    cb: NemoRelayNativeToolExecutionCb,
3405    user_data: *mut c_void,
3406    free_fn: NemoRelayNativeFreeFn,
3407) -> ToolExecutionFn {
3408    let user_data = make_user_data(instance, user_data, free_fn);
3409    Arc::new(move |name, args, next| {
3410        let name = name.to_owned();
3411        let user_data = user_data.clone();
3412        Box::pin(async move {
3413            clear_native_last_error();
3414            let name_string = native_string_from_str(&name)
3415                .ok_or_else(|| FlowError::Internal("failed to allocate native name".into()))?;
3416            let args_string = native_string_from_json(&args)
3417                .ok_or_else(|| FlowError::Internal("failed to allocate native args".into()))?;
3418            let next_ctx = Box::into_raw(Box::new(next)) as *mut c_void;
3419            let mut out_outcome = ptr::null_mut();
3420            let status = unsafe {
3421                cb(
3422                    user_data.ptr,
3423                    name_string,
3424                    args_string,
3425                    native_tool_next,
3426                    next_ctx,
3427                    &mut out_outcome,
3428                )
3429            };
3430            unsafe {
3431                drop(Box::from_raw(next_ctx as *mut ToolExecutionNextFn));
3432                native_string_free(name_string);
3433                native_string_free(args_string);
3434            }
3435            if status != NemoRelayStatus::Ok {
3436                if !out_outcome.is_null() {
3437                    unsafe { native_string_free(out_outcome) };
3438                }
3439                return Err(flow_error_from_status(
3440                    status,
3441                    "native tool execution failed",
3442                ));
3443            }
3444            let outcome_json = take_json_from_native_string(
3445                out_outcome,
3446                "native tool execution returned null outcome",
3447            )?;
3448            serde_json::from_value::<ToolExecutionInterceptOutcome>(outcome_json).map_err(|err| {
3449                FlowError::Internal(format!("invalid native tool execution outcome JSON: {err}"))
3450            })
3451        })
3452    })
3453}
3454
3455unsafe extern "C" fn native_tool_next(
3456    args_json: *const NemoRelayNativeString,
3457    next_ctx: *mut c_void,
3458    out_json: *mut *mut NemoRelayNativeString,
3459) -> NemoRelayStatus {
3460    if next_ctx.is_null() || out_json.is_null() {
3461        set_native_last_error("native tool next received null pointer");
3462        return NemoRelayStatus::NullPointer;
3463    }
3464    let args = match parse_json_arg(args_json, "native tool next args") {
3465        Ok(args) => args,
3466        Err(status) => return status,
3467    };
3468    let next = unsafe { (*(next_ctx as *const ToolExecutionNextFn)).clone() };
3469    let context = MiddlewareContinuationContext::capture();
3470    let result = spawn_with_continuation_context(context, move || next(args)).join();
3471    match result {
3472        Ok(Ok(result)) => write_native_json(&result, out_json),
3473        Ok(Err(err)) => status_from_flow_error(err),
3474        Err(_) => {
3475            set_native_last_error("native tool next panicked");
3476            NemoRelayStatus::Internal
3477        }
3478    }
3479}
3480
3481fn wrap_llm_sanitize_request_fn(
3482    instance: Arc<NativePluginInstance>,
3483    cb: NemoRelayNativeLlmSanitizeRequestCb,
3484    user_data: *mut c_void,
3485    free_fn: NemoRelayNativeFreeFn,
3486) -> LlmSanitizeRequestFn {
3487    let user_data = make_user_data(instance, user_data, free_fn);
3488    Arc::new(move |request, context| {
3489        let user_data = user_data.clone();
3490        Box::pin(
3491            async move { call_llm_sanitize_request_callback(cb, user_data.ptr, &request, context) },
3492        )
3493    })
3494}
3495
3496fn wrap_llm_sanitize_response_fn(
3497    instance: Arc<NativePluginInstance>,
3498    cb: NemoRelayNativeLlmSanitizeResponseCb,
3499    user_data: *mut c_void,
3500    free_fn: NemoRelayNativeFreeFn,
3501) -> LlmSanitizeResponseFn {
3502    let user_data = make_user_data(instance, user_data, free_fn);
3503    Arc::new(move |payload, context| {
3504        let user_data = user_data.clone();
3505        Box::pin(async move {
3506            call_llm_sanitize_response_callback(cb, user_data.ptr, &payload, context)
3507        })
3508    })
3509}
3510
3511fn call_llm_sanitize_request_callback(
3512    cb: NemoRelayNativeLlmSanitizeRequestCb,
3513    user_data: *mut c_void,
3514    request: &LlmRequest,
3515    context: LlmSanitizeRequestContext,
3516) -> FlowResult<Option<LlmRequest>> {
3517    clear_native_last_error();
3518    let codec = context.resolve_codec().map(NativeHostLlmRequestCodec);
3519    let (codec_kind, context_id) = native_llm_codec_identity(context.codec())?;
3520    let request_json = match serde_json::to_value(request) {
3521        Ok(request_json) => request_json,
3522        Err(err) => {
3523            if let Some(context_id) = context_id {
3524                unsafe { native_string_free(context_id) };
3525            }
3526            return Err(FlowError::Internal(format!(
3527                "failed to serialize LLM request: {err}"
3528            )));
3529        }
3530    };
3531    let request_string = match native_string_from_json(&request_json) {
3532        Some(request_string) => request_string,
3533        None => {
3534            if let Some(context_id) = context_id {
3535                unsafe { native_string_free(context_id) };
3536            }
3537            return Err(FlowError::Internal(
3538                "failed to allocate native LLM request".into(),
3539            ));
3540        }
3541    };
3542    let context = NemoRelayNativeLlmSanitizeRequestContext {
3543        codec_kind,
3544        codec_id: context_id.map_or(ptr::null(), |value| value.cast_const()),
3545        codec: codec
3546            .as_ref()
3547            .map_or(ptr::null(), |value| std::ptr::from_ref(value).cast()),
3548    };
3549    let mut out = ptr::null_mut();
3550    let status = unsafe { cb(user_data, request_string, context, &mut out) };
3551    if status != NemoRelayStatus::Ok {
3552        unsafe { free_native_sanitizer_strings(request_string, context_id, out) };
3553        return Err(flow_error_from_status(
3554            status,
3555            "native LLM sanitize-request callback failed",
3556        ));
3557    }
3558    if out.is_null() {
3559        unsafe { free_native_sanitizer_strings(request_string, context_id, out) };
3560        return Ok(None);
3561    }
3562    let result_json =
3563        json_from_native_string(out, "native LLM sanitize-request returned invalid JSON");
3564    unsafe { free_native_sanitizer_strings(request_string, context_id, out) };
3565    let result_json = result_json?;
3566    serde_json::from_value(result_json)
3567        .map(Some)
3568        .map_err(|err| FlowError::Internal(format!("invalid LLM request JSON: {err}")))
3569}
3570
3571fn call_llm_sanitize_response_callback(
3572    cb: NemoRelayNativeLlmSanitizeResponseCb,
3573    user_data: *mut c_void,
3574    payload: &Json,
3575    context: LlmSanitizeResponseContext,
3576) -> FlowResult<Option<Json>> {
3577    clear_native_last_error();
3578    let codec = context.resolve_codec().map(NativeHostLlmResponseCodec);
3579    let (codec_kind, context_id) = native_llm_codec_identity(context.codec())?;
3580    let payload_string = match native_string_from_json(payload) {
3581        Some(payload_string) => payload_string,
3582        None => {
3583            if let Some(context_id) = context_id {
3584                unsafe { native_string_free(context_id) };
3585            }
3586            return Err(FlowError::Internal(
3587                "failed to allocate native LLM response".into(),
3588            ));
3589        }
3590    };
3591    let context = NemoRelayNativeLlmSanitizeResponseContext {
3592        codec_kind,
3593        codec_id: context_id.map_or(ptr::null(), |value| value.cast_const()),
3594        codec: codec
3595            .as_ref()
3596            .map_or(ptr::null(), |value| std::ptr::from_ref(value).cast()),
3597    };
3598    let mut out = ptr::null_mut();
3599    let status = unsafe { cb(user_data, payload_string, context, &mut out) };
3600    if status != NemoRelayStatus::Ok {
3601        unsafe { free_native_sanitizer_strings(payload_string, context_id, out) };
3602        return Err(flow_error_from_status(
3603            status,
3604            "native LLM sanitize-response callback failed",
3605        ));
3606    }
3607    if out.is_null() {
3608        unsafe { free_native_sanitizer_strings(payload_string, context_id, out) };
3609        return Ok(None);
3610    }
3611    let result = json_from_native_string(out, "native LLM sanitize-response returned invalid JSON");
3612    unsafe { free_native_sanitizer_strings(payload_string, context_id, out) };
3613    result.map(Some)
3614}
3615
3616fn native_llm_codec_identity(
3617    context: &LlmCodecIdentity,
3618) -> FlowResult<(
3619    NemoRelayNativeLlmCodecKind,
3620    Option<*mut NemoRelayNativeString>,
3621)> {
3622    let (codec_kind, codec_id) = match context {
3623        LlmCodecIdentity::None => (NemoRelayNativeLlmCodecKind::None, None),
3624        LlmCodecIdentity::BuiltIn(codec) => {
3625            (NemoRelayNativeLlmCodecKind::BuiltIn, Some(codec.id()))
3626        }
3627        LlmCodecIdentity::Runtime(id) => (NemoRelayNativeLlmCodecKind::Runtime, Some(id.as_str())),
3628        LlmCodecIdentity::Opaque => (NemoRelayNativeLlmCodecKind::Opaque, None),
3629    };
3630    let codec_id =
3631        match codec_id {
3632            Some(codec_id) => Some(native_string_from_str(codec_id).ok_or_else(|| {
3633                FlowError::Internal("failed to allocate native LLM codec ID".into())
3634            })?),
3635            None => None,
3636        };
3637    Ok((codec_kind, codec_id))
3638}
3639
3640fn wrap_llm_conditional_fn(
3641    instance: Arc<NativePluginInstance>,
3642    cb: NemoRelayNativeLlmConditionalCb,
3643    user_data: *mut c_void,
3644    free_fn: NemoRelayNativeFreeFn,
3645) -> LlmConditionalFn {
3646    let user_data = make_user_data(instance, user_data, free_fn);
3647    Arc::new(move |request| {
3648        let user_data = user_data.clone();
3649        Box::pin(async move {
3650            clear_native_last_error();
3651            let request_json = serde_json::to_value(request).map_err(|err| {
3652                FlowError::Internal(format!("failed to serialize LLM request: {err}"))
3653            })?;
3654            let request_string = native_string_from_json(&request_json).ok_or_else(|| {
3655                FlowError::Internal("failed to allocate native LLM request".into())
3656            })?;
3657            let mut out = ptr::null_mut();
3658            let status = unsafe { cb(user_data.ptr, request_string, &mut out) };
3659            unsafe { native_string_free(request_string) };
3660            if status != NemoRelayStatus::Ok {
3661                if !out.is_null() {
3662                    unsafe { native_string_free(out) };
3663                }
3664                return Err(flow_error_from_status(
3665                    status,
3666                    "native LLM conditional failed",
3667                ));
3668            }
3669            if out.is_null() {
3670                Ok(None)
3671            } else {
3672                let reason = take_native_string(out)?;
3673                Ok(Some(reason))
3674            }
3675        })
3676    })
3677}
3678
3679fn wrap_llm_request_intercept_fn(
3680    instance: Arc<NativePluginInstance>,
3681    cb: NemoRelayNativeLlmRequestInterceptCb,
3682    user_data: *mut c_void,
3683    free_fn: NemoRelayNativeFreeFn,
3684) -> LlmRequestInterceptFn {
3685    let user_data = make_user_data(instance, user_data, free_fn);
3686    Arc::new(move |name, request, annotated| {
3687        let user_data = user_data.clone();
3688        Box::pin(async move {
3689            clear_native_last_error();
3690            let name_string = native_string_from_str(&name)
3691                .ok_or_else(|| FlowError::Internal("failed to allocate native name".into()))?;
3692            let request_json = serde_json::to_value(&request).map_err(|err| {
3693                FlowError::Internal(format!("failed to serialize LLM request: {err}"))
3694            })?;
3695            let request_string = native_string_from_json(&request_json).ok_or_else(|| {
3696                FlowError::Internal("failed to allocate native LLM request".into())
3697            })?;
3698            let annotated_string = match &annotated {
3699                Some(annotated) => {
3700                    let value = serde_json::to_value(annotated).map_err(|err| {
3701                        FlowError::Internal(format!("failed to serialize annotated request: {err}"))
3702                    })?;
3703                    native_string_from_json(&value).ok_or_else(|| {
3704                        FlowError::Internal("failed to allocate annotated request".into())
3705                    })?
3706                }
3707                None => ptr::null_mut(),
3708            };
3709            let mut out_outcome = ptr::null_mut();
3710            let status = unsafe {
3711                cb(
3712                    user_data.ptr,
3713                    name_string,
3714                    request_string,
3715                    annotated_string,
3716                    &mut out_outcome,
3717                )
3718            };
3719            unsafe {
3720                native_string_free(name_string);
3721                native_string_free(request_string);
3722                native_string_free(annotated_string);
3723            }
3724            if status != NemoRelayStatus::Ok {
3725                unsafe {
3726                    native_string_free(out_outcome);
3727                }
3728                return Err(flow_error_from_status(
3729                    status,
3730                    "native LLM request intercept failed",
3731                ));
3732            }
3733            let outcome_json = json_from_native_string(
3734                out_outcome,
3735                "native LLM request intercept returned null outcome",
3736            );
3737            unsafe {
3738                native_string_free(out_outcome);
3739            }
3740            serde_json::from_value::<LlmRequestInterceptOutcome>(outcome_json?).map_err(|err| {
3741                FlowError::Internal(format!("invalid LLM request intercept outcome JSON: {err}"))
3742            })
3743        })
3744    })
3745}
3746
3747fn wrap_llm_execution_fn(
3748    instance: Arc<NativePluginInstance>,
3749    cb: NemoRelayNativeLlmExecutionCb,
3750    user_data: *mut c_void,
3751    free_fn: NemoRelayNativeFreeFn,
3752) -> LlmExecutionFn {
3753    let user_data = make_user_data(instance, user_data, free_fn);
3754    Arc::new(move |name, request, next| {
3755        let name = name.to_owned();
3756        let user_data = user_data.clone();
3757        Box::pin(async move { call_llm_execution_callback(cb, &user_data, &name, &request, next) })
3758    })
3759}
3760
3761fn call_llm_execution_callback(
3762    cb: NemoRelayNativeLlmExecutionCb,
3763    user_data: &NativeCallbackUserData,
3764    name: &str,
3765    request: &LlmRequest,
3766    next: LlmExecutionNextFn,
3767) -> FlowResult<Json> {
3768    clear_native_last_error();
3769    let name_string = native_string_from_str(name)
3770        .ok_or_else(|| FlowError::Internal("failed to allocate native name".into()))?;
3771    let request_json = serde_json::to_value(request)
3772        .map_err(|err| FlowError::Internal(format!("failed to serialize LLM request: {err}")))?;
3773    let request_string = native_string_from_json(&request_json)
3774        .ok_or_else(|| FlowError::Internal("failed to allocate native LLM request".into()))?;
3775    let next_ctx = Box::into_raw(Box::new(next)) as *mut c_void;
3776    let mut out = ptr::null_mut();
3777    let status = unsafe {
3778        cb(
3779            user_data.ptr,
3780            name_string,
3781            request_string,
3782            native_llm_next,
3783            next_ctx,
3784            &mut out,
3785        )
3786    };
3787    unsafe {
3788        drop(Box::from_raw(next_ctx as *mut LlmExecutionNextFn));
3789        native_string_free(name_string);
3790        native_string_free(request_string);
3791    }
3792    if status != NemoRelayStatus::Ok {
3793        if !out.is_null() {
3794            unsafe { native_string_free(out) };
3795        }
3796        return Err(flow_error_from_status(
3797            status,
3798            "native LLM execution failed",
3799        ));
3800    }
3801    take_json_from_native_string(out, "native LLM execution returned null")
3802}
3803
3804unsafe extern "C" fn native_llm_next(
3805    request_json: *const NemoRelayNativeString,
3806    next_ctx: *mut c_void,
3807    out_json: *mut *mut NemoRelayNativeString,
3808) -> NemoRelayStatus {
3809    if next_ctx.is_null() || out_json.is_null() {
3810        set_native_last_error("native LLM next received null pointer");
3811        return NemoRelayStatus::NullPointer;
3812    }
3813    let request = match parse_llm_request_arg(request_json, "native LLM next request") {
3814        Ok(request) => request,
3815        Err(status) => return status,
3816    };
3817    let next = unsafe { (*(next_ctx as *const LlmExecutionNextFn)).clone() };
3818    let context = MiddlewareContinuationContext::capture();
3819    let result = spawn_with_continuation_context(context, move || next(request)).join();
3820    match result {
3821        Ok(Ok(result)) => write_native_json(&result, out_json),
3822        Ok(Err(err)) => status_from_flow_error(err),
3823        Err(_) => {
3824            set_native_last_error("native LLM next panicked");
3825            NemoRelayStatus::Internal
3826        }
3827    }
3828}
3829fn wrap_llm_stream_execution_fn(
3830    instance: Arc<NativePluginInstance>,
3831    cb: NemoRelayNativeLlmStreamExecutionCb,
3832    user_data: *mut c_void,
3833    free_fn: NemoRelayNativeFreeFn,
3834) -> LlmStreamExecutionFn {
3835    let user_data = make_user_data(instance, user_data, free_fn);
3836    Arc::new(move |name, request, next| {
3837        let name = name.to_owned();
3838        let user_data = user_data.clone();
3839        Box::pin(
3840            async move { call_llm_stream_execution_callback(cb, user_data, &name, &request, next) },
3841        )
3842    })
3843}
3844
3845fn call_llm_stream_execution_callback(
3846    cb: NemoRelayNativeLlmStreamExecutionCb,
3847    user_data: Arc<NativeCallbackUserData>,
3848    name: &str,
3849    request: &LlmRequest,
3850    next: LlmStreamExecutionNextFn,
3851) -> FlowResult<LlmJsonStream> {
3852    clear_native_last_error();
3853    let name_string = native_string_from_str(name)
3854        .ok_or_else(|| FlowError::Internal("failed to allocate native name".into()))?;
3855    let request_json = serde_json::to_value(request)
3856        .map_err(|err| FlowError::Internal(format!("failed to serialize LLM request: {err}")))?;
3857    let request_string = native_string_from_json(&request_json)
3858        .ok_or_else(|| FlowError::Internal("failed to allocate native LLM request".into()))?;
3859    let next_ctx = NativeStreamNextContext::new(Box::into_raw(Box::new(next)) as *mut c_void);
3860    let mut out = NemoRelayNativeLlmStreamV1::default();
3861    let status = unsafe {
3862        cb(
3863            user_data.ptr,
3864            name_string,
3865            request_string,
3866            native_llm_stream_next,
3867            next_ctx.ptr,
3868            &mut out,
3869        )
3870    };
3871    unsafe {
3872        native_string_free(name_string);
3873        native_string_free(request_string);
3874    }
3875    if status != NemoRelayStatus::Ok {
3876        drop_native_stream(out);
3877        return Err(flow_error_from_status(
3878            status,
3879            "native LLM stream execution failed",
3880        ));
3881    }
3882    native_stream_to_relay_stream(out, Some(next_ctx), Some(user_data))
3883}
3884
3885unsafe extern "C" fn native_llm_stream_next(
3886    request_json: *const NemoRelayNativeString,
3887    next_ctx: *mut c_void,
3888    out_stream: *mut NemoRelayNativeLlmStreamV1,
3889) -> NemoRelayStatus {
3890    if next_ctx.is_null() || out_stream.is_null() {
3891        set_native_last_error("native LLM stream next received null pointer");
3892        return NemoRelayStatus::NullPointer;
3893    }
3894    unsafe { *out_stream = NemoRelayNativeLlmStreamV1::default() };
3895    let request = match parse_llm_request_arg(request_json, "native LLM stream next request") {
3896        Ok(request) => request,
3897        Err(status) => return status,
3898    };
3899    let next = unsafe { (*(next_ctx as *const LlmStreamExecutionNextFn)).clone() };
3900    let context = MiddlewareContinuationContext::capture();
3901    let stream_context = context.clone();
3902    let result = spawn_with_continuation_context(context, move || next(request)).join();
3903    match result {
3904        Ok(Ok(stream)) => {
3905            unsafe {
3906                *out_stream = relay_stream_to_native_stream_with_context(stream, stream_context)
3907            };
3908            NemoRelayStatus::Ok
3909        }
3910        Ok(Err(err)) => status_from_flow_error(err),
3911        Err(_) => {
3912            set_native_last_error("native LLM stream next panicked");
3913            NemoRelayStatus::Internal
3914        }
3915    }
3916}
3917
3918struct NativeRelayLlmStream {
3919    raw: NemoRelayNativeLlmStreamV1,
3920    finished: bool,
3921    _next_ctx: Option<NativeStreamNextContext>,
3922    _callback_user_data: Option<Arc<NativeCallbackUserData>>,
3923}
3924
3925unsafe impl Send for NativeRelayLlmStream {}
3926
3927impl NativeRelayLlmStream {
3928    fn from_raw(
3929        raw: NemoRelayNativeLlmStreamV1,
3930        next_ctx: Option<NativeStreamNextContext>,
3931        callback_user_data: Option<Arc<NativeCallbackUserData>>,
3932    ) -> FlowResult<Self> {
3933        if raw.struct_size != std::mem::size_of::<NemoRelayNativeLlmStreamV1>() {
3934            let struct_size = raw.struct_size;
3935            drop_native_stream(raw);
3936            return Err(FlowError::Internal(format!(
3937                "unsupported native LLM stream struct size: {}",
3938                struct_size
3939            )));
3940        }
3941        if raw.next.is_none() {
3942            drop_native_stream(raw);
3943            return Err(FlowError::Internal(
3944                "native LLM stream next callback was null".into(),
3945            ));
3946        }
3947        Ok(Self {
3948            raw,
3949            finished: false,
3950            _next_ctx: next_ctx,
3951            _callback_user_data: callback_user_data,
3952        })
3953    }
3954
3955    fn finish(&mut self) {
3956        self.finished = true;
3957        if let Some(drop_fn) = self.raw.drop.take() {
3958            unsafe { drop_fn(self.raw.user_data) };
3959        }
3960        self.raw.user_data = ptr::null_mut();
3961    }
3962}
3963
3964impl Stream for NativeRelayLlmStream {
3965    type Item = FlowResult<Json>;
3966
3967    fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
3968        if self.finished {
3969            return Poll::Ready(None);
3970        }
3971        let Some(next) = self.raw.next else {
3972            self.finish();
3973            return Poll::Ready(Some(Err(FlowError::Internal(
3974                "native LLM stream next callback was null".into(),
3975            ))));
3976        };
3977        let mut out = ptr::null_mut();
3978        let status = unsafe { next(self.raw.user_data, &mut out) };
3979        match status {
3980            NemoRelayStatus::Ok => {
3981                if out.is_null() {
3982                    let error = FlowError::Internal(
3983                        native_last_error_message()
3984                            .unwrap_or_else(|| "native LLM stream returned null chunk".into()),
3985                    );
3986                    self.finish();
3987                    return Poll::Ready(Some(Err(error)));
3988                }
3989                let result =
3990                    take_json_from_native_string(out, "native LLM stream returned null chunk");
3991                if result.is_err() {
3992                    self.finish();
3993                }
3994                Poll::Ready(Some(result))
3995            }
3996            NemoRelayStatus::StreamEnd => {
3997                if !out.is_null() {
3998                    unsafe { native_string_free(out) };
3999                }
4000                self.finish();
4001                Poll::Ready(None)
4002            }
4003            status => {
4004                if !out.is_null() {
4005                    unsafe { native_string_free(out) };
4006                }
4007                let error = flow_error_from_status(status, "native LLM stream poll failed");
4008                self.finish();
4009                Poll::Ready(Some(Err(error)))
4010            }
4011        }
4012    }
4013}
4014
4015impl Drop for NativeRelayLlmStream {
4016    fn drop(&mut self) {
4017        if !self.finished
4018            && let Some(cancel) = self.raw.cancel
4019        {
4020            let _ = unsafe { cancel(self.raw.user_data) };
4021        }
4022        self.finish();
4023    }
4024}
4025
4026fn native_stream_to_relay_stream(
4027    raw: NemoRelayNativeLlmStreamV1,
4028    next_ctx: Option<NativeStreamNextContext>,
4029    callback_user_data: Option<Arc<NativeCallbackUserData>>,
4030) -> FlowResult<LlmJsonStream> {
4031    Ok(LlmJsonStream::new(NativeRelayLlmStream::from_raw(
4032        raw,
4033        next_ctx,
4034        callback_user_data,
4035    )?))
4036}
4037
4038fn drop_native_stream(mut raw: NemoRelayNativeLlmStreamV1) {
4039    if let Some(drop_fn) = raw.drop.take() {
4040        unsafe { drop_fn(raw.user_data) };
4041    }
4042}
4043
4044struct NativeHostLlmStream {
4045    stream: Arc<Mutex<Option<LlmJsonStream>>>,
4046    context: MiddlewareContinuationContext,
4047}
4048
4049struct NativeStreamNextContext {
4050    ptr: *mut c_void,
4051}
4052
4053unsafe impl Send for NativeStreamNextContext {}
4054
4055impl NativeStreamNextContext {
4056    fn new(ptr: *mut c_void) -> Self {
4057        Self { ptr }
4058    }
4059}
4060
4061impl Drop for NativeStreamNextContext {
4062    fn drop(&mut self) {
4063        if !self.ptr.is_null() {
4064            drop(unsafe { Box::from_raw(self.ptr as *mut LlmStreamExecutionNextFn) });
4065            self.ptr = ptr::null_mut();
4066        }
4067    }
4068}
4069
4070#[cfg(test)]
4071fn relay_stream_to_native_stream(stream: LlmJsonStream) -> NemoRelayNativeLlmStreamV1 {
4072    relay_stream_to_native_stream_with_context(stream, MiddlewareContinuationContext::capture())
4073}
4074
4075fn relay_stream_to_native_stream_with_context(
4076    stream: LlmJsonStream,
4077    context: MiddlewareContinuationContext,
4078) -> NemoRelayNativeLlmStreamV1 {
4079    let state = Box::new(NativeHostLlmStream {
4080        stream: Arc::new(Mutex::new(Some(stream))),
4081        context,
4082    });
4083    NemoRelayNativeLlmStreamV1 {
4084        struct_size: std::mem::size_of::<NemoRelayNativeLlmStreamV1>(),
4085        user_data: Box::into_raw(state).cast(),
4086        next: Some(poll_relay_llm_stream),
4087        cancel: Some(cancel_relay_llm_stream),
4088        drop: Some(drop_relay_llm_stream),
4089    }
4090}
4091
4092unsafe extern "C" fn poll_relay_llm_stream(
4093    user_data: *mut c_void,
4094    out_json: *mut *mut NemoRelayNativeString,
4095) -> NemoRelayStatus {
4096    if user_data.is_null() || out_json.is_null() {
4097        set_native_last_error("native host LLM stream poll received null pointer");
4098        return NemoRelayStatus::NullPointer;
4099    }
4100    unsafe { *out_json = ptr::null_mut() };
4101    let state = unsafe { &*(user_data as *const NativeHostLlmStream) };
4102    let stream = state.stream.clone();
4103    let context = state.context.clone();
4104    let result = spawn_with_continuation_context(context, move || async move {
4105        let Some(mut current) = stream
4106            .lock()
4107            .map_err(|_| FlowError::Internal("native host LLM stream lock poisoned".into()))?
4108            .take()
4109        else {
4110            return Ok(None);
4111        };
4112        match current.next().await {
4113            Some(Ok(chunk)) => {
4114                *stream.lock().map_err(|_| {
4115                    FlowError::Internal("native host LLM stream lock poisoned".into())
4116                })? = Some(current);
4117                Ok(Some(chunk))
4118            }
4119            Some(Err(err)) => Err(err),
4120            None => Ok(None),
4121        }
4122    })
4123    .join();
4124    match result {
4125        Ok(Ok(Some(chunk))) => write_native_json(&chunk, out_json),
4126        Ok(Ok(None)) => NemoRelayStatus::StreamEnd,
4127        Ok(Err(err)) => status_from_flow_error(err),
4128        Err(_) => {
4129            set_native_last_error("native host LLM stream poll panicked");
4130            NemoRelayStatus::Internal
4131        }
4132    }
4133}
4134
4135unsafe extern "C" fn cancel_relay_llm_stream(user_data: *mut c_void) -> NemoRelayStatus {
4136    if user_data.is_null() {
4137        set_native_last_error("native host LLM stream cancel received null pointer");
4138        return NemoRelayStatus::NullPointer;
4139    }
4140    let state = unsafe { &*(user_data as *const NativeHostLlmStream) };
4141    match state.stream.lock() {
4142        Ok(mut stream) => {
4143            stream.take();
4144            NemoRelayStatus::Ok
4145        }
4146        Err(_) => {
4147            set_native_last_error("native host LLM stream lock poisoned");
4148            NemoRelayStatus::Internal
4149        }
4150    }
4151}
4152
4153unsafe extern "C" fn drop_relay_llm_stream(user_data: *mut c_void) {
4154    if !user_data.is_null() {
4155        drop(unsafe { Box::from_raw(user_data as *mut NativeHostLlmStream) });
4156    }
4157}
4158
4159fn parse_json_arg(
4160    value: *const NemoRelayNativeString,
4161    label: &str,
4162) -> Result<Json, NemoRelayStatus> {
4163    let text = match read_native_string(value) {
4164        Ok(text) => text,
4165        Err(err) => {
4166            set_native_last_error(err.to_string());
4167            return Err(NemoRelayStatus::InvalidUtf8);
4168        }
4169    };
4170    serde_json::from_str(&text).map_err(|err| {
4171        set_native_last_error(format!("{label} was invalid JSON: {err}"));
4172        NemoRelayStatus::InvalidJson
4173    })
4174}
4175
4176fn parse_llm_request_arg(
4177    value: *const NemoRelayNativeString,
4178    label: &str,
4179) -> Result<LlmRequest, NemoRelayStatus> {
4180    let value = parse_json_arg(value, label)?;
4181    serde_json::from_value(value).map_err(|err| {
4182        set_native_last_error(format!("{label} was not an LLM request: {err}"));
4183        NemoRelayStatus::InvalidJson
4184    })
4185}
4186
4187fn write_native_json(value: &Json, out: *mut *mut NemoRelayNativeString) -> NemoRelayStatus {
4188    if out.is_null() {
4189        set_native_last_error("out JSON pointer is null");
4190        return NemoRelayStatus::NullPointer;
4191    }
4192    let Some(handle) = native_string_from_json(value) else {
4193        set_native_last_error("failed to serialize native JSON output");
4194        return NemoRelayStatus::Internal;
4195    };
4196    unsafe { *out = handle };
4197    NemoRelayStatus::Ok
4198}
4199
4200#[cfg(test)]
4201#[path = "../../../tests/unit/native_plugin_tests.rs"]
4202mod tests;