muse-code-sdk 1.4.13__tar.gz → 1.4.15__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.
Files changed (82) hide show
  1. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/PKG-INFO +41 -13
  2. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/README.md +39 -11
  3. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/pyproject.toml +2 -2
  4. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/src/muse_code/__init__.py +21 -7
  5. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/src/muse_code/connection/discovery.py +5 -4
  6. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/src/muse_code/errors.py +50 -3
  7. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/src/muse_code/facade/__init__.py +6 -2
  8. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/src/muse_code/facade/approval.py +5 -0
  9. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/src/muse_code/facade/client.py +232 -24
  10. muse_code_sdk-1.4.15/src/muse_code/facade/run.py +208 -0
  11. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/src/muse_code/facade/session.py +473 -8
  12. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/src/muse_code/facade/turn_handle.py +126 -10
  13. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/src/muse_code/facade/turn_submit.py +129 -28
  14. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/src/muse_code/fold/session_fold.py +76 -5
  15. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/src/muse_code/sync_facade.py +255 -6
  16. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/src/muse_code_sdk.egg-info/PKG-INFO +41 -13
  17. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/src/muse_code_sdk.egg-info/SOURCES.txt +11 -0
  18. muse_code_sdk-1.4.15/src/muse_code_sdk.egg-info/requires.txt +2 -0
  19. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/tests/test_compatibility_row.py +245 -25
  20. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/tests/test_external_audience.py +23 -0
  21. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/tests/test_facade_approval.py +32 -0
  22. muse_code_sdk-1.4.15/tests/test_facade_control_verbs.py +709 -0
  23. muse_code_sdk-1.4.15/tests/test_facade_raw_command.py +162 -0
  24. muse_code_sdk-1.4.15/tests/test_facade_run.py +719 -0
  25. muse_code_sdk-1.4.15/tests/test_facade_session_mcp.py +211 -0
  26. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/tests/test_facade_turn.py +38 -0
  27. muse_code_sdk-1.4.15/tests/test_facade_unqueue_retirement.py +238 -0
  28. muse_code_sdk-1.4.15/tests/test_facade_wire_queries.py +317 -0
  29. muse_code_sdk-1.4.15/tests/test_fingerprint_vectors.py +86 -0
  30. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/tests/test_host_discovery.py +98 -1
  31. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/tests/test_manifests.py +35 -12
  32. muse_code_sdk-1.4.15/tests/test_msp_py_fingerprint_derivation.py +490 -0
  33. muse_code_sdk-1.4.15/tests/test_msp_py_host_version_derivation.py +274 -0
  34. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/tests/test_public_mirror_text.py +4 -1
  35. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/tests/test_publish_shape.py +1 -1
  36. muse_code_sdk-1.4.15/tests/test_run_abort_busy_unqueue_real_binary.py +491 -0
  37. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/tests/test_session_fold.py +155 -3
  38. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/tests/test_source_closure.py +5 -3
  39. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/tests/test_sync_facade.py +45 -38
  40. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/tests/test_typing_gates.py +71 -0
  41. muse_code_sdk-1.4.13/src/muse_code_sdk.egg-info/requires.txt +0 -2
  42. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/LICENSE +0 -0
  43. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/setup.cfg +0 -0
  44. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/src/muse_code/connection/__init__.py +0 -0
  45. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/src/muse_code/connection/connection.py +0 -0
  46. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/src/muse_code/connection/spawn.py +0 -0
  47. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/src/muse_code/connection/validation.py +0 -0
  48. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/src/muse_code/facade/discarded.py +0 -0
  49. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/src/muse_code/facade/gap_fill.py +0 -0
  50. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/src/muse_code/facade/host_death.py +0 -0
  51. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/src/muse_code/facade/host_start.py +0 -0
  52. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/src/muse_code/fold/__init__.py +0 -0
  53. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/src/muse_code/fold/item_store.py +0 -0
  54. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/src/muse_code/fold/state_store.py +0 -0
  55. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/src/muse_code/pending/__init__.py +0 -0
  56. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/src/muse_code/pending/pending_command_set.py +0 -0
  57. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/src/muse_code/py.typed +0 -0
  58. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/src/muse_code/replay.py +0 -0
  59. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/src/muse_code_sdk.egg-info/dependency_links.txt +0 -0
  60. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/src/muse_code_sdk.egg-info/top_level.txt +0 -0
  61. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/tests/test_asyncio_cancellation.py +0 -0
  62. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/tests/test_connection.py +0 -0
  63. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/tests/test_connection_error_arms.py +0 -0
  64. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/tests/test_docs_generation.py +0 -0
  65. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/tests/test_facade_approval_resend.py +0 -0
  66. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/tests/test_facade_gap_fill.py +0 -0
  67. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/tests/test_facade_open_same_chunk.py +0 -0
  68. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/tests/test_facade_persistent_approval.py +0 -0
  69. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/tests/test_facade_required_selection.py +0 -0
  70. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/tests/test_facade_user_input_resend.py +0 -0
  71. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/tests/test_item_store.py +0 -0
  72. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/tests/test_muse_serve_child.py +0 -0
  73. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/tests/test_pending_command_corpus.py +0 -0
  74. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/tests/test_pending_command_set.py +0 -0
  75. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/tests/test_pending_command_wire_binding.py +0 -0
  76. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/tests/test_permission_state_wire_types.py +0 -0
  77. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/tests/test_recovery_observation.py +0 -0
  78. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/tests/test_spawn_handshake.py +0 -0
  79. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/tests/test_spawn_host_lost.py +0 -0
  80. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/tests/test_state_store.py +0 -0
  81. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/tests/test_tolerance_corpus.py +0 -0
  82. {muse_code_sdk-1.4.13 → muse_code_sdk-1.4.15}/tests/test_transcript_replay.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: muse-code-sdk
