simulo 0.31.2__tar.gz → 0.32.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 (63) hide show
  1. {simulo-0.31.2/src/simulo.egg-info → simulo-0.32.0}/PKG-INFO +5 -5
  2. {simulo-0.31.2 → simulo-0.32.0}/PYPI.md +3 -3
  3. {simulo-0.31.2 → simulo-0.32.0}/pyproject.toml +2 -2
  4. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/__init__.py +2 -0
  5. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/_runner.py +7 -0
  6. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/app.py +34 -8
  7. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/bundle.py +25 -18
  8. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/cli.py +313 -74
  9. simulo-0.32.0/src/simulo/_client/evaluation_report.py +581 -0
  10. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/facades.py +52 -4
  11. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/jobs_api.py +43 -17
  12. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/learning.py +48 -6
  13. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/packaging.py +43 -21
  14. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/policies_api.py +12 -0
  15. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/preview_report.py +60 -22
  16. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/submit_api.py +51 -4
  17. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/templates/__init__.py +3 -3
  18. simulo-0.32.0/src/simulo/_client/templates/training/eval.py.tmpl +34 -0
  19. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/templates/training/task.py.tmpl +23 -17
  20. {simulo-0.31.2 → simulo-0.32.0/src/simulo.egg-info}/PKG-INFO +5 -5
  21. {simulo-0.31.2 → simulo-0.32.0}/src/simulo.egg-info/SOURCES.txt +2 -0
  22. {simulo-0.31.2 → simulo-0.32.0}/src/simulo.egg-info/requires.txt +1 -1
  23. {simulo-0.31.2 → simulo-0.32.0}/MANIFEST.in +0 -0
  24. {simulo-0.31.2 → simulo-0.32.0}/setup.cfg +0 -0
  25. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/__init__.py +0 -0
  26. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/_arguments.py +0 -0
  27. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/_mounts.py +0 -0
  28. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/_secure_downloads.py +0 -0
  29. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/asset.py +0 -0
  30. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/asset_api.py +0 -0
  31. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/asset_package.py +0 -0
  32. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/asset_pins.py +0 -0
  33. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/builtin_aliases.py +0 -0
  34. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/cancel_api.py +0 -0
  35. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/config.py +0 -0
  36. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/credentials.py +0 -0
  37. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/discovery.py +0 -0
  38. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/export_api.py +0 -0
  39. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/http.py +0 -0
  40. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/identity_api.py +0 -0
  41. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/install_samples.py +0 -0
  42. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/login.py +0 -0
  43. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/mode.py +0 -0
  44. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/onnx_bundle.py +0 -0
  45. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/outputs.py +0 -0
  46. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/preflight_api.py +0 -0
  47. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/preflight_render.py +0 -0
  48. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/registry.py +0 -0
  49. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/runtime.py +0 -0
  50. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/runtime_display.py +0 -0
  51. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/stub.py +0 -0
  52. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/templates/training/play.py.tmpl +0 -0
  53. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/templates/training/preview.py.tmpl +0 -0
  54. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/templates/training/simuloignore.tmpl +0 -0
  55. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/templates/training/train.py.tmpl +0 -0
  56. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/view_fragment.py +0 -0
  57. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/view_session_api.py +0 -0
  58. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/_client/volume.py +0 -0
  59. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/callbacks.py +0 -0
  60. {simulo-0.31.2 → simulo-0.32.0}/src/simulo/py.typed +0 -0
  61. {simulo-0.31.2 → simulo-0.32.0}/src/simulo.egg-info/dependency_links.txt +0 -0
  62. {simulo-0.31.2 → simulo-0.32.0}/src/simulo.egg-info/entry_points.txt +0 -0
  63. {simulo-0.31.2 → simulo-0.32.0}/src/simulo.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simulo
3
- Version: 0.31.2
3
+ Version: 0.32.0
4
4
  Summary: Python SDK and CLI for cloud-native robotics simulation, reinforcement learning, and robot policy training on managed GPUs.
5
5
  Author-email: Simulo Team <team@simulo.ai>
