Runtime Events and Scopes

View as Markdown

Worker code cannot manipulate host scope objects directly. The SDK exposes an authenticated PluginRuntime proxy whose operations preserve the invocation’s captured scope context while the RelayHostRuntime service performs the real work.

Runtime Operation Map

IntentRust Worker SDKPython Worker SDK
Emit a markAwait emit_mark with name, data, and metadata.Await emit_mark with the same JSON fields.
Create or drop an isolated stackAwait create_scope_stack and drop_scope_stack.Await the same operations and retain the returned stack ID only for its owned lifetime.
Bind and restore a stackRun a future through with_scope_stack.Enter bind_scope_stack; use clear_scope_stack when code must deliberately run without a bound stack.
Push and pop a scopeAwait push_scope, retain its handle, and await pop_scope.Await the same pair; the handle, not a guessed scope ID, owns the pop.
Inspect inherited contextThe runtime proxy carries the task scope snapshot.current_scope_stack_id and current_parent_scope_id expose the current binding.
Emit typed telemetryUse emit_mark_with_options or emit_metric.Use the matching async runtime methods.
Read runtime diagnosticsAwait runtime_diagnostics.Await runtime_diagnostics.
Discover global runtime registrationsAwait list_runtime_registrations.Await list_runtime_registrations.
Disable or enable a global registrationAwait register_conditional_middleware_guardrail, retain the returned handle, and await deregister_conditional_middleware_guardrail.Await the matching methods and retain the returned opaque handle.

Every host request carries the activation ID, token, and relevant scope context. A worker must not expose, log, or persist the activation token or codec capability IDs. Those values identify a live local capability and expire with the activation or invocation.

Control a Registration from a Background Task

Worker gates are host-resident. Relay does not call the worker while it resolves a middleware chain or publishes an event. A worker timer or health monitor can therefore register a constant-reason gate, retain its activation-owned handle, and deregister it later.

The following worker flows discover the observability plugin’s OpenTelemetry subscriber and disable it for one timer interval:

1import asyncio
2
3from nemo_relay_plugin import RuntimeRegistrationKind
4
5subscribers = await ctx.runtime.list_runtime_registrations(
6 {RuntimeRegistrationKind.SUBSCRIBER}
7)
8opentelemetry = next(
9 (
10 registration
11 for registration in subscribers
12 if registration.local_name == "opentelemetry"
13 and registration.owner.plugin_kind == "observability"
14 ),
15 None,
16)
17if opentelemetry is None:
18 raise RuntimeError("OpenTelemetry subscriber was not found")
19
20gate = await ctx.runtime.register_conditional_middleware_guardrail(
21 "pause-opentelemetry",
22 {RuntimeRegistrationKind.SUBSCRIBER},
23 opentelemetry.effective_name,
24 "worker timer active",
25)
26try:
27 await asyncio.sleep(30)
28finally:
29 await ctx.runtime.deregister_conditional_middleware_guardrail(gate)

The worker can remove only gates owned by its activation. Relay removes any remaining owned gates during ordinary activation teardown. A worker can also call PluginContext.register_conditional_middleware_guardrail during registration to declare an initial host-resident gate; use PluginRuntime for timer-controlled changes after activation.

Refer to Conditional Middleware Guardrails for global-only behavior, effective-name discovery, all-matching-gate semantics, and the distinction between activation-owned and runtime-discovered gates.

Declare an Initial Gate

Use the component context when the validated configuration requires the gate for the complete worker activation. The following examples install the same host-resident constant reason:

1ctx.register_conditional_middleware_guardrail(
2 "documentation_registration_control",
3 {RuntimeRegistrationKind.SUBSCRIBER},
4 registration_name,
5 "disabled by documentation plugin",
6)

Relay validates initial gate names, kinds, targets, and duplicate names before it commits the worker registrations. Failed registration returns no initial gates. Ordinary teardown removes every gate that remains owned by the worker activation.

Use typed mark options when an exported mark needs a data schema or log severity. Use emit_metric for Relay metric measurements. runtime_diagnostics returns the bounded, ordered host-level { code, message, count } snapshot. It does not identify the plugin that recorded a diagnostic.