3
- Version: 1.4.13
3
+ Version: 1.4.15
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.13
18
+ Requires-Dist: muse-code-msp==1.4.15
19
19
  Dynamic: license-file
20
20
 
21
21
  # `muse-code-sdk`
@@ -61,29 +61,57 @@ path passed at spawn time.
61
61
  ```python
62
62
  import asyncio
63
63
 
64
- from muse_code import (
65
- MuseClient, MuseClientSpawnOptions, SendUserTurnOptions, StartSessionOptions,
66
- )
64
+ from muse_code import MuseClient, MuseClientSpawnOptions, StartSessionOptions
67
65
 
68
66
  async def main() -> None:
69
67
  client = await MuseClient.spawn(MuseClientSpawnOptions(
70
- muse_bin="muse",
68
+ muse_bin=None, # None finds `muse` on PATH; or pass a path
71
69
  args=["serve"],
72
70
  client_info={"name": "my-app", "version": "1.0.0"},
73
71
  ))
74
72
  session = await client.start_session(StartSessionOptions(workspace_root="."))
75
- turn = await session.send_user_turn(SendUserTurnOptions(
76
- input=[{"type": "text", "text": "Summarize this repository."}],
77
- composer_input="Summarize this repository.",
78
- ))
79
- async for item in turn.items():
80
- print(item)
81
- await turn.completed
73
+ result = await session.run("Summarize this repository.")
74
+ print(result.final_text)
82
75
  await client.close()
83
76
 
84
77
  asyncio.run(main())
85
78
  ```
86
79
 
