soothe-client-python 1.0.13__tar.gz → 1.0.14__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-1.0.13 → soothe_client_python-1.0.14}/PKG-INFO +1 -1
- {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/daemon_session.py +54 -12
- {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/websocket.py +7 -2
- {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/.gitignore +0 -0
- {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/LICENSE +0 -0
- {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/README.md +0 -0
- {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/pyproject.toml +0 -0
- {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/__init__.py +0 -0
- {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/__init__.py +0 -0
- {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/attachments.py +0 -0
- {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/broadcaster.py +0 -0
- {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/card_projection.py +0 -0
- {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/chunk_filter.py +0 -0
- {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/classifier.py +0 -0
- {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/events.py +0 -0
- {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/loop_session_store.py +0 -0
- {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/managed_client.py +0 -0
- {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/observability.py +0 -0
- {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/pool.py +0 -0
- {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/query_gate.py +0 -0
- {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/thinking_step.py +0 -0
- {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/turn.py +0 -0
- {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/turn_boundary.py +0 -0
- {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/turn_runner.py +0 -0
- {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/command_client.py +0 -0
- {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/errors.py +0 -0
- {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/helpers.py +0 -0
- {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/intent_hints.py +0 -0
- {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/protocol_params.py +0 -0
- {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/py.typed +0 -0
- {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/schemas.py +0 -0
- {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/session.py +0 -0
- {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/stream_terminal.py +0 -0
- {soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/turn_boundary.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: soothe-client-python
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.14
|
|
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
|
|
@@ -396,6 +396,7 @@ class DaemonSession:
|
|
|
396
396
|
self,
|
|
397
397
|
*,
|
|
398
398
|
max_wait_s: float | None = None,
|
|
399
|
+
idle_timeout_s: float | None = None,
|
|
399
400
|
) -> AsyncIterator[tuple[tuple[Any, ...], str, Any]]:
|
|
400
401
|
"""Yield ``(namespace, mode, data)`` stream chunks until the turn ends.
|
|
401
402
|
|
|
@@ -406,6 +407,13 @@ class DaemonSession:
|
|
|
406
407
|
max_wait_s: Optional absolute deadline for the whole turn. When set,
|
|
407
408
|
raises ``TimeoutError`` if the daemon never emits a turn-end
|
|
408
409
|
signal in time.
|
|
410
|
+
idle_timeout_s: Optional hard cap on how long to wait for the *first*
|
|
411
|
+
progress event before giving up. When set and no progress is seen
|
|
412
|
+
within this many seconds, raises ``TimeoutError`` instead of the
|
|
413
|
+
default 30s-log-and-reset behavior. Used by attach-only reads
|
|
414
|
+
(``skip_daemon_send_turn``) so a stale ``live`` probe that turns
|
|
415
|
+
out to have no real follow-on turn does not spin the thinking
|
|
416
|
+
row for minutes.
|
|
409
417
|
"""
|
|
410
418
|
self._ensure_turn_floor_attrs()
|
|
411
419
|
self.turn_event_stats = self._new_turn_stats()
|
|
@@ -426,6 +434,12 @@ class DaemonSession:
|
|
|
426
434
|
turn_read_started = time.monotonic()
|
|
427
435
|
first_event_logged = False
|
|
428
436
|
progress_seen = False
|
|
437
|
+
# Wall-clock of the last *real* progress (a yielded chunk). A stale
|
|
438
|
+
# ``status=running`` frame sets ``progress_seen`` but yields nothing;
|
|
439
|
+
# for the attach-only idle timeout we need a signal that advances when
|
|
440
|
+
# the daemon actually streams content, not when it merely re-announces
|
|
441
|
+
# a (possibly stale) running status.
|
|
442
|
+
last_real_progress_at = turn_read_started
|
|
429
443
|
absolute_deadline = (
|
|
430
444
|
time.monotonic() + max_wait_s if max_wait_s is not None and max_wait_s > 0 else None
|
|
431
445
|
)
|
|
@@ -446,15 +460,31 @@ class DaemonSession:
|
|
|
446
460
|
f"Turn timed out after {max_wait_s:.0f}s "
|
|
447
461
|
f"(loop={expected_loop_id or '?'})"
|
|
448
462
|
)
|
|
449
|
-
if not progress_seen
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
463
|
+
if not progress_seen:
|
|
464
|
+
elapsed = time.monotonic() - turn_read_started
|
|
465
|
+
if elapsed > 30.0:
|
|
466
|
+
logger.warning(
|
|
467
|
+
"No daemon stream progress after %.0fs (loop=%s, "
|
|
468
|
+
"query_started=%s); check daemon sender / WebSocket reader",
|
|
469
|
+
elapsed,
|
|
470
|
+
(expected_loop_id or "?")[:16],
|
|
471
|
+
query_started,
|
|
472
|
+
)
|
|
473
|
+
turn_read_started = time.monotonic()
|
|
474
|
+
# Attach-only idle timeout: even after a stale
|
|
475
|
+
# ``status=running`` sets ``progress_seen``, no real content
|
|
476
|
+
# may ever arrive (the runner already exited). Bound the wait
|
|
477
|
+
# for the first yielded chunk so the TUI falls back instead
|
|
478
|
+
# of spinning the thinking row for minutes.
|
|
479
|
+
if (
|
|
480
|
+
idle_timeout_s is not None
|
|
481
|
+
and idle_timeout_s > 0
|
|
482
|
+
and time.monotonic() - last_real_progress_at > idle_timeout_s
|
|
483
|
+
):
|
|
484
|
+
raise TimeoutError(
|
|
485
|
+
f"No daemon stream progress within {idle_timeout_s:.0f}s "
|
|
486
|
+
f"attach window (loop={expected_loop_id or '?'})"
|
|
456
487
|
)
|
|
457
|
-
turn_read_started = time.monotonic()
|
|
458
488
|
event = await self._client.read_event()
|
|
459
489
|
if event and not first_event_logged:
|
|
460
490
|
first_event_logged = True
|
|
@@ -653,6 +683,7 @@ class DaemonSession:
|
|
|
653
683
|
continue
|
|
654
684
|
|
|
655
685
|
progress_seen = True
|
|
686
|
+
last_real_progress_at = time.monotonic()
|
|
656
687
|
if is_turn_progress_chunk(mode, data):
|
|
657
688
|
turn_progress_seen = True
|
|
658
689
|
yield (namespace, mode, data)
|
|
@@ -695,7 +726,7 @@ class DaemonSession:
|
|
|
695
726
|
"""Return skill rows from the daemon catalog."""
|
|
696
727
|
async with self._rpc_lock:
|
|
697
728
|
await self._ensure_rpc_connected()
|
|
698
|
-
response = await self._rpc_client.list_skills(timeout=15.0)
|
|
729
|
+
response = await self._rpc_client.list_skills(workspace=self._workspace, timeout=15.0)
|
|
699
730
|
skills = response.get("skills", [])
|
|
700
731
|
if not isinstance(skills, list):
|
|
701
732
|
return []
|
|
@@ -844,9 +875,10 @@ class DaemonSession:
|
|
|
844
875
|
"""Fetch execution-progress snapshot (plan, step_index, iteration, status).
|
|
845
876
|
|
|
846
877
|
Returns a namespace with ``plan``, ``step_index``, ``iteration``,
|
|
847
|
-
``status`` for the loop's bound checkpoint
|
|
848
|
-
fields degrade gracefully (``step_index=0``,
|
|
849
|
-
``status=None``, ``plan=None``)
|
|
878
|
+
``status``, and ``active_runner`` for the loop's bound checkpoint
|
|
879
|
+
thread. On RPC failure the fields degrade gracefully (``step_index=0``,
|
|
880
|
+
``iteration=0``, ``status=None``, ``plan=None``, ``active_runner=False``)
|
|
881
|
+
so a resume gate can still proceed.
|
|
850
882
|
"""
|
|
851
883
|
lid = str(loop_id or "").strip()
|
|
852
884
|
empty = SimpleNamespace(
|
|
@@ -854,6 +886,7 @@ class DaemonSession:
|
|
|
854
886
|
step_index=0,
|
|
855
887
|
iteration=0,
|
|
856
888
|
status=None,
|
|
889
|
+
active_runner=False,
|
|
857
890
|
loop_id=lid or None,
|
|
858
891
|
success=False,
|
|
859
892
|
)
|
|
@@ -881,12 +914,21 @@ class DaemonSession:
|
|
|
881
914
|
iteration = iteration_raw if isinstance(iteration_raw, int) else 0
|
|
882
915
|
status_raw = resp.get("status")
|
|
883
916
|
status = status_raw if isinstance(status_raw, str) else None
|
|
917
|
+
# ``active_runner`` is absent on daemons older than this field; treat
|
|
918
|
+
# absence as unknown (None) so callers can fall back to status only.
|
|
919
|
+
active_runner_raw = resp.get("active_runner")
|
|
920
|
+
active_runner: bool | None
|
|
921
|
+
if isinstance(active_runner_raw, bool):
|
|
922
|
+
active_runner = active_runner_raw
|
|
923
|
+
else:
|
|
924
|
+
active_runner = None
|
|
884
925
|
success = bool(resp.get("success", True))
|
|
885
926
|
return SimpleNamespace(
|
|
886
927
|
plan=plan,
|
|
887
928
|
step_index=step_index,
|
|
888
929
|
iteration=iteration,
|
|
889
930
|
status=status,
|
|
931
|
+
active_runner=active_runner,
|
|
890
932
|
loop_id=resp.get("loop_id", lid),
|
|
891
933
|
success=success,
|
|
892
934
|
)
|
|
@@ -1232,9 +1232,14 @@ class WebSocketClient:
|
|
|
1232
1232
|
except Exception as exc:
|
|
1233
1233
|
raise StaleLoopError(lid, exc) from exc
|
|
1234
1234
|
|
|
1235
|
-
async def list_skills(
|
|
1235
|
+
async def list_skills(
|
|
1236
|
+
self, *, workspace: str | None = None, timeout: float = 15.0
|
|
1237
|
+
) -> dict[str, Any]:
|
|
1236
1238
|
"""Request wire-safe skill metadata from the daemon."""
|
|
1237
|
-
|
|
1239
|
+
params: dict[str, Any] = {}
|
|
1240
|
+
if workspace:
|
|
1241
|
+
params["workspace"] = str(workspace).strip()
|
|
1242
|
+
return await self.request("skills_list", params, timeout=timeout)
|
|
1238
1243
|
|
|
1239
1244
|
async def list_models(self, *, timeout: float = 15.0) -> dict[str, Any]:
|
|
1240
1245
|
"""Request model catalog rows from the daemon host ``SootheConfig``."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/__init__.py
RENAMED
|
File without changes
|
{soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/attachments.py
RENAMED
|
File without changes
|
{soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/broadcaster.py
RENAMED
|
File without changes
|
|
File without changes
|
{soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/chunk_filter.py
RENAMED
|
File without changes
|
{soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/classifier.py
RENAMED
|
File without changes
|
{soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/events.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/pool.py
RENAMED
|
File without changes
|
{soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/query_gate.py
RENAMED
|
File without changes
|
|
File without changes
|
{soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/turn.py
RENAMED
|
File without changes
|
|
File without changes
|
{soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/appkit/turn_runner.py
RENAMED
|
File without changes
|
{soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/command_client.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/intent_hints.py
RENAMED
|
File without changes
|
{soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/protocol_params.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/stream_terminal.py
RENAMED
|
File without changes
|
{soothe_client_python-1.0.13 → soothe_client_python-1.0.14}/src/soothe_client/turn_boundary.py
RENAMED
|
File without changes
|