positronic-model-server 0.2.2__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.
@@ -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,214 @@
1
+ Metadata-Version: 2.5
2
+ Name: positronic-model-server
3
+ Version: 0.2.2
4
+ Summary: Model serving, session lifecycle and serialization without Positronic dependencies
5
+ License-Expression: Apache-2.0
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: msgpack>=1
8
+ Requires-Dist: numpy>=1.26
9
+ Requires-Dist: pillow>=10
10
+ Requires-Dist: positronic-wire==0.12.0
11
+ Provides-Extra: grpc
12
+ Requires-Dist: positronic-wire[grpc]==0.12.0; extra == 'grpc'
13
+ Provides-Extra: websocket
14
+ Requires-Dist: positronic-wire[websocket]==0.12.0; extra == 'websocket'
15
+ Requires-Dist: uvicorn>=0.35; extra == 'websocket'
16
+ Description-Content-Type: text/markdown
17
+
18
+ # positronic-model-server
19
+
20
+ Serve a native model inside its own Python environment, without Positronic, Torch or JAX dependencies.
21
+ The wrapper owns transport, sessions, serialization and cleanup. Vendor scripts load and call their
22
+ model directly in the same process and describe the client pipeline as plain data.
23
+
24
+ ```bash
25
+ uv pip install "positronic-model-server==0.2.2"
26
+ uv pip install "positronic-model-server[websocket]==0.2.2"
27
+ uv pip install "positronic-model-server[grpc]==0.2.2"
28
+ ```
29
+
30
+ The core depends on NumPy, Pillow, msgpack and `positronic-wire`'s dependency-free transport
31
+ definitions. The extras select transport libraries independently; combine them as
32
+ `[websocket,grpc]` when both are needed. WebSocket serving uses Uvicorn directly; gRPC does not
33
+ require it. Starlette, AnyIO, FastAPI and Pydantic are not dependencies of either installation.
34
+ In a checkout, install the workspace packages together: `uv pip install ./wire ./model_server`.
35
+
36
+ ## Serving and lifetime
37
+
38
+ This test server returns each observation as a one-element result list:
39
+
40
+ ```python
41
+ from positronic_model_server.server import Model, ModelServer, Session
42
+ from positronic_model_server.server_wire import ServedHostPort
43
+ from positronic_model_server.spec import component
44
+ from positronic_model_server.websocket_wire import WebsocketWire
45
+
46
+ class EchoSession(Session):
47
+ def __init__(self, fps):
48
+ super().__init__(component("chunked_schedule", version=2, fps=fps))
49
+
50
+ def infer(self, observation):
51
+ return [observation]
52
+
53
+
54
+ class EchoModel(Model):
55
+ def __init__(self):
56
+ super().__init__(parameters={"fps": 20}, metadata={"checkpoint_id": "example"})
57
+
58
+ def prepare_session(self, params):
59
+ if params["fps"] <= 0:
60
+ raise ValueError("fps must be positive")
61
+ return EchoSession(params["fps"])
62
+
63
+
64
+ server = ModelServer(EchoModel, idle_timeout_min=10)
65
+ server.serve([WebsocketWire(ServedHostPort("0.0.0.0", 8000))])
66
+ ```
67
+
68
+ Implement `Model.prepare_session` and `Session.infer` in subclasses. Each `Session` owns its state
69
+ and may reference the loaded model. Override `close()` to release owned resources; its default does
70
+ nothing. The constructors accept the client description, parameters, metadata and output image settings.
71
+
72
+ Each vendor supplies its own script and argument parsing. The factory passed to `ModelServer` can be
73
+ a model class or a function. It loads weights and finishes shared warm-up before returning.
74
+ Listeners bind afterwards, so any successful keepalive answer means shared startup has finished.
75
+ The factory owns cleanup if loading raises.
76
+
77
+ `Model.parameters` declares accepted session names and defaults. Query values are decoded as JSON,
78
+ or retained as strings when they are not JSON. Duplicate or unknown names are refused; strings
79
+ never name Python imports. Every session gets an independent copy of the resolved parameters.
80
+ `prepare_session` decides how they affect inference and the client description, validates their
81
+ meaning and completes configuration-specific warm-up. It returns a `Session` only when the first
82
+ real inference needs no deferred loading or compilation. Warm-up must preserve the intended initial
83
+ session state. A preparation that raises owns cleanup of anything it acquired.
84
+
85
+ The server sends `waiting` messages during preparation, including while another session holds the
86
+ model. It then sends `ready` with a session ID, protocol v3, the pipeline description and metadata:
87
+
88
+ - `session_params`: the received query parameters, after decoding;
89
+ - `effective_params`: those parameters with declared defaults applied;
90
+ - `model_server_version`: this distribution's version;
91
+ - model/session metadata supplied by the vendor, plus the listener address.
92
+
93
+ The client may report any additional metadata and owns how it records or combines that information.
94
+ The server checks description structure without importing client components.
95
+
96
+ All model operations run on one worker thread in the server process: load, preparation, inference,
97
+ session cleanup and model cleanup. Each session retains its own settings and history.
98
+ Inference is serialized across all listeners. Queue time is reported separately from model
99
+ time. The event loop answers keepalive while the worker is busy; no waiting messages interrupt an
100
+ inference response. A failed inference reports an error and keeps the session open. A malformed
101
+ session request ends that session.
102
+
103
+ Clients own recovery from reported inference errors. The recommended client behavior is to close
104
+ the affected session and, if retrying, open a fresh session and wait for `ready`. Continuing in the
105
+ same session is an explicit client choice for failures it knows are recoverable. An open session
106
+ does not guarantee that a failed inference left the model's state valid.
107
+
108
+ `Session.close` runs after outstanding work, on explicit close, disconnect or server shutdown.
109
+ The end-session acknowledgement follows cleanup. `Model.close` runs after sessions finish and
110
+ listeners stop, including when startup fails after loading. Call `server.shutdown()` from any thread;
111
+ an idle timeout ends the server only when no sessions remain. A model operation must return for
112
+ graceful shutdown to complete.
113
+
114
+ Model-specific implementations own cleanup and its failure policy. Cleanup methods are expected
115
+ to release their resources and complete without raising. If `Session.close` raises, the wrapper logs
116
+ the failure, attempts to report it to the client and ends the connection; it continues serving other
117
+ sessions. A cleanup failure can leak resources or leave shared model state invalid. The implementation
118
+ is responsible for recovery or for requesting `server.shutdown()` when continued serving is unsafe.
119
+
120
+ Pass `auth_token` to require a bearer token on both session and keepalive calls. `None` serves open;
121
+ empty or malformed tokens are refused at construction. For gRPC, use `grpc_wire.GrpcWire` with its
122
+ own `ServedHostPort`. Both listeners can be passed to one server and share its loaded model.
123
+
124
+ ## Protocol compatibility
125
+
126
+ The shared server sends v3 results without interpreting robot-command fields or reshaping chunks.
127
+ Updated Positronic clients select this behavior while preserving v1/v2 command decoding for legacy
128
+ servers. A client that only supports v1/v2 rejects v3 during opening. Upgrade clients before migrating
129
+ their endpoint. Legacy servers and their server-side codecs remain supported in `positronic.offboard`.
130
+
131
+ ## Client pipeline descriptions
132
+
133
+ Vendor code describes components as JSON-compatible data. Only the Positronic client imports
134
+ and instantiates their implementations:
135
+
136
+ ```python
137
+ from positronic_model_server.spec import component, parallel, sequence
138
+
139
+ description = sequence(
140
+ component("chunked_schedule", version=2, fps=20),
141
+ parallel(
142
+ component("restrict_image_size", width=320, height=180),
143
+ component("metadata", values={"model": "example"}),
144
+ ),
145
+ )
146
+ ```
147
+
148
+ `component` accepts a name, explicit version and keyword arguments, normalizing arguments through
149
+ JSON. `sequence` and `parallel` reject empty compositions. Component names and argument meanings
150
+ belong to the client registry; these helpers do not resolve classes or validate model parameters.
151
+ The structural keys (`NAME`, `VERSION`, `ARGS`, `SEQ`, `PAR`) have one definition in `spec`.
152
+
153
+ The wrapper defines only constants used by its own production code. Vendor settings belong to small
154
+ vendor modules that training and serving can both import; dataset file conventions belong to the
155
+ Positronic exporters and their producers. Client component
156
+ names belong to the client implementations; vendor descriptions spell those names as part of the
157
+ client's wire contract. Validate each description against the real client registry in vendor tests.
158
+ The wrapper forwards model metadata and parameter values without owning their field names.
159
+
160
+ ## Serialization
161
+
162
+ ```python
163
+ from positronic_model_server.serialization import deserialise, encode_jpeg, serialise
164
+
165
+ message = serialise({"camera": encode_jpeg(image, quality=90), "state": state})
166
+ restored = deserialise(message)
167
+ ```
168
+
169
+ Plain values, nested mappings/sequences, NumPy arrays and NumPy scalars use the shared msgpack
170
+ representation. Ordinary arrays are lossless, including arrays whose dimensions resemble images.
171
+ JPEG compression is explicit: call `encode_jpeg` on the values to compress. It accepts RGB arrays
172
+ shaped `(..., H, W, 3)`, casts pixels to uint8 and preserves every leading dimension. It is lossy.
173
+ The receiver recognizes each encoded value without a field schema. The same functions encode
174
+ observations or model results, including returned images.
175
+
176
+ `serialization.JpegEncoding(path, quality)` selects a nested mapping/list value, with an empty path
177
+ selecting the whole result. Supply these selectors through `Session.output_images` for returned
178
+ images. `serialization.encode_images` applies the same selections to observations without changing
179
+ the source containers. A missing path fails instead of silently skipping compression.
180
+
181
+ The client component `encode_images` defaults to recursively JPEG-encoding `uint8` arrays shaped
182
+ `(..., H, W, 3)` with positive height and width, including batches, inside mappings, lists and tuples.
183
+ Supply `paths` to encode only selected values; `paths=[]` disables compression. `quality` sets the
184
+ JPEG quality. Automatic detection is a heuristic: float images are left lossless, and unrelated
185
+ `uint8` arrays matching the image shape are compressed. Its result decoding leaves native values
186
+ unchanged. The wire serializer itself does not infer image fields.
187
+
188
+ Nonempty 3D and 4D images retain the v1/v2 JPEG representation. Extra leading dimensions and empty
189
+ batches carry a full-shape marker; send those only to receivers supporting it. NumPy object,
190
+ structured and complex dtypes are unsupported. Python tuples arrive as lists.
191
+
192
+ `protocol` owns shared message fields, authentication, timing and protocol version names.
193
+ Serialization here never interprets robot-command fields or `__cmd__` envelopes. Positronic's
194
+ legacy offboard protocol owns those hooks and its supported-version selection.
195
+
196
+ ## Package checks
197
+
198
+ Run `uv run pytest model_server positronic/offboard/tests positronic/policy/tests wire` from the
199
+ repository root. CI also builds wheels and installs them in isolated environments with NumPy
200
+ 1.26.4 on Python 3.11 and 3.12, selecting no transport, WebSocket alone, or gRPC alone. Root tests
201
+ exercise the repository's locked NumPy version.
202
+
203
+ Runtime dependencies are a design constraint. Before installing test tools, CI checks every
204
+ installed distribution against the per-transport allowlist in
205
+ `utilities/check_model_server_package.py`. Any additional runtime package fails the check,
206
+ whether direct or indirect, including dependencies introduced by upstream package updates.
207
+ Transport dependencies must stay optional. Changes to the allowlist require explicit design
208
+ review; do not expand it merely to make CI pass. The dependency check uses only the standard library.
209
+
210
+ The same check scans every wrapper Python module for module-level constants with no internal use.
211
+ Adding one fails CI, including in a new module. Test/example references and imports or assignments
212
+ that only re-export a constant do not count. Move such definitions to their owning vendor, client or
213
+ legacy protocol module; do not add artificial uses to satisfy the check. Source references establish
214
+ use, while review checks that the behavior itself belongs to the wrapper.
@@ -0,0 +1,197 @@
1
+ # positronic-model-server
2
+
3
+ Serve a native model inside its own Python environment, without Positronic, Torch or JAX dependencies.
4
+ The wrapper owns transport, sessions, serialization and cleanup. Vendor scripts load and call their
5
+ model directly in the same process and describe the client pipeline as plain data.
6
+
7
+ ```bash
8
+ uv pip install "positronic-model-server==0.2.2"
9
+ uv pip install "positronic-model-server[websocket]==0.2.2"
10
+ uv pip install "positronic-model-server[grpc]==0.2.2"
11
+ ```
12
+
13
+ The core depends on NumPy, Pillow, msgpack and `positronic-wire`'s dependency-free transport
14
+ definitions. The extras select transport libraries independently; combine them as
15
+ `[websocket,grpc]` when both are needed. WebSocket serving uses Uvicorn directly; gRPC does not
16
+ require it. Starlette, AnyIO, FastAPI and Pydantic are not dependencies of either installation.
17
+ In a checkout, install the workspace packages together: `uv pip install ./wire ./model_server`.
18
+
19
+ ## Serving and lifetime
20
+
21
+ This test server returns each observation as a one-element result list:
22
+
23
+ ```python
24
+ from positronic_model_server.server import Model, ModelServer, Session
25
+ from positronic_model_server.server_wire import ServedHostPort
26
+ from positronic_model_server.spec import component
27
+ from positronic_model_server.websocket_wire import WebsocketWire
28
+
29
+ class EchoSession(Session):
30
+ def __init__(self, fps):
31
+ super().__init__(component("chunked_schedule", version=2, fps=fps))
32
+
33
+ def infer(self, observation):
34
+ return [observation]
35
+
36
+
37
+ class EchoModel(Model):
38
+ def __init__(self):
39
+ super().__init__(parameters={"fps": 20}, metadata={"checkpoint_id": "example"})
40
+
41
+ def prepare_session(self, params):
42
+ if params["fps"] <= 0:
43
+ raise ValueError("fps must be positive")
44
+ return EchoSession(params["fps"])
45
+
46
+
47
+ server = ModelServer(EchoModel, idle_timeout_min=10)
48
+ server.serve([WebsocketWire(ServedHostPort("0.0.0.0", 8000))])
49
+ ```
50
+
51
+ Implement `Model.prepare_session` and `Session.infer` in subclasses. Each `Session` owns its state
52
+ and may reference the loaded model. Override `close()` to release owned resources; its default does
53
+ nothing. The constructors accept the client description, parameters, metadata and output image settings.
54
+
55
+ Each vendor supplies its own script and argument parsing. The factory passed to `ModelServer` can be
56
+ a model class or a function. It loads weights and finishes shared warm-up before returning.
57
+ Listeners bind afterwards, so any successful keepalive answer means shared startup has finished.
58
+ The factory owns cleanup if loading raises.
59
+
60
+ `Model.parameters` declares accepted session names and defaults. Query values are decoded as JSON,
61
+ or retained as strings when they are not JSON. Duplicate or unknown names are refused; strings
62
+ never name Python imports. Every session gets an independent copy of the resolved parameters.
63
+ `prepare_session` decides how they affect inference and the client description, validates their
64
+ meaning and completes configuration-specific warm-up. It returns a `Session` only when the first
65
+ real inference needs no deferred loading or compilation. Warm-up must preserve the intended initial
66
+ session state. A preparation that raises owns cleanup of anything it acquired.
67
+
68
+ The server sends `waiting` messages during preparation, including while another session holds the
69
+ model. It then sends `ready` with a session ID, protocol v3, the pipeline description and metadata:
70
+
71
+ - `session_params`: the received query parameters, after decoding;
72
+ - `effective_params`: those parameters with declared defaults applied;
73
+ - `model_server_version`: this distribution's version;
74
+ - model/session metadata supplied by the vendor, plus the listener address.
75
+
76
+ The client may report any additional metadata and owns how it records or combines that information.
77
+ The server checks description structure without importing client components.
78
+
79
+ All model operations run on one worker thread in the server process: load, preparation, inference,
80
+ session cleanup and model cleanup. Each session retains its own settings and history.
81
+ Inference is serialized across all listeners. Queue time is reported separately from model
82
+ time. The event loop answers keepalive while the worker is busy; no waiting messages interrupt an
83
+ inference response. A failed inference reports an error and keeps the session open. A malformed
84
+ session request ends that session.
85
+
86
+ Clients own recovery from reported inference errors. The recommended client behavior is to close
87
+ the affected session and, if retrying, open a fresh session and wait for `ready`. Continuing in the
88
+ same session is an explicit client choice for failures it knows are recoverable. An open session
89
+ does not guarantee that a failed inference left the model's state valid.
90
+
91
+ `Session.close` runs after outstanding work, on explicit close, disconnect or server shutdown.
92
+ The end-session acknowledgement follows cleanup. `Model.close` runs after sessions finish and
93
+ listeners stop, including when startup fails after loading. Call `server.shutdown()` from any thread;
94
+ an idle timeout ends the server only when no sessions remain. A model operation must return for
95
+ graceful shutdown to complete.
96
+
97
+ Model-specific implementations own cleanup and its failure policy. Cleanup methods are expected
98
+ to release their resources and complete without raising. If `Session.close` raises, the wrapper logs
99
+ the failure, attempts to report it to the client and ends the connection; it continues serving other
100
+ sessions. A cleanup failure can leak resources or leave shared model state invalid. The implementation
101
+ is responsible for recovery or for requesting `server.shutdown()` when continued serving is unsafe.
102
+
103
+ Pass `auth_token` to require a bearer token on both session and keepalive calls. `None` serves open;
104
+ empty or malformed tokens are refused at construction. For gRPC, use `grpc_wire.GrpcWire` with its
105
+ own `ServedHostPort`. Both listeners can be passed to one server and share its loaded model.
106
+
107
+ ## Protocol compatibility
108
+
109
+ The shared server sends v3 results without interpreting robot-command fields or reshaping chunks.
110
+ Updated Positronic clients select this behavior while preserving v1/v2 command decoding for legacy
111
+ servers. A client that only supports v1/v2 rejects v3 during opening. Upgrade clients before migrating
112
+ their endpoint. Legacy servers and their server-side codecs remain supported in `positronic.offboard`.
113
+
114
+ ## Client pipeline descriptions
115
+
116
+ Vendor code describes components as JSON-compatible data. Only the Positronic client imports
117
+ and instantiates their implementations:
118
+
119
+ ```python
120
+ from positronic_model_server.spec import component, parallel, sequence
121
+
122
+ description = sequence(
123
+ component("chunked_schedule", version=2, fps=20),
124
+ parallel(
125
+ component("restrict_image_size", width=320, height=180),
126
+ component("metadata", values={"model": "example"}),
127
+ ),
128
+ )
129
+ ```
130
+
131
+ `component` accepts a name, explicit version and keyword arguments, normalizing arguments through
132
+ JSON. `sequence` and `parallel` reject empty compositions. Component names and argument meanings
133
+ belong to the client registry; these helpers do not resolve classes or validate model parameters.
134
+ The structural keys (`NAME`, `VERSION`, `ARGS`, `SEQ`, `PAR`) have one definition in `spec`.
135
+
136
+ The wrapper defines only constants used by its own production code. Vendor settings belong to small
137
+ vendor modules that training and serving can both import; dataset file conventions belong to the
138
+ Positronic exporters and their producers. Client component
139
+ names belong to the client implementations; vendor descriptions spell those names as part of the
140
+ client's wire contract. Validate each description against the real client registry in vendor tests.
141
+ The wrapper forwards model metadata and parameter values without owning their field names.
142
+
143
+ ## Serialization
144
+
145
+ ```python
146
+ from positronic_model_server.serialization import deserialise, encode_jpeg, serialise
147
+
148
+ message = serialise({"camera": encode_jpeg(image, quality=90), "state": state})
149
+ restored = deserialise(message)
150
+ ```
151
+
152
+ Plain values, nested mappings/sequences, NumPy arrays and NumPy scalars use the shared msgpack
153
+ representation. Ordinary arrays are lossless, including arrays whose dimensions resemble images.
154
+ JPEG compression is explicit: call `encode_jpeg` on the values to compress. It accepts RGB arrays
155
+ shaped `(..., H, W, 3)`, casts pixels to uint8 and preserves every leading dimension. It is lossy.
156
+ The receiver recognizes each encoded value without a field schema. The same functions encode
157
+ observations or model results, including returned images.
158
+
159
+ `serialization.JpegEncoding(path, quality)` selects a nested mapping/list value, with an empty path
160
+ selecting the whole result. Supply these selectors through `Session.output_images` for returned
161
+ images. `serialization.encode_images` applies the same selections to observations without changing
162
+ the source containers. A missing path fails instead of silently skipping compression.
163
+
164
+ The client component `encode_images` defaults to recursively JPEG-encoding `uint8` arrays shaped
165
+ `(..., H, W, 3)` with positive height and width, including batches, inside mappings, lists and tuples.
166
+ Supply `paths` to encode only selected values; `paths=[]` disables compression. `quality` sets the
167
+ JPEG quality. Automatic detection is a heuristic: float images are left lossless, and unrelated
168
+ `uint8` arrays matching the image shape are compressed. Its result decoding leaves native values
169
+ unchanged. The wire serializer itself does not infer image fields.
170
+
171
+ Nonempty 3D and 4D images retain the v1/v2 JPEG representation. Extra leading dimensions and empty
172
+ batches carry a full-shape marker; send those only to receivers supporting it. NumPy object,
173
+ structured and complex dtypes are unsupported. Python tuples arrive as lists.
174
+
175
+ `protocol` owns shared message fields, authentication, timing and protocol version names.
176
+ Serialization here never interprets robot-command fields or `__cmd__` envelopes. Positronic's
177
+ legacy offboard protocol owns those hooks and its supported-version selection.
178
+
179
+ ## Package checks
180
+
181
+ Run `uv run pytest model_server positronic/offboard/tests positronic/policy/tests wire` from the
182
+ repository root. CI also builds wheels and installs them in isolated environments with NumPy
183
+ 1.26.4 on Python 3.11 and 3.12, selecting no transport, WebSocket alone, or gRPC alone. Root tests
184
+ exercise the repository's locked NumPy version.
185
+
186
+ Runtime dependencies are a design constraint. Before installing test tools, CI checks every
187
+ installed distribution against the per-transport allowlist in
188
+ `utilities/check_model_server_package.py`. Any additional runtime package fails the check,
189
+ whether direct or indirect, including dependencies introduced by upstream package updates.
190
+ Transport dependencies must stay optional. Changes to the allowlist require explicit design
191
+ review; do not expand it merely to make CI pass. The dependency check uses only the standard library.
192
+
193
+ The same check scans every wrapper Python module for module-level constants with no internal use.
194
+ Adding one fails CI, including in a new module. Test/example references and imports or assignments
195
+ that only re-export a constant do not count. Move such definitions to their owning vendor, client or
196
+ legacy protocol module; do not add artificial uses to satisfy the check. Source references establish
197
+ use, while review checks that the behavior itself belongs to the wrapper.