soothe-client-python 1.0.8__tar.gz → 1.0.10__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.8 → soothe_client_python-1.0.10}/PKG-INFO +1 -1
- {soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/appkit/daemon_session.py +64 -20
- {soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/appkit/turn_boundary.py +58 -10
- {soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/command_client.py +35 -31
- {soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/protocol_params.py +11 -0
- {soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/turn_boundary.py +57 -0
- {soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/.gitignore +0 -0
- {soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/LICENSE +0 -0
- {soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/README.md +0 -0
- {soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/pyproject.toml +0 -0
- {soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/__init__.py +0 -0
- {soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/appkit/__init__.py +0 -0
- {soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/appkit/attachments.py +0 -0
- {soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/appkit/broadcaster.py +0 -0
- {soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/appkit/card_projection.py +0 -0
- {soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/appkit/chunk_filter.py +0 -0
- {soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/appkit/classifier.py +0 -0
- {soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/appkit/events.py +0 -0
- {soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/appkit/loop_session_store.py +0 -0
- {soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/appkit/managed_client.py +0 -0
- {soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/appkit/observability.py +0 -0
- {soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/appkit/pool.py +0 -0
- {soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/appkit/query_gate.py +0 -0
- {soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/appkit/thinking_step.py +0 -0
- {soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/appkit/turn.py +0 -0
- {soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/appkit/turn_runner.py +0 -0
- {soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/errors.py +0 -0
- {soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/helpers.py +0 -0
- {soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/intent_hints.py +0 -0
- {soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/py.typed +0 -0
- {soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/schemas.py +0 -0
- {soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/session.py +0 -0
- {soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/stream_terminal.py +0 -0
- {soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/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.
|
|
3
|
+
Version: 1.0.10
|
|
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
|
|
@@ -24,7 +24,14 @@ from soothe_client.stream_terminal import (
|
|
|
24
24
|
is_turn_end_custom_data,
|
|
25
25
|
is_turn_progress_chunk,
|
|
26
26
|
)
|
|
27
|
-
from soothe_client.turn_boundary import
|
|
27
|
+
from soothe_client.turn_boundary import (
|
|
28
|
+
frame_seq,
|
|
29
|
+
frame_turn_id,
|
|
30
|
+
is_idle_terminal_allowed,
|
|
31
|
+
is_turn_terminal_allowed,
|
|
32
|
+
parse_turn_generation,
|
|
33
|
+
turn_ids_match,
|
|
34
|
+
)
|
|
28
35
|
from soothe_client.websocket import WebSocketClient
|
|
29
36
|
|
|
30
37
|
logger = logging.getLogger(__name__)
|
|
@@ -368,7 +375,6 @@ class DaemonSession:
|
|
|
368
375
|
# Bind turn_id only after this turn's status=running (do not reuse prior).
|
|
369
376
|
self._expected_turn_id = None
|
|
370
377
|
expected_turn_id: str | None = None
|
|
371
|
-
stream_payload_seen = False
|
|
372
378
|
turn_progress_seen = False
|
|
373
379
|
last_end_seq = int(getattr(self, "_last_turn_end_seq", 0) or 0)
|
|
374
380
|
self._last_turn_end_seq = last_end_seq
|
|
@@ -439,23 +445,37 @@ class DaemonSession:
|
|
|
439
445
|
if ev_seq is not None and last_end_seq > 0 and ev_seq <= last_end_seq:
|
|
440
446
|
continue
|
|
441
447
|
|
|
442
|
-
#
|
|
443
|
-
# which may rebind to a newer
|
|
444
|
-
# running can
|
|
448
|
+
# Once bound, drop absent/mismatched turn_id on event/status
|
|
449
|
+
# (except status=running, which may rebind to a newer gen;
|
|
450
|
+
# pre-admit early running can omit turn_id). Idle/stopped
|
|
451
|
+
# fall through to their gates (cancel idle may omit turn_id).
|
|
445
452
|
ev_turn_id = frame_turn_id(event)
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
453
|
+
status_state = str(event.get("state") or "") if event_type == "status" else ""
|
|
454
|
+
is_running_status = status_state == "running"
|
|
455
|
+
is_terminal_status = status_state in {"idle", "stopped"}
|
|
449
456
|
if (
|
|
450
457
|
expected_turn_id
|
|
451
|
-
and ev_turn_id
|
|
452
|
-
and ev_turn_id != expected_turn_id
|
|
453
458
|
and event_type in {"event", "status"}
|
|
454
459
|
and not is_running_status
|
|
460
|
+
and not is_terminal_status
|
|
461
|
+
and not turn_ids_match(expected_turn_id, ev_turn_id)
|
|
455
462
|
):
|
|
456
463
|
logger.debug(
|
|
457
|
-
"Ignoring mismatched turn_id frame type=%s got=%s expected=%s",
|
|
464
|
+
"Ignoring unbound/mismatched turn_id frame type=%s got=%s expected=%s",
|
|
458
465
|
event_type,
|
|
466
|
+
(ev_turn_id or "<absent>")[-24:],
|
|
467
|
+
expected_turn_id[-24:],
|
|
468
|
+
)
|
|
469
|
+
continue
|
|
470
|
+
if (
|
|
471
|
+
expected_turn_id
|
|
472
|
+
and is_terminal_status
|
|
473
|
+
and ev_turn_id
|
|
474
|
+
and not turn_ids_match(expected_turn_id, ev_turn_id)
|
|
475
|
+
):
|
|
476
|
+
logger.debug(
|
|
477
|
+
"Ignoring mismatched terminal status=%s got=%s expected=%s",
|
|
478
|
+
status_state,
|
|
459
479
|
ev_turn_id[-24:],
|
|
460
480
|
expected_turn_id[-24:],
|
|
461
481
|
)
|
|
@@ -478,6 +498,9 @@ class DaemonSession:
|
|
|
478
498
|
query_started = True
|
|
479
499
|
progress_seen = True
|
|
480
500
|
status_turn = frame_turn_id(event)
|
|
501
|
+
# Bind only from running frames that carry turn_id.
|
|
502
|
+
# Pre-admit early running omits turn_id and must not
|
|
503
|
+
# unlock terminals or poison the expected id.
|
|
481
504
|
if status_turn:
|
|
482
505
|
new_gen = parse_turn_generation(status_turn)
|
|
483
506
|
old_gen = parse_turn_generation(expected_turn_id)
|
|
@@ -487,11 +510,13 @@ class DaemonSession:
|
|
|
487
510
|
if expected_turn_id and status_turn != expected_turn_id:
|
|
488
511
|
# Successor turn admitted while this reader
|
|
489
512
|
# still held a stale/early binding.
|
|
490
|
-
stream_payload_seen = False
|
|
491
513
|
turn_progress_seen = False
|
|
492
514
|
expected_turn_id = status_turn
|
|
493
515
|
self._expected_turn_id = status_turn
|
|
494
516
|
elif query_started and state == "stopped":
|
|
517
|
+
stop_turn = frame_turn_id(event)
|
|
518
|
+
if expected_turn_id and not turn_ids_match(expected_turn_id, stop_turn):
|
|
519
|
+
continue
|
|
495
520
|
self.last_turn_end_state = state
|
|
496
521
|
if ev_seq is not None:
|
|
497
522
|
self._last_turn_end_seq = max(self._last_turn_end_seq, ev_seq)
|
|
@@ -501,7 +526,22 @@ class DaemonSession:
|
|
|
501
526
|
yield chunk
|
|
502
527
|
break
|
|
503
528
|
elif query_started and state == "idle":
|
|
504
|
-
|
|
529
|
+
idle_turn = frame_turn_id(event)
|
|
530
|
+
if not is_idle_terminal_allowed(
|
|
531
|
+
expected_turn_id=expected_turn_id,
|
|
532
|
+
frame_turn_id=idle_turn,
|
|
533
|
+
query_started=query_started,
|
|
534
|
+
turn_progress_seen=turn_progress_seen,
|
|
535
|
+
cancellation_seen=self.last_turn_cancellation_seen,
|
|
536
|
+
):
|
|
537
|
+
logger.debug(
|
|
538
|
+
"Ignoring premature idle "
|
|
539
|
+
"(loop=%s bound=%s idle_turn=%s progress=%s)",
|
|
540
|
+
(expected_loop_id or "?")[:16],
|
|
541
|
+
bool(expected_turn_id),
|
|
542
|
+
(idle_turn or "<absent>")[-24:],
|
|
543
|
+
turn_progress_seen,
|
|
544
|
+
)
|
|
505
545
|
continue
|
|
506
546
|
self.last_turn_end_state = state
|
|
507
547
|
if ev_seq is not None:
|
|
@@ -529,30 +569,34 @@ class DaemonSession:
|
|
|
529
569
|
self.turn_event_stats.filtered_early += 1
|
|
530
570
|
continue
|
|
531
571
|
|
|
532
|
-
#
|
|
572
|
+
# Strict turn_id match: absent ids never unlock turn-end.
|
|
533
573
|
if mode == "custom" and is_turn_end_custom_data(data):
|
|
534
574
|
data_turn = (
|
|
535
575
|
frame_turn_id(data if isinstance(data, dict) else None) or ev_turn_id
|
|
536
576
|
)
|
|
537
|
-
turn_ok = (
|
|
538
|
-
|
|
577
|
+
turn_ok = is_turn_terminal_allowed(
|
|
578
|
+
expected_turn_id=expected_turn_id,
|
|
579
|
+
frame_turn_id=data_turn,
|
|
580
|
+
query_started=query_started,
|
|
581
|
+
turn_progress_seen=turn_progress_seen,
|
|
539
582
|
)
|
|
540
|
-
if not turn_ok
|
|
583
|
+
if not turn_ok:
|
|
541
584
|
logger.debug(
|
|
542
585
|
"Ignoring premature turn-end frame %s "
|
|
543
|
-
"(loop=%s query_started=%s progress=%s
|
|
586
|
+
"(loop=%s query_started=%s progress=%s "
|
|
587
|
+
"bound=%s data_turn=%s)",
|
|
544
588
|
str(data.get("type", "")).strip()
|
|
545
589
|
if isinstance(data, dict)
|
|
546
590
|
else "?",
|
|
547
591
|
(expected_loop_id or "?")[:16],
|
|
548
592
|
query_started,
|
|
549
593
|
turn_progress_seen,
|
|
550
|
-
|
|
594
|
+
bool(expected_turn_id),
|
|
595
|
+
(data_turn or "<absent>")[-24:],
|
|
551
596
|
)
|
|
552
597
|
continue
|
|
553
598
|
|
|
554
599
|
progress_seen = True
|
|
555
|
-
stream_payload_seen = True
|
|
556
600
|
if is_turn_progress_chunk(mode, data):
|
|
557
601
|
turn_progress_seen = True
|
|
558
602
|
yield (namespace, mode, data)
|
{soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/appkit/turn_boundary.py
RENAMED
|
@@ -14,6 +14,13 @@ from soothe_client.stream_terminal import (
|
|
|
14
14
|
is_turn_end_custom_data,
|
|
15
15
|
is_turn_progress_chunk,
|
|
16
16
|
)
|
|
17
|
+
from soothe_client.turn_boundary import (
|
|
18
|
+
frame_turn_id,
|
|
19
|
+
is_idle_terminal_allowed,
|
|
20
|
+
is_turn_terminal_allowed,
|
|
21
|
+
parse_turn_generation,
|
|
22
|
+
turn_ids_match,
|
|
23
|
+
)
|
|
17
24
|
|
|
18
25
|
TURN_END_STREAM_END = STREAM_END
|
|
19
26
|
TURN_END_IDLE = "status.idle"
|
|
@@ -25,8 +32,9 @@ class TurnLifecycleGate:
|
|
|
25
32
|
"""Per-turn progress flags (DaemonSession parity; not shared across chats)."""
|
|
26
33
|
|
|
27
34
|
saw_running: bool = False
|
|
28
|
-
saw_stream_payload: bool = False
|
|
29
35
|
saw_turn_progress: bool = False
|
|
36
|
+
expected_turn_id: str | None = None
|
|
37
|
+
cancellation_seen: bool = False
|
|
30
38
|
|
|
31
39
|
def observe(self, msg: Any) -> None:
|
|
32
40
|
frame = _normalize_frame(msg)
|
|
@@ -36,18 +44,38 @@ class TurnLifecycleGate:
|
|
|
36
44
|
if typ == "status":
|
|
37
45
|
if str(frame.get("state") or "").strip().lower() == "running":
|
|
38
46
|
self.saw_running = True
|
|
47
|
+
status_turn = frame_turn_id(frame)
|
|
48
|
+
if status_turn:
|
|
49
|
+
new_gen = parse_turn_generation(status_turn)
|
|
50
|
+
old_gen = parse_turn_generation(self.expected_turn_id)
|
|
51
|
+
if self.expected_turn_id is None or (
|
|
52
|
+
new_gen is not None and (old_gen is None or new_gen >= old_gen)
|
|
53
|
+
):
|
|
54
|
+
if self.expected_turn_id and status_turn != self.expected_turn_id:
|
|
55
|
+
self.saw_turn_progress = False
|
|
56
|
+
self.expected_turn_id = status_turn
|
|
39
57
|
return
|
|
40
58
|
if typ == "event":
|
|
41
|
-
self.saw_stream_payload = True
|
|
42
59
|
mode = str(frame.get("mode") or "")
|
|
43
60
|
if is_turn_progress_chunk(mode, frame.get("data")):
|
|
44
61
|
self.saw_turn_progress = True
|
|
45
62
|
|
|
46
|
-
def allow_stream_end(self) -> bool:
|
|
47
|
-
return
|
|
63
|
+
def allow_stream_end(self, frame_turn: str | None) -> bool:
|
|
64
|
+
return is_turn_terminal_allowed(
|
|
65
|
+
expected_turn_id=self.expected_turn_id,
|
|
66
|
+
frame_turn_id=frame_turn,
|
|
67
|
+
query_started=self.saw_running,
|
|
68
|
+
turn_progress_seen=self.saw_turn_progress,
|
|
69
|
+
)
|
|
48
70
|
|
|
49
|
-
def allow_idle_complete(self) -> bool:
|
|
50
|
-
return
|
|
71
|
+
def allow_idle_complete(self, frame_turn: str | None) -> bool:
|
|
72
|
+
return is_idle_terminal_allowed(
|
|
73
|
+
expected_turn_id=self.expected_turn_id,
|
|
74
|
+
frame_turn_id=frame_turn,
|
|
75
|
+
query_started=self.saw_running,
|
|
76
|
+
turn_progress_seen=self.saw_turn_progress,
|
|
77
|
+
cancellation_seen=self.cancellation_seen,
|
|
78
|
+
)
|
|
51
79
|
|
|
52
80
|
|
|
53
81
|
@dataclass
|
|
@@ -69,16 +97,28 @@ class TurnBoundary:
|
|
|
69
97
|
typ = str(frame.get("type") or "")
|
|
70
98
|
if typ == "status":
|
|
71
99
|
state = str(frame.get("state") or "").strip().lower()
|
|
100
|
+
frame_turn = frame_turn_id(frame)
|
|
72
101
|
if state == "stopped" and self.gate.saw_running:
|
|
102
|
+
if self.gate.expected_turn_id and not turn_ids_match(
|
|
103
|
+
self.gate.expected_turn_id, frame_turn
|
|
104
|
+
):
|
|
105
|
+
return False, ""
|
|
73
106
|
return self._mark(TURN_END_STOPPED)
|
|
74
|
-
if state == "idle" and self.gate.allow_idle_complete():
|
|
107
|
+
if state == "idle" and self.gate.allow_idle_complete(frame_turn):
|
|
75
108
|
return self._mark(TURN_END_IDLE)
|
|
76
109
|
return False, ""
|
|
77
110
|
|
|
78
111
|
if typ == "event":
|
|
79
112
|
mode = str(frame.get("mode") or "")
|
|
80
113
|
data = frame.get("data")
|
|
81
|
-
|
|
114
|
+
data_turn = frame_turn_id(data if isinstance(data, dict) else None) or frame_turn_id(
|
|
115
|
+
frame
|
|
116
|
+
)
|
|
117
|
+
if (
|
|
118
|
+
mode == "custom"
|
|
119
|
+
and is_turn_end_custom_data(data)
|
|
120
|
+
and self.gate.allow_stream_end(data_turn)
|
|
121
|
+
):
|
|
82
122
|
return self._mark(TURN_END_STREAM_END)
|
|
83
123
|
return False, ""
|
|
84
124
|
|
|
@@ -109,19 +149,27 @@ def _normalize_frame(msg: Any) -> dict[str, Any] | None:
|
|
|
109
149
|
if isinstance(inner, dict) and inner.get("type") == "status":
|
|
110
150
|
return inner
|
|
111
151
|
if isinstance(inner, dict) and inner.get("mode"):
|
|
112
|
-
|
|
152
|
+
out = {
|
|
113
153
|
"type": "event",
|
|
114
154
|
"mode": inner.get("mode"),
|
|
115
155
|
"data": inner.get("data"),
|
|
116
156
|
"namespace": inner.get("namespace") or payload.get("namespace"),
|
|
117
157
|
}
|
|
158
|
+
tid = inner.get("turn_id") or payload.get("turn_id") or msg.get("turn_id")
|
|
159
|
+
if tid:
|
|
160
|
+
out["turn_id"] = tid
|
|
161
|
+
return out
|
|
118
162
|
mode = payload.get("mode")
|
|
119
163
|
if mode:
|
|
120
|
-
|
|
164
|
+
out = {
|
|
121
165
|
"type": "event",
|
|
122
166
|
"mode": mode,
|
|
123
167
|
"data": payload.get("data"),
|
|
124
168
|
"namespace": payload.get("namespace"),
|
|
125
169
|
}
|
|
170
|
+
tid = payload.get("turn_id") or msg.get("turn_id")
|
|
171
|
+
if tid:
|
|
172
|
+
out["turn_id"] = tid
|
|
173
|
+
return out
|
|
126
174
|
return None
|
|
127
175
|
return msg
|
{soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/command_client.py
RENAMED
|
@@ -132,34 +132,6 @@ async def _perform_handshake(ws: Any, *, timeout: float) -> None:
|
|
|
132
132
|
raise RuntimeError(f"Daemon state is {state}")
|
|
133
133
|
|
|
134
134
|
|
|
135
|
-
# Command message types
|
|
136
|
-
_AUTOPilot_COMMANDS = {
|
|
137
|
-
"status": "autopilot_status",
|
|
138
|
-
"submit": "autopilot_submit",
|
|
139
|
-
"list_goals": "autopilot_list_goals",
|
|
140
|
-
"get_goal": "autopilot_get_goal",
|
|
141
|
-
"cancel_goal": "autopilot_cancel_goal",
|
|
142
|
-
"cancel_all": "autopilot_cancel_all",
|
|
143
|
-
"wake": "autopilot_wake",
|
|
144
|
-
"dream": "autopilot_dream",
|
|
145
|
-
"resume": "autopilot_resume",
|
|
146
|
-
"list_jobs": "autopilot_list_jobs",
|
|
147
|
-
"get_job": "autopilot_get_job",
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
_CRON_COMMANDS = {
|
|
151
|
-
"add": "cron_add",
|
|
152
|
-
"list": "cron_list",
|
|
153
|
-
"list_jobs": "cron_list",
|
|
154
|
-
"show": "cron_show",
|
|
155
|
-
"cancel": "cron_cancel",
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
_MEMORY_COMMANDS = {
|
|
159
|
-
"stats": "memory_stats",
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
|
|
163
135
|
class AsyncCommandClient:
|
|
164
136
|
"""Async client for one-shot daemon RPCs (jobs, autopilot, cron).
|
|
165
137
|
|
|
@@ -270,12 +242,19 @@ class AsyncCommandClient:
|
|
|
270
242
|
return await self._send_command("autopilot_status")
|
|
271
243
|
|
|
272
244
|
async def autopilot_submit(
|
|
273
|
-
self,
|
|
245
|
+
self,
|
|
246
|
+
description: str,
|
|
247
|
+
*,
|
|
248
|
+
priority: int = 50,
|
|
249
|
+
workspace: str | None = None,
|
|
250
|
+
rail_id: str | None = None,
|
|
274
251
|
) -> dict[str, Any]:
|
|
275
252
|
"""Submit a new autopilot goal (returns ``goal_id``)."""
|
|
276
253
|
payload: dict[str, Any] = {"description": description, "priority": priority}
|
|
277
254
|
if workspace:
|
|
278
255
|
payload["workspace"] = workspace
|
|
256
|
+
if rail_id:
|
|
257
|
+
payload["rail_id"] = rail_id
|
|
279
258
|
return await self._send_command("autopilot_submit", payload)
|
|
280
259
|
|
|
281
260
|
async def autopilot_list_goals(self) -> dict[str, Any]:
|
|
@@ -318,6 +297,14 @@ class AsyncCommandClient:
|
|
|
318
297
|
"""Get a root job with DAG snapshot. Prefer ``job_status`` / ``job_dag``."""
|
|
319
298
|
return await self._send_command("autopilot_get_job", {"job_id": job_id})
|
|
320
299
|
|
|
300
|
+
async def autopilot_top(self, *, include_terminal: bool = False) -> dict[str, Any]:
|
|
301
|
+
"""Jobs → goals → loops snapshot for CLI ``top`` (IG-679 / IG-688).
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
include_terminal: When true, include terminal goals and jobs.
|
|
305
|
+
"""
|
|
306
|
+
return await self._send_command("autopilot_top", {"include_terminal": include_terminal})
|
|
307
|
+
|
|
321
308
|
async def autopilot_subscribe(self) -> dict[str, Any]:
|
|
322
309
|
"""Subscribe this connection to autopilot worker events."""
|
|
323
310
|
return await self._send_command("autopilot_subscribe")
|
|
@@ -442,13 +429,23 @@ class CommandClient:
|
|
|
442
429
|
return cast(dict[str, Any], self._run_async(self._client.autopilot_status()))
|
|
443
430
|
|
|
444
431
|
def autopilot_submit(
|
|
445
|
-
self,
|
|
432
|
+
self,
|
|
433
|
+
description: str,
|
|
434
|
+
*,
|
|
435
|
+
priority: int = 50,
|
|
436
|
+
workspace: str | None = None,
|
|
437
|
+
rail_id: str | None = None,
|
|
446
438
|
) -> dict[str, Any]:
|
|
447
439
|
"""Submit a new autopilot goal."""
|
|
448
440
|
return cast(
|
|
449
441
|
dict[str, Any],
|
|
450
442
|
self._run_async(
|
|
451
|
-
self._client.autopilot_submit(
|
|
443
|
+
self._client.autopilot_submit(
|
|
444
|
+
description,
|
|
445
|
+
priority=priority,
|
|
446
|
+
workspace=workspace,
|
|
447
|
+
rail_id=rail_id,
|
|
448
|
+
)
|
|
452
449
|
),
|
|
453
450
|
)
|
|
454
451
|
|
|
@@ -488,6 +485,13 @@ class CommandClient:
|
|
|
488
485
|
"""Get a root job with DAG snapshot."""
|
|
489
486
|
return cast(dict[str, Any], self._run_async(self._client.autopilot_get_job(job_id)))
|
|
490
487
|
|
|
488
|
+
def autopilot_top(self, *, include_terminal: bool = False) -> dict[str, Any]:
|
|
489
|
+
"""Jobs → goals → loops snapshot for CLI ``top``."""
|
|
490
|
+
return cast(
|
|
491
|
+
dict[str, Any],
|
|
492
|
+
self._run_async(self._client.autopilot_top(include_terminal=include_terminal)),
|
|
493
|
+
)
|
|
494
|
+
|
|
491
495
|
def autopilot_subscribe(self) -> dict[str, Any]:
|
|
492
496
|
"""Subscribe to autopilot worker events."""
|
|
493
497
|
return cast(dict[str, Any], self._run_async(self._client.autopilot_subscribe()))
|
{soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/protocol_params.py
RENAMED
|
@@ -38,6 +38,7 @@ __all__ = [
|
|
|
38
38
|
"AutopilotResumeParams",
|
|
39
39
|
"AutopilotListJobsParams",
|
|
40
40
|
"AutopilotGetJobParams",
|
|
41
|
+
"AutopilotTopParams",
|
|
41
42
|
# Job RPC params
|
|
42
43
|
"JobCreateParams",
|
|
43
44
|
"JobStatusParams",
|
|
@@ -401,6 +402,16 @@ class AutopilotGetJobParams(ParamsBase):
|
|
|
401
402
|
job_id: str = Field(..., min_length=1)
|
|
402
403
|
|
|
403
404
|
|
|
405
|
+
class AutopilotTopParams(ParamsBase):
|
|
406
|
+
"""Params for ``method=autopilot_top`` (IG-679 / IG-688).
|
|
407
|
+
|
|
408
|
+
Attributes:
|
|
409
|
+
include_terminal: Include terminal goals/jobs when true (default false).
|
|
410
|
+
"""
|
|
411
|
+
|
|
412
|
+
include_terminal: bool = False
|
|
413
|
+
|
|
414
|
+
|
|
404
415
|
# ---------------------------------------------------------------------------
|
|
405
416
|
# Job RPC params
|
|
406
417
|
# ---------------------------------------------------------------------------
|
{soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/turn_boundary.py
RENAMED
|
@@ -61,9 +61,66 @@ def frame_seq(frame: dict[str, Any] | None) -> int | None:
|
|
|
61
61
|
return None
|
|
62
62
|
|
|
63
63
|
|
|
64
|
+
def turn_ids_match(expected: str | None, candidate: str | None) -> bool:
|
|
65
|
+
"""True when both ids are non-empty and equal.
|
|
66
|
+
|
|
67
|
+
Absent ids never match — callers must not treat missing ``turn_id`` as
|
|
68
|
+
compatible with a bound turn (prior-goal terminals / idle can omit or
|
|
69
|
+
carry a stale generation).
|
|
70
|
+
"""
|
|
71
|
+
exp = str(expected or "").strip()
|
|
72
|
+
cand = str(candidate or "").strip()
|
|
73
|
+
return bool(exp) and bool(cand) and exp == cand
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def is_turn_terminal_allowed(
|
|
77
|
+
*,
|
|
78
|
+
expected_turn_id: str | None,
|
|
79
|
+
frame_turn_id: str | None,
|
|
80
|
+
query_started: bool,
|
|
81
|
+
turn_progress_seen: bool,
|
|
82
|
+
) -> bool:
|
|
83
|
+
"""Gate for turn-scoped ``stream.end`` / ``strange_loop.completed``.
|
|
84
|
+
|
|
85
|
+
Requires an active query, real turn progress, a bound ``expected_turn_id``,
|
|
86
|
+
and an exact ``frame_turn_id`` match. Absent ids are rejected.
|
|
87
|
+
"""
|
|
88
|
+
if not query_started or not turn_progress_seen:
|
|
89
|
+
return False
|
|
90
|
+
return turn_ids_match(expected_turn_id, frame_turn_id)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def is_idle_terminal_allowed(
|
|
94
|
+
*,
|
|
95
|
+
expected_turn_id: str | None,
|
|
96
|
+
frame_turn_id: str | None,
|
|
97
|
+
query_started: bool,
|
|
98
|
+
turn_progress_seen: bool,
|
|
99
|
+
cancellation_seen: bool = False,
|
|
100
|
+
) -> bool:
|
|
101
|
+
"""Gate for ``status=idle`` soft-complete.
|
|
102
|
+
|
|
103
|
+
Requires a bound turn. Matching ``turn_id`` needs progress or cancel.
|
|
104
|
+
Absent idle ``turn_id`` is allowed only when cancellation was already seen
|
|
105
|
+
(legacy cancel finalize); mismatched ids are always rejected.
|
|
106
|
+
"""
|
|
107
|
+
if not query_started or not str(expected_turn_id or "").strip():
|
|
108
|
+
return False
|
|
109
|
+
cand = str(frame_turn_id or "").strip()
|
|
110
|
+
if cand:
|
|
111
|
+
if not turn_ids_match(expected_turn_id, cand):
|
|
112
|
+
return False
|
|
113
|
+
return bool(turn_progress_seen or cancellation_seen)
|
|
114
|
+
# Absent idle turn_id: never end a healthy turn; cancel path only.
|
|
115
|
+
return bool(cancellation_seen)
|
|
116
|
+
|
|
117
|
+
|
|
64
118
|
__all__ = [
|
|
65
119
|
"format_turn_id",
|
|
66
120
|
"frame_seq",
|
|
67
121
|
"frame_turn_id",
|
|
122
|
+
"is_idle_terminal_allowed",
|
|
123
|
+
"is_turn_terminal_allowed",
|
|
68
124
|
"parse_turn_generation",
|
|
125
|
+
"turn_ids_match",
|
|
69
126
|
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/appkit/__init__.py
RENAMED
|
File without changes
|
{soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/appkit/attachments.py
RENAMED
|
File without changes
|
{soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/appkit/broadcaster.py
RENAMED
|
File without changes
|
|
File without changes
|
{soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/appkit/chunk_filter.py
RENAMED
|
File without changes
|
{soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/appkit/classifier.py
RENAMED
|
File without changes
|
{soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/appkit/events.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/appkit/observability.py
RENAMED
|
File without changes
|
|
File without changes
|
{soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/appkit/query_gate.py
RENAMED
|
File without changes
|
{soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/appkit/thinking_step.py
RENAMED
|
File without changes
|
|
File without changes
|
{soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/appkit/turn_runner.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/intent_hints.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{soothe_client_python-1.0.8 → soothe_client_python-1.0.10}/src/soothe_client/stream_terminal.py
RENAMED
|
File without changes
|
|
File without changes
|