soothe-client-python 0.9.6__tar.gz → 0.9.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/PKG-INFO +1 -1
- {soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/pyproject.toml +6 -0
- {soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/src/soothe_client/appkit/classifier.py +16 -12
- {soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/src/soothe_client/appkit/daemon_session.py +82 -22
- {soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/src/soothe_client/appkit/managed_client.py +20 -14
- {soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/src/soothe_client/appkit/turn_runner.py +22 -19
- {soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/src/soothe_client/helpers.py +11 -9
- soothe_client_python-0.9.8/src/soothe_client/py.typed +0 -0
- soothe_client_python-0.9.8/src/soothe_client/stream_terminal.py +113 -0
- soothe_client_python-0.9.8/src/soothe_client/turn_boundary.py +54 -0
- {soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/src/soothe_client/websocket.py +14 -51
- {soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/src/soothe_client/ws_command_client.py +51 -39
- {soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/.gitignore +0 -0
- {soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/LICENSE +0 -0
- {soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/README.md +0 -0
- {soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/src/soothe_client/__init__.py +0 -0
- {soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/src/soothe_client/appkit/__init__.py +0 -0
- {soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/src/soothe_client/appkit/attachments.py +0 -0
- {soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/src/soothe_client/appkit/broadcaster.py +0 -0
- {soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/src/soothe_client/appkit/chunk_filter.py +0 -0
- {soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/src/soothe_client/appkit/events.py +0 -0
- {soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/src/soothe_client/appkit/observability.py +0 -0
- {soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/src/soothe_client/appkit/pool.py +0 -0
- {soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/src/soothe_client/appkit/query_gate.py +0 -0
- {soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/src/soothe_client/appkit/session_store.py +0 -0
- {soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/src/soothe_client/appkit/thinking_step.py +0 -0
- {soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/src/soothe_client/appkit/turn.py +0 -0
- {soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/src/soothe_client/errors.py +0 -0
- {soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/src/soothe_client/intent_hints.py +0 -0
- {soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/src/soothe_client/protocol_params.py +0 -0
- {soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/src/soothe_client/schemas.py +0 -0
- {soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/src/soothe_client/session.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: soothe-client-python
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.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
|
|
@@ -74,6 +74,12 @@ python_version = "3.12"
|
|
|
74
74
|
warn_return_any = true
|
|
75
75
|
warn_unused_configs = true
|
|
76
76
|
disallow_untyped_defs = true
|
|
77
|
+
packages = ["soothe_client"]
|
|
78
|
+
mypy_path = "src"
|
|
79
|
+
|
|
80
|
+
[[tool.mypy.overrides]]
|
|
81
|
+
module = ["soothe_sdk", "soothe_sdk.*"]
|
|
82
|
+
ignore_missing_imports = true
|
|
77
83
|
|
|
78
84
|
[tool.pytest.ini_options]
|
|
79
85
|
asyncio_mode = "auto"
|
{soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/src/soothe_client/appkit/classifier.py
RENAMED
|
@@ -155,13 +155,12 @@ class EventClassifier:
|
|
|
155
155
|
return ChatEventResult(terminal=ChatEventTerminal.CONTINUE)
|
|
156
156
|
|
|
157
157
|
if typ == "error":
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
)
|
|
158
|
+
raw_err = msg.get("error")
|
|
159
|
+
err_obj: dict[str, Any] = raw_err if isinstance(raw_err, dict) else {}
|
|
160
|
+
raw_code = err_obj.get("code")
|
|
161
|
+
code = raw_code if isinstance(raw_code, int) else -32603
|
|
162
|
+
raw_message = err_obj.get("message")
|
|
163
|
+
message = raw_message if isinstance(raw_message, str) else "daemon error"
|
|
165
164
|
return self._failed_result(DaemonError(code, message, err_obj.get("data")))
|
|
166
165
|
|
|
167
166
|
if typ == "event":
|
|
@@ -174,8 +173,10 @@ class EventClassifier:
|
|
|
174
173
|
return ChatEventResult(terminal=ChatEventTerminal.CONTINUE)
|
|
175
174
|
|
|
176
175
|
def _classify_next_envelope(self, env: dict[str, Any]) -> ChatEventResult:
|
|
177
|
-
|
|
178
|
-
|
|
176
|
+
raw_payload = env.get("payload")
|
|
177
|
+
payload: dict[str, Any] = raw_payload if isinstance(raw_payload, dict) else {}
|
|
178
|
+
raw_inner = payload.get("data")
|
|
179
|
+
inner_data: dict[str, Any] | None = raw_inner if isinstance(raw_inner, dict) else None
|
|
179
180
|
if inner_data is not None:
|
|
180
181
|
inner_mode = str(inner_data.get("mode") or "")
|
|
181
182
|
if inner_mode:
|
|
@@ -372,8 +373,10 @@ def _first_message_payload(data: Any) -> tuple[str, str, str, bool]:
|
|
|
372
373
|
msg_map = data[0]
|
|
373
374
|
if not isinstance(msg_map, dict):
|
|
374
375
|
return "", "", "", False
|
|
375
|
-
|
|
376
|
-
|
|
376
|
+
type_raw = msg_map.get("type")
|
|
377
|
+
msg_type = type_raw if isinstance(type_raw, str) else ""
|
|
378
|
+
phase_raw = msg_map.get("phase")
|
|
379
|
+
phase = phase_raw if isinstance(phase_raw, str) else ""
|
|
377
380
|
content = _extract_content_from_message(msg_map)
|
|
378
381
|
return msg_type, content, phase, True
|
|
379
382
|
|
|
@@ -388,7 +391,8 @@ def _loop_ai_message(data: Any) -> dict[str, str] | None:
|
|
|
388
391
|
phase = phase_raw.strip() if isinstance(phase_raw, str) else ""
|
|
389
392
|
if not phase:
|
|
390
393
|
return None
|
|
391
|
-
|
|
394
|
+
type_raw = msg_map.get("type")
|
|
395
|
+
msg_type = type_raw if isinstance(type_raw, str) else ""
|
|
392
396
|
content = _extract_content_from_message(msg_map)
|
|
393
397
|
return {"type": msg_type, "content": content, "phase": phase}
|
|
394
398
|
|
{soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/src/soothe_client/appkit/daemon_session.py
RENAMED
|
@@ -18,18 +18,18 @@ from soothe_client.appkit.chunk_filter import should_drop_stream_chunk_early
|
|
|
18
18
|
from soothe_client.appkit.events import unwrap_next
|
|
19
19
|
from soothe_client.appkit.observability import TurnEventStats
|
|
20
20
|
from soothe_client.session import bootstrap_loop_session, connect_websocket_with_retries
|
|
21
|
+
from soothe_client.stream_terminal import (
|
|
22
|
+
STREAM_END,
|
|
23
|
+
is_turn_end_custom_data,
|
|
24
|
+
is_turn_progress_chunk,
|
|
25
|
+
)
|
|
26
|
+
from soothe_client.turn_boundary import frame_seq, frame_turn_id
|
|
21
27
|
from soothe_client.websocket import WebSocketClient
|
|
22
28
|
|
|
23
29
|
logger = logging.getLogger(__name__)
|
|
24
30
|
|
|
25
31
|
DEFAULT_POST_IDLE_DRAIN_S = 0.5
|
|
26
32
|
_RPC_HANDSHAKE_TIMEOUT_S = 20.0
|
|
27
|
-
_TURN_END_CUSTOM_TYPES = frozenset(
|
|
28
|
-
{
|
|
29
|
-
"soothe.cognition.strange_loop.completed",
|
|
30
|
-
"soothe.stream.end",
|
|
31
|
-
}
|
|
32
|
-
)
|
|
33
33
|
|
|
34
34
|
EarlyDropFn = Callable[[tuple[Any, ...], str, Any], bool]
|
|
35
35
|
StatsFactory = Callable[[], Any]
|
|
@@ -67,6 +67,9 @@ class DaemonSession:
|
|
|
67
67
|
self.last_turn_end_state: str | None = None
|
|
68
68
|
self.last_turn_cancellation_seen: bool = False
|
|
69
69
|
self.last_turn_error_message: str | None = None
|
|
70
|
+
# IG-659: last completed turn's seq floor (drop stale prior-turn frames).
|
|
71
|
+
self._last_turn_end_seq: int = 0
|
|
72
|
+
self._expected_turn_id: str | None = None
|
|
70
73
|
|
|
71
74
|
@property
|
|
72
75
|
def client(self) -> WebSocketClient:
|
|
@@ -315,7 +318,13 @@ class DaemonSession:
|
|
|
315
318
|
inbound_dropped_baseline = getattr(self._client, "inbound_dropped", 0)
|
|
316
319
|
query_started = False
|
|
317
320
|
expected_loop_id = self._loop_id
|
|
321
|
+
# Bind turn_id only after this turn's status=running (do not reuse prior).
|
|
322
|
+
self._expected_turn_id = None
|
|
323
|
+
expected_turn_id: str | None = None
|
|
318
324
|
stream_payload_seen = False
|
|
325
|
+
turn_progress_seen = False
|
|
326
|
+
last_end_seq = int(getattr(self, "_last_turn_end_seq", 0) or 0)
|
|
327
|
+
self._last_turn_end_seq = last_end_seq
|
|
319
328
|
self._streaming = True
|
|
320
329
|
turn_read_started = time.monotonic()
|
|
321
330
|
first_event_logged = False
|
|
@@ -353,9 +362,10 @@ class DaemonSession:
|
|
|
353
362
|
if event and not first_event_logged:
|
|
354
363
|
first_event_logged = True
|
|
355
364
|
logger.debug(
|
|
356
|
-
"First daemon event on turn: type=%s loop_id=%s",
|
|
365
|
+
"First daemon event on turn: type=%s loop_id=%s turn_id=%s",
|
|
357
366
|
event.get("type"),
|
|
358
367
|
event.get("loop_id"),
|
|
368
|
+
frame_turn_id(event),
|
|
359
369
|
)
|
|
360
370
|
if not event:
|
|
361
371
|
if query_started and not self._client.is_connection_alive():
|
|
@@ -377,6 +387,27 @@ class DaemonSession:
|
|
|
377
387
|
):
|
|
378
388
|
continue
|
|
379
389
|
|
|
390
|
+
# IG-659 phase 5: drop frames at or before prior turn end seq.
|
|
391
|
+
ev_seq = frame_seq(event)
|
|
392
|
+
if ev_seq is not None and last_end_seq > 0 and ev_seq <= last_end_seq:
|
|
393
|
+
continue
|
|
394
|
+
|
|
395
|
+
# IG-659 phases 1–2: drop mismatched turn_id once bound.
|
|
396
|
+
ev_turn_id = frame_turn_id(event)
|
|
397
|
+
if (
|
|
398
|
+
expected_turn_id
|
|
399
|
+
and ev_turn_id
|
|
400
|
+
and ev_turn_id != expected_turn_id
|
|
401
|
+
and event_type in {"event", "status"}
|
|
402
|
+
):
|
|
403
|
+
logger.debug(
|
|
404
|
+
"Ignoring mismatched turn_id frame type=%s got=%s expected=%s",
|
|
405
|
+
event_type,
|
|
406
|
+
ev_turn_id[-24:],
|
|
407
|
+
expected_turn_id[-24:],
|
|
408
|
+
)
|
|
409
|
+
continue
|
|
410
|
+
|
|
380
411
|
if event_type == "error":
|
|
381
412
|
err_obj = event.get("error") or {}
|
|
382
413
|
err_msg = str(
|
|
@@ -393,8 +424,14 @@ class DaemonSession:
|
|
|
393
424
|
if state == "running":
|
|
394
425
|
query_started = True
|
|
395
426
|
progress_seen = True
|
|
427
|
+
status_turn = frame_turn_id(event)
|
|
428
|
+
if status_turn:
|
|
429
|
+
expected_turn_id = status_turn
|
|
430
|
+
self._expected_turn_id = status_turn
|
|
396
431
|
elif query_started and state == "stopped":
|
|
397
432
|
self.last_turn_end_state = state
|
|
433
|
+
if ev_seq is not None:
|
|
434
|
+
self._last_turn_end_seq = max(self._last_turn_end_seq, ev_seq)
|
|
398
435
|
async for chunk in self._drain_stream_events_after_idle(
|
|
399
436
|
expected_loop_id=expected_loop_id,
|
|
400
437
|
):
|
|
@@ -404,6 +441,8 @@ class DaemonSession:
|
|
|
404
441
|
if not stream_payload_seen and not self.last_turn_cancellation_seen:
|
|
405
442
|
continue
|
|
406
443
|
self.last_turn_end_state = state
|
|
444
|
+
if ev_seq is not None:
|
|
445
|
+
self._last_turn_end_seq = max(self._last_turn_end_seq, ev_seq)
|
|
407
446
|
async for chunk in self._drain_stream_events_after_idle(
|
|
408
447
|
expected_loop_id=expected_loop_id,
|
|
409
448
|
):
|
|
@@ -426,25 +465,46 @@ class DaemonSession:
|
|
|
426
465
|
if self._should_drop(namespace, mode, data):
|
|
427
466
|
self.turn_event_stats.filtered_early += 1
|
|
428
467
|
continue
|
|
468
|
+
|
|
469
|
+
# Prefer turn_id match when present; else IG-658 progress gate.
|
|
470
|
+
if mode == "custom" and is_turn_end_custom_data(data):
|
|
471
|
+
data_turn = (
|
|
472
|
+
frame_turn_id(data if isinstance(data, dict) else None) or ev_turn_id
|
|
473
|
+
)
|
|
474
|
+
turn_ok = (
|
|
475
|
+
not expected_turn_id or not data_turn or data_turn == expected_turn_id
|
|
476
|
+
)
|
|
477
|
+
if not turn_ok or not query_started or not turn_progress_seen:
|
|
478
|
+
logger.debug(
|
|
479
|
+
"Ignoring premature turn-end frame %s "
|
|
480
|
+
"(loop=%s query_started=%s progress=%s turn_ok=%s)",
|
|
481
|
+
str(data.get("type", "")).strip()
|
|
482
|
+
if isinstance(data, dict)
|
|
483
|
+
else "?",
|
|
484
|
+
(expected_loop_id or "?")[:16],
|
|
485
|
+
query_started,
|
|
486
|
+
turn_progress_seen,
|
|
487
|
+
turn_ok,
|
|
488
|
+
)
|
|
489
|
+
continue
|
|
490
|
+
|
|
429
491
|
progress_seen = True
|
|
430
492
|
stream_payload_seen = True
|
|
493
|
+
if is_turn_progress_chunk(mode, data):
|
|
494
|
+
turn_progress_seen = True
|
|
431
495
|
yield (namespace, mode, data)
|
|
432
|
-
if mode == "custom" and
|
|
496
|
+
if mode == "custom" and is_turn_end_custom_data(data):
|
|
433
497
|
custom_type = str(data.get("type", "")).strip()
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
expected_loop_id=expected_loop_id,
|
|
445
|
-
):
|
|
446
|
-
yield chunk
|
|
447
|
-
break
|
|
498
|
+
self.last_turn_end_state = (
|
|
499
|
+
"stream_end" if custom_type == STREAM_END else "completed"
|
|
500
|
+
)
|
|
501
|
+
if ev_seq is not None:
|
|
502
|
+
self._last_turn_end_seq = max(self._last_turn_end_seq, ev_seq)
|
|
503
|
+
async for chunk in self._drain_stream_events_after_idle(
|
|
504
|
+
expected_loop_id=expected_loop_id,
|
|
505
|
+
):
|
|
506
|
+
yield chunk
|
|
507
|
+
break
|
|
448
508
|
if mode == "updates" and isinstance(data, dict) and "__interrupt__" in data:
|
|
449
509
|
continue
|
|
450
510
|
except Exception as exc:
|
{soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/src/soothe_client/appkit/managed_client.py
RENAMED
|
@@ -7,7 +7,7 @@ Wraps ``WebSocketClient`` and upgrades flat appkit payloads
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
from collections.abc import AsyncIterator, Awaitable, Callable
|
|
10
|
-
from typing import Any, Protocol, runtime_checkable
|
|
10
|
+
from typing import Any, Protocol, cast, runtime_checkable
|
|
11
11
|
|
|
12
12
|
from soothe_client.errors import DisconnectCause
|
|
13
13
|
from soothe_client.websocket import WebSocketClient
|
|
@@ -60,23 +60,29 @@ def _coerce_appkit_wire_message(msg: dict[str, Any], client: WebSocketClient) ->
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
if msg_type in _NOTIFICATION_METHODS:
|
|
63
|
-
return
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
63
|
+
return cast(
|
|
64
|
+
dict[str, Any],
|
|
65
|
+
WireEnvelope(
|
|
66
|
+
proto="1",
|
|
67
|
+
type=MessageType.NOTIFICATION.value,
|
|
68
|
+
method=msg_type,
|
|
69
|
+
params=params,
|
|
70
|
+
).to_wire_dict(),
|
|
71
|
+
)
|
|
69
72
|
|
|
70
73
|
if msg_type in _REQUEST_METHOD_ALIASES or msg_type:
|
|
71
74
|
request_method = _REQUEST_METHOD_ALIASES.get(msg_type, msg_type)
|
|
72
75
|
req_id = str(msg.get("request_id") or msg.get("id") or client._next_request_id())
|
|
73
|
-
return
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
return cast(
|
|
77
|
+
dict[str, Any],
|
|
78
|
+
WireEnvelope(
|
|
79
|
+
proto="1",
|
|
80
|
+
type=MessageType.REQUEST.value,
|
|
81
|
+
method=request_method,
|
|
82
|
+
params=params,
|
|
83
|
+
id=req_id,
|
|
84
|
+
).to_wire_dict(),
|
|
85
|
+
)
|
|
80
86
|
|
|
81
87
|
return msg
|
|
82
88
|
|
{soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/src/soothe_client/appkit/turn_runner.py
RENAMED
|
@@ -241,23 +241,23 @@ class TurnRunner:
|
|
|
241
241
|
input_msg = self._build_input(message, loop_id, atts, opts)
|
|
242
242
|
try:
|
|
243
243
|
await conn.client.send_message(input_msg)
|
|
244
|
-
except Exception as
|
|
245
|
-
await self._persist_failed(session_id, loop_id,
|
|
246
|
-
self._broadcast_error(session_id,
|
|
244
|
+
except Exception as send_err:
|
|
245
|
+
await self._persist_failed(session_id, loop_id, send_err)
|
|
246
|
+
self._broadcast_error(session_id, send_err)
|
|
247
247
|
if self._on_error is not None:
|
|
248
|
-
self._on_error(session_id, loop_id,
|
|
248
|
+
self._on_error(session_id, loop_id, send_err)
|
|
249
249
|
raise
|
|
250
250
|
|
|
251
251
|
event_stream = conn.event_stream
|
|
252
252
|
if event_stream is None:
|
|
253
|
-
|
|
253
|
+
missing = RuntimeError(
|
|
254
254
|
f"missing event stream for session {session_id} (loop {loop_id})"
|
|
255
255
|
)
|
|
256
|
-
await self._persist_failed(session_id, loop_id,
|
|
257
|
-
self._broadcast_error(session_id,
|
|
256
|
+
await self._persist_failed(session_id, loop_id, missing)
|
|
257
|
+
self._broadcast_error(session_id, missing)
|
|
258
258
|
if self._on_error is not None:
|
|
259
|
-
self._on_error(session_id, loop_id,
|
|
260
|
-
raise
|
|
259
|
+
self._on_error(session_id, loop_id, missing)
|
|
260
|
+
raise missing
|
|
261
261
|
|
|
262
262
|
assistant_content = ""
|
|
263
263
|
started_at = time.time()
|
|
@@ -266,9 +266,9 @@ class TurnRunner:
|
|
|
266
266
|
|
|
267
267
|
while True:
|
|
268
268
|
if cancel_event is not None and cancel_event.is_set():
|
|
269
|
-
|
|
270
|
-
await self._fail_turn(session_id, loop_id,
|
|
271
|
-
raise
|
|
269
|
+
aborted = RuntimeError("aborted")
|
|
270
|
+
await self._fail_turn(session_id, loop_id, aborted)
|
|
271
|
+
raise aborted
|
|
272
272
|
|
|
273
273
|
if query_timed_out.is_set():
|
|
274
274
|
with contextlib.suppress(Exception):
|
|
@@ -298,7 +298,10 @@ class TurnRunner:
|
|
|
298
298
|
)
|
|
299
299
|
return
|
|
300
300
|
|
|
301
|
-
|
|
301
|
+
async def _pull_next() -> Any:
|
|
302
|
+
return await agen.__anext__()
|
|
303
|
+
|
|
304
|
+
next_task: asyncio.Task[Any] = asyncio.create_task(_pull_next())
|
|
302
305
|
waiters: list[asyncio.Task[Any]] = [
|
|
303
306
|
next_task,
|
|
304
307
|
asyncio.create_task(query_timed_out.wait()),
|
|
@@ -350,9 +353,9 @@ class TurnRunner:
|
|
|
350
353
|
return
|
|
351
354
|
|
|
352
355
|
if caller_wait is not None and caller_wait in done:
|
|
353
|
-
|
|
354
|
-
await self._fail_turn(session_id, loop_id,
|
|
355
|
-
raise
|
|
356
|
+
aborted = RuntimeError("aborted")
|
|
357
|
+
await self._fail_turn(session_id, loop_id, aborted)
|
|
358
|
+
raise aborted
|
|
356
359
|
|
|
357
360
|
try:
|
|
358
361
|
msg = next_task.result()
|
|
@@ -369,9 +372,9 @@ class TurnRunner:
|
|
|
369
372
|
"stream_closed",
|
|
370
373
|
)
|
|
371
374
|
return
|
|
372
|
-
|
|
373
|
-
await self._fail_turn(session_id, loop_id,
|
|
374
|
-
raise
|
|
375
|
+
closed = RuntimeError("event stream closed")
|
|
376
|
+
await self._fail_turn(session_id, loop_id, closed)
|
|
377
|
+
raise closed from None
|
|
375
378
|
|
|
376
379
|
arm_idle()
|
|
377
380
|
|
|
@@ -7,7 +7,7 @@ import contextlib
|
|
|
7
7
|
import logging
|
|
8
8
|
from collections.abc import AsyncIterator
|
|
9
9
|
from contextlib import asynccontextmanager
|
|
10
|
-
from typing import Any
|
|
10
|
+
from typing import Any, cast
|
|
11
11
|
|
|
12
12
|
from soothe_sdk.wire.codec import ProtocolError
|
|
13
13
|
|
|
@@ -310,20 +310,20 @@ async def is_daemon_live(
|
|
|
310
310
|
|
|
311
311
|
# Standard liveness check without waiting
|
|
312
312
|
for attempt in range(attempts):
|
|
313
|
-
|
|
313
|
+
probe: WebSocketClient | None = None
|
|
314
314
|
try:
|
|
315
|
-
|
|
316
|
-
await
|
|
317
|
-
status = await check_daemon_status(
|
|
315
|
+
probe = WebSocketClient(url=ws_url)
|
|
316
|
+
await probe.connect()
|
|
317
|
+
status = await check_daemon_status(probe, timeout=timeout)
|
|
318
318
|
return _daemon_status_indicates_live(status)
|
|
319
319
|
except Exception as exc:
|
|
320
320
|
last_error = exc
|
|
321
321
|
if attempt < attempts - 1:
|
|
322
322
|
await asyncio.sleep(delay_s)
|
|
323
323
|
finally:
|
|
324
|
-
if
|
|
324
|
+
if probe is not None:
|
|
325
325
|
with contextlib.suppress(Exception):
|
|
326
|
-
await
|
|
326
|
+
await probe.close()
|
|
327
327
|
|
|
328
328
|
if last_error is not None:
|
|
329
329
|
logger.debug("Daemon health check failed for %s: %s", ws_url, last_error)
|
|
@@ -383,7 +383,8 @@ async def fetch_skills_catalog(client: WebSocketClient, timeout: float = 15.0) -
|
|
|
383
383
|
ConnectionError: If daemon not reachable
|
|
384
384
|
"""
|
|
385
385
|
response = await client.request("skills_list", {}, timeout=timeout)
|
|
386
|
-
|
|
386
|
+
skills = response.get("skills", [])
|
|
387
|
+
return cast(list[dict[Any, Any]], skills if isinstance(skills, list) else [])
|
|
387
388
|
|
|
388
389
|
|
|
389
390
|
async def fetch_config_section(client: WebSocketClient, section: str, timeout: float = 5.0) -> dict:
|
|
@@ -405,7 +406,8 @@ async def fetch_config_section(client: WebSocketClient, section: str, timeout: f
|
|
|
405
406
|
"""
|
|
406
407
|
await _ensure_handshake(client, timeout=timeout)
|
|
407
408
|
response = await client.request("config_get", {"section": section}, timeout=timeout)
|
|
408
|
-
|
|
409
|
+
section_data = response.get(section, {})
|
|
410
|
+
return cast(dict[Any, Any], section_data if isinstance(section_data, dict) else {})
|
|
409
411
|
|
|
410
412
|
|
|
411
413
|
async def fetch_loop_history(
|
|
File without changes
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Shared stream/turn terminal frame helpers for WebSocket and DaemonSession.
|
|
2
|
+
|
|
3
|
+
Keeps peel-at-turn-start and turn-end detection on one vocabulary so leftover
|
|
4
|
+
prior-goal terminals cannot blank the next TUI query.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, TypeGuard
|
|
10
|
+
|
|
11
|
+
from soothe_sdk.core.events import (
|
|
12
|
+
PLAN_CREATED,
|
|
13
|
+
STRANGE_LOOP_COMPLETED,
|
|
14
|
+
STRANGE_LOOP_STEP_COMPLETED,
|
|
15
|
+
STRANGE_LOOP_STEP_QUEUED,
|
|
16
|
+
STRANGE_LOOP_STEP_STARTED,
|
|
17
|
+
STREAM_END,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
TURN_END_CUSTOM_TYPES = frozenset({STREAM_END, STRANGE_LOOP_COMPLETED})
|
|
21
|
+
|
|
22
|
+
# Customs that prove this turn has real work (not mere intake plan.phase).
|
|
23
|
+
# Intake-only phases must not unlock turn-end — prior-goal stream.end can still
|
|
24
|
+
# arrive after status=running (loop 3e43).
|
|
25
|
+
_TURN_PROGRESS_CUSTOM_TYPES = frozenset(
|
|
26
|
+
{
|
|
27
|
+
PLAN_CREATED,
|
|
28
|
+
STRANGE_LOOP_STEP_STARTED,
|
|
29
|
+
STRANGE_LOOP_STEP_QUEUED,
|
|
30
|
+
STRANGE_LOOP_STEP_COMPLETED,
|
|
31
|
+
}
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Handshake / card-replay / subscription leftovers safe to drop at turn start.
|
|
35
|
+
STALE_TURN_PENDING_TYPES = frozenset(
|
|
36
|
+
{
|
|
37
|
+
"connection_ack",
|
|
38
|
+
"card.replay_begin",
|
|
39
|
+
"card.replay_end",
|
|
40
|
+
"card.created",
|
|
41
|
+
"complete",
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def is_turn_end_custom_data(data: Any) -> TypeGuard[dict[str, Any]]:
|
|
47
|
+
"""True when ``data`` is a turn-scoped terminal custom payload."""
|
|
48
|
+
if not isinstance(data, dict):
|
|
49
|
+
return False
|
|
50
|
+
custom_type = str(data.get("type", "")).strip()
|
|
51
|
+
if custom_type not in TURN_END_CUSTOM_TYPES:
|
|
52
|
+
return False
|
|
53
|
+
if custom_type == STREAM_END:
|
|
54
|
+
scope = str(data.get("scope") or "turn").strip().lower()
|
|
55
|
+
return scope in {"", "turn"}
|
|
56
|
+
return True
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def is_turn_progress_chunk(mode: str, data: Any) -> bool:
|
|
60
|
+
"""True when a chunk proves the active turn has non-intake progress.
|
|
61
|
+
|
|
62
|
+
Used so late prior-goal ``stream.end`` cannot close a turn that has only
|
|
63
|
+
seen intake lifecycle (e.g. plan.phase "Interpreting goal").
|
|
64
|
+
"""
|
|
65
|
+
if mode in {"messages", "updates"}:
|
|
66
|
+
return True
|
|
67
|
+
if mode != "custom" or not isinstance(data, dict):
|
|
68
|
+
return False
|
|
69
|
+
if is_turn_end_custom_data(data):
|
|
70
|
+
return False
|
|
71
|
+
custom_type = str(data.get("type", "")).strip()
|
|
72
|
+
if custom_type in _TURN_PROGRESS_CUSTOM_TYPES:
|
|
73
|
+
return True
|
|
74
|
+
if custom_type.startswith("soothe.cognition.strange_loop.step"):
|
|
75
|
+
return True
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def stale_pending_frame_label(event: dict[str, Any]) -> str | None:
|
|
80
|
+
"""Return a peel label when ``event`` is safe to drop at turn start."""
|
|
81
|
+
event_type = str(event.get("type") or "")
|
|
82
|
+
if event_type in STALE_TURN_PENDING_TYPES:
|
|
83
|
+
return event_type
|
|
84
|
+
|
|
85
|
+
if event_type == "next":
|
|
86
|
+
payload = event.get("payload")
|
|
87
|
+
if not isinstance(payload, dict):
|
|
88
|
+
return None
|
|
89
|
+
stale_mode = str(payload.get("mode") or "")
|
|
90
|
+
if stale_mode in STALE_TURN_PENDING_TYPES:
|
|
91
|
+
return stale_mode
|
|
92
|
+
inner = payload.get("data")
|
|
93
|
+
if isinstance(inner, dict):
|
|
94
|
+
return stale_pending_frame_label(inner)
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
if event_type == "event":
|
|
98
|
+
mode = str(event.get("mode") or "")
|
|
99
|
+
data = event.get("data")
|
|
100
|
+
if mode == "custom" and is_turn_end_custom_data(data):
|
|
101
|
+
return str(data.get("type") or "").strip()
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
__all__ = [
|
|
106
|
+
"STALE_TURN_PENDING_TYPES",
|
|
107
|
+
"STREAM_END",
|
|
108
|
+
"STRANGE_LOOP_COMPLETED",
|
|
109
|
+
"TURN_END_CUSTOM_TYPES",
|
|
110
|
+
"is_turn_end_custom_data",
|
|
111
|
+
"is_turn_progress_chunk",
|
|
112
|
+
"stale_pending_frame_label",
|
|
113
|
+
]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Turn / stream boundary helpers (IG-659).
|
|
2
|
+
|
|
3
|
+
``turn_id`` correlates a user turn across status, events, and terminals.
|
|
4
|
+
``seq`` is a daemon-assigned monotonic counter per loop for drop-stale filtering.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def format_turn_id(loop_id: str, generation: int) -> str:
|
|
13
|
+
"""Return wire ``turn_id`` for ``loop_id`` + admit generation."""
|
|
14
|
+
lid = str(loop_id or "").strip()
|
|
15
|
+
gen = int(generation)
|
|
16
|
+
if not lid or gen <= 0:
|
|
17
|
+
return ""
|
|
18
|
+
return f"{lid}:{gen}"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def frame_turn_id(frame: dict[str, Any] | None) -> str | None:
|
|
22
|
+
"""Return ``turn_id`` from a status/event frame or nested custom data."""
|
|
23
|
+
if not isinstance(frame, dict):
|
|
24
|
+
return None
|
|
25
|
+
tid = frame.get("turn_id")
|
|
26
|
+
if isinstance(tid, str) and tid.strip():
|
|
27
|
+
return tid.strip()
|
|
28
|
+
data = frame.get("data")
|
|
29
|
+
if isinstance(data, dict):
|
|
30
|
+
inner = data.get("turn_id")
|
|
31
|
+
if isinstance(inner, str) and inner.strip():
|
|
32
|
+
return inner.strip()
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def frame_seq(frame: dict[str, Any] | None) -> int | None:
|
|
37
|
+
"""Return non-negative ``seq`` from a wire frame, or None."""
|
|
38
|
+
if not isinstance(frame, dict):
|
|
39
|
+
return None
|
|
40
|
+
raw = frame.get("seq")
|
|
41
|
+
if isinstance(raw, bool):
|
|
42
|
+
return None
|
|
43
|
+
if isinstance(raw, int) and raw >= 0:
|
|
44
|
+
return raw
|
|
45
|
+
if isinstance(raw, float) and raw >= 0 and raw == int(raw):
|
|
46
|
+
return int(raw)
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
"format_turn_id",
|
|
52
|
+
"frame_seq",
|
|
53
|
+
"frame_turn_id",
|
|
54
|
+
]
|
|
@@ -9,7 +9,7 @@ import time
|
|
|
9
9
|
import uuid
|
|
10
10
|
from collections import deque
|
|
11
11
|
from collections.abc import AsyncGenerator, Callable
|
|
12
|
-
from typing import Any
|
|
12
|
+
from typing import Any, cast
|
|
13
13
|
|
|
14
14
|
import websockets.asyncio.client
|
|
15
15
|
import websockets.exceptions
|
|
@@ -18,6 +18,7 @@ from soothe_sdk.wire.protocol import decode_websocket_text, encode_websocket_tex
|
|
|
18
18
|
|
|
19
19
|
from soothe_client.errors import DisconnectCause, ReconnectError, StaleLoopError
|
|
20
20
|
from soothe_client.intent_hints import validate_loop_input_intent_hint
|
|
21
|
+
from soothe_client.stream_terminal import TURN_END_CUSTOM_TYPES, stale_pending_frame_label
|
|
21
22
|
|
|
22
23
|
logger = logging.getLogger(__name__)
|
|
23
24
|
|
|
@@ -115,9 +116,7 @@ def _inbound_frame_drop_priority(event: dict[str, Any] | None) -> int:
|
|
|
115
116
|
data = event.get("data")
|
|
116
117
|
if isinstance(data, dict):
|
|
117
118
|
custom_type = data.get("type", "")
|
|
118
|
-
if custom_type
|
|
119
|
-
return _DROP_PRIORITY_CRITICAL
|
|
120
|
-
if custom_type == "soothe.cognition.strange_loop.completed":
|
|
119
|
+
if custom_type in TURN_END_CUSTOM_TYPES:
|
|
121
120
|
return _DROP_PRIORITY_CRITICAL
|
|
122
121
|
# Cognition events (step started/completed) - prefer keep
|
|
123
122
|
if custom_type.startswith("soothe.cognition."):
|
|
@@ -181,11 +180,7 @@ def _inbound_needs_delivery_ack(event: dict[str, Any]) -> bool:
|
|
|
181
180
|
body = data[0]
|
|
182
181
|
return isinstance(body, dict) and is_stream_terminal_wire_dict(body)
|
|
183
182
|
if inner_mode == "custom" and isinstance(data, dict):
|
|
184
|
-
|
|
185
|
-
return ctype in (
|
|
186
|
-
"soothe.cognition.strange_loop.completed",
|
|
187
|
-
"soothe.stream.end",
|
|
188
|
-
)
|
|
183
|
+
return data.get("type", "") in TURN_END_CUSTOM_TYPES
|
|
189
184
|
if event.get("type") == "event":
|
|
190
185
|
mode = event.get("mode", "")
|
|
191
186
|
data = event.get("data")
|
|
@@ -193,11 +188,7 @@ def _inbound_needs_delivery_ack(event: dict[str, Any]) -> bool:
|
|
|
193
188
|
body = data[0]
|
|
194
189
|
return isinstance(body, dict) and is_stream_terminal_wire_dict(body)
|
|
195
190
|
if mode == "custom" and isinstance(data, dict):
|
|
196
|
-
|
|
197
|
-
return ctype in (
|
|
198
|
-
"soothe.cognition.strange_loop.completed",
|
|
199
|
-
"soothe.stream.end",
|
|
200
|
-
)
|
|
191
|
+
return data.get("type", "") in TURN_END_CUSTOM_TYPES
|
|
201
192
|
return False
|
|
202
193
|
|
|
203
194
|
|
|
@@ -931,7 +922,7 @@ class WebSocketClient:
|
|
|
931
922
|
from soothe_sdk.wire.codec import MessageType
|
|
932
923
|
|
|
933
924
|
if self._pending_events:
|
|
934
|
-
event = self._pending_events.popleft()
|
|
925
|
+
event: dict[str, Any] | None = self._pending_events.popleft()
|
|
935
926
|
else:
|
|
936
927
|
event = await self._read_inbound_event()
|
|
937
928
|
|
|
@@ -1552,31 +1543,14 @@ class WebSocketClient:
|
|
|
1552
1543
|
"""
|
|
1553
1544
|
self._pending_events.clear()
|
|
1554
1545
|
|
|
1555
|
-
# Handshake / RPC responses that must not count as turn progress (TUI stall detection).
|
|
1556
|
-
# ``card.replay_*`` / ``card.created`` are emitted by the daemon during
|
|
1557
|
-
# ``loop_subscribe`` for non-TUI clients. The TUI consumes its
|
|
1558
|
-
# cards via the synchronous ``loop_cards_fetch`` RPC so these frames are
|
|
1559
|
-
# peeled silently here. Under protocol-1, RPC responses arrive as
|
|
1560
|
-
# ``type:"response"`` correlated by ``id`` (peeled by the reader loop), so
|
|
1561
|
-
# the legacy ``*_response`` type entries are gone. Under protocol-1 (
|
|
1562
|
-
# §9.3) card replay frames arrive wrapped in ``next`` envelopes with
|
|
1563
|
-
# ``payload.mode`` set to the originating frame type; the peel logic below
|
|
1564
|
-
# inspects both the raw ``type`` and the wrapped ``payload.mode``.
|
|
1565
|
-
_STALE_TURN_PENDING_TYPES = frozenset(
|
|
1566
|
-
{
|
|
1567
|
-
"connection_ack",
|
|
1568
|
-
"card.replay_begin",
|
|
1569
|
-
"card.replay_end",
|
|
1570
|
-
"card.created",
|
|
1571
|
-
}
|
|
1572
|
-
)
|
|
1573
|
-
|
|
1574
1546
|
def peel_stale_pending_control_events(self) -> list[str]:
|
|
1575
|
-
"""Remove stale handshake/
|
|
1547
|
+
"""Remove stale handshake/terminal frames left in ``_pending_events`` before a turn.
|
|
1576
1548
|
|
|
1577
1549
|
``request`` queues unrelated inbound frames while waiting for a matching
|
|
1578
1550
|
``id``. If a handshake/control frame remains at turn start, the TUI can
|
|
1579
|
-
mistake it for live progress and never log a stalled stream.
|
|
1551
|
+
mistake it for live progress and never log a stalled stream. Leftover
|
|
1552
|
+
prior-goal terminal frames must also be peeled so they cannot end the
|
|
1553
|
+
next turn before ``status=running``.
|
|
1580
1554
|
|
|
1581
1555
|
Returns:
|
|
1582
1556
|
List of removed frame types (in order).
|
|
@@ -1588,20 +1562,9 @@ class WebSocketClient:
|
|
|
1588
1562
|
removed: list[str] = []
|
|
1589
1563
|
while self._pending_events:
|
|
1590
1564
|
event = self._pending_events.popleft()
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
# live progress at turn start.
|
|
1595
|
-
stale_mode = ""
|
|
1596
|
-
if event_type == "next":
|
|
1597
|
-
payload = event.get("payload")
|
|
1598
|
-
if isinstance(payload, dict):
|
|
1599
|
-
stale_mode = str(payload.get("mode") or "")
|
|
1600
|
-
if event_type in self._STALE_TURN_PENDING_TYPES:
|
|
1601
|
-
removed.append(event_type)
|
|
1602
|
-
continue
|
|
1603
|
-
if stale_mode and stale_mode in self._STALE_TURN_PENDING_TYPES:
|
|
1604
|
-
removed.append(stale_mode)
|
|
1565
|
+
label = stale_pending_frame_label(event)
|
|
1566
|
+
if label is not None:
|
|
1567
|
+
removed.append(label)
|
|
1605
1568
|
continue
|
|
1606
1569
|
kept.append(event)
|
|
1607
1570
|
self._pending_events = kept
|
|
@@ -1617,7 +1580,7 @@ class WebSocketClient:
|
|
|
1617
1580
|
message = await self._ws.recv()
|
|
1618
1581
|
if isinstance(message, bytes):
|
|
1619
1582
|
message = message.decode("utf-8")
|
|
1620
|
-
return decode_websocket_text(message)
|
|
1583
|
+
return cast(dict[str, Any] | None, decode_websocket_text(message))
|
|
1621
1584
|
except websockets.exceptions.ConnectionClosed:
|
|
1622
1585
|
return None
|
|
1623
1586
|
except Exception:
|
{soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/src/soothe_client/ws_command_client.py
RENAMED
|
@@ -9,7 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
import asyncio
|
|
11
11
|
import logging
|
|
12
|
-
from typing import Any
|
|
12
|
+
from typing import Any, cast
|
|
13
13
|
from uuid import uuid4
|
|
14
14
|
|
|
15
15
|
from soothe_sdk.wire.codec import (
|
|
@@ -241,7 +241,10 @@ class WsCommandClient:
|
|
|
241
241
|
if response.get("id") != req_id:
|
|
242
242
|
# Not our response; keep waiting for the match.
|
|
243
243
|
continue
|
|
244
|
-
|
|
244
|
+
result = response.get("result") or {}
|
|
245
|
+
if not isinstance(result, dict):
|
|
246
|
+
raise RuntimeError(f"Unexpected result payload: {result!r}")
|
|
247
|
+
return cast(dict[str, Any], result)
|
|
245
248
|
|
|
246
249
|
# Other message types (next/complete/etc.) are unexpected
|
|
247
250
|
# for a blocking request; keep reading.
|
|
@@ -266,7 +269,7 @@ class WsCommandClient:
|
|
|
266
269
|
self, description: str, *, priority: int = 50, workspace: str | None = None
|
|
267
270
|
) -> dict[str, Any]:
|
|
268
271
|
"""Submit a new autopilot task."""
|
|
269
|
-
payload = {"description": description, "priority": priority}
|
|
272
|
+
payload: dict[str, Any] = {"description": description, "priority": priority}
|
|
270
273
|
if workspace:
|
|
271
274
|
payload["workspace"] = workspace
|
|
272
275
|
return await self._send_command("autopilot_submit", payload)
|
|
@@ -334,7 +337,7 @@ class WsCommandClient:
|
|
|
334
337
|
Returns:
|
|
335
338
|
Dict with job_id and status.
|
|
336
339
|
"""
|
|
337
|
-
payload = {"goal": goal}
|
|
340
|
+
payload: dict[str, Any] = {"goal": goal}
|
|
338
341
|
if workspace:
|
|
339
342
|
payload["workspace"] = workspace
|
|
340
343
|
if autonomous:
|
|
@@ -413,7 +416,7 @@ class WsCommandClient:
|
|
|
413
416
|
Returns:
|
|
414
417
|
Dict with job_id, goal_id, absorbed.
|
|
415
418
|
"""
|
|
416
|
-
payload = {"job_id": job_id, "content": content}
|
|
419
|
+
payload: dict[str, Any] = {"job_id": job_id, "content": content}
|
|
417
420
|
if goal_id:
|
|
418
421
|
payload["goal_id"] = goal_id
|
|
419
422
|
return await self._send_command("job_guidance", payload)
|
|
@@ -422,7 +425,7 @@ class WsCommandClient:
|
|
|
422
425
|
|
|
423
426
|
async def cron_add(self, text: str, *, priority: int | None = None) -> dict[str, Any]:
|
|
424
427
|
"""Submit a natural-language scheduled job."""
|
|
425
|
-
payload = {"text": text}
|
|
428
|
+
payload: dict[str, Any] = {"text": text}
|
|
426
429
|
if priority is not None:
|
|
427
430
|
payload["priority"] = priority
|
|
428
431
|
result = await self._send_command("cron_add", payload)
|
|
@@ -441,7 +444,7 @@ class WsCommandClient:
|
|
|
441
444
|
Returns:
|
|
442
445
|
Dict with jobs list.
|
|
443
446
|
"""
|
|
444
|
-
payload = {}
|
|
447
|
+
payload: dict[str, Any] = {}
|
|
445
448
|
if status:
|
|
446
449
|
payload["status"] = status
|
|
447
450
|
return await self._send_command("cron_list", payload)
|
|
@@ -492,55 +495,61 @@ class SyncWsCommandClient:
|
|
|
492
495
|
|
|
493
496
|
def autopilot_status(self) -> dict[str, Any]:
|
|
494
497
|
"""Get autopilot status (sync)."""
|
|
495
|
-
return self._run_async(self._client.autopilot_status())
|
|
498
|
+
return cast(dict[str, Any], self._run_async(self._client.autopilot_status()))
|
|
496
499
|
|
|
497
500
|
def autopilot_submit(
|
|
498
501
|
self, description: str, *, priority: int = 50, workspace: str | None = None
|
|
499
502
|
) -> dict[str, Any]:
|
|
500
503
|
"""Submit a new autopilot task (sync)."""
|
|
501
|
-
return
|
|
502
|
-
|
|
504
|
+
return cast(
|
|
505
|
+
dict[str, Any],
|
|
506
|
+
self._run_async(
|
|
507
|
+
self._client.autopilot_submit(description, priority=priority, workspace=workspace)
|
|
508
|
+
),
|
|
503
509
|
)
|
|
504
510
|
|
|
505
511
|
def autopilot_list_goals(self) -> dict[str, Any]:
|
|
506
512
|
"""List all goals (sync)."""
|
|
507
|
-
return self._run_async(self._client.autopilot_list_goals())
|
|
513
|
+
return cast(dict[str, Any], self._run_async(self._client.autopilot_list_goals()))
|
|
508
514
|
|
|
509
515
|
def autopilot_get_goal(self, goal_id: str) -> dict[str, Any]:
|
|
510
516
|
"""Get goal details (sync)."""
|
|
511
|
-
return self._run_async(self._client.autopilot_get_goal(goal_id))
|
|
517
|
+
return cast(dict[str, Any], self._run_async(self._client.autopilot_get_goal(goal_id)))
|
|
512
518
|
|
|
513
519
|
def autopilot_cancel_goal(self, goal_id: str) -> dict[str, Any]:
|
|
514
520
|
"""Cancel a goal (sync)."""
|
|
515
|
-
return self._run_async(self._client.autopilot_cancel_goal(goal_id))
|
|
521
|
+
return cast(dict[str, Any], self._run_async(self._client.autopilot_cancel_goal(goal_id)))
|
|
516
522
|
|
|
517
523
|
def autopilot_wake(self) -> dict[str, Any]:
|
|
518
524
|
"""Exit dreaming mode (sync)."""
|
|
519
|
-
return self._run_async(self._client.autopilot_wake())
|
|
525
|
+
return cast(dict[str, Any], self._run_async(self._client.autopilot_wake()))
|
|
520
526
|
|
|
521
527
|
def autopilot_dream(self) -> dict[str, Any]:
|
|
522
528
|
"""Force dreaming mode (sync)."""
|
|
523
|
-
return self._run_async(self._client.autopilot_dream())
|
|
529
|
+
return cast(dict[str, Any], self._run_async(self._client.autopilot_dream()))
|
|
524
530
|
|
|
525
531
|
def autopilot_resume(self, goal_id: str) -> dict[str, Any]:
|
|
526
532
|
"""Resume a suspended/blocked goal (sync)."""
|
|
527
|
-
return self._run_async(self._client.autopilot_resume(goal_id))
|
|
533
|
+
return cast(dict[str, Any], self._run_async(self._client.autopilot_resume(goal_id)))
|
|
528
534
|
|
|
529
535
|
def autopilot_list_jobs(self) -> dict[str, Any]:
|
|
530
536
|
"""List root goals (jobs) only (sync)."""
|
|
531
|
-
return self._run_async(self._client.autopilot_list_jobs())
|
|
537
|
+
return cast(dict[str, Any], self._run_async(self._client.autopilot_list_jobs()))
|
|
532
538
|
|
|
533
539
|
def autopilot_get_job(self, job_id: str) -> dict[str, Any]:
|
|
534
540
|
"""Get job status with DAG snapshot (sync)."""
|
|
535
|
-
return self._run_async(self._client.autopilot_get_job(job_id))
|
|
541
|
+
return cast(dict[str, Any], self._run_async(self._client.autopilot_get_job(job_id)))
|
|
536
542
|
|
|
537
543
|
def job_pause(self, job_id: str) -> dict[str, Any]:
|
|
538
544
|
"""Pause a running autopilot job (sync)."""
|
|
539
|
-
return self._run_async(self._client.job_pause(job_id))
|
|
545
|
+
return cast(dict[str, Any], self._run_async(self._client.job_pause(job_id)))
|
|
540
546
|
|
|
541
547
|
def job_guidance(self, job_id: str, text: str, *, goal_id: str | None = None) -> dict[str, Any]:
|
|
542
548
|
"""Send guidance to an autopilot job or specific goal (sync)."""
|
|
543
|
-
return
|
|
549
|
+
return cast(
|
|
550
|
+
dict[str, Any],
|
|
551
|
+
self._run_async(self._client.job_guidance(job_id, text, goal_id=goal_id)),
|
|
552
|
+
)
|
|
544
553
|
|
|
545
554
|
def job_create(
|
|
546
555
|
self,
|
|
@@ -552,59 +561,62 @@ class SyncWsCommandClient:
|
|
|
552
561
|
guidance: str | None = None,
|
|
553
562
|
) -> dict[str, Any]:
|
|
554
563
|
"""Create a new autopilot job."""
|
|
555
|
-
return
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
564
|
+
return cast(
|
|
565
|
+
dict[str, Any],
|
|
566
|
+
self._run_async(
|
|
567
|
+
self._client.job_create(
|
|
568
|
+
goal,
|
|
569
|
+
workspace=workspace,
|
|
570
|
+
autonomous=autonomous,
|
|
571
|
+
max_iterations=max_iterations,
|
|
572
|
+
guidance=guidance,
|
|
573
|
+
)
|
|
574
|
+
),
|
|
563
575
|
)
|
|
564
576
|
|
|
565
577
|
def job_status(self, job_id: str) -> dict[str, Any]:
|
|
566
578
|
"""Get job status with goal counts and workers."""
|
|
567
|
-
return self._run_async(self._client.job_status(job_id))
|
|
579
|
+
return cast(dict[str, Any], self._run_async(self._client.job_status(job_id)))
|
|
568
580
|
|
|
569
581
|
def job_resume(self, job_id: str) -> dict[str, Any]:
|
|
570
582
|
"""Resume a paused autopilot job."""
|
|
571
|
-
return self._run_async(self._client.job_resume(job_id))
|
|
583
|
+
return cast(dict[str, Any], self._run_async(self._client.job_resume(job_id)))
|
|
572
584
|
|
|
573
585
|
def job_cancel(self, job_id: str) -> dict[str, Any]:
|
|
574
586
|
"""Cancel an autopilot job."""
|
|
575
|
-
return self._run_async(self._client.job_cancel(job_id))
|
|
587
|
+
return cast(dict[str, Any], self._run_async(self._client.job_cancel(job_id)))
|
|
576
588
|
|
|
577
589
|
def job_dag(self, job_id: str) -> dict[str, Any]:
|
|
578
590
|
"""Get job DAG snapshot for visualization."""
|
|
579
|
-
return self._run_async(self._client.job_dag(job_id))
|
|
591
|
+
return cast(dict[str, Any], self._run_async(self._client.job_dag(job_id)))
|
|
580
592
|
|
|
581
593
|
def autopilot_subscribe(self) -> dict[str, Any]:
|
|
582
594
|
"""Subscribe to autopilot worker events (sync)."""
|
|
583
|
-
return self._run_async(self._client.autopilot_subscribe())
|
|
595
|
+
return cast(dict[str, Any], self._run_async(self._client.autopilot_subscribe()))
|
|
584
596
|
|
|
585
597
|
def autopilot_unsubscribe(self) -> dict[str, Any]:
|
|
586
598
|
"""Unsubscribe from autopilot worker events (sync)."""
|
|
587
|
-
return self._run_async(self._client.autopilot_unsubscribe())
|
|
599
|
+
return cast(dict[str, Any], self._run_async(self._client.autopilot_unsubscribe()))
|
|
588
600
|
|
|
589
601
|
def cron_add(self, text: str, *, priority: int | None = None) -> dict[str, Any]:
|
|
590
602
|
"""Submit a natural-language scheduled job (sync)."""
|
|
591
|
-
return self._run_async(self._client.cron_add(text, priority=priority))
|
|
603
|
+
return cast(dict[str, Any], self._run_async(self._client.cron_add(text, priority=priority)))
|
|
592
604
|
|
|
593
605
|
def cron_list(self, *, status: str | None = None) -> dict[str, Any]:
|
|
594
606
|
"""List scheduled jobs (sync)."""
|
|
595
|
-
return self._run_async(self._client.cron_list(status=status))
|
|
607
|
+
return cast(dict[str, Any], self._run_async(self._client.cron_list(status=status)))
|
|
596
608
|
|
|
597
609
|
def cron_show(self, job_id: str) -> dict[str, Any]:
|
|
598
610
|
"""Get job details (sync)."""
|
|
599
|
-
return self._run_async(self._client.cron_show(job_id))
|
|
611
|
+
return cast(dict[str, Any], self._run_async(self._client.cron_show(job_id)))
|
|
600
612
|
|
|
601
613
|
def cron_cancel(self, job_id: str) -> dict[str, Any]:
|
|
602
614
|
"""Cancel a scheduled job (sync)."""
|
|
603
|
-
return self._run_async(self._client.cron_cancel(job_id))
|
|
615
|
+
return cast(dict[str, Any], self._run_async(self._client.cron_cancel(job_id)))
|
|
604
616
|
|
|
605
617
|
def memory_stats(self, mode: str = "daemon") -> dict[str, Any]:
|
|
606
618
|
"""Query daemon memory profiling stats (sync)."""
|
|
607
|
-
return self._run_async(self._client.memory_stats(mode))
|
|
619
|
+
return cast(dict[str, Any], self._run_async(self._client.memory_stats(mode)))
|
|
608
620
|
|
|
609
621
|
|
|
610
622
|
def ws_command_client_from_config(cfg: Any) -> SyncWsCommandClient:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/src/soothe_client/appkit/__init__.py
RENAMED
|
File without changes
|
{soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/src/soothe_client/appkit/attachments.py
RENAMED
|
File without changes
|
{soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/src/soothe_client/appkit/broadcaster.py
RENAMED
|
File without changes
|
{soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/src/soothe_client/appkit/chunk_filter.py
RENAMED
|
File without changes
|
{soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/src/soothe_client/appkit/events.py
RENAMED
|
File without changes
|
{soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/src/soothe_client/appkit/observability.py
RENAMED
|
File without changes
|
|
File without changes
|
{soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/src/soothe_client/appkit/query_gate.py
RENAMED
|
File without changes
|
{soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/src/soothe_client/appkit/session_store.py
RENAMED
|
File without changes
|
{soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/src/soothe_client/appkit/thinking_step.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{soothe_client_python-0.9.6 → soothe_client_python-0.9.8}/src/soothe_client/protocol_params.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|