sequence-base 0.4.6__tar.gz → 0.4.8__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.
Files changed (108) hide show
  1. {sequence_base-0.4.6 → sequence_base-0.4.8}/PKG-INFO +2 -1
  2. {sequence_base-0.4.6 → sequence_base-0.4.8}/pyproject.toml +2 -2
  3. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/constants/version.py +2 -1
  4. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/contract/benchmarks.py +27 -1
  5. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/contract/eval.py +30 -6
  6. sequence_base-0.4.8/sequence_base/presets/frames.py +180 -0
  7. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/ActRequest.json +1 -1
  8. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/ActResponse.json +1 -1
  9. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/ActionChunk.json +1 -1
  10. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/ActionContract.json +1 -1
  11. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/ActionStep.json +1 -1
  12. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/AdapterTransform.json +1 -1
  13. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/AddLocal.json +1 -1
  14. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/Autoscaler.json +1 -1
  15. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/BenchmarkConditionSpec.json +2 -2
  16. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/BenchmarkRegisterRequest.json +21 -2
  17. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/BenchmarkSpec.json +2 -2
  18. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/BenchmarkStatusResponse.json +1 -1
  19. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/BootstrapResponse.json +1 -1
  20. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/Camera.json +1 -1
  21. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/ClampInfo.json +1 -1
  22. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/CodeBundle.json +1 -1
  23. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/Concurrency.json +1 -1
  24. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/Conditions.json +1 -1
  25. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/ConnectRequest.json +1 -1
  26. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/ConnectResponse.json +1 -1
  27. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/DeploymentAccepted.json +1 -1
  28. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/DeploymentPolicy.json +1 -1
  29. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/DeploymentRequest.json +1 -1
  30. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/DeploymentStatusResponse.json +1 -1
  31. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/Embodiment.json +1 -1
  32. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/ErrorBody.json +1 -1
  33. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/ErrorCodes.json +1 -1
  34. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/Estimate.json +1 -1
  35. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/EvalDryRunResponse.json +1 -1
  36. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/EvalReport.json +3 -3
  37. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/EvalRequest.json +22 -4
  38. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/EvalResponse.json +1 -1
  39. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/ImageFrame.json +1 -1
  40. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/ImageSpec.json +1 -1
  41. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/ImageStepAddLocal.json +1 -1
  42. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/ImageStepApt.json +1 -1
  43. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/ImageStepEnv.json +1 -1
  44. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/ImageStepRun.json +1 -1
  45. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/ImageStepTemplateFiles.json +1 -1
  46. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/ImageStepUvPip.json +1 -1
  47. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/InferParam.json +1 -1
  48. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/Lease.json +1 -1
  49. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/LossyTransform.json +1 -1
  50. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/MetricAgg.json +3 -3
  51. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/Observation.json +1 -1
  52. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/ObservationContract.json +1 -1
  53. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/PolicyRegistration.json +2 -2
  54. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/PolicySpec.json +1 -1
  55. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/Proprioception.json +1 -1
  56. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/RenderSpec.json +1 -1
  57. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/ReplaySpec.json +1 -1
  58. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/Resources.json +1 -1
  59. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/RolloutResult.json +1 -1
  60. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/SecretCreateRequest.json +1 -1
  61. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/SecretInfo.json +1 -1
  62. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/SecretRef.json +1 -1
  63. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/SeqFlags.json +1 -1
  64. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/Step.json +1 -1
  65. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/SuiteAgg.json +2 -2
  66. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/TaskAgg.json +1 -1
  67. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/TemplateFiles.json +1 -1
  68. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/Timeouts.json +1 -1
  69. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/UploadTarget.json +1 -1
  70. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/Variant.json +1 -1
  71. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/VolumeAttach.json +1 -1
  72. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/VolumeFile.json +1 -1
  73. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/VolumeInfo.json +1 -1
  74. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/VolumeMount.json +1 -1
  75. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/VolumeRef.json +1 -1
  76. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/VolumeUploadRequest.json +1 -1
  77. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/VolumeUploadResponse.json +1 -1
  78. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schemas/WarmingError.json +1 -1
  79. {sequence_base-0.4.6 → sequence_base-0.4.8}/.gitignore +0 -0
  80. {sequence_base-0.4.6 → sequence_base-0.4.8}/README.md +0 -0
  81. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/__init__.py +0 -0
  82. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/auth.py +0 -0
  83. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/codec/__init__.py +0 -0
  84. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/codec/image.py +0 -0
  85. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/constants/__init__.py +0 -0
  86. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/constants/headers.py +0 -0
  87. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/contract/__init__.py +0 -0
  88. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/contract/accounts.py +0 -0
  89. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/contract/act.py +0 -0
  90. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/contract/connect.py +0 -0
  91. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/contract/deployments.py +0 -0
  92. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/contract/errors.py +0 -0
  93. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/contract/flags.py +0 -0
  94. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/contract/io.py +0 -0
  95. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/contract/policy.py +0 -0
  96. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/contract/templates.py +0 -0
  97. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/contract/volumes.py +0 -0
  98. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/contract/wire.py +0 -0
  99. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/errors/__init__.py +0 -0
  100. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/errors/transport.py +0 -0
  101. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/obs/__init__.py +0 -0
  102. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/obs/log.py +0 -0
  103. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/obs/rid.py +0 -0
  104. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/presets/__init__.py +0 -0
  105. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/presets/robot_arm.py +0 -0
  106. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/schema_export.py +0 -0
  107. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/telemetry/__init__.py +0 -0
  108. {sequence_base-0.4.6 → sequence_base-0.4.8}/sequence_base/telemetry/events.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sequence-base
