About Language Binding Plugins

View as Markdown

A language-binding plugin is application code that registers a stable plugin kind with the Relay runtime already loaded by Rust, Python, or Node.js. It has no manifest, shared library, worker process, integrity digest, or separately managed environment. That makes it the most direct choice for behavior owned and deployed by one application.

The checked examples/language-binding-plugin project implements one documentation-plugin in all three bindings. Every version follows the same sequence:

  1. Validate the same JSON-compatible component settings.
  2. Install equivalent event and request behavior.
  3. Print the activation report and exercise managed tool, model, stream, and event paths.
  4. Clear registrations and deregister the kind.

The Smallest Useful Language-Binding Plugin

These complete programs contain the same boundary in each binding: the plugin validates component-local JSON, installs component-owned middleware, the host activates a PluginConfig, and cleanup tears the behavior down.

1import asyncio
2import nemo_relay
3from nemo_relay import plugin, tools
4
5class AddTagPlugin:
6 def validate(self, config):
7 tag = config.get("tag")
8 if not isinstance(tag, str) or not tag:
9 return [{
10 "level": "error",
11 "code": "example.invalid_tag",
12 "component": "example.add-tag",
13 "field": "tag",
14 "message": "tag must be a non-empty string",
15 }]
16 return []
17
18 def register(self, config, context):
19 tag = config["tag"]
20 context.register_tool_request_intercept(
21 "add-tag",
22 20,
23 False,
24 lambda _name, request: {**request, "plugin_tag": tag},
25 )
26
27async def main():
28 plugin.register("example.add-tag", AddTagPlugin())
29 config = plugin.PluginConfig(components=[
30 plugin.ComponentSpec(
31 kind="example.add-tag",
32 enabled=True,
33 config={"tag": "documentation"},
34 )
35 ])
36 try:
37 async with plugin.activate(config) as activation:
38 print("activation:", activation.report)
39 result = await tools.execute(
40 "lookup",
41 {"id": 7},
42 lambda request: nemo_relay.ToolExecutionResult(request),
43 )
44 assert result.result == {"id": 7, "plugin_tag": "documentation"}
45 finally:
46 plugin.deregister("example.add-tag")
47
48asyncio.run(main())

The host passes only {"tag": "documentation"} to both hooks. register does not receive the surrounding document, enabled, or another component’s settings. Relay records the registration against this component, which is why clearing configuration can remove it without the plugin keeping a global deregistration handle.

How the Bindings Differ

ConcernPythonNode.jsRust
Plugin identityplugin.register(kind, implementation) supplies the kind.plugin.register(kind, implementation) supplies the kind.Plugin::plugin_kind() supplies the kind passed to register_plugin.
Validationvalidate is optional in the protocol, but a published component should implement it.validate is optional in the interface, but a published component should implement it.validate is required. Override validate_with_policy when component diagnostics should honor the host’s non-default policy.
Registration hookRuns synchronously. Installed middleware callbacks can be synchronous or asynchronous according to their typed API.Runs synchronously. Installed middleware callbacks can return values or promises according to their declarations.Returns a Send future, so initialization can await resource setup and registration.
ClosingAwait activation.close() after application work finishes.Await relay.flushSubscribers() when queued publication is active, then await activation.close().activation.close() removes active registrations and can report teardown failure.
Deregistrationderegister(kind) returns whether a kind was removed.deregister(kind) returns whether a kind was removed.deregister_plugin(kind) removes the implementation from future lookup.

Configuration keys remain snake_case in all three examples. Only Node.js API methods such as listKinds or registerLlmRequestIntercept use camelCase.

Follow the Complete Workflow

Follow these pages in order to build, activate, exercise, and remove the same plugin in each language binding:

  1. Validate Configuration turns wrong types, unsupported modes, unknown fields, and disabled-invalid components into stable diagnostics before runtime state changes.
  2. Register Behavior connects valid feature groups to component-owned registrations and verifies rollback.
  3. Advanced Configuration covers multiple instances, host policy, reports, activation close, deregistration, and async lifecycle differences.
  4. Runnable Examples gives the clean commands and expected output for Rust, Python, and Node.js.

Success means the same operator intent produces the same visible behavior in each binding: invalid configuration is inert, valid configuration reports activation, representative calls show the plugin effect, and teardown removes both active behavior and future kind lookup when requested.