sequence-base 0.5.0__tar.gz → 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. {sequence_base-0.5.0 → sequence_base-0.6.0}/PKG-INFO +1 -1
  2. {sequence_base-0.5.0 → sequence_base-0.6.0}/pyproject.toml +1 -1
  3. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/__init__.py +1 -3
  4. sequence_base-0.6.0/sequence_base/codec/compressed.py +174 -0
  5. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/constants/limits.py +1 -1
  6. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/constants/version.py +3 -1
  7. sequence_base-0.6.0/sequence_base/construct.py +173 -0
  8. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/contract/__init__.py +27 -16
  9. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/contract/benchmarks.py +48 -19
  10. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/contract/connect.py +13 -22
  11. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/contract/deployments.py +14 -12
  12. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/contract/flags.py +2 -1
  13. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/contract/observe.py +8 -3
  14. sequence_base-0.6.0/sequence_base/contract/own_io.py +97 -0
  15. sequence_base-0.6.0/sequence_base/contract/planner.py +234 -0
  16. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/contract/policy.py +38 -25
  17. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schema_export.py +24 -24
  18. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/ActRequest.json +1 -1
  19. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/ActResponse.json +1 -1
  20. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/ActionChunk.json +1 -1
  21. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/ActionContract.json +1 -1
  22. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/ActionStep.json +1 -1
  23. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/AdapterPair.json +1 -1
  24. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/AddLocal.json +1 -1
  25. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/Autoscaler.json +1 -1
  26. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/BenchmarkRegisterRequest.json +2 -2
  27. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/BenchmarkSpec.json +50 -4
  28. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/BenchmarkStatusResponse.json +1 -1
  29. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/BootstrapResponse.json +1 -1
  30. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/Camera.json +1 -1
  31. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/CodeBundle.json +1 -1
  32. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/Concurrency.json +1 -1
  33. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/ConnectRequest.json +1 -27
  34. sequence_base-0.6.0/sequence_base/schemas/ConnectResponse.json +204 -0
  35. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/DeploymentAccepted.json +1 -1
  36. sequence_base-0.6.0/sequence_base/schemas/DeploymentPolicy.json +332 -0
  37. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/DeploymentRequest.json +28 -300
  38. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/DeploymentStatusResponse.json +1 -1
  39. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/ErrorBody.json +1 -1
  40. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/ErrorCodes.json +1 -1
  41. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/Estimate.json +1 -1
  42. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/EvalDryRunResponse.json +1 -1
  43. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/EvalReport.json +1 -1
  44. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/EvalRequest.json +1 -1
  45. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/EvalResponse.json +1 -1
  46. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/ImageFrame.json +1 -1
  47. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/ImageSpec.json +1 -1
  48. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/ImageStepAddLocal.json +1 -1
  49. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/ImageStepApt.json +1 -1
  50. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/ImageStepEnv.json +1 -1
  51. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/ImageStepProject.json +1 -1
  52. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/ImageStepRun.json +1 -1
  53. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/ImageStepTemplateFiles.json +1 -1
  54. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/ImageStepUvPip.json +1 -1
  55. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/InferParam.json +1 -1
  56. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/Lease.json +1 -1
  57. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/MetricAgg.json +1 -1
  58. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/Observation.json +1 -1
  59. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/ObservationContract.json +1 -1
  60. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/ObserveRequest.json +1 -1
  61. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/ObserveResponse.json +1 -1
  62. sequence_base-0.6.0/sequence_base/schemas/PlanAppend.json +13 -0
  63. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/PlanAppended.json +1 -1
  64. sequence_base-0.6.0/sequence_base/schemas/PlannerMember.json +88 -0
  65. sequence_base-0.5.0/sequence_base/schemas/GroupObservation.json → sequence_base-0.6.0/sequence_base/schemas/PlannerObservation.json +4 -4
  66. sequence_base-0.6.0/sequence_base/schemas/PlannerObserve.json +13 -0
  67. sequence_base-0.6.0/sequence_base/schemas/PlannerObserved.json +16 -0
  68. sequence_base-0.6.0/sequence_base/schemas/PlannerPlanIn.json +33 -0
  69. sequence_base-0.6.0/sequence_base/schemas/PlannerPlans.json +66 -0
  70. sequence_base-0.5.0/sequence_base/schemas/TeamPlansWritten.json → sequence_base-0.6.0/sequence_base/schemas/PlannerPlansWritten.json +3 -3
  71. sequence_base-0.5.0/sequence_base/schemas/TeamMember.json → sequence_base-0.6.0/sequence_base/schemas/PlannerRobot.json +4 -9
  72. sequence_base-0.6.0/sequence_base/schemas/PlannerRound.json +200 -0
  73. sequence_base-0.5.0/sequence_base/schemas/TeamGroupsRequest.json → sequence_base-0.6.0/sequence_base/schemas/PlannerRoundRequest.json +3 -3
  74. sequence_base-0.6.0/sequence_base/schemas/PlannerSpec.json +22 -0
  75. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/PolicyRegistration.json +21 -293
  76. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/PolicySpec.json +19 -291
  77. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/Project.json +1 -1
  78. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/Proprioception.json +1 -1
  79. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/ReplaySpec.json +1 -1
  80. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/Resources.json +1 -1
  81. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/RolloutResult.json +1 -1
  82. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/SecretCreateRequest.json +1 -1
  83. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/SecretInfo.json +1 -1
  84. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/SecretRef.json +1 -1
  85. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/SeqFlags.json +1 -1
  86. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/SettingsChange.json +1 -1
  87. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/SettingsChanged.json +1 -1
  88. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/SettingsView.json +1 -1
  89. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/Standby.json +1 -1
  90. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/TaskAgg.json +1 -1
  91. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/TemplateFiles.json +1 -1
  92. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/Tensors.json +2 -2
  93. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/Timeouts.json +1 -1
  94. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/UploadTarget.json +1 -1
  95. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/VolumeFile.json +1 -1
  96. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/VolumeInfo.json +1 -1
  97. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/VolumeMount.json +1 -1
  98. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/VolumeRef.json +1 -1
  99. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/VolumeUploadRequest.json +1 -1
  100. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/VolumeUploadResponse.json +1 -1
  101. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/WarmingError.json +1 -1
  102. sequence_base-0.5.0/sequence_base/construct.py +0 -124
  103. sequence_base-0.5.0/sequence_base/contract/obs_schedule.py +0 -170
  104. sequence_base-0.5.0/sequence_base/contract/own_io.py +0 -208
  105. sequence_base-0.5.0/sequence_base/contract/team.py +0 -185
  106. sequence_base-0.5.0/sequence_base/schemas/ConnectResponse.json +0 -461
  107. sequence_base-0.5.0/sequence_base/schemas/DeploymentPolicy.json +0 -604
  108. sequence_base-0.5.0/sequence_base/schemas/GroupObserve.json +0 -13
  109. sequence_base-0.5.0/sequence_base/schemas/GroupObserved.json +0 -16
  110. sequence_base-0.5.0/sequence_base/schemas/Observations.json +0 -264
  111. sequence_base-0.5.0/sequence_base/schemas/PlanAppend.json +0 -13
  112. sequence_base-0.5.0/sequence_base/schemas/Stream.json +0 -193
  113. sequence_base-0.5.0/sequence_base/schemas/TeamGroup.json +0 -120
  114. sequence_base-0.5.0/sequence_base/schemas/TeamGroups.json +0 -135
  115. sequence_base-0.5.0/sequence_base/schemas/TeamPlans.json +0 -51
  116. sequence_base-0.5.0/sequence_base/schemas/TeamSpec.json +0 -22
  117. {sequence_base-0.5.0 → sequence_base-0.6.0}/.gitignore +0 -0
  118. {sequence_base-0.5.0 → sequence_base-0.6.0}/README.md +0 -0
  119. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/auth.py +0 -0
  120. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/by_name.py +0 -0
  121. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/codec/__init__.py +0 -0
  122. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/codec/image.py +0 -0
  123. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/codec/marked.py +0 -0
  124. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/constants/__init__.py +0 -0
  125. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/constants/cards.py +0 -0
  126. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/constants/headers.py +0 -0
  127. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/contract/accounts.py +0 -0
  128. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/contract/act.py +0 -0
  129. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/contract/adapters.py +0 -0
  130. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/contract/errors.py +0 -0
  131. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/contract/eval.py +0 -0
  132. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/contract/io.py +0 -0
  133. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/contract/retired.py +0 -0
  134. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/contract/settings.py +0 -0
  135. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/contract/templates.py +0 -0
  136. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/contract/volumes.py +0 -0
  137. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/contract/wire.py +0 -0
  138. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/episode.py +0 -0
  139. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/errors/__init__.py +0 -0
  140. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/errors/transport.py +0 -0
  141. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/msgpack_io.py +0 -0
  142. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/obs/__init__.py +0 -0
  143. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/obs/log.py +0 -0
  144. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/obs/rid.py +0 -0
  145. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/presets/__init__.py +0 -0
  146. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/presets/frames.py +0 -0
  147. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/presets/robot_arm.py +0 -0
  148. {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/telemetry/__init__.py +0 -0
  149. {sequence_base-0.5.0 → sequence_base-0.6.0}/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.5.0
3
+ Version: 0.6.0
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sequence-base"
3
- version = "0.5.0"
3
+ version = "0.6.0"
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"]
@@ -86,8 +86,6 @@ from sequence_base.contract import (
86
86
  PolicyConstructor,
87
87
  PolicyRegistration,
88
88
  PolicySpec,
89
- Observations,
90
- Stream,
91
89
  Tensors,
92
90
  Proprioception,
93
91
  ReplaySpec,
@@ -145,7 +143,7 @@ __all__ = [
145
143
  "FROZEN_PATH", "TemplateFiles", "ImageStepTemplateFiles", "Project", "ImageStepProject", "ImageStep", "ImageSpec",
146
144
  "VolumeMount", "VolumeRef", "Concurrency", "ActionContract", "Camera", "ObservationContract",
147
145
  "PolicySpec", "PolicyRegistration", "PolicyConstructor",
148
- "Observations", "Stream", "Tensors",
146
+ "Tensors",
149
147
 
150
148
  "MAX_CODE_BUNDLE_BYTES", "CodeBundle",
151
149
  "Resources", "Autoscaler", "Timeouts", "DeploymentPolicy", "InferParam", "DeploymentRequest",
@@ -0,0 +1,174 @@
1
+ """
2
+ Compressed observe items: what a robot's SDK puts in place of a large array or bytes of its observe data when sending it
3
+ compressed saves more upload time than compressing it costs, and what a worker turns back - losslessly, so every reader
4
+ after the worker is handed the bytes the robot built.
5
+
6
+
7
+
8
+ In one sentence: the robot's SDK compresses an unmarked numpy array or bytes of 16 KiB or more with zstd level 1 (zlib
9
+ level 1 where it has no zstd) and puts the map below in its place → the worker names contract.observe.ZSTD_FORMAT, so
10
+ only it is sent such maps → as a post comes, before anything keeps it, expand() turns every map back into the array or
11
+ bytes it was (np.frombuffer: read-only, as msgpack_io reads an array) and says how many bytes that added.
12
+
13
+ The map, version 1 (inside the observe body's msgpack; `data` is bin):
14
+ {"__seq__": "compressed", "v": 1, "codec": "zstd" | "zlib", "n": <bytes before>, "data": <compressed>}
15
+ bytes as they were
16
+ ... and "type": numpy's string for its dtype, "shape": the array's - an array, its data C-ordered
17
+ `n` is checked against the bytes the data turns back into, and against the most one item may hold (the bytes of an
18
+ episode's observations a worker keeps): a map that says less than it holds is refused rather than decompressed past it.
19
+
20
+ WHY the observation format's reserved key: marked items (codec/marked.py) already take a map with "__seq__", so one key
21
+ is reserved in observe data, not two; a compressed item is never inside a marked one - the SDK compresses after encoding
22
+ its markers and leaves their maps as they are - and expand() does not look inside one.
23
+ WHY decompressed as the post comes, at the parent: the stream, Field data and the standby's saves all read a post's
24
+ frames as it comes, and each must keep the robot's data, not the wire's.
25
+
26
+ wire = compress(frame, "zstd") # a reference encoder (the SDK has its own, with its rule)
27
+ value, grew = expand(post_inputs, limit=64 << 20) # the worker, as a post comes
28
+ """
29
+ from __future__ import annotations
30
+
31
+ import zlib
32
+ from typing import Any
33
+
34
+ from sequence_base.codec.marked import MARK, is_marked
35
+
36
+ __all__ = ["CODECS", "KIND", "VERSION", "CompressedError", "compress", "expand", "holds", "is_compressed"]
37
+
38
+ KIND = "compressed"
39
+ VERSION = 1
40
+ CODECS = ("zstd", "zlib")
41
+ _LEVEL = 1
42
+
43
+
44
+ class CompressedError(ValueError):
45
+ """A compressed item that does not read: an unknown codec, data that does not turn back into the `n` bytes it says,
46
+ an `n` past the limit, or an array's type or shape that does not fit them."""
47
+
48
+
49
+ def is_compressed(value: Any) -> bool:
50
+ return isinstance(value, dict) and value.get(MARK) == KIND
51
+
52
+
53
+ def _zstd():
54
+ """zstd's (compress, decompress) - zstandard's, else Python 3.14's compression.zstd. The decompress takes the bytes
55
+ the item says it holds and refuses a frame whose header says other: a header is what sizes the buffer."""
56
+ try:
57
+ import zstandard
58
+
59
+ def unzstd(b: bytes, n: int) -> bytes:
60
+ said = zstandard.frame_content_size(b)
61
+ if said not in (-1, n):
62
+ raise CompressedError(f"a zstd item whose frame says {said} bytes and whose map says {n}")
63
+ return zstandard.ZstdDecompressor().decompress(b, max_output_size=n)
64
+
65
+ return (lambda b: zstandard.ZstdCompressor(level=_LEVEL).compress(b), unzstd)
66
+ except ImportError:
67
+ from compression import zstd
68
+
69
+ def unzstd(b: bytes, n: int) -> bytes:
70
+ said = zstd.get_frame_info(b).decompressed_size
71
+ if said not in (None, n):
72
+ raise CompressedError(f"a zstd item whose frame says {said} bytes and whose map says {n}")
73
+ d = zstd.ZstdDecompressor()
74
+ raw = d.decompress(b, max_length=n)
75
+ if not d.eof:
76
+ raise CompressedError(f"a zstd item that holds more than the {n} bytes it says")
77
+ return raw
78
+
79
+ return (lambda b: zstd.compress(b, level=_LEVEL), unzstd)
80
+
81
+
82
+ def compress(value: Any, codec: str = "zstd") -> dict:
83
+ """`value` - a numpy array or bytes - as its compressed item: a reference encoder, for tests and the platform's own
84
+ senders; the robot's SDK decides by its own rule what to compress."""
85
+ import numpy as np
86
+
87
+ if isinstance(value, np.ndarray):
88
+ raw = np.ascontiguousarray(value).tobytes()
89
+ extra = {"type": value.dtype.str, "shape": list(value.shape)}
90
+ else:
91
+ raw, extra = bytes(value), {}
92
+ if codec == "zstd":
93
+ data = _zstd()[0](raw)
94
+ elif codec == "zlib":
95
+ data = zlib.compress(raw, _LEVEL)
96
+ else:
97
+ raise CompressedError(f"no codec {codec!r}: {', '.join(CODECS)}")
98
+ return dict([(MARK, KIND), ("v", VERSION), ("codec", codec), ("n", len(raw)), *extra.items(), ("data", data)])
99
+
100
+
101
+ def _raw(item: dict, limit: int) -> bytes:
102
+ codec, n, data = item.get("codec"), item.get("n"), item.get("data")
103
+ if codec not in CODECS:
104
+ raise CompressedError(f"a compressed item of codec {codec!r}: {', '.join(CODECS)} are read")
105
+ if isinstance(n, bool) or not isinstance(n, int) or not 0 <= n <= limit:
106
+ raise CompressedError(f"a compressed item of {n!r} bytes: at most {limit} are taken")
107
+ if not isinstance(data, (bytes, bytearray)):
108
+ raise CompressedError("a compressed item whose data is not bytes")
109
+ try:
110
+ if codec == "zstd":
111
+ raw = _zstd()[1](bytes(data), n)
112
+ else:
113
+ d = zlib.decompressobj()
114
+ raw = d.decompress(bytes(data), n)
115
+ if d.unconsumed_tail or not d.eof:
116
+ raise CompressedError(f"a zlib item that holds more than the {n} bytes it says")
117
+ except CompressedError:
118
+ raise
119
+ except Exception as exc:
120
+ raise CompressedError(f"a {codec} item that does not read: {exc}") from None
121
+ if len(raw) != n:
122
+ raise CompressedError(f"a {codec} item of {len(raw)} bytes that says it holds {n}")
123
+ return raw
124
+
125
+
126
+ def _value(item: dict, limit: int) -> Any:
127
+ raw = _raw(item, limit)
128
+ if "type" not in item and "shape" not in item:
129
+ return raw
130
+ import numpy as np
131
+
132
+ from sequence_base.msgpack_io import MsgpackError, _dtype
133
+
134
+ shape = item.get("shape")
135
+ if not isinstance(shape, list) or not all(isinstance(d, int) and not isinstance(d, bool) and d >= 0 for d in shape):
136
+ raise CompressedError("a compressed array whose shape is not a list of non-negative ints")
137
+ try:
138
+ dtype = _dtype(item.get("type"))
139
+ except MsgpackError as exc:
140
+ raise CompressedError(f"a compressed array: {exc}") from None
141
+ if dtype.itemsize * int(np.prod(shape, dtype=np.int64)) != len(raw):
142
+ raise CompressedError(f"a compressed {dtype} array of shape {shape} that holds {len(raw)} bytes")
143
+ return np.frombuffer(raw, dtype=dtype).reshape(shape)
144
+
145
+
146
+ def expand(value: Any, *, limit: int) -> "tuple[Any, int]":
147
+ """`value` with every compressed item in it - in dicts and lists at any depth, never inside a marked item - turned
148
+ back into the array or bytes it was, the same objects where none is; and how many bytes that added. `limit`: the
149
+ most bytes one item may turn back into. CompressedError for an item that does not read."""
150
+ grew = [0]
151
+
152
+ def walk(v: Any) -> Any:
153
+ if isinstance(v, dict):
154
+ if is_compressed(v):
155
+ out = _value(v, limit)
156
+ grew[0] += max(0, int(v["n"]) - len(v["data"]))
157
+ return out
158
+ if is_marked(v) or not holds(v):
159
+ return v
160
+ return {k: walk(x) for k, x in v.items()}
161
+ if isinstance(v, list):
162
+ return [walk(x) for x in v] if holds(v) else v
163
+ return v
164
+
165
+ return walk(value), grew[0]
166
+
167
+
168
+ def holds(value: Any) -> bool:
169
+ """Whether `value` holds a compressed item - in dicts and lists at any depth, not inside a marked item."""
170
+ if isinstance(value, dict):
171
+ return is_compressed(value) or (not is_marked(value) and any(holds(v) for v in value.values()))
172
+ if isinstance(value, list):
173
+ return any(holds(v) for v in value)
174
+ return False
@@ -36,4 +36,4 @@ METRIC_UNIT_MAX = 16
36
36
  MAX_STREAM_FRAMES = 1024
37
37
  MAX_STREAM_BYTES = 64 * 1024 * 1024
38
38
  MAX_WORKER_STREAM_BYTES = 1024 * 1024 * 1024
39
- STREAM_WAIT_S = 10.0
39
+ STREAM_WAIT_S = 3.0
@@ -4,7 +4,9 @@
4
4
 
5
5
 
6
6
 
7
- CONTRACT_VERSION = "0.5.32"
7
+ CONTRACT_VERSION = "0.5.34"
8
+
9
+
8
10
 
9
11
 
10
12
 
@@ -0,0 +1,173 @@
1
+ """
2
+ How a class built by its constructor is built - a policy's or a planner's (PolicyRegistration.constructor), a benchmark's
3
+ (BenchmarkSpec.constructor): its constructor - or the factory it names, a classmethod of it or a module's function -
4
+ called with the arguments its decorator was given (init=), once the forms in them are filled in.
5
+
6
+ In one sentence: a value {"$config": path, "$set": {...}} becomes the YAML or JSON file at `path` - from the folder of
7
+ the class's file - read where the class runs, with `$set`'s keys laid over its top level → in every string, a policy's
8
+ "{weights}" becomes the weights directory and "{weights.<part>}" a named part's, a benchmark's "{num_worlds}" how many
9
+ worlds this eval builds and "{policy.<field>}" what the evaluated policy declares (POLICY_FIELDS) - a string that is one
10
+ form alone becomes its value as it is, an int or None, one among other text its value as text → `cls.<make>(*args,
11
+ **kwargs)`, or `function(*args, **kwargs)` for make="package.module:function" - the module imported where the class
12
+ runs - or `cls(*args, **kwargs)` without a factory.
13
+
14
+ WHY a config file is read where the policy runs: it is part of the project (XPolicyLab's deploy.yml beside its
15
+ model.py) and goes up with it, so one copy - the one the policy runs with - is read, and "{weights}" in it means the
16
+ worker's directory.
17
+
18
+ WHY a benchmark's too: its worlds are built after the machine's snapshot by the class's own constructor - a repository's
19
+ env as it is, handed how many worlds and the policy's rate where its own code takes them - where a method of the
20
+ platform's (@seq.setup) built them before.
21
+
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import importlib
26
+ import json
27
+ import os
28
+ import re
29
+ from typing import Any
30
+
31
+ __all__ = ["CONFIG_KEY", "FORMS", "POLICY_FIELDS", "SET_KEY", "ConstructError", "build", "config", "expand",
32
+ "is_config", "maker", "read_config"]
33
+
34
+ CONFIG_KEY, SET_KEY = "$config", "$set"
35
+
36
+
37
+
38
+ POLICY_FIELDS = ("hz", "action_chunk_len", "action_chunk_exec_len")
39
+
40
+
41
+ FORMS = re.compile(r"\{(?:weights(?:\.(?P<part>[A-Za-z_][A-Za-z0-9_-]*))?|(?P<worlds>num_worlds)"
42
+ r"|policy\.(?P<field>[A-Za-z_][A-Za-z0-9_]*))\}")
43
+
44
+
45
+ class ConstructError(ValueError):
46
+ """Arguments that cannot be filled in: a config file missing, unreadable or of another kind, a weights placeholder
47
+ with no weights - or no such part - to fill it, "{num_worlds}" where no worlds are built, or a policy field no policy
48
+ declares."""
49
+
50
+
51
+ def config(path: str, **overrides: Any) -> dict:
52
+ """A config file's contents with `overrides` laid over its top level, as the worker reads it (`seq.config`)."""
53
+ if not isinstance(path, str) or not path:
54
+ raise ConstructError("a config file is a path, from the folder of the class's file")
55
+ if os.path.isabs(path):
56
+ raise ConstructError(f"{path}: give a config file's path from the folder of the class's file - the policy runs "
57
+ "it where the upload puts it")
58
+ return {CONFIG_KEY: path, SET_KEY: dict(overrides)}
59
+
60
+
61
+ def is_config(value: Any) -> bool:
62
+ return isinstance(value, dict) and set(value) == {CONFIG_KEY, SET_KEY} and isinstance(value[CONFIG_KEY], str)
63
+
64
+
65
+ def read_config(path: str) -> Any:
66
+ """The YAML (.yml, .yaml) or JSON (.json) file at `path`."""
67
+ kind = os.path.splitext(path)[1].lower()
68
+ if kind not in (".json", ".yml", ".yaml"):
69
+ raise ConstructError(f"{path}: a config file is .json, .yml or .yaml")
70
+ if not os.path.isfile(path):
71
+ raise ConstructError(f"{path}: no such config file")
72
+ with open(path, encoding="utf-8") as f:
73
+ if kind == ".json":
74
+ return json.load(f)
75
+ try:
76
+ import yaml
77
+ except ImportError:
78
+ raise ConstructError(f"{path}: reading YAML needs pyyaml") from None
79
+ return yaml.safe_load(f)
80
+
81
+
82
+ def expand(value: Any, *, base_dir: str, weights: Any = None, worlds: int | None = None, policy: Any = None) -> Any:
83
+ """`value` with its config files read (from `base_dir`) and its forms filled: a policy's from `weights` - the
84
+ weights directory, or {part: directory} for named parts, or None for a policy without weights - a benchmark's from
85
+ `worlds`, how many worlds this eval builds, and `policy`, what the evaluated policy declares ({field: value}, or an
86
+ object with them; None where none is evaluated, each field None). A string that is one form alone becomes its value
87
+ as it is; one form among other text, its value as text."""
88
+ forms = {"base_dir": base_dir, "weights": weights, "worlds": worlds, "policy": policy}
89
+ if is_config(value):
90
+ loaded = read_config(os.path.join(base_dir, value[CONFIG_KEY]))
91
+ if value[SET_KEY] and not isinstance(loaded, dict):
92
+ raise ConstructError(f"{value[CONFIG_KEY]} holds a {type(loaded).__name__}, which has no keys to set")
93
+ merged = {**loaded, **value[SET_KEY]} if isinstance(loaded, dict) else loaded
94
+ return expand(merged, **forms)
95
+ if isinstance(value, dict):
96
+ return {k: expand(v, **forms) for k, v in value.items()}
97
+ if isinstance(value, (list, tuple)):
98
+ return [expand(v, **forms) for v in value]
99
+ if isinstance(value, str):
100
+ alone = FORMS.fullmatch(value)
101
+ if alone:
102
+ return _filled(alone, weights, worlds, policy)
103
+ return FORMS.sub(lambda m: str(_filled(m, weights, worlds, policy)), value)
104
+ return value
105
+
106
+
107
+ def _filled(form: re.Match, weights: Any, worlds: int | None, policy: Any) -> Any:
108
+ if form.group("worlds"):
109
+ if worlds is None:
110
+ raise ConstructError("{num_worlds} is how many worlds this eval builds, and none is handed: a benchmark of one "
111
+ "world an instance takes none")
112
+ return worlds
113
+ field = form.group("field")
114
+ if field is not None:
115
+ if field not in POLICY_FIELDS:
116
+ raise ConstructError(f"{{policy.{field}}}: a policy declares {', '.join(POLICY_FIELDS)}")
117
+ if policy is None:
118
+ return None
119
+ return policy.get(field) if isinstance(policy, dict) else getattr(policy, field, None)
120
+ return _weights_of(form.group("part"), weights)
121
+
122
+
123
+ def _weights_of(part: str | None, weights: Any) -> str:
124
+ if weights is None:
125
+ raise ConstructError("{weights} names the weights directory, and this policy declares no weights=")
126
+ if isinstance(weights, dict):
127
+ if part is None:
128
+ raise ConstructError(f"its weights come in named parts ({', '.join(sorted(weights))}): name one, "
129
+ f"{{weights.<part>}}")
130
+ if part not in weights:
131
+ raise ConstructError(f"{{weights.{part}}}: its weights have no part {part!r} "
132
+ f"({', '.join(sorted(weights))})")
133
+ return str(weights[part])
134
+ if part is not None:
135
+ raise ConstructError(f"{{weights.{part}}} names a part, and its weights come whole: {{weights}}")
136
+ return str(weights)
137
+
138
+
139
+ def build(cls: type, constructor: Any, *, base_dir: str, weights: Any = None, worlds: int | None = None,
140
+ policy: Any = None, params: dict | None = None) -> Any:
141
+ """The instance `constructor` (a PolicyConstructor) makes of `cls`: `cls.<make>(*args, **kwargs)`, the function
142
+ make="package.module:function" names called so, or `cls(...)` - its arguments filled (expand), and `params`, what
143
+ an eval set of the constructor's other arguments (--param benchmark.NAME=VALUE), laid over them. A keyword argument
144
+ that is "{policy.<field>}" alone is left out while that field is None - no policy evaluated, at a registration
145
+ check, or one that declares none - so the constructor's own default holds, as a benchmark's own code did with what
146
+ @seq.setup was handed before."""
147
+ forms = {"base_dir": base_dir, "weights": weights, "worlds": worlds, "policy": policy}
148
+ args = expand(list(constructor.args), **forms)
149
+ kwargs = {}
150
+ for name, value in dict(constructor.kwargs).items():
151
+ filled = expand(value, **forms)
152
+ alone = FORMS.fullmatch(value) if isinstance(value, str) else None
153
+ if filled is None and alone is not None and alone.group("field") is not None:
154
+ continue
155
+ kwargs[name] = filled
156
+ return maker(cls, constructor.make)(*args, **{**kwargs, **(params or {})})
157
+
158
+
159
+ def maker(cls: type, make: str | None) -> Any:
160
+ """What builds the instance: `cls` for no make=, its classmethod or staticmethod `make`, or - make written
161
+ "package.module:function" - that module's function, the module imported (as the policy's code imports it).
162
+ """
163
+ if not make:
164
+ return cls
165
+ module, colon, name = make.rpartition(":")
166
+ if not colon:
167
+ return getattr(cls, make)
168
+ found = getattr(importlib.import_module(module), name, None)
169
+ if found is None:
170
+ raise ConstructError(f"make={make!r}: {module} has no function {name!r}")
171
+ if not callable(found):
172
+ raise ConstructError(f"make={make!r}: {module}.{name} is not callable ({type(found).__name__})")
173
+ return found
@@ -45,7 +45,7 @@ from sequence_base.contract.policy import (
45
45
  )
46
46
 
47
47
 
48
- from sequence_base.contract.own_io import Observations, Stream, Tensors
48
+ from sequence_base.contract.own_io import OBSERVATIONS_GONE, Tensors
49
49
 
50
50
 
51
51
  from sequence_base.contract.deployments import (
@@ -124,20 +124,30 @@ from sequence_base.contract.settings import (
124
124
  )
125
125
 
126
126
 
127
- from sequence_base.contract.team import (
128
- TEAM_DEFAULT_GROUP,
129
- GroupObservation,
130
- GroupObserve,
131
- GroupObserved,
127
+
128
+ from sequence_base.contract.planner import (
129
+ KEPT,
130
+ MOST_LAYERS,
131
+ PLANS_MOST,
132
+ STATE_MOST,
133
+ PLAN_ON_A_POLICY,
134
+ PLANNER_ID,
135
+ POLICY_ID,
136
+ ROBOT_ID,
137
+ TEAM_GONE,
132
138
  PlanAppend,
133
139
  PlanAppended,
134
- TeamGroup,
135
- TeamGroups,
136
- TeamGroupsRequest,
137
- TeamMember,
138
- TeamPlans,
139
- TeamPlansWritten,
140
- TeamSpec,
140
+ PlannerMember,
141
+ PlannerObservation,
142
+ PlannerObserve,
143
+ PlannerObserved,
144
+ PlannerPlanIn,
145
+ PlannerPlans,
146
+ PlannerPlansWritten,
147
+ PlannerRobot,
148
+ PlannerRound,
149
+ PlannerRoundRequest,
150
+ PlannerSpec,
141
151
  )
142
152
 
143
153
 
@@ -154,7 +164,7 @@ __all__ = [
154
164
  "TemplateFiles", "ImageStepTemplateFiles", "Project", "ImageStepProject", "FROZEN_PATH", "ImageStep", "ImageSpec",
155
165
  "VolumeMount", "VolumeRef", "Concurrency", "ActionContract", "Camera", "ObservationContract",
156
166
  "PolicySpec", "PolicyRegistration", "PolicyConstructor",
157
- "Observations", "Stream", "Tensors",
167
+ "OBSERVATIONS_GONE", "Tensors",
158
168
 
159
169
  "MAX_CODE_BUNDLE_BYTES", "CodeBundle",
160
170
  "Resources", "Autoscaler", "Timeouts", "DeploymentPolicy", "InferParam", "DeploymentRequest",
@@ -174,8 +184,9 @@ __all__ = [
174
184
  "SETTINGS", "Setting", "SettingsChange", "Applied", "SettingState", "SettingsView", "SettingChanged",
175
185
  "SettingsChanged",
176
186
 
177
- "TEAM_DEFAULT_GROUP", "TeamSpec", "TeamGroupsRequest", "GroupObservation", "TeamMember", "TeamGroup", "TeamGroups",
178
- "TeamPlans", "TeamPlansWritten", "PlanAppend", "PlanAppended", "GroupObserve", "GroupObserved",
187
+ "KEPT", "MOST_LAYERS", "PLANS_MOST", "STATE_MOST", "PLAN_ON_A_POLICY", "PLANNER_ID", "POLICY_ID", "ROBOT_ID", "TEAM_GONE", "PlannerSpec",
188
+ "PlannerRoundRequest", "PlannerRobot", "PlannerMember", "PlannerObservation", "PlannerPlanIn", "PlannerRound",
189
+ "PlannerPlans", "PlannerPlansWritten", "PlanAppend", "PlanAppended", "PlannerObserve", "PlannerObserved",
179
190
 
180
191
  "TEMPLATE_SCHEME", "parse_template",
181
192
 
@@ -1,9 +1,10 @@
1
1
  """
2
- Contract 5 (registration half) - a benchmark is the env you already have, run as it is: a short class of yours marks
3
- three methods - @seq.setup builds this eval's worlds after the machine's snapshot, @seq.reset starts an episode of a task
4
- at a seed, @seq.step plays one action and says what came of it - and declares its tasks, its seeds and how long an episode
5
- may run. The platform reads none of its observations or actions: they go to the policy, and come from it, as a robot's
6
- do. The eval *run* wire types (RolloutResult, EvalReport, ...) live in contract/eval.py.
2
+ Contract 5 (registration half) - a benchmark is the env you already have, run as it is: its class is built by its own
3
+ constructor after the machine's snapshot (init= its arguments, "{num_worlds}" and "{policy.<field>}" filled), a short class
4
+ of yours marks two methods - @seq.reset starts an episode of a task at a seed, @seq.step plays one action and says what
5
+ came of it - and declares its tasks, its seeds and how long an episode may run. The platform reads none of its
6
+ observations or actions: they go to the policy, and come from it, as a robot's do. The eval *run* wire types
7
+ (RolloutResult, EvalReport, ...) live in contract/eval.py.
7
8
 
8
9
 
9
10
  """
@@ -18,7 +19,8 @@ from sequence_base.constants.limits import MAX_ENVS, TASK_DESCRIPTION_MAX
18
19
  from sequence_base.contract.accounts import SecretRef
19
20
  from sequence_base.contract.deployments import CodeBundle, DeploymentState, InferParam
20
21
  from sequence_base.contract.flags import BENCHMARK_BINDABLE, SeqFlags
21
- from sequence_base.contract.policy import KIND_NAME, ImageSpec, VolumeRef
22
+ from sequence_base.contract.policy import KIND_NAME, ImageSpec, PolicyConstructor, VolumeRef
23
+ from sequence_base.construct import FORMS
22
24
 
23
25
  __all__ = [
24
26
  "ReplaySpec",
@@ -31,6 +33,27 @@ __all__ = [
31
33
  TASK_NAME = r"^[A-Za-z0-9][A-Za-z0-9._/:-]{0,127}$"
32
34
 
33
35
 
36
+
37
+ SETUP_GONE = ("@seq.setup is gone: a benchmark builds its worlds in its constructor, after the machine's snapshot - "
38
+ 'init= its arguments, "{num_worlds}" how many worlds this eval builds and "{policy.<field>}" what the '
39
+ "evaluated policy declares; a benchmark registered with @seq.setup is registered again with sequence-ai "
40
+ "0.51.1 or later (seq deploy)")
41
+
42
+
43
+ def _names_worlds(constructor: PolicyConstructor | None) -> bool:
44
+ """Whether init= hands the constructor "{num_worlds}" anywhere - in a string, at any depth."""
45
+ def walk(value) -> bool:
46
+ if isinstance(value, str):
47
+ return any(m.group("worlds") for m in FORMS.finditer(value))
48
+ if isinstance(value, dict):
49
+ return any(walk(v) for v in value.values())
50
+ if isinstance(value, (list, tuple)):
51
+ return any(walk(v) for v in value)
52
+ return False
53
+
54
+ return constructor is not None and walk([constructor.args, constructor.kwargs])
55
+
56
+
34
57
  def _check_tasks(tasks: dict, seeds: list, max_seconds) -> None:
35
58
  import re as _re
36
59
 
@@ -75,11 +98,11 @@ class ReplaySpec(BaseModel):
75
98
 
76
99
 
77
100
  class BenchmarkSpec(BaseModel):
78
- """What `@seq.benchmark(...)` and its marked methods add up to, as an eval runs it. `bindings` names the methods:
79
- RESET and STEP always, SETUP when it builds its worlds after the machine's snapshot - which a vectorized env, one
80
- stepping `worlds` worlds a call (@seq.concurrent(sizes=[N]) on its step), always does. `max_seconds` is an episode's
81
- time on the robot's clock, the same for every task or {task: seconds}; `assets` are versioned scene volumes mounted
82
- at /assets; `secrets` by name."""
101
+ """What `@seq.benchmark(...)` and its marked methods add up to, as an eval runs it. `bindings` names the methods,
102
+ RESET and STEP; `constructor` the arguments its class is built with after the machine's snapshot (init=) - which a
103
+ vectorized env, one stepping `worlds` worlds a call (@seq.concurrent(sizes=[N]) on its step), hands how many,
104
+ "{num_worlds}". `max_seconds` is an episode's time on the robot's clock, the same for every task or {task: seconds};
105
+ `assets` are versioned scene volumes mounted at /assets; `secrets` by name."""
83
106
 
84
107
  model_config = ConfigDict(extra="forbid")
85
108
 
@@ -89,7 +112,7 @@ class BenchmarkSpec(BaseModel):
89
112
  seeds: list[int] = Field(..., description="The seeds an episode starts at; trial k of a task runs seeds[k % len]")
90
113
  max_seconds: float | dict[str, float] = Field(..., description="An episode's time limit, or {task: seconds}")
91
114
  handler_ref: str
92
- bindings: dict[SeqFlags, str] = Field(..., description="SETUP / RESET / STEP -> method name")
115
+ bindings: dict[SeqFlags, str] = Field(..., description="RESET / STEP -> method name")
93
116
 
94
117
 
95
118
  worlds: int | None = Field(default=None, ge=1, le=MAX_ENVS)
@@ -106,6 +129,10 @@ class BenchmarkSpec(BaseModel):
106
129
 
107
130
  engine: str | None = Field(default=None, pattern=KIND_NAME)
108
131
 
132
+
133
+
134
+ constructor: PolicyConstructor | None = None
135
+
109
136
  @model_validator(mode="after")
110
137
  def _tasks(self) -> "BenchmarkSpec":
111
138
  _check_tasks(self.tasks, self.seeds, self.max_seconds)
@@ -113,15 +140,18 @@ class BenchmarkSpec(BaseModel):
113
140
 
114
141
  @model_validator(mode="after")
115
142
  def _bindings(self) -> "BenchmarkSpec":
143
+ if SeqFlags.SETUP in self.bindings:
144
+ raise ValueError(SETUP_GONE)
116
145
  bad = set(self.bindings) - set(BENCHMARK_BINDABLE)
117
146
  if bad:
118
147
  names = ", ".join(sorted(f.name or str(f.value) for f in bad))
119
- raise ValueError(f"a benchmark marks @seq.setup, @seq.reset and @seq.step only; got {names}")
148
+ raise ValueError(f"a benchmark marks @seq.reset and @seq.step only; got {names}")
120
149
  missing = [f.name for f in (SeqFlags.RESET, SeqFlags.STEP) if f not in self.bindings]
121
150
  if missing:
122
151
  raise ValueError(f"a benchmark marks @seq.reset and @seq.step; {', '.join(missing).lower()} is missing")
123
- if self.worlds is not None and SeqFlags.SETUP not in self.bindings:
124
- raise ValueError("a vectorized benchmark (@seq.concurrent on its step) builds its worlds in @seq.setup")
152
+ if self.worlds is not None and not _names_worlds(self.constructor):
153
+ raise ValueError("a vectorized benchmark (@seq.concurrent on its step) builds its worlds in its "
154
+ 'constructor, handed how many by init= - init={"worlds": "{num_worlds}"}')
125
155
  return self
126
156
 
127
157
  @model_validator(mode="after")
@@ -132,7 +162,7 @@ class BenchmarkSpec(BaseModel):
132
162
 
133
163
  class BenchmarkRegisterRequest(BaseModel):
134
164
  """POST /v1/benchmarks - what `seq benchmark register` sends: the declaration, the code, and the parameters its
135
- @seq.setup takes beside worlds and policy (each settable by an eval's --param, with its type)."""
165
+ constructor takes with a default beside what init= hands it (each settable by an eval's --param, with its type)."""
136
166
 
137
167
  model_config = ConfigDict(extra="forbid")
138
168
 
@@ -172,9 +202,8 @@ class BenchmarkRegisterRequest(BaseModel):
172
202
  @model_validator(mode="after")
173
203
  def _param_names(self) -> "BenchmarkRegisterRequest":
174
204
  for name in self.params or {}:
175
- if not name.isidentifier() or name.startswith("_") or name in ("worlds", "policy"):
176
- raise ValueError(f"setup parameter {name!r} is not a public Python identifier other than worlds and "
177
- "policy")
205
+ if not name.isidentifier() or name.startswith("_"):
206
+ raise ValueError(f"parameter {name!r} is not a public Python identifier")
178
207
  return self
179
208
 
180
209
  @model_validator(mode="after")