muse-code-sdk 1.3.0__tar.gz → 1.3.1__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.
- muse_code_sdk-1.3.1/PKG-INFO +86 -0
- muse_code_sdk-1.3.1/README.md +67 -0
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/pyproject.toml +8 -8
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/__init__.py +16 -17
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/connection/__init__.py +2 -2
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/connection/connection.py +42 -48
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/connection/discovery.py +13 -13
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/connection/spawn.py +75 -86
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/connection/validation.py +17 -19
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/errors.py +18 -21
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/facade/__init__.py +2 -3
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/facade/approval.py +22 -23
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/facade/client.py +27 -32
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/facade/discarded.py +3 -4
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/facade/gap_fill.py +20 -22
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/facade/host_death.py +16 -17
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/facade/session.py +54 -58
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/facade/turn_handle.py +24 -26
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/facade/turn_submit.py +19 -22
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/fold/__init__.py +2 -3
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/fold/item_store.py +17 -19
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/fold/session_fold.py +45 -41
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/fold/state_store.py +8 -9
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/pending/__init__.py +1 -1
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/pending/pending_command_set.py +57 -62
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/replay.py +10 -10
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/sync_facade.py +31 -36
- muse_code_sdk-1.3.1/src/muse_code_sdk.egg-info/PKG-INFO +86 -0
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code_sdk.egg-info/SOURCES.txt +1 -0
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_asyncio_cancellation.py +5 -6
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_compatibility_row.py +47 -20
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_connection.py +25 -28
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_connection_error_arms.py +22 -23
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_docs_generation.py +9 -10
- muse_code_sdk-1.3.1/tests/test_external_audience.py +676 -0
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_facade_approval.py +20 -22
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_facade_gap_fill.py +24 -26
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_facade_persistent_approval.py +3 -3
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_facade_turn.py +27 -29
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_host_discovery.py +27 -28
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_item_store.py +1 -2
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_manifests.py +10 -11
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_muse_serve_child.py +47 -48
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_pending_command_corpus.py +3 -4
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_pending_command_set.py +15 -15
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_pending_command_wire_binding.py +5 -5
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_publish_shape.py +34 -28
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_session_fold.py +24 -26
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_source_closure.py +7 -7
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_spawn_handshake.py +9 -10
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_state_store.py +5 -6
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_sync_facade.py +28 -28
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_tolerance_corpus.py +5 -6
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_transcript_replay.py +14 -16
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/tests/test_typing_gates.py +7 -7
- muse_code_sdk-1.3.0/PKG-INFO +0 -47
- muse_code_sdk-1.3.0/README.md +0 -28
- muse_code_sdk-1.3.0/src/muse_code_sdk.egg-info/PKG-INFO +0 -47
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/LICENSE +0 -0
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/setup.cfg +0 -0
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code/py.typed +0 -0
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code_sdk.egg-info/dependency_links.txt +0 -0
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code_sdk.egg-info/requires.txt +0 -0
- {muse_code_sdk-1.3.0 → muse_code_sdk-1.3.1}/src/muse_code_sdk.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: muse-code-sdk
|
|
3
|
+
Version: 1.3.1
|
|
4
|
+
Summary: Muse Code SDK for MSP: asyncio-first facade over the generated wire types (muse-code-msp), with a thin sync wrapper.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/meta-models/muse-code-sdk
|
|
7
|
+
Project-URL: Repository, https://github.com/meta-models/muse-code-sdk
|
|
8
|
+
Project-URL: Issues, https://github.com/meta-models/muse-code-sdk/issues
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Typing :: Typed
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: pydantic==2.13.5
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# `muse-code-sdk`
|
|
21
|
+
|
|
22
|
+
The Python SDK for Muse Code: an asyncio-first client for driving a Muse
|
|
23
|
+
Code agent over the Muse Session Protocol (MSP), with a thin synchronous
|
|
24
|
+
wrapper for scripts and notebooks.
|
|
25
|
+
|
|
26
|
+
- Open and resume sessions against a spawned `muse serve` host, or attach to
|
|
27
|
+
one you already run.
|
|
28
|
+
- Stream a turn's items and text deltas as async iterators; await the turn's
|
|
29
|
+
server-authored outcome.
|
|
30
|
+
- Answer tool-approval requests with a handler you register.
|
|
31
|
+
- A client-side fold keeps a live, typed view of the session — items, session
|
|
32
|
+
state, pending commands — with gap recovery after reconnects, and safe
|
|
33
|
+
same-command retry that can never double-submit.
|
|
34
|
+
- `SyncMuseClient` mirrors the same surface with blocking verbs.
|
|
35
|
+
|
|
36
|
+
Status: developer preview. Experimental protocol surfaces may change.
|
|
37
|
+
|
|
38
|
+
The generated MSP wire types live in the companion package
|
|
39
|
+
[`muse-code-msp`](https://pypi.org/project/muse-code-msp/) and are never
|
|
40
|
+
restated here. One runtime dependency: `pydantic`, exact-pinned.
|
|
41
|
+
Python 3.10+. Fully typed (`py.typed`).
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
pip install muse-code-sdk muse-code-msp
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
You also need the Muse Code CLI: a `muse` binary on `PATH`, or an explicit
|
|
50
|
+
path passed at spawn time.
|
|
51
|
+
|
|
52
|
+
## Use
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
import asyncio
|
|
56
|
+
|
|
57
|
+
from muse_code import (
|
|
58
|
+
MuseClient, MuseClientSpawnOptions, SendUserTurnOptions, StartSessionOptions,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
async def main() -> None:
|
|
62
|
+
client = await MuseClient.spawn(MuseClientSpawnOptions(
|
|
63
|
+
muse_bin="muse",
|
|
64
|
+
args=["serve"],
|
|
65
|
+
client_info={"name": "my-app", "version": "1.0.0"},
|
|
66
|
+
))
|
|
67
|
+
session = await client.start_session(StartSessionOptions(workspace_root="."))
|
|
68
|
+
turn = await session.send_user_turn(SendUserTurnOptions(
|
|
69
|
+
input=[{"type": "text", "text": "Summarize this repository."}],
|
|
70
|
+
composer_input="Summarize this repository.",
|
|
71
|
+
))
|
|
72
|
+
async for item in turn.items():
|
|
73
|
+
print(item)
|
|
74
|
+
await turn.completed
|
|
75
|
+
await client.close()
|
|
76
|
+
|
|
77
|
+
asyncio.run(main())
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Documentation
|
|
81
|
+
|
|
82
|
+
- Guides, cookbook recipes, and the protocol reference:
|
|
83
|
+
<https://meta-models.github.io/muse-code-sdk/>
|
|
84
|
+
- Python API reference:
|
|
85
|
+
<https://meta-models.github.io/muse-code-sdk/next/generated/python/>
|
|
86
|
+
- Source and issues: <https://github.com/meta-models/muse-code-sdk>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# `muse-code-sdk`
|
|
2
|
+
|
|
3
|
+
The Python SDK for Muse Code: an asyncio-first client for driving a Muse
|
|
4
|
+
Code agent over the Muse Session Protocol (MSP), with a thin synchronous
|
|
5
|
+
wrapper for scripts and notebooks.
|
|
6
|
+
|
|
7
|
+
- Open and resume sessions against a spawned `muse serve` host, or attach to
|
|
8
|
+
one you already run.
|
|
9
|
+
- Stream a turn's items and text deltas as async iterators; await the turn's
|
|
10
|
+
server-authored outcome.
|
|
11
|
+
- Answer tool-approval requests with a handler you register.
|
|
12
|
+
- A client-side fold keeps a live, typed view of the session — items, session
|
|
13
|
+
state, pending commands — with gap recovery after reconnects, and safe
|
|
14
|
+
same-command retry that can never double-submit.
|
|
15
|
+
- `SyncMuseClient` mirrors the same surface with blocking verbs.
|
|
16
|
+
|
|
17
|
+
Status: developer preview. Experimental protocol surfaces may change.
|
|
18
|
+
|
|
19
|
+
The generated MSP wire types live in the companion package
|
|
20
|
+
[`muse-code-msp`](https://pypi.org/project/muse-code-msp/) and are never
|
|
21
|
+
restated here. One runtime dependency: `pydantic`, exact-pinned.
|
|
22
|
+
Python 3.10+. Fully typed (`py.typed`).
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
pip install muse-code-sdk muse-code-msp
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
You also need the Muse Code CLI: a `muse` binary on `PATH`, or an explicit
|
|
31
|
+
path passed at spawn time.
|
|
32
|
+
|
|
33
|
+
## Use
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import asyncio
|
|
37
|
+
|
|
38
|
+
from muse_code import (
|
|
39
|
+
MuseClient, MuseClientSpawnOptions, SendUserTurnOptions, StartSessionOptions,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
async def main() -> None:
|
|
43
|
+
client = await MuseClient.spawn(MuseClientSpawnOptions(
|
|
44
|
+
muse_bin="muse",
|
|
45
|
+
args=["serve"],
|
|
46
|
+
client_info={"name": "my-app", "version": "1.0.0"},
|
|
47
|
+
))
|
|
48
|
+
session = await client.start_session(StartSessionOptions(workspace_root="."))
|
|
49
|
+
turn = await session.send_user_turn(SendUserTurnOptions(
|
|
50
|
+
input=[{"type": "text", "text": "Summarize this repository."}],
|
|
51
|
+
composer_input="Summarize this repository.",
|
|
52
|
+
))
|
|
53
|
+
async for item in turn.items():
|
|
54
|
+
print(item)
|
|
55
|
+
await turn.completed
|
|
56
|
+
await client.close()
|
|
57
|
+
|
|
58
|
+
asyncio.run(main())
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Documentation
|
|
62
|
+
|
|
63
|
+
- Guides, cookbook recipes, and the protocol reference:
|
|
64
|
+
<https://meta-models.github.io/muse-code-sdk/>
|
|
65
|
+
- Python API reference:
|
|
66
|
+
<https://meta-models.github.io/muse-code-sdk/next/generated/python/>
|
|
67
|
+
- Source and issues: <https://github.com/meta-models/muse-code-sdk>
|
|
@@ -5,23 +5,23 @@ build-backend = "setuptools.build_meta"
|
|
|
5
5
|
|
|
6
6
|
[project]
|
|
7
7
|
name = "muse-code-sdk"
|
|
8
|
-
version = "1.3.
|
|
8
|
+
version = "1.3.1"
|
|
9
9
|
description = "Muse Code SDK for MSP: asyncio-first facade over the generated wire types (muse-code-msp), with a thin sync wrapper."
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
requires-python = ">=3.10"
|
|
12
12
|
license = "MIT"
|
|
13
13
|
license-files = ["LICENSE"]
|
|
14
|
-
# Exactly ONE runtime dependency, exact-pinned:
|
|
15
|
-
#
|
|
16
|
-
# bumps are deliberate reviewed PRs
|
|
14
|
+
# Exactly ONE runtime dependency, exact-pinned: a deliberate, ruled
|
|
15
|
+
# supply-chain exception. Any second dependency is an owner escalation;
|
|
16
|
+
# bumps are deliberate reviewed PRs.
|
|
17
17
|
# muse-code-msp is deliberately NOT declared: it is not on the registry yet,
|
|
18
18
|
# and promoting it is the owner escalation decided with the first publish
|
|
19
19
|
# (the @muse-code/msp devDependency precedent; test_publish_shape pins this).
|
|
20
20
|
dependencies = ["pydantic==2.13.5"]
|
|
21
21
|
classifiers = [
|
|
22
|
-
# Alpha states the withheld promise:
|
|
23
|
-
#
|
|
24
|
-
#
|
|
22
|
+
# Alpha states the withheld promise: publication is permitted, but the
|
|
23
|
+
# stability promise and conformance claim stay withheld — experimental
|
|
24
|
+
# surfaces may change.
|
|
25
25
|
"Development Status :: 3 - Alpha",
|
|
26
26
|
"Intended Audience :: Developers",
|
|
27
27
|
"Operating System :: OS Independent",
|
|
@@ -30,7 +30,7 @@ classifiers = [
|
|
|
30
30
|
]
|
|
31
31
|
|
|
32
32
|
[project.urls]
|
|
33
|
-
# The mirror is the one public venue
|
|
33
|
+
# The mirror is the one public venue; a published manifest never
|
|
34
34
|
# names the private producer repository.
|
|
35
35
|
Homepage = "https://github.com/meta-models/muse-code-sdk"
|
|
36
36
|
Repository = "https://github.com/meta-models/muse-code-sdk"
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
"""Muse Code SDK for MSP: the asyncio-first Python facade.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
generated ``muse_code_msp`` package, never restated here (INV-638-01/02).
|
|
3
|
+
Wire shapes are imported from the generated ``muse_code_msp`` package,
|
|
4
|
+
never restated here.
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
The transport-less core: the client fold (``muse_code.fold``), the pending
|
|
7
|
+
command set (``muse_code.pending``), the transcript replay runner
|
|
9
8
|
(``muse_code.replay``), and the typed error family (``muse_code.errors``).
|
|
10
|
-
|
|
11
|
-
discovery, and the strict fingerprint gate
|
|
12
|
-
|
|
13
|
-
``Turn``) with the
|
|
14
|
-
the
|
|
15
|
-
(``muse_code.sync_facade`` —
|
|
9
|
+
The connection plane: the NDJSON connection machine, the owned-process spawn
|
|
10
|
+
path, host discovery, and the strict fingerprint gate
|
|
11
|
+
(``muse_code.connection``). The surface: the asyncio facade
|
|
12
|
+
(``muse_code.facade`` — ``MuseClient``, ``Session``, ``Turn``) with the
|
|
13
|
+
host-death discharge, the approval/user-input round trip, the gap
|
|
14
|
+
splice-fill, and the sync loop-runner wrapper (``muse_code.sync_facade`` —
|
|
15
|
+
``SyncMuseClient``).
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
18
|
from __future__ import annotations
|
|
@@ -77,20 +77,19 @@ __all__ = [
|
|
|
77
77
|
]
|
|
78
78
|
|
|
79
79
|
EXPECTED_SCHEMA_FINGERPRINT: Final[str] = (
|
|
80
|
-
"sha256:
|
|
80
|
+
"sha256:b1e6676d624e116e2c1b150fec3192200d2cbca8ed79898e44f8921759c7872f"
|
|
81
81
|
)
|
|
82
82
|
"""The stable schema-bundle fingerprint this SDK was built against.
|
|
83
83
|
|
|
84
|
-
The one deliberately duplicated fact
|
|
85
|
-
pattern): ``clients/sdk-py/tests/test_manifests.py`` binds it to
|
|
84
|
+
The one deliberately duplicated fact: ``clients/sdk-py/tests/test_manifests.py`` binds it to
|
|
86
85
|
``schema/msp/stable/manifest.json``, so a schema advance that forgets the
|
|
87
86
|
Python SDK reds this lane instead of drifting silently.
|
|
88
87
|
"""
|
|
89
88
|
|
|
90
|
-
#
|
|
89
|
+
# The lockstep rule: the release train bumps pyproject.toml, the
|
|
91
90
|
# distribution-metadata authority; this constant is aligned at publication
|
|
92
|
-
# time
|
|
93
|
-
__version__: Final[str] = "1.3.
|
|
91
|
+
# time.
|
|
92
|
+
__version__: Final[str] = "1.3.1"
|
|
94
93
|
|
|
95
94
|
# The facade is imported last so the constants above are already bound: the
|
|
96
95
|
# spawn path reads ``EXPECTED_SCHEMA_FINGERPRINT`` (lazily, at initialize
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
"""The MSP connection plane
|
|
2
|
-
|
|
1
|
+
"""The MSP connection plane: the transport-less NDJSON connection machine,
|
|
2
|
+
the owned-process spawn path, and host discovery.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
"""Duplex-agnostic MSP NDJSON connection
|
|
1
|
+
"""Duplex-agnostic MSP NDJSON connection.
|
|
2
2
|
|
|
3
|
-
Faithful asyncio port of
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
and stays O(bytes) per chunk.
|
|
3
|
+
Faithful asyncio port of the TypeScript SDK's connection module. The
|
|
4
|
+
transport supplies decoded UTF-8 text chunks; chunks may be empty and may
|
|
5
|
+
split anywhere — Python strings are whole code points, so the TS
|
|
6
|
+
surrogate-pair byte reconciliation has no twin here: the running byte count
|
|
7
|
+
is exact by construction and stays O(bytes) per chunk.
|
|
9
8
|
|
|
10
9
|
One deliberate adaptation, noted per the port rules: in TS "invoking
|
|
11
10
|
``write()`` IS submission" because the transport's synchronous prefix runs
|
|
@@ -74,7 +73,7 @@ RetryDelay = Callable[[int, "MspError"], Awaitable[None]]
|
|
|
74
73
|
|
|
75
74
|
|
|
76
75
|
class MspError(Exception):
|
|
77
|
-
"""The one typed error family consumers branch on
|
|
76
|
+
"""The one typed error family consumers branch on.
|
|
78
77
|
|
|
79
78
|
Attributes:
|
|
80
79
|
code: The JSON-RPC error code.
|
|
@@ -104,7 +103,7 @@ class ProtocolError(Exception):
|
|
|
104
103
|
|
|
105
104
|
Attributes:
|
|
106
105
|
line: The offending inbound line, preserved as evidence and bounded
|
|
107
|
-
in UTF-8 bytes
|
|
106
|
+
in UTF-8 bytes, when one exists.
|
|
108
107
|
"""
|
|
109
108
|
|
|
110
109
|
def __init__(self, message: str, line: str | None = None) -> None:
|
|
@@ -122,7 +121,7 @@ class _UnencodableFrameError(ProtocolError):
|
|
|
122
121
|
|
|
123
122
|
A caller defect, never transport death: it rejects only that frame's
|
|
124
123
|
outcome, so ``notify()``'s finish-on-write-failure rule must not treat
|
|
125
|
-
it as the pipe dying
|
|
124
|
+
it as the pipe dying.
|
|
126
125
|
"""
|
|
127
126
|
|
|
128
127
|
|
|
@@ -147,12 +146,11 @@ def _utf8_len(text: str) -> int:
|
|
|
147
146
|
|
|
148
147
|
|
|
149
148
|
def _truncate_to_utf8_bytes(text: str, limit_bytes: int) -> str:
|
|
150
|
-
"""
|
|
149
|
+
"""The evidence bound: error evidence is measured in UTF-8 BYTES, never characters.
|
|
151
150
|
|
|
152
151
|
Encode once and back the cut up over continuation bytes (the
|
|
153
152
|
``StderrTail._trim_bytes`` pattern): a per-scalar walk re-encoded every
|
|
154
|
-
character and blocked the event loop for seconds at the frame limit
|
|
155
|
-
(PR #30094 review).
|
|
153
|
+
character and blocked the event loop for seconds at the frame limit.
|
|
156
154
|
"""
|
|
157
155
|
data = text.encode("utf-8", "surrogatepass")
|
|
158
156
|
if len(data) <= limit_bytes:
|
|
@@ -179,7 +177,7 @@ def _canonical(value: Any) -> str:
|
|
|
179
177
|
|
|
180
178
|
|
|
181
179
|
def create_uuid_v7_mint() -> Callable[[], str]:
|
|
182
|
-
"""Creates the SDK-owned UUIDv7 command-id source
|
|
180
|
+
"""Creates the SDK-owned UUIDv7 command-id source.
|
|
183
181
|
|
|
184
182
|
Returns:
|
|
185
183
|
A zero-argument callable minting RFC 9562 v7-shaped ids, strictly
|
|
@@ -216,7 +214,7 @@ def create_uuid_v7_mint() -> Callable[[], str]:
|
|
|
216
214
|
|
|
217
215
|
|
|
218
216
|
async def _default_retry_delay(attempt: int, _error: MspError) -> None:
|
|
219
|
-
"""
|
|
217
|
+
"""The retry backoff's full-jitter budget: uniform in 0..ceiling, 50 ms doubling, 2 s cap."""
|
|
220
218
|
ceiling_ms = min(2_000, 50 * 2 ** max(0, attempt - 1))
|
|
221
219
|
await asyncio.sleep(secrets.randbelow(ceiling_ms + 1) / 1000)
|
|
222
220
|
|
|
@@ -272,9 +270,8 @@ class Connection:
|
|
|
272
270
|
|
|
273
271
|
Args:
|
|
274
272
|
transport: The duplex text channel.
|
|
275
|
-
frame_limit_bytes: Inbound frame-size sanity bound
|
|
276
|
-
|
|
277
|
-
mint_command_id: Injectable command-id source (INV-013);
|
|
273
|
+
frame_limit_bytes: Inbound frame-size sanity bound.
|
|
274
|
+
mint_command_id: Injectable command-id source;
|
|
278
275
|
defaults to the UUIDv7 mint.
|
|
279
276
|
mint_request_id: Injectable for deterministic transcript
|
|
280
277
|
clients; values must be unique in flight.
|
|
@@ -313,15 +310,14 @@ class Connection:
|
|
|
313
310
|
|
|
314
311
|
A fresh shield per access: a consumer's ``wait_for`` timeout must
|
|
315
312
|
cancel only its own wait, never the connection's one real future —
|
|
316
|
-
a cancelled ``_closed`` would break ``close()`` for everyone
|
|
317
|
-
(PR #30094 review; a TS Promise cannot be cancelled).
|
|
313
|
+
a cancelled ``_closed`` would break ``close()`` for everyone.
|
|
318
314
|
"""
|
|
319
315
|
return asyncio.shield(self._closed)
|
|
320
316
|
|
|
321
317
|
def _submission_tail(self) -> asyncio.Future[None]:
|
|
322
318
|
"""The friend seam a process-owning transport adopts as ``flushed``.
|
|
323
319
|
|
|
324
|
-
Module-private, like the TS twin's Symbol fence
|
|
320
|
+
Module-private, like the TS twin's Symbol fence:
|
|
325
321
|
it lets a close routed around this class — the public
|
|
326
322
|
``child.close()`` — wait for accepted frames' SUBMISSION the same
|
|
327
323
|
way :meth:`close` does.
|
|
@@ -332,8 +328,8 @@ class Connection:
|
|
|
332
328
|
"""Takes one id from THIS connection's single command-id minter.
|
|
333
329
|
|
|
334
330
|
Exposed for callers that need the id BEFORE the ack: the facade's
|
|
335
|
-
``send_user_turn`` records its optimistic
|
|
336
|
-
second facade-side minter is exactly what
|
|
331
|
+
``send_user_turn`` records its optimistic pending entry under it. A
|
|
332
|
+
second facade-side minter is exactly what the single-minter rule forbids. Pair with
|
|
337
333
|
``command(..., command_id=...)``, which reuses the id.
|
|
338
334
|
|
|
339
335
|
Returns:
|
|
@@ -344,7 +340,7 @@ class Connection:
|
|
|
344
340
|
def request(
|
|
345
341
|
self, method: str, params: Params | None = None
|
|
346
342
|
) -> asyncio.Future[Params]:
|
|
347
|
-
"""Sends one request and correlates its response
|
|
343
|
+
"""Sends one request and correlates its response.
|
|
348
344
|
|
|
349
345
|
Args:
|
|
350
346
|
method: The MSP method name.
|
|
@@ -352,7 +348,7 @@ class Connection:
|
|
|
352
348
|
|
|
353
349
|
Returns:
|
|
354
350
|
A future resolving with the result object, or rejecting with an
|
|
355
|
-
:class:`MspError`
|
|
351
|
+
:class:`MspError` or :class:`ProtocolError`.
|
|
356
352
|
"""
|
|
357
353
|
outcome: asyncio.Future[Params] = self._loop.create_future()
|
|
358
354
|
if self._finished:
|
|
@@ -393,7 +389,7 @@ class Connection:
|
|
|
393
389
|
value, a lone surrogate) is the caller's defect, detected before
|
|
394
390
|
anything reaches the transport — it raises :class:`ProtocolError`
|
|
395
391
|
synchronously instead of being dropped or finishing a healthy
|
|
396
|
-
connection
|
|
392
|
+
connection.
|
|
397
393
|
|
|
398
394
|
Raises:
|
|
399
395
|
ProtocolError: The frame is not JSON/UTF-8 encodable.
|
|
@@ -441,9 +437,9 @@ class Connection:
|
|
|
441
437
|
max_attempts: int | None = None,
|
|
442
438
|
retry_delay: RetryDelay | None = None,
|
|
443
439
|
) -> asyncio.Future[Params]:
|
|
444
|
-
"""Sends one logical command with the
|
|
440
|
+
"""Sends one logical command with the same-``commandId`` retry contract.
|
|
445
441
|
|
|
446
|
-
Same-id retries on the
|
|
442
|
+
Same-id retries on the retryable pair (``backpressured`` /
|
|
447
443
|
``overloaded``) with the bounded full-jitter budget; a value-identical
|
|
448
444
|
ack settles a replay; ``inputTooLarge`` (and every other error) is
|
|
449
445
|
never auto-retried.
|
|
@@ -460,13 +456,12 @@ class Connection:
|
|
|
460
456
|
A future for the ack object (eager: the first attempt is in
|
|
461
457
|
flight when this returns) — or one already rejected, with NO
|
|
462
458
|
attempt in flight, when the params are not JSON-serializable
|
|
463
|
-
|
|
464
|
-
posture as :meth:`request`) or the ``commandId`` is reused with a
|
|
459
|
+
or the ``commandId`` is reused with a
|
|
465
460
|
different payload.
|
|
466
461
|
"""
|
|
467
462
|
# The prelude and the FIRST attempt run synchronously so the frame
|
|
468
463
|
# joins the submit tail NOW: a command() immediately followed by
|
|
469
|
-
# close() must still write it (
|
|
464
|
+
# close() must still write it (a prior review — a deferred task
|
|
470
465
|
# enqueued nothing until the loop's next yield, silently dropping
|
|
471
466
|
# the frame; request()/notify() already enqueue synchronously).
|
|
472
467
|
cid = command_id if command_id is not None else self._mint_command_id()
|
|
@@ -475,11 +470,11 @@ class Connection:
|
|
|
475
470
|
signature = _canonical({"method": method, "params": command_params})
|
|
476
471
|
except (TypeError, ValueError):
|
|
477
472
|
# A param json.dumps cannot serialize is the caller's defect
|
|
478
|
-
|
|
473
|
+
#, surfaced on the future this method promises — never
|
|
479
474
|
# a raw synchronous TypeError. request() with the same params
|
|
480
475
|
# rejects through _enqueue_write; the two surfaces must agree.
|
|
481
476
|
# Nothing is stranded: no _commands entry, no request in flight
|
|
482
|
-
|
|
477
|
+
#.
|
|
483
478
|
rejected: asyncio.Future[Params] = self._loop.create_future()
|
|
484
479
|
rejected.set_exception(
|
|
485
480
|
_UnencodableFrameError("outbound frame is not JSON/UTF-8 encodable")
|
|
@@ -519,7 +514,7 @@ class Connection:
|
|
|
519
514
|
ack = await pending
|
|
520
515
|
# session/start and session/resume results omit the redundant
|
|
521
516
|
# commandId echo (contracts/sdk-surface.md); one that carries
|
|
522
|
-
# it must echo exactly
|
|
517
|
+
# it must echo exactly.
|
|
523
518
|
if "commandId" in ack and ack["commandId"] != cid:
|
|
524
519
|
raise ProtocolError(
|
|
525
520
|
f"{method} ack commandId {ack['commandId']} did not echo {cid}"
|
|
@@ -547,7 +542,7 @@ class Connection:
|
|
|
547
542
|
"""Closes the connection without gating teardown on the peer.
|
|
548
543
|
|
|
549
544
|
COMPLETION must not gate teardown — a peer that stopped reading
|
|
550
|
-
never completes a write
|
|
545
|
+
never completes a write. SUBMISSION must: the submission
|
|
551
546
|
tail goes TO the transport's optional ``close``, which waits for it
|
|
552
547
|
inside its own shutdown budget; this class knows nothing about
|
|
553
548
|
processes, signals, or timeouts. A close-less transport supplies no
|
|
@@ -575,7 +570,7 @@ class Connection:
|
|
|
575
570
|
# allow_nan=False so a NaN/Infinity float raises ValueError here
|
|
576
571
|
# rather than emitting a bare `NaN`/`Infinity` token — invalid
|
|
577
572
|
# JSON the Rust host cannot parse, which would leave the caller's
|
|
578
|
-
# request hanging until EOF (
|
|
573
|
+
# request hanging until EOF (the governing rule; the TS twin coerces to
|
|
579
574
|
# null). It joins the caller-defect settle below.
|
|
580
575
|
line = json.dumps(
|
|
581
576
|
frame, separators=(",", ":"), ensure_ascii=False, allow_nan=False
|
|
@@ -588,7 +583,7 @@ class Connection:
|
|
|
588
583
|
# reject just this frame and leave the connection (and every
|
|
589
584
|
# sibling in flight) alive. Detected here, the one outbound
|
|
590
585
|
# boundary, so all three writers — request(), notify(), and the
|
|
591
|
-
# server-request reply — get it (
|
|
586
|
+
# server-request reply — get it (a prior review; the TS twin
|
|
592
587
|
# survives the same input).
|
|
593
588
|
outcome.set_exception(
|
|
594
589
|
_UnencodableFrameError("outbound frame is not JSON/UTF-8 encodable")
|
|
@@ -655,7 +650,7 @@ class Connection:
|
|
|
655
650
|
return
|
|
656
651
|
self._segments.append(remaining)
|
|
657
652
|
self._buffer_bytes += _utf8_len(remaining)
|
|
658
|
-
#
|
|
653
|
+
# The rule: the retained buffer is newline-free on entry (the drain
|
|
659
654
|
# below runs to exhaustion), so only the fresh chunk can carry a
|
|
660
655
|
# newline — ingest stays O(bytes), never a full-buffer rescan.
|
|
661
656
|
if "\n" in remaining:
|
|
@@ -722,7 +717,7 @@ class Connection:
|
|
|
722
717
|
# A Ctrl-C that cancels the run cancels this handler task;
|
|
723
718
|
# `Future.exception()` would then raise CancelledError back
|
|
724
719
|
# into the callback and log a spurious traceback in the
|
|
725
|
-
# consumer's shutdown
|
|
720
|
+
# consumer's shutdown.
|
|
726
721
|
if done.cancelled():
|
|
727
722
|
return
|
|
728
723
|
error = done.exception()
|
|
@@ -749,9 +744,9 @@ class Connection:
|
|
|
749
744
|
if pending.future.done():
|
|
750
745
|
# The caller cancelled its await (or an asyncio.wait_for timed
|
|
751
746
|
# out): the request is abandoned, not unknown — the late reply is
|
|
752
|
-
# dropped and the CONNECTION stays healthy (spec
|
|
747
|
+
# dropped and the CONNECTION stays healthy (the owning spec Edge Cases:
|
|
753
748
|
# cancellation is a consumer-side act and must not corrupt the
|
|
754
|
-
# connection;
|
|
749
|
+
# connection; a prior review — settling a done future
|
|
755
750
|
# raised InvalidStateError inside the read loop and tore
|
|
756
751
|
# everything down).
|
|
757
752
|
del self._pending[_request_key(frame_id)]
|
|
@@ -759,8 +754,8 @@ class Connection:
|
|
|
759
754
|
has_result = "result" in frame
|
|
760
755
|
has_error = "error" in frame
|
|
761
756
|
if has_result == has_error:
|
|
762
|
-
#
|
|
763
|
-
# timeout
|
|
757
|
+
# The rule: the MATCHED caller must settle — with no local
|
|
758
|
+
# timeout an unrejected caller would hang until EOF.
|
|
764
759
|
violation = ProtocolError(
|
|
765
760
|
"response must carry exactly one of result or error", line
|
|
766
761
|
)
|
|
@@ -809,7 +804,7 @@ class Connection:
|
|
|
809
804
|
}
|
|
810
805
|
)
|
|
811
806
|
result = await self._server_request_handler(request)
|
|
812
|
-
#
|
|
807
|
+
# The rule: a failed reply write finishes the connection; it must
|
|
813
808
|
# not escape this fire-and-forget task as an unhandled exception.
|
|
814
809
|
reply = self._enqueue_write(
|
|
815
810
|
{"jsonrpc": "2.0", "id": request_id, "result": result}
|
|
@@ -845,10 +840,9 @@ class Connection:
|
|
|
845
840
|
)
|
|
846
841
|
reply.add_done_callback(
|
|
847
842
|
lambda done: self._reply_done(request_id, done)
|
|
848
|
-
)
|
|
849
|
-
|
|
843
|
+
)
|
|
850
844
|
def _reply_done(self, request_id: object, done: asyncio.Future[None]) -> None:
|
|
851
|
-
"""
|
|
845
|
+
"""The reply-write rule, minus the caller-defect arm.
|
|
852
846
|
|
|
853
847
|
A reply frame that cannot serialize/encode (a handler returned or
|
|
854
848
|
raised a non-JSON value — a set, a lone surrogate) is the HANDLER's
|
|
@@ -857,7 +851,7 @@ class Connection:
|
|
|
857
851
|
connection (the third writer after request()/notify()). The fallback
|
|
858
852
|
frame cannot re-enter this arm because ``_line`` admits a server
|
|
859
853
|
request only with a positive-INTEGER id, so ``request_id`` is always
|
|
860
|
-
an int and the fallback is ASCII by construction
|
|
854
|
+
an int and the fallback is ASCII by construction.
|
|
861
855
|
"""
|
|
862
856
|
error = done.exception()
|
|
863
857
|
if error is None:
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
"""Host discovery for the UNBUNDLED wheel
|
|
2
|
-
|
|
3
|
-
The wheel carries no host binary and no code path assumes one
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
"""Host discovery for the UNBUNDLED wheel.
|
|
2
|
+
|
|
3
|
+
The wheel carries no host binary and no code path assumes one. Exactly the
|
|
4
|
+
two ruled tiers, first hit wins: an explicit ``muse_bin`` argument, then a
|
|
5
|
+
``muse`` on ``PATH``. The LIBRARY reads no environment variable (TS parity:
|
|
6
|
+
the TS spawn module takes a required command; ``MUSE_BIN`` is a knob of the
|
|
7
|
+
quickstart/cookbook HARNESSES, which read it themselves and pass the
|
|
8
|
+
explicit parameter down). No discoverable host fails BEFORE any process is
|
|
9
|
+
spawned, with an exact error naming the parameter and the compatibility
|
|
10
|
+
page.
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
from __future__ import annotations
|
|
@@ -21,13 +21,13 @@ HOST_BINARY_NAME = "muse"
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
def discover_muse_bin(muse_bin: str | None = None) -> str:
|
|
24
|
-
"""Resolves the MSP host binary to spawn
|
|
24
|
+
"""Resolves the MSP host binary to spawn.
|
|
25
25
|
|
|
26
|
-
Exactly two ruled inputs
|
|
26
|
+
Exactly two ruled inputs: an explicit
|
|
27
27
|
``muse_bin``, else the process ``PATH``. There is deliberately no ``env``
|
|
28
28
|
knob — no production caller passes one, the TS twin has none, and a
|
|
29
29
|
harness wanting hermetic discovery exports ``PATH`` itself; a future S3
|
|
30
|
-
consumer that needs the child's own env adds it there
|
|
30
|
+
consumer that needs the child's own env adds it there.
|
|
31
31
|
|
|
32
32
|
Args:
|
|
33
33
|
muse_bin: An explicit host path; when given it wins outright and is
|