Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8e0cb46
docs: rewrite plugin authoring guides
willkill07 Aug 12, 2026
8ddacc5
docs: address plugin guide review feedback
willkill07 Aug 12, 2026
311266d
docs: address verified plugin review findings
willkill07 Aug 12, 2026
13faa69
docs: link native example to rendered guide
willkill07 Aug 12, 2026
d9ea59a
docs: remove unpublished native guide link
willkill07 Aug 12, 2026
f93d54a
docs: resolve plugin review findings
willkill07 Aug 12, 2026
9caca35
fix: declare worker interceptor compatibility
willkill07 Aug 13, 2026
e7ee659
docs: expand plugin authoring examples
willkill07 Aug 13, 2026
4e44352
docs: make plugin examples independently verifiable
willkill07 Aug 13, 2026
57669b9
test: exercise Python worker runtime in managed calls
willkill07 Aug 13, 2026
1eaaaad
docs: rename plugin overviews to about pages
willkill07 Aug 13, 2026
61f6597
test: escape native lifecycle manifest paths
willkill07 Aug 13, 2026
a788767
test: escape worker lifecycle manifest paths
willkill07 Aug 13, 2026
e9a210c
docs: align plugin guides with canonical tool results
willkill07 Aug 14, 2026
55fb953
test: preserve worker request intercept coverage
willkill07 Aug 14, 2026
ba7a22e
fix: preserve Python worker tool annotations
willkill07 Aug 14, 2026
24cae06
docs: align plugin examples with current contracts
willkill07 Aug 14, 2026
c768ec5
fix(cli): complete bootstrap key initialization before reuse
willkill07 Aug 14, 2026
ceb2416
revert: keep bootstrap key fix on the release branch
willkill07 Aug 14, 2026
3f7d22d
Merge branch 'main' into docs/human-first-plugin-documentation
willkill07 Aug 14, 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
12 changes: 2 additions & 10 deletions crates/core/tests/integration/worker_plugin_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1209,10 +1209,6 @@ async fn python_worker_host_runtime_mark_and_mutated_request_round_trip() {
.await
.expect("Python tool execution intercept should call ToolNext and return its outcome");
assert_eq!(tool_result.result["provider_result"], true);
assert_eq!(
tool_result.result["_nemo_relay_plugin"]["tag"],
"managed-environment"
);
assert_eq!(
tool_result.result["args"]["_nemo_relay_plugin"]["tag"],
"managed-environment"
Expand All @@ -1230,14 +1226,10 @@ async fn python_worker_host_runtime_mark_and_mutated_request_round_trip() {

flush_subscribers().expect("Python callback mark should flush");
let captured_events = events.lock().unwrap();
find_event(
&captured_events,
"examples.python_grpc_worker.tool_request",
None,
);
find_event(&captured_events, "example.python_worker.tool_request", None);
let tool_mark = find_event(
&captured_events,
"examples.python_grpc_worker.tool_execution",
"example.python_worker.tool_execution",
None,
);
assert_eq!(
Expand Down
66 changes: 19 additions & 47 deletions crates/plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,35 +24,16 @@ Native plugins run in the Relay process and are not sandboxed. They should
depend on this crate rather than the host `nemo-relay` runtime crate, keeping
the dynamic-library boundary on the stable C-compatible ABI.

## Why Use It?

- **Author native plugins safely**: Implement `NativePlugin` with typed Rust
callbacks instead of constructing ABI tables directly.
- **Register real runtime behavior**: Use `PluginContext` for subscribers,
guardrails, and intercepts.
- **Keep a stable boundary**: Export one versioned native entry point through
the `nemo_relay_plugin!` macro.
- **Use host runtime helpers**: Emit events and manage scope state through the
high-level `PluginRuntime` wrapper.

## What You Get

- **`NativePlugin`**: Plugin kind, configuration validation, and registration
lifecycle contract.
- **`PluginContext`**: Component-scoped registration APIs for middleware and
subscribers.
- **`PluginRuntime`**: Typed helpers for Relay-owned scopes and marks.
- **Stable native ABI v4**: C-compatible host and plugin tables behind the
safe Rust authoring interface. Relay negotiates frozen v3 and v2 tables for
Relay 0.8-built plugins that target those layouts.
- **Typed async middleware**: Every typed guardrail, sanitizer, and intercept
returns a future driven by a per-plugin SDK-owned Tokio runtime. Subscribers
and raw synchronous ABI registrations remain synchronous.
- **Async continuations and streams**: Cloneable `ToolNext`, `LlmNext`, and
`LlmStreamNext` handles support repeated or concurrent calls. Streaming LLM
continuations use a pull-based host handle.
- **Canonical tool results**: `ToolNext` returns `ToolExecutionResult`, keeping
application results and opaque annotations adjacent across native API 1.
## Authoring Surface

| Surface | Role |
|---|---|
| `NativePlugin` | Defines plugin identity, configuration validation, registration, and multiple-component behavior without requiring an author to construct ABI tables. |
| `PluginContext` | Installs component-owned subscribers, guardrails, intercepts, continuations, and streams. |
| `PluginRuntime` | Emits marks and manages Relay-owned scopes and scope stacks through typed host helpers. |
| `nemo_relay_plugin!` | Exports the one versioned native entry point used by the loader. |
| Native ABI v4 | Keeps C-compatible host and plugin tables behind the safe Rust interface while the host retains frozen v3 and v2 tables for previously compiled plugins. |
| Typed async middleware | Drives guardrails, sanitizers, and intercepts on a per-component SDK-owned Tokio executor. Subscribers and raw ABI registrations remain synchronous. |

## Installation

Expand Down Expand Up @@ -96,27 +77,24 @@ nemo_relay_plugin::nemo_relay_plugin!(nemo_relay_register_plugin, || ExamplePlug
```

Build the `cdylib`, describe its entry symbol and compatibility in a
`relay-plugin.toml` manifest, then register it through the Relay CLI. See the
`relay-plugin.toml` manifest, then register it through the Relay CLI. Refer to the
complete example for platform-specific artifact and manifest setup.
Comment thread
willkill07 marked this conversation as resolved.

Use `compat.native_api = "1"`. Relay 0.8 establishes the canonical
`ToolExecutionResult` JSON contract as the native API 1 baseline. Every native
plugin must be rebuilt for Relay 0.8 and declare a `compat.relay` range that
excludes earlier versions. The recommended range is `>=0.8.0,<1.0`; an
open-ended range such as `>=0.8.0` is also valid. The manifest is the plugin
author's compatibility assertion, not proof that the artifact was rebuilt.

The JSON contract is independent of the native host-table layout. This SDK
continues to export ABI v4, whose C-compatible layouts and callback signatures
are unchanged by the tool-result cutover. Future incompatible native JSON
contract changes must increment `compat.native_api`. Relay creates one
Typed async plugins require `compat.relay = ">=0.8.0,<1.0"`. Relay creates one
SDK-owned Tokio executor for each configured plugin component. It defaults to
two workers: enough for modest concurrent async I/O without broadly
oversubscribing the host. Increase the count only when measured I/O concurrency
leaves callbacks queued; lower it when the host runs many components or has a
tight CPU budget. Do not block these workers; use async I/O or
`tokio::task::spawn_blocking`.

Relay 0.8 establishes canonical tool results as the native API 1 baseline. Tool
callbacks and `ToolNext` return `ToolExecutionResult`, preserving an application result
and optional opaque annotation. Tool execution intercepts return the same pair plus
Relay-owned pending marks. The manifest contract remains `compat.native_api = "1"` and
the C host-table ABI remains v4, but plugins must rebuild and exclude pre-0.8 Relay
versions because the JSON result boundary changed.

Set a plugin-wide default in Rust, then let the component's TOML configuration
override it:

Expand Down Expand Up @@ -154,9 +132,3 @@ already accepted typed middleware before the plugin library unloads.
Relay scope context is restored around every poll of a registered middleware
future. Child tasks created with `tokio::spawn` do not automatically inherit
that scope context.

## Documentation

- [NeMo Relay documentation](https://docs.nvidia.com/nemo/relay)
- [Build Plugins guide](https://docs.nvidia.com/nemo/relay/build-plugins/about)
- [Rust native plugin example](https://github.com/NVIDIA/NeMo-Relay/blob/main/examples/rust-native-plugin/README.md)
69 changes: 17 additions & 52 deletions crates/worker-proto/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,52 +25,23 @@ Use `nemo-relay-worker` to author Rust workers. Depend on this crate directly
only when implementing another worker SDK, a custom host, or protocol-level
tooling.

Relay 0.8 establishes the canonical tool-result contract as the `grpc-v1`
baseline. Workers built for an earlier Relay release must be rebuilt, and their
manifests must declare a `compat.relay` range beginning at `0.8.0` or later.
The protocol identifier and protobuf package remain `grpc-v1` and
`nemo.relay.worker.v1`, respectively. However, the generated protobuf API
changes at the tool-result boundary: `ToolNext` returns
`ToolExecutionResultResponse`, and `ToolExecutionInterceptResult.outcome` is a
typed `ToolExecutionInterceptOutcome`. Rebuild every worker against the Relay
0.8 protocol definitions.

## Why Use It?

- **Share the stable transport contract**: Use the `grpc-v1` service and
message definitions accepted by Relay worker manifests.
- **Use generated Tonic bindings**: Access versioned client and server types
from `v1` without generating protobuf code in a consumer project.
- **Keep data ownership clear**: Use structural protobuf wrappers for tool
results while preserving open application payloads as lossless JSON bytes.
Other Relay DTOs continue to use JSON envelopes backed by
`nemo-relay-types`.

## What You Get

- **`WORKER_PROTOCOL_GRPC_V1`**: The stable `grpc-v1` protocol identifier.
- **`v1` module**: Generated `PluginWorker` and `RelayHostRuntime` gRPC
clients, servers, services, and messages.
- **JSON envelope helpers**: `json_envelope` and `decode_json_envelope` for
serializing Relay DTOs into protocol payloads.
- **JSON value helpers**: `json_value` and `decode_json_value` for the opaque
application values inside structural tool-result messages.

## Structural Tool Result Contract

The `grpc-v1` tool-result boundary uses these generated message types:

| Protocol Location | Protobuf Type |
| --- | --- |
| Successful `RelayHostRuntime.ToolNext` response | `ToolExecutionResultResponse.value` containing `ToolExecutionResult` |
| `ToolExecutionInterceptResult.outcome` | `ToolExecutionInterceptOutcome` |

Both messages define `result` and optional `annotation` fields. Intercept
outcomes also carry their ordered `pending_marks` as one JSON array. Arbitrary
JSON values use `JsonValue`, whose bytes contain exactly one JSON value; this
preserves JSON integers and other application data without the numeric coercion
of `google.protobuf.Value`. Hosts and SDKs reject a missing required `result`
or invalid JSON bytes. JSON null annotations normalize to absence.
Relay 0.8 establishes canonical tool results as the `grpc-v1` baseline. Workers built
for earlier releases must regenerate their bindings, rebuild, and declare
`compat.relay` beginning at `0.8.0`. `ToolNext` returns `ToolExecutionResultResponse`,
and tool execution intercepts use structural `ToolExecutionInterceptOutcome` messages.

## Protocol Surface

| Surface | Role |
|---|---|
| `WORKER_PROTOCOL_GRPC_V1` | Identifies the stable protocol accepted by Relay worker manifests. |
| `v1` module | Exposes generated `PluginWorker` and `RelayHostRuntime` Tonic clients, servers, services, and messages without regenerating protobuf in a consumer. |
| JSON envelope helpers | Serialize Relay DTOs through `json_envelope` and `decode_json_envelope`, keeping protobuf responsible for transport flow rather than runtime data modeling. |
| JSON value helpers | Serialize application-owned fields inside structural tool-result messages through `json_value` and `decode_json_value`. |

`ToolExecutionResult` contains required `result` and optional `annotation` values.
`ToolExecutionInterceptOutcome` adds Relay-owned `pending_marks`. These fields use
lossless protobuf `JsonValue` wrappers rather than `google.protobuf.Value`.

## Installation

Expand All @@ -97,9 +68,3 @@ fn main() -> Result<(), serde_json::Error> {
Ok(())
}
```

## Documentation

- [NeMo Relay documentation](https://docs.nvidia.com/nemo/relay)
- [Build Plugins guide](https://docs.nvidia.com/nemo/relay/build-plugins/about)
- [Rust worker SDK](https://github.com/NVIDIA/NeMo-Relay/blob/main/crates/worker/README.md)
53 changes: 19 additions & 34 deletions crates/worker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,34 +20,25 @@ SPDX-License-Identifier: Apache-2.0
dynamic worker plugins. Use it when plugin code needs process isolation and
communicates with Relay through the versioned `grpc-v1` worker protocol.

Relay 0.8 establishes canonical tool results as the `grpc-v1` baseline.
Workers built for an earlier Relay release must be rebuilt with this SDK and
declare a `compat.relay` range beginning at `0.8.0` or later. The protocol name
remains `grpc-v1`, but its generated `ToolNext` response and tool-execution
outcome fields now use structural protobuf messages.

## Why Use It?

- **Isolate plugin code**: Run custom runtime behavior outside the Relay host
process.
- **Use typed registration APIs**: Implement `WorkerPlugin` and register
subscribers, guardrails, or intercepts with `PluginContext`.
- **Call the host runtime**: Emit marks, manage scopes, and invoke middleware
continuations through `PluginRuntime`.
- **Keep lifecycle managed**: Let Relay provide authenticated endpoints and
start the worker with `serve_plugin`.

## What You Get

- **`WorkerPlugin`**: The plugin identity, validation, and registration
contract.
- **`PluginContext`**: Typed registrations for all supported worker surfaces.
- **`PluginRuntime` and continuations**: Host-runtime callbacks and tool/LLM
execution-chain helpers.
- **Canonical tool results**: `ToolNext` returns `ToolExecutionResult`, so
workers can preserve an opaque annotation independently of the tool result.
- **`serve_plugin`**: Tokio gRPC server startup using the Relay-provided worker
environment.
Relay 0.8 establishes canonical tool results as the `grpc-v1` baseline. Workers built
for an earlier release must rebuild with this SDK and declare `compat.relay` beginning at
`0.8.0`. The protocol identifier remains `grpc-v1`, but `ToolNext` now returns
`ToolExecutionResult`, which keeps an optional opaque annotation beside the application
result.

## Authoring Surface

| Surface | Role |
|---|---|
| `WorkerPlugin` | Defines plugin identity, validation, registration, and multiple-component behavior in the worker process. |
| `PluginContext` | Installs typed handlers for all 15 supported registration surfaces. |
| `PluginRuntime` and continuations | Emit marks, manage scopes, and call the remaining tool or LLM execution chain through the authenticated host service. |
| Canonical tool results | Preserve application results and opaque annotations across tool callbacks and continuations. |
| `serve_plugin` | Starts the Tokio gRPC server from the activation identity, local endpoints, and token supplied by Relay. |

This model keeps plugin dependencies and crashes outside the Relay process, while the
SDK retains the shared runtime contract and manages authentication, cancellation, and
shutdown.

## Installation

Expand Down Expand Up @@ -100,9 +91,3 @@ Relay sends `CancelInvocation` when a managed caller is cancelled, times out,
or stops consuming a stream, and the SDK aborts the matching async callback
task. An accepted cancellation confirms the task was found; it cannot prove
that arbitrary blocking work started by the callback has stopped.

## Documentation

- [NeMo Relay documentation](https://docs.nvidia.com/nemo/relay)
- [Build Plugins guide](https://docs.nvidia.com/nemo/relay/build-plugins/about)
- [Python gRPC worker plugin example](https://github.com/NVIDIA/NeMo-Relay/blob/main/examples/python-grpc-worker-plugin/README.md)
44 changes: 16 additions & 28 deletions docs/about-nemo-relay/concepts/plugins.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,8 @@ Plugins package reusable runtime components.

## Plugin Configuration Model

A plugin configuration document has three main areas:

- `version`
- `components`
- `policy`
A plugin configuration document combines a schema `version`, an ordered set of
`components`, and a `policy` for unsupported configuration.

### Version

Expand All @@ -49,7 +46,7 @@ Two similarly named files serve different purposes:

Refer to [Plugin Configuration Files](/configure-plugins/plugin-configuration-files)
for runtime configuration and
[Discoverable Plugins](/build-plugins/dynamic-plugins/about) for the package
[Package Discoverable Plugins](/build-plugins/package-discoverable-plugins) for the package
manifest.

## Component Lifecycle
Expand Down Expand Up @@ -147,11 +144,9 @@ It connects plugin configuration to the active runtime.

## What Plugins Can Register

Depending on the component, a plugin can register:

- Middleware
- Subscribers
- Related runtime helpers
Depending on the component, a plugin can register middleware, subscribers, and
runtime helpers. These are the same execution surfaces available elsewhere in
Relay, but the component context gives them shared configuration and ownership.

This is what makes plugins a packaging mechanism rather than a separate runtime
model. Plugins do not replace scopes, middleware, or subscribers. They install
Expand All @@ -176,7 +171,7 @@ ordering, and events still own the canonical runtime record.
All plugin components use the same validation and activation model. They differ
in how their component kind becomes available to the host:

| Delivery model | How it becomes available | Use when |
| Delivery Model | How It Becomes Available | Use When |
| --- | --- | --- |
| Built-in | The Relay host registers a linked first-party component automatically. | The component ships as part of that host. |
| Host-registered | An embedding application links a component crate and registers its kind before validation. | An embedding application decides which component crates to link. |
Expand Down Expand Up @@ -230,11 +225,9 @@ built-in integration is deprecated and scheduled for removal in NeMo Relay
0.9. There is no replacement in NeMo Relay 0.8; any replacement will target 0.9
or later. Do not use the built-in component for new deployments.

The current shipped user-facing paths are:

- The remote backend for Guardrails-service integration
- The Python-backed local backend for `nemoguardrails` integration through a
subprocess worker
The current user-facing paths are the remote backend for Guardrails-service
integration and the Python-backed local backend that runs `nemoguardrails`
through a subprocess worker.

Detailed Guardrails plugin configuration belongs in
[NeMo Guardrails Configuration](/configure-plugins/nemo-guardrails/configuration).
Expand Down Expand Up @@ -283,7 +276,7 @@ manifest, optional static schema, host policy, compatibility, and trust
evidence before enabling or running a dynamic plugin. During startup, Relay
loads the enabled adapter and then validates the synthesized component. Refer
to [Configure Discoverable Plugins](/configure-plugins/discoverable-plugins)
for the operator workflow and [Discoverable Plugins](/build-plugins/dynamic-plugins/about)
for the operator workflow and [Package Discoverable Plugins](/build-plugins/package-discoverable-plugins)
for the authoring workflow.

The operator lifecycle is explicit:
Expand All @@ -304,13 +297,8 @@ The operator lifecycle is explicit:
These commands manage future host activation. Teardown of an already running
host remains part of that host's owned plugin cleanup.

## Practical Guidance

Use these practices when applying the concept in application or integration code.

- Use plugins when behavior should be reusable across applications or
integrations.
- Validate plugin config before initialization.
- Treat plugins as the configuration-driven installation path for runtime
behavior.
- Keep detailed field-by-field config questions in the relevant guide for that plugin component.
Plugins are most useful when runtime behavior should be reusable across
applications or integrations. Validate configuration before initialization,
and keep field-by-field settings in the guide for the component that owns
them. The [plugin authoring overview](/build-plugins/about) explains when a
language-binding, native, or worker plugin is the right delivery model.
Loading
Loading