3
- Version: 0.4.6
3
+ Version: 0.4.8
4
4
  Summary: Shared foundation for the Sequences platform — wire contract, correlation, error taxonomy, constants. Used by the robot SDK, gateway, and container harness.
5
5
  Project-URL: Changelog, https://generalsequences.com/docs/changelog/
6
6
  Requires-Python: >=3.10
@@ -9,5 +9,6 @@ Provides-Extra: codec
9
9
  Requires-Dist: numpy>=1.26; extra == 'codec'
10
10
  Requires-Dist: pillow>=10; extra == 'codec'
11
11
  Provides-Extra: test
12
+ Requires-Dist: hypothesis>=6; extra == 'test'
12
13
  Requires-Dist: jsonschema>=4; extra == 'test'
13
14
  Requires-Dist: pytest; extra == 'test'
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sequence-base"
3
- version = "0.4.6"
3
+ version = "0.4.8"
4
4
  description = "Shared foundation for the Sequences platform — wire contract, correlation, error taxonomy, constants. Used by the robot SDK, gateway, and container harness."
5
5
  requires-python = ">=3.10"
6
6
  dependencies = ["pydantic>=2.0"]
@@ -10,7 +10,7 @@ Changelog = "https://generalsequences.com/docs/changelog/"
10
10
 
11
11
  [project.optional-dependencies]
12
12
  codec = ["pillow>=10", "numpy>=1.26"]
13
- test = ["pytest", "jsonschema>=4"]
13
+ test = ["pytest", "jsonschema>=4", "hypothesis>=6"]
14
14
 
15
15
  [build-system]
16
16
  requires = ["hatchling"]
@@ -4,7 +4,8 @@
4
4
 
5
5
 
6
6
 
7
- CONTRACT_VERSION = "0.5.6"
7
+ CONTRACT_VERSION = "0.5.8"
8
+
8
9
 
9
10
 
10
11
 
@@ -155,7 +155,7 @@ class BenchmarkSpec(BaseModel):
155
155
 
156
156
  class BenchmarkConditionSpec(BaseModel):
