Register Behavior

View as Markdown

Registration converts a valid component document into owned runtime behavior. The example installs only the feature groups whose enabled values are true and reads priority and break_chain directly from configuration. It never calls a process-global middleware registrar from inside the plugin.

An optional registration_control group in each complete example demonstrates an activation-owned conditional middleware guardrail. It is disabled by default. When enabled, the context qualifies the gate name and removes the gate automatically during rollback or teardown. Refer to Conditional Middleware Guardrails before selecting and storing the target name.

Register an Activation-Owned Gate

The following examples register the same constant-result gate after configuration has supplied a discovered effective target name:

1from nemo_relay.runtime_registrations import RuntimeRegistrationKind
2
3control = settings["registration_control"]
4if control["enabled"]:
5 context.register_conditional_middleware_guardrail(
6 "documentation-registration-control",
7 {RuntimeRegistrationKind(kind) for kind in control["kinds"]},
8 control["registration_name"],
9 lambda _kinds, _name: control["reason"],
10 )

The component-local gate name does not need a plugin prefix. Relay qualifies the name and records its rollback operation. The target name is different: it must already be the effective name that discovery returned for the current runtime activation.

Register Event Metadata Injectors

An event metadata injector receives an event snapshot and returns flat metadata additions. Relay validates and inserts accepted additions before event sanitizers run. Existing metadata values are preserved.

The following examples register a component-owned callback through PluginContext:

1def register(self, plugin_config, context):
2 tag = plugin_config["tag"]
3 context.register_event_metadata_injector(
4 "component-metadata",
5 10,
6 lambda event: {"example.plugin.tag": tag},
7 )

Applications can instead use nemo_relay.event_metadata.register_injector() for a global callback or nemo_relay.scope_local.register_event_metadata_injector() for a callback owned by an active scope. The matching deregistration functions remove those registrations.

Python and Node.js callbacks can return additions directly or asynchronously. Rust callbacks return a future. In every binding, callback failures and invalid return values omit that callback’s additions without dropping the event.

Register One Equivalent Request Intercept

The following excerpts show the same model-header rewrite. The full checked examples add event observation, tool policy, execution wrappers, and streaming verification around this common center.

1settings = normalized_config(config)
2tag = settings["tag"]
3observe = settings["observe"]
4requests = settings["requests"]
5execution = settings["execution"]
6
7if observe["enabled"]:
8 context.register_subscriber(
9 "events", lambda event: self.events.append(event.name)
10 )
11
12def tool_policy(name, _args):
13 if requests["mode"] == "enforce" and name in requests["blocked_tools"]:
14 return f"tool '{name}' is blocked"
15 return None
16
17context.register_tool_conditional_execution_guardrail(
18 "tool-policy", 10, tool_policy
19)
20context.register_tool_request_intercept(
21 "tool-request",
22 requests["priority"],
23 requests["break_chain"],
24 lambda _name, args: {**args, "plugin_tag": tag},
25)
26
27def add_header(name, request, annotated):
28 headers = dict(request.headers)
29 headers[requests["header_name"]] = requests["header_value"]
30 return LLMRequestInterceptOutcome(
31 request=LLMRequest(headers=headers, content=request.content),
32 annotated_request=annotated,
33 )
34
35context.register_llm_request_intercept(
36 "documentation-header",
37 requests["priority"],
38 requests["break_chain"],
39 add_header,
40)
41
42async def stream_request(request, next_call):
43 async for chunk in await next_call(request):
44 yield {**chunk, "plugin_stream": True}
45
46context.register_llm_stream_execution_intercept(
47 "documentation-stream", execution["priority"], stream_request
48)

The LLM intercept returns the complete outcome rather than relying on mutation. In particular, it preserves annotated. The Rust request is mutable inside its owned callback value; Python creates a new typed request, and Node.js creates a new plain object. Those language differences do not change Relay semantics.

Initialize and Inspect

Use the following procedure to verify successful activation and transactional rollback:

  1. Register the kind, validate the shared component, and stop if the report contains an error. Duplicate kind registration is itself an error and should fail the test.
  2. Initialize the valid document with Rust initialize, Python nemo_relay.plugin.initialize, or Node.js plugin.initialize. Keep the returned activation handle alive for the entire host lifetime.
  3. Inspect the report on that handle. Rust uses activation.report(), while Python and Node.js use activation.report. It describes that activation without rerunning validation.
  4. Execute an LLM request and inspect the real callback headers. Then emit an event and execute the representative tool and stream paths from the checked example.
  5. Force a later registration in the same component to fail. Initialization should reject, the new partial registrations should disappear, and Relay should restore the previous configuration when it can prove cleanup succeeded.

Success means configuration controls the installed surfaces, names are component-owned, the activation report matches the runtime effect, and failed registration leaves no half-active middleware.