Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
6a2ff49
feat!: add async middleware across bindings
willkill07 Jul 27, 2026
1b1b57c
fix: harden async middleware bridges
willkill07 Jul 27, 2026
54e1780
fix: repair ffi CI validation
willkill07 Jul 27, 2026
6f1dbf3
fix: address async middleware review findings
willkill07 Jul 27, 2026
c6d57bc
fix: address remaining async middleware feedback
willkill07 Jul 27, 2026
6044fa6
fix: synchronize Rust attributions
willkill07 Jul 27, 2026
78fda3a
fix: harden middleware feedback paths
willkill07 Jul 27, 2026
5dd66a9
fix: preserve middleware serialization failures
willkill07 Jul 27, 2026
0b80595
fix: match generated attribution layout
willkill07 Jul 27, 2026
098a3c4
fix: preserve callback rejection boundaries
willkill07 Jul 27, 2026
3fac046
test: wait for queued subscriber delivery
willkill07 Jul 27, 2026
a8ecf81
test: wait for async scope event delivery
willkill07 Jul 27, 2026
ec07e8e
fix: correct async LLM intercept declarations
willkill07 Jul 27, 2026
cba52e6
test: validate canonical intercept declaration
willkill07 Jul 27, 2026
66850f4
test: await async OpenClaw export delivery
willkill07 Jul 27, 2026
d988616
test: update native ABI version expectation
willkill07 Jul 27, 2026
bc301d8
test: await async LLM event delivery
willkill07 Jul 27, 2026
0b56d31
fix: finalize stream events before completion
willkill07 Jul 27, 2026
8caa18b
test: tighten async middleware coverage
willkill07 Jul 27, 2026
66dc4f4
fix: preserve node sanitizer serialization errors
willkill07 Jul 27, 2026
bc4501b
test: await async LLM sanitizer event delivery
willkill07 Jul 27, 2026
057e210
fix: preserve async middleware event semantics
willkill07 Jul 27, 2026
17bded2
test: flush manual session LLM events
willkill07 Jul 27, 2026
760b15e
test: cover async middleware bindings
willkill07 Jul 27, 2026
1c862c3
test: cover async middleware registrations
willkill07 Jul 28, 2026
c36a4a5
test: exercise async middleware callbacks
willkill07 Jul 28, 2026
9d7e847
test: await LLM sanitizer callback
willkill07 Jul 28, 2026
ada9062
fix: harden async middleware publication
willkill07 Jul 28, 2026
43df441
fix: restore CI middleware checks
willkill07 Jul 28, 2026
f7b2556
refactor: consolidate async middleware registrations
willkill07 Jul 28, 2026
44ca679
fix: retain async callback typedef in C header
willkill07 Jul 28, 2026
2196ff2
fix: address async middleware review findings
willkill07 Jul 28, 2026
4724e51
fix: tighten async ABI review checks
willkill07 Jul 28, 2026
8501193
fix: reject duplicate async ABI declarations
willkill07 Jul 28, 2026
7576403
test: raise runtime and binding coverage
willkill07 Jul 28, 2026
e635472
test: wait for contextual sanitizer events
willkill07 Jul 28, 2026
6421da2
test: wait for failed LLM sanitizer event
willkill07 Jul 28, 2026
d3ffab5
fix: retain native plugin during async callbacks
willkill07 Jul 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 26 additions & 20 deletions crates/adaptive/src/acg_component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -582,26 +582,32 @@ pub(crate) fn create_acg_llm_request_intercept(
provider: String,
plugin: Arc<dyn ProviderPlugin>,
) -> LlmRequestInterceptFn {
Arc::new(move |_name: &str, request: LlmRequest, annotated| {
let input_content = request.content.clone();
let translated =
translate_request(&request, &agent_id, &provider, plugin.as_ref(), &hot_cache)
.unwrap_or(request);
if annotated.is_some() && translated.content != input_content {
let translated_annotated = build_semantic_request_view(&translated)
.map_err(|error| nemo_relay::error::FlowError::Internal(error.to_string()))?
.annotated_request;
return Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new(
LlmRequest {
headers: translated.headers,
content: input_content,
},
Some(translated_annotated),
));
}
Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new(
translated, annotated,
))
Arc::new(move |_name: String, request: LlmRequest, annotated| {
let hot_cache = hot_cache.clone();
let agent_id = agent_id.clone();
let provider = provider.clone();
let plugin = plugin.clone();
Box::pin(async move {
let input_content = request.content.clone();
let translated =
translate_request(&request, &agent_id, &provider, plugin.as_ref(), &hot_cache)
.unwrap_or(request);
if annotated.is_some() && translated.content != input_content {
let translated_annotated = build_semantic_request_view(&translated)
.map_err(|error| nemo_relay::error::FlowError::Internal(error.to_string()))?
.annotated_request;
return Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new(
LlmRequest {
headers: translated.headers,
content: input_content,
},
Some(translated_annotated),
));
}
Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new(
translated, annotated,
))
})
})
}