157
157
  """One episode setting exactly as the author declared it — a sparse list or a cross product, with its own step
158
- cap, wall-clock limit and suite."""
158
+ cap and wall-clock limit."""
159
159
 
160
160
  model_config = ConfigDict(extra="forbid")
161
161
 
@@ -164,6 +164,9 @@ class BenchmarkConditionSpec(BaseModel):
164
164
  variant: dict[str, str] = Field(default_factory=dict)
165
165
  max_steps: int | None = Field(default=None, gt=0)
166
166
  timeout_s: float | None = Field(default=None, gt=0)
167
+
168
+
169
+
167
170
  suite: str | None = None
168
171
 
169
172
 
@@ -193,6 +196,15 @@ class BenchmarkRegisterRequest(BaseModel):
193
196
  suite_of: dict[str, str] = Field(default_factory=dict)
194
197
 
195
198
 
199
+
200
+ task_descriptions: dict[str, str] = Field(default_factory=dict)
201
+
202
+
203
+
204
+
205
+ seeds_repeat: bool | None = None
206
+
207
+
196
208
  action_contract: ActionContract | None = None
197
209
 
198
210
 
@@ -231,6 +243,20 @@ class BenchmarkRegisterRequest(BaseModel):
231
243
  raise ValueError(f"setup parameter {name!r} is not a public Python identifier")
232
244
  return self
233
245
 
246
+ @model_validator(mode="after")
247
+ def _task_descriptions(self) -> "BenchmarkRegisterRequest":
248
+ """Each description names a task the benchmark declares - in `conditions` or `condition_list` - and is at most
249
+ 300 characters, as the platform holds it.
250
+
251
+ """
252
+ declared = set(self.conditions.tasks) | {c.task for c in self.condition_list or []}
253
+ for task, text in self.task_descriptions.items():
254
+ if task not in declared:
255
+ raise ValueError(f"task_descriptions names {task!r}, which is not a declared task")
256
+ if len(text) > 300:
257
+ raise ValueError(f"task_descriptions[{task!r}] is {len(text)} characters: at most 300")
258
+ return self
259
+
234
260
 
235
261
  class BenchmarkStatusResponse(BaseModel):
236
262
  """GET /v1/benchmarks/{name} — the build state of a registered benchmark (same lifecycle as a deployment:
@@ -47,8 +47,13 @@ class EvalRequest(BaseModel):
47
47
 
48
48
  model: str
49
49
  benchmark: str
50
- suite: str | None = None
51
- trials: int | None = None
50
+
51
+
52
+
53
+
54
+
55
+ trials: int | dict[str, int] | None = None
56
+ seeds: list[int] | dict[str, list[int]] | None = None
52
57
 
53
58
 
54
59
 
@@ -84,6 +89,24 @@ class EvalRequest(BaseModel):
84
89
 
85
90
  replay: Literal["3d", "none"] | None = None
86
91
 
92
+ @model_validator(mode="after")
93
+ def _selection(self) -> "EvalRequest":
94
+ """The selection's own bounds, as the platform holds them: a count 1-100000, at most 5000 seeds in a list and
95
+ 1000 tasks in a dict, no task twice in `tasks`. Whether the tasks and seeds are the benchmark's is the
96
+ platform's to say."""
97
+ counts = (self.trials.values() if isinstance(self.trials, dict)
98
+ else [self.trials] if self.trials is not None else [])
99
+ if any(not 1 <= c <= 100000 for c in counts):
100
+ raise ValueError("trials is a count of 1 to 100000 seeds a task")
101
+ lists = self.seeds.values() if isinstance(self.seeds, dict) else [self.seeds] if self.seeds is not None else []
102
+ if any(len(s) > 5000 for s in lists):
103
+ raise ValueError("seeds names at most 5000 seeds")
104
+ if any(isinstance(x, dict) and len(x) > 1000 for x in (self.trials, self.seeds)):
105
+ raise ValueError("trials and seeds name at most 1000 tasks")
106
+ if self.tasks is not None and len(set(self.tasks)) != len(self.tasks):
107
+ raise ValueError("tasks names a task twice")
108
+ return self
109
+
87
110
 
88
111
  class ClampInfo(BaseModel):
89
112
  """Why the granted GPU count was clamped below the request — structured, not a bare value."""
@@ -219,8 +242,9 @@ class EvalStatus(str, Enum):
219
242
 
220
243
 
221
244
  class SuiteAgg(BaseModel):
222
- """Per-suite aggregate of ONE user-named metric — structured detail, not a bare dict. `value` is the
223
- metric aggregated over that suite; the platform does not name or interpret the metric."""
245
+ """Per-suite aggregate of ONE user-named metric, as an eval from before contract 0.5.8 reported it: kept so those
246
+ reports stay readable. A benchmark is one set of tasks now, and no eval since reports one.
247
+ """
224
248
 
225
249
  model_config = ConfigDict(extra="forbid")
226
250
 
@@ -240,8 +264,8 @@ class TaskAgg(BaseModel):
240
264
 
241
265
 
242
266
  class MetricAgg(BaseModel):
243
- """The aggregate of one user-named metric key across the eval — overall plus per-suite / per-task
244
- breakdowns and the sample count. The platform aggregates whatever the user returned in `Step.metrics`;
267
+ """The aggregate of one user-named metric key across the eval — overall plus per-task breakdowns (and, in a
268
+ report from before contract 0.5.8, per-suite ones) and the sample count. The platform aggregates whatever the user returned in `Step.metrics`;
245
269
  it neither defines nor injects `success_rate` (success is just one key the user may or may not name)."""
246
270
 
247
271
  model_config = ConfigDict(extra="forbid")
@@ -0,0 +1,180 @@
1
+ """
2
+ The robot-arm preset's /act body as binary frames (application/x-seq-frames, version 1): the camera frames' own bytes
3
+ after the act's JSON instead of base64 inside it. A robot's direct acts only, and only to a worker that says it takes
4
+ them. Here and not in the core contract: it knows the preset's Observation (its `images`), which the core never does.
5
+
6
+ In one sentence: the SDK takes the act's envelope (the JSON it would send) → every `images[i].data` of the act's inputs
7
+ that is canonical base64 is decoded and moved out as a segment, its place in the JSON holding the segment's number → the
8
+ body is a 12-byte head, the segments' lengths, the JSON, then the segments → the worker checks every length before it
9
+ reads the JSON, puts each segment back as the base64 it was, and serves the envelope as it serves a JSON act: the
10
+ contract check, the policy and every other reader see the very same request.
11
+
12
+ The act's inputs are where the worker reads them from a JSON act: `inputs`, else `observation` (a robot's Observation
13
+ act, as the SDK sends it: {"model", "observation": {"images", "proprioception", ...}, "episode_id", ...}), else the body
14
+ itself.
15
+
16
+ WHY lossless by construction: a frame moves only when its string is canonical base64 - b64encode(b64decode(s)) == s -
17
+ so the worker's b64encode of the segment is that string again, character for character; any other string (line breaks,
18
+ no padding, a data: URI, a character outside base64) stays in the JSON as it was. The rest of the JSON is the same dict
19
+ through json.dumps and json.loads on both paths, so every float comes back to the bit.
20
+ WHY the worker turns the bytes back into base64: `ImageFrame.data` is base64, and a policy reads it as such - many decode
21
+ it themselves - so the request a policy sees must not change. What binary frames save is the upload: base64 is four
22
+ characters for three bytes, a third more on every act.
23
+ WHY a worker must say it takes them (FORMATS_HEADER on each of its answers): a worker before them reads any body that is
24
+ not JSON as the policy's own inputs (custom I/O), so frames reaching it would go to the policy untouched.
25
+
26
+
27
+
28
+ Layout, little-endian:
29
+
30
+ offset length what
31
+ 0 4 MAGIC, b"SQF1" (its last byte the version)
32
+ 4 4 json_len, u32
33
+ 8 4 n, the segments, u32 (at most MAX_SEGMENTS)
34
+ 12 4 n each segment's length, u32
35
+ 12 + 4 n json_len the envelope's JSON (UTF-8): a moved frame's `data` is its segment's number
36
+ then sum the segments, in order
37
+
38
+ The body is exactly that long, and each segment is named once, by an `images[i].data` of the act's inputs.
39
+
40
+ body = frames.pack(envelope) # None: nothing would move - send the envelope as JSON
41
+ envelope = frames.unpack(body) # FramesError: not version 1 (the worker's 400, reason "frames")
42
+ """
43
+ from __future__ import annotations
44
+
45
+ import base64
46
+ import binascii
47
+ import json
48
+ import struct
49
+ from typing import Any
50
+
51
+ __all__ = ["FORMAT", "FORMATS_HEADER", "MAGIC", "MAX_SEGMENTS", "MEDIA_TYPE", "FramesError", "formats_of", "pack",
52
+ "unpack"]
53
+
54
+ MEDIA_TYPE = "application/x-seq-frames"
55
+
56
+
57
+ FORMATS_HEADER = "X-Seq-Act-Formats"
58
+ FORMAT = "x-seq-frames/1"
59
+ MAGIC = b"SQF1"
60
+
61
+ MAX_SEGMENTS = 256
62
+
63
+ _HEAD = struct.Struct("<4sII")
64
+
65
+
66
+ class FramesError(ValueError):
67
+ """A body that is not binary frames version 1: the worker answers 400 (reason "frames") with this message."""
68
+
69
+
70
+ def formats_of(value: str | None) -> set[str]:
71
+ """The formats a FORMATS_HEADER value names ("x-seq-frames/1, ..."): an empty set for none."""
72
+ return {part.strip().lower() for part in (value or "").split(",") if part.strip()}
73
+
74
+
75
+ def _key(envelope: dict) -> str | None:
76
+ """Where the act's inputs are, as the worker reads a JSON act: `inputs`, else `observation`, else None - the body
77
+ itself."""
78
+ return "inputs" if "inputs" in envelope else "observation" if "observation" in envelope else None
79
+
80
+
81
+ def _images(envelope: Any) -> list | None:
82
+ """The `images` of the act's inputs when they have the robot-arm preset's shape (a list of objects), else None."""
83
+ if not isinstance(envelope, dict):
84
+ return None
85
+ key = _key(envelope)
86
+ inputs = envelope[key] if key is not None else envelope
87
+ images = inputs.get("images") if isinstance(inputs, dict) else None
88
+ if not isinstance(images, list) or not all(isinstance(frame, dict) for frame in images):
89
+ return None
90
+ return images
91
+
92
+
93
+ def _canonical(data: Any) -> bytes | None:
94
+ """The bytes a frame's `data` stands for, when it is canonical base64 (b64encode gives the string back exactly)."""
95
+ if not isinstance(data, str):
96
+ return None
97
+ try:
98
+ raw = base64.b64decode(data, validate=True)
99
+ except (binascii.Error, ValueError):
100
+ return None
101
+ return raw if base64.b64encode(raw).decode("ascii") == data else None
102
+
103
+
104
+ def _a_number(data: Any) -> bool:
105
+ """Whether a frame's `data` would read as a segment's number (unpack takes any int but a bool as one)."""
106
+ return isinstance(data, int) and not isinstance(data, bool)
107
+
108
+
109
+ def pack(envelope: dict) -> bytes | None:
110
+ """`envelope` - the act's JSON body - as binary frames; None when none of its frames would move, when a frame that
111
+ stays has a number for its `data` (the worker would read it as a segment's), or when its JSON would not encode (a
112
+ NaN, a lone surrogate): the caller sends it as JSON, as before. The caller's dict is untouched."""
113
+ images = _images(envelope)
114
+ if not images:
115
+ return None
116
+ moved: dict[int, int] = {}
117
+ segments: list[bytes] = []
118
+ for i, frame in enumerate(images):
119
+ raw = _canonical(frame.get("data"))
120
+ if raw is not None:
121
+ moved[i] = len(segments)
122
+ segments.append(raw)
123
+ elif _a_number(frame.get("data")):
124
+ return None
125
+ if not segments or len(segments) > MAX_SEGMENTS:
126
+ return None
127
+ key = _key(envelope)
128
+ inputs = dict(envelope[key] if key is not None else envelope)
129
+ inputs["images"] = [{**frame, "data": moved[i]} if i in moved else frame for i, frame in enumerate(images)]
130
+ try:
131
+ text = json.dumps({**envelope, key: inputs} if key is not None else inputs, ensure_ascii=False,
132
+ separators=(",", ":"), allow_nan=False).encode("utf-8")
133
+ except (TypeError, ValueError):
134
+ return None
135
+ head = _HEAD.pack(MAGIC, len(text), len(segments)) + struct.pack(f"<{len(segments)}I", *map(len, segments))
136
+ return b"".join((head, text, *segments))
137
+
138
+
139
+ def unpack(body: bytes) -> dict:
140
+ """The act's JSON body from binary frames, each segment back in its frame as the base64 it was. Raises FramesError,
141
+ saying what is wrong, for anything that is not exactly version 1 - checked before the JSON is read."""
142
+ size = len(body)
143
+ if size < _HEAD.size:
144
+ raise FramesError(f"the body is {size} bytes; binary frames begin with a {_HEAD.size}-byte head")
145
+ magic, json_len, count = _HEAD.unpack_from(body, 0)
146
+ if magic != MAGIC:
147
+ raise FramesError(f"the body does not begin with {MAGIC!r}: not binary frames version 1")
148
+ if count > MAX_SEGMENTS:
149
+ raise FramesError(f"the head names {count} segments; at most {MAX_SEGMENTS}")
150
+ table_end = _HEAD.size + 4 * count
151
+ if size < table_end:
152
+ raise FramesError(f"the body is {size} bytes and ends inside its table of {count} segment lengths")
153
+ lengths = struct.unpack_from(f"<{count}I", body, _HEAD.size)
154
+ expected = table_end + json_len + sum(lengths)
155
+ if size != expected:
156
+ raise FramesError(f"the body is {size} bytes; its head and segment lengths make it {expected}")
157
+ try:
158
+ envelope = json.loads(body[table_end:table_end + json_len].decode("utf-8"))
159
+ except (UnicodeDecodeError, ValueError):
160
+ raise FramesError("the JSON part does not parse") from None
161
+ if not isinstance(envelope, dict):
162
+ raise FramesError("the JSON part is not an object")
163
+ starts, at = [], table_end + json_len
164
+ for length in lengths:
165
+ starts.append(at)
166
+ at += length
167
+ named: set[int] = set()
168
+ for frame in _images(envelope) or ():
169
+ ref = frame.get("data")
170
+ if not _a_number(ref):
171
+ continue
172
+ if not 0 <= ref < count:
173
+ raise FramesError(f"a frame names segment {ref}; the body has {count}")
174
+ if ref in named:
175
+ raise FramesError(f"segment {ref} is named by two frames")
176
+ named.add(ref)
177
+ frame["data"] = base64.b64encode(body[starts[ref]:starts[ref] + lengths[ref]]).decode("ascii")
178
+ if len(named) != count:
179
+ raise FramesError(f"{count - len(named)} of the body's {count} segments are named by no frame")
180
+ return envelope
@@ -79,5 +79,5 @@
79
79
  ],