6
6
  License: BSD-3-Clause
@@ -31,7 +31,7 @@ Classifier: Topic :: System :: Distributed Computing
31
31
  Classifier: Typing :: Typed
32
32
  Requires-Python: >=3.11
33
33
  Description-Content-Type: text/markdown
34
- Requires-Dist: simulo-interfaces<0.23,>=0.22.0
34
+ Requires-Dist: simulo-interfaces<0.24,>=0.23.0
35
35
  Requires-Dist: mcap<2,>=1.3
36
36
  Requires-Dist: defusedxml>=0.7.1
37
37
  Requires-Dist: usd-core<27,>=26.5
@@ -115,7 +115,7 @@ See the repository README for setup and usage.
115
115
 
116
116
  ```bash
117
117
  simulo jobs # list submitted jobs and status
118
- simulo logs --follow # stream the latest job's logs
118
+ simulo logs --follow # stream logs from your most recent job you submitted (train, play, preview or eval)
119
119
  simulo result # read the job's result
120
120
  simulo policy list # list training policies and checkpoints
121
121
  simulo policy get policy_x:best # download a verified checkpoint
@@ -126,8 +126,8 @@ simulo view # view a running --viewstream job in 3D
126
126
  simulo cancel # stop a queued or running job
127
127
  ```
128
128
 
129
- Commands that accept a job ID default to the latest job and tell you which job
130
- they selected.
129
+ Commands that accept a job ID default to the latest user-submitted job
130
+ (`train`, `play`, `preview`, or `eval`) and tell you its ID and type.
131
131
 
132
132
  ## Use versioned robot assets
133
133
 
@@ -59,7 +59,7 @@ See the repository README for setup and usage.
59
59
 
60
60
  ```bash
61
61
  simulo jobs # list submitted jobs and status
62
- simulo logs --follow # stream the latest job's logs
62
+ simulo logs --follow # stream logs from your most recent job you submitted (train, play, preview or eval)
63
63
  simulo result # read the job's result
64
64
  simulo policy list # list training policies and checkpoints
65
65
  simulo policy get policy_x:best # download a verified checkpoint
@@ -70,8 +70,8 @@ simulo view # view a running --viewstream job in 3D
70
70
  simulo cancel # stop a queued or running job
71
71
  ```
72
72
 
73
- Commands that accept a job ID default to the latest job and tell you which job
74
- they selected.
73
+ Commands that accept a job ID default to the latest user-submitted job
74
+ (`train`, `play`, `preview`, or `eval`) and tell you its ID and type.
75
75
 
76
76
  ## Use versioned robot assets
77
77
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "simulo"
7
- version = "0.31.2"
7
+ version = "0.32.0"
8
8
  description = "Python SDK and CLI for cloud-native robotics simulation, reinforcement learning, and robot policy training on managed GPUs."
9
9
  readme = "PYPI.md"
10
10
  requires-python = ">=3.11"
