A remote call starts with a real function object, not a string route.
from app import run

result = await client.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

Passing the function object keeps the host and sandbox API aligned:
  • Your editor sees kwargs and return types from the local signature.
  • The worker validates against the sandbox-installed signature before execution.
  • Refactors stay visible to Python tooling instead of hiding in RPC strings.
  • Agents, tools, and scorers all use the same call path.

Payload Shape

This call:
from app import run

result = await client.remote(run, input="hello")
becomes this wire payload:
{
    "callable": "app::run",
    "arguments": "pickle.dumps(((), {'input': 'hello'}))",
    "call_id": "optional-correlation-key",
}
Unary, streaming, and bidirectional calls use Socket.IO events on the same runtime server. HTTP is only used for /health.

Call Shapes

The function signature determines how the client behaves.
Function shapeClient usageTransport
async def f(...) -> Tawait client.remote(f, ...)Socket.IO unary
async def f(...) -> AsyncIterator[T]async for item in client.remote(f, ...)Socket.IO stream
async def f(..., inbox: Channel[I]) -> AsyncIterator[T]send inputs through Channel, receive with async forSocket.IO bidi
Sync functions are valid for unary calls. Streams and bidi calls require async generators.

Unary Example

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

result = await client.remote(run, input="hello")

Streaming Example

app.py
from collections.abc import AsyncIterator


async def count(limit: int) -> AsyncIterator[int]:
    for value in range(limit):
        yield value
from app import count

async for value in client.remote(count, limit=3):
    print(value)

Bidirectional Example

app.py
from collections.abc import AsyncIterator
from agentix import Channel


async def echo(inbox: Channel[str]) -> AsyncIterator[str]:
    async for message in inbox:
        yield message
from agentix import Channel
from app import echo

inbox = Channel[str]()
conversation = client.remote(echo, inbox)

await inbox.send("hello")
await inbox.close()

async for message in conversation:
    print(message)

Correlation

Attach a call_id when rollout traces need a stable key:
with client.call_context(call_id="rollout-42.step-7"):
    result = await client.remote(run, input="hello")
The runtime forwards the call_id in the request payload. Agentix keeps errors in-band: unary requests return an RPC response with either a value or an error, and stream or bidi calls emit an error event.