80
80
  "title": "ActRequest",
81
81
  "type": "object",
82
- "x-contract-version": "0.5.6"
82
+ "x-contract-version": "0.5.8"
83
83
  }
@@ -89,5 +89,5 @@
89
89
  ],
90
90
  "title": "ActResponse",
91
91
  "type": "object",
92
- "x-contract-version": "0.5.6"
92
+ "x-contract-version": "0.5.8"
93
93
  }
@@ -99,5 +99,5 @@
99
99
  ],
100
100
  "title": "ActionChunk",
101
101
  "type": "object",
102
- "x-contract-version": "0.5.6"
102
+ "x-contract-version": "0.5.8"
103
103
  }
@@ -116,5 +116,5 @@
116
116
  ],
117
117
  "title": "ActionContract",
118
118
  "type": "object",
119
- "x-contract-version": "0.5.6"
119
+ "x-contract-version": "0.5.8"
120
120
  }
@@ -19,5 +19,5 @@
19
19
  ],
20
20
  "title": "ActionStep",
21
21
  "type": "object",
22
- "x-contract-version": "0.5.6"
22
+ "x-contract-version": "0.5.8"
23
23
  }
@@ -38,5 +38,5 @@
38
38
  ],
39
39
  "title": "AdapterTransform",
40
40
  "type": "object",
