soothe-client-python 0.9.9__tar.gz → 0.9.10__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 (33) hide show
  1. {soothe_client_python-0.9.9 → soothe_client_python-0.9.10}/PKG-INFO +6 -3
  2. {soothe_client_python-0.9.9 → soothe_client_python-0.9.10}/README.md +5 -2
  3. soothe_client_python-0.9.10/src/soothe_client/__init__.py +189 -0
  4. {soothe_client_python-0.9.9 → soothe_client_python-0.9.10}/src/soothe_client/appkit/__init__.py +9 -44
  5. {soothe_client_python-0.9.9 → soothe_client_python-0.9.10}/src/soothe_client/appkit/daemon_session.py +32 -8
  6. {soothe_client_python-0.9.9 → soothe_client_python-0.9.10}/src/soothe_client/helpers.py +5 -1
  7. {soothe_client_python-0.9.9 → soothe_client_python-0.9.10}/src/soothe_client/protocol_params.py +5 -0
  8. {soothe_client_python-0.9.9 → soothe_client_python-0.9.10}/src/soothe_client/session.py +6 -9
  9. {soothe_client_python-0.9.9 → soothe_client_python-0.9.10}/src/soothe_client/stream_terminal.py +3 -3
  10. {soothe_client_python-0.9.9 → soothe_client_python-0.9.10}/src/soothe_client/turn_boundary.py +4 -3
  11. {soothe_client_python-0.9.9 → soothe_client_python-0.9.10}/src/soothe_client/ws_command_client.py +121 -156
  12. soothe_client_python-0.9.9/src/soothe_client/__init__.py +0 -194
  13. {soothe_client_python-0.9.9 → soothe_client_python-0.9.10}/.gitignore +0 -0
  14. {soothe_client_python-0.9.9 → soothe_client_python-0.9.10}/LICENSE +0 -0
  15. {soothe_client_python-0.9.9 → soothe_client_python-0.9.10}/pyproject.toml +0 -0
  16. {soothe_client_python-0.9.9 → soothe_client_python-0.9.10}/src/soothe_client/appkit/attachments.py +0 -0
  17. {soothe_client_python-0.9.9 → soothe_client_python-0.9.10}/src/soothe_client/appkit/broadcaster.py +0 -0
  18. {soothe_client_python-0.9.9 → soothe_client_python-0.9.10}/src/soothe_client/appkit/chunk_filter.py +0 -0
  19. {soothe_client_python-0.9.9 → soothe_client_python-0.9.10}/src/soothe_client/appkit/classifier.py +0 -0
  20. {soothe_client_python-0.9.9 → soothe_client_python-0.9.10}/src/soothe_client/appkit/events.py +0 -0
  21. {soothe_client_python-0.9.9 → soothe_client_python-0.9.10}/src/soothe_client/appkit/managed_client.py +0 -0
  22. {soothe_client_python-0.9.9 → soothe_client_python-0.9.10}/src/soothe_client/appkit/observability.py +0 -0
  23. {soothe_client_python-0.9.9 → soothe_client_python-0.9.10}/src/soothe_client/appkit/pool.py +0 -0
  24. {soothe_client_python-0.9.9 → soothe_client_python-0.9.10}/src/soothe_client/appkit/query_gate.py +0 -0
  25. {soothe_client_python-0.9.9 → soothe_client_python-0.9.10}/src/soothe_client/appkit/session_store.py +0 -0
  26. {soothe_client_python-0.9.9 → soothe_client_python-0.9.10}/src/soothe_client/appkit/thinking_step.py +0 -0
  27. {soothe_client_python-0.9.9 → soothe_client_python-0.9.10}/src/soothe_client/appkit/turn.py +0 -0
  28. {soothe_client_python-0.9.9 → soothe_client_python-0.9.10}/src/soothe_client/appkit/turn_runner.py +0 -0
  29. {soothe_client_python-0.9.9 → soothe_client_python-0.9.10}/src/soothe_client/errors.py +0 -0
  30. {soothe_client_python-0.9.9 → soothe_client_python-0.9.10}/src/soothe_client/intent_hints.py +0 -0
  31. {soothe_client_python-0.9.9 → soothe_client_python-0.9.10}/src/soothe_client/py.typed +0 -0
  32. {soothe_client_python-0.9.9 → soothe_client_python-0.9.10}/src/soothe_client/schemas.py +0 -0
  33. {soothe_client_python-0.9.9 → soothe_client_python-0.9.10}/src/soothe_client/websocket.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: soothe-client-python
3
- Version: 0.9.9
3
+ Version: 0.9.10
4
4
  Summary: WebSocket client + appkit for soothe-daemon (Python)
5
5
  Project-URL: Homepage, https://github.com/mirasoth/soothe-client-python
6
6
  Project-URL: Documentation, https://soothe.readthedocs.io
