soothe-client-python 1.0.7__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.
Files changed (34) hide show
  1. {soothe_client_python-1.0.7 → soothe_client_python-1.0.8}/PKG-INFO +1 -1
  2. {soothe_client_python-1.0.7 → soothe_client_python-1.0.8}/src/soothe_client/appkit/daemon_session.py +40 -3
  3. {soothe_client_python-1.0.7 → soothe_client_python-1.0.8}/src/soothe_client/stream_terminal.py +17 -0
  4. {soothe_client_python-1.0.7 → soothe_client_python-1.0.8}/.gitignore +0 -0
  5. {soothe_client_python-1.0.7 → soothe_client_python-1.0.8}/LICENSE +0 -0
  6. {soothe_client_python-1.0.7 → soothe_client_python-1.0.8}/README.md +0 -0
  7. {soothe_client_python-1.0.7 → soothe_client_python-1.0.8}/pyproject.toml +0 -0
  8. {soothe_client_python-1.0.7 → soothe_client_python-1.0.8}/src/soothe_client/__init__.py +0 -0
  9. {soothe_client_python-1.0.7 → soothe_client_python-1.0.8}/src/soothe_client/appkit/__init__.py +0 -0
  10. {soothe_client_python-1.0.7 → soothe_client_python-1.0.8}/src/soothe_client/appkit/attachments.py +0 -0
  11. {soothe_client_python-1.0.7 → soothe_client_python-1.0.8}/src/soothe_client/appkit/broadcaster.py +0 -0
  12. {soothe_client_python-1.0.7 → soothe_client_python-1.0.8}/src/soothe_client/appkit/card_projection.py +0 -0
  13. {soothe_client_python-1.0.7 → soothe_client_python-1.0.8}/src/soothe_client/appkit/chunk_filter.py +0 -0
  14. {soothe_client_python-1.0.7 → soothe_client_python-1.0.8}/src/soothe_client/appkit/classifier.py +0 -0
  15. {soothe_client_python-1.0.7 → soothe_client_python-1.0.8}/src/soothe_client/appkit/events.py +0 -0
  16. {soothe_client_python-1.0.7 → soothe_client_python-1.0.8}/src/soothe_client/appkit/loop_session_store.py +0 -0
  17. {soothe_client_python-1.0.7 → soothe_client_python-1.0.8}/src/soothe_client/appkit/managed_client.py +0 -0
  18. {soothe_client_python-1.0.7 → soothe_client_python-1.0.8}/src/soothe_client/appkit/observability.py +0 -0
  19. {soothe_client_python-1.0.7 → soothe_client_python-1.0.8}/src/soothe_client/appkit/pool.py +0 -0
  20. {soothe_client_python-1.0.7 → soothe_client_python-1.0.8}/src/soothe_client/appkit/query_gate.py +0 -0
  21. {soothe_client_python-1.0.7 → soothe_client_python-1.0.8}/src/soothe_client/appkit/thinking_step.py +0 -0
  22. {soothe_client_python-1.0.7 → soothe_client_python-1.0.8}/src/soothe_client/appkit/turn.py +0 -0
  23. {soothe_client_python-1.0.7 → soothe_client_python-1.0.8}/src/soothe_client/appkit/turn_boundary.py +0 -0
  24. {soothe_client_python-1.0.7 → soothe_client_python-1.0.8}/src/soothe_client/appkit/turn_runner.py +0 -0
  25. {soothe_client_python-1.0.7 → soothe_client_python-1.0.8}/src/soothe_client/command_client.py +0 -0
  26. {soothe_client_python-1.0.7 → soothe_client_python-1.0.8}/src/soothe_client/errors.py +0 -0
  27. {soothe_client_python-1.0.7 → soothe_client_python-1.0.8}/src/soothe_client/helpers.py +0 -0
  28. {soothe_client_python-1.0.7 → soothe_client_python-1.0.8}/src/soothe_client/intent_hints.py +0 -0
  29. {soothe_client_python-1.0.7 → soothe_client_python-1.0.8}/src/soothe_client/protocol_params.py +0 -0
  30. {soothe_client_python-1.0.7 → soothe_client_python-1.0.8}/src/soothe_client/py.typed +0 -0
  31. {soothe_client_python-1.0.7 → soothe_client_python-1.0.8}/src/soothe_client/schemas.py +0 -0
  32. {soothe_client_python-1.0.7 → soothe_client_python-1.0.8}/src/soothe_client/session.py +0 -0
  33. {soothe_client_python-1.0.7 → soothe_client_python-1.0.8}/src/soothe_client/turn_boundary.py +0 -0
  34. {soothe_client_python-1.0.7 → soothe_client_python-1.0.8}/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: 1.0.7
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
@@ -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
@@ -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
- self.last_turn_end_state = (
559
- "stream_end" if custom_type == STREAM_END else "completed"
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(
@@ -697,6 +705,35 @@ class DaemonSession:
697
705
  values: dict[str, Any] = dict(raw) if isinstance(raw, dict) else {}
698
706
  return SimpleNamespace(values=values)
699
707
 
708
+ async def aupdate_loop_state(
709
+ self,
710
+ loop_id: str,
711
+ values: dict[str, Any],
712
+ *,
713
+ timeout: float = 10.0,
714
+ as_node: str | None = None,
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
+ """
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
+
700
737
  async def fetch_execution_state(self, loop_id: str) -> SimpleNamespace:
701
738
  """Fetch execution-progress snapshot (plan, step_index, iteration, status).
702
739
 
@@ -24,6 +24,16 @@ from soothe_sdk.core.events import (
24
24
 
25
25
  TURN_END_CUSTOM_TYPES = frozenset({STREAM_END, STRANGE_LOOP_COMPLETED})
26
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
+
27
37
  # Customs that prove this turn has real work (not mere intake plan.phase).
28
38
  # Intake-only phases must not unlock turn-end — prior-goal stream.end can still
29
39
  # arrive after status=running (loop 3e43).
@@ -64,6 +74,11 @@ def is_turn_end_custom_data(data: Any) -> TypeGuard[dict[str, Any]]:
64
74
  return True
65
75
 
66
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
+
67
82
  def is_turn_progress_chunk(mode: str, data: Any) -> bool:
68
83
  """True when a chunk proves the active turn has non-intake progress.
69
84
 
@@ -113,8 +128,10 @@ def stale_pending_frame_label(event: dict[str, Any]) -> str | None:
113
128
  __all__ = [
114
129
  "STALE_TURN_PENDING_TYPES",
115
130
  "STREAM_END",
131
+ "STREAM_END_CANCEL_REASONS",
116
132
  "STRANGE_LOOP_COMPLETED",
117
133
  "TURN_END_CUSTOM_TYPES",
134
+ "is_stream_end_cancel_reason",
118
135
  "is_turn_end_custom_data",
119
136
  "is_turn_progress_chunk",
120
137
  "stale_pending_frame_label",