positronic-wire 0.12.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- positronic_wire-0.12.0/.gitignore +167 -0
- positronic_wire-0.12.0/PKG-INFO +240 -0
- positronic_wire-0.12.0/README.md +228 -0
- positronic_wire-0.12.0/positronic_wire/grpc.py +309 -0
- positronic_wire-0.12.0/positronic_wire/py.typed +0 -0
- positronic_wire-0.12.0/positronic_wire/registry.py +37 -0
- positronic_wire-0.12.0/positronic_wire/roboarena.py +123 -0
- positronic_wire-0.12.0/positronic_wire/tests/test_grpc.py +288 -0
- positronic_wire-0.12.0/positronic_wire/tests/test_registry.py +72 -0
- positronic_wire-0.12.0/positronic_wire/tests/test_roboarena.py +259 -0
- positronic_wire-0.12.0/positronic_wire/tests/test_websocket.py +581 -0
- positronic_wire-0.12.0/positronic_wire/websocket.py +435 -0
- positronic_wire-0.12.0/positronic_wire/wire.py +188 -0
- positronic_wire-0.12.0/pyproject.toml +21 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py,cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
#Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# poetry
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
102
|
+
#poetry.lock
|
|
103
|
+
|
|
104
|
+
# pdm
|
|
105
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
106
|
+
#pdm.lock
|
|
107
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
108
|
+
# in version control.
|
|
109
|
+
# https://pdm.fming.dev/#use-with-ide
|
|
110
|
+
.pdm.toml
|
|
111
|
+
|
|
112
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
113
|
+
__pypackages__/
|
|
114
|
+
|
|
115
|
+
# Celery stuff
|
|
116
|
+
celerybeat-schedule
|
|
117
|
+
celerybeat.pid
|
|
118
|
+
|
|
119
|
+
# SageMath parsed files
|
|
120
|
+
*.sage.py
|
|
121
|
+
|
|
122
|
+
# Environments
|
|
123
|
+
.env
|
|
124
|
+
.venv
|
|
125
|
+
.venv-typecheck
|
|
126
|
+
env/
|
|
127
|
+
venv/
|
|
128
|
+
ENV/
|
|
129
|
+
env.bak/
|
|
130
|
+
venv.bak/
|
|
131
|
+
|
|
132
|
+
# Spyder project settings
|
|
133
|
+
.spyderproject
|
|
134
|
+
.spyproject
|
|
135
|
+
|
|
136
|
+
# Rope project settings
|
|
137
|
+
.ropeproject
|
|
138
|
+
|
|
139
|
+
# mkdocs documentation
|
|
140
|
+
/site
|
|
141
|
+
|
|
142
|
+
# mypy
|
|
143
|
+
.mypy_cache/
|
|
144
|
+
.dmypy.json
|
|
145
|
+
dmypy.json
|
|
146
|
+
|
|
147
|
+
# Pyre type checker
|
|
148
|
+
.pyre/
|
|
149
|
+
|
|
150
|
+
# pytype static type analyzer
|
|
151
|
+
.pytype/
|
|
152
|
+
|
|
153
|
+
# Cython debug symbols
|
|
154
|
+
cython_debug/
|
|
155
|
+
|
|
156
|
+
# PyCharm
|
|
157
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
158
|
+
# be added to the global gitignore or merged into this project gitignore. For a PyCharm
|
|
159
|
+
# project, it is generally recommended to exclude comment-based line additions.
|
|
160
|
+
.idea/
|
|
161
|
+
|
|
162
|
+
# VSCode
|
|
163
|
+
.vscode/
|
|
164
|
+
*.code-workspace
|
|
165
|
+
|
|
166
|
+
# Environment variables
|
|
167
|
+
.env*
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: positronic-wire
|
|
3
|
+
Version: 0.12.0
|
|
4
|
+
Summary: The client side of the transports a Positronic inference session runs over
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Provides-Extra: grpc
|
|
8
|
+
Requires-Dist: grpcio; extra == 'grpc'
|
|
9
|
+
Provides-Extra: websocket
|
|
10
|
+
Requires-Dist: websockets>=15.0.1; extra == 'websocket'
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# positronic-wire
|
|
14
|
+
|
|
15
|
+
The client side of the transports a Positronic inference session runs over: websockets, gRPC, and a
|
|
16
|
+
partner's own protocol. It carries the facts both ends of a wire share. It is one distribution,
|
|
17
|
+
installable on its own. The core has no third-party dependencies; the `grpc` and `websocket` extras
|
|
18
|
+
install `grpcio` and `websockets` independently.
|
|
19
|
+
|
|
20
|
+
> **Alpha, under rapid development.** Names and behaviour change without notice, and nothing here is
|
|
21
|
+
> covered by a backwards-compatibility guarantee. Pin the exact version you tested against.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
uv add "positronic-wire[websocket,grpc]==0.12.0"
|
|
25
|
+
uv add "positronic-wire[websocket,grpc] @ git+https://github.com/Positronic-Robotics/positronic@<tag or commit>#subdirectory=wire"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
For an unpublished revision, use the Git installation pinned to a tag or commit.
|
|
29
|
+
|
|
30
|
+
`positronic_wire` never imports `positronic` or `positronic_model_server`. Both packages depend on
|
|
31
|
+
it: the server transports in `positronic_model_server` import the shared wire definitions, and
|
|
32
|
+
`positronic.offboard.client.InferenceClient` dials through the client wires.
|
|
33
|
+
|
|
34
|
+
## What a wire is
|
|
35
|
+
|
|
36
|
+
A wire is one transport that carries a protocol's frames as opaque bytes. It has two ends. The
|
|
37
|
+
client end dials a session and probes a server; the server end accepts sessions and refuses an
|
|
38
|
+
unauthorized peer. Everything specific to a transport — the library, its exception types, its
|
|
39
|
+
status codes, its metadata keys, the path a probe asks for, the URL scheme it writes — lives inside
|
|
40
|
+
that transport's wire classes. Code outside a wire speaks to every transport through one interface
|
|
41
|
+
and reads one answer. A wire over TLS is a member of its own, so no caller holds a `secure` flag,
|
|
42
|
+
and nothing anywhere reads a transport off a URL scheme: a caller names the wire.
|
|
43
|
+
|
|
44
|
+
WebSocket and gRPC carry the session frames used by `positronic_model_server` and the legacy
|
|
45
|
+
`positronic.offboard` server. Both servers use transports from `positronic_model_server`.
|
|
46
|
+
`roboarena` carries a partner's own protocol. The partner serves it, and this package holds the
|
|
47
|
+
client end alone.
|
|
48
|
+
|
|
49
|
+
## The package boundary
|
|
50
|
+
|
|
51
|
+
| Module | Holds |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `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` |
|
|
54
|
+
| `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 |
|
|
55
|
+
| `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` |
|
|
56
|
+
| `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 |
|
|
57
|
+
| `positronic_wire.registry` | `CLIENT_WIRES`, every installed member by its `NAME`, and `client_wire(name)` |
|
|
58
|
+
|
|
59
|
+
[`positronic_model_server`](../model_server/README.md) owns the server transports:
|
|
60
|
+
`server_wire.Wire`, `server_wire.ServerConnection`, `websocket_wire.WebsocketWire` and
|
|
61
|
+
`grpc_wire.GrpcWire`. It also owns `ModelServer`, shared message definitions and value serialization.
|
|
62
|
+
The WebSocket server uses Uvicorn directly; the gRPC server uses `grpcio`'s `grpc.aio` API.
|
|
63
|
+
Each server transport has its own optional installation extra.
|
|
64
|
+
|
|
65
|
+
`positronic.offboard` owns `InferenceClient`, the legacy `PolicyServer`, and v1/v2 robot-command
|
|
66
|
+
encoding. `positronic-wire` does not depend on either server package.
|
|
67
|
+
|
|
68
|
+
## The client interface
|
|
69
|
+
|
|
70
|
+
A caller selects a wire by `NAME`: `websocket`, `websocket_tls`, `websocket_unix`, `grpc`, `grpc_tls`,
|
|
71
|
+
`roboarena`. A wire dials an address of its `ADDRESS` type, and `DEFAULT_PORT` names the port a URL
|
|
72
|
+
leaves out on the members that carry one.
|
|
73
|
+
|
|
74
|
+
- `session_url(address)` — the session as this wire names it, for a log and for an error. The
|
|
75
|
+
websocket members write `ws://` or `wss://`, `websocket_unix` writes `ws+unix://`, and the gRPC
|
|
76
|
+
members write `host:port`, and `roboarena` writes the root it dials. What a wire dials is its own:
|
|
77
|
+
`websocket`, `websocket_tls` and `roboarena` dial this spelling, and the others dial a socket or a
|
|
78
|
+
target instead.
|
|
79
|
+
- `dial(address, headers, open_timeout)` — a client's end of one session. It raises
|
|
80
|
+
`ConnectRefused` when the session does not open, whatever refused it. The `refusal` on the
|
|
81
|
+
exception says what the caller does next: `COLD` (a backend still starting) and `SILENT`
|
|
82
|
+
(nothing answered) retry, `FORBIDDEN` retries a few times, `FINAL` surfaces at once.
|
|
83
|
+
- `probe(address, headers, open_timeout)` — whether a server answers at the address, without opening
|
|
84
|
+
a session. It carries the same `headers` as `dial`, so an edge that authenticates on them lets the
|
|
85
|
+
probe through to the server behind it: the probe wakes what a session would reach. `None` when a
|
|
86
|
+
server answers; a `Refusal` says why none did, in the terms `dial` uses, `FORBIDDEN` among them for
|
|
87
|
+
a credential the edge refused. The websocket wire asks the host's root for an upgrade, which the
|
|
88
|
+
server refuses with 403 and nothing else answers 403 there. The gRPC wire calls `PROBE_PATH`,
|
|
89
|
+
which a server that is up answers `UNIMPLEMENTED`. The roboarena wire opens the root and reads the
|
|
90
|
+
frame the server announces itself with.
|
|
91
|
+
- `keepalive(address, headers, timeout)` — resets the server's idle timer outside a session, and
|
|
92
|
+
returns the seconds the server stays alive after the call, or `None` for a server with no idle
|
|
93
|
+
timeout. `timeout` bounds the whole call. The websocket members send `POST` to `KEEPALIVE_PATH`, and
|
|
94
|
+
the gRPC members call `KEEPALIVE_METHOD_PATH`. `keepalive` raises
|
|
95
|
+
`KeepaliveUnsupported` where the server serves sessions but not the call, and `ConnectRefused`
|
|
96
|
+
otherwise, in the terms `probe` uses: a 200 that does not carry the keepalive answer is `FINAL`.
|
|
97
|
+
`roboarena` always raises `KeepaliveUnsupported`, and dials nothing.
|
|
98
|
+
|
|
99
|
+
`registry.client_wire(name)` selects an installed transport. `CLIENT_WIRES` includes only families
|
|
100
|
+
whose libraries are installed; requesting an unavailable transport raises with installation guidance.
|
|
101
|
+
Use `[websocket]` for WebSocket and Roboarena, `[grpc]` for gRPC, or `[websocket,grpc]` for all wires.
|
|
102
|
+
Importing the core registry without either extra produces an empty registry. Broken imports from an
|
|
103
|
+
installed transport surface as errors.
|
|
104
|
+
|
|
105
|
+
**Each wire declares the address it dials, and takes no other.** `ClientWire.ADDRESS` names that
|
|
106
|
+
type, and every verb above takes it. `websocket`, `websocket_tls`, `grpc` and `grpc_tls` take a
|
|
107
|
+
`HostPortAddress` — `host`, `port`, `path` (`SESSION_PATH`), `query`, as written.
|
|
108
|
+
`websocket_unix` takes a `UnixSocketAddress` — `uds`, `path`, `query`. `roboarena` takes a
|
|
109
|
+
`RoboarenaAddress` — `host` and `port`. A record that names an endpoint carries the wire's name and
|
|
110
|
+
that wire's fields, never a URL, and no address carries a field a wire ignores.
|
|
111
|
+
|
|
112
|
+
`uds` is an absolute path to a Unix socket a server on the same machine bound, dialled instead of
|
|
113
|
+
the network. The address refuses a relative path when it is built, because a relative one names a
|
|
114
|
+
different socket to each caller. It names no host and no port, because a socket has neither: the
|
|
115
|
+
handshake carries `localhost` as a stand-in the server never resolves. A socket is same-machine by
|
|
116
|
+
construction, so no TLS member sits beside it. An absent path is `SILENT`: the client cannot tell a
|
|
117
|
+
misspelt path from a socket nobody has bound yet, so it retries either to its deadline. A refusal
|
|
118
|
+
from a socket a server is restarting on is `SILENT` too. A path holding something that is not a
|
|
119
|
+
socket, and a refused permission, are `FINAL`, because no retry reaches them. A handshake that timed
|
|
120
|
+
out or was reset reached the socket, so the server rather than the path was not ready, and it reads
|
|
121
|
+
`SILENT` as it does on a port.
|
|
122
|
+
|
|
123
|
+
`roboarena` is a partner's own protocol: msgpack frames on a websocket at the bare root of a port the
|
|
124
|
+
partner publishes. The server closes any other path, and it routes on a key inside each frame, so the
|
|
125
|
+
address names no route and no query. It publishes no default port either, so every address states one.
|
|
126
|
+
The server announces its configuration as the first frame of every connection: `dial` leaves that frame
|
|
127
|
+
for the caller's codec, and `probe` reads it and closes. A server that upgrades the connection and
|
|
128
|
+
announces nothing is a backend still starting, so it reads `COLD`. The server reports a failure in a
|
|
129
|
+
text frame and serves nothing more on that connection, so `recv` and `probe` raise `TextAnswer`, which
|
|
130
|
+
carries the text: a retry does not change it. The protocol names no URL scheme and the port is plain,
|
|
131
|
+
so no TLS member sits beside this wire; a partner who terminates TLS in front of it refuses a handshake
|
|
132
|
+
in the terms the websocket members already read.
|
|
133
|
+
|
|
134
|
+
Typing carries the split: a wire handed the other wire's address is a type error at the call site.
|
|
135
|
+
`registry.client_wire(name)` answers by name and cannot, so `InferenceClient` checks `ADDRESS` once,
|
|
136
|
+
before it dials, and names both in the refusal.
|
|
137
|
+
|
|
138
|
+
## Whether a policy server is up
|
|
139
|
+
|
|
140
|
+
`keepalive` answers it, and `probe` answers it where a server serves no keepalive call. Each one's
|
|
141
|
+
timeout is one wall-clock deadline over the name lookup, every address the name resolves to, and every
|
|
142
|
+
read, so a server the caller did not write cannot hold the call past it. Both open their own socket, so
|
|
143
|
+
they ignore a proxy the environment names, where `dial` goes through it.
|
|
144
|
+
|
|
145
|
+
| What came back | Means |
|
|
146
|
+
|---|---|
|
|
147
|
+
| `keepalive` returns | a policy server admitted the headers |
|
|
148
|
+
| `KeepaliveUnsupported`, then `probe` returns `None` | a server answers and serves no keepalive call |
|
|
149
|
+
| `FORBIDDEN` | an answer that refused the credential |
|
|
150
|
+
| `COLD` | an answer that asks for a retry: a 5xx, a 429 |
|
|
151
|
+
| `FINAL` | an answer no retry changes: any other status, a 200 without the keepalive answer, a host with no address |
|
|
152
|
+
| `SILENT` | no answer: no connection, or none before the timeout or the close |
|
|
153
|
+
|
|
154
|
+
`COLD` and `SILENT` mean the server is not up yet. `roboarena` raises `KeepaliveUnsupported` without a
|
|
155
|
+
dial, and its `probe` raises `TextAnswer` where the server announces a failure. The gRPC members read
|
|
156
|
+
`SILENT` off the details gRPC writes for a connect nothing answered, and off `DEADLINE_EXCEEDED`.
|
|
157
|
+
|
|
158
|
+
The libraries cap what a call reads: `websockets` reads at most 128 headers of 8 KiB and 1 MiB of a
|
|
159
|
+
refused upgrade's body, and `http.client` at most 100 headers of 64 KiB. The websocket members read at
|
|
160
|
+
most 16 KiB of a keepalive body, and the roboarena member one frame of at most `MAX_MESSAGE_BYTES`.
|
|
161
|
+
|
|
162
|
+
## What each consumer pays
|
|
163
|
+
|
|
164
|
+
| Consumer | Needs | Installs |
|
|
165
|
+
|---|---|---|
|
|
166
|
+
| A rig client, and `positronic` itself | Every verb, the session protocol, the policy stack | `positronic`, which pins `positronic-wire` exactly |
|
|
167
|
+
| 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 |
|
|
168
|
+
| A service that validates an endpoint record | `registry.CLIENT_WIRES` | `positronic-wire` with the transport extras it accepts |
|
|
169
|
+
| 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 |
|
|
170
|
+
| A native model server | The shared server and its transports | `positronic-model-server[websocket,grpc]`, or just the required transport extra |
|
|
171
|
+
| A legacy policy server | `positronic.offboard.server.PolicyServer` | `positronic` |
|
|
172
|
+
|
|
173
|
+
A consumer whose lockfile already carries `grpcio` (through a cloud SDK) and `websockets` (through
|
|
174
|
+
`uvicorn`) adds no third-party package when it adds this one.
|
|
175
|
+
|
|
176
|
+
## What the wire keeps out of its consumers
|
|
177
|
+
|
|
178
|
+
A transport-specific dial disappears from a consumer once it speaks to the wire. A consumer that
|
|
179
|
+
opens a `websockets.connect` of its own to wake an endpoint has to skip every gRPC endpoint, because
|
|
180
|
+
the same call cannot reach one; `probe` reaches both. A consumer that classifies a failed dial by
|
|
181
|
+
matching library exception names over the raised type's bases has to list `grpc.RpcError`,
|
|
182
|
+
`websockets.*` and `httpx.*`; `dial` raises one `ConnectRefused` for all of them, so `isinstance`
|
|
183
|
+
answers.
|
|
184
|
+
|
|
185
|
+
A copied fact disappears too. The gRPC probe path, the session route and the keepalive route are each
|
|
186
|
+
one symbol here, and the set of wires is `registry.CLIENT_WIRES`. A literal spelled a second time
|
|
187
|
+
in another repository drifts the day either side edits it, and nothing reports the drift; an
|
|
188
|
+
imported symbol cannot.
|
|
189
|
+
|
|
190
|
+
A scheme table disappears with them. A record that names an endpoint carries the wire's name and the
|
|
191
|
+
fields that wire's address declares, so no reader of the record derives a transport, a TLS setting or
|
|
192
|
+
a default port from the spelling of a URL.
|
|
193
|
+
|
|
194
|
+
The port a server serves a wire on, and the server flag that names it, stay literals where they are
|
|
195
|
+
spelled: they are a deployment's configuration, not wire facts. A test in the consumer that installs
|
|
196
|
+
`positronic` pins the flag name against the server's own signature.
|
|
197
|
+
|
|
198
|
+
## Versioning and skew
|
|
199
|
+
|
|
200
|
+
`positronic` pins `positronic-wire==<version>` exactly, as it pins `positronic-platform-client`.
|
|
201
|
+
`utilities/check_workspace_version_bump.py` refuses a change under `wire/` that reuses the version,
|
|
202
|
+
and one that leaves the root's pin behind. The release workflow publishes `wire/` to the index
|
|
203
|
+
before `positronic`, with `skip-existing`, so an unchanged wire is a no-op and a changed one is on
|
|
204
|
+
the index before the release that depends on it.
|
|
205
|
+
|
|
206
|
+
A consumer outside this repository pins the wire the way it pins the client: by the index once the
|
|
207
|
+
wire is published there, and until then by git subdirectory at a commit
|
|
208
|
+
(`positronic-wire @ git+…positronic@<sha>#subdirectory=wire`). A consumer that also installs
|
|
209
|
+
`positronic` at a commit names the same commit for both, so the wire the git install provides is
|
|
210
|
+
the version `positronic` requires.
|
|
211
|
+
|
|
212
|
+
Two deployments of one consumer can hold different wire versions: a rig runs a pinned console on
|
|
213
|
+
its own schedule while a coordinator runs its checkout. The wire keeps that skew readable instead
|
|
214
|
+
of guarding it with a version floor. Each side reports the names its registry lists, and the side
|
|
215
|
+
that hands an endpoint record across refuses a wire name the other side's registry lacks. A
|
|
216
|
+
transport added to the wire is then one entry in `registry.CLIENT_WIRES`, and no consumer has to
|
|
217
|
+
raise a constant naming the version that first dialled it.
|
|
218
|
+
|
|
219
|
+
## Adopting the wire in a consumer
|
|
220
|
+
|
|
221
|
+
A consumer moves onto the wire in this order, each step green on its own:
|
|
222
|
+
|
|
223
|
+
1. Depend on `positronic-wire` with the required transport extras. Import the routes, the probe path and the registry; delete the
|
|
224
|
+
local copies, the tables keyed by scheme, and every read of a URL scheme. **An endpoint record
|
|
225
|
+
names its wire, then that wire's address** — the fields `ClientWire.ADDRESS` declares, which differ
|
|
226
|
+
per wire. Report the registry's names in the deploy handshake and retire any per-transport version
|
|
227
|
+
floor.
|
|
228
|
+
2. Replace the transport-specific dial with `probe`, and the exception-name match with
|
|
229
|
+
`isinstance(raised, (wire.ConnectRefused, wire.PeerDisconnected, TimeoutError))`.
|
|
230
|
+
3. Move the consumer's own transports into their own `ClientWire` subclasses, each declaring the
|
|
231
|
+
address it dials, registered by name beside the wire's own.
|
|
232
|
+
4. Deploy the pinned side at the commit that carries the wire, then the checkout side.
|
|
233
|
+
|
|
234
|
+
## What is not shared
|
|
235
|
+
|
|
236
|
+
- Server transports live in `positronic-model-server` and use Uvicorn or `grpc.aio`.
|
|
237
|
+
- Model message definitions and value serialization live in `positronic-model-server`. The policy
|
|
238
|
+
stack and robotics codecs live in `positronic`. Neither is a dependency of `positronic-wire`.
|
|
239
|
+
- A transport this package does not implement. A consumer that owns one writes it as a `ClientWire`
|
|
240
|
+
of its own.
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# positronic-wire
|
|
2
|
+
|
|
3
|
+
The client side of the transports a Positronic inference session runs over: websockets, gRPC, and a
|
|
4
|
+
partner's own protocol. It carries the facts both ends of a wire share. It is one distribution,
|
|
5
|
+
installable on its own. The core has no third-party dependencies; the `grpc` and `websocket` extras
|
|
6
|
+
install `grpcio` and `websockets` independently.
|
|
7
|
+
|
|
8
|
+
> **Alpha, under rapid development.** Names and behaviour change without notice, and nothing here is
|
|
9
|
+
> covered by a backwards-compatibility guarantee. Pin the exact version you tested against.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
uv add "positronic-wire[websocket,grpc]==0.12.0"
|
|
13
|
+
uv add "positronic-wire[websocket,grpc] @ git+https://github.com/Positronic-Robotics/positronic@<tag or commit>#subdirectory=wire"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
For an unpublished revision, use the Git installation pinned to a tag or commit.
|
|
17
|
+
|
|
18
|
+
`positronic_wire` never imports `positronic` or `positronic_model_server`. Both packages depend on
|
|
19
|
+
it: the server transports in `positronic_model_server` import the shared wire definitions, and
|
|
20
|
+
`positronic.offboard.client.InferenceClient` dials through the client wires.
|
|
21
|
+
|
|
22
|
+
## What a wire is
|
|
23
|
+
|
|
24
|
+
A wire is one transport that carries a protocol's frames as opaque bytes. It has two ends. The
|
|
25
|
+
client end dials a session and probes a server; the server end accepts sessions and refuses an
|
|
26
|
+
unauthorized peer. Everything specific to a transport — the library, its exception types, its
|
|
27
|
+
status codes, its metadata keys, the path a probe asks for, the URL scheme it writes — lives inside
|
|
28
|
+
that transport's wire classes. Code outside a wire speaks to every transport through one interface
|
|
29
|
+
and reads one answer. A wire over TLS is a member of its own, so no caller holds a `secure` flag,
|
|
30
|
+
and nothing anywhere reads a transport off a URL scheme: a caller names the wire.
|
|
31
|
+
|
|
32
|
+
WebSocket and gRPC carry the session frames used by `positronic_model_server` and the legacy
|
|
33
|
+
`positronic.offboard` server. Both servers use transports from `positronic_model_server`.
|
|
34
|
+
`roboarena` carries a partner's own protocol. The partner serves it, and this package holds the
|
|
35
|
+
client end alone.
|
|
36
|
+
|
|
37
|
+
## The package boundary
|
|
38
|
+
|
|
39
|
+
| Module | Holds |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `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` |
|
|
42
|
+
| `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 |
|
|
43
|
+
| `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` |
|
|
44
|
+
| `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 |
|
|
45
|
+
| `positronic_wire.registry` | `CLIENT_WIRES`, every installed member by its `NAME`, and `client_wire(name)` |
|
|
46
|
+
|
|
47
|
+
[`positronic_model_server`](../model_server/README.md) owns the server transports:
|
|
48
|
+
`server_wire.Wire`, `server_wire.ServerConnection`, `websocket_wire.WebsocketWire` and
|
|
49
|
+
`grpc_wire.GrpcWire`. It also owns `ModelServer`, shared message definitions and value serialization.
|
|
50
|
+
The WebSocket server uses Uvicorn directly; the gRPC server uses `grpcio`'s `grpc.aio` API.
|
|
51
|
+
Each server transport has its own optional installation extra.
|
|
52
|
+
|
|
53
|
+
`positronic.offboard` owns `InferenceClient`, the legacy `PolicyServer`, and v1/v2 robot-command
|
|
54
|
+
encoding. `positronic-wire` does not depend on either server package.
|
|
55
|
+
|
|
56
|
+
## The client interface
|
|
57
|
+
|
|
58
|
+
A caller selects a wire by `NAME`: `websocket`, `websocket_tls`, `websocket_unix`, `grpc`, `grpc_tls`,
|
|
59
|
+
`roboarena`. A wire dials an address of its `ADDRESS` type, and `DEFAULT_PORT` names the port a URL
|
|
60
|
+
leaves out on the members that carry one.
|
|
61
|
+
|
|
62
|
+
- `session_url(address)` — the session as this wire names it, for a log and for an error. The
|
|
63
|
+
websocket members write `ws://` or `wss://`, `websocket_unix` writes `ws+unix://`, and the gRPC
|
|
64
|
+
members write `host:port`, and `roboarena` writes the root it dials. What a wire dials is its own:
|
|
65
|
+
`websocket`, `websocket_tls` and `roboarena` dial this spelling, and the others dial a socket or a
|
|
66
|
+
target instead.
|
|
67
|
+
- `dial(address, headers, open_timeout)` — a client's end of one session. It raises
|
|
68
|
+
`ConnectRefused` when the session does not open, whatever refused it. The `refusal` on the
|
|
69
|
+
exception says what the caller does next: `COLD` (a backend still starting) and `SILENT`
|
|
70
|
+
(nothing answered) retry, `FORBIDDEN` retries a few times, `FINAL` surfaces at once.
|
|
71
|
+
- `probe(address, headers, open_timeout)` — whether a server answers at the address, without opening
|
|
72
|
+
a session. It carries the same `headers` as `dial`, so an edge that authenticates on them lets the
|
|
73
|
+
probe through to the server behind it: the probe wakes what a session would reach. `None` when a
|
|
74
|
+
server answers; a `Refusal` says why none did, in the terms `dial` uses, `FORBIDDEN` among them for
|
|
75
|
+
a credential the edge refused. The websocket wire asks the host's root for an upgrade, which the
|
|
76
|
+
server refuses with 403 and nothing else answers 403 there. The gRPC wire calls `PROBE_PATH`,
|
|
77
|
+
which a server that is up answers `UNIMPLEMENTED`. The roboarena wire opens the root and reads the
|
|
78
|
+
frame the server announces itself with.
|
|
79
|
+
- `keepalive(address, headers, timeout)` — resets the server's idle timer outside a session, and
|
|
80
|
+
returns the seconds the server stays alive after the call, or `None` for a server with no idle
|
|
81
|
+
timeout. `timeout` bounds the whole call. The websocket members send `POST` to `KEEPALIVE_PATH`, and
|
|
82
|
+
the gRPC members call `KEEPALIVE_METHOD_PATH`. `keepalive` raises
|
|
83
|
+
`KeepaliveUnsupported` where the server serves sessions but not the call, and `ConnectRefused`
|
|
84
|
+
otherwise, in the terms `probe` uses: a 200 that does not carry the keepalive answer is `FINAL`.
|
|
85
|
+
`roboarena` always raises `KeepaliveUnsupported`, and dials nothing.
|
|
86
|
+
|
|
87
|
+
`registry.client_wire(name)` selects an installed transport. `CLIENT_WIRES` includes only families
|
|
88
|
+
whose libraries are installed; requesting an unavailable transport raises with installation guidance.
|
|
89
|
+
Use `[websocket]` for WebSocket and Roboarena, `[grpc]` for gRPC, or `[websocket,grpc]` for all wires.
|
|
90
|
+
Importing the core registry without either extra produces an empty registry. Broken imports from an
|
|
91
|
+
installed transport surface as errors.
|
|
92
|
+
|
|
93
|
+
**Each wire declares the address it dials, and takes no other.** `ClientWire.ADDRESS` names that
|
|
94
|
+
type, and every verb above takes it. `websocket`, `websocket_tls`, `grpc` and `grpc_tls` take a
|
|
95
|
+
`HostPortAddress` — `host`, `port`, `path` (`SESSION_PATH`), `query`, as written.
|
|
96
|
+
`websocket_unix` takes a `UnixSocketAddress` — `uds`, `path`, `query`. `roboarena` takes a
|
|
97
|
+
`RoboarenaAddress` — `host` and `port`. A record that names an endpoint carries the wire's name and
|
|
98
|
+
that wire's fields, never a URL, and no address carries a field a wire ignores.
|
|
99
|
+
|
|
100
|
+
`uds` is an absolute path to a Unix socket a server on the same machine bound, dialled instead of
|
|
101
|
+
the network. The address refuses a relative path when it is built, because a relative one names a
|
|
102
|
+
different socket to each caller. It names no host and no port, because a socket has neither: the
|
|
103
|
+
handshake carries `localhost` as a stand-in the server never resolves. A socket is same-machine by
|
|
104
|
+
construction, so no TLS member sits beside it. An absent path is `SILENT`: the client cannot tell a
|
|
105
|
+
misspelt path from a socket nobody has bound yet, so it retries either to its deadline. A refusal
|
|
106
|
+
from a socket a server is restarting on is `SILENT` too. A path holding something that is not a
|
|
107
|
+
socket, and a refused permission, are `FINAL`, because no retry reaches them. A handshake that timed
|
|
108
|
+
out or was reset reached the socket, so the server rather than the path was not ready, and it reads
|
|
109
|
+
`SILENT` as it does on a port.
|
|
110
|
+
|
|
111
|
+
`roboarena` is a partner's own protocol: msgpack frames on a websocket at the bare root of a port the
|
|
112
|
+
partner publishes. The server closes any other path, and it routes on a key inside each frame, so the
|
|
113
|
+
address names no route and no query. It publishes no default port either, so every address states one.
|
|
114
|
+
The server announces its configuration as the first frame of every connection: `dial` leaves that frame
|
|
115
|
+
for the caller's codec, and `probe` reads it and closes. A server that upgrades the connection and
|
|
116
|
+
announces nothing is a backend still starting, so it reads `COLD`. The server reports a failure in a
|
|
117
|
+
text frame and serves nothing more on that connection, so `recv` and `probe` raise `TextAnswer`, which
|
|
118
|
+
carries the text: a retry does not change it. The protocol names no URL scheme and the port is plain,
|
|
119
|
+
so no TLS member sits beside this wire; a partner who terminates TLS in front of it refuses a handshake
|
|
120
|
+
in the terms the websocket members already read.
|
|
121
|
+
|
|
122
|
+
Typing carries the split: a wire handed the other wire's address is a type error at the call site.
|
|
123
|
+
`registry.client_wire(name)` answers by name and cannot, so `InferenceClient` checks `ADDRESS` once,
|
|
124
|
+
before it dials, and names both in the refusal.
|
|
125
|
+
|
|
126
|
+
## Whether a policy server is up
|
|
127
|
+
|
|
128
|
+
`keepalive` answers it, and `probe` answers it where a server serves no keepalive call. Each one's
|
|
129
|
+
timeout is one wall-clock deadline over the name lookup, every address the name resolves to, and every
|
|
130
|
+
read, so a server the caller did not write cannot hold the call past it. Both open their own socket, so
|
|
131
|
+
they ignore a proxy the environment names, where `dial` goes through it.
|
|
132
|
+
|
|
133
|
+
| What came back | Means |
|
|
134
|
+
|---|---|
|
|
135
|
+
| `keepalive` returns | a policy server admitted the headers |
|
|
136
|
+
| `KeepaliveUnsupported`, then `probe` returns `None` | a server answers and serves no keepalive call |
|
|
137
|
+
| `FORBIDDEN` | an answer that refused the credential |
|
|
138
|
+
| `COLD` | an answer that asks for a retry: a 5xx, a 429 |
|
|
139
|
+
| `FINAL` | an answer no retry changes: any other status, a 200 without the keepalive answer, a host with no address |
|
|
140
|
+
| `SILENT` | no answer: no connection, or none before the timeout or the close |
|
|
141
|
+
|
|
142
|
+
`COLD` and `SILENT` mean the server is not up yet. `roboarena` raises `KeepaliveUnsupported` without a
|
|
143
|
+
dial, and its `probe` raises `TextAnswer` where the server announces a failure. The gRPC members read
|
|
144
|
+
`SILENT` off the details gRPC writes for a connect nothing answered, and off `DEADLINE_EXCEEDED`.
|
|
145
|
+
|
|
146
|
+
The libraries cap what a call reads: `websockets` reads at most 128 headers of 8 KiB and 1 MiB of a
|
|
147
|
+
refused upgrade's body, and `http.client` at most 100 headers of 64 KiB. The websocket members read at
|
|
148
|
+
most 16 KiB of a keepalive body, and the roboarena member one frame of at most `MAX_MESSAGE_BYTES`.
|
|
149
|
+
|
|
150
|
+
## What each consumer pays
|
|
151
|
+
|
|
152
|
+
| Consumer | Needs | Installs |
|
|
153
|
+
|---|---|---|
|
|
154
|
+
| A rig client, and `positronic` itself | Every verb, the session protocol, the policy stack | `positronic`, which pins `positronic-wire` exactly |
|
|
155
|
+
| 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 |
|
|
156
|
+
| A service that validates an endpoint record | `registry.CLIENT_WIRES` | `positronic-wire` with the transport extras it accepts |
|
|
157
|
+
| 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 |
|
|
158
|
+
| A native model server | The shared server and its transports | `positronic-model-server[websocket,grpc]`, or just the required transport extra |
|
|
159
|
+
| A legacy policy server | `positronic.offboard.server.PolicyServer` | `positronic` |
|
|
160
|
+
|
|
161
|
+
A consumer whose lockfile already carries `grpcio` (through a cloud SDK) and `websockets` (through
|
|
162
|
+
`uvicorn`) adds no third-party package when it adds this one.
|
|
163
|
+
|
|
164
|
+
## What the wire keeps out of its consumers
|
|
165
|
+
|
|
166
|
+
A transport-specific dial disappears from a consumer once it speaks to the wire. A consumer that
|
|
167
|
+
opens a `websockets.connect` of its own to wake an endpoint has to skip every gRPC endpoint, because
|
|
168
|
+
the same call cannot reach one; `probe` reaches both. A consumer that classifies a failed dial by
|
|
169
|
+
matching library exception names over the raised type's bases has to list `grpc.RpcError`,
|
|
170
|
+
`websockets.*` and `httpx.*`; `dial` raises one `ConnectRefused` for all of them, so `isinstance`
|
|
171
|
+
answers.
|
|
172
|
+
|
|
173
|
+
A copied fact disappears too. The gRPC probe path, the session route and the keepalive route are each
|
|
174
|
+
one symbol here, and the set of wires is `registry.CLIENT_WIRES`. A literal spelled a second time
|
|
175
|
+
in another repository drifts the day either side edits it, and nothing reports the drift; an
|
|
176
|
+
imported symbol cannot.
|
|
177
|
+
|
|
178
|
+
A scheme table disappears with them. A record that names an endpoint carries the wire's name and the
|
|
179
|
+
fields that wire's address declares, so no reader of the record derives a transport, a TLS setting or
|
|
180
|
+
a default port from the spelling of a URL.
|
|
181
|
+
|
|
182
|
+
The port a server serves a wire on, and the server flag that names it, stay literals where they are
|
|
183
|
+
spelled: they are a deployment's configuration, not wire facts. A test in the consumer that installs
|
|
184
|
+
`positronic` pins the flag name against the server's own signature.
|
|
185
|
+
|
|
186
|
+
## Versioning and skew
|
|
187
|
+
|
|
188
|
+
`positronic` pins `positronic-wire==<version>` exactly, as it pins `positronic-platform-client`.
|
|
189
|
+
`utilities/check_workspace_version_bump.py` refuses a change under `wire/` that reuses the version,
|
|
190
|
+
and one that leaves the root's pin behind. The release workflow publishes `wire/` to the index
|
|
191
|
+
before `positronic`, with `skip-existing`, so an unchanged wire is a no-op and a changed one is on
|
|
192
|
+
the index before the release that depends on it.
|
|
193
|
+
|
|
194
|
+
A consumer outside this repository pins the wire the way it pins the client: by the index once the
|
|
195
|
+
wire is published there, and until then by git subdirectory at a commit
|
|
196
|
+
(`positronic-wire @ git+…positronic@<sha>#subdirectory=wire`). A consumer that also installs
|
|
197
|
+
`positronic` at a commit names the same commit for both, so the wire the git install provides is
|
|
198
|
+
the version `positronic` requires.
|
|
199
|
+
|
|
200
|
+
Two deployments of one consumer can hold different wire versions: a rig runs a pinned console on
|
|
201
|
+
its own schedule while a coordinator runs its checkout. The wire keeps that skew readable instead
|
|
202
|
+
of guarding it with a version floor. Each side reports the names its registry lists, and the side
|
|
203
|
+
that hands an endpoint record across refuses a wire name the other side's registry lacks. A
|
|
204
|
+
transport added to the wire is then one entry in `registry.CLIENT_WIRES`, and no consumer has to
|
|
205
|
+
raise a constant naming the version that first dialled it.
|
|
206
|
+
|
|
207
|
+
## Adopting the wire in a consumer
|
|
208
|
+
|
|
209
|
+
A consumer moves onto the wire in this order, each step green on its own:
|
|
210
|
+
|
|
211
|
+
1. Depend on `positronic-wire` with the required transport extras. Import the routes, the probe path and the registry; delete the
|
|
212
|
+
local copies, the tables keyed by scheme, and every read of a URL scheme. **An endpoint record
|
|
213
|
+
names its wire, then that wire's address** — the fields `ClientWire.ADDRESS` declares, which differ
|
|
214
|
+
per wire. Report the registry's names in the deploy handshake and retire any per-transport version
|
|
215
|
+
floor.
|
|
216
|
+
2. Replace the transport-specific dial with `probe`, and the exception-name match with
|
|
217
|
+
`isinstance(raised, (wire.ConnectRefused, wire.PeerDisconnected, TimeoutError))`.
|
|
218
|
+
3. Move the consumer's own transports into their own `ClientWire` subclasses, each declaring the
|
|
219
|
+
address it dials, registered by name beside the wire's own.
|
|
220
|
+
4. Deploy the pinned side at the commit that carries the wire, then the checkout side.
|
|
221
|
+
|
|
222
|
+
## What is not shared
|
|
223
|
+
|
|
224
|
+
- Server transports live in `positronic-model-server` and use Uvicorn or `grpc.aio`.
|
|
225
|
+
- Model message definitions and value serialization live in `positronic-model-server`. The policy
|
|
226
|
+
stack and robotics codecs live in `positronic`. Neither is a dependency of `positronic-wire`.
|
|
227
|
+
- A transport this package does not implement. A consumer that owns one writes it as a `ClientWire`
|
|
228
|
+
of its own.
|