Emit Typed Telemetry and Read Diagnostics

Choose your worker language to add schema and severity metadata to a mark and inspect runtime diagnostics:

1from nemo_relay_plugin import DataSchema, LogSeverity
2
3await ctx.runtime.emit_mark(
4 "example.python_worker.policy_decision",
5 {"allowed": True},
6 data_schema=DataSchema("example.policy.decision", "1"),
7 severity=LogSeverity.INFO,
8)
9await ctx.runtime.emit_metric(
10 "example.python_worker.requests",
11 [{"name": "example.requests", "kind": "counter", "value_type": "u64", "value": 1}],
12)
13
14diagnostic = (await ctx.runtime.runtime_diagnostics()).get("otel.metric_mark_invalid")
15if diagnostic is not None:
16 print(f"{diagnostic.message} occurred {diagnostic.count} times")

An older host can return UNIMPLEMENTED for runtime_diagnostics. The SDK reports that as an unsupported runtime-diagnostics error. Relay validates the data schema and severity at the host boundary. It validates metric measurements after sanitization, so it rejects an invalid group as a whole.

Execute the Complete Cleanup Sequence

Choose your worker language to review the complete cleanup sequence:

The Python helper pushes a child scope, emits the ordinary mark inside it, and closes the scope by its returned handle. It then binds an isolated stack only for work that should have independent parentage. The finally block drops the stack even if isolated mark emission fails or the callback is cancelled.

1async def emit_runtime_events(ctx, tag, settings):
2 handle = await ctx.runtime.push_scope(
3 "example.python_worker.request",
4 scope_type=ScopeType.CUSTOM,
5 data={"tag": tag},
6 )
7 try:
8 if settings["emit_marks"]:
9 await ctx.runtime.emit_mark(
10 "example.python_worker.tool_request",
11 {"tag": tag},
12 )
13 except BaseException:
14 try:
15 await ctx.runtime.pop_scope(handle, metadata={"failed": True})
16 except BaseException:
17 pass
18 raise
19 else:
20 await ctx.runtime.pop_scope(handle, output={"done": True})
21
22 if settings["emit_isolated_scope"]:
23 stack_id = await ctx.runtime.create_scope_stack()
24 try:
25 with ctx.runtime.bind_scope_stack(stack_id):
26 if settings["emit_marks"]:
27 await ctx.runtime.emit_mark(
28 "example.python_worker.isolated.mark",
29 {"tag": tag},
30 )
31 finally:
32 await ctx.runtime.drop_scope_stack(stack_id)

Both implementations pop the handle exactly once. Successful work supplies scope output; failed work supplies failure metadata and then propagates the original callback error.

Clean Up Under Failure

The examples use a structured cleanup sequence: capture the previous binding, create an isolated stack only when configured, bind it, push a scope, run plugin work, pop the scope with success or error metadata, restore the previous binding, and drop the isolated stack. Python uses try/finally; Rust uses explicit result handling and awaits cleanup before returning.

Shutdown can race with these operations. The worker stops accepting new invocations, cancels active callbacks, and continues only the bounded cleanup that the SDK can still authenticate. An unreachable host can reject final cleanup, so the worker also releases its local handles and terminates rather than retrying forever.

Verify Parentage and Restoration

Use the following procedure to verify scope parentage, restoration, and failure cleanup:

  1. Invoke middleware inside a managed LLM scope and emit a mark through the runtime proxy. Confirm the mark’s parent is the invocation scope.
  2. Push and pop a child custom scope and verify its start and end event ordering.
  3. Create and bind an isolated stack, emit another mark, and confirm it belongs to the isolated root rather than the application call.
  4. Raise an error after the push and confirm the pop, prior binding restoration, and stack drop still occur.
  5. Cancel the callback during runtime work and repeat the same cleanup assertions.

Success means marks and scopes have intentional parentage, invocation tokens stay private, no stack remains owned after failure or cancellation, and subsequent worker callbacks receive their own unmodified scope snapshots.