soothe-client-python 1.0.11__tar.gz → 1.0.13__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.11 → soothe_client_python-1.0.13}/PKG-INFO +1 -1
- {soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/src/soothe_client/__init__.py +0 -2
- {soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/src/soothe_client/appkit/daemon_session.py +99 -37
- {soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/src/soothe_client/appkit/turn_boundary.py +33 -20
- {soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/src/soothe_client/appkit/turn_runner.py +3 -4
- {soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/src/soothe_client/command_client.py +0 -10
- soothe_client_python-1.0.13/src/soothe_client/intent_hints.py +23 -0
- {soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/src/soothe_client/protocol_params.py +2 -8
- {soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/src/soothe_client/session.py +0 -5
- {soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/src/soothe_client/stream_terminal.py +12 -0
- {soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/src/soothe_client/turn_boundary.py +54 -4
- {soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/src/soothe_client/websocket.py +10 -17
- soothe_client_python-1.0.11/src/soothe_client/intent_hints.py +0 -44
- {soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/.gitignore +0 -0
- {soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/LICENSE +0 -0
- {soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/README.md +0 -0
- {soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/pyproject.toml +0 -0
- {soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/src/soothe_client/appkit/__init__.py +0 -0
- {soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/src/soothe_client/appkit/attachments.py +0 -0
- {soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/src/soothe_client/appkit/broadcaster.py +0 -0
- {soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/src/soothe_client/appkit/card_projection.py +0 -0
- {soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/src/soothe_client/appkit/chunk_filter.py +0 -0
- {soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/src/soothe_client/appkit/classifier.py +0 -0
- {soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/src/soothe_client/appkit/events.py +0 -0
- {soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/src/soothe_client/appkit/loop_session_store.py +0 -0
- {soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/src/soothe_client/appkit/managed_client.py +0 -0
- {soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/src/soothe_client/appkit/observability.py +0 -0
- {soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/src/soothe_client/appkit/pool.py +0 -0
- {soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/src/soothe_client/appkit/query_gate.py +0 -0
- {soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/src/soothe_client/appkit/thinking_step.py +0 -0
- {soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/src/soothe_client/appkit/turn.py +0 -0
- {soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/src/soothe_client/errors.py +0 -0
- {soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/src/soothe_client/helpers.py +0 -0
- {soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/src/soothe_client/py.typed +0 -0
- {soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/src/soothe_client/schemas.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.13
|
|
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
|
|
@@ -49,7 +49,6 @@ from soothe_client.intent_hints import (
|
|
|
49
49
|
IMAGE_TO_TEXT,
|
|
50
50
|
OCR,
|
|
51
51
|
TEXT_COMPLETION,
|
|
52
|
-
validate_loop_input_intent_hint,
|
|
53
52
|
)
|
|
54
53
|
from soothe_client.session import (
|
|
55
54
|
bootstrap_loop_session,
|
|
@@ -100,5 +99,4 @@ __all__ = [
|
|
|
100
99
|
"OCR",
|
|
101
100
|
"EMBED",
|
|
102
101
|
"DEFAULT_DELIVERABLE_PHASES",
|
|
103
|
-
"validate_loop_input_intent_hint",
|
|
104
102
|
]
|
|
@@ -27,9 +27,10 @@ from soothe_client.stream_terminal import (
|
|
|
27
27
|
from soothe_client.turn_boundary import (
|
|
28
28
|
frame_seq,
|
|
29
29
|
frame_turn_id,
|
|
30
|
-
|
|
30
|
+
is_status_terminal_allowed,
|
|
31
31
|
is_turn_terminal_allowed,
|
|
32
32
|
parse_turn_generation,
|
|
33
|
+
should_bind_running_turn,
|
|
33
34
|
turn_ids_match,
|
|
34
35
|
)
|
|
35
36
|
from soothe_client.websocket import WebSocketClient
|
|
@@ -94,7 +95,12 @@ class DaemonSession:
|
|
|
94
95
|
# Scoped to the current peer seq namespace: reset on reconnect / loop
|
|
95
96
|
# change — daemon seq is per-process and restarts after daemon restart.
|
|
96
97
|
self._last_turn_end_seq: int = 0
|
|
98
|
+
# Last accepted terminal's turn generation (refuse leftover prior-gen
|
|
99
|
+
# status=running binds when expected_turn_id is still unset).
|
|
100
|
+
self._last_completed_turn_gen: int = 0
|
|
97
101
|
self._expected_turn_id: str | None = None
|
|
102
|
+
# Set by post-idle drain when a successor status=running is re-queued.
|
|
103
|
+
self._drain_saw_successor_running: bool = False
|
|
98
104
|
|
|
99
105
|
def _reset_turn_seq_floor(self) -> None:
|
|
100
106
|
"""Clear seq/turn filters when the peer seq namespace may have restarted.
|
|
@@ -104,8 +110,27 @@ class DaemonSession:
|
|
|
104
110
|
``status=running`` — leaving ``query_started=False`` and a silent TUI.
|
|
105
111
|
"""
|
|
106
112
|
self._last_turn_end_seq = 0
|
|
113
|
+
self._last_completed_turn_gen = 0
|
|
107
114
|
self._expected_turn_id = None
|
|
108
115
|
|
|
116
|
+
def _note_completed_turn_gen(self, turn_id: str | None) -> None:
|
|
117
|
+
"""Raise the completed-generation floor after an accepted terminal."""
|
|
118
|
+
floor = int(getattr(self, "_last_completed_turn_gen", 0) or 0)
|
|
119
|
+
gen = parse_turn_generation(turn_id)
|
|
120
|
+
if gen is not None and gen > floor:
|
|
121
|
+
self._last_completed_turn_gen = gen
|
|
122
|
+
|
|
123
|
+
def _ensure_turn_floor_attrs(self) -> None:
|
|
124
|
+
"""Initialize turn-floor attrs for partially constructed sessions (tests)."""
|
|
125
|
+
if not hasattr(self, "_last_completed_turn_gen"):
|
|
126
|
+
self._last_completed_turn_gen = 0
|
|
127
|
+
if not hasattr(self, "_last_turn_end_seq"):
|
|
128
|
+
self._last_turn_end_seq = 0
|
|
129
|
+
if not hasattr(self, "_drain_saw_successor_running"):
|
|
130
|
+
self._drain_saw_successor_running = False
|
|
131
|
+
if not hasattr(self, "_expected_turn_id"):
|
|
132
|
+
self._expected_turn_id = None
|
|
133
|
+
|
|
109
134
|
@property
|
|
110
135
|
def client(self) -> WebSocketClient:
|
|
111
136
|
"""Subscribed stream WebSocket (loop events)."""
|
|
@@ -254,8 +279,6 @@ class DaemonSession:
|
|
|
254
279
|
self,
|
|
255
280
|
text: str,
|
|
256
281
|
*,
|
|
257
|
-
autonomous: bool = False,
|
|
258
|
-
max_iterations: int | None = None,
|
|
259
282
|
preferred_subagent: str | None = None,
|
|
260
283
|
model: str | None = None,
|
|
261
284
|
model_params: dict[str, Any] | None = None,
|
|
@@ -276,8 +299,6 @@ class DaemonSession:
|
|
|
276
299
|
await self._client.send_input(
|
|
277
300
|
self._loop_id,
|
|
278
301
|
text,
|
|
279
|
-
autonomous=autonomous,
|
|
280
|
-
max_iterations=max_iterations,
|
|
281
302
|
preferred_subagent=preferred_subagent,
|
|
282
303
|
model=model,
|
|
283
304
|
model_params=model_params,
|
|
@@ -302,7 +323,13 @@ class DaemonSession:
|
|
|
302
323
|
*,
|
|
303
324
|
expected_loop_id: str | None,
|
|
304
325
|
) -> AsyncIterator[tuple[tuple[Any, ...], str, Any]]:
|
|
305
|
-
"""Yield stream chunks that arrive just after ``idle``.
|
|
326
|
+
"""Yield stream chunks that arrive just after ``idle``.
|
|
327
|
+
|
|
328
|
+
If a successor ``status=running`` (generation above the completed floor)
|
|
329
|
+
arrives, re-queue it and set ``_drain_saw_successor_running`` so the
|
|
330
|
+
caller can continue the reader instead of ending the turn.
|
|
331
|
+
"""
|
|
332
|
+
self._drain_saw_successor_running = False
|
|
306
333
|
loop = asyncio.get_running_loop()
|
|
307
334
|
deadline = loop.time() + self._drain_deadline
|
|
308
335
|
exp = expected_loop_id
|
|
@@ -313,6 +340,7 @@ class DaemonSession:
|
|
|
313
340
|
break
|
|
314
341
|
if not event:
|
|
315
342
|
break
|
|
343
|
+
raw_event = event
|
|
316
344
|
event_type = event.get("type", "")
|
|
317
345
|
if event_type == "next":
|
|
318
346
|
event = unwrap_next(event) or event
|
|
@@ -329,6 +357,21 @@ class DaemonSession:
|
|
|
329
357
|
if isinstance(loop_ev, str) and loop_ev:
|
|
330
358
|
self._loop_id = loop_ev
|
|
331
359
|
exp = loop_ev
|
|
360
|
+
state = str(event.get("state") or "").strip().lower()
|
|
361
|
+
if state == "running":
|
|
362
|
+
status_turn = frame_turn_id(event)
|
|
363
|
+
new_gen = parse_turn_generation(status_turn)
|
|
364
|
+
if new_gen is not None and new_gen > int(
|
|
365
|
+
getattr(self, "_last_completed_turn_gen", 0) or 0
|
|
366
|
+
):
|
|
367
|
+
push = getattr(self._client, "push_pending_event_front", None)
|
|
368
|
+
if callable(push):
|
|
369
|
+
push(raw_event)
|
|
370
|
+
else:
|
|
371
|
+
# Fallback: put unwrapped event if client lacks helper.
|
|
372
|
+
self._client._pending_events.appendleft(event) # noqa: SLF001
|
|
373
|
+
self._drain_saw_successor_running = True
|
|
374
|
+
return
|
|
332
375
|
continue
|
|
333
376
|
if event_type != "event":
|
|
334
377
|
continue
|
|
@@ -364,6 +407,7 @@ class DaemonSession:
|
|
|
364
407
|
raises ``TimeoutError`` if the daemon never emits a turn-end
|
|
365
408
|
signal in time.
|
|
366
409
|
"""
|
|
410
|
+
self._ensure_turn_floor_attrs()
|
|
367
411
|
self.turn_event_stats = self._new_turn_stats()
|
|
368
412
|
self.last_turn_end_state = None
|
|
369
413
|
self.last_turn_cancellation_seen = False
|
|
@@ -495,61 +539,73 @@ class DaemonSession:
|
|
|
495
539
|
expected_loop_id = loop_ev
|
|
496
540
|
state = event.get("state", "")
|
|
497
541
|
if state == "running":
|
|
498
|
-
query_started = True
|
|
499
|
-
progress_seen = True
|
|
500
542
|
status_turn = frame_turn_id(event)
|
|
501
543
|
# Bind only from running frames that carry turn_id.
|
|
502
544
|
# Pre-admit early running omits turn_id and must not
|
|
503
545
|
# unlock terminals or poison the expected id.
|
|
504
546
|
if status_turn:
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
547
|
+
completed_gen = int(
|
|
548
|
+
getattr(self, "_last_completed_turn_gen", 0) or 0
|
|
549
|
+
)
|
|
550
|
+
if not should_bind_running_turn(
|
|
551
|
+
status_turn=status_turn,
|
|
552
|
+
expected_turn_id=expected_turn_id,
|
|
553
|
+
last_completed_turn_gen=completed_gen,
|
|
509
554
|
):
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
555
|
+
logger.debug(
|
|
556
|
+
"Ignoring stale/older status=running "
|
|
557
|
+
"(loop=%s turn=%s expected=%s completed_gen=%s)",
|
|
558
|
+
(expected_loop_id or "?")[:16],
|
|
559
|
+
status_turn[-24:],
|
|
560
|
+
(expected_turn_id or "<none>")[-24:],
|
|
561
|
+
completed_gen,
|
|
562
|
+
)
|
|
563
|
+
continue
|
|
564
|
+
if expected_turn_id and status_turn != expected_turn_id:
|
|
565
|
+
# Successor turn admitted while this reader
|
|
566
|
+
# still held a stale/early binding.
|
|
567
|
+
turn_progress_seen = False
|
|
568
|
+
expected_turn_id = status_turn
|
|
569
|
+
self._expected_turn_id = status_turn
|
|
570
|
+
query_started = True
|
|
571
|
+
progress_seen = True
|
|
572
|
+
else:
|
|
573
|
+
# Pre-admit early running: query start, no bind.
|
|
574
|
+
query_started = True
|
|
575
|
+
progress_seen = True
|
|
576
|
+
elif query_started and state in {"stopped", "idle"}:
|
|
577
|
+
status_turn = frame_turn_id(event)
|
|
578
|
+
if not is_status_terminal_allowed(
|
|
531
579
|
expected_turn_id=expected_turn_id,
|
|
532
|
-
frame_turn_id=
|
|
580
|
+
frame_turn_id=status_turn,
|
|
533
581
|
query_started=query_started,
|
|
534
582
|
turn_progress_seen=turn_progress_seen,
|
|
535
583
|
cancellation_seen=self.last_turn_cancellation_seen,
|
|
536
584
|
):
|
|
537
585
|
logger.debug(
|
|
538
|
-
"Ignoring premature
|
|
539
|
-
"(loop=%s bound=%s
|
|
586
|
+
"Ignoring premature %s "
|
|
587
|
+
"(loop=%s bound=%s status_turn=%s progress=%s)",
|
|
588
|
+
state,
|
|
540
589
|
(expected_loop_id or "?")[:16],
|
|
541
590
|
bool(expected_turn_id),
|
|
542
|
-
(
|
|
591
|
+
(status_turn or "<absent>")[-24:],
|
|
543
592
|
turn_progress_seen,
|
|
544
593
|
)
|
|
545
594
|
continue
|
|
546
595
|
self.last_turn_end_state = state
|
|
547
596
|
if ev_seq is not None:
|
|
548
597
|
self._last_turn_end_seq = max(self._last_turn_end_seq, ev_seq)
|
|
598
|
+
self._note_completed_turn_gen(expected_turn_id)
|
|
549
599
|
async for chunk in self._drain_stream_events_after_idle(
|
|
550
600
|
expected_loop_id=expected_loop_id,
|
|
551
601
|
):
|
|
552
602
|
yield chunk
|
|
603
|
+
if self._drain_saw_successor_running:
|
|
604
|
+
# Successor turn already re-queued; continue reading.
|
|
605
|
+
turn_progress_seen = False
|
|
606
|
+
expected_turn_id = None
|
|
607
|
+
self._expected_turn_id = None
|
|
608
|
+
continue
|
|
553
609
|
break
|
|
554
610
|
continue
|
|
555
611
|
|
|
@@ -612,10 +668,16 @@ class DaemonSession:
|
|
|
612
668
|
self.last_turn_end_state = "completed"
|
|
613
669
|
if ev_seq is not None:
|
|
614
670
|
self._last_turn_end_seq = max(self._last_turn_end_seq, ev_seq)
|
|
671
|
+
self._note_completed_turn_gen(expected_turn_id)
|
|
615
672
|
async for chunk in self._drain_stream_events_after_idle(
|
|
616
673
|
expected_loop_id=expected_loop_id,
|
|
617
674
|
):
|
|
618
675
|
yield chunk
|
|
676
|
+
if self._drain_saw_successor_running:
|
|
677
|
+
turn_progress_seen = False
|
|
678
|
+
expected_turn_id = None
|
|
679
|
+
self._expected_turn_id = None
|
|
680
|
+
continue
|
|
619
681
|
break
|
|
620
682
|
if mode == "updates" and isinstance(data, dict) and "__interrupt__" in data:
|
|
621
683
|
continue
|
|
@@ -16,10 +16,10 @@ from soothe_client.stream_terminal import (
|
|
|
16
16
|
)
|
|
17
17
|
from soothe_client.turn_boundary import (
|
|
18
18
|
frame_turn_id,
|
|
19
|
-
|
|
19
|
+
is_status_terminal_allowed,
|
|
20
20
|
is_turn_terminal_allowed,
|
|
21
21
|
parse_turn_generation,
|
|
22
|
-
|
|
22
|
+
should_bind_running_turn,
|
|
23
23
|
)
|
|
24
24
|
|
|
25
25
|
TURN_END_STREAM_END = STREAM_END
|
|
@@ -35,6 +35,7 @@ class TurnLifecycleGate:
|
|
|
35
35
|
saw_turn_progress: bool = False
|
|
36
36
|
expected_turn_id: str | None = None
|
|
37
37
|
cancellation_seen: bool = False
|
|
38
|
+
last_completed_turn_gen: int = 0
|
|
38
39
|
|
|
39
40
|
def observe(self, msg: Any) -> None:
|
|
40
41
|
frame = _normalize_frame(msg)
|
|
@@ -43,17 +44,24 @@ class TurnLifecycleGate:
|
|
|
43
44
|
typ = str(frame.get("type") or "")
|
|
44
45
|
if typ == "status":
|
|
45
46
|
if str(frame.get("state") or "").strip().lower() == "running":
|
|
46
|
-
self.saw_running = True
|
|
47
47
|
status_turn = frame_turn_id(frame)
|
|
48
48
|
if status_turn:
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
if not should_bind_running_turn(
|
|
50
|
+
status_turn=status_turn,
|
|
51
|
+
expected_turn_id=self.expected_turn_id,
|
|
52
|
+
last_completed_turn_gen=self.last_completed_turn_gen,
|
|
53
53
|
):
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
return
|
|
55
|
+
if self.expected_turn_id and status_turn != self.expected_turn_id:
|
|
56
|
+
# Successor turn admitted while this reader
|
|
57
|
+
# still held a stale/early binding.
|
|
58
|
+
self.saw_turn_progress = False
|
|
59
|
+
self.expected_turn_id = status_turn
|
|
60
|
+
self.saw_running = True
|
|
61
|
+
else:
|
|
62
|
+
# Pre-admit early running omits turn_id — mark query start
|
|
63
|
+
# without binding (DaemonSession parity).
|
|
64
|
+
self.saw_running = True
|
|
57
65
|
return
|
|
58
66
|
if typ == "event":
|
|
59
67
|
mode = str(frame.get("mode") or "")
|
|
@@ -68,8 +76,8 @@ class TurnLifecycleGate:
|
|
|
68
76
|
turn_progress_seen=self.saw_turn_progress,
|
|
69
77
|
)
|
|
70
78
|
|
|
71
|
-
def
|
|
72
|
-
return
|
|
79
|
+
def allow_status_complete(self, frame_turn: str | None) -> bool:
|
|
80
|
+
return is_status_terminal_allowed(
|
|
73
81
|
expected_turn_id=self.expected_turn_id,
|
|
74
82
|
frame_turn_id=frame_turn,
|
|
75
83
|
query_started=self.saw_running,
|
|
@@ -77,6 +85,15 @@ class TurnLifecycleGate:
|
|
|
77
85
|
cancellation_seen=self.cancellation_seen,
|
|
78
86
|
)
|
|
79
87
|
|
|
88
|
+
def allow_idle_complete(self, frame_turn: str | None) -> bool:
|
|
89
|
+
return self.allow_status_complete(frame_turn)
|
|
90
|
+
|
|
91
|
+
def note_completed_turn(self, turn_id: str | None) -> None:
|
|
92
|
+
"""Raise the completed-generation floor after an accepted terminal."""
|
|
93
|
+
gen = parse_turn_generation(turn_id)
|
|
94
|
+
if gen is not None and gen > self.last_completed_turn_gen:
|
|
95
|
+
self.last_completed_turn_gen = gen
|
|
96
|
+
|
|
80
97
|
|
|
81
98
|
@dataclass
|
|
82
99
|
class TurnBoundary:
|
|
@@ -98,14 +115,9 @@ class TurnBoundary:
|
|
|
98
115
|
if typ == "status":
|
|
99
116
|
state = str(frame.get("state") or "").strip().lower()
|
|
100
117
|
frame_turn = frame_turn_id(frame)
|
|
101
|
-
if state
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
):
|
|
105
|
-
return False, ""
|
|
106
|
-
return self._mark(TURN_END_STOPPED)
|
|
107
|
-
if state == "idle" and self.gate.allow_idle_complete(frame_turn):
|
|
108
|
-
return self._mark(TURN_END_IDLE)
|
|
118
|
+
if state in {"stopped", "idle"} and self.gate.allow_status_complete(frame_turn):
|
|
119
|
+
self.gate.note_completed_turn(self.gate.expected_turn_id)
|
|
120
|
+
return self._mark(TURN_END_STOPPED if state == "stopped" else TURN_END_IDLE)
|
|
109
121
|
return False, ""
|
|
110
122
|
|
|
111
123
|
if typ == "event":
|
|
@@ -119,6 +131,7 @@ class TurnBoundary:
|
|
|
119
131
|
and is_turn_end_custom_data(data)
|
|
120
132
|
and self.gate.allow_stream_end(data_turn)
|
|
121
133
|
):
|
|
134
|
+
self.gate.note_completed_turn(self.gate.expected_turn_id)
|
|
122
135
|
return self._mark(TURN_END_STREAM_END)
|
|
123
136
|
return False, ""
|
|
124
137
|
|
{soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/src/soothe_client/appkit/turn_runner.py
RENAMED
|
@@ -25,7 +25,6 @@ from soothe_client.appkit.loop_session_store import LoopSessionStore, SessionMes
|
|
|
25
25
|
from soothe_client.appkit.pool import ConnectionPool, PooledConn
|
|
26
26
|
from soothe_client.appkit.query_gate import QueryGate
|
|
27
27
|
from soothe_client.appkit.turn_boundary import TurnBoundary, is_daemon_turn_end_event
|
|
28
|
-
from soothe_client.intent_hints import validate_loop_input_intent_hint
|
|
29
28
|
|
|
30
29
|
Attachment = dict[str, Any]
|
|
31
30
|
OnComplete = Callable[[str, str, str, str, float], None]
|
|
@@ -82,6 +81,7 @@ class InputOpts:
|
|
|
82
81
|
|
|
83
82
|
intent_hint: str | None = None
|
|
84
83
|
preferred_subagent: str | None = None
|
|
84
|
+
intake_scope: str | None = None
|
|
85
85
|
response_schema: dict[str, Any] | None = None
|
|
86
86
|
response_schema_name: str | None = None
|
|
87
87
|
response_schema_strict: bool | None = None
|
|
@@ -101,12 +101,11 @@ def input_message_for_loop(
|
|
|
101
101
|
msg["attachments"] = attachments
|
|
102
102
|
if opts is not None:
|
|
103
103
|
if opts.intent_hint and opts.intent_hint.strip():
|
|
104
|
-
hint_err = validate_loop_input_intent_hint(opts.intent_hint)
|
|
105
|
-
if hint_err:
|
|
106
|
-
raise ValueError(hint_err)
|
|
107
104
|
msg["intent_hint"] = opts.intent_hint.strip()
|
|
108
105
|
if opts.preferred_subagent and opts.preferred_subagent.strip():
|
|
109
106
|
msg["preferred_subagent"] = opts.preferred_subagent.strip()
|
|
107
|
+
if opts.intake_scope and opts.intake_scope.strip():
|
|
108
|
+
msg["intake_scope"] = opts.intake_scope.strip()
|
|
110
109
|
if opts.response_schema:
|
|
111
110
|
msg["response_schema"] = opts.response_schema
|
|
112
111
|
if opts.response_schema_name and opts.response_schema_name.strip():
|
{soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/src/soothe_client/command_client.py
RENAMED
|
@@ -331,18 +331,12 @@ class AsyncCommandClient:
|
|
|
331
331
|
goal: str,
|
|
332
332
|
*,
|
|
333
333
|
workspace: str | None = None,
|
|
334
|
-
autonomous: bool = False,
|
|
335
|
-
max_iterations: int | None = None,
|
|
336
334
|
guidance: str | None = None,
|
|
337
335
|
) -> dict[str, Any]:
|
|
338
336
|
"""Create a background job (root goal). Returns ``job_id`` + status."""
|
|
339
337
|
payload: dict[str, Any] = {"goal": goal}
|
|
340
338
|
if workspace:
|
|
341
339
|
payload["workspace"] = workspace
|
|
342
|
-
if autonomous:
|
|
343
|
-
payload["autonomous"] = autonomous
|
|
344
|
-
if max_iterations:
|
|
345
|
-
payload["max_iterations"] = max_iterations
|
|
346
340
|
if guidance:
|
|
347
341
|
payload["guidance"] = guidance
|
|
348
342
|
return await self._send_command("job_create", payload)
|
|
@@ -518,8 +512,6 @@ class CommandClient:
|
|
|
518
512
|
goal: str,
|
|
519
513
|
*,
|
|
520
514
|
workspace: str | None = None,
|
|
521
|
-
autonomous: bool = False,
|
|
522
|
-
max_iterations: int | None = None,
|
|
523
515
|
guidance: str | None = None,
|
|
524
516
|
) -> dict[str, Any]:
|
|
525
517
|
"""Create a background job (root goal)."""
|
|
@@ -529,8 +521,6 @@ class CommandClient:
|
|
|
529
521
|
self._client.job_create(
|
|
530
522
|
goal,
|
|
531
523
|
workspace=workspace,
|
|
532
|
-
autonomous=autonomous,
|
|
533
|
-
max_iterations=max_iterations,
|
|
534
524
|
guidance=guidance,
|
|
535
525
|
)
|
|
536
526
|
),
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""``loop_input.intent_hint`` constants."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Final
|
|
6
|
+
|
|
7
|
+
TEXT_COMPLETION: Final = "text_completion"
|
|
8
|
+
IMAGE_TO_TEXT: Final = "image_to_text"
|
|
9
|
+
OCR: Final = "ocr"
|
|
10
|
+
EMBED: Final = "embed"
|
|
11
|
+
|
|
12
|
+
# Default deliverable phases for turn-ending replies (excludes plan_direct narration).
|
|
13
|
+
DEFAULT_DELIVERABLE_PHASES: frozenset[str] = frozenset(
|
|
14
|
+
{
|
|
15
|
+
"quiz",
|
|
16
|
+
"goal_completion",
|
|
17
|
+
"chitchat",
|
|
18
|
+
"text_completion",
|
|
19
|
+
"image_to_text",
|
|
20
|
+
"ocr",
|
|
21
|
+
"embed",
|
|
22
|
+
}
|
|
23
|
+
)
|
{soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/src/soothe_client/protocol_params.py
RENAMED
|
@@ -194,9 +194,8 @@ class LoopInputParams(ParamsBase):
|
|
|
194
194
|
Attributes:
|
|
195
195
|
loop_id: Loop identifier (required).
|
|
196
196
|
content: User input text or structured content (required).
|
|
197
|
-
autonomous: Enable autonomous mode.
|
|
198
|
-
max_iterations: Max iterations for autonomous mode.
|
|
199
197
|
preferred_subagent: Routing hint.
|
|
198
|
+
intake_scope: Forced StrangeLoop scope (``trivial``|``simple``|``complex``).
|
|
200
199
|
model: Provider:model override.
|
|
201
200
|
model_params: Additional model parameters.
|
|
202
201
|
router_profile: Named ``router_profiles`` overlay for chat roles this turn.
|
|
@@ -212,9 +211,8 @@ class LoopInputParams(ParamsBase):
|
|
|
212
211
|
|
|
213
212
|
loop_id: str = Field(..., min_length=1)
|
|
214
213
|
content: str | dict[str, Any] = Field(..., description="User input text or structured content")
|
|
215
|
-
autonomous: bool = False
|
|
216
|
-
max_iterations: int | None = Field(default=None, gt=0)
|
|
217
214
|
preferred_subagent: str | None = None
|
|
215
|
+
intake_scope: str | None = None
|
|
218
216
|
model: str | None = None
|
|
219
217
|
model_params: dict[str, Any] | None = None
|
|
220
218
|
router_profile: str | None = None
|
|
@@ -424,8 +422,6 @@ class JobCreateParams(ParamsBase):
|
|
|
424
422
|
goal: Root goal description (required).
|
|
425
423
|
workspace: Optional workspace path.
|
|
426
424
|
user_id: Optional user identifier.
|
|
427
|
-
autonomous: Enable autonomous mode.
|
|
428
|
-
max_iterations: Max iterations.
|
|
429
425
|
guidance: Initial guidance.
|
|
430
426
|
intent_hint: Daemon intent_hint (text_completion, image_to_text, ocr, embed).
|
|
431
427
|
"""
|
|
@@ -433,8 +429,6 @@ class JobCreateParams(ParamsBase):
|
|
|
433
429
|
goal: str = Field(..., min_length=1, description="Root goal text")
|
|
434
430
|
workspace: str | None = None
|
|
435
431
|
user_id: str | None = None
|
|
436
|
-
autonomous: bool = False
|
|
437
|
-
max_iterations: int | None = Field(default=None, gt=0)
|
|
438
432
|
guidance: str | None = None
|
|
439
433
|
intent_hint: str | None = None
|
|
440
434
|
|
|
@@ -21,9 +21,6 @@ _CONNECT_TIMEOUT_S = 5.0
|
|
|
21
21
|
_DAEMON_READY_TIMEOUT_S = 20.0
|
|
22
22
|
_SESSION_BOOTSTRAP_TIMEOUT_S = 30.0
|
|
23
23
|
|
|
24
|
-
# Wire-compat: interactive loops are always solo; daemon jobs use AutopilotService.
|
|
25
|
-
_LEGACY_LOOP_AUTOPILOT_MODE = "solo"
|
|
26
|
-
|
|
27
24
|
|
|
28
25
|
async def connect_websocket_with_retries(client: Any) -> None:
|
|
29
26
|
"""Connect a ``WebSocketClient`` with retries (daemon cold-start safe)."""
|
|
@@ -181,8 +178,6 @@ async def bootstrap_loop_session(
|
|
|
181
178
|
"type": "session_ready",
|
|
182
179
|
"loop_id": loop_id,
|
|
183
180
|
"success": True,
|
|
184
|
-
# Deprecated wire field: interactive loops are always solo.
|
|
185
|
-
"autopilot_mode": _LEGACY_LOOP_AUTOPILOT_MODE,
|
|
186
181
|
}
|
|
187
182
|
if mapping_data and mapping_data.get("host_root"):
|
|
188
183
|
result["workspace_mapping"] = mapping_data
|
{soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/src/soothe_client/stream_terminal.py
RENAMED
|
@@ -105,6 +105,14 @@ def stale_pending_frame_label(event: dict[str, Any]) -> str | None:
|
|
|
105
105
|
if event_type in STALE_TURN_PENDING_TYPES:
|
|
106
106
|
return event_type
|
|
107
107
|
|
|
108
|
+
# Leftover terminal status from the prior turn must not bind/kill the next
|
|
109
|
+
# reader. Keep ``status=running`` — attach / live-query paths need it.
|
|
110
|
+
if event_type == "status":
|
|
111
|
+
state = str(event.get("state") or "").strip().lower()
|
|
112
|
+
if state in {"idle", "stopped"}:
|
|
113
|
+
return f"status.{state}"
|
|
114
|
+
return None
|
|
115
|
+
|
|
108
116
|
if event_type == "next":
|
|
109
117
|
payload = event.get("payload")
|
|
110
118
|
if not isinstance(payload, dict):
|
|
@@ -115,6 +123,10 @@ def stale_pending_frame_label(event: dict[str, Any]) -> str | None:
|
|
|
115
123
|
inner = payload.get("data")
|
|
116
124
|
if isinstance(inner, dict):
|
|
117
125
|
return stale_pending_frame_label(inner)
|
|
126
|
+
# Bare status nested under next.payload
|
|
127
|
+
nested_type = str(payload.get("type") or "")
|
|
128
|
+
if nested_type == "status":
|
|
129
|
+
return stale_pending_frame_label(payload)
|
|
118
130
|
return None
|
|
119
131
|
|
|
120
132
|
if event_type == "event":
|
{soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/src/soothe_client/turn_boundary.py
RENAMED
|
@@ -90,7 +90,7 @@ def is_turn_terminal_allowed(
|
|
|
90
90
|
return turn_ids_match(expected_turn_id, frame_turn_id)
|
|
91
91
|
|
|
92
92
|
|
|
93
|
-
def
|
|
93
|
+
def is_status_terminal_allowed(
|
|
94
94
|
*,
|
|
95
95
|
expected_turn_id: str | None,
|
|
96
96
|
frame_turn_id: str | None,
|
|
@@ -98,11 +98,14 @@ def is_idle_terminal_allowed(
|
|
|
98
98
|
turn_progress_seen: bool,
|
|
99
99
|
cancellation_seen: bool = False,
|
|
100
100
|
) -> bool:
|
|
101
|
-
"""Gate for ``status=idle`` soft-complete.
|
|
101
|
+
"""Gate for ``status=idle`` / ``status=stopped`` soft-complete.
|
|
102
102
|
|
|
103
103
|
Requires a bound turn. Matching ``turn_id`` needs progress or cancel.
|
|
104
|
-
Absent
|
|
104
|
+
Absent status ``turn_id`` is allowed only when cancellation was already seen
|
|
105
105
|
(legacy cancel finalize); mismatched ids are always rejected.
|
|
106
|
+
|
|
107
|
+
``stopped`` must use the same progress gate as ``idle`` so leftover prior-turn
|
|
108
|
+
``stopped`` cannot end a reader that only bound a stale ``status=running``.
|
|
106
109
|
"""
|
|
107
110
|
if not query_started or not str(expected_turn_id or "").strip():
|
|
108
111
|
return False
|
|
@@ -111,16 +114,63 @@ def is_idle_terminal_allowed(
|
|
|
111
114
|
if not turn_ids_match(expected_turn_id, cand):
|
|
112
115
|
return False
|
|
113
116
|
return bool(turn_progress_seen or cancellation_seen)
|
|
114
|
-
# Absent
|
|
117
|
+
# Absent status turn_id: never end a healthy turn; cancel path only.
|
|
115
118
|
return bool(cancellation_seen)
|
|
116
119
|
|
|
117
120
|
|
|
121
|
+
def is_idle_terminal_allowed(
|
|
122
|
+
*,
|
|
123
|
+
expected_turn_id: str | None,
|
|
124
|
+
frame_turn_id: str | None,
|
|
125
|
+
query_started: bool,
|
|
126
|
+
turn_progress_seen: bool,
|
|
127
|
+
cancellation_seen: bool = False,
|
|
128
|
+
) -> bool:
|
|
129
|
+
"""Gate for ``status=idle`` soft-complete (alias of status terminal rules)."""
|
|
130
|
+
return is_status_terminal_allowed(
|
|
131
|
+
expected_turn_id=expected_turn_id,
|
|
132
|
+
frame_turn_id=frame_turn_id,
|
|
133
|
+
query_started=query_started,
|
|
134
|
+
turn_progress_seen=turn_progress_seen,
|
|
135
|
+
cancellation_seen=cancellation_seen,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def should_bind_running_turn(
|
|
140
|
+
*,
|
|
141
|
+
status_turn: str | None,
|
|
142
|
+
expected_turn_id: str | None,
|
|
143
|
+
last_completed_turn_gen: int = 0,
|
|
144
|
+
) -> bool:
|
|
145
|
+
"""True when a stamped ``status=running`` may bind/rebind ``expected_turn_id``.
|
|
146
|
+
|
|
147
|
+
Refuses generations at or below the last completed floor (leftover prior-turn
|
|
148
|
+
``running`` when ``expected`` is still unset) and older gens than the current
|
|
149
|
+
bind.
|
|
150
|
+
"""
|
|
151
|
+
if not status_turn:
|
|
152
|
+
return False
|
|
153
|
+
new_gen = parse_turn_generation(status_turn)
|
|
154
|
+
if new_gen is not None and last_completed_turn_gen > 0 and new_gen <= last_completed_turn_gen:
|
|
155
|
+
return False
|
|
156
|
+
if expected_turn_id is None:
|
|
157
|
+
return True
|
|
158
|
+
old_gen = parse_turn_generation(expected_turn_id)
|
|
159
|
+
if new_gen is None:
|
|
160
|
+
return old_gen is None
|
|
161
|
+
if old_gen is None:
|
|
162
|
+
return True
|
|
163
|
+
return new_gen >= old_gen
|
|
164
|
+
|
|
165
|
+
|
|
118
166
|
__all__ = [
|
|
119
167
|
"format_turn_id",
|
|
120
168
|
"frame_seq",
|
|
121
169
|
"frame_turn_id",
|
|
122
170
|
"is_idle_terminal_allowed",
|
|
171
|
+
"is_status_terminal_allowed",
|
|
123
172
|
"is_turn_terminal_allowed",
|
|
124
173
|
"parse_turn_generation",
|
|
174
|
+
"should_bind_running_turn",
|
|
125
175
|
"turn_ids_match",
|
|
126
176
|
]
|
|
@@ -17,7 +17,6 @@ from soothe_sdk.ux.loop_stream import is_stream_terminal_wire_dict
|
|
|
17
17
|
from soothe_sdk.wire.protocol import decode_websocket_text, encode_websocket_text
|
|
18
18
|
|
|
19
19
|
from soothe_client.errors import DisconnectCause, ReconnectError, StaleLoopError
|
|
20
|
-
from soothe_client.intent_hints import validate_loop_input_intent_hint
|
|
21
20
|
from soothe_client.stream_terminal import TURN_END_CUSTOM_TYPES, stale_pending_frame_label
|
|
22
21
|
|
|
23
22
|
logger = logging.getLogger(__name__)
|
|
@@ -939,9 +938,8 @@ class WebSocketClient:
|
|
|
939
938
|
loop_id: str,
|
|
940
939
|
text: str,
|
|
941
940
|
*,
|
|
942
|
-
autonomous: bool = False,
|
|
943
|
-
max_iterations: int | None = None,
|
|
944
941
|
preferred_subagent: str | None = None,
|
|
942
|
+
intake_scope: str | None = None,
|
|
945
943
|
model: str | None = None,
|
|
946
944
|
model_params: dict[str, Any] | None = None,
|
|
947
945
|
router_profile: str | None = None,
|
|
@@ -959,9 +957,10 @@ class WebSocketClient:
|
|
|
959
957
|
Args:
|
|
960
958
|
loop_id: Loop identifier for the subscribed loop.
|
|
961
959
|
text: User input text.
|
|
962
|
-
autonomous: Enable autonomous iteration mode.
|
|
963
|
-
max_iterations: Maximum iterations for autonomous mode.
|
|
964
960
|
preferred_subagent: Preferred subagent hint for routing.
|
|
961
|
+
intake_scope: Forced StrangeLoop intake scope
|
|
962
|
+
(``trivial``|``simple``|``complex``). When set, the daemon skips
|
|
963
|
+
Pass 1 and Pass 2 LLM intake.
|
|
965
964
|
model: Provider:model override string.
|
|
966
965
|
model_params: Additional model parameters.
|
|
967
966
|
router_profile: Named router profile for chat-role overlay this turn.
|
|
@@ -974,11 +973,6 @@ class WebSocketClient:
|
|
|
974
973
|
``response_schema`` is supported for ``text_completion`` and
|
|
975
974
|
``image_to_text``. Agent-path pass-through hints (e.g.
|
|
976
975
|
``resume_clarification``, ``skill:foo``) are forwarded unchanged.
|
|
977
|
-
Legacy ``direct_llm``, ``quiz``, and ``direct_model`` are rejected
|
|
978
|
-
before send.
|
|
979
|
-
|
|
980
|
-
Raises:
|
|
981
|
-
ValueError: When ``intent_hint`` is a removed legacy value.
|
|
982
976
|
clarification_mode: clarification relay mode for this turn
|
|
983
977
|
(``"auto"`` / ``"manual"``). ``None`` lets the daemon fall back
|
|
984
978
|
to its configured default.
|
|
@@ -998,12 +992,10 @@ class WebSocketClient:
|
|
|
998
992
|
"loop_id": loop_id,
|
|
999
993
|
"content": text,
|
|
1000
994
|
}
|
|
1001
|
-
if autonomous:
|
|
1002
|
-
params["autonomous"] = True
|
|
1003
|
-
if max_iterations is not None:
|
|
1004
|
-
params["max_iterations"] = max_iterations
|
|
1005
995
|
if preferred_subagent is not None:
|
|
1006
996
|
params["preferred_subagent"] = preferred_subagent
|
|
997
|
+
if intake_scope:
|
|
998
|
+
params["intake_scope"] = intake_scope
|
|
1007
999
|
if model:
|
|
1008
1000
|
params["model"] = model
|
|
1009
1001
|
if model_params:
|
|
@@ -1013,9 +1005,6 @@ class WebSocketClient:
|
|
|
1013
1005
|
if attachments:
|
|
1014
1006
|
params["attachments"] = attachments
|
|
1015
1007
|
if intent_hint:
|
|
1016
|
-
hint_error = validate_loop_input_intent_hint(intent_hint)
|
|
1017
|
-
if hint_error is not None:
|
|
1018
|
-
raise ValueError(hint_error)
|
|
1019
1008
|
params["intent_hint"] = intent_hint
|
|
1020
1009
|
if response_schema:
|
|
1021
1010
|
params["response_schema"] = response_schema
|
|
@@ -1550,6 +1539,10 @@ class WebSocketClient:
|
|
|
1550
1539
|
"""
|
|
1551
1540
|
self._pending_events.clear()
|
|
1552
1541
|
|
|
1542
|
+
def push_pending_event_front(self, event: dict[str, Any]) -> None:
|
|
1543
|
+
"""Re-queue ``event`` at the front of ``_pending_events`` (successor bind)."""
|
|
1544
|
+
self._pending_events.appendleft(event)
|
|
1545
|
+
|
|
1553
1546
|
def peel_stale_pending_control_events(self) -> list[str]:
|
|
1554
1547
|
"""Remove stale handshake/terminal frames left in ``_pending_events`` before a turn.
|
|
1555
1548
|
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
"""``loop_input.intent_hint`` constants and client-side validation."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from typing import Final
|
|
6
|
-
|
|
7
|
-
TEXT_COMPLETION: Final = "text_completion"
|
|
8
|
-
IMAGE_TO_TEXT: Final = "image_to_text"
|
|
9
|
-
OCR: Final = "ocr"
|
|
10
|
-
EMBED: Final = "embed"
|
|
11
|
-
|
|
12
|
-
# Default deliverable phases for turn-ending replies (excludes plan_direct narration).
|
|
13
|
-
DEFAULT_DELIVERABLE_PHASES: frozenset[str] = frozenset(
|
|
14
|
-
{
|
|
15
|
-
"quiz",
|
|
16
|
-
"goal_completion",
|
|
17
|
-
"chitchat",
|
|
18
|
-
"text_completion",
|
|
19
|
-
"image_to_text",
|
|
20
|
-
"ocr",
|
|
21
|
-
"embed",
|
|
22
|
-
}
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
_REMOVED_INTENT_HINT_MESSAGES: dict[str, str] = {
|
|
26
|
-
"direct_llm": (
|
|
27
|
-
"intent_hint direct_llm is removed; "
|
|
28
|
-
"use text_completion (text-only) or image_to_text (with attachments)"
|
|
29
|
-
),
|
|
30
|
-
"quiz": "intent_hint quiz is removed; omit intent_hint and let intake classify the turn",
|
|
31
|
-
"direct_model": (
|
|
32
|
-
"intent_hint direct_model is removed; use text_completion, image_to_text, ocr, or embed"
|
|
33
|
-
),
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def validate_loop_input_intent_hint(hint: str) -> str | None:
|
|
38
|
-
"""Return an error message when ``hint`` is a removed legacy value.
|
|
39
|
-
|
|
40
|
-
Intent-hint values and agent-path pass-through values (e.g.
|
|
41
|
-
``resume_clarification``, ``skill:foo``) are allowed.
|
|
42
|
-
"""
|
|
43
|
-
key = hint.strip().lower()
|
|
44
|
-
return _REMOVED_INTENT_HINT_MESSAGES.get(key)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/src/soothe_client/appkit/__init__.py
RENAMED
|
File without changes
|
{soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/src/soothe_client/appkit/attachments.py
RENAMED
|
File without changes
|
{soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/src/soothe_client/appkit/broadcaster.py
RENAMED
|
File without changes
|
|
File without changes
|
{soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/src/soothe_client/appkit/chunk_filter.py
RENAMED
|
File without changes
|
{soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/src/soothe_client/appkit/classifier.py
RENAMED
|
File without changes
|
{soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/src/soothe_client/appkit/events.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/src/soothe_client/appkit/pool.py
RENAMED
|
File without changes
|
{soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/src/soothe_client/appkit/query_gate.py
RENAMED
|
File without changes
|
|
File without changes
|
{soothe_client_python-1.0.11 → soothe_client_python-1.0.13}/src/soothe_client/appkit/turn.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|