sequence-ai 0.44.2__tar.gz → 0.45.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 (129) hide show
  1. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/PKG-INFO +3 -2
  2. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/README.md +2 -1
  3. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/pyproject.toml +1 -1
  4. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/named.py +11 -4
  5. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/direct.py +10 -337
  6. sequence_ai-0.45.0/sequence_ai/_core/direct_episode.py +181 -0
  7. sequence_ai-0.45.0/sequence_ai/_core/direct_observe.py +220 -0
  8. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/window.py +38 -7
  9. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/worker_transport.py +60 -18
  10. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/.gitignore +0 -0
  11. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/LICENSE +0 -0
  12. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/eval.md +0 -0
  13. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/observability.md +0 -0
  14. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/__init__.py +0 -0
  15. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/__main__.py +0 -0
  16. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/_account.py +0 -0
  17. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/_deploy_gateway.py +0 -0
  18. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/_eval_gateway.py +0 -0
  19. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/_gateway.py +0 -0
  20. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/_http.py +0 -0
  21. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/_observe_gateway.py +0 -0
  22. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/_partial.py +0 -0
  23. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/adapter.py +0 -0
  24. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/adapter_check.py +0 -0
  25. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/adapter_cli.py +0 -0
  26. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/audit_cli.py +0 -0
  27. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/base.py +0 -0
  28. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/benchmark.py +0 -0
  29. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/benchmark_cancel.py +0 -0
  30. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/benchmark_registry.py +0 -0
  31. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/benchmark_rm.py +0 -0
  32. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/benchmark_source.py +0 -0
  33. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/capacity_cli.py +0 -0
  34. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/cli.py +0 -0
  35. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/cli_json.py +0 -0
  36. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/cli_parser.py +0 -0
  37. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/clock.py +0 -0
  38. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/code_bundle.py +0 -0
  39. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/collate.py +0 -0
  40. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/columns.py +0 -0
  41. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/data_cli.py +0 -0
  42. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/deploy.py +0 -0
  43. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/deploy_cli.py +0 -0
  44. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/deployments_cli.py +0 -0
  45. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/deps_cost.py +0 -0
  46. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/discover.py +0 -0
  47. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/errors.py +0 -0
  48. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/eval.py +0 -0
  49. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/eval_cli.py +0 -0
  50. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/eval_engine.py +0 -0
  51. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/freeze.py +0 -0
  52. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/image.py +0 -0
  53. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/kinds.py +0 -0
  54. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/listing.py +0 -0
  55. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/loading.py +0 -0
  56. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/observe.py +0 -0
  57. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/ops_cli.py +0 -0
  58. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/params.py +0 -0
  59. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/policy.py +0 -0
  60. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/port/__init__.py +0 -0
  61. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/port/check.py +0 -0
  62. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/port/cli.py +0 -0
  63. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/port/errors.py +0 -0
  64. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/port/export.py +0 -0
  65. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/port/motion.py +0 -0
  66. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/port/mujoco.py +0 -0
  67. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/port/sapien.py +0 -0
  68. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/port/scaffold.py +0 -0
  69. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/presets/__init__.py +0 -0
  70. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/presets/robot_arm.py +0 -0
  71. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/project_deps.py +0 -0
  72. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/registry.py +0 -0
  73. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/replay.py +0 -0
  74. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/run_logs.py +0 -0
  75. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/safety.py +0 -0
  76. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/secret.py +0 -0
  77. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/selection.py +0 -0
  78. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/source.py +0 -0
  79. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/storage_cli.py +0 -0
  80. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/template_fetch.py +0 -0
  81. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/tf.py +0 -0
  82. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/units.py +0 -0
  83. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/upgrade_cli.py +0 -0
  84. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/usage_hist.py +0 -0
  85. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/volume.py +0 -0
  86. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/volume_upload.py +0 -0
  87. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq/weights.py +0 -0
  88. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/seq_launch.py +0 -0
  89. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/__init__.py +0 -0
  90. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/__init__.py +0 -0
  91. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/act_size.py +0 -0
  92. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/capacity.py +0 -0
  93. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/catalogue.py +0 -0
  94. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/diagnostics.py +0 -0
  95. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/encode.py +0 -0
  96. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/error_codes.py +0 -0
  97. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/frames.py +0 -0
  98. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/logsource.py +0 -0
  99. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/mock_gate.py +0 -0
  100. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/money.py +0 -0
  101. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/msgpack_io.py +0 -0
  102. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/names.py +0 -0
  103. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/obs_schedule.py +0 -0
  104. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/observe_stream.py +0 -0
  105. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/own_rows.py +0 -0
  106. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/pinned_tls.py +0 -0
  107. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/public_tls.py +0 -0
  108. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/reporting.py +0 -0
  109. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/rid.py +0 -0
  110. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/transport.py +0 -0
  111. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/_core/warn.py +0 -0
  112. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/act/__init__.py +0 -0
  113. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/act/frames.py +0 -0
  114. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/act/observation.py +0 -0
  115. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/act/player.py +0 -0
  116. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/act/policy.py +0 -0
  117. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/act/run_loop.py +0 -0
  118. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/act/types.py +0 -0
  119. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/chat/__init__.py +0 -0
  120. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/chat/content.py +0 -0
  121. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/chat/conversation.py +0 -0
  122. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/chat/infer.py +0 -0
  123. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/chat/types.py +0 -0
  124. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/decide/__init__.py +0 -0
  125. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/decide/infer.py +0 -0
  126. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/decide/types.py +0 -0
  127. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/errors.py +0 -0
  128. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/py.typed +0 -0
  129. {sequence_ai-0.44.2 → sequence_ai-0.45.0}/sequence_ai/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sequence-ai
