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:
NamespaceSystemUser-facing APIExtension point
/rpcRPCawait sandbox.remote(fn, *args, **kwargs)expose a normal importable Python callable
/logloggingstdlib logging in sandbox codeconfigure host logging handlers, levels, and formatters
/tracetracingagentix.trace.trace(...), agentix.trace.span(...)register agentix.trace.Processor implementations
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:
# app.py, installed in the bundle
async def run(input: str) -> dict:
    return {"output": input.upper()}
The host calls the function object:
from app import run

result = await sandbox.remote(run, input="hello")
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:
import logging

logger = logging.getLogger(__name__)

async def run() -> None:
    logger.info("starting rollout")
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:
import logging
from agentix.utils.log import configure_logging

configure_logging(default_context="host")
logging.getLogger("my_eval").setLevel(logging.INFO)
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:
from agentix import trace

async def run() -> None:
    with trace.trace("eval"):
        with trace.span("agent.step", model="claude") as span:
            span.add_event("first_token")
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:
from agentix import trace

class CaptureProcessor(trace.Processor):
    def on_trace_start(self, t: trace.Trace) -> None:
        ...

    def on_trace_end(self, t: trace.Trace) -> None:
        ...

    def on_span_start(self, s: trace.Span) -> None:
        ...

    def on_span_end(self, s: trace.Span) -> None:
        ...

    def force_flush(self) -> None:
        ...

    def shutdown(self) -> None:
        ...

trace.add_processor(CaptureProcessor())
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:
import agentix

class MyService(agentix.Namespace):
    namespace = "/my-plugin"

    async def on_configure(self, payload):
        ...

agentix.register_namespace(MyService())
The host side receives events or replies to sandbox requests:
import agentix
from agentix.provider.docker import DockerProvider, DockerProviderConfig

class MyHost(agentix.AsyncClientNamespace):
    def __init__(self):
        super().__init__("/my-plugin")

    async def on_record(self, payload):
        ...

provider = DockerProvider(DockerProviderConfig(...))

async with provider.session(SandboxConfig(image=..., bundle=...)) as sandbox:
    sandbox.register_namespace(MyHost())
    result = await sandbox.remote(run, input="hello")
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:
from agentix import AsyncClientNamespace, request_handler

class MyHost(AsyncClientNamespace):
    def __init__(self):
        super().__init__("/my-plugin")

    @request_handler("fetch")
    async def on_fetch(self, body):
        return await call_upstream(body)   # return value → fetch:result
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.