@@ -59,7 +59,7 @@ classifiers = [
59
59
  # mcap: used by `simulo recordings` to read back a downloaded recording and
60
60
  # verify it (message count) — MIT-licensed, deps only lz4+zstandard.
61
61
  dependencies = [
62
- "simulo-interfaces>=0.22.0,<0.23",
62
+ "simulo-interfaces>=0.23.0,<0.24",
63
63
  "mcap>=1.3,<2",
64
64
  # Hardened XML parsing for URDF ingestion (USD Asset Catalogs, PR-12 fix
65
65
  # loop 2, security NIT): stdlib xml.etree.ElementTree relies on
@@ -277,6 +277,7 @@ __all__ = [
277
277
  "Trainer",
278
278
  "Visual",
279
279
  "World",
280
+ "evaluate",
280
281
  "preview",
281
282
  "save_output",
282
283
  ]
@@ -381,6 +382,7 @@ if TYPE_CHECKING:
381
382
  from simulo._client.facades import RLPlayer as RLPlayer
382
383
  from simulo._client.facades import RLTrainer as RLTrainer
383
384
  from simulo._client.facades import Robot as Robot
385
+ from simulo._client.facades import evaluate as evaluate
384
386
  from simulo._client.facades import preview as preview
385
387
  from simulo.interfaces.authoring import SceneProtocol
386
388
  from simulo.interfaces.runtime import (
@@ -92,6 +92,7 @@ class JobHandle:
92
92
  token: Optional[str] = None,
93
93
  job_type: Optional[JobType] = None,
94
94
  input_checkpoint: Optional[str] = None,
95
+ compare_checkpoint: Optional[str] = None,
95
96
  deferred_stdout: str = "",
96
97
  deferred_stderr: str = "",
97
98
  ) -> None:
@@ -104,6 +105,7 @@ class JobHandle:
104
105
  self._token = token
105
106
  self._job_type = job_type
106
107
  self._input_checkpoint = input_checkpoint
108
+ self._compare_checkpoint = compare_checkpoint
107
109
  self._deferred_stdout = deferred_stdout
108
110
  self._deferred_stderr = deferred_stderr
109
111
 
@@ -152,6 +154,11 @@ class JobHandle:
152
154
  """Policy checkpoint requested with ``simulo run --from``."""
153
155
  return self._input_checkpoint
154
156
 
157
+ @property
158
+ def compare_checkpoint(self) -> Optional[str]:
159
+ """Policy checkpoint requested with ``simulo run --compare``."""
160
+ return self._compare_checkpoint
161
+
155
162
  @property
156
163
  def deferred_stdout(self) -> str:
157
164
  """Submit-time output held until the cloud job header is printed."""
@@ -160,6 +160,7 @@ def viewstream_requested(enabled: bool) -> Iterator[None]:
160
160
 
161
161
  _INPUT_CHECKPOINT_REF: Optional[str] = None
162
162
  _INPUT_CHECKPOINT_FLAG = "--from"
163
+ _COMPARE_CHECKPOINT_REF: Optional[str] = None
163
164
 
164
165
 
165
166
  class InputCheckpointRequiresCloudError(RuntimeError):
@@ -188,6 +189,18 @@ def input_checkpoint_requested(checkpoint_ref: Optional[str], *, flag: str = "--
188
189
  _INPUT_CHECKPOINT_FLAG = previous_flag
189
190
 
190
191
 
192
+ @contextlib.contextmanager
193
+ def compare_checkpoint_requested(checkpoint_ref: Optional[str]) -> Iterator[None]:
194
+ """Scope ``--compare``'s second checkpoint for one ``simulo run`` submit."""
195
+ global _COMPARE_CHECKPOINT_REF
196
+ previous = _COMPARE_CHECKPOINT_REF
197
+ _COMPARE_CHECKPOINT_REF = checkpoint_ref
198
+ try:
199
+ yield
200
+ finally:
201
+ _COMPARE_CHECKPOINT_REF = previous
202
+
203
+
191
204
  # ---------------------------------------------------------------------------
192
205
  # Asset-pin ambient signals — `simulo run --frozen` / `--strict-assets` scope
193
206
  # these for one submit, exactly like `--viewstream` / `--from` above and for the
@@ -302,6 +315,17 @@ def _require_no_input_checkpoint_for_local_submit(reason: str) -> None:
302
315
  )
303
316
 
304
317
 
318
+ def _require_no_compare_checkpoint_for_local_submit(reason: str) -> None:
319
+ """Refuse a local-disk submit that would drop ``--compare``."""
320
+ if _COMPARE_CHECKPOINT_REF is None:
321
+ return
322
+ raise InputCheckpointRequiresCloudError(
323
+ f"--compare {_COMPARE_CHECKPOINT_REF!r} needs a cloud control plane to resolve the checkpoint, but {reason}. "
324
+ "A local-disk submit only writes the package and cannot compare policy checkpoints. Log in first "
325
+ "(`simulo login`) or point SIMULO_API_URL/SIMULO_ENV at a control plane (and unset SIMULO_SUBMIT=local)."
326
+ )
327
+
328
+
305
329
  @dataclass
306
330
  class JobSpec:
307
331
  """Recorded metadata for one ``@app.job``-decorated function."""
@@ -491,6 +515,7 @@ def _spawn_cloud(
491
515
  *,
492
516
  viewstream: bool = False,
493
517
  input_checkpoint: Optional[str] = None,
518
+ compare_checkpoint: Optional[str] = None,
494
519
  frozen_assets: bool = False,
495
520
  strict_assets: bool = False,
496
521
  skip_preflight: bool = False,
@@ -683,6 +708,7 @@ def _spawn_cloud(
683
708
  args=args,
684
709
  viewstream=viewstream,
685
710
  input_checkpoint=input_checkpoint,
711
+ compare_checkpoint=compare_checkpoint,
686
712
  assets=asset_pins,
687
713
  app_name=app_name,
688
714
  system=system,
@@ -716,6 +742,7 @@ def _spawn_cloud(
716
742
  token=token,
717
743
  job_type=job_type,
718
744
  input_checkpoint=input_checkpoint,
745
+ compare_checkpoint=compare_checkpoint,
719
746
  deferred_stdout=deferred_stdout,
720
747
  deferred_stderr=deferred_stderr,
721
748
  )
@@ -807,6 +834,7 @@ class JobFunction:
807
834
  # local-disk submit must never depend on the network succeeding.
808
835
  if config.is_submit_forced_local():
809
836
  _require_no_input_checkpoint_for_local_submit("SIMULO_SUBMIT=local forces a local-disk submit")
837
+ _require_no_compare_checkpoint_for_local_submit("SIMULO_SUBMIT=local forces a local-disk submit")
810
838
  handle: JobHandle = _spawn_local(self._spec.name, package_path)
811
839
  else:
812
840
  creds = credentials.try_get_valid_credentials()
@@ -819,6 +847,7 @@ class JobFunction:
819
847
  creds,
820
848
  viewstream=_VIEWSTREAM_REQUESTED,
821
849
  input_checkpoint=_INPUT_CHECKPOINT_REF,
850
+ compare_checkpoint=_COMPARE_CHECKPOINT_REF,
822
851
  frozen_assets=_FROZEN_ASSETS_REQUESTED,
823
852
  strict_assets=_STRICT_ASSETS_REQUESTED,
824
853
  skip_preflight=_SKIP_PREFLIGHT_REQUESTED,
@@ -850,6 +879,7 @@ class JobFunction:
850
879
  )
851
880
  else:
852
881
  _require_no_input_checkpoint_for_local_submit("no cloud target is configured")
882
+ _require_no_compare_checkpoint_for_local_submit("no cloud target is configured")
853
883
  handle = _spawn_local(self._spec.name, package_path)
854
884
  _record_spawn(handle)
855
885
  return handle
@@ -1430,11 +1460,11 @@ class App:
1430
1460
  raise AttributeError(f"{type(self).__name__!s} object has no attribute {name!r}")
1431
1461
 
1432
1462
  def success(self, fn: F) -> F:
1433
- """Register an evaluation success predicate for CLI validation.
1463
+ """Register an evaluation success rule.
1434
1464
 
1435
- Evaluation execution is implemented in a later milestone. This
1436
- decorator records the function only, so discovery can enforce that an
1437
- eval file has exactly one and every other job type has none.
1465
+ The client only records the function, so discovery can enforce that an
1466
+ eval file has exactly one and every other job type has none; the
1467
+ evaluation job calls it on each episode's last step.
1438
1468
  """
1439
1469
  self._success_functions.append(fn)
1440
1470
  return fn
@@ -1449,8 +1479,6 @@ class App:
1449
1479
  spec = self._jobs[job_name]
1450
1480
  where = location or f"app {self.name!r}"
1451
1481
  successes = self.success_functions
1452
- if spec.job_type is JobType.EVAL and not successes:
1453
- raise JobDeclarationError("evaluation isn't available yet. Use a train job for now.")
1454
1482
  if spec.job_type is JobType.EVAL:
1455
1483
  if len(successes) != 1:
1456
1484
  names = ", ".join(fn.__name__ for fn in successes) or "none"
@@ -1474,8 +1502,6 @@ class App:
1474
1502
  "but a preview job takes no policy. Remove it; a policy goes to a play or eval file "
1475
1503
  "with --policy."
1476
1504
  )
1477
- if spec.job_type is JobType.EVAL:
1478
- raise JobDeclarationError("evaluation isn't available yet. Use a train job for now.")
1479
1505
  if spec.job_type is JobType.ASSET_VALIDATION:
1480
1506
  raise JobDeclarationError("Asset validation runs when you publish an asset: simulo asset publish.")
1481
1507
  if spec.job_type is JobType.EXPORT:
@@ -4,9 +4,9 @@ Shared by ``simulo run`` (the CLI) and ``JobFunction.spawn`` — the *submit* pa
4
4
  discover the app's metadata and write the package (manifest + source bundle) to
5
5
  disk. The manifest builder reads only the public ``App`` surface, so it works in
6
6
  discovery mode (where submit runs). ``ensure_bundle`` memoises the written package
7
- per *submission* — ``(app, job_name, args)`` — so re-submitting the same job with
8
- the same args reuses the package, while a second spawn with a different job or
9
- different args writes a distinct package (no clobber).
7
+ per *submission* — ``(app, entry_file, job_name, args)`` — so re-submitting the
8
+ same file and job with the same args reuses the package, while a second spawn with
9
+ a different entry file, job, or args writes a distinct package (no clobber).
10
10
 
11
11
  Submit NEVER executes a job and NEVER invokes the backend — it only writes the
12
12
  package. This module imports no heavy library; it stays on the torch-free side of
@@ -16,11 +16,17 @@ package it writes.
16
16
 
17
17
  from __future__ import annotations
18
18
 
19
- from pathlib import Path
19
+ from pathlib import Path, PurePosixPath
20
20
  from typing import TYPE_CHECKING, Any, Optional, Sequence
21
21
 
22
22
  from simulo._client.asset import Asset
23
- from simulo._client.packaging import assert_credentials_outside_project, canonical_args, package, read_bundle_package_id
23
+ from simulo._client.packaging import (
24
+ assert_credentials_outside_project,
25
+ canonical_args,
26
+ package,
27
+ project_relative_entry_file,
28
+ read_bundle_package_id,
29
+ )
24
30
  from simulo._client.volume import Volume
25
31
  from simulo.interfaces.platform import is_platform_pinned
26
32
  from simulo.interfaces.platform.manifest import Manifest
@@ -33,10 +39,10 @@ if TYPE_CHECKING: # typing-only; avoids any import cycle with app.py at runtime
33
39
  DEFAULT_OUT_SUBPATH = (".simulo", "packages")
34
40
 
35
41
  # Memoise the built bundle per *submission* (per process). Keyed on
36
- # ``(id(app), job_name, canonical_json(args))`` — an App lives for the whole run,
37
- # so identity reuse is not a concern, and keying on the job + args means multiple
38
- # programmatic spawns each write their own package instead of the first one being
39
- # reused for all.
42
+ # ``(id(app), entry_file, job_name, canonical_json(args))`` — an App lives for
43
+ # the whole run, so identity reuse is not a concern, and keying on the selected
44
+ # file + job + args means multiple programmatic spawns each write their own
45
+ # package instead of the first one being reused for all.
40
46
  #
41
47
  # Each entry stores ``(bundle_dir, package_id)`` — the package_id the bundle
42
48
  # was written for when THIS process cached it — not just the path. The local
@@ -48,15 +54,15 @@ DEFAULT_OUT_SUBPATH = (".simulo", "packages")
48
54
  # rmtree+rewrites whatever it finds there). ``ensure_bundle`` re-verifies the
49
55
  # manifest on every cache hit before trusting it (see below) — turning a
50
56
  # prefix collision into a redundant rebuild, never a silently wrong package.
51
- #: Cache key's 4th element is the sorted, de-duplicated URIs of every
57
+ #: Cache key's 5th element is the sorted, de-duplicated URIs of every
52
58
  #: ambiently-CAPTURED asset visible at this ``ensure_bundle`` call (construction-
53
59
  #: capture — see ``asset.py``) — included so a LATER spawn() in the same process,
54
60
  #: after user code has constructed MORE assets since an earlier spawn() with
55
- #: the identical ``(app, job_name, args)``, is never served a stale cached bundle
61
+ #: the identical ``(app, entry_file, job_name, args)``, is never served a stale cached bundle
56
62
  #: whose ``resources`` predates those constructions (``package_id`` itself never
57
63
  #: changes with the asset set — see the package-hash-invariance rule — so the
58
64
  #: `package_id` re-check below cannot by itself catch this staleness).
59
- _BUNDLE_CACHE: dict[tuple[int, str, str, tuple[str, ...]], tuple[Path, str]] = {}
65
+ _BUNDLE_CACHE: dict[tuple[int, str, str, str, tuple[str, ...]], tuple[Path, str]] = {}
60
66
 
61
67
 
62
68
  def _mount_ref(mount: object) -> dict[str, Any]:
@@ -133,8 +139,8 @@ def build_manifest(
133
139
  runtime's compiled camera-sensor bindings in ways undiagnosable from the
134
140
  job's own traceback, so this raises — it never skips-and-warns.
135
141
  """
136
- rel = app_file.resolve().relative_to(project_root)
137
- module = ".".join(rel.with_suffix("").parts)
142
+ entry_file = project_relative_entry_file(app_file, project_root)
143
+ module = ".".join(PurePosixPath(entry_file).with_suffix("").parts)
138
144
 
139
145
  volumes: dict[str, Any] = {}
140
146
  resource_by_uri: dict[str, dict[str, Any]] = {}
@@ -166,7 +172,7 @@ def build_manifest(
166
172
  # ``local_entrypoint`` remains the stable v2 wire key. The backend
167
173
  # runner reads only ``entrypoint.module``, but the complete v2 bytes are
168
174
  # pinned by ``test_manifest_wire_stability.py``.
169
- entrypoint={"file": rel.as_posix(), "module": module, "local_entrypoint": None},
175
+ entrypoint={"file": entry_file, "module": module, "local_entrypoint": None},
170
176
  runtime_env=dict(getattr(runtime, "runtime_env", {})),
171
177
  jobs={name: _job_entry(spec) for name, spec in app.jobs.items()},
172
178
  resources=resources,
@@ -191,9 +197,9 @@ def ensure_bundle(
191
197
  This is the *submit* primitive: the manifest is built from the live app object
192
198
  (no second discovery import) with the submitted job's ``args`` recorded, and
193
199
  the source tree is written to ``<out>/<bundle_dir_name(package_id)>/`` — the
194
- per-submission directory keyed on source + ``job_name`` + ``args`` (named by
200
+ per-submission directory keyed on source + entry file + ``job_name`` + ``args`` (named by
195
201
  the id's short, filesystem-safe form; see ``packaging.bundle_dir_name``).
196
- Memoised per submission (``(id(app), job_name, canonical_json(args), sorted
202
+ Memoised per submission (``(id(app), entry_file, job_name, canonical_json(args), sorted
197
203
  extra_assets uris)``) so re-submitting the same job with the same args AND the
198
204
  same captured-asset set reuses the package, while a different job, different
199
205
  args, or a DIFFERENT captured-asset set (more constructions since an earlier
@@ -212,8 +218,9 @@ def ensure_bundle(
212
218
  submission), never served as a silently wrong package.
213
219
  """
214
220
  assert_credentials_outside_project(project_root)
221
+ entry_file = project_relative_entry_file(app_file, project_root)
215
222
  extra_asset_uris = tuple(sorted({asset.uri for asset in extra_assets}))
216
- cache_key = (id(app), job_name, canonical_args(args), extra_asset_uris)
223
+ cache_key = (id(app), entry_file, job_name, canonical_args(args), extra_asset_uris)
217
224
  cached = _BUNDLE_CACHE.get(cache_key)
218
225
  if cached is not None:
219
226
  cached_dir, cached_package_id = cached