41
- "x-contract-version": "0.5.6"
41
+ "x-contract-version": "0.5.8"
42
42
  }
@@ -23,5 +23,5 @@
23
23
  ],
24
24
  "title": "AddLocal",
25
25
  "type": "object",
26
- "x-contract-version": "0.5.6"
26
+ "x-contract-version": "0.5.8"
27
27
  }
@@ -69,5 +69,5 @@
69
69
  },
70
70
  "title": "Autoscaler",
71
71
  "type": "object",
72
- "x-contract-version": "0.5.6"
72
+ "x-contract-version": "0.5.8"
73
73
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "additionalProperties": false,
3
- "description": "One episode setting exactly as the author declared it — a sparse list or a cross product, with its own step\ncap, wall-clock limit and suite.",
3
+ "description": "One episode setting exactly as the author declared it — a sparse list or a cross product, with its own step\ncap and wall-clock limit.",
4
4
  "properties": {
5
5
  "max_steps": {
6
6
  "anyOf": [
@@ -62,5 +62,5 @@
62
62
  ],
63
63
  "title": "BenchmarkConditionSpec",
64
64
  "type": "object",
65
- "x-contract-version": "0.5.6"
65
+ "x-contract-version": "0.5.8"
66
66
  }
@@ -145,7 +145,7 @@
145
145
  },