3
- Version: 0.44.2
3
+ Version: 0.45.0
4
4
  Summary: The inference and benchmark platform for physical AI: deploy a robot policy, run it on your robot, score it.
5
5
  Project-URL: Homepage, https://generalsequences.com
6
6
  Project-URL: Documentation, https://generalsequences.com/docs/
@@ -991,7 +991,8 @@ adapter's `__init__` keywords and a benchmark's `@seq.setup` keywords the same w
991
991
  (`observations=seq.Observations({"cam": seq.Stream(at=[-15, 0])})`) has its worker keep the frames its acts
992
992
  read: hand it every step's observation (`run()` and a `Player` do), only the steps a window reads go up, and each act
993
993
  is handed them with its own. When the copy holding an episode stops (out of memory, a crash), the robot goes on in a new episode with a
994
- warning, and `run()` and a `Player` count it in `episode_resets`; on a worker of one copy the act sent again finds
994
+ warning - its steps counted from 0 again, none of the old one's observations handed to it - and `run()` and a `Player`
995
+ count it in `episode_resets`; on a worker of one copy the act sent again finds
995
996
  it still starting - a loop of `act()` calls waits that out, while `run()` and a `Player` raise it, saying so, and
996
997
  `run()` called again goes on in the new episode. A policy that only needs the last N
997
998
  frames needs no state: declare `observation=seq.ObservationContract(history=N)` and the robot sends
@@ -945,7 +945,8 @@ adapter's `__init__` keywords and a benchmark's `@seq.setup` keywords the same w
945
945
  (`observations=seq.Observations({"cam": seq.Stream(at=[-15, 0])})`) has its worker keep the frames its acts
946
946
  read: hand it every step's observation (`run()` and a `Player` do), only the steps a window reads go up, and each act
947
947
  is handed them with its own. When the copy holding an episode stops (out of memory, a crash), the robot goes on in a new episode with a
948
- warning, and `run()` and a `Player` count it in `episode_resets`; on a worker of one copy the act sent again finds
948
+ warning - its steps counted from 0 again, none of the old one's observations handed to it - and `run()` and a `Player`
949
+ count it in `episode_resets`; on a worker of one copy the act sent again finds
949
950
  it still starting - a loop of `act()` calls waits that out, while `run()` and a `Player` raise it, saying so, and
950
951
  `run()` called again goes on in the new episode. A policy that only needs the last N
951
952
  frames needs no state: declare `observation=seq.ObservationContract(history=N)` and the robot sends
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sequence-ai"
7
- version = "0.44.2"
7
+ version = "0.45.0"
8
8
  description = "The inference and benchmark platform for physical AI: deploy a robot policy, run it on your robot, score it."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -10,7 +10,8 @@ each name is a method of the class (its own or inherited) taking what the worker
10
10
  ones its constructor - or the factory `make` names - takes → the class keeps every method as written, and its
11
11
  registration names them (INFER, OBSERVE, RESET, ON_EPISODE) beside how the worker builds it (PolicyConstructor): the
12
12
  worker calls the constructor at load, config files read and "{weights}" filled where it runs (sequence_base.construct),
13
- resets the class at an episode's first act, hands its observe method each observation, then asks its infer method.
13
+ resets the class at an episode's first act, hands its observe method each observation - and its step in the episode,
14
+ from 0, by name, when the method takes `step` - then asks its infer method.
14
15
 
15
16
  WHY the platform calls the user's methods rather than the user writing @seq.load / @seq.infer around them: the goal
16
17
  is that a class that runs locally deploys as it is - "just add a decoration and now you can upload it" - and nothing
@@ -106,7 +107,8 @@ def _method(cls: type, arg: str, name: Any) -> str:
106
107
 
107
108
  def _takes(cls: type, arg: str, name: str | None, positional: int) -> None:
