Skip to main content

positronic-wire

The client side of the transports a Positronic inference session runs over: websockets, gRPC, and a partner's own protocol. It carries the facts both ends of a wire share. It is one distribution, installable on its own. The core has no third-party dependencies; the grpc and websocket extras install grpcio and websockets independently.

Alpha, under rapid development. Names and behaviour change without notice, and nothing here is covered by a backwards-compatibility guarantee. Pin the exact version you tested against.

uv add "positronic-wire[websocket,grpc]==0.12.0"
uv add "positronic-wire[websocket,grpc] @ git+https://github.com/Positronic-Robotics/positronic@<tag or commit>#subdirectory=wire"

For an unpublished revision, use the Git installation pinned to a tag or commit.

positronic_wire never imports positronic or positronic_model_server. Both packages depend on it: the server transports in positronic_model_server import the shared wire definitions, and positronic.offboard.client.InferenceClient dials through the client wires.

What a wire is

A wire is one transport that carries a protocol's frames as opaque bytes. It has two ends. The client end dials a session and probes a server; the server end accepts sessions and refuses an unauthorized peer. Everything specific to a transport — the library, its exception types, its status codes, its metadata keys, the path a probe asks for, the URL scheme it writes — lives inside that transport's wire classes. Code outside a wire speaks to every transport through one interface and reads one answer. A wire over TLS is a member of its own, so no caller holds a secure flag, and nothing anywhere reads a transport off a URL scheme: a caller names the wire.

WebSocket and gRPC carry the session frames used by positronic_model_server and the legacy positronic.offboard server. Both servers use transports from positronic_model_server. roboarena carries a partner's own protocol. The partner serves it, and this package holds the client end alone.

The package boundary

Module Holds
positronic_wire.wire The routes (API_PATH, SESSION_PATH, KEEPALIVE_PATH), ALIVE_SECONDS, the key the keepalive answer carries, MAX_MESSAGE_BYTES, the addresses HostPortAddress(host, port, path, query) and UnixSocketAddress(uds, path, query) under the abstract SessionAddress, the type variable AddressT over them, netloc, bracket_ipv6(host), Refusal, ConnectRefused, PeerDisconnected, KeepaliveUnsupported, and the abstract ClientWire and ClientConnection
positronic_wire.websocket WebsocketClientWire, WebsocketTlsClientWire, WebsocketUnixClientWire, WebsocketClientConnection, refusal_of(raised), which reads a failed handshake as a Refusal, and connected_socket(host, port, timeout), a TCP connect whose timeout covers the name lookup and every address
positronic_wire.grpc GrpcClientWire, GrpcTlsClientWire, GrpcClientConnection, target(host, port), and the calls both ends agree on: SERVICE, METHOD, METHOD_PATH, KEEPALIVE_METHOD, KEEPALIVE_METHOD_PATH, PROBE_PATH, SESSION_PATH_HEADER, SESSION_QUERY_HEADER, MESSAGE_SIZE_OPTIONS, PING_EVERY_MS
positronic_wire.roboarena RoboarenaClientWire, RoboarenaClientConnection, RoboarenaAddress, and TextAnswer, which a text frame raises. The handshake carries the headers the caller gives, and none where it gives none
positronic_wire.registry CLIENT_WIRES, every installed member by its NAME, and client_wire(name)

positronic_model_server owns the server transports: server_wire.Wire, server_wire.ServerConnection, websocket_wire.WebsocketWire and grpc_wire.GrpcWire. It also owns ModelServer, shared message definitions and value serialization. The WebSocket server uses Uvicorn directly; the gRPC server uses grpcio's grpc.aio API. Each server transport has its own optional installation extra.

positronic.offboard owns InferenceClient, the legacy PolicyServer, and v1/v2 robot-command encoding. positronic-wire does not depend on either server package.

The client interface

A caller selects a wire by NAME: websocket, websocket_tls, websocket_unix, grpc, grpc_tls, roboarena. A wire dials an address of its ADDRESS type, and DEFAULT_PORT names the port a URL leaves out on the members that carry one.

  • session_url(address) — the session as this wire names it, for a log and for an error. The websocket members write ws:// or wss://, websocket_unix writes ws+unix://, and the gRPC members write host:port, and roboarena writes the root it dials. What a wire dials is its own: websocket, websocket_tls and roboarena dial this spelling, and the others dial a socket or a target instead.
  • dial(address, headers, open_timeout) — a client's end of one session. It raises ConnectRefused when the session does not open, whatever refused it. The refusal on the exception says what the caller does next: COLD (a backend still starting) and SILENT (nothing answered) retry, FORBIDDEN retries a few times, FINAL surfaces at once.
  • probe(address, headers, open_timeout) — whether a server answers at the address, without opening a session. It carries the same headers as dial, so an edge that authenticates on them lets the probe through to the server behind it: the probe wakes what a session would reach. None when a server answers; a Refusal says why none did, in the terms dial uses, FORBIDDEN among them for a credential the edge refused. The websocket wire asks the host's root for an upgrade, which the server refuses with 403 and nothing else answers 403 there. The gRPC wire calls PROBE_PATH, which a server that is up answers UNIMPLEMENTED. The roboarena wire opens the root and reads the frame the server announces itself with.
  • keepalive(address, headers, timeout) — resets the server's idle timer outside a session, and returns the seconds the server stays alive after the call, or None for a server with no idle timeout. timeout bounds the whole call. The websocket members send POST to KEEPALIVE_PATH, and the gRPC members call KEEPALIVE_METHOD_PATH. keepalive raises KeepaliveUnsupported where the server serves sessions but not the call, and ConnectRefused otherwise, in the terms probe uses: a 200 that does not carry the keepalive answer is FINAL. roboarena always raises KeepaliveUnsupported, and dials nothing.