146
146
  "BenchmarkConditionSpec": {
147
147
  "additionalProperties": false,
148
- "description": "One episode setting exactly as the author declared it — a sparse list or a cross product, with its own step\ncap, wall-clock limit and suite.",
148
+ "description": "One episode setting exactly as the author declared it — a sparse list or a cross product, with its own step\ncap and wall-clock limit.",
149
149
  "properties": {
150
150
  "max_steps": {
151
151
  "anyOf": [
@@ -942,6 +942,18 @@
942
942
  "title": "Secrets",
943
943
  "type": "array"
944
944
  },
945
+ "seeds_repeat": {
946
+ "anyOf": [
947
+ {
948
+ "type": "boolean"
949
+ },
950
+ {
951
+ "type": "null"
952
+ }
953
+ ],
954
+ "default": null,
955
+ "title": "Seeds Repeat"
956
+ },
945
957
  "suite_of": {
946
958
  "additionalProperties": {
947
959
  "type": "string"
@@ -949,6 +961,13 @@
949
961
  "title": "Suite Of",
950
962
  "type": "object"
951
963
  },
964
+ "task_descriptions": {
965
+ "additionalProperties": {
966
+ "type": "string"
967
+ },
968
+ "title": "Task Descriptions",
969
+ "type": "object"
970
+ },
952
971
  "timeout_s": {
953
972
  "anyOf": [
954
973
  {
@@ -972,5 +991,5 @@
972
991
  ],
973
992
  "title": "BenchmarkRegisterRequest",
974
993
  "type": "object",
975
- "x-contract-version": "0.5.6"
994
+ "x-contract-version": "0.5.8"
976
995
  }
@@ -368,7 +368,7 @@
368
368
  ],
369
369
  "title": "SeqFlags",
370
370
  "type": "integer",
371
- "x-contract-version": "0.5.6",
371
+ "x-contract-version": "0.5.8",
372
372
  "x-flag-names": {
373
373
  "CHECK": 65536,
374
374
  "INFER": 16,
@@ -625,5 +625,5 @@
625
625
  ],
626
626
  "title": "BenchmarkSpec",
627
627
  "type": "object",
628
- "x-contract-version": "0.5.6"
628
+ "x-contract-version": "0.5.8"
629
629
  }
@@ -40,5 +40,5 @@
40
40
  ],
41
41
  "title": "BenchmarkStatusResponse",
42
42
  "type": "object",
43
- "x-contract-version": "0.5.6"
43
+ "x-contract-version": "0.5.8"
44
44
  }
@@ -79,5 +79,5 @@
79
79
  ],