Expand Down
49 changes: 26 additions & 23 deletions crates/adaptive/src/adaptive_hints_intercept.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,31 +174,34 @@ impl AdaptiveHintsIntercept {
pub fn into_request_fn(self) -> LlmRequestInterceptFn {
let this = Arc::new(self);
Arc::new(
move |_name: &str,
move |_name: String,
mut request: LlmRequest,
mut annotated: Option<AnnotatedLlmRequest>| {
let scope_path = extract_scope_path();
let manual_ls = read_manual_latency_sensitivity();
let scope_depth = scope_path.len();
let call_index = this.call_counter.fetch_add(1, Ordering::Relaxed);

let effective_agent_id = this.effective_agent_id();
let cached_hints =
this.load_hints(&scope_path, &effective_agent_id, call_index, scope_depth);
let final_hints = apply_manual_latency_override(
cached_hints,
manual_ls,
&effective_agent_id,
scope_depth,
);

if let Some(hints) = final_hints {
inject_agent_hints(&mut request, &mut annotated, &hints);
}

Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new(
request, annotated,
))
let this = this.clone();
Box::pin(async move {
let scope_path = extract_scope_path();
let manual_ls = read_manual_latency_sensitivity();
let scope_depth = scope_path.len();
let call_index = this.call_counter.fetch_add(1, Ordering::Relaxed);

let effective_agent_id = this.effective_agent_id();
let cached_hints =
this.load_hints(&scope_path, &effective_agent_id, call_index, scope_depth);
let final_hints = apply_manual_latency_override(
cached_hints,
manual_ls,
&effective_agent_id,
scope_depth,
);

if let Some(hints) = final_hints {
inject_agent_hints(&mut request, &mut annotated, &hints);
}

Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new(
request, annotated,
))
})
},
)
}
Expand Down
4 changes: 4 additions & 0 deletions crates/adaptive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
pub(crate) static TEST_GLOBAL_CONTEXT_MUTEX: tokio::sync::Mutex<()> =
tokio::sync::Mutex::const_new(());

#[cfg(test)]
#[path = "../tests/support/mod.rs"]
pub(crate) mod test_support;

pub mod acg;
pub mod acg_component;
pub mod acg_learner;
Expand Down
12 changes: 8 additions & 4 deletions crates/adaptive/tests/integration/runtime_integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,7 @@ async fn test_adaptive_plugin_registers_and_passes_calls_through() {
content: json!({"messages": []}),
},
)
.await
.unwrap();
assert_eq!(request.request.content["messages"], json!([]));

Expand Down Expand Up @@ -739,9 +740,11 @@ impl Plugin for HeaderPlugin {
false,
Arc::new(|_name, mut request, annotated| {
request.headers.insert("x-plugin".into(), json!("set"));
Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new(
request, annotated,
))
Box::pin(async move {
Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new(
request, annotated,
))
})
}),
)?;
ctx.register_tool_request_intercept(
Expand All @@ -752,7 +755,7 @@ impl Plugin for HeaderPlugin {
if let Json::Object(ref mut map) = args {
map.insert("x-tool-plugin".into(), json!(true));
}
Ok(args)
Box::pin(async move { Ok(args) })
}),
)?;
ctx.register_llm_execution_intercept(
Expand Down Expand Up @@ -823,6 +826,7 @@ async fn test_top_level_plugin_registers_request_and_execution_intercepts() {
content: json!({"messages": []}),
},
)
.await
.unwrap();
assert_eq!(request.request.headers.get("x-plugin"), Some(&json!("set")));

Expand Down
12 changes: 12 additions & 0 deletions crates/adaptive/tests/support/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

use std::future::Future;

pub(crate) fn block_on<F: Future>(future: F) -> F::Output {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("test runtime should build")
.block_on(future)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
13 changes: 9 additions & 4 deletions crates/adaptive/tests/unit/acg_component_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1087,11 +1087,11 @@ fn acg_component_request_intercept_passes_original_request_and_annotation_when_t
plugin,
);

let outcome = intercept(
"anthropic",
let outcome = crate::test_support::block_on(intercept(
"anthropic".to_string(),
invalid_request.clone(),
Some(annotated.clone()),
)
))
.expect("request intercept should pass through");
let translated = outcome.request;
let returned_annotated = outcome.annotated_request;
Expand Down Expand Up @@ -1335,7 +1335,12 @@ fn acg_component_request_intercept_rewrites_annotation_without_mutating_provider
plugin,
);