@@ -69,10 +69,13 @@ More patterns: [`examples/`](examples/) (hello → streaming → multi-turn →
69
69
 
70
70
  | Need | Use |
71
71
  |------|-----|
72
- | One conversation, stream replies | `DaemonSession` |
72
+ | One conversation, stream replies | `DaemonSession` (`soothe_client.appkit`) |
73
+ | Jobs / cron (async) | `AsyncCommandClient` (alias of `WsCommandClient`) |
74
+ | Jobs / cron (scripts / sync) | `CommandClient` (alias of `SyncWsCommandClient`) |
73
75
  | Raw WebSocket / custom RPCs | `WebSocketClient` |
74
76
  | Many users / HTTP backend | `ConnectionPool` + `TurnRunner` |
75
- | Jobs / autopilot / cron | `WsCommandClient` |
77
+
78
+ Wire request param models live in `soothe_client.protocol_params` (not at package root).
76
79
 
77
80
  ## Develop
78
81
 
@@ -36,10 +36,13 @@ More patterns: [`examples/`](examples/) (hello → streaming → multi-turn →
36
36
 
37
37
  | Need | Use |
38
38
  |------|-----|
39
- | One conversation, stream replies | `DaemonSession` |
39
+ | One conversation, stream replies | `DaemonSession` (`soothe_client.appkit`) |
40
+ | Jobs / cron (async) | `AsyncCommandClient` (alias of `WsCommandClient`) |
41
+ | Jobs / cron (scripts / sync) | `CommandClient` (alias of `SyncWsCommandClient`) |
40
42
  | Raw WebSocket / custom RPCs | `WebSocketClient` |
41
43
  | Many users / HTTP backend | `ConnectionPool` + `TurnRunner` |
42
- | Jobs / autopilot / cron | `WsCommandClient` |
44
+
45
+ Wire request param models live in `soothe_client.protocol_params` (not at package root).
43
46
 
44
47
  ## Develop
45
48
 
@@ -0,0 +1,189 @@
1
+ """Soothe WebSocket client for talking to a running soothe-daemon.
2
+
3
+ Public surface (community apps)::
4
+
5
+ from soothe_client import WebSocketClient, is_daemon_live
6
+ from soothe_client.appkit import DaemonSession
7
+ from soothe_client import AsyncCommandClient, CommandClient # preferred aliases
8
+
9
+ Advanced wire validation lives in ``soothe_client.protocol_params``.
10
+ Higher-level session / multi-user helpers live in ``soothe_client.appkit``.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import importlib.metadata
16
+ import warnings
17
+ from typing import Any
18
+
19
+ from soothe_sdk.wire.codec import ProtocolError
20
+
21
+ from soothe_client.errors import (
22
+ DaemonError,
23
+ DisconnectCause,
24
+ ReconnectError,
25
+ StaleLoopError,
26
+ disconnect_cause_name,
27
+ )
28
+ from soothe_client.helpers import (
29
+ check_daemon_status,
30
+ connected_websocket,
31
+ fetch_config_section,
32
+ fetch_loop_cards,
33
+ fetch_loop_history,
34
+ fetch_loop_messages,
35
+ fetch_skills_catalog,
36
+ is_daemon_live,
37
+ protocol1_rpc,
38
+ request_daemon_config_reload,
39
+ request_daemon_shutdown,
40
+ websocket_url_from_config,
41
+ )
42
+ from soothe_client.intent_hints import (
43
+ DEFAULT_DELIVERABLE_PHASES,
44
+ EMBED,
45
+ IMAGE_TO_TEXT,
46
+ OCR,
47
+ TEXT_COMPLETION,
48
+ validate_loop_input_intent_hint,
49
+ )
50
+ from soothe_client.session import (
51
+ bootstrap_loop_session,
52
+ connect_websocket_with_retries,
53
+ )
54
+ from soothe_client.websocket import WebSocketClient
55
+ from soothe_client.ws_command_client import (
56
+ AsyncCommandClient,
57
+ CommandClient,
58
+ SyncWsCommandClient,
59
+ WsCommandClient,
60
+ async_command_client_from_config,
61
+ async_ws_command_client_from_config,
62
+ command_client_from_config,
63
+ ws_command_client_from_config,
64
+ )
65
+
66
+ try:
67
+ __version__ = importlib.metadata.version("soothe-client-python")
68
+ except importlib.metadata.PackageNotFoundError:
69
+ __version__ = "0.0.0"
70
+
71
+ # Legacy ``*Params`` names formerly re-exported at package root.
72
+ _LEGACY_PARAM_NAMES = frozenset(
73
+ {
74
+ "AuthParams",
75
+ "AuthRefreshParams",
76
+ "AutopilotCancelAllParams",
77
+ "AutopilotCancelGoalParams",
78
+ "AutopilotDreamParams",
79
+ "AutopilotGetGoalParams",
80
+ "AutopilotGetJobParams",
81
+ "AutopilotListGoalsParams",
82
+ "AutopilotListJobsParams",
83
+ "AutopilotResumeParams",
84
+ "AutopilotStatusParams",
85
+ "AutopilotSubmitParams",
86
+ "AutopilotSubscribeParams",
87
+ "AutopilotWakeParams",
88
+ "ConfigGetParams",
89
+ "ConfigReloadParams",
90
+ "CronAddParams",
91
+ "CronCancelParams",
92
+ "CronListParams",
93
+ "CronShowParams",
94
+ "DaemonShutdownParams",
95
+ "DaemonStatusParams",
96
+ "DisconnectParams",
97
+ "InvokeSkillParams",
98
+ "JobCancelParams",
99
+ "JobCreateParams",
100
+ "JobDagParams",
101
+ "JobGuidanceParams",
102
+ "JobPauseParams",
103
+ "JobResumeParams",
104
+ "JobStatusParams",
105
+ "LoopCardsFetchParams",
106
+ "LoopDeleteParams",
107
+ "LoopDetachParams",
108
+ "LoopGetParams",
109
+ "LoopInputParams",
110
+ "LoopListParams",
111
+ "LoopMessagesParams",
112
+ "LoopNewParams",
113
+ "LoopPruneParams",
114
+ "LoopReattachParams",
115
+ "LoopStateGetParams",
116
+ "LoopStateUpdateParams",
117
+ "LoopTreeParams",
118
+ "McpStatusParams",
119
+ "ModelsListParams",
120
+ "RpcCommandParams",
121
+ "SkillsListParams",
122
+ "SlashCommandParams",
123
+ "SubscribeParams",
124
+ }
125
+ )
126
+
127
+ __all__ = [
128
+ "__version__",
129
+ # Transport
130
+ "WebSocketClient",
131
+ "ProtocolError",
132
+ # Errors
133
+ "DaemonError",
134
+ "DisconnectCause",
135
+ "ReconnectError",
136
+ "StaleLoopError",
137
+ "disconnect_cause_name",
138
+ # Command clients (canonical + preferred aliases)
139
+ "WsCommandClient",
140
+ "SyncWsCommandClient",
141
+ "AsyncCommandClient",
142
+ "CommandClient",
143
+ "ws_command_client_from_config",
144
+ "async_ws_command_client_from_config",
145
+ "command_client_from_config",
146
+ "async_command_client_from_config",
147
+ # Session bootstrap
148
+ "bootstrap_loop_session",
149
+ "connect_websocket_with_retries",
150
+ # Helpers
151
+ "websocket_url_from_config",
152
+ "connected_websocket",
153
+ "protocol1_rpc",
154
+ "check_daemon_status",
155
+ "is_daemon_live",
156
+ "request_daemon_config_reload",
157
+ "request_daemon_shutdown",
158
+ "fetch_skills_catalog",
159
+ "fetch_config_section",
160
+ "fetch_loop_history",
161
+ "fetch_loop_cards",
162
+ "fetch_loop_messages",
163
+ # Intent hints
164
+ "TEXT_COMPLETION",
165
+ "IMAGE_TO_TEXT",
166
+ "OCR",
167
+ "EMBED",
168
+ "DEFAULT_DELIVERABLE_PHASES",
169
+ "validate_loop_input_intent_hint",
170
+ ]
171
+
172
+
173
+ def __getattr__(name: str) -> Any:
174
+ """Lazy-load legacy ``*Params`` with a deprecation warning.
175
+
176
+ Prefer ``from soothe_client.protocol_params import LoopInputParams``.
177
+ """
178
+ if name in _LEGACY_PARAM_NAMES:
179
+ warnings.warn(
180
+ f"Importing {name!r} from soothe_client is deprecated; "
181
+ f"use soothe_client.protocol_params.{name} instead.",
182
+ DeprecationWarning,
183
+ stacklevel=2,
184
+ )
185
+ from soothe_client import protocol_params as _params
186
+
187
+ return getattr(_params, name)
188
+ msg = f"module {__name__!r} has no attribute {name!r}"
189
+ raise AttributeError(msg)
@@ -1,13 +1,16 @@
1
1
  """Application helpers built on WebSocketClient.
2
2
 
3
- Product-agnostic building blocks for agent UIs and backends:
3
+ Primary building blocks for agent UIs and backends:
4
4
 
5
5
  - ``DaemonSession`` — dual-socket loop session + streamed turns
6
6
  - ``ConnectionPool`` / ``TurnRunner`` — multi-session turn execution
7
7
  - ``QueryGate`` — one-in-flight query per session
8
- - ``EventClassifier`` / ``extract_thinking_step`` — stream → deliverable mapping
8
+ - ``EventClassifier`` — stream → deliverable mapping
9
9
  - ``SSEBroadcaster`` — drop-on-full fan-out to subscribers
10
10
  - ``SessionStore`` — persistence seam (Protocol)
11
+
12
+ Advanced stream/pipeline plumbing lives in submodules
13
+ (``events``, ``chunk_filter``, ``turn``, ``managed_client``).
11
14
  """
12
15
 
13
16
  from __future__ import annotations
@@ -18,7 +21,6 @@ from soothe_client.appkit.attachments import (
18
21
  compact_image_attachment,
19
22
  )
20
23
  from soothe_client.appkit.broadcaster import SSEBroadcaster, SSEEvent
21
- from soothe_client.appkit.chunk_filter import should_drop_stream_chunk_early
22
24
  from soothe_client.appkit.classifier import (
23
25
  EVENT_FINAL_REPORT,
24
26
  ChatEventResult,
@@ -26,17 +28,7 @@ from soothe_client.appkit.classifier import (
26
28
  ClassifierConfig,
27
29
  EventClassifier,
28
30
  )
29
- from soothe_client.appkit.daemon_session import DEFAULT_POST_IDLE_DRAIN_S, DaemonSession
30
- from soothe_client.appkit.events import is_loop_scoped_event, unwrap_next
31
- from soothe_client.appkit.managed_client import (
32
- BootstrapFunc,
33
- ClientFactory,
34
- ManagedClient,
35
- WebSocketManagedClient,
36
- default_bootstrap_func,
37
- default_client_factory,
38
- )
39
- from soothe_client.appkit.observability import TurnEventStats
31
+ from soothe_client.appkit.daemon_session import DaemonSession
40
32
  from soothe_client.appkit.pool import (
41
33
  ConnectionPool,
42
34
  ErrPoolExhausted,
@@ -50,15 +42,6 @@ from soothe_client.appkit.thinking_step import (
50
42
  DEFAULT_THINKING_STEP_EVENTS,
51
43
  extract_thinking_step,
52
44
  )
53
- from soothe_client.appkit.turn import (
54
- PRIORITY_CRITICAL,
55
- PRIORITY_HIGH,
56
- PRIORITY_LOW,
57
- PRIORITY_NORMAL,
58
- TurnApplyBatcher,
59
- TurnEventPipeline,
60
- run_turn_pipeline,
61
- )
62
45
  from soothe_client.appkit.turn_runner import (
63
46
  STREAM_CLOSE_FAIL,
64
47
  STREAM_CLOSE_SOFT_COMPLETE,
@@ -77,13 +60,13 @@ from soothe_client.appkit.turn_runner import (
77
60
  )
78
61
 
79
62
  __all__ = [
80
- "DEFAULT_POST_IDLE_DRAIN_S",
81
63
  "DEFAULT_THINKING_STEP_EVENTS",
82
64
  "STREAM_CLOSE_FAIL",
83
65
  "STREAM_CLOSE_SOFT_COMPLETE",
84
66
  "Attachment",
85
- "BootstrapFunc",
86
- "ClientFactory",
67
+ "ChatEventResult",
68
+ "ChatEventTerminal",
69
+ "ClassifierConfig",
87
70
  "CompactImageOptions",
88
71
  "ConnectionPool",
89
72
  "DaemonSession",
@@ -92,18 +75,10 @@ __all__ = [
92
75
  "ErrPoolExhausted",
93
76
  "ErrQueryBusy",
94
77
  "ErrQueryTimeout",
95
- "ChatEventResult",
96
- "ChatEventTerminal",
97
- "ClassifierConfig",
98
78
  "EventClassifier",
99
79
  "InputOpts",
100
- "ManagedClient",
101
80
  "OnComplete",
102
81
  "OnError",
103
- "PRIORITY_CRITICAL",
104
- "PRIORITY_HIGH",
105
- "PRIORITY_LOW",
106
- "PRIORITY_NORMAL",
107
82
  "PoolConfig",
108
83
  "PooledConn",
109
84
  "QueryGate",
@@ -114,22 +89,12 @@ __all__ = [
114
89
  "SessionStore",
115
90
  "StreamClosePolicy",
116
91
  "TimeoutPolicy",
117
- "TurnApplyBatcher",
118
92
  "TurnConfig",
119
- "TurnEventPipeline",
120
- "TurnEventStats",
121
93
  "TurnRunner",
122
- "WebSocketManagedClient",
123
94
  "compact_attachments",
124
95
  "compact_image_attachment",
125
- "default_bootstrap_func",
126
- "default_client_factory",
127
96
  "default_pool_config",
128
97
  "extract_thinking_step",
129
98
  "idle_timeout_for_turn",
130
99
  "input_message_for_loop",
131
- "is_loop_scoped_event",
132
- "run_turn_pipeline",
133
- "should_drop_stream_chunk_early",
134
- "unwrap_next",
135
100
  ]
@@ -37,7 +37,21 @@ StreamDeliveryResolver = Callable[[], str]
37
37
 
38
38
 
39
39
  class DaemonSession:
40
- """Daemon-backed loop session with stream + RPC sockets."""
40
+ """One conversation with a daemon loop: connect, send turns, stream chunks.
41
+
42
+ Owns a subscribed stream WebSocket plus an RPC sidecar so metadata calls
43
+ do not starve loop events. Typical usage::
44
+
45
+ session = DaemonSession("ws://127.0.0.1:8765")
46
+ await session.connect()
47
+ await session.send_turn("Hello")
48
+ async for _ns, mode, data in session.iter_turn_chunks():
49
+ ...
50
+ await session.close()
51
+
52
+ Advanced: ``early_drop_fn`` / ``stats_factory`` customize stream filtering
53
+ and metrics for product UIs; most apps leave the defaults.
54
+ """
41
55
 
42
56
  def __init__(
43
57
  self,
@@ -67,7 +81,7 @@ class DaemonSession:
67
81
  self.last_turn_end_state: str | None = None
68
82
  self.last_turn_cancellation_seen: bool = False
69
83
  self.last_turn_error_message: str | None = None
70
- # IG-659: last completed turn's seq floor (drop stale prior-turn frames).
84
+ # Last completed turn's seq floor (drop stale prior-turn frames).
71
85
  self._last_turn_end_seq: int = 0
72
86
  self._expected_turn_id: str | None = None
73
87
 
@@ -108,7 +122,11 @@ class DaemonSession:
108
122
  )
109
123
 
110
124
  async def connect(self, *, resume_loop_id: str | None = None) -> dict[str, Any]:
111
- """Connect and bootstrap a daemon loop session."""
125
+ """Open the stream socket and bootstrap (or resume) a loop session.
126
+
127
+ Returns:
128
+ Status event from the daemon (includes ``loop_id`` on success).
129
+ """
112
130
  await connect_websocket_with_retries(self._client)
113
131
  return await self._bootstrap_loop(resume_loop_id=resume_loop_id)
114
132
 
@@ -182,7 +200,7 @@ class DaemonSession:
182
200
  await self._bootstrap_loop(resume_loop_id=resume_loop_id)
183
201
 
184
202
  async def close(self, *, handshake_timeout: float = 2.0) -> None:
185
- """Close stream and RPC sockets (idempotent)."""
203
+ """Close stream and RPC sockets. Safe to call more than once."""
186
204
  if self._closed:
187
205
  return
188
206
  self._closed = True
@@ -219,7 +237,11 @@ class DaemonSession:
219
237
  clarification_answers: list[str] | None = None,
220
238
  intent_hint: str | None = None,
221
239
  ) -> None:
222
- """Send a new user turn to the daemon."""
240
+ """Send a user message on the active loop (start of a turn).
241
+
242
+ Call ``iter_turn_chunks`` afterward to consume the streamed reply.
243
+ Requires ``connect()`` (or another bootstrap) first.
244
+ """
223
245
  if not self._loop_id:
224
246
  raise RuntimeError("No active loop session")
225
247
  await self._client.send_input(
@@ -303,13 +325,15 @@ class DaemonSession:
303
325
  *,
304
326
  max_wait_s: float | None = None,
305
327
  ) -> AsyncIterator[tuple[tuple[Any, ...], str, Any]]:
306
- """Yield ``(namespace, mode, data)`` chunks for the active daemon turn.
328
+ """Yield ``(namespace, mode, data)`` stream chunks until the turn ends.
329
+
330
+ Handles idle timeout, post-idle drain, and turn boundary filtering.
331
+ Call after ``send_turn``.
307
332
 
308
333
  Args:
309
334
  max_wait_s: Optional absolute deadline for the whole turn. When set,
310
335
  raises ``TimeoutError`` if the daemon never emits a turn-end
311
- signal in time (idle after payload, stopped, stream.end, or
312
- strange_loop.completed).
336
+ signal in time.
313
337
  """
314
338
  self.turn_event_stats = self._new_turn_stats()
315
339
  self.last_turn_end_state = None
@@ -1,4 +1,8 @@
1
- """WebSocket helper functions for daemon communication."""
1
+ """Small WebSocket helpers for CLI and scripts (status, fetch, one-shot RPC).
2
+
3
+ For interactive turns use ``DaemonSession``. For jobs/cron use
4
+ ``WsCommandClient`` / ``CommandClient``.
5
+ """
2
6
 
3
7
  from __future__ import annotations
4
8
 
@@ -32,6 +32,7 @@ __all__ = [
32
32
  "AutopilotListGoalsParams",
33
33
  "AutopilotGetGoalParams",
34
34
  "AutopilotCancelGoalParams",
35
+ "AutopilotCancelAllParams",
35
36
  "AutopilotWakeParams",
36
37
  "AutopilotDreamParams",
37
38
  "AutopilotResumeParams",
@@ -363,6 +364,10 @@ class AutopilotCancelGoalParams(ParamsBase):
363
364
  goal_id: str = Field(..., min_length=1)
364
365
 
365
366
 
367
+ class AutopilotCancelAllParams(EmptyParams):
368
+ """Params for ``method=autopilot_cancel_all`` — no required fields."""
369
+
370
+
366
371
  class AutopilotWakeParams(EmptyParams):
367
372
  """Params for ``method=autopilot_wake`` — no required fields."""
368
373
 
@@ -1,4 +1,8 @@
1
- """Shared WebSocket session bootstrap for CLI headless and TUI."""
1
+ """Session bootstrap helpers for CLI, TUI, and appkit.
2
+
3
+ Prefer ``DaemonSession`` for application code. These helpers are the lower-level
4
+ connect + loop-subscribe steps used when you own the ``WebSocketClient``.
5
+ """
2
6
 
3
7
  from __future__ import annotations
4
8
 
@@ -22,14 +26,7 @@ _LEGACY_LOOP_AUTOPILOT_MODE = "solo"
22
26
 
23
27
 
24
28
  async def connect_websocket_with_retries(client: Any) -> None:
25
- """Connect to the daemon with bounded retries for cold-start races.
26
-
27
- Args:
28
- client: WebSocketClient instance.
29
-
30
- Raises:
31
- ConnectionError: If connection fails after all retries.
32
- """
29
+ """Connect a ``WebSocketClient`` with retries (daemon cold-start safe)."""
33
30
  last_error: OSError | ConnectionError | TimeoutError | None = None
34
31
  for attempt in range(_CONNECT_RETRY_COUNT):
35
32
  try:
@@ -1,7 +1,7 @@
1
- """Shared stream/turn terminal frame helpers for WebSocket and DaemonSession.
1
+ """Internal stream/turn terminal frame helpers.
2
2
 
3
- Keeps peel-at-turn-start and turn-end detection on one vocabulary so leftover
4
- prior-goal terminals cannot blank the next TUI query.
3
+ Used by ``WebSocketClient`` and ``DaemonSession`` so leftover prior-turn
4
+ terminals cannot blank the next query. Not part of the community public API.
5
5
  """
6
6
 
7
7
  from __future__ import annotations
@@ -1,7 +1,8 @@
1
- """Turn / stream boundary helpers (IG-659).
1
+ """Internal turn / stream boundary helpers.
2
2
 
3
- ``turn_id`` correlates a user turn across status, events, and terminals.
4
- ``seq`` is a daemon-assigned monotonic counter per loop for drop-stale filtering.
3
+ Used by ``DaemonSession`` and ``WebSocketClient`` for ``turn_id`` / ``seq``
4
+ filtering. Not part of the community public API import only if you are
5
+ extending stream handling.
5
6
  """
6
7
 
7
8
  from __future__ import annotations
@@ -141,6 +141,7 @@ _AUTOPilot_COMMANDS = {
141
141
  "list_goals": "autopilot_list_goals",
142
142
  "get_goal": "autopilot_get_goal",
143
143
  "cancel_goal": "autopilot_cancel_goal",
144
+ "cancel_all": "autopilot_cancel_all",
144
145
  "wake": "autopilot_wake",
145
146
  "dream": "autopilot_dream",
146
147
  "resume": "autopilot_resume",
@@ -162,19 +163,27 @@ _MEMORY_COMMANDS = {
162
163
 
163
164
 
164
165
  class WsCommandClient:
165
- """Async WebSocket client for daemon command endpoints.
166
+ """Async client for one-shot daemon RPCs (jobs, autopilot, cron).
166
167
 
167
- Connects to daemon WebSocket, sends command messages, and waits for
168
- response messages with a request/response pattern.
168
+ Prefer this for asyncio apps. For scripts/CLI without an event loop, use
169
+ ``SyncWsCommandClient`` (alias ``CommandClient``). For streaming agent
170
+ turns, use ``DaemonSession`` instead.
169
171
 
170
- Usage:
171
- client = WsCommandClient(ws_url)
172
- result = await client.autopilot_status()
173
- result = await client.cron_add("in 1 hour remind me to deploy")
172
+ Preferred community alias: ``AsyncCommandClient``.
173
+
174
+ Job lifecycle uses ``job_*`` methods. Autopilot goal helpers use
175
+ ``autopilot_*``. ``job_cancel`` cancels a root job and its descendants;
176
+ ``autopilot_cancel_all`` cancels every open goal in one call.
177
+
178
+ Example::
179
+
180
+ client = WsCommandClient("ws://127.0.0.1:8765")
181
+ created = await client.job_create("Summarize the README")
182
+ await client.job_cancel(created["job_id"])
174
183
 
175
184
  Args:
176
- ws_url: WebSocket URL (e.g. ``ws://127.0.0.1:8765``).
177
- timeout: Command timeout in seconds.
185
+ ws_url: Daemon WebSocket URL (e.g. ``ws://127.0.0.1:8765``).
186
+ timeout: Per-command timeout in seconds.
178
187
  """
179
188
 
180
189
  def __init__(self, ws_url: str, *, timeout: float = 30.0) -> None:
@@ -259,35 +268,43 @@ class WsCommandClient:
259
268
  except Exception as exc:
260
269
  raise RuntimeError(f"Command failed: {exc}") from exc
261
270
 
262
- # Autopilot commands
271
+ # --- Autopilot goals -------------------------------------------------
263
272
 
264
273
  async def autopilot_status(self) -> dict[str, Any]:
265
- """Get autopilot status."""
274
+ """Return autopilot scheduler status (running / dreaming / pool)."""
266
275
  return await self._send_command("autopilot_status")
267
276
 
268
277
  async def autopilot_submit(
269
278
  self, description: str, *, priority: int = 50, workspace: str | None = None
270
279
  ) -> dict[str, Any]:
271
- """Submit a new autopilot task."""
280
+ """Submit a new autopilot goal (returns ``goal_id``)."""
272
281
  payload: dict[str, Any] = {"description": description, "priority": priority}
273
282
  if workspace:
274
283
  payload["workspace"] = workspace
275
284
  return await self._send_command("autopilot_submit", payload)
276
285
 
277
286
  async def autopilot_list_goals(self) -> dict[str, Any]:
278
- """List all goals."""
287
+ """List all goals (including non-root children)."""
279
288
  return await self._send_command("autopilot_list_goals")
280
289
 
281
290
  async def autopilot_get_goal(self, goal_id: str) -> dict[str, Any]:
282
- """Get goal details."""
291
+ """Fetch one goal by id."""
283
292
  return await self._send_command("autopilot_get_goal", {"goal_id": goal_id})
284
293
 
285
294
  async def autopilot_cancel_goal(self, goal_id: str) -> dict[str, Any]:
286
- """Cancel a goal."""
295
+ """Cancel a goal and its non-terminal descendants."""
287
296
  return await self._send_command("autopilot_cancel_goal", {"goal_id": goal_id})
288
297
 
298
+ async def autopilot_cancel_all(self) -> dict[str, Any]:
299
+ """Cancel every open (non-terminal) goal in one call.
300
+
301
+ Prefer ``job_cancel(job_id)`` when you know the job root. Use this when
302
+ you need a bulk stop of leftover pending/active goals.
303
+ """
304
+ return await self._send_command("autopilot_cancel_all")
305
+
289
306
  async def autopilot_wake(self) -> dict[str, Any]:
290
- """Exit dreaming mode."""
307
+ """Exit dreaming mode and resume scheduling."""
291
308
  return await self._send_command("autopilot_wake")
292
309
 
293
310
  async def autopilot_dream(self) -> dict[str, Any]:
@@ -295,26 +312,26 @@ class WsCommandClient:
295
312
  return await self._send_command("autopilot_dream")
296
313
 
297
314
  async def autopilot_resume(self, goal_id: str) -> dict[str, Any]:
298
- """Resume a suspended/blocked goal."""
315
+ """Resume a suspended or blocked goal."""
299
316
  return await self._send_command("autopilot_resume", {"goal_id": goal_id})
300
317
 
301
318
  async def autopilot_list_jobs(self) -> dict[str, Any]:
302
- """List root goals (jobs) only."""
319
+ """List root goals only (jobs). Prefer ``job_*`` for job control."""
303
320
  return await self._send_command("autopilot_list_jobs")
304
321
 
305
322
  async def autopilot_get_job(self, job_id: str) -> dict[str, Any]:
306
- """Get job status with DAG snapshot."""
323
+ """Get a root job with DAG snapshot. Prefer ``job_status`` / ``job_dag``."""
307
324
  return await self._send_command("autopilot_get_job", {"job_id": job_id})
308
325
 
309
326
  async def autopilot_subscribe(self) -> dict[str, Any]:
310
- """Subscribe to autopilot worker events."""
327
+ """Subscribe this connection to autopilot worker events."""
311
328
  return await self._send_command("autopilot_subscribe")
312
329
 
313
330
  async def autopilot_unsubscribe(self) -> dict[str, Any]:
314
331
  """Unsubscribe from autopilot worker events."""
315
332
  return await self._send_command("autopilot_unsubscribe")
316
333
 
317
- # canonical job commands (recommended)
334
+ # --- Jobs (preferred for job lifecycle) ------------------------------
318
335
 
319
336
  async def job_create(
320
337
  self,
@@ -325,18 +342,7 @@ class WsCommandClient:
325
342
  max_iterations: int | None = None,
326
343
  guidance: str | None = None,
327
344
  ) -> dict[str, Any]:
328
- """Create a new autopilot job.
329
-
330
- Args:
331
- goal: Job goal text (required).
332
- workspace: Optional workspace path.
333
- autonomous: Whether to run autonomously.
334
- max_iterations: Optional iteration limit.
335
- guidance: Optional initial guidance.
336
-
337
- Returns:
338
- Dict with job_id and status.
339
- """
345
+ """Create a background job (root goal). Returns ``job_id`` + status."""
340
346
  payload: dict[str, Any] = {"goal": goal}
341
347
  if workspace:
342
348
  payload["workspace"] = workspace
@@ -349,79 +355,35 @@ class WsCommandClient:
349
355
  return await self._send_command("job_create", payload)
350
356
 
351
357
  async def job_status(self, job_id: str) -> dict[str, Any]:
352
- """Get job status with goal counts and workers.
353
-
354
- Args:
355
- job_id: Job identifier.
356
-
357
- Returns:
358
- Dict with job_id, status, active_goals, completed_goals, workers.
359
- """
358
+ """Get job status with goal counts and workers."""
360
359
  return await self._send_command("job_status", {"job_id": job_id})
361
360
 
362
361
  async def job_pause(self, job_id: str) -> dict[str, Any]:
363
- """Pause a running job.
364
-
365
- Args:
366
- job_id: Job identifier.
367
-
368
- Returns:
369
- Dict with job_id and status="suspended".
370
- """
362
+ """Pause a running job (status becomes suspended)."""
371
363
  return await self._send_command("job_pause", {"job_id": job_id})
372
364
 
373
365
  async def job_resume(self, job_id: str) -> dict[str, Any]:
374
- """Resume a paused job.
375
-
376
- Args:
377
- job_id: Job identifier.
378
-
379
- Returns:
380
- Dict with job_id and status="pending".
381
- """
366
+ """Resume a paused job."""
382
367
  return await self._send_command("job_resume", {"job_id": job_id})
383
368
 
384
369
  async def job_cancel(self, job_id: str) -> dict[str, Any]:
385
- """Cancel a job.
386
-
387
- Args:
388
- job_id: Job identifier.
389
-
390
- Returns:
391
- Dict with job_id and status="cancelled".
392
- """
370
+ """Cancel a job root and all non-terminal descendants."""
393
371
  return await self._send_command("job_cancel", {"job_id": job_id})
394
372
 
395
373
  async def job_dag(self, job_id: str) -> dict[str, Any]:
396
- """Get job DAG snapshot for visualization.
397
-
398
- Args:
399
- job_id: Job identifier.
400
-
401
- Returns:
402
- Dict with job_id and dag (nodes/edges).
403
- """
374
+ """Get the job goal DAG (nodes/edges) for visualization."""
404
375
  return await self._send_command("job_dag", {"job_id": job_id})
405
376
 
406
377
  async def job_guidance(
407
378
  self, job_id: str, content: str, *, goal_id: str | None = None
408
379
  ) -> dict[str, Any]:
409
- """Send guidance to a job or specific goal.
410
-
411
- Args:
412
- job_id: Job identifier.
413
- content: Guidance text.
414
- goal_id: Optional specific goal to target.
415
-
416
- Returns:
417
- Dict with job_id, goal_id, absorbed.
418
- """
380
+ """Send guidance text to a job or a specific goal under it."""
419
381
  payload: dict[str, Any] = {"job_id": job_id, "content": content}
420
382
  if goal_id:
421
383
  payload["goal_id"] = goal_id
422
384
  return await self._send_command("job_guidance", payload)
423
385
 
424
- # Cron commands
386
+ # --- Cron ------------------------------------------------------------
425
387
 
426
388
  async def cron_add(self, text: str, *, priority: int | None = None) -> dict[str, Any]:
427
389
  """Submit a natural-language scheduled job."""
@@ -432,75 +394,63 @@ class WsCommandClient:
432
394
  return _normalize_cron_add_result(result)
433
395
 
434
396
  async def cron_list(self, *, status: str | None = None) -> dict[str, Any]:
435
- """List scheduled jobs.
436
-
437
- Sends the ``cron_list`` method, which the daemon routes to
438
- ``_handle_cron_list``. The former ``cron_list_jobs`` method name is
439
- deprecated; it sent a method the daemon did not handle.
440
-
441
- Args:
442
- status: Optional status filter.
443
-
444
- Returns:
445
- Dict with jobs list.
446
- """
397
+ """List scheduled cron jobs (optional ``status`` filter)."""
447
398
  payload: dict[str, Any] = {}
448
399
  if status:
449
400
  payload["status"] = status
450
401
  return await self._send_command("cron_list", payload)
451
402
 
452
403
  async def cron_show(self, job_id: str) -> dict[str, Any]:
453
- """Get job details."""
404
+ """Get details for one cron job."""
454
405
  result = await self._send_command("cron_show", {"job_id": job_id})
455
406
  return _normalize_cron_show_result(result)
456
407
 
457
408
  async def cron_cancel(self, job_id: str) -> dict[str, Any]:
458
- """Cancel a scheduled job."""
409
+ """Cancel a scheduled cron job."""
459
410
  return await self._send_command("cron_cancel", {"job_id": job_id})
460
411
 
461
- # Memory commands
412
+ # --- Memory (admin) --------------------------------------------------
462
413
 
463
414
  async def memory_stats(self, mode: str = "daemon") -> dict[str, Any]:
464
- """Query daemon memory profiling stats."""
415
+ """Query daemon memory profiling stats (admin)."""
465
416
  return await self._send_command("memory_stats", {"mode": mode})
466
417
 
467
418
 
468
419
  class SyncWsCommandClient:
469
- """Synchronous wrapper for WsCommandClient.
420
+ """Synchronous wrapper around ``WsCommandClient`` for scripts and CLI.
470
421
 
471
- Provides a synchronous interface for CLI commands that need to call
472
- daemon endpoints without async context.
422
+ Preferred community alias: ``CommandClient``. Each method opens a short
423
+ WebSocket RPC (same behavior as the async client).
473
424
 
474
425
  Args:
475
- ws_url: WebSocket URL.
476
- timeout: Command timeout in seconds.
426
+ ws_url: Daemon WebSocket URL.
427
+ timeout: Per-command timeout in seconds.
477
428
  """
478
429
 
479
430
  def __init__(self, ws_url: str, *, timeout: float = 30.0) -> None:
480
431
  self._client = WsCommandClient(ws_url, timeout=timeout)
481
432
 
482
433
  def _run_async(self, coro: Any) -> Any:
483
- """Run async coroutine in sync context."""
434
+ """Run an async coroutine from sync code."""
484
435
  try:
485
436
  loop = asyncio.get_running_loop()
486
437
  except RuntimeError:
487
438
  loop = None
488
439
 
489
440
  if loop is not None:
490
- # Already in async context - create task
491
441
  return asyncio.ensure_future(coro)
492
- else:
493
- # Not in async context - run in new loop
494
- return asyncio.run(coro)
442
+ return asyncio.run(coro)
443
+
444
+ # --- Autopilot goals -------------------------------------------------
495
445
 
496
446
  def autopilot_status(self) -> dict[str, Any]:
497
- """Get autopilot status (sync)."""
447
+ """Return autopilot scheduler status."""
498
448
  return cast(dict[str, Any], self._run_async(self._client.autopilot_status()))
499
449
 
500
450
  def autopilot_submit(
501
451
  self, description: str, *, priority: int = 50, workspace: str | None = None
502
452
  ) -> dict[str, Any]:
503
- """Submit a new autopilot task (sync)."""
453
+ """Submit a new autopilot goal."""
504
454
  return cast(
505
455
  dict[str, Any],
506
456
  self._run_async(
@@ -509,47 +459,50 @@ class SyncWsCommandClient:
509
459
  )
510
460
 
511
461
  def autopilot_list_goals(self) -> dict[str, Any]:
512
- """List all goals (sync)."""
462
+ """List all goals."""
513
463
  return cast(dict[str, Any], self._run_async(self._client.autopilot_list_goals()))
514
464
 
515
465
  def autopilot_get_goal(self, goal_id: str) -> dict[str, Any]:
516
- """Get goal details (sync)."""
466
+ """Fetch one goal by id."""
517
467
  return cast(dict[str, Any], self._run_async(self._client.autopilot_get_goal(goal_id)))
518
468
 
519
469
  def autopilot_cancel_goal(self, goal_id: str) -> dict[str, Any]:
520
- """Cancel a goal (sync)."""
470
+ """Cancel a goal and its non-terminal descendants."""
521
471
  return cast(dict[str, Any], self._run_async(self._client.autopilot_cancel_goal(goal_id)))
522
472
 
473
+ def autopilot_cancel_all(self) -> dict[str, Any]:
474
+ """Cancel every open (non-terminal) goal."""
475
+ return cast(dict[str, Any], self._run_async(self._client.autopilot_cancel_all()))
476
+
523
477
  def autopilot_wake(self) -> dict[str, Any]:
524
- """Exit dreaming mode (sync)."""
478
+ """Exit dreaming mode."""
525
479
  return cast(dict[str, Any], self._run_async(self._client.autopilot_wake()))
526
480
 
527
481
  def autopilot_dream(self) -> dict[str, Any]:
528
- """Force dreaming mode (sync)."""
482
+ """Force dreaming mode."""
529
483
  return cast(dict[str, Any], self._run_async(self._client.autopilot_dream()))
530
484
 
531
485
  def autopilot_resume(self, goal_id: str) -> dict[str, Any]:
532
- """Resume a suspended/blocked goal (sync)."""
486
+ """Resume a suspended or blocked goal."""
533
487
  return cast(dict[str, Any], self._run_async(self._client.autopilot_resume(goal_id)))
534
488
 
535
489
  def autopilot_list_jobs(self) -> dict[str, Any]:
536
- """List root goals (jobs) only (sync)."""
490
+ """List root goals only (jobs)."""
537
491
  return cast(dict[str, Any], self._run_async(self._client.autopilot_list_jobs()))
538
492
 
539
493
  def autopilot_get_job(self, job_id: str) -> dict[str, Any]:
540
- """Get job status with DAG snapshot (sync)."""
494
+ """Get a root job with DAG snapshot."""
541
495
  return cast(dict[str, Any], self._run_async(self._client.autopilot_get_job(job_id)))
542
496
 
543
- def job_pause(self, job_id: str) -> dict[str, Any]:
544
- """Pause a running autopilot job (sync)."""
545
- return cast(dict[str, Any], self._run_async(self._client.job_pause(job_id)))
497
+ def autopilot_subscribe(self) -> dict[str, Any]:
498
+ """Subscribe to autopilot worker events."""
499
+ return cast(dict[str, Any], self._run_async(self._client.autopilot_subscribe()))
546
500
 
547
- def job_guidance(self, job_id: str, text: str, *, goal_id: str | None = None) -> dict[str, Any]:
548
- """Send guidance to an autopilot job or specific goal (sync)."""
549
- return cast(
550
- dict[str, Any],
551
- self._run_async(self._client.job_guidance(job_id, text, goal_id=goal_id)),
552
- )
501
+ def autopilot_unsubscribe(self) -> dict[str, Any]:
502
+ """Unsubscribe from autopilot worker events."""
503
+ return cast(dict[str, Any], self._run_async(self._client.autopilot_unsubscribe()))
504
+
505
+ # --- Jobs ------------------------------------------------------------
553
506
 
554
507
  def job_create(
555
508
  self,
@@ -560,7 +513,7 @@ class SyncWsCommandClient:
560
513
  max_iterations: int | None = None,
561
514
  guidance: str | None = None,
562
515
  ) -> dict[str, Any]:
563
- """Create a new autopilot job."""
516
+ """Create a background job (root goal)."""
564
517
  return cast(
565
518
  dict[str, Any],
566
519
  self._run_async(
@@ -578,68 +531,69 @@ class SyncWsCommandClient:
578
531
  """Get job status with goal counts and workers."""
579
532
  return cast(dict[str, Any], self._run_async(self._client.job_status(job_id)))
580
533
 
534
+ def job_pause(self, job_id: str) -> dict[str, Any]:
535
+ """Pause a running job."""
536
+ return cast(dict[str, Any], self._run_async(self._client.job_pause(job_id)))
537
+
581
538
  def job_resume(self, job_id: str) -> dict[str, Any]:
582
- """Resume a paused autopilot job."""
539
+ """Resume a paused job."""
583
540
  return cast(dict[str, Any], self._run_async(self._client.job_resume(job_id)))
584
541
 
585
542
  def job_cancel(self, job_id: str) -> dict[str, Any]:
586
- """Cancel an autopilot job."""
543
+ """Cancel a job root and all non-terminal descendants."""
587
544
  return cast(dict[str, Any], self._run_async(self._client.job_cancel(job_id)))
588
545
 
589
546
  def job_dag(self, job_id: str) -> dict[str, Any]:
590
- """Get job DAG snapshot for visualization."""
547
+ """Get the job goal DAG for visualization."""
591
548
  return cast(dict[str, Any], self._run_async(self._client.job_dag(job_id)))
592
549
 
593
- def autopilot_subscribe(self) -> dict[str, Any]:
594
- """Subscribe to autopilot worker events (sync)."""
595
- return cast(dict[str, Any], self._run_async(self._client.autopilot_subscribe()))
550
+ def job_guidance(
551
+ self, job_id: str, content: str, *, goal_id: str | None = None
552
+ ) -> dict[str, Any]:
553
+ """Send guidance text to a job or a specific goal under it."""
554
+ return cast(
555
+ dict[str, Any],
556
+ self._run_async(self._client.job_guidance(job_id, content, goal_id=goal_id)),
557
+ )
596
558
 
597
- def autopilot_unsubscribe(self) -> dict[str, Any]:
598
- """Unsubscribe from autopilot worker events (sync)."""
599
- return cast(dict[str, Any], self._run_async(self._client.autopilot_unsubscribe()))
559
+ # --- Cron ------------------------------------------------------------
600
560
 
601
561
  def cron_add(self, text: str, *, priority: int | None = None) -> dict[str, Any]:
602
- """Submit a natural-language scheduled job (sync)."""
562
+ """Submit a natural-language scheduled job."""
603
563
  return cast(dict[str, Any], self._run_async(self._client.cron_add(text, priority=priority)))
604
564
 
605
565
  def cron_list(self, *, status: str | None = None) -> dict[str, Any]:
606
- """List scheduled jobs (sync)."""
566
+ """List scheduled cron jobs."""
607
567
  return cast(dict[str, Any], self._run_async(self._client.cron_list(status=status)))
608
568
 
609
569
  def cron_show(self, job_id: str) -> dict[str, Any]:
610
- """Get job details (sync)."""
570
+ """Get details for one cron job."""
611
571
  return cast(dict[str, Any], self._run_async(self._client.cron_show(job_id)))
612
572
 
613
573
  def cron_cancel(self, job_id: str) -> dict[str, Any]:
614
- """Cancel a scheduled job (sync)."""
574
+ """Cancel a scheduled cron job."""
615
575
  return cast(dict[str, Any], self._run_async(self._client.cron_cancel(job_id)))
616
576
 
577
+ # --- Memory (admin) --------------------------------------------------
578
+
617
579
  def memory_stats(self, mode: str = "daemon") -> dict[str, Any]:
618
- """Query daemon memory profiling stats (sync)."""
580
+ """Query daemon memory profiling stats (admin)."""
619
581
  return cast(dict[str, Any], self._run_async(self._client.memory_stats(mode)))
620
582
 
621
583
 
622
584
  def ws_command_client_from_config(cfg: Any) -> SyncWsCommandClient:
623
- """Build a WebSocket command client from CLI or soothe config.
585
+ """Build a sync command client from soothe/CLI config (host/port).
624
586
 
625
- Args:
626
- cfg: CLI, daemon, or soothe config exposing websocket host/port.
627
-
628
- Returns:
629
- SyncWsCommandClient instance.
587
+ Preferred alias: ``command_client_from_config``.
630
588
  """
631
589
  ws_url = websocket_url_from_config(cfg)
632
590
  return SyncWsCommandClient(ws_url)
633
591
 
634
592
 
635
593
  def async_ws_command_client_from_config(cfg: Any) -> WsCommandClient:
636
- """Build an async WebSocket command client from config.
637
-
638
- Args:
639
- cfg: CLI, daemon, or soothe config exposing websocket host/port.
594
+ """Build an async command client from soothe/CLI config (host/port).
640
595
 
641
- Returns:
642
- WsCommandClient instance.
596
+ Preferred alias: ``async_command_client_from_config``.
643
597
  """
644
598
  ws_url = websocket_url_from_config(cfg)
645
599
  return WsCommandClient(ws_url)
@@ -648,6 +602,17 @@ def async_ws_command_client_from_config(cfg: Any) -> WsCommandClient:
648
602
  __all__ = [
649
603
  "WsCommandClient",
650
604
  "SyncWsCommandClient",
605
+ "AsyncCommandClient",
606
+ "CommandClient",
651
607
  "ws_command_client_from_config",
652
608
  "async_ws_command_client_from_config",
609
+ "command_client_from_config",
610
+ "async_command_client_from_config",
653
611
  ]
612
+
613
+
614
+ # Preferred community names (aliases; wire method names unchanged).
615
+ AsyncCommandClient = WsCommandClient
616
+ CommandClient = SyncWsCommandClient
617
+ command_client_from_config = ws_command_client_from_config
618
+ async_command_client_from_config = async_ws_command_client_from_config
@@ -1,194 +0,0 @@
1
- """Soothe WebSocket client for talking to a running soothe-daemon.
2
-
3
- Transport helpers (connect, loops, RPCs) live here. Higher-level session
4
- and multi-user turn tools live in ``soothe_client.appkit``.
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- import importlib.metadata
10
-
11
- from soothe_sdk.core.types import VerbosityLevel
12
- from soothe_sdk.wire.codec import ProtocolError
13
-
14
- from soothe_client.errors import (
15
- DaemonError,
16
- DisconnectCause,
17
- ReconnectError,
18
- StaleLoopError,
19
- disconnect_cause_name,
20
- )
21
- from soothe_client.helpers import (
22
- check_daemon_status,
23
- connected_websocket,
24
- fetch_config_section,
25
- fetch_loop_cards,
26
- fetch_loop_history,
27
- fetch_loop_messages,
28
- fetch_skills_catalog,
29
- is_daemon_live,
30
- protocol1_rpc,
31
- request_daemon_config_reload,
32
- request_daemon_shutdown,
33
- websocket_url_from_config,
34
- )
35
- from soothe_client.intent_hints import (
36
- DEFAULT_DELIVERABLE_PHASES,
37
- EMBED,
38
- IMAGE_TO_TEXT,
39
- OCR,
40
- TEXT_COMPLETION,
41
- validate_loop_input_intent_hint,
42
- )
43
- from soothe_client.protocol_params import (
44
- AuthParams,
45
- AuthRefreshParams,
46
- AutopilotCancelGoalParams,
47
- AutopilotDreamParams,
48
- AutopilotGetGoalParams,
49
- AutopilotGetJobParams,
50
- AutopilotListGoalsParams,
51
- AutopilotListJobsParams,
52
- AutopilotResumeParams,
53
- AutopilotStatusParams,
54
- AutopilotSubmitParams,
55
- AutopilotSubscribeParams,
56
- AutopilotWakeParams,
57
- ConfigGetParams,
58
- ConfigReloadParams,
59
- CronAddParams,
60
- CronCancelParams,
61
- CronListParams,
62
- CronShowParams,
63
- DaemonShutdownParams,
64
- DaemonStatusParams,
65
- DisconnectParams,
66
- InvokeSkillParams,
67
- JobCancelParams,
68
- JobCreateParams,
69
- JobDagParams,
70
- JobGuidanceParams,
71
- JobPauseParams,
72
- JobResumeParams,
73
- JobStatusParams,
74
- LoopCardsFetchParams,
75
- LoopDeleteParams,
76
- LoopDetachParams,
77
- LoopGetParams,
78
- LoopInputParams,
79
- LoopListParams,
80
- LoopMessagesParams,
81
- LoopNewParams,
82
- LoopPruneParams,
83
- LoopReattachParams,
84
- LoopStateGetParams,
85
- LoopStateUpdateParams,
86
- LoopTreeParams,
87
- McpStatusParams,
88
- ModelsListParams,
89
- RpcCommandParams,
90
- SkillsListParams,
91
- SlashCommandParams,
92
- SubscribeParams,
93
- )
94
- from soothe_client.session import (
95
- bootstrap_loop_session,
96
- connect_websocket_with_retries,
97
- )
98
- from soothe_client.websocket import WebSocketClient
99
- from soothe_client.ws_command_client import (
100
- SyncWsCommandClient,
101
- WsCommandClient,
102
- async_ws_command_client_from_config,
103
- ws_command_client_from_config,
104
- )
105
-
106
- try:
107
- __version__ = importlib.metadata.version("soothe-client-python")
108
- except importlib.metadata.PackageNotFoundError:
109
- __version__ = "0.0.0"
110
-
111
- __all__ = [
112
- "__version__",
113
- "WebSocketClient",
114
- "VerbosityLevel",
115
- "ProtocolError",
116
- "DaemonError",
117
- "DisconnectCause",
118
- "ReconnectError",
119
- "StaleLoopError",
120
- "disconnect_cause_name",
121
- "WsCommandClient",
122
- "SyncWsCommandClient",
123
- "ws_command_client_from_config",
124
- "async_ws_command_client_from_config",
125
- "bootstrap_loop_session",
126
- "connect_websocket_with_retries",
127
- "websocket_url_from_config",
128
- "connected_websocket",
129
- "protocol1_rpc",
130
- "check_daemon_status",
131
- "is_daemon_live",
132
- "request_daemon_config_reload",
133
- "request_daemon_shutdown",
134
- "fetch_skills_catalog",
135
- "fetch_config_section",
136
- "fetch_loop_history",
137
- "fetch_loop_cards",
138
- "fetch_loop_messages",
139
- "TEXT_COMPLETION",
140
- "IMAGE_TO_TEXT",
141
- "OCR",
142
- "EMBED",
143
- "DEFAULT_DELIVERABLE_PHASES",
144
- "validate_loop_input_intent_hint",
145
- "LoopGetParams",
146
- "LoopListParams",
147
- "LoopTreeParams",
148
- "LoopPruneParams",
149
- "LoopDeleteParams",
150
- "LoopNewParams",
151
- "LoopReattachParams",
152
- "LoopInputParams",
153
- "LoopMessagesParams",
154
- "LoopStateGetParams",
155
- "LoopStateUpdateParams",
156
- "LoopCardsFetchParams",
157
- "LoopDetachParams",
158
- "SubscribeParams",
159
- "AutopilotSubscribeParams",
160
- "AutopilotStatusParams",
161
- "AutopilotSubmitParams",
162
- "AutopilotListGoalsParams",
163
- "AutopilotGetGoalParams",
164
- "AutopilotCancelGoalParams",
165
- "AutopilotWakeParams",
166
- "AutopilotDreamParams",
167
- "AutopilotResumeParams",
168
- "AutopilotListJobsParams",
169
- "AutopilotGetJobParams",
170
- "JobCreateParams",
171
- "JobStatusParams",
172
- "JobPauseParams",
173
- "JobResumeParams",
174
- "JobCancelParams",
175
- "JobDagParams",
176
- "JobGuidanceParams",
177
- "CronAddParams",
178
- "CronListParams",
179
- "CronShowParams",
180
- "CronCancelParams",
181
- "DaemonStatusParams",
182
- "DaemonShutdownParams",
183
- "ConfigGetParams",
184
- "ConfigReloadParams",
185
- "SkillsListParams",
186
- "ModelsListParams",
187
- "InvokeSkillParams",
188
- "McpStatusParams",
189
- "AuthParams",
190
- "AuthRefreshParams",
191
- "SlashCommandParams",
192
- "RpcCommandParams",
193
- "DisconnectParams",
194
- ]