80
80
  "title": "BootstrapResponse",
81
81
  "type": "object",
82
- "x-contract-version": "0.5.6"
82
+ "x-contract-version": "0.5.8"
83
83
  }
@@ -72,5 +72,5 @@
72
72
  ],
73
73
  "title": "Camera",
74
74
  "type": "object",
75
- "x-contract-version": "0.5.6"
75
+ "x-contract-version": "0.5.8"
76
76
  }
@@ -23,5 +23,5 @@
23
23
  ],
24
24
  "title": "ClampInfo",
25
25
  "type": "object",
26
- "x-contract-version": "0.5.6"
26
+ "x-contract-version": "0.5.8"
27
27
  }
@@ -23,5 +23,5 @@
23
23
  ],
24
24
  "title": "CodeBundle",
25
25
  "type": "object",
26
- "x-contract-version": "0.5.6"
26
+ "x-contract-version": "0.5.8"
27
27
  }
@@ -96,5 +96,5 @@
96
96
  },
97
97
  "title": "Concurrency",
98
98
  "type": "object",
99
- "x-contract-version": "0.5.6"
99
+ "x-contract-version": "0.5.8"
100
100
  }
@@ -55,5 +55,5 @@
55
55
  ],
56
56
  "title": "Conditions",
57
57
  "type": "object",
