qbraid-cli 0.16.15__tar.gz → 0.16.16__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.
- {qbraid_cli-0.16.15/qbraid_cli.egg-info → qbraid_cli-0.16.16}/PKG-INFO +4 -4
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/pyproject.toml +4 -4
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/agents/queue.py +180 -12
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16/qbraid_cli.egg-info}/PKG-INFO +4 -4
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli.egg-info/requires.txt +3 -3
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/LICENSE +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/README.md +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/__init__.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/_build_info.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/_version.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/account/__init__.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/account/app.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/admin/__init__.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/admin/app.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/admin/headers.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/admin/validation.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/agents/__init__.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/agents/app.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/agents/keep_alive.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/agents/permission_response.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/agents/provider_children.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/agents/work.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/ai/__init__.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/ai/app.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/ai/token.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/comms/__init__.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/comms/app.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/compute/__init__.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/compute/app.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/compute/instances.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/compute/profiles.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/compute/server.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/compute/ssh.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/configure/__init__.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/configure/actions.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/configure/app.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/devices/__init__.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/devices/app.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/devices/validation.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/envs/__init__.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/envs/activate.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/envs/app.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/envs/create.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/envs/data_handling.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/envs/groups.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/envs/publish.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/envs/registry.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/envs/share.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/exceptions.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/handlers.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/jobs/__init__.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/jobs/app.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/jobs/groups.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/kernels/__init__.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/kernels/app.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/lab/__init__.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/lab/app.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/main.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/pip/__init__.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/pip/app.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/pip/hooks.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/projects/__init__.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/projects/app.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/py.typed +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/skills/__init__.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/skills/app.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/troubleshoot/__init__.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli/troubleshoot/app.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli.egg-info/SOURCES.txt +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli.egg-info/dependency_links.txt +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli.egg-info/entry_points.txt +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/qbraid_cli.egg-info/top_level.txt +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/setup.cfg +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/tests/test_cli_e2e.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.16}/tests/test_version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qbraid-cli
|
|
3
|
-
Version: 0.16.
|
|
3
|
+
Version: 0.16.16
|
|
4
4
|
Summary: Command Line Interface for interacting with all parts of the qBraid platform.
|
|
5
5
|
Author-email: qBraid Development Team <contact@qbraid.com>
|
|
6
6
|
License-Expression: LicenseRef-Proprietary
|
|
@@ -30,11 +30,11 @@ Requires-Dist: typer>=0.12.1
|
|
|
30
30
|
Requires-Dist: rich>=10.11.0
|
|
31
31
|
Requires-Dist: click<=8.5.0
|
|
32
32
|
Requires-Dist: questionary>=2.0.0
|
|
33
|
-
Requires-Dist: qbraid-core>=0.6.
|
|
33
|
+
Requires-Dist: qbraid-core>=0.6.35
|
|
34
34
|
Provides-Extra: compute
|
|
35
|
-
Requires-Dist: qbraid-core[compute]>=0.6.
|
|
35
|
+
Requires-Dist: qbraid-core[compute]>=0.6.35; extra == "compute"
|
|
36
36
|
Provides-Extra: envs
|
|
37
|
-
Requires-Dist: qbraid-core[environments]>=0.6.
|
|
37
|
+
Requires-Dist: qbraid-core[environments]>=0.6.35; extra == "envs"
|
|
38
38
|
Provides-Extra: dev
|
|
39
39
|
Requires-Dist: isort; extra == "dev"
|
|
40
40
|
Requires-Dist: black; extra == "dev"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "qbraid-cli"
|
|
7
|
-
version = "0.16.
|
|
7
|
+
version = "0.16.16"
|
|
8
8
|
description = "Command Line Interface for interacting with all parts of the qBraid platform."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [{ name = "qBraid Development Team", email = "contact@qbraid.com" }]
|
|
@@ -32,7 +32,7 @@ dependencies = [
|
|
|
32
32
|
"rich>=10.11.0",
|
|
33
33
|
"click<=8.5.0",
|
|
34
34
|
"questionary>=2.0.0",
|
|
35
|
-
"qbraid-core>=0.6.
|
|
35
|
+
"qbraid-core>=0.6.35",
|
|
36
36
|
]
|
|
37
37
|
requires-python = ">= 3.10"
|
|
38
38
|
|
|
@@ -43,8 +43,8 @@ Documentation = "https://docs.qbraid.com/v2/cli/api-reference/qbraid"
|
|
|
43
43
|
Discord = "https://discord.gg/KugF6Cnncm"
|
|
44
44
|
|
|
45
45
|
[project.optional-dependencies]
|
|
46
|
-
compute = ["qbraid-core[compute]>=0.6.
|
|
47
|
-
envs = ["qbraid-core[environments]>=0.6.
|
|
46
|
+
compute = ["qbraid-core[compute]>=0.6.35"]
|
|
47
|
+
envs = ["qbraid-core[environments]>=0.6.35"]
|
|
48
48
|
dev = ["isort", "black", "pytest", "pytest-cov", "pytest-asyncio"]
|
|
49
49
|
|
|
50
50
|
[project.scripts]
|
|
@@ -25,15 +25,17 @@ _MAX_RESPONSE = 2 * 1024 * 1024
|
|
|
25
25
|
_SSH_TIMEOUT = 120
|
|
26
26
|
_STATES = {"queued", "sending", "sent", "uncertain", "cancelled", "stale", "expired", "resolved"}
|
|
27
27
|
_ACTION_STATES = {"queue_edit": "queued", "queue_cancel": "cancelled", "queue_resolve": "resolved"}
|
|
28
|
+
_HOLD_CAPABILITY = "message-queue-hold-v1"
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
class QueueCommandError(Exception):
|
|
31
32
|
"""A safe failure with a request identifier for reconciliation."""
|
|
32
33
|
|
|
33
|
-
def __init__(self, code, message, request_id=None):
|
|
34
|
+
def __init__(self, code, message, request_id=None, *, retryable=False):
|
|
34
35
|
super().__init__(message)
|
|
35
36
|
self.code = code
|
|
36
37
|
self.request_id = request_id
|
|
38
|
+
self.retryable = retryable
|
|
37
39
|
|
|
38
40
|
|
|
39
41
|
def _plain(value, multiline=False):
|
|
@@ -112,6 +114,59 @@ def _valid_receipt(value, session_id, machine_id):
|
|
|
112
114
|
)
|
|
113
115
|
|
|
114
116
|
|
|
117
|
+
def _valid_hold(value, session_id, binding, *, state="held"):
|
|
118
|
+
"""Validate the Core hold schema and exact destination binding."""
|
|
119
|
+
keys = {
|
|
120
|
+
"id",
|
|
121
|
+
"sessionId",
|
|
122
|
+
"expectedRuntime",
|
|
123
|
+
"receiver",
|
|
124
|
+
"machineId",
|
|
125
|
+
"bootId",
|
|
126
|
+
"turn",
|
|
127
|
+
"revision",
|
|
128
|
+
"state",
|
|
129
|
+
"reason",
|
|
130
|
+
"createdAt",
|
|
131
|
+
"updatedAt",
|
|
132
|
+
}
|
|
133
|
+
if not isinstance(value, dict) or set(value) != keys or not _valid_runtime(value["expectedRuntime"]):
|
|
134
|
+
return False
|
|
135
|
+
runtime = value["expectedRuntime"]
|
|
136
|
+
if set(runtime) != {"pid", "startedAt", "tool", "terminalId", "serverId", "transcriptPath"}:
|
|
137
|
+
return False
|
|
138
|
+
receiver = value["receiver"]
|
|
139
|
+
native = receiver is None and all(
|
|
140
|
+
isinstance(runtime[key], str) and runtime[key] for key in ("terminalId", "serverId")
|
|
141
|
+
)
|
|
142
|
+
tmux = (
|
|
143
|
+
runtime["terminalId"] is None
|
|
144
|
+
and runtime["serverId"] is None
|
|
145
|
+
and isinstance(receiver, dict)
|
|
146
|
+
and bool(receiver)
|
|
147
|
+
and len(json.dumps(receiver)) <= 4096
|
|
148
|
+
)
|
|
149
|
+
return all(
|
|
150
|
+
(
|
|
151
|
+
_canonical_uuid(value["id"]),
|
|
152
|
+
value["sessionId"] == session_id,
|
|
153
|
+
value["machineId"] == binding["expectedMachineId"],
|
|
154
|
+
value["bootId"] == binding["expectedBootId"],
|
|
155
|
+
_stamp(runtime["startedAt"]),
|
|
156
|
+
isinstance(runtime["transcriptPath"], str) and bool(runtime["transcriptPath"]),
|
|
157
|
+
native or tmux,
|
|
158
|
+
isinstance(value["turn"], str) and re.fullmatch(r"[0-9a-f]{64}", value["turn"]),
|
|
159
|
+
isinstance(value["revision"], int)
|
|
160
|
+
and not isinstance(value["revision"], bool)
|
|
161
|
+
and value["revision"] > 0,
|
|
162
|
+
value["state"] == state,
|
|
163
|
+
isinstance(value["reason"], str),
|
|
164
|
+
_stamp(value["createdAt"]),
|
|
165
|
+
_stamp(value["updatedAt"]),
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
115
170
|
def _new_protocol():
|
|
116
171
|
try:
|
|
117
172
|
from qbraid_core.services.agents.machine_protocol import MachineProtocol
|
|
@@ -281,7 +336,10 @@ class Destination:
|
|
|
281
336
|
"UNKNOWN_OUTCOME", "Unconfirmed destination error; inspect request status.", rid
|
|
282
337
|
)
|
|
283
338
|
raise QueueCommandError(
|
|
284
|
-
error.get("code", "OPERATION_FAILED"),
|
|
339
|
+
error.get("code", "OPERATION_FAILED"),
|
|
340
|
+
error.get("message", "Request failed."),
|
|
341
|
+
rid,
|
|
342
|
+
retryable=error.get("retryable") is True,
|
|
285
343
|
)
|
|
286
344
|
result = response.get("result")
|
|
287
345
|
if not isinstance(result, dict):
|
|
@@ -381,6 +439,10 @@ def _emit(result, json_output, request_id=None, *, ok=True):
|
|
|
381
439
|
envelope["requestId"] = request_id
|
|
382
440
|
typer.echo(json.dumps(envelope, ensure_ascii=True))
|
|
383
441
|
elif "messages" in result:
|
|
442
|
+
if result.get("hold"):
|
|
443
|
+
hold = result["hold"]
|
|
444
|
+
typer.echo(f"Queue paused. Hold {_plain(hold['id'])}, revision {_plain(hold['revision'])}.")
|
|
445
|
+
typer.echo("Use queue continue with this hold ID and revision to allow automatic delivery.")
|
|
384
446
|
for row in result["messages"]:
|
|
385
447
|
typer.echo(f"{_plain(row['id'])} {_plain(row['state'])} revision {_plain(row['revision'])}")
|
|
386
448
|
prose = _plain(row["text"], multiline=True).replace("\n", "\n ")
|
|
@@ -396,18 +458,64 @@ def _emit(result, json_output, request_id=None, *, ok=True):
|
|
|
396
458
|
typer.echo(status)
|
|
397
459
|
if result.get("reason"):
|
|
398
460
|
typer.echo(_plain(result["reason"]))
|
|
461
|
+
elif result.get("interruptRequested") is True:
|
|
462
|
+
typer.echo("Interrupt requested; queued input is paused.")
|
|
463
|
+
elif isinstance(result.get("hold"), dict) and result["hold"].get("state") == "released":
|
|
464
|
+
typer.echo("Queue continued. A running watcher will deliver when the agent is ready.")
|
|
465
|
+
elif result.get("waiting") == "BUSY":
|
|
466
|
+
typer.echo("Queue is busy; the watcher will retry on its next tick.")
|
|
467
|
+
elif "delivered" in result and isinstance(result["delivered"], list):
|
|
468
|
+
typer.echo(f"Delivered {len(result['delivered'])} queued message(s).")
|
|
469
|
+
elif isinstance(result.get("state"), str):
|
|
470
|
+
typer.echo(f"Request status: {_plain(result['state'])}")
|
|
471
|
+
elif type(result.get("canInterrupt")) is bool: # pylint: disable=unidiomatic-typecheck
|
|
472
|
+
if result["canInterrupt"]:
|
|
473
|
+
typer.echo("Interrupt is available for the verified active turn.")
|
|
474
|
+
else:
|
|
475
|
+
reason = result.get("reason")
|
|
476
|
+
typer.echo(
|
|
477
|
+
f"Interrupt unavailable: {_plain(reason)}"
|
|
478
|
+
if isinstance(reason, str)
|
|
479
|
+
else "Interrupt unavailable."
|
|
480
|
+
)
|
|
399
481
|
else:
|
|
400
|
-
|
|
482
|
+
# Never serialize arbitrary protocol results to a human terminal. Some
|
|
483
|
+
# operations include receiver identities and transcript paths.
|
|
484
|
+
typer.echo("Operation completed.")
|
|
401
485
|
|
|
402
486
|
|
|
403
487
|
def _mutation(destination, operation, json_output, request_id, **fields):
|
|
404
488
|
rid = _uuid(request_id)
|
|
405
489
|
# Written before crossing the transport, so even Ctrl+C leaves the ID visible.
|
|
406
490
|
typer.echo(f"Request ID: {rid}", err=True)
|
|
407
|
-
result =
|
|
491
|
+
result = None
|
|
492
|
+
for attempt in range(4):
|
|
493
|
+
try:
|
|
494
|
+
result = destination.call(operation, rid, **fields)
|
|
495
|
+
break
|
|
496
|
+
except QueueCommandError as error:
|
|
497
|
+
# Core marks only queue-lock refusals that happened before any
|
|
498
|
+
# durable state or terminal write as retryable. A BUSY code alone
|
|
499
|
+
# is insufficient: transport-queue BUSY may not carry that proof.
|
|
500
|
+
if not error.retryable or error.code != "BUSY" or attempt == 3:
|
|
501
|
+
raise
|
|
502
|
+
time.sleep(0.1 * (2**attempt))
|
|
503
|
+
assert result is not None # every successful destination call returns a JSON object
|
|
408
504
|
if operation == "interrupt":
|
|
409
505
|
confirmed = (
|
|
410
|
-
result.get("interruptRequested") is True
|
|
506
|
+
result.get("interruptRequested") is True
|
|
507
|
+
and result.get("sessionId") == fields["sessionId"]
|
|
508
|
+
and _valid_hold(result.get("hold"), fields["sessionId"], destination.binding)
|
|
509
|
+
and result["hold"]["expectedRuntime"] == fields["expectedRuntime"]
|
|
510
|
+
and result["hold"]["turn"] == fields["expectedTurn"]
|
|
511
|
+
)
|
|
512
|
+
elif operation == "queue_resume":
|
|
513
|
+
hold = result.get("hold")
|
|
514
|
+
confirmed = (
|
|
515
|
+
_valid_hold(hold, fields["sessionId"], destination.binding, state="released")
|
|
516
|
+
and hold["id"] == fields["holdId"]
|
|
517
|
+
and hold["revision"] == fields["revision"] + 1
|
|
518
|
+
and hold["expectedRuntime"] == fields["expectedRuntime"]
|
|
411
519
|
)
|
|
412
520
|
else:
|
|
413
521
|
expected_id = rid if operation == "queue_enqueue" else fields["messageId"]
|
|
@@ -463,8 +571,8 @@ def enqueue(
|
|
|
463
571
|
)
|
|
464
572
|
if not json_output:
|
|
465
573
|
typer.echo(
|
|
466
|
-
"Queued input needs a running queue watcher
|
|
467
|
-
"
|
|
574
|
+
"Queued input needs a running queue watcher. Check queue list for its status; "
|
|
575
|
+
"if paused, use queue continue with the displayed hold ID and revision."
|
|
468
576
|
)
|
|
469
577
|
|
|
470
578
|
|
|
@@ -482,12 +590,29 @@ def list_messages(
|
|
|
482
590
|
messages: list[dict] = []
|
|
483
591
|
seen, seen_ids = set(), set()
|
|
484
592
|
cursor = None
|
|
593
|
+
hold = None
|
|
594
|
+
hold_supported = _HOLD_CAPABILITY in destination.snapshot["capabilities"]
|
|
485
595
|
for _ in range(257):
|
|
486
596
|
page = destination.call(
|
|
487
597
|
"queue_list", sessionId=session_id, **({"cursor": cursor} if cursor else {})
|
|
488
598
|
)
|
|
489
599
|
if not isinstance(page.get("messages"), list) or "nextCursor" not in page:
|
|
490
600
|
raise QueueCommandError("INVALID_RESPONSE", "Invalid queue listing.")
|
|
601
|
+
if hold_supported:
|
|
602
|
+
current_hold = page.get("hold")
|
|
603
|
+
if (
|
|
604
|
+
"hold" not in page
|
|
605
|
+
or current_hold is not None
|
|
606
|
+
and not _valid_hold(current_hold, session_id, destination.binding)
|
|
607
|
+
):
|
|
608
|
+
raise QueueCommandError("INVALID_RESPONSE", "Invalid queue hold receipt.")
|
|
609
|
+
if current_hold is not None and current_hold["expectedRuntime"] != destination.runtime(
|
|
610
|
+
session_id
|
|
611
|
+
):
|
|
612
|
+
raise QueueCommandError("QUEUE_CHANGED", "The paused runtime changed; list it again.")
|
|
613
|
+
if cursor is not None and current_hold != hold:
|
|
614
|
+
raise QueueCommandError("QUEUE_CHANGED", "The queue hold changed; list it again.")
|
|
615
|
+
hold = current_hold
|
|
491
616
|
for row in page["messages"]:
|
|
492
617
|
if (
|
|
493
618
|
not _valid_receipt(row, session_id, destination.snapshot["machineId"])
|
|
@@ -499,7 +624,10 @@ def list_messages(
|
|
|
499
624
|
messages.append(row)
|
|
500
625
|
cursor = page["nextCursor"]
|
|
501
626
|
if cursor is None:
|
|
502
|
-
_emit(
|
|
627
|
+
_emit(
|
|
628
|
+
{"messages": messages, "nextCursor": None, **({"hold": hold} if hold_supported else {})},
|
|
629
|
+
json_output,
|
|
630
|
+
)
|
|
503
631
|
return
|
|
504
632
|
if (
|
|
505
633
|
not _canonical_uuid(cursor)
|
|
@@ -512,6 +640,33 @@ def list_messages(
|
|
|
512
640
|
raise QueueCommandError("QUEUE_CHANGED", "Queue pagination changed; start the list again.")
|
|
513
641
|
|
|
514
642
|
|
|
643
|
+
@queue_app.command("continue")
|
|
644
|
+
def continue_queue(
|
|
645
|
+
session_id: str = typer.Argument(..., help="Exact destination session ID."),
|
|
646
|
+
hold_id: str = typer.Argument(..., help="Hold UUID shown by queue list."),
|
|
647
|
+
revision: int = typer.Option(..., "--revision", min=1, help="Hold revision shown by queue list."),
|
|
648
|
+
compute: Optional[str] = typer.Option(None, "--compute"),
|
|
649
|
+
request_id: Optional[str] = typer.Option(None, "--request-id"),
|
|
650
|
+
json_output: bool = typer.Option(False, "--json"),
|
|
651
|
+
):
|
|
652
|
+
"""Continue this paused queue.
|
|
653
|
+
|
|
654
|
+
Releases only the displayed hold. The watcher still checks turn completion,
|
|
655
|
+
pending approvals and uncertain messages; no input is sent by this command.
|
|
656
|
+
"""
|
|
657
|
+
with _action(json_output, compute, _HOLD_CAPABILITY) as destination:
|
|
658
|
+
_mutation(
|
|
659
|
+
destination,
|
|
660
|
+
"queue_resume",
|
|
661
|
+
json_output,
|
|
662
|
+
request_id,
|
|
663
|
+
sessionId=session_id,
|
|
664
|
+
expectedRuntime=destination.runtime(session_id),
|
|
665
|
+
holdId=_uuid(hold_id),
|
|
666
|
+
revision=revision,
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
|
|
515
670
|
@queue_app.command("edit")
|
|
516
671
|
def edit(
|
|
517
672
|
session_id: str = typer.Argument(...),
|
|
@@ -634,7 +789,12 @@ def watch(
|
|
|
634
789
|
raise QueueCommandError(
|
|
635
790
|
"UNSUPPORTED_OPERATION", "Update qbraid-core to host queued delivery."
|
|
636
791
|
) from None
|
|
637
|
-
|
|
792
|
+
# Opt in only when this CLI and destination share the hold contract.
|
|
793
|
+
# Older Core versions retain their original constructor signature.
|
|
794
|
+
watcher_options = (
|
|
795
|
+
{"track_native_holds": True} if _HOLD_CAPABILITY in destination.snapshot["capabilities"] else {}
|
|
796
|
+
)
|
|
797
|
+
watcher = MessageQueue(destination.protocol, **watcher_options)
|
|
638
798
|
if not once and not json_output:
|
|
639
799
|
typer.echo(f"Watching this machine's queue every {interval:g}s. Ctrl+C pauses delivery.")
|
|
640
800
|
try:
|
|
@@ -670,11 +830,18 @@ def interrupt(
|
|
|
670
830
|
):
|
|
671
831
|
"""Interrupt one verified active turn.
|
|
672
832
|
|
|
673
|
-
Request one Escape, without ending the agent process.
|
|
833
|
+
Request one Escape and pause queued input, without ending the agent process.
|
|
674
834
|
"""
|
|
675
835
|
with _action(json_output, compute, "interrupt-v1") as destination:
|
|
836
|
+
if _HOLD_CAPABILITY not in destination.snapshot["capabilities"]:
|
|
837
|
+
raise QueueCommandError(
|
|
838
|
+
"UNSUPPORTED_OPERATION",
|
|
839
|
+
"Update qbraid-core on this destination to pause queued input safely when interrupting.",
|
|
840
|
+
)
|
|
676
841
|
runtime = destination.runtime(session_id)
|
|
677
|
-
offer = destination.call(
|
|
842
|
+
offer = destination.call(
|
|
843
|
+
"interrupt_preflight", sessionId=session_id, expectedRuntime=runtime, requireQueueHold=True
|
|
844
|
+
)
|
|
678
845
|
if preflight:
|
|
679
846
|
available = offer.get("canInterrupt") is True
|
|
680
847
|
_emit(
|
|
@@ -697,6 +864,7 @@ def interrupt(
|
|
|
697
864
|
sessionId=session_id,
|
|
698
865
|
expectedRuntime=runtime,
|
|
699
866
|
expectedTurn=offer["turn"],
|
|
867
|
+
requireQueueHold=True,
|
|
700
868
|
)
|
|
701
869
|
if not json_output:
|
|
702
|
-
typer.echo("
|
|
870
|
+
typer.echo("This acknowledges Escape input, not that the agent has stopped.")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qbraid-cli
|
|
3
|
-
Version: 0.16.
|
|
3
|
+
Version: 0.16.16
|
|
4
4
|
Summary: Command Line Interface for interacting with all parts of the qBraid platform.
|
|
5
5
|
Author-email: qBraid Development Team <contact@qbraid.com>
|
|
6
6
|
License-Expression: LicenseRef-Proprietary
|
|
@@ -30,11 +30,11 @@ Requires-Dist: typer>=0.12.1
|
|
|
30
30
|
Requires-Dist: rich>=10.11.0
|
|
31
31
|
Requires-Dist: click<=8.5.0
|
|
32
32
|
Requires-Dist: questionary>=2.0.0
|
|
33
|
-
Requires-Dist: qbraid-core>=0.6.
|
|
33
|
+
Requires-Dist: qbraid-core>=0.6.35
|
|
34
34
|
Provides-Extra: compute
|
|
35
|
-
Requires-Dist: qbraid-core[compute]>=0.6.
|
|
35
|
+
Requires-Dist: qbraid-core[compute]>=0.6.35; extra == "compute"
|
|
36
36
|
Provides-Extra: envs
|
|
37
|
-
Requires-Dist: qbraid-core[environments]>=0.6.
|
|
37
|
+
Requires-Dist: qbraid-core[environments]>=0.6.35; extra == "envs"
|
|
38
38
|
Provides-Extra: dev
|
|
39
39
|
Requires-Dist: isort; extra == "dev"
|
|
40
40
|
Requires-Dist: black; extra == "dev"
|
|
@@ -2,10 +2,10 @@ typer>=0.12.1
|
|
|
2
2
|
rich>=10.11.0
|
|
3
3
|
click<=8.5.0
|
|
4
4
|
questionary>=2.0.0
|
|
5
|
-
qbraid-core>=0.6.
|
|
5
|
+
qbraid-core>=0.6.35
|
|
6
6
|
|
|
7
7
|
[compute]
|
|
8
|
-
qbraid-core[compute]>=0.6.
|
|
8
|
+
qbraid-core[compute]>=0.6.35
|
|
9
9
|
|
|
10
10
|
[dev]
|
|
11
11
|
isort
|
|
@@ -15,4 +15,4 @@ pytest-cov
|
|
|
15
15
|
pytest-asyncio
|
|
16
16
|
|
|
17
17
|
[envs]
|
|
18
|
-
qbraid-core[environments]>=0.6.
|
|
18
|
+
qbraid-core[environments]>=0.6.35
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|