registry.client_wire(name) selects an installed transport. CLIENT_WIRES includes only families whose libraries are installed; requesting an unavailable transport raises with installation guidance. Use [websocket] for WebSocket and Roboarena, [grpc] for gRPC, or [websocket,grpc] for all wires. Importing the core registry without either extra produces an empty registry. Broken imports from an installed transport surface as errors.

Each wire declares the address it dials, and takes no other. ClientWire.ADDRESS names that type, and every verb above takes it. websocket, websocket_tls, grpc and grpc_tls take a HostPortAddress — host, port, path (SESSION_PATH), query, as written. websocket_unix takes a UnixSocketAddress — uds, path, query. roboarena takes a RoboarenaAddress — host and port. A record that names an endpoint carries the wire's name and that wire's fields, never a URL, and no address carries a field a wire ignores.

uds is an absolute path to a Unix socket a server on the same machine bound, dialled instead of the network. The address refuses a relative path when it is built, because a relative one names a different socket to each caller. It names no host and no port, because a socket has neither: the handshake carries localhost as a stand-in the server never resolves. A socket is same-machine by construction, so no TLS member sits beside it. An absent path is SILENT: the client cannot tell a misspelt path from a socket nobody has bound yet, so it retries either to its deadline. A refusal from a socket a server is restarting on is SILENT too. A path holding something that is not a socket, and a refused permission, are FINAL, because no retry reaches them. A handshake that timed out or was reset reached the socket, so the server rather than the path was not ready, and it reads SILENT as it does on a port.

roboarena is a partner's own protocol: msgpack frames on a websocket at the bare root of a port the partner publishes. The server closes any other path, and it routes on a key inside each frame, so the address names no route and no query. It publishes no default port either, so every address states one. The server announces its configuration as the first frame of every connection: dial leaves that frame for the caller's codec, and probe reads it and closes. A server that upgrades the connection and announces nothing is a backend still starting, so it reads COLD. The server reports a failure in a text frame and serves nothing more on that connection, so recv and probe raise TextAnswer, which carries the text: a retry does not change it. The protocol names no URL scheme and the port is plain, so no TLS member sits beside this wire; a partner who terminates TLS in front of it refuses a handshake in the terms the websocket members already read.

Typing carries the split: a wire handed the other wire's address is a type error at the call site. registry.client_wire(name) answers by name and cannot, so InferenceClient checks ADDRESS once, before it dials, and names both in the refusal.

Whether a policy server is up

keepalive answers it, and probe answers it where a server serves no keepalive call. Each one's timeout is one wall-clock deadline over the name lookup, every address the name resolves to, and every read, so a server the caller did not write cannot hold the call past it. Both open their own socket, so they ignore a proxy the environment names, where dial goes through it.

What came back Means
keepalive returns a policy server admitted the headers
KeepaliveUnsupported, then probe returns None a server answers and serves no keepalive call
FORBIDDEN an answer that refused the credential
COLD an answer that asks for a retry: a 5xx, a 429
FINAL an answer no retry changes: any other status, a 200 without the keepalive answer, a host with no address
SILENT no answer: no connection, or none before the timeout or the close

COLD and SILENT mean the server is not up yet. roboarena raises KeepaliveUnsupported without a dial, and its probe raises TextAnswer where the server announces a failure. The gRPC members read SILENT off the details gRPC writes for a connect nothing answered, and off DEADLINE_EXCEEDED.

The libraries cap what a call reads: websockets reads at most 128 headers of 8 KiB and 1 MiB of a refused upgrade's body, and http.client at most 100 headers of 64 KiB. The websocket members read at most 16 KiB of a keepalive body, and the roboarena member one frame of at most MAX_MESSAGE_BYTES.

What each consumer pays

Consumer Needs Installs
A rig client, and positronic itself Every verb, the session protocol, the policy stack positronic, which pins positronic-wire exactly
A coordinator that probes an endpoint and warms it registry.client_wire, probe, PROBE_PATH, the routes positronic-wire[websocket,grpc], or just the required transport extra
A service that validates an endpoint record registry.CLIENT_WIRES positronic-wire with the transport extras it accepts
A host that runs a policy server and waits for it before it sends sessions keepalive, probe positronic-wire[websocket,grpc], or just the required transport extra
A native model server The shared server and its transports positronic-model-server[websocket,grpc], or just the required transport extra
A legacy policy server positronic.offboard.server.PolicyServer positronic