58
- "x-contract-version": "0.5.6"
58
+ "x-contract-version": "0.5.8"
59
59
  }
@@ -25,5 +25,5 @@
25
25
  ],
26
26
  "title": "ConnectRequest",
27
27
  "type": "object",
28
- "x-contract-version": "0.5.6"
28
+ "x-contract-version": "0.5.8"
29
29
  }
@@ -342,5 +342,5 @@
342
342
  ],
343
343
  "title": "ConnectResponse",
344
344
  "type": "object",
345
- "x-contract-version": "0.5.6"
345
+ "x-contract-version": "0.5.8"
346
346
  }
@@ -18,5 +18,5 @@
18
18
  ],
19
19
  "title": "DeploymentAccepted",
20
20
  "type": "object",
21
- "x-contract-version": "0.5.6"
21
+ "x-contract-version": "0.5.8"
22
22
  }
@@ -530,5 +530,5 @@
530
530
  ],
531
531
  "title": "DeploymentPolicy",
532
532
  "type": "object",
533
- "x-contract-version": "0.5.6"
533
+ "x-contract-version": "0.5.8"
534
534
  }
@@ -1224,5 +1224,5 @@
1224
1224
  ],
1225
1225
  "title": "DeploymentRequest",
1226
1226
  "type": "object",
1227
- "x-contract-version": "0.5.6"
1227
+ "x-contract-version": "0.5.8"
1228
1228
  }
@@ -58,5 +58,5 @@
58
58
  ],
59
59
  "title": "DeploymentStatusResponse",
60
60
  "type": "object",
61
- "x-contract-version": "0.5.6"
61
+ "x-contract-version": "0.5.8"
62
62
  }
@@ -267,5 +267,5 @@
267
267
  ],
268
268
  "title": "Embodiment",
269
269
  "type": "object",
270
- "x-contract-version": "0.5.6"
270
+ "x-contract-version": "0.5.8"
271
271
  }
@@ -94,5 +94,5 @@
94
94
  ],
95
95
  "title": "ErrorBody",
96
96
  "type": "object",
97
- "x-contract-version": "0.5.6"
97
+ "x-contract-version": "0.5.8"
98
98
  }
@@ -22,7 +22,7 @@
22
22
  ],
23
23
  "title": "ErrorCode",
24
24
  "type": "string",
25
- "x-contract-version": "0.5.6",
25
+ "x-contract-version": "0.5.8",
26
26
  "x-errors": {
27
27
  "budget_exhausted": {
28
28
  "exit_code": 6,
@@ -60,5 +60,5 @@
60
60
  ],
61
61
  "title": "Estimate",
62
62
  "type": "object",
63
- "x-contract-version": "0.5.6"
63
+ "x-contract-version": "0.5.8"
64
64
  }
@@ -240,5 +240,5 @@
240
240
  ],
241
241
  "title": "EvalDryRunResponse",
242
242
  "type": "object",
243
- "x-contract-version": "0.5.6"
243
+ "x-contract-version": "0.5.8"
244
244
  }