Skip to content

Commit 71b90d5

Browse files
authored
docs: update runtime architecture ownership (#697)
#### Overview Update the Architecture page to reflect current runtime ownership and delivery behavior. - [x] I confirm this contribution is my own work, or I have the right to submit it under this project's license. - [x] I searched existing issues and open pull requests, and this does not duplicate existing work. #### Details - Show the CLI and language bindings as hosts of the shared Relay runtime. - Show plugin components as configuration-installed behavior instead of presenting Adaptive as a peer layer. - Separate the active scope stack from parent-linked event records. - Distinguish awaited async middleware from background subscriber delivery. - Add concrete middleware and exporter examples at first mention and remove the generic related-links section. #### Where should the reviewer start? Start with the architecture diagram and the middleware and subscriber sections in `docs/about-nemo-relay/architecture.mdx`. #### Related Issues: (use one of the action keywords Closes / Fixes / Resolves / Relates to) - Closes [RELAY-621](https://linear.app/nvidia/issue/RELAY-621/update-the-architecture-page-around-current-runtime-ownership) ## Summary by CodeRabbit * **Documentation** * Expanded architecture guidance for CLI and runtime integration. * Clarified plugin components, middleware behavior, observability, guardrails, event handling, scopes, and managed execution. * Added diagrams for scope stacks, middleware resolution, and managed calls. * Documented parent-linked events and asynchronous subscriber delivery. * Updated terminology and related links for improved clarity. * Removed outdated sections and streamlined the overall architecture reference. Authors: - Alex Fournier (https://github.com/afourniernv) - Will Killian (https://github.com/willkill07) Approvers: - https://github.com/lvojtku - Will Killian (https://github.com/willkill07) URL: #697
1 parent eab6790 commit 71b90d5

1 file changed

Lines changed: 188 additions & 56 deletions

File tree

docs/about-nemo-relay/architecture.mdx

Lines changed: 188 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,24 @@ import { MermaidStyles } from "@/components/MermaidStyles";
88
{/* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
99
SPDX-License-Identifier: Apache-2.0 */}
1010

11-
This page explains how NeMo Relay connects scopes, middleware, plugins, events,
12-
subscribers, and exporters.
11+
This page explains how NeMo Relay connects [scopes](/about-nemo-relay/concepts/scopes),
12+
[middleware](/about-nemo-relay/concepts/middleware),
13+
[plugins](/about-nemo-relay/concepts/plugins),
14+
[events](/about-nemo-relay/concepts/events), and
15+
[subscribers](/about-nemo-relay/concepts/subscribers).
1316

1417
## Architecture Diagram
1518

16-
This diagram connects the runtime pieces to the layers they inhabit.
19+
This diagram shows how runtime hosts and integrations reach the shared Rust
20+
runtime. Plugin components install reusable behavior, and subscribers deliver
21+
event snapshots to in-process consumers or external backends.
1722

1823
<MermaidStyles />
1924

2025
```mermaid
2126
flowchart TB
22-
subgraph AppLayer[Framework Integrations and Application Code]
27+
subgraph AppLayer[Runtime Hosts and Integrations]
28+
CLI[NeMo Relay CLI]
2329
App[Application Code]
2430
Framework[Framework Integration]
2531
end
@@ -28,9 +34,10 @@ flowchart TB
2834
Bindings[Language Bindings]
2935
end
3036
31-
subgraph PluginLayer[Plugin and Adaptive Layer]
37+
subgraph PluginLayer[Plugin System and Components]
3238
PluginSystem[Plugin System]
33-
Adaptive[Adaptive Component]
39+
Components[Adaptive / Observability / Guardrail Components]
40+
PluginSystem -->|activates| Components
3441
end
3542
3643
subgraph CoreLayer[Core Runtime]
@@ -54,10 +61,11 @@ flowchart TB
5461
App -. direct use .-> Bindings
5562
App -->|registers and configures| PluginSystem
5663
Framework -->|calls| Bindings
64+
CLI -->|hosts| Core
65+
CLI -->|loads configuration| PluginSystem
5766
Bindings --> Core
58-
Adaptive -->|activates via| PluginSystem
59-
PluginSystem -->|installs| Registry
60-
PluginSystem -->|installs| Subs
67+
Components -->|register middleware| Registry
68+
Components -->|register subscribers| Subs
6169
Core -->|updates| Scope
6270
Core -->|resolves| Registry
6371
Core -->|emits| Events
@@ -71,41 +79,55 @@ flowchart TB
7179
class CoreLayer grey-hint;
7280
class ObsLayer grey-hint;
7381
class RuntimeState grey-lightest;
82+
class CLI purple-lightest;
7483
class App purple-lightest;
7584
class Framework yellow-lightest;
7685
class Bindings green-lightest;
7786
class PluginSystem green-light;
78-
class Adaptive blue-lightest;
87+
class Components blue-lightest;
7988
class Core green-light;
8089
class Scope green-light;
8190
class Registry green-light;
8291
class Events green-light;
92+
class Dispatcher green-light;
8393
class Subs green-light;
8494
class Backends grey-light;
8595
```
8696

87-
Adaptive appears here as a built-in plugin component rather than a separate runtime model because it activates through the same plugin lifecycle.
97+
Adaptive behavior is one plugin component installed through the same lifecycle
98+
as observability or PII redaction. It is not a separate runtime layer.
8899

89100
## Runtime Model
90101

91-
NeMo Relay combines a small number of runtime pieces into one shared execution model:
92-
93-
- The **scope stack** answers where work belongs
94-
- The **middleware registries** answer what should happen around that work
95-
- The **plugin system** installs reusable runtime behavior from configuration
96-
- The **event stream** records what happened
97-
- The **async subscriber dispatcher** delivers event snapshots after emission
98-
- **subscribers** consume those events
99-
100-
Every emitted scope, tool, LLM, or mark event attaches to the active scope stack. Every managed tool or LLM call resolves the currently visible middleware before it executes.
102+
NeMo Relay combines a small number of runtime pieces into one shared execution
103+
model:
104+
105+
- The [**scope stack**](/about-nemo-relay/concepts/scopes) answers where work
106+
currently belongs.
107+
- The [**middleware registries**](/about-nemo-relay/concepts/middleware) answer
108+
what should happen around that work.
109+
- The [**plugin system**](/about-nemo-relay/concepts/plugins) installs reusable
110+
runtime behavior from configuration.
111+
- The [**event records**](/about-nemo-relay/concepts/events) preserve what
112+
happened and how work was related.
113+
- The [**async subscriber dispatcher**](/about-nemo-relay/concepts/subscribers#waiting-for-delivery)
114+
delivers event snapshots after emission.
115+
- [**Subscribers and exporters**](/about-nemo-relay/concepts/subscribers#common-subscriber-roles)
116+
consume those snapshots.
117+
118+
Every managed tool or LLM call resolves the middleware visible from the active
119+
scope before it executes. When the runtime emits an event, it records the active
120+
scope UUID as parentage. The scope stack changes as work opens and closes; the
121+
parent-linked event records remain available to subscribers.
101122

102123
## Main Runtime Pieces
103124

104125
These components are the primary building blocks that make up the runtime model.
105126

106127
### Scope Stack
107128

108-
The active scope stack defines the ownership tree for runtime work. It establishes:
129+
The active scope stack defines the current context for runtime work.
130+
It establishes:
109131

110132
- Parent-child relationships between events
111133
- Scope-local visibility for middleware and subscribers
@@ -114,21 +136,54 @@ The active scope stack defines the ownership tree for runtime work. It establish
114136

115137
### Middleware Registries
116138

117-
The middleware registries hold the active intercepts and guardrails for tool and LLM execution. Managed helpers read those registries before invoking the real callback.
139+
The middleware registries hold the active intercepts and guardrails for tool and
140+
LLM execution. Request intercepts can rewrite real requests, conditional
141+
guardrails can reject execution, and sanitize guardrails can change emitted
142+
observability payloads. Managed helpers read those registries before invoking
143+
the real callback.
144+
145+
Async middleware callbacks are awaited as part of managed execution. The
146+
managed call does not advance past that middleware callback until the callback
147+
returns.
118148

119149
### Plugin System
120150

121-
The plugin system installs reusable runtime components from configuration. A plugin can register middleware, subscribers, or related behavior without requiring each application call site to do the work manually.
151+
The plugin system installs reusable runtime components from configuration. A
152+
plugin can register middleware, subscribers, or related behavior without
153+
requiring each application call site to repeat the setup.
122154

123155
### Event Emission
124156

125-
The runtime emits structured events for scopes, tools, LLMs, and named marks. Those events are the canonical record of runtime behavior. Native Rust, Python, Node.js, and FFI event-producing APIs enqueue subscriber work and return without waiting for subscriber callbacks or exporter work.
157+
The runtime emits structured events for scopes, tools, LLMs, and named marks.
158+
Those parent-linked records are the canonical history of runtime behavior.
159+
Native Rust, Python, Node.js, and FFI event-producing APIs enqueue subscriber
160+
work and return without waiting for subscriber callbacks or exporter work.
126161

127162
### Subscribers and Exporters
128163

129-
Subscribers consume the event stream through the background dispatcher. Some subscribers stay in-process. Others export that stream into files or tracing systems. Use the binding flush API when a test or shutdown path must wait for already-queued subscriber work.
130-
131-
## Two Axes of Runtime State
164+
Subscribers consume event snapshots through the background dispatcher. Some
165+
stay in process. Exporter subscribers can write
166+
[ATOF JSONL](/configure-plugins/observability/atof), project events into
167+
[ATIF trajectories](/configure-plugins/observability/atif), or emit
168+
[OpenTelemetry traces](/configure-plugins/observability/opentelemetry).
169+
Typical destinations include in-process application logic, local files and
170+
artifact pipelines, OTLP-compatible observability backends, and evaluation or
171+
visualization tools.
172+
173+
This delivery happens after event submission and is separate from awaited async
174+
middleware. Use the binding flush API when a test or shutdown path must wait for
175+
already queued subscriber work. For manually registered exporters, follow the
176+
exporter's documented teardown order before process exit. The
177+
[ATOF](/configure-plugins/observability/atof),
178+
[OpenTelemetry](/configure-plugins/observability/opentelemetry), and
179+
[OpenInference](/configure-plugins/observability/openinference) exporters
180+
deregister to stop new deliveries, force-flush queued work, and then shut down.
181+
The [ATIF](/configure-plugins/observability/atif) export operation drains queued
182+
subscriber work before taking its snapshot; after export, deregister the
183+
subscriber and clear the exporter state. Clearing plugin configuration owns
184+
teardown for plugin-installed exporters.
185+
186+
## Where Runtime State Lives
132187

133188
Runtime state is easiest to understand by separating ownership from process-wide
134189
registration.
@@ -142,6 +197,29 @@ The scope stack defines:
142197
- When scope-local registrations are cleaned up
143198
- Whether concurrent requests stay isolated
144199

200+
Only scopes are pushed onto the stack. Managed LLM and tool calls emit lifecycle
201+
records that use the current top scope as `parent_uuid`; they do not become
202+
stack entries. This example shows an agent scope with a nested function scope
203+
named `turn-a`.
204+
205+
```mermaid
206+
flowchart BT
207+
Root["BOTTOM: Implicit root<br/>uuid = root-a"]
208+
Agent["Agent scope<br/>uuid = agent-a<br/>parent_uuid = root-a"]
209+
Turn["TOP: Function scope (turn)<br/>uuid = turn-a<br/>parent_uuid = agent-a"]
210+
211+
Root -->|"push agent-a"| Agent
212+
Agent -->|"push turn-a"| Turn
213+
214+
class Root grey-light;
215+
class Agent,Turn green-light;
216+
```
217+
218+
With `turn-a` at the top, managed LLM and tool records receive
219+
`parent_uuid = turn-a`, so they are siblings in the event tree. Popping
220+
`turn-a` returns `agent-a` to the top and removes registrations owned by the
221+
turn. A concurrent request uses a separate stack.
222+
145223
### Middleware Ownership
146224

147225
Middleware exists at two levels:
@@ -151,6 +229,43 @@ Middleware exists at two levels:
151229

152230
That split lets long-lived defaults coexist with request-specific or task-specific behavior.
153231

232+
The following diagram shows how global and scope-local middleware are resolved
233+
for two concurrent requests.
234+
235+
```mermaid
236+
flowchart TB
237+
Global["Global middleware<br/>process-wide"]
238+
239+
subgraph RequestA["Request A scope stack"]
240+
LocalA["Scope-local middleware<br/>owned by agent-a"]
241+
ResolveA["Merge visible entries<br/>and order by priority"]
242+
CallA["Managed call A"]
243+
CloseA["Close agent-a"]
244+
LocalA --> ResolveA --> CallA
245+
CloseA -.->|"removes"| LocalA
246+
end
247+
248+
subgraph RequestB["Request B scope stack"]
249+
LocalB["Scope-local middleware<br/>owned by agent-b"]
250+
ResolveB["Merge visible entries<br/>and order by priority"]
251+
CallB["Managed call B"]
252+
LocalB --> ResolveB --> CallB
253+
end
254+
255+
Global --> ResolveA
256+
Global --> ResolveB
257+
258+
class Global blue-lightest;
259+
class RequestA,RequestB grey-lightest;
260+
class LocalA,LocalB green-lightest;
261+
class ResolveA,ResolveB green-light;
262+
class CallA,CallB yellow-lightest;
263+
class CloseA grey-light;
264+
```
265+
266+
Global entries are visible to both calls. Each scope-local entry is visible
267+
only through its owning stack and is removed when that scope closes.
268+
154269
## Managed Execution Pipeline
155270

156271
Managed tool and LLM execution follows the same high-level order:
@@ -162,36 +277,53 @@ Managed tool and LLM execution follows the same high-level order:
162277
5. The user callback runs.
163278
6. Sanitize-response guardrails can rewrite the emitted end-event payload.
164279

280+
The following sequence shows where each middleware family runs during a managed
281+
call.
282+
283+
```mermaid
284+
sequenceDiagram
285+
autonumber
286+
actor Caller as Application / Framework
287+
participant Runtime as NeMo Relay Runtime
288+
participant Conditional as Conditional Guardrails
289+
participant Request as Request Intercepts
290+
participant Sanitizers as Request / Response Sanitizers
291+
participant Execution as Execution Intercepts
292+
participant Callback as Real Callback
293+
participant Dispatcher as Async Subscriber Dispatcher
294+
295+
Caller->>Runtime: managed tool or LLM call
296+
Runtime->>Conditional: evaluate real request
297+
alt rejected
298+
Conditional-->>Runtime: rejection
299+
Runtime-->>Caller: guardrail error
300+
else allowed
301+
Conditional-->>Runtime: continue
302+
Runtime->>Request: transform real request
303+
Request-->>Runtime: intercepted request
304+
Runtime->>Sanitizers: sanitize event-only request copy
305+
Sanitizers-->>Runtime: start-event payload
306+
Runtime->>Dispatcher: enqueue start event
307+
Runtime->>Execution: invoke intercept chain
308+
alt intercept replaces execution
309+
Execution-->>Runtime: replacement result
310+
else intercept calls next
311+
Execution->>Callback: invoke real callback
312+
Callback-->>Execution: real result
313+
Execution-->>Runtime: real result
314+
end
315+
Runtime->>Sanitizers: sanitize event-only response copy
316+
Sanitizers-->>Runtime: end-event payload
317+
Runtime->>Dispatcher: enqueue end event
318+
Runtime-->>Caller: return real result
319+
end
320+
```
321+
165322
Two distinctions matter:
166323

167324
- Intercepts affect the real execution path
168325
- Sanitize guardrails affect the emitted observability payload
169326

170-
For the expanded request-to-response runtime path, including streaming and subscriber handoff, refer to [Middleware](/about-nemo-relay/concepts/middleware#detailed-execution-flow).
171-
172-
## Runtime Layers
173-
174-
From bottom to top, NeMo Relay is organized as:
175-
176-
1. The Rust core runtime
177-
2. The plugin and adaptive layer
178-
3. Language bindings
179-
4. Framework integrations and application code
180-
5. Subscribers and observability backends
181-
182-
The details of a binding can vary, but the conceptual model stays the same across those layers.
183-
184-
## Design Goal
185-
186-
NeMo Relay is designed so that application developers, framework integrators, plugin authors, and observability consumers all reason about the same runtime semantics. One conceptual model should remain stable even when the binding or integration style changes.
187-
188-
## Related Concepts
189-
190-
The following concepts are related to this architecture:
191-
192-
- [Scopes](/about-nemo-relay/concepts/scopes)
193-
- [Middleware](/about-nemo-relay/concepts/middleware)
194-
- [Events](/about-nemo-relay/concepts/events)
195-
- [Subscribers](/about-nemo-relay/concepts/subscribers)
196-
- [Plugins](/about-nemo-relay/concepts/plugins)
197-
- [Codecs](/about-nemo-relay/concepts/codecs)
327+
For the expanded request-to-response runtime path, including streaming and
328+
subscriber handoff, refer to
329+
[Managed Execution Order](/about-nemo-relay/concepts/middleware#managed-execution-order).

0 commit comments

Comments
 (0)