Skip to main content

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 (the nexo-plugin-sdk name was already taken); the importable module is nexo_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.id regex validation, 21 tests, pyproject.toml publish-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-sdks mono-repo and published to PyPI as nexoai (the nexo-plugin-sdk name was taken). Tagged python-vX.Y.Z.
  • 31.9 (shipped, 0.3.0) — child→host call surface: broker.memory_recall / broker.llm_complete / broker.llm_complete_stream, the RpcError hierarchy, 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)

Source distribution for nexoai 0.4.0
File Size Uploaded Provenance
nexoai-0.4.0.tar.gz 36.4 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for nexoai 0.4.0
File Interpreter ABI Platform
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 log

Release 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 log

Security 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.

Select from 2 files

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 log

A 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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page