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.
@@ -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``."""
@@ -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]: ...