1use std::collections::BTreeMap;
7use std::ffi::c_void;
8use std::future::Future;
9use std::marker::PhantomData;
10use std::panic::{AssertUnwindSafe, catch_unwind};
11use std::pin::Pin;
12use std::ptr;
13use std::sync::atomic::{AtomicBool, Ordering};
14use std::sync::{Arc, Mutex};
15use std::task::{Context, Poll};
16use std::time::Duration;
17
18use futures::{FutureExt, Stream};
19use serde::Deserialize;
20use serde_json::{Map, Value as Json};
21use tokio::runtime::{Handle, Runtime};
22use tokio_util::task::TaskTracker;
23
24use super::*;
25
26const CANCELLATION_POLL_INTERVAL: Duration = Duration::from_millis(10);
27const CANCELLATION_POLL_MAX_INTERVAL: Duration = Duration::from_millis(160);
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub struct NativeExecutorConfig {
32 pub worker_threads: usize,
34}
35
36impl Default for NativeExecutorConfig {
37 fn default() -> Self {
38 Self { worker_threads: 2 }
39 }
40}
41
42impl NativeExecutorConfig {
43 pub fn with_component_config(mut self, plugin_config: &Map<String, Json>) -> Result<Self> {
49 let Some(executor) = plugin_config.get("executor") else {
50 return self.validate();
51 };
52 let executor = executor
53 .as_object()
54 .ok_or_else(|| "executor configuration must be an object".to_string())?;
55 if let Some(worker_threads) = executor.get("worker_threads") {
56 let worker_threads = worker_threads
57 .as_u64()
58 .and_then(|value| usize::try_from(value).ok())
59 .ok_or_else(|| "executor.worker_threads must be a positive integer".to_string())?;
60 self.worker_threads = worker_threads;
61 }
62 self.validate()
63 }
64
65 fn validate(self) -> Result<Self> {
66 if self.worker_threads == 0 {
67 Err("executor.worker_threads must be greater than zero".into())
68 } else {
69 Ok(self)
70 }
71 }
72}
73
74pub(crate) struct NativeExecutor {
75 config: NativeExecutorConfig,
76 thread_name: String,
77 runtime: Mutex<Option<Runtime>>,
78 tracker: TaskTracker,
79 accepting: AtomicBool,
80}
81
82impl NativeExecutor {
83 pub(crate) fn new(config: NativeExecutorConfig, plugin_kind: &str) -> Arc<Self> {
84 Arc::new(Self {
85 config,
86 thread_name: format!("nemo-relay-plugin-{plugin_kind}"),
87 runtime: Mutex::new(None),
88 tracker: TaskTracker::new(),
89 accepting: AtomicBool::new(true),
90 })
91 }
92
93 fn ensure_started(&self) -> Result<Handle> {
94 if !self.accepting.load(Ordering::Acquire) {
95 return Err("native plugin executor is shutting down".into());
96 }
97 let mut runtime = self
98 .runtime
99 .lock()
100 .unwrap_or_else(|error| error.into_inner());
101 if runtime.is_none() {
102 let built = tokio::runtime::Builder::new_multi_thread()
103 .worker_threads(self.config.worker_threads)
104 .thread_name(self.thread_name.clone())
105 .enable_all()
106 .build()
107 .map_err(|error| format!("failed to start native plugin executor: {error}"))?;
108 *runtime = Some(built);
109 }
110 Ok(runtime
111 .as_ref()
112 .expect("runtime was initialized")
113 .handle()
114 .clone())
115 }
116
117 fn spawn(&self, future: impl Future<Output = ()> + Send + 'static) -> Result<()> {
118 let handle = self.ensure_started()?;
119 self.tracker.spawn_on(future, &handle);
120 Ok(())
121 }
122}
123
124impl Drop for NativeExecutor {
125 fn drop(&mut self) {
126 self.accepting.store(false, Ordering::Release);
127 self.tracker.close();
128 let runtime = self
129 .runtime
130 .get_mut()
131 .unwrap_or_else(|error| error.into_inner())
132 .take();
133 if let Some(runtime) = runtime {
134 let tracker = self.tracker.clone();
135 std::thread::Builder::new()
141 .name(format!("{}-shutdown", self.thread_name))
142 .spawn(move || runtime.block_on(tracker.wait()))
143 .expect("native plugin executor shutdown thread should start")
144 .join()
145 .expect("native plugin executor shutdown thread should not panic");
146 }
147 }
148}
149
150#[derive(Clone, Copy)]
151struct HostV4(NemoRelayNativeHostApiV4);
152
153unsafe impl Send for HostV4 {}
154unsafe impl Sync for HostV4 {}
155
156struct Completion {
157 host: HostV4,
158 raw: *const NemoRelayNativeAsyncCompletion,
159}
160
161unsafe impl Send for Completion {}
162unsafe impl Sync for Completion {}
163
164impl Completion {
165 fn resolve<T: Serialize>(&self, value: &T) -> Result<()> {
166 let value = HostString::from_json(&self.host.0.v3.v1, value)
167 .ok_or_else(|| "failed to serialize native async middleware result".to_string())?;
168 let status =
169 unsafe { (self.host.0.v3.async_completion_resolve_json)(self.raw, value.as_ptr()) };
170 status_result(status, "resolve native async middleware completion")
171 }
172
173 fn reject(&self, message: &str) {
174 if let Some(message) = HostString::new(&self.host.0.v3.v1, message) {
175 unsafe {
176 (self.host.0.v3.async_completion_reject)(self.raw, message.as_ptr());
177 }
178 }
179 }
180
181 fn is_cancelled(&self) -> bool {
182 unsafe { (self.host.0.v3.async_completion_is_cancelled)(self.raw) }
183 }
184}
185
186impl Drop for Completion {
187 fn drop(&mut self) {
188 unsafe { (self.host.0.v3.async_completion_release)(self.raw) };
189 }
190}
191
192#[derive(Clone, Copy)]
193struct CompletionRef {
194 host: HostV4,
195 raw: *const NemoRelayNativeAsyncCompletion,
196}
197
198unsafe impl Send for CompletionRef {}
199
200impl CompletionRef {
201 fn request_context(
202 self,
203 codec: LlmCodecIdentity,
204 ) -> Result<LlmSanitizeRequestContext<'static>> {
205 let resolved = if matches!(codec, LlmCodecIdentity::None) {
206 None
207 } else {
208 let status = unsafe { (self.host.0.async_completion_retain)(self.raw) };
209 status_result(status, "retain native async completion capability")?;
210 Some(LlmSanitizeRequestCodec {
211 async_host: self.host.0,
212 completion: self.raw,
213 completion_release: self.host.0.v3.async_completion_release,
214 _lifetime: PhantomData,
215 })
216 };
217 Ok(LlmSanitizeRequestContext { codec, resolved })
218 }
219
220 fn response_context(
221 self,
222 codec: LlmCodecIdentity,
223 ) -> Result<LlmSanitizeResponseContext<'static>> {
224 let resolved = if matches!(codec, LlmCodecIdentity::None) {
225 None
226 } else {
227 let status = unsafe { (self.host.0.async_completion_retain)(self.raw) };
228 status_result(status, "retain native async completion capability")?;
229 Some(LlmSanitizeResponseCodec {
230 async_host: self.host.0,
231 completion: self.raw,
232 completion_release: self.host.0.v3.async_completion_release,
233 _lifetime: PhantomData,
234 })
235 };
236 Ok(LlmSanitizeResponseContext { codec, resolved })
237 }
238}
239
240struct NextInner {
241 host: HostV4,
242 raw: *const NemoRelayNativeAsyncNext,
243}
244
245unsafe impl Send for NextInner {}
246unsafe impl Sync for NextInner {}
247
248impl Drop for NextInner {
249 fn drop(&mut self) {
250 unsafe { (self.host.0.v3.async_next_release)(self.raw) };
251 }
252}
253
254#[derive(Clone)]
256pub struct ToolNext(Arc<NextInner>);
257
258impl ToolNext {
259 pub async fn call(&self, args: Json) -> Result<ToolExecutionResult> {
261 let result = invoke_unary_next(&self.0, &args).await?;
262 serde_json::from_value(result)
263 .map_err(|error| format!("invalid canonical tool execution result: {error}"))
264 }
265}
266
267#[derive(Clone)]
269pub struct LlmNext(Arc<NextInner>);
270
271impl LlmNext {
272 pub async fn call(&self, request: LlmRequest) -> Result<Json> {
274 invoke_unary_next(&self.0, &request).await
275 }
276}
277
278pub type LlmJsonAsyncStream = Pin<Box<dyn Stream<Item = Result<Json>> + Send>>;
280
281#[derive(Clone)]
283pub struct LlmStreamNext(Arc<NextInner>);
284
285impl LlmStreamNext {
286 pub async fn call(&self, request: LlmRequest) -> Result<LlmJsonAsyncStream> {
288 let request = HostString::from_json(&self.0.host.0.v3.v1, &request)
289 .ok_or_else(|| "failed to serialize LLM stream request".to_string())?;
290 let (sender, receiver) = futures::channel::oneshot::channel();
291 let callback_state = Box::into_raw(Box::new(OpenState {
292 sender,
293 host: self.0.host,
294 }));
295 let status = unsafe {
296 (self.0.host.0.async_next_open_llm_stream)(
297 self.0.raw,
298 request.as_ptr(),
299 open_stream_callback,
300 callback_state.cast(),
301 )
302 };
303 if status != NemoRelayStatus::Ok {
304 drop(unsafe { Box::from_raw(callback_state) });
305 return Err(status_message(
306 &self.0.host.0.v3.v1,
307 status,
308 "open LLM stream",
309 ));
310 }
311 let mut opened = receiver
312 .await
313 .map_err(|_| "LLM stream open callback was dropped".to_string())??;
314 let raw = opened.take();
315 Ok(Box::pin(PullStream {
316 host: self.0.host,
317 raw,
318 pending: None,
319 finished: false,
320 }))
321 }
322}
323
324struct OpenedStream {
325 host: HostV4,
326 raw: *const NemoRelayNativeLlmAsyncStream,
327}
328unsafe impl Send for OpenedStream {}
329
330impl OpenedStream {
331 fn take(&mut self) -> *const NemoRelayNativeLlmAsyncStream {
332 std::mem::replace(&mut self.raw, ptr::null())
333 }
334}
335
336impl Drop for OpenedStream {
337 fn drop(&mut self) {
338 if !self.raw.is_null() {
339 unsafe {
340 (self.host.0.async_llm_stream_cancel)(self.raw);
341 (self.host.0.async_llm_stream_release)(self.raw);
342 }
343 }
344 }
345}
346type OpenSender = futures::channel::oneshot::Sender<Result<OpenedStream>>;
347struct OpenState {
348 sender: OpenSender,
349 host: HostV4,
350}
351
352unsafe extern "C" fn open_stream_callback(
353 user_data: *mut c_void,
354 stream: *const NemoRelayNativeLlmAsyncStream,
355 error: *const NemoRelayNativeString,
356) {
357 let state = unsafe { Box::from_raw(user_data.cast::<OpenState>()) };
358 let result = if !stream.is_null() {
359 Ok(OpenedStream {
360 host: state.host,
361 raw: stream,
362 })
363 } else if !error.is_null() {
364 Err(read_host_string(&state.host.0.v3.v1, error)
365 .unwrap_or_else(|_| "failed to open LLM stream".into()))
366 } else {
367 Err("host returned neither an LLM stream nor an error".into())
368 };
369 let _ = state.sender.send(result);
370}
371
372struct PullStream {
373 host: HostV4,
374 raw: *const NemoRelayNativeLlmAsyncStream,
375 pending: Option<futures::channel::oneshot::Receiver<Result<Option<Json>>>>,
376 finished: bool,
377}
378
379unsafe impl Send for PullStream {}
380
381impl Stream for PullStream {
382 type Item = Result<Json>;
383
384 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
385 if self.finished {
386 return Poll::Ready(None);
387 }
388 if self.pending.is_none() {
389 let (sender, receiver) = futures::channel::oneshot::channel();
390 let callback_state = Box::into_raw(Box::new(PullState {
391 sender,
392 host: self.host,
393 }));
394 let status = unsafe {
395 (self.host.0.async_llm_stream_pull)(
396 self.raw,
397 pull_stream_callback,
398 callback_state.cast(),
399 )
400 };
401 if status != NemoRelayStatus::Ok {
402 drop(unsafe { Box::from_raw(callback_state) });
403 self.finished = true;
404 return Poll::Ready(Some(Err(status_message(
405 &self.host.0.v3.v1,
406 status,
407 "pull LLM stream",
408 ))));
409 }
410 self.pending = Some(receiver);
411 }
412 let pending = self
413 .pending
414 .as_mut()
415 .expect("pull receiver was initialized");
416 match Pin::new(pending).poll(cx) {
417 Poll::Pending => Poll::Pending,
418 Poll::Ready(result) => {
419 self.pending = None;
420 match result {
421 Ok(Ok(Some(chunk))) => Poll::Ready(Some(Ok(chunk))),
422 Ok(Ok(None)) => {
423 self.finished = true;
424 Poll::Ready(None)
425 }
426 Ok(Err(error)) => {
427 self.finished = true;
428 Poll::Ready(Some(Err(error)))
429 }
430 Err(_) => {
431 self.finished = true;
432 Poll::Ready(Some(Err("LLM stream pull callback was dropped".into())))
433 }
434 }
435 }
436 }
437 }
438}
439
440impl Drop for PullStream {
441 fn drop(&mut self) {
442 if !self.finished {
443 unsafe { (self.host.0.async_llm_stream_cancel)(self.raw) };
444 }
445 unsafe { (self.host.0.async_llm_stream_release)(self.raw) };
446 }
447}
448
449type PullSender = futures::channel::oneshot::Sender<Result<Option<Json>>>;
450struct PullState {
451 sender: PullSender,
452 host: HostV4,
453}
454
455unsafe extern "C" fn pull_stream_callback(
456 user_data: *mut c_void,
457 chunk_json: *const NemoRelayNativeString,
458 error: *const NemoRelayNativeString,
459 done: bool,
460) {
461 let state = unsafe { Box::from_raw(user_data.cast::<PullState>()) };
462 let result = if !error.is_null() {
463 Err(read_host_string(&state.host.0.v3.v1, error)
464 .unwrap_or_else(|_| "LLM stream pull failed".into()))
465 } else if done {
466 Ok(None)
467 } else if !chunk_json.is_null() {
468 read_json_value(&state.host.0.v3.v1, chunk_json, "LLM stream chunk")
469 .map_err(|status| format!("invalid LLM stream chunk: {status:?}"))
470 .map(Some)
471 } else {
472 Err("host returned an invalid LLM stream pull result".into())
473 };
474 let _ = state.sender.send(result);
475}
476
477async fn invoke_unary_next<T: Serialize>(next: &NextInner, value: &T) -> Result<Json> {
478 let value = HostString::from_json(&next.host.0.v3.v1, value)
479 .ok_or_else(|| "failed to serialize native continuation input".to_string())?;
480 let (sender, receiver) = futures::channel::oneshot::channel();
481 let callback_state = Box::into_raw(Box::new(UnaryState {
482 sender,
483 host: next.host,
484 }));
485 let status = unsafe {
486 (next.host.0.v3.async_next_invoke_result)(
487 next.raw,
488 value.as_ptr(),
489 unary_next_callback,
490 callback_state.cast(),
491 )
492 };
493 if status != NemoRelayStatus::Ok {
494 drop(unsafe { Box::from_raw(callback_state) });
495 return Err(status_message(
496 &next.host.0.v3.v1,
497 status,
498 "invoke native continuation",
499 ));
500 }
501 receiver
502 .await
503 .map_err(|_| "native continuation callback was dropped".to_string())?
504}
505
506#[cfg(test)]
507#[path = "../tests/unit/async_sdk_tests.rs"]
508mod tests;
509
510type UnarySender = futures::channel::oneshot::Sender<Result<Json>>;
511struct UnaryState {
512 sender: UnarySender,
513 host: HostV4,
514}
515
516unsafe extern "C" fn unary_next_callback(
517 user_data: *mut c_void,
518 value_json: *const NemoRelayNativeString,
519 error: *const NemoRelayNativeString,
520) {
521 let state = unsafe { Box::from_raw(user_data.cast::<UnaryState>()) };
522 let result = if !error.is_null() {
523 Err(read_host_string(&state.host.0.v3.v1, error)
524 .unwrap_or_else(|_| "native continuation failed".into()))
525 } else {
526 read_json_value(
527 &state.host.0.v3.v1,
528 value_json,
529 "native continuation result",
530 )
531 .map_err(|status| format!("invalid native continuation result: {status:?}"))
532 };
533 let _ = state.sender.send(result);
534}
535
536type UnaryFuture = Pin<Box<dyn Future<Output = Result<Json>> + Send>>;
537type UnaryAdapter =
538 dyn Fn(Json, Option<Arc<NextInner>>, CompletionRef) -> UnaryFuture + Send + Sync;
539
540struct UnaryCallbackState {
541 host: HostV4,
542 executor: Arc<NativeExecutor>,
543 adapter: Box<UnaryAdapter>,
544}
545
546unsafe extern "C" fn drop_unary_callback(user_data: *mut c_void) {
547 if !user_data.is_null() {
548 drop(unsafe { Box::from_raw(user_data.cast::<UnaryCallbackState>()) });
549 }
550}
551
552unsafe extern "C" fn unary_trampoline(
553 user_data: *mut c_void,
554 invocation_json: *const NemoRelayNativeString,
555 next: *const NemoRelayNativeAsyncNext,
556 completion: *const NemoRelayNativeAsyncCompletion,
557) -> u32 {
558 let state = unsafe { &*user_data.cast::<UnaryCallbackState>() };
559 let completion = Completion {
560 host: state.host,
561 raw: completion,
562 };
563 let completion_ref = CompletionRef {
564 host: state.host,
565 raw: completion.raw,
566 };
567 let next = (!next.is_null()).then(|| {
568 Arc::new(NextInner {
569 host: state.host,
570 raw: next,
571 })
572 });
573 let invocation = read_json_value(&state.host.0.v3.v1, invocation_json, "async invocation")
574 .map_err(|status| format!("invalid async invocation: {status:?}"));
575 let binding = ScopePollBinding::capture(state.host.0.v3.v1);
576 let future = catch_unwind(AssertUnwindSafe(|| match invocation {
577 Ok(invocation) => (state.adapter)(invocation, next, completion_ref),
578 Err(error) => Box::pin(async move { Err(error) }) as UnaryFuture,
579 }));
580 if let Err(error) = state.executor.ensure_started() {
581 completion.reject(&error);
582 set_last_error(&state.host.0.v3.v1, &error);
583 return NemoRelayNativeAsyncCallbackState::Pending as u32;
584 }
585 let task = match future {
586 Ok(future) => drive_unary(future, binding, completion),
587 Err(_) => drive_unary(
588 Box::pin(async move { Err("typed native middleware callback panicked".into()) }),
589 binding,
590 completion,
591 ),
592 };
593 if let Err(error) = state.executor.spawn(async move {
594 let _ = task.await;
595 }) {
596 set_last_error(&state.host.0.v3.v1, &error);
599 }
600 NemoRelayNativeAsyncCallbackState::Pending as u32
601}
602
603fn drive_unary(
604 future: UnaryFuture,
605 binding: Result<ScopePollBinding>,
606 completion: Completion,
607) -> Pin<Box<dyn Future<Output = ()> + Send>> {
608 Box::pin(async move {
609 let future: UnaryFuture = match binding {
610 Ok(binding) => Box::pin(ScopedFuture::new(future, binding)),
611 Err(error) => {
612 completion.reject(&error);
613 return;
614 }
615 };
616 let result = tokio::select! {
617 result = AssertUnwindSafe(future).catch_unwind() => {
618 result.unwrap_or_else(|_| Err("typed native middleware future panicked".into()))
619 }
620 () = wait_for_completion_cancellation(&completion) => return,
621 };
622 match result {
623 Ok(value) => {
624 if let Err(error) = completion.resolve(&value) {
625 completion.reject(&error);
626 }
627 }
628 Err(error) => completion.reject(&error),
629 }
630 })
631}
632
633async fn wait_for_completion_cancellation(completion: &Completion) {
634 let mut delay = CANCELLATION_POLL_INTERVAL;
635 loop {
636 tokio::time::sleep(delay).await;
637 if completion.is_cancelled() {
638 return;
639 }
640 delay = delay.saturating_mul(2).min(CANCELLATION_POLL_MAX_INTERVAL);
641 }
642}
643
644struct ScopePollBinding {
645 host: NemoRelayNativeHostApiV1,
646 captured: *mut NemoRelayNativeScopeStackBinding,
647}
648
649unsafe impl Send for ScopePollBinding {}
650
651impl ScopePollBinding {
652 fn capture(host: NemoRelayNativeHostApiV1) -> Result<Self> {
653 let mut captured = ptr::null_mut();
654 let status = unsafe { (host.scope_stack_capture_thread)(&mut captured) };
655 if status == NemoRelayStatus::Ok && !captured.is_null() {
656 Ok(Self { host, captured })
657 } else {
658 Err(format!(
659 "failed to capture native callback scope stack: {status:?}"
660 ))
661 }
662 }
663
664 fn enter(&mut self) -> Result<*mut NemoRelayNativeScopeStackBinding> {
665 let mut previous = ptr::null_mut();
666 let status = unsafe { (self.host.scope_stack_capture_thread)(&mut previous) };
667 if status != NemoRelayStatus::Ok || previous.is_null() {
668 return Err(format!(
669 "failed to capture executor scope stack: {status:?}"
670 ));
671 }
672 let captured = std::mem::replace(&mut self.captured, ptr::null_mut());
673 let status = unsafe { (self.host.scope_stack_restore_thread)(captured) };
674 if status != NemoRelayStatus::Ok {
675 unsafe { (self.host.scope_stack_binding_free)(previous) };
676 return Err(format!(
677 "failed to install callback scope stack: {status:?}"
678 ));
679 }
680 Ok(previous)
681 }
682
683 fn exit(&mut self, previous: *mut NemoRelayNativeScopeStackBinding) -> Result<()> {
684 let status = unsafe { (self.host.scope_stack_capture_thread)(&mut self.captured) };
685 if status != NemoRelayStatus::Ok || self.captured.is_null() {
686 let restore_status = unsafe { (self.host.scope_stack_restore_thread)(previous) };
687 if restore_status != NemoRelayStatus::Ok {
688 unsafe { (self.host.scope_stack_binding_free)(previous) };
689 return Err(format!(
690 "failed to recapture callback scope stack: {status:?}; failed to restore executor scope stack: {restore_status:?}"
691 ));
692 }
693 return Err(format!(
694 "failed to recapture callback scope stack: {status:?}"
695 ));
696 }
697 let status = unsafe { (self.host.scope_stack_restore_thread)(previous) };
698 status_result(status, "restore executor scope stack")
699 }
700}
701
702struct ScopePollRestore<'a> {
703 binding: &'a mut ScopePollBinding,
704 previous: Option<*mut NemoRelayNativeScopeStackBinding>,
705}
706
707impl<'a> ScopePollRestore<'a> {
708 fn new(
709 binding: &'a mut ScopePollBinding,
710 previous: *mut NemoRelayNativeScopeStackBinding,
711 ) -> Self {
712 Self {
713 binding,
714 previous: Some(previous),
715 }
716 }
717
718 fn restore(&mut self) -> Result<()> {
719 let previous = self.previous.take().expect("scope binding restored once");
720 self.binding.exit(previous)
721 }
722}
723
724impl Drop for ScopePollRestore<'_> {
725 fn drop(&mut self) {
726 if let Some(previous) = self.previous.take() {
727 let _ = self.binding.exit(previous);
731 }
732 }
733}
734
735impl Drop for ScopePollBinding {
736 fn drop(&mut self) {
737 if !self.captured.is_null() {
738 unsafe { (self.host.scope_stack_binding_free)(self.captured) };
739 }
740 }
741}
742
743struct ScopedFuture<F> {
744 future: F,
745 binding: ScopePollBinding,
746}
747
748impl<F> ScopedFuture<F> {
749 fn new(future: F, binding: ScopePollBinding) -> Self {
750 Self { future, binding }
751 }
752}
753
754impl<F: Future> Future for ScopedFuture<F> {
755 type Output = F::Output;
756
757 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
758 let this = unsafe { self.get_unchecked_mut() };
760 let previous = this
761 .binding
762 .enter()
763 .unwrap_or_else(|error| panic!("{error}"));
764 let mut restore = ScopePollRestore::new(&mut this.binding, previous);
765 let result = unsafe { Pin::new_unchecked(&mut this.future) }.poll(cx);
766 restore.restore().unwrap_or_else(|error| panic!("{error}"));
767 result
768 }
769}
770
771struct ScopedStream<S> {
772 stream: S,
773 binding: ScopePollBinding,
774}
775
776impl<S> ScopedStream<S> {
777 fn new(stream: S, binding: ScopePollBinding) -> Self {
778 Self { stream, binding }
779 }
780}
781
782impl<S: Stream> Stream for ScopedStream<S> {
783 type Item = S::Item;
784
785 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
786 let this = unsafe { self.get_unchecked_mut() };
788 let previous = this
789 .binding
790 .enter()
791 .unwrap_or_else(|error| panic!("{error}"));
792 let mut restore = ScopePollRestore::new(&mut this.binding, previous);
793 let result = unsafe { Pin::new_unchecked(&mut this.stream) }.poll_next(cx);
794 restore.restore().unwrap_or_else(|error| panic!("{error}"));
795 result
796 }
797}
798
799#[derive(Deserialize)]
800struct NameValueInvocation {
801 name: String,
802 value: Json,
803}
804
805#[derive(Deserialize)]
806struct RequestInvocation {
807 request: LlmRequest,
808}
809
810#[derive(Deserialize)]
811struct NameRequestInvocation {
812 name: String,
813 request: LlmRequest,
814}
815
816#[derive(Deserialize)]
817struct LlmRequestInterceptInvocation {
818 name: String,
819 request: LlmRequest,
820 annotated: Option<AnnotatedLlmRequest>,
821}
822
823#[derive(Deserialize)]
824struct EventInvocation {
825 event: Event,
826 fields: EventSanitizeFields,
827}
828
829#[derive(Deserialize)]
830struct EventMetadataInvocation {
831 event: Event,
832}
833
834type StreamFuture = Pin<Box<dyn Future<Output = Result<LlmJsonAsyncStream>> + Send>>;
835type StreamAdapter = dyn Fn(Json, LlmStreamNext) -> StreamFuture + Send + Sync;
836
837struct StreamCallbackState {
838 host: HostV4,
839 executor: Arc<NativeExecutor>,
840 adapter: Box<StreamAdapter>,
841}
842
843unsafe extern "C" fn drop_stream_callback(user_data: *mut c_void) {
844 if !user_data.is_null() {
845 drop(unsafe { Box::from_raw(user_data.cast::<StreamCallbackState>()) });
846 }
847}
848
849struct OutputStream {
850 host: HostV4,
851 raw: *const NemoRelayNativeAsyncStream,
852}
853
854unsafe impl Send for OutputStream {}
855unsafe impl Sync for OutputStream {}
856
857impl OutputStream {
858 fn cancelled(&self) -> bool {
859 unsafe { (self.host.0.v3.async_stream_is_cancelled)(self.raw) }
860 }
861
862 async fn push(&self, value: &Json) -> Result<()> {
863 let value = HostString::from_json(&self.host.0.v3.v1, value)
864 .ok_or_else(|| "failed to serialize native stream chunk".to_string())?;
865 loop {
866 if self.cancelled() {
867 return Err("native stream consumer cancelled".into());
868 }
869 let status =
870 unsafe { (self.host.0.v3.async_stream_push_json)(self.raw, value.as_ptr()) };
871 match status {
872 NemoRelayStatus::Ok => return Ok(()),
873 NemoRelayStatus::Backpressured => {
874 tokio::time::sleep(CANCELLATION_POLL_INTERVAL).await;
875 }
876 status => return Err(format!("push native stream chunk failed: {status:?}")),
877 }
878 }
879 }
880
881 fn finish(&self) -> Result<()> {
882 status_result(
883 unsafe { (self.host.0.v3.async_stream_finish)(self.raw) },
884 "finish native stream",
885 )
886 }
887
888 async fn reject(&self, error: &str) {
889 if let Some(error) = HostString::new(&self.host.0.v3.v1, error) {
890 loop {
891 if self.cancelled() {
892 break;
893 }
894 let status =
895 unsafe { (self.host.0.v3.async_stream_reject)(self.raw, error.as_ptr()) };
896 match status {
897 NemoRelayStatus::Backpressured => {
898 tokio::time::sleep(CANCELLATION_POLL_INTERVAL).await;
899 }
900 _ => break,
901 }
902 }
903 }
904 }
905
906 fn reject_once(&self, error: &str) {
907 if let Some(error) = HostString::new(&self.host.0.v3.v1, error) {
908 unsafe {
909 (self.host.0.v3.async_stream_reject)(self.raw, error.as_ptr());
910 }
911 }
912 }
913}
914
915impl Drop for OutputStream {
916 fn drop(&mut self) {
917 unsafe { (self.host.0.v3.async_stream_release)(self.raw) };
918 }
919}
920
921unsafe extern "C" fn stream_trampoline(
922 user_data: *mut c_void,
923 invocation_json: *const NemoRelayNativeString,
924 next: *const NemoRelayNativeAsyncNext,
925 stream: *const NemoRelayNativeAsyncStream,
926) -> u32 {
927 let state = unsafe { &*user_data.cast::<StreamCallbackState>() };
928 let output = OutputStream {
929 host: state.host,
930 raw: stream,
931 };
932 if next.is_null() {
933 output.reject_once("native stream middleware requires a continuation");
934 return NemoRelayNativeAsyncCallbackState::Pending as u32;
935 }
936 let next = LlmStreamNext(Arc::new(NextInner {
937 host: state.host,
938 raw: next,
939 }));
940 let invocation = read_json_value(&state.host.0.v3.v1, invocation_json, "stream invocation")
941 .map_err(|status| format!("invalid native stream invocation: {status:?}"));
942 let bindings = ScopePollBinding::capture(state.host.0.v3.v1).and_then(|future| {
943 ScopePollBinding::capture(state.host.0.v3.v1).map(|stream| (future, stream))
944 });
945 let future = catch_unwind(AssertUnwindSafe(|| match invocation {
946 Ok(invocation) => (state.adapter)(invocation, next),
947 Err(error) => Box::pin(async move { Err(error) }) as StreamFuture,
948 }));
949 let future = future.unwrap_or_else(|_| {
950 Box::pin(async move { Err("typed native stream callback panicked".into()) })
951 });
952 if let Err(error) = state.executor.ensure_started() {
953 output.reject_once(&error);
954 set_last_error(&state.host.0.v3.v1, &error);
955 return NemoRelayNativeAsyncCallbackState::Pending as u32;
956 }
957 let task = async move {
958 let (future_binding, stream_binding) = match bindings {
959 Ok(bindings) => bindings,
960 Err(error) => {
961 output.reject(&error).await;
962 return;
963 }
964 };
965 let future: StreamFuture = Box::pin(ScopedFuture::new(future, future_binding));
966 let stream = tokio::select! {
967 result = AssertUnwindSafe(future).catch_unwind() => match result {
968 Ok(result) => result,
969 Err(_) => Err("typed native stream future panicked".into()),
970 },
971 () = wait_for_stream_cancellation(&output) => return,
972 };
973 let mut stream = match stream {
974 Ok(stream) => ScopedStream::new(stream, stream_binding),
975 Err(error) => {
976 output.reject(&error).await;
977 return;
978 }
979 };
980 loop {
981 let item = tokio::select! {
982 result = AssertUnwindSafe(futures::StreamExt::next(&mut stream)).catch_unwind() => {
983 match result {
984 Ok(item) => item,
985 Err(_) => {
986 output.reject("typed native stream panicked while polling").await;
987 return;
988 }
989 }
990 },
991 () = wait_for_stream_cancellation(&output) => return,
992 };
993 let Some(item) = item else {
994 break;
995 };
996 match item {
997 Ok(chunk) => {
998 if let Err(error) = output.push(&chunk).await {
999 if !output.cancelled() {
1000 output.reject(&error).await;
1001 }
1002 return;
1003 }
1004 }
1005 Err(error) => {
1006 output.reject(&error).await;
1007 return;
1008 }
1009 }
1010 }
1011 if !output.cancelled() {
1012 let _ = output.finish();
1013 }
1014 };
1015 if let Err(error) = state.executor.spawn(task) {
1016 set_last_error(&state.host.0.v3.v1, &error);
1017 }
1018 NemoRelayNativeAsyncCallbackState::Pending as u32
1019}
1020
1021async fn wait_for_stream_cancellation(output: &OutputStream) {
1022 let mut delay = CANCELLATION_POLL_INTERVAL;
1023 loop {
1024 tokio::time::sleep(delay).await;
1025 if output.cancelled() {
1026 return;
1027 }
1028 delay = delay.saturating_mul(2).min(CANCELLATION_POLL_MAX_INTERVAL);
1029 }
1030}
1031
1032#[derive(Deserialize)]
1033struct CodecInvocation<T> {
1034 #[serde(flatten)]
1035 payload: T,
1036 context: CodecIdentityInvocation,
1037}
1038
1039#[derive(Deserialize)]
1040struct CodecIdentityInvocation {
1041 codec_kind: String,
1042 codec_id: Option<String>,
1043}
1044
1045impl CodecIdentityInvocation {
1046 fn identity(self) -> Result<LlmCodecIdentity> {
1047 match (self.codec_kind.as_str(), self.codec_id) {
1048 ("none", _) => Ok(LlmCodecIdentity::None),
1049 ("opaque", _) => Ok(LlmCodecIdentity::Opaque),
1050 ("builtin", Some(id)) => BuiltinLlmCodec::from_id(&id)
1051 .map(LlmCodecIdentity::BuiltIn)
1052 .ok_or_else(|| format!("unknown built-in LLM codec: {id}")),
1053 ("runtime", Some(id)) => Ok(LlmCodecIdentity::Runtime(id)),
1054 (kind, _) => Err(format!("invalid LLM codec context: {kind}")),
1055 }
1056 }
1057}
1058
1059impl PluginContext<'_> {
1060 fn host_v4(&self) -> Result<HostV4> {
1061 if self.host.abi_version < NEMO_RELAY_NATIVE_ABI_VERSION
1062 || self.host.struct_size < std::mem::size_of::<NemoRelayNativeHostApiV4>()
1063 {
1064 return Err("typed async native middleware requires Relay ABI v4".into());
1065 }
1066 Ok(HostV4(unsafe {
1067 *(self.host as *const _ as *const NemoRelayNativeHostApiV4)
1068 }))
1069 }
1070
1071 fn register_unary_adapter(
1072 &mut self,
1073 kind: NemoRelayNativeAsyncMiddlewareKind,
1074 name: &str,
1075 priority: i32,
1076 break_chain: bool,
1077 adapter: Box<UnaryAdapter>,
1078 ) -> Result<()> {
1079 let state = Box::into_raw(Box::new(UnaryCallbackState {
1080 host: self.host_v4()?,
1081 executor: Arc::clone(&self.executor),
1082 adapter,
1083 }));
1084 let status = unsafe {
1085 self.register_async_middleware_raw(
1086 kind,
1087 name,
1088 priority,
1089 break_chain,
1090 unary_trampoline,
1091 state.cast(),
1092 Some(drop_unary_callback),
1093 )
1094 };
1095 if status == NemoRelayStatus::Ok {
1096 Ok(())
1097 } else {
1098 Err(status_message(
1099 self.host,
1100 status,
1101 registration_operation(kind),
1102 ))
1103 }
1104 }
1105
1106 fn register_event_adapter<F, Fut>(
1107 &mut self,
1108 kind: NemoRelayNativeAsyncMiddlewareKind,
1109 name: &str,
1110 priority: i32,
1111 callback: F,
1112 ) -> Result<()>
1113 where
1114 F: Fn(Arc<Event>, EventSanitizeFields) -> Fut + Send + Sync + 'static,
1115 Fut: Future<Output = Result<EventSanitizeFields>> + Send + 'static,
1116 {
1117 let callback = Arc::new(callback);
1118 self.register_unary_adapter(
1119 kind,
1120 name,
1121 priority,
1122 false,
1123 Box::new(move |value, _, _| {
1124 let callback = Arc::clone(&callback);
1125 Box::pin(async move {
1126 let invocation: EventInvocation = serde_json::from_value(value)
1127 .map_err(|error| format!("invalid event sanitizer invocation: {error}"))?;
1128 serde_json::to_value(
1129 callback(Arc::new(invocation.event), invocation.fields).await?,
1130 )
1131 .map_err(|error| error.to_string())
1132 })
1133 }),
1134 )
1135 }
1136
1137 pub fn register_event_metadata_injector<F, Fut>(
1139 &mut self,
1140 name: &str,
1141 priority: i32,
1142 callback: F,
1143 ) -> Result<()>
1144 where
1145 F: Fn(Arc<Event>) -> Fut + Send + Sync + 'static,
1146 Fut: Future<Output = Result<BTreeMap<String, Json>>> + Send + 'static,
1147 {
1148 let callback = Arc::new(callback);
1149 self.register_unary_adapter(
1150 NemoRelayNativeAsyncMiddlewareKind::EventMetadataInjector,
1151 name,
1152 priority,
1153 false,
1154 Box::new(move |value, _, _| {
1155 let callback = Arc::clone(&callback);
1156 Box::pin(async move {
1157 let invocation: EventMetadataInvocation = serde_json::from_value(value)
1158 .map_err(|error| {
1159 format!("invalid Event metadata injector invocation: {error}")
1160 })?;
1161 serde_json::to_value(callback(Arc::new(invocation.event)).await?)
1162 .map_err(|error| error.to_string())
1163 })
1164 }),
1165 )
1166 }
1167
1168 pub fn register_mark_sanitize_guardrail<F, Fut>(
1170 &mut self,
1171 name: &str,
1172 priority: i32,
1173 callback: F,
1174 ) -> Result<()>
1175 where
1176 F: Fn(Arc<Event>, EventSanitizeFields) -> Fut + Send + Sync + 'static,
1177 Fut: Future<Output = Result<EventSanitizeFields>> + Send + 'static,
1178 {
1179 self.register_event_adapter(
1180 NemoRelayNativeAsyncMiddlewareKind::MarkSanitize,
1181 name,
1182 priority,
1183 callback,
1184 )
1185 }
1186
1187 pub fn register_scope_sanitize_start_guardrail<F, Fut>(
1189 &mut self,
1190 name: &str,
1191 priority: i32,
1192 callback: F,
1193 ) -> Result<()>
1194 where
1195 F: Fn(Arc<Event>, EventSanitizeFields) -> Fut + Send + Sync + 'static,
1196 Fut: Future<Output = Result<EventSanitizeFields>> + Send + 'static,
1197 {
1198 self.register_event_adapter(
1199 NemoRelayNativeAsyncMiddlewareKind::ScopeSanitizeStart,
1200 name,
1201 priority,
1202 callback,
1203 )
1204 }
1205
1206 pub fn register_scope_sanitize_end_guardrail<F, Fut>(
1208 &mut self,
1209 name: &str,
1210 priority: i32,
1211 callback: F,
1212 ) -> Result<()>
1213 where
1214 F: Fn(Arc<Event>, EventSanitizeFields) -> Fut + Send + Sync + 'static,
1215 Fut: Future<Output = Result<EventSanitizeFields>> + Send + 'static,
1216 {
1217 self.register_event_adapter(
1218 NemoRelayNativeAsyncMiddlewareKind::ScopeSanitizeEnd,
1219 name,
1220 priority,
1221 callback,
1222 )
1223 }
1224
1225 fn register_tool_json_adapter<F, Fut>(
1226 &mut self,
1227 kind: NemoRelayNativeAsyncMiddlewareKind,
1228 name: &str,
1229 priority: i32,
1230 break_chain: bool,
1231 callback: F,
1232 ) -> Result<()>
1233 where
1234 F: Fn(String, Json) -> Fut + Send + Sync + 'static,
1235 Fut: Future<Output = Result<Json>> + Send + 'static,
1236 {
1237 let callback = Arc::new(callback);
1238 self.register_unary_adapter(
1239 kind,
1240 name,
1241 priority,
1242 break_chain,
1243 Box::new(move |value, _, _| {
1244 let callback = Arc::clone(&callback);
1245 Box::pin(async move {
1246 let invocation: NameValueInvocation =
1247 serde_json::from_value(value).map_err(|error| error.to_string())?;
1248 callback(invocation.name, invocation.value).await
1249 })
1250 }),
1251 )
1252 }
1253
1254 pub fn register_tool_sanitize_request_guardrail<F, Fut>(
1256 &mut self,
1257 name: &str,
1258 priority: i32,
1259 callback: F,
1260 ) -> Result<()>
1261 where
1262 F: Fn(String, Json) -> Fut + Send + Sync + 'static,
1263 Fut: Future<Output = Result<Json>> + Send + 'static,
1264 {
1265 self.register_tool_json_adapter(
1266 NemoRelayNativeAsyncMiddlewareKind::ToolSanitizeRequest,
1267 name,
1268 priority,
1269 false,
1270 callback,
1271 )
1272 }
1273
1274 pub fn register_tool_sanitize_response_guardrail<F, Fut>(
1276 &mut self,
1277 name: &str,
1278 priority: i32,
1279 callback: F,
1280 ) -> Result<()>
1281 where
1282 F: Fn(String, Json) -> Fut + Send + Sync + 'static,
1283 Fut: Future<Output = Result<Json>> + Send + 'static,
1284 {
1285 self.register_tool_json_adapter(
1286 NemoRelayNativeAsyncMiddlewareKind::ToolSanitizeResponse,
1287 name,
1288 priority,
1289 false,
1290 callback,
1291 )
1292 }
1293
1294 pub fn register_tool_conditional_execution_guardrail<F, Fut>(
1296 &mut self,
1297 name: &str,
1298 priority: i32,
1299 callback: F,
1300 ) -> Result<()>
1301 where
1302 F: Fn(String, Json) -> Fut + Send + Sync + 'static,
1303 Fut: Future<Output = Result<Option<String>>> + Send + 'static,
1304 {
1305 let callback = Arc::new(callback);
1306 self.register_unary_adapter(
1307 NemoRelayNativeAsyncMiddlewareKind::ToolConditionalExecution,
1308 name,
1309 priority,
1310 false,
1311 Box::new(move |value, _, _| {
1312 let callback = Arc::clone(&callback);
1313 Box::pin(async move {
1314 let invocation: NameValueInvocation =
1315 serde_json::from_value(value).map_err(|error| error.to_string())?;
1316 serde_json::to_value(callback(invocation.name, invocation.value).await?)
1317 .map_err(|error| error.to_string())
1318 })
1319 }),
1320 )
1321 }
1322
1323 pub fn register_tool_request_intercept<F, Fut>(
1325 &mut self,
1326 name: &str,
1327 priority: i32,
1328 break_chain: bool,
1329 callback: F,
1330 ) -> Result<()>
1331 where
1332 F: Fn(String, Json) -> Fut + Send + Sync + 'static,
1333 Fut: Future<Output = Result<Json>> + Send + 'static,
1334 {
1335 self.register_tool_json_adapter(
1336 NemoRelayNativeAsyncMiddlewareKind::ToolRequestIntercept,
1337 name,
1338 priority,
1339 break_chain,
1340 callback,
1341 )
1342 }
1343
1344 pub fn register_tool_execution_intercept<F, Fut>(
1346 &mut self,
1347 name: &str,
1348 priority: i32,
1349 callback: F,
1350 ) -> Result<()>
1351 where
1352 F: Fn(String, Json, ToolNext) -> Fut + Send + Sync + 'static,
1353 Fut: Future<Output = Result<ToolExecutionInterceptOutcome>> + Send + 'static,
1354 {
1355 let callback = Arc::new(callback);
1356 self.register_unary_adapter(
1357 NemoRelayNativeAsyncMiddlewareKind::ToolExecutionIntercept,
1358 name,
1359 priority,
1360 false,
1361 Box::new(move |value, next, _| {
1362 let callback = Arc::clone(&callback);
1363 Box::pin(async move {
1364 let invocation: NameValueInvocation =
1365 serde_json::from_value(value).map_err(|error| error.to_string())?;
1366 let next = ToolNext(
1367 next.ok_or_else(|| "tool execution continuation was null".to_string())?,
1368 );
1369 serde_json::to_value(callback(invocation.name, invocation.value, next).await?)
1370 .map_err(|error| error.to_string())
1371 })
1372 }),
1373 )
1374 }
1375
1376 pub fn register_llm_sanitize_request_guardrail<F, Fut>(
1378 &mut self,
1379 name: &str,
1380 priority: i32,
1381 callback: F,
1382 ) -> Result<()>
1383 where
1384 F: Fn(LlmRequest, LlmSanitizeRequestContext<'static>) -> Fut + Send + Sync + 'static,
1385 Fut: Future<Output = Result<Option<LlmRequest>>> + Send + 'static,
1386 {
1387 let callback = Arc::new(callback);
1388 self.register_unary_adapter(
1389 NemoRelayNativeAsyncMiddlewareKind::LlmSanitizeRequest,
1390 name,
1391 priority,
1392 false,
1393 Box::new(move |value, _, completion| {
1394 let callback = Arc::clone(&callback);
1395 Box::pin(async move {
1396 #[derive(Deserialize)]
1397 struct Payload {
1398 request: LlmRequest,
1399 }
1400 let invocation: CodecInvocation<Payload> =
1401 serde_json::from_value(value).map_err(|error| error.to_string())?;
1402 let codec = invocation.context.identity()?;
1403 let context = completion.request_context(codec)?;
1404 serde_json::to_value(callback(invocation.payload.request, context).await?)
1405 .map_err(|error| error.to_string())
1406 })
1407 }),
1408 )
1409 }
1410
1411 pub fn register_llm_sanitize_response_guardrail<F, Fut>(
1413 &mut self,
1414 name: &str,
1415 priority: i32,
1416 callback: F,
1417 ) -> Result<()>
1418 where
1419 F: Fn(Json, LlmSanitizeResponseContext<'static>) -> Fut + Send + Sync + 'static,
1420 Fut: Future<Output = Result<Option<Json>>> + Send + 'static,
1421 {
1422 let callback = Arc::new(callback);
1423 self.register_unary_adapter(
1424 NemoRelayNativeAsyncMiddlewareKind::LlmSanitizeResponse,
1425 name,
1426 priority,
1427 false,
1428 Box::new(move |value, _, completion| {
1429 let callback = Arc::clone(&callback);
1430 Box::pin(async move {
1431 #[derive(Deserialize)]
1432 struct Payload {
1433 response: Json,
1434 }
1435 let invocation: CodecInvocation<Payload> =
1436 serde_json::from_value(value).map_err(|error| error.to_string())?;
1437 let codec = invocation.context.identity()?;
1438 let context = completion.response_context(codec)?;
1439 serde_json::to_value(callback(invocation.payload.response, context).await?)
1440 .map_err(|error| error.to_string())
1441 })
1442 }),
1443 )
1444 }
1445
1446 pub fn register_llm_conditional_execution_guardrail<F, Fut>(
1448 &mut self,
1449 name: &str,
1450 priority: i32,
1451 callback: F,
1452 ) -> Result<()>
1453 where
1454 F: Fn(LlmRequest) -> Fut + Send + Sync + 'static,
1455 Fut: Future<Output = Result<Option<String>>> + Send + 'static,
1456 {
1457 let callback = Arc::new(callback);
1458 self.register_unary_adapter(
1459 NemoRelayNativeAsyncMiddlewareKind::LlmConditionalExecution,
1460 name,
1461 priority,
1462 false,
1463 Box::new(move |value, _, _| {
1464 let callback = Arc::clone(&callback);
1465 Box::pin(async move {
1466 let invocation: RequestInvocation =
1467 serde_json::from_value(value).map_err(|error| error.to_string())?;
1468 serde_json::to_value(callback(invocation.request).await?)
1469 .map_err(|error| error.to_string())
1470 })
1471 }),
1472 )
1473 }
1474
1475 pub fn register_llm_request_intercept<F, Fut>(
1477 &mut self,
1478 name: &str,
1479 priority: i32,
1480 break_chain: bool,
1481 callback: F,
1482 ) -> Result<()>
1483 where
1484 F: Fn(String, LlmRequest, Option<AnnotatedLlmRequest>) -> Fut + Send + Sync + 'static,
1485 Fut: Future<Output = Result<LlmRequestInterceptOutcome>> + Send + 'static,
1486 {
1487 let callback = Arc::new(callback);
1488 self.register_unary_adapter(
1489 NemoRelayNativeAsyncMiddlewareKind::LlmRequestIntercept,
1490 name,
1491 priority,
1492 break_chain,
1493 Box::new(move |value, _, _| {
1494 let callback = Arc::clone(&callback);
1495 Box::pin(async move {
1496 let invocation: LlmRequestInterceptInvocation =
1497 serde_json::from_value(value).map_err(|error| error.to_string())?;
1498 serde_json::to_value(
1499 callback(invocation.name, invocation.request, invocation.annotated).await?,
1500 )
1501 .map_err(|error| error.to_string())
1502 })
1503 }),
1504 )
1505 }
1506
1507 pub fn register_llm_execution_intercept<F, Fut>(
1509 &mut self,
1510 name: &str,
1511 priority: i32,
1512 callback: F,
1513 ) -> Result<()>
1514 where
1515 F: Fn(String, LlmRequest, LlmNext) -> Fut + Send + Sync + 'static,
1516 Fut: Future<Output = Result<Json>> + Send + 'static,
1517 {
1518 let callback = Arc::new(callback);
1519 self.register_unary_adapter(
1520 NemoRelayNativeAsyncMiddlewareKind::LlmExecutionIntercept,
1521 name,
1522 priority,
1523 false,
1524 Box::new(move |value, next, _| {
1525 let callback = Arc::clone(&callback);
1526 Box::pin(async move {
1527 let invocation: NameRequestInvocation =
1528 serde_json::from_value(value).map_err(|error| error.to_string())?;
1529 let next = LlmNext(
1530 next.ok_or_else(|| "LLM execution continuation was null".to_string())?,
1531 );
1532 callback(invocation.name, invocation.request, next).await
1533 })
1534 }),
1535 )
1536 }
1537
1538 pub fn register_llm_stream_execution_intercept<F, Fut>(
1540 &mut self,
1541 name: &str,
1542 priority: i32,
1543 callback: F,
1544 ) -> Result<()>
1545 where
1546 F: Fn(String, LlmRequest, LlmStreamNext) -> Fut + Send + Sync + 'static,
1547 Fut: Future<Output = Result<LlmJsonAsyncStream>> + Send + 'static,
1548 {
1549 let callback = Arc::new(callback);
1550 let state = Box::into_raw(Box::new(StreamCallbackState {
1551 host: self.host_v4()?,
1552 executor: Arc::clone(&self.executor),
1553 adapter: Box::new(move |value, next| {
1554 let callback = Arc::clone(&callback);
1555 Box::pin(async move {
1556 let invocation: NameRequestInvocation =
1557 serde_json::from_value(value).map_err(|error| error.to_string())?;
1558 callback(invocation.name, invocation.request, next).await
1559 })
1560 }),
1561 }));
1562 let status = unsafe {
1563 self.register_async_stream_middleware_raw(
1564 name,
1565 priority,
1566 stream_trampoline,
1567 state.cast(),
1568 Some(drop_stream_callback),
1569 )
1570 };
1571 if status == NemoRelayStatus::Ok {
1572 Ok(())
1573 } else {
1574 Err(status_message(
1575 self.host,
1576 status,
1577 "register typed async stream middleware",
1578 ))
1579 }
1580 }
1581}
1582
1583fn status_result(status: NemoRelayStatus, operation: &str) -> Result<()> {
1584 if status == NemoRelayStatus::Ok {
1585 Ok(())
1586 } else {
1587 Err(format!("{operation} failed: {status:?}"))
1588 }
1589}
1590
1591fn registration_operation(kind: NemoRelayNativeAsyncMiddlewareKind) -> &'static str {
1592 match kind {
1593 NemoRelayNativeAsyncMiddlewareKind::ToolSanitizeRequest => "tool request sanitizer",
1594 NemoRelayNativeAsyncMiddlewareKind::ToolSanitizeResponse => "tool response sanitizer",
1595 NemoRelayNativeAsyncMiddlewareKind::ToolConditionalExecution => {
1596 "tool conditional guardrail"
1597 }
1598 NemoRelayNativeAsyncMiddlewareKind::ToolRequestIntercept => "tool request intercept",
1599 NemoRelayNativeAsyncMiddlewareKind::ToolExecutionIntercept => "tool execution intercept",
1600 NemoRelayNativeAsyncMiddlewareKind::LlmSanitizeRequest => "LLM request sanitizer",
1601 NemoRelayNativeAsyncMiddlewareKind::LlmSanitizeResponse => "LLM response sanitizer",
1602 NemoRelayNativeAsyncMiddlewareKind::LlmConditionalExecution => "LLM conditional guardrail",
1603 NemoRelayNativeAsyncMiddlewareKind::LlmRequestIntercept => "LLM request intercept",
1604 NemoRelayNativeAsyncMiddlewareKind::LlmExecutionIntercept => "LLM execution intercept",
1605 NemoRelayNativeAsyncMiddlewareKind::LlmStreamExecutionIntercept => {
1606 "LLM stream execution intercept"
1607 }
1608 NemoRelayNativeAsyncMiddlewareKind::MarkSanitize => "mark sanitizer",
1609 NemoRelayNativeAsyncMiddlewareKind::ScopeSanitizeStart => "scope start sanitizer",
1610 NemoRelayNativeAsyncMiddlewareKind::ScopeSanitizeEnd => "scope end sanitizer",
1611 NemoRelayNativeAsyncMiddlewareKind::EventMetadataInjector => "Event metadata injector",
1612 }
1613}
1614
1615fn status_message(
1616 host: &NemoRelayNativeHostApiV1,
1617 status: NemoRelayStatus,
1618 operation: &str,
1619) -> String {
1620 let _ = host;
1621 format!("{operation} failed: {status:?}")
1622}