soothe-client-python 1.0.9__tar.gz → 1.0.11__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. {soothe_client_python-1.0.9 → soothe_client_python-1.0.11}/PKG-INFO +1 -1
  2. {soothe_client_python-1.0.9 → soothe_client_python-1.0.11}/src/soothe_client/appkit/daemon_session.py +3 -8
  3. {soothe_client_python-1.0.9 → soothe_client_python-1.0.11}/src/soothe_client/command_client.py +53 -38
  4. {soothe_client_python-1.0.9 → soothe_client_python-1.0.11}/src/soothe_client/protocol_params.py +11 -0
  5. {soothe_client_python-1.0.9 → soothe_client_python-1.0.11}/.gitignore +0 -0
  6. {soothe_client_python-1.0.9 → soothe_client_python-1.0.11}/LICENSE +0 -0
  7. {soothe_client_python-1.0.9 → soothe_client_python-1.0.11}/README.md +0 -0
  8. {soothe_client_python-1.0.9 → soothe_client_python-1.0.11}/pyproject.toml +0 -0
  9. {soothe_client_python-1.0.9 → soothe_client_python-1.0.11}/src/soothe_client/__init__.py +0 -0
  10. {soothe_client_python-1.0.9 → soothe_client_python-1.0.11}/src/soothe_client/appkit/__init__.py +0 -0
  11. {soothe_client_python-1.0.9 → soothe_client_python-1.0.11}/src/soothe_client/appkit/attachments.py +0 -0
  12. {soothe_client_python-1.0.9 → soothe_client_python-1.0.11}/src/soothe_client/appkit/broadcaster.py +0 -0
  13. {soothe_client_python-1.0.9 → soothe_client_python-1.0.11}/src/soothe_client/appkit/card_projection.py +0 -0
  14. {soothe_client_python-1.0.9 → soothe_client_python-1.0.11}/src/soothe_client/appkit/chunk_filter.py +0 -0
  15. {soothe_client_python-1.0.9 → soothe_client_python-1.0.11}/src/soothe_client/appkit/classifier.py +0 -0
  16. {soothe_client_python-1.0.9 → soothe_client_python-1.0.11}/src/soothe_client/appkit/events.py +0 -0
  17. {soothe_client_python-1.0.9 → soothe_client_python-1.0.11}/src/soothe_client/appkit/loop_session_store.py +0 -0
  18. {soothe_client_python-1.0.9 → soothe_client_python-1.0.11}/src/soothe_client/appkit/managed_client.py +0 -0
  19. {soothe_client_python-1.0.9 → soothe_client_python-1.0.11}/src/soothe_client/appkit/observability.py +0 -0
  20. {soothe_client_python-1.0.9 → soothe_client_python-1.0.11}/src/soothe_client/appkit/pool.py +0 -0
  21. {soothe_client_python-1.0.9 → soothe_client_python-1.0.11}/src/soothe_client/appkit/query_gate.py +0 -0
  22. {soothe_client_python-1.0.9 → soothe_client_python-1.0.11}/src/soothe_client/appkit/thinking_step.py +0 -0
  23. {soothe_client_python-1.0.9 → soothe_client_python-1.0.11}/src/soothe_client/appkit/turn.py +0 -0
  24. {soothe_client_python-1.0.9 → soothe_client_python-1.0.11}/src/soothe_client/appkit/turn_boundary.py +0 -0
  25. {soothe_client_python-1.0.9 → soothe_client_python-1.0.11}/src/soothe_client/appkit/turn_runner.py +0 -0
  26. {soothe_client_python-1.0.9 → soothe_client_python-1.0.11}/src/soothe_client/errors.py +0 -0
  27. {soothe_client_python-1.0.9 → soothe_client_python-1.0.11}/src/soothe_client/helpers.py +0 -0
  28. {soothe_client_python-1.0.9 → soothe_client_python-1.0.11}/src/soothe_client/intent_hints.py +0 -0
  29. {soothe_client_python-1.0.9 → soothe_client_python-1.0.11}/src/soothe_client/py.typed +0 -0
  30. {soothe_client_python-1.0.9 → soothe_client_python-1.0.11}/src/soothe_client/schemas.py +0 -0
  31. {soothe_client_python-1.0.9 → soothe_client_python-1.0.11}/src/soothe_client/session.py +0 -0
  32. {soothe_client_python-1.0.9 → soothe_client_python-1.0.11}/src/soothe_client/stream_terminal.py +0 -0
  33. {soothe_client_python-1.0.9 → soothe_client_python-1.0.11}/src/soothe_client/turn_boundary.py +0 -0
  34. {soothe_client_python-1.0.9 → soothe_client_python-1.0.11}/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.9
