soothe-client-python 1.0.3__tar.gz → 1.0.7__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 (34) hide show
  1. {soothe_client_python-1.0.3 → soothe_client_python-1.0.7}/PKG-INFO +2 -2
  2. {soothe_client_python-1.0.3 → soothe_client_python-1.0.7}/pyproject.toml +1 -1
  3. {soothe_client_python-1.0.3 → soothe_client_python-1.0.7}/src/soothe_client/__init__.py +2 -2
  4. {soothe_client_python-1.0.3 → soothe_client_python-1.0.7}/src/soothe_client/appkit/__init__.py +11 -4
  5. soothe_client_python-1.0.7/src/soothe_client/appkit/card_projection.py +10 -0
  6. {soothe_client_python-1.0.3 → soothe_client_python-1.0.7}/src/soothe_client/appkit/daemon_session.py +67 -59
  7. soothe_client_python-1.0.3/src/soothe_client/appkit/session_store.py → soothe_client_python-1.0.7/src/soothe_client/appkit/loop_session_store.py +4 -4
  8. {soothe_client_python-1.0.3 → soothe_client_python-1.0.7}/src/soothe_client/appkit/pool.py +3 -3
  9. {soothe_client_python-1.0.3 → soothe_client_python-1.0.7}/src/soothe_client/appkit/turn_runner.py +2 -2
  10. {soothe_client_python-1.0.3 → soothe_client_python-1.0.7}/src/soothe_client/helpers.py +4 -4
  11. {soothe_client_python-1.0.3 → soothe_client_python-1.0.7}/src/soothe_client/intent_hints.py +0 -2
  12. {soothe_client_python-1.0.3 → soothe_client_python-1.0.7}/src/soothe_client/protocol_params.py +6 -5
  13. {soothe_client_python-1.0.3 → soothe_client_python-1.0.7}/src/soothe_client/stream_terminal.py +11 -3
  14. {soothe_client_python-1.0.3 → soothe_client_python-1.0.7}/src/soothe_client/websocket.py +14 -8
  15. {soothe_client_python-1.0.3 → soothe_client_python-1.0.7}/.gitignore +0 -0
  16. {soothe_client_python-1.0.3 → soothe_client_python-1.0.7}/LICENSE +0 -0
  17. {soothe_client_python-1.0.3 → soothe_client_python-1.0.7}/README.md +0 -0
  18. {soothe_client_python-1.0.3 → soothe_client_python-1.0.7}/src/soothe_client/appkit/attachments.py +0 -0
  19. {soothe_client_python-1.0.3 → soothe_client_python-1.0.7}/src/soothe_client/appkit/broadcaster.py +0 -0
  20. {soothe_client_python-1.0.3 → soothe_client_python-1.0.7}/src/soothe_client/appkit/chunk_filter.py +0 -0
  21. {soothe_client_python-1.0.3 → soothe_client_python-1.0.7}/src/soothe_client/appkit/classifier.py +0 -0
  22. {soothe_client_python-1.0.3 → soothe_client_python-1.0.7}/src/soothe_client/appkit/events.py +0 -0
  23. {soothe_client_python-1.0.3 → soothe_client_python-1.0.7}/src/soothe_client/appkit/managed_client.py +0 -0
  24. {soothe_client_python-1.0.3 → soothe_client_python-1.0.7}/src/soothe_client/appkit/observability.py +0 -0
  25. {soothe_client_python-1.0.3 → soothe_client_python-1.0.7}/src/soothe_client/appkit/query_gate.py +0 -0
  26. {soothe_client_python-1.0.3 → soothe_client_python-1.0.7}/src/soothe_client/appkit/thinking_step.py +0 -0
  27. {soothe_client_python-1.0.3 → soothe_client_python-1.0.7}/src/soothe_client/appkit/turn.py +0 -0
  28. {soothe_client_python-1.0.3 → soothe_client_python-1.0.7}/src/soothe_client/appkit/turn_boundary.py +0 -0
  29. {soothe_client_python-1.0.3 → soothe_client_python-1.0.7}/src/soothe_client/command_client.py +0 -0
  30. {soothe_client_python-1.0.3 → soothe_client_python-1.0.7}/src/soothe_client/errors.py +0 -0
  31. {soothe_client_python-1.0.3 → soothe_client_python-1.0.7}/src/soothe_client/py.typed +0 -0
  32. {soothe_client_python-1.0.3 → soothe_client_python-1.0.7}/src/soothe_client/schemas.py +0 -0
  33. {soothe_client_python-1.0.3 → soothe_client_python-1.0.7}/src/soothe_client/session.py +0 -0
  34. {soothe_client_python-1.0.3 → soothe_client_python-1.0.7}/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