let outcome = intercept("anthropic", request, Some(original_annotation.clone())).unwrap();
let outcome = crate::test_support::block_on(intercept(
"anthropic".to_string(),
request,
Some(original_annotation.clone()),
))
.unwrap();

assert_eq!(outcome.request.content, original_content);
let annotation = outcome
Expand Down
19 changes: 10 additions & 9 deletions crates/adaptive/tests/unit/adaptive_hints_intercept_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
//! Unit tests for adaptive hints intercept in the NeMo Relay adaptive crate.

use super::*;

use std::sync::{Mutex, OnceLock};

use crate::trie::data_models::{LlmCallPrediction, PredictionMetrics};
Expand Down Expand Up @@ -196,14 +197,14 @@ fn test_adaptive_hints_intercept_injects_prediction_hints_and_manual_override()
stream: None,
extra: serde_json::Map::new(),
};
let outcome = req_fn(
"model",
let outcome = crate::test_support::block_on(req_fn(
"model".to_string(),
LlmRequest {
headers: serde_json::Map::new(),
content: serde_json::json!({}),
},
Some(annotated.clone()),
)
))
.unwrap();
let request = outcome.request;
let returned_annotated = outcome.annotated_request;
Expand Down Expand Up @@ -266,14 +267,14 @@ fn test_adaptive_hints_intercept_uses_defaults_and_ignores_poisoned_cache() {
}));
let req_fn =
AdaptiveHintsIntercept::new(hot_cache, "fallback-agent".to_string()).into_request_fn();
let outcome = req_fn(
"model",
let outcome = crate::test_support::block_on(req_fn(
"model".to_string(),
LlmRequest {
headers: serde_json::Map::new(),
content: serde_json::json!({}),
},
None,
)
))
.unwrap();
let request = outcome.request;
let annotated = outcome.annotated_request;
Expand Down Expand Up @@ -305,14 +306,14 @@ fn test_adaptive_hints_intercept_uses_defaults_and_ignores_poisoned_cache() {
});
let poisoned_req_fn =
AdaptiveHintsIntercept::new(poisoned_cache, "fallback-agent".to_string()).into_request_fn();
let poisoned_outcome = poisoned_req_fn(
"model",
let poisoned_outcome = crate::test_support::block_on(poisoned_req_fn(
"model".to_string(),
LlmRequest {
headers: serde_json::Map::new(),
content: serde_json::json!({"existing": true}),
},
None,
)
))
.unwrap();
let poisoned_request = poisoned_outcome.request;
assert!(
Expand Down
1 change: 1 addition & 0 deletions crates/adaptive/tests/unit/plugin_component_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ async fn adaptive_plugin_registers_runtime_and_rolls_back_registration() {
content: json!({}),
},
)
.await
.unwrap();
assert!(request.request.headers.is_empty());

Expand Down
25 changes: 16 additions & 9 deletions crates/adaptive/tests/unit/runtime_features_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,11 @@ fn assert_llm_request_intercept_registered(name: &str) {
i32::MAX,
false,
Arc::new(|_name, request, annotated| {
Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new(
request, annotated,
))
Box::pin(async move {
Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new(
request, annotated,
))
})
}),
),
name,
Expand All @@ -154,9 +156,11 @@ fn assert_llm_request_intercept_absent(name: &str) {
i32::MAX,
false,
Arc::new(|_name, request, annotated| {
Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new(
request, annotated,
))
Box::pin(async move {
Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new(
request, annotated,
))
})
}),
)
.unwrap();
Expand Down Expand Up @@ -565,6 +569,7 @@ async fn adaptive_hints_feature_registers_request_intercept() {
content: json!({}),
},
)
.await
.unwrap();
assert!(request.request.headers.contains_key(AGENT_HINTS_HEADER_KEY));

Expand Down Expand Up @@ -730,9 +735,11 @@ async fn registration_context_registers_all_supported_callback_types() {
5,
false,
Arc::new(|_name, request, annotated| {
Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new(
request, annotated,
))
Box::pin(async move {
Ok(nemo_relay::api::llm::LlmRequestInterceptOutcome::new(
request, annotated,
))
})
}),
)
.unwrap();
Expand Down
1 change: 1 addition & 0 deletions crates/adaptive/tests/unit/runtime_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,7 @@ async fn adaptive_runtime_bind_scope_requires_registration_and_passes_through_wi
};

let translated = llm_request_intercepts("anthropic", request.clone())
.await
.expect("request intercept chain should pass through when no hot-cache state exists");

assert_eq!(translated.request.content, request.content);
Expand Down
Loading
Loading