Agentix plugins extend the runtime in two different ways:
  1. expose importable Python functions that callers run with sandbox.remote
  2. open side-channel Socket.IO namespaces for events between host and sandbox
The runtime server stays generic. It does not know plugin event names or payload schemas; it only forwards namespace traffic between the host client and the worker process. Agentix Runtime extension model

The Three Core Systems

Agentix core owns three reserved Socket.IO namespaces: Plugins must not claim these namespaces. A plugin that needs its own event protocol uses its own namespace, conventionally /<package-name>.

RPC: Extend With Callables

The /rpc namespace is the remote-call protocol. Users extend it by installing functions into the bundle, not by subclassing a base class:
The host calls the function object:
The sandbox serializes the target as fn.__module__ + "::" + fn.__qualname__, pickles args and kwargs, and the worker imports the same callable inside the sandbox.

Logging: Extend With stdlib logging

The /log namespace is the logging bridge. Sandbox code uses standard Python logging:
At worker boot, Agentix installs a root logging.Handler that forwards LogRecord data over /log. The sandbox automatically registers the host consumer and replays those records into the host logging tree. Beyond stdlib records, the worker also captures the process’s raw stdout and stderr — print(), subprocess output, C-extension writes — and replays each line over the same /log namespace under the logger names agentix.sandbox.stdout and agentix.sandbox.stderr. The identical output is also appended to a durable $AGENTIX_LOG_DIR/sandbox-<worker-id>.log (default /tmp/agentix; set the variable empty to disable), so output that outlives a dropped connection is still recoverable inside the sandbox. Users customize logging with normal logging configuration on the host:
Do not register your own /log namespace. If a plugin needs structured events that are not log records, give the plugin its own namespace.

Tracing: Extend With Processors

The /trace namespace is the trace bridge. User code creates traces and spans:
At worker boot, Agentix installs an internal trace.Processor that forwards trace and span lifecycle events over /trace. The sandbox automatically registers the host consumer, which replays those events into the host trace provider. Users extend tracing by registering processors:
A processor registered in the host process receives host-local spans and spans replayed from sandbox /trace events. A processor registered inside sandbox code receives sandbox-local spans. Do not register your own /trace namespace. If a plugin needs a different event model, use a plugin namespace and optionally convert those events into spans through a trace.Processor.

Choosing: Callables vs. Namespaces

Two ways to expose sandbox behavior — pick by data-flow shape:
  • Callable (sandbox.remote(fn, ...)) — one call in, one return value out, after the work finishes. No data flows back mid-execution. This is the default; bash run, file upload/download, and agent/scorer wrappers are all callables.
  • Callable returning AsyncIterator[T] — progressive results streamed to the host as they happen, host → sandbox only. bash.run_stream yields BashStdout / BashStderr / BashExit events this way.
  • Namespace — host and sandbox exchange data during execution (bidirectional), or the plugin bridges an external service the sandbox must reach through the host. abridge uses a namespace: sandbox code forwards an LLM request to the host, the host calls the real upstream, and the reply comes back on the same namespace — all while the agent keeps running in the sandbox.
Rule of thumb: reach for a callable first. Use a namespace only when you need a persistent, bidirectional channel; it costs an extra class on each side.

Plugin Namespaces

A plugin namespace has two optional halves:
  • a sandbox-side agentix.Namespace, registered inside the worker
  • a host-side agentix.AsyncClientNamespace, registered on the live sandbox
The sandbox side emits events or handles host requests:
The host side receives events or replies to sandbox requests:
sandbox.register_namespace(...) must run before the first remote call, because the Socket.IO connection plan is fixed at connect time.

Round-trip Requests

When the sandbox needs an answer from the host mid-run, the sandbox side calls await self.request("fetch", body). It emits the request with a generated request_id and waits for the host to reply on fetch:result (success) or fetch:error (failure). On the host, wrap the handler with request_handler so the envelope, the reply event name, and error replies are handled for you:
The wrapped coroutine receives the unwrapped request body and returns the reply value; a raised exception becomes a fetch:error reply carrying {"type", "message"} — the shape Namespace.request re-raises as RemoteSioError. Without the decorator you must pull request_id out of the envelope yourself and emit the exact fetch:result / fetch:error event by hand; a typo or a forgotten reply hangs the sandbox until its request timeout.

What the Server Does

The runtime server is namespace-opaque: When the worker registers a namespace, it sends sio_open to the server. The server then registers a matching Socket.IO forwarder. This is why plugins do not need runtime-server patches for new event protocols.

Packaging a Plugin

A plugin package can contribute any combination of:
  • importable functions that callers invoke with sandbox.remote
  • sandbox-side namespaces registered by code running in the worker
  • host-side namespace handlers registered on the live sandbox
  • sandbox providers through the agentix.provider entry-point group
  • system dependency closures through the agentix.nix entry-point group
Keep these boundaries separate. Remote calls are the main RPC system; /log and /trace are core side channels; plugin-specific protocols belong on the plugin’s own namespace.