3
+ Version: 1.0.7
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
@@ -21,7 +21,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
21
  Requires-Python: <3.15,>=3.11
22
22
  Requires-Dist: pillow<12.0.0,>=10.0.0
23
23
  Requires-Dist: pydantic<3.0.0,>=2.0.0
24
- Requires-Dist: soothe-sdk<2.0.0,>=1.0.0
24
+ Requires-Dist: soothe-sdk<2.0.0,>=1.0.2
25
25
  Requires-Dist: websockets>=12.0
26
26
  Provides-Extra: dev
27
27
  Requires-Dist: mypy>=1.0.0; extra == 'dev'
@@ -23,7 +23,7 @@ classifiers = [
23
23
  "Topic :: Software Development :: Libraries :: Python Modules",
24
24
  ]
25
25
  dependencies = [
26
- "soothe-sdk>=1.0.0,<2.0.0",
26
+ "soothe-sdk>=1.0.2,<2.0.0",
27
27
  "pydantic>=2.0.0,<3.0.0",
28
28
  "websockets>=12.0",
29
29
  "pillow>=10.0.0,<12.0.0",
@@ -33,7 +33,7 @@ from soothe_client.helpers import (
33
33
  check_daemon_status,
34
34
  connected_websocket,
35
35
  fetch_config_section,
36
- fetch_loop_cards,
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",
@@ -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
- - ``SessionStore`` — persistence seam (Protocol)
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
+ ]
@@ -82,9 +82,21 @@ class DaemonSession:
82
82
  self.last_turn_cancellation_seen: bool = False
83
83
  self.last_turn_error_message: str | None = None
84
84
  # Last completed turn's seq floor (drop stale prior-turn frames).
85
+ # Scoped to the current peer seq namespace: reset on reconnect / loop
86
+ # change — daemon seq is per-process and restarts after daemon restart.
85
87
  self._last_turn_end_seq: int = 0
86
88
  self._expected_turn_id: str | None = None
87
89
 
90
+ def _reset_turn_seq_floor(self) -> None:
91
+ """Clear seq/turn filters when the peer seq namespace may have restarted.
92
+
93
+ Keep-TUI reconnect after a daemon restart (or subscribe to a different
94
+ loop) would otherwise drop all low-seq frames — including
95
+ ``status=running`` — leaving ``query_started=False`` and a silent TUI.
96
+ """
97
+ self._last_turn_end_seq = 0
98
+ self._expected_turn_id = None
99
+
88
100
  @property
89
101
  def client(self) -> WebSocketClient:
90
102
  """Subscribed stream WebSocket (loop events)."""
@@ -131,6 +143,7 @@ class DaemonSession:
131
143
  return await self._bootstrap_loop(resume_loop_id=resume_loop_id)
132
144
 
133
145
  async def _bootstrap_loop(self, *, resume_loop_id: str | None = None) -> dict[str, Any]:
146
+ prev_loop_id = self._loop_id
134
147
  status_event = await bootstrap_loop_session(
135
148
  self._client,
136
149
  resume_loop_id=resume_loop_id,
@@ -140,6 +153,9 @@ class DaemonSession:
140
153
  if status_event.get("type") == "error":
141
154
  raise RuntimeError(str(status_event.get("message", "daemon bootstrap failed")))
142
155
  self._loop_id = status_event.get("loop_id")
156
+ # New/changed loop has an independent seq counter on the daemon.
157
+ if prev_loop_id != self._loop_id:
158
+ self._reset_turn_seq_floor()
143
159
  return status_event
144
160
 
145
161
  async def new_loop(self) -> dict[str, Any]:
@@ -179,6 +195,10 @@ class DaemonSession:
179
195
  await self._client.close()
180
196
  await connect_websocket_with_retries(self._client)
181
197
 
198
+ # Peer seq is process-local; daemon restart restarts counters at 1 while
199
+ # this session may still hold a high floor from before the disconnect.
200
+ self._reset_turn_seq_floor()
201
+
182
202
  if resume_loop_id:
183
203
  reattach = getattr(self._client, "reattach_and_probe", None)
184
204
  if callable(reattach):
@@ -411,7 +431,7 @@ class DaemonSession:
411
431
  ):
412
432
  continue
413
433
 
414
- # IG-659 phase 5: drop frames at or before prior turn end seq.
434
+ # IG-616 phase 5: drop frames at or before prior turn end seq.
415
435
  ev_seq = frame_seq(event)
416
436
  if ev_seq is not None and last_end_seq > 0 and ev_seq <= last_end_seq:
417
437
  continue
@@ -506,7 +526,7 @@ class DaemonSession:
506
526
  self.turn_event_stats.filtered_early += 1
507
527
  continue
508
528
 
509
- # Prefer turn_id match when present; else IG-658 progress gate.
529
+ # Prefer turn_id match when present; else IG-615 progress gate.
510
530
  if mode == "custom" and is_turn_end_custom_data(data):
511
531
  data_turn = (
512
532
  frame_turn_id(data if isinstance(data, dict) else None) or ev_turn_id
@@ -603,40 +623,6 @@ class DaemonSession:
603
623
  await self._rpc_client.wait_for_connection_ack(ack_timeout_s=_RPC_HANDSHAKE_TIMEOUT_S)
604
624
  self._rpc_connected = True
605
625
 
606
- async def fetch_loop_cards(self, loop_id: str) -> SimpleNamespace:
607
- """Fetch bound display-card snapshot for a loop."""
608
- lid = str(loop_id or "").strip()
609
- if not lid:
610
- return SimpleNamespace(cards=[], seq=0, success=False)
611
-
612
- async with self._rpc_lock:
613
- await self._ensure_rpc_connected()
614
- try:
615
- resp = await self._rpc_client.request(
616
- "loop_cards_fetch",
617
- {"loop_id": lid},
618
- timeout=30.0,
619
- )
620
- except Exception:
621
- logger.warning("loop_cards_fetch failed for loop %s", lid[:16], exc_info=True)
622
- return SimpleNamespace(cards=[], seq=0, success=False)
623
-
624
- raw_cards = resp.get("cards")
625
- cards = list(raw_cards) if isinstance(raw_cards, list) else []
626
- seq = int(resp.get("seq") or 0)
627
- context_tokens_raw = resp.get("context_tokens")
628
- context_tokens = (
629
- context_tokens_raw
630
- if isinstance(context_tokens_raw, int) and context_tokens_raw >= 0
631
- else 0
632
- )
633
- return SimpleNamespace(
634
- cards=cards,
635
- seq=seq,
636
- context_tokens=context_tokens,
637
- success=True,
638
- )
639
-
640
626
  async def fetch_loop_history(self, loop_id: str) -> SimpleNamespace:
641
627
  """Fetch goal display snapshots plus live card tail."""
642
628
  lid = str(loop_id or "").strip()
@@ -711,34 +697,56 @@ class DaemonSession:
711
697
  values: dict[str, Any] = dict(raw) if isinstance(raw, dict) else {}
712
698
  return SimpleNamespace(values=values)
713
699
 
714
- async def aupdate_loop_state(
715
- self,
716
- loop_id: str,
717
- values: dict[str, Any],
718
- *,
719
- timeout: float = 10.0,
720
- as_node: str | None = None,
721
- ) -> None:
722
- """Merge partial state into the loop on the daemon host."""
700
+ async def fetch_execution_state(self, loop_id: str) -> SimpleNamespace:
701
+ """Fetch execution-progress snapshot (plan, step_index, iteration, status).
702
+
703
+ Returns a namespace with ``plan``, ``step_index``, ``iteration``,
704
+ ``status`` for the loop's bound checkpoint thread. On RPC failure the
705
+ fields degrade gracefully (``step_index=0``, ``iteration=0``,
706
+ ``status=None``, ``plan=None``) so a resume gate can still proceed.
707
+ """
723
708
  lid = str(loop_id or "").strip()
709
+ empty = SimpleNamespace(
710
+ plan=None,
711
+ step_index=0,
712
+ iteration=0,
713
+ status=None,
714
+ loop_id=lid or None,
715
+ success=False,
716
+ )
724
717
  if not lid:
725
- return
718
+ return empty
726
719
 
727
720
  async with self._rpc_lock:
728
721
  await self._ensure_rpc_connected()
729
- from soothe_sdk.wire.protocol import _serialize_for_json
730
-
731
- payload_values = _serialize_for_json(values)
732
- if not isinstance(payload_values, dict):
733
- return
734
- params: dict[str, Any] = {"loop_id": lid, "values": payload_values}
735
- if as_node:
736
- params["as_node"] = as_node
737
- await self._rpc_client.request(
738
- "loop_state_update",
739
- params,
740
- timeout=timeout,
741
- )
722
+ try:
723
+ resp = await self._rpc_client.request(
724
+ "loop_execution_state_fetch",
725
+ {"loop_id": lid},
726
+ timeout=30.0,
727
+ )
728
+ except Exception:
729
+ logger.warning(
730
+ "loop_execution_state_fetch failed for loop %s", lid[:16], exc_info=True
731
+ )
732
+ return empty
733
+
734
+ plan = resp.get("plan")
735
+ step_index_raw = resp.get("step_index")
736
+ step_index = step_index_raw if isinstance(step_index_raw, int) else 0
737
+ iteration_raw = resp.get("iteration")
738
+ iteration = iteration_raw if isinstance(iteration_raw, int) else 0
739
+ status_raw = resp.get("status")
740
+ status = status_raw if isinstance(status_raw, str) else None
741
+ success = bool(resp.get("success", True))
742
+ return SimpleNamespace(
743
+ plan=plan,
744
+ step_index=step_index,
745
+ iteration=iteration,
746
+ status=status,
747
+ loop_id=resp.get("loop_id", lid),
748
+ success=success,
749
+ )
742
750
 
743
751
  async def fetch_conversation_log(
744
752
  self,
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  from typing import Any, Protocol, runtime_checkable
6
6
 
7
7
 
8
- class SessionEntry:
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 SessionStore(Protocol):
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) -> SessionEntry | None:
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__ = ["SessionEntry", "SessionMessage", "SessionStore"]
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 ``SessionStore``.
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: SessionStore,
99
+ store: LoopSessionStore,
100
100
  cfg: PoolConfig | None = None,
101
101
  factory: ClientFactory | None = None,
102
102
  ) -> None:
@@ -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: SessionStore,
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 fetch_loop_cards(
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 the daemon's bound display-card snapshot for a loop."""
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.loop_cards_fetch(lid, timeout=timeout)
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
  {
@@ -22,7 +22,7 @@ __all__ = [
22
22
  "LoopMessagesParams",
23
23
  "LoopStateGetParams",
24
24
  "LoopStateUpdateParams",
25
- "LoopCardsFetchParams",
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 LoopCardsFetchParams(ParamsBase):
273
- """Params for ``method=loop_cards_fetch``.
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):
@@ -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,
@@ -28,6 +33,9 @@ _TURN_PROGRESS_CUSTOM_TYPES = frozenset(
28
33
  STRANGE_LOOP_STEP_STARTED,
29
34
  STRANGE_LOOP_STEP_QUEUED,
30
35
  STRANGE_LOOP_STEP_COMPLETED,
36
+ CARD_CREATED,
37
+ CARD_UPDATED,
38
+ CARD_FINALIZED,
31
39
  }
32
40
  )
33
41
 
@@ -35,9 +43,9 @@ _TURN_PROGRESS_CUSTOM_TYPES = frozenset(
35
43
  STALE_TURN_PENDING_TYPES = frozenset(
36
44
  {
37
45
  "connection_ack",
38
- "card.replay_begin",
39
- "card.replay_end",
40
- "card.created",
46
+ CARD_REPLAY_BEGIN,
47
+ CARD_REPLAY_END,
48
+ CARD_CREATED,
41
49
  "complete",
42
50
  }
43
51
  )
@@ -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,