simulo-interfaces 0.1.0__py3-none-any.whl
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.
- simulo/interfaces/__init__.py +65 -0
- simulo/interfaces/exceptions.py +31 -0
- simulo/interfaces/ids.py +21 -0
- simulo/interfaces/platform/__init__.py +120 -0
- simulo/interfaces/platform/app.py +47 -0
- simulo/interfaces/platform/asset.py +15 -0
- simulo/interfaces/platform/callbacks.py +17 -0
- simulo/interfaces/platform/debug.py +31 -0
- simulo/interfaces/platform/domain.py +118 -0
- simulo/interfaces/platform/enums.py +49 -0
- simulo/interfaces/platform/runs.py +112 -0
- simulo/interfaces/platform/runtime.py +19 -0
- simulo/interfaces/platform/submit.py +538 -0
- simulo/interfaces/platform/volume.py +13 -0
- simulo/interfaces/py.typed +0 -0
- simulo/interfaces/runtime/__init__.py +50 -0
- simulo/interfaces/runtime/anomaly.py +195 -0
- simulo/interfaces/runtime/components.py +35 -0
- simulo/interfaces/runtime/env.py +27 -0
- simulo/interfaces/runtime/player.py +19 -0
- simulo/interfaces/runtime/policy.py +16 -0
- simulo/interfaces/runtime/scenario.py +13 -0
- simulo/interfaces/runtime/task.py +21 -0
- simulo/interfaces/runtime/tensors.py +12 -0
- simulo/interfaces/runtime/trainer.py +37 -0
- simulo_interfaces-0.1.0.dist-info/METADATA +166 -0
- simulo_interfaces-0.1.0.dist-info/RECORD +29 -0
- simulo_interfaces-0.1.0.dist-info/WHEEL +5 -0
- simulo_interfaces-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Simulo Interfaces — implementation-free contract layer for the Simulo platform.
|
|
2
|
+
|
|
3
|
+
Torch-free, zero-runtime-dependency (Rule #11). Importable on a laptop with no
|
|
4
|
+
torch / numpy / gymnasium / Isaac / AWS / HTTP / DB installed. Two parts:
|
|
5
|
+
|
|
6
|
+
* **Part A — ``simulo.interfaces.runtime``**: tensor-library-agnostic structural
|
|
7
|
+
Protocols mirroring the ``simulo.core`` runtime surface (Task, Policy,
|
|
8
|
+
Trainer, Player, LearningEnv, Scenario, and the observation/reward/termination
|
|
9
|
+
components).
|
|
10
|
+
* **Part B — ``simulo.interfaces.platform``**: platform/workspace domain
|
|
11
|
+
contracts — wire-stable enums, frozen domain dataclasses (Project, Package,
|
|
12
|
+
Job, Resource, Checkpoint, TrainedModel, Tag), and the cloud-execution
|
|
13
|
+
Protocols (App, Runtime, Asset, Volume, callbacks, and
|
|
14
|
+
debug/replay/visualization sessions).
|
|
15
|
+
|
|
16
|
+
This package contributes ``simulo.interfaces.*`` to the shared ``simulo`` PEP 420
|
|
17
|
+
namespace that ``simulo-backend`` also contributes to; it ships NO
|
|
18
|
+
``src/simulo/__init__.py``.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from simulo.interfaces import platform, runtime
|
|
22
|
+
from simulo.interfaces.exceptions import (
|
|
23
|
+
ContractViolationError,
|
|
24
|
+
ImportResolutionError,
|
|
25
|
+
JobFailedError,
|
|
26
|
+
ResourceNotFoundError,
|
|
27
|
+
ResumeIncompatibleError,
|
|
28
|
+
SimuloError,
|
|
29
|
+
)
|
|
30
|
+
from simulo.interfaces.ids import (
|
|
31
|
+
CheckpointId,
|
|
32
|
+
Digest,
|
|
33
|
+
JobId,
|
|
34
|
+
OrganizationId,
|
|
35
|
+
PackageId,
|
|
36
|
+
ProjectId,
|
|
37
|
+
ResourceId,
|
|
38
|
+
ResourceUri,
|
|
39
|
+
TagId,
|
|
40
|
+
TrainedModelId,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
# subpackages
|
|
45
|
+
"platform",
|
|
46
|
+
"runtime",
|
|
47
|
+
# exception hierarchy
|
|
48
|
+
"SimuloError",
|
|
49
|
+
"ContractViolationError",
|
|
50
|
+
"ImportResolutionError",
|
|
51
|
+
"ResourceNotFoundError",
|
|
52
|
+
"ResumeIncompatibleError",
|
|
53
|
+
"JobFailedError",
|
|
54
|
+
# identifiers
|
|
55
|
+
"OrganizationId",
|
|
56
|
+
"ProjectId",
|
|
57
|
+
"PackageId",
|
|
58
|
+
"JobId",
|
|
59
|
+
"ResourceId",
|
|
60
|
+
"CheckpointId",
|
|
61
|
+
"TrainedModelId",
|
|
62
|
+
"TagId",
|
|
63
|
+
"ResourceUri",
|
|
64
|
+
"Digest",
|
|
65
|
+
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Shared Simulo contract exception hierarchy.
|
|
2
|
+
|
|
3
|
+
Implementation-free (Rule #11): no I/O, no heavy deps. These types are the
|
|
4
|
+
common error vocabulary the thin ``simulo`` client and the ``simulo-backend``
|
|
5
|
+
GPU SDK both raise/catch when a contract is violated.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SimuloError(Exception):
|
|
10
|
+
"""Root of the shared Simulo contract exception hierarchy."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ContractViolationError(SimuloError):
|
|
14
|
+
"""An object failed to satisfy a Simulo contract/Protocol."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ImportResolutionError(SimuloError):
|
|
18
|
+
"""A deferred ``runtime.imports()`` symbol was used in local discovery mode
|
|
19
|
+
without a resolved cloud import (Rule #3)."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ResourceNotFoundError(SimuloError):
|
|
23
|
+
"""``from_registry()``/``from_name()`` could not resolve a resource/volume (Rule #6)."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ResumeIncompatibleError(SimuloError):
|
|
27
|
+
"""A discovered checkpoint is incompatible with the current job (Rule #9)."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class JobFailedError(SimuloError):
|
|
31
|
+
"""A submitted job terminated in ``JobStatus.FAILED``."""
|
simulo/interfaces/ids.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Strongly-typed string identifiers for the platform domain model (Part B).
|
|
2
|
+
|
|
3
|
+
``NewType`` aliases — zero runtime cost (each is the underlying ``str`` at
|
|
4
|
+
runtime) but distinct to the type checker, so a ``ProjectId`` can never be
|
|
5
|
+
passed where a ``JobId`` is expected.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import NewType
|
|
9
|
+
|
|
10
|
+
OrganizationId = NewType("OrganizationId", str)
|
|
11
|
+
ProjectId = NewType("ProjectId", str)
|
|
12
|
+
PackageId = NewType("PackageId", str)
|
|
13
|
+
JobId = NewType("JobId", str)
|
|
14
|
+
ResourceId = NewType("ResourceId", str)
|
|
15
|
+
CheckpointId = NewType("CheckpointId", str)
|
|
16
|
+
TrainedModelId = NewType("TrainedModelId", str)
|
|
17
|
+
TagId = NewType("TagId", str)
|
|
18
|
+
ResourceUri = NewType("ResourceUri", str) # e.g. "robot/so-arm-100:v3"
|
|
19
|
+
Digest = NewType("Digest", str) # content-addressed package source digest
|
|
20
|
+
RecordingId = NewType("RecordingId", str) # a job-produced MCAP recording (PR-0 wire contract)
|
|
21
|
+
WorkerId = NewType("WorkerId", str) # a claiming worker process (PR-0 wire contract)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Part B — platform/workspace domain contracts.
|
|
2
|
+
|
|
3
|
+
Wire-stable enums, frozen domain dataclasses (Project, Package, Job, Resource,
|
|
4
|
+
Checkpoint, TrainedModel, Tag), and the cloud-execution Protocols (App, Runtime,
|
|
5
|
+
Asset, Volume, job callbacks, and debug/replay/visualization sessions). All
|
|
6
|
+
implementation-free (Rule #11) — concrete behaviour lives in the future
|
|
7
|
+
``simulo`` client and the ``simulo-backend`` SDK.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from simulo.interfaces.platform.app import AppProtocol, JobFunctionProtocol, Mount
|
|
11
|
+
from simulo.interfaces.platform.asset import AssetProtocol
|
|
12
|
+
from simulo.interfaces.platform.callbacks import JobCallbackProtocol
|
|
13
|
+
from simulo.interfaces.platform.debug import DebugSessionProtocol, ReplaySessionProtocol, VisualizationSessionProtocol
|
|
14
|
+
from simulo.interfaces.platform.domain import Checkpoint, Job, Package, Project, Resource, Tag, TrainedModel
|
|
15
|
+
from simulo.interfaces.platform.enums import ArtifactKind, JobStatus, ResourceKind, ResumePolicy
|
|
16
|
+
from simulo.interfaces.platform.runs import (
|
|
17
|
+
JOB_LOGS_ROUTE_TEMPLATE,
|
|
18
|
+
JOB_RESULT_ROUTE_TEMPLATE,
|
|
19
|
+
JOB_ROUTE_TEMPLATE,
|
|
20
|
+
JOBS_API_DEFAULT_PORT,
|
|
21
|
+
JOBS_ROUTE,
|
|
22
|
+
LOG_CHUNK_MAX_BYTES,
|
|
23
|
+
TERMINAL_JOB_STATUSES,
|
|
24
|
+
JobRecord,
|
|
25
|
+
)
|
|
26
|
+
from simulo.interfaces.platform.runtime import RuntimeProtocol
|
|
27
|
+
from simulo.interfaces.platform.submit import (
|
|
28
|
+
JOB_MODEL_DOWNLOAD_ROUTE_TEMPLATE,
|
|
29
|
+
JOB_MODEL_ROUTE_TEMPLATE,
|
|
30
|
+
JOB_MODELS_ROUTE_TEMPLATE,
|
|
31
|
+
JOB_RECORDING_DOWNLOAD_ROUTE_TEMPLATE,
|
|
32
|
+
JOB_RECORDINGS_ROUTE_TEMPLATE,
|
|
33
|
+
MAX_COMPLETE_MESSAGE_CHARS,
|
|
34
|
+
MAX_COMPLETE_REASON_CODE_CHARS,
|
|
35
|
+
MAX_LOG_CHUNK_BYTES,
|
|
36
|
+
MAX_LOG_TOTAL_BYTES,
|
|
37
|
+
MAX_PACKAGE_BYTES,
|
|
38
|
+
MAX_RESULT_BYTES,
|
|
39
|
+
MODEL_KINDS,
|
|
40
|
+
PACKAGE_ARCHIVE_ROUTE_TEMPLATE,
|
|
41
|
+
PACKAGES_ROUTE,
|
|
42
|
+
RESERVED_RUNTIME_ENV_KEYS,
|
|
43
|
+
RESERVED_RUNTIME_ENV_PREFIXES,
|
|
44
|
+
WORKER_CLAIM_ROUTE,
|
|
45
|
+
WORKER_JOB_COMPLETE_ROUTE_TEMPLATE,
|
|
46
|
+
WORKER_JOB_HEARTBEAT_ROUTE_TEMPLATE,
|
|
47
|
+
WORKER_JOB_LOGS_ROUTE_TEMPLATE,
|
|
48
|
+
WORKER_JOB_MODELS_PRESIGN_ROUTE_TEMPLATE,
|
|
49
|
+
WORKER_JOB_MODELS_ROUTE_TEMPLATE,
|
|
50
|
+
WORKER_JOB_RECORDINGS_PRESIGN_ROUTE_TEMPLATE,
|
|
51
|
+
WORKER_JOB_RECORDINGS_ROUTE_TEMPLATE,
|
|
52
|
+
WORKER_LEASE_HEADER,
|
|
53
|
+
ModelRecord,
|
|
54
|
+
RecordingRecord,
|
|
55
|
+
)
|
|
56
|
+
from simulo.interfaces.platform.volume import VolumeProtocol
|
|
57
|
+
|
|
58
|
+
__all__ = [
|
|
59
|
+
# enums
|
|
60
|
+
"JobStatus",
|
|
61
|
+
"ResumePolicy",
|
|
62
|
+
"ResourceKind",
|
|
63
|
+
"ArtifactKind",
|
|
64
|
+
# domain dataclasses
|
|
65
|
+
"Tag",
|
|
66
|
+
"Project",
|
|
67
|
+
"Package",
|
|
68
|
+
"Resource",
|
|
69
|
+
"Job",
|
|
70
|
+
"Checkpoint",
|
|
71
|
+
"TrainedModel",
|
|
72
|
+
# job-run contract (run record schema + jobs HTTP API shape)
|
|
73
|
+
"JobRecord",
|
|
74
|
+
"TERMINAL_JOB_STATUSES",
|
|
75
|
+
"JOBS_API_DEFAULT_PORT",
|
|
76
|
+
"JOBS_ROUTE",
|
|
77
|
+
"JOB_ROUTE_TEMPLATE",
|
|
78
|
+
"JOB_LOGS_ROUTE_TEMPLATE",
|
|
79
|
+
"JOB_RESULT_ROUTE_TEMPLATE",
|
|
80
|
+
"LOG_CHUNK_MAX_BYTES",
|
|
81
|
+
# submit / worker / recordings wire contract (PR-0)
|
|
82
|
+
"PACKAGES_ROUTE",
|
|
83
|
+
"PACKAGE_ARCHIVE_ROUTE_TEMPLATE",
|
|
84
|
+
"JOB_RECORDINGS_ROUTE_TEMPLATE",
|
|
85
|
+
"JOB_RECORDING_DOWNLOAD_ROUTE_TEMPLATE",
|
|
86
|
+
"JOB_MODELS_ROUTE_TEMPLATE",
|
|
87
|
+
"JOB_MODEL_ROUTE_TEMPLATE",
|
|
88
|
+
"JOB_MODEL_DOWNLOAD_ROUTE_TEMPLATE",
|
|
89
|
+
"WORKER_CLAIM_ROUTE",
|
|
90
|
+
"WORKER_JOB_LOGS_ROUTE_TEMPLATE",
|
|
91
|
+
"WORKER_JOB_HEARTBEAT_ROUTE_TEMPLATE",
|
|
92
|
+
"WORKER_JOB_COMPLETE_ROUTE_TEMPLATE",
|
|
93
|
+
"WORKER_JOB_RECORDINGS_PRESIGN_ROUTE_TEMPLATE",
|
|
94
|
+
"WORKER_JOB_RECORDINGS_ROUTE_TEMPLATE",
|
|
95
|
+
"WORKER_JOB_MODELS_PRESIGN_ROUTE_TEMPLATE",
|
|
96
|
+
"WORKER_JOB_MODELS_ROUTE_TEMPLATE",
|
|
97
|
+
"WORKER_LEASE_HEADER",
|
|
98
|
+
"MAX_PACKAGE_BYTES",
|
|
99
|
+
"MAX_LOG_CHUNK_BYTES",
|
|
100
|
+
"MAX_LOG_TOTAL_BYTES",
|
|
101
|
+
"MAX_RESULT_BYTES",
|
|
102
|
+
"MAX_COMPLETE_MESSAGE_CHARS",
|
|
103
|
+
"MAX_COMPLETE_REASON_CODE_CHARS",
|
|
104
|
+
"MODEL_KINDS",
|
|
105
|
+
"ModelRecord",
|
|
106
|
+
"RecordingRecord",
|
|
107
|
+
"RESERVED_RUNTIME_ENV_KEYS",
|
|
108
|
+
"RESERVED_RUNTIME_ENV_PREFIXES",
|
|
109
|
+
# protocols
|
|
110
|
+
"RuntimeProtocol",
|
|
111
|
+
"AssetProtocol",
|
|
112
|
+
"VolumeProtocol",
|
|
113
|
+
"JobCallbackProtocol",
|
|
114
|
+
"JobFunctionProtocol",
|
|
115
|
+
"AppProtocol",
|
|
116
|
+
"Mount",
|
|
117
|
+
"DebugSessionProtocol",
|
|
118
|
+
"ReplaySessionProtocol",
|
|
119
|
+
"VisualizationSessionProtocol",
|
|
120
|
+
]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""User-facing job-container Protocols (Part B) — ``App`` / ``@app.job`` surface."""
|
|
2
|
+
|
|
3
|
+
from typing import Callable, Mapping, Optional, Protocol, Sequence, TypeVar, Union, overload, runtime_checkable
|
|
4
|
+
|
|
5
|
+
from simulo.interfaces.ids import JobId
|
|
6
|
+
from simulo.interfaces.platform.asset import AssetProtocol
|
|
7
|
+
from simulo.interfaces.platform.callbacks import JobCallbackProtocol
|
|
8
|
+
from simulo.interfaces.platform.enums import ResumePolicy
|
|
9
|
+
from simulo.interfaces.platform.runtime import RuntimeProtocol
|
|
10
|
+
from simulo.interfaces.platform.volume import VolumeProtocol
|
|
11
|
+
|
|
12
|
+
F = TypeVar("F", bound=Callable[..., object])
|
|
13
|
+
Mount = Union[AssetProtocol, VolumeProtocol]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@runtime_checkable
|
|
17
|
+
class JobFunctionProtocol(Protocol):
|
|
18
|
+
"""What ``@app.job`` returns: the user's original function, wrapped (Rule #1 — source-preserving). FORWARD."""
|
|
19
|
+
|
|
20
|
+
def __call__(self, *args: object, **kwargs: object) -> object: ...
|
|
21
|
+
def submit(self, *args: object, **kwargs: object) -> JobId: ...
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@runtime_checkable
|
|
25
|
+
class AppProtocol(Protocol):
|
|
26
|
+
"""User-facing job container: ``app = simulo.App(name, runtime=, mounts={...})``. FORWARD."""
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def name(self) -> str: ...
|
|
30
|
+
@property
|
|
31
|
+
def runtime(self) -> RuntimeProtocol: ...
|
|
32
|
+
@property
|
|
33
|
+
def mounts(self) -> Mapping[str, Mount]: ...
|
|
34
|
+
@overload
|
|
35
|
+
def job(self, fn: F) -> JobFunctionProtocol: ...
|
|
36
|
+
@overload
|
|
37
|
+
def job(
|
|
38
|
+
self, *, callbacks: Sequence[JobCallbackProtocol] = ..., resume: ResumePolicy = ...
|
|
39
|
+
) -> Callable[[F], JobFunctionProtocol]: ...
|
|
40
|
+
def job(
|
|
41
|
+
self,
|
|
42
|
+
fn: Optional[F] = None,
|
|
43
|
+
*,
|
|
44
|
+
callbacks: Sequence[JobCallbackProtocol] = (),
|
|
45
|
+
resume: ResumePolicy = ResumePolicy.AUTO,
|
|
46
|
+
) -> Union[JobFunctionProtocol, Callable[[F], JobFunctionProtocol]]: ...
|
|
47
|
+
def local_entrypoint(self, fn: F) -> F: ...
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""``AssetProtocol`` — read-only mounted cloud resource handle (Part B, Rule #6)."""
|
|
2
|
+
|
|
3
|
+
from typing import Protocol, runtime_checkable
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@runtime_checkable
|
|
7
|
+
class AssetProtocol(Protocol):
|
|
8
|
+
"""Read-only mounted cloud resource handle (Rule #6). FORWARD.
|
|
9
|
+
|
|
10
|
+
Distinct from ``simulo.core.asset.Asset`` (a USD/URDF simulation asset).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
@classmethod
|
|
14
|
+
def from_registry(cls, uri: str) -> "AssetProtocol": ...
|
|
15
|
+
def read_only(self) -> "AssetProtocol": ...
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Job lifecycle callback marker contract (Part B)."""
|
|
2
|
+
|
|
3
|
+
from typing import Protocol, runtime_checkable
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@runtime_checkable
|
|
7
|
+
class JobCallbackProtocol(Protocol):
|
|
8
|
+
"""Marker contract for job lifecycle callbacks passed to ``@app.job(callbacks=...)``.
|
|
9
|
+
|
|
10
|
+
Concrete callbacks (ResumableCheckpoint, CommitVolume, DebugOnAnomaly) are
|
|
11
|
+
implemented in the future ``simulo`` package. FORWARD. All hooks optional.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def on_job_start(self, job_id: str) -> None: ...
|
|
15
|
+
def on_checkpoint(self, checkpoint_uri: str, step: int) -> None: ...
|
|
16
|
+
def on_anomaly(self, detail: str) -> None: ...
|
|
17
|
+
def on_job_end(self, job_id: str, status: str) -> None: ...
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Remote debug / replay / visualization session Protocols (Part B, Rule #10)."""
|
|
2
|
+
|
|
3
|
+
from contextlib import AbstractContextManager
|
|
4
|
+
from typing import Protocol, runtime_checkable
|
|
5
|
+
|
|
6
|
+
from simulo.interfaces.ids import JobId
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@runtime_checkable
|
|
10
|
+
class DebugSessionProtocol(Protocol):
|
|
11
|
+
"""Remote attach/breakpoint/shell session (Rule #10). FORWARD."""
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def job_id(self) -> JobId: ...
|
|
15
|
+
def attach(self) -> AbstractContextManager[None]: ...
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@runtime_checkable
|
|
19
|
+
class ReplaySessionProtocol(Protocol):
|
|
20
|
+
"""Replay of a recorded run (Rule #10). FORWARD."""
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def job_id(self) -> JobId: ...
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@runtime_checkable
|
|
27
|
+
class VisualizationSessionProtocol(Protocol):
|
|
28
|
+
"""Cloud visualization runtime session (Rule #10). FORWARD."""
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def job_id(self) -> JobId: ...
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Platform domain dataclasses — the workspace object model (Part B).
|
|
2
|
+
|
|
3
|
+
Frozen, slotted, keyword-only value objects shared by the thin ``simulo`` client
|
|
4
|
+
and the ``simulo-backend`` SDK. Implementation-free (Rule #11): no behaviour, no
|
|
5
|
+
I/O, no persistence — these mirror the records the control plane owns.
|
|
6
|
+
|
|
7
|
+
Domain invariants encoded here:
|
|
8
|
+
|
|
9
|
+
* **Rule #5** — ``Package`` (immutable, content-addressed source archive) and
|
|
10
|
+
``Job`` (one execution of a package) are distinct; a ``Job`` references its
|
|
11
|
+
``Package`` by id, never the other way around.
|
|
12
|
+
* **Rule #6** — ``Resource`` is a read-only, versioned, registry-resolved mount
|
|
13
|
+
(robot / environment / dataset / …); it is NOT a package.
|
|
14
|
+
* **Rule #7** — ``Project`` is the workspace anchor every other object hangs off.
|
|
15
|
+
* **Rule #8** — ``Checkpoint`` (intermediate training artifact) and
|
|
16
|
+
``TrainedModel`` (final, deployable model) are separate types — never collapse.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from datetime import datetime
|
|
21
|
+
from typing import Optional, Tuple
|
|
22
|
+
|
|
23
|
+
from simulo.interfaces.ids import (
|
|
24
|
+
CheckpointId,
|
|
25
|
+
Digest,
|
|
26
|
+
JobId,
|
|
27
|
+
OrganizationId,
|
|
28
|
+
PackageId,
|
|
29
|
+
ProjectId,
|
|
30
|
+
ResourceId,
|
|
31
|
+
ResourceUri,
|
|
32
|
+
TagId,
|
|
33
|
+
TrainedModelId,
|
|
34
|
+
)
|
|
35
|
+
from simulo.interfaces.platform.enums import JobStatus, ResourceKind, ResumePolicy
|
|
36
|
+
|
|
37
|
+
# Shared dataclass options: immutable value objects (frozen), memory-lean
|
|
38
|
+
# (slots), and keyword-only construction (kw_only) so positional drift across
|
|
39
|
+
# the client/SDK boundary is impossible.
|
|
40
|
+
_DC = {"frozen": True, "slots": True, "kw_only": True}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(**_DC)
|
|
44
|
+
class Tag:
|
|
45
|
+
"""A key/value (value optional) label attachable to workspace objects."""
|
|
46
|
+
|
|
47
|
+
id: TagId
|
|
48
|
+
key: str
|
|
49
|
+
value: Optional[str] = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(**_DC)
|
|
53
|
+
class Project:
|
|
54
|
+
"""Workspace anchor (Rule #7) — every other domain object hangs off a project."""
|
|
55
|
+
|
|
56
|
+
id: ProjectId
|
|
57
|
+
organization_id: OrganizationId
|
|
58
|
+
name: str
|
|
59
|
+
slug: str
|
|
60
|
+
created_at: datetime
|
|
61
|
+
tags: Tuple[TagId, ...] = field(default_factory=tuple)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(**_DC)
|
|
65
|
+
class Package:
|
|
66
|
+
"""Immutable, content-addressed source archive (Rule #5)."""
|
|
67
|
+
|
|
68
|
+
id: PackageId
|
|
69
|
+
project_id: ProjectId
|
|
70
|
+
digest: Digest
|
|
71
|
+
created_at: datetime
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(**_DC)
|
|
75
|
+
class Resource:
|
|
76
|
+
"""Read-only, versioned, registry-resolved mount (Rule #6) — not a package."""
|
|
77
|
+
|
|
78
|
+
id: ResourceId
|
|
79
|
+
project_id: ProjectId
|
|
80
|
+
uri: ResourceUri
|
|
81
|
+
kind: ResourceKind
|
|
82
|
+
version: str
|
|
83
|
+
read_only: bool = True
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(**_DC)
|
|
87
|
+
class Job:
|
|
88
|
+
"""One execution of a package (Rule #5 lineage) — references its package by id."""
|
|
89
|
+
|
|
90
|
+
id: JobId
|
|
91
|
+
project_id: ProjectId
|
|
92
|
+
package_id: PackageId
|
|
93
|
+
status: JobStatus
|
|
94
|
+
resume_policy: ResumePolicy = ResumePolicy.AUTO
|
|
95
|
+
created_at: Optional[datetime] = None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass(**_DC)
|
|
99
|
+
class Checkpoint:
|
|
100
|
+
"""Intermediate training artifact (Rule #8) — distinct from a TrainedModel."""
|
|
101
|
+
|
|
102
|
+
id: CheckpointId
|
|
103
|
+
job_id: JobId
|
|
104
|
+
step: int
|
|
105
|
+
uri: ResourceUri
|
|
106
|
+
created_at: Optional[datetime] = None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass(**_DC)
|
|
110
|
+
class TrainedModel:
|
|
111
|
+
"""Final, deployable model artifact (Rule #8) — distinct from a Checkpoint."""
|
|
112
|
+
|
|
113
|
+
id: TrainedModelId
|
|
114
|
+
project_id: ProjectId
|
|
115
|
+
job_id: JobId
|
|
116
|
+
uri: ResourceUri
|
|
117
|
+
format: str
|
|
118
|
+
created_at: Optional[datetime] = None
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Wire-stable platform enums (Part B).
|
|
2
|
+
|
|
3
|
+
Enum *values* are a stable wire ABI: append-only, never renumber or rename. The
|
|
4
|
+
thin client and the SDK both serialize/deserialize against these strings.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from enum import StrEnum
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class JobStatus(StrEnum):
|
|
11
|
+
"""Lifecycle state of a submitted job."""
|
|
12
|
+
|
|
13
|
+
QUEUED = "queued"
|
|
14
|
+
RUNNING = "running"
|
|
15
|
+
COMPLETED = "completed"
|
|
16
|
+
FAILED = "failed"
|
|
17
|
+
CANCELLED = "cancelled"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ResumePolicy(StrEnum):
|
|
21
|
+
"""How a job resumes from a discovered checkpoint (Rule #9)."""
|
|
22
|
+
|
|
23
|
+
AUTO = "auto"
|
|
24
|
+
NEVER = "never"
|
|
25
|
+
MANUAL = "manual"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ResourceKind(StrEnum):
|
|
29
|
+
"""Kind of a registry-resolved, read-only resource mount (Rule #6)."""
|
|
30
|
+
|
|
31
|
+
ROBOT = "robot"
|
|
32
|
+
ENVIRONMENT = "environment"
|
|
33
|
+
DATASET = "dataset"
|
|
34
|
+
REPLAY_INPUT = "replay_input"
|
|
35
|
+
EVAL_ASSET = "eval_asset"
|
|
36
|
+
CHECKPOINT = "checkpoint"
|
|
37
|
+
TRAINED_MODEL = "trained_model"
|
|
38
|
+
CONFIG = "config"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ArtifactKind(StrEnum):
|
|
42
|
+
"""Kind of a job-produced artifact. Rule #8 — a checkpoint is NOT a trained model."""
|
|
43
|
+
|
|
44
|
+
MODEL_CHECKPOINT = "model_checkpoint"
|
|
45
|
+
TRAINED_MODEL = "trained_model"
|
|
46
|
+
LOG = "log"
|
|
47
|
+
METRIC = "metric"
|
|
48
|
+
REPLAY_OUTPUT = "replay_output"
|
|
49
|
+
EVALUATION_OUTPUT = "evaluation_output"
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Job-run contract — the run-record schema and the jobs HTTP API shape (Part B).
|
|
2
|
+
|
|
3
|
+
This module is the DURABLE contract between the executor side (which records runs
|
|
4
|
+
and serves them — today ``simulo-backend run-package`` + ``simulo-backend serve``,
|
|
5
|
+
the local stand-ins; eventually the cloud worker + control plane) and the thin
|
|
6
|
+
``simulo`` client (``simulo jobs`` / ``simulo logs`` / ``simulo result``, which
|
|
7
|
+
talk HTTP only — never the run store on disk). Implementation-free (Rule #11):
|
|
8
|
+
schema, enum reuse, endpoint constants, and semantics docstrings only.
|
|
9
|
+
|
|
10
|
+
Field names and lifecycle align with ``context/use-cases/Job.md``: a job is one
|
|
11
|
+
managed execution of exactly one package, with lifecycle states drawn from
|
|
12
|
+
:class:`~simulo.interfaces.platform.enums.JobStatus` (``queued``/``running``/
|
|
13
|
+
``completed``/``failed``/``cancelled`` — Job.md's wording; the local executor
|
|
14
|
+
uses the ``running`` → ``completed``|``failed`` subset, the cloud adds
|
|
15
|
+
``queued``/``cancelled``).
|
|
16
|
+
|
|
17
|
+
HTTP API shape (all responses JSON; all timestamps ISO 8601 UTC, e.g.
|
|
18
|
+
``2026-07-03T12:00:00Z``):
|
|
19
|
+
|
|
20
|
+
* ``GET /v1/jobs?page=1&limit=20`` — list job records, most recent first
|
|
21
|
+
(``created_at`` descending). Paginated envelope::
|
|
22
|
+
|
|
23
|
+
{"items": [<JobRecord>, ...], "total": 3, "page": 1, "limit": 20, "pages": 1}
|
|
24
|
+
|
|
25
|
+
``limit`` defaults to 20, maximum 100; out-of-range ``page``/``limit`` → 422.
|
|
26
|
+
* ``GET /v1/jobs/{job_id}`` — one job record (the :class:`JobRecord` fields).
|
|
27
|
+
* ``GET /v1/jobs/{job_id}/logs?offset=N`` — one log chunk::
|
|
28
|
+
|
|
29
|
+
{"chunk": "<text>", "next_offset": 4096, "status": "running"}
|
|
30
|
+
|
|
31
|
+
**Offset semantics:** ``offset`` is a BYTE offset into the job's log stream
|
|
32
|
+
(``offset=0`` is the start). The server returns at most
|
|
33
|
+
:data:`LOG_CHUNK_MAX_BYTES` bytes per request (it may return fewer, e.g. to
|
|
34
|
+
avoid splitting a multi-byte UTF-8 character); the client resumes from the
|
|
35
|
+
returned ``next_offset``. A reconnect is just "resume from your offset". An
|
|
36
|
+
offset at or beyond end-of-log yields an empty ``chunk`` with
|
|
37
|
+
``next_offset == offset``; the accompanying ``status`` tells the client
|
|
38
|
+
whether more output may still arrive (poll while the status is
|
|
39
|
+
non-terminal, stop once it is in :data:`TERMINAL_JOB_STATUSES`).
|
|
40
|
+
* ``GET /v1/jobs/{job_id}/result`` — the job's result JSON object. 404 (code
|
|
41
|
+
``result_not_available``) until the job has completed successfully.
|
|
42
|
+
|
|
43
|
+
**Scoping (evolves toward the cloud):** every route above is UNSCOPED locally
|
|
44
|
+
— there is no project/org filter because the local stand-in has exactly one
|
|
45
|
+
run store and one caller. The real control plane scopes ``GET /v1/jobs`` (and
|
|
46
|
+
the per-job routes) to the caller's authenticated organization/project, via
|
|
47
|
+
query parameters (e.g. ``?project_id=...``) and/or the auth context resolved
|
|
48
|
+
from the request's bearer token — never a client-supplied trust boundary.
|
|
49
|
+
That scoping is additive at the transport layer: the :class:`JobRecord`
|
|
50
|
+
fields and route shapes defined here do not change, only the server-side
|
|
51
|
+
filtering they are evaluated under.
|
|
52
|
+
|
|
53
|
+
Errors use the platform-standard shape (NFR contract)::
|
|
54
|
+
|
|
55
|
+
{"error": {"code": "job_not_found", "message": "...", "request_id": "req_..."}}
|
|
56
|
+
|
|
57
|
+
Unknown ``job_id`` → 404 ``job_not_found``.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
from collections.abc import Mapping
|
|
61
|
+
from dataclasses import dataclass, field
|
|
62
|
+
from typing import Any, Optional
|
|
63
|
+
|
|
64
|
+
from simulo.interfaces.ids import JobId, PackageId
|
|
65
|
+
from simulo.interfaces.platform.enums import JobStatus
|
|
66
|
+
|
|
67
|
+
#: Default TCP port for the jobs API. Locally this is where ``simulo-backend
|
|
68
|
+
#: serve`` listens (127.0.0.1 only) and where the thin client points when
|
|
69
|
+
#: ``SIMULO_API_URL`` is unset (``http://127.0.0.1:8765``).
|
|
70
|
+
JOBS_API_DEFAULT_PORT = 8765
|
|
71
|
+
|
|
72
|
+
#: ``GET`` — list job records (paginated, most recent first).
|
|
73
|
+
JOBS_ROUTE = "/v1/jobs"
|
|
74
|
+
#: ``GET`` — one job record. ``.format(job_id=...)``.
|
|
75
|
+
JOB_ROUTE_TEMPLATE = "/v1/jobs/{job_id}"
|
|
76
|
+
#: ``GET`` — one log chunk by byte offset (``?offset=N``). ``.format(job_id=...)``.
|
|
77
|
+
JOB_LOGS_ROUTE_TEMPLATE = "/v1/jobs/{job_id}/logs"
|
|
78
|
+
#: ``GET`` — the result JSON of a successfully completed job. ``.format(job_id=...)``.
|
|
79
|
+
JOB_RESULT_ROUTE_TEMPLATE = "/v1/jobs/{job_id}/result"
|
|
80
|
+
|
|
81
|
+
#: Upper bound on the log bytes a single ``/logs`` response carries. The server
|
|
82
|
+
#: may return fewer bytes (never more); clients keep reading from ``next_offset``.
|
|
83
|
+
LOG_CHUNK_MAX_BYTES = 64 * 1024
|
|
84
|
+
|
|
85
|
+
#: Statuses after which a job's log stream and record no longer change. A log
|
|
86
|
+
#: follower drains the remaining bytes and stops once the status is terminal.
|
|
87
|
+
TERMINAL_JOB_STATUSES = frozenset({JobStatus.COMPLETED, JobStatus.FAILED, JobStatus.CANCELLED})
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
91
|
+
class JobRecord:
|
|
92
|
+
"""One recorded run of a packaged job — the wire schema of a job record.
|
|
93
|
+
|
|
94
|
+
This is the JSON object stored per run and returned by ``GET /v1/jobs`` /
|
|
95
|
+
``GET /v1/jobs/{job_id}``. Timestamps are ISO 8601 UTC strings (wire-shaped
|
|
96
|
+
by construction so ``dataclasses.asdict`` serializes directly; ``status`` is
|
|
97
|
+
a :class:`~simulo.interfaces.platform.enums.JobStatus` ``StrEnum`` and
|
|
98
|
+
JSON-encodes as its string value).
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
job_id: JobId
|
|
102
|
+
package_id: PackageId
|
|
103
|
+
job_name: str
|
|
104
|
+
status: JobStatus
|
|
105
|
+
created_at: str
|
|
106
|
+
args: Mapping[str, Any] = field(default_factory=dict)
|
|
107
|
+
started_at: Optional[str] = None
|
|
108
|
+
finished_at: Optional[str] = None
|
|
109
|
+
#: Process exit code of the run (0 on success). ``None`` while running.
|
|
110
|
+
exit_code: Optional[int] = None
|
|
111
|
+
#: Number of execution attempts started so far (retries make this > 1).
|
|
112
|
+
attempts: int = 0
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""``RuntimeProtocol`` — cloud runtime envelope contract (Part B, Rule #4)."""
|
|
2
|
+
|
|
3
|
+
from contextlib import AbstractContextManager
|
|
4
|
+
from typing import Mapping, Protocol, runtime_checkable
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@runtime_checkable
|
|
8
|
+
class RuntimeProtocol(Protocol):
|
|
9
|
+
"""Cloud runtime envelope (Rule #4). FORWARD.
|
|
10
|
+
|
|
11
|
+
``imports()`` is the real remote-only-import boundary (Rule #3): locally it
|
|
12
|
+
records deferred import intent; in the cloud it resolves the real imports.
|
|
13
|
+
Heavy imports (torch / Isaac / …) live ONLY inside this context manager.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def from_registry(cls, image: str) -> "RuntimeProtocol": ...
|
|
18
|
+
def env(self, variables: Mapping[str, str]) -> "RuntimeProtocol": ...
|
|
19
|
+
def imports(self) -> AbstractContextManager[None]: ...
|