3
+ Version: 1.0.11
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
@@ -450,9 +450,7 @@ class DaemonSession:
450
450
  # pre-admit early running can omit turn_id). Idle/stopped
451
451
  # fall through to their gates (cancel idle may omit turn_id).
452
452
  ev_turn_id = frame_turn_id(event)
453
- status_state = (
454
- str(event.get("state") or "") if event_type == "status" else ""
455
- )
453
+ status_state = str(event.get("state") or "") if event_type == "status" else ""
456
454
  is_running_status = status_state == "running"
457
455
  is_terminal_status = status_state in {"idle", "stopped"}
458
456
  if (
@@ -463,8 +461,7 @@ class DaemonSession:
463
461
  and not turn_ids_match(expected_turn_id, ev_turn_id)
464
462
  ):
465
463
  logger.debug(
466
- "Ignoring unbound/mismatched turn_id frame type=%s "
467
- "got=%s expected=%s",
464
+ "Ignoring unbound/mismatched turn_id frame type=%s got=%s expected=%s",
468
465
  event_type,
469
466
  (ev_turn_id or "<absent>")[-24:],
470
467
  expected_turn_id[-24:],
@@ -518,9 +515,7 @@ class DaemonSession:
518
515
  self._expected_turn_id = status_turn
519
516
  elif query_started and state == "stopped":
520
517
  stop_turn = frame_turn_id(event)
521
- if expected_turn_id and not turn_ids_match(
522
- expected_turn_id, stop_turn
523
- ):
518
+ if expected_turn_id and not turn_ids_match(expected_turn_id, stop_turn):
524
519
  continue
525
520
  self.last_turn_end_state = state
526
521
  if ev_seq is not None:
@@ -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
 
@@ -186,7 +158,11 @@ class AsyncCommandClient:
186
158
  self._timeout = timeout
187
159
 
188
160
  async def _send_command(
189
- self, command_type: str, payload: dict[str, Any] | None = None
161
+ self,
162
+ command_type: str,
163
+ payload: dict[str, Any] | None = None,
164
+ *,
165
+ timeout: float | None = None,
190
166
  ) -> dict[str, Any]:
191
167
  """Send a protocol-1 request envelope and wait for the response.
192
168
 
@@ -199,6 +175,8 @@ class AsyncCommandClient:
199
175
  Args:
200
176
  command_type: RPC method name (e.g. ``"autopilot_status"``).
201
177
  payload: Structured parameters object for the method.
178
+ timeout: Optional per-call override (seconds). Defaults to the
179
+ client constructor timeout.
202
180
 
203
181
  Returns:
204
182
  The ``result`` dict from the matching ``response`` envelope.
@@ -209,6 +187,7 @@ class AsyncCommandClient:
209
187
  """
210
188
  import websockets
211
189
 
190
+ cmd_timeout = self._timeout if timeout is None else timeout
212
191
  req_id = str(uuid4())
213
192
  envelope = WireEnvelope(
214
193
  type=MessageType.REQUEST.value,
@@ -218,15 +197,15 @@ class AsyncCommandClient:
218
197
  )
219
198
 
220
199
  try:
221
- async with websockets.connect(self._ws_url, open_timeout=self._timeout) as ws:
222
- await _perform_handshake(ws, timeout=self._timeout)
200
+ async with websockets.connect(self._ws_url, open_timeout=cmd_timeout) as ws:
201
+ await _perform_handshake(ws, timeout=cmd_timeout)
223
202
 
224
203
  # Send the protocol-1 request envelope.
225
204
  await ws.send(encode_envelope(envelope))
226
205
 
227
206
  # Wait for the matching response/error envelope.
228
207
  while True:
229
- response_str = await asyncio.wait_for(ws.recv(), timeout=self._timeout)
208
+ response_str = await asyncio.wait_for(ws.recv(), timeout=cmd_timeout)
230
209
  response = decode_envelope(response_str)
231
210
  if not isinstance(response, dict):
232
211
  raise RuntimeError(f"Unexpected response: {response_str!r}")
@@ -255,7 +234,7 @@ class AsyncCommandClient:
255
234
  continue
256
235
 
257
236
  except TimeoutError:
258
- raise RuntimeError(f"Command timeout after {self._timeout}s") from None
237
+ raise RuntimeError(f"Command timeout after {cmd_timeout}s") from None
259
238
  except websockets.exceptions.ConnectionClosedError as exc:
260
239
  raise RuntimeError(f"WebSocket connection closed: {exc}") from exc
261
240
  except RuntimeError:
@@ -270,13 +249,24 @@ class AsyncCommandClient:
270
249
  return await self._send_command("autopilot_status")
271
250
 
272
251
  async def autopilot_submit(
273
- self, description: str, *, priority: int = 50, workspace: str | None = None
252
+ self,
253
+ description: str,
254
+ *,
255
+ priority: int = 50,
256
+ workspace: str | None = None,
257
+ rail_id: str | None = None,
274
258
  ) -> dict[str, Any]:
275
- """Submit a new autopilot goal (returns ``goal_id``)."""
259
+ """Submit a new autopilot goal (returns ``goal_id``).
260
+
261
+ Uses a 120s RPC timeout so placement analysis can finish before the
262
+ client gives up (intake may call the verifier LLM).
263
+ """
276
264
  payload: dict[str, Any] = {"description": description, "priority": priority}
277
265
  if workspace:
278
266
  payload["workspace"] = workspace
279
- return await self._send_command("autopilot_submit", payload)
267
+ if rail_id:
268
+ payload["rail_id"] = rail_id
269
+ return await self._send_command("autopilot_submit", payload, timeout=120.0)
280
270
 
281
271
  async def autopilot_list_goals(self) -> dict[str, Any]:
282
272
  """List all goals (including non-root children)."""
@@ -318,6 +308,14 @@ class AsyncCommandClient:
318
308
  """Get a root job with DAG snapshot. Prefer ``job_status`` / ``job_dag``."""
319
309
  return await self._send_command("autopilot_get_job", {"job_id": job_id})
320
310
 
311
+ async def autopilot_top(self, *, include_terminal: bool = False) -> dict[str, Any]:
312
+ """Jobs → goals → loops snapshot for CLI ``top`` (IG-679 / IG-688).
313
+
314
+ Args:
315
+ include_terminal: When true, include terminal goals and jobs.
316
+ """
317
+ return await self._send_command("autopilot_top", {"include_terminal": include_terminal})
318
+
321
319
  async def autopilot_subscribe(self) -> dict[str, Any]:
322
320
  """Subscribe this connection to autopilot worker events."""
323
321
  return await self._send_command("autopilot_subscribe")
@@ -442,13 +440,23 @@ class CommandClient:
442
440
  return cast(dict[str, Any], self._run_async(self._client.autopilot_status()))
443
441
 
444
442
  def autopilot_submit(
445
- self, description: str, *, priority: int = 50, workspace: str | None = None
443
+ self,
444
+ description: str,
445
+ *,
446
+ priority: int = 50,
447
+ workspace: str | None = None,
448
+ rail_id: str | None = None,
446
449
  ) -> dict[str, Any]:
447
450
  """Submit a new autopilot goal."""
448
451
  return cast(
449
452
  dict[str, Any],
450
453
  self._run_async(
451
- self._client.autopilot_submit(description, priority=priority, workspace=workspace)
454
+ self._client.autopilot_submit(
455
+ description,
456
+ priority=priority,
457
+ workspace=workspace,
458
+ rail_id=rail_id,
459
+ )
452
460
  ),
453
461
  )
454
462
 
@@ -488,6 +496,13 @@ class CommandClient:
488
496
  """Get a root job with DAG snapshot."""
489
497
  return cast(dict[str, Any], self._run_async(self._client.autopilot_get_job(job_id)))
490
498
 
499
+ def autopilot_top(self, *, include_terminal: bool = False) -> dict[str, Any]:
500
+ """Jobs → goals → loops snapshot for CLI ``top``."""
501
+ return cast(
502
+ dict[str, Any],
503
+ self._run_async(self._client.autopilot_top(include_terminal=include_terminal)),
504
+ )
505
+
491
506
  def autopilot_subscribe(self) -> dict[str, Any]:
492
507
  """Subscribe to autopilot worker events."""
493
508
  return cast(dict[str, Any], self._run_async(self._client.autopilot_subscribe()))
@@ -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
  # ---------------------------------------------------------------------------