Affected area
- Rust core runtime
- Language bindings
- Middleware
- Plugins
Current behavior
Managed tool execution accepts an optional tool_call_id. Relay preserves this
value on the managed tool handle and lifecycle events, but does not pass it to
tool execution intercepts.
The current execution-intercept callback receives only:
tool_name, arguments, next_call
The runtime already has the tool_call_id when it constructs the execution
chain, but the value is omitted from the callback invocation.
As a result, a plugin cannot inspect or preserve the originating tool-call
identity when it returns a result without invoking next_call. The value cannot
be recovered through the existing callback arguments.
Relevant source:
crates/core/src/api/tool.rs
crates/core/src/api/runtime/state.rs
crates/core/src/api/runtime/callbacks.rs
Expected behavior
Tool execution intercepts should receive the optional tool_call_id supplied
to managed execution.
Impact
Plugins do not have access to the complete identity of the tool execution they
are intercepting.
This prevents a plugin from reliably associating a result with the originating
tool call when it completes the execution itself instead of invoking the
remaining execution chain.
There is no equivalent value available through the existing callback arguments,
and invoking next_call is not a valid way to recover information that should
already be part of the interception context.
API design options
Both viable approaches change the existing tool execution-intercept callback
contract and are breaking API changes.
Option A: Introduce ToolExecutionContext
Replace:
async def intercept(tool_name, arguments, next_call):
...
with:
async def intercept(
context: ToolExecutionContext,
next_call,
) -> ToolExecutionInterceptOutcome:
...
The initial context contains:
tool_name
arguments
- optional
tool_call_id
This requires existing callbacks to migrate, but future optional context fields
can be added without changing the callback signature again.
Option B: Add tool_call_id as a callback parameter
Replace:
async def intercept(tool_name, arguments, next_call):
...
with:
async def intercept(
tool_name,
arguments,
tool_call_id,
next_call,
) -> ToolExecutionInterceptOutcome:
...
This is the smaller change, but another required execution value would require
another callback-signature migration.
Recommended direction
Prefer Option A because Relay maintains this callback contract across multiple
language bindings and plugin transports. A typed execution context requires a
larger initial migration but establishes a stable interface for future optional
execution data.
Whichever option is selected:
- Document the breaking callback migration.
- Update primary bindings together.
- Version native-plugin ABI changes instead of modifying an existing ABI table
in place.
- Update worker protocol and SDK compatibility requirements.
- Reject incompatible plugin/runtime combinations with a clear version error.
Acceptance criteria
- An execution intercept receives the exact
tool_call_id supplied to managed
tool execution.
- An omitted
tool_call_id remains absent.
- An intercept can return a result without invoking
next_call while retaining
the originating tool-call identity.
- The selected callback contract is implemented consistently across Rust,
Python, and Node.js.
- Experimental and dynamic-plugin surfaces preserve their documented
compatibility boundaries.
- Native and worker plugin boundaries reject incompatible versions safely.
- Documentation explains the selected API and migration path.
Affected area
Current behavior
Managed tool execution accepts an optional
tool_call_id. Relay preserves thisvalue on the managed tool handle and lifecycle events, but does not pass it to
tool execution intercepts.
The current execution-intercept callback receives only:
The runtime already has the
tool_call_idwhen it constructs the executionchain, but the value is omitted from the callback invocation.
As a result, a plugin cannot inspect or preserve the originating tool-call
identity when it returns a result without invoking
next_call. The value cannotbe recovered through the existing callback arguments.
Relevant source:
crates/core/src/api/tool.rscrates/core/src/api/runtime/state.rscrates/core/src/api/runtime/callbacks.rsExpected behavior
Tool execution intercepts should receive the optional
tool_call_idsuppliedto managed execution.
Impact
Plugins do not have access to the complete identity of the tool execution they
are intercepting.
This prevents a plugin from reliably associating a result with the originating
tool call when it completes the execution itself instead of invoking the
remaining execution chain.
There is no equivalent value available through the existing callback arguments,
and invoking
next_callis not a valid way to recover information that shouldalready be part of the interception context.
API design options
Both viable approaches change the existing tool execution-intercept callback
contract and are breaking API changes.
Option A: Introduce
ToolExecutionContextReplace:
with:
The initial context contains:
tool_nameargumentstool_call_idThis requires existing callbacks to migrate, but future optional context fields
can be added without changing the callback signature again.
Option B: Add
tool_call_idas a callback parameterReplace:
with:
This is the smaller change, but another required execution value would require
another callback-signature migration.
Recommended direction
Prefer Option A because Relay maintains this callback contract across multiple
language bindings and plugin transports. A typed execution context requires a
larger initial migration but establishes a stable interface for future optional
execution data.
Whichever option is selected:
in place.
Acceptance criteria
tool_call_idsupplied to managedtool execution.
tool_call_idremains absent.next_callwhile retainingthe originating tool-call identity.
Python, and Node.js.
compatibility boundaries.