> This page is for NeMo Relay, version v0.8.4.
> For other versions, use one of these documentation indexes:
> - Latest (v0.10.0) (default): https://docs.nvidia.com/nemo/relay/latest/llms.txt
> - dev: https://docs.nvidia.com/nemo/relay/dev/llms.txt
> - v0.10.0: https://docs.nvidia.com/nemo/relay/v0.10.0/llms.txt
> - v0.9.4: https://docs.nvidia.com/nemo/relay/v0.9.4/llms.txt
> - v0.8.4: https://docs.nvidia.com/nemo/relay/v0.8.4/llms.txt
> - v0.7.3: https://docs.nvidia.com/nemo/relay/v0.7.3/llms.txt
> - v0.6.0: https://docs.nvidia.com/nemo/relay/v0.6.0/llms.txt
> - v0.5.0: https://docs.nvidia.com/nemo/relay/v0.5.0/llms.txt

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.nvidia.com/nemo/relay/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.nvidia.com/nemo/relay/_mcp/server.

# Tool Execution Intercept Outcomes

> Canonical tool execution results, intercept outcomes, and managed lifecycle behavior.

A managed tool callback and every execution-intercept continuation return one
canonical application-visible result:

```json
{
  "result": {}
}
```

`result` is required and contains the application-owned tool payload.
`annotation` is optional opaque adjacent metadata. It can be any JSON value.
An absent or JSON `null` annotation means that the result has no annotation; an
empty object (`{}`) is a present annotation.
Relay transports the annotation without interpreting its schema.

A tool execution intercept wraps or short-circuits that callback and returns a
canonical outcome:

```json
{
  "result": {},
  "annotation": {"source": "cache"},
  "pending_marks": []
}
```

`result` and `annotation` form the application-visible result passed to the
remaining middleware. `pending_marks` defaults to an empty list and remains
Relay-owned lifecycle data. Each pending mark contains `name`, optional
`category` and `category_profile`, and optional `data`, `data_schema`,
`metadata`, and typed log `severity`. Relay assigns event UUIDs, parent UUIDs,
and timestamps.

## Continuation Semantics

The default tool callback and an intercept's `next(args)` continuation both
return `ToolExecutionResult`. The continuation exposes the downstream `result`
and `annotation`, but not downstream pending marks. An intercept can preserve,
replace, or remove the annotation in its returned outcome. Relay retains
pending marks separately, combines them in effective middleware order, and
materializes them only after the final outcome succeeds.

Use the existing global, scope-local, or plugin-context tool execution
registration APIs to produce pending marks, and return the canonical outcome
from every registered callback. Use a
[mark event sanitizer](/reference/event-sanitizers) when the emitted pending
mark's `data`, `category_profile`, or `metadata` must be sanitized. Public APIs,
native API 1 plugins, and `grpc-v1` workers require canonical tool callback and
intercept returns. Relay 0.8 does not provide a legacy raw-result adapter.

## Event and Exporter Projection

For a successful tool end event, Relay records `result` as the event output and
places a non-null annotation at
`category_profile.tool_result_annotation`. Tool sanitize-response guardrails
receive and sanitize only `result`. They cannot read or rewrite the annotation.
The scope-end event sanitizer runs after the tool response sanitizer and can
replace or remove `category_profile.tool_result_annotation` before subscribers
or exporters receive the event.

ATOF preserves the annotation in the tool end event's category profile. ATIF
places it in the observation result's `extra.tool_result_annotation` field.
The `full` and `openinference` OpenTelemetry projections serialize it as one
opaque JSON-valued `nemo_relay.tool.result.annotation` attribute rather than
flattening application-defined keys. The standards-only `gen_ai` projection
does not emit Relay-private attributes.

Relay returns the unsanitized `ToolExecutionResult` to the application. Event
sanitizers affect observability only.

## Managed Lifecycle

On successful managed execution, Relay emits the tool end event before any
pending marks. The end timestamp is no earlier than one microsecond after the
tool start timestamp. Relay emits pending marks in their resolved middleware
order, assigns each mark the managed tool UUID as its parent, and gives each
mark a timestamp after the tool end event.