108
109
  """That the method `name` takes `positional` arguments besides self and no more required ones: the worker hands
109
- observe one observation, infer the act's inputs (or nothing when the class observes), reset and end nothing."""
110
+ observe one observation - and step=, its step in the episode, when it takes one - infer the act's inputs (or nothing
111
+ when the class observes), reset and end nothing."""
110
112
  if name is None:
111
113
  return
112
114
  fn = getattr(cls, name)
@@ -117,11 +119,16 @@ def _takes(cls: type, arg: str, name: str | None, positional: int) -> None:
117
119
  params = list(sig.parameters.values())
118
120
  if params and inspect.isfunction(inspect.getattr_static(cls, name)):
119
121
  params = params[1:]
122
+
123
+ by_name = {"step": 0} if arg == "observe" and any(
124
+ p.kind is inspect.Parameter.VAR_KEYWORD or (p.name == "step" and p.kind in (
125
+ inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY)) for p in params) else {}
120
126
  try:
121
- inspect.Signature(params).bind(*([None] * positional))
127
+ inspect.Signature(params).bind(*([None] * positional), **by_name)
122
128
  except TypeError:
123
129
  hands = {0: "nothing", 1: "one argument"}[positional]
124
- why = {"observe": "each observation", "infer": "the act's inputs" if positional else
130
+ why = {"observe": "each observation, and step= - its step in the episode, from 0 - when it takes one",
131
+ "infer": "the act's inputs" if positional else
125
132
  "nothing - its observations went to observe=", "reset": "nothing", "end": "nothing",
126
133
  "sample": "nothing - it returns the episode's act inputs"}[arg]
127
134
  raise PolicyDefinitionError(f"{cls.__name__}.{name}{sig} ({arg}=): the worker hands it {hands} ({why})") \
@@ -25,7 +25,8 @@ still does `self._t.post("/v1/act", payload)`, and this class routes that one pa
25
25
  worker while `/v1/models` and `/v1/act/ready` stay on the gateway.
26
26
 
27
27
  The data plane is worker_transport.py's (one kept-alive client to one worker's `/act`), the pin
28
- pinned_tls.py's (the worker's certificate checked by the fingerprint the gateway pinned).
28
+ pinned_tls.py's (the worker's certificate checked by the fingerprint the gateway pinned). A robot's observations between
29
+ acts are direct_observe.py's, its episodes direct_episode.py's: DirectConnection mixes both in.
29
30
 
30
31
  Stdlib `ssl` + `hashlib` only for the pin (no `cryptography`, no `certifi`: a worker checked by the
31
32
  CAs instead is public_tls.py's); httpx-only, Python 3.9-clean (no runtime `X | Y`, no `match`); and none
@@ -48,15 +49,13 @@ from typing import Any, Callable, NamedTuple, Optional
48
49
 
49
50
  from ..errors import AuthError, Conflict, InternalError, InvalidRequest, SequencesError, Unavailable
50
51
  from . import act_size
51
- from .names import segment
52
52
  from .public_tls import unmask_name
53
53
  from .transport import DEFAULT_TIMEOUT_S, Transport
54
54
  from .warn import warn
55
- from . import obs_schedule as _obs_schedule
56
55
  from . import observe_stream as _observe
57
- from .window import Window as _Window
58
- from .worker_transport import (_EPISODE_END_S, _KEEP_PING_TIMEOUT_S, _STILL_OPENING, _WorkerGone, _WorkerTransport,
59
- _frames_on, _wait_set)
56
+ from .worker_transport import _STILL_OPENING, _WorkerGone, _WorkerTransport, _frames_on, _wait_set
57
+ from .direct_episode import _EPISODE_LOST, _STATE_GONE, _Episodes
58
+ from .direct_observe import _ObservationStream
60
59
 
61
60
 
62
61
 
@@ -107,21 +106,6 @@ def _busy_backoff(attempt: int, retry_after_s: Optional[float]) -> float:
107
106
 
108
107
 
109
108
 
110
-
111
-
112
-
113
-
114
- _COPY_STOPPED = frozenset({"gpu_out_of_memory", "host_out_of_memory", "crashed", "hung"})
115
- _EPISODE_LOST = "episode_lost"
116
-
117
-
118
-
119
-
120
- _BATCH_FAILED = "batch_failed"
121
- _STATE_GONE = _COPY_STOPPED | {_BATCH_FAILED}
122
-
123
-
124
-
125
109
  _SAY_BUSY_AFTER_S = 5.0
126
110
 
127
111
 
@@ -366,7 +350,7 @@ def _keep_alive(ref: "weakref.ref[DirectConnection]", stop: threading.Event) ->
366
350
  del conn
367
351
 
368
352
 
369
- class DirectConnection:
353
+ class DirectConnection(_ObservationStream, _Episodes):
370
354
  """
371
355
  Control/data-plane split behind the `Transport` surface `Policy` already drives.
372
356
 
@@ -531,6 +515,7 @@ class DirectConnection:
531
515
  feedback = payload.pop("_feedback", None)
532
516
 
533
517
  own = self._own_act(asked, step_at, payload.get("plan")) if asked is not None else None
518
+ resets = self.episode_resets
534
519
  resent = False
535
520
  lost = 0
536
521
  busy_since: Optional[float] = None
@@ -541,6 +526,9 @@ class DirectConnection:
541
526
  if asked is not None:
542
527
  payload["episode_id"] = asked
543
528
  self._apply_episode(payload)