80
+ `session.run` sends one turn and returns when the turn has ended, with the
81
+ outcome, the final answer text, the turn's items, and its token usage. A turn
82
+ that failed or was cancelled still returns; read `result.outcome` to tell. The
83
+ final answer is the last completed agent message that is not commentary. Pass a
84
+ list of input parts instead of a string, and turn options as keywords:
85
+ `await session.run(parts, reasoning_effort="high")`.
86
+
87
+ To stop it, cancel the task that awaits it, for example with
88
+ `asyncio.timeout(60)` on Python 3.11 or newer. A turn still waiting in the
89
+ queue is taken back, and a running turn is cancelled; `run` then waits for
90
+ the turn to end and re-raises `CancelledError`. `SyncSession.run` is the
91
+ blocking version, with no way to cancel.
92
+
93
+ To follow a turn as it runs, send it yourself and read its items (paste
94
+ inside `main()` above, before `await client.close()`; add
95
+ `SendUserTurnOptions` to the import):
96
+
97
+ ```python
98
+ turn = await session.send_user_turn(SendUserTurnOptions(
99
+ input=[{"type": "text", "text": "Summarize this repository."}],
100
+ composer_input="Summarize this repository.",
101
+ ))
102
+ async for item in turn.items():
103
+ print(item)
104
+ await turn.completed
105
+ ```
106
+
107
+ To stop a running turn, `await turn.interrupt()` (the stop gesture; pass
108
+ `retract=True` to also take the prompt back) or `await turn.cancel()`. Both are
109
+ requests: the turn is over when `turn.completed` settles. `turn.steer(...)`
110
+ adds input to the running turn, and `turn.unqueue()` takes back a queued one.
111
+ The session itself can `fork`, `compact`, `rename`, and `leave`, and switch its
112
+ model, reasoning effort, or approval mode (`set_model`, `set_reasoning_effort`,
113
+ `set_approval_mode`); `client.list_models()` lists the models it accepts.
114
+
87
115
  To run a session and the child agents it starts without approval prompts,
88
116
  start it under the built-in `:unrestricted` permission profile. Current
89
117
  releases admit every child of such a session without prompts; a child admitted
@@ -41,29 +41,57 @@ path passed at spawn time.
41
41
  ```python
42
42
  import asyncio
43
43
 
44
- from muse_code import (
45
- MuseClient, MuseClientSpawnOptions, SendUserTurnOptions, StartSessionOptions,
46
- )
44
+ from muse_code import MuseClient, MuseClientSpawnOptions, StartSessionOptions
47
45
 
48
46
  async def main() -> None:
49
47
  client = await MuseClient.spawn(MuseClientSpawnOptions(
50
- muse_bin="muse",
48
+ muse_bin=None, # None finds `muse` on PATH; or pass a path
51
49
  args=["serve"],
52
50
  client_info={"name": "my-app", "version": "1.0.0"},
53
51
  ))
54
52
  session = await client.start_session(StartSessionOptions(workspace_root="."))
55
- turn = await session.send_user_turn(SendUserTurnOptions(
56
- input=[{"type": "text", "text": "Summarize this repository."}],
57
- composer_input="Summarize this repository.",
58
- ))
59
- async for item in turn.items():
60
- print(item)
61
- await turn.completed
53
+ result = await session.run("Summarize this repository.")
54
+ print(result.final_text)
62
55
  await client.close()
63
56
 
64
57
  asyncio.run(main())
65
58
  ```
66
59
 
60
+ `session.run` sends one turn and returns when the turn has ended, with the
61
+ outcome, the final answer text, the turn's items, and its token usage. A turn
62
+ that failed or was cancelled still returns; read `result.outcome` to tell. The
63
+ final answer is the last completed agent message that is not commentary. Pass a
64
+ list of input parts instead of a string, and turn options as keywords:
65
+ `await session.run(parts, reasoning_effort="high")`.
66
+
67
+ To stop it, cancel the task that awaits it, for example with
68
+ `asyncio.timeout(60)` on Python 3.11 or newer. A turn still waiting in the
69
+ queue is taken back, and a running turn is cancelled; `run` then waits for
70
+ the turn to end and re-raises `CancelledError`. `SyncSession.run` is the
71
+ blocking version, with no way to cancel.
72
+
73
+ To follow a turn as it runs, send it yourself and read its items (paste
74
+ inside `main()` above, before `await client.close()`; add
75
+ `SendUserTurnOptions` to the import):
76
+
77
+ ```python
78
+ turn = await session.send_user_turn(SendUserTurnOptions(
79
+ input=[{"type": "text", "text": "Summarize this repository."}],
80
+ composer_input="Summarize this repository.",
81
+ ))
82
+ async for item in turn.items():
83
+ print(item)
84
+ await turn.completed
85
+ ```
86
+
87
+ To stop a running turn, `await turn.interrupt()` (the stop gesture; pass
88
+ `retract=True` to also take the prompt back) or `await turn.cancel()`. Both are
89
+ requests: the turn is over when `turn.completed` settles. `turn.steer(...)`
90
+ adds input to the running turn, and `turn.unqueue()` takes back a queued one.
91
+ The session itself can `fork`, `compact`, `rename`, and `leave`, and switch its
92
+ model, reasoning effort, or approval mode (`set_model`, `set_reasoning_effort`,
93
+ `set_approval_mode`); `client.list_models()` lists the models it accepts.
94
+
67
95
  To run a session and the child agents it starts without approval prompts,
