soothe-client-python 0.9.4__tar.gz → 0.9.6__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.
- soothe_client_python-0.9.6/PKG-INFO +85 -0
- soothe_client_python-0.9.6/README.md +52 -0
- {soothe_client_python-0.9.4 → soothe_client_python-0.9.6}/pyproject.toml +6 -5
- {soothe_client_python-0.9.4 → soothe_client_python-0.9.6}/src/soothe_client/__init__.py +27 -4
- {soothe_client_python-0.9.4 → soothe_client_python-0.9.6}/src/soothe_client/appkit/__init__.py +7 -10
- {soothe_client_python-0.9.4 → soothe_client_python-0.9.6}/src/soothe_client/appkit/attachments.py +4 -7
- {soothe_client_python-0.9.4 → soothe_client_python-0.9.6}/src/soothe_client/appkit/broadcaster.py +1 -1
- {soothe_client_python-0.9.4 → soothe_client_python-0.9.6}/src/soothe_client/appkit/classifier.py +1 -1
- {soothe_client_python-0.9.4 → soothe_client_python-0.9.6}/src/soothe_client/appkit/daemon_session.py +40 -8
- {soothe_client_python-0.9.4 → soothe_client_python-0.9.6}/src/soothe_client/appkit/managed_client.py +68 -6
- {soothe_client_python-0.9.4 → soothe_client_python-0.9.6}/src/soothe_client/appkit/pool.py +1 -1
- {soothe_client_python-0.9.4 → soothe_client_python-0.9.6}/src/soothe_client/appkit/query_gate.py +1 -1
- {soothe_client_python-0.9.4 → soothe_client_python-0.9.6}/src/soothe_client/appkit/session_store.py +1 -1
- {soothe_client_python-0.9.4 → soothe_client_python-0.9.6}/src/soothe_client/appkit/thinking_step.py +1 -1
- {soothe_client_python-0.9.4 → soothe_client_python-0.9.6}/src/soothe_client/appkit/turn_runner.py +2 -2
- {soothe_client_python-0.9.4 → soothe_client_python-0.9.6}/src/soothe_client/helpers.py +93 -18
- {soothe_client_python-0.9.4 → soothe_client_python-0.9.6}/src/soothe_client/protocol_params.py +131 -67
- {soothe_client_python-0.9.4 → soothe_client_python-0.9.6}/src/soothe_client/schemas.py +3 -9
- {soothe_client_python-0.9.4 → soothe_client_python-0.9.6}/src/soothe_client/session.py +6 -7
- {soothe_client_python-0.9.4 → soothe_client_python-0.9.6}/src/soothe_client/websocket.py +53 -45
- {soothe_client_python-0.9.4 → soothe_client_python-0.9.6}/src/soothe_client/ws_command_client.py +29 -21
- soothe_client_python-0.9.4/PKG-INFO +0 -131
- soothe_client_python-0.9.4/README.md +0 -97
- {soothe_client_python-0.9.4 → soothe_client_python-0.9.6}/.gitignore +0 -0
- {soothe_client_python-0.9.4 → soothe_client_python-0.9.6}/LICENSE +0 -0
- {soothe_client_python-0.9.4 → soothe_client_python-0.9.6}/src/soothe_client/appkit/chunk_filter.py +0 -0
- {soothe_client_python-0.9.4 → soothe_client_python-0.9.6}/src/soothe_client/appkit/events.py +0 -0
- {soothe_client_python-0.9.4 → soothe_client_python-0.9.6}/src/soothe_client/appkit/observability.py +0 -0
- {soothe_client_python-0.9.4 → soothe_client_python-0.9.6}/src/soothe_client/appkit/turn.py +0 -0
- {soothe_client_python-0.9.4 → soothe_client_python-0.9.6}/src/soothe_client/errors.py +0 -0
- {soothe_client_python-0.9.4 → soothe_client_python-0.9.6}/src/soothe_client/intent_hints.py +0 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: soothe-client-python
|
|
3
|
+
Version: 0.9.6
|
|
4
|
+
Summary: WebSocket client + appkit for soothe-daemon (Python)
|
|
5
|
+
Project-URL: Homepage, https://github.com/mirasoth/soothe-client-python
|
|
6
|
+
Project-URL: Documentation, https://soothe.readthedocs.io
|
|
7
|
+
Project-URL: Repository, https://github.com/mirasoth/soothe-client-python
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: agents,ai,client,soothe,websocket
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: <3.15,>=3.11
|
|
22
|
+
Requires-Dist: pillow<12.0.0,>=10.0.0
|
|
23
|
+
Requires-Dist: pydantic<3.0.0,>=2.0.0
|
|
24
|
+
Requires-Dist: soothe-sdk<1.0.0,>=0.8.1
|
|
25
|
+
Requires-Dist: websockets>=12.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: mypy>=1.0.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-asyncio>=1.3.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: ruff>=0.12.0; extra == 'dev'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# soothe-client-python
|
|
35
|
+
|
|
36
|
+
Talk to a running **soothe-daemon** over WebSocket — send prompts, stream agent
|
|
37
|
+
turns, run jobs.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install soothe-client-python
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Requires a local daemon (default `ws://127.0.0.1:8765`).
|
|
44
|
+
|
|
45
|
+
## Quick start
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
import asyncio
|
|
49
|
+
from soothe_client.appkit import DaemonSession
|
|
50
|
+
|
|
51
|
+
async def main() -> None:
|
|
52
|
+
session = DaemonSession("ws://127.0.0.1:8765")
|
|
53
|
+
await session.connect()
|
|
54
|
+
await session.send_turn("Summarize this in one sentence: agents need tools.")
|
|
55
|
+
async for _namespace, mode, data in session.iter_turn_chunks():
|
|
56
|
+
if mode == "custom" and isinstance(data, dict):
|
|
57
|
+
text = data.get("content") or data.get("text")
|
|
58
|
+
if text:
|
|
59
|
+
print(text, end="", flush=True)
|
|
60
|
+
print()
|
|
61
|
+
await session.close()
|
|
62
|
+
|
|
63
|
+
asyncio.run(main())
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
More patterns: [`examples/`](examples/) (hello → streaming → multi-turn → pool → jobs).
|
|
67
|
+
|
|
68
|
+
## What you get
|
|
69
|
+
|
|
70
|
+
| Need | Use |
|
|
71
|
+
|------|-----|
|
|
72
|
+
| One conversation, stream replies | `DaemonSession` |
|
|
73
|
+
| Raw WebSocket / custom RPCs | `WebSocketClient` |
|
|
74
|
+
| Many users / HTTP backend | `ConnectionPool` + `TurnRunner` |
|
|
75
|
+
| Jobs / autopilot / cron | `WsCommandClient` |
|
|
76
|
+
|
|
77
|
+
## Develop
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
make sync-dev
|
|
81
|
+
make check # lint + unit tests
|
|
82
|
+
make test-examples-offline # offline appkit examples
|
|
83
|
+
make test-examples # live 01–06 (needs soothed)
|
|
84
|
+
make test-integration # live integration suite (needs soothed)
|
|
85
|
+
```
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# soothe-client-python
|
|
2
|
+
|
|
3
|
+
Talk to a running **soothe-daemon** over WebSocket — send prompts, stream agent
|
|
4
|
+
turns, run jobs.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pip install soothe-client-python
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Requires a local daemon (default `ws://127.0.0.1:8765`).
|
|
11
|
+
|
|
12
|
+
## Quick start
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
import asyncio
|
|
16
|
+
from soothe_client.appkit import DaemonSession
|
|
17
|
+
|
|
18
|
+
async def main() -> None:
|
|
19
|
+
session = DaemonSession("ws://127.0.0.1:8765")
|
|
20
|
+
await session.connect()
|
|
21
|
+
await session.send_turn("Summarize this in one sentence: agents need tools.")
|
|
22
|
+
async for _namespace, mode, data in session.iter_turn_chunks():
|
|
23
|
+
if mode == "custom" and isinstance(data, dict):
|
|
24
|
+
text = data.get("content") or data.get("text")
|
|
25
|
+
if text:
|
|
26
|
+
print(text, end="", flush=True)
|
|
27
|
+
print()
|
|
28
|
+
await session.close()
|
|
29
|
+
|
|
30
|
+
asyncio.run(main())
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
More patterns: [`examples/`](examples/) (hello → streaming → multi-turn → pool → jobs).
|
|
34
|
+
|
|
35
|
+
## What you get
|
|
36
|
+
|
|
37
|
+
| Need | Use |
|
|
38
|
+
|------|-----|
|
|
39
|
+
| One conversation, stream replies | `DaemonSession` |
|
|
40
|
+
| Raw WebSocket / custom RPCs | `WebSocketClient` |
|
|
41
|
+
| Many users / HTTP backend | `ConnectionPool` + `TurnRunner` |
|
|
42
|
+
| Jobs / autopilot / cron | `WsCommandClient` |
|
|
43
|
+
|
|
44
|
+
## Develop
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
make sync-dev
|
|
48
|
+
make check # lint + unit tests
|
|
49
|
+
make test-examples-offline # offline appkit examples
|
|
50
|
+
make test-examples # live 01–06 (needs soothed)
|
|
51
|
+
make test-integration # live integration suite (needs soothed)
|
|
52
|
+
```
|
|
@@ -23,15 +23,13 @@ classifiers = [
|
|
|
23
23
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
24
24
|
]
|
|
25
25
|
dependencies = [
|
|
26
|
-
"soothe-sdk>=0.
|
|
26
|
+
"soothe-sdk>=0.8.1,<1.0.0",
|
|
27
27
|
"pydantic>=2.0.0,<3.0.0",
|
|
28
28
|
"websockets>=12.0",
|
|
29
|
+
"pillow>=10.0.0,<12.0.0",
|
|
29
30
|
]
|
|
30
31
|
|
|
31
32
|
[project.optional-dependencies]
|
|
32
|
-
image = [
|
|
33
|
-
"pillow>=10.0.0",
|
|
34
|
-
]
|
|
35
33
|
dev = [
|
|
36
34
|
"pytest>=8.0.0",
|
|
37
35
|
"pytest-asyncio>=1.3.0",
|
|
@@ -79,5 +77,8 @@ disallow_untyped_defs = true
|
|
|
79
77
|
|
|
80
78
|
[tool.pytest.ini_options]
|
|
81
79
|
asyncio_mode = "auto"
|
|
82
|
-
testpaths = ["tests", "examples"]
|
|
80
|
+
testpaths = ["tests/unit", "examples/appkit"]
|
|
83
81
|
addopts = "--import-mode=importlib"
|
|
82
|
+
markers = [
|
|
83
|
+
"integration: live soothe-daemon tests (skipped when unreachable)",
|
|
84
|
+
]
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
"""Soothe WebSocket client for soothe-daemon.
|
|
1
|
+
"""Soothe WebSocket client for talking to a running soothe-daemon.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
soothe-sdk (`soothe_sdk.wire`, `soothe_sdk.paths`).
|
|
3
|
+
Transport helpers (connect, loops, RPCs) live here. Higher-level session
|
|
4
|
+
and multi-user turn tools live in ``soothe_client.appkit``.
|
|
6
5
|
"""
|
|
7
6
|
|
|
8
7
|
from __future__ import annotations
|
|
@@ -21,12 +20,14 @@ from soothe_client.errors import (
|
|
|
21
20
|
)
|
|
22
21
|
from soothe_client.helpers import (
|
|
23
22
|
check_daemon_status,
|
|
23
|
+
connected_websocket,
|
|
24
24
|
fetch_config_section,
|
|
25
25
|
fetch_loop_cards,
|
|
26
26
|
fetch_loop_history,
|
|
27
27
|
fetch_loop_messages,
|
|
28
28
|
fetch_skills_catalog,
|
|
29
29
|
is_daemon_live,
|
|
30
|
+
protocol1_rpc,
|
|
30
31
|
request_daemon_config_reload,
|
|
31
32
|
request_daemon_shutdown,
|
|
32
33
|
websocket_url_from_config,
|
|
@@ -42,7 +43,17 @@ from soothe_client.intent_hints import (
|
|
|
42
43
|
from soothe_client.protocol_params import (
|
|
43
44
|
AuthParams,
|
|
44
45
|
AuthRefreshParams,
|
|
46
|
+
AutopilotCancelGoalParams,
|
|
47
|
+
AutopilotDreamParams,
|
|
48
|
+
AutopilotGetGoalParams,
|
|
49
|
+
AutopilotGetJobParams,
|
|
50
|
+
AutopilotListGoalsParams,
|
|
51
|
+
AutopilotListJobsParams,
|
|
52
|
+
AutopilotResumeParams,
|
|
53
|
+
AutopilotStatusParams,
|
|
54
|
+
AutopilotSubmitParams,
|
|
45
55
|
AutopilotSubscribeParams,
|
|
56
|
+
AutopilotWakeParams,
|
|
46
57
|
ConfigGetParams,
|
|
47
58
|
ConfigReloadParams,
|
|
48
59
|
CronAddParams,
|
|
@@ -114,6 +125,8 @@ __all__ = [
|
|
|
114
125
|
"bootstrap_loop_session",
|
|
115
126
|
"connect_websocket_with_retries",
|
|
116
127
|
"websocket_url_from_config",
|
|
128
|
+
"connected_websocket",
|
|
129
|
+
"protocol1_rpc",
|
|
117
130
|
"check_daemon_status",
|
|
118
131
|
"is_daemon_live",
|
|
119
132
|
"request_daemon_config_reload",
|
|
@@ -144,6 +157,16 @@ __all__ = [
|
|
|
144
157
|
"LoopDetachParams",
|
|
145
158
|
"SubscribeParams",
|
|
146
159
|
"AutopilotSubscribeParams",
|
|
160
|
+
"AutopilotStatusParams",
|
|
161
|
+
"AutopilotSubmitParams",
|
|
162
|
+
"AutopilotListGoalsParams",
|
|
163
|
+
"AutopilotGetGoalParams",
|
|
164
|
+
"AutopilotCancelGoalParams",
|
|
165
|
+
"AutopilotWakeParams",
|
|
166
|
+
"AutopilotDreamParams",
|
|
167
|
+
"AutopilotResumeParams",
|
|
168
|
+
"AutopilotListJobsParams",
|
|
169
|
+
"AutopilotGetJobParams",
|
|
147
170
|
"JobCreateParams",
|
|
148
171
|
"JobStatusParams",
|
|
149
172
|
"JobPauseParams",
|
{soothe_client_python-0.9.4 → soothe_client_python-0.9.6}/src/soothe_client/appkit/__init__.py
RENAMED
|
@@ -1,15 +1,12 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Application helpers built on WebSocketClient.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
in the application (e.g. soothe-cli). Building blocks:
|
|
3
|
+
Product-agnostic building blocks for agent UIs and backends:
|
|
5
4
|
|
|
6
|
-
- ``
|
|
7
|
-
- ``
|
|
8
|
-
- ``
|
|
9
|
-
- ``
|
|
10
|
-
- ``
|
|
11
|
-
- ``SSEBroadcaster`` — drop-on-full SSE-style fan-out
|
|
12
|
-
- ``ConnectionPool`` / ``TurnRunner`` — pooled multi-session turn execution
|
|
5
|
+
- ``DaemonSession`` — dual-socket loop session + streamed turns
|
|
6
|
+
- ``ConnectionPool`` / ``TurnRunner`` — multi-session turn execution
|
|
7
|
+
- ``QueryGate`` — one-in-flight query per session
|
|
8
|
+
- ``EventClassifier`` / ``extract_thinking_step`` — stream → deliverable mapping
|
|
9
|
+
- ``SSEBroadcaster`` — drop-on-full fan-out to subscribers
|
|
13
10
|
- ``SessionStore`` — persistence seam (Protocol)
|
|
14
11
|
"""
|
|
15
12
|
|
{soothe_client_python-0.9.4 → soothe_client_python-0.9.6}/src/soothe_client/appkit/attachments.py
RENAMED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
"""Image attachment compaction for appkit
|
|
1
|
+
"""Image attachment compaction for appkit.
|
|
2
2
|
|
|
3
3
|
Downscales ``image/*`` payloads when either dimension exceeds a max size.
|
|
4
|
-
Non-images and decode failures pass through unchanged.
|
|
5
|
-
compaction is requested; without Pillow, inputs are returned unchanged.
|
|
4
|
+
Non-images and decode failures pass through unchanged.
|
|
6
5
|
"""
|
|
7
6
|
|
|
8
7
|
from __future__ import annotations
|
|
@@ -12,6 +11,8 @@ import io
|
|
|
12
11
|
from dataclasses import dataclass
|
|
13
12
|
from typing import Any
|
|
14
13
|
|
|
14
|
+
from PIL import Image
|
|
15
|
+
|
|
15
16
|
|
|
16
17
|
@dataclass(slots=True)
|
|
17
18
|
class CompactImageOptions:
|
|
@@ -49,10 +50,6 @@ def compact_image_attachment(
|
|
|
49
50
|
"""
|
|
50
51
|
if not data_b64 or not mime_type.startswith("image/"):
|
|
51
52
|
return mime_type, data_b64
|
|
52
|
-
try:
|
|
53
|
-
from PIL import Image
|
|
54
|
-
except ImportError:
|
|
55
|
-
return mime_type, data_b64
|
|
56
53
|
|
|
57
54
|
try:
|
|
58
55
|
raw = base64.b64decode(data_b64, validate=False)
|
{soothe_client_python-0.9.4 → soothe_client_python-0.9.6}/src/soothe_client/appkit/daemon_session.py
RENAMED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
"""Dual-socket daemon loop session with turn streaming
|
|
1
|
+
"""Dual-socket daemon loop session with turn streaming.
|
|
2
2
|
|
|
3
3
|
Owns a subscribed stream WebSocket plus an RPC sidecar so metadata calls do not
|
|
4
|
-
starve
|
|
5
|
-
|
|
6
|
-
originated in soothe-cli and is not yet mirrored in Go/TS TurnRunner.
|
|
4
|
+
starve loop events. ``iter_turn_chunks`` handles idle timeout, post-idle drain,
|
|
5
|
+
loop scoping, and connection-loss detection.
|
|
7
6
|
"""
|
|
8
7
|
|
|
9
8
|
from __future__ import annotations
|
|
@@ -25,7 +24,12 @@ logger = logging.getLogger(__name__)
|
|
|
25
24
|
|
|
26
25
|
DEFAULT_POST_IDLE_DRAIN_S = 0.5
|
|
27
26
|
_RPC_HANDSHAKE_TIMEOUT_S = 20.0
|
|
28
|
-
_TURN_END_CUSTOM_TYPES = frozenset(
|
|
27
|
+
_TURN_END_CUSTOM_TYPES = frozenset(
|
|
28
|
+
{
|
|
29
|
+
"soothe.cognition.strange_loop.completed",
|
|
30
|
+
"soothe.stream.end",
|
|
31
|
+
}
|
|
32
|
+
)
|
|
29
33
|
|
|
30
34
|
EarlyDropFn = Callable[[tuple[Any, ...], str, Any], bool]
|
|
31
35
|
StatsFactory = Callable[[], Any]
|
|
@@ -210,6 +214,7 @@ class DaemonSession:
|
|
|
210
214
|
clarification_mode: str | None = None,
|
|
211
215
|
clarification_answer: bool = False,
|
|
212
216
|
clarification_answers: list[str] | None = None,
|
|
217
|
+
intent_hint: str | None = None,
|
|
213
218
|
) -> None:
|
|
214
219
|
"""Send a new user turn to the daemon."""
|
|
215
220
|
if not self._loop_id:
|
|
@@ -227,6 +232,7 @@ class DaemonSession:
|
|
|
227
232
|
clarification_mode=clarification_mode,
|
|
228
233
|
clarification_answer=clarification_answer,
|
|
229
234
|
clarification_answers=clarification_answers,
|
|
235
|
+
intent_hint=intent_hint,
|
|
230
236
|
)
|
|
231
237
|
|
|
232
238
|
async def cancel_remote_query(self) -> None:
|
|
@@ -289,8 +295,19 @@ class DaemonSession:
|
|
|
289
295
|
await self._ensure_rpc_connected()
|
|
290
296
|
return await self._rpc_client.request("loop_list", {"limit": limit}, timeout=15.0)
|
|
291
297
|
|
|
292
|
-
async def iter_turn_chunks(
|
|
293
|
-
|
|
298
|
+
async def iter_turn_chunks(
|
|
299
|
+
self,
|
|
300
|
+
*,
|
|
301
|
+
max_wait_s: float | None = None,
|
|
302
|
+
) -> AsyncIterator[tuple[tuple[Any, ...], str, Any]]:
|
|
303
|
+
"""Yield ``(namespace, mode, data)`` chunks for the active daemon turn.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
max_wait_s: Optional absolute deadline for the whole turn. When set,
|
|
307
|
+
raises ``TimeoutError`` if the daemon never emits a turn-end
|
|
308
|
+
signal in time (idle after payload, stopped, stream.end, or
|
|
309
|
+
strange_loop.completed).
|
|
310
|
+
"""
|
|
294
311
|
self.turn_event_stats = self._new_turn_stats()
|
|
295
312
|
self.last_turn_end_state = None
|
|
296
313
|
self.last_turn_cancellation_seen = False
|
|
@@ -303,6 +320,9 @@ class DaemonSession:
|
|
|
303
320
|
turn_read_started = time.monotonic()
|
|
304
321
|
first_event_logged = False
|
|
305
322
|
progress_seen = False
|
|
323
|
+
absolute_deadline = (
|
|
324
|
+
time.monotonic() + max_wait_s if max_wait_s is not None and max_wait_s > 0 else None
|
|
325
|
+
)
|
|
306
326
|
peel = getattr(self._client, "peel_stale_pending_control_events", None)
|
|
307
327
|
stale_pending = peel() if callable(peel) else []
|
|
308
328
|
if stale_pending:
|
|
@@ -315,6 +335,11 @@ class DaemonSession:
|
|
|
315
335
|
async with self._read_lock:
|
|
316
336
|
try:
|
|
317
337
|
while True:
|
|
338
|
+
if absolute_deadline is not None and time.monotonic() >= absolute_deadline:
|
|
339
|
+
raise TimeoutError(
|
|
340
|
+
f"Turn timed out after {max_wait_s:.0f}s "
|
|
341
|
+
f"(loop={expected_loop_id or '?'})"
|
|
342
|
+
)
|
|
318
343
|
if not progress_seen and time.monotonic() - turn_read_started > 30.0:
|
|
319
344
|
logger.warning(
|
|
320
345
|
"No daemon stream progress after %.0fs (loop=%s, "
|
|
@@ -407,7 +432,14 @@ class DaemonSession:
|
|
|
407
432
|
if mode == "custom" and isinstance(data, dict):
|
|
408
433
|
custom_type = str(data.get("type", "")).strip()
|
|
409
434
|
if custom_type in _TURN_END_CUSTOM_TYPES:
|
|
410
|
-
|
|
435
|
+
# stream.end may be scope-tagged; treat blank / turn as end.
|
|
436
|
+
if custom_type == "soothe.stream.end":
|
|
437
|
+
scope = str(data.get("scope") or "turn").strip().lower()
|
|
438
|
+
if scope not in {"", "turn"}:
|
|
439
|
+
continue
|
|
440
|
+
self.last_turn_end_state = (
|
|
441
|
+
"stream_end" if custom_type == "soothe.stream.end" else "completed"
|
|
442
|
+
)
|
|
411
443
|
async for chunk in self._drain_stream_events_after_idle(
|
|
412
444
|
expected_loop_id=expected_loop_id,
|
|
413
445
|
):
|
{soothe_client_python-0.9.4 → soothe_client_python-0.9.6}/src/soothe_client/appkit/managed_client.py
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
"""ManagedClient
|
|
1
|
+
"""ManagedClient adapter used by ConnectionPool and TurnRunner.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Wraps ``WebSocketClient`` and upgrades flat appkit payloads
|
|
4
|
+
(``loop_input``, ``command_request``, …) to protocol-1 envelopes before send.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
@@ -18,10 +18,72 @@ BootstrapFunc = Callable[
|
|
|
18
18
|
Awaitable[str],
|
|
19
19
|
]
|
|
20
20
|
|
|
21
|
+
# Flat appkit payloads still use legacy ``{"type": "<op>", ...}`` maps (parity
|
|
22
|
+
# with Go InputMessageForLoop). The live daemon accepts protocol-1 envelopes
|
|
23
|
+
# only — coerce at the ManagedClient boundary before send.
|
|
24
|
+
_ALREADY_ENVELOPE_TYPES = frozenset(
|
|
25
|
+
{
|
|
26
|
+
"request",
|
|
27
|
+
"response",
|
|
28
|
+
"notification",
|
|
29
|
+
"subscribe",
|
|
30
|
+
"next",
|
|
31
|
+
"error",
|
|
32
|
+
"complete",
|
|
33
|
+
"unsubscribe",
|
|
34
|
+
"connection_init",
|
|
35
|
+
"connection_ack",
|
|
36
|
+
"ping",
|
|
37
|
+
"pong",
|
|
38
|
+
"receipt_response",
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
_NOTIFICATION_METHODS = frozenset({"loop_input", "slash_command", "disconnect", "delivery_ack"})
|
|
42
|
+
_REQUEST_METHOD_ALIASES = {
|
|
43
|
+
# TurnRunner cancel uses flat command_request; wire method is rpc_command.
|
|
44
|
+
"command_request": "rpc_command",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _coerce_appkit_wire_message(msg: dict[str, Any], client: WebSocketClient) -> dict[str, Any]:
|
|
49
|
+
"""Upgrade flat appkit dicts to protocol-1 envelopes when needed."""
|
|
50
|
+
from soothe_sdk.wire.codec import MessageType, WireEnvelope
|
|
51
|
+
|
|
52
|
+
msg_type = str(msg.get("type") or "")
|
|
53
|
+
if msg.get("proto") == "1" or msg_type in _ALREADY_ENVELOPE_TYPES:
|
|
54
|
+
return msg
|
|
55
|
+
|
|
56
|
+
params = {
|
|
57
|
+
key: value
|
|
58
|
+
for key, value in msg.items()
|
|
59
|
+
if key not in {"type", "proto", "method", "params", "id", "request_id"}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if msg_type in _NOTIFICATION_METHODS:
|
|
63
|
+
return WireEnvelope(
|
|
64
|
+
proto="1",
|
|
65
|
+
type=MessageType.NOTIFICATION.value,
|
|
66
|
+
method=msg_type,
|
|
67
|
+
params=params,
|
|
68
|
+
).to_wire_dict()
|
|
69
|
+
|
|
70
|
+
if msg_type in _REQUEST_METHOD_ALIASES or msg_type:
|
|
71
|
+
request_method = _REQUEST_METHOD_ALIASES.get(msg_type, msg_type)
|
|
72
|
+
req_id = str(msg.get("request_id") or msg.get("id") or client._next_request_id())
|
|
73
|
+
return WireEnvelope(
|
|
74
|
+
proto="1",
|
|
75
|
+
type=MessageType.REQUEST.value,
|
|
76
|
+
method=request_method,
|
|
77
|
+
params=params,
|
|
78
|
+
id=req_id,
|
|
79
|
+
).to_wire_dict()
|
|
80
|
+
|
|
81
|
+
return msg
|
|
82
|
+
|
|
21
83
|
|
|
22
84
|
@runtime_checkable
|
|
23
85
|
class ManagedClient(Protocol):
|
|
24
|
-
"""Subset of
|
|
86
|
+
"""Subset of WebSocketClient that ConnectionPool and TurnRunner depend on."""
|
|
25
87
|
|
|
26
88
|
async def connect(self) -> None:
|
|
27
89
|
"""Dial and handshake."""
|
|
@@ -72,7 +134,7 @@ class WebSocketManagedClient:
|
|
|
72
134
|
|
|
73
135
|
@property
|
|
74
136
|
def underlying(self) -> WebSocketClient:
|
|
75
|
-
"""Return the wrapped
|
|
137
|
+
"""Return the wrapped WebSocketClient."""
|
|
76
138
|
return self._client
|
|
77
139
|
|
|
78
140
|
async def connect(self) -> None:
|
|
@@ -89,7 +151,7 @@ class WebSocketManagedClient:
|
|
|
89
151
|
async def send_message(self, msg: Any) -> None:
|
|
90
152
|
if not isinstance(msg, dict):
|
|
91
153
|
raise TypeError("send_message expects a dict payload")
|
|
92
|
-
await self._client.send(msg)
|
|
154
|
+
await self._client.send(_coerce_appkit_wire_message(msg, self._client))
|
|
93
155
|
|
|
94
156
|
async def receive_messages(
|
|
95
157
|
self,
|
{soothe_client_python-0.9.4 → soothe_client_python-0.9.6}/src/soothe_client/appkit/turn_runner.py
RENAMED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
"""Turn runner for appkit
|
|
1
|
+
"""Turn runner for appkit.
|
|
2
2
|
|
|
3
3
|
Executes one query turn end-to-end: acquire a pooled connection, enforce
|
|
4
4
|
single-flight, send loop_input, consume the event stream, classify events,
|
|
5
5
|
resolve the deliverable, persist the reply, and broadcast completion.
|
|
6
6
|
|
|
7
7
|
Supports absolute query timeout, optional idle silence watchdog, soft-complete
|
|
8
|
-
policies
|
|
8
|
+
policies, and optional attachment compaction before send.
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|