529
+ if own is not None and self.episode_resets != resets:
530
+
531
+ own, resets = self._own_anew(asked, own), self.episode_resets
544
532
  if own is not None:
545
533
  self._observed_act(payload, worker, own, timeout_s)
546
534
  if not by_hand:
@@ -660,183 +648,6 @@ class DirectConnection:
660
648
  warn(f"waiting for a free copy of the policy on its worker: {exc}")
661
649
  time.sleep(wait)
662
650
 
663
- def observe(self, inputs: Any, episode_id: str, step: Optional[int] = None) -> int:
664
- """One observation of the caller's episode `episode_id`, handed over as the robot makes it: its number in the
665
- episode, at once. Packed here, on the robot's own thread (what msgpack cannot carry is refused as an act's inputs
666
- are), then posted on a connection of its own to a worker that takes them (observe_stream), held for the
667
- episode's next act while the worker has not said whether it does, and sent nowhere when it does not. The next
668
- act of the episode is the next number. A policy that declares what it reads keeps it by step (_observe_window);
669
- `step`: the step it was made at (a Player's rows played), else the next. Mid-episode, its worker gone and the
670
- one in its place starting (or the robot in line for one), the observation is kept for the episode's next act,
671
- which waits for that worker as an act does and sends it: never waited for here."""
672
- self._assert_open()
673
- try:
674
- self._ensure_connected()
675
- reachable = True
676
- except Unavailable as exc:
677
- if self.contract is None or not (exc.warming or exc.queued):
678
- raise
679
-
680
-
681
-
682
- reachable = False
683
- if _obs_schedule.reads_past(self._observations):
684
- return self._observe_window(inputs, episode_id, step, reachable)
685
- try:
686
- packed = _observe.packed(inputs)
687
- except ValueError as exc:
688
- raise InvalidRequest(f"observe(): this observation cannot be sent - {exc}") from None
689
- with self._lock:
690
- n = self._numbers.get(episode_id, 0)
691
- self._numbers[episode_id] = n + 1
692
- worker = self._worker if reachable else None
693
- wid = self._worker_episode if episode_id == self._caller_episode and self._worker_episode else episode_id
694
- takes = None if worker is None else worker.takes_observations
695
- stream = self._stream_for(worker) if takes else None
696
- held = self._held.pop(episode_id, []) if takes is not None else []
697
- if takes is None:
698
- self._held.setdefault(episode_id, []).append((n, _observe.frame(n, packed)))
699
- if stream is not None:
700
- for s, f in [*held, (n, _observe.frame(n, packed))]:
701
- stream.put(wid, s, f)
702
- self._say_refusals(stream)
703
- elif takes is False:
704
- self._say_unread()
705
- return n
706
-
707
- def expect_ask(self, episode_id: str, step: Optional[int]) -> None:
708
- """The step a Player expects to ask at next in `episode_id` (its playback left until its lead, or the planning
709
- worker's time) - None: none, its run's last rows in hand: its window's frames go up as that act reads them, not
710
- by a guess from the last spacing, and none go up ahead when no act is coming."""
711
- with self._lock:
712
- win = self._windows.get(episode_id)
713
- if win is not None:
714
- win.expect(step)
715
-
716
- def reads_past(self) -> bool:
717
- """Whether the policy reads steps before an act's own (its Observations, as connect answered them): a Player then
718
- hands it every tick's observation (observe), each numbered by its step."""
719
- return _obs_schedule.reads_past(self._observations)
720
-
721
- def _observe_window(self, inputs: Any, episode_id: str, step: Optional[int], reachable: bool = True) -> int:
722
- """observe() for a policy that declares what it reads, steps before an act's own among it: numbered by step and
723
- kept (its Window) - what the next act is expected to read at that step posted now, to a worker that takes them;
724
- the rest goes up just before the act that reads it. Not `reachable` (its worker restarting): all of it goes up
725
- just before the act. Returns its step."""
726
- with self._lock:
727
- worker = self._worker if reachable else None
728
- takes = None if worker is None else worker.takes_observations
729
- win = self._window_for(episode_id, None)
730
- s = win.step(step)
731
- uploads = win.observe(s, inputs)
732
- if takes is None:
733
- win.lost()
734
- wid = self._worker_episode if episode_id == self._caller_episode and self._worker_episode else episode_id
735
- stream = self._stream_for(worker) if takes else None
736
- if stream is not None:
737
- for st, f in self._packed_steps(uploads):
738
- stream.put(wid, st, f)
739
- self._say_refusals(stream)
740
- elif takes is False:
741
- self._say_unread()
742
- return s
743
-
744
- def _window_for(self, episode_id: str, plan: Any) -> "_Window":
745
- """The episode's Window, made at its first observation or act (called under self._lock): its schedule taken to
746
- steps by the robot's rate - its act's plan.hz (a Player's), else connect(control_hz=) - and the next act expected
747
- as far on as the policy's open_loop_horizon until there are two."""
748
- win = self._windows.get(episode_id)
749
- if win is None:
750
- rate = plan.get("hz") if isinstance(plan, dict) else None
751
- try:
752
- sched = _obs_schedule.of(self._observations, rate=rate or self.control_hz)
753
- except ValueError as exc:
754
- raise InvalidRequest(str(exc)) from None
755
- gap = (self._actions or {}).get("open_loop_horizon") if isinstance(self._actions, dict) else None
756
- win = self._windows[episode_id] = _Window(sched, gap=gap or 1)
757
- return win
758
-
759
- @staticmethod
760
- def _packed_steps(uploads: list) -> list:
761
- """[(step, frame)] of a Window's uploads, packed on the caller's thread: what msgpack cannot carry refused here."""
762
- try:
763
- return [(s, _observe.frame(s, _observe.packed(obs))) for s, obs in uploads]
764
- except ValueError as exc:
765
- raise InvalidRequest(f"observe(): an observation cannot be sent - {exc}") from None
766
-
767
- def _stream_for(self, worker: "_WorkerTransport") -> "_observe.ObserveStream":
768
- """The stream posting to `worker` - made the first time it is asked for, and anew for another worker, posting
769
- again what the one before kept since each episode's last served act; another worker holds none of a window's
770
- frames, which go again as their acts read them. Called under self._lock."""
771
- if self._stream is None or self._stream_to is not worker:
772
- carried = {}
773
- if self._stream is not None:
774
- carried = self._stream.carry()
775
- self._stream.close()
776
- for win in self._windows.values():
777
- win.lost()
778
- self._stream, self._stream_to = _observe.ObserveStream(worker.post_observe, carried), worker
779
- return self._stream
780
-
781
- def _own_act(self, episode_id: str, step_at: Optional[int] = None, plan: Any = None) -> Optional[tuple]:
782
- """For an act of an episode the robot hands observations to (observe), or of a policy that reads steps before an
783
- act's own: (its number - `step_at`, a Player's rows played, else the next - the frames held for it, its Window
784
- or None); None for any other act."""
785
- self._ensure_connected()
786
- with self._lock:
787
- if _obs_schedule.reads_past(self._observations):
788
- if self._observations.get("unit") == "seconds" and not (isinstance(plan, dict) and plan.get("hz")):
789
-
790
- raise InvalidRequest("this policy counts its window in seconds, which its worker takes to steps by "
791
- "your robot's rate: send each act with steps_left= (its plan carries the rate "
792
- "connect(control_hz=) says), or let run() or a Player send them")
793
- win = self._window_for(episode_id, plan)
794
- return win.step(step_at), [], win
795
- if episode_id not in self._numbers:
796
- return None
797
- n = self._numbers[episode_id]
798
- self._numbers[episode_id] = n + 1
799
- return n, self._held.pop(episode_id, []), None
800
-
801
- def _observed_act(self, payload: dict, worker: "_WorkerTransport", own: tuple, timeout_s: Optional[float]) -> None:
802
- """The act's body, each try, in place: its number (`seq`) when its worker takes the stream - the frames held for
803
- it posted first - and nothing of the stream when it does not: its policy reads the act's own alone. A worker
804
- that has not answered yet (the first act, or one after a reconnect, whose stream posts what the one before kept)
805
- is waited for as the act would wait for it anyway (await_answer)."""
806
- n, held, win = own
807
- if worker.takes_observations is None:
808
- worker.await_answer(timeout_s)
809
- if worker.takes_observations:
810
- with self._lock:
811
- stream = self._stream_for(worker)
812
- uploads = win.act(n) if win is not None else []
813
- for s, f in [*self._packed_steps(uploads), *held]:
814
- stream.put(payload["episode_id"], s, f)
815
- held.clear()
816
- payload["seq"] = n
817
- self._say_refusals(stream)
818
- return
819
- payload.pop("seq", None)
820
- if held or win is not None:
821
- held.clear()
822
- self._say_unread()
823
-
824
- def _say_unread(self) -> None:
825
- """Said once a connection: observations handed to observe() went nowhere, as its worker does not take them."""
826
- with self._lock:
827
- said, self._said_unread = self._said_unread, True
828
- if not said:
829
- warn("observe() hands this policy nothing: its worker reads each act's own observation alone. A policy is "
830
- "handed the ones between acts when it declares observations=seq.Observations(every=1) (naming an "
831
- "observe= method does) and its worker takes them - one deployed before workers did, once `seq deploy` builds it "
832
- "again")
833
-
834
- def _say_refusals(self, stream: "_observe.ObserveStream") -> None:
835
- """What the worker refused to keep of an episode's observations (more than it keeps between two acts), said once
836
- an episode on the robot's own thread: those were left out, and its acts read the ones it kept."""
837
- for why in stream.refusals():
838
- warn(f"observations left out: the worker refused them ({why}); the episode's acts read the ones it kept")
839
-
840
651
  def handshake(self) -> Optional[dict]:
841
652
  """Connect now if not yet connected — the same handshake the first act makes — and return the contract
842
653
  the deployment declared ({"action", "observation", "embodiment"}). Lets Policy check an observation
@@ -1416,144 +1227,6 @@ A robot in line had no seat: it is told it is no longer in line. `why` is the pl
1416
1227
  else:
1417
1228
  self._lease_deadline = None
1418
1229
 
1419
- def end_episode(self, episode_id: str) -> None:
1420
- """The robot's episode is over: the worker frees its state now (a stateful policy's @seq.on_episode runs
1421
- and its replica returns to the pool) instead of after its TTL with no act. Never raises. When the platform asked the
1422
- robot to move (its worker is leaving), this boundary is where it does: the next episode starts on a worker
1423
- already up - at its deployment's cap, where the worker is replaced in place, on the new one once it is up - and
1424
- nothing is cut. The worker it leaves is told the episode ended while the move's connect goes, not before it;
1425
- kept on that worker after all, its next episode starts once the worker has been told, as with no move.
1426
- """
1427
- with self._lock:
1428
- worker = None if self._closed else self._worker
1429
- wid = self._worker_episode if episode_id == self._caller_episode else episode_id
1430
- self._episode_open = False
1431
- self._settle_params()
1432
-
1433
- self._numbers.pop(episode_id, None)
1434
- self._held.pop(episode_id, None)
1435
- self._windows.pop(episode_id, None)
1436
- if self._stream is not None and wid:
1437
- self._stream.end(wid)
1438
- moving = worker is not None and self._rotate is not None and _ready(self._rotate)
1439
- told = None
1440
- if worker is not None and wid:
1441
- if moving:
1442
- told = worker.end_episode_behind(wid)
1443
- else:
1444
- worker.post_episode_end(wid)
1445
- try:
1446
- with self._lock:
1447
- if self._rotate is None or self._closed or self._worker is None:
1448
- return
1449
- if not _ready(self._rotate):
1450
-
1451
-
1452
-
1453
-
1454
- return
1455
-
1456
- self._caller_episode = self._worker_episode = None
1457
- try:
1458
- self._connect(renew=False, move=True)
1459
- except Unavailable:
1460
-
1461
-
1462
-
1463
-
1464
-
1465
-
1466
- if self._worker is not None:
1467
- self._worker.gone = True
1468
- except Exception:
1469
- pass
1470
- finally:
1471
- if told is not None:
1472
- with self._lock:
1473
- stays = self._worker is worker and not worker.gone
1474
- if stays:
1475
-
1476
-
1477
- told.join(_EPISODE_END_S + _KEEP_PING_TIMEOUT_S)
1478
-
1479
- def label_episode(self, episode_id: str, *, success: bool, notes: str = "") -> None:
1480
- """How the robot's episode went, kept with the deployment's captured field data (`seq policy capture`):
1481
- written for the id the workers saw it under — and every id it carried after a reconnect moved it. Raises on
1482
- failure.
1483
- """
1484
- with self._lock:
1485
- ids = [episode_id, *self._aliases.get(episode_id, [])]
1486
- for wid in ids:
1487
-
1488
- self._gateway.post(f"/v1/deployments/{segment(self._model)}/episodes/{segment(wid)}/label",
1489
- {"success": bool(success), "notes": notes})
1490
-
1491
- def _take_params(self, answered: dict) -> None:
1492
- """What the platform answered the session runs with: at once, or — while an episode runs and they differ —
1493
- at its boundary, so one episode never runs on two sets of parameters."""
1494
- if self._episode_open and (answered or None) != self._params:
1495
- self._pending_params = answered
1496
- else:
1497
- self._params, self._pending_params = answered or None, None
1498
-
1499
- def _settle_params(self) -> None:
1500
- """An episode boundary: parameters a renewal answered during the episode take effect now."""
1501
- if self._pending_params is not None:
1502
- self._params, self._pending_params = self._pending_params or None, None
1503
-
1504
- def _apply_episode(self, payload: dict) -> None:
1505
- """Remap the caller's episode id to the current worker-side id (in place)."""
1506
- with self._lock:
1507
- caller = payload.get("episode_id")
1508
- if caller is None:
1509
- return
1510
- if caller != self._caller_episode:
1511
-
1512
- self._caller_episode = caller
1513
- self._worker_episode = caller
1514
- self._settle_params()
1515
- self._episode_open = True
1516
- payload["episode_id"] = self._worker_episode
1517
-
1518
- def _rotate_episode(self, notice: Optional[dict] = None, *, stopped: Optional[str] = None) -> Optional[str]:
1519
- """New worker → the episode restarts there; surface it, do not continue silently - with why, when the platform
1520
- had said the robot was to move (`notice`, its `rotate`). `stopped`: the copy of the policy that held it stopped
1521
- (its cause, _COPY_STOPPED), or ended it for a batch that raised (_BATCH_FAILED), on the same worker. Returns the
1522
- worker-side id it leaves, None on a stateless path."""
1523
- if self._caller_episode is None:
1524
- return None
1525
- left = self._worker_episode
1526
- self._worker_episode = uuid.uuid4().hex
1527
- self._aliases.setdefault(self._caller_episode, []).append(self._worker_episode)
1528
- self.episode_resets += 1
1529
- if self._stream is not None and left:
1530
-
1531
- self._stream.move(left, self._worker_episode)
1532
- if self._caller_episode in self._windows:
1533
- self._windows[self._caller_episode].lost()
1534
- if stopped == _BATCH_FAILED:
1535
- warn("episode was reset: a batch it was in failed, so what the policy kept of it may be half changed and was "
1536
- "given up; the robot goes on in a new episode")
1537
- return left
1538
- if stopped is not None:
1539
- warn(f"episode was reset: the copy of the policy that held it stopped ({stopped}); its next act starts a new "
1540
- "episode, and what the policy kept of this one did not carry over")
1541
- return left
1542
- why = f" ({_moving_words(notice, moved=True)})" if notice else ""
1543
- warn("episode was reset after a reconnect to a new worker" + why + "; what the policy kept of this episode "
1544
- "on the previous worker did not carry over")
1545
- return left
1546
-
1547
- def _lose_episode(self, worker: "_WorkerTransport", cause: Optional[str]) -> None:
1548
- """The copy that held the caller's episode stopped, or ended it for a batch that raised (_STATE_GONE): a new
1549
- worker-side episode, said with the cause (_rotate_episode), and the one it leaves ended on the worker - its lost
1550
- mark cleared, nothing else to free - so the act goes again under the new id. Best effort: an end that does not
1551
- arrive leaves the mark to run out."""
1552
- with self._lock:
1553
- left = self._rotate_episode(stopped=cause or "unknown cause")
1554
- if left:
1555
- worker.post_episode_end(left)
1556
-
1557
1230
  def _assert_open(self) -> None:
1558
1231
  if self._closed:
1559
1232
  raise SequencesError("this connection is closed; open a new one with connect()")
@@ -0,0 +1,181 @@
1
+ """
2
+ A DirectConnection's episodes, mixed into it (_Episodes): the caller's episode id mapped to the one its worker sees
3
+ (_apply_episode) - a new one when a reconnect moved the robot or the copy that held it stopped (_rotate_episode,
4
+ _lose_episode) - its end told to the worker, where a move the platform asked for happens (end_episode), how it went kept
5
+ with the captured field data (label_episode), and the session's parameters taking effect at its boundaries. The
6
+ connection, its worker and its lock are DirectConnection's (direct.py), whose move-notice words these read.
7
+
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import uuid
12
+ from typing import Optional
13
+
14
+ from ..errors import Unavailable
15
+ from .names import segment
16
+ from .warn import warn
17
+ from .worker_transport import _EPISODE_END_S, _KEEP_PING_TIMEOUT_S, _WorkerTransport
18
+
19
+
20
+
21
+
22
+
23
+
24
+
25
+
26
+ _COPY_STOPPED = frozenset({"gpu_out_of_memory", "host_out_of_memory", "crashed", "hung"})
27
+ _EPISODE_LOST = "episode_lost"
28
+
29
+
30
+
31
+
32
+ _BATCH_FAILED = "batch_failed"
33
+ _STATE_GONE = _COPY_STOPPED | {_BATCH_FAILED}
34
+
35
+
36
+ class _Episodes:
37
+ """DirectConnection's episodes (the module's docstring)."""
38
+
39
+ def end_episode(self, episode_id: str) -> None:
40
+ """The robot's episode is over: the worker frees its state now (a stateful policy's @seq.on_episode runs
41
+ and its replica returns to the pool) instead of after its TTL with no act. Never raises. When the platform asked the
42
+ robot to move (its worker is leaving), this boundary is where it does: the next episode starts on a worker
43
+ already up - at its deployment's cap, where the worker is replaced in place, on the new one once it is up - and
44
+ nothing is cut. The worker it leaves is told the episode ended while the move's connect goes, not before it;
45
+ kept on that worker after all, its next episode starts once the worker has been told, as with no move.
46
+ """
47
+ from .direct import _ready
48
+ with self._lock:
49
+ worker = None if self._closed else self._worker
50
+ wid = self._worker_episode if episode_id == self._caller_episode else episode_id
51
+ self._episode_open = False
52
+ self._settle_params()
53
+
54
+ self._numbers.pop(episode_id, None)
55
+ self._held.pop(episode_id, None)
56
+ self._windows.pop(episode_id, None)
57
+ if self._stream is not None and wid:
58
+ self._stream.end(wid)
59
+ moving = worker is not None and self._rotate is not None and _ready(self._rotate)
60
+ told = None
61
+ if worker is not None and wid:
62
+ if moving:
63
+ told = worker.end_episode_behind(wid)
64
+ else:
65
+ worker.post_episode_end(wid)
66
+ try:
67
+ with self._lock:
68
+ if self._rotate is None or self._closed or self._worker is None:
69
+ return
70
+ if not _ready(self._rotate):
71
+
72
+
73
+
74
+
75
+ return
76
+
77
+ self._caller_episode = self._worker_episode = None
78
+ try:
79
+ self._connect(renew=False, move=True)
80
+ except Unavailable:
81
+
82
+
83
+
84
+
85
+
86
+
87
+ if self._worker is not None:
88
+ self._worker.gone = True
89
+ except Exception:
90
+ pass
91
+ finally:
92
+ if told is not None:
93
+ with self._lock:
94
+ stays = self._worker is worker and not worker.gone
95
+ if stays:
96
+
97
+
98
+ told.join(_EPISODE_END_S + _KEEP_PING_TIMEOUT_S)
99
+
100
+ def label_episode(self, episode_id: str, *, success: bool, notes: str = "") -> None:
101
+ """How the robot's episode went, kept with the deployment's captured field data (`seq policy capture`):
102
+ written for the id the workers saw it under — and every id it carried after a reconnect moved it. Raises on
103
+ failure.
104
+ """
105
+ with self._lock:
106
+ ids = [episode_id, *self._aliases.get(episode_id, [])]
107
+ for wid in ids:
108
+
109
+ self._gateway.post(f"/v1/deployments/{segment(self._model)}/episodes/{segment(wid)}/label",
110
+ {"success": bool(success), "notes": notes})
111
+
112
+ def _take_params(self, answered: dict) -> None:
113
+ """What the platform answered the session runs with: at once, or — while an episode runs and they differ —
114
+ at its boundary, so one episode never runs on two sets of parameters."""
115
+ if self._episode_open and (answered or None) != self._params:
116
+ self._pending_params = answered
117
+ else:
118
+ self._params, self._pending_params = answered or None, None
119
+
120
+ def _settle_params(self) -> None:
121
+ """An episode boundary: parameters a renewal answered during the episode take effect now."""
122
+ if self._pending_params is not None:
123
+ self._params, self._pending_params = self._pending_params or None, None
124
+
125
+ def _apply_episode(self, payload: dict) -> None:
126
+ """Remap the caller's episode id to the current worker-side id (in place)."""
127
+ with self._lock:
128
+ caller = payload.get("episode_id")
129
+ if caller is None:
130
+ return
131
+ if caller != self._caller_episode:
132
+
133
+ self._caller_episode = caller
134
+ self._worker_episode = caller
135
+ self._settle_params()
136
+ self._episode_open = True
137
+ payload["episode_id"] = self._worker_episode
138
+
139
+ def _rotate_episode(self, notice: Optional[dict] = None, *, stopped: Optional[str] = None) -> Optional[str]:
140
+ """New worker → the episode restarts there; surface it, do not continue silently - with why, when the platform
141
+ had said the robot was to move (`notice`, its `rotate`). `stopped`: the copy of the policy that held it stopped
142
+ (its cause, _COPY_STOPPED), or ended it for a batch that raised (_BATCH_FAILED), on the same worker. The new
143
+ episode is handed none of the old one's observations and counts its steps from 0 - the act on its way its first
144
+ (Window.restart, post) - as the policy's Episode does; a run's counts go on. Returns the worker-side id it leaves,
145
+ None on a stateless path."""
146
+ if self._caller_episode is None:
147
+ return None
148
+ left = self._worker_episode
149
+ self._worker_episode = uuid.uuid4().hex
150
+ self._aliases.setdefault(self._caller_episode, []).append(self._worker_episode)
151
+ self.episode_resets += 1
152
+ if self._stream is not None and left:
153
+ self._stream.end(left)
154
+ win = self._windows.get(self._caller_episode)
155
+ if win is not None:
156
+ win.restart(win.asked if win.asked > win.acted else win.unreached)
157
+ elif self._caller_episode in self._numbers:
158
+ self._number_anew(self._caller_episode)
159
+ if stopped == _BATCH_FAILED:
160
+ warn("episode was reset: a batch it was in failed, so what the policy kept of it may be half changed and was "
161
+ "given up; the robot goes on in a new episode")
162
+ return left
163
+ if stopped is not None:
164
+ warn(f"episode was reset: the copy of the policy that held it stopped ({stopped}); its next act starts a new "
165
+ "episode, and what the policy kept of this one did not carry over")
166
+ return left
167
+ from .direct import _moving_words
168
+ why = f" ({_moving_words(notice, moved=True)})" if notice else ""
169
+ warn("episode was reset after a reconnect to a new worker" + why + "; what the policy kept of this episode "
170
+ "on the previous worker did not carry over")
171
+ return left
172
+
173
+ def _lose_episode(self, worker: "_WorkerTransport", cause: Optional[str]) -> None:
174
+ """The copy that held the caller's episode stopped, or ended it for a batch that raised (_STATE_GONE): a new
175
+ worker-side episode, said with the cause (_rotate_episode), and the one it leaves ended on the worker - its lost
176
+ mark cleared, nothing else to free - so the act goes again under the new id. Best effort: an end that does not
177
+ arrive leaves the mark to run out."""
178
+ with self._lock:
179
+ left = self._rotate_episode(stopped=cause or "unknown cause")
180
+ if left:
181
+ worker.post_episode_end(left)