68
96
  start it under the built-in `:unrestricted` permission profile. Current
69
97
  releases admit every child of such a session without prompts; a child admitted
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
 
6
6
  [project]
7
7
  name = "muse-code-sdk"
8
- version = "1.4.13"
8
+ version = "1.4.15"
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.13"]
21
+ dependencies = ["pydantic==2.13.5", "muse-code-msp==1.4.15"]
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
@@ -19,10 +19,15 @@ from __future__ import annotations
19
19
 
20
20
  from typing import Final
21
21
 
22
+ import muse_code_msp
23
+
22
24
  from .errors import (
23
25
  GapFillFailureReason,
26
+ MuseCapabilityNotGrantedError,
27
+ MuseFacadeOwnedMethodError,
24
28
  MuseForeignSessionError,
25
29
  MuseGapFillError,
30
+ MuseHostDiscoveryError,
26
31
  MuseSessionDiscardedError,
27
32
  PermissionProfileNotApplied,
28
33
  )
@@ -42,18 +47,22 @@ __all__ = [
42
47
  "HostDeathDischarge",
43
48
  "HostDeathNotification",
44
49
  "ItemStore",
50
+ "MuseCapabilityNotGrantedError",
45
51
  "MuseClient",
46
52
  "MuseClientOptions",
47
53
  "MuseClientSpawnOptions",
54
+ "MuseFacadeOwnedMethodError",
48
55
  "MuseForeignSessionError",
49
56
  "MuseGapFillError",
50
57
  "MuseHostDiedError",
58
+ "MuseHostDiscoveryError",
51
59
  "MuseHostStartError",
52
60
  "MuseSessionDiscardedError",
53
61
  "PendingCommandSet",
54
62
  "PendingCommandView",
55
63
  "PermissionProfileNotApplied",
56
64
  "ResumeSessionOptions",
65
+ "RunResult",
57
66
  "SendUserTurnOptions",
58
67
  "Session",
59
68
  "SessionApplyOutcome",
@@ -64,6 +73,7 @@ __all__ = [
64
73
  "SessionGapOverlap",
65
74
  "SessionStateStore",
66
75
  "StartSessionOptions",
76
+ "SteerTurnOptions",
67
77
  "SyncMuseClient",
68
78
  "SyncSession",
69
79
  "SyncTurn",
@@ -71,6 +81,7 @@ __all__ = [
71
81
  "TransportEof",
72
82
  "Turn",
73
83
  "TurnOutcome",
84
+ "TurnUnqueueOutcome",
74
85
  "is_abnormal_host_death",
75
86
  "is_launch_failure",
76
87
  "load_transcript",
@@ -79,20 +90,20 @@ __all__ = [
79
90
  "transcript_dirs",
80
91
  ]
81
92
 
82
- EXPECTED_SCHEMA_FINGERPRINT: Final[str] = (
83
- "sha256:7fddaf646dbc317e5fcac0785e135eb04df14fbb0db1863089dfd6da21730e5b"
84
- )
93
+ EXPECTED_SCHEMA_FINGERPRINT: Final[str] = muse_code_msp.SCHEMA_FINGERPRINT
85
94
  """The stable schema-bundle fingerprint this SDK was built against.
86
95
 
87
- The one deliberately duplicated fact: ``clients/sdk-py/tests/test_manifests.py`` binds it to
88
- ``schema/msp/stable/manifest.json``, so a schema advance that forgets the
89
- Python SDK reds this lane instead of drifting silently.
96
+ No second port and no second copy: this is the generated package's stable
97
+ ``SCHEMA_FINGERPRINT``, derived at import from the schema bytes. The
98
+ manifests suite binds it to the value derived from the stable bundle, so a
99
+ drift between the port the package vendors and the shared derivation reds
100
+ that lane instead of passing silently.
90
101
  """
91
102
 
92
103
  # The lockstep rule: the release train bumps pyproject.toml, the
93
104
  # distribution-metadata authority; this constant is aligned at publication
94
105
  # time.
95
- __version__: Final[str] = "1.4.13"
106
+ __version__: Final[str] = "1.4.15"
96
107
 
97
108
  # The facade is imported last so the constants above are already bound: the
98
109
  # spawn path reads ``EXPECTED_SCHEMA_FINGERPRINT`` (lazily, at initialize
@@ -114,6 +125,7 @@ from .facade import ( # noqa: E402
114
125
  MuseHostStartError,
115
126
  PendingCommandView,
116
127
  ResumeSessionOptions,
128
+ RunResult,
117
129
  SendUserTurnOptions,
118
130
  Session,
119
131
  SessionApplyOutcome,
@@ -122,9 +134,11 @@ from .facade import ( # noqa: E402
122
134
  SessionGapBuffered,
123
135
  SessionGapOverlap,
124
136
  StartSessionOptions,
137
+ SteerTurnOptions,
125
138
  TransportEof,
126
139
  Turn,
127
140
  TurnOutcome,
141
+ TurnUnqueueOutcome,
128
142
  is_abnormal_host_death,
129
143
  is_launch_failure,
130
144
  read_session_durability,
@@ -2,10 +2,11 @@
2
2
 
3
3
  The wheel carries no host binary and no code path assumes one. Exactly the
4
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
5
+ ``muse`` on ``PATH`` (the same two tiers as the TS and Rust SDKs, and what
6
+ ``MuseClient.spawn`` applies when ``muse_bin`` is ``None``). The LIBRARY reads
7
+ no other environment variable: ``MUSE_BIN`` is a knob of the
8
+ quickstart/cookbook HARNESSES, which read it themselves and pass the explicit
9
+ parameter down. No discoverable host fails BEFORE any process is
9
10
  spawned, with an exact error naming the parameter and the compatibility
10
11
  page.
11
12
  """
@@ -115,6 +115,53 @@ class PermissionProfileNotApplied(Exception):
115
115
  self.session_id: str = session_id
116
116
 
117
117
 
118
+
119
+ class MuseFacadeOwnedMethodError(Exception):
120
+ """``MuseClient.command`` was asked to send a method whose state the
121
+ facade owns. Sending it raw would bypass that state, so it is refused
122
+ before anything is written; use the facade verb instead.
123
+
124
+ Attributes:
125
+ method: The refused wire method.
126
+ """
127
+
128
+ def __init__(self, method: str) -> None:
129
+ """Builds the refusal.
130
+
131
+ Args:
132
+ method: The refused wire method.
133
+ """
134
+ super().__init__(
135
+ f"{method} is sent by the facade itself and cannot go through "
136
+ "MuseClient.command; use the facade verb that owns it"
137
+ )
138
+ self.method: str = method
139
+
140
+
141
+ class MuseCapabilityNotGrantedError(Exception):
142
+ """A session open asked for something the host did not grant on this
143
+ connection: today, per-session MCP servers (``config["mcpServers"]``)
144
+ without the ``sessionMcp`` capability. Refused before anything is sent;
145
+ the client stays usable, and an open without that request still goes
146
+ through.
147
+
148
+ Attributes:
149
+ capability: The capability the request needs.
150
+ method: The wire method that was refused.
151
+ """
152
+
153
+ def __init__(self, capability: str, method: str, reason: str) -> None:
154
+ """Builds the refusal.
155
+
156
+ Args:
157
+ capability: The capability the request needs.
158
+ method: The wire method that was refused.
159
+ reason: Why the grant is missing.
160
+ """
161
+ super().__init__(f"{method} was not sent: {reason}")
162
+ self.capability: str = capability
163
+ self.method: str = method
164
+
118
165
  ValidationSeam = Literal["initializeResult", "spawnOptions"]
119
166
  """Which validating pydantic seam refused its input.
120
167
 
@@ -207,9 +254,9 @@ class MuseHostMismatchError(Exception):
207
254
  pinned: ``muse_code.EXPECTED_SCHEMA_FINGERPRINT``.
208
255
  host_version: ``serverInfo.version`` from the handshake result.
209
256
  required_host_version: The generated
210
- ``muse_code_msp.REQUIRED_HOST_VERSION`` — tree-derived by the
211
- renderer from the host crate's manifest and kept honest by
212
- the regen gate, never hand-typed.
257
+ ``muse_code_msp.REQUIRED_HOST_VERSION`` — derived at import
258
+ through the vendored resolver and kept honest by the regen
259
+ gate, never hand-typed.
213
260
  Required, not defaulted: the contract names the exact version, so
214
261
  a version-less message is not a state this error can carry.
215
262
  """
@@ -50,8 +50,9 @@ from .session import (
50
50
  SessionGapOverlap,
51
51
  SessionOpening,
52
52
  )
53
- from .turn_handle import FoldedItem, Turn, TurnOutcome, is_launch_failure
54
- from .turn_submit import SendUserTurnOptions
53
+ from .run import RunResult
54
+ from .turn_handle import FoldedItem, Turn, TurnOutcome, TurnUnqueueOutcome, is_launch_failure
55
+ from .turn_submit import SendUserTurnOptions, SteerTurnOptions
55
56
 
56
57
  __all__ = [
57
58
  "AbnormalExit",
@@ -71,6 +72,7 @@ __all__ = [
71
72
  "MuseHostStartError",
72
73
  "PendingCommandView",
73
74
  "ResumeSessionOptions",
75
+ "RunResult",
74
76
  "SendUserTurnOptions",
75
77
  "Session",
76
78
  "SessionApplyOutcome",
@@ -81,9 +83,11 @@ __all__ = [
81
83
  "SessionGapOverlap",
82
84
  "SessionOpening",
83
85
  "StartSessionOptions",
86
+ "SteerTurnOptions",
84
87
  "TransportEof",
85
88
  "Turn",
86
89
  "TurnOutcome",
90
+ "TurnUnqueueOutcome",
87
91
  "is_abnormal_host_death",
88
92
  "is_launch_failure",
89
93
  "read_session_durability",
@@ -66,6 +66,7 @@ _REFRESH_FROM_REQUEST = frozenset(
66
66
  "judgeEscalated",
67
67
  "protectedWrite",
68
68
  "rawArgs",
69
+ "reviewerReason",
69
70
  "taskId",
70
71
  "toolCallId",
71
72
  "toolName",
@@ -302,6 +303,10 @@ class ApprovalRouter:
302
303
  # the retained request across stages.
303
304
  if "cuaCardFacts" in requested:
304
305
  refreshed["cuaCardFacts"] = requested["cuaCardFacts"]
306
+ # The escalation reason belongs to the call, so it stays with the
307
+ # retained request across stages.
308
+ if "reviewerReason" in requested:
309
+ refreshed["reviewerReason"] = requested["reviewerReason"]
305
310
  acked = self._in_flight.get(requested["approvalId"])
306
311
  if acked is not None:
307
312
  return self._after_ack(acked, refreshed)