nexoai — nexo plugin SDK (Python)
Child-side SDK for nexo subprocess plugins written in Python 3.10+.
Mirrors the Rust counterpart in
crates/microapp-sdk/, the TypeScript
counterpart in typescript/ and
the PHP counterpart in php/ — same wire
format (nexo-plugin-contract.md),
different language.
pip install nexoai
Distribution name vs import name — the PyPI package is
nexoai(thenexo-plugin-sdkname was already taken); the importable module isnexo_plugin_sdk(from nexo_plugin_sdk import PluginAdapter).
The reference plugin template lives at
the Python plugin template (or run nexo plugin new --lang python);
copy that directory to start a new plugin.
Public API
from nexo_plugin_sdk import (
PluginAdapter, # async dispatch loop
BrokerSender, # write-only handle to publish events back
Event, # dataclass mirror of the host's broker event
EventHandler, # type alias: Callable[[str, Event, BrokerSender], Awaitable[None]]
ShutdownHandler, # type alias: Callable[[], Awaitable[None]]
PluginError, # base exception
ManifestError, # raised when nexo-plugin.toml is malformed
WireError, # raised on malformed / oversized JSON-RPC frames
read_manifest, # standalone manifest TOML parser + validator
install_stdout_guard, # defensive guard installable independently
uninstall_stdout_guard,
is_stdout_guard_installed,
STDOUT_GUARD_MARKER, # sentinel prefixed onto diverted stdout lines
MAX_FRAME_BYTES, # default inbound frame cap (1 MiB)
JSONRPC_VERSION,
serialize_frame, build_response, build_error_response, build_notification,
)
Minimal example
import asyncio
from nexo_plugin_sdk import PluginAdapter, Event
MANIFEST = open("nexo-plugin.toml").read()
async def on_event(topic: str, event: Event, broker) -> None:
out = Event.new(
"plugin.inbound.my_kind",
"my_plugin",
{"echoed": event.payload},
)
await broker.publish("plugin.inbound.my_kind", out)
async def main() -> None:
adapter = PluginAdapter(manifest_toml=MANIFEST, on_event=on_event)
await adapter.run()
if __name__ == "__main__":
asyncio.run(main())
Host calls
The broker handle passed into on_event can call back into the host
— read the agent's long-term memory, or run an LLM completion via the
agent's configured providers:
async def on_event(topic, event, broker):
entries = await broker.memory_recall(agent_id="my_agent", query="user prefers concise answers", limit=5)
# entries: list[MemoryEntry] (id, agent_id, content, tags, concept_tags, created_at, memory_type)
result = await broker.llm_complete(
provider="minimax", model="minimax-m2.5",
messages=[{"role": "user", "content": "summarize: ..."}],
system_prompt="You answer concisely.",
)
# result.content, result.finish_reason, result.usage.{prompt_tokens, completion_tokens}
stream = broker.llm_complete_stream(provider="minimax", model="minimax-m2.5",
messages=[{"role": "user", "content": "..."}])
async for chunk in stream:
... # str chunks, in order
final = await stream.final_result() # LlmCompleteResult; final.content is None (chunks were the content)
Failures raise an RpcError: RpcServerError (.code — -32603 =
backend/not-configured, -32602 = bad params, -32601 = not wired
host-side), RpcTimeoutError (.seconds; default 30 s, override per
call with timeout=), RpcTransportError, RpcDecodeError. Concurrent
broker.event handlers can each have a host call in flight at once.
Robustness defaults
The PluginAdapter constructor defaults are picked to make the most
common plugin-author mistakes recoverable rather than fatal — matching
the TypeScript and PHP SDKs:
| Default | What it gives you |
|---|---|
enable_stdout_guard=True |
A stray print("hi") from your handler (or a chatty transitive dep) is diverted to stderr tagged [stdout-guard] rather than corrupting the JSON-RPC frame stream the host parses. The SDK's own replies and broker.publish frames write through the captured original stdout, so they bypass the guard. |
max_frame_bytes=1<<20 |
Inbound JSON-RPC frames larger than 1 MiB are rejected with a WireError log; dispatch continues. An adversarial host cannot OOM the plugin via a single huge line. |
handle_process_signals=True |
SIGTERM / SIGINT trigger a graceful shutdown — in-flight handler tasks are awaited (no mid-publish cancellation), then the process exits 0 instead of the default -15. Uses loop.add_signal_handler, falling back to signal.signal where that is unavailable (Windows ProactorEventLoop / non-main-thread). |
In-flight drain on shutdown |
Handlers spawned for broker.event are awaited via asyncio.gather(...) before the SDK replies {ok: true} to a host shutdown request. Same idiom as the TypeScript SDK's Promise.allSettled([...inflight]) and the PHP SDK's Scheduler::drain(). |
The stdin reader is fully async (loop.connect_read_pipe + an
asyncio.StreamReader) — no threadpool worker — so signal-driven
cancellation is clean.
Stdout guard limitation
The guard replaces sys.stdout with a line-buffering proxy, so it only
intercepts the text-stream API (print, sys.stdout.write). A C
extension or subprocess that writes to file descriptor 1 directly
bypasses it. Plugin authors who need stdout output should use
print() / sys.stdout.write(); those are guarded.
What the daemon expects
| Method | Direction | Reply |
|---|---|---|
initialize |
host → child | { manifest, server_version } automatically — the SDK reads + validates your manifest TOML at construction time (incl. the ^[a-z][a-z0-9_]{0,31}$ plugin.id slug regex the host enforces). |
broker.event (notification) |
host → child | No JSON reply. Your on_event handler runs in a detached task so the dispatch loop continues reading stdin while the handler awaits broker round-trips. |
shutdown |
host → child | { ok: true } after draining in-flight handler tasks + invoking your on_shutdown (if set). |
memory.recall / llm.complete (+ llm.complete.delta) |
child → host | Issued by broker.memory_recall / broker.llm_complete / broker.llm_complete_stream — the SDK assigns the request id, awaits the matching reply, and multiplexes concurrent calls. |
Full spec: nexo-plugin-contract.md.
Tests
cd python
PYTHONPATH=. python3 -m unittest discover -v tests/
31 tests covering: the handshake (initialize reply, unknown method
-32601, unknown notification ignored), manifest validation (missing
id, invalid TOML, id-regex violation), dispatch (handler invocation,
non-blocking reader, in-flight drain, oversized frame rejected with
continued dispatch), the stdout guard (idempotent install, install /
uninstall round-trip, divert vs passthrough, chunked print-style
writes, partial-line flush, attr delegation, handler-print diverted
while the blessed frame stays clean), the broker.publish back
channel, host calls (memory.recall / llm.complete happy paths,
streaming, -32603, per-call timeout, out-of-order multiplexing,
shutdown-while-in-flight, unknown-response-id dropped), and lifecycle
(double run() rejected, SIGTERM exits 0, SIGTERM drains an in-flight
handler before exiting).
Phase tracking
- 31.4 (shipped) — child-side SDK + 6 tests.
- 31.4.c (shipped, this package) — robustness parity with the
TypeScript/PHP SDKs: default-on stdout guard, 1 MiB inbound frame
cap, SIGTERM/SIGINT graceful drain, async stdin reader,
plugin.idregex validation, 21 tests,pyproject.tomlpublish-ready. - 31.4.b (deferred) — per-target Python tarballs (
pyXY-<triple>targets) for plugins that need native extensions. - 31.8 (shipped) — extracted to the
nexo-plugin-sdksmono-repo and published to PyPI asnexoai(thenexo-plugin-sdkname was taken). Taggedpython-vX.Y.Z. - 31.9 (shipped, 0.3.0) — child→host call surface:
broker.memory_recall/broker.llm_complete/broker.llm_complete_stream, theRpcErrorhierarchy, request multiplexing. Parity with the Rust child SDK. 31 tests.
Metadata
Release files for nexoai 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| nexoai-0.4.0.tar.gz | 36.4 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| nexoai-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 62.3 kB
Release files / nexoai-0.4.0.tar.gz
| Download URL | nexoai-0.4.0.tar.gz |
|---|---|
| Size | 36.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b6eb817af13a3ff9eb6b67c5d26c6d7c4c40c07def56eeae3ace0a78261706a5
|
|
BLAKE2b-256 checksum How to use checksums |
079f3b23246780a57a7bd0013df7643edda116a03557c336205256c7e060eccf
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on May 11, 2026.
Transparency logRelease files / nexoai-0.4.0-py3-none-any.whl
| Download URL | nexoai-0.4.0-py3-none-any.whl |
|---|---|
| Size | 26.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e463fe26adc1be29cc3d1acb3c53595ef6f2b53df3197660920ce1fe559b91fd
|
|
BLAKE2b-256 checksum How to use checksums |
b0aa98d46c8453d9564e7631c618ca0f0e58bbcbba5793cfa1ed250489ae11d5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on May 11, 2026.
Transparency logSecurity for nexoai 0.4.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for nexoai-0.4.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on May 11, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.