Native ABI Reference
Use the typed nemo-relay-plugin SDK for normal authoring. This page records the raw
contract needed to audit compatibility or implement an escape hatch; unsafe *_raw
functions are intentionally not tutorial examples.
Entry and Version Negotiation
The exported symbol receives a pointer to the v1 prefix of the host table and fills a
v1 plugin descriptor. The implementation must inspect abi_version and struct_size
before casting a longer table.
The current host supplies the append-only ABI v8 extension and retains v7
negotiation for existing plugins. The SDK accepts v7 and v8 callback layouts;
inspection requires v8 and reports unsupported functionality on v7. Versions
before v7 remain incompatible because the LLM execution callback layouts changed. Rebuild every native plugin for Relay 0.10 and raise its
compat.relay lower bound to 0.10.0. The authored manifest label remains
compat.native_api = "1".
ABI v7 adds request and completed-response codec context to raw, typed, and
asynchronous LLM execution callbacks. The generic asynchronous middleware
callback remains unchanged; ABI v7 appends an execution-specific non-streaming
registration, and the existing stream callback gains the context parameter.
ABI v6 adds a host-routed operational logging function. Native plugins pass a level,
optional target, message, and optional JSON-object fields; Relay applies its operational
logging policy and preserves those fields in structured JSONL output.
ABI v4 extends the complete v3 prefix with completion-scoped
codecs and
pull-based downstream LLM streams, plus an activation-owned runtime capability for
registration discovery and dynamic conditional middleware guardrail control. These table
versions are independent of authored
compat.native_api = "1".
The following table enumerates the operations introduced at each table level. The exact
function signatures and field order are defined by the public
nemo-relay-plugin declarations.
The prefix and descriptor layout are explicit. A plugin fills the descriptor with its stable kind, component multiplicity, opaque state, callbacks, and destructor. The host table is immutable after negotiation.
The shortened host-table declaration shows the mandatory prefix, not a replacement
definition that a raw plugin can copy. A raw implementation must compile against the
complete public declarations in nemo-relay-plugin so field offsets match exactly.
Values and Ownership
Text and JSON cross the boundary as host-owned NemoRelayNativeString handles. ABI
structs otherwise contain scalars, opaque handles, callback pointers, and plugin-owned
user_data. Rust trait objects, futures, serde_json::Value, allocator-owned strings,
and unwinding must never cross the boundary. Release host strings with the matching host
operation and retain plugin state until the host releases the registration and every
callback-owned reference derived from it.
A successful plugin_context_runtime call returns one owned runtime-capability
reference. Each successful plugin_runtime_retain call creates another owned reference,
and the plugin must pass every owned reference to plugin_runtime_release exactly once.
Activation teardown makes runtime operations unavailable, but it does not release
references retained by plugin code; those references must still be released.
The v4 conditional middleware callback receives the configured registration kinds as
JSON and the target’s effective registration name. It returns a host-allocated reason
to disable the target or a null reason to leave the target enabled. Callback registration
consumes user_data on every return path. Relay calls free_fn after deregistration,
activation rollback, or teardown. The callback runs synchronously in the host process,
must not unwind across the ABI, and must not make reentrant Relay calls. Callback errors
and invalid reason strings fail open.
The callback slots extend v4 through struct_size. Existing v4 binaries continue to use
the original table prefix and constant-reason slots at unchanged offsets. A plugin must
verify that struct_size includes a callback slot before reading or calling it.
Ok means the output pointers required by that operation were populated according to
its contract. Backpressured is retryable only for the bounded stream operation that
returned it. Other errors should be propagated with the host’s thread-local error
message set when additional context is available.
The table exposes registrations for subscriber; mark, scope-start, and scope-end sanitizers; five tool surfaces; and six LLM surfaces. Runtime operations cover current scope, mark emission, scope push and pop, isolated stack creation and drop, stack capture or binding, and restoration.
Async Completions and Continuations
PluginContext::register_async_middleware_raw registers non-stream middleware other
than LLM execution. ABI v7 uses the execution-specific
plugin_context_register_async_llm_execution_intercept entry for asynchronous non-streaming
LLM execution callbacks. Both paths can
settle later. Return Complete only after resolving or rejecting the completion inside
the callback. Return Pending only after retaining it. A retained completion must settle
exactly once and then be released. Release every async next reference after its last
use.
async_next_invoke_result supports repeated or concurrent unary continuation calls with
independent result callbacks. The older completion-coupled async_next_invoke is
one-shot because the continuation result settles the middleware completion. Settle the
owner only after every started continuation call has finished. When the owner settles or
is cancelled, the host rejects new continuation calls and cancels unfinished ones.
For a tool continuation, the result callback receives canonical
nemo.relay.ToolExecutionResult@1 JSON with required result and optional opaque
annotation. A raw tool-execution middleware completion returns
nemo.relay.ToolExecutionInterceptOutcome@2: the same result and annotation fields plus
optional pending_marks. Pending marks belong to the intercept outcome only. Release
each host-owned result string with the matching host-table operation after decoding it.
The asynchronous tool-execution invocation envelope carries name, value, and
tool_call_id. tool_call_id is the provider-issued correlation identifier
recorded on the managed tool call, and is JSON null when the call did not
record one. It is an additive field, so it needs no ABI change and plugins built
before it existed ignore it; conversely, plugins that read it must tolerate its
absence when running against an older host. The raw
NemoRelayNativeToolExecutionCb callback does not carry the identifier — use the
asynchronous middleware path when a native plugin needs it.
The host can poll a callback on different Tokio workers, and separate invocations can
run concurrently. Plugin code must synchronize user_data and opaque handles. Do not
race final release with settlement, cancellation inspection, stream operations, codec
operations, or next invocation.
Streaming
The generic v3 completion API rejects the LLM stream intercept kind. Register it through
plugin_context_register_async_stream_middleware and invoke downstream streams through
async_next_invoke_stream. Each repeated or concurrent invocation needs independent
callback state.
Finish and rejection settle chunk delivery. Release the output stream capability with
async_stream_release only after producer-owned cleanup is complete. Balance each
async_stream_retain with a release. Managed stream close waits for all plugin-owned
output capabilities to be released; the typed SDK handles this ordering automatically.
The output queue is bounded. async_stream_push_json and async_stream_reject never
block a callback thread. Backpressured means the same logical chunk or rejection must
be retried after the consumer advances. InvalidArg means the stream is closed or
cancelled and the operation must not be retried. async_stream_is_backpressured is only
a point-in-time observation; it does not replace checking the result of a push.
A downstream terminal callback reports failure or consumer cancellation with a non-null
error and reports clean completion with done = true. Reclaim its user_data in that
terminal callback. If a chunk callback returns false, reclaim state before returning
because the host does not call it again.
Codec Handles
LLM sanitizer contexts report codec kind None, BuiltIn, Runtime, or Opaque, an
optional codec ID, and a borrowed callback-lifetime handle. Built-in IDs are
openai_chat, openai_responses, anthropic_messages, oci_genai, and
gemini_generate_content. Runtime and opaque codecs can still have a usable handle.
Request handles support decode to an annotated request and encode of normalized changes onto the original envelope. Response handles support decode to an annotated response. A successful null sanitizer result omits the observability payload and annotation; an error also omits them and records the callback failure. Neither outcome changes the real request or application response. Never retain a raw handle or resolved typed facade after the sanitizer callback ends.
LLM execution context uses the same identity and opaque codec operations. Its request direction is always present and may resolve request decode/encode. Non-streaming execution also carries an optional response direction with response decode. Streaming execution sets the response direction to unavailable because the response codecs operate on completed provider responses, not individual chunks. Raw execution-context handles are borrowed for the callback. A synchronous raw stream callback may store its request handle only in the state of the returned stream; Relay keeps it valid until that stream’s drop callback. Typed non-streaming facades retain their completion and typed streaming request facades retain their output stream, so they remain memory-safe while the callback future or returned stream owns them. Codec calls fail after the completion or stream settles. Asynchronous raw stream callbacks must retain the output stream and use the v7 stream-scoped decode and encode operations; release that stream reference after the last call.
Unload Ordering
Relay keeps the library loaded while any owned registration or callback can reference plugin code. Shutdown stops new invocations, cooperatively cancels unfinished async work, waits for completion and stream references to be released, deregisters component-owned surfaces, runs component cleanup, and only then unloads the shared library. A plugin that retains a completion, continuation, stream, codec, or runtime handle indefinitely can therefore delay safe unload.
Scope Snapshot Ownership
NemoRelayNativeHostApiV8 starts with the complete unchanged v7 table and
appends current_scope_stack_snapshot(out). Check abi_version >= 8 and
struct_size >= sizeof(NemoRelayNativeHostApiV8) before accessing that field.
The callback returns success with a null output for no explicit binding, or an
owned JSON object with scopes, each containing scope_id and lowercase
scope_type. An active root-only stack has one entry. Release every non-null
buffer through the inherited string_free, including decode or status-error
paths. Errors use the existing status and last-error channel; output is
initialized to null before the read.
The typed getter returns a detached value. The host releases its read lock before encoding and returns no pointers into stack handles, registries, or Rust containers.
Native Diagnostics Extension
NativeHostApiV8WithDiagnostics preserves the complete ABI-8 prefix and appends
an immutable pointer to NativeDiagnosticsApiV1. Read the pointer only when
abi_version == 8 and struct_size includes the full tail. Then check the
pointed table’s size and independent contract_version (currently 1) before copying callbacks.
A missing, null, truncated, or unknown table is unsupported. The execution ABI
remains 8, ABI-7 negotiation is retained, and compat.native_api = "1" is unchanged.
The existing get_runtime_diagnostics callback keeps its original position and
legacy host-report JSON contract. The diagnostics extension supplies separate
registration, recording, and canonical-read callbacks. It does not replace that
reader. Existing plugins continue to use the frozen host prefix. A new SDK on a
compatible host without the extension exposes an unsupported facade and skips
recording factories while preserving execution and legacy-reader behavior. A
plugin that requires the extension must declare a Relay compatibility range that
includes its availability. Refer to the native SDK guide
for the authoring and reader APIs.
The table provides capabilities, component registration, recording, closure, and canonical reads. Registration and recording use fixed C layouts and borrowed UTF-8 byte spans, never Rust-owned objects. Arrays and outputs must be aligned and valid for the call. The host retains no input pointers. Code handles are one-based declaration positions. Component tokens are activation-scoped and never reused. Recording is synchronous and state-only, with a fixed acceptance receipt and no exporter projection or retry queue.
Diagnostics uses its own numeric statuses, defined by
diagnostics::DiagnosticError::from_status, independently of NemoRelayStatus.
Zero means accepted. One means disabled for registration/recording. Unknown
statuses are internal failures, not success. Adapter errors contain bounded
reason labels and never place input detail in the legacy last-error channel.
The native adapter has these limits:
SDK clones also share per-activation admission: four data-call slots and one in-flight read query/output-buffer pair. These SDK limits are separate from the host permits above. Refer to concurrent calls and recursion for admission order, recursion protection, and caller-owned results.
Registration exhaustion can reject new components. Full core history evicts evidence without disabling existing recorders.
Capabilities report adapter layout bounds and current activation session bytes separately from core storage. They are not process RSS or allocator measurements.
Reads check activation and the host’s native_diagnostics_read_context grant
before parsing queries or capturing data. Query input uses core’s existing byte
limit. The host uses the selected context’s canonical read leases and copies
complete JSON into caller-owned output up to the configured page cap. It releases
the lease before returning. written is zero on any failure. Callers must ignore
all output bytes on failure. A short buffer never yields a usable partial page.
Decoded pages and SDK buffers belong to the caller and are outside core’s bound.
The grant includes whole-context restricted data and defaults to false. It does not grant worker reads. Registration/read failures do not change execution or legacy-report semantics. Teardown fences recording, closes component owners, and releases session backing and strong context references through the existing native runtime cleanup. Retained diagnostic evidence contains only host-owned data and cannot keep plugin code loaded.