soothe-client-python 1.0.4__tar.gz → 1.0.8__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.4 → soothe_client_python-1.0.8}/PKG-INFO +1 -1
- {soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/src/soothe_client/__init__.py +2 -2
- {soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/src/soothe_client/appkit/__init__.py +11 -4
- soothe_client_python-1.0.8/src/soothe_client/appkit/card_projection.py +10 -0
- {soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/src/soothe_client/appkit/daemon_session.py +80 -55
- soothe_client_python-1.0.4/src/soothe_client/appkit/session_store.py → soothe_client_python-1.0.8/src/soothe_client/appkit/loop_session_store.py +4 -4
- {soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/src/soothe_client/appkit/pool.py +3 -3
- {soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/src/soothe_client/appkit/turn_runner.py +2 -2
- {soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/src/soothe_client/helpers.py +4 -4
- {soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/src/soothe_client/intent_hints.py +0 -2
- {soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/src/soothe_client/protocol_params.py +6 -5
- {soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/src/soothe_client/stream_terminal.py +28 -3
- {soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/src/soothe_client/websocket.py +14 -8
- {soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/.gitignore +0 -0
- {soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/LICENSE +0 -0
- {soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/README.md +0 -0
- {soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/pyproject.toml +0 -0
- {soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/src/soothe_client/appkit/attachments.py +0 -0
- {soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/src/soothe_client/appkit/broadcaster.py +0 -0
- {soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/src/soothe_client/appkit/chunk_filter.py +0 -0
- {soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/src/soothe_client/appkit/classifier.py +0 -0
- {soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/src/soothe_client/appkit/events.py +0 -0
- {soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/src/soothe_client/appkit/managed_client.py +0 -0
- {soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/src/soothe_client/appkit/observability.py +0 -0
- {soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/src/soothe_client/appkit/query_gate.py +0 -0
- {soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/src/soothe_client/appkit/thinking_step.py +0 -0
- {soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/src/soothe_client/appkit/turn.py +0 -0
- {soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/src/soothe_client/appkit/turn_boundary.py +0 -0
- {soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/src/soothe_client/command_client.py +0 -0
- {soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/src/soothe_client/errors.py +0 -0
- {soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/src/soothe_client/py.typed +0 -0
- {soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/src/soothe_client/schemas.py +0 -0
- {soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/src/soothe_client/session.py +0 -0
- {soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/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.8
|
|
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
|
|
@@ -33,7 +33,7 @@ from soothe_client.helpers import (
|
|
|
33
33
|
check_daemon_status,
|
|
34
34
|
connected_websocket,
|
|
35
35
|
fetch_config_section,
|
|
36
|
-
|
|
36
|
+
fetch_execution_state,
|
|
37
37
|
fetch_loop_history,
|
|
38
38
|
fetch_loop_messages,
|
|
39
39
|
fetch_skills_catalog,
|
|
@@ -91,8 +91,8 @@ __all__ = [
|
|
|
91
91
|
"request_daemon_shutdown",
|
|
92
92
|
"fetch_skills_catalog",
|
|
93
93
|
"fetch_config_section",
|
|
94
|
+
"fetch_execution_state",
|
|
94
95
|
"fetch_loop_history",
|
|
95
|
-
"fetch_loop_cards",
|
|
96
96
|
"fetch_loop_messages",
|
|
97
97
|
# Intent hints
|
|
98
98
|
"TEXT_COMPLETION",
|
{soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/src/soothe_client/appkit/__init__.py
RENAMED
|
@@ -7,7 +7,7 @@ Primary building blocks for agent UIs and backends:
|
|
|
7
7
|
- ``QueryGate`` — one-in-flight query per session
|
|
8
8
|
- ``EventClassifier`` — stream → deliverable mapping
|
|
9
9
|
- ``SSEBroadcaster`` — drop-on-full fan-out to subscribers
|
|
10
|
-
- ``
|
|
10
|
+
- ``LoopSessionStore`` — persistence seam (Protocol)
|
|
11
11
|
|
|
12
12
|
Advanced stream/pipeline plumbing lives in submodules
|
|
13
13
|
(``events``, ``chunk_filter``, ``turn``, ``managed_client``).
|
|
@@ -21,6 +21,7 @@ from soothe_client.appkit.attachments import (
|
|
|
21
21
|
compact_image_attachment,
|
|
22
22
|
)
|
|
23
23
|
from soothe_client.appkit.broadcaster import SSEBroadcaster, SSEEvent
|
|
24
|
+
from soothe_client.appkit.card_projection import CardProjection, parse_card_custom_payload
|
|
24
25
|
from soothe_client.appkit.classifier import (
|
|
25
26
|
EVENT_FINAL_REPORT,
|
|
26
27
|
ChatEventResult,
|
|
@@ -29,6 +30,11 @@ from soothe_client.appkit.classifier import (
|
|
|
29
30
|
EventClassifier,
|
|
30
31
|
)
|
|
31
32
|
from soothe_client.appkit.daemon_session import DaemonSession
|
|
33
|
+
from soothe_client.appkit.loop_session_store import (
|
|
34
|
+
LoopSessionEntry,
|
|
35
|
+
LoopSessionStore,
|
|
36
|
+
SessionMessage,
|
|
37
|
+
)
|
|
32
38
|
from soothe_client.appkit.pool import (
|
|
33
39
|
ConnectionPool,
|
|
34
40
|
ErrPoolExhausted,
|
|
@@ -37,7 +43,6 @@ from soothe_client.appkit.pool import (
|
|
|
37
43
|
default_pool_config,
|
|
38
44
|
)
|
|
39
45
|
from soothe_client.appkit.query_gate import ErrQueryBusy, QueryGate
|
|
40
|
-
from soothe_client.appkit.session_store import SessionEntry, SessionMessage, SessionStore
|
|
41
46
|
from soothe_client.appkit.thinking_step import (
|
|
42
47
|
DEFAULT_THINKING_STEP_EVENTS,
|
|
43
48
|
extract_thinking_step,
|
|
@@ -72,6 +77,7 @@ __all__ = [
|
|
|
72
77
|
"STREAM_CLOSE_FAIL",
|
|
73
78
|
"STREAM_CLOSE_SOFT_COMPLETE",
|
|
74
79
|
"Attachment",
|
|
80
|
+
"CardProjection",
|
|
75
81
|
"ChatEventResult",
|
|
76
82
|
"ChatEventTerminal",
|
|
77
83
|
"ClassifierConfig",
|
|
@@ -85,6 +91,8 @@ __all__ = [
|
|
|
85
91
|
"ErrQueryTimeout",
|
|
86
92
|
"EventClassifier",
|
|
87
93
|
"InputOpts",
|
|
94
|
+
"LoopSessionEntry",
|
|
95
|
+
"LoopSessionStore",
|
|
88
96
|
"OnComplete",
|
|
89
97
|
"OnError",
|
|
90
98
|
"PoolConfig",
|
|
@@ -92,9 +100,7 @@ __all__ = [
|
|
|
92
100
|
"QueryGate",
|
|
93
101
|
"SSEBroadcaster",
|
|
94
102
|
"SSEEvent",
|
|
95
|
-
"SessionEntry",
|
|
96
103
|
"SessionMessage",
|
|
97
|
-
"SessionStore",
|
|
98
104
|
"StreamClosePolicy",
|
|
99
105
|
"TURN_END_IDLE",
|
|
100
106
|
"TURN_END_STOPPED",
|
|
@@ -111,4 +117,5 @@ __all__ = [
|
|
|
111
117
|
"extract_thinking_step",
|
|
112
118
|
"idle_timeout_for_turn",
|
|
113
119
|
"input_message_for_loop",
|
|
120
|
+
"parse_card_custom_payload",
|
|
114
121
|
]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""In-memory apply helpers for daemon ``soothe.card.*`` frames."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from soothe_sdk.display.card_wire import CardProjection, parse_card_custom_payload
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"CardProjection",
|
|
9
|
+
"parse_card_custom_payload",
|
|
10
|
+
]
|
{soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/src/soothe_client/appkit/daemon_session.py
RENAMED
|
@@ -20,6 +20,7 @@ from soothe_client.appkit.observability import TurnEventStats
|
|
|
20
20
|
from soothe_client.session import bootstrap_loop_session, connect_websocket_with_retries
|
|
21
21
|
from soothe_client.stream_terminal import (
|
|
22
22
|
STREAM_END,
|
|
23
|
+
is_stream_end_cancel_reason,
|
|
23
24
|
is_turn_end_custom_data,
|
|
24
25
|
is_turn_progress_chunk,
|
|
25
26
|
)
|
|
@@ -81,6 +82,7 @@ class DaemonSession:
|
|
|
81
82
|
self.last_turn_end_state: str | None = None
|
|
82
83
|
self.last_turn_cancellation_seen: bool = False
|
|
83
84
|
self.last_turn_error_message: str | None = None
|
|
85
|
+
self.last_turn_stream_end_reason: str | None = None
|
|
84
86
|
# Last completed turn's seq floor (drop stale prior-turn frames).
|
|
85
87
|
# Scoped to the current peer seq namespace: reset on reconnect / loop
|
|
86
88
|
# change — daemon seq is per-process and restarts after daemon restart.
|
|
@@ -359,6 +361,7 @@ class DaemonSession:
|
|
|
359
361
|
self.last_turn_end_state = None
|
|
360
362
|
self.last_turn_cancellation_seen = False
|
|
361
363
|
self.last_turn_error_message = None
|
|
364
|
+
self.last_turn_stream_end_reason = None
|
|
362
365
|
inbound_dropped_baseline = getattr(self._client, "inbound_dropped", 0)
|
|
363
366
|
query_started = False
|
|
364
367
|
expected_loop_id = self._loop_id
|
|
@@ -431,7 +434,7 @@ class DaemonSession:
|
|
|
431
434
|
):
|
|
432
435
|
continue
|
|
433
436
|
|
|
434
|
-
# IG-
|
|
437
|
+
# IG-616 phase 5: drop frames at or before prior turn end seq.
|
|
435
438
|
ev_seq = frame_seq(event)
|
|
436
439
|
if ev_seq is not None and last_end_seq > 0 and ev_seq <= last_end_seq:
|
|
437
440
|
continue
|
|
@@ -526,7 +529,7 @@ class DaemonSession:
|
|
|
526
529
|
self.turn_event_stats.filtered_early += 1
|
|
527
530
|
continue
|
|
528
531
|
|
|
529
|
-
# Prefer turn_id match when present; else IG-
|
|
532
|
+
# Prefer turn_id match when present; else IG-615 progress gate.
|
|
530
533
|
if mode == "custom" and is_turn_end_custom_data(data):
|
|
531
534
|
data_turn = (
|
|
532
535
|
frame_turn_id(data if isinstance(data, dict) else None) or ev_turn_id
|
|
@@ -555,9 +558,14 @@ class DaemonSession:
|
|
|
555
558
|
yield (namespace, mode, data)
|
|
556
559
|
if mode == "custom" and is_turn_end_custom_data(data):
|
|
557
560
|
custom_type = str(data.get("type", "")).strip()
|
|
558
|
-
|
|
559
|
-
"
|
|
560
|
-
|
|
561
|
+
if custom_type == STREAM_END:
|
|
562
|
+
reason = str(data.get("reason") or "").strip().lower()
|
|
563
|
+
self.last_turn_stream_end_reason = reason or None
|
|
564
|
+
if is_stream_end_cancel_reason(reason):
|
|
565
|
+
self.last_turn_cancellation_seen = True
|
|
566
|
+
self.last_turn_end_state = "stream_end"
|
|
567
|
+
else:
|
|
568
|
+
self.last_turn_end_state = "completed"
|
|
561
569
|
if ev_seq is not None:
|
|
562
570
|
self._last_turn_end_seq = max(self._last_turn_end_seq, ev_seq)
|
|
563
571
|
async for chunk in self._drain_stream_events_after_idle(
|
|
@@ -623,40 +631,6 @@ class DaemonSession:
|
|
|
623
631
|
await self._rpc_client.wait_for_connection_ack(ack_timeout_s=_RPC_HANDSHAKE_TIMEOUT_S)
|
|
624
632
|
self._rpc_connected = True
|
|
625
633
|
|
|
626
|
-
async def fetch_loop_cards(self, loop_id: str) -> SimpleNamespace:
|
|
627
|
-
"""Fetch bound display-card snapshot for a loop."""
|
|
628
|
-
lid = str(loop_id or "").strip()
|
|
629
|
-
if not lid:
|
|
630
|
-
return SimpleNamespace(cards=[], seq=0, success=False)
|
|
631
|
-
|
|
632
|
-
async with self._rpc_lock:
|
|
633
|
-
await self._ensure_rpc_connected()
|
|
634
|
-
try:
|
|
635
|
-
resp = await self._rpc_client.request(
|
|
636
|
-
"loop_cards_fetch",
|
|
637
|
-
{"loop_id": lid},
|
|
638
|
-
timeout=30.0,
|
|
639
|
-
)
|
|
640
|
-
except Exception:
|
|
641
|
-
logger.warning("loop_cards_fetch failed for loop %s", lid[:16], exc_info=True)
|
|
642
|
-
return SimpleNamespace(cards=[], seq=0, success=False)
|
|
643
|
-
|
|
644
|
-
raw_cards = resp.get("cards")
|
|
645
|
-
cards = list(raw_cards) if isinstance(raw_cards, list) else []
|
|
646
|
-
seq = int(resp.get("seq") or 0)
|
|
647
|
-
context_tokens_raw = resp.get("context_tokens")
|
|
648
|
-
context_tokens = (
|
|
649
|
-
context_tokens_raw
|
|
650
|
-
if isinstance(context_tokens_raw, int) and context_tokens_raw >= 0
|
|
651
|
-
else 0
|
|
652
|
-
)
|
|
653
|
-
return SimpleNamespace(
|
|
654
|
-
cards=cards,
|
|
655
|
-
seq=seq,
|
|
656
|
-
context_tokens=context_tokens,
|
|
657
|
-
success=True,
|
|
658
|
-
)
|
|
659
|
-
|
|
660
634
|
async def fetch_loop_history(self, loop_id: str) -> SimpleNamespace:
|
|
661
635
|
"""Fetch goal display snapshots plus live card tail."""
|
|
662
636
|
lid = str(loop_id or "").strip()
|
|
@@ -738,27 +712,78 @@ class DaemonSession:
|
|
|
738
712
|
*,
|
|
739
713
|
timeout: float = 10.0,
|
|
740
714
|
as_node: str | None = None,
|
|
741
|
-
) ->
|
|
742
|
-
"""Merge partial state
|
|
715
|
+
) -> dict[str, Any]:
|
|
716
|
+
"""Merge partial StrangeLoop state via daemon ``loop_state_update``.
|
|
717
|
+
|
|
718
|
+
Used by the TUI for interrupt cleanup and token-total persistence.
|
|
719
|
+
"""
|
|
743
720
|
lid = str(loop_id or "").strip()
|
|
721
|
+
if not lid or not isinstance(values, dict):
|
|
722
|
+
return {}
|
|
723
|
+
|
|
724
|
+
async with self._rpc_lock:
|
|
725
|
+
await self._ensure_rpc_connected()
|
|
726
|
+
try:
|
|
727
|
+
return await self._rpc_client.loop_state_update(
|
|
728
|
+
lid,
|
|
729
|
+
values,
|
|
730
|
+
as_node=as_node,
|
|
731
|
+
timeout=timeout,
|
|
732
|
+
)
|
|
733
|
+
except Exception:
|
|
734
|
+
logger.warning("loop_state_update failed for loop %s", lid[:16], exc_info=True)
|
|
735
|
+
return {}
|
|
736
|
+
|
|
737
|
+
async def fetch_execution_state(self, loop_id: str) -> SimpleNamespace:
|
|
738
|
+
"""Fetch execution-progress snapshot (plan, step_index, iteration, status).
|
|
739
|
+
|
|
740
|
+
Returns a namespace with ``plan``, ``step_index``, ``iteration``,
|
|
741
|
+
``status`` for the loop's bound checkpoint thread. On RPC failure the
|
|
742
|
+
fields degrade gracefully (``step_index=0``, ``iteration=0``,
|
|
743
|
+
``status=None``, ``plan=None``) so a resume gate can still proceed.
|
|
744
|
+
"""
|
|
745
|
+
lid = str(loop_id or "").strip()
|
|
746
|
+
empty = SimpleNamespace(
|
|
747
|
+
plan=None,
|
|
748
|
+
step_index=0,
|
|
749
|
+
iteration=0,
|
|
750
|
+
status=None,
|
|
751
|
+
loop_id=lid or None,
|
|
752
|
+
success=False,
|
|
753
|
+
)
|
|
744
754
|
if not lid:
|
|
745
|
-
return
|
|
755
|
+
return empty
|
|
746
756
|
|
|
747
757
|
async with self._rpc_lock:
|
|
748
758
|
await self._ensure_rpc_connected()
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
759
|
+
try:
|
|
760
|
+
resp = await self._rpc_client.request(
|
|
761
|
+
"loop_execution_state_fetch",
|
|
762
|
+
{"loop_id": lid},
|
|
763
|
+
timeout=30.0,
|
|
764
|
+
)
|
|
765
|
+
except Exception:
|
|
766
|
+
logger.warning(
|
|
767
|
+
"loop_execution_state_fetch failed for loop %s", lid[:16], exc_info=True
|
|
768
|
+
)
|
|
769
|
+
return empty
|
|
770
|
+
|
|
771
|
+
plan = resp.get("plan")
|
|
772
|
+
step_index_raw = resp.get("step_index")
|
|
773
|
+
step_index = step_index_raw if isinstance(step_index_raw, int) else 0
|
|
774
|
+
iteration_raw = resp.get("iteration")
|
|
775
|
+
iteration = iteration_raw if isinstance(iteration_raw, int) else 0
|
|
776
|
+
status_raw = resp.get("status")
|
|
777
|
+
status = status_raw if isinstance(status_raw, str) else None
|
|
778
|
+
success = bool(resp.get("success", True))
|
|
779
|
+
return SimpleNamespace(
|
|
780
|
+
plan=plan,
|
|
781
|
+
step_index=step_index,
|
|
782
|
+
iteration=iteration,
|
|
783
|
+
status=status,
|
|
784
|
+
loop_id=resp.get("loop_id", lid),
|
|
785
|
+
success=success,
|
|
786
|
+
)
|
|
762
787
|
|
|
763
788
|
async def fetch_conversation_log(
|
|
764
789
|
self,
|
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
from typing import Any, Protocol, runtime_checkable
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
class
|
|
8
|
+
class LoopSessionEntry:
|
|
9
9
|
"""Persisted mapping between an application session id and a daemon loop id."""
|
|
10
10
|
|
|
11
11
|
__slots__ = (
|
|
@@ -63,10 +63,10 @@ class SessionMessage:
|
|
|
63
63
|
|
|
64
64
|
|
|
65
65
|
@runtime_checkable
|
|
66
|
-
class
|
|
66
|
+
class LoopSessionStore(Protocol):
|
|
67
67
|
"""Persistence seam between appkit and the application's storage backend."""
|
|
68
68
|
|
|
69
|
-
async def get_session(self, session_id: str) ->
|
|
69
|
+
async def get_session(self, session_id: str) -> LoopSessionEntry | None:
|
|
70
70
|
"""Return the persisted entry for ``session_id``, or None."""
|
|
71
71
|
...
|
|
72
72
|
|
|
@@ -97,4 +97,4 @@ class SessionStore(Protocol):
|
|
|
97
97
|
...
|
|
98
98
|
|
|
99
99
|
|
|
100
|
-
__all__ = ["
|
|
100
|
+
__all__ = ["LoopSessionEntry", "SessionMessage", "LoopSessionStore"]
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Manages a pool of daemon connections, one active per session. Reuses an
|
|
4
4
|
active connection when still live, otherwise bootstraps a fresh loop or
|
|
5
5
|
reattaches an existing one. Persistence of session↔loop mappings is
|
|
6
|
-
abstracted behind ``
|
|
6
|
+
abstracted behind ``LoopSessionStore``.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
@@ -15,6 +15,7 @@ from collections.abc import AsyncIterator
|
|
|
15
15
|
from dataclasses import dataclass
|
|
16
16
|
from typing import Any
|
|
17
17
|
|
|
18
|
+
from soothe_client.appkit.loop_session_store import LoopSessionStore
|
|
18
19
|
from soothe_client.appkit.managed_client import (
|
|
19
20
|
BootstrapFunc,
|
|
20
21
|
ClientFactory,
|
|
@@ -22,7 +23,6 @@ from soothe_client.appkit.managed_client import (
|
|
|
22
23
|
default_bootstrap_func,
|
|
23
24
|
default_client_factory,
|
|
24
25
|
)
|
|
25
|
-
from soothe_client.appkit.session_store import SessionStore
|
|
26
26
|
from soothe_client.errors import StaleLoopError
|
|
27
27
|
|
|
28
28
|
|
|
@@ -96,7 +96,7 @@ class ConnectionPool:
|
|
|
96
96
|
def __init__(
|
|
97
97
|
self,
|
|
98
98
|
url: str,
|
|
99
|
-
store:
|
|
99
|
+
store: LoopSessionStore,
|
|
100
100
|
cfg: PoolConfig | None = None,
|
|
101
101
|
factory: ClientFactory | None = None,
|
|
102
102
|
) -> None:
|
{soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/src/soothe_client/appkit/turn_runner.py
RENAMED
|
@@ -21,9 +21,9 @@ from typing import Any
|
|
|
21
21
|
from soothe_client.appkit.attachments import CompactImageOptions, compact_attachments
|
|
22
22
|
from soothe_client.appkit.broadcaster import SSEBroadcaster, SSEEvent
|
|
23
23
|
from soothe_client.appkit.classifier import ChatEventTerminal, EventClassifier
|
|
24
|
+
from soothe_client.appkit.loop_session_store import LoopSessionStore, SessionMessage
|
|
24
25
|
from soothe_client.appkit.pool import ConnectionPool, PooledConn
|
|
25
26
|
from soothe_client.appkit.query_gate import QueryGate
|
|
26
|
-
from soothe_client.appkit.session_store import SessionMessage, SessionStore
|
|
27
27
|
from soothe_client.appkit.turn_boundary import TurnBoundary, is_daemon_turn_end_event
|
|
28
28
|
from soothe_client.intent_hints import validate_loop_input_intent_hint
|
|
29
29
|
|
|
@@ -135,7 +135,7 @@ class TurnRunner:
|
|
|
135
135
|
pool: ConnectionPool,
|
|
136
136
|
gate: QueryGate,
|
|
137
137
|
classifier: EventClassifier,
|
|
138
|
-
store:
|
|
138
|
+
store: LoopSessionStore,
|
|
139
139
|
broadcaster: SSEBroadcaster | None,
|
|
140
140
|
cfg: TurnConfig | None = None,
|
|
141
141
|
) -> None:
|
|
@@ -428,18 +428,18 @@ async def fetch_loop_history(
|
|
|
428
428
|
return await client.loop_history_fetch(lid, timeout=timeout)
|
|
429
429
|
|
|
430
430
|
|
|
431
|
-
async def
|
|
431
|
+
async def fetch_execution_state(
|
|
432
432
|
client: WebSocketClient,
|
|
433
433
|
loop_id: str,
|
|
434
434
|
*,
|
|
435
435
|
timeout: float = 30.0,
|
|
436
436
|
) -> dict[str, Any]:
|
|
437
|
-
"""Fetch
|
|
437
|
+
"""Fetch execution-progress snapshot (plan, step_index, iteration, status)."""
|
|
438
438
|
lid = str(loop_id or "").strip()
|
|
439
439
|
if not lid:
|
|
440
440
|
raise ValueError("loop_id is required")
|
|
441
441
|
await _ensure_handshake(client, timeout=timeout)
|
|
442
|
-
return await client.
|
|
442
|
+
return await client.loop_execution_state_fetch(lid, timeout=timeout)
|
|
443
443
|
|
|
444
444
|
|
|
445
445
|
async def fetch_loop_messages(
|
|
@@ -480,6 +480,6 @@ __all__ = [
|
|
|
480
480
|
"fetch_skills_catalog",
|
|
481
481
|
"fetch_config_section",
|
|
482
482
|
"fetch_loop_history",
|
|
483
|
-
"fetch_loop_cards",
|
|
484
483
|
"fetch_loop_messages",
|
|
484
|
+
"fetch_execution_state",
|
|
485
485
|
]
|
|
@@ -9,8 +9,6 @@ IMAGE_TO_TEXT: Final = "image_to_text"
|
|
|
9
9
|
OCR: Final = "ocr"
|
|
10
10
|
EMBED: Final = "embed"
|
|
11
11
|
|
|
12
|
-
REMOVED_INTENT_HINTS: frozenset[str] = frozenset({"direct_llm", "quiz", "direct_model"})
|
|
13
|
-
|
|
14
12
|
# Default deliverable phases for turn-ending replies (excludes plan_direct narration).
|
|
15
13
|
DEFAULT_DELIVERABLE_PHASES: frozenset[str] = frozenset(
|
|
16
14
|
{
|
{soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/src/soothe_client/protocol_params.py
RENAMED
|
@@ -22,7 +22,7 @@ __all__ = [
|
|
|
22
22
|
"LoopMessagesParams",
|
|
23
23
|
"LoopStateGetParams",
|
|
24
24
|
"LoopStateUpdateParams",
|
|
25
|
-
"
|
|
25
|
+
"LoopExecutionStateFetchParams",
|
|
26
26
|
"LoopDetachParams",
|
|
27
27
|
# Subscription params
|
|
28
28
|
"SubscribeParams",
|
|
@@ -269,16 +269,17 @@ class LoopStateUpdateParams(ParamsBase):
|
|
|
269
269
|
as_node: str | None = None
|
|
270
270
|
|
|
271
271
|
|
|
272
|
-
class
|
|
273
|
-
"""Params for ``method=
|
|
272
|
+
class LoopExecutionStateFetchParams(ParamsBase):
|
|
273
|
+
"""Params for ``method=loop_execution_state_fetch``.
|
|
274
|
+
|
|
275
|
+
Returns a focused execution-progress snapshot (plan, step_index,
|
|
276
|
+
iteration, status) for the loop's bound checkpoint thread.
|
|
274
277
|
|
|
275
278
|
Attributes:
|
|
276
279
|
loop_id: Loop identifier (required).
|
|
277
|
-
since: Fetch cards after this sequence.
|
|
278
280
|
"""
|
|
279
281
|
|
|
280
282
|
loop_id: str = Field(..., min_length=1)
|
|
281
|
-
since: str | None = None
|
|
282
283
|
|
|
283
284
|
|
|
284
285
|
class LoopHistoryFetchParams(ParamsBase):
|
{soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/src/soothe_client/stream_terminal.py
RENAMED
|
@@ -9,6 +9,11 @@ from __future__ import annotations
|
|
|
9
9
|
from typing import Any, TypeGuard
|
|
10
10
|
|
|
11
11
|
from soothe_sdk.core.events import (
|
|
12
|
+
CARD_CREATED,
|
|
13
|
+
CARD_FINALIZED,
|
|
14
|
+
CARD_REPLAY_BEGIN,
|
|
15
|
+
CARD_REPLAY_END,
|
|
16
|
+
CARD_UPDATED,
|
|
12
17
|
PLAN_CREATED,
|
|
13
18
|
STRANGE_LOOP_COMPLETED,
|
|
14
19
|
STRANGE_LOOP_STEP_COMPLETED,
|
|
@@ -19,6 +24,16 @@ from soothe_sdk.core.events import (
|
|
|
19
24
|
|
|
20
25
|
TURN_END_CUSTOM_TYPES = frozenset({STREAM_END, STRANGE_LOOP_COMPLETED})
|
|
21
26
|
|
|
27
|
+
# ``soothe.stream.end`` reasons that mean the turn was cancelled (not a clean end).
|
|
28
|
+
STREAM_END_CANCEL_REASONS = frozenset(
|
|
29
|
+
{
|
|
30
|
+
"cancelled",
|
|
31
|
+
"canceled",
|
|
32
|
+
"user_cancelled",
|
|
33
|
+
"client_disconnect",
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
|
|
22
37
|
# Customs that prove this turn has real work (not mere intake plan.phase).
|
|
23
38
|
# Intake-only phases must not unlock turn-end — prior-goal stream.end can still
|
|
24
39
|
# arrive after status=running (loop 3e43).
|
|
@@ -28,6 +43,9 @@ _TURN_PROGRESS_CUSTOM_TYPES = frozenset(
|
|
|
28
43
|
STRANGE_LOOP_STEP_STARTED,
|
|
29
44
|
STRANGE_LOOP_STEP_QUEUED,
|
|
30
45
|
STRANGE_LOOP_STEP_COMPLETED,
|
|
46
|
+
CARD_CREATED,
|
|
47
|
+
CARD_UPDATED,
|
|
48
|
+
CARD_FINALIZED,
|
|
31
49
|
}
|
|
32
50
|
)
|
|
33
51
|
|
|
@@ -35,9 +53,9 @@ _TURN_PROGRESS_CUSTOM_TYPES = frozenset(
|
|
|
35
53
|
STALE_TURN_PENDING_TYPES = frozenset(
|
|
36
54
|
{
|
|
37
55
|
"connection_ack",
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
56
|
+
CARD_REPLAY_BEGIN,
|
|
57
|
+
CARD_REPLAY_END,
|
|
58
|
+
CARD_CREATED,
|
|
41
59
|
"complete",
|
|
42
60
|
}
|
|
43
61
|
)
|
|
@@ -56,6 +74,11 @@ def is_turn_end_custom_data(data: Any) -> TypeGuard[dict[str, Any]]:
|
|
|
56
74
|
return True
|
|
57
75
|
|
|
58
76
|
|
|
77
|
+
def is_stream_end_cancel_reason(reason: str | None) -> bool:
|
|
78
|
+
"""True when a ``stream.end`` reason is a cancel/disconnect (not clean end)."""
|
|
79
|
+
return str(reason or "").strip().lower() in STREAM_END_CANCEL_REASONS
|
|
80
|
+
|
|
81
|
+
|
|
59
82
|
def is_turn_progress_chunk(mode: str, data: Any) -> bool:
|
|
60
83
|
"""True when a chunk proves the active turn has non-intake progress.
|
|
61
84
|
|
|
@@ -105,8 +128,10 @@ def stale_pending_frame_label(event: dict[str, Any]) -> str | None:
|
|
|
105
128
|
__all__ = [
|
|
106
129
|
"STALE_TURN_PENDING_TYPES",
|
|
107
130
|
"STREAM_END",
|
|
131
|
+
"STREAM_END_CANCEL_REASONS",
|
|
108
132
|
"STRANGE_LOOP_COMPLETED",
|
|
109
133
|
"TURN_END_CUSTOM_TYPES",
|
|
134
|
+
"is_stream_end_cancel_reason",
|
|
110
135
|
"is_turn_end_custom_data",
|
|
111
136
|
"is_turn_progress_chunk",
|
|
112
137
|
"stale_pending_frame_label",
|
|
@@ -1066,14 +1066,6 @@ class WebSocketClient:
|
|
|
1066
1066
|
timeout=timeout,
|
|
1067
1067
|
)
|
|
1068
1068
|
|
|
1069
|
-
async def loop_cards_fetch(self, loop_id: str, *, timeout: float = 30.0) -> dict[str, Any]:
|
|
1070
|
-
"""Fetch bound display-card snapshot (``loop_cards_fetch``)."""
|
|
1071
|
-
return await self.request(
|
|
1072
|
-
"loop_cards_fetch",
|
|
1073
|
-
{"loop_id": loop_id},
|
|
1074
|
-
timeout=timeout,
|
|
1075
|
-
)
|
|
1076
|
-
|
|
1077
1069
|
async def loop_messages(
|
|
1078
1070
|
self,
|
|
1079
1071
|
loop_id: str,
|
|
@@ -1103,6 +1095,20 @@ class WebSocketClient:
|
|
|
1103
1095
|
timeout=timeout,
|
|
1104
1096
|
)
|
|
1105
1097
|
|
|
1098
|
+
async def loop_execution_state_fetch(
|
|
1099
|
+
self, loop_id: str, *, timeout: float = 30.0
|
|
1100
|
+
) -> dict[str, Any]:
|
|
1101
|
+
"""Fetch execution-progress snapshot (``loop_execution_state_fetch``).
|
|
1102
|
+
|
|
1103
|
+
Returns ``{plan, step_index, iteration, status}`` for the loop's bound
|
|
1104
|
+
checkpoint thread.
|
|
1105
|
+
"""
|
|
1106
|
+
return await self.request(
|
|
1107
|
+
"loop_execution_state_fetch",
|
|
1108
|
+
{"loop_id": loop_id},
|
|
1109
|
+
timeout=timeout,
|
|
1110
|
+
)
|
|
1111
|
+
|
|
1106
1112
|
async def loop_state_update(
|
|
1107
1113
|
self,
|
|
1108
1114
|
loop_id: str,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/src/soothe_client/appkit/attachments.py
RENAMED
|
File without changes
|
{soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/src/soothe_client/appkit/broadcaster.py
RENAMED
|
File without changes
|
{soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/src/soothe_client/appkit/chunk_filter.py
RENAMED
|
File without changes
|
{soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/src/soothe_client/appkit/classifier.py
RENAMED
|
File without changes
|
{soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/src/soothe_client/appkit/events.py
RENAMED
|
File without changes
|
{soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/src/soothe_client/appkit/managed_client.py
RENAMED
|
File without changes
|
{soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/src/soothe_client/appkit/observability.py
RENAMED
|
File without changes
|
{soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/src/soothe_client/appkit/query_gate.py
RENAMED
|
File without changes
|
{soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/src/soothe_client/appkit/thinking_step.py
RENAMED
|
File without changes
|
|
File without changes
|
{soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/src/soothe_client/appkit/turn_boundary.py
RENAMED
|
File without changes
|
{soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/src/soothe_client/command_client.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{soothe_client_python-1.0.4 → soothe_client_python-1.0.8}/src/soothe_client/turn_boundary.py
RENAMED
|
File without changes
|