Agentix is built around a small execution loop:
Host Python
  RuntimeClient.remote(fn, ...)
        |
        v
Sandbox runtime server
  Socket.IO endpoint
        |
        v
Worker subprocess
  import module
  validate args
  call function
  encode result
Everything else supports that loop.

Systems

SystemResponsibility
agentix.runtime.sharedWire models, msgpack codec, framing, Socket.IO event names, call-shape helpers
agentix.runtime.clientRuntimeClient.remote(fn, ...) and transport-specific client behavior
agentix.runtime.serverFastAPI and Socket.IO server, worker process management, callable invocation, call correlation
agentix.deploymentHost-side sandbox lifecycle protocol and backend plugin lookup
agentix.cliagentix build

Remote Target

The caller passes a normal imported function:
from app import run

result = await client.remote(run, input="hello")
The client serializes the function object with stdlib pickle:
callable_payload = pickle.dumps(run)
The runtime worker unpickles the callable inside the sandbox, validates the provided arguments, and invokes it.

Call Flow

1. Host imports `app.run`.
2. Host calls `client.remote(run, input="hello")`.
3. Client inspects the signature and detects unary, stream, or bidi.
4. Client encodes args and kwargs with the shared codec.
5. Runtime server receives the call.
6. Server forwards the request to the worker subprocess.
7. Worker imports `app`, resolves `run`, validates args, and calls it.
8. Worker encodes the return value or stream item.
9. Client decodes the result into the function's annotated return type.

Transports

Call shapeFunction shapeTransportClient behavior
Unaryasync def f(...) -> T or sync def f(...) -> TSocket.IOawait client.remote(f, ...)
Streamasync def f(...) -> AsyncIterator[T]Socket.IOasync for item in client.remote(f, ...)
Bidiasync def f(..., inbox: Channel[I]) -> AsyncIterator[T]Socket.IOsend through Channel, receive with async for
Unary responses are msgpack RPC envelopes with either value or error. Stream and bidi calls emit item, end, and error events keyed by call_id.

Bundle Layout

agentix build [path] installs one Python project into the runtime venv:
/nix/runtime/bin/pip install --no-cache-dir /src/project
The install brings in user code, direct dependencies, transitive dependencies, and integration packages declared in pyproject.toml.
/nix/runtime/
├── bin/
│   ├── python
│   ├── pip
│   └── agentix-server
└── lib/python3.11/site-packages/
    ├── agentix/
    ├── agentix/bash/
    ├── agentix/swebench/
    └── app.py
If the project includes default.nix, the build adds a Nix builder stage and links binaries into /nix/runtime/bin.

Worker Model

The runtime server owns one worker subprocess. The worker handles remote function invocation and keeps the runtime server isolated from user code. For each target, the worker prepares and caches invocation metadata:
  • module import
  • function lookup
  • signature inspection
  • pydantic argument adapters
  • return-value serialization metadata
The worker uses the same /nix/runtime environment as the runtime server, so anything installed into the bundle can be imported on demand.

Deployment Boundary

Deployments are host-side plugins that start bundle images. They return a runtime_url; after that, the runtime protocol is the same regardless of where the sandbox is running.
Deployment.create(SandboxConfig(image="python:3.13-slim", bundle="my-rollout:0.1.0"))
        |
        v
Sandbox(runtime_url="http://...")
        |
        v
RuntimeClient(runtime_url)

Mental Model

Bundle = what code and dependencies exist in the sandbox
Remote call = which installed function to call
Worker = where user code executes
Deployment = where the bundle image runs