A remote call starts with a real function object, not a string route.
from app import run
from agentix.provider.docker import DockerProvider, DockerProviderConfig

provider = DockerProvider(DockerProviderConfig())
async with provider.session(SandboxConfig(image=..., bundle=...)) as sandbox:
    result = await sandbox.remote(run, input="hello")
Agentix reads exactly two attributes from run:
target = f"{run.__module__}::{run.__qualname__}"
The runtime worker receives that target, imports the module inside the sandbox, resolves the function, unpickles the arguments, and calls it.

Why Function Objects

sandbox is a live handle returned by the provider — call sandbox.remote(fn, ...) on it directly. Passing the function object keeps the host and sandbox API aligned:
  • Your editor sees kwargs and return types from the local signature.
  • Refactors stay visible to Python tooling instead of hiding in RPC strings.
  • Agents, tools, and scorers all use the same call path.
The host rejects callables that cannot be encoded as an import path (lambdas, bound methods, partials, local closures). Put remote code behind an importable top-level function instead. To catch that at development time instead of at call time, validate a function with RemoteCallable.validate:
from agentix import RemoteCallable

RemoteCallable.validate(run)           # returns "app::run"
RemoteCallable.validate(lambda x: x)   # raises TypeError

Payload Shape

This call:
from app import run

result = await sandbox.remote(run, input="hello")
becomes this wire payload:
{
    "callable": "app::run",
    "arguments": pickle.dumps(((), {"input": "hello"})),
    "call_id": "uuid-hex",
}
sandbox.remote() generates a fresh call_id per call. The runtime uses it to correlate call:result / call:error responses and to support cancellation. The return value comes back as its own pickle blob. Because a sandbox may run less-trusted code, the host does not decode it with plain pickle.loads: it uses a restricted allowlist loader that admits only reviewed value types and refuses anything else with agentix.RestrictedUnpickleError. Returning a workload’s own custom class raises that error unless the class is explicitly allowed (see agentix.runtime.shared.safepickle); return plain data or the framework’s own result types and it just works. Remote calls use Socket.IO events on the /rpc namespace. The sandbox may use the internal HTTP /call fast path for short calls, but accepted long-running calls and replayed results still complete over /rpc.

Example

app.py
async def run(input: str) -> dict[str, str]:
    return {"output": input.upper()}
from app import run

result = await sandbox.remote(run, input="hello")
Both sync and async functions are valid targets. The worker awaits the return value only when it is awaitable.

Typing

sandbox.remote(fn, ...) returns the unwrapped result of fn — type R — whether fn is sync or async. remote is itself a coroutine you await, so an async def target does not surface as Awaitable[R] on the host:
def total(a: int, b: int) -> int: ...
async def fetch(url: str) -> bytes: ...

n: int = await sandbox.remote(total, 1, 2)        # R = int
data: bytes = await sandbox.remote(fetch, "...")  # R = bytes, not Awaitable[bytes]
Your editor sees fn’s real signature, so argument and return types are checked against the local definition.

Side Channels

Trace, log, and plugin traffic share the same Socket.IO connection but ride their own namespaces, separate from sandbox.remote():
NamespaceDirectionPurpose
/rpchost ↔ sandboxsandbox.remote()
/tracesandbox → hostspan lifecycle (auto-registered)
/logsandbox → hoststdlib logging records + captured stdout/stderr (auto-registered)
/<plugin>bothplugin-defined events via agentix.sio
Register a host-side handler before the first remote call:
async with provider.session(SandboxConfig(image=..., bundle=...)) as sandbox:
    sandbox.register_namespace(MyHostNamespace())
    await sandbox.remote(my_sandbox_fn, ...)

Errors and Cancellation

Agentix keeps errors in-band. A failed call emits call:error with a structured payload; the client raises RemoteCallError (or its subclass CallCancelled when error.cancelled=True — a terminal server-side state, distinct from local task cancellation). RemoteCallError’s message includes the sandbox-side traceback when one is available, and exc.error.traceback holds it on the exception for programmatic access. When the sandbox disconnects or the caller’s task is cancelled before a result arrives, the sandbox emits cancel for the in-flight call_id. Per-call timeouts are caller-owned — wrap await sandbox.remote(...) in asyncio.wait_for(...) when you need a deadline.