A consumer whose lockfile already carries grpcio (through a cloud SDK) and websockets (through uvicorn) adds no third-party package when it adds this one.

What the wire keeps out of its consumers

A transport-specific dial disappears from a consumer once it speaks to the wire. A consumer that opens a websockets.connect of its own to wake an endpoint has to skip every gRPC endpoint, because the same call cannot reach one; probe reaches both. A consumer that classifies a failed dial by matching library exception names over the raised type's bases has to list grpc.RpcError, websockets.* and httpx.*; dial raises one ConnectRefused for all of them, so isinstance answers.

A copied fact disappears too. The gRPC probe path, the session route and the keepalive route are each one symbol here, and the set of wires is registry.CLIENT_WIRES. A literal spelled a second time in another repository drifts the day either side edits it, and nothing reports the drift; an imported symbol cannot.

A scheme table disappears with them. A record that names an endpoint carries the wire's name and the fields that wire's address declares, so no reader of the record derives a transport, a TLS setting or a default port from the spelling of a URL.

The port a server serves a wire on, and the server flag that names it, stay literals where they are spelled: they are a deployment's configuration, not wire facts. A test in the consumer that installs positronic pins the flag name against the server's own signature.

Versioning and skew

positronic pins positronic-wire==<version> exactly, as it pins positronic-platform-client. utilities/check_workspace_version_bump.py refuses a change under wire/ that reuses the version, and one that leaves the root's pin behind. The release workflow publishes wire/ to the index before positronic, with skip-existing, so an unchanged wire is a no-op and a changed one is on the index before the release that depends on it.

A consumer outside this repository pins the wire the way it pins the client: by the index once the wire is published there, and until then by git subdirectory at a commit (positronic-wire @ git+…positronic@<sha>#subdirectory=wire). A consumer that also installs positronic at a commit names the same commit for both, so the wire the git install provides is the version positronic requires.

Two deployments of one consumer can hold different wire versions: a rig runs a pinned console on its own schedule while a coordinator runs its checkout. The wire keeps that skew readable instead of guarding it with a version floor. Each side reports the names its registry lists, and the side that hands an endpoint record across refuses a wire name the other side's registry lacks. A transport added to the wire is then one entry in registry.CLIENT_WIRES, and no consumer has to raise a constant naming the version that first dialled it.

Adopting the wire in a consumer

A consumer moves onto the wire in this order, each step green on its own:

  1. Depend on positronic-wire with the required transport extras. Import the routes, the probe path and the registry; delete the local copies, the tables keyed by scheme, and every read of a URL scheme. An endpoint record names its wire, then that wire's address — the fields ClientWire.ADDRESS declares, which differ per wire. Report the registry's names in the deploy handshake and retire any per-transport version floor.
  2. Replace the transport-specific dial with probe, and the exception-name match with isinstance(raised, (wire.ConnectRefused, wire.PeerDisconnected, TimeoutError)).
  3. Move the consumer's own transports into their own ClientWire subclasses, each declaring the address it dials, registered by name beside the wire's own.
  4. Deploy the pinned side at the commit that carries the wire, then the checkout side.

What is not shared

  • Server transports live in positronic-model-server and use Uvicorn or grpc.aio.
  • Model message definitions and value serialization live in positronic-model-server. The policy stack and robotics codecs live in positronic. Neither is a dependency of positronic-wire.
  • A transport this package does not implement. A consumer that owns one writes it as a ClientWire of its own.

Metadata

Release files for positronic-wire 0.12.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for positronic-wire 0.12.0
File Size Uploaded
positronic_wire-0.12.0.tar.gz 32.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for positronic-wire 0.12.0
File Interpreter ABI Platform
positronic_wire-0.12.0-py3-none-any.whl Python 3 none any Details

Total release size: 71.0 kB

Release files / positronic_wire-0.12.0.tar.gz

Download URL positronic_wire-0.12.0.tar.gz
Size 32.2 kB
Tags Source
SHA-256 checksum
How to use checksums
6a13a592931778e7be6f0dabaecbdf3f14af28450b78501e0b0e642476e7743f
BLAKE2b-256 checksum
How to use checksums
c17ca1a10ade5e1be7a56f6c6a0dcff2bc87a963222cf3f3b42797fff1382c4d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / positronic_wire-0.12.0-py3-none-any.whl

Download URL positronic_wire-0.12.0-py3-none-any.whl
Size 38.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a481aafd0b934f9f561960249443c11ea1917cc97cbab666f7d1098c8d8e364a
BLAKE2b-256 checksum
How to use checksums
bb7d8c9e38b5e4c1d578097fe91a4ada06123e5b0b17597b0ac82cc601eb83f3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release history Release notifications | RSS feed

This release

0.12.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page