How the iii Engine's WebSocket Protocol Enables Cross-Language Function Invocation Between Python, TypeScript, and Rust

The iii engine uses a unified JSON-over-WebSocket protocol defined in engine/src/protocol.rs to route function calls between workers written in Python, TypeScript, and Rust, enabling seamless polyglot interoperability through language-agnostic message frames.

The iii engine (iii-hq/iii) implements a polyglot function-as-a-service architecture where workers in different languages communicate through a single, shared WebSocket protocol. By standardizing on JSON message frames defined by a central Message enum, the engine enables cross-language function invocation without language-specific serialization layers or adapters. This design allows a Python worker to execute functions registered by TypeScript, Rust services to trigger Node.js handlers, and any combination thereof—all through identical wire formats.

Protocol Architecture and the Message Enum

At the heart of the iii engine's cross-language capability lies a single source of truth defined in engine/src/protocol.rs. This file contains the Message enum, which specifies every frame type traversing the WebSocket connection: WorkerRegistered, RegisterFunction, InvokeFunction, InvocationResult, and InvocationError.

Because all three SDKs (Python, TypeScript/Node, and Rust) serialize and deserialize these identical JSON structures, the engine acts purely as a router. When a worker connects, it performs a handshake (WorkerRegistered), after which the engine can push InvokeFunction frames to any connected worker and receive InvocationResult frames in return. The payload remains untouched JSON as it travels between language runtimes.

Worker Registration and Handshake

Before cross-language invocation can occur, workers must register their exposed functions with the engine. The SDKs open a WebSocket connection to the engine port and send RegisterFunction messages for each capability they offer.

In engine/src/engine/mod.rs, the WebSocket server handles the upgrade from HTTP, tracks connected workers, and stores the mapping between function_id strings and their hosting workers. Each worker receives a unique worker_id upon successful registration.

Python registration (sdk/packages/python/iii/src/iii/iii.py):

from iii import register_worker

iii = register_worker("ws://localhost:49134")

@iii.register_function("hello::greet")
def greet(name: str) -> str:
    return f"Hello, {name}!"

TypeScript registration (sdk/packages/node/iii/src/iii.ts):

import { registerWorker } from "iii-sdk";

const iii = registerWorker("ws://localhost:49134");

iii.registerFunction("analytics::track", async (data) => {
  console.log("Processing event:", data);
  return { status: "recorded" };
});

Rust registration (sdk/packages/rust/iii/src/lib.rs):

use iii_sdk::{register_worker, RegisterFunction};

let iii = register_worker("ws://localhost:49134");

iii.register_function("compute::factorial", |n: u64| {
    (1..=n).product::<u64>()
}).await?;

Cross-Language Invocation Flows

Once functions are registered, the engine routes invocations based solely on the function_id, ignoring the implementation language. The protocol supports two invocation patterns defined in engine/src/protocol.rs.

Synchronous Invocation with Result Awaiting

For synchronous calls, the engine generates a unique invocation_id, sends an InvokeFunction frame to the target worker, and blocks until receiving a matching InvocationResult or InvocationError frame.

TypeScript invoking Python:

const result = await iii.invokeFunction({
  function_id: "hello::greet",
  data: "World"
});
// Returns: "Hello, World!"

The resulting JSON frame carries the original invocation_id, allowing the TypeScript SDK to correlate the response with the pending promise.

Fire-and-Forget Asynchronous Calls

For high-throughput scenarios, workers can invoke functions without awaiting responses by omitting the invocation_id field entirely.

Rust invoking a metrics handler (potentially Python or TypeScript):

iii.invoke_async(InvokeFunction {
    function_id: "metrics::track".into(),
    data: serde_json::json!({
        "event": "click",
        "label": "signup"
    }),
    // No invocation_id → fire-and-forget
}).await;

The engine forwards the message to the appropriate worker (registered in any language) and immediately returns without awaiting a response, maximizing throughput for telemetry and logging workloads.

Why Cross-Language Interoperability Works

The iii engine achieves seamless polyglot function calls through four architectural principles:

  • Single source of truth: The Message enum in engine/src/protocol.rs serves as the authoritative schema. Each SDK implements thin wrappers around this definition, ensuring all workers speak the same wire protocol.

  • JSON payloads: By standardizing on JSON for all data transfer, the engine avoids language-specific serialization formats. Primitive types, objects, arrays, and binary data (base64-encoded) travel unchanged between Python, TypeScript, and Rust runtimes.

  • Language-agnostic routing: The engine in engine/src/engine/mod.rs handles only connection management and frame routing. It inspects function_id and invocation_id fields but never deserializes the data payload, maintaining strict isolation between worker implementations.

  • Typed SDK abstractions: While the wire format is uniform, each SDK provides idiomatic interfaces—register_worker() in all three languages hide WebSocket management, frame serialization (serde_json in Rust, Python's json module, JavaScript's JSON.stringify), and promise handling.

Summary

  • The iii engine defines a unified Message enum in engine/src/protocol.rs that all SDKs use for WebSocket communication.
  • Workers register functions via RegisterFunction messages, and the engine stores these mappings in engine/src/engine/mod.rs.
  • Cross-language function invocation occurs when the engine routes InvokeFunction frames to workers based on function_id, regardless of implementation language.
  • Synchronous calls use invocation_id to correlate requests and responses, while invoke_async provides fire-and-forget semantics.
  • JSON payloads ensure data remains interpretable across Python, TypeScript, and Rust without translation layers.

Frequently Asked Questions

How does the iii engine route function calls between different programming languages?

The engine maintains a registry mapping function_id strings to active WebSocket connections. When a worker calls invokeFunction, the engine looks up the target function_id, retrieves the associated worker's connection from its internal state (managed in engine/src/engine/mod.rs), and forwards the JSON frame unchanged. Because the payload format is identical across all SDKs, the receiving worker deserializes the message using its native JSON library and executes the handler.

What happens if a cross-language function invocation fails?

When a registered handler throws an exception or returns an error, the target worker's SDK catches the failure and serializes an InvocationError frame containing the error message and stack trace. The engine routes this frame back to the original caller using the invocation_id. If the calling SDK is TypeScript, the promise rejects; if Python, it raises an exception; if Rust, it returns an Err variant—each SDK translates the protocol-level error into idiomatic language constructs.

Can a Python worker synchronously invoke a Rust function and receive complex data structures?

Yes. Synchronous invocation works identically regardless of language direction. A Python worker can call iii.invoke_function() targeting a Rust-registered function_id, passing JSON-serializable dictionaries. The Rust handler receives the data as serde_json::Value or a typed struct, processes it, and returns a serializable response. The iii engine routes the InvocationResult frame back to Python, where the SDK decodes the JSON into native Python objects (dicts, lists, primitives).

Does the iii WebSocket protocol support binary data transfer between languages?

Yes, though JSON is the primary format, binary data can be transferred by encoding it as base64 strings within the JSON payload. Since all three SDKs (Python's base64, JavaScript's Buffer, and Rust's base64 crates) handle this encoding consistently, workers can exchange images, files, or serialized protobuf messages across language boundaries while remaining compliant with the Message enum protocol defined in engine/src/protocol.rs.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →