sequence-base 0.4.5__tar.gz → 0.4.7__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.5 → sequence_base-0.4.7}/PKG-INFO +2 -1
  2. {sequence_base-0.4.5 → sequence_base-0.4.7}/pyproject.toml +2 -2
  3. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/__init__.py +2 -1
  4. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/constants/version.py +2 -1
  5. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/contract/__init__.py +2 -1
  6. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/contract/benchmarks.py +25 -0
  7. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/contract/eval.py +3 -0
  8. sequence_base-0.4.7/sequence_base/presets/frames.py +180 -0
  9. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schema_export.py +2 -0
  10. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/ActRequest.json +1 -1
  11. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/ActResponse.json +1 -1
  12. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/ActionChunk.json +1 -1
  13. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/ActionContract.json +1 -1
  14. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/ActionStep.json +1 -1
  15. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/AdapterTransform.json +1 -1
  16. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/AddLocal.json +1 -1
  17. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/Autoscaler.json +1 -1
  18. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/BenchmarkConditionSpec.json +1 -1
  19. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/BenchmarkRegisterRequest.json +67 -1
  20. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/BenchmarkSpec.json +68 -2
  21. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/BenchmarkStatusResponse.json +1 -1
  22. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/BootstrapResponse.json +1 -1
  23. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/Camera.json +1 -1
  24. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/ClampInfo.json +1 -1
  25. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/CodeBundle.json +1 -1
  26. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/Concurrency.json +1 -1
  27. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/Conditions.json +1 -1
  28. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/ConnectRequest.json +1 -1
  29. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/ConnectResponse.json +1 -1
  30. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/DeploymentAccepted.json +1 -1
  31. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/DeploymentPolicy.json +1 -1
  32. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/DeploymentRequest.json +1 -1
  33. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/DeploymentStatusResponse.json +1 -1
  34. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/Embodiment.json +1 -1
  35. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/ErrorBody.json +1 -1
  36. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/ErrorCodes.json +1 -1
  37. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/Estimate.json +1 -1
  38. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/EvalDryRunResponse.json +1 -1
  39. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/EvalReport.json +1 -1
  40. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/EvalRequest.json +17 -1
  41. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/EvalResponse.json +1 -1
  42. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/ImageFrame.json +1 -1
  43. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/ImageSpec.json +1 -1
  44. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/ImageStepAddLocal.json +1 -1
  45. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/ImageStepApt.json +1 -1
  46. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/ImageStepEnv.json +1 -1
  47. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/ImageStepRun.json +1 -1
  48. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/ImageStepTemplateFiles.json +1 -1
  49. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/ImageStepUvPip.json +1 -1
  50. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/InferParam.json +1 -1
  51. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/Lease.json +1 -1
  52. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/LossyTransform.json +1 -1
  53. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/MetricAgg.json +1 -1
  54. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/Observation.json +1 -1
  55. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/ObservationContract.json +1 -1
  56. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/PolicyRegistration.json +2 -2
  57. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/PolicySpec.json +1 -1
  58. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/Proprioception.json +1 -1
  59. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/RenderSpec.json +1 -1
  60. sequence_base-0.4.7/sequence_base/schemas/ReplaySpec.json +56 -0
  61. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/Resources.json +1 -1
  62. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/RolloutResult.json +1 -1
  63. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/SecretCreateRequest.json +1 -1
  64. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/SecretInfo.json +1 -1
  65. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/SecretRef.json +1 -1
  66. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/SeqFlags.json +1 -1
  67. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/Step.json +1 -1
  68. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/SuiteAgg.json +1 -1
  69. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/TaskAgg.json +1 -1
  70. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/TemplateFiles.json +1 -1
  71. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/Timeouts.json +1 -1
  72. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/UploadTarget.json +1 -1
  73. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/Variant.json +1 -1
  74. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/VolumeAttach.json +1 -1
  75. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/VolumeFile.json +1 -1
  76. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/VolumeInfo.json +1 -1
  77. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/VolumeMount.json +1 -1
  78. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/VolumeRef.json +1 -1
  79. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/VolumeUploadRequest.json +1 -1
  80. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/VolumeUploadResponse.json +1 -1
  81. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/schemas/WarmingError.json +1 -1
  82. {sequence_base-0.4.5 → sequence_base-0.4.7}/.gitignore +0 -0
  83. {sequence_base-0.4.5 → sequence_base-0.4.7}/README.md +0 -0
  84. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/auth.py +0 -0
  85. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/codec/__init__.py +0 -0
  86. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/codec/image.py +0 -0
  87. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/constants/__init__.py +0 -0
  88. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/constants/headers.py +0 -0
  89. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/contract/accounts.py +0 -0
  90. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/contract/act.py +0 -0
  91. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/contract/connect.py +0 -0
  92. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/contract/deployments.py +0 -0
  93. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/contract/errors.py +0 -0
  94. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/contract/flags.py +0 -0
  95. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/contract/io.py +0 -0
  96. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/contract/policy.py +0 -0
  97. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/contract/templates.py +0 -0
  98. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/contract/volumes.py +0 -0
  99. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/contract/wire.py +0 -0
  100. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/errors/__init__.py +0 -0
  101. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/errors/transport.py +0 -0
  102. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/obs/__init__.py +0 -0
  103. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/obs/log.py +0 -0
  104. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/obs/rid.py +0 -0
  105. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/presets/__init__.py +0 -0
  106. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/presets/robot_arm.py +0 -0
  107. {sequence_base-0.4.5 → sequence_base-0.4.7}/sequence_base/telemetry/__init__.py +0 -0
  108. {sequence_base-0.4.5 → sequence_base-0.4.7}/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.5
