Skip to content

Commit 6ea55f1

Browse files
committed
feat: add opaque tool result annotations
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
1 parent 7b7f2ad commit 6ea55f1

69 files changed

Lines changed: 5032 additions & 240 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

crates/core/src/api/registry.rs

Lines changed: 85 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
use crate::api::runtime::{
88
EventSanitizeFn, LlmConditionalFn, LlmExecutionFn, LlmRequestInterceptFn, LlmSanitizeRequestFn,
99
LlmSanitizeResponseFn, LlmStreamExecutionFn, ToolConditionalFn, ToolExecutionFn,
10-
ToolInterceptFn, ToolSanitizeFn,
10+
ToolExecutionFrameFn, ToolInterceptFn, ToolSanitizeFn,
1111
};
1212
use crate::api::runtime::{current_scope_stack, global_context};
1313
use crate::api::shared::ensure_runtime_owner;
@@ -203,12 +203,10 @@ macro_rules! global_intercept_registry_api {
203203
};
204204
}
205205

206-
macro_rules! global_execution_registry_api {
206+
macro_rules! global_execution_registry_register_api {
207207
(
208208
$(#[$register_meta:meta])*
209209
$register_name:ident,
210-
$(#[$deregister_meta:meta])*
211-
$deregister_name:ident,
212210
$field:ident,
213211
$fn_type:ty
214212
) => {
@@ -233,10 +231,18 @@ macro_rules! global_execution_registry_api {
233231
.map_err(|error| FlowError::Internal(error.to_string()))?;
234232
state
235233
.$field
236-
.register(ExecutionIntercept::new(name, priority, callable))
234+
.register(ExecutionIntercept::new(name, priority, callable.into()))
237235
.map_err(FlowError::AlreadyExists)
238236
}
237+
};
238+
}
239239

240+
macro_rules! global_execution_registry_deregister_api {
241+
(
242+
$(#[$deregister_meta:meta])*
243+
$deregister_name:ident,
244+
$field:ident
245+
) => {
240246
$(#[$deregister_meta])*
241247
///
242248
/// # Parameters
@@ -259,6 +265,29 @@ macro_rules! global_execution_registry_api {
259265
};
260266
}
261267

268+
macro_rules! global_execution_registry_api {
269+
(
270+
$(#[$register_meta:meta])*
271+
$register_name:ident,
272+
$(#[$deregister_meta:meta])*
273+
$deregister_name:ident,
274+
$field:ident,
275+
$fn_type:ty
276+
) => {
277+
global_execution_registry_register_api!(
278+
$(#[$register_meta])*
279+
$register_name,
280+
$field,
281+
$fn_type
282+
);
283+
global_execution_registry_deregister_api!(
284+
$(#[$deregister_meta])*
285+
$deregister_name,
286+
$field
287+
);
288+
};
289+
}
290+
262291
macro_rules! scope_guardrail_registry_api {
263292
(
264293
$(#[$register_meta:meta])*
@@ -400,12 +429,10 @@ macro_rules! scope_intercept_registry_api {
400429
};
401430
}
402431

403-
macro_rules! scope_execution_registry_api {
432+
macro_rules! scope_execution_registry_register_api {
404433
(
405434
$(#[$register_meta:meta])*
406435
$register_name:ident,
407-
$(#[$deregister_meta:meta])*
408-
$deregister_name:ident,
409436
$field:ident,
410437
$fn_type:ty
411438
) => {
@@ -438,10 +465,18 @@ macro_rules! scope_execution_registry_api {
438465
.ok_or_else(|| FlowError::NotFound(format!("scope {scope_uuid} not found")))?;
439466
registries
440467
.$field
441-
.register(ExecutionIntercept::new(name, priority, callable))
468+
.register(ExecutionIntercept::new(name, priority, callable.into()))
442469
.map_err(FlowError::AlreadyExists)
443470
}
471+
};
472+
}
444473

474+
macro_rules! scope_execution_registry_deregister_api {
475+
(
476+
$(#[$deregister_meta:meta])*
477+
$deregister_name:ident,
478+
$field:ident
479+
) => {
445480
$(#[$deregister_meta])*
446481
///
447482
/// # Parameters
@@ -467,6 +502,29 @@ macro_rules! scope_execution_registry_api {
467502
};
468503
}
469504

505+
macro_rules! scope_execution_registry_api {
506+
(
507+
$(#[$register_meta:meta])*
508+
$register_name:ident,
509+
$(#[$deregister_meta:meta])*
510+
$deregister_name:ident,
511+
$field:ident,
512+
$fn_type:ty
513+
) => {
514+
scope_execution_registry_register_api!(
515+
$(#[$register_meta])*
516+
$register_name,
517+
$field,
518+
$fn_type
519+
);
520+
scope_execution_registry_deregister_api!(
521+
$(#[$deregister_meta])*
522+
$deregister_name,
523+
$field
524+
);
525+
};
526+
}
527+
470528
global_guardrail_registry_api!(
471529
/// Register a global mark event sanitizer.
472530
register_mark_sanitize_guardrail,
@@ -535,14 +593,22 @@ global_intercept_registry_api!(
535593
global_execution_registry_api!(
536594
/// Register a global tool execution intercept.
537595
/// Execution intercepts can wrap or replace the tool callback. Each
538-
/// callback returns a canonical tool execution outcome, while its
596+
/// callback returns Relay's tool execution outcome wrapper, while its
539597
/// continuation resolves to the raw downstream result JSON.
540598
register_tool_execution_intercept,
541599
/// Deregister a global tool execution intercept.
542600
deregister_tool_execution_intercept,
543601
tool_execution_intercepts,
544602
ToolExecutionFn
545603
);
604+
global_execution_registry_register_api!(
605+
/// Register a global annotation-aware tool execution intercept.
606+
/// Frame intercepts share the existing tool execution registry, namespace,
607+
/// and priority order with raw-JSON intercepts.
608+
register_tool_execution_frame_intercept,
609+
tool_execution_intercepts,
610+
ToolExecutionFrameFn
611+
);
546612

547613
global_guardrail_registry_api!(
548614
/// Register a global LLM sanitize-request guardrail.
@@ -669,14 +735,22 @@ scope_intercept_registry_api!(
669735
scope_execution_registry_api!(
670736
/// Register a scope-local tool execution intercept.
671737
/// Execution intercepts can wrap or replace the tool callback inside the
672-
/// owning scope. Each callback returns a canonical tool execution outcome,
738+
/// owning scope. Each callback returns Relay's tool execution outcome wrapper,
673739
/// while its continuation resolves to the raw downstream result JSON.
674740
scope_register_tool_execution_intercept,
675741
/// Deregister a scope-local tool execution intercept.
676742
scope_deregister_tool_execution_intercept,
677743
tool_execution_intercepts,
678744
ToolExecutionFn
679745
);
746+
scope_execution_registry_register_api!(
747+
/// Register a scope-local annotation-aware tool execution intercept.
748+
/// Frame intercepts share the existing tool execution registry, namespace,
749+
/// and priority order with raw-JSON intercepts.
750+
scope_register_tool_execution_frame_intercept,
751+
tool_execution_intercepts,
752+
ToolExecutionFrameFn
753+
);
680754

681755
scope_guardrail_registry_api!(
682756
/// Register a scope-local LLM sanitize-request guardrail.

crates/core/src/api/runtime.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ pub use callbacks::{
1515
LlmRequestInterceptFn, LlmSanitizeRequestContext, LlmSanitizeRequestFn,
1616
LlmSanitizeResponseContext, LlmSanitizeResponseFn, LlmStreamExecutionFn,
1717
LlmStreamExecutionNextFn, LlmStreamInner, ToolConditionalFn, ToolExecutionFn,
18-
ToolExecutionNextFn, ToolInterceptFn, ToolSanitizeFn,
18+
ToolExecutionFrameFn, ToolExecutionFrameNextFn, ToolExecutionNextFn, ToolInterceptFn,
19+
ToolSanitizeFn,
1920
};
2021
pub use global::global_context;
2122
pub use scope_stack::{

crates/core/src/api/runtime/callbacks.rs

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ use tokio_stream::Stream;
1717

1818
use crate::api::event::{Event, EventSanitizeFields};
1919
use crate::api::llm::{LlmRequest, LlmRequestInterceptOutcome};
20-
use crate::api::tool::ToolExecutionInterceptOutcome;
20+
use crate::api::tool::{
21+
ToolExecutionFrame, ToolExecutionFrameOutcome, ToolExecutionInterceptOutcome,
22+
};
2123
use crate::codec::request::AnnotatedLlmRequest;
2224
use crate::codec::traits::{LlmCodec, LlmResponseCodec};
2325
use crate::error::Result;
@@ -110,8 +112,9 @@ pub type ToolExecutionNextFn =
110112
/// - Third argument: Continuation for the remaining execution chain.
111113
///
112114
/// # Returns
113-
/// A future resolving to the canonical tool execution outcome, containing the
114-
/// tool result and any pending lifecycle marks produced by this intercept.
115+
/// A future resolving to Relay's execution outcome wrapper, containing the
116+
/// harness-owned tool result and any pending lifecycle marks produced by this
117+
/// intercept.
115118
///
116119
/// # Errors
117120
/// The future resolves to an error when the intercept or remaining execution
@@ -126,13 +129,61 @@ pub type ToolExecutionFn = Arc<
126129
+ Sync,
127130
>;
128131

129-
/// Internal continuation carrying both a tool result and accumulated marks.
130-
pub(crate) type ToolExecutionOutcomeNextFn = Arc<
131-
dyn Fn(Json) -> Pin<Box<dyn Future<Output = Result<ToolExecutionInterceptOutcome>> + Send>>
132+
/// Annotation-aware continuation invoked by tool execution frame intercepts.
133+
///
134+
/// The continuation exposes the raw downstream result together with its
135+
/// optional opaque annotation. Relay retains downstream pending marks
136+
/// internally, matching [`ToolExecutionNextFn`].
137+
pub type ToolExecutionFrameNextFn = Arc<
138+
dyn Fn(Json) -> Pin<Box<dyn Future<Output = Result<ToolExecutionFrame>> + Send>> + Send + Sync,
139+
>;
140+
141+
/// Annotation-aware tool execution intercept.
142+
///
143+
/// This callback participates in the same priority-ordered chain as
144+
/// [`ToolExecutionFn`], but its continuation and outcome carry a
145+
/// [`ToolExecutionFrame`].
146+
pub type ToolExecutionFrameFn = Arc<
147+
dyn Fn(
148+
&str,
149+
Json,
150+
ToolExecutionFrameNextFn,
151+
) -> Pin<Box<dyn Future<Output = Result<ToolExecutionFrameOutcome>> + Send>>
152+
+ Send
153+
+ Sync,
154+
>;
155+
156+
/// Internal continuation carrying a tool frame and accumulated marks.
157+
pub(crate) type ToolExecutionFrameOutcomeNextFn = Arc<
158+
dyn Fn(Json) -> Pin<Box<dyn Future<Output = Result<ToolExecutionFrameOutcome>> + Send>>
132159
+ Send
133160
+ Sync,
134161
>;
135162

163+
/// One registry payload for legacy and annotation-aware tool intercepts.
164+
///
165+
/// Keeping both callback forms in this enum preserves a single namespace and
166+
/// priority order rather than creating a second middleware chain.
167+
#[derive(Clone)]
168+
pub(crate) enum ToolExecutionCallable {
169+
/// Existing raw-JSON execution intercept.
170+
Legacy(ToolExecutionFn),
171+
/// Annotation-aware frame execution intercept.
172+
Frame(ToolExecutionFrameFn),
173+
}
174+
175+
impl From<ToolExecutionFn> for ToolExecutionCallable {
176+
fn from(value: ToolExecutionFn) -> Self {
177+
Self::Legacy(value)
178+
}
179+
}
180+
181+
impl From<ToolExecutionFrameFn> for ToolExecutionCallable {
182+
fn from(value: ToolExecutionFrameFn) -> Self {
183+
Self::Frame(value)
184+
}
185+
}
186+
136187
/// Relay's built-in LLM codec identities.
137188
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138189
pub enum BuiltinLlmCodec {

0 commit comments

Comments
 (0)