envloop-cli 0.2.1__tar.gz → 0.4.0__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.
- envloop_cli-0.2.1/README.pypi.md → envloop_cli-0.4.0/PKG-INFO +39 -4
- envloop_cli-0.2.1/PKG-INFO → envloop_cli-0.4.0/README.pypi.md +26 -15
- {envloop_cli-0.2.1 → envloop_cli-0.4.0}/envloop/agent_runs.py +8 -8
- {envloop_cli-0.2.1 → envloop_cli-0.4.0}/envloop/auth_cli.py +4 -1
- {envloop_cli-0.2.1 → envloop_cli-0.4.0}/envloop/cli.py +195 -88
- envloop_cli-0.4.0/envloop/cli_errors.py +251 -0
- {envloop_cli-0.2.1 → envloop_cli-0.4.0}/envloop/cli_output.py +53 -20
- {envloop_cli-0.2.1 → envloop_cli-0.4.0}/envloop/client.py +130 -9
- {envloop_cli-0.2.1 → envloop_cli-0.4.0}/envloop/http.py +67 -13
- envloop_cli-0.4.0/envloop/job_logs.py +16 -0
- {envloop_cli-0.2.1 → envloop_cli-0.4.0}/envloop/navigation_resources.py +106 -18
- envloop_cli-0.4.0/envloop/profile_secrets.py +27 -0
- envloop_cli-0.4.0/envloop/profile_updates.py +56 -0
- {envloop_cli-0.2.1 → envloop_cli-0.4.0}/envloop/resource_cli.py +40 -38
- {envloop_cli-0.2.1 → envloop_cli-0.4.0}/envloop/resources.py +4 -2
- envloop_cli-0.4.0/envloop/sandbox.py +27 -0
- envloop_cli-0.4.0/envloop/train.py +68 -0
- envloop_cli-0.4.0/envloop/train_log_cli.py +97 -0
- {envloop_cli-0.2.1 → envloop_cli-0.4.0}/envloop/training/__init__.py +5 -2
- envloop_cli-0.4.0/envloop/training/credentials.py +107 -0
- envloop_cli-0.4.0/envloop/training/sampling_client.py +58 -0
- envloop_cli-0.4.0/envloop/training/service_client.py +165 -0
- {envloop_cli-0.2.1 → envloop_cli-0.4.0}/pyproject.toml +4 -2
- envloop_cli-0.2.1/envloop/train.py +0 -51
- envloop_cli-0.2.1/envloop/training/credentials.py +0 -56
- envloop_cli-0.2.1/envloop/training/service_client.py +0 -40
- {envloop_cli-0.2.1 → envloop_cli-0.4.0}/.gitignore +0 -0
- {envloop_cli-0.2.1 → envloop_cli-0.4.0}/envloop/__init__.py +0 -0
- {envloop_cli-0.2.1 → envloop_cli-0.4.0}/envloop/auth_login.py +0 -0
- {envloop_cli-0.2.1 → envloop_cli-0.4.0}/envloop/auth_workspace.py +0 -0
- {envloop_cli-0.2.1 → envloop_cli-0.4.0}/envloop/config.py +0 -0
- {envloop_cli-0.2.1 → envloop_cli-0.4.0}/envloop/lifecycle_cli.py +0 -0
- {envloop_cli-0.2.1 → envloop_cli-0.4.0}/envloop/utils/__init__.py +0 -0
- {envloop_cli-0.2.1 → envloop_cli-0.4.0}/envloop/utils/logging.py +0 -0
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: envloop-cli
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: EnvLoop CLI and Python SDK for jobs, training, checkpoints and inference
|
|
5
|
+
Project-URL: Homepage, https://envloop.ai
|
|
6
|
+
Project-URL: Documentation, https://docs.envloop.ai
|
|
7
|
+
Project-URL: Repository, https://github.com/EnvLoop/envloop-cli
|
|
8
|
+
Project-URL: Issues, https://github.com/EnvLoop/envloop-cli/issues
|
|
9
|
+
Project-URL: Agent instructions, https://docs.envloop.ai/skill.md
|
|
10
|
+
Requires-Python: >=3.11
|
|
11
|
+
Requires-Dist: tinker==0.34.0
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# envloop-cli: EnvLoop CLI and Python SDK
|
|
2
15
|
|
|
3
16
|
Install the public client to run agent Jobs, inspect Trials, manage training and
|
|
4
17
|
checkpoints, and deploy or call inference endpoints. The `el` and `envloop`
|
|
@@ -9,7 +22,7 @@ commands are equivalent. The Python import package is `envloop`.
|
|
|
9
22
|
Use a Python virtual environment:
|
|
10
23
|
|
|
11
24
|
```bash
|
|
12
|
-
python -m pip install envloop-cli
|
|
25
|
+
python -m pip install --upgrade envloop-cli
|
|
13
26
|
el --help
|
|
14
27
|
```
|
|
15
28
|
|
|
@@ -46,9 +59,11 @@ your secret manager. Public API access is scoped to the authorized project.
|
|
|
46
59
|
|
|
47
60
|
```bash
|
|
48
61
|
el job list --limit 5
|
|
49
|
-
el
|
|
62
|
+
el job list --type train --limit 5
|
|
63
|
+
el trail list --limit 5
|
|
64
|
+
el trail list --type train --job-id JOB_ID
|
|
50
65
|
el checkpoint list --limit 5
|
|
51
|
-
el
|
|
66
|
+
el job list --type inference
|
|
52
67
|
```
|
|
53
68
|
|
|
54
69
|
Query a returned Job ID with `el job get JOB_ID` and `el job result JOB_ID`.
|
|
@@ -62,12 +77,32 @@ from envloop import Client
|
|
|
62
77
|
|
|
63
78
|
with Client.remote() as client:
|
|
64
79
|
print(client.jobs.list(limit=5))
|
|
80
|
+
print(client.trials.list(type="train", limit=5))
|
|
65
81
|
```
|
|
66
82
|
|
|
67
83
|
Submitting work, training a model or starting inference can allocate billable
|
|
68
84
|
resources. Follow the task-specific guide and retain the outputs you need before
|
|
69
85
|
cleaning up temporary resources.
|
|
70
86
|
|
|
87
|
+
## Sandbox jobs
|
|
88
|
+
|
|
89
|
+
With a matching platform and a published `envhub/base` ANS Runner, submit a
|
|
90
|
+
Sandbox Job without a Dataset:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
el job submit --type sandbox --runner base --command 'python --version'
|
|
94
|
+
el job submit --type sandbox --runner base --image python:3.11 --wait
|
|
95
|
+
el job list --type sandbox
|
|
96
|
+
el job log JOB_ID --follow
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The platform resolves `base` to the registered `envhub/base@main` source ref and
|
|
100
|
+
freezes its immutable Revision. The Sandbox image defaults to `python:3.11`;
|
|
101
|
+
`--image` accepts an image name, tag or digest. The execution provider is configured
|
|
102
|
+
by the server. Job logs retain the actual captured output and collection metadata.
|
|
103
|
+
Failures include a code, readable explanation, HTTP status when available, and
|
|
104
|
+
recovery guidance without echoing arbitrary response bodies or credentials.
|
|
105
|
+
|
|
71
106
|
## Existing installations
|
|
72
107
|
|
|
73
108
|
This distribution provides both the CLI and the `envloop` import package. Use a
|
|
@@ -1,15 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
Name: envloop-cli
|
|
3
|
-
Version: 0.2.1
|
|
4
|
-
Summary: EnvLoop CLI and Python SDK for jobs, training, checkpoints and inference
|
|
5
|
-
Project-URL: Homepage, https://envloop.ai
|
|
6
|
-
Project-URL: Documentation, https://docs.envloop.ai
|
|
7
|
-
Project-URL: Agent instructions, https://docs.envloop.ai/skill.md
|
|
8
|
-
Requires-Python: >=3.11
|
|
9
|
-
Requires-Dist: tinker==0.30.2
|
|
10
|
-
Description-Content-Type: text/markdown
|
|
11
|
-
|
|
12
|
-
# EnvLoop CLI and Python SDK
|
|
1
|
+
# envloop-cli: EnvLoop CLI and Python SDK
|
|
13
2
|
|
|
14
3
|
Install the public client to run agent Jobs, inspect Trials, manage training and
|
|
15
4
|
checkpoints, and deploy or call inference endpoints. The `el` and `envloop`
|
|
@@ -20,7 +9,7 @@ commands are equivalent. The Python import package is `envloop`.
|
|
|
20
9
|
Use a Python virtual environment:
|
|
21
10
|
|
|
22
11
|
```bash
|
|
23
|
-
python -m pip install envloop-cli
|
|
12
|
+
python -m pip install --upgrade envloop-cli
|
|
24
13
|
el --help
|
|
25
14
|
```
|
|
26
15
|
|
|
@@ -57,9 +46,11 @@ your secret manager. Public API access is scoped to the authorized project.
|
|
|
57
46
|
|
|
58
47
|
```bash
|
|
59
48
|
el job list --limit 5
|
|
60
|
-
el
|
|
49
|
+
el job list --type train --limit 5
|
|
50
|
+
el trail list --limit 5
|
|
51
|
+
el trail list --type train --job-id JOB_ID
|
|
61
52
|
el checkpoint list --limit 5
|
|
62
|
-
el
|
|
53
|
+
el job list --type inference
|
|
63
54
|
```
|
|
64
55
|
|
|
65
56
|
Query a returned Job ID with `el job get JOB_ID` and `el job result JOB_ID`.
|
|
@@ -73,12 +64,32 @@ from envloop import Client
|
|
|
73
64
|
|
|
74
65
|
with Client.remote() as client:
|
|
75
66
|
print(client.jobs.list(limit=5))
|
|
67
|
+
print(client.trials.list(type="train", limit=5))
|
|
76
68
|
```
|
|
77
69
|
|
|
78
70
|
Submitting work, training a model or starting inference can allocate billable
|
|
79
71
|
resources. Follow the task-specific guide and retain the outputs you need before
|
|
80
72
|
cleaning up temporary resources.
|
|
81
73
|
|
|
74
|
+
## Sandbox jobs
|
|
75
|
+
|
|
76
|
+
With a matching platform and a published `envhub/base` ANS Runner, submit a
|
|
77
|
+
Sandbox Job without a Dataset:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
el job submit --type sandbox --runner base --command 'python --version'
|
|
81
|
+
el job submit --type sandbox --runner base --image python:3.11 --wait
|
|
82
|
+
el job list --type sandbox
|
|
83
|
+
el job log JOB_ID --follow
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The platform resolves `base` to the registered `envhub/base@main` source ref and
|
|
87
|
+
freezes its immutable Revision. The Sandbox image defaults to `python:3.11`;
|
|
88
|
+
`--image` accepts an image name, tag or digest. The execution provider is configured
|
|
89
|
+
by the server. Job logs retain the actual captured output and collection metadata.
|
|
90
|
+
Failures include a code, readable explanation, HTTP status when available, and
|
|
91
|
+
recovery guidance without echoing arbitrary response bodies or credentials.
|
|
92
|
+
|
|
82
93
|
## Existing installations
|
|
83
94
|
|
|
84
95
|
This distribution provides both the CLI and the `envloop` import package. Use a
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The platform stores no run object. Agents tag every Train Job they create with
|
|
4
4
|
``user_metadata`` (the RSI loop: ``rsi_run``, ``rsi_round``, ``rsi_checkpoint``...);
|
|
5
|
-
this module pages through ``GET /v1/
|
|
5
|
+
this module pages through ``GET /v1/job`` and derives runs from those tags,
|
|
6
6
|
using the same rules as the Console's agent runs page.
|
|
7
7
|
"""
|
|
8
8
|
|
|
@@ -19,8 +19,8 @@ KEYS = {
|
|
|
19
19
|
"checkpoint": ("agent_checkpoint", "rsi_checkpoint"),
|
|
20
20
|
}
|
|
21
21
|
RUN_KEYS = KEYS["run"] # first non-empty string wins
|
|
22
|
-
ACTIVE_STATUSES = frozenset({"LAUNCHING", "STARTING", "RUNNING", "RECOVERING"})
|
|
23
|
-
TERMINAL_STATUSES = frozenset({"SUCCEEDED", "FAILED", "CANCELLED"})
|
|
22
|
+
ACTIVE_STATUSES = frozenset({"LAUNCHING", "STARTING", "RUNNING", "RECOVERING", "queued", "pending", "starting", "running", "recovering", "cancelling"})
|
|
23
|
+
TERMINAL_STATUSES = frozenset({"SUCCEEDED", "FAILED", "CANCELLED", "completed", "failed", "cancelled"})
|
|
24
24
|
PAGE_LIMIT = 100 # the platform's maximum page size
|
|
25
25
|
|
|
26
26
|
|
|
@@ -70,7 +70,7 @@ def _round(job) -> int | None:
|
|
|
70
70
|
|
|
71
71
|
def _order(job):
|
|
72
72
|
# Creation order, as the platform pages jobs (newest first, reversed here).
|
|
73
|
-
return (str(job.get("created_at") or ""), str(job.get("train_job_id") or ""))
|
|
73
|
+
return (str(job.get("created_at") or ""), str(job.get("job_id") or job.get("train_job_id") or ""))
|
|
74
74
|
|
|
75
75
|
|
|
76
76
|
def _duration_sec(job) -> int | None:
|
|
@@ -97,7 +97,7 @@ def _checkpoint(job) -> dict | None:
|
|
|
97
97
|
def _job_view(job) -> dict:
|
|
98
98
|
metadata = _metadata(job)
|
|
99
99
|
return {
|
|
100
|
-
"train_job_id": job.get("train_job_id"),
|
|
100
|
+
"train_job_id": job.get("job_id") or job.get("train_job_id"),
|
|
101
101
|
"status": job.get("status"),
|
|
102
102
|
"round": _round(job),
|
|
103
103
|
"purpose": _field(job, "purpose"),
|
|
@@ -121,11 +121,11 @@ def _summary(run: str, jobs: list) -> dict:
|
|
|
121
121
|
status = "running"
|
|
122
122
|
else:
|
|
123
123
|
status = str(latest.get("status") or "unknown").lower()
|
|
124
|
-
succeeded = [job for job in jobs if job.get("status")
|
|
124
|
+
succeeded = [job for job in jobs if job.get("status") in {"SUCCEEDED", "completed"}]
|
|
125
125
|
latest_saved = next((job for job in reversed(jobs) if _checkpoint(job) is not None), None)
|
|
126
126
|
latest_checkpoint = _checkpoint(latest_saved) if latest_saved is not None else None
|
|
127
127
|
if latest_checkpoint is not None:
|
|
128
|
-
latest_checkpoint["train_job_id"] = latest_saved.get("train_job_id")
|
|
128
|
+
latest_checkpoint["train_job_id"] = latest_saved.get("job_id") or latest_saved.get("train_job_id")
|
|
129
129
|
rounds = [number for number in map(_round, jobs) if number is not None]
|
|
130
130
|
activity = [stamp for job in jobs if isinstance(stamp := job.get("updated_at") or job.get("created_at"), str)]
|
|
131
131
|
return {
|
|
@@ -172,7 +172,7 @@ class AgentRuns:
|
|
|
172
172
|
if observed_at is None:
|
|
173
173
|
observed_at = page.get("observed_at")
|
|
174
174
|
for job in page.get("items") or []:
|
|
175
|
-
job_id = job.get("train_job_id") if isinstance(job, dict) else None
|
|
175
|
+
job_id = job.get("job_id") or job.get("train_job_id") if isinstance(job, dict) else None
|
|
176
176
|
if not isinstance(job, dict) or job_id in ids:
|
|
177
177
|
continue
|
|
178
178
|
if job_id is not None:
|
|
@@ -277,9 +277,12 @@ def run_auth(args, parser):
|
|
|
277
277
|
config=False, environment=False).list_jobs(limit=1)
|
|
278
278
|
report["check"] = "access_confirmed" if connection.api_key else "reachable_anonymous"
|
|
279
279
|
except (ConfigError, RemoteError) as error:
|
|
280
|
+
from .cli_errors import format_error, safe_error_code
|
|
281
|
+
|
|
280
282
|
report["check"] = {401: "authentication_rejected", 403: "authorization_denied"}.get(
|
|
281
|
-
getattr(error, "status_code", None), error
|
|
283
|
+
getattr(error, "status_code", None), safe_error_code(error))
|
|
282
284
|
_print(report, args.json)
|
|
285
|
+
print(format_error(error, args=args, program=parser.prog), file=sys.stderr)
|
|
283
286
|
return 1
|
|
284
287
|
_print(report, getattr(args, "json", False))
|
|
285
288
|
return 0
|
|
@@ -19,8 +19,8 @@ except PackageNotFoundError:
|
|
|
19
19
|
|
|
20
20
|
SECTIONS = {
|
|
21
21
|
'OVERVIEW': ('dashboard',),
|
|
22
|
-
'EXPERIMENT': ('job', '
|
|
23
|
-
'DATA': ('dataset', 'checkpoint', 'profile'),
|
|
22
|
+
'EXPERIMENT': ('job', 'trail'),
|
|
23
|
+
'DATA': ('dataset', 'runner', 'checkpoint', 'profile'),
|
|
24
24
|
'MANAGE': ('billing', 'limit', 'member', 'setting'),
|
|
25
25
|
'TOOLS': ('auth', 'help'),
|
|
26
26
|
}
|
|
@@ -111,20 +111,32 @@ def nonnegative_seconds(value):
|
|
|
111
111
|
return seconds
|
|
112
112
|
|
|
113
113
|
|
|
114
|
-
def
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
def wait_seconds(value):
|
|
115
|
+
try:
|
|
116
|
+
seconds = float(value)
|
|
117
|
+
except ValueError:
|
|
118
|
+
raise argparse.ArgumentTypeError("seconds must be finite and non-negative") from None
|
|
119
|
+
if not math.isfinite(seconds) or seconds < 0:
|
|
120
|
+
raise argparse.ArgumentTypeError("seconds must be finite and non-negative")
|
|
121
|
+
return seconds
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def log_page_size(value):
|
|
125
|
+
try:
|
|
126
|
+
count = int(value)
|
|
127
|
+
except ValueError:
|
|
128
|
+
raise argparse.ArgumentTypeError("expected an integer from 1 to 1000") from None
|
|
129
|
+
if not 1 <= count <= 1000:
|
|
130
|
+
raise argparse.ArgumentTypeError("expected an integer from 1 to 1000")
|
|
131
|
+
return count
|
|
117
132
|
|
|
118
|
-
try:
|
|
119
|
-
return runner_selector(value)
|
|
120
|
-
except ValueError as exc:
|
|
121
|
-
raise argparse.ArgumentTypeError(str(exc)) from None
|
|
122
133
|
|
|
123
|
-
|
|
124
|
-
|
|
134
|
+
def runner_arguments(submit):
|
|
135
|
+
submit.add_argument("--runner", metavar="NAME[@REVISION]",
|
|
136
|
+
help="Agent: override the Dataset runner with NAME@REVISION; Sandbox: registered Runner name (for example base)")
|
|
125
137
|
submit.add_argument("--job-id", help="caller-selected Job ID")
|
|
126
138
|
submit.add_argument("--tag", help="run label")
|
|
127
|
-
submit.add_argument("--priority", type=int,
|
|
139
|
+
submit.add_argument("--priority", type=int, help="waiting priority; larger values run first (default: 0)")
|
|
128
140
|
submit.add_argument("--command", help="shell script override executed in the runner directory; preserves multiline text")
|
|
129
141
|
|
|
130
142
|
|
|
@@ -132,7 +144,7 @@ def parser():
|
|
|
132
144
|
cli = HelpArgumentParser(
|
|
133
145
|
prog=Path(sys.argv[0]).name if Path(sys.argv[0]).name in {"envloop", "el"} else "el",
|
|
134
146
|
usage="%(prog)s [options] [command]", formatter_class=CommandHelpFormatter,
|
|
135
|
-
description="Manage
|
|
147
|
+
description="Manage Agent, Sandbox, Train and Inference Jobs and worker trials with EnvLoop.\nConfigure your connection with 'auth config'.",
|
|
136
148
|
)
|
|
137
149
|
cli._optionals.title = "Options"
|
|
138
150
|
cli.set_defaults(api_url=None, workspace_id=None)
|
|
@@ -140,8 +152,11 @@ def parser():
|
|
|
140
152
|
groups = cli.add_subparsers(dest="group", title="Commands", prog=cli.prog, required=True)
|
|
141
153
|
from .auth_cli import add_auth_commands
|
|
142
154
|
|
|
143
|
-
jobs = groups.add_parser("job", help="Submit and manage Agent
|
|
144
|
-
job_submit = jobs.add_parser("submit")
|
|
155
|
+
jobs = groups.add_parser("job", help="Submit and manage Agent, Sandbox, Train and Inference Jobs").add_subparsers(dest="action", required=True)
|
|
156
|
+
job_submit = jobs.add_parser("submit", help="Submit an Agent, Sandbox, Train or Inference Job")
|
|
157
|
+
job_submit.add_argument("--type", choices=("agent", "sandbox", "train", "inference"), default="agent", help="Job type (default: agent)")
|
|
158
|
+
job_submit.add_argument("--image", metavar="IMAGE", help="Sandbox Docker image override (server default: python:3.11)")
|
|
159
|
+
job_submit.add_argument("--file", type=Path, help="Train or Inference Job request JSON (spec or Train trials array)")
|
|
145
160
|
job_submit.add_argument("--source", type=Path, help="two-file directory for a single-task Job")
|
|
146
161
|
job_submit.add_argument("--task-id", help="select one task within the dataset")
|
|
147
162
|
job_submit.add_argument(
|
|
@@ -154,42 +169,49 @@ def parser():
|
|
|
154
169
|
job_submit.add_argument("--split", help="one complete split name; omitted selects all splits")
|
|
155
170
|
job_submit.add_argument("--revision", help="Git branch/tag/commit; must match the platform's pinned snapshot")
|
|
156
171
|
runner_arguments(job_submit)
|
|
157
|
-
job_submit.add_argument("--attempts", type=int
|
|
172
|
+
job_submit.add_argument("--attempts", type=int)
|
|
158
173
|
job_submit.add_argument("--request-id")
|
|
159
174
|
job_submit.add_argument("--profile", type=profile_reference, metavar="NAME[@VERSION]",
|
|
160
175
|
help="named runtime Profile; omitted version requires an explicit default")
|
|
161
176
|
job_submit.add_argument("--seed", type=int)
|
|
162
|
-
job_submit.add_argument("--timeout-sec", type=
|
|
177
|
+
job_submit.add_argument("--timeout-sec", type=wait_seconds,
|
|
178
|
+
help="Agent/Sandbox execution timeout (whole seconds); Train wait timeout (default: 600)")
|
|
163
179
|
job_submit.add_argument("--delete-delay", type=nonnegative_seconds, metavar="SECONDS",
|
|
164
180
|
help="keep each sandbox (E2B or Fly) after completion for SECONDS (default: 0); cancellation/timeout delete immediately")
|
|
165
181
|
job_submit.add_argument("--wait", action="store_true")
|
|
166
|
-
for action in ("get", "stop", "result"):
|
|
167
|
-
jobs.add_parser(action)
|
|
182
|
+
for action in ("get", "stop", "result", "pause", "resume", "wake", "event"):
|
|
183
|
+
command = jobs.add_parser(action)
|
|
184
|
+
command.add_argument("job_id")
|
|
185
|
+
if action == 'event':
|
|
186
|
+
command.add_argument('--json', action='store_true')
|
|
187
|
+
command.set_defaults(_list_view=True)
|
|
188
|
+
from .resource_cli import add_job_resources
|
|
189
|
+
add_job_resources(jobs)
|
|
168
190
|
listing = jobs.add_parser("list")
|
|
169
|
-
listing.add_argument('--type', choices=['agent', 'train'], help='Select Agent or
|
|
191
|
+
listing.add_argument('--type', choices=['agent', 'sandbox', 'train', 'inference'], help='Select Agent, Sandbox, Train or Inference Jobs')
|
|
192
|
+
listing.add_argument('--status', help='Filter by Job status')
|
|
170
193
|
listing.add_argument("--cursor")
|
|
171
194
|
listing.add_argument("--json", action="store_true", help="Output the complete response as JSON (default: table)")
|
|
172
195
|
listing.add_argument("--limit", type=int, default=25)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
train_list.add_argument("--limit", type=int, default=25)
|
|
184
|
-
train_list.add_argument("--json", action="store_true", help="Output the complete response as JSON (default: table)")
|
|
185
|
-
runs = trains.add_parser("run", help="Inspect runs grouped from their Train Jobs").add_subparsers(dest="run_action", required=True)
|
|
196
|
+
train_log = jobs.add_parser("log", help="Read or follow Agent, Sandbox or Train Job logs")
|
|
197
|
+
train_log.add_argument("job_id")
|
|
198
|
+
train_log.add_argument("--source", choices=("all", "system", "runtime"), default="all")
|
|
199
|
+
position = train_log.add_mutually_exclusive_group()
|
|
200
|
+
position.add_argument("--tail", type=log_page_size, help="Start with the last N records (default: 200)")
|
|
201
|
+
position.add_argument("--cursor", help="Resume from a previously returned log cursor")
|
|
202
|
+
train_log.add_argument("--limit", type=log_page_size, default=200, help="Maximum records per page (default: 200)")
|
|
203
|
+
train_log.add_argument("--follow", action="store_true", help="Drain pages and poll until collection ends; Ctrl-C exits without stopping the Job")
|
|
204
|
+
train_log.add_argument("--json", action="store_true", help="Output the complete page as JSON; --follow emits one page per line (NDJSON)")
|
|
205
|
+
runs = jobs.add_parser("run", help="Inspect runs grouped from their Train Jobs").add_subparsers(dest="run_action", required=True)
|
|
186
206
|
runs_list = runs.add_parser("list", help="List agent runs, most recently updated first")
|
|
187
207
|
runs_list.add_argument("--json", action="store_true", help="Output the complete response as JSON (default: table)")
|
|
188
208
|
runs_get = runs.add_parser("get", help="Show a run's rounds, Train Jobs and checkpoints")
|
|
189
209
|
runs_get.add_argument("run", metavar="RUN", help="run label: user_metadata.rsi_run or agent_run")
|
|
190
210
|
runs_get.add_argument("--json", action="store_true", help="Output the complete run as JSON (default: tables)")
|
|
191
211
|
tasks = groups.add_parser("trail", help="Inspect and control existing trials").add_subparsers(dest="action", required=True)
|
|
192
|
-
listing = tasks.add_parser("list")
|
|
212
|
+
listing = tasks.add_parser("list", help="List Agent, Sandbox, Train and Inference worker Trials")
|
|
213
|
+
listing.add_argument("--type", choices=("agent", "sandbox", "train", "inference"), help="Filter by Trial type")
|
|
214
|
+
listing.add_argument("--job-id", help="Filter by parent Job ID")
|
|
193
215
|
listing.add_argument("--cursor")
|
|
194
216
|
listing.add_argument("--limit", type=int, default=25)
|
|
195
217
|
snapshot = tasks.add_parser("snapshot")
|
|
@@ -212,6 +234,19 @@ def parser():
|
|
|
212
234
|
put.add_argument("name")
|
|
213
235
|
put.add_argument("version")
|
|
214
236
|
put.add_argument("--file", type=Path, required=True, help="ProfileSpec JSON file")
|
|
237
|
+
put.add_argument("--runner", metavar="RUNNER", help="Bind org/name@<full-commit> to this version")
|
|
238
|
+
secret_input = put.add_mutually_exclusive_group()
|
|
239
|
+
secret_input.add_argument("--prompt-model-api-key", action="store_true", help="Read the model API key from a non-echoing prompt")
|
|
240
|
+
secret_input.add_argument("--model-api-key-stdin", action="store_true", help="Read the model API key from one line of standard input")
|
|
241
|
+
update = profiles.add_parser("update", help="Edit an existing Profile by publishing a new immutable version")
|
|
242
|
+
update.add_argument("name")
|
|
243
|
+
update.add_argument("version", help="New version; existing versions cannot be overwritten")
|
|
244
|
+
update.add_argument("--from-version", help="Source version; omit to use the explicit default")
|
|
245
|
+
update.add_argument("--file", type=Path, required=True, help="JSON patch: merge literal params keys, replace supplied metadata; preserve omitted fields")
|
|
246
|
+
update.add_argument("--runner", metavar="RUNNER", help="Bind a Runner name@full-commit; omit to preserve the source binding")
|
|
247
|
+
update_secret = update.add_mutually_exclusive_group()
|
|
248
|
+
update_secret.add_argument("--prompt-model-api-key", action="store_true", help="Read an encrypted model connection key from a non-echoing prompt")
|
|
249
|
+
update_secret.add_argument("--model-api-key-stdin", action="store_true", help="Read an encrypted model connection key from one line of standard input")
|
|
215
250
|
get = profiles.add_parser("get")
|
|
216
251
|
get.add_argument("name")
|
|
217
252
|
get.add_argument("--version")
|
|
@@ -245,11 +280,11 @@ def parser():
|
|
|
245
280
|
|
|
246
281
|
def submission_options(args):
|
|
247
282
|
options = {}
|
|
248
|
-
for key in ("runner", "job_id", "tag", "command"):
|
|
283
|
+
for key in ("runner", "job_id", "tag", "command", "image"):
|
|
249
284
|
value = getattr(args, key, None)
|
|
250
285
|
if value is not None:
|
|
251
286
|
options[key] = value
|
|
252
|
-
options["priority"] = args.priority
|
|
287
|
+
options["priority"] = args.priority if args.priority is not None else 0
|
|
253
288
|
if args.profile:
|
|
254
289
|
if args.runner is not None:
|
|
255
290
|
options["profile_id"] = args.profile
|
|
@@ -322,58 +357,101 @@ def main(argv=None):
|
|
|
322
357
|
|
|
323
358
|
return run_auth(args, cli)
|
|
324
359
|
except (EOFError, KeyboardInterrupt):
|
|
325
|
-
|
|
360
|
+
from .cli_errors import format_error
|
|
361
|
+
from .config import ConfigError
|
|
362
|
+
|
|
363
|
+
print(format_error(ConfigError('AUTH_INPUT_CANCELLED'), args=args, program=cli.prog), file=sys.stderr)
|
|
326
364
|
return 1
|
|
327
365
|
except Exception as exc:
|
|
328
|
-
|
|
366
|
+
from .cli_errors import format_error
|
|
367
|
+
|
|
368
|
+
print(format_error(exc, args=args, program=cli.prog), file=sys.stderr)
|
|
329
369
|
return 1
|
|
330
370
|
if args.group == "job" and args.action == "submit":
|
|
331
|
-
if
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
371
|
+
if args.type in {"train", "inference"}:
|
|
372
|
+
if args.file is None:
|
|
373
|
+
cli.error(f"--type {args.type} requires --file REQUEST.json")
|
|
374
|
+
if args.type == 'inference' and (args.wait or args.timeout_sec is not None):
|
|
375
|
+
cli.error('Inference is a continuous service; --wait and --timeout-sec do not apply')
|
|
376
|
+
agent_fields = ("dataset", "dataset_option", "source", "task_id", "config", "split", "revision",
|
|
377
|
+
"runner", "job_id", "tag", "priority", "command", "attempts", "request_id",
|
|
378
|
+
"profile", "seed", "delete_delay", "image")
|
|
379
|
+
if any(getattr(args, key) is not None for key in agent_fields):
|
|
380
|
+
cli.error(f"--type {args.type} accepts its request through --file; Agent submission options cannot be combined")
|
|
381
|
+
elif args.type == 'sandbox':
|
|
382
|
+
from .sandbox import sandbox_runner_selector, validate_image
|
|
383
|
+
|
|
384
|
+
if any(getattr(args, key) is not None for key in (
|
|
385
|
+
'file', 'dataset', 'dataset_option', 'source', 'task_id', 'config', 'split', 'revision', 'seed')):
|
|
386
|
+
cli.error('--type sandbox accepts --runner without Dataset, Task, Train file or seed options')
|
|
387
|
+
try:
|
|
388
|
+
args.runner = sandbox_runner_selector(args.runner)
|
|
389
|
+
if args.image is not None:
|
|
390
|
+
args.image = validate_image(args.image)
|
|
391
|
+
except ValueError as exc:
|
|
392
|
+
cli.error(str(exc))
|
|
393
|
+
if args.timeout_sec is not None:
|
|
394
|
+
if not args.timeout_sec.is_integer():
|
|
395
|
+
cli.error('Sandbox --timeout-sec must be whole seconds')
|
|
396
|
+
args.timeout_sec = int(args.timeout_sec)
|
|
397
|
+
else:
|
|
398
|
+
if args.file is not None:
|
|
399
|
+
cli.error("--file requires --type train or inference")
|
|
400
|
+
if args.image is not None:
|
|
401
|
+
cli.error('--image requires --type sandbox')
|
|
402
|
+
if args.runner is not None:
|
|
403
|
+
from .client import runner_selector
|
|
404
|
+
|
|
405
|
+
try:
|
|
406
|
+
args.runner = runner_selector(args.runner)
|
|
407
|
+
except ValueError as exc:
|
|
408
|
+
cli.error(str(exc))
|
|
409
|
+
if sum(value is not None for value in (args.dataset, args.dataset_option, args.source)) != 1:
|
|
410
|
+
cli.error("provide exactly one positional dataset, --dataset or --source")
|
|
411
|
+
args.dataset = args.dataset if args.dataset is not None else args.dataset_option
|
|
412
|
+
if args.source is not None and any(getattr(args, key) is not None for key in ("runner", "task_id", "config", "split", "revision")):
|
|
413
|
+
cli.error("dataset selection options require a dataset")
|
|
414
|
+
if args.timeout_sec is not None:
|
|
415
|
+
if not args.timeout_sec.is_integer():
|
|
416
|
+
cli.error("Agent --timeout-sec must be whole seconds")
|
|
417
|
+
args.timeout_sec = int(args.timeout_sec)
|
|
336
418
|
args._command_path = args._help_parser.prog.split()[1:]
|
|
337
419
|
if args.group == 'setting' and args.action == 'update' and not any(getattr(args, key) is not None for key in ('name', 'slug', 'region')):
|
|
338
420
|
cli.error('provide at least one of --name, --slug or --region')
|
|
339
421
|
try:
|
|
340
422
|
resolved = resolve_cli_connection(args.api_url, args.workspace_id)
|
|
341
423
|
args.api_url = resolved.api_url
|
|
342
|
-
if args.group == "train" and args.action != "run":
|
|
343
|
-
if args.action == "submit" and args.wait and (not math.isfinite(args.timeout_sec) or args.timeout_sec < 0):
|
|
344
|
-
print("INVALID_TRAIN_WAIT_TIMEOUT", file=sys.stderr)
|
|
345
|
-
return 1
|
|
346
|
-
if not args.api_url:
|
|
347
|
-
print("TRAIN_API_URL_REQUIRED", file=sys.stderr)
|
|
348
|
-
return 1
|
|
349
|
-
from .http import HttpService
|
|
350
|
-
from .train import TrainJobs
|
|
351
|
-
|
|
352
|
-
train = TrainJobs(HttpService(args.api_url, api_key=resolved.api_key, workspace_id=resolved.workspace_id,
|
|
353
|
-
project_id=resolved.project_id, config=False, environment=False))
|
|
354
|
-
if args.action == "submit":
|
|
355
|
-
result = train.submit(json.loads(args.file.read_text(encoding="utf-8")))
|
|
356
|
-
if args.wait:
|
|
357
|
-
result = train.wait(result["train_job_id"], timeout=args.timeout_sec)
|
|
358
|
-
elif args.action == "list":
|
|
359
|
-
result = train.list(status=args.status, cursor=args.cursor, limit=args.limit)
|
|
360
|
-
else:
|
|
361
|
-
result = getattr(train, 'status' if args.action == 'get' else args.action)(args.train_job_id)
|
|
362
|
-
if args.action == "list" and not args.json:
|
|
363
|
-
from .cli_output import list_output
|
|
364
|
-
|
|
365
|
-
print(list_output(result, args, cli.prog))
|
|
366
|
-
else:
|
|
367
|
-
print(json.dumps(result, ensure_ascii=False))
|
|
368
|
-
return 1 if result.get("status") in {"FAILED", "CANCELLED"} else 0
|
|
369
424
|
connection = Client.remote(args.api_url, api_key=resolved.api_key, workspace_id=resolved.workspace_id,
|
|
370
425
|
project_id=resolved.project_id, config=False)
|
|
371
426
|
with connection as client:
|
|
372
427
|
if args.group == "profile":
|
|
373
|
-
if args.action
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
)
|
|
428
|
+
if args.action in {"create", "update"}:
|
|
429
|
+
from .profile_updates import read_profile_patch, with_runner
|
|
430
|
+
|
|
431
|
+
raw = args.file.read_bytes()
|
|
432
|
+
patch = read_profile_patch(raw)
|
|
433
|
+
if args.runner is not None:
|
|
434
|
+
patch = with_runner(patch, args.runner)
|
|
435
|
+
raw = json.dumps(patch).encode('utf-8')
|
|
436
|
+
secrets = None
|
|
437
|
+
if args.prompt_model_api_key:
|
|
438
|
+
import getpass
|
|
439
|
+
import warnings
|
|
440
|
+
|
|
441
|
+
# Fail closed if the terminal cannot disable echo.
|
|
442
|
+
with warnings.catch_warnings():
|
|
443
|
+
warnings.simplefilter("error", getpass.GetPassWarning)
|
|
444
|
+
key = getpass.getpass("Model API key: ")
|
|
445
|
+
secrets = {"model_api_key": key}
|
|
446
|
+
elif args.model_api_key_stdin:
|
|
447
|
+
secrets = {"model_api_key": sys.stdin.readline(4098).rstrip("\r\n")}
|
|
448
|
+
if args.action == "update":
|
|
449
|
+
result = client.profiles.update(args.name, args.version,
|
|
450
|
+
patch=patch, from_version=args.from_version, secrets=secrets)
|
|
451
|
+
else:
|
|
452
|
+
result = client.profiles.import_spec(
|
|
453
|
+
raw, name=args.name, version=args.version, secrets=secrets
|
|
454
|
+
)
|
|
377
455
|
elif args.action == "get":
|
|
378
456
|
result = client.profiles.get(args.name, args.version)
|
|
379
457
|
elif args.action == "list":
|
|
@@ -396,20 +474,37 @@ def main(argv=None):
|
|
|
396
474
|
return 0
|
|
397
475
|
elif args.group == "job":
|
|
398
476
|
if args.action == "submit":
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
477
|
+
if args.type == 'inference':
|
|
478
|
+
result = client.jobs.submit_inference(json.loads(args.file.read_text(encoding='utf-8')))
|
|
479
|
+
elif args.type == "train":
|
|
480
|
+
result = client.training_jobs.submit(json.loads(args.file.read_text(encoding="utf-8")))
|
|
481
|
+
else:
|
|
482
|
+
result = client.jobs.submit(args.dataset, type=args.type, source=args.source, task_id=args.task_id,
|
|
483
|
+
attempts=args.attempts if args.attempts is not None else 1,
|
|
484
|
+
request_id=args.request_id, config=args.config, split=args.split,
|
|
485
|
+
revision=args.revision, **submission_options(args))
|
|
402
486
|
if args.wait:
|
|
403
|
-
|
|
487
|
+
wait_options = {"timeout": args.timeout_sec} if args.type == "train" and args.timeout_sec is not None else {}
|
|
488
|
+
result = client.jobs.wait(result["job_id"], **wait_options)
|
|
489
|
+
elif args.action == 'event':
|
|
490
|
+
result = client.jobs.events(args.job_id)
|
|
491
|
+
elif args.action in {'serving-profile', 'adapter'}:
|
|
492
|
+
from .resource_cli import run_job_resource
|
|
493
|
+
result = run_job_resource(client, args)
|
|
494
|
+
elif args.action == "log":
|
|
495
|
+
from .train_log_cli import read_logs
|
|
496
|
+
|
|
497
|
+
return read_logs(client.jobs, args)
|
|
498
|
+
elif args.action == "run":
|
|
499
|
+
result = client.agent_runs.list() if args.run_action == "list" else client.agent_runs.get(args.run)
|
|
404
500
|
elif args.action == "list":
|
|
405
501
|
options = {'cursor': args.cursor, 'limit': args.limit}
|
|
406
|
-
|
|
407
|
-
|
|
502
|
+
for field in ('type', 'status'):
|
|
503
|
+
if getattr(args, field) is not None:
|
|
504
|
+
options[field] = getattr(args, field)
|
|
408
505
|
result = client.jobs.list(**options)
|
|
409
506
|
else:
|
|
410
507
|
result = getattr(client.jobs, 'status' if args.action == 'get' else args.action)(args.job_id)
|
|
411
|
-
elif args.group == "train":
|
|
412
|
-
result = client.agent_runs.list() if args.run_action == "list" else client.agent_runs.get(args.run)
|
|
413
508
|
elif args.group == "trail":
|
|
414
509
|
if args.action == "artifact":
|
|
415
510
|
if args.artifact_action == "download":
|
|
@@ -417,19 +512,30 @@ def main(argv=None):
|
|
|
417
512
|
else:
|
|
418
513
|
result = client.trials.artifacts(args.trial_id, cursor=args.cursor, limit=args.limit)
|
|
419
514
|
elif args.action == "list":
|
|
420
|
-
|
|
515
|
+
options = {'cursor': args.cursor, 'limit': args.limit}
|
|
516
|
+
for field in ('type', 'job_id'):
|
|
517
|
+
if getattr(args, field) is not None:
|
|
518
|
+
options[field] = getattr(args, field)
|
|
519
|
+
result = client.trials.list(**options)
|
|
421
520
|
else:
|
|
422
521
|
result = getattr(client.trials, 'status' if args.action == 'get' else args.action)(args.trial_id)
|
|
423
522
|
else:
|
|
424
523
|
from .resource_cli import execute
|
|
425
524
|
|
|
426
525
|
result = execute(args, client)
|
|
427
|
-
if
|
|
526
|
+
if isinstance(result, dict) and 'operation_id' in result:
|
|
527
|
+
from .cli_errors import operation_output
|
|
528
|
+
from .resources import CleanupError
|
|
529
|
+
|
|
530
|
+
if result.get('status') in {'FAILED', 'CANCELLED', 'RETRYABLE_FAILED', 'BLOCKED'}:
|
|
531
|
+
raise CleanupError(result)
|
|
532
|
+
result = operation_output(result)
|
|
533
|
+
if args.group == 'job' and args.action == 'run' and args.run_action == 'get' and not args.json:
|
|
428
534
|
from .cli_output import run_output
|
|
429
535
|
|
|
430
536
|
print(run_output(result))
|
|
431
537
|
elif (args.action == 'list' or getattr(args, '_list_view', False)
|
|
432
|
-
or (args.group == '
|
|
538
|
+
or (args.group == 'job' and args.action == 'run' and args.run_action == 'list')) and not args.json:
|
|
433
539
|
from .cli_output import list_output
|
|
434
540
|
|
|
435
541
|
print(list_output(result, args, cli.prog))
|
|
@@ -437,14 +543,15 @@ def main(argv=None):
|
|
|
437
543
|
print(json.dumps(result, ensure_ascii=False))
|
|
438
544
|
return 1 if isinstance(result, dict) and (result.get('status') in {'failed', 'FAILED', 'CANCELLED', 'RETRYABLE_FAILED', 'BLOCKED'} or result.get('skipped')) else 0
|
|
439
545
|
except Exception as exc:
|
|
546
|
+
from .cli_errors import format_error, operation_output
|
|
440
547
|
from .resources import CleanupError
|
|
441
548
|
|
|
442
549
|
if isinstance(exc, CleanupError):
|
|
443
|
-
print(json.dumps(exc.operation, ensure_ascii=False))
|
|
444
|
-
print(
|
|
550
|
+
print(json.dumps(operation_output(exc.operation), ensure_ascii=False))
|
|
551
|
+
print(format_error(exc, args=args, program=cli.prog), file=sys.stderr)
|
|
445
552
|
return 1
|
|
446
553
|
# Do not emit arbitrary exception messages, file content or credentials.
|
|
447
|
-
print(
|
|
554
|
+
print(format_error(exc, args=args, program=cli.prog), file=sys.stderr)
|
|
448
555
|
return 1
|
|
449
556
|
|
|
450
557
|
|