muse-code-sdk 1.4.15__tar.gz → 1.4.20__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.4.15/src/muse_code_sdk.egg-info → muse_code_sdk-1.4.20}/PKG-INFO +54 -2
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/README.md +52 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/pyproject.toml +2 -2
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/src/muse_code/__init__.py +11 -1
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/src/muse_code/connection/connection.py +59 -9
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/src/muse_code/connection/spawn.py +2 -1
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/src/muse_code/errors.py +7 -5
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/src/muse_code/facade/__init__.py +10 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/src/muse_code/facade/client.py +241 -19
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/src/muse_code/facade/host_death.py +5 -1
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/src/muse_code/facade/session.py +109 -9
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/src/muse_code/sync_facade.py +69 -21
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20/src/muse_code_sdk.egg-info}/PKG-INFO +54 -2
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/src/muse_code_sdk.egg-info/SOURCES.txt +5 -0
- muse_code_sdk-1.4.20/src/muse_code_sdk.egg-info/requires.txt +2 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/tests/test_compatibility_row.py +5 -3
- muse_code_sdk-1.4.20/tests/test_connection_retryable_override.py +91 -0
- muse_code_sdk-1.4.20/tests/test_facade_client_observe.py +594 -0
- muse_code_sdk-1.4.20/tests/test_facade_host_exit_cause.py +245 -0
- muse_code_sdk-1.4.20/tests/test_facade_raw_command.py +320 -0
- muse_code_sdk-1.4.20/tests/test_facade_session_events.py +434 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/tests/test_facade_wire_queries.py +59 -5
- muse_code_sdk-1.4.20/tests/test_needs_input_wire_types.py +71 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/tests/test_sync_facade.py +26 -0
- muse_code_sdk-1.4.15/src/muse_code_sdk.egg-info/requires.txt +0 -2
- muse_code_sdk-1.4.15/tests/test_facade_raw_command.py +0 -162
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/LICENSE +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/setup.cfg +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/src/muse_code/connection/__init__.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/src/muse_code/connection/discovery.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/src/muse_code/connection/validation.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/src/muse_code/facade/approval.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/src/muse_code/facade/discarded.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/src/muse_code/facade/gap_fill.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/src/muse_code/facade/host_start.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/src/muse_code/facade/run.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/src/muse_code/facade/turn_handle.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/src/muse_code/facade/turn_submit.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/src/muse_code/fold/__init__.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/src/muse_code/fold/item_store.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/src/muse_code/fold/session_fold.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/src/muse_code/fold/state_store.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/src/muse_code/pending/__init__.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/src/muse_code/pending/pending_command_set.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/src/muse_code/py.typed +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/src/muse_code/replay.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/src/muse_code_sdk.egg-info/dependency_links.txt +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/src/muse_code_sdk.egg-info/top_level.txt +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/tests/test_asyncio_cancellation.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/tests/test_connection.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/tests/test_connection_error_arms.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/tests/test_docs_generation.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/tests/test_external_audience.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/tests/test_facade_approval.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/tests/test_facade_approval_resend.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/tests/test_facade_control_verbs.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/tests/test_facade_gap_fill.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/tests/test_facade_open_same_chunk.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/tests/test_facade_persistent_approval.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/tests/test_facade_required_selection.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/tests/test_facade_run.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/tests/test_facade_session_mcp.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/tests/test_facade_turn.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/tests/test_facade_unqueue_retirement.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/tests/test_facade_user_input_resend.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/tests/test_fingerprint_vectors.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/tests/test_host_discovery.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/tests/test_item_store.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/tests/test_manifests.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/tests/test_msp_py_fingerprint_derivation.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/tests/test_msp_py_host_version_derivation.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/tests/test_muse_serve_child.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/tests/test_pending_command_corpus.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/tests/test_pending_command_set.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/tests/test_pending_command_wire_binding.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/tests/test_permission_state_wire_types.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/tests/test_public_mirror_text.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/tests/test_publish_shape.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/tests/test_recovery_observation.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/tests/test_run_abort_busy_unqueue_real_binary.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/tests/test_session_fold.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/tests/test_source_closure.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/tests/test_spawn_handshake.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/tests/test_spawn_host_lost.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/tests/test_state_store.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/tests/test_tolerance_corpus.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/tests/test_transcript_replay.py +0 -0
- {muse_code_sdk-1.4.15 → muse_code_sdk-1.4.20}/tests/test_typing_gates.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: muse-code-sdk
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.20
|
|
4
4
|
Summary: Muse Code SDK for MSP: asyncio-first facade over the generated wire types (muse-code-msp), with a thin sync wrapper.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/meta-models/muse-code-sdk
|
|
@@ -15,7 +15,7 @@ Requires-Python: >=3.10
|
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
16
|
License-File: LICENSE
|
|
17
17
|
Requires-Dist: pydantic==2.13.5
|
|
18
|
-
Requires-Dist: muse-code-msp==1.4.
|
|
18
|
+
Requires-Dist: muse-code-msp==1.4.20
|
|
19
19
|
Dynamic: license-file
|
|
20
20
|
|
|
21
21
|
# `muse-code-sdk`
|
|
@@ -28,6 +28,8 @@ wrapper for scripts and notebooks.
|
|
|
28
28
|
one you already run.
|
|
29
29
|
- Stream a turn's items and text deltas as async iterators; await the turn's
|
|
30
30
|
server-authored outcome.
|
|
31
|
+
- Stream what each incoming event changed in a session with
|
|
32
|
+
`session.events()`, to update a view incrementally.
|
|
31
33
|
- Answer tool-approval requests with a handler you register, or pass it as
|
|
32
34
|
`ResumeSessionOptions(on_approval=...)`, with `on_approval_error=...` for
|
|
33
35
|
its failures, so approvals still pending at resume reach it.
|
|
@@ -37,6 +39,9 @@ wrapper for scripts and notebooks.
|
|
|
37
39
|
- A client-side fold keeps a live, typed view of the session — items, session
|
|
38
40
|
state, pending commands — with gap recovery after reconnects, and safe
|
|
39
41
|
same-command retry that can never double-submit.
|
|
42
|
+
- Observe the connection itself: every notification the host sends (including
|
|
43
|
+
ones that name no session, such as `usage/changed`), messages the SDK
|
|
44
|
+
rejects as malformed, and the connection closing.
|
|
40
45
|
- `SyncMuseClient` mirrors the same surface with blocking verbs.
|
|
41
46
|
|
|
42
47
|
Status: developer preview. Experimental protocol surfaces may change.
|
|
@@ -112,6 +117,29 @@ The session itself can `fork`, `compact`, `rename`, and `leave`, and switch its
|
|
|
112
117
|
model, reasoning effort, or approval mode (`set_model`, `set_reasoning_effort`,
|
|
113
118
|
`set_approval_mode`); `client.list_models()` lists the models it accepts.
|
|
114
119
|
|
|
120
|
+
To render incrementally, read `session.events()`: every event the session
|
|
121
|
+
folds from then on, with what it changed. Read the current state first, then
|
|
122
|
+
subscribe before the next `await`, and nothing is missed, including the events
|
|
123
|
+
that arrived before `start_session` or `resume_session` returned:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
import contextlib
|
|
127
|
+
|
|
128
|
+
render_all(session.fold) # the state so far
|
|
129
|
+
async with contextlib.aclosing(session.events()) as events:
|
|
130
|
+
async for event in events:
|
|
131
|
+
# `event.item` is the item as of this event; `session.fold` may be ahead.
|
|
132
|
+
render(event.frame["method"], event.outcome.fold, event.item)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Each event arrives once, in order, including events the SDK fetches to fill a
|
|
136
|
+
gap in delivery. `aclosing` stops listening when the loop ends or breaks (or
|
|
137
|
+
call `aclose()`). The stream ends when an ephemeral host dies and raises
|
|
138
|
+
`MuseHostDiedError` when a durable one does, as `turn.items()` does. Reading
|
|
139
|
+
it changes nothing else: `turn.unqueue()` still returns the queued input it
|
|
140
|
+
takes back, and that input is not repeated on an event. `SyncSession.events()`
|
|
141
|
+
is the blocking version: `for event in session.events(): ...`.
|
|
142
|
+
|
|
115
143
|
To run a session and the child agents it starts without approval prompts,
|
|
116
144
|
start it under the built-in `:unrestricted` permission profile. Current
|
|
117
145
|
releases admit every child of such a session without prompts; a child admitted
|
|
@@ -153,6 +181,30 @@ mode, but its child agents are capped by the session's permission profile.
|
|
|
153
181
|
Under a prompting profile (such as `:ask-me`, whose approval is `on_request`),
|
|
154
182
|
the children still ask.
|
|
155
183
|
|
|
184
|
+
## Observe the connection
|
|
185
|
+
|
|
186
|
+
The client routes each event to its session, and also lets you listen to the
|
|
187
|
+
raw feed. Each `on_*` call returns a function that removes that one listener:
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
unsubscribe = client.on_notification(
|
|
191
|
+
lambda notification: print(notification["method"], notification.get("params"))
|
|
192
|
+
)
|
|
193
|
+
client.on_notification_error(lambda error, notification: print("listener failed:", error))
|
|
194
|
+
client.on_protocol_error(lambda error: print("rejected frame:", error.line))
|
|
195
|
+
await client.closed # resolves when the connection closes, for any reason
|
|
196
|
+
unsubscribe()
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
A notification for a session you opened reaches your listener after the
|
|
200
|
+
session has applied it, except during a gap fill (the event is folded when
|
|
201
|
+
the fill completes) or while `start_session`/`resume_session` is still running
|
|
202
|
+
(it is folded before the call returns). Listeners run as frames are read, so keep them quick;
|
|
203
|
+
an `async def` listener runs as a task. An exception from a listener goes to
|
|
204
|
+
`on_notification_error` and never disconnects the client. `SyncMuseClient`
|
|
205
|
+
has the same three `on_*` methods (listeners run while a blocking call is in
|
|
206
|
+
progress) and `wait_closed()` in place of `closed`.
|
|
207
|
+
|
|
156
208
|
## Documentation
|
|
157
209
|
|
|
158
210
|
- Guides, cookbook recipes, and the protocol reference:
|
|
@@ -8,6 +8,8 @@ wrapper for scripts and notebooks.
|
|
|
8
8
|
one you already run.
|
|
9
9
|
- Stream a turn's items and text deltas as async iterators; await the turn's
|
|
10
10
|
server-authored outcome.
|
|
11
|
+
- Stream what each incoming event changed in a session with
|
|
12
|
+
`session.events()`, to update a view incrementally.
|
|
11
13
|
- Answer tool-approval requests with a handler you register, or pass it as
|
|
12
14
|
`ResumeSessionOptions(on_approval=...)`, with `on_approval_error=...` for
|
|
13
15
|
its failures, so approvals still pending at resume reach it.
|
|
@@ -17,6 +19,9 @@ wrapper for scripts and notebooks.
|
|
|
17
19
|
- A client-side fold keeps a live, typed view of the session — items, session
|
|
18
20
|
state, pending commands — with gap recovery after reconnects, and safe
|
|
19
21
|
same-command retry that can never double-submit.
|
|
22
|
+
- Observe the connection itself: every notification the host sends (including
|
|
23
|
+
ones that name no session, such as `usage/changed`), messages the SDK
|
|
24
|
+
rejects as malformed, and the connection closing.
|
|
20
25
|
- `SyncMuseClient` mirrors the same surface with blocking verbs.
|
|
21
26
|
|
|
22
27
|
Status: developer preview. Experimental protocol surfaces may change.
|
|
@@ -92,6 +97,29 @@ The session itself can `fork`, `compact`, `rename`, and `leave`, and switch its
|
|
|
92
97
|
model, reasoning effort, or approval mode (`set_model`, `set_reasoning_effort`,
|
|
93
98
|
`set_approval_mode`); `client.list_models()` lists the models it accepts.
|
|
94
99
|
|
|
100
|
+
To render incrementally, read `session.events()`: every event the session
|
|
101
|
+
folds from then on, with what it changed. Read the current state first, then
|
|
102
|
+
subscribe before the next `await`, and nothing is missed, including the events
|
|
103
|
+
that arrived before `start_session` or `resume_session` returned:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
import contextlib
|
|
107
|
+
|
|
108
|
+
render_all(session.fold) # the state so far
|
|
109
|
+
async with contextlib.aclosing(session.events()) as events:
|
|
110
|
+
async for event in events:
|
|
111
|
+
# `event.item` is the item as of this event; `session.fold` may be ahead.
|
|
112
|
+
render(event.frame["method"], event.outcome.fold, event.item)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Each event arrives once, in order, including events the SDK fetches to fill a
|
|
116
|
+
gap in delivery. `aclosing` stops listening when the loop ends or breaks (or
|
|
117
|
+
call `aclose()`). The stream ends when an ephemeral host dies and raises
|
|
118
|
+
`MuseHostDiedError` when a durable one does, as `turn.items()` does. Reading
|
|
119
|
+
it changes nothing else: `turn.unqueue()` still returns the queued input it
|
|
120
|
+
takes back, and that input is not repeated on an event. `SyncSession.events()`
|
|
121
|
+
is the blocking version: `for event in session.events(): ...`.
|
|
122
|
+
|
|
95
123
|
To run a session and the child agents it starts without approval prompts,
|
|
96
124
|
start it under the built-in `:unrestricted` permission profile. Current
|
|
97
125
|
releases admit every child of such a session without prompts; a child admitted
|
|
@@ -133,6 +161,30 @@ mode, but its child agents are capped by the session's permission profile.
|
|
|
133
161
|
Under a prompting profile (such as `:ask-me`, whose approval is `on_request`),
|
|
134
162
|
the children still ask.
|
|
135
163
|
|
|
164
|
+
## Observe the connection
|
|
165
|
+
|
|
166
|
+
The client routes each event to its session, and also lets you listen to the
|
|
167
|
+
raw feed. Each `on_*` call returns a function that removes that one listener:
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
unsubscribe = client.on_notification(
|
|
171
|
+
lambda notification: print(notification["method"], notification.get("params"))
|
|
172
|
+
)
|
|
173
|
+
client.on_notification_error(lambda error, notification: print("listener failed:", error))
|
|
174
|
+
client.on_protocol_error(lambda error: print("rejected frame:", error.line))
|
|
175
|
+
await client.closed # resolves when the connection closes, for any reason
|
|
176
|
+
unsubscribe()
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
A notification for a session you opened reaches your listener after the
|
|
180
|
+
session has applied it, except during a gap fill (the event is folded when
|
|
181
|
+
the fill completes) or while `start_session`/`resume_session` is still running
|
|
182
|
+
(it is folded before the call returns). Listeners run as frames are read, so keep them quick;
|
|
183
|
+
an `async def` listener runs as a task. An exception from a listener goes to
|
|
184
|
+
`on_notification_error` and never disconnects the client. `SyncMuseClient`
|
|
185
|
+
has the same three `on_*` methods (listeners run while a blocking call is in
|
|
186
|
+
progress) and `wait_closed()` in place of `closed`.
|
|
187
|
+
|
|
136
188
|
## Documentation
|
|
137
189
|
|
|
138
190
|
- Guides, cookbook recipes, and the protocol reference:
|
|
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
|
|
5
5
|
|
|
6
6
|
[project]
|
|
7
7
|
name = "muse-code-sdk"
|
|
8
|
-
version = "1.4.
|
|
8
|
+
version = "1.4.20"
|
|
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"
|
|
@@ -18,7 +18,7 @@ license-files = ["LICENSE"]
|
|
|
18
18
|
# `pip install muse-code-sdk` must import, and the release bump moves this
|
|
19
19
|
# package's version and its muse-code-msp pin together; pydantic bumps stay
|
|
20
20
|
# reviewed PRs (test_publish_shape pins the pairing).
|
|
21
|
-
dependencies = ["pydantic==2.13.5", "muse-code-msp==1.4.
|
|
21
|
+
dependencies = ["pydantic==2.13.5", "muse-code-msp==1.4.20"]
|
|
22
22
|
classifiers = [
|
|
23
23
|
# Alpha states the withheld promise: publication is permitted, but the
|
|
24
24
|
# stability promise and conformance claim stay withheld — experimental
|
|
@@ -58,15 +58,19 @@ __all__ = [
|
|
|
58
58
|
"MuseHostDiscoveryError",
|
|
59
59
|
"MuseHostStartError",
|
|
60
60
|
"MuseSessionDiscardedError",
|
|
61
|
+
"NotificationFailureHandler",
|
|
62
|
+
"NotificationObserver",
|
|
61
63
|
"PendingCommandSet",
|
|
62
64
|
"PendingCommandView",
|
|
63
65
|
"PermissionProfileNotApplied",
|
|
66
|
+
"ProtocolErrorObserver",
|
|
64
67
|
"ResumeSessionOptions",
|
|
65
68
|
"RunResult",
|
|
66
69
|
"SendUserTurnOptions",
|
|
67
70
|
"Session",
|
|
68
71
|
"SessionApplyOutcome",
|
|
69
72
|
"SessionDurabilityProfile",
|
|
73
|
+
"SessionEvent",
|
|
70
74
|
"SessionFold",
|
|
71
75
|
"SessionFoldView",
|
|
72
76
|
"SessionGapBuffered",
|
|
@@ -82,6 +86,7 @@ __all__ = [
|
|
|
82
86
|
"Turn",
|
|
83
87
|
"TurnOutcome",
|
|
84
88
|
"TurnUnqueueOutcome",
|
|
89
|
+
"ViewFrame",
|
|
85
90
|
"is_abnormal_host_death",
|
|
86
91
|
"is_launch_failure",
|
|
87
92
|
"load_transcript",
|
|
@@ -103,7 +108,7 @@ that lane instead of passing silently.
|
|
|
103
108
|
# The lockstep rule: the release train bumps pyproject.toml, the
|
|
104
109
|
# distribution-metadata authority; this constant is aligned at publication
|
|
105
110
|
# time.
|
|
106
|
-
__version__: Final[str] = "1.4.
|
|
111
|
+
__version__: Final[str] = "1.4.20"
|
|
107
112
|
|
|
108
113
|
# The facade is imported last so the constants above are already bound: the
|
|
109
114
|
# spawn path reads ``EXPECTED_SCHEMA_FINGERPRINT`` (lazily, at initialize
|
|
@@ -123,13 +128,17 @@ from .facade import ( # noqa: E402
|
|
|
123
128
|
MuseClientSpawnOptions,
|
|
124
129
|
MuseHostDiedError,
|
|
125
130
|
MuseHostStartError,
|
|
131
|
+
NotificationFailureHandler,
|
|
132
|
+
NotificationObserver,
|
|
126
133
|
PendingCommandView,
|
|
134
|
+
ProtocolErrorObserver,
|
|
127
135
|
ResumeSessionOptions,
|
|
128
136
|
RunResult,
|
|
129
137
|
SendUserTurnOptions,
|
|
130
138
|
Session,
|
|
131
139
|
SessionApplyOutcome,
|
|
132
140
|
SessionDurabilityProfile,
|
|
141
|
+
SessionEvent,
|
|
133
142
|
SessionFoldView,
|
|
134
143
|
SessionGapBuffered,
|
|
135
144
|
SessionGapOverlap,
|
|
@@ -139,6 +148,7 @@ from .facade import ( # noqa: E402
|
|
|
139
148
|
Turn,
|
|
140
149
|
TurnOutcome,
|
|
141
150
|
TurnUnqueueOutcome,
|
|
151
|
+
ViewFrame,
|
|
142
152
|
is_abnormal_host_death,
|
|
143
153
|
is_launch_failure,
|
|
144
154
|
read_session_durability,
|
|
@@ -101,6 +101,11 @@ class MspError(Exception):
|
|
|
101
101
|
class ProtocolError(Exception):
|
|
102
102
|
"""A local framing/correlation violation, never a server-authored error.
|
|
103
103
|
|
|
104
|
+
When a call fails because a host ``MuseClient.spawn`` started died, the
|
|
105
|
+
error it raises (usually this class; the transport's own error when a
|
|
106
|
+
write failed) has a ``MuseHostDiedError`` ``__cause__`` saying how the
|
|
107
|
+
host ended, so check ``__cause__`` alone.
|
|
108
|
+
|
|
104
109
|
Attributes:
|
|
105
110
|
line: The offending inbound line, preserved as evidence and bounded
|
|
106
111
|
in UTF-8 bytes, when one exists.
|
|
@@ -296,6 +301,10 @@ class Connection:
|
|
|
296
301
|
self._buffer_bytes = 0
|
|
297
302
|
self._dropping_oversized = False
|
|
298
303
|
self._finished = False
|
|
304
|
+
self._read_peer_end_cause: (
|
|
305
|
+
Callable[[], asyncio.Future[BaseException | None] | None] | None
|
|
306
|
+
) = None
|
|
307
|
+
self._end_cause: asyncio.Future[BaseException | None] | None = None
|
|
299
308
|
self._closed: asyncio.Future[None] = self._loop.create_future()
|
|
300
309
|
self._read_task = self._loop.create_task(self._read_loop())
|
|
301
310
|
|
|
@@ -324,6 +333,22 @@ class Connection:
|
|
|
324
333
|
"""
|
|
325
334
|
return self._submit_tail
|
|
326
335
|
|
|
336
|
+
def _peer_end_cause(
|
|
337
|
+
self, read: Callable[[], asyncio.Future[BaseException | None] | None]
|
|
338
|
+
) -> None:
|
|
339
|
+
"""Friend seam: lets the owner of the peer process say, once when the
|
|
340
|
+
connection finishes, why the peer is gone.
|
|
341
|
+
|
|
342
|
+
Module-private, like the TS twin's Symbol fence. A future ``read``
|
|
343
|
+
returns settles (never with an exception) with the ``__cause__`` given
|
|
344
|
+
to every call the connection then fails; ``None`` keeps the bare error
|
|
345
|
+
and its timing.
|
|
346
|
+
"""
|
|
347
|
+
self._read_peer_end_cause = read
|
|
348
|
+
# The host can die between the handshake and this registration.
|
|
349
|
+
if self._finished and self._end_cause is None:
|
|
350
|
+
self._end_cause = read()
|
|
351
|
+
|
|
327
352
|
def mint_command_id(self) -> str:
|
|
328
353
|
"""Takes one id from THIS connection's single command-id minter.
|
|
329
354
|
|
|
@@ -352,7 +377,7 @@ class Connection:
|
|
|
352
377
|
"""
|
|
353
378
|
outcome: asyncio.Future[Params] = self._loop.create_future()
|
|
354
379
|
if self._finished:
|
|
355
|
-
|
|
380
|
+
self._fail_with_end_cause(outcome, ProtocolError("connection is closed"))
|
|
356
381
|
return outcome
|
|
357
382
|
request_id = self._mint_request_id()
|
|
358
383
|
if not _valid_request_id(request_id):
|
|
@@ -376,8 +401,7 @@ class Connection:
|
|
|
376
401
|
error = done.exception()
|
|
377
402
|
if error is not None:
|
|
378
403
|
self._pending.pop(key, None)
|
|
379
|
-
|
|
380
|
-
outcome.set_exception(error)
|
|
404
|
+
self._fail_with_end_cause(outcome, error)
|
|
381
405
|
|
|
382
406
|
write_outcome.add_done_callback(on_write_done)
|
|
383
407
|
return outcome
|
|
@@ -440,7 +464,8 @@ class Connection:
|
|
|
440
464
|
"""Sends one logical command with the same-``commandId`` retry contract.
|
|
441
465
|
|
|
442
466
|
Same-id retries on the retryable pair (``backpressured`` /
|
|
443
|
-
``overloaded``) with the bounded full-jitter budget
|
|
467
|
+
``overloaded``) with the bounded full-jitter budget, unless the error
|
|
468
|
+
carries an explicit ``retryable: false``; a value-identical
|
|
444
469
|
ack settles a replay; ``inputTooLarge`` (and every other error) is
|
|
445
470
|
never auto-retried.
|
|
446
471
|
|
|
@@ -529,9 +554,12 @@ class Connection:
|
|
|
529
554
|
remembered.ack = dict(ack)
|
|
530
555
|
return ack
|
|
531
556
|
except MspError as error:
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
557
|
+
# A present data.retryable overrides the code's table default,
|
|
558
|
+
# so an explicit False stops the retry; absent keeps it.
|
|
559
|
+
retryable_nothing_admitted = error.retryable is not False and (
|
|
560
|
+
(error.code == -32001 and error.kind == "overloaded")
|
|
561
|
+
or (error.code == -32031 and error.kind == "backpressured")
|
|
562
|
+
)
|
|
535
563
|
if not retryable_nothing_admitted or attempt == attempts:
|
|
536
564
|
raise
|
|
537
565
|
await delay(attempt, error)
|
|
@@ -878,6 +906,27 @@ class Connection:
|
|
|
878
906
|
if error is not None:
|
|
879
907
|
self._finish(error)
|
|
880
908
|
|
|
909
|
+
def _fail_with_end_cause(
|
|
910
|
+
self, outcome: asyncio.Future[Any], error: BaseException
|
|
911
|
+
) -> None:
|
|
912
|
+
"""Fails ``outcome`` with ``error``, first giving it the peer's end
|
|
913
|
+
cause when the owner reported one."""
|
|
914
|
+
end_cause = self._end_cause
|
|
915
|
+
if end_cause is None or error.__cause__ is not None:
|
|
916
|
+
if not outcome.done():
|
|
917
|
+
outcome.set_exception(error)
|
|
918
|
+
return
|
|
919
|
+
|
|
920
|
+
def settle(done: asyncio.Future[BaseException | None]) -> None:
|
|
921
|
+
if not done.cancelled():
|
|
922
|
+
cause = done.result()
|
|
923
|
+
if cause is not None:
|
|
924
|
+
error.__cause__ = cause
|
|
925
|
+
if not outcome.done():
|
|
926
|
+
outcome.set_exception(error)
|
|
927
|
+
|
|
928
|
+
end_cause.add_done_callback(settle)
|
|
929
|
+
|
|
881
930
|
def _protocol_error(self, error: ProtocolError) -> None:
|
|
882
931
|
if self._protocol_error_handler is not None:
|
|
883
932
|
self._protocol_error_handler(error)
|
|
@@ -891,9 +940,10 @@ class Connection:
|
|
|
891
940
|
if isinstance(reason, BaseException)
|
|
892
941
|
else ProtocolError(str(reason))
|
|
893
942
|
)
|
|
943
|
+
if self._read_peer_end_cause is not None:
|
|
944
|
+
self._end_cause = self._read_peer_end_cause()
|
|
894
945
|
for pending in self._pending.values():
|
|
895
|
-
|
|
896
|
-
pending.future.set_exception(error)
|
|
946
|
+
self._fail_with_end_cause(pending.future, error)
|
|
897
947
|
self._pending.clear()
|
|
898
948
|
if not self._closed.done():
|
|
899
949
|
self._closed.set_result(None)
|
|
@@ -55,7 +55,8 @@ SIGTERM_GRACE_MS = 2_000
|
|
|
55
55
|
its whole drain window, so this covers only a signal handler's final flush.
|
|
56
56
|
Also bounds ``_classified``'s post-exit stderr-drain wait (py3.10 settles
|
|
57
57
|
``wait()`` at the reap, before pipe EOF), so tuning it down also shrinks the
|
|
58
|
-
window for capturing a host's final stderr evidence.
|
|
58
|
+
window for capturing a host's final stderr evidence. And it bounds how long a
|
|
59
|
+
call failed by a host death waits for the exit."""
|
|
59
60
|
OnStderr = Callable[[str], None]
|
|
60
61
|
|
|
61
62
|
|
|
@@ -117,9 +117,10 @@ class PermissionProfileNotApplied(Exception):
|
|
|
117
117
|
|
|
118
118
|
|
|
119
119
|
class MuseFacadeOwnedMethodError(Exception):
|
|
120
|
-
"""``MuseClient.command`` was asked to send a
|
|
121
|
-
facade owns. Sending it raw would bypass that
|
|
122
|
-
before anything is written; use the facade verb
|
|
120
|
+
"""``MuseClient.command`` or ``MuseClient.request`` was asked to send a
|
|
121
|
+
method whose state the facade owns. Sending it raw would bypass that
|
|
122
|
+
state, so it is refused before anything is written; use the facade verb
|
|
123
|
+
instead.
|
|
123
124
|
|
|
124
125
|
Attributes:
|
|
125
126
|
method: The refused wire method.
|
|
@@ -133,7 +134,8 @@ class MuseFacadeOwnedMethodError(Exception):
|
|
|
133
134
|
"""
|
|
134
135
|
super().__init__(
|
|
135
136
|
f"{method} is sent by the facade itself and cannot go through "
|
|
136
|
-
"MuseClient.command; use the facade verb that
|
|
137
|
+
"MuseClient.command or MuseClient.request; use the facade verb that "
|
|
138
|
+
"owns it"
|
|
137
139
|
)
|
|
138
140
|
self.method: str = method
|
|
139
141
|
|
|
@@ -205,7 +207,7 @@ class MuseValidationError(ValueError):
|
|
|
205
207
|
self.__cause__ = cause
|
|
206
208
|
|
|
207
209
|
|
|
208
|
-
COMPATIBILITY_PAGE_URL = "https://meta-models.github.io/muse-code-sdk/compatibility/"
|
|
210
|
+
COMPATIBILITY_PAGE_URL = "https://meta-models.github.io/muse-code-sdk/latest/compatibility/"
|
|
209
211
|
"""The developer-docs compatibility page the discovery errors cite.
|
|
210
212
|
|
|
211
213
|
The Python row there is the customer-facing statement of the supported
|
|
@@ -24,6 +24,9 @@ from .client import (
|
|
|
24
24
|
MuseClient,
|
|
25
25
|
MuseClientOptions,
|
|
26
26
|
MuseClientSpawnOptions,
|
|
27
|
+
NotificationFailureHandler,
|
|
28
|
+
NotificationObserver,
|
|
29
|
+
ProtocolErrorObserver,
|
|
27
30
|
ResumeSessionOptions,
|
|
28
31
|
StartSessionOptions,
|
|
29
32
|
)
|
|
@@ -45,10 +48,12 @@ from .session import (
|
|
|
45
48
|
Session,
|
|
46
49
|
SessionApplyOutcome,
|
|
47
50
|
SessionApplyRefusal,
|
|
51
|
+
SessionEvent,
|
|
48
52
|
SessionFoldView,
|
|
49
53
|
SessionGapBuffered,
|
|
50
54
|
SessionGapOverlap,
|
|
51
55
|
SessionOpening,
|
|
56
|
+
ViewFrame,
|
|
52
57
|
)
|
|
53
58
|
from .run import RunResult
|
|
54
59
|
from .turn_handle import FoldedItem, Turn, TurnOutcome, TurnUnqueueOutcome, is_launch_failure
|
|
@@ -70,7 +75,10 @@ __all__ = [
|
|
|
70
75
|
"MuseClientSpawnOptions",
|
|
71
76
|
"MuseHostDiedError",
|
|
72
77
|
"MuseHostStartError",
|
|
78
|
+
"NotificationFailureHandler",
|
|
79
|
+
"NotificationObserver",
|
|
73
80
|
"PendingCommandView",
|
|
81
|
+
"ProtocolErrorObserver",
|
|
74
82
|
"ResumeSessionOptions",
|
|
75
83
|
"RunResult",
|
|
76
84
|
"SendUserTurnOptions",
|
|
@@ -78,6 +86,7 @@ __all__ = [
|
|
|
78
86
|
"SessionApplyOutcome",
|
|
79
87
|
"SessionApplyRefusal",
|
|
80
88
|
"SessionDurabilityProfile",
|
|
89
|
+
"SessionEvent",
|
|
81
90
|
"SessionFoldView",
|
|
82
91
|
"SessionGapBuffered",
|
|
83
92
|
"SessionGapOverlap",
|
|
@@ -88,6 +97,7 @@ __all__ = [
|
|
|
88
97
|
"Turn",
|
|
89
98
|
"TurnOutcome",
|
|
90
99
|
"TurnUnqueueOutcome",
|
|
100
|
+
"ViewFrame",
|
|
91
101
|
"is_abnormal_host_death",
|
|
92
102
|
"is_launch_failure",
|
|
93
103
|
"read_session_durability",
|