qbraid-cli 0.16.12__tar.gz → 0.16.14__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.12/qbraid_cli.egg-info → qbraid_cli-0.16.14}/PKG-INFO +4 -4
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/pyproject.toml +4 -4
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/agents/app.py +248 -74
- qbraid_cli-0.16.14/qbraid_cli/agents/keep_alive.py +357 -0
- qbraid_cli-0.16.14/qbraid_cli/agents/provider_children.py +87 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14/qbraid_cli.egg-info}/PKG-INFO +4 -4
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli.egg-info/SOURCES.txt +2 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli.egg-info/requires.txt +3 -3
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/tests/test_cli_e2e.py +5 -1
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/LICENSE +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/README.md +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/__init__.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/_build_info.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/_version.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/account/__init__.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/account/app.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/admin/__init__.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/admin/app.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/admin/headers.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/admin/validation.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/agents/__init__.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/agents/permission_response.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/agents/queue.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/agents/work.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/ai/__init__.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/ai/app.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/ai/token.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/comms/__init__.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/comms/app.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/compute/__init__.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/compute/app.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/compute/instances.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/compute/profiles.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/compute/server.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/compute/ssh.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/configure/__init__.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/configure/actions.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/configure/app.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/devices/__init__.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/devices/app.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/devices/validation.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/envs/__init__.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/envs/activate.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/envs/app.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/envs/create.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/envs/data_handling.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/envs/groups.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/envs/publish.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/envs/registry.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/envs/share.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/exceptions.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/handlers.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/jobs/__init__.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/jobs/app.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/jobs/groups.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/kernels/__init__.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/kernels/app.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/lab/__init__.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/lab/app.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/main.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/pip/__init__.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/pip/app.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/pip/hooks.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/projects/__init__.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/projects/app.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/py.typed +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/skills/__init__.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/skills/app.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/troubleshoot/__init__.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli/troubleshoot/app.py +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli.egg-info/dependency_links.txt +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli.egg-info/entry_points.txt +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/qbraid_cli.egg-info/top_level.txt +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/setup.cfg +0 -0
- {qbraid_cli-0.16.12 → qbraid_cli-0.16.14}/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.14
|
|
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.33
|
|
34
34
|
Provides-Extra: compute
|
|
35
|
-
Requires-Dist: qbraid-core[compute]>=0.6.
|
|
35
|
+
Requires-Dist: qbraid-core[compute]>=0.6.33; extra == "compute"
|
|
36
36
|
Provides-Extra: envs
|
|
37
|
-
Requires-Dist: qbraid-core[environments]>=0.6.
|
|
37
|
+
Requires-Dist: qbraid-core[environments]>=0.6.33; 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.14"
|
|
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.33",
|
|
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.33"]
|
|
47
|
+
envs = ["qbraid-core[environments]>=0.6.33"]
|
|
48
48
|
dev = ["isort", "black", "pytest", "pytest-cov", "pytest-asyncio"]
|
|
49
49
|
|
|
50
50
|
[project.scripts]
|
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
|
|
4
4
|
"""Agents CLI - Monitor and manage AI coding agent sessions."""
|
|
5
5
|
|
|
6
|
+
import errno
|
|
6
7
|
import inspect
|
|
7
8
|
import json
|
|
8
9
|
import os
|
|
9
10
|
import re
|
|
10
11
|
import signal
|
|
11
12
|
import time
|
|
13
|
+
from contextlib import nullcontext
|
|
12
14
|
from datetime import datetime
|
|
13
15
|
from enum import Enum
|
|
14
16
|
from pathlib import Path
|
|
@@ -23,6 +25,7 @@ from rich.table import Table
|
|
|
23
25
|
|
|
24
26
|
from qbraid_cli.handlers import handle_error, run_progress_task
|
|
25
27
|
|
|
28
|
+
from .keep_alive import KeepAliveCommandError, keep_alive_app, stop_plan
|
|
26
29
|
from .queue import interrupt, queue_app
|
|
27
30
|
from .work import register_work_commands
|
|
28
31
|
|
|
@@ -55,6 +58,7 @@ hooks_app = typer.Typer(
|
|
|
55
58
|
)
|
|
56
59
|
|
|
57
60
|
agents_app.add_typer(hooks_app, name="hooks")
|
|
61
|
+
agents_app.add_typer(keep_alive_app, name="keep-alive")
|
|
58
62
|
agents_app.add_typer(queue_app, name="queue")
|
|
59
63
|
agents_app.command("interrupt")(interrupt)
|
|
60
64
|
|
|
@@ -452,17 +456,23 @@ def _not_running_reason(session):
|
|
|
452
456
|
return None
|
|
453
457
|
|
|
454
458
|
|
|
455
|
-
def _signal_agent(pid, force):
|
|
459
|
+
def _signal_agent(pid, force, *, quiet=False):
|
|
456
460
|
sig = signal.SIGKILL if force else signal.SIGTERM
|
|
457
461
|
sig_name = "SIGKILL" if force else "SIGTERM"
|
|
458
462
|
try:
|
|
459
463
|
os.kill(pid, sig)
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
464
|
+
if not quiet:
|
|
465
|
+
console.print(f"[green]Sent {sig_name} to process {pid}.[/green]")
|
|
466
|
+
except OSError as error:
|
|
467
|
+
if not isinstance(error, ProcessLookupError) and error.errno != errno.ESRCH:
|
|
468
|
+
raise KeepAliveCommandError(
|
|
469
|
+
"STOP_FAILED", f"Stop was not confirmed for PID {pid}: {error}. Inspect its current status."
|
|
470
|
+
) from error
|
|
471
|
+
if not quiet:
|
|
472
|
+
console.print(f"[dim]Process {pid} already stopped.[/dim]")
|
|
463
473
|
|
|
464
474
|
|
|
465
|
-
def _kill_tmux_session(session):
|
|
475
|
+
def _kill_tmux_session(session, *, launcher=None, confirmation=None, as_json=False):
|
|
466
476
|
"""Stop a tmux-hosted agent only through a verified target.
|
|
467
477
|
|
|
468
478
|
A saved window name is mutable: another session may hold it now. Core's
|
|
@@ -472,25 +482,150 @@ def _kill_tmux_session(session):
|
|
|
472
482
|
"""
|
|
473
483
|
from qbraid_core.services.agents import AgentLauncher, AgentLauncherError
|
|
474
484
|
|
|
475
|
-
launcher = AgentLauncher()
|
|
485
|
+
launcher = launcher or AgentLauncher()
|
|
476
486
|
if not callable(getattr(launcher, "kill_bound", None)):
|
|
477
487
|
# Core has had kill_bound since 0.6.5; a saved window name alone is
|
|
478
488
|
# never trusted, so without it nothing is closed.
|
|
479
|
-
|
|
480
|
-
"[red]Not stopped:[/red] this qbraid-core cannot verify a tmux window; update qbraid-core.",
|
|
481
|
-
highlight=False,
|
|
482
|
-
)
|
|
483
|
-
raise typer.Exit(code=1)
|
|
489
|
+
_stop_refused("This qbraid-core cannot verify a tmux window; update qbraid-core.", as_json)
|
|
484
490
|
try:
|
|
485
|
-
launcher.kill_bound(session.session_id)
|
|
491
|
+
launcher.kill_bound(session.session_id, **_stop_options(confirmation))
|
|
486
492
|
except AgentLauncherError as error:
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
493
|
+
if as_json:
|
|
494
|
+
from .keep_alive import _error
|
|
495
|
+
|
|
496
|
+
_error(error, True)
|
|
497
|
+
_stop_refused(f"Core could not verify this session's tmux window. {error}", False)
|
|
498
|
+
_stop_success(session, as_json, f"Stopped the agent in tmux window '{session.tmux_window}'.")
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def _stop_options(confirmation):
|
|
502
|
+
return {"keep_alive_confirmation": confirmation} if confirmation is not None else {}
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _stop_refused(message, as_json, code="STOP_REFUSED"):
|
|
506
|
+
from .keep_alive import _error
|
|
507
|
+
from .keep_alive import _plain as safe_text
|
|
508
|
+
|
|
509
|
+
if as_json:
|
|
510
|
+
_error(KeepAliveCommandError(code, message), True)
|
|
511
|
+
console.print(f"[red]Not stopped:[/red] {escape(safe_text(message))}", highlight=False)
|
|
512
|
+
raise typer.Exit(code=1)
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def _stop_success(session, as_json, message):
|
|
516
|
+
from .keep_alive import _plain as safe_text
|
|
517
|
+
|
|
518
|
+
if as_json:
|
|
519
|
+
typer.echo(json.dumps({"ok": True, "sessionId": session.session_id, "stopped": True}))
|
|
520
|
+
else:
|
|
521
|
+
console.print(f"[green]{escape(safe_text(message))}[/green]", highlight=False)
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def _require_remote_stop_cli(compute):
|
|
525
|
+
"""Inspect help before any Stop; a legacy CLI cannot provide consent receipts."""
|
|
526
|
+
import subprocess
|
|
527
|
+
|
|
528
|
+
try:
|
|
529
|
+
probe = subprocess.run(
|
|
530
|
+
[
|
|
531
|
+
"ssh",
|
|
532
|
+
"-T",
|
|
533
|
+
"-o",
|
|
534
|
+
"BatchMode=yes",
|
|
535
|
+
"-o",
|
|
536
|
+
"ConnectTimeout=10",
|
|
537
|
+
compute,
|
|
538
|
+
"qbraid agents kill --help",
|
|
539
|
+
],
|
|
540
|
+
capture_output=True,
|
|
541
|
+
text=True,
|
|
542
|
+
check=False,
|
|
543
|
+
timeout=30,
|
|
544
|
+
stdin=subprocess.DEVNULL,
|
|
545
|
+
)
|
|
546
|
+
except (OSError, subprocess.TimeoutExpired) as error:
|
|
547
|
+
raise KeepAliveCommandError(
|
|
548
|
+
"DESTINATION_UNAVAILABLE",
|
|
549
|
+
"Could not inspect destination Stop capabilities. No Stop was attempted.",
|
|
550
|
+
{"attempted": False},
|
|
551
|
+
) from error
|
|
552
|
+
if probe.returncode == 255:
|
|
553
|
+
raise KeepAliveCommandError(
|
|
554
|
+
"DESTINATION_UNAVAILABLE",
|
|
555
|
+
"Destination SSH connection failed during the capability check. No Stop was attempted.",
|
|
556
|
+
{"attempted": False},
|
|
557
|
+
)
|
|
558
|
+
help_text = re.sub(r"\x1b\[[0-9;]*m", "", probe.stdout)
|
|
559
|
+
if probe.returncode or not all(option in help_text for option in ("--json", "--yes")):
|
|
560
|
+
raise KeepAliveCommandError(
|
|
561
|
+
"UNSUPPORTED_OPERATION",
|
|
562
|
+
"Update the destination qbraid-cli for confirmed remote Stop. No Stop was attempted.",
|
|
563
|
+
{"attempted": False},
|
|
491
564
|
)
|
|
492
|
-
|
|
493
|
-
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def _remote_stop(compute, target, force, yes, as_json):
|
|
568
|
+
"""New destination CLI owns consent; unknown authority cannot run legacy kill."""
|
|
569
|
+
import shlex
|
|
570
|
+
import subprocess
|
|
571
|
+
|
|
572
|
+
if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.@:-]{0,254}", compute):
|
|
573
|
+
raise KeepAliveCommandError("INVALID_ARGUMENT", "Use a configured SSH alias for --compute.")
|
|
574
|
+
_require_remote_stop_cli(compute)
|
|
575
|
+
command = "qbraid agents kill --json"
|
|
576
|
+
command += " --force" if force else ""
|
|
577
|
+
command += " --yes" if yes else ""
|
|
578
|
+
command += f" -- {shlex.quote(target)}"
|
|
579
|
+
try:
|
|
580
|
+
response = subprocess.run(
|
|
581
|
+
["ssh", "-T", "-o", "BatchMode=yes", "-o", "ConnectTimeout=10", compute, command],
|
|
582
|
+
capture_output=True,
|
|
583
|
+
text=True,
|
|
584
|
+
check=False,
|
|
585
|
+
timeout=30,
|
|
586
|
+
stdin=subprocess.DEVNULL,
|
|
587
|
+
)
|
|
588
|
+
except (OSError, subprocess.TimeoutExpired) as error:
|
|
589
|
+
raise KeepAliveCommandError(
|
|
590
|
+
"UNKNOWN_OUTCOME",
|
|
591
|
+
"Remote Stop was not confirmed. It was not retried; inspect the destination.",
|
|
592
|
+
) from error
|
|
593
|
+
try:
|
|
594
|
+
result = json.loads(response.stdout)
|
|
595
|
+
except (ValueError, UnicodeError, RecursionError):
|
|
596
|
+
raise KeepAliveCommandError(
|
|
597
|
+
"UNKNOWN_OUTCOME",
|
|
598
|
+
"Destination Stop could not be verified. Update its CLI/Core for confirmation support. "
|
|
599
|
+
"No command was retried; inspect the destination before retrying.",
|
|
600
|
+
) from None
|
|
601
|
+
if not isinstance(result, dict) or not isinstance(result.get("ok"), bool):
|
|
602
|
+
raise KeepAliveCommandError(
|
|
603
|
+
"UNKNOWN_OUTCOME", "Destination Stop receipt was invalid; no command was retried."
|
|
604
|
+
)
|
|
605
|
+
if not result["ok"]:
|
|
606
|
+
failure = result.get("error")
|
|
607
|
+
if not isinstance(failure, dict) or not isinstance(failure.get("message"), str):
|
|
608
|
+
raise KeepAliveCommandError(
|
|
609
|
+
"UNKNOWN_OUTCOME", "Destination Stop error was invalid; no command was retried."
|
|
610
|
+
)
|
|
611
|
+
raise KeepAliveCommandError(
|
|
612
|
+
failure.get("code") or "STOP_REFUSED", failure["message"], failure.get("details")
|
|
613
|
+
)
|
|
614
|
+
if (
|
|
615
|
+
response.returncode
|
|
616
|
+
or result.get("stopped") is not True
|
|
617
|
+
or not isinstance(result.get("sessionId"), str)
|
|
618
|
+
or not result["sessionId"]
|
|
619
|
+
):
|
|
620
|
+
raise KeepAliveCommandError(
|
|
621
|
+
"UNKNOWN_OUTCOME", "Destination did not confirm an exact stopped session; no command was retried."
|
|
622
|
+
)
|
|
623
|
+
if as_json:
|
|
624
|
+
typer.echo(json.dumps(result, ensure_ascii=True))
|
|
625
|
+
else:
|
|
626
|
+
from .keep_alive import _plain as safe_text
|
|
627
|
+
|
|
628
|
+
console.print(f"[green]Stopped '{escape(safe_text(target))}' on {escape(compute)}.[/green]")
|
|
494
629
|
|
|
495
630
|
|
|
496
631
|
_GONE = "gone"
|
|
@@ -1241,6 +1376,9 @@ def agents_update(
|
|
|
1241
1376
|
console.print("[dim]Use 'qbraid agents list' to see active sessions.[/dim]")
|
|
1242
1377
|
raise typer.Exit(code=1)
|
|
1243
1378
|
except Exception as e:
|
|
1379
|
+
if _is_registry_error(e):
|
|
1380
|
+
console.print(f"[red]Cannot update session: {_plain(e)}[/red]", highlight=False)
|
|
1381
|
+
raise typer.Exit(code=1) from e
|
|
1244
1382
|
handle_error(message=str(e))
|
|
1245
1383
|
raise typer.Exit(code=1)
|
|
1246
1384
|
|
|
@@ -1255,6 +1393,8 @@ def agents_kill(
|
|
|
1255
1393
|
target: str = typer.Argument(..., help="Agent name, ID prefix, or PID"),
|
|
1256
1394
|
force: bool = typer.Option(False, "--force", "-f", help="Use SIGKILL instead of SIGTERM"),
|
|
1257
1395
|
compute: Optional[str] = typer.Option(None, "--compute", "-c", help="SSH alias for remote compute"),
|
|
1396
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Confirm disabling keep-alive before Stop."),
|
|
1397
|
+
as_json: bool = typer.Option(False, "--json", help="Print a JSON receipt; enrolled Stop requires --yes."),
|
|
1258
1398
|
):
|
|
1259
1399
|
"""Stop an agent by name, session ID, or PID.
|
|
1260
1400
|
|
|
@@ -1266,11 +1406,7 @@ def agents_kill(
|
|
|
1266
1406
|
from qbraid_core.services.agents import AgentSessionRegistryManager
|
|
1267
1407
|
|
|
1268
1408
|
if compute:
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
flags = " --force" if force else ""
|
|
1272
|
-
_ssh_cmd(compute, f"qbraid agents kill {shlex.quote(target)}{flags}")
|
|
1273
|
-
console.print(f"[green]Stopped '{target}' on {compute}.[/green]")
|
|
1409
|
+
_remote_stop(compute, target, force, yes, as_json)
|
|
1274
1410
|
return
|
|
1275
1411
|
|
|
1276
1412
|
registry = AgentSessionRegistryManager()
|
|
@@ -1300,11 +1436,14 @@ def agents_kill(
|
|
|
1300
1436
|
]
|
|
1301
1437
|
|
|
1302
1438
|
if not matches:
|
|
1303
|
-
|
|
1304
|
-
raise typer.Exit(code=1)
|
|
1439
|
+
_stop_refused(f"No session found matching '{target}'.", as_json, "SESSION_NOT_FOUND")
|
|
1305
1440
|
|
|
1306
1441
|
# If multiple matches, show filtered picker
|
|
1307
1442
|
if len(matches) > 1:
|
|
1443
|
+
if as_json or not _can_ask():
|
|
1444
|
+
_stop_refused(
|
|
1445
|
+
"Multiple sessions match; use a complete, exact session ID.", as_json, "AMBIGUOUS_SESSION"
|
|
1446
|
+
)
|
|
1308
1447
|
import questionary
|
|
1309
1448
|
|
|
1310
1449
|
console.print(f"[yellow]Multiple sessions match '{target}':[/yellow]")
|
|
@@ -1334,87 +1473,104 @@ def agents_kill(
|
|
|
1334
1473
|
matches = [s for s in matches if s.session_id == selected]
|
|
1335
1474
|
|
|
1336
1475
|
session = matches[0]
|
|
1476
|
+
if getattr(session, "runtime_kind", None) == "provider_child":
|
|
1477
|
+
_stop_refused(
|
|
1478
|
+
"Native provider children are read-only. Manage this child from its parent agent.",
|
|
1479
|
+
as_json,
|
|
1480
|
+
"READ_ONLY_SESSION",
|
|
1481
|
+
)
|
|
1337
1482
|
pid = session.pid
|
|
1338
1483
|
|
|
1339
1484
|
# Shown as interrupted (another Lab's row this Lab cannot verify):
|
|
1340
1485
|
# refused before anything, Core included, so nothing is written for
|
|
1341
1486
|
# it. Other native rows go to Core, which asks their owning server.
|
|
1342
1487
|
if (_lifecycle_view(registry, session).metadata or {}).get("status_overlay"):
|
|
1343
|
-
|
|
1344
|
-
"
|
|
1488
|
+
_stop_refused(
|
|
1489
|
+
"It belongs to another Lab that this Lab cannot verify, so it is "
|
|
1345
1490
|
"listed as interrupted. Resume it with --confirm-owner-gone, or stop it from its own Lab. "
|
|
1346
1491
|
"Nothing was signalled or closed.",
|
|
1347
|
-
|
|
1492
|
+
as_json,
|
|
1348
1493
|
)
|
|
1349
|
-
raise typer.Exit(code=1)
|
|
1350
1494
|
|
|
1351
|
-
|
|
1495
|
+
native = session.metadata.get("terminal_backend") == "jupyter"
|
|
1496
|
+
if not native:
|
|
1497
|
+
refusal = _foreign_pid_reason(session) or _not_running_reason(session)
|
|
1498
|
+
if refusal:
|
|
1499
|
+
_stop_refused(f"{refusal} Nothing was signalled or closed.", as_json)
|
|
1500
|
+
|
|
1501
|
+
from qbraid_core.services.agents import AgentLauncher
|
|
1502
|
+
|
|
1503
|
+
launcher = AgentLauncher(registry=registry)
|
|
1504
|
+
recovery, confirmation = stop_plan(launcher, session, yes=yes, as_json=as_json, can_ask=_can_ask)
|
|
1505
|
+
|
|
1506
|
+
if native:
|
|
1352
1507
|
# A native agent belongs to its Lab server, which Core reaches and
|
|
1353
1508
|
# verifies; its PID may be another namespace's. Never signalled here.
|
|
1354
1509
|
from qbraid_core.services.agents import AgentLauncher, AgentLauncherError
|
|
1355
1510
|
from qbraid_core.services.agents.exceptions import AgentRuntimeUnavailableError
|
|
1356
1511
|
|
|
1357
1512
|
try:
|
|
1358
|
-
|
|
1513
|
+
launcher.kill_bound(session.session_id, **_stop_options(confirmation))
|
|
1359
1514
|
except AgentRuntimeUnavailableError as error:
|
|
1360
1515
|
if _not_running_reason(session):
|
|
1361
1516
|
# Nothing to verify: Core says why (e.g. no longer active).
|
|
1362
|
-
|
|
1517
|
+
_stop_refused(str(error), as_json)
|
|
1363
1518
|
else:
|
|
1364
|
-
|
|
1365
|
-
"
|
|
1366
|
-
|
|
1367
|
-
f"{escape(str(error))}",
|
|
1368
|
-
highlight=False,
|
|
1519
|
+
_stop_refused(
|
|
1520
|
+
f"The owning Lab did not confirm Stop. Inspect its current status. {error}",
|
|
1521
|
+
as_json,
|
|
1369
1522
|
)
|
|
1370
|
-
raise typer.Exit(code=1)
|
|
1371
1523
|
except AgentLauncherError as error:
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1524
|
+
if as_json:
|
|
1525
|
+
from .keep_alive import _error
|
|
1526
|
+
|
|
1527
|
+
_error(error, True)
|
|
1528
|
+
_stop_refused(str(error), False)
|
|
1529
|
+
_stop_success(session, as_json, "Stopped native Lab agent.")
|
|
1375
1530
|
return
|
|
1376
1531
|
|
|
1377
1532
|
# Refused before anything is touched: a row of another PID namespace or
|
|
1378
1533
|
# machine, or one no longer running, names a PID and a tmux window that
|
|
1379
1534
|
# may now belong to someone else.
|
|
1380
|
-
refusal = _foreign_pid_reason(session) or _not_running_reason(session)
|
|
1381
|
-
if refusal:
|
|
1382
|
-
console.print(
|
|
1383
|
-
f"[red]Not stopped:[/red] {escape(refusal)} Nothing was signalled or closed.", highlight=False
|
|
1384
|
-
)
|
|
1385
|
-
raise typer.Exit(code=1)
|
|
1386
|
-
|
|
1387
1535
|
if session.tmux_session and session.tmux_window:
|
|
1388
|
-
_kill_tmux_session(session)
|
|
1536
|
+
_kill_tmux_session(session, launcher=launcher, confirmation=confirmation, as_json=as_json)
|
|
1389
1537
|
return
|
|
1390
1538
|
|
|
1391
1539
|
if not (pid and pid > 0):
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1540
|
+
_stop_refused("No agent process or terminal is recorded for this session.", as_json)
|
|
1541
|
+
guard = recovery.keep_alive_stop_guard(launcher, None, suppress=False) if recovery else nullcontext()
|
|
1542
|
+
with guard:
|
|
1543
|
+
if recovery and recovery.recovery_bound_pids(registry, {pid}):
|
|
1544
|
+
_stop_refused(
|
|
1545
|
+
"This PID belongs to a recovery-bound runtime. Use its exact managed session Stop.",
|
|
1546
|
+
as_json,
|
|
1547
|
+
)
|
|
1548
|
+
identity = _agent_identity_refusal(session)
|
|
1549
|
+
if identity == _GONE:
|
|
1550
|
+
if not as_json:
|
|
1551
|
+
console.print(f"[dim]Process {pid} already stopped.[/dim]")
|
|
1552
|
+
elif identity:
|
|
1553
|
+
_stop_refused(f"Did not signal PID {pid}: {identity}", as_json)
|
|
1554
|
+
else:
|
|
1555
|
+
_signal_agent(pid, force, quiet=as_json)
|
|
1556
|
+
registry.deregister_session(session_id=session.session_id)
|
|
1557
|
+
_stop_success(
|
|
1558
|
+
session,
|
|
1559
|
+
as_json,
|
|
1560
|
+
f"Session {session.session_id[:8]} "
|
|
1561
|
+
f"({session.terminal_name or session.tmux_window or '-'}) stopped.",
|
|
1412
1562
|
)
|
|
1413
1563
|
|
|
1414
1564
|
except (typer.Exit, SystemExit):
|
|
1415
1565
|
raise
|
|
1416
1566
|
except Exception as e:
|
|
1417
|
-
|
|
1567
|
+
from .keep_alive import _plain as safe_text
|
|
1568
|
+
|
|
1569
|
+
if as_json:
|
|
1570
|
+
from .keep_alive import _error
|
|
1571
|
+
|
|
1572
|
+
_error(e, True)
|
|
1573
|
+
console.print(f"[red]Stop was not confirmed:[/red] {escape(safe_text(e))}", highlight=False)
|
|
1418
1574
|
raise typer.Exit(code=1)
|
|
1419
1575
|
|
|
1420
1576
|
|
|
@@ -2848,7 +3004,8 @@ def agents_watch(
|
|
|
2848
3004
|
import subprocess as _sp
|
|
2849
3005
|
|
|
2850
3006
|
remote_flags = f"qbraid agents watch {shlex.quote(session_id)} --stream --lines {lines}"
|
|
2851
|
-
|
|
3007
|
+
if not stream:
|
|
3008
|
+
console.print(f"[dim]Watching {session_id} on {compute}... (Ctrl+C to stop)[/dim]")
|
|
2852
3009
|
|
|
2853
3010
|
proc = _sp.Popen( # pylint: disable=consider-using-with
|
|
2854
3011
|
["ssh", "-o", "ConnectTimeout=10", compute, remote_flags],
|
|
@@ -2871,6 +3028,11 @@ def agents_watch(
|
|
|
2871
3028
|
try:
|
|
2872
3029
|
|
|
2873
3030
|
event = json.loads(line)
|
|
3031
|
+
if event.get("runtime_kind") == "provider_child":
|
|
3032
|
+
from .provider_children import render_child_event
|
|
3033
|
+
|
|
3034
|
+
render_child_event(console, event)
|
|
3035
|
+
continue
|
|
2874
3036
|
entry_type = event.get("type", "unknown")
|
|
2875
3037
|
content = event.get("content", "")
|
|
2876
3038
|
ts = event.get("timestamp")
|
|
@@ -2895,12 +3057,18 @@ def agents_watch(
|
|
|
2895
3057
|
console.print(line_text, highlight=False)
|
|
2896
3058
|
except Exception:
|
|
2897
3059
|
console.print(line, highlight=False)
|
|
3060
|
+
remote_status = proc.wait()
|
|
3061
|
+
if remote_status:
|
|
3062
|
+
raise typer.Exit(code=remote_status if remote_status > 0 else 1)
|
|
2898
3063
|
except KeyboardInterrupt:
|
|
2899
|
-
proc.terminate()
|
|
2900
|
-
console.print("\n[dim]Stopped watching.[/dim]")
|
|
2901
|
-
finally:
|
|
2902
3064
|
proc.terminate()
|
|
2903
3065
|
proc.wait()
|
|
3066
|
+
if not stream:
|
|
3067
|
+
console.print("\n[dim]Stopped watching.[/dim]")
|
|
3068
|
+
finally:
|
|
3069
|
+
if proc.poll() is None:
|
|
3070
|
+
proc.terminate()
|
|
3071
|
+
proc.wait()
|
|
2904
3072
|
return
|
|
2905
3073
|
|
|
2906
3074
|
from qbraid_core.services.agents import get_parser_for_tool, watch_transcript
|
|
@@ -2913,6 +3081,12 @@ def agents_watch(
|
|
|
2913
3081
|
console.print("[dim]Use 'qbraid agents list' to see active sessions.[/dim]")
|
|
2914
3082
|
raise typer.Exit(code=1)
|
|
2915
3083
|
|
|
3084
|
+
if getattr(session, "runtime_kind", None) == "provider_child":
|
|
3085
|
+
from .provider_children import watch_child
|
|
3086
|
+
|
|
3087
|
+
watch_child(registry, session.session_id, lines=lines, stream=stream, console=console)
|
|
3088
|
+
return
|
|
3089
|
+
|
|
2916
3090
|
tool_value = session.tool.value if session.tool else None
|
|
2917
3091
|
|
|
2918
3092
|
# OpenCode uses SQLite DB instead of transcript files
|