If execution or an intercept fails, Relay emits the error end event without a
tool result annotation and discards accumulated pending marks. Pending marks
are never included in the application-visible tool result or passed to tool
sanitize-response guardrails. They pass through the mark sanitizer registry
when materialized.

## Binding Contract

Managed callbacks, continuations, and execute helpers use the same logical
result in their native type or object shape:

| Surface  | Managed callback or continuation result                           |
| -------- | ----------------------------------------------------------------- |
| Python   | `ToolExecutionResult(result, annotation=None)`                    |
| Rust     | `ToolExecutionResult { result, annotation }` and its constructors |
| Go       | `ToolExecutionResult { Result, Annotation }`                      |
| Node.js  | `{ result, annotation? }`                                         |
| Public C | Canonical JSON with required `result` and optional `annotation`   |

Execution intercepts add lifecycle-owned pending marks to that shape:

| Surface               | Execution-intercept outcome                                                                                                 |
| --------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| Python                | `ToolExecutionInterceptOutcome`                                                                                             |
| Rust and native API 1 | `ToolExecutionInterceptOutcome`                                                                                             |
| Go                    | `ToolExecutionInterceptOutcome`                                                                                             |
| Node.js               | `{ result, annotation?, pendingMarks? }`; JavaScript pending-mark DTOs use `categoryProfile`                                |
| Public C              | Canonical JSON with `result`, optional `annotation`, and optional `pending_marks`                                           |
| `grpc-v1` worker SDKs | Protobuf `ToolExecutionResult` and `ToolExecutionInterceptOutcome`; arbitrary JSON fields use lossless `JsonValue` wrappers |

Canonical JSON uses `annotation`, `pending_marks`, and `category_profile`
across bindings.

Relay 0.8 establishes this canonical result as the native API 1 and `grpc-v1`
baseline. Rebuild every dynamic plugin and declare a `compat.relay` range that
excludes Relay versions before 0.8. The recommended range is
`>=0.8.0,<1.0`; open-ended ranges such as `>=0.8.0` are valid. The native ABI
remains v4. Workers retain the `grpc-v1` identifier and
`nemo.relay.worker.v1` package, but Relay 0.8 changes the `ToolNext` response
type and tool-execution outcome field type. Regenerate worker protobuf bindings
as part of the required rebuild. Future incompatible contract changes must
increment the corresponding native API or worker protocol version.

## Migration

This release deliberately replaces raw tool execution results. Make these
changes together:

1. Wrap every managed tool callback return in `ToolExecutionResult`.
2. Treat every execution intercept's `next(args)` return as
   `ToolExecutionResult`.
3. Return the downstream `result` and `annotation` from forwarding intercepts,
   or deliberately replace or remove the annotation.
4. Read `.result` when an application or framework needs the original business
   payload.
5. Wrap the result passed to a manual tool end helper in
   `ToolExecutionResult`.

### Return-Shape Errors

Python distinguishes `ToolExecutionResult` from
`ToolExecutionInterceptOutcome`. A callback that returns a raw dictionary is
rejected; return `ToolExecutionResult(payload)` instead. A forwarding intercept
must unwrap the downstream result:

```python
downstream = await next(args)
return ToolExecutionInterceptOutcome(
    result=downstream.result,
    annotation=downstream.annotation,
)
```

Do not pass `downstream` itself as the outcome result or return it directly.
The resulting `RuntimeError` names the required wrapper and this unwrapping
form.

Node.js represents both values as compatible object shapes. Therefore,
`return await next(args)` is a valid forwarding intercept in Node.js. Raw tool
callback returns and raw intercept outcomes are still rejected with guidance to
return `{ result: payload }` or
`{ result: downstream.result, annotation: downstream.annotation }`.

Rebuild native plugins and workers against the same NeMo Relay release that
hosts them. For binding-specific examples, refer to the
[Migration Guides](/reference/migration-guides#return-canonical-tool-execution-results).

LLM request intercepts use a different outcome because they must preserve an
annotated request and codec authority. That contract is described in [LLM Request Intercept Outcomes](/reference/llm-request-intercept-outcomes). Worker
authors can trace both outcome envelopes through the [grpc-v1 protocol reference](/build-plugins/workers/grpc-v1-protocol).