3
+ Version: 0.4.7
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.5"
3
+ version = "0.4.7"
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"]
@@ -82,6 +82,7 @@ from sequence_base.contract import (
82
82
  PolicySpec,
83
83
  Proprioception,
84
84
  RenderSpec,
85
+ ReplaySpec,
85
86
  Resources,
86
87
  RolloutResult,
87
88
  SecretCreateRequest,
@@ -139,7 +140,7 @@ __all__ = [
139
140
 
140
141
  "Lease", "ConnectRequest", "ConnectResponse",
141
142
 
142
- "Variant", "Conditions", "RenderSpec", "Step", "BenchmarkSpec", "BenchmarkConditionSpec", "BenchmarkRegisterRequest", "BenchmarkStatusResponse",
143
+ "Variant", "Conditions", "RenderSpec", "ReplaySpec", "Step", "BenchmarkSpec", "BenchmarkConditionSpec", "BenchmarkRegisterRequest", "BenchmarkStatusResponse",
143
144
  "EvalRequest", "ClampInfo", "Estimate", "EvalResponse", "EvalDryRunResponse", "RolloutResult",
144
145
  "EvalStatus", "SuiteAgg", "TaskAgg", "MetricAgg", "LossyTransform", "EvalReport",
145
146
  "AdapterKind", "AdapterTransform",
@@ -4,7 +4,8 @@
4
4
 
5
5
 
6
6
 
7
- CONTRACT_VERSION = "0.5.5"
7
+ CONTRACT_VERSION = "0.5.7"
8
+
8
9
 
9
10
 
10
11
 
@@ -67,6 +67,7 @@ from sequence_base.contract.benchmarks import (
67
67
  BenchmarkSpec,
68
68
  Conditions,
69
69
  RenderSpec,
70
+ ReplaySpec,
70
71
  Step,
71
72
  Variant,
72
73
  )
@@ -131,7 +132,7 @@ __all__ = [
131
132
 
132
133
  "Lease", "ConnectRequest", "ConnectResponse",
133
134
 
134
- "Variant", "Conditions", "RenderSpec", "Step", "BenchmarkSpec", "BenchmarkConditionSpec", "BenchmarkRegisterRequest", "BenchmarkStatusResponse",
135
+ "Variant", "Conditions", "RenderSpec", "ReplaySpec", "Step", "BenchmarkSpec", "BenchmarkConditionSpec", "BenchmarkRegisterRequest", "BenchmarkStatusResponse",
135
136
  "EvalRequest", "ClampInfo", "Estimate", "EvalResponse", "EvalDryRunResponse", "RolloutResult",
136
137
  "EvalStatus", "SuiteAgg", "TaskAgg", "MetricAgg", "LossyTransform", "EvalReport",
137
138
  "AdapterKind", "AdapterTransform",
@@ -25,6 +25,7 @@ __all__ = [
25
25
  "Variant",
26
26
  "Conditions",
27
27
  "RenderSpec",
28
+ "ReplaySpec",
28
29
  "Step",
29
30
  "BenchmarkSpec",
30
31
  "BenchmarkRegisterRequest",
@@ -69,6 +70,26 @@ class RenderSpec(BaseModel):
69
70
  "None: as drawn")
70
71
 
71
72
 
73
+ class ReplaySpec(BaseModel):
74
+ """a.k.a. `seq.Replay` — declares that the platform records the eval's 3D replay: each step every node's world
75
+ transform, drawn in the owner's browser after the eval. `engine` picks the platform's
76
+ adapter that reads the simulation's state ("mujoco", "sapien", "isaac"); "own" means the benchmark hands the frames itself
77
+ (`seq.replay.scene` / `seq.replay.frame`); None takes the benchmark's own `engine`. `state` names the benchmark
78
+ method an adapter calls for the engine's state of an env (or of a batched benchmark's slots), `looks` the one that
79
+ says what the adapter cannot read - a colour or a visibility each episode sets. Whether an eval records it is the
80
+ eval's `replay` ("3d" by default, "none")."""
81
+
82
+ model_config = ConfigDict(extra="forbid")
83
+
84
+ engine: Literal["mujoco", "sapien", "isaac", "own"] | None = Field(
85
+ None, description="The adapter that reads the simulation: 'mujoco' (incl. MuJoCo Warp), 'sapien', 'isaac', or "
86
+ "'own' (the benchmark hands frames itself); None: the benchmark's engine")
87
+ state: str | None = Field(None, pattern=r"^[A-Za-z_][A-Za-z0-9_]*$",
88
+ description="The benchmark method an adapter calls for an env's (or slots') engine state")
89
+ looks: str | None = Field(None, pattern=r"^[A-Za-z_][A-Za-z0-9_]*$",
90
+ description="The benchmark method that says each episode's colours or visibility")
91
+
92
+
72
93
  class Step(BaseModel):
73
94
  """What `@seq.check` returns each step. The platform settles each env per-step from this and NEVER
74
95
  invents a success metric — `done` is the user's call, and `metrics` keys are user-named (the platform
@@ -107,6 +128,8 @@ class BenchmarkSpec(BaseModel):
107
128
  envs: int | None = Field(default=None, ge=1, le=65536)
108
129
  assets: list[VolumeRef] = Field(default_factory=list)
109
130
  render: RenderSpec | None = None
131
+
132
+ replay: ReplaySpec | None = None
110
133
  cpu: int | None = None
111
134
  memory_gb: int | None = None
112
135
  secrets: list[SecretRef] = Field(default_factory=list)
@@ -158,6 +181,8 @@ class BenchmarkRegisterRequest(BaseModel):
158
181
  handler_ref: str
159
182
  assets: list[VolumeRef] = Field(default_factory=list)
160
183
  render: RenderSpec | None = None
184
+
185
+ replay: ReplaySpec | None = None
161
186
  cpu: int | None = None
162
187
  memory_gb: int | None = None
163
188
 
@@ -82,6 +82,9 @@ class EvalRequest(BaseModel):
82
82
  record: Literal["per-task", "all", "none"] | None = None
83
83
 
84
84
 
85
+ replay: Literal["3d", "none"] | None = None
86
+
87
+
85
88
  class ClampInfo(BaseModel):
86
89
  """Why the granted GPU count was clamped below the request — structured, not a bare value."""
87
90
 
@@ -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
@@ -40,6 +40,7 @@ from sequence_base.contract.benchmarks import (
40
40
  BenchmarkSpec,
41
41
  Conditions,
42
42
  RenderSpec,
43
+ ReplaySpec,
43
44
  Step,
44
45
  Variant,
45
46
  )
@@ -153,6 +154,7 @@ WIRE_MODELS: dict[str, type[BaseModel]] = {
153
154
  "Variant": Variant,
154
155
  "Conditions": Conditions,
155
156
  "RenderSpec": RenderSpec,
157
+ "ReplaySpec": ReplaySpec,
156
158
  "Step": Step,
157
159
  "BenchmarkSpec": BenchmarkSpec,
158
160
  "BenchmarkConditionSpec": BenchmarkConditionSpec,
@@ -79,5 +79,5 @@
79
79
  ],
80
80
  "title": "ActRequest",
81
81
  "type": "object",
82
- "x-contract-version": "0.5.5"
82
+ "x-contract-version": "0.5.7"
83
83
  }
@@ -89,5 +89,5 @@
89
89
  ],
90
90
  "title": "ActResponse",
91
91
  "type": "object",
92
- "x-contract-version": "0.5.5"
92
+ "x-contract-version": "0.5.7"
93
93
  }
@@ -99,5 +99,5 @@
99
99
  ],
100
100
  "title": "ActionChunk",
101
101
  "type": "object",
102
- "x-contract-version": "0.5.5"
102
+ "x-contract-version": "0.5.7"
103
103
  }
@@ -116,5 +116,5 @@
116
116
  ],
117
117
  "title": "ActionContract",
118
118
  "type": "object",
119
- "x-contract-version": "0.5.5"
119
+ "x-contract-version": "0.5.7"
120
120
  }
@@ -19,5 +19,5 @@
19
19
  ],
20
20
  "title": "ActionStep",
21
21
  "type": "object",
22
- "x-contract-version": "0.5.5"
22
+ "x-contract-version": "0.5.7"
23
23
  }
@@ -38,5 +38,5 @@
38
38
  ],
39
39
  "title": "AdapterTransform",
40
40
  "type": "object",
41
- "x-contract-version": "0.5.5"
41
+ "x-contract-version": "0.5.7"
42
42
  }
@@ -23,5 +23,5 @@
23
23
  ],
24
24
  "title": "AddLocal",
25
25
  "type": "object",
26
- "x-contract-version": "0.5.5"
26
+ "x-contract-version": "0.5.7"
27
27
  }
@@ -69,5 +69,5 @@
69
69
  },
70
70
  "title": "Autoscaler",
71
71
  "type": "object",
72
- "x-contract-version": "0.5.5"
72
+ "x-contract-version": "0.5.7"
73
73
  }
@@ -62,5 +62,5 @@
62
62
  ],
63
63
  "title": "BenchmarkConditionSpec",
64
64
  "type": "object",
65
- "x-contract-version": "0.5.5"
65
+ "x-contract-version": "0.5.7"
66
66
  }
@@ -588,6 +588,61 @@
588
588
  "title": "RenderSpec",
589
589
  "type": "object"
590
590
  },
591
+ "ReplaySpec": {
592
+ "additionalProperties": false,
593
+ "description": "a.k.a. `seq.Replay` — declares that the platform records the eval's 3D replay: each step every node's world\ntransform, drawn in the owner's browser after the eval. `engine` picks the platform's\nadapter that reads the simulation's state (\"mujoco\", \"sapien\", \"isaac\"); \"own\" means the benchmark hands the frames itself\n(`seq.replay.scene` / `seq.replay.frame`); None takes the benchmark's own `engine`. `state` names the benchmark\nmethod an adapter calls for the engine's state of an env (or of a batched benchmark's slots), `looks` the one that\nsays what the adapter cannot read - a colour or a visibility each episode sets. Whether an eval records it is the\neval's `replay` (\"3d\" by default, \"none\").",
594
+ "properties": {
595
+ "engine": {
596
+ "anyOf": [
597
+ {
598
+ "enum": [
599
+ "mujoco",
600
+ "sapien",
601
+ "isaac",
602
+ "own"
603
+ ],
604
+ "type": "string"
605
+ },
606
+ {
607
+ "type": "null"
608
+ }
609
+ ],
610
+ "default": null,
611
+ "description": "The adapter that reads the simulation: 'mujoco' (incl. MuJoCo Warp), 'sapien', 'isaac', or 'own' (the benchmark hands frames itself); None: the benchmark's engine",
612
+ "title": "Engine"
613
+ },
614
+ "looks": {
615
+ "anyOf": [
616
+ {
617
+ "pattern": "^[A-Za-z_][A-Za-z0-9_]*$",
618
+ "type": "string"
619
+ },
620
+ {
621
+ "type": "null"
622
+ }
623
+ ],
624
+ "default": null,
625
+ "description": "The benchmark method that says each episode's colours or visibility",
626
+ "title": "Looks"
627
+ },
628
+ "state": {
629
+ "anyOf": [
630
+ {
631
+ "pattern": "^[A-Za-z_][A-Za-z0-9_]*$",
632
+ "type": "string"
633
+ },
634
+ {
635
+ "type": "null"
636
+ }
637
+ ],
638
+ "default": null,
639
+ "description": "The benchmark method an adapter calls for an env's (or slots') engine state",
640
+ "title": "State"
641
+ }
642
+ },
643
+ "title": "ReplaySpec",
644
+ "type": "object"
645
+ },
591
646
  "SecretRef": {
592
647
  "additionalProperties": false,
593
648
  "description": "A by-name reference to a secret (PolicySpec.secrets / BenchmarkSpec.secrets). Name only — the\nplatform keeps the value and injects it when a worker starts. Never carries a value.",
@@ -864,6 +919,17 @@
864
919
  ],
865
920
  "default": null
866
921
  },
922
+ "replay": {
923
+ "anyOf": [
924
+ {
925
+ "$ref": "#/$defs/ReplaySpec"
926
+ },
927
+ {
928
+ "type": "null"
929
+ }
930
+ ],
931
+ "default": null
932
+ },
867
933
  "score": {
868
934
  "default": false,
869
935
  "title": "Score",
@@ -906,5 +972,5 @@
906
972
  ],
907
973
  "title": "BenchmarkRegisterRequest",
908
974
  "type": "object",
909
- "x-contract-version": "0.5.5"
975
+ "x-contract-version": "0.5.7"
910
976
  }
@@ -278,6 +278,61 @@
278
278
  "title": "RenderSpec",
279
279
  "type": "object"
280
280
  },
281
+ "ReplaySpec": {
282
+ "additionalProperties": false,
283
+ "description": "a.k.a. `seq.Replay` — declares that the platform records the eval's 3D replay: each step every node's world\ntransform, drawn in the owner's browser after the eval. `engine` picks the platform's\nadapter that reads the simulation's state (\"mujoco\", \"sapien\", \"isaac\"); \"own\" means the benchmark hands the frames itself\n(`seq.replay.scene` / `seq.replay.frame`); None takes the benchmark's own `engine`. `state` names the benchmark\nmethod an adapter calls for the engine's state of an env (or of a batched benchmark's slots), `looks` the one that\nsays what the adapter cannot read - a colour or a visibility each episode sets. Whether an eval records it is the\neval's `replay` (\"3d\" by default, \"none\").",
284
+ "properties": {
285
+ "engine": {
286
+ "anyOf": [
287
+ {
288
+ "enum": [
289
+ "mujoco",
290
+ "sapien",
291
+ "isaac",
292
+ "own"
293
+ ],
294
+ "type": "string"
295
+ },
296
+ {
297
+ "type": "null"
298
+ }
299
+ ],
300
+ "default": null,
301
+ "description": "The adapter that reads the simulation: 'mujoco' (incl. MuJoCo Warp), 'sapien', 'isaac', or 'own' (the benchmark hands frames itself); None: the benchmark's engine",
302
+ "title": "Engine"
303
+ },
304
+ "looks": {
305
+ "anyOf": [
306
+ {
307
+ "pattern": "^[A-Za-z_][A-Za-z0-9_]*$",
308
+ "type": "string"
309
+ },
310
+ {
311
+ "type": "null"
312
+ }
313
+ ],
314
+ "default": null,
315
+ "description": "The benchmark method that says each episode's colours or visibility",
316
+ "title": "Looks"
317
+ },
318
+ "state": {
319
+ "anyOf": [
320
+ {
321
+ "pattern": "^[A-Za-z_][A-Za-z0-9_]*$",
322
+ "type": "string"
323
+ },
324
+ {
325
+ "type": "null"
326
+ }
327
+ ],
328
+ "default": null,
329
+ "description": "The benchmark method an adapter calls for an env's (or slots') engine state",
330
+ "title": "State"
331
+ }
332
+ },
333
+ "title": "ReplaySpec",
334
+ "type": "object"
335
+ },
281
336
  "SecretRef": {
282
337
  "additionalProperties": false,
283
338
  "description": "A by-name reference to a secret (PolicySpec.secrets / BenchmarkSpec.secrets). Name only — the\nplatform keeps the value and injects it when a worker starts. Never carries a value.",
@@ -313,7 +368,7 @@
313
368
  ],
314
369
  "title": "SeqFlags",
315
370
  "type": "integer",
316
- "x-contract-version": "0.5.5",
371
+ "x-contract-version": "0.5.7",
317
372
  "x-flag-names": {
318
373
  "CHECK": 65536,
319
374
  "INFER": 16,
@@ -541,6 +596,17 @@
541
596
  ],
542
597
  "default": null
543
598
  },
599
+ "replay": {
600
+ "anyOf": [
601
+ {
602
+ "$ref": "#/$defs/ReplaySpec"
603
+ },
604
+ {
605
+ "type": "null"
606
+ }
607
+ ],
608
+ "default": null
609
+ },
544
610
  "secrets": {
545
611
  "items": {
546
612
  "$ref": "#/$defs/SecretRef"
@@ -559,5 +625,5 @@
559
625
  ],
560
626
  "title": "BenchmarkSpec",
561
627
  "type": "object",
562
- "x-contract-version": "0.5.5"
628
+ "x-contract-version": "0.5.7"
563
629
  }
@@ -40,5 +40,5 @@
40
40
  ],
41
41
  "title": "BenchmarkStatusResponse",
42
42
  "type": "object",
43
- "x-contract-version": "0.5.5"
43
+ "x-contract-version": "0.5.7"
44
44
  }
@@ -79,5 +79,5 @@
79
79
  ],
80
80
  "title": "BootstrapResponse",
81
81
  "type": "object",
82
- "x-contract-version": "0.5.5"
82
+ "x-contract-version": "0.5.7"
83
83
  }
@@ -72,5 +72,5 @@
72
72
  ],
73
73
  "title": "Camera",
74
74
  "type": "object",
75
- "x-contract-version": "0.5.5"
75
+ "x-contract-version": "0.5.7"
76
76
  }
@@ -23,5 +23,5 @@
23
23
  ],
24
24
  "title": "ClampInfo",
25
25
  "type": "object",
26
- "x-contract-version": "0.5.5"
26
+ "x-contract-version": "0.5.7"
27
27
  }
@@ -23,5 +23,5 @@
23
23
  ],
24
24
  "title": "CodeBundle",
25
25
  "type": "object",
26
- "x-contract-version": "0.5.5"
26
+ "x-contract-version": "0.5.7"
27
27
  }
@@ -96,5 +96,5 @@
96
96
  },
97
97
  "title": "Concurrency",
98
98
  "type": "object",
99
- "x-contract-version": "0.5.5"
99
+ "x-contract-version": "0.5.7"
100
100
  }
@@ -55,5 +55,5 @@
55
55
  ],
56
56
  "title": "Conditions",
57
57
  "type": "object",
58
- "x-contract-version": "0.5.5"
58
+ "x-contract-version": "0.5.7"
59
59
  }