muse-code-sdk 1.3.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,149 @@
1
+ """The two C-638-2 pydantic seams: inbound frame parsing and spawn options.
2
+
3
+ Owner ruling #31276, arm (a) (Constitution IV record in the issue comment):
4
+ the seams spec 638 C-638-2 names validate through pydantic models and reject
5
+ with the SDK's typed :class:`~muse_code.errors.MuseValidationError` wrapping
6
+ pydantic's ``ValidationError`` — never a bare ``isinstance`` check, an
7
+ ``AttributeError``/``KeyError``, or a silent partial object. pydantic is the
8
+ package's one exact-pinned runtime dependency (ADR 638 D2, INV-638-03); this
9
+ module is where that grant earns its keep.
10
+
11
+ The frame models are NARROWED views: they validate exactly the members this
12
+ facade dereferences (INV-638-02 — narrow and compose, never restate). A
13
+ full-fidelity pydantic restatement of the generated ``InitializeResult`` is
14
+ the arm C-638-2 forbids and routes back to the owner; deriving a validator
15
+ from the generated declaration is off the table too, because pydantic
16
+ refuses stdlib ``typing.TypedDict`` on the 3.10 floor. Unknown members pass
17
+ through untouched (``extra="allow"``), so the frame a consumer indexes stays
18
+ the generated shape.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from typing import Mapping, Sequence
24
+
25
+ from pydantic import BaseModel, ConfigDict, Field, ValidationError
26
+
27
+ from ..errors import MuseValidationError
28
+
29
+ MAX_SHUTDOWN_TIMEOUT_MS = 2_147_483_647
30
+ """The TS boundary's ceiling, kept identical so the two SDKs refuse the same
31
+ budgets (a budget past it is a consumer bug on either side)."""
32
+
33
+
34
+ class _SchemaInfoFrame(BaseModel):
35
+ """The ``schema`` member's seam view: the C-638-4 gate's input."""
36
+
37
+ model_config = ConfigDict(extra="allow", frozen=True)
38
+
39
+ fingerprint: str
40
+
41
+
42
+ class _ServerInfoFrame(BaseModel):
43
+ """The ``serverInfo`` member's seam view: the mismatch error's evidence."""
44
+
45
+ model_config = ConfigDict(extra="allow", frozen=True)
46
+
47
+ version: str
48
+
49
+
50
+ class InitializeResultFrame(BaseModel):
51
+ """The handshake seam: the members ``initialize()`` reads, validated.
52
+
53
+ ``schema`` is aliased because the name shadows ``BaseModel``'s own
54
+ (deprecated) attribute; the wire member is unchanged.
55
+ """
56
+
57
+ model_config = ConfigDict(extra="allow", frozen=True)
58
+
59
+ schema_info: _SchemaInfoFrame = Field(alias="schema")
60
+ server_info: _ServerInfoFrame = Field(alias="serverInfo")
61
+
62
+
63
+ def parse_initialize_result(raw: object) -> InitializeResultFrame:
64
+ """Validates one inbound ``initialize`` result frame (C-638-2 seam 1).
65
+
66
+ Args:
67
+ raw: The decoded result member, verbatim from the wire.
68
+
69
+ Returns:
70
+ The validated seam view; the caller keeps binding the raw ``dict``
71
+ as the generated ``InitializeResult``.
72
+
73
+ Raises:
74
+ MuseValidationError: The frame is malformed; pydantic's
75
+ ``ValidationError`` rides as the cause.
76
+ """
77
+ try:
78
+ return InitializeResultFrame.model_validate(raw)
79
+ except ValidationError as error:
80
+ raise MuseValidationError("initializeResult", error) from error
81
+
82
+
83
+ class SpawnOptions(BaseModel):
84
+ """The data half of the spawn surface, validated (C-638-2 seam 2).
85
+
86
+ Exactly the options that become process state: the callable plumbing
87
+ (``on_stderr``, ``connection_options``, the test-only deadline factory)
88
+ is composition, not data — ``Connection``'s constructor owns its own
89
+ options refusal (the never-orphan arm ``spawn_msp_connection`` already
90
+ guards), and ``ConnectionOptions`` is a stdlib ``TypedDict`` pydantic
91
+ cannot validate on the 3.10 floor.
92
+
93
+ ``shutdown_timeout_ms`` is strict — ``True`` and ``"5"`` are not
94
+ budgets — and bounded exactly like the TS boundary; a clamped budget
95
+ would fabricate a crash row for a host that was draining normally
96
+ (the PR #22819 lesson, kept identical).
97
+ """
98
+
99
+ model_config = ConfigDict(frozen=True)
100
+
101
+ command: str
102
+ args: tuple[str, ...]
103
+ cwd: str | None
104
+ env: dict[str, str] | None
105
+ shutdown_timeout_ms: int = Field(strict=True, ge=0, le=MAX_SHUTDOWN_TIMEOUT_MS)
106
+
107
+
108
+ def parse_spawn_options(
109
+ *,
110
+ command: str,
111
+ args: Sequence[str],
112
+ cwd: str | None,
113
+ env: Mapping[str, str] | None,
114
+ shutdown_timeout_ms: int,
115
+ ) -> SpawnOptions:
116
+ """Validates spawn options at construction, before any process exists.
117
+
118
+ Args:
119
+ command: The host binary to run.
120
+ args: Passed through verbatim once validated.
121
+ cwd: Working directory for the host.
122
+ env: The host's environment (inherited when ``None``).
123
+ shutdown_timeout_ms: The FR-017a drain budget.
124
+
125
+ Returns:
126
+ The validated, frozen options the spawn path reads from.
127
+
128
+ Raises:
129
+ MuseValidationError: An option is malformed; pydantic's
130
+ ``ValidationError`` rides as the cause. A ``ValueError`` by
131
+ inheritance, so the boundary's pre-#31276 refusal contract
132
+ holds.
133
+ """
134
+ try:
135
+ # The raw values go in untouched: pydantic owns the coercion
136
+ # (list -> tuple, Mapping -> dict) AND the refusals — pre-coercing
137
+ # here would turn a bare-string ``args`` into a tuple of characters
138
+ # before the model could reject it.
139
+ return SpawnOptions(
140
+ command=command,
141
+ # The wider runtime types are deliberate: pydantic owns the
142
+ # Sequence -> tuple / Mapping -> dict coercion and the refusals.
143
+ args=args, # type: ignore[arg-type]
144
+ cwd=cwd,
145
+ env=env, # type: ignore[arg-type]
146
+ shutdown_timeout_ms=shutdown_timeout_ms,
147
+ )
148
+ except ValidationError as error:
149
+ raise MuseValidationError("spawnOptions", error) from error
muse_code/errors.py ADDED
@@ -0,0 +1,199 @@
1
+ """Typed errors shared by the stores and the facade.
2
+
3
+ They live here rather than in ``facade/`` because the stores raise them too,
4
+ and a store importing from the facade would invert the layering. Typed
5
+ rather than bare ``Exception`` because each names a STATE an embedder must
6
+ branch on; the only alternative is matching message text, which silently
7
+ turns those strings into the package's contract.
8
+
9
+ Port of ``clients/sdk-ts/src/errors.ts`` (spec 638 INV-638-04 parity).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import Literal
15
+
16
+
17
+ class MuseSessionDiscardedError(Exception):
18
+ """The session was discarded after an ephemeral host's abnormal death.
19
+
20
+ Raised when the operation attempted would either resurrect the discarded
21
+ session or replay one of its ``commandId``\\ s against a new host
22
+ (tdd SS2.13.3b, SS3.1.3).
23
+ """
24
+
25
+
26
+ class MuseForeignSessionError(Exception):
27
+ """An event whose ``sessionId`` names a different session than the one folding it."""
28
+
29
+
30
+ GapFillFailureReason = Literal["noConnection", "pageFailed", "pageStalled"]
31
+ """Why one SS4.8 splice-fill did not complete.
32
+
33
+ Each arm names a different owner (this SDK's own composition, the transport,
34
+ the host), because the repair differs:
35
+
36
+ - ``noConnection``: the ``Session`` was built fold-only, so it has no seam
37
+ to page through. Composition, not a runtime fault.
38
+ - ``pageFailed``: a ``view/page`` round trip failed, or answered with a
39
+ frame this session may not fold. ``cause`` carries it.
40
+ - ``pageStalled``: the walk made no progress — a page that neither carried
41
+ events nor ended the view, or a ``nextCursor`` that did not advance. The
42
+ wire is an external boundary and this is the only bound on the walk; an
43
+ attempt cap would silently truncate a legitimate long fill.
44
+ """
45
+
46
+
47
+ class MuseGapFillError(Exception):
48
+ """A gap the client could not fill (spec 14990 FR-020, tdd SS4.8).
49
+
50
+ REPORTED to the session's gap-error callback, never raised across the
51
+ pump: the fill is driven from a notification, so an escaping exception
52
+ would land in the consumer's event pump with nothing to catch it. Typed
53
+ because the hole it names is a STATE a consumer must branch on — the
54
+ fold stays not-current until a later recovery, and ``(after, next)``
55
+ names exactly what is missing.
56
+
57
+ Attributes:
58
+ reason: Which owner failed (see ``GapFillFailureReason``).
59
+ after: The gap's lower bound, verbatim and opaque (tdd SS4.1).
60
+ next: The gap's upper bound, verbatim and opaque.
61
+ """
62
+
63
+ def __init__(
64
+ self,
65
+ reason: GapFillFailureReason,
66
+ after: str,
67
+ next: str, # noqa: A002 - the wire member's own name
68
+ cause: BaseException | None = None,
69
+ ) -> None:
70
+ """Builds the error; ``cause`` chains like ``raise ... from cause``.
71
+
72
+ Args:
73
+ reason: Which owner failed.
74
+ after: The gap's lower bound cursor.
75
+ next: The gap's upper bound cursor.
76
+ cause: The underlying failure for the ``pageFailed`` arm.
77
+ """
78
+ super().__init__(f"view/gap ({after}, {next}) could not be filled: {reason}")
79
+ self.reason: GapFillFailureReason = reason
80
+ self.after: str = after
81
+ self.next: str = next
82
+ if cause is not None:
83
+ self.__cause__ = cause
84
+
85
+
86
+ ValidationSeam = Literal["initializeResult", "spawnOptions"]
87
+ """Which C-638-2 pydantic seam refused its input.
88
+
89
+ The two seams the contract names (spec 638 C-638-2; owner ruling #31276,
90
+ arm (a)): the inbound ``InitializeResult`` frame parse and the spawn
91
+ options. Each arm names a different owner — ``initializeResult`` is the
92
+ host's frame (a protocol-level malformation), ``spawnOptions`` is the
93
+ caller's construction bug — so an embedder branches here to decide whether
94
+ to blame its own inputs or the discovered host.
95
+ """
96
+
97
+
98
+ class MuseValidationError(ValueError):
99
+ """A C-638-2 validating seam rejected its input through pydantic.
100
+
101
+ Raised instead of a bare ``isinstance`` failure, an ``AttributeError``/
102
+ ``KeyError``, or a silent partial object (spec 638 C-638-2; owner ruling
103
+ #31276, arm (a)). Wraps pydantic's ``ValidationError``, which rides as
104
+ ``__cause__`` and in the message. A ``ValueError`` by inheritance for
105
+ the same reason pydantic's own ``ValidationError`` is one: the spawn
106
+ boundary's pre-existing out-of-range refusal was a ``ValueError``, and
107
+ a consumer catching that must keep working.
108
+
109
+ Attributes:
110
+ seam: Which validating seam refused (see :data:`ValidationSeam`).
111
+ """
112
+
113
+ def __init__(self, seam: ValidationSeam, cause: Exception) -> None:
114
+ """Builds the typed wrapper; ``cause`` chains like ``raise ... from``.
115
+
116
+ Args:
117
+ seam: Which validating seam refused.
118
+ cause: The underlying ``pydantic.ValidationError``.
119
+ """
120
+ described = (
121
+ "initialize result frame"
122
+ if seam == "initializeResult"
123
+ else "spawn options"
124
+ )
125
+ super().__init__(f"malformed {described}: {cause}")
126
+ self.seam: ValidationSeam = seam
127
+ self.__cause__ = cause
128
+
129
+
130
+ COMPATIBILITY_PAGE_URL = "https://meta-models.github.io/muse-code-sdk/compatibility/"
131
+ """The developer-docs compatibility page the discovery errors cite.
132
+
133
+ The Python row there is the customer-facing statement of the supported
134
+ Python floor, the host version + schema fingerprint per wheel, and the exact
135
+ pydantic pin (spec 638 FR-638-028); FR-638-020's failures point at it.
136
+ """
137
+
138
+
139
+ class MuseHostDiscoveryError(Exception):
140
+ """No MSP host binary could be found (spec 638 FR-638-020, INV-638-08).
141
+
142
+ The wheel is UNBUNDLED (#29216): the SDK discovers an installed ``muse``
143
+ and never carries one. Raised BEFORE any process is spawned.
144
+ """
145
+
146
+
147
+ class MuseHostMismatchError(Exception):
148
+ """The discovered host serves a schema this SDK was not built against.
149
+
150
+ Spec 638 C-638-4 / FM-638-2: at ``initialize``, a fingerprint mismatch
151
+ FAILS with an exact message naming the served and required fingerprints,
152
+ the host's self-reported version, and the compatibility page whose
153
+ Python row states the compatible host version per wheel. There is no
154
+ bypass — the posture is owner-directed and deliberately stricter than
155
+ SS1.4.1's warning-only rule for the TS facade.
156
+
157
+ Attributes:
158
+ served: The fingerprint the host advertised.
159
+ pinned: The fingerprint this SDK requires.
160
+ host_version: The host's self-reported ``serverInfo.version``, when
161
+ the handshake carried one.
162
+ """
163
+
164
+ def __init__(
165
+ self,
166
+ served: str,
167
+ pinned: str,
168
+ host_version: str | None = None,
169
+ *,
170
+ required_host_version: str,
171
+ ) -> None:
172
+ """Builds the exact, page-citing failure.
173
+
174
+ Args:
175
+ served: ``InitializeResult.schema.fingerprint``.
176
+ pinned: ``muse_code.EXPECTED_SCHEMA_FINGERPRINT``.
177
+ host_version: ``serverInfo.version`` from the handshake result.
178
+ required_host_version: The generated
179
+ ``muse_code_msp.REQUIRED_HOST_VERSION`` — tree-derived by the
180
+ renderer from the host crate's manifest and kept honest by
181
+ the regen gate (spec 638 FR-638-006), never hand-typed.
182
+ Required, not defaulted: C-638-4 names the exact version, so
183
+ a version-less message is not a state this error can carry
184
+ (PR #30094 review).
185
+ """
186
+ version_note = (
187
+ f" (host version {host_version})" if host_version is not None else ""
188
+ )
189
+ super().__init__(
190
+ f"the muse host{version_note} serves schema fingerprint {served} "
191
+ f"but this muse-code-sdk requires {pinned}; "
192
+ f"install host version {required_host_version} — "
193
+ "see the compatibility page's Python "
194
+ f"row: {COMPATIBILITY_PAGE_URL}"
195
+ )
196
+ self.served: str = served
197
+ self.pinned: str = pinned
198
+ self.host_version: str | None = host_version
199
+ self.required_host_version: str = required_host_version
@@ -0,0 +1,89 @@
1
+ """The SS7.1 asyncio facade (spec 638 FR-638-019, plan slice S3).
2
+
3
+ The hand-written surface over the transport-less core and the connection
4
+ plane: :class:`~muse_code.facade.client.MuseClient` opens sessions,
5
+ :class:`~muse_code.facade.session.Session` folds a session's view and submits
6
+ this client's half of the exchange, and
7
+ :class:`~muse_code.facade.turn_handle.Turn` follows one turn's items and
8
+ deltas to its outcome. Port of ``clients/sdk-ts/src/facade`` (spec 638
9
+ INV-638-04 parity).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ # `ApprovalRouter` and `GapFiller` themselves stay OFF the barrel
15
+ # (Constitution XI, TS index.ts parity): they are `Session`'s internals with
16
+ # no consumer. Their handler/decision TYPES are exported because a consumer
17
+ # writes one.
18
+ from .approval import (
19
+ ApprovalDecisionInput,
20
+ ApprovalFailure,
21
+ ApprovalFailureHandler,
22
+ ApprovalHandler,
23
+ )
24
+ from .client import (
25
+ MuseClient,
26
+ MuseClientOptions,
27
+ MuseClientSpawnOptions,
28
+ ResumeSessionOptions,
29
+ StartSessionOptions,
30
+ )
31
+ from .discarded import DiscardedSessions
32
+ from .gap_fill import GapFillFailureHandler
33
+ from .host_death import (
34
+ AbnormalExit,
35
+ HostDeathDischarge,
36
+ HostDeathNotification,
37
+ MuseHostDiedError,
38
+ SessionDurabilityProfile,
39
+ TransportEof,
40
+ is_abnormal_host_death,
41
+ read_session_durability,
42
+ )
43
+ from .session import (
44
+ PendingCommandView,
45
+ Session,
46
+ SessionApplyOutcome,
47
+ SessionApplyRefusal,
48
+ SessionFoldView,
49
+ SessionGapBuffered,
50
+ SessionGapOverlap,
51
+ SessionOpening,
52
+ )
53
+ from .turn_handle import FoldedItem, Turn, TurnOutcome, is_launch_failure
54
+ from .turn_submit import SendUserTurnOptions
55
+
56
+ __all__ = [
57
+ "AbnormalExit",
58
+ "ApprovalDecisionInput",
59
+ "ApprovalFailure",
60
+ "ApprovalFailureHandler",
61
+ "ApprovalHandler",
62
+ "DiscardedSessions",
63
+ "FoldedItem",
64
+ "GapFillFailureHandler",
65
+ "HostDeathDischarge",
66
+ "HostDeathNotification",
67
+ "MuseClient",
68
+ "MuseClientOptions",
69
+ "MuseClientSpawnOptions",
70
+ "MuseHostDiedError",
71
+ "PendingCommandView",
72
+ "ResumeSessionOptions",
73
+ "SendUserTurnOptions",
74
+ "Session",
75
+ "SessionApplyOutcome",
76
+ "SessionApplyRefusal",
77
+ "SessionDurabilityProfile",
78
+ "SessionFoldView",
79
+ "SessionGapBuffered",
80
+ "SessionGapOverlap",
81
+ "SessionOpening",
82
+ "StartSessionOptions",
83
+ "TransportEof",
84
+ "Turn",
85
+ "TurnOutcome",
86
+ "is_abnormal_host_death",
87
+ "is_launch_failure",
88
+ "read_session_durability",
89
+ ]