1#[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#[derive(Debug, Clone, PartialEq, Eq)]
83pub struct NativePluginLoadSpec {
84 pub plugin_id: String,
86 pub manifest_ref: String,
88}
89
90pub struct NativePluginActivation {
96 plugins: Vec<Arc<NativePluginInstance>>,
97 plugin_registrations: Vec<(String, u64)>,
98}
99
100impl NativePluginActivation {
101 pub fn is_empty(&self) -> bool {
103 self.plugins.is_empty()
104 }
105
106 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
130pub 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 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 _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 _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 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
1950unsafe